zoukankan      html  css  js  c++  java
  • 多线程同步问题

    先看Demo1:

    public class Demo1_Synchronized {
        public static void main(String[] args) {
            final Printer p = new Printer();
            new Thread() {
                public void run() {
                    for (int i = 0; i < 100; ++i) {
                        p.print1(); // jdk1.8不需要显式将p用final修饰,默认用final修饰
                    }
                }
            }.start();
            new Thread() {
                public void run() {
                    for (int i = 0; i < 100; ++i) {
                        p.print2();
                    }
                }
            }.start();
        }
    }
    
    class Printer {
        // 零长度的byte数组对象创建起来将比任何对象都经济――查看编译后的字节码:
        // 生成零长度的byte[]对象只需3条操作码,而Object lock = new Object()则需要7行操作码。
        private byte[] lock = new byte[0]; // 特殊的锁
        // 注意:匿名对象不可以当做锁对象,因为不能保证两个锁对象是同一个对象
        // 非静态的同步方法,锁对象是this,锁方法和锁this是一样的效果
        // 静态的同步方法,锁对象是当前类的字节码对象,锁方法和锁Printer.class是一样的
    
        public static void print1() {
            // synchronized (lock) {
            synchronized (Printer.class) {
                System.out.print("我");
                System.out.print("最");
                System.out.print("帅");
                System.out.print("
    ");
            }
            // }
        }
    
        public synchronized static void print2() {
            // synchronized (lock) {
            System.out.print("你");
            System.out.print("很");
            System.out.print("丑");
            System.out.print("
    ");
            // }
        }
    }

    注意:做好不要把匿名对象不可以当做锁对象,因为不能保证两个锁对象是同一个对象,这样就只能锁住类了。

    非静态的同步方法,锁对象是this,锁方法和锁this是一样的效果

    比如public synchronized void print(){...}

    就和public void print(){

         synchronized(this){...}

    }

    效果是一样的。

    静态的同步方法,锁对象是当前类的字节码对象,锁方法和锁Printer.class是一样的,相当于给当前的类加锁

    public synchronized static void print(){...}

    和public static void print(){

        synchronized(Printer.class){...}// 类名.class

    }

    效果是一样的。

    经验总结:

    如果是多个线程指向相同的对象,那么多个线程操作这个共享变量时,这个变量是不需要加static的,锁住当前this即可保证线程安全。如下:

    public class MainClass implements Runnable {
        static MainClass instance = new MainClass();
        volatile int i = 0;
    
        public synchronized void increase() {
            ++i;
        }
    
        public static void main(String[] args) throws InterruptedException {
            Thread t1 = new Thread(instance);
            Thread t2 = new Thread(instance);
            t1.start();
            t2.start();
            t1.join();
            t2.join();
        }
    
        @Override
        public void run() {
            // TODO Auto-generated method stub
            for (int i = 0; i < 10000; ++i) {
                increase();
            }
            System.out.println(i);
        }
    }

    如果是多个线程指向不同的对象,那么多个线程操作这个共享变量时,这个变量是需要加上static的,操作这个变量的方法必须设置为static方法才能操作这个变量,再锁住这个静态方法,就是锁住了这个类,可以正确同步,保证线程安全。如下:

    public class MainClass implements Runnable {
        static MainClass instance = new MainClass();
        static volatile int i = 0;
    
        public static synchronized void increase() {
            ++i;
        }
    
        public static void main(String[] args) throws InterruptedException {
            Thread t1 = new Thread(new MainClass());
            Thread t2 = new Thread(new MainClass());
            t1.start();
            t2.start();
            t1.join();
            t2.join();
        }
    
        @Override
        public void run() {
            // TODO Auto-generated method stub
            for (int i = 0; i < 10000; ++i) {
                increase();
            }
            System.out.println(i);
        }
    }

    来看下一个Demo2

    public class Demo2_Ticket {
        /**
         * @param args
         *            模拟铁路售票, 4个窗口卖100张票
         */
        public static void main(String[] args) {
            for (int i = 0; i < 4; ++i) {
                new TicketSeller("窗口" + i).start();
            }
        }
    }
    
    class TicketSeller extends Thread {
        private static int tickets = 100;
    
        public TicketSeller(String name) {
            super(name);
            // TODO Auto-generated constructor stub
        }
    
        public void run() {
            while (true) {
                synchronized (TicketSeller.class) { // ==========提问点提问点提问点======
                    if (tickets == 0) {
                        break;
                    }
                    try {
                        Thread.sleep(10);
                    } catch (InterruptedException e) {
                        // TODO Auto-generated catch block
                        e.printStackTrace();
                    }
                    System.out.println(getName() + "...这是第" + tickets-- + "号票");
                }
            }
        }
    }

    模拟铁路卖票100张,肯定不能重复卖票,不然上车该争车位发生矛盾了。

    看到提问点

    如果这里不加synchronized,可以吗?

        不可以,如果不加,可能卖出负数号票,比如线程3刚要进行下一次循环,此时tickets=1,而线程2和线程1正好在执行tickets--,结果抢先在线程3之前执行了,tickets=-1了,然后就跳过了判断==0阶段,无限循环停不下来了。

    那么问题来了,我判断条件改为tickets<=0不就好了?

        不可以,哪怕这么改动(为了放大现象,加上了Thread.sleep(10)睡眠10ms),比如线程3, 2, 1都在睡眠,此时tickets=1,然后线程3睡醒了,先执行tickets--,打印这是第1号票,接着下一轮循环跳出,线程3结束,然后线程2和线程1睡醒了,依然执行tickets--,依次打印这是第0号票,这是第-1号票。。???卖出负数票了??

    那么问题来了,我加上synchronized (this){...}不就好了?

        不可以,这里是new Thread了4次,是4个线程对象,每个对象都有自己的this,互不干扰,这种方法等于没加synchronized。

    那么问题来了,我加上锁对象就好了,private Object obj = new Object();再synchronized (obj) {...}

        这个可以,不过这个是成员变量,每个线程对象都有自己的成员变量obj,所以要改为共享的,加上static。

        不过零长度的byte数组对象创建起来将比任何对象都经济――查看编译后的字节码,生成零长度的byte[]对象只需3条操作码,而Object lock = new Object()则需要7行操作码。这里改为private static byte[] lock = new byte[0];再synchronized (lock) {...}会比较好。当然,锁字节码对象也可以。synchronized(TicketSeller.class){...}// 类名.class

    那么看看Demo3,用Runnable实现

    public class Demo3_Ticket {
        /**
         * @param args
         * 多次开启一条线程是非法的
         */
        public static void main(String[] args) {
            Ticket t = new Ticket();
            new Thread(t, "窗口1").start();
            new Thread(t, "窗口2").start();
            new Thread(t, "窗口3").start();
            new Thread(t, "窗口4").start();
        }
    }
    
    class Ticket implements Runnable {
        private int tickets = 100;
        @Override
        public void run() {
            // TODO Auto-generated method stub
            while (true) {
                synchronized (this) {   // ===========提问点提问点提问点=========
                    if (tickets == 0) {
                        break;
                    }
                    try {
                        Thread.sleep(10);
                    } catch (InterruptedException e) {
                        // TODO Auto-generated catch block
                        e.printStackTrace();
                    }
                    System.out.println(Thread.currentThread().getName() + "...这是第" + tickets-- + "号票");
                }
            }
        }
    }

    看到提问点

        在这里用synchronized (this) {...}对吗?

            是对的,这里4个线程都是用的同一个Ticket对象。里面的tickets不需要加static,因为这个代码块同时只能一个线程执行,不会有并发问题。也可以synchronized (Ticket.class) {...}。

        这里的synchronized (this) {...}能和上面一句while (true) {...}交换吗?

            可以,但是!就只有窗口1在售票(即线程1执行),执行完才轮到窗口2,然后发现tickets已经为0,线程2结束,同理,线程3,4结束。程序结束。

    我们要避免死锁问题,我们简化一下哲学家的例子,一个人吃饭,习惯先拿左筷子,另一个人习惯先拿右筷子,每个人拿起一只筷子就不会放下,除非吃完一顿后才放下一双筷子供其他人使用。

    见下面一个例子Demo5:

    public class Demo4_DeadLock {
        /**
         * 尽量避免同步代码块的嵌套
         */
        private static String s1 = "筷子左";
        private static String s2 = "筷子右";
    
        public static void main(String[] args) {
            new Thread() {
                public void run() {
                    while (true) {
                        synchronized (s1) {
                            System.out.println(getName() + "获取"+ s1 + "等待" + s2);
                            synchronized (s2) {
                                System.out.println(getName() + "获取"+ s2 + "开始吃饭");
                            }
                        }
                        System.out.println("吃完了,放下一双筷子");
                    }
                }
            }.start();
            
            new Thread() {
                public void run() {
                    while (true) {
                        synchronized (s2) {
                            System.out.println(getName() + "获取"+ s2 + "等待" + s1);
                            synchronized (s1) {
                                System.out.println(getName() + "获取"+ s1 + "开始吃饭");
                            }
                        }
                        System.out.println("吃完了,放下一双筷子");
                    }
                }
            }.start();
        }
    }

    运行结果:

    结果互相等待,拿着左筷子的人等待右筷子,拿着右筷子的人等待左筷子。谁也不让谁,就造成了死锁。所以我们要避免synchronized的嵌套使用。

    更详细的总结见此处:

    Java中Synchronized的用法:https://blog.csdn.net/qq_34115899/article/details/80356581

    ========================================Talk is cheap, show me the code=======================================

    CSDN博客地址:https://blog.csdn.net/qq_34115899
  • 相关阅读:
    深入理解浏览器的缓存机制
    【ES6】Set、Map、WeakSet 和 WeakMap 的区别
    js的防抖(debounce) 和 节流(throttling)
    git对比两个分支的差异——git checkout
    纯CSS实现可自定义间距虚线边框
    无语,非也
    Spring AOP
    Spring集成Junit
    Spring注解开发-新注解
    Spring注解开发-原始注解
  • 原文地址:https://www.cnblogs.com/lcy0515/p/10807893.html
Copyright © 2011-2022 走看看