java架构之路(多线程)AQS之ReetrantLock显示锁的使用和底层源码解读

时间:2021-10-01 12:02:58

  说完了我们的synchronized,这次我们来说说我们的显示锁ReetrantLock。

上期回顾:

  上次博客我们主要说了锁的分类,synchronized的使用,和synchronized隐式锁的膨胀升级过程,从无锁是如何一步步升级到我们的重量级锁的,还有我们的逃逸分析。

锁的粗化和锁的消除

  这个本来应该是在synchronized里面去说的,忘记了,不是很重要,但是需要知道有这么一个东西啦。

  我们先来演示一下锁的粗化:

StringBuffer sb = new StringBuffer();

public void lockCoarseningMethod(){
//jvm的优化,锁的粗化
sb.append("1"); sb.append("2"); sb.append("3"); sb.append("4");
}

  我们都知道我们的StringBuffer是线程安全的,也就是说我们的StringBuffer是用synchronized修饰过的。那么我们可以得出我们的4次append都应该是套在一个synchronized里面的。

StringBuffer sb = new StringBuffer();

public void lockCoarseningMethod() {
synchronized (Test.class) {
sb.append("1");
} synchronized (Test.class) {
sb.append("2");
}
synchronized (Test.class) {
sb.append("3");
}
synchronized (Test.class) {
sb.append("4");
}
}

  按照理论来说应该是这样的,其实JVM对synchronized做了优化处理,底层会优化成一次的synchronized修饰,感兴趣的可以用javap -c 自己看一下,这里就不带大家去看了,我以前的博客有javap看汇编指令码的过程。

StringBuffer sb = new StringBuffer();

public void lockCoarseningMethod() {
synchronized (Test.class) {
sb.append("1");
sb.append("2");
sb.append("3");
sb.append("4");
}
}

  再来看一下锁的消除,其实这个锁的消除,真的对于synchronized理解了,锁的消除一眼就知道是什么了。

public static void main(String[] args) {
synchronized (new Object()){
System.out.println("开始处理逻辑");
}
}

  对于synchronized而言,我们每次去锁的都是对象,而你每次都创建的一个新对象,那还锁毛线了,每个线程都可以拿到对象,都可以拿到对象锁啊,所以没不会产生锁的效果了。

概述AQS:

  AQS是AbstractQueuedSynchronizer的简称,字面意思,抽象队列同步器。Java并发编程核心在于java.concurrent.util包而juc当中的大多数同步器 实现都是围绕着共同的基础行为,比如等待队列、条件队列、独占获取、共享获 取等,而这个行为的抽象就是基于AbstractQueuedSynchronizer简称AQS,AQS定 义了一套多线程访问共享资源的同步器框架,是一个依赖状态(state)的同步器。就是我们上次博客说的什么公平锁,独占锁等等。

AQS具备特性
  • 阻塞等待队列
  • 共享/独占
  • 公平/非公平
  • 可重入
  • 允许中断

AQS的简单原理解读:

  ReetrantLock的内部功能还是很强大的,有很多的功能,我们来一点点缕缕。如Lock,Latch,Barrier等,都是基于AQS框架实现,一般通过定义内部类Sync继承AQS将同步器所有调用都映射到Sync对应的方法AQS内部维护属性volatile int state (32位),state表示资源的可用状态

  • State三种访问方式
  1. getState()
  2. setState()
  3. compareAndSetState()
  • AQS定义两种资源共享方式
  1. Exclusive-独占,只有一个线程能执行,如ReentrantLock
  2. Share-共享,多个线程可以同时执行,如Semaphore/CountDownLatch
  • AQS定义两种队列
  1. 同步等待队列
  2. 条件等待队列
  • AQS已经在顶层实现好了。自定义同步器实现时主要实现以下几种方法:
  1. isHeldExclusively():该线程是否正在独占资源。只有用到condition才需要去实现它。
  2. tryAcquire(int):独占方式。尝试获取资源,成功则返回true,失败则返回false。
  3. tryRelease(int):独占方式。尝试释放资源,成功则返回true,失败则返回false。
  4. tryAcquireShared(int):共享方式。尝试获取资源。负数表示失败;0表示成功,但没有剩余可用资源;正数表示成功,且有剩余资源。
  5. tryReleaseShared(int):共享方式。尝试释放资源,如果释放后允许唤醒后续等待结点返回true,否则返回false。

  刚才提到那么多属性,可能会有一些懵,我们来看一下ReentrantLock内部是怎么来实现哪些锁的吧。

  打开我们的ReetrantLock源代码可以看到一个关键的属性

