synchronized 与wait、notify的关系,多线程安全 与 线程通信 的关系。

      上一篇博客讲解了 synchronized 关键字的 所有具体用法, 以及它是如何解决多线程安全问题的。  借这篇文章回顾一下基础,可能大家都能略知一二,但实际项目开发中 几个月不用,可能又会忘记一半,对有些概念似是而非、模棱两可,导致项目代码出现bug,通常这种多线程的bug还很难被测试出来。

一、什么是 “线程间通信”

      如果仅仅是为了解决 多线程操作数据 的 安全问题(即多线程可见性),用 synchronized 关键字 完全能满足需求。  但如果 多个线程之间 针对数据的不同形态 有一些 要求, 这就是涉及到  线程间通信。  

     举个例子

      比如 生产者和消费者问题, 生产者 和 消费者 分别位于各自的线程中, 生产者线程 不停的生产出 产品, 而消费者线程 不停的从 生产出的 产品中拿出来消费, 当生产的速度较慢,消费的速度较快时,消费者线程就必须进入阻塞等待状态, 这时一段生产者线程生产出一个新产品时,就必须立即唤醒 被阻塞的 消费者线程 进行消费。   这就是 线程间通信 的 具体实例。

     “多线程安全” 和 “线程间通信”的关系

     这里我的理解: “多线程安全” 和 “线程间通信”  存在一定的关系,但又不完全等价,   “多线程安全”描述的是存在的问题这一事实,  “线程间通信” 是解决多线程安全这个问题的手段, 但同时“线程间通信” 还可以用来 做更多更有用的事情,比如解决上述例子中的生产者消费者通信的问题。  

     synchronized 是 实现 “线程间通信” 的手段之一

      从某种角度来说,synchronized 关键字 也是 “线程间通信” 的手段, 线程1 在执行 synchronized 代码块时,线程2 再执行到这段代码块时就会被阻塞,那么线程2是如何知道 这段代码正在被 线程1 执行的呢? 线程1执行完同步代码块时,是如何唤醒阻塞的线程2呢?    这应该是通过 jvm层来 实现 线程间通信的,  具体的原理细节 涉及到 jvm 的 monitor , 甚至 操作系统底层 的东西,这篇文章里就不再铺开讲述, 有空再专门开一篇博客来讲解。  对于应用层开发,我们只需要知道jvm层的东西在 应用层的表现 即可。 有兴趣想深究的童鞋,可以参考  JVM源码分析之Object.wait/notify实现

二、如何通过java实现“线程间通信”

     用java实现 线程间 通信 涉及到 两个方法, wait() 和 notify();     看下图源码,这个方法是 java万类之王——Object 类的 非静态成员方法。



这样,我们就可以推测 java的 任何对象实例 都能被用作 线程间通信 的 信号量。 因为 任何对象实例都是从  Object继承出来。

      所谓线程间通信, 

      其一:既然是 通信, 那是谁跟谁发起通信呢? 自然有一个 主动通知方 和 被动接受通知方,比如:我通知你明天会下雨,我就是主动通知方,你是被动接受通知方。  wait() 和 notify() 正是这样,  notify 是主动通知方, wait 是被动接受方。 

      其二:既然是 多线程间,那必然是一个线程 主动通知 另一个线程,所以 wait 和 notify 必须执行在不同的线程,事实它们也无法执行在相同的线程,原因下方慢慢讲。

      当一个线程A执行对象锁的wait()方法时,这个线程A会在wait方法处被阻塞住, 直到有其它线程B调用这个 对象锁的notify()方法,这个线程A才能被唤醒。  但是这里面仍然有许多细节:

三、直接拿出代码的例子

      这段代码中,运行了两个线程,暂且命名上面的线程为 “线程1”,下面的线程为“线程2 ”, 线程1休眠 1秒后 被 lock.wait 阻塞住 ,线程2 休眠5秒后 通过 lock.nofity来 唤醒 被 lock.wait 阻塞住的 线程1。 这样实际 线程1 从开始执行到 结束执行 也耗费了大约6秒(5+1=6)。  

