上一篇博客 《synchronized 的用法》中讲解了 synchronized 关键字的 所有具体用法, 以及它是如何解决多线程安全问题的。 借这篇文章回顾一下线程通信的基础知识,可能这些知识点大家都能略知一二,但实际项目开发中 几个月不用,可能又会忘记一半,对有些概念似是而非、模棱两可,导致业务项目用到时又需要重新学习,这里自己完整总结一次,方便下次使用时查阅。
如果仅仅是为了解决 多线程操作数据 的 安全问题(即多线程可见性),用 synchronized 关键字 完全能满足需求。 但如果 多个线程之间 针对数据的不同形态 有一些 要求, 这就是涉及到 线程间通信。
举个例子
比如 生产者和消费者问题, 生产者 和 消费者 分别位于各自的线程中, 生产者线程 不停的生产出 产品, 而消费者线程 不停的从 生产出的 产品中拿出来消费, 当生产的速度较慢,消费的速度较快时,消费者线程就必须进入阻塞等待状态, 这时一段生产者线程生产出一个新产品时,就必须立即唤醒 被阻塞的 消费者线程 进行消费。 这就是 线程间通信 的 具体实例。
“多线程安全” 和 “线程间通信”的关系
这里我的理解: “多线程安全” 和 “线程间通信” 存在一定的关系,但又不完全等价, “多线程安全”描述的是存在的问题这一事实, “线程间通信” 是解决多线程安全这个问题的手段, 但同时“线程间通信” 还可以用来 做更多更有用的事情,比如解决上述例子中的生产者消费者通信的问题。
synchronized 是 实现 “线程间通信” 的手段之一
从某种角度来说,synchronized 关键字 也是 “线程间通信” 的手段, 线程1 在执行 synchronized 代码块时,线程2 再执行到这段代码块时就会被阻塞,那么线程2是如何知道 这段代码正在被 线程1 执行的呢? 线程1执行完同步代码块时,是如何唤醒阻塞的线程2呢? 这应该是通过 jvm层来 实现 线程间通信的, 具体的原理细节 涉及到 jvm 的 monitor , 甚至 操作系统底层 的东西,这篇文章里就不再铺开讲述, 有空再专门开一篇博客来讲解。 对于应用层开发,我们只需要知道jvm层的东西在 应用层的表现 即可。 有兴趣想深究的童鞋,可以参考 JVM源码分析之Object.wait/notify实现
用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并没有被阻塞。
前提一:由同一个lock对象调用wait、notify方法,和 synchronized 锁。
前提二:wait、nofity调用时必须加 synchronized(lock) 同步
1、当线程A执行wait方法时,该线程会被挂起(即阻塞),同时会释放对象锁(这就能解释上面的例子不会发生死锁);
2、当线程B执行notify方法时,会唤醒一个被挂起的线程A;
疑问一:为什么 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对象锁。