ava并发编程解析 | 基于JDK源码解析Java领域中并发锁之StampedLock锁的设计思想与实现原理 (三)
苍穹之边,浩瀚之挚,眰恦之美; 悟心悟性,善始善终,惟善惟道! —— 朝槿《朝槿兮年说》
1|0写在开头
在并发编程领域,有两大核心问题:一个是互斥,即同一时刻只允许一个线程访问共享资源;另一个是同步,即线程之间如何通信、协作。
主要原因是,对于多线程实现实现并发,一直以来,多线程都存在2个问题:
- 线程之间内存共享,需要通过加锁进行控制,但是加锁会导致性能下降,同时复杂的加锁机制也会增加编程编码难度
- 过多线程造成线程之间的上下文切换,导致效率低下
因此,在并发编程领域中,一直有一个很重要的设计原则: “ 不要通过内存共享来实现通信,而应该通过通信来实现内存共享。”
简单来说,就是尽可能通过消息通信,而不是内存共享来实现进程或者线程之间的同步。
2|0关健术语
- 并发(Concurrent): 在操作系统中,是指一个时间段中有几个程序都处于已启动运行到运行完毕之间,且这几个程序都是在同一个处理机上运行。
- 并行(Parallel): 当系统有一个以上CPU时,当一个CPU执行一个进程时,另一个CPU可以执行另一个进程,两个进程互不抢占CPU资源,可以同时进行。
- 信号量(Semaphore): 是在多线程环境下使用的一种设施,是可以用来保证两个或多个关键代码段不被并发调用,也是作系统用来解决并发中的互斥和同步问题的一种方法。
- 信号量机制(Semaphores): 用来解决同步/互斥的问题的,它是1965年,荷兰学者 Dijkstra提出了一种卓有成效的实现进程互斥与同步的方法。
- 管程(Monitor) : 一般是指管理共享变量以及对共享变量的操作过程,让它们支持并发的一种机制。
- 互斥(Mutual Exclusion):一个公共资源同一时刻只能被一个进程或线程使用,多个进程或线程不能同时使用公共资源。即就是同一时刻只允许一个线程访问共享资源的问题。
- 同步(Synchronization):两个或两个以上的进程或线程在运行过程中协同步调,按预定的先后次序运行。即就是线程之间如何通信、协作的问题。
- 对象池(Object Pool): 指的是一次性创建出 N 个对象,之后所有的线程重复利用这 N 个对象,当然对象在被释放前,也是不允许其他线程使用的, 一般指保存实例对象的容器。
3|0基本概述
在Java领域中,我们可以将锁大致分为基于Java语法层面(关键词)实现的锁和基于JDK层面实现的锁。
在Java领域中, 尤其是在并发编程领域,对于多线程并发执行一直有两大核心问题:同步和互斥。其中:
- 互斥(Mutual Exclusion):一个公共资源同一时刻只能被一个进程或线程使用,多个进程或线程不能同时使用公共资源。即就是同一时刻只允许一个线程访问共享资源的问题。
- 同步(Synchronization):两个或两个以上的进程或线程在运行过程中协同步调,按预定的先后次序运行。即就是线程之间如何通信、协作的问题。
针对对于这两大核心问题,利用管程是能够解决和实现的,因此可以说,管程是并发编程的万能钥匙。
虽然,Java在基于语法层面(synchronized 关键字)实现了对管程技术,但是从使用方式和性能上来说,内置锁(synchronized 关键字)的粒度相对过大,不支持超时和中断等问题。
为了弥补这些问题,从JDK层面对其“重复造轮子”,在JDK内部对其重新设计和定义,甚至实现了新的特性。
在Java领域中,从JDK源码分析来看,基于JDK层面实现的锁大致主要可以分为以下4种方式:
- 基于Lock接口实现的锁:JDK1.5版本提供的ReentrantLock类
- 基于ReadWriteLock接口实现的锁:JDK1.5版本提供的ReentrantReadWriteLock类
- 基于AQS基础同步器实现的锁:JDK1.5版本提供的并发相关的同步器Semaphore,CyclicBarrier以及CountDownLatch等
- 基于自定义API操作实现的锁:JDK1.8版本中提供的StampedLock类
从阅读源码不难发现,在Java SDK 并发包主要通过AbstractQueuedSynchronizer(AQS)实现多线程同步机制的封装与定义,而通过Lock 和 Condition 两个接口来实现管程,其中 Lock 用于解决互斥问题,Condition 用于解决同步问题。
3|1一.AQS基础同步器基本理论
在Java领域中,同步器是专门为多线程并发设计的同步机制,主要是多线程并发执行时线程之间通过某种共享状态来实现同步,只有当状态满足这种条件时线程才往下执行的一种同步机制。
一个标准的AQS同步器主要有同步状态机制,等待队列,条件队列,独占模式,共享模式等五大核心要素组成。
在Java领域中,JDK的JUC(java.util.concurrent.)包中提供了各种并发工具,但是大部分同步工具的实现基于AbstractQueuedSynchronizer类实现,其内部结构主要如下:
- 同步状态机制(Synchronization Status):主要用于实现锁(Lock)机制,是指同步状态,其要求对于状态的更新必须原子性的
- 等待队列(Wait Queue):主要用于存放等待线程获取到的锁资源,并且把线程维护到一个Node(节点)里面和维护一个非阻塞的CHL Node FIFO(先进先出)队列,主要是采用自旋锁+CAS操作来保证节点插入和移除的原子性操作。
- 条件队列(Condition Queue):用于实现锁的条件机制,一般主要是指替换“等待-通知”工作机制,主要是通过ConditionObject对象实现Condition接口提供的方法实现。
- 独占模式(Exclusive Mode):主要用于实现独占锁,主要是基于静态内部类Node的常量标志EXCLUSIVE来标识该节点是独占模式
- 共享模式(Shared Mode):主要用于实现共享锁,主要是基于静态内部类Node的常量标志SHARED来标识该节点是共享模式
我们可以得到一个比较通用的并发同步工具基础模型,大致包含如下几个内容,其中:
- 条件变量(Conditional Variable): 利用线程间共享的变量进行同步的一种工作机制
- 共享变量((Shared Variable)):一般指对象实体对象的成员变量和属性
- 阻塞队列(Blocking Queue):共享变量(Shared Variable)及其对共享变量的操作统一封装
- 等待队列(Wait Queue):每个条件变量都对应有一个等待队列(Wait Queue),内部需要实现入队操作(Enqueue)和出队操作(Dequeue)方法
- 变量状态描述机(Synchronization Status):描述条件变量和共享变量之间状态变化,又可以称其为同步状态
- 工作模式(Operation Mode): 线程资源具有排他性,因此定义独占模式和共享模式两种工作模式
综上所述,条件变量和等待队列的作用是解决线程之间的同步问题;共享变量与阻塞队列的作用是解决线程之间的互斥问题。
3|2二. JDK显式锁统一概念模型
在并发编程领域,有两大核心问题:一个是互斥,即同一时刻只允许一个线程访问共享资源;另一个是同步,即线程之间如何通信、协作。
综合Java领域中的并发锁的各种实现与应用分析来看,一把锁或者一种锁,基本上都会包含以下几个方面:
- 锁的同步器工作机制:主要是考虑共享模式还是独享模式,是否支持超时机制,以及是否支持超时机制?
- 锁的同步器工作模式:主要是基于AQS基础同步器封装内部同步器,是否考虑公平/非公平模式?
- 锁的状态变量机制: 主要锁的状态设置,是否共享状态变量?
- 锁的队列封装定义:主要是指等待队列和条件队列,是否需要条件队列或者等待队列定义?
- 锁的底层实现操作: 主要是指底层CL锁和CAS操作,是否需要考虑自旋锁或者CAS操作实例对象方法?
- 锁的组合实现新锁: 主要是基于独占锁和共享锁,是否考虑对应API自定义操作实现?
综上所述,大致可以根据上述这些方向,我们便可以清楚🉐️知道Java领域中各种锁实现的基本理论时和实现思想。
3|3五.StampedLock(印戳锁)的设计与实现
在Java领域中,StampedLock(印戳锁)是针对于Java多线程并发控制中引入一个共享锁定义读操作与独占锁定义读操作等场景共同组合构成一把锁来提高并发,主要是基于自定义API操作实现的一种并发控制工具类。
1. 设计思想
StampedLock(印戳锁)是对ReentrantReadWriteLock读写锁的一 种改进,主要的改进为:在没有写只有读的场景下,StampedLock支持 不用加读锁而是直接进行读操作,最大程度提升读的效率,只有在发 生过写操作之后,再加读锁才能进行读操作。
一般来说,StampedLock 里的写锁和悲观读锁加锁成功之后,都会返回一个 stamp;然后解锁的时候,需要传入这个 stamp。
1.1 印戳锁的基本理论
虽然基于AQS基础同步器实现了各种锁,但是由于采用的自旋锁+CAS操作方式会导致如下两个问题:
- CAS恶性空自旋会浪费大量的CPU资源
- 在SMP架构的CPU上会导致“总线风暴”问题
解决CAS恶性空自旋的有效方式之一是以空间换时间,较为常见的 方案有两种:分散操作热点和使用队列削峰。
基于这个基础,在JDK1.8版本中,基于使用队列削峰的方式,自定义API操作,提供了StampedLock(印戳锁)的实现。
简单来说,StampedLock(印戳锁)提供了三种锁的实现模式,其中:
- 悲观读锁:与ReadWriteLock的读锁类似,多个线程可以同 时获取悲观读锁,悲观读锁是一个共享锁。
- 乐观读锁:相当于直接操作数据,不加任何锁,连读锁都不 要。
- 写锁:与ReadWriteLock的写锁类似,写锁和悲观读锁是互 斥的。虽然写锁与乐观读锁不会互斥,但是在数据被更新之后,之前 通过乐观读锁获得的数据已经变成了脏数据。
1.1 印戳锁的实现思想
StampedLock(印戳锁)与其他显式锁不同的是,主要是是最早在JDK1.8版本中提供的,从设计思想上来看,主要包括共享状态变量机制,内置的等待数据队列,读锁视图,写锁视图以及读写锁视图等5个核心要素。其中:
- 共享状态变量机制:主要是在内部封装一些静态私有的常量,用于描述各个模式之间的状态描述等。
- 内置的等待数据队列:主要是自定义实现一个基于CLH锁的等待队列
- 读锁视图:基于Lock接口实现一个对应读锁的视图
- 写锁视图:基于Lock接口实现一个对应写锁的视图
- 读写锁视图:基于ReadWriteLock接口实现一个包含读锁和写锁的视图
2. 基本实现
在StampedLock(印戳锁)类的JDK1.8版本中,对于StampedLock的基本实现如下:
2.1 共享状态变量机制
对于StampedLock锁中对于各种资源的标记,其封装了一系列的常量,主要可以分为以下几个方面,其中:
- 核心资源常量标识:是对线程操作资源的提供的常量封装,其中:
- NCPU:自旋控制的核心线程数量,主要通过Runtime.getRuntime().availableProcessors()获取设置。
- SPINS:等待队列自旋控制的最大自旋阈值,主要通过 (_NCPU _> 1) ? 1 << 6 : 0获取设置
- HEAD_SPINS: 等待队列头节点自旋控制的自旋阈值,主要通过 (_NCPU _> 1) ? 1 << 10 : 0获取设置
- MAX_HEAD_SPINS:等待队列头节点自旋控制的最大自旋阈值,主要通过(_NCPU _> 1) ? 1 << 16 : 0获取设置
- OVERFLOW_YIELD_RATE:线程让步操作等待的自旋阈值,默认值为7
- LG_READERS:读锁溢出的最大阈值,默认值为7
- _INTERRUPTED:线程中断标识,_默认值为1L
- 锁状态值设置标识:
- ORIGIN:锁状态的初始值,默认值为WBIT << 1,如果分配失败默认设置为0
- 锁状态的操作标识:
- RUNIT:读锁移动的位数,默认值为 1
- WBIT:写锁移动的位数,默认值为1L << LG_READERS
- RBITS:读锁移动的位数_,_默认值为_WBIT _- 1L
- RFULL:移动的位数,默认值为_RBITS _- 1L
- ABITS:锁移动的位数,默认值为 _RBITS _| WBIT
- SBITS:锁移动的位数,默认值为 ~RBITS
- 等待队列节点标识:
- WAITING:等待状态的初始值,默认值为-1
- CANCELLED:取消状态的初始值,默认值为1
- 读写锁的模式标识:
- RMODE:读锁模式,默认值为0
- WMODE:写锁模式,默认值为1
- CAS操作状态标识:封装了CAS操作状态标识,还通过反射实例化了Unsafe对象实例。
2.2 内置的等待队列WNode
对于StampedLock锁对于等待队列的实现,主要包含以下几个方面的内容,其中:
- 封装了一个等待队列WNode的静态内部类,其中:
- prev:等待队列的前驱节点
- next:等待队列的后驱节点
- cowait:表示依据锁标记存储当前线程入队的情况,队列锁列表
- thread: 线程对象,一般都是当前获取锁的线程
- status:用于表示锁的状态变量,对应着常量0,WAITING(-1), CANCELLED(1),其中,0表示正常状态,WAITING(-1)为等待状态,CANCELLED(1)为取消状态。
- mode:用于表示锁的模式,对应着常量RMODE和WMODE,其中RMODE为写模式,WMOD为读模式
- 构造方法WNode(int m, WNode p):用于实例化WNode对象,实现一个等待队列
- 实例化等待队列对象,主要封装一个头部节点whead和尾部节点wtail的对象
2.3 共用的读锁核心处理逻辑
首先,对于StampedLock锁的读锁视图与写锁视图的队列操作,有一个核心的处理逻辑:
其次,对于StampedLock锁的读锁视图的实现作来看,主要核心处理如下:
然后,对于StampedLock锁的写锁视图的实现作来看,主要核心处理如下:
最后,综合对于StampedLock锁的读锁和写锁的获取和释放等操作来看,主要核心处理都会调用以下2个方法,其中:
- tryIncReaderOverflow()方法:主要是实现对于锁获取自旋时最大重试次数的递增运算。其中:
- 对于满足_stamp_ >= RFULL条件时,利用compareAndSwapLong()方法来实现CAS操作加持修改状态值。对于readerOverflow作自增运算后返回一个_stamp,可能存在更新和释放操作。_
- 否则,利用LockSupport.nextSecondarySeed() 判断,对于线程做让步处理,默认返回0
- tryDecReaderOverflow()方法:主要是实现对于锁获取自旋时最大重试次数的递减运算。其中:
- 对于满足_stamp_ == RFULL条件时,利用compareAndSwapLong()方法来实现CAS操作加持修改状态值。对于readerOverflow>0做递减运算后返回一个_stamp,可能存在更新和释放操作。_
- 否则,利用LockSupport.nextSecondarySeed() 判断,对于线程做让步处理,默认返回0
2.4 基于Lock接口实现的ReadLockView
对于ReadLock的实现,主要包含以下几个方面的内容,其中:
- 基本实现方式:基于Lock接口实现,提供了对应的锁获取和释放操作方法,其中:
- lock()方法:一般模式,主要通过StampedLock类中readLock()方法实现
- lockInterruptibly()方法:可中断模式,主要通过StampedLock类中readLockInterruptibly()方法实现
- 无参数tryLock() 方法:尝试获取锁,主要依据StampedLock类中tryReadLock() != 0L来实现
- 有参数tryLock() 方法:尝试获取锁,主要依据StampedLock类中tryReadLock(long time, TimeUnit unit)!= 0L来实现
- unlock()方法:锁的释放,主要通过StampedLock类中unstampedUnlockRead()方法实现
- newCondition() 方法:不支持条件变量的定义,默认设置抛出UnsupportedOperationException
- 对应处理方法:主要是在StampedLock外层实现的操作方法,其中:
- readLock()方法:读锁的实现,主要核心逻辑在acquireRead()方法
- tryReadLock()方法:尝试获取读锁,核心处理逻辑是根据对应的条件返回对应的锁的_stamp,否则抛出_InterruptedException。
- readLockInterruptibly()方法:读锁的可中断机制实现,核心处理逻辑是判断线程是否中断以及利用acquireRead方法验证,条件成立时,返回锁的_stamp,否则抛出_InterruptedException。
- unstampedUnlockRead()方法:释放锁,核心处理逻辑自旋操作+compareAndSwapLong实现。
2.5 基于Lock接口实现的WriteLockView
对于WriteLockView的实现,主要包含以下几个方面的内容,其中:
- 基本实现方式:基于Lock接口实现,提供了对应的锁获取和释放操作方法,其中:
- lock()方法:一般模式,主要通过StampedLock类中WriteLock()方法实现
- lockInterruptibly()方法:可中断模式,主要通过StampedLock类中writeLockInterruptibly()方法实现
- 无参数tryLock() 方法:尝试获取锁,主要依据StampedLock类中tryWriteLock() != 0L来实现
- 有参数tryLock() 方法:尝试获取锁,主要依据StampedLock类中tryWriteLock(long time, TimeUnit unit) != 0L来实现
- unlock()方法:锁的释放,主要通过StampedLock类中unstampedUnlockWrite()方法实现
- newCondition() 方法:不支持条件变量的定义,默认设置抛出UnsupportedOperationException
- 核心处理方法:主要是在StampedLock外层实现的操作方法,其中:
- writeLock()方法:写锁的实现,主要核心逻辑在acquireWrite()方法
- tryWriteLock()方法:尝试获取写锁,核心处理逻辑是根据对应的条件返回对应的锁的_stamp,否则抛出_InterruptedException。
- writeLockInterruptibly()方法:写锁的可中断机制实现,核心处理逻辑是判断线程是否中断以及利用acquireWrite方法验证,条件成立时,返回锁的_stamp,否则抛出_InterruptedException。
- unstampedUnlockWrite()方法:释放锁,核心处理逻辑主要是通过调用release(WNode h) 方法实现。
2.6 基于ReadWriteLock接口实现ReadWriteLockView
对于ReadWriteLockView的实现,主要包含两个部分,其中:
- 基于ReadWriteLock接口实现,主要是实现readLock()和writeLock()方法
- 在asReadWriteLock()方法中,实例化ReadWriteLockView对象
3. 具体实现
对于StampedLock的具体实现,我们可以从如下几个方面拆解开来分析:
- 共享锁ReadLock锁获取操作实现: 需要区分悲观读锁和乐观读锁的获取个有不同,一般有默认获取方式和尝试获取两种方式。
- 独占锁WriteLock写锁获取操作实现: 写锁与悲观读锁互斥,一般有默认获取方式和尝试获取两种方式
- 共享锁ReadLock锁释放操作实现: 一般分为全释放和半释放ReadLock锁操作两种方式
- 独占锁WriteLock锁释放操作实现:一般分为全释放和半释放WriteLock锁操作两种方式
接下来,我们便从具体的代码中来分析以上内容的基本实现,以方便我们正确认识和了解StampedLock锁。
3.1 共享锁ReadLock读锁获取操作实现
对于读锁的获取来说,都属于是共享锁,主要提供了以下几种方式:
- 无参数tryReadLock()方法:悲观读锁的获取方式,默认模式,不支持超时机制
- 有参数tryReadLock()方法:悲观读锁的获取方式,指定参数模式,支持超时机制
- 无参数tryOptimisticRead()方法:乐观读锁的获取方式,没有加锁操作
3.2 独占锁WriteLock写锁获取操作实现
对于写锁的获取来说,都属于是独占锁,主要提供了以下几种方式:
- 无参数tryWriteLock()方法:默认模式,不支持超时机制
- 有参数tryWriteLock()方法:指定模式,依据参数来实现,支持超时机制
3.3 共享锁ReadLock释放操作实现
对于读锁的释放来说,主要提供了以下几种方式:
- unlock() 方法:依据锁的状态status来匹配对应锁的stamp,然后释放锁操作
- unlockRead()方法: 依据锁的状态status来匹配对应读锁的stamp,然后释放锁操作
- tryUnlockRead()方法:释放当前持有的读锁,会设置一个stamp然后返回true,否则,返回false
- tryConvertToReadLock()方法:依据锁的状态status来匹配对应读锁的stamp,然后根据对应情况处理。其中:
- 单写锁模式:一般返回一个对应读锁的stamp
- 悲观读模式:直接返回对应读锁的stamp
- 乐观读模式:需要获取一个读锁,然后是立即返回对应读锁的stamp
- tryConvertToOptimisticRead(): 依据锁的状态status来匹配对应读锁的stamp,然后转换升级处理释放。其中:
- 悲观读模式:属于一般读锁模式,返回的是检测到对应读锁的stamp
- 乐观读模式:需要返回通过验证的对应读锁的stamp
3.4 独占锁WriteLock写锁释放操作实现
对于写锁的释放来说,主要提供了以下种方式:
- unlock() 方法:依据锁的状态status来匹配对应锁的stamp,然后释放锁操作
- unlockWrite()方法:依据锁的状态status来匹配对应写锁的stamp,然后释放锁操作
- tryUnlockWrite()方法:释放当前持有的写锁,会设置一个stamp然后返回true,否则,返回false
- tryConvertToWriteLock()方法:依据锁的状态status来匹配stamp,根据对应锁的做升级处理。其中:
- 单写锁模式:直接返回对应的写锁标记stamp
- 读写锁模式:需要释放读锁锁,并返回对应的写锁标记stamp
- 乐观读模式:直接返回对应的写锁标记stamp
综上所述,StampedLock锁本质上依然是一种读写锁,只是没有基于AQS基础同步器来实现,是自定义封装API操作实现的。
4|0写在最后
通过对Java领域中,JDK内部提供的各种锁的实现来看,一直围绕的核心主要还是基于AQS基础同步器来实现的,但是AQS基础同步器不是一种非它不可的技术标准规范,更多的只是一套技术参考指南。
但是,实际上,Java对于锁的实现与运用远远不止这些,还有相位器(Phaser)和交换器(Exchanger),以及在Java JDK1.8版本之前并发容器ConcurrentHashMap中使用的分段锁(Segment)。
不论是何种实现和应用,在Java并发编程领域来讲,都是围绕线程安全问题的角度去考虑的,只是针对于各种各样的业务场景做的具体的实现。
一定意义上来讲,对线程加锁只是并发编程的实现方式之一,相对于实际应用来说,Java领域中的锁都只是一种单一应用的锁,只是给我们掌握Java并发编程提供一种思想没,三言两语也不可能详尽。
到此为止,这算是对于Java领域中并发锁的最终章,文中表述均为个人看法和个人理解,如有不到之处,忘请谅解也请给予批评指正。
最后,技术研究之路任重而道远,愿我们熬的每一个通宵,都撑得起我们想在这条路上走下去的勇气,未来仍然可期,与各位程序编程君共勉!
__EOF__

本文链接:https://www.cnblogs.com/mazhilin/p/16717833.html
关于博主:评论和私信会在第一时间回复。或者直接私信我。
版权声明:Copyright © 2018-2021 PivotalCloud Technology Systems Incorporated. All rights reserved.
声援博主:如果您觉得文章对您有帮助,可以点击文章右下角【推荐】一下。您的鼓励是博主的最大动力!
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 分享4款.NET开源、免费、实用的商城系统
· 全程不用写代码,我用AI程序员写了一个飞机大战
· MongoDB 8.0这个新功能碉堡了,比商业数据库还牛
· 白话解读 Dapr 1.15:你的「微服务管家」又秀新绝活了
· 记一次.NET内存居高不下排查解决与启示