public class WaitNotifyCase {
    public static void main(String[] args) {
        final Object lock = new Object();

        new Thread(new Runnable() {
            @Override
            public void run() {
                synchronized (lock) {
                    try {
                        TimeUnit.SECONDS.sleep(1);
                        lock.wait();
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
            }
        }).start();

        new Thread(new Runnable() {
            @Override
            public void run() {
                synchronized (lock) {
                    try {
                        TimeUnit.SECONDS.sleep(5);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    lock.notify();
                }
            }
        }).start();
    }
}

      为什么是5+1呢?  这个例子也不完全正确,因为同时start 两个线程时,并不能确定哪个线程会最先获得cpu时间片,我们暂且认为 线程1 会 先获得时间片,从而先获得 lock锁 代码块的执行权。

      在上一篇博客中我们有提到,当一个线程执行被 lock对象锁 锁住 的代码块时,其它线程是在执行 lock锁代码块的时候 会被阻塞住,  那么线程2将会在 synchronized(lock)  处被阻塞住。  而线程1 又在 lock.wait() 处被阻塞,  那岂不是发生了 死锁?  答案是:线程2并没有被阻塞。

四、wait()、notify()

前提一由同一个lock对象调用wait、notify方法,和 synchronized 锁。  

前提二:wait、nofity调用时必须加 synchronized(lock) 同步

1、当线程A执行wait方法时,该线程会被挂起(即阻塞),同时会释放对象锁(这就能解释上面的例子不会发生死锁);
2、当线程B执行notify方法时,会唤醒一个被挂起的线程A;

lock对象、线程A和线程B三者是一种什么关系?根据上面的结论,可以想象一个场景:
1、lock对象维护了一个等待队列list;
2、线程A中执行lock的wait方法,把线程A保存到lock锁的 阻塞队列 中;
3、线程B中执行lock的notify方法,从lock锁的等待队列中取出线程A继续执行;若有多条线程被lock锁阻塞,则会随机唤醒一条线程继续执行。
4、若线程B中执行 lock.nofityAll方法,则能一次性 唤醒 所有被 lock锁 阻塞住的线程。


疑问一:为什么 wait 前必须加 synchronized 同步

答: 线程执行lock.wait()方法时,当前线程必须持有该lock对象的monitor,这是jvm层要求,如果wait方法在synchronized代码中执行,该线程已经获取synchronized的锁,从而持有了lock对象的monitor。 monitor是jvm层表述每个对象实例的一个flag,每个对象的对象头信息中都有这样一个flag。     

简单说:wait会释放当前线程的对象锁,既然是要释放锁,那就必须先获取锁,而 synchronized 就是同步锁,线程能执行同步代码块,则必须获得synchronized的锁。

因此:waite()notify()因为会对对象的“锁标志”进行操作,所以它们必须在 synchronized函数 或 synchronized代码块 中进行调用。如果在 non-synchronized函数 或 non-synchronized代码块 中进行调用,虽然能编译通过,但在运行时会发生IllegalMonitorStateException的异常。

疑问二:为什么 notify 前必须加 synchronized 同步
个人理解:wait释放了锁后被阻塞,notify用于唤醒被wait阻塞的线程,并让出锁给wait所在的线程。  既然notify要让出锁,那notify必然先获得锁,不然拿什么让给wait线程呢?    
obj.notify():该方法的调用,会从所有正在等待obj对象锁的线程中,唤醒其中的一个(选择算法依赖于不同实现),被唤醒的线程此时加入到了obj对象锁的争夺之中。
然而该notify方法的执行线程在 调用  lock.notify() 时并未立即释放obj的对象锁,毕竟这段代码还是执行在 synchronized同步代码中的 。  实际上释放动作是在执行完 lock.notify后并且离开synchronized代码块时释放锁的。  因此在notify方法之后,synchronized代码块结束之前,所有其他被唤醒的,等待obj对象锁的线程依旧被阻塞。

疑问二:线程A获取了synchronized锁,执行wait方法并挂起,线程B又如何再次获取锁?

答:线程A 在 执行lock.wait() 时,会阻塞线程A,同时立即释放 lock锁, 这样 线程B 才能再次获取 lock对象锁。






猜你喜欢

转载自blog.csdn.net/u013394527/article/details/80560153
今日推荐