线程间的协作
有些时候,线程之间需要相互配合,完成某项工作。
例如:A线程修改了一个对象的值,而B线程想要感知这个变化,然后进行相应的操作。整个过程中,A线程可以看成是“生产者”,B线程可以看成是消费者。
在这个场景中,简单的方式是让消费者线程不断地循环检查变量是否符合预期。但这种方式会存在以下弊端:
- 难以保证及时性
- 资源开销大,自旋会浪费cpu资源
因此,我们需要寻找新的方式来解决线程之间的协作问题。
显然,jdk已经为我们考虑在内了。解决此问题的方法在java中有以下两种方式:
- 基于内置锁synchronized,通过Object提供的wait方法以及notify、notifyAll方法来实现。
- 基于显示锁,通过Condition对象的await方法和signal()/signaAll方法来实现。
基于内置锁的线程间的通信
Java的 Object类本身就是监视者对象,Java对于Monitor Object模式做了内建的支持:
- 每个 Object 都带了一把看不见的锁,通常叫 Monitor锁, 这把锁就是监控锁
- synchronized 关键字修饰方法和代码块就是 同步方法
- wait()/notify()/notifyAll()方法构成监控条件(Monitor Condition)
下图描述了 Java Monitor 的工作远理:
- 线程进入同步方法中
- 为了继续执行临界区代码,线程必须获取Monitor锁。如果获取锁成功,将成为该监视者对象的拥有者。任一时刻内,监视者对象只属于一个活动线程(The Owner)。
- 拥有监视者对象的线程可以调用wait()进入等待集合(WaitSet),同时释放监视锁,进入等待状态。
- 其他线程调用notify()/notifyAll()接口唤醒等待集合中的线程,这些等待的线程需要重新获取监视锁后才能执行 wait() 之后的代码。
- 同步方法执行完毕了,线程退出临界区,并释放监视锁。
wait()
调用该方法的线程会进入WAITING状态,只有等待其他线程的通知或者被中断才会返回。调用wait()方法后,会释放对象的锁,进入等待池。
既然是释放对象的锁,那么肯定先要获得锁,所以wait是写在synchronized中的,否则会报异常。
wait(long time)/wait(long time,int nanos)
超时等待一段时间,如果没有通知则返回。
notify()
通知一个对象上等待的线程,使其从wait()方法返回,而返回的前提是该线程获取到了对象的锁。
notify()调用时并不会真正释放对象锁,必须等到synchronized方法或者语法块执行完才真正释放锁。举个例子:
Object object = new Object();
synchronized (object) {
object.notify();
while (true){
}
}
如上, 虽然调用了notify,但是后面进入了一个死循环。这会一直不能释放对象锁。
所以,即使它把在等待池中的线程唤醒放到了对象的锁池中,但是锁池中的线程不会运行,因为它始终拿不到锁。
notifyAll()
通知所有等待在该对象上的线程。
代码示例
public static void main(String[] args) {
final Object innerLock = new Object();
new Thread(() -> {
System.out.println("thread 0 init...");
synchronized (innerLock) {
try {
System.out.println("thread 0 get lock...");
System.out.println("thread 0 start exe wait()...");
innerLock.wait();
System.out.println("thread 0 exe wait() end...");
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}).start();
new Thread(() -> {
System.out.println("thread 1 init...");
synchronized (innerLock) {
System.out.println("thread 1 get lock...");
innerLock.notify();
System.out.println("thread 1 exe notify() end...");
}
}).start();
}
thread 0 init...
thread 0 get lock...
thread 0 start exe wait()...
thread 1 init...
thread 1 get lock...
thread 1 exe notify() end...
thread 0 exe wait() end...
根据上面的案例,我们可以得出大致流程是:
- lock对象维护了一个等待队列queue;
- 线程0中执行lock的wait方法,把线程0保存到queue中;
- 线程1中执行lock的notify方法,从等待队列中取出线程0继续执行;
基于显示锁的线程间的通信
基于显示锁实现的线程间同步机制是通过Condition接口的await()方法和signal()/signalAll()方法实现的。
Condition是在jdk 1.5中才出现的,它用来替代传统的Object的wait()、notify()实现线程间的协作。阻塞队列也是使用了Condition来模拟线程间的协作。
内置Monitor锁缺陷
过早唤醒
wait 方法的返回并不一定意味着线程正在等待的条件谓词已经成真。考虑下图的阻塞队列代码段:
假设有A,B两个线程阻塞在put函数,C线程调用take,获取并推出队列中一个数据,同时调用notifyAll唤醒A,B线程;若A线程获取到了内置锁,B阻塞在锁获取中,A 又向队列压入一个数据,此时队列又满了;A释放锁后,B线程获取锁,但是队列已满,条件谓词判断失败,再次 wait 阻塞。
信号丢失
信号丢失是指:线程正在等待一个已经发生过的唤醒信号。错误的编程模式通常会造成信号丢失。考虑图 3 的阻塞队列代码段:
假设有A线程阻塞在put函数,B线程阻塞在take函数,C线程调用take,然后使用 notify接口唤醒其中一个线程;不巧的是B线程被唤醒,B检查队列仍然为空,继续等待阻塞,此时应该被唤醒的 A 只能等待下一次的唤醒。
Condition
内置Monitor锁的过早唤醒和信号丢失,它们其实有一个共同的原因:多个线程在同一个条件队列上等待不同的条件谓词。
如果想编写一个带有多个条件谓词的并发对象,或者想拥有除了条件队列可见性以外的更多控制权,就可以使用显示的Lock和Condition而不是内置锁和条件队列。
与内置条件队列不同,对于每一个Lock,可以有任意数量的Condition对象,因此对于不同的条件谓词,对于同一个锁,可以用不同的Condition对象来控制。
Condition也提供了丰富的接口等待挂起(可轮询,可中断,可超时等)。接口如下所示:
void await() throws InterruptedException;
void awaitUninterruptibly();
long awaitNanos(long nanosTimeout) throws InterruptedException;
boolean await(long time, TimeUnit unit) throws InterruptedException;
void signal();
void signalAll();
代码示例
public static void main(String[] args) {
//锁对象
final Lock lock = new ReentrantLock();
final Condition writeCond = lock.newCondition();//写线程条件变量
final Condition readCond = lock.newCondition();//读线程条件变量
final LinkedList<V> items = new LinkedList<V>();
final int totalCount = 100;
public void put (V x) throws InterruptedException {
lock.lock();
try {
//如果队列满了
while (totalCount >= items.size()) {
writeCond.await();//阻塞写线程
}
items.addLast(x);
readCond.signal();//唤醒读线程
} finally {
lock.unlock();
}
}
public V take () throws InterruptedException {
lock.lock();
try {
//如果队列为空
while (items.size() == 0) {
readCond.await();//阻塞读线程
}
V x = items.removeFirst();
writeCond.signal();//唤醒写线程
return x;
} finally {
lock.unlock();
}
}
}
在put的时候写入数据,signal只会唤醒等待读的线程;对应的take取出数据后,唤醒的也只会是等待写的线程。
Condition比内置锁做的更加细致,能够很好的解决过早唤醒和信号丢失的问题。
https://www.jianshu.com/p/6fe4bc3374a2
https://www.jianshu.com/p/ffc0c755fd8d
http://jianshu.com/p/1c52f17efaab
https://www.cnblogs.com/dolphin0520/p/3920385.html
https://hellofrank.github.io/2020/06/22/Java%E5%B9%B6%E5%8F%91%E7%BC%96%E7%A8%8B%E9%82%A3%E4%BA%9B%E4%BA%8B%E5%84%BF-%E5%9B%9B-%E2%80%94%E2%80%94%E7%BA%BF%E7%A8%8B%E9%97%B4%E7%9A%84%E5%8D%8F%E4%BD%9C/