线程协作-JDK API
JDK中对于需要多线程协作完成某一任务的场景,提供了对应API支持。
多线程协作的典型场景是:生产者-消费者模型。(线程阻塞,线程唤醒)
示例:线程1去买包子,没有包子,则不再执行。线程-2生产包子,通知线程-1继续执行
- 被弃用的suspend和resume
作用:调用suspend挂起目标线程,通过resume可以恢复线程执行
-
能正确执行的代码
- 那么为什么被弃用?
因为容易写出死锁的代码,uspend挂起之后并不会释放锁
错误示例有下面两种情况
- wait/notify机制
这些方法只能由同一对象锁的持有者线程调用,也就是写在同步代码块(例如synchronized中,而且锁对象要是同一个)里面,否则会抛出illegalMonitorStateException异常。
wait方法导致当前线程等待,加入该对象(就是锁对象)的等待集合中,并且放弃当前持有的对象锁。
notify/notifyAll方法唤醒一个或所有正在等待这个对象锁的线程。
注意:虽然wait会自动解锁,但是对顺序还是有要求,如果在notify被调用之后,才开始wait方法的调用,线程会永远处于WAITING状态。
也就是说,使用wait/notify必须配合同步代码块
-
park/unpark机制
线程调用park则等待"许可",unpark方法为指定线程提供"许可"。
不要求park和unpark方法的调用顺序。
多次调用unpark之后,再调用park,线程会直接运行,但不会叠加(许可类似标记位,不会累加,每次park之后标记位重置,必须再unpark之后才能后续正确park),也就是说,连续多次调用park方法,第一次会拿到"许可"直接运行,后续调用会进入等待。
但是:park不会使用锁,这是它的缺点。
- 总结:
- park/unpark机制:不会使用锁,但是顺序可变
- wait/notify机制:wait释放锁,但是顺序有先后
- suspend/resume机制:即不释放锁,也有先后顺序的问题
-
补充:伪唤醒
在JDK的官方的wait()方法的注释中明确表示线程可能被“虚假唤醒“,JDK也明确推荐使用while来判断状态信息。那么这种情况的发生的可能性有多大呢?
使用生产者消费者模型来说明,伪唤醒造成的后果是本来未被唤醒的线程被唤醒了,那么就破坏了生产者消费者中的判断条件,也就是例子中的while条件number == 0或者number == 1。最终导致的结果就死0和1不能交替出现。
JDK的两种同步方案均可能出现这种伪唤醒的问题(API说明明确表示会出现这种现象),这两种组合是synchronized+wait+notify和ReentrantLock+await+signal。下面的例子中,如果把while换成if,那么就0、1就不能交替出现,反制则会,例子中是100个线程进行增加,100个线程进行减少。
在Java并发编程书上面引用了Thinking In Java的一句话说,大概意思是:任何并发编程都是通过加锁来解决。其实JDK并发编程也是通过加锁解决,每个对象都有一个对象锁,并且有一个与这个锁相关的队列,来实现并发编程。区别在于加锁的粒度问题,读读可以并发(在读远大于写的场景下比较合适),其他三种情况不能并发。而关系型数据库也是利用这一思想,只不过做得更加彻底,除了写写不能并发,其他三种情况都能并发,这得益于MVCC模型。
public class Resource {
public int number = 0;
public synchronized void add() {
while (number == 1) {
try {
wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
number++;
System.err.println(number + "-" + Thread.currentThread().getId());
notifyAll();
}
public synchronized void minus() {
while (number == 0) {
try {
wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
number--;
System.err.println(number + "-" + Thread.currentThread().getId());
notifyAll();
}
public static class AddThread implements Runnable {
private Resource resource;
public AddThread(Resource resource) {
this.resource = resource;
}
@Override
public void run() {
for (;;) resource.add();
}
}
public static class MinusThread implements Runnable {
private Resource resource;
public MinusThread(Resource resource) {
this.resource = resource;
}
@Override
public void run() {
for (;;) resource.minus();
}
}
public static void main(String[] args) {
Resource resource = new Resource();
for (int i = 0; i < 100; i++) {
new Thread(new AddThread(resource)).start();
new Thread(new MinusThread(resource)).start();
}
}
}
public class ResourceLock {
private int number = 0;
private final ReentrantLock lock = new ReentrantLock();
private final Condition condition = lock.newCondition();
public int getNumber() {
return this.number;
}
public void increase() {
lock.lock();
try {
while (number == 1) {
condition.await();
}
number++;
System.err.println(number + "-" + Thread.currentThread().getId());
condition.signalAll();
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
if (lock.isHeldByCurrentThread()) {
lock.unlock();
}
}
}
public void decrease() {
lock.lock();
try {
while (number == 0) {
condition.await();
}
number--;
System.err.println(number + "-" + Thread.currentThread().getId());
condition.signalAll();
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
if (lock.isHeldByCurrentThread()) {
lock.unlock();
}
}
}
static class IncreaseThread implements Runnable {
private ResourceLock resource;
public IncreaseThread(ResourceLock resource) {
this.resource = resource;
}
@Override
public void run() {
for (;;) resource.increase();
}
}
static class DecreaseThread implements Runnable {
private ResourceLock resource;
public DecreaseThread(ResourceLock resource) {
this.resource = resource;
}
@Override
public void run() {
for (;;) resource.decrease();
}
}
public static void main(String[] args) throws Exception {
ResourceLock resource = new ResourceLock();
ExecutorService es = Executors.newFixedThreadPool(5);
for (int i = 0; i < 100; i++) {
es.submit(new IncreaseThread(resource));
es.submit(new DecreaseThread(resource));
}
}
}
所以,伪唤醒的概率很大,建议书写相关代码的时候,用while替代if