Java锁事之Unsafe、CAS、AQS知识点总结

Wesley13
• 阅读 522

Java锁事之Unsafe、CAS、AQS知识点总结

关注 “Java艺术”一起来充电吧!

Unsafe、CAS、AQS是我们了解Java中除synchronized之外的锁必须要掌握的重要知识点。CAS是一个比较和替换的原子操作,AQS的实现强依赖CAS,而在Java中,CAS操作需通过使用Unsafe提供的方法实现。

0

sun.misc.Unsafe

Java不能像C++中那样可以自己申请内存和释放内存,想要实现直接读取某内存地址中存储的数据,就必须要通过JNI调用C/C++方法。Java中的Unsafe类正是为我们提供了类似C++手动管理内存的能力。

Unsafe类实现了很多功能,如Volatile读写、直接内存操作、获取字段在对象中的偏移地址、线程调度、内存屏障。

Unsafe类是"final"的,不允许继承,且构造函数是private的,无法实例化。如果我们想使用Unsafe提供的功能,就必须要使用反射去获取Unsafe实例。

public static Unsafe getUnsafe() throws Exception{

如果想通过Unsafe自己实现一个锁,那么我们需要关心两个方法,一个是获取字段在对象中的偏移地址的方法,另一个则是CAS方法。使用Unsafe的objectFieldOffset方法可获取字段在对象中的偏移地址。

Field field = MyLock.class.getDeclaredField("state");

1

Conmpare And Swap

Java中Unsafe提供的compareAndSwapXXX方法,第一个参数是要修改的对象,第二个参数是要修改的字段的偏移地址,第三个参数是期望当前内存中存储的值,第四个参数是想要写入的新值。当且仅当期望值与当前内存值相等时,写入成功。

// 字段类型为引用类型

每次调用CAS之前都需要先获取一次当前内存中的最新值,作为期望值。字段需要使用volatile关键字声明,确保字段的可见性,能够获取到因被其它线程修改的最新值。

Unsafe提供的CAS方法底层是通过汇编指令cmpxchg实现的,cmpxchg指令实现原子性比较替换操作。

// exchange_value:改变值,新值

通过CAS可以实现乐观锁。先通过CAS尝试修改共享资源,当发现别人在修改时,再去加锁,通过自旋,直到CAS修改成功。悲观锁的定义是,总认为别人会修改,因此先上锁再修改。

synchronized、Lock都是悲观锁。虽然Lock是基于AQS实现的,而AQS使用CAS实现加锁,但使用Lock都只能是先调用lock方法获取锁才能去修改共享资源,使用完后必须调用unlock释放锁,因此Lock也是悲观锁。

CAS会存在 ABA问题。如线程1将值由A改为B后,线程3又将B改为A,由于线程2在线程1修改之前将获取到的当前值A作为期望值时,所以当线程1将B改为A后,线程2无需重新获取期望值CAS也能操作成功,于是又将A改为C。出现这种问题都是由于线程调度引起的。

例如链表,用CAS修改链表的表头,那么ABA问题将导致修改后的链表不是预期的链表。

Java锁事之Unsafe、CAS、AQS知识点总结

2

AbstractQueuedSynchronizer

AQS是一个抽象类,提供实现锁的模板方法,用于实现依赖先进先出(FIFO)等待队列的阻塞锁和相关同步器。AQS屏蔽了同步状态管理、线程的排队、等待与唤醒等底层操作。AQS提供独占式获取与释放同步状态、共享式获取与释放同步状态操作。

AQS内部维护一个volatile修饰的整型变量state,称为同步状态,且维护一个获取同步状态的等待队列,是一个双向链表。获取与释放锁其实就是获取与释放同步状态state。

public abstract class AbstractQueuedSynchronizer extends AbstractOwnableSynchronizer implements java.io.Serializable {

以独占式获取或释放同步状态为例。当state为0时,表示当前没有任何线程占用锁,当有线程想要获取锁时,就通过CAS将state增加1。由于使用volatile确保线程的可见性,其它线程能够读到这个状态的改变。当有其它线程同时想要获取锁时,发现state不为0就会将当前线程封装为Node节点加入等待队列,通过自旋获取锁。

Java锁事之Unsafe、CAS、AQS知识点总结

线程被挂起后,当前驱节点释放锁时,会唤醒其后继节点,继而使后继节点重新尝试获取锁。线程的挂起与唤醒是通过LockSupport实现的,而LockSupport也是通过Unsafe实现的。

Java锁事之Unsafe、CAS、AQS知识点总结

Java锁事之Unsafe、CAS、AQS知识点总结

图片来源于《Java并发编程的艺术》

AQS还有一个字段,保存当前持有锁的线程,也是用于实现可重入锁的关键,当state不为0,且当前持有锁的线程是自己时,就将state加1,成功获取锁,获取多少次锁就对应要调用多少次释放锁,将state减为0时才会真正的释放锁。

public abstract class AbstractOwnableSynchronizer implements java.io.Serializable {

ReentrantLock

ReentrantLock有一个带参数的构造方法,可以指定创建公平锁还是非公平锁。

如果是公平锁,则每次调用lock方法,先判断当前是否存在其它线程等待获取锁,如果是就将当前线程包装为Node放入等待队列,实现谁先来谁先获取到锁。

Java锁事之Unsafe、CAS、AQS知识点总结

图一 公平锁的lock方法、tryAcquire方法的实现

如果是非公平锁,则每次调用lock方法都会先CAS尝试获取锁,如果获取失败,再放入等待队列。

Java锁事之Unsafe、CAS、AQS知识点总结

图二 非公平锁的lock方法的实现

非公平锁与公平锁的代码实现区别很简单,非公平锁除了lock方法会先使用CAS尝试获取锁之外,tryAcquire方法的实现不会去判断当前是否已经有线程在等待获取锁,因此不管当前等待队列有多少线程在等待获取锁,只要CAS操作成功就能获取到锁。

Java锁事之Unsafe、CAS、AQS知识点总结

图三 非公平锁的tryAcquire方法的实现

非公平锁相比公平锁的优点是吞吐量更高,非公平锁会有更少的线程被挂起,缺点是会导致一些线程阻塞时间太长。

ConditionObject

AQS还实现了诸如synchronized的wait、notify的支持。AQS的内部类ConditionObject也维护了一个等待队列。

public class ConditionObject implements Condition{

当外部获取到锁时调用await方法,会将当前线程从锁的等待队列中移出,并放入ConditionObject维护的条件等待队列,将线程挂起。当外部获取到锁的线程调用signal方法时,将条件等待队列中的第一个节点放入AQS的锁等待队列,并将线程唤醒。signalAll方法则是将当前条件等待队列中的所有节点按顺序放入AQS的锁等待队列。判断exclusiveOwnerThread是否等于当前线程可知当前线程是否持有锁。

ReentrantLock lock = new ReentrantLock();

关于共享锁与排他锁

共享锁是当前有线程占用锁时,其它线程还能以共享方式获取到这个锁;排他锁是当前有线程占用锁时,其它线程不能再获取到锁。

最常用的读写锁ReentrantReadWriteLock也是通过AQS实现的,读锁也叫共享锁,写锁也叫排他锁。通过AQS的同步状态state判断是否持有锁,通过acquire、release实现独占式获取与释放同步状态,通过acquireShared和releaseShared实现共享式获取与释放同步状态。

static final class Node {

在将当前线程包装为Node节点放入等待队列时,都是调用addWaiter方法实现的。调用addWaiter需要传入一个参数,这个参数就标志这个节点是一个共享式节点还是独占式节点。

private Node addWaiter(Node mode) {

添加共享式节点:

addWaiter(Node.SHARED)

Java锁事之Unsafe、CAS、AQS知识点总结

添加独占式节点:

addWaiter(Node.EXCLUSIVE)

Java锁事之Unsafe、CAS、AQS知识点总结

共享式获取同步状态调用acquireShared方法。

public final void acquireShared(int arg) {

先调用tryAcquireShared尝试获取同步状态,获取失败后再调用doAcquireShared方法,将当前线程包装为共享式节点放入等待队列,并自旋获取同步状态。

private void doAcquireShared(int arg) {

当前节点的前驱节点是头节点且获取同步状态成功后,调用setHeadAndPropagate方法将当前节点设置为头节点,并传递tryAcquireShared返回的值(ReentrantReadWriteLock的返回值是1)。

private void setHeadAndPropagate(Node node, int propagate) {

setHeadAndPropagate方法将当前节点设置为头节点,并判断下一个节点是否也是共享式节点,或者下一个节点为null,如果是,则调用doReleaseShared方法唤醒后继节点。

private void doReleaseShared() {

调用doReleaseShared方法唤醒后继节点。如果后继节点是独占式节点,则后继节点调用tryAcquire独占式获取同步状态不会成功,直到当前所有共享式节点都调用了releaseShared(releaseShared调用tryReleaseShared)释放同步状态。

多个共享式线程连续获取同步状态的过程:

  1. 第一个节点的线程获取到同步状态后,将自己设置为头节点,并唤醒其后继节点;

  2. 第二个节点的线程获取到同步状态后又将自己设置为头节点,自然上一个节点就被移出队列了,接着唤醒其后继节点;

  3. 如果被唤醒的节点为独占式节点,则由于同步状态不为0,还会自旋,调用parkAndCheckInterrupt将自己挂起;

  4. 最后一个共享式获取同步状态的线程调用tryReleaseShared方法释放同步状态时,会调用一次doReleaseShared将当前头节点(最后一个共享式获取同步状态的节点)的后继节点换醒。

  5. 独占式节点成功获取到同步状态。

唤醒后继独占式节点的线程不一定是最后一个共享式获取同步状态的线程,但头节点一定是最后一个共享式获取同步状态的节点。

Java锁事之Unsafe、CAS、AQS知识点总结

Java锁事之Unsafe、CAS、AQS知识点总结

公众号:Java艺术

扫码关注最新动态

本文分享自微信公众号 - Java艺术(javaskill)。
如有侵权,请联系 support@oschina.cn 删除。
本文参与“OSC源创计划”,欢迎正在阅读的你也加入,一起分享。

点赞
收藏
评论区
推荐文章
blmius blmius
2年前
MySQL:[Err] 1292 - Incorrect datetime value: ‘0000-00-00 00:00:00‘ for column ‘CREATE_TIME‘ at row 1
文章目录问题用navicat导入数据时,报错:原因这是因为当前的MySQL不支持datetime为0的情况。解决修改sql\mode:sql\mode:SQLMode定义了MySQL应支持的SQL语法、数据校验等,这样可以更容易地在不同的环境中使用MySQL。全局s
Jacquelyn38 Jacquelyn38
2年前
2020年前端实用代码段,为你的工作保驾护航
有空的时候,自己总结了几个代码段,在开发中也经常使用,谢谢。1、使用解构获取json数据let jsonData  id: 1,status: "OK",data: 'a', 'b';let  id, status, data: number   jsonData;console.log(id, status, number )
皕杰报表之UUID
​在我们用皕杰报表工具设计填报报表时,如何在新增行里自动增加id呢?能新增整数排序id吗?目前可以在新增行里自动增加id,但只能用uuid函数增加UUID编码,不能新增整数排序id。uuid函数说明:获取一个UUID,可以在填报表中用来创建数据ID语法:uuid()或uuid(sep)参数说明:sep布尔值,生成的uuid中是否包含分隔符'',缺省为
Wesley13 Wesley13
2年前
Java多线程并发06——CAS与AQS
在进行更近一步的了解Java锁的知识之前,我们需要先了解与锁有关的两个概念CAS与AQS。关注我的公众号「Java面典」了解更多Java相关知识点。CAS(CompareAndSwap/Set)概念CAS函数,是比较并交换函数,它是原子操作函数。原理CA
Stella981 Stella981
2年前
KVM调整cpu和内存
一.修改kvm虚拟机的配置1、virsheditcentos7找到“memory”和“vcpu”标签,将<namecentos7</name<uuid2220a6d1a36a4fbb8523e078b3dfe795</uuid
Stella981 Stella981
2年前
Android So动态加载 优雅实现与原理分析
背景:漫品Android客户端集成适配转换功能(基于目标识别(So库35M)和人脸识别库(5M)),导致apk体积50M左右,为优化客户端体验,决定实现So文件动态加载.!(https://oscimg.oschina.net/oscnet/00d1ff90e4b34869664fef59e3ec3fdd20b.png)点击上方“蓝字”关注我
Easter79 Easter79
2年前
Twitter的分布式自增ID算法snowflake (Java版)
概述分布式系统中,有一些需要使用全局唯一ID的场景,这种时候为了防止ID冲突可以使用36位的UUID,但是UUID有一些缺点,首先他相对比较长,另外UUID一般是无序的。有些时候我们希望能使用一种简单一些的ID,并且希望ID能够按照时间有序生成。而twitter的snowflake解决了这种需求,最初Twitter把存储系统从MySQL迁移
Wesley13 Wesley13
2年前
00:Java简单了解
浅谈Java之概述Java是SUN(StanfordUniversityNetwork),斯坦福大学网络公司)1995年推出的一门高级编程语言。Java是一种面向Internet的编程语言。随着Java技术在web方面的不断成熟,已经成为Web应用程序的首选开发语言。Java是简单易学,完全面向对象,安全可靠,与平台无关的编程语言。
Wesley13 Wesley13
2年前
MySQL部分从库上面因为大量的临时表tmp_table造成慢查询
背景描述Time:20190124T00:08:14.70572408:00User@Host:@Id:Schema:sentrymetaLast_errno:0Killed:0Query_time:0.315758Lock_
Python进阶者 Python进阶者
3个月前
Excel中这日期老是出来00:00:00,怎么用Pandas把这个去除
大家好,我是皮皮。一、前言前几天在Python白银交流群【上海新年人】问了一个Pandas数据筛选的问题。问题如下:这日期老是出来00:00:00,怎么把这个去除。二、实现过程后来【论草莓如何成为冻干莓】给了一个思路和代码如下:pd.toexcel之前把这