ReentrantLock是可重入的独占锁,同时只能有一个线程可以获取该锁,其他获取该锁的线程会被阻塞后放入该锁的AQS阻塞队列里面。

首先我们先看一下ReentrantLock的类图结构,如下图所示:

从类图可以知道,ReentrantLock最终还是使用AQS来实现,并且根据参数决定内部是公平锁还是非公平锁,默认是非公平锁。

首先我们先看ReentrantLock源码,看到其构造函数及其参数,这是决定内部是公平锁还是非公平锁,如下源码所示:

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

其中类Sync直接继承自AQS,它的子类NonfairSync和FairSync分别实现了获取锁的公平和非公平策略。

在这里AQS的状态值state代表线程获取该锁的可重入次数,默认情况下state的值为0,标示当前锁没有被任何线程持有,当一个线程第一次获取该锁的时候会尝试使用CAS设置state的值为1,如果CAS成功则当前线程获取了该锁,然后记录该锁的持有者为当前线程,

在该线程没有释放锁,第二次获取该锁后,状态值会加1,被设置为2,这就是可重入次数,在该线程释放该锁的时候,会尝试使用CAS让状态值减1,如果减1 后状态值为0 则当前线程释放该锁。

接下来我们看一下ReentrantLock是如何获取锁的,如下:

  1.void lock() 当一个线程调用该方法,说明该线程希望获取该锁,如果锁当前没有被其它线程占用并且当前线程之前没有获取该锁,则当前线程会获取到该锁,然后设置当前锁的拥有者为当前线程,并设置 AQS 的状态值为 1 后直接返回。

如果当前线程之前已经获取过该锁,则这次只是简单的把 AQS 的状态值 status 加 1 后返回。 如果该锁已经被其它线程持有,则调用该方法的线程会被放入 AQS 队列后阻塞挂起。源码如下:

  public void lock() {
sync.lock();
}

如上面代码所示,ReentrantLock的lock()是委托给sync类,根据创建ReentrantLock的时候,构造函数选择sync的实现是NonfairSync或者FairSync,这里先看sync的子类NonfairSync的情况,也就是非公平锁的时候,源码如下:

final void lock() {
//(1)CAS设置状态值
if (compareAndSetState(, ))
setExclusiveOwnerThread(Thread.currentThread());
else
//(2)调用AQS的acquire方法
acquire();
}

如上面代码所示,代码(1)因为默认AQS的状态值为0,所以第一个调用Lock的线程会通过CAS设置状态值为1,CAS成功则代表当前线程获取到了锁,然后setExclusiveOwnerThread 设置了该锁持有者是当前线程。

