并发编程中,容易混淆的一些概念和方法使用,本文来解惑。
常用的sleep两种方式:(都会抛出InterruptedException) 1、Thread.sleep(2000) 2、TimeUnit.SECONDS.sleep(2) 推荐使用,因为语意更加清晰
调用sleep和yield的时候不释放当前线程所获得的锁,但是调用await/wait的时候却释放了其获取的锁并阻塞等待。
sleep让线程阻塞,且在指定的时间之内都不会执行,时间到了之后恢复到就绪状态,也不一定被立即调度执行; yield只是让当前对象回到就绪状态,还是有可能马上被再次被调用执行。 await/wait,它会一直阻塞在条件队列之上,之后某个线程调用对应的notify/signal方法,才会使得await/wait的线程回到就绪状态,也是不一定立即执行。 下面贡献一幅图,一目了然:
在使用Lock之前,我们都使用Object 的wait和notify实现同步的。举例来说,一个producer和consumer,consumer发现没有东西了,等待,produer生成东西了,唤醒。形如下面的伪代码:
线程consumer
synchronize(obj){
obj.wait();//没东西了,等待
}
线程producer
synchronize(obj){
obj.notify();//有东西了,唤醒
}
有了lock后,世道变了,现在是:
//生产
lock.lock();
condition.await();
lock.unlock();
//消费
lock.lock();
condition.signal();
lock.unlock();
为了突出区别,省略了若干细节。区别有三点:
为什么需要使用condition呢?简单一句话,lock更灵活。以前的方式只能有一个等待队列,在实际应用时可能需要多个,比如读和写。为了这个灵活性,lock将同步互斥控制和等待队列分离开来,互斥保证在某个时刻只有一个线程访问临界区(lock自己完成),等待队列负责保存被阻塞的线程(condition完成)。
通过查看ReentrantLock的源代码发现,condition其实是等待队列的一个管理者,condition确保阻塞的对象按顺序被唤醒。
wait()和notify()必须在synchronized的代码块中使用 因为只有在获取当前对象的锁时才能进行这两个操作 否则会报异常 而await()和signal()一般与Lock()配合使用
notify():唤醒在此对象监视器上等待的单个线程。 notifyAll():唤醒在此对象监视器上等待的所有线程。线程通过调用其中一个 wait 方法,在对象的监视器上等待。
先说两个概念:锁池和等待池
所以我们可以很容易看到这两者的区别了:
综上,所谓唤醒线程,另一种解释可以说是将线程由等待池移动到锁池,notifyAll调用后,会将全部线程由等待池移到锁池,然后参与锁的竞争,竞争成功则继续执行,如果不成功则留在锁池等待锁被释放后再次参与竞争。而notify只会唤醒一个线程。有了这些理论基础,后面的notify可能会导致死锁,而notifyAll则不会的例子也就好解释了
public class NotifyDeadLockDemo {
public static void main(String[] args) {
final OutTurn outTurn = new OutTurn();
for (int i = 0; i < 100; i++) {
new Thread(() -> {
for (int j = 0; j < 5; j++) {
outTurn.sub();
}
}).start();
new Thread(() -> {
for (int j = 0; j < 5; j++) {
outTurn.main();
}
}).start();
}
}
}
class OutTurn {
private boolean isSub = true;
private int count = 0;
public synchronized void sub() {
try {
while (!isSub) {
this.wait();
}
System.out.println("sub --- " + count);
isSub = false;
this.notify();
} catch (InterruptedException e) {
e.printStackTrace();
}
count++;
}
public synchronized void main() {
try {
while (isSub) {
this.wait();
}
System.out.println("main --- " + count);
isSub = true;
this.notify();
} catch (InterruptedException e) {
e.printStackTrace();
}
count++;
}
}
输出:
死锁了
原因分析: OutTurn类中的sub和main方法都是同步方法,所以多个调用sub和main方法的线程都会处于阻塞状态,等待一个正在运行的线程来唤醒它们。下面分别分析一下使用notify和notifyAll方法唤醒线程的不同之处:
总结:notify方法很容易引起死锁,除非你根据自己的程序设计,确定不会发生死锁,notifyAll方法则是线程的安全唤醒方法。