private final Sync sync;

  后面有一个抽象方法并且继承了AbstractQueuedSynchronizer类,内部有一个用volatile修饰过的整型变量state,他就是用来记录上锁次数的,这样就实现了我们刚才的说的重入锁和非可重入锁。我们来画一个图。

java架构之路(多线程)AQS之ReetrantLock显示锁的使用和底层源码解读

  AbstractQueuedSynchronizer这个类里面定义了详细的ReetrantLock的属性,后面我会一点点去说,带着解读一下源码(上面都是摘自源码的)。state和线程exclusiveOwnerThread比较好理解,最后那个队列可能不太好弄,我这里写的也是比较泛化的,后面我会弄一个专题一个个去说。 相面说的CLH队列其实不是很准确,我们可以理解为就是一个泛型为Node的双向链表结构就可以了。

  等待队列中Node节点内还有三个很重要的属性就是prev前驱指针指向我们的前一个Node节点,和一个next后继指针来指向我们的下一个Node节点,这样就形成了一个双向链表的结构,于此同时还有一个Thread来记录我们的当前线程。

java架构之路(多线程)AQS之ReetrantLock显示锁的使用和底层源码解读

  在条件队列中,prev和next指针都是null的,不管是什么队列,他都有一个waitStatus的属性来记录我们的节点状态的,就是我们刚才说的CANCELLED结束、SIGNAL可唤醒那四个常量值。

AQS中ReetrantLock的使用:

  公平锁和非公平锁:这个还是比较好记忆的,举一个栗子,我们去车站排队上车,总有**插队,用蛇形走位可以上车的是吧,这就是一个非公平的锁,如果说,我们在排队的时候加上护栏,每次只能排一个人,他人无法插队的,这时就是一个公平锁。总之就是不加塞的就是公平的,我们都讨厌不公平。

java架构之路(多线程)AQS之ReetrantLock显示锁的使用和底层源码解读

  重入锁与非可重入锁:这个也很好理解,重入锁就是当我们的线程A拿到锁以后,可以继续去拿多把锁,然后再陆陆续续的做完任务再去解锁,非可重入呢,就是只能获得一把锁,如果想获取多把锁,不好意思,去后面排下队伍。下面我化了一个重入锁的栗子,快过年了,大家提着行李回老家,我们进去了会一并带着行李进去(不带行李的基本是行李丢了),这就是一个重入锁的栗子,我们人进去了获得通道通过(锁),然后我们也拖着行李获得了通道通过(锁),然后我们才空出通道供后面的人使用。如果是非可重入锁就是人进去就进去吧,行李再次排队,说不准什么时候能进来。

java架构之路(多线程)AQS之ReetrantLock显示锁的使用和底层源码解读

  上一段代码来验证一下我们上面说的那些知识点。

import java.util.concurrent.locks.ReentrantLock;

public class Test {

    private ReentrantLock lock = new ReentrantLock(true);//true公平锁,false非公平锁