如果这时候有其他线程调用lock方法企图获取该锁,执行代码(1)CAS会失败,然后会调用AQS的acquire方法,这里注意传递参数为1,接下来我们看AQS的acquire的核心代码,如下:

   public final void acquire(int arg) {
//(3)调用ReentrantLock重写的tryAcquire方法
if (!tryAcquire(arg) &&
// tryAcquiref返回false会把当前线程放入AQS阻塞队列
acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
selfInterrupt();
}

之前说过 AQS 并没有提供可用的 tryAcquire 方法,tryAcquire 方法需要子类自己定制化,所以这里代码(3)会调用 ReentrantLock 重写的 tryAcquire 方法代码。

这里先看下非公平锁的源码代码如下:

protected final boolean tryAcquire(int acquires) {
return nonfairTryAcquire(acquires);
} final boolean nonfairTryAcquire(int acquires) {
final Thread current = Thread.currentThread();
int c = getState();
//(4)当前AQS状态值为0
if (c == ) {
if (compareAndSetState(, acquires)) {
setExclusiveOwnerThread(current);
return true;
}
}//(5)当前线程是该锁持有者
else if (current == getExclusiveOwnerThread()) {
int nextc = c + acquires;
if (nextc < ) // overflow
throw new Error("Maximum lock count exceeded");
setState(nextc);
return true;
}//(6)
return false;
}

正如上面代码(4)会看当前锁的状态值是否为0,为0则说明当前该锁空闲,那么就尝试CAS获取该锁(尝试将 AQS 的状态值从 0 设置为 1),并设置当前锁的持有者为当前线程返回返回 true。

如果当前状态值不为0 则说明该锁已经被某个县城持有,所以代码(5)看当前线程是否是该锁的持有者,如果当前线程是该锁持有者,状态值增加1,然后返回true。

如果当前线程不是锁的持有者则返回 false, 然后会被放入 AQS 阻塞队列。

到目前为止,介绍完了非公平锁的实现代码,回过头看看非公平锁在这里是怎么体现的,首先非公平是说:先尝试获取锁的线程并不一定比后尝试获取锁的线程优先获取锁。

这里假设线程 A 调用 lock()方法时候执行到了 nonfairTryAcquire 的代码(4)发现当前状态值不为 0,所以执行代码(5)发现当前线程不是线程持有者,则执行代码(6)返回 false,然后当前线程会被放入了 AQS 阻塞队列。

这时候线程 B 也调用了 lock() 方法执行到 nonfairTryAcquire 的代码(4)时候发现当前状态值为 0 了(假设占有该锁的其它线程释放了该锁)所以通过 CAS 设置获取到了该锁。而明明是线程 A 先请求获取的该锁那,这就是非公平锁的实现,

这里线程 B 在获取锁前并没有看当前 AQS 队列里面是否有比自己请求该锁更早的线程,而是使用了抢夺策略。

好了,知道非公平锁的实现了,那么我们接下来看一下公平锁是如何实现的呢?

公平锁的实现只需要看FairSync重写的tryAcquire方法,源码如下:

  protected final boolean tryAcquire(int acquires) {
final Thread current = Thread.currentThread();
int c = getState();
//(7)当前AQS状态值为0
if (c == ) {
//(8)公平性策略
if (!hasQueuedPredecessors() &&
compareAndSetState(, acquires)) {
setExclusiveOwnerThread(current);
return true;
}
}
//(9)当前线程是该锁持有者
else if (current == getExclusiveOwnerThread()) {
int nextc = c + acquires;
if (nextc < )
throw new Error("Maximum lock count exceeded"); setState(nextc);
return true;
}//(10)
return false;
}
}

如上代码公平性的tryAcquire策略与非公平锁的类似,不同在于代码(8)处设置CAS前添加了hasQueuedPredecessors 方法,该方法是实现公平性的核心代码,源代码如下:

  public final boolean hasQueuedPredecessors() {

        Node t = tail; // Read fields in reverse initialization order
Node h = head;
Node s;
return h != t &&((s = h.next) == null || s.thread != Thread.currentThread());
}

如上代码所示,如果当前线程节点有前驱节点则返回true,否则如果当前AQS队列为空或者当前线程节点是AQS的第一个节点则返回false。

其中如果h == t 则说明当前队列为空则直接返回false,如果h != t 并且 (s = h.next) ==null 说明有一个元素将要作为AQS的第一个节点入队列,那么返回true, 如果h != t 并且 (s = h.next) !=null 并且 s.thread != Thread.currentThread() 则说明队列里面的第一个元素不是当前线程则返回 true。

  2.void lockInterruptibly() 与 lock() 方法类似,不同在于该方法对中断响应,就是当前线程在调用该方式时候,如果其它线程调用了当前线程线程的 interrupt()方法,当前线程会抛出 InterruptedException 异常然后返回,源代码如下:

public void lockInterruptibly() throws InterruptedException {
sync.acquireInterruptibly();
} public final void acquireInterruptibly(int arg)throws InterruptedException {
//当前线程被中断则直接抛出异常
if (Thread.interrupted())
throw new InterruptedException();
//尝试获取资源
if (!tryAcquire(arg))
//调用AQS可被状态的方法
doAcquireInterruptibly(arg);
}

  3.boolean tryLock() 尝试获取锁,如果当前该锁没有被其它线程持有则当前线程获取该锁并返回 true, 否者返回 false,注意该方法不会引起当前线程阻塞。源码如下所示:

public boolean tryLock() {
return sync.nonfairTryAcquire();
} final boolean nonfairTryAcquire(int acquires) {
final Thread current = Thread.currentThread();
int c = getState();
if (c == ) {
if (compareAndSetState(, acquires)) {
setExclusiveOwnerThread(current);
return true;
}
}
else if (current == getExclusiveOwnerThread()) {
int nextc = c + acquires;
if (nextc < ) // overflow
throw new Error("Maximum lock count exceeded");
setState(nextc);
return true;
}
return false;
}

