概述

  java.util.concurrent.locks.ReentrantLock 实现 java.util.concurrent.locks.Lock 接口,加锁(lock)和 解锁(unlock)方法都基于 AQS(java.util.concurrent.locks.AbstractQueuedSynchronizer)实现,AQS 是基于 sun.misc.Unsafe 类的 CAS算法相关方法实现的。

代码实例

import java.util.concurrent.locks.ReentrantLock;

public class Main {

	public static void main(String[] args) {
Command c = new Command();
int nThreads = 5;
for(int i = 0; i < nThreads; i++){
new Thread(c).start();
}
} } class Command implements Runnable { ReentrantLock lock = new ReentrantLock(); @Override
public void run() {
try{
lock.lock();
System.out.println(Thread.currentThread().getName() + ": 获得锁!");
}catch(Exception e){
//处理异常
}finally{
System.out.println(Thread.currentThread().getName() + ": 释放锁!");
lock.unlock();
}
}
}

打印结果: 一个线程独占锁(排他锁),释放锁后其他的线程才能获得锁。

ReentrantLock 初始化

  1). 首先来看下 ReentrantLock 的属性定义:

  

  ReentrantLock 的功能主要依靠定义的这个 Sync 类型的变量 sync 来实现。Sync 是 ReentrantLock 创建的一个静态内部类,它继承了 AQS(java.util.concurrent.locks.AbstractQueuedSynchronizer),即 ReentrantLock 的主要功能是依靠 AQS 实现的。

  2). ReentrantLock 的构造器

  

  ReentrantLock 提供了两种构造器,主要功能是对变量 sync 进行初始化,提供了 FairSync 和 NonFairSync两种实现,从命名可以看出分别对应公平锁和非公平锁的实现;其中 fair 为 true 时初始化为 FairSync(公平锁),fair 为 false 时初始化为 NonfairSync(非公平锁);无参构造器默认初始化为 NonfairSync。所谓 “公平” 在于是否按照线程申请锁的顺序获得锁,FairSync(公平锁)使用队列实现首先申请锁的先获得锁(FIFO),增加了额外的队列操作开销;相对而言,NonfairSync(非公平锁)的并发效能更高,吞吐量更高。

AQS 初始化

  1). AQS 的实现依靠 sun.misc.Unsafe 类中 CAS 算法相关的方法,故这里初始化了 Unsafe 对象 unsafe,以及相关变量的内存偏移量(offset),用于后面的 CAS 操作。

  

  2). AQS 属性介绍:AQS 使用双向链表的数据结构存储请求锁的线程,所以设计了一个节点类 Node,Node 类除了双向链表的前继指针(volatile Node prev)和后继指针(volatile Node next),存储数据包括 线程对象(volatile Thread thread)、等待状态(volatile int waitStatus)、下一个等待指针(Node nextWaiter)。一个节点表示一个线程。

  下面是一个关于 waitStatus 的值说明:

  AQS 除了双向链表的首节点(volatile Node head) 和 尾节点(volatile Node tail)外,还有一个状态属性(volatile int state)用于表示锁的状态。如果当前没有线程获得锁,state 的值为 0;若当前已经有一个线程获得锁,由于锁可重入,每获得一次锁加数加 1。

  下面代码展示了重入锁的实现,打印了线程重新获得同一把锁时,state 的值变化。

import java.util.concurrent.locks.ReentrantLock;

public class Main{
public static void main(String[] args){
Command c = new Command();
int nThreads = 5;
for(int n = 0; n < nThreads; n++){
new Thread(c).start();
}
}
} class Command implements Runnable{ ReentrantLock lock = new ReentrantLock(); @Override
public void run() {
try{
lock.lock();
run2();
}catch(Exception e){ }finally{
lock.unlock();
}
} public void run2(){
try{
lock.lock();
System.out.println(Thread.currentThread().getName() + ":" + lock.getHoldCount());
}catch(Exception e){ }finally{
lock.unlock();
}
} }

  打印结果:

  

  • getHoldCount 方法返回了当前线程获得锁的数量,如果当前线程没有获得锁则返回 0。可以看到这里调用的是变量 sync 的 getHoldCount() 方法。

 

  • 这里需要判断当前线程是否获得锁,即是否为独占锁的线程,是则调用 getState() 方法返回获得锁的数量,否则返回 0。

 

  • AQS 继承了 java.util.concurrent.locks.AbstractOwnableSynchronizer,该类存储了独占锁的线程的值。

  

  • AQS 中的 getState() 方法返回的就是 state 的值。 

