1.同步阻塞与异步非阻塞
1.1 同步阻塞消息处理
有这样一个系统功能,客户端提交Event至服务器,服务器接收到客户请求之后创建线程处理客户请求,经过复杂的业务计算后将结果返回给客户端。
这样的设计存在几个显著的缺陷:
- 同步Event提交,客户端等待时间太长(提交Event时长 + 接收Event创建Thread时间 + 业务处理时间 + 返回结果时长)会陷入阻塞,导致二次提交Event耗时过长。
- 由于客户端提交的Event数量不多,导致系统同时受理业务数量有限,也就是系统整体吞吐量不高。
- 这种一个线程处理一个Event的方式,会导致频繁的创建和开启,增加系统开销。
- 在业务达到峰值的时候,大量的业务处理线程会导致频繁的CPU上下文切换,进而降低系统性能。
1.2 异步非阻塞消息处理
在分析了同步阻塞消息处理缺陷后,我们采用异步非阻塞的方式,可以提高系统的吞吐量,且业务处理线程也可以控制在一个固定的范围,以增加系统的稳定性,如图:
客户端提交Event后会得到一个相应的工单号并且立即返回,Event则会被放置在Event队列中。服务器有若干个工作线程,不断从Event队列中获取任务并且进行异步处理,最后将处理结果保存在另外一个结果集中,如果客户端想要获得结果处理,则可凭借工单号再次获取。
两种方式相比较,我们发现异步非阻塞方式优势明显:
- 客户端不用等到结果处理结束之后才返回,从而提高了系统的吞吐量和并发量
- 服务器端的线程数量维持在一个可控的范围之内不会因太多的线程上下文切换而导致的系统额外开销
- 服务端线程可以重复使用,减少了线程创建带来的资源浪费
2.单线程间通信
2.1 初识wait和notify
实现一个EventQueue,该Queue有如下三种状态:
- 队列满----最多可容纳多少个Event,好比一个系统最多同时可受理多少业务;
- 队列空----当所有Event被处理并且没有新的Event提交的时候,此时队列为空的状态;
- 有Event但是没有满—有新的Event提交,但是没有达到队列的上限。
示例:EventQueue
public class EventQueue {
//事件队列的最大值
private final int max;
static class Event {
}
private final static int DEFAULT_MAX_EVENT = 10;
final LinkedList<Event> eventQueue = new LinkedList<>();
public EventQueue() {
this(DEFAULT_MAX_EVENT);
}
public EventQueue(int max) {
this.max = max;
}
/**
* 如果事件队列没有满则添加到队尾,否则等待
* @param event
*/
public void offer(Event event) {
synchronized (eventQueue) {
if (eventQueue.size() >= max) { //事件队列 > 队列定义的最大值
try {
console("the Queue is full.");
eventQueue.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
console("the new event is committed.");
eventQueue.addLast(event);
eventQueue.notify(); //唤醒那些曾经执行monitor的wait方法而陷入阻塞的线程
}
}
/**
* 从队头获取数据,如果队列中无可用的数据那么工作线程就会调用wait阻塞
*/
public Event take() {
synchronized (eventQueue) {
if (eventQueue.isEmpty()) {
try {
console("the queue is empty. 没有可以拿的要我怎么办!");
eventQueue.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
Event event = eventQueue.removeLast();
this.eventQueue.notify();
console("the event" + event + "is handled.");
return event;
}
}
public void console(String message) {
System.out.printf("%s:%s\n", Thread.currentThread().getName(), message);
}
}
定义测试类
public class EventClient {
public static void main(String args[]){
final EventQueue eventQueue = new EventQueue();
new Thread(() -> {
for( ; ; ){
eventQueue.offer(new EventQueue.Event()); //内部类实例
}
}, "producer").start();
new Thread(() -> {
for( ; ; ){
eventQueue.take();
try {
TimeUnit.MILLISECONDS.sleep(10);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}, "consumer").start();;
}
}
其中 Producer线程模拟提交Event的客户端几乎没有任何延迟,而Consumer线程则用于模拟处理请求的工作线程。运行上面代码,分析一下输出结果,输出结果如下:
从上述的日志可以看出,producer线程生产了10个线程,此时队列已经满了,那么它将会执行eventQueue的wait方法进入阻塞状态,consumer线程要处理数据,所以要花费10毫秒处理其中的一条数据,然后通知producer线程可以继续提交数据了,如此循环往复。
2.2 wait和notify方法详解
wait和notify方法是Object中的方法, 也就是说JDK中的每一个类都拥有这两个方法,先来看看wait方法,下面是wait方法的三个重载方法:
public final void wait() throws InterruptedException
public final void wait(long timeout)throws InterruptedException
public final void wait(long timeout, int nanos)throws InterruptedException
- wait的这三个方法都将调用wait(long timeout)这个方法,前文使用的wait方法等价于wait(0),0代表永不超时。
- Object中的wait(long timeout)方法会导致当前线程陷入阻塞,直到其他线程调用notify或者notifyAll方法才能将其唤醒,或者阻塞时间到达了timeout时间而自动唤醒。
- wait方法必须拥有该对象的monitor,也就是wait方法必须在同步方法中使用。
- 当前线程执行了该对象的wait方法之后,将会放弃对该monitor的所有权并且进入与该对象关联的wait set中,也就是说一旦线程执行了某个object的wait方法之后,它就会释放对该对象monitor的所有权,其它线程也就有机会继续争夺该monitor的所有权。
再来分析一下notify方:
public final native void notify();
- 唤醒单个正在执行该对象wait方法的线程
- 如果有某个线程由于执行该对象的wait方法而进入阻塞则会被唤醒,如果没有则会忽略
- 被唤醒的线程需要重新获取该对象的monitor锁才能继续执行
2.3 关于wait和notify注意事项
- wait方法是可中断的方法,这也意味着,当前线程一旦调用了wait方法进入阻塞状态,其它线程可以使用interrupt方法将其打断,可中断方法被打断后收到中断异常InterruptException,同时interrupt标识也会被擦除。
- 线程执行了某个对象的wait方法之后,会加入对应的wait set中,每一个线程的monitor都有一个与之有关联的wait set。
- 当线程加入wait set之后,notify可以将其唤醒,也就是从wait set中弹出,同时中断wait中的线程也会将其唤醒。
- 必须在同步方法中使用wait和notify方法,因为执行wait和notify的前提条件是必须持有同步方法的monitor的所有权。运行下任何一个方法都会抛出异常:
public void testWait()
{
try{
this.wait();
}cathc(InterruptedException e)
{
e.printStackTrace();
}
}
public void testNotify()
{
this.notify();
}
- 同步代码的monitor必须与执行wait、notify方法的对象一致,简单地说就是用那个monitor对象进行同步,就用哪个对象进行wait和notify操作。运行下面代码中的会抛出IllegalMonitorException异常信息:
public class Test {
private final Object MUTEX = new Object();
private synchronized void testWait(){
try {
MUTEX.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
private synchronized void testNotify(){
MUTEX.notify();
}
public static void main(String args[]){
Test test = new Test();
test.testWait();
test.testNotify();
}
}
2.4 wait和sleep
从上面可以看出,wait和sleep都可以使线程进入阻塞状态,但是两者有着本质的区别:
- wait和sleep方法都可以使线程进入阻塞状态
- wait和sleep方法均是可中断的方法,被中断后回抛出中断异常
- wait是Object级别的方法,而sleep是Thread特有的方法
- wait方法的执行必须在同步方法中,而sleep则不然
- 线程在同步方法中执行sleep方法时,并不会释放monitor锁,而wait方法则会
- sleep方法进行短暂的休眠后会自动退出阻塞,而wait方法则需要被其它线程中断后才能退出阻塞
3.多线程通信
3.1 notifyAll方法
多线程的通信要用到Object的notifyAll方法,notifyAll方法可以同时唤醒阻塞的全部线程,当然了被唤醒的线程仍需要继续争抢monitor的锁。
3.2 生产者消费者
在2.1中我们定义了一个EventQueue,此队列的在多线程的环境中会出现数据不一致的情况,其中的情形是:LinkedList中没有元素的时候仍然调用removeFirst方法;当LinkList中的元素超过10个的时候仍旧执行了addLast方法
- LinkedList为空时执行removeFirst方法
其实我想着我们在EventQueue的方法中都加了synchronized数据同步,为什么还会出现不一致的情况?我们假设EventQueue的元素为空,两个线程在执行take方法时都陷入阻塞中,另外一个offer线程执行了addLast方法之后唤醒了其中一个阻塞的take线程,该线程消费了一个元素之后刚好唤醒了一个take线程,这时就会执行空LinkedList
- LinkedList为10时执行addLast方法
假设某个时刻EventQueue中存在10个Event数据,其中两个线程在执行offer方法的时候分别因为调用了wait方法而进入阻塞中,另外一个线程执行了take方法消费了元素并唤醒了一个offer线程,而该offer线程执行了addLast方法之后,queue中的元素为10,并且再次执行唤醒方法,恰巧另外一个offer线程也被唤醒,因此可绕开阈值检查eventQueue.size()>=max,制使EventQueue中的元素超过10个,执行过程如下图所示。
- 改进
只需要将临界值的判断if改为while,将notify更改为notifyAll即可。
/**
* 如果事件队列没有满则添加到队尾,否则等待
* @param event
*/
public void offer(Event event) {
synchronized (eventQueue) {
while (eventQueue.size() >= max) { //事件队列 > 队列定义的最大值
try {
console("the Queue is full.");
eventQueue.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
console("the new event is committed.");
eventQueue.addLast(event);
eventQueue.notifyAll(); //唤醒那些曾经执行monitor的wait方法而陷入阻塞的线程
}
}
/**
* 从队头获取数据,如果队列中可用的数据那么工作线程就会调用wait阻塞
*/
public Event take() {
synchronized (eventQueue) {
if (eventQueue.isEmpty()) {
try {
console("the queue is empty. 没有可以拿的要我怎么办!");
eventQueue.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
Event event = eventQueue.removeLast();
this.eventQueue.notifyAll();
console("the event" + event + "is handled.");
return event;
}
}
3.3 线程休息室wait set
在虚拟机规范中存在一个wait set的概念,线程调用了对象的wait方法之后线程会被加入与该对象monitor关联的wait set中,并且释放monitor的所有权。
下图是若干个线程调用wait方法之后被加入与monitor关联的wait set中,当另外一个线程调用该monitor的notify方法之后,其中一个线程会从wait set中弹出,至于是随机弹出还是以先进先出的方式弹出,虚拟机没说
而执行notifyAll则不需要考虑那个线程会被弹出,因为wait set中的所有wait线程都将被弹出