如上代码与非公平锁的 tryAcquire() 方法类似,所以 tryLock() 使用的是非公平策略。

  4.boolean tryLock(long timeout, TimeUnit unit) 尝试获取锁与 tryLock()不同在于设置了超时时间,如果超时没有获取该锁则返回 false。源代码如下:

 public boolean tryLock(long timeout, TimeUnit unit)throws InterruptedException {
//调用AQS的tryAcquireNanos方法。
return sync.tryAcquireNanos(, unit.toNanos(timeout));
}

接下来我们要看一下,ReentrantLock是如何释放锁的。

  1.void unlock() 尝试释放锁,如果当前线程持有该锁,调用该方法会让该线程对该线程持有的 AQS 状态值减一,如果减去 1 后当前状态值为 0 则当前线程会释放对该锁的持有,否者仅仅减一而已。

如果当前线程没有持有该锁调用了该方法则会抛出 IllegalMonitorStateException 异常 ,源代码如下:

  public void unlock() {
sync.release();
} protected final boolean tryRelease(int releases) {
//(11)如果不是锁持有者调用UNlock则抛出异常。
int c = getState() - releases;
if (Thread.currentThread() != getExclusiveOwnerThread())
throw new IllegalMonitorStateException();
boolean free = false;
//(12)如果当前可重入次数为0,则清空锁持有线程
if (c == ) {
free = true;
setExclusiveOwnerThread(null);
}
//(13)设置可重入次数为原始值-1
setState(c);
return free;
}

如上代码所示(11)如果当前线程不是该锁持有者则直接抛异常,否则,看状态值剩余值是否为0,为0 则说明当前线程要释放对该锁的持有权,则执行代码(12)把当前锁持有者设置为null。

如果剩余值不为0,则仅仅让当前线程对该锁的可重入次数减1。

到目前基本了解了ReentrantLock的原理,那么接下来我们是否可以用ReentrantLock来实现一个简单的线程安全的list呢?

例子如下:

import java.util.ArrayList;
import java.util.concurrent.locks.ReentrantLock; /**
* Created by cong on 2018/6/12.
*/
public class ReentrantLockList { //线程不安全的list
private ArrayList<String> array = new ArrayList<String>();
//独占锁
private volatile ReentrantLock lock = new ReentrantLock(); //添加元素
public void add(String e) { lock.lock();
try {
array.add(e);
} finally {
lock.unlock();
}
}
//删元素
public void remove(String e) { lock.lock();
try {
array.remove(e);
} finally {
lock.unlock(); }
} //获取数据
public String get(int index) { lock.lock();
try {
return array.get(index);
} finally {
lock.unlock(); }
} }

如上代码通过在操作 array 元素前进行加锁保证同时只有一个线程可以对 array 数组进行修改,但是同时也只能有一个线程对 array 元素进行访问。

最后几个图加深前面所学的内容,如下图所示:

如上图,假如线程 Thread1,Thread2,Thread3 同时尝试获取独占锁 ReentrantLock,假设 Thread1 获取到了,则 Thread2 和 Thread3 就会被转换为 Node 节点后放入 ReentrantLock 对应的 AQS 阻塞队列后阻塞挂起。

如上图,假设 Thread1 获取锁后调用了对应的锁创建的条件变量 1,那么 Thread1 就会释放获取到的锁,然后当前线程就会被转换为 Node 节点后插入到条件变量 1 的条件队列,由于 Thread1 释放了锁,

所以阻塞到 AQS 队列里面 Thread2 和 Thread3 就有机会获取到该锁,假如使用的公平策略,那么这时候 Thread2 会获取到该锁,会从 AQS 队列里面移除 Thread2 对应的 Node 节点。