    public void lockMethod(String threadName) {
lock.lock();
System.out.println(threadName + "得到了一把锁1"); lock.lock();
System.out.println(threadName + "得到了一把锁2"); lock.lock();
System.out.println(threadName + "得到了一把锁3"); lock.unlock();
System.out.println(threadName + "释放了一把锁1"); lock.unlock();
System.out.println(threadName + "释放了一把锁2"); lock.unlock();
System.out.println(threadName + "释放了一把锁3");
} public static void main(String[] args) {
Test test = new Test(); new Thread(() -> {
String threadName = Thread.currentThread().getName();
test.lockMethod(threadName); }, "线程A").start();
} }

   通过代码阅读我们知道我们弄一个重入锁,加三次锁,解三次锁,我们来看一下内部sync的变化,调试一下。

java架构之路(多线程)AQS之ReetrantLock显示锁的使用和底层源码解读

  我们看到了我们的state变量是用来存储我们的入锁次数的。刚才去看过源码的小伙伴知道了我们的state是通过volatile修饰过的,虽然可以保证我们的有序性和可见性,但是一个int++的操作,他是无法保证原子性的,我们继续来深挖一下代码看看内部是怎么实现高并发场景下保证数据准确的。点击lock方法进去,我们看到lock方法是基于sync来操作的,就是我们上面的画的那个ReetrantLock的图。

/**
* Sync object for fair locks
*/
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) {//判断是否上过锁
if (!hasQueuedPredecessors() &&//hasQueuedPredecessors判断是否有正在等待的节点,
compareAndSetState(0, acquires)) {//通过unsafe去更新上锁次数
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;
}
}

  这次我们开启多个线程来同时访问来看一下我们的Node的变化。同时开启ABCD四个线程来执行这个

java架构之路(多线程)AQS之ReetrantLock显示锁的使用和底层源码解读

  这次我们看到了head属性和tail属性不再是空的。head是也是一个node节点,前驱指针是空的,后驱指针指向后继节点,Thread为空,tail的node节点正好是和head相对应的节点。这样的设计就是为了更好的去验证队列中还是否存在剩余的线程节点需要处理。然后该线程运行结束以后会唤醒在队列中的节点,然其它线程继续运行。

  我们知道我们创建的公平锁,如果说BCD好好的在排队,E线程来了,只能好好的去排队,因为公平,所以排队,如果我们创建的是非公平锁,E线程就有机会拿到锁,拿到就运行,拿不到就去排队。

