概要:

ReentrantLock类内部总共存在Sync、NonfairSync、FairSync三个类,NonfairSync与FairSync类继承自Sync类,Sync类继承自AbstractQueuedSynchronizer抽象类

对ReentrantLock类的操作大部分都直接转化为对Sync和AbstractQueuedSynchronizer类的操作。

公平锁资源被其他线程占用且不是重入情况,该线程就会添加到同步队列的尾部,而不会先尝试获取资源。

非公平锁会尝试去获取资源,如果此时该资源恰好被释放,则会被当前线程获取,这就造成了不公平的现象,当获取不成功,再加入队列尾部。

一、前言

  在分析了AbstractQueuedSynchronier源码后,接着分析ReentrantLock源码,其实在AbstractQueuedSynchronizer的分析中,已经提到过ReentrantLock,ReentrantLock表示下面具体分析ReentrantLock源码。

二、ReentrantLock数据结构

  ReentrantLock的底层是借助AbstractQueuedSynchronizer实现,所以其数据结构依附于AbstractQueuedSynchronizer的数据结构,关于AQS的数据结构,在前一篇已经介绍过,不再累赘。

锁可重入的意义:防止死锁

  • 可重入锁,也叫做递归锁,指的是同一线程外层函数获得锁之后 ,内层递归函数仍然有获取该锁的代码,但不受影响。
  • 可重入锁(Reentrant Lock),是指允许同一个线程多次对该锁进行AQS的acquire动作。对于不可重入的锁,当一个线程多次调用acquire后将造成死锁。

三、ReentrantLock源码分析

  3.1 类的继承关系 

public class ReentrantLock implements Lock, java.io.Serializable

  说明:ReentrantLock实现了Lock接口,Lock接口中定义了lock与unlock相关操作,并且还存在newCondition方法,表示生成一个条件。

  3.2 类的内部类

  ReentrantLock总共有三个内部类,并且三个内部类是紧密相关的,下面先看三个类的关系。

  说明:ReentrantLock类内部总共存在Sync、NonfairSync、FairSync三个类,NonfairSync与FairSync类继承自Sync类,Sync类继承自AbstractQueuedSynchronizer抽象类。下面逐个进行分析。

  1. Sync类

  Sync类的源码如下  

abstract static class Sync extends AbstractQueuedSynchronizer {
// 序列号
private static final long serialVersionUID = -5179523762034025860L; // 获取锁
abstract void lock(); // 非公平方式获取
final boolean nonfairTryAcquire(int acquires) {
// 当前线程
final Thread current = Thread.currentThread();
// 获取状态
int c = getState();
if (c == 0) { // 表示没有线程正在竞争该锁
if (compareAndSetState(0, acquires)) { // 比较并设置状态成功,状态0表示锁没有被占用
// 设置当前线程独占
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;
} // 试图在共享模式下获取对象状态,此方法应该查询是否允许它在共享模式下获取对象状态,如果允许,则获取它
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;
} // 判断资源是否被当前线程占有
protected final boolean isHeldExclusively() {
// While we must in general read state before owner,
// we don't need to do so to check if current thread is owner
return getExclusiveOwnerThread() == Thread.currentThread();
} // 新生一个条件
final ConditionObject newCondition() {
return new ConditionObject();
} // Methods relayed from outer class
// 返回资源的占用线程
final Thread getOwner() {
return getState() == 0 ? null : getExclusiveOwnerThread();
}
// 返回状态
final int getHoldCount() {
return isHeldExclusively() ? getState() : 0;
} // 资源是否被占用
final boolean isLocked() {
return getState() != 0;
} /**
* Reconstitutes the instance from a stream (that is, deserializes it).
*/
// 自定义反序列化逻辑
private void readObject(java.io.ObjectInputStream s)
throws java.io.IOException, ClassNotFoundException {
s.defaultReadObject();
setState(0); // reset to unlocked state
}
}

说明:Sync类存在如下方法和作用如下。

  2. NonfairSync类

  NonfairSync类继承了Sync类,表示采用非公平策略获取锁,其实现了Sync类中抽象的lock方法,源码如下。

// 非公平锁
static final class NonfairSync extends Sync {
// 版本号
private static final long serialVersionUID = 7316153563782823691L; // 获得锁
final void lock() {
if (compareAndSetState(0, 1)) // 比较并设置状态成功,状态0表示锁没有被占用
// 把当前线程设置独占了锁
setExclusiveOwnerThread(Thread.currentThread());
else // 锁已经被占用,或者set失败
// 以独占模式获取对象,忽略中断
acquire(1);
} protected final boolean tryAcquire(int acquires) {
return nonfairTryAcquire(acquires);
}
}

说明:从NonfairSync类lock方法的源码可知,每一次都尝试获取锁,而并不会按照公平等待的原则进行等待,让等待时间最久的线程获得锁。