Java并发编程笔记之ReentrantLock源码分析的更多相关文章

  1. Java并发编程笔记之CopyOnWriteArrayList源码分析

    并发包中并发List只有CopyOnWriteArrayList这一个,CopyOnWriteArrayList是一个线程安全的ArrayList,对其进行修改操作和元素迭代操作都是在底层创建一个拷贝 ...

  2. Java并发编程笔记之CyclicBarrier源码分析

    JUC 中 回环屏障 CyclicBarrier 的使用与分析,它也可以实现像 CountDownLatch 一样让一组线程全部到达一个状态后再全部同时执行,但是 CyclicBarrier 可以被复 ...

  3. Java并发编程笔记之PriorityBlockingQueue源码分析

    JDK 中无界优先级队列PriorityBlockingQueue 内部使用堆算法保证每次出队都是优先级最高的元素,元素入队时候是如何建堆的,元素出队后如何调整堆的平衡的? PriorityBlock ...

  4. Java并发编程笔记之ArrayBlockingQueue源码分析

    JDK 中基于数组的阻塞队列 ArrayBlockingQueue 原理剖析,ArrayBlockingQueue 内部如何基于一把独占锁以及对应的两个条件变量实现出入队操作的线程安全? 首先我们先大 ...

  5. Java并发编程笔记之AbstractQueuedSynchronizer源码分析

    为什么要说AbstractQueuedSynchronizer呢? 因为AbstractQueuedSynchronizer是JUC并发包中锁的底层支持,AbstractQueuedSynchroni ...

  6. Java并发编程笔记之ThreadLocalRandom源码分析

    JDK 并发包中 ThreadLocalRandom 类原理剖析,经常使用的随机数生成器 Random 类的原理是什么?及其局限性是什么?ThreadLocalRandom 是如何利用 ThreadL ...

  7. Java并发编程笔记之ThreadLocal源码分析

    多线程的线程安全问题是微妙而且出乎意料的,因为在没有进行适当同步的情况下多线程中各个操作的顺序是不可预期的,多线程访问同一个共享变量特别容易出现并发问题,特别是多个线程需要对一个共享变量进行写入时候, ...

  8. Java并发编程笔记之FutureTask源码分析

    FutureTask可用于异步获取执行结果或取消执行任务的场景.通过传入Runnable或者Callable的任务给FutureTask,直接调用其run方法或者放入线程池执行,之后可以在外部通过Fu ...

  9. Java并发编程笔记之SimpleDateFormat源码分析

    SimpleDateFormat 是 Java 提供的一个格式化和解析日期的工具类,日常开发中应该经常会用到,但是由于它是线程不安全的,多线程公用一个 SimpleDateFormat 实例对日期进行 ...

随机推荐

  1. Aria2+WebUI,迅雷倒下之后的代替品

    Aria2+WebUI,迅雷倒下之后的代替品 (2017-07-24 12:56:28) 转载▼   分类: 软件 最近迅雷越来越作死了,砍第三方远程下载,强推迅雷9喂用户的屎,下载资源能砍就砍,以前 ...

  2. linux命令-crontab

    一.安装 yum install crontabs 二.基本使用 1.crontab -e:创建任务,进入编辑 格式: 基本格式 : ——————————————————— * * * * * com ...

  3. redis在游戏服务器中的使用初探(四) redis应用

    文章系列先介绍环境搭建 介绍redis操作和代码编写运行  这是典型的实战工程过程.那么我们为何要使用redis而不是常规的数据库比如 mysql呢? 因为KV内存数据库最大的优势所有数据全部存储在内 ...

  4. Delphi过程和函数中变量的作用域

    变量的作用域是指变量能被某一子程序识别的范围. 全局变量和局部变量.全局变量是指在程序的type区定义的变量,而局部变量是在过程或函数的定义部分声明的变量.全局变量在整个程序中都有意义,局部变量只在它 ...

  5. 《Miracle-House团队》第三次作业:团队项目的原型设计与开发

    一.实验目的与要求 1.掌握软件原型开发技术 2.学习使用软件原型开发工具 二.实验内容与步骤 1.开发工具: 使用的工具:墨刀(APP端开发原型) 工具简介: 墨刀(MockingBot)是一款简单 ...

  6. To handling editor letter

    一般崔稿信写法: Dear Editor: Sorry for disturbing you. We’re not sure if it is the right time to contact yo ...

  7. ABP框架系列之四十:(Notification-System-通知系统)

    Introduction Notifications are used to inform users on specific events in the system. ASP.NET Boiler ...

  8. vs2015 打开项目自动运行 npm install

    问题:VS2015(visual studio 2015) 打开项目自动运行  npm install 解决办法: 打开工具-选项-项目与解决方案--外部web工具   去掉npm勾选 还有如果文件g ...

  9. HttpClient Fluent API 高并发优化

    apache的httpcomponents-client 4.2之后提供了一套易于使用的facade API称为Fluent API,对于一般使用场景来说,使用起来非常简便,且性能也有一定保证,因为其 ...

  10. Bootstrap框架的基本使用

    Bootstrap是什么 简介 就是已经写好的一个html和css的样式组合 Bootstrap是Twitter开源的基于HTML.CSS.JavaScript的前端框架. 它是为实现快速开发Web应 ...