Java并发包源码学习之AQS框架(三)LockSupport和interrupt
接着上一篇文章今天我们来介绍下LockSupport
和Java中线程的中断(interrupt)
。
其实除了LockSupport,Java之初就有Object
对象的wait和notify方法可以实现线程的阻塞和唤醒。那么它们的区别 是什么呢?
主要的区别应该说是它们面向的对象不同。阻塞和唤醒是对于线程来说的,LockSupport的park/unpark更符合这个语义,以“线程”作为方法的参数, 语义更清晰,使用起来也更方便。而wait/notify的实现使得“线程”的阻塞/唤醒对线程本身来说是被动的,要准确的控制哪个线程、什么时候阻塞/唤醒很困难, 要不随机唤醒一个线程(notify)要不唤醒所有的(notifyAll)。
wait/notify
最典型的例子应该就是生产者/消费者了:
class BoundedBuffer1 {
private int contents; final Object[] items = new Object[100];
int putptr, takeptr, count; public synchronized void put(Object x) {
while (count == items.length) {
try {
wait();
} catch (InterruptedException e) {
}
} items[putptr] = x;
if (++putptr == items.length)
putptr = 0;
++count;
notifyAll();
} public synchronized Object take() {
while (count == 0) {
try {
wait();
} catch (InterruptedException e) {
}
}
Object x = items[takeptr];
if (++takeptr == items.length)
takeptr = 0;
--count;
notifyAll();
return x;
} public static class Producer implements Runnable { private BoundedBuffer1 q; Producer(BoundedBuffer1 q) {
this.q = q;
new Thread(this, "Producer").start();
} int i = 0; public void run() {
int i = 0;
while (true) {
q.put(i++);
}
}
} public static class Consumer implements Runnable { private BoundedBuffer1 q; Consumer(BoundedBuffer1 q) {
this.q = q;
new Thread(this, "Consumer").start();
} public void run() {
while (true) {
System.out.println(q.take());
}
}
} public static void main(String[] args) throws InterruptedException {
final BoundedBuffer1 buffer = new BoundedBuffer1();
new Thread(new Producer(buffer)).start();
new Thread(new Consumer(buffer)).start();
}
}
上面的例子中有一点需要知道,在调用对象的wait
之前当前线程必须先获得该对象的监视器(synchronized
),被唤醒之后需要重新获取到监视器才能继续执行。
//wait会先释放当前线程拥有的监视器
obj.wait();
//会re-acquire监视器
而LockSupport
并不需要获取对象的监视器。LockSupport机制是每次unpark
给线程1个“许可”——最多只能是1,而park
则相反,如果当前 线程有许可,那么park方法会消耗1个并返回,否则会阻塞线程直到线程重新获得许可,在线程启动之前调用park/unpark
方法没有任何效果。
// 1次unpark给线程1个许可
LockSupport.unpark(Thread.currentThread());
// 如果线程非阻塞重复调用没有任何效果
LockSupport.unpark(Thread.currentThread());
// 消耗1个许可
LockSupport.park(Thread.currentThread());
// 阻塞
LockSupport.park(Thread.currentThread());
因为它们本身的实现机制不一样,所以它们之间没有交集,也就是说LockSupport阻塞的线程,notify/notifyAll没法唤醒。
实际上现在很少能看到直接用wait/notify的代码了,即使生产者/消费者也基本都会用Lock
和Condition
来实现,我会在后面《Java并发包源码学习之AQS框架(五)ConditionObject源码分析》 文章中再回头看这个例子。
总结下LockSupport
的park/unpark
和Object
的wait/notify
:
- 面向的对象不同;
- 跟Object的wait/notify不同LockSupport的park/unpark不需要获取对象的监视器;
- 实现的机制不同,因此两者没有交集。
虽然两者用法不同,但是有一点,LockSupport
的park和Object的wait一样也能响应中断。
public static void main(String[] args) throws InterruptedException {
final Thread t = new Thread(new Runnable() {
@Override
public void run() {
LockSupport.park();
System.out.println("thread " + Thread.currentThread().getId() + " awake!");
}
}); t.start();
Thread.sleep(3000); // 2. 中断
t.interrupt();
}
thread 9 awake!
在我之前的一篇博客“如何正确停止一个线程”有介绍过Thread.interrupt()
Thread.interrupt()方法不会中断一个正在运行的线程。这一方法实际上完成的是,在线程受到阻塞时抛出一个中断信号,这样线程就得以退出阻塞的状态。更确切的说,如果线程被Object.wait, Thread.join和Thread.sleep三种方法之一阻塞,那么,它将接收到一个中断异常(InterruptedException),从而提早地终结被阻塞状态。
LockSupport.park()
也能响应中断信号,但是跟Thread.sleep()
不同的是它不会抛出InterruptedException
, 那怎么知道线程是被unpark还是被中断的呢,这就依赖线程的interrupted status,如果线程是被中断退出阻塞的那么该值被设置为true, 通过Thread的interrupted
和isInterrupted
方法都能获取该值,两个方法的区别是interrupted
获取后会Clear,也就是将interrupted status重新置为false。
AQS和Java线程池中都大量用到了中断,主要的作用是唤醒线程、取消任务和清理(如ThreadPoolExecutor的shutdown方法),AQS中的acquire
方法也有中断和不可中断两种。 其中对于InterruptedException
如何处理最重要的一个原则就是Don't swallow interrupts,一般两种方法:
- 继续设置interrupted status
- 抛出新的InterruptedException
try {
………
} catch (InterruptedException e) {
// Restore the interrupted status
Thread.currentThread().interrupt();
// or thow a new
//throw new InterruptedException();
}
AQS的acquire
就用到了第一种方法。
关于InterruptedException
处理的最佳实践可以看IBM的这篇文章。
最后按照惯例做下引申。上面BoundedBuffer1
类的put
和take
方法中的wait为什么要放在一个while循环里呢? 你如果去看Object.wait()
方法的Javadoc的话会发现官方也是建议下面这样的用法:
synchronized (obj) {
while (<condition does not hold>)
……
obj.wait();
……
}
StackOverflow上有一个问题里一个叫xagyg的回答解释的比较清楚,有兴趣的可以看下。 简单来说因为:
wait前会释放监视器,被唤醒后又要重新获取,这瞬间可能有其他线程刚好先获取到了监视器,从而导致状态发生了变化, 这时候用while循环来再判断一下条件(比如队列是否为空)来避免不必要或有问题的操作。 这种机制还可以用来处理伪唤醒(spurious wakeup),所谓伪唤醒就是no reason wakeup
,对于LockSupport.park()
来说就是除了unpark
和interrupt
之外的原因。
LockSupport
也会有同样的问题,所以看AQS的源码会发现很多地方都有这种re-check的思路,我们下一篇文就来看下AbstractQueuedSynchronizer
类的源码。
Java并发包源码学习之AQS框架(三)LockSupport和interrupt的更多相关文章
- Java并发包源码学习之AQS框架(四)AbstractQueuedSynchronizer源码分析
经过前面几篇文章的铺垫,今天我们终于要看看AQS的庐山真面目了,建议第一次看AbstractQueuedSynchronizer 类源码的朋友可以先看下我前面几篇文章: <Java并发包源码学习 ...
- Java并发包源码学习之AQS框架(一)概述
AQS其实就是java.util.concurrent.locks.AbstractQueuedSynchronizer这个类. 阅读Java的并发包源码你会发现这个类是整个java.util.con ...
- Java并发包源码学习之AQS框架(二)CLH lock queue和自旋锁
上一篇文章提到AQS是基于CLH lock queue,那么什么是CLH lock queue,说复杂很复杂说简单也简单, 所谓大道至简: CLH lock queue其实就是一个FIFO的队列,队列 ...
- Java并发包源码学习系列:AQS共享式与独占式获取与释放资源的区别
目录 Java并发包源码学习系列:AQS共享模式获取与释放资源 独占式获取资源 void acquire(int arg) boolean acquireQueued(Node, int) 独占式释放 ...
- Java并发包源码学习系列:CLH同步队列及同步资源获取与释放
目录 本篇学习目标 CLH队列的结构 资源获取 入队Node addWaiter(Node mode) 不断尝试Node enq(final Node node) boolean acquireQue ...
- Java并发包源码学习系列:ReentrantLock可重入独占锁详解
目录 基本用法介绍 继承体系 构造方法 state状态表示 获取锁 void lock()方法 NonfairSync FairSync 公平与非公平策略的差异 void lockInterrupti ...
- Java并发包源码学习系列:ReentrantReadWriteLock读写锁解析
目录 ReadWriteLock读写锁概述 读写锁案例 ReentrantReadWriteLock架构总览 Sync重要字段及内部类表示 写锁的获取 void lock() boolean writ ...
- Java并发包源码学习系列:详解Condition条件队列、signal和await
目录 Condition接口 AQS条件变量的支持之ConditionObject内部类 回顾AQS中的Node void await() 添加到条件队列 Node addConditionWaite ...
- Java并发包源码学习系列:挂起与唤醒线程LockSupport工具类
目录 LockSupport概述 park与unpark相关方法 中断演示 blocker的作用 测试无blocker 测试带blocker JDK提供的demo 总结 参考阅读 系列传送门: Jav ...
随机推荐
- EF 知识点
EntityFrameWorak知识点记录 发展史 EF1.0时,只支持Database First,数据库优先.必须将设计器指向一个现有的数据库. EF4时,支持Model First,模型优先.可 ...
- Ubuntu学习总结-05 安装和学习MySQL
1 更新源列表 在终端窗口输入以下命令更新安装源. sudo apt-get update 效果如下图所示: 2 安装MySQL 在终端输入如下命令安装MySQL, sudo apt-get inst ...
- http+mysql结合keepalived做热备
preface 公司要求http+mysql+redis+二次开发的ldap要求做高可用,所以此处写写keepalived在这种 环境下的高可用.keepalived这个软件我就不啰嗦了,众所周知,基 ...
- GitHub的三个按钮
star 的作用是收藏,目的是方便以后查找. watch 的作用是关注,目的是等作者更新的时候,可以收到通知 fork 的作用是参与,目的是你可以增加新的内容,然后 Pull Request,把你的修 ...
- uC/OS-II任务(OS_task)块
/*************************************************************************************************** ...
- Servlet Request的 getInputStream() getReader() getParameter()
如果你知道了这三者的区别,请忽略 最近碰到了servlet对入参获取方式的处理问题,因为二方库处理不当,导致了获取不到入参的情况,之前也知道这三个方法不兼容,现简单介绍下 1.这三个获取入参的方法,是 ...
- 通过Nginx,Tomcat访问日志(access log)记录请求耗时
一.Nginx通过$upstream_response_time $request_time统计请求和后台服务响应时间 nginx.conf使用配置方式: log_format main '$remo ...
- ntp服务器搭建与客户端设置
服务器对时间要求非常严格,linux服务器文件的生成,日志,以及数据库的连接都是根据时间的逻辑 进行任务处理,如果时间不同步,那么,处理的任务,以及出问题的分析日志,时间不对,很难分析 直接更改lin ...
- js正则匹配浮点数或整数
var pos='point(12.4 -45.423453)';var re = /([+]\d+[.]\d+|[-]\d+[.]\d+|\d+[.]\d+|[+]\d+|[-]\d+|\d+)/i ...
- JavaScript学习笔记——对表单的操作
javascript-对表单的操作实例讲解 <form name="myform" id="form1" action="" meth ...