  3. FairSyn类

  FairSync类也继承了Sync类,表示采用公平策略获取锁,其实现了Sync类中的抽象lock方法,源码如下。 

// 公平锁
static final class FairSync extends Sync {
// 版本序列化
private static final long serialVersionUID = -3000897897090466540L; final void lock() {
// 以独占模式获取对象,忽略中断
acquire(1);
} /**
* Fair version of tryAcquire. Don't grant access unless
* recursive call or no waiters or is first.
*/
// 尝试公平获取锁
protected final boolean tryAcquire(int acquires) {
// 获取当前线程
final Thread current = Thread.currentThread();
// 获取状态
int c = getState();
if (c == 0) { // 状态为0,资源未被占据
if (!hasQueuedPredecessors() &&
compareAndSetState(0, acquires)) { // 不存在已经等待更久的线程并且比较并且设置状态成功
// 设置当前线程独占
setExclusiveOwnerThread(current);
return true;
}
}
else if (current == getExclusiveOwnerThread()) { // 状态不为0,即资源已经被线程占据。且当前进入的线程即独占资源的线程,这是重入的情况
// 下一个状态
int nextc = c + acquires;
if (nextc < 0) // 超过了int的表示范围
throw new Error("Maximum lock count exceeded");
// 设置状态
setState(nextc);
return true;
}
return false; // 状态为0但存在已经等待更久的线程或状态不为0且不是重入的情况,返回false,加入队列尾部
}

说明:跟踪FairSync类的lock方法的源码可知:

1.当资源空闲时,它总是会先判断sync队列(AbstractQueuedSynchronizer中的数据结构)是否有等待时间更长的线程,如果存在,则将该线程加入到等待队列的尾部,实现了公平获取原则。其中,FairSync类的lock的方法调用如下,只给出了主要的方法。

2.资源被其他线程占用且不是重入情况,该线程就会添加到sync queue中的尾部,而不会先尝试获取资源。

这也是和Nonfair最大的区别,Nonfair会尝试去获取资源,如果此时该资源恰好被释放,则会被当前线程获取,这就造成了不公平的现象,当获取不成功,再加入队列尾部。

附上AbstractQueuedSynchronizer中acquire()的源码:

1 public final void acquire(int arg) {
2 if (!tryAcquire(arg) &&
3 acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
4 selfInterrupt();
5 }

函数流程如下:

    1. tryAcquire()尝试直接去获取资源,如果成功则直接返回;
    2. addWaiter()将该线程加入等待队列的尾部,并标记为独占模式;
    3. acquireQueued()使线程在等待队列中获取资源,一直获取到资源后才返回。如果在整个等待过程中被中断过,则返回true,否则返回false。
    4. 如果线程在等待过程中被中断过,它是不响应的。只是获取资源后才再进行自我中断selfInterrupt(),将中断补上。

  3.3 类的属性

public class ReentrantLock implements Lock, java.io.Serializable {
// 序列号
private static final long serialVersionUID = 7373984872572414699L;
// 同步队列
private final Sync sync;
}

说明:ReentrantLock类的sync非常重要,对ReentrantLock类的操作大部分都直接转化为对Sync和AbstractQueuedSynchronizer类的操作。

  3.4 类的构造函数

  1. ReentrantLock()型构造函数  

public ReentrantLock() {
// 默认非公平策略
sync = new NonfairSync();
}

说明:可以看到默认是采用的非公平策略获取锁。

  2. ReentrantLock(boolean)型构造函数

public ReentrantLock(boolean fair) {
sync = fair ? new FairSync() : new NonfairSync();
}

说明:可以传递参数确定采用公平策略或者是非公平策略,参数为true表示公平策略,否则,采用非公平策略。

  3.5 核心函数分析

  通过分析ReentrantLock的源码,可知对其操作都转化为对Sync对象的操作,由于Sync继承了AQS,所以基本上都可以转化为对AQS的操作。如将ReentrantLock的lock函数转化为对Sync的lock函数的调用,而具体会根据采用的策略(如公平策略或者非公平策略)的不同而调用到Sync的不同子类。

  所以可知,在ReentrantLock的背后,是AQS对其服务提供了支持,由于之前我们分析AQS的核心源码,遂不再累赘。下面还是通过例子来更进一步分析源码。

四、示例分析

  4.1 公平锁 

package com.hust.grid.leesf.abstractqueuedsynchronizer;

import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock; class MyThread extends Thread {
private Lock lock;
public MyThread(String name, Lock lock) {
super(name);
this.lock = lock;
} public void run () {
lock.lock();
try {
System.out.println(Thread.currentThread() + " running");
try {
Thread.sleep(500);
} catch (InterruptedException e) {
e.printStackTrace();
}
} finally {
lock.unlock();
}
}
} public class AbstractQueuedSynchonizerDemo {
public static void main(String[] args) throws InterruptedException {
Lock lock = new ReentrantLock(true); MyThread t1 = new MyThread("t1", lock);
MyThread t2 = new MyThread("t2", lock);
MyThread t3 = new MyThread("t3", lock);
t1.start();
t2.start();
t3.start();
}
}

运行结果(某一次): 

Thread[t1,5,main] running
Thread[t2,5,main] running
Thread[t3,5,main] running

  说明:该示例使用的是公平策略,由结果可知,可能会存在如下一种时序。

  说明:首先,t1线程的lock操作 -> t2线程的lock操作 -> t3线程的lock操作 -> t1线程的unlock操作 -> t2线程的unlock操作 -> t3线程的unlock操作。根据这个时序图来进一步分析源码的工作流程。

  ① t1线程执行lock.lock,下图给出了方法调用中的主要方法。

  说明:由调用流程可知,t1线程成功获取了资源,可以继续执行。

  ② t2线程执行lock.lock,下图给出了方法调用中的主要方法。

  

  说明:由上图可知,最后的结果是t2线程会被禁止,因为调用了LockSupport.park。

  ③ t3线程执行lock.lock,下图给出了方法调用中的主要方法。

  说明:由上图可知,最后的结果是t3线程会被禁止,因为调用了LockSupport.park。

  ④ t1线程调用了lock.unlock,下图给出了方法调用中的主要方法。

  说明:如上图所示,最后,head的状态会变为0,t2线程会被unpark,即t2线程可以继续运行。此时t3线程还是被禁止。

  ⑤ t2获得cpu资源,继续运行,由于t2之前被park了,现在需要恢复之前的状态,下图给出了方法调用中的主要方法。

  说明:在setHead函数中会将head设置为之前head的下一个结点,并且将pre域与thread域都设置为null,在acquireQueued返回之前,sync queue就只有两个结点了。

  ⑥ t2执行lock.unlock,下图给出了方法调用中的主要方法。

  说明:由上图可知,最终unpark t3线程,让t3线程可以继续运行。

  ⑦ t3线程获取cpu资源,恢复之前的状态,继续运行。

  说明:最终达到的状态是sync queue中只剩下了一个结点,并且该节点除了状态为0外,其余均为null。

  ⑧ t3执行lock.unlock,下图给出了方法调用中的主要方法。

  说明:最后的状态和之前的状态是一样的,队列中有一个空节点,头结点为尾节点均指向它。

  使用公平策略和Condition的情况可以参考上一篇关于AQS的源码示例分析部分,不再累赘。

五、总结

  再掌握了AQS后,再来分析ReentrantLock的源码,就会非常简单,因为ReentrantLock的绝大部分操作都是基于AQS类的。所以,进行分析时要找准方向,就会事半功倍。谢谢各位园友观看~

转自https://www.cnblogs.com/leesf456/p/5383609.html

【JUC】JDK1.8源码分析之AbstractQueuedSynchronizer:http://www.cnblogs.com/leesf456/p/5350186.html

【JUC】JDK1.8源码分析之CountDownLatch:https://www.cnblogs.com/leesf456/p/5406191.html

AbstractQueuedSynchronizer类底层的数据结构是使用双向链表,是队列的一种实现,故也可看成是队列,其中Sync queue,即同步队列,是双向链表,包括head结点和tail结点,head结点主要用作后续的调度。而Condition queue不是必须的,其是一个单向链表,只有当使用Condition时,才会存在此单向链表。并且可能会有多个Condition queue。

【1】【JUC】JDK1.8源码分析之ReentrantLock的更多相关文章

  1. 【JUC】JDK1.8源码分析之ReentrantLock(三)

    一.前言 在分析了AbstractQueuedSynchronier源码后,接着分析ReentrantLock源码,其实在AbstractQueuedSynchronizer的分析中,已经提到过Ree ...

  2. 【JUC】JDK1.8源码分析之ArrayBlockingQueue(三)

    一.前言 在完成Map下的并发集合后,现在来分析ArrayBlockingQueue,ArrayBlockingQueue可以用作一个阻塞型队列,支持多任务并发操作,有了之前看源码的积累,再看Arra ...

  3. 【1】【JUC】JDK1.8源码分析之ArrayBlockingQueue,LinkedBlockingQueue

    概要: ArrayBlockingQueue的内部是通过一个可重入锁ReentrantLock和两个Condition条件对象来实现阻塞 注意这两个Condition即ReentrantLock的Co ...

  4. 【集合框架】JDK1.8源码分析HashSet && LinkedHashSet(八)

    一.前言 分析完了List的两个主要类之后,我们来分析Set接口下的类,HashSet和LinkedHashSet,其实,在分析完HashMap与LinkedHashMap之后,再来分析HashSet ...

  5. 【集合框架】JDK1.8源码分析之HashMap(一) 转载

    [集合框架]JDK1.8源码分析之HashMap(一)   一.前言 在分析jdk1.8后的HashMap源码时,发现网上好多分析都是基于之前的jdk,而Java8的HashMap对之前做了较大的优化 ...

  6. 【集合框架】JDK1.8源码分析之ArrayList详解(一)

    [集合框架]JDK1.8源码分析之ArrayList详解(一) 一. 从ArrayList字表面推测 ArrayList类的命名是由Array和List单词组合而成,Array的中文意思是数组,Lis ...

  7. 集合之TreeSet(含JDK1.8源码分析)

    一.前言 前面分析了Set接口下的hashSet和linkedHashSet,下面接着来看treeSet,treeSet的底层实现是基于treeMap的. 四个关注点在treeSet上的答案 二.tr ...

  8. 集合之LinkedHashSet(含JDK1.8源码分析)

    一.前言 上篇已经分析了Set接口下HashSet,我们发现其操作都是基于hashMap的,接下来看LinkedHashSet,其底层实现都是基于linkedHashMap的. 二.linkedHas ...

  9. 集合之HashSet(含JDK1.8源码分析)

    一.前言 我们已经分析了List接口下的ArrayList和LinkedList,以及Map接口下的HashMap.LinkedHashMap.TreeMap,接下来看的是Set接口下HashSet和 ...

随机推荐

  1. Java 编码规范 StandardCharsets.UTF_8 三个方法 toString() name() displayName(),到底用哪个方法更合适?

    想用StandardCharsets.UTF_8 返回"UTF-8"这个字符,测试一下,三个方法toString() name() displayName(),均能返回" ...

  2. jquery的extend方法(源码解析)

    1.前段时间一直忙于研究数据可视化(d3.js,three.js) 以及 php的 laravel框架,生活上也遇到很多事情,这大概就是人生中的迷茫期吧. 回想起,刚出来工作的时候,目标很明确,要学习 ...

  3. Docker 网络部分的简单学习以及转帖别人的blog

    1. 感谢一下 大神: http://www.cnblogs.com/sparkdev/ 最近有时间的话 就会读一下他的博客.学习了解docker相关的知识 今天简单做了下 测试 在这里面记录一下. ...

  4. PP学习笔记-业务基础

    生产主数据.生产计划.生产订单与生产执行 生产模块主要子模块及功能:PP-BD 基本数据管理 PP-SFC车间订单管理 PP-MRP物料需求计划 PP-MPS主生产计划 PP-CRP能力计划 PP-I ...

  5. 小项目分析之C++ 实现模拟银行排队

      一.问题定义与分析 问题定义 •要解决的问题——银行一天之内的: 1.总客户数 2.客户总逗留时间 3.客户平均逗留时间 问题分析 •新来的人找个短的队伍,站在队尾开始排队 •排在队头的人可以办理 ...

  6. String()与toString的区别

    1..toString()可以将所有的的数据都转换为字符串,但是要排除null 和 undefined 代码示例: var a = null.toString()--报错 var b = underf ...

  7. python学习笔记七——字典

    4.3 字典结构 字典是Python中重要的数据类型,字典的由“键-值”对组成的集合,字典中的“值”通过“键”来引用. 4.3.1 字典的创建 字典由一系列的“键-值”(key-value)对组成,“ ...

  8. 浅谈cpu.idle和cpu.load

    1.概述 大家经常对一个系统的容量进行评估时,会参考cpu.idle和cpu.load指标,但是这两个指标到底在什么区间,表示系统是正常或者异常呢,业内有不同的说法.因此本文搜集一些资料,并对一个系统 ...

  9. Java多线程之原子性 volatile、atomicInteger测试

    原文链接:http://www.cnblogs.com/zhengbin/p/5653051.html 一.补充概念 1.什么是线程安全性? <Java Concurrency in Pract ...

  10. ceph 安装ceph问题汇总

    1.在不同节点安装ceph时,出现以下异常: 参考这里 ceph deploy RuntimeError: NoSectionError: No section: 'ceph' 解决方法: 在报错的机 ...