线程间通讯

    线程间通讯分为两部分:(1)逻辑传递。(2)、数据传递。什么是逻辑传递,比如A是买东西的,B是卖东西的,A到B这里买东西,发现东西还没做好,A就需要等B了,然后B做好东西告诉A,A在买完东西,往下继续做别的事情。AB相当于两个线程,A线程工作到某一点,需要依赖于B做好另一件事情才能继续往下做,这时就要等待,然后B做完另一件事情后来告诉A,A在继续往下做。面对这种等待和通知的场景,java提供了三种解决方案:(1)suspend和resume(这种已经不建议使用了,后面说明原因)。(2)、wait和 notify\ notifyAll 。(3)、park和unpark。我们先说一下suspend和resume被弃用的原因,正常情况下,suspend和resume如下使用一点问题都没有:

public static void main(String[] args) throws InterruptedException {

        Thread thread = new Thread(()->{
            System.out.println("A等待B~~~~~");
            Thread.currentThread().suspend();
            System.out.println("A继续执行~~~~~");
        });
        thread.start();
        Thread.sleep(1000);
        System.out.println("B通知A~~~~~");
        thread.resume();

    }

输出:

A等待B~~~~~
B通知A~~~~~
A继续执行~~~~~

但如果这样呢:

public static void main(String[] args) throws InterruptedException {

        Thread thread = new Thread(()->{
            synchronized (ThreadTest2.class) {
                System.out.println("A等待B~~~~~");
                Thread.currentThread().suspend();
                System.out.println("A继续执行~~~~~");
            }
        });
        thread.start();
        Thread.sleep(1000);
        synchronized (ThreadTest2.class) {
            System.out.println("B通知A~~~~~");
            thread.resume();
        }

    }

我们会发现主线程永远获取不到ThreadTest2.class的锁,也就意味中不会执行到thread.resume();那thread线程也就永远不会释放锁了,从而出现了死锁,我们再来看另一种情况:

public static void main(String[] args) throws InterruptedException {
        Thread thread = new Thread(() -> {
            try {
                Thread.sleep(2000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println("A等待B~~~~~");
            Thread.currentThread().suspend();
            System.out.println("A继续执行~~~~~");
        });
        thread.start();
        Thread.sleep(1000);
        System.out.println("B通知A~~~~~");
        thread.resume();
        Thread.sleep(2000);
        System.out.println("B再次通知A~~~~~");
        thread.resume();
    }

结果:

B通知A~~~~~
A等待B~~~~~
B再次通知A~~~~~
A继续执行~~~~~

    由于thread线程进行了2秒的睡眠,所以是先是主线程resume,thread子线程再suspend,这种情况同样,会死锁,我们从打印可以看到,必须在suspend之后,调用resume才是有效的, 否则就是死锁。面对这两种情况wait和 notify\notifyAll以及park和unpark应运而生。

  1. wait、notify解决了第一个加锁死锁问题:
public static void main(String[] args) throws InterruptedException {
        Thread thread = new Thread(() -> {

            try {
                synchronized (ThreadTest2.class) {
                    System.out.println("A等待B~~~~~");
                    ThreadTest2.class.wait();
                    System.out.println("A继续执行~~~~~");
                }
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        });
        thread.start();
        Thread.sleep(1000);
        synchronized (ThreadTest2.class) {
            System.out.println("B通知A~~~~~");
            ThreadTest2.class.notify();
        }
    }

这样就不会进入死锁了,我们可以看到wait和notify方法并不是作用在线程上的,而是被加锁的对象上的,同时也必须作用在被加锁的对象上,原因是当执行到wait方法时,会释放当前对象的锁,从而实现另外一边能够得到这个锁,这也就是为什么能够避免死锁了。

  1. park和unpark 解决了第二个通知早于等待的问题:
public static void main(String[] args) throws InterruptedException {
        Thread thread = new Thread(() -> {
            try {
                Thread.sleep(2000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println("A等待B~~~~~");
            LockSupport.park();
            System.out.println("A继续执行~~~~~");
        });
        thread.start();
        Thread.sleep(1000);
        System.out.println("B通知A~~~~~");
        LockSupport.unpark(thread);
    }

B通知A~~~~~
A等待B~~~~~
A继续执行~~~~~

我们可以看到,尽管park方法晚于unpark方法,但还是有效的,执行了park之后,直接被放行了,执行后面的打印。

类型 同步造成死锁 唤醒晚于等待造成死锁
suspend、resume 存在 存在
wait、notify\notifyAll 不存在 存在
park、unpark 存在 不存在

逻辑传递最后我们说一下一个问题:这上面三种等待唤醒,都有可能发生意外唤醒的情况,原因是在JVM更底层下出现了问题,从而造成意外唤醒,虽然出现概率极低,但仍然有可能,对于这种情况的解决方案是,把睡眠外层的判断改为while循环,这么说可能大家一脸懵逼,我们举一个例子:

public static void main(String[] args) throws InterruptedException {
       TestEntity testEntity = new TestEntity();
       Thread thread = new Thread(() -> {
           try {
               synchronized (ThreadTest2.class) {
                   System.out.println("A等待B~~~~~");
                   if(testEntity.getC()==0){
                       ThreadTest2.class.wait();
                   }
                   System.out.println("A继续执行~~~~~");
               }
           } catch (InterruptedException e) {
               e.printStackTrace();
           }
       });
       thread.start();
       Thread.sleep(1000);
       synchronized (ThreadTest2.class) {
           System.out.println("B通知A~~~~~");
           ThreadTest2.class.notify();
       }
   }

我们可以看到,wait前面有一个if判断,我们正常情况下也会这么写的,毕竟程序不可能无缘无故就进入等待状态,现在假设线程1出现了意外唤醒,我们可以看到System.out.println(“A继续执行~~~~~”);这样代码会被执行到,这是我们不希望看到的,所以我们现在来看这么一份代码:

public static void main(String[] args) throws InterruptedException {
       TestEntity testEntity = new TestEntity();
       Thread thread = new Thread(() -> {
           try {
               synchronized (ThreadTest2.class) {
                   System.out.println("A等待B~~~~~");
                   while (testEntity.getC()==0){
                       ThreadTest2.class.wait();
                   }
                   System.out.println("A继续执行~~~~~");
               }
           } catch (InterruptedException e) {
               e.printStackTrace();
           }
       });
       thread.start();
       Thread.sleep(1000);
       synchronized (ThreadTest2.class) {
           System.out.println("B通知A~~~~~");
           ThreadTest2.class.notify();
       }
   }

我们把代码中的if换成了while,我们再来看逻辑,假设子线程被意外唤醒,然后程序又会执行testEntity.getC()==0,发现任然成立,从而再次执行ThreadTest2.class.wait();使线程进入等待,从而解决了意外唤醒可能带来的影响,这就是上面我们所说的:把睡眠外层的判断改为while循环。有人可能会问,你都写成while轮训了,为什么还需要ThreadTest2.class.wait();这个呢,让他一直轮训这个共享变量从而实现逻辑控制不就好了嘛,其实看逻辑确实没毛病的。但是,wait是不占用CPU的,而你一直在那里轮序而且还是不间断的在那里做轮序,是非常占用CPU的,我们在多线程综述里说过,对于线程来说CPU是串行的,你一个子线程在那里无间断轮训,那CPU的使用率就直接爆炸了。

猜你喜欢

转载自blog.csdn.net/qq_30095631/article/details/106000381