深入浅出多线程——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 . 正文 ...
随机推荐
- SpringBoot01 InteliJ IDEA安装、Maven配置、创建SpringBoot项目、属性配置、多环境配置
1 InteliJ IDEA 安装 下载地址:点击前往 注意:需要下载专业版本的,注册码在网上随便搜一个就行啦 2 MAVEN工具的安装 2.1 获取安装包 下载地址:点击前往 2.2 安装过程 到官 ...
- flying-saucer + iText + Freemarker实现pdf的导出, 支持中文、css以及图片
前言 项目中有个需求,需要将合同内容导出成pdf.上网查阅到了 iText , iText 是一个生成PDF文档的开源Java库,能够动态的从XML或者数据库生成PDF,同时还可以对文档进行加密,权限 ...
- ArcGis for flex查询FeatureLayer数据
1. 首先实例化一个FeatureLayer对象 private var featureLayer:FeatureLayer=new FeatureLayer(); 2.指定FeatureLayer对 ...
- win10 uwp Window.Current.Dispatcher中Current为null
本文说的是进行网络中异步界面出现的错误,可能带有一定的主观性和局限性,说的东西可能不对或者不符合每个人的预期.如果觉得我有讲的不对的,就多多包含,或者直接关掉这篇文章,但是请勿生气或者发怒吐槽,可以在 ...
- LeetCode 563. Binary Tree Tilt (二叉树的倾斜度)
Given a binary tree, return the tilt of the whole tree. The tilt of a tree node is defined as the ab ...
- Crazy Calendar (阶梯博弈变形)
2011 was a crazy year. Many people all over the world proposed on 11-11-11, married on 11-11-11, som ...
- leaflet 利用ajax 将前端地图上的数据post到后台
生成Google地图,在地图上单击后,将该点的经纬度反馈给后台. 前端HTML代码: <!DOCTYPE html> <html> <head> <meta ...
- Nginx详解以及LNMP的搭建
v\:* {behavior:url(#default#VML);} o\:* {behavior:url(#default#VML);} w\:* {behavior:url(#default#VM ...
- cross-document message 跨文档通信 HTML5
跨域通信HTML5提供了XDM(cross-document message)安全简单接口: 核心是 postMessage()方法,用来向另一个地址传送信息: var iframeWindow ...
- 介绍一款好用 mongodb 可视化工具
最近想自己搭建一个个人博客,所以学了下mongodb,mongodb是用命令行输入的,有些人可能不太习惯,我自己找了下mongodb的一些可视化工具,一开始安装的是mongoVUE,mongoVUE页 ...