加锁操作 lock()

  • lock() 的用途是当前线程尝试去获得锁,如果当前锁没有被其他线程持有,则当前线程可以立即获得锁,设置持有锁数量为 1;如果当前线程已经持有锁,再次调用 lock() 方法时会在原来持有锁数量上加 1;如果锁被其他线程持有,则当前线程保持休眠状态直到锁的持有数被设置为 1 的时候,才会被激活尝试去获得锁。

  

  首先来看下,NonfairSync(非公平锁)的实现:

  

  • compareAndSetState(0, 1) 是去获得锁,如果返回 true, 则获得锁成功,否则获得锁失败。这个方法是 AQS 中的方法,使用 Unsafe 类的 CAS 方法 compareAndSwapInt 尝试将 state 的值从 0 更新为 1。如果当前没有任何线程持有锁,state 为 0,则 CAS 成功,返回 true。注意 state 使用 volatile 修饰,state 的值被当前线程修改后,立即从工作内存刷新回主内存,其他线程可见最新的修改。

setExclusiveOwnerThread(Thread.currentThread());  则是将当前线程设置独占锁的线程。

  • 如果锁已经被持有(state != 0),持有线程可能是当前线程,也可能是其他线程。执行 acquire(1);

  • tryAcquire 方式是锁已经被持有的情况下,尝试去获得锁。

  获取锁的最新状态,通过是否为 0 判断锁是否被持有,如果没有被持有,跟上面一样的操作,通过 CAS 操作尝试去获得锁,如果获得锁成功则设置当前线程为独占锁的线程,返回 true;如果当前锁已被持有,判断独占锁的线程是否为当前线程,为当前线程则累加锁的数量,修改 state 变量,返回 true。例如一个线程持有锁的数量为 5,则 state 的值为 5。如果当前线程无法获得锁,则返回 false。

  • 如果 tryAcquire 返回 false, 则添加到独占锁等待队列中(addWaiter(Node.EXCLUSIVE))

   新建了一个 Node 节点 node,设置线程为当前线程,模式为独占锁(Node.EXCLUSIVE)

   这里创建了一个 CHL 队列锁,队列中的元素通过自旋操作尝试去获得锁。在队列中的节点按照 FIFO 的规则,越先进入队列的节点对应的线程越先获得锁。

  

 

 

  • 创建完 Node 节点 node 并加入 CHL 队列后,尝试通过队列获得锁。这里是一个自旋操作,判断当前节点 node 的前继节点(node.prev)是否为 head 节点,如果是,则使用 tryAcquire() 方法去尝试获得锁。

  

  • 成功获得锁后,通过 setHead(node) 将 head 节点设置为 node,并将 node 节点的 prev(前继节点) 和 thread(存储线程)设置为 null。设置 failed 变量为 false,并将 interrupted(是否中断)返回。

  • 每一次自旋操作,如果获取锁失败,则线程可以shouldParkAfterFailedAcquire 判断 pred (node 的前继节点)是否已经被阻塞。

  Node.SIGNAL : 值为 -1,waitStatus(等待状态) 设置为这个值说明节点线程已经要求其他的线程去将它唤醒(LockSupport#unpark),所以可以安全地对它进行阻塞(LockSupport#park),返回 true,其他情况都返回 false。

  Node.CANCEL:值为 1,唯一一个大于 0 的值,设置为这个值说明当前 pred (node 的前继节点)已经取消获得锁的操作,应该继续查找其前继节点(ws != Node.CANCEL)作为 node 的前继节点。

  其他情况,会调用 compareAndSetWaitStatus 方法将 pred (node 的前继节点)的 waitStatus(等待状态)更新为 NODE.SIGNAL。

  • 如果 shouldParkAfterFailedAcquire 返回 true, 即 node 的 前继节点已经被阻塞,则对当前线程进行 park 操作,并检查是否中断。这里使用 LockSupport#park() 方法进行阻塞线程。通过 Thread.interrupted() 判断线程是否被中断,并重置中断状态。如果线程被中断,则该方法返回 true, acquireQueued 方法中设置  interrupted 为 true。

  • 线程被重置中断后,返回是否中断为 true, 故在 acquire 方法中执行了 selfInterrupt 进行线程中断。

  • 最后在 finally 块中判断节点线程是否获得锁成功(failed = false),如果失败(failed = true),则执行 cancelAcquire 操作。(这里的代码不细说)
  • 接下来看 FairSync(公平锁)如何实现 lock(加锁操作)

    对比 NonFairSync(非公平锁),NonFairSync 会尝试先去获得锁,即后申请锁的线程有可能优先获得锁;而 FairSync(公平锁)则按照 FIFO 的原则让先申请锁的线程去获得锁。

  

  • 可以看到 FairSync(公平锁)的 tryAcquire 方法中与 NonFairSync(非公平锁)方法不同的是,它会先去进行 hasQueuedPredecessors 方法判断当前队列是否有线程正在进行获得锁操作。若返回 true,则有其他线程正在进行获得锁操作,当前线程不再进行获得锁操作,tryAcquire 方法返回 false。

  • 然后同样进入队列,执行与 NonfairSync(非公平锁)一样的操作

尝试获得锁 tryLock()

  • tryLock() 表示当前线程尝试去获得锁,若获得锁成功,则返回 true;若获得锁失败,则返回 false。

  • 这里用到的 nonfairTryAcquire 在上面 NonfairSync(非公平锁)实现方法中已经讲过了,但它属于 AQS 的部分,不属于 NonfairSync 的实现。可以看到 tryLock() 只会尝试一次去获得锁,并没有加入 CHL 锁队列中。

可以抛出异常的加锁方法  lockInterruptibly()

  • lockInterruptibly() 方法的实现逻辑与 lock() 大致相同,lockInterruptibly() 遇到线程中断的情况会抛出 InterruptedException 异常,lock() 不会抛出任何异常。

  • 同样加入 CHL 队列锁,不同的是 线程中断的时候,以下方法会抛出  InterruptedException 异常。

解锁操作  unlock()】 

  • unlock() 方法用于释放锁。如果当前线程是锁的持有者,则将持有锁的数量减 1;若持有锁的数量为 0,则锁已经被释放;如果当前线程不是锁的持有者,调用 unlock() 方法会抛出 IllegalMonitorStateException 异常。

  • 使用 tryRelease 释放锁,如果释放锁成功,则返回 true;释放锁失败则返回 false。

  • tryRelease  方法中首先判断当前线程是否持有锁(即判断当前线程是否为独占锁的线程),如果不是则抛出 IllegalMonitorStateException 异常。getState() 返回当前线程持有锁的数量,减去 releases (1) 得到释放锁后当前线程持有锁的数量 c。
  • 如果 c 为 0,则说明当前线程已经释放了锁,设置 free 为 true,设置独占锁的线程为 null。
  • 更新锁的状态为 c,返回是否释放锁 free。

  • tryRelease 返回 true,若 head 节点不为空且其  waitStatus(等待状态)不为 0,则调用 unparkSuccessor 唤醒队列的第一个线程节点

 

B9 Concurrent 重入锁(ReentrantLock)的更多相关文章

  1. synchronized关键字,Lock接口以及可重入锁ReentrantLock

    多线程环境下,必须考虑线程同步的问题,这是因为多个线程同时访问变量或者资源时会有线程争用,比如A线程读取了一个变量,B线程也读取了这个变量,然后他们同时对这个变量做了修改,写回到内存中,由于是同时做修 ...

  2. 轻松学习java可重入锁(ReentrantLock)的实现原理

    转载自https://blog.csdn.net/yanyan19880509/article/details/52345422,(做了一些补充) 前言 相信学过java的人都知道 synchroni ...

  3. 轻松学习java可重入锁(ReentrantLock)的实现原理(转 图解)

    前言 相信学过java的人都知道 synchronized 这个关键词,也知道它用于控制多线程对并发资源的安全访问,兴许,你还用过Lock相关的功能,但你可能从来没有想过java中的锁底层的机制是怎么 ...

  4. 17_重入锁ReentrantLock

    [概述] 重入锁可以完全代替synchronized关键字. 与synchronized相比,重入锁ReentrantLock有着显示的操作过程,即开发人员必须手动指定何时加锁,何时释放锁,所以重入锁 ...

  5. Java 重入锁 ReentrantLock 原理分析

    1.简介 可重入锁ReentrantLock自 JDK 1.5 被引入,功能上与synchronized关键字类似.所谓的可重入是指,线程可对同一把锁进行重复加锁,而不会被阻塞住,这样可避免死锁的产生 ...

  6. java 可重入锁ReentrantLock的介绍

    一个小例子帮助理解(我们常用的synchronized也是可重入锁) 话说从前有一个村子,在这个村子中有一口水井,家家户户都需要到这口井里打水喝.由于井水有限,大家只能依次打水.为了实现家家有水喝,户 ...

  7. Java 显示锁 之 重入锁 ReentrantLock(七)

    ReentrantLock 重入锁简介 重入锁 ReentrantLock,顾名思义,就是支持同一个线程对资源的重复加锁.另外,该锁还支持获取锁时的公平与非公平性的选择. 重入锁 ReentrantL ...

  8. Java多线程——深入重入锁ReentrantLock

    简述 ReentrantLock 是一个可重入的互斥(/独占)锁,又称为“独占锁”. ReentrantLock通过自定义队列同步器(AQS-AbstractQueuedSychronized,是实现 ...

  9. Java多线程系列——深入重入锁ReentrantLock

    简述 ReentrantLock 是一个可重入的互斥(/独占)锁,又称为“独占锁”. ReentrantLock通过自定义队列同步器(AQS-AbstractQueuedSychronized,是实现 ...

随机推荐

  1. 题解 CF1063B 【Labyrinth】

    题解 CF1063B [Labyrinth] 完了我发现我做CF的题大部分思路都和别人不一样qwq 这道题其实很水,不至于到紫题 我们只要bfs一下,向四个方向剪下枝,就A了(好像还跑的蛮快?) 是一 ...

  2. P1143 进制转换

    漂亮小姐姐点击就送:https://www.luogu.org/problemnew/show/P1143 题目描述 请你编一程序实现两种不同进制之间的数据转换. 输入输出格式 输入格式: 输入数据共 ...

  3. TensorFlow(十):卷积神经网络实现手写数字识别以及可视化

    上代码: import tensorflow as tf from tensorflow.examples.tutorials.mnist import input_data mnist = inpu ...

  4. 在OpenFOAM中做用户自定义库——编译library【转载】

    转载自:http://openfoam.blog.sohu.com/22041538.html OpenFOAM自己提供的标准类都是以库的形式提供的,并且利用头文件给出了库的应用接口.这样一来,用户的 ...

  5. 代码检查p626

    1 编译运行p626 图10-3代码,提交编译运行的截图 2 STDOUT_FILENO的值是多少?提交在Ubuntu中查找这个值的命令截图

  6. [RK3288] 外接USB设备出现丢数

    CPU:RK3288 系统:Android 5.1 主板外接 USB 接口的外设,经常会出现丢数的现象,这种问题在很多 USB 接口的外设上都遇到过,例如:USB读卡器.USB扫描枪等 有一个共同点是 ...

  7. centos7 windows7 双系统重新构建引导和启动顺序

    安装centos后无法引导启动windows7的解决方法 在电脑Windows7系统上安装Centos7,安装后找不到Windows7引导菜单. 原因:因为CentOS 7已采用新式的grub2系统, ...

  8. LC 990. Satisfiability of Equality Equations

    Given an array equations of strings that represent relationships between variables, each string equa ...

  9. Git<一> 手工编辑冲突

    一:背景 Neo君之前在写东西时,都是自己负责各自的模块,没有出现代码拉下来冲突的情况.最近Neo君在搞一搞前端的东东,跟同事功能有些冲突,所以就难免会冲突. 所以简单总结下,这次针对的情况是不同的用 ...

  10. [webpack]手写一个mvp版本的webpack

    let fs = require('fs'); let path = require('path'); let babylon = require('babylon'); // Babylon 把源码 ...