java架构之路(多线程)AQS之ReetrantLock显示锁的使用和底层源码解读的更多相关文章

  1. &lbrack;转帖&rsqb;java架构之路-(面试篇)JVM虚拟机面试大全

    java架构之路-(面试篇)JVM虚拟机面试大全 https://www.cnblogs.com/cxiaocai/p/11634918.html   下文连接比较多啊,都是我过整理的博客,很多答案都 ...

  2. java架构之路-(spring源码篇)由浅入深-spring实战详细使用

    今天我更新了一篇jvm垃圾回收的算法和垃圾回收器的内部逻辑,但是看的人不多啊......貌似大家还是比较喜欢看源码吧,毕竟实战要比理论用的多. 这篇文章不会详细的深入底层源码,只是基于注解和配置来说说 ...

  3. java架构之路-(netty专题)netty的基本使用和netty聊天室

    上次回顾: 上次博客,我们主要说了我们的IO模型,BIO同步阻塞,NIO同步非阻塞,AIO基于NIO二次封装的异步非阻塞,最重要的就是我们的NIO,脑海中应该有NIO的模型图. Netty概念: Ne ...

  4. Java泛型底层源码解析-ArrayList&comma;LinkedList&comma;HashSet和HashMap

    声明:以下源代码使用的都是基于JDK1.8_112版本 1. ArrayList源码解析 <1. 集合中存放的依然是对象的引用而不是对象本身,且无法放置原生数据类型,我们需要使用原生数据类型的包 ...

  5. jvm源码解读--17 Java的wait&lpar;&rpar;、notify&lpar;&rpar;学习

    write and debug by 张艳涛 wait()和notify()的通常用法 A线程取得锁,执行wait(),释放锁; B线程取得锁,完成业务后执行notify(),再释放锁; B线程释放锁 ...

  6. java&period;lang&period;system 类源码解读

    通过每块代码进行源码解读,并发现源码使用的技术栈,扩展视野. registerNatives 方法解读 /* register the natives via the static initializ ...

  7. Spark学习之路 (十六)SparkCore的源码解读(二)spark-submit提交脚本

    一.概述 上一篇主要是介绍了spark启动的一些脚本,这篇主要分析一下Spark源码中提交任务脚本的处理逻辑,从spark-submit一步步深入进去看看任务提交的整体流程,首先看一下整体的流程概要图 ...

  8. java架构之路(多线程)JUC并发编程之Semaphore信号量、CountDownLatch、CyclicBarrier栅栏、Executors线程池

    上期回顾: 上次博客我们主要说了我们juc并发包下面的ReetrantLock的一些简单使用和底层的原理,是如何实现公平锁.非公平锁的.内部的双向链表到底是什么意思,prev和next到底是什么,为什 ...

  9. java架构之路(多线程)synchronized详解以及锁的膨胀升级过程

    上几次博客,我们把volatile基本都说完了,剩下的还有我们的synchronized,还有我们的AQS,这次博客我来说一下synchronized的使用和原理. synchronized是jvm内 ...

随机推荐

  1. 【12-26】go&period;js

    var $ = go.GraphObject.make; // for conciseness in defining templates function buildAlarm(row,column ...

  2. MicroERP开发技术分享:vsFlexGrid、scriptControl实现工资表自定义列与表间关系计算

    开发大型的MIS系统,肯定是离不开第三方控件的,同时也要根据项目需要自己写几个. MicroERP共用了以下几个控件: 第三方商业控件: vsFlexGrid:大名鼎鼎的表格控件,不用多说,配合vsP ...

  3. git 学习使用总结二&lpar;远程仓库操作&rpar;

    这篇文章仅供自己以后翻阅加深记忆,要系统的学习 git 教程(中文版),请移步到 liaoxuefeng.com 学习 git 教程部分. 我使用的是 windows 系统,所以使用 Git Bash ...

  4. shell截取字符串

    image_tag="pangu-20151021102145\"" 1.用#号截取,符号-右面所有字符串 TMP=${image_tag#*-} echo $TMP 得 ...

  5. 重学STM32---(三) 中断分组和优先级

    看了大半天,终于把原子哥的例程中的中断分组和优先级看懂了,勉勉强强知道了怎么设置中断分组和优先级,,不容易啊.下面就是我收集的资料及我的理解 分组不是很难,就是有一点知道就就全部明白了: // 设置N ...

  6. DB2 的create or update方法

    通过merge方法实现的: MERGE INTO IFEBASE.STYLE AS MT USING (SELECT :scenario AS SCENARIO_ID, :style AS SHAPE ...

  7. codevs1051单词接龙(栈)

    /* 看到n的范围就觉得这个不可能是DP啥的 因为这个接龙的规则十分的简单 只要前缀相同即可 所以先按字典序排一遍 这样保证符合规则的一定挨着 然后弄一个stack 每次拿栈顶元素看看待入栈的元素是否 ...

  8. DOS批处理命令判断操作系统版本、执行各版本对应语句

    DOS批处理命令判断操作系统版本.执行各版本对应语句   昨天在家里试用  netsh interface ip set address 这些命令更改上网IP.DNS.网关等,今天将那些代码拿来办公室 ...

  9. Solr6&period;5&period;0配置中文分词器配置

    准备工作: solr6.5.0安装成功 1.去官网https://github.com/wks/ik-analyzer下载IK分词器 2.Solr集成IK a)将ik-analyzer-solr6.x ...

  10. 【特性】select语句中使用字符串链接获取字段值失败

    坑1 在一个多行的表中,想把其中的一个字段值拿出来,组成一个字符串供后面使用. 按照以往,自己就如以下这么写了: declare @sql varchar(8000) set @sql='insert ...