Java 线程间通信
1 同步阻塞&异步阻塞
同步和异步是相对于执行结果来说,会不会等待结果返回;
阻塞和非阻塞是相对于线程是否被阻塞;
知乎
博客1
博客2
1.1 同步阻塞
一个业务逻辑需要等待上个线程完整的生命周期;
请求量大,程序同一时间受理的数量有限,也就是系统的整体吞吐量不高;
一个线程处理一个请求的方式,会导致频繁创建和销毁线程,会增加系统的额外开销;
业务达到峰值时,大量的业务处理线程阻塞会导致频繁的CPU上下文切换,从而降低系统性能;
1.2 异步阻塞
一个请求来之后会立刻得到一个结果,而系统有若干个业务处理线程,同时处理这个请求,处理结果可从工单等标记获取;
客户端不需要等到程序执行完毕,从而提高了系统的吞吐量和并发量;
服务端线程控制在一定范围内,并进行重复利用,可减少上线程下文切换造成的开销;
2 单个线程间通信
2.1 wait¬ify
它们是Object的方法,wait使线程进入阻塞,会释放锁,notify将线程唤醒,仅仅只是通知,不释放锁;
- wait方法和notify方法都是Object的方法
- wait()和wait(long,int)最终掉的都是wait(long)这个本地方法
- notify()和notifyAll()是本地方法
- 线程A调对象Object的wait方法,线程A进入阻塞状态
- 线程B调对象Object的notify方法,线程A被唤醒
wait:
使线程进入到竞争monitor锁的wait Set中,并处于阻塞状态;
wait方法的三个重载方法都调用wait(long timeout)这个方法;
wait方法会导致线程进入阻塞状态,只有其他线程调用Object的notify或notifyAll方法或阻塞时间到达后,才可将其唤醒;
wait方法必须拥有该对象的monitor锁,也就是wait方法必须在同步方法中使用;
当前线程执行了该对象的wait方法后,就失去了该对象的monitor锁,并进入其wait set中;
notify:
唤醒竞争monitor锁而陷入阻塞状态的线程;
唤醒单个正在执行对象wait方法的线程;
如果有某个线程由于执行该对象wait方法而进入阻塞则会被唤醒,如果没有则忽略;
被唤醒的线程需要重新竞争该对象所关联的monitor锁才能继续执行;
通过案例熟悉wait¬ify的用法,了解其对线程的影响:
事件队列有固定长度,其有两个方法,一个往队列中塞值,错过最大长度则不允许塞值了,一个从队列中取值,如果为空就等着:
/**
* 事件队列
*/
public class EventQueue {
private final int max;
//内部类 只是用作队列中存储的对象
static class Event {
}
//定义一个事件链表List
private final LinkedList<Event> eventQueue = new LinkedList<>();
//最大10个时间
private final static int DEFAULT_MAX_EVENT = 10;
public EventQueue() {
this(DEFAULT_MAX_EVENT);
}
public EventQueue(int max) {
this.max = max;
}
/**
* 往队列中添加Event
*
* @param event
*/
public void offer(Event event) {
//同步块
synchronized (eventQueue) {
//如果超过最大数量 则使当前线程陷入阻塞
if (eventQueue.size() >= max) {
try {
console("the queue is full");
//调用wait方法使当前线程陷入阻塞 并放置到eventQueue's waitSet
eventQueue.wait();
} catch (Exception e) {
e.printStackTrace();
}
}
console("the new event is submmit");
//未超过最大数量 在添加至最后
eventQueue.addLast(event);
//调用notify方法唤醒eventQueue's waitSet中的线程
eventQueue.notify();
}
}
/**
* 从队列中取Event
*
* @return
*/
public Event take() {
synchronized (eventQueue) {
//当队列中为空时 则将线程至为阻塞状态
if (eventQueue.isEmpty()) {
try {
console("the queue is empty");
//调用wait方法使当前线程陷入阻塞 并放置到eventQueue's waitSet
eventQueue.wait();
} catch (Exception e) {
e.printStackTrace();
}
}
Event event = eventQueue.removeFirst();
//调用notify方法唤醒eventQueue's waitSet中的线程
eventQueue.notify();
console("the event " + event + "is handle");
return event;
}
}
private void console(String str) {
System.out.printf("%s:%s\n", Thread.currentThread().getName(), str);
}
}
事件队列客户端,启两个线程,一个不断地往队列中塞值,使其被塞满,一个不停地从队列中取值,只要有就取出来:
/**
* 事件队列客户端
*/
public class EventClient {
public static void main(String[] args) {
final EventQueue eventQueue = new EventQueue();
/**
* 不停地往eventQueue中塞event
*/
new Thread(() -> {
while (true) {
eventQueue.offer(new EventQueue.Event());
}
}, "Producer").start();
/**
* 每5s从eventQueue取出一个Event
*/
new Thread(() -> {
while (true) {
eventQueue.take();
try {
TimeUnit.SECONDS.sleep(5);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}, "Consumer").start();
}
}
执行结果:
Producer:the new event is submmit
Producer:the new event is submmit
Producer:the new event is submmit
Producer:the new event is submmit
Producer:the new event is submmit
Producer:the new event is submmit
Producer:the new event is submmit
Producer:the new event is submmit
Producer:the new event is submmit
Producer:the new event is submmit
Producer:the queue is full
Consumer:the event com.wxx.thread.waitnotify.EventQueue$Event@16187bcdis handle
Producer:the new event is submmit
Producer:the queue is full
Consumer:the event com.wxx.thread.waitnotify.EventQueue$Event@18df977eis handle
Producer:the new event is submmit
...
...
分析:
- 线程Producer一启动,就不停地往eventQueue塞值,eventQueue不一会就被塞满;
- eventQueue被塞满后,eventQueue的wait方法被调用,Producer线程陷入阻塞,并被放置到eventQueue的waitSet中进行等待;
- 线程Consumer一启动,就不停地从eventQueue取值,取一个值,同时调用eventQueue的notify方法,使eventQueue的waitSet中等待的线程唤醒,唤醒后的线程重新竞争eventQueue的monitor锁,往eventQueue塞值;
- 线程Consumer调用notify后休眠5s,所以eventQueue永不会没有值
- 3-4的逻辑不停地重复执行
由以上现象和分析得出如下结论:
- nodify唤醒单个正在执行该对象wati方法的线程
- 如果有某个线程由于执行该对象的wait方法而进入阻塞则会被唤醒,如果没有则忽略
- 被唤醒的线程需要重新获取对该对象关联的monitor锁才能继续执行
2.2 wait¬ify注意事项
- wait是可中断方法,当前线程一旦调用wait方法进入阻塞状态,其他线程调用Intterput方法可将其打断,被打断后会跑出InterruptedException;
- 线程执行了某个对象的wait方法后,会加入与之对应的wait set中,每个对象的monitor 锁都有一个与之关联的set;
- 当线程进入monitor set后,notify方法可以将其唤醒,也就是从wait set中将其弹出;
- 必须在同步方法中使用wait和notify方法,因为执行wait和notify的前提是必须持有同步方法的monitor锁;
- 同步代码的monitor必须与执行wait notify方法的对象一致;
2.3 wait&sleep
- wait和sleep方法都可以使线程进入阻塞状态;
- wait和sleep方法都是可中断方法,中断后收到中断异常;
- wait是Object的方法,sleep是Thread的方法;
- wait方法必须执行在同步方法中,而sleep不需要;
- 同步方法中执行sleep不会释放monitor锁,而wait会;
3 多线程间通信
前边讲述的是两个线程间的通信,如果在前边EventQueue案例中同时启多个线程进行offer和take操作,那就是多线程间的通信问题了,需要对其进行修改,因为eventQueue的wait set中可以存有多个等待的线程,与单线程间通信的区别就是notify和notifyAll的区别;
3.1 notifyAll方法
与notify类似,notify是每次唤醒一个线程,notifyAll可同时唤醒全部的阻塞线程,被唤醒后仍需要继续争抢关联对象的monitor锁,刚才的程序如果客户端有多个程序在执行take和offer方法时,会出现问题,这时需要将notify换成notifyAll;
4 线程休息室wait set
在虚拟机规范中存在一个wait set的概念,至于该wait set是怎样的数据结构,JDK官方没有给出明确定义,不同厂家JDK有着不同的实现,甚至相同的JDK厂家也存在着差异,anyway,线程调用了某个对象的wait方法之后都会被加入与该对象monitor关联的wait set中,并且释放monitor的所有权;
若干个 线程调用wait方法后,被加入到了与monitor关联的wait set中
notify:另外一个线程调用该monitor的notify方法后,其中一个线程会从wait set中弹出,至于是随机弹出还是以先进先出的方式弹出,虚拟机规范同样没有强制要求;
notifyAll:另外一个线程调用该monitor的notifyAll方法后,wait set中的所有线程都会被弹出;
5 自定义显示锁
5.1 synchronized的缺陷
synchronized关键字提供了一种排他的数据同步机制,某个线程在获取monitor lock的时候可能会陷入阻塞,这种阻塞有两个很明显的缺陷
- 无法控制阻塞时长
定义一个同步方法,休眠1天,当两个线程竞争该同步方法时,Thread2线程的等待时长完全取决于Thread1何时释放,如果Thread2计划最多1分钟获得执行权,否则就放弃,这个思路显然无法实现;
public class SynchronizedDefect {
public static void main(String[] args) throws InterruptedException {
SynchronizedDefect synchronizedDefect = new SynchronizedDefect();
Thread thread1 = new Thread(synchronizedDefect::syncMethod, "Thread1");
thread1.start();
TimeUnit.MILLISECONDS.sleep(2);
Thread thread2 = new Thread(synchronizedDefect::syncMethod, "Thread2");
thread2.start();
}
public synchronized void syncMethod() {
try {
TimeUnit.DAYS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
- Thread2若是因竞争某个monitor lock而陷入阻塞,那么它是无法中断的,虽然可以其的interrupt标识,但是sunchronized不像sleep和wait方法一样可以获取到终端信息
把刚才的main方法稍作修改:
public static void main(String[] args) throws InterruptedException {
SynchronizedDefect synchronizedDefect = new SynchronizedDefect();
Thread thread1 = new Thread(synchronizedDefect::syncMethod, "Thread1");
thread1.start();
TimeUnit.MILLISECONDS.sleep(2);
Thread thread2 = new Thread(synchronizedDefect::syncMethod, "Thread2");
thread2.start();
TimeUnit.MILLISECONDS.sleep(2);
thread2.interrupt();
System.out.println("Thread2 isInterrupted: "+thread2.isInterrupted());
System.out.println("Thread2's state: "+thread2.getState());
}
5.2 自定义显示锁
构造一个显式的BooleanLock,使其具备synchronized的所有功能,并可中断和lock超时的功能;
Lock接口:
public interface Lock {
void lock() throws InterruptedException;
void lock(long mills) throws InterruptedException, TimeoutException;
void unlock();
List<Thread> getBlockedThreads();
}
BooleanLock实现:
public class BooleanLock implements Lock {
private Thread currentThread;
private boolean locked = false;
private final List<Thread> blockedList = new ArrayList<>();
/**
* lock方法
* 1.使BooleanLock具有synchronized同步的特效
* 2.使线程可获取中断信息
*
* @throws InterruptedException
*/
@Override
public void lock() throws InterruptedException {
synchronized (this) {
while (locked) {
//暂存当前线程
final Thread tempThread = currentThread();
try {
//防止线程被中断 该线程还在blockedList中
if (blockedList.contains(tempThread))
blockedList.add(tempThread);
this.wait();
} catch (InterruptedException e) {
//如果当前线程在wait时被中断,则从blockedList中将其删除,避免内存泄漏
blockedList.remove(tempThread);
//继续抛出中断异常
throw e;
}
}
blockedList.remove(currentThread());
this.locked = true;
this.currentThread = currentThread();
}
}
/**
* @param mills
* @throws InterruptedException
* @throws TimeoutException
*/
@Override
public void lock(long mills) throws InterruptedException, TimeoutException {
synchronized (this) {
if (mills < 0) {
this.lock();
} else {
long remainingMills = mills;
long endMills = currentTimeMillis() + remainingMills;
while (locked) {
if (remainingMills <= 0) {
throw new TimeoutException("can not get the lock during" + mills);
}
if (!blockedList.contains(currentThread)) {
blockedList.add(currentThread);
}
this.wait(remainingMills);
remainingMills = endMills - currentTimeMillis();
}
blockedList.remove(currentThread());
this.locked = true;
this.currentThread = currentThread();
}
}
}
@Override
public void unlock() {
synchronized (this) {
if (currentThread == currentThread()) {
this.locked = false;
this.notifyAll();
}
}
}
@Override
public List<Thread> getBlockedThreads() {
return null;
}
}
客户端调用:
public class LockClient {
private final Lock lock = new BooleanLock();
/**
* 可中断同步方法
*/
public void syncMethod() {
try {
lock.lock();
int randomInt = current().nextInt(10);
System.out.println(currentThread() + "get the lock.");
TimeUnit.SECONDS.sleep(randomInt);
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
lock.unlock();
}
}
/**
* 可超时同步方法
*/
public void syncMethodTimeoutable() {
try {
lock.lock(1000);
System.out.println(currentThread() + "get the lock.");
int randomInt = current().nextInt(10);
TimeUnit.SECONDS.sleep(randomInt);
} catch (InterruptedException | TimeoutException e) {
e.printStackTrace();
} finally {
lock.unlock();
}
}
public static void main(String[] args) throws InterruptedException {
//1 lock()方法使BooleanLock具有synchronized同步的效果
LockClient lockClient = new LockClient();
IntStream.range(0, 10).mapToObj(i -> new Thread(lockClient::syncMethod)).forEach(Thread::start);
//2 syncMethod方法捕获了中断异常 使线程可获得中断信息
LockClient lockClientInterrupt = new LockClient();
new Thread(lockClientInterrupt::syncMethod, "T1").start();
TimeUnit.MILLISECONDS.sleep(2);
Thread t2 = new Thread(lockClientInterrupt::syncMethod, "T2");
t2.start();
t2.interrupt();
//3 syncMethodTimeoutable方法调用lock(long mills) 使线程竞争monitor锁可超时
LockClient lockClientTimeoutable = new LockClient();
new Thread(lockClientTimeoutable::syncMethod, "T3").start();
TimeUnit.MILLISECONDS.sleep(2);
Thread t4 = new Thread(lockClientTimeoutable::syncMethodTimeoutable, "T4");
t4.start();
TimeUnit.MILLISECONDS.sleep(10);
}
}
第一部分执行结果:
lock方法使BooleanLock具有synchronized同步的效果,线程按顺序执行:
Thread[Thread-0,5,main]get the lock.
Thread[Thread-9,5,main]get the lock.
Thread[Thread-2,5,main]get the lock.
Thread[Thread-8,5,main]get the lock.
Thread[Thread-1,5,main]get the lock.
Thread[Thread-7,5,main]get the lock.
Thread[Thread-3,5,main]get the lock.
Thread[Thread-6,5,main]get the lock.
Thread[Thread-4,5,main]get the lock.
Thread[Thread-5,5,main]get the lock.
第二部分执行结果,syncMethod方法捕获了中断异常,使程序可获得中断信息:
Thread[T1,5,main]get the lock.
java.lang.InterruptedException
at java.lang.Object.wait(Native Method)
at java.lang.Object.wait(Object.java:502)
at com.wxx.thread.obviouslock.BooleanLock.lock(BooleanLock.java:30)
at com.wxx.thread.obviouslock.LockClient.syncMethod(LockClient.java:14)
at java.lang.Thread.run(Thread.java:748)
第三部分执行结果,是线程在竞争monitor锁时可超时:
Thread[T3,5,main]get the lock.
java.util.concurrent.TimeoutException: can not get the lock during1000
at com.wxx.thread.obviouslock.BooleanLock.lock(BooleanLock.java:65)
at com.wxx.thread.obviouslock.LockClient.syncMethodTimeoutable(LockClient.java:35)
at java.lang.Thread.run(Thread.java:748)
参考文献:
[ 1 ] Java高并发编程详解 汪文君著。–北京:机械工业出版社,2018年6月第1版