下图是Lock接口清单,定义了一些抽象的锁操作。Java本身提供了内部锁机制,那么还需要显示Lock,何用?与内部加锁机制不同,Lock提供了无条件、可轮询、定时、可中断的锁获取操作;所有加锁和解锁的操作都是显式的。既然是锁,那么Lock的实现必须提供具有与内部加锁相同内存可见性语义,同时语义、调度算法、顺序保证、性能特性这些可以不同——即功能可以更强大,但是基本核心基本功能不能丢失。

    

  ReentrantLock实现了Lock接口,那么看看这个接口实现是如何在保证内部锁功能同时显式调用的。通常调用范式如下:

    

  譬如CyclicBarrier中的实现:

    

  阅读ReentrantLock源码可知其实现基于AQS(参考Java并发基础之AbstractQueuedSynchronizer(AQS) - 池塘里洗澡的鸭子 - 博客园 (cnblogs.com)):

    

  ReentrantLock只支持独占的获取操作,因此它实现了AQS中的tryAcquire/tryRelease和isHeldExclusively:

    

  下面通过具体源码分析ReentrantLock实现:

  1、公平与非公平lock()的差异:  

    ReentrantLock使用同步状态持有锁获取操作的计数,同时还维护了一个owner变量来持有当前拥有的线程标识符。只有在当期线程刚刚获取到锁,或者刚刚释放了锁的时候,才会修改owner。在tryRelease中,检查owner域以确保当期线程在执行一个unlock操作之前已经拥有了锁;在tryAcquire中,使用这个域来区分重进入的获取操作尝试与竞争的获取操作尝试:

    

  通过源码比较可知,公平锁增加了判断是否有等待线程排队这一条件:

    当一个线程A尝试去获取锁时tryAcquire会首先请求锁的状态,state 初始化为 0,表示未锁定状态;A 线程lock()时会调用 tryAcquire()独占该锁并将 state+1,如果锁未被占有尝试更新锁的状态,表明锁已被占有。因为状态可能在被观察后的几条指令中被修改了,所以tryAcquire使用compareAndSetState来尝试原子地更新状态表明这个锁现在已经被占有,并确保状态自最后一次观察后没有被修改过。此后,其他线程再 tryAcquire()时就会失败,直到 A 线程 unlock(到 state=0(即释放锁)为止,其它线程才有机会获取该锁。当然,释放锁之前, A 线程自己是可以重复获取此锁的(state 会累加),这就是可重入的概念。但要注意,获取多少次就要释放多么次,这样才能保证 state 是能回到零态的。

    假设锁状态表明它已经被占有,如果当前线程是锁的拥有者,那么获取计数会递减;如果当前线程不是锁的拥有者,那么获取操作的尝试会失败。

  2、阻塞队列与唤醒机制:

    此处为锁的最为关键的部分,即acquireQueued(...)方法内部如何实现。这部分内容AQ已经实现,如下:

            

    addWaiter(...)方法创建节点,尝试将节点追加到队列尾部。获取tail节点,将tail节点的next设置为当前节点。如果tail不存在,就初始化队列。

    在addWaiter(...)方法把Thread对象加入阻塞队列之后的工作就要靠acquireQueued(...)方法完成。线程一旦进入acquireQueued(...)就会被无限期阻塞,即使有其他线程调用interrupt()方法也不能将其唤醒,除非有其他线程释放了锁,并且该线程拿到了锁,才会从accquireQueued(...)返回。    

    那么阻塞方法在哪里呢?

      

    线程调用park()方法,自己把自己阻塞起来直到被其他线程唤醒,该方法返回。park()方法返回有两种情况:

      1) 其他线程调用了unpark(Thread t)。

      2)其他线程调用了t.interrupt()。这里要注意的是,lock()不能响应中断,但LockSupport.park()会响应中断。

    也正因为LockSupport.park()可能被中断唤醒,acquireQueued(...)方法才写了一个for死循环。唤醒之后,如果发现自己排在队列头部,就去拿锁;如果拿不到锁,则再次自己阻塞自己。不断重复此过程,直到拿到锁。

    被唤醒之后,通过Thread.interrupted()来判断是否被中断唤醒。如果是情况1,会返回false;如果是情况2,则返回true。

    需要注意的是acquireQueued方法有个返回值,这个值啥意思?虽然该方法不会中断响应,但它会记录被阻塞期间有没有其他线程向它发送过中断信号。如果有,则该方法会返回true;否则,返回false。

      

    当 acquireQueued(...)返回 true 时,会调用 selfInterrupt(),自己给自己发送中断信号,也就是自己把自己的中断标志位设为true。之所以要这么做,是因为自己在阻塞期间,收到其他线程中断信号没有及时响应,现在要进行补偿。这样一来,如果该线程在lock代码块内部有调用sleep()之类的阻塞方法,就可以抛出异常,响应该中断信号。
  3、unlock()实现分析
    上面两个点阐述了lock的实现,那么unlock是如何实现的呢?与lock不同释放锁无是否公平区别:

      

    实际上,release()里面就干了两件实事:tryRelease(...)方法释放锁;unparkSuccessor(...)方法唤醒队列中的后继者。

    



    

 

未完,待续……

    

  

