前面我们分析了AQS的基本原理,然后也试着基于AQS实现了一个可重入的锁了,现在我们再来看看官方的ReentrantLock锁,这个锁是可重入的独占锁,也就是说同时只有一个线程可以获取该锁,而且这个线程还能继续尝试获取锁;

一.简单的使用

  我们先根据ReentrantLock来简单实现一个线程安全的List,然后再分析常用的方法;

package com.example.demo.study;

import java.util.ArrayList;
import java.util.concurrent.locks.ReentrantLock; public class Study0204 {
//线程不安全的List
private ArrayList<String> list = new ArrayList<String>();
//独占锁,默认是非公平锁,传入true可以是公平锁
private volatile ReentrantLock lock = new ReentrantLock(); //往集合中添加元素
public void add(String str) {
lock.lock();
try {
list.add(str);
} finally {
lock.unlock();
}
}
//删除集合中的元素
public void remove(String str) {
lock.lock();
try {
list.remove(str);
} finally {
lock.unlock();
}
}
//根据索引获取集合中某个元素
public String get(int index) {
lock.lock();
try {
return list.get(index);
} finally {
lock.unlock();
}
}
}

  其实就是在每一步都会进行一个加锁的过程,用法和synchronized关键字一样,但是要注意一定要在finally中释放锁,而且不知道大家有没有发现一个问题,在get方法中,需要用锁吗?为什么多线程去读数据也要加锁呢,又没有改变集合中的数据,这也是ReentrantLock这个锁的一个缺陷,使用于写多读少的情况;

  所以后面我们会说一下ReentrantReadWriteLock这个锁,这个锁应用于读多写少的情景,可以把读锁和写锁分开使用,如果是读数据可以多个线程都可以获取读锁,后面会说到的

二.看看ReentrantLock锁结构

  这个锁其实也很简单,下图所示:实现Lock接口,还有一个内部工具类Sync继承自AQS,然后还有两个类NonfairSync和FairSync继承Sync实现自己的方法,看名字就知道这两个类其实就是非公平锁和公平锁的实现策略;

  首先我们看看ReentrantLock的lock方法,发现就是调用sync的lock方法,所以下一步我们看看这个sync对象是怎么构建出来的;

 

  看看构造器,可以知道根据传入的参数是true和false构建sync对象是公平策略还是非公平策略;顺便一说,在这里AQS中的state表示的是锁的可重入次数,默认情况state为0;

  当第一次一个线程CAS成功获取了该锁,那么state就加一,当这个已经获取该锁的线程继续获取锁,state继续加一,释放锁state就减一,直到变为0,那么说明该锁就没有线程占有了,此时AQS中阻塞队列中的某个线程就可以获取锁了;

  我们可以看看这里的Sync实现了一些什么方法,下图所示,只有lock()方法没有实现,留给NonfairSync和FairSync各自根据自己的场景去实现;

  公平锁策略实现如下,实现了lock方法还有tryAcquire()方法,其中这个tryAcquire方法以前说活是AQS中特意留给子类根据实际场景去实现的

  非公平策略实现如下,和公平策略实现的方法一样;

三.非公平策略获取锁

  前面说了ReentrantLock的基本结构,然后我们分析看看是怎么获取锁的,从NonfairSync中的lcok方法开始,我们梳理一下:首先当前线程A使用CAS尝试将AQS中的state从0设置为1,如果成功,那就直接将当前线程设置为锁的占有者;失败的话,那就去获取state的值,如果为0,还是用CAS设置为1,将当前线程设置为锁的占有者,不为0,那就看看占有锁的是不是当前线程;如果是当前线程,就将state加一;不是当前线程,那就是其他线程占用该锁,这才真正说明当前线程获取锁失败,我们才将当前线程封装成一个Node.EXCLUSIVE类型的节点,丢到阻塞队列中去;

  这里我们需要想一下为什么说这里是非公平的?假如在下面nonfairTryAcquire方法中,线程A获取到的c的值为1,占有锁的不是线程A,那么线程就被封装成节点丢到阻塞队列中去了,这个时候线程B来获取c的值,刚好是0(因为这个时候可能那个占有了该锁的线程释放了锁),于是线程B就能成功的将state从0变为1,能获得该锁;

  虽然线程A先尝试获取该锁,线程B后去获取,然而线程B居然可以先成功获取到锁,就好像你排队吃饭,一个后来的人居然先打到饭去吃了,真的是日了狗了!

