深入浅出多线程——ReentrantLock (一)
ReentrantLock是一个排它重入锁,与synchronized关键字语意类似,但比其功能更为强大。该类位于java.util.concurrent.locks包下,是Lock接口的实现类。基本用法如下:
class X {
private final ReentrantLock lock = new ReentrantLock();
// ...
public void m() {
lock.lock(); // block until condition holds
try {
// ... method body
} finally {
lock.unlock()
}
}
}
本文章会围绕核心方法lock(),unlock()进行分析。在开始之前,对部分概念进行阐述:
1,RenntrantLock是一个排它重入锁,重入次数为Integer.MAX_VALUE,其中通过构造实现两大核心(公平锁,非公平锁)。在默认情况下是非公平锁。
2,RenntrantLock的公平锁和非公平锁基于抽象类AbstractQueuedSynchornizer,简称AQS。在源码分析阶段,也会涉及该类相关的原理分析。更加详细的会在后续文章中单独说明。
3,AQS中涉及到了大量的Compare and swap操作,简称CAS。CAS利用的是cpu级别原子指令无锁的去修改目标值,在并发场景下只会有一个成功。在java中有大量的应用,其中最经典的为java.util.concurrent.atomic包下的相关类。更加详细的阐述会在后续文章中单独说明。
原理分析
我们按照默认的不公平锁为例子进行深入。
lock()方法分析
NonfairSync.lock()
final void lock() {
if (compareAndSetState(0, 1))
setExclusiveOwnerThread(Thread.currentThread());
else
acquire(1);
}
首先去尝试将state值从0改为1,如果修改成功,把该线程设置为Owner。因为用CAS的方式去修改这个值,在并发环境下只会有一个成功。不成功的则进入acquire(1)方法。
AbstractQueuedSynchronizer.acquire(int)
public final void acquire(int arg) {
if (!tryAcquire(arg) &&
acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
selfInterrupt();
}
该方法,首先在此尝试修改state的值,尽量用最小的代价设置成功。
具体方法代码如下:
NonfairSync.tryAcquire(int)
protected final boolean tryAcquire(int acquires) {
return nonfairTryAcquire(acquires);
}
直接调用了nonfairTryAcquire(acquires)。如下:
Sync.nonfairTryAcquire(int)
final boolean nonfairTryAcquire(int acquires) {
final Thread current = Thread.currentThread();
int c = getState();
if (c == 0) {
if (compareAndSetState(0, acquires)) {
setExclusiveOwnerThread(current);
return true;
}
}
else if (current == getExclusiveOwnerThread()) {
int nextc = c + acquires;
if (nextc < 0) // overflow
throw new Error("Maximum lock count exceeded");
setState(nextc);
return true;
}
return false;
}
首先判断state的值是否为0。如果为0,则尝试修改state为1,如果设置成功,则将执行线程Owner为当前线程。如果state不为0,则判断当前线程是否与执行线程Owner一致。如果一致则只对state加1,这个地方实现了类似偏向锁。
如果条件都不满足,返回false,则执行acquireQueued(addWaiter(Node.EXCLUSIVE), arg)方法。从里往外看,
首先调用的是addWaiter(Node.EXCLUSIVE)方法,该方法的参数为Node.EXCLUSIVE,表示为队列为独占模式。
AbstractQueuedSynchronizer.addWaiter(Node)
private Node addWaiter(Node mode) {
Node node = new Node(Thread.currentThread(), mode);
// Try the fast path of enq; backup to full enq on failure
Node pred = tail;
if (pred != null) {
node.prev = pred;
if (compareAndSetTail(pred, node)) {
pred.next = node;
return node;
}
}
enq(node);
return node;
}
第2行代码是 创建了一个独占模式的队列节点node,通过node实现可以看出是双向链表数据结构。
判断列队pred是否为空,如果不为空,则node的节点prev变量设置为pred。尝试去修改列队tail的值为node,如果成功则直接返回。
因为第一次进入,tail肯定为空,直接执行enq(node)方法。
AbstractQueuedSynchronizer.enq(Node)
private Node enq(final Node node) {
for (;;) {
Node t = tail;
if (t == null) { // Must initialize
if (compareAndSetHead(new Node()))
tail = head;
} else {
node.prev = t;
if (compareAndSetTail(t, node)) {
t.next = node;
return t;
}
}
}
}
enq(node)方法进来是一个自旋操作,一段很经典的代码。
首先判断tail是否为空,因为第一次进入肯定为空。那么实例化一个空节点,将队列head,tail指向该空节点。完成该动作后再次自旋,此时tail肯定是不为空的,则直接执行else内容。
首先将node节点的上游指向tail后利用cas将队列tail设置为node,然后将原先的tail(t)的next指向node,此时node节点成功加入列队中。
再次回到acquire(1)方法,执行acquireQueued(addWaiter(Node.EXCLUSIVE), arg)方法。
AbstractQueuedSynchronizer.acquireQueued(Node,int)
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);
}
}
该方法进来也是一个自旋操作,与enq方法类似。
第6行,node的上游此时指向的是空节点,虽然和head相等,但是由于是空线程,那么在执行tryAcquire(arg)方法肯定返回false。
代码直接来到了第13行,shouldParkAfterFailedAcquire(p,node)方法先执行,如下:
AbstractQueuedSynchronizer.shouldParkAfterFailedAcquire(Node,Node)
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;
}
首先判断node的上游节点的等待状态是否为-1,因为node的上游节点是空对象,waitStatus为初始值0。该方法会直接返回运行else里面内容,将waitStatus修改为-1。
通过acquireQueued自旋会再次来到该方法,此时waitStatus的值为-1,返回true。然后执行第二个方法parkAndCheckInterrupt()方法。如下:
AbstractQueuedSynchronizer.parkAndCheckInterrupt()
private final boolean parkAndCheckInterrupt() {
LockSupport.park(this);
return Thread.interrupted();
}
进入该方法直接调用LockSupport.park(this)方法,意思是将该线程直接暂停,其线程状态在Runnable变为WAITING。等待调用LockSupport.unpark(this)将其唤醒,再次进入acquireQueued的自旋当中,直至能成功的把state的值从0变为1为止,当修改成功后,将队列的head设置为当前node。
unlock()方法分析
ReentrantLock.lock()
public void unlock() {
sync.release(1);
}
这个没啥好说的,直接调用了AQS里面的release方法了。
AbstractQueuedSynchronizer.release(int)
public final boolean release(int arg) {
if (tryRelease(arg)) {
Node h = head;
if (h != null && h.waitStatus != 0)
unparkSuccessor(h);
return true;
}
return false;
}
首先调用tryRelease(int)方法,尝试去释放该锁。如果释放成功,则进入if方法体,首先判断队列的head不为空,在判断head.waitStatus不为0(当前实际值为-1),则调用unparkSuccessor(Node)。
Sync.tryRelease(int)
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;
}
首先将state减1后,判断是否为0,如果为0,则正式释放,如果不为0,仅仅将state的值更新。在这个方法可以反映出 lock()调用几次,必须有相应的unlock()调用次数,否则造成死锁。
AbstractQueuedSynchronizer.unparkSuccessor(int)
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);
}
首先判断node.waitStatus是否小于0,如果小于0,则将state变量修改为0。其次获取到node的下游节点,如果下游节点s 为空或者被取消,则从队列尾部向前查找符合条件的节点。如果不为空或者未被取消,则调用LockSupport.unpark(s.thread)将其唤醒。
总结
通过分析lock()和unlock(),我们得知AQS内部实现了一个基于双向链表的队列。发成资源竞争时,因为CAS的特性只会有一个成功,其他的均进入该队列,有点类似于synchronized的临界区。
执行中的线程再次调用lock()时,并不会进入等待列队,而是将state加1继续执行,基于偏向锁的思想去实现的。
在线程释放时,也要对应着将state进行每次减1。直到state值为0,才认为当前线程真正的释放。释放后调用当前线程的下游节点去执行,此时,因为是非公平锁的缘故,可能新加入的线程在当前线程释放时征用成功,state值又变为1。那当前线程的下游节点再次陷入WAITING状态。
深入浅出多线程——ReentrantLock (一)的更多相关文章
- 深入浅出多线程——ReentrantLock (二)
深入浅出多线程——ReentrantLock (一)文章中介绍了该类的基本使用,以及在源码的角度分析lock().unlock()方法.这次打算在此基础上介绍另一个极为重要的方法newConditio ...
- 多线程---ReentrantLock
package com.test; import java.util.Collection; import java.util.concurrent.locks.Lock; import java.u ...
- Java多线程——ReentrantLock源码阅读
上一章<AQS源码阅读>讲了AQS框架,这次讲讲它的应用类(注意不是子类实现,待会细讲). ReentrantLock,顾名思义重入锁,但什么是重入,这个锁到底是怎样的,我们来看看类的注解 ...
- java多线程 ReentrantLock
本章对ReentrantLock包进行基本介绍,这一章主要对ReentrantLock进行概括性的介绍,内容包括:ReentrantLock介绍ReentrantLock函数列表ReentrantLo ...
- java多线程---ReentrantLock源码分析
ReentrantLock源码分析 基础知识复习 synchronized和lock的区别 synchronized是非公平锁,无法保证线程按照申请锁的顺序获得锁,而Lock锁提供了可选参数,可以配置 ...
- 深入浅出Java多线程(2)-Swing中的EDT(事件分发线程) [转载]
本系列文章导航 深入浅出Java多线程(1)-方法 join 深入浅出Java多线程(2)-Swing中的EDT(事件分发线程) 深入浅出多线程(3)-Future异步模式以及在JDK1.5Concu ...
- java多线程(三)线程的安全问题
1.1. 什么是线程安全 如果有多个线程同时运行同一个实现了Runnable接口的类,程序每次运行结果和单线程运行的结果是一样的,而且其他的变量的值也和预期的是一样的,就是线程安全的:反之,则是线程不 ...
- Disruptor-NET和内存栅栏
Disruptor-NET算法(是一种无锁算法)需要我们自己实现某一种特定的内存操作的语义以保证算法的正确性.这时我们就需要显式的使用一些指令来控制内存操作指令的顺序以及其可见性定义.这种指令称为内存 ...
- [Java并发编程(一)] 线程池 FixedThreadPool vs CachedThreadPool ...
[Java并发编程(一)] 线程池 FixedThreadPool vs CachedThreadPool ... 摘要 介绍 Java 并发包里的几个主要 ExecutorService . 正文 ...
随机推荐
- win10 3dmax 激活后反复激活和激活码无效问题
我也是遇到这个问题在网上找答案,像什么断网,清理注册表,删除某个.dat文件 各种试了好多都没管用 弄这个弄了五六个小时才总算成功 心累 现在我总结一下这些方法 我是第一条成功的 其他的我试着都没用 ...
- ubuntu 下修改文件访问权限chmod 777 -R *血的教训!没事别乱开权限!用谁开谁的就行。。。最后不要用这个命令,文件操作全部改用终端
本文转自: 个人建议 Ubuntu下修改目录权限命令如下:chmod 600 name (只有所有者有读和写的权限)chmod 644 name (所有者有读和写的权限,组用户只有读的权限)chmod ...
- Akka(29): Http:Server-Side-Api,Low-Level-Api
Akka-http针对Connection的两头都提供了方便编程的Api,分别是Server-Side-Api和Client-Side-Api.通过这两个Api可以大大提高编程效率.当然,上期我们提到 ...
- Hadoop(十四)MapReduce原理分析
前言 上一篇我们分析了一个MapReduce在执行中的一些细节问题,这一篇分享的是MapReduce并行处理的基本过程和原理. Mapreduce是一个分布式运算程序的编程框架,是用户开发“基于had ...
- 【ASP.NET MVC 学习笔记】- 04 依赖注入(DI)
本文参考:http://www.cnblogs.com/willick/p/3223042.html 1.在一个类内部,不通过创建对象的实例而能够获得某个实现了公开接口的对象的引用.这种"需 ...
- 100. Same Tree(leetcode)
Given two binary trees, write a function to check if they are equal or not. Two binary trees are con ...
- java笔记3(动手动脑)
1.以下代码为何无法通过编译?哪儿出错了? 原因:已有的Foo()是带一个int型参数的构造方法,不存在无参的构造方法Foo() "构造方法" 当创建一个对象时,它的构造方法会被自 ...
- jquery ajax 数据传输
在 form表单中,需要发送给后台的是一串长数据,后台才能接受,而用户则只需要输入字符串中的一部分,这种情况下,就需要将用户输入内容,和剩余部分进行拼串,然后添加进 formData 中传输. 另一种 ...
- python、java和php的百度指数对比
Python: Python是纯粹的自由软件, 源代码和解释器CPython遵循 GPL(GNU General Public License)协议.Python语法简洁清晰,特色之一是强制用空白符( ...
- js让input失去焦点
要求:当我点击页面非文本框的地方,令文本框失去焦点 问题:一开始我的做法是让点击的地方得到焦点,实际上是无效的 $(this).focus(); 当时我也不知道为什么focus会失效, 问题在于f ...