显式锁之ReentrantLock实现的更多相关文章

  1. 4.显式锁 Lock

    4.1 概念 内置锁 vs 显示锁 synchronize是java语言层面实现的锁,称为内置锁.使用方便代码简洁,而且在jdk新版本优化后,性能也得到了很大的提高.synchronize是一个可重入 ...

  2. 深入理解java内置锁(synchronized)和显式锁(ReentrantLock)

    多线程编程中,当代码需要同步时我们会用到锁.Java为我们提供了内置锁(synchronized)和显式锁(ReentrantLock)两种同步方式.显式锁是JDK1.5引入的,这两种锁有什么异同呢? ...

  3. 深刨显式锁ReentrantLock原理及其与内置锁的区别,以及读写锁ReentrantReadWriteLock使用场景

    13.显示锁 在Java5.0之前,在协调对共享对象的访问时可以使用的机制只有synchronized和volatile.Java5.0增加了一种新的机制:ReentrantLock.与之前提到过的机 ...

  4. Java并发编程之显式锁机制

    我们之前介绍过synchronized关键字实现程序的原子性操作,它的内部也是一种加锁和解锁机制,是一种声明式的编程方式,我们只需要对方法或者代码块进行声明,Java内部帮我们在调用方法之前和结束时加 ...

  5. 深入理解Java内置锁和显式锁

    synchronized and Reentrantlock 多线程编程中,当代码需要同步时我们会用到锁.Java为我们提供了内置锁(synchronized)和显式锁(ReentrantLock)两 ...

  6. java 并发多线程显式锁概念简介 什么是显式锁 多线程下篇(一)

    目前对于同步,仅仅介绍了一个关键字synchronized,可以用于保证线程同步的原子性.可见性.有序性 对于synchronized关键字,对于静态方法默认是以该类的class对象作为锁,对于实例方 ...

  7. Java编程的逻辑 (71) - 显式锁

    ​本系列文章经补充和完善,已修订整理成书<Java编程的逻辑>,由机械工业出版社华章分社出版,于2018年1月上市热销,读者好评如潮!各大网店和书店有售,欢迎购买,京东自营链接:http: ...

  8. 显式锁(二)Lock接口与显示锁介绍

    一.显式锁简介    显式锁,这个叫法是相对于隐式锁synchronized而言的,加锁和解锁都要用户显式地控制.显示锁Lock是在Java5中添加到jdk的,同synchronized一样,这也是一 ...

  9. 显式锁(三)读写锁ReadWriteLock

    前言:   上一篇文章,已经很详细地介绍了 显式锁Lock 以及 其常用的实现方式- - ReetrantLock(重入锁),本文将介绍另一种显式锁 - - 读写锁ReadWriteLock.    ...

随机推荐

  1. CTF中的变量覆盖问题

    0x00 前言 最近在干代码审计,于是就把之前学习的CTF题目中有关变量覆盖的题目结合下进一步研究. 通常将可以用自定义的参数值替换原有变量值的情况称为变量覆盖漏洞.经常导致变量覆盖漏洞场景有:$$使 ...

  2. 《剑指offer》面试题63. 股票的最大利润

    问题描述 假设把某股票的价格按照时间先后顺序存储在数组中,请问买卖该股票一次可能获得的最大利润是多少? 示例 1: 输入: [7,1,5,3,6,4] 输出: 5 解释: 在第 2 天(股票价格 = ...

  3. Rust 使用 dotenv 来设置环境变量

    在项目中,我们通常需要设置一些环境变量,用来保存一些凭证或其它数据,这时我们可以使用 dotenv 这个 crate. 首先在项目中添加 dotenv 这个依赖: 例如在下面这个项目中,需要设置数据库 ...

  4. k8s 基于RBAC的认证、授权介绍和实践

    在K8S中,当我们试图通过API与集群资源交互时,必定经过集群资源管理对象入口kube-apiserver.显然不是随随便便来一个请求它都欢迎的,每个请求都需要经过合规检查,包括Authenticat ...

  5. 集合框架-Map集合特点及常用方法

    1 package cn.itcast.p6.map.demo; 2 3 import java.util.HashMap; 4 import java.util.Iterator; 5 import ...

  6. a 标签一些特殊用法

    发邮件 <a href="mailto:youemail@mail.com?subject=邮件标题&body=邮件内容">告诉我们</a> 打电话 ...

  7. vue 快速入门 系列 —— 侦测数据的变化 - [vue api 原理]

    其他章节请看: vue 快速入门 系列 侦测数据的变化 - [vue api 原理] 前面(侦测数据的变化 - [基本实现])我们已经介绍了新增属性无法被侦测到,以及通过 delete 删除数据也不会 ...

  8. zabbix安装 报错 socket '/var/lib/mysql/mysql.sock' (13)]

    安装界面提示: Error connecting to database: Can't connect to local MySQL server through socket '/var/lib/m ...

  9. 不难懂------适配移动端flexible

    基于 vue-cli 配置手淘的 lib-flexible + rem,实现移动端自适应 安装 flexible npm install lib-flexible --save 引入 flexible ...

  10. JDBC固定步骤-java连接MySQL

    static final String JDBC_DRIVER = "com.mysql.cj.jdbc.Driver"; static final String DB_URL = ...