并发编程实践五:ReentrantLock
ReentrantLock是一个可重入的相互排斥锁,实现了接口Lock,和synchronized相比,它们提供了同样的功能。但ReentrantLock使用更灵活。功能更强大,也更复杂。这篇文章将为你介绍ReentrantLock。以及它的实现机制。
ReentrantLock介绍
通常,ReentrantLock按以下的方式使用:
public class ReentrantLockTest {
private final ReentrantLock lock = new ReentrantLock();//问题1:lock为什么定义为final public void m() {
lock.lock();
try {
// method body
} finally {
lock.unlock();
}
}
}
首先须要定义一个lock,在使用时首先通过lock的lock方法加锁,然后运行临界区代码。最后在final中调用lock的unlock方法解锁(防止异常后无法解锁)。想了解锁的实现原理,能够參考上一篇:“并发编程实践四:实现正确和高效的锁”。
ReentrantLock提供了两种锁:公平锁和非公平锁,默认是非公平锁。
若指定为公平锁,则全部线程尽量依照调用lock的先后次序获取锁(问题二:为什么说尽量?),否则,假设为非公平锁,则调用lock的线程和等待队列中的线程将竞争锁。公平锁更加公平,但非公平锁则具有更好的性能。
ReentrantLock是可重入的。也就是一个线程能够多次调用lock成功,但要求调用了多少次lock,就须要相应调用多少次unlock。而且该锁最多支持同一个线程发起的2147483648(锁的数量是用一个int变量保存)个递归锁。超出这个限制将会导致lock方法抛出error。
ReentrantLock除了实现Lock接口外,还提供了一些辅助的方法,如:isLocked和getQueueLength等,这些方法对检測和监视可能非常实用。
以下我将对这些功能的内部实现做具体的介绍。
ReentrantLock实现
ReentrantLock内部使用了一个AQS的实现类,我在“并发编程实践二:AbstractQueuedSynchronizer”中对AQS的基本流程做过一个主要的介绍。并涉及到一些代码细节,只是不了解AQS也不会影响对这篇文章的理解。
ReentrantLock使用了AQS的相互排斥模式,以下我将分别介绍非公平锁、公平锁和ReentrantLock提供的辅助功能。
非公平锁
你能够使用以下的方法定义一个公平锁:
private final ReentrantLock lock = new ReentrantLock(false)。或者直接
private final ReentrantLock lock = new ReentrantLock()。
线程首先通过ReentrantLock的lock来申请锁,ReentrantLock的lock调用NonfairSync(AQS的实现类)的lock方法。
//NonfairSync的lock
final void lock() {
if (compareAndSetState(0, 1))
setExclusiveOwnerThread(Thread.currentThread());
else
acquire(1);
}
NonfairSync的lock方法首先尝试更改AQS的状态(这里也就是新到的线程和等待队列中的线程竞争获取锁,新到的线程可能会获得成功,导致不公平),假设更改成功则改动当前锁的owner为自己。然后返回。否则进入acquire。
public final void acquire(int arg) {
if (!tryAcquire(arg) &&
acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
selfInterrupt();
}
acquire调用tryAcquire来再次尝试获取锁,假设成功。则返回,否则调用addWaiter将自己增加等待队列,最后在acquireQueued中等待唤醒和运行唤醒后的操作。从tryAcquire開始:
protected final boolean tryAcquire(int acquires) {
return nonfairTryAcquire(acquires);
}
final boolean nonfairTryAcquire(int acquires) {
final Thread current = Thread.currentThread();
int c = getState();
if (c == 0) {//锁空暇
if (compareAndSetState(0, acquires)) {//尝试获取锁
setExclusiveOwnerThread(current);//设置锁owner
return true;
}
}
else if (current == getExclusiveOwnerThread()) {//owner是自己
int nextc = c + acquires;
if (nextc < 0) // 溢出,因为state是一个int。因此最多仅仅能申请2147483648次
throw new Error("Maximum lock count exceeded");
setState(nextc);
return true;
}
return false;
}
tryAcquire直接调用nonfairTryAcquire。nonfairTryAcquire首先获取AQS状态,假设状态为0,则说明当前锁已经空暇。则再次尝试更改状态,假设成功,则将锁的owner设置为自己。然后返回true。失败则返回false;假设AQS状态不为0,则说明锁已经被占用。假设owner是自己。则能够再次获取锁,假设锁已经溢出。则报错,否则设置AQS状态。返回true。不符合上述情况,返回false。
tryAcquire失败后,线程进入addWaiter将自己增加等待队列。
private Node addWaiter(Node mode) {
Node node = new Node(Thread.currentThread(), mode);
Node pred = tail;
if (pred != null) {//队列已经初始化
node.prev = pred;
if (compareAndSetTail(pred, node)) {//尝试
pred.next = node;
return node;
}
}
enq(node);
return node;
}
private Node enq(final Node node) {
for (;;) {
Node t = tail;
if (t == null) { //初始化队列
if (compareAndSetHead(new Node()))
tail = head;
} else {
node.prev = t;
if (compareAndSetTail(t, node)) {
t.next = node;
return t;
}
}
}
}
addWaiter中。假设tail不为空,则将tail通过CAS设置为当前线程节点。假设成功。则返回;否则将进入enq中循环加入节点到tail,直到成功。enq中。假设tail为空。则应该是首次使用队列。须要初始化,则将队列的head设置为一个空节点,假设成功,则将tail等于head,否则,假设失败,则说明有其他线程已经初始化了head,进入下一个循环又一次開始。
若队列不为空,则更改tail为当前节点,循环直到成功。
在线程将自己加入到等待队列后,线程则进入acquireQueued中。
final boolean acquireQueued(final Node node, int arg) {
boolean failed = true;
try {
boolean interrupted = false;
for (;;) {
final Node p = node.predecessor();
if (p == head && tryAcquire(arg)) {
setHead(node);
p.next = null; // help GC
failed = false;
return interrupted;
}
if (shouldParkAfterFailedAcquire(p, node) &&
parkAndCheckInterrupt())
interrupted = true;
}
} finally {
if (failed)
cancelAcquire(node);
}
}
acquireQueued中首先进行一个检查,假设当前节点的前续节点为head(说明当前节点已经为队列的第一个节点),则再次调用tryAcquire尝试获取锁(这个尝试是必须的。由于其他线程可能在该线程入队列之前已经释放了锁,假设不再次尝试,可能导致线程长时间等待)。成功或则更改head(节点出队列),然后返回。
假设当前节点的前续节点不为head,则首先在shouldParkAfterFailedAcquire中检查并更改前续节点状态,然后在parkAndCheckInterrupt中进入堵塞睡眠。
private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) {
int ws = pred.waitStatus;
if (ws == Node.SIGNAL)
return true;
if (ws > 0) {
do {
node.prev = pred = pred.prev;
} while (pred.waitStatus > 0);
pred.next = node;
} else {
compareAndSetWaitStatus(pred, ws, Node.SIGNAL);
}
return false;
}
shouldParkAfterFailedAcquire中需要推断pred的waitStatus。假设为Node.SIGNAL(表示其他线程释放锁后,会唤醒pred的兴许节点的线程)。则返回true,线程将在parkAndCheckInterrupt中进入堵塞睡眠。否则假设ws大于0(表示pred已经被取消),则将已经取消的节点删除,并返回false(可能node已经是队列的第一个节点。返回false将导致线程在acquireQueued中再次尝试获取锁,假设获取锁失败将再次进入shouldParkAfterFailedAcquire中);否则线程将尝试将pred的值设置为Node.SIGNAL,并返回false(返回false将导致在acquireQueued中再次尝试获取锁,这一点很重要,由于释放锁的线程仅仅有在pred的waitStatus为Node.SIGNAL时。才会运行唤醒线程的操作,而在这里将pred的waitStatus设置为Node.SIGNAL之前,可能其他线程已经释放了锁,假设不再尝试一次获取锁,可能会导致线程长时间堵塞,因此,在pred的waitStatus设置成功后。必需要又一次再尝试一次)。
当pred的waitStatus为Node.SIGNAL后,则线程在parkAndCheckInterrupt中进入堵塞睡眠。
private final boolean parkAndCheckInterrupt() {
LockSupport.park(this);
return Thread.interrupted();
}
线程进入堵塞睡眠后,就须要还有一个线程在释放了锁之后将其唤醒,还有一个线程会调用lock的unlock方法。
public void unlock() {
sync.release(1);
}
unlock终于调用AQS的release方法。
public final boolean release(int arg) {
if (tryRelease(arg)) {
Node h = head;
if (h != null && h.waitStatus != 0)
unparkSuccessor(h);
return true;
}
return false;
}
release中线程使用tryRelease释放锁。释放锁成功后将进入唤醒等待线程的流程:假设队列不为空。而且head的waitStatus不为0(表示存在兴许节点的线程等待被唤醒)。则调用unparkSuccessor唤醒兴许节点的线程。
protected final boolean tryRelease(int releases) {
int c = getState() - releases;
if (Thread.currentThread() != getExclusiveOwnerThread())//限制其他线程进入
throw new IllegalMonitorStateException();
boolean free = false;
if (c == 0) {
free = true;
setExclusiveOwnerThread(null);
}
setState(c);
return free;
}
tryRelease中获取AQS的状态,并减去releases(释放锁)。得到c。假设运行的线程不是锁的owner。则抛出异常(这里就限制了后面的代码仅仅有锁的owner线程可以进入),假设c为0。则表示锁已经被释放(假设线程获取了多次锁,则须要unlock多次后锁才干释放),将锁的owner设置为空,然后设置AQS的状态到c,返回锁是否已经释放。
释放锁成功后。线程将在unparkSuccessor中唤醒等待队列中的线程。
private void unparkSuccessor(Node node) {
int ws = node.waitStatus;
if (ws < 0)
compareAndSetWaitStatus(node, ws, 0);
Node s = node.next;
if (s == null || s.waitStatus > 0) {
s = null;
for (Node t = tail; t != null && t != node; t = t.prev)
if (t.waitStatus <= 0)
s = t;
}
if (s != null)
LockSupport.unpark(s.thread);
}
unparkSuccessor中,首先将node的waitStatus设置到0(node不再须要唤醒兴许节点了),然后删除掉已经取消的节点,将终于有效的节点保存到s,假设s不为空。则运行唤醒操作。
线程运行完唤醒操作后。就退出结束了,然后唤醒的线程将又一次进入acquireQueued中运行。
final boolean acquireQueued(final Node node, int arg) {
boolean failed = true;
try {
boolean interrupted = false;
for (;;) {
final Node p = node.predecessor();
if (p == head && tryAcquire(arg)) {
setHead(node);
p.next = null; // help GC
failed = false;
return interrupted;
}
if (shouldParkAfterFailedAcquire(p, node) &&
parkAndCheckInterrupt())
interrupted = true;
}
} finally {
if (failed)
cancelAcquire(node);
}
}
acquireQueued中,唤醒的线程的节点的前续肯定为head,线程将调用tryAcquire尝试获取锁(唤醒的线程将和新到的线程一起竞争锁),假设获取锁成功。则改动head(出队列)。并退出;否则将又一次进入堵塞状态(是不是非常郁闷)。
到这里,整个的流程就结束了,以下我们来看看公平锁。
公平锁
公平锁和非公平锁流程大致同样,仅仅是对新到的线程的处理上不一样,非公平锁是新到的线程和等待队列中的线程一起竞争锁。但公平锁则始终保证等待最长的线程获取锁。
公平锁的定义方式为:
private final ReentrantLock lock = new ReentrantLock(true);
公平锁和非公平锁的差异在于锁的获取上,公平锁的lock方法例如以下:
final void lock() {
acquire(1);
}
不像非公平锁直接尝试获取锁,公平锁不尝试获取锁,直接进入acquire,这里acquire的操作和非公平锁是一致的,差别在tryAcquire上。
protected final boolean tryAcquire(int acquires) {
final Thread current = Thread.currentThread();
int c = getState();
if (c == 0) {
if (!hasQueuedPredecessors() &&
compareAndSetState(0, acquires)) {
setExclusiveOwnerThread(current);
return true;
}
}
else if (current == getExclusiveOwnerThread()) {//owner是自己
int nextc = c + acquires;
if (nextc < 0)
throw new Error("Maximum lock count exceeded");
setState(nextc);
return true;
}
return false;
}
tryAcquire中公平锁在锁空暇(c==0)的情况下,首先通过hasQueuedPredecessors推断是否有等待线程,假设没有。才尝试获取锁,若获取锁成功,则将自己设置为锁的owner,并返回。假设锁不空暇。假设自己是锁的owner,则能够再次获取锁,否则返回false。
因此公平锁和非公平锁的差别就在于多了hasQueuedPredecessors推断。
public final boolean hasQueuedPredecessors() {
Node t = tail;
Node h = head;
Node s;
return h != t &&
((s = h.next) == null || s.thread != Thread.currentThread());
}
hasQueuedPredecessors中。假设tail和head不同,而且head的next为空或者head的next的线程不是当前线程,则表示队列不为空。
有两种情况会导致h的next为空:
1)当前线程进入hasQueuedPredecessors的同一时候。还有一个线程已经更改了tail(在enq中),但还没有将head的next指向自己。这中情况表明队列不为空;
2)当前线程将head赋予h后,head被还有一个线程移出队列,导致h的next为空。这样的情况说明锁已经被占用。
假设队列不为空(hasQueuedPredecessors返回true)。则tryAcquire返回false,线程将进入等待队列(后面的流程和非公平锁一致)。
因为线程的调度。非公平锁在推断的过程中可能出现:
线程A调用tryAcquire失败后,并在调用addWaiter之前,线程B释放了锁,且线程C推断到锁空暇,进入hasQueuedPredecessors返回false(等待队列为空)。终于C比A先获取到锁。
由此来看。公平锁也并不是绝对公平。
而且。公平锁在使用中,后来的线程总是须要进入等待队列等待,会导致效率减少,从JDK文档的描写叙述,效率将减少非常多。
结束语
这篇文章主要介绍了ReentrantLock的公平锁和非公平锁的实现流程。公平锁尽量保证获取锁的公平性,採用先来先得的原则。但因为线程的调度,会导致某些后到的线程先获取到锁;非公平锁不保证锁的获取的公平性,新到的线程将和等待队列中的线程竞争锁。公平锁具备公平性但性能差,而非公平锁不保证公平性但具有更好的性能。
并发编程实践五:ReentrantLock的更多相关文章
- 并发编程实践三:Condition
Condition实例始终被绑定到一个锁(Lock)上.Lock替代了Java的synchronized方法,而Condition则替代了Object的监视器方法,包含wait.notify和noti ...
- [Java 并发] Java并发编程实践 思维导图 - 第一章 简单介绍
阅读<Java并发编程实践>一书后整理的思维导图.
- [Java 并发] Java并发编程实践 思维导图 - 第二章 线程安全性
依据<Java并发编程实践>一书整理的思维导图.
- 读Java并发编程实践中,向已有线程安全类添加功能--客户端加锁实现示例
在Java并发编程实践中4.4中提到向客户端加锁的方法.此为验证示例,写的不好,但可以看出结果来. package com.blackbread.test; import java.util.Arra ...
- [Java并发编程(五)] Java volatile 的实现原理
[Java并发编程(五)] Java volatile 的实现原理 简介 在多线程并发编程中 synchronized 和 volatile 都扮演着重要的角色,volatile 是轻量级的 sync ...
- Java并发编程(五)JVM指令重排
我是不是学了一门假的java...... 引言:在Java中看似顺序的代码在JVM中,可能会出现编译器或者CPU对这些操作指令进行了重新排序:在特定情况下,指令重排将会给我们的程序带来不确定的结果.. ...
- 并发编程(五)LockSupport
并发编程(五)LockSupport LockSupport 提供 park() 和 unpark() 方法实现阻塞线程和解除线程阻塞,实现的阻塞和解除阻塞是基于"许可(permit)&qu ...
- [Java 并发] Java并发编程实践 思维导图 - 第四章 对象的组合
依据<Java并发编程实践>一书整理的思维导图. 第一部分: 第二部分:
- Java并发编程实践
最近阅读了<Java并发编程实践>这本书,总结了一下几个相关的知识点. 线程安全 当多个线程访问某个类时,不管运行时环境采用何种调度方式或者这些线程将如何交替执行,并且在主调代码中不需要任 ...
随机推荐
- linux服务之NFS和SAMBA服务
这几种网络文件传输最适合局域网.网络中用FTP 一:NFS服务 nfs(network file system)网络文件系统,改服务依赖于rpcbind服务.client通过rpc訪问server端的 ...
- C#面向对象1 类 以及 类的继承(new、ovverride)
类 的典型代码============================== 包括 属性及其判断赋值 方法 构造函数及其重载 ) { ...
- “/” 应用程序中的服务器错误 - IIS 发布错误
解决方法, 将bin目录下的全部文件复制到iis下
- HTML5的优缺点是什么?
HTML5的优缺点是什么?作为HTML的第五次重大修改,HTML5有哪些改进?HTML5又有哪些缺点? 网络标准 HTML5本身是由W3C推荐出来的,它的开发是通过谷歌.苹果,诺基亚.中国移动等几百家 ...
- python模块介绍- xlwt 创建xls文件(excel)
python模块介绍- xlwt 创建xls文件(excel) 2013-06-24磁针石 #承接软件自动化实施与培训等gtalk:ouyangchongwu#gmail.comqq 37391319 ...
- extjs_04_grid(弹出窗口&行编辑器 CRUD数据)
1.弹出窗口(添加.删除) watermark/2/text/aHR0cDovL2Jsb2cuY3Nkbi5uZXQvYWRhbV93enM=/font/5a6L5L2T/fontsize/400/f ...
- sql: sql developer使用
在sql developer中登陆某数据库,在procedure里面加入一个proc,种类选ARBOR: CREATE OR REPLACE PROCEDURE PROCEDURE23 IS NAM ...
- android的单元测试
1.新建android Test project 2. 选择针对测试的项目 3.新建类继承AndroidTestCase即可: package com.howlaa.sms.test; import ...
- Mac 下安装配置Mysql
在Mac 下载 Mysql Server : 参考:http://www.mysql.com/downloads/ 下载Mysql 安装程序 打开下载地址: http://www.mysql.com/ ...
- JS的预编译和执行顺序 详析
原文:JS的预编译和执行顺序 详析 最近在复习javascript的事件处理时发现了一个问题,然后也是我来写javascript的预编译和执行顺序的问题 代码: 复制代码 代码一 <ht ...