final void lock() {
//尝试通过CAS将state从0改为1,如果成功那就将当前线程占用该锁
if (compareAndSetState(0, 1))
setExclusiveOwnerThread(Thread.currentThread());
//CAS失败,说明有其他线程已经获取锁了,于是就将当前线程丢到阻塞队列中去,重点看看acquire方法
else
acquire(1);
} //主要看tryAcquire方法,在NonfairSync中实现
//当前方法中,if判断中第二个条件已经看过了,主要是将当前线程封装成一个Node.EXCLUSIVE类型的节点
//然后丢到阻塞队列的最后面去
public final void acquire(int arg) {
if (!tryAcquire(arg) && acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
selfInterrupt();
} protected final boolean tryAcquire(int acquires) {
return nonfairTryAcquire(acquires);
} final boolean nonfairTryAcquire(int acquires) {
final Thread current = Thread.currentThread();
//获取state的值
int c = getState();
//state的值位0的话,那就CAS设置为1,并且设置当前线程占用该锁
if (c == 0) {
if (compareAndSetState(0, acquires)) {
setExclusiveOwnerThread(current);
return true;
}
}
//CAS设置失败,肯定有其他线程占用该锁,由于是可重入锁,所以这里先判断占有该锁的是不是当前线程
else if (current == getExclusiveOwnerThread()) {
//是,那就将state加一
int nextc = c + acquires;
if (nextc < 0) // overflow
throw new Error("Maximum lock count exceeded");
setState(nextc);
return true;
}
//能到这里,说明state的值不是0,占有锁的也不是当前线程,肯定是其他线程,那就返回false
return false;
}

四.公平策略获取锁

  其实公平策略和非公平策略的实现基本一样,我们只看看tryAcquire方法的实现:

protected final boolean tryAcquire(int acquires) {
final Thread current = Thread.currentThread();
int c = getState();
if (c == 0) {
//重点是这里不同,不会直接去通过CAS获取锁,而是调用hasQueuedPredecessors方法查看阻塞队列中有没有前驱节点,
//这个方法是核心,如果阻塞队列中当前线程节点的前面有节点,那么这里不会进去,直接会走下面的else if
//如果前面没有节点,那么说明当前阻塞队列为null或者只有当前线程这一个节点,那就可以获取CAS修改state,获取锁成功
if (!hasQueuedPredecessors() &&
compareAndSetState(0, acquires)) {
setExclusiveOwnerThread(current);
return true;
}
}
else if (current == getExclusiveOwnerThread()) {
int nextc = c + acquires;
if (nextc < 0)
throw new Error("Maximum lock count exceeded");
setState(nextc);
return true;
}
return false;
} //判断阻塞队列中当前线程节点前面有没有节点?
public final boolean hasQueuedPredecessors() { Node t = tail; // Read fields in reverse initialization order
Node h = head;
Node s;
//注意,头节点head指向的是哨兵节点
//如果头节点和尾节点一样即h==t,前面博客画图都说了都指向哨兵节点,此时阻塞队列为空,没有前驱节点,就返回false
//如果h != t同时(s = h.next) == null,说明阻塞队列中哨兵节点后面正在插入一个节点,此时表示有前驱节点,返回true
//如果h != t同时(s = h.next) != null,而且s.thread != Thread.currentThread(),说明哨兵节点后面有一个节点,而且这个节点还不是当前线程节点
//也就是说有前驱节点,返回true
return h != t && ((s = h.next) == null || s.thread != Thread.currentThread());
}

五.其他一些方法

  上面我们看了公平策略和非公平策略的实现方式,其实没什么,比较容易,继续看看一些其他的方法,这些方法是通用的;

  1.lockInterruptibly()方法

//前面说过加了Interruptibly表示如果当前线程在调用该方法时,其他线程调用了当前线程的interrupt()方法的时候,那么
//当前线程就会抛出InterruptedException异常,我们可以看看内在的机制是怎么实现的
public void lockInterruptibly() throws InterruptedException {
sync.acquireInterruptibly(1);
} public final void acquireInterruptibly(int arg) throws InterruptedException {
//如果当前线程被中断,就抛出异常
if (Thread.interrupted())
throw new InterruptedException();
//尝试获取资源,这里分为公平策略和非公平策略,前面已经说过了;
//获取资源失败的话,就调用AQS可被中断的方法
if (!tryAcquire(arg))
doAcquireInterruptibly(arg);
}

  2.tyLock()方法

//很明显这个tryLock方法是非公平策略
public boolean tryLock() {
return sync.nonfairTryAcquire(1);
}
//这个方法在前面说非公平策略的时候tryAcquire方法调用的也是这个方法
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;
}

  

  3.tryLock(long timeout, TimeUnit unit)方法

  这个方法和上面的方法不一样的就是可以设置超时时间,其实和前面说的也差不多,就是多了一个时间的判断,这个也是会对线程中断有响应的;

public boolean tryLock(long timeout, TimeUnit unit)
throws InterruptedException {
return sync.tryAcquireNanos(1, unit.toNanos(timeout));
} public final boolean tryAcquireNanos(int arg, long nanosTimeout) throws InterruptedException {
if (Thread.interrupted())
throw new InterruptedException();
return tryAcquire(arg) ||
doAcquireNanos(arg, nanosTimeout);
}

  4.unlock()释放锁

public void unlock() {
sync.release(1);
} public final boolean release(int arg) {
//从下面的方法我们可以知道tryRelease只有当state为0的时候才会返回true,也就是当前锁没有被线程持有
//此时,如果头节点不为null,而且头节点的waitStatus不为初始状态,就唤醒头节点
if (tryRelease(arg)) {
Node h = head;
if (h != null && h.waitStatus != 0)
unparkSuccessor(h);
return true;
}
return false;
} protected final boolean tryRelease(int releases) {
//将AQS中的state减一
int c = getState() - releases;
//当前线程如果不是锁的拥有者,却调用了unlock方法,那么就会抛出IllegalMonitorStateException异常
//在这里就能判断了如果原先的state为0,那么上面的c应该就是负一,就会走到这里来抛错,如果state为1,就会到下面的if(c==0)里面
//如果state大于1,那么只会到最下面的setState(c)将减一之后的state更新到AQS中
if (Thread.currentThread() != getExclusiveOwnerThread())
throw new IllegalMonitorStateException();
boolean free = false;
//可重入数字为0,就将当前持有锁的线程设置为null
if (c == 0) {
free = true;
setExclusiveOwnerThread(null);
}
//这里不管重入数字是不是0,我们只会将最新的state更新获取
setState(c);
return free;
}

六.总结

  我们已经看到了ReentrantLock这个锁的基本功能,其实就是一个独占锁,而且是可重入的,这里的重入指的是一个已经占有了该锁的线程,还可以继续获取该锁!

  下图所示,同时有三个线程去争夺ReentrantLock锁,此时,只有Thread1成功占有锁了,那么其他的两个线程就被丢到AQS阻塞队列中去了(这里是有顺序的,先是Thread2,然后是Thread3);

  如果这个时候Thread1调用了条件变量1的await方法,那么Thread1就被丢到条件队列1中,并且释放锁,这个时候阻塞队列中就会有一个线程可以获取锁,如果是公平锁,那么就是阻塞队列中最前面的那一个获取锁,此时阻塞队列中只有Thread3了;(如果是非公平锁,那么就是看运气,Thread2和Thread那个先去尝试获取锁那么就获得锁)

简单看看ReentrantLock的更多相关文章

  1. Lock的实现之ReentrantLock详解

    摘要 Lock在硬件层面依赖CPU指令,完全由Java代码完成,底层利用LockSupport类和Unsafe类进行操作: 虽然锁有很多实现,但是都依赖AbstractQueuedSynchroniz ...

  2. java ReentrantLock结合条件队列 实现生产者-消费者模式 以及ReentratLock和Synchronized对比

    package reentrantlock; import java.util.ArrayList; public class ProviderAndConsumerTest { static Pro ...

  3. AbstractQueuedSynchronizer与ReentrantLock

    介绍 j.u.c包中的Lock定义了锁的行为. 而ReentrantLock是并发包下提供的一个锁的实现,它是一个可重入的.排他的锁. ReentrantLock有的属性也很简单,除了一个serial ...

  4. 没听说过这些,就不要说你懂并发了,three。

    引言 很久没有跟大家再聊聊并发了,今天LZ闲来无事,跟大家再聊聊并发.由于时间过去的有点久,因此LZ就不按照常理出牌了,只是把自己的理解记录在此,如果各位猿友觉得有所收获,就点个推荐或者留言激励下LZ ...

  5. 并发编程(十):AQS

    AQS全称为AbstractQueuedSynchronizer,是并发容器中的同步器,AQS是J.U.C的核心,它是抽象的队列式的同步器,AQS定义了一套多线程访问共享资源的同步器框架,许多同步类都 ...

  6. 并发,three

    引言 很久没有跟大家再聊聊并发了,今天LZ闲来无事,跟大家再聊聊并发.由于时间过去的有点久,因此LZ就不按照常理出牌了,只是把自己的理解记录在此,如果各位猿友觉得有所收获,就点个推荐或者留言激励下LZ ...

  7. J.U.C剖析与解读1(Lock的实现)

    J.U.C剖析与解读1(Lock的实现) 前言 为了节省各位的时间,我简单介绍一下这篇文章.这篇文章主要分为三块:Lock的实现,AQS的由来(通过演变的方式),JUC三大工具类的使用与原理剖析. L ...

  8. 基于AQS自己实现一个同步器

    前面说了这个多,我们可以自己尝试实现一个同步器,我们可以简单的参考一下ReentrantLock这个类的实现方式,我们就简单的实现一个不可重入的独占锁吧! 一.简单分析ReentrantLock的结构 ...

  9. Java_面试札记

    Java_面试札记  为了不死,我愿献出生命 背景:记录下寄几和friend在2020年Java面试中遇到的problem. 1.MySQL索引结构? 基本上所有的索引都是B-Tree结构,还有一部分 ...

随机推荐

  1. 【代码审计】MenInfo文件包含漏洞

    代码审计是我之前一直不敢涉及的领域,它包含的知识面太广.最近才开始学习,前段时间写的第一篇代码审计的文章 更多的感觉像是一个黑盒测试.我也会尽量把文章写的通俗易懂.代码审计 0基础从大牛的蜕变 从这里 ...

  2. Vue 项目 在局域网内访问

    之前在百度上看到过很多中  关于局域网访问项目的方法, 在这里,个人推荐两种比较喜欢的方法 一.直接在项目的package.json文件中进行配置 ,代码如下 "scripts": ...

  3. 【PAT甲级】1040 Longest Symmetric String (25 分)(cin.getline(s,1007))

    题意: 输入一个包含空格的字符串,输出它的最长回文子串的长度. AAAAAccepted code: #define HAVE_STRUCT_TIMESPEC #include<bits/std ...

  4. HttpClient和HtmlUnit的比较总结以及使用技巧

    本文转自: https://blog.csdn.net/zstu_cc/article/details/39250903 https://blog.csdn.net/zstu_cc/article/d ...

  5. css简单整理

    style1.css /*统一设置h1.h2.a标签的样式*/ h1,h2,a{ color: aqua; font-size: 50px; } /*如果h1.h2没有上面的单独设置样式那么就会继承b ...

  6. 使用Servlet处理AJAX请求

    AJAX用于异步更新页面的局部内容. ajax常用的请求数据类型 text    纯文本字符串 json    json数据 使用ajax获取text示例 此种方式常用于前端向后台查询实体的一个属性( ...

  7. 关于window.location.href页面跳转的坑

    "window.location.href"."location.href"是本页面跳转 "parent.location.href"是上一 ...

  8. unittest中的parameterized参数化

    一.安装插件 pip install parameterized 二.有默认参数情况与没有默认参数情况---1 注意:这种写法,只能给单个用例进行参数化,不能给多个用例使用,要每个用例都进行参数化. ...

  9. 【代码总结】PHP文件的上传和下载

    ===================== 文件上传和下载 ===================== 一.php.ini的配置信息 file_uploads = On /Off    是否允许文件上 ...

  10. PAT T1022 Werewolf

    暴力搜索加剪枝~ #include<bits/stdc++.h> using namespace std; ; int a[maxn]; bool visit[maxn]; vector& ...