遗忘悠剑

🍀 记录精彩的程序人生 开始使用

乐观锁、悲观锁、【多线程系列】CAS、AQS 等的介绍和典型用法~

一、并发控制

当程序中可能出现并发的情况时,就需要通过一定的手段来保证在并发情况下数据的准确性,通过这种手段保证了当前用户和其他用户一起操作时,所得到的结果和他单独操作时的结果是一样的。这种手段就叫做并发控制。并发控制的目的是保证一个用户的工作不会对另一个用户的工作产生不合理的影响。

没有做好并发控制,就可能导致脏读、幻读和不可重复读等问题。

常说的并发控制,一般都和数据库管理系统(DBMS)有关。在 DBMS 中的并发控制的任务,是确保在多个事务同时存取数据库中同一数据时,不破坏事务的隔离性和统一性以及数据库的统一性。

实现并发控制的主要手段大致可以分为乐观并发控制和悲观并发控制两种。

首先要明确:无论是悲观锁还是乐观锁,都是人们定义出来的概念,可以认为是一种思想。其实不仅仅是关系型数据库系统中有乐观锁和悲观锁的概念,像 hibernate、tair、memcache 等都有类似的概念。所以,不应该拿乐观锁、悲观锁和其他的数据库锁等进行对比。

二、悲观锁(Pessimistic Lock)

当要对数据库中的一条数据进行修改的时候,为了避免同时被其他人修改,最好的办法就是直接对该数据进行加锁以防止并发。这种借助数据库锁机制,在修改数据之前先锁定,再修改的方式被称之为悲观并发控制【又名“悲观锁”,Pessimistic Concurrency Control,缩写“PCC”】。

百度百科:

悲观锁,正如其名,具有强烈的独占和排他特性。它指的是对数据被外界(包括本系统当前的其他事务,以及来自外部系统的事务处理)修改持保守态度。因此,在整个数据处理过程中,将数据处于锁定状态。悲观锁的实现,往往依靠数据库提供的锁机制(也只有数据库层提供的锁机制才能真正保证数据访问的排他性,否则,即使在本系统中实现了加锁机制,也无法保证外部系统不会修改数据)。

之所以叫做悲观锁,是因为这是一种对数据的修改抱有悲观态度的并发控制方式。我们一般认为数据被并发修改的概率比较大,所以需要在修改之前先加锁。

悲观锁主要分为共享锁或排他锁

  • 共享锁【Shared lock】又称为读锁,简称 S 锁。顾名思义,共享锁就是多个事务对于同一数据可以共享一把锁,都能访问到数据,但是只能读不能修改。
  • 排他锁【Exclusive lock】又称为写锁,简称 X 锁。顾名思义,排他锁就是不能与其他锁并存,如果一个事务获取了一个数据行的排他锁,其他事务就不能再获取该行的其他锁,包括共享锁和排他锁,但是获取排他锁的事务是可以对数据行读取和修改。

悲观并发控制实际上是“先取锁再访问”的保守策略,为数据处理的安全提供了保证。

悲观锁

但是在效率方面,处理加锁的机制会让数据库产生额外的开销,还有增加产生死锁的机会。另外还会降低并行性,一个事务如果锁定了某行数据,其他事务就必须等待该事务处理完才可以处理那行数据。

三、乐观锁(Optimistic Locking)

乐观锁是相对悲观锁而言的,乐观锁假设数据一般情况下不会造成冲突,所以在数据进行提交更新的时候,才会正式对数据的冲突与否进行检测,如果发现冲突了,则返回给用户错误的信息,让用户决定如何去做。

百度百科:

乐观锁机制采取了更加宽松的加锁机制。乐观锁是相对悲观锁而言,也是为了避免数据库幻读、业务处理时间过长等原因引起数据处理错误的一种机制,但乐观锁不会刻意使用数据库本身的锁机制,而是依据数据本身来保证数据的正确性。

相对于悲观锁,在对数据库进行处理的时候,乐观锁并不会使用数据库提供的锁机制。一般的实现乐观锁的方式就是记录数据版本。

乐观锁

乐观并发控制相信事务之间的数据竞争(data race)的概率是比较小的,因此尽可能直接做下去,直到提交的时候才去锁定,所以不会产生任何锁和死锁。

四、实现方式

1️⃣ 悲观锁实现方式

悲观锁的实现,往往依靠数据库提供的锁机制。在数据库中,悲观锁的流程如下:

  1. 在对记录进行修改前,先尝试为该记录加上排他锁(exclusive locking)。
  2. 如果加锁失败,说明该记录正在被修改,那么当前查询可能要等待或者抛出异常。具体响应方式由开发者根据实际需要决定。
  3. 如果成功加锁,那么就可以对记录做修改,事务完成后就会解锁了。
  4. 期间如果有其他对该记录做修改或加排他锁的操作,都会等待解锁或直接抛出异常。

拿比较常用的 MySQL Innodb 引擎举例,来说明一下在 SQL 中如何使用悲观锁。

要使用悲观锁,必须关闭 MySQL 数据库的自动提交属性。因为 MySQL 默认使用 autocommit 模式,也就是说,当执行一个更新操作后,MySQL 会立刻将结果进行提交。(sql 语句:set autocommit=0)

以电商下单扣减库存的过程说明一下悲观锁的使用:

悲观锁使用

以上,在对 id = 1 的记录修改前,先通过 for update 的方式进行加锁,然后再进行修改。这就是比较典型的悲观锁策略。

如果以上修改库存的代码发生并发,同一时间只有一个线程可以开启事务并获得 id=1 的锁,其它的事务必须等本次事务提交之后才能执行。这样可以保证当前的数据不会被其它事务修改。

上面提到,使用 select…for update 会把数据给锁住,不过需要注意一些锁的级别,MySQL InnoDB 默认行级锁。行级锁都是基于索引的,如果一条 SQL 语句用不到索引是不会使用行级锁的,会使用表级锁把整张表锁住,这点需要注意。

2️⃣ 乐观锁实现方式

使用乐观锁就不需要借助数据库的锁机制了。

乐观锁的概念中其实已经阐述了它的具体实现细节。主要就是两个步骤:冲突检测和数据更新。其实现方式有一种比较典型的就是 CAS(Compare and Swap)。

CAS 是项乐观锁技术,当多个线程尝试使用 CAS 同时更新同一个变量时,只有其中一个线程能更新变量的值,而其它线程都失败,失败的线程并不会被挂起,而是被告知这次竞争中失败,并可以再次尝试。

比如前面的扣减库存问题,通过乐观锁可以实现如下:

乐观锁使用

以上,在更新之前,先查询一下库存表中当前库存数(quantity),然后在做 update 的时候,以库存数作为一个修改条件。当提交更新的时候,判断数据库表对应记录的当前库存数与第一次取出来的库存数进行比对,如果数据库表当前库存数与第一次取出来的库存数相等,则予以更新,否则认为是过期数据。

以上更新语句存在一个比较重要的问题,即传说中的 ABA 问题

比如说一个线程 one 从数据库中取出库存数 3,这时候另一个线程 two 也从数据库中取出库存数 3,并且 two 进行了一些操作变成了 2,然后 two 又将库存数变成 3,这时候线程 one 进行 CAS 操作发现数据库中仍然是 3,然后 one 操作成功。尽管线程 one 的 CAS 操作成功,但是不代表这个过程就是没有问题的。

ABA

有一个比较好的办法可以解决 ABA 问题,那就是通过一个单独的可以顺序递增的 version 字段。改为以下方式即可:

ABA 的解决

乐观锁每次在执行数据的修改操作时,都会带上一个版本号,一旦版本号和数据的版本号一致就可以执行修改操作并对版本号执行 +1 操作,否则就执行失败。因为每次操作的版本号都会随之增加,所以不会出现 ABA 问题,因为版本号只会增加不会减少。

除了 version 以外,还可以使用时间戳,因为时间戳天然具有顺序递增性。

以上 SQL 其实还是有一定的问题的,就是一旦遇上高并发的时候,就只有一个线程可以修改成功,那么就会存在大量的失败。对于像淘宝这样的电商网站,高并发是常有的事,总让用户感知到失败显然是不合理的。所以,还是要想办法减少乐观锁的粒度的。有一条比较好的建议,可以减小乐观锁力度,最大程度的提升吞吐率,提高并发能力!如下:

优化

以上 SQL 语句中,如果用户下单数为 1,则通过 quantity - 1 > 0 的方式进行乐观锁控制。

以上 update 语句,在执行过程中,会在一次原子操作中自己查询一遍 quantity 的值,并将其扣减掉 1。

高并发环境下锁粒度把控是一门重要的学问,选择一个好的锁,在保证数据安全的情况下,可以大大提升吞吐率,进而提升性能。

五、如何选择

在乐观锁与悲观锁的选择上面,主要看下两者的区别以及适用场景就可以了。

  1. 乐观锁并未真正加锁,效率高。一旦锁的粒度掌握不好,更新失败的概率就会比较高,容易发生业务失败。
  2. 悲观锁依赖数据库锁,效率低。更新失败的概率比较低。

随着互联网三高架构(高并发、高性能、高可用)的提出,悲观锁已经越来越少的被应用到生产环境中了,尤其是并发量比较大的业务场景。

作者:o__p__0
链接:https://www.jianshu.com/p/d2ac26ca6525
来源:简书
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。

一、案例说明

银行两操作员同时操作同一账户。

比如 A、B 操作员同时读取一余额为 1000 元的账户,A 操作员为该账户增加 100 元,B 操作员同时为该账户扣除 50 元,A 先提交,B 后提交。最后实际账户余额为 1000-50=950 元,但本该为 1000+100-50=1050。这就是典型的并发问题。

乐观锁机制在一定程度上解决了这个问题。乐观锁,大多是基于数据版本(Version)记录机制实现。何谓数据版本?即为数据增加一个版本标识,在基于数据库表的版本解决方案中,一般是通过为数据库表增加一个 “version” 字段来实现。

读取出数据时,将此版本号一同读出,之后更新时,对此版本号加一。此时,将提交数据的版本数据与数据库表对应记录的当前版本信息进行比对,如果提交的数据版本号大于数据库表当前版本号,则予以更新,否则认为是过期数据。

对于上面修改用户帐户信息的例子而言,假设数据库中帐户信息表中有一个 version 字段,当前值为 1;而当前帐户余额字段(balance)为 1000 元。假设操作员 A 先更新完,操作员 B 后更新。

  1. 操作员 A 此时将其读出(version=1),并将其帐户余额增加 100(1000+100=1100)。
  2. 在操作员 A 操作的过程中,操作员 B 也读入此用户信息(version=1),并从其帐户余额中扣除 50(1000-50=950)。
  3. 操作员 A 完成了修改工作,将数据版本号加一(version=2),连同帐户增加后余额(balance=1100),提交至数据库更新,此时由于提交数据版本大于数据库记录当前版本,数据被更新,数据库记录 version 更新为 2。
  4. 操作员 B 完成了操作,也将版本号加一(version=2)试图向数据库提交数据(balance=950),但此时比对数据库记录版本时发现,操作员 B 提交的数据版本号为 2,数据库记录当前版本也为 2,不满足 “提交版本必须大于记录当前版本才能执行更新 “的乐观锁策略,因此,操作员 B 的提交被驳回。

这样,就避免了操作员 B 用基于 version=1 的旧数据修改的结果覆盖操作员 A 的操作结果的可能。

二、乐观锁介绍

乐观锁(Optimistic Locking)相对悲观锁而言,乐观锁假设认为数据一般情况下不会造成冲突,所以在数据进行提交更新的时候,才会正式对数据的冲突与否进行检测,如果发现冲突了,则让返回用户错误的信息,让用户决定如何去做。实现乐观锁有以下 2 种方式:

1️⃣ 使用版本号实现乐观锁:数据版本机制和时间戳机制

① 使用数据版本(Version)记录机制实现,这是乐观锁最常用的一种实现方式。何谓数据版本?即为数据增加一个版本标识,一般是通过为数据库表增加一个数字类型的 “version” 字段来实现。当读取数据时,将 version 字段的值一同读出,数据每更新一次,对此 version 值加一。当提交更新的时候,判断数据库表对应记录的当前版本信息与第一次取出来的 version 值进行比对,如果数据库表当前版本号与第一次取出来的 version 值相等,则予以更新,否则认为是过期数据。如图:

如图,如果更新操作顺序执行,则数据的版本(version)依次递增,不会产生冲突。但是如果发生有不同的业务操作对同一版本的数据进行修改,那么,先提交的操作(图中 B)会把数据 version 更新为 2,当 A 在 B 之后提交更新时发现数据的 version 已经被修改了,那么 A 的更新操作会失败。

② 时间戳机制,同样是在需要乐观锁控制的 table 中增加一个字段,字段类型使用时间戳(timestamp), 和上面的 version 类似,也是在更新提交的时候检查当前数据库中数据的时间戳和自己更新前取到的时间戳进行对比,如果一致则 OK,否则就是版本冲突。

2️⃣ 使用条件限制实现乐观锁

这个适用于只更新时做数据安全校验,适合库存模型,扣份额和回滚份额,性能更高。

三、解决方案一:使用版本号实现乐观锁

商品 goods 表中有一个字段 status,status 为 1 代表商品未被下单,status 为 2 代表商品已经被下单。那么对某个商品下单时必须确保该商品 status 为 1。假设商品的 id 为 1。下单操作包括 3 步骤:

1.查询出商品信息:

**

select name,status,version from goods where id=#{id}

2.根据商品信息生成订单

3.修改商品 status 为 2:

**

update goods set status=2,version=version+1
where id=#{id} and version=#{version};

为使用乐观锁,修改 goods 表,增加一个 version 字段,数据默认 version 值为 1。

goods 表初始数据如下:

**

mysql> select * from t_goods;  
+----+--------+------+---------+  
| id | status | name | version |  
+----+--------+------+---------+  
|  1 |      1 | 道具 |       1 |  
|  2 |      2 | 装备 |       2 |  
+----+--------+------+---------+  
2 rows in set  

mysql>

对于乐观锁的实现,使用 MyBatis 来进行实践,具体如下:

Goods 实体类:

**

public class Goods implements Serializable {  
    /** 
     * serialVersionUID:序列化ID
     */  
    private static final long serialVersionUID = 6803791908148880587L;
    /** 
     * id:主键id
     */  
    private int id;
    /** 
     * status:商品状态:1-未下单;2-已下单
     */  
    private int status;
    /** 
     * name:商品名称
     */  
    private String name;
    /** 
     * version:商品数据版本号
     */  
    private int version;
    @Override
    public String toString(){
        return "good id:"+id+", goods status:"+status+", goods name:"+name+
               ", goods version:"+version;
    }
    //setter and getter
}

GoodsDao:

**

/** 
 * updateGoodsUseCAS:使用CAS(Compare and set)更新商品信息
 * @param goods 商品对象 
 * @return 影响的行数 
 */  
int updateGoodsUseCAS(Goods goods);

mapper.xml:

**

<update id="updateGoodsUseCAS" parameterType="Goods">  
    <![CDATA[ 
        update goods 
        set status=#{status},name=#{name},version=version+1 
        where id=#{id} and version=#{version} 
    ]]>  
</update>

GoodsDaoTest 测试类:

**

@Test  
public void goodsDaoTest(){  
    int goodsId = 1;  
    //根据相同的id查询出商品信息,赋给2个对象  
    Goods goods1 = this.goodsDao.getGoodsById(goodsId);  
    Goods goods2 = this.goodsDao.getGoodsById(goodsId);  

    //打印当前商品信息  
    System.out.println(goods1);  
    System.out.println(goods2);  

    //更新商品信息1  
    goods1.setStatus(2);//修改status为2  
    int updateResult1 = this.goodsDao.updateGoodsUseCAS(goods1);  
    System.out.println("修改商品信息1"+(updateResult1==1?"成功":"失败"));  

    //更新商品信息2  
    goods1.setStatus(2);//修改status为2  
    int updateResult2 = this.goodsDao.updateGoodsUseCAS(goods2);  
    System.out.println("修改商品信息2"+(updateResult2==1?"成功":"失败"));  
}

输出结果:

**

good id:1, goods status:1, goods name:道具, goods version:1  
good id:1, goods status:1, goods name:道具, goods version:1  
修改商品信息1成功  
修改商品信息2失败

说明:

在 GoodsDaoTest 测试方法中,同时查出同一个版本的数据,赋给不同的 goods 对象,然后先修改 good1 对象然后执行更新操作,执行成功。然后修改 goods2,执行更新操作时提示操作失败。此时 goods 表中数据如下:

**

mysql> select * from t_goods;  
+----+--------+------+---------+  
| id | status | name | version |  
+----+--------+------+---------+  
|  1 |      2 | 道具 |       2 |  
|  2 |      2 | 装备 |       2 |  
+----+--------+------+---------+  
2 rows in set  

mysql>

可以看到 id 为 1 的数据 version 已经在第一次更新时修改为 2 了。所以更新 good2 时 update where 条件已经不匹配了,所以更新不会成功。这样就用版本号实现了乐观锁。

其实这种版本号的方法并不是适用于所有的乐观锁场景。举个例子,当电商抢购活动时,大量并发进入,如果仅仅使用版本号或者时间戳,就会出现大量的用户查询出库存存在,但是却在扣减库存时失败了,而这个时候库存是确实存在的。想象一下,版本号每次只会有一个用户扣减成功,不可避免的人为造成失败。这种时候就需要第二种场景的乐观锁方法。

四、解决方案二:使用条件限制实现乐观锁

同样以上述案例为例。将表结构修改如下:

**

mysql> select * from t_goods;  
+----+--------+------+---------+  
| id | status | name | quantity|  
+----+--------+------+---------+  
|  1 |      1 | 道具 |      10 |  
|  2 |      2 | 装备 |      10 |  
+----+--------+------+---------+  
rows in set  

mysql>

status 表示产品状态:1-在售;2-暂停出售。quantity 表示产品库存。

更新库存操作如下:

**

update goods
set quantity = quantity- #{buyQuantity} 
where id = #{id} 
AND quantity - #{buyQuantity} >= 0 
AND status = 1

说明:quantity -#{buyQuantity}>=0 这个情景适合不用版本号,只更新是做数据安全校验,适合库存模型,扣份额和回滚份额,性能更高。

注意:乐观锁的更新操作,最好用主键或者唯一索引来更新,这样是行锁,否则更新时会锁表。

作者:o__p__0
链接:https://www.jianshu.com/p/efe85c5b4d62
来源:简书
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。

什么是 CAS

CAS(Compare And Swap),即比较并交换。是解决多线程并行情况下使用锁造成性能损耗的一种机制,CAS 操作包含三个操作数——内存位置(V)、预期原值(A)和新值(B)。如果内存位置的值与预期原值相匹配,那么处理器会自动将该位置值更新为新值。否则,处理器不做任何操作。无论哪种情况,它都会在 CAS 指令之前返回该位置的值。CAS 有效地说明了“我认为位置 V 应该包含值 A;如果包含该值,则将 B 放到这个位置;否则,不要更改该位置,只告诉我这个位置现在的值即可。

在 Java 中,sun.misc.Unsafe 类提供了硬件级别的原子操作来实现这个 CAS。 java.util.concurrent 包下的大量类都使用了这个 Unsafe.java 类的 CAS 操作。

CAS 典型应用

java.util.concurrent.atomic 包下的类大多是使用 CAS 操作来实现的,比如 AtomicInteger、AtomicBoolean、AtomicLong。一般来说在竞争不是特别激烈的时候,使用该包下的原子操作性能比使用 synchronized 关键字的方式高效的多(查看 getAndSet(),可知如果资源竞争十分激烈的话,这个 for 循环可能会持续很久都不能成功跳出。不过这种情况可能需要考虑降低资源竞争才是)。

在较多的场景我们都可能会使用到这些原子类操作。一个典型应用就是计数了,在多线程的情况下需要考虑线程安全问题。

一、实现一个支持并发的计数功能

1

在并发环境下对 count 进行自增运算是不安全的,为什么不安全以及如何解决这个问题呢?

二、为什么并发环境下的 count 自增操作不安全

因为 count++ 不是原子操作,而是三个原子操作的组合:

  1. 读取内存中的 count 值赋值给局部变量 temp
  2. 执行 temp+1 操作
  3. 将 temp 赋值给 count

所以如果两个线程同时执行 count++ 操作的话,我们不能保证线程一按顺序执行完上述三步后线程二才开始执行。

三、并发环境下 count++ 不安全问题的解决方案

方案一:synchronized 加锁

2

加锁后代码如上,同一时间只有一个线程能加锁,其他线程需要等待锁,这样就不会出现 count 计数不准确的问题了,现在线程安全。。

但是引入 synchronized 会造成多个线程排队的问题,同一时间只有一个线程执行,这样的锁有点儿“重量级”了。这类似于悲观锁的实现,我需要获取这个资源,那么我就给它加锁,别的线程都无法访问该资源,直到我操作完后释放对该资源的锁。虽然随着 Java 版本更新,也对 synchronized 做了很多优化,但是处理这种简单的累加操作,仍然显得“太重了”。人家 synchronized 是可以解决更加复杂的并发编程场景和问题的。

而且在这个场景下,若用 synchronized,不就相当于让各个线程串行化了么?一个接一个的排队、加锁、处理数据、释放锁,下一个再进来。

方案二:Atomic 原子类

对于这种的 count++ 类的操作,我们完全可以换一种做法,Java 并发包下面提供了一系列的 Atomic 原子类,比如说 AtomicInteger

3

多个线程可以并发的执行 AtomicInteger 的 incrementAndGet() 方法,意思就是给我把 count 的值累加 1,接着返回累加后最新的值。实际上,Atomic 原子类底层用的不是传统意义的锁机制,而是无锁化的 CAS 机制,通过 CAS 机制保证多线程修改一个数值的安全性。

四、CAS 底层实现原理是什么

流程图如下:

流程图

假如说有 3 个线程并发的要修改一个 AtomicInteger 的值,他们底层的机制如下:

  1. 首先,每个线程都会先获取当前的值。接着走一个原子的 CAS 操作,原子的意思就是这个 CAS 操作一定是自己完整执行完的,不会被别人打断。
  2. 然后 CAS 操作里,会比较一下,现在你的值是不是刚才我获取到的那个值。如果是,说明没人改过这个值,那你给我设置成累加 1 之后的一个值。
  3. 同理,如果有人在执行 CAS 的时候,发现自己之前获取的值跟当前的值不一样,会导致 CAS 失败,失败之后,进入一个无限循环,再次获取值,接着执行 CAS 操作。

CAS 有没有什么问题?

五、CAS 性能优化

从上面的流程图其实可以看出来,比如说大量的线程同时并发修改一个 AtomicInteger,可能有很多线程会不停的自旋,进入一个无限重复的循环中。

这些线程不停地获取值,然后发起 CAS 操作,但是发现这个值被别人改过了,于是再次进入下一个循环,获取值,发起 CAS 操作又失败了,再次进入下一个循环。

在大量线程高并发更新 AtomicInteger 的时候,这种问题可能会比较明显,导致大量线程空循环,自旋转,性能和效率都不是特别好。那么如何优化呢?

Java 8 有一个新的类,LongAdder,他就是尝试使用分段 CAS 以及自动分段迁移的方式来大幅度提升多线程高并发执行 CAS 操作的性能,这个类具体是如何优化性能的呢?如图:

LongAdder

LongAdder 核心思想就是热点分离,这一点和 ConcurrentHashMap 的设计思想相似。就是将 value 值分离成一个数组,当多线程访问时,通过 hash 算法映射到其中的一个数字进行计数。而最终的结果,就是这些数组的求和累加。这样一来,就减小了锁的粒度。

LongAddr 的兄弟类如下:

LongAdder 兄弟类

六、什么是 AQS

AQS(AbstractQueuedSynchronizer),AQS 是 JDK 下提供的一套用于实现基于 FIFO 等待队列的阻塞锁和相关的同步器的一个同步框架。它使用了一个原子的 int value status 来作为同步器的状态(如:独占锁,1 代表已占有,0 代表未占有),通过该类提供的原子修改 status 方法(getState setState and compareAnsSetState),我们可以把它作为同步器的基础框架类来实现各种同步器。AQS 还定义了一个实现了 Condition 接口的 ConditionObject 内部类。Condition 将 Object 监视器方法(wait、notify 和 notifyAll)分解成截然不同的对象,以便通过将这些对象与任意 Lock 实现组合使用,为每个对象提供多个等待 set (wait-set)。其中,Lock 替代了 synchronized 方法和语句的使用,Condition 替代了 Object 监视器方法的使用。

简单来说,就是 Condition 提供类似于 Object 的 wait、notify 的功能 signal 和 await,都是可以使一个正在执行的线程挂起(推迟执行),直到被其他线程唤醒。但是 Condition 更加强大,如支持多个条件谓词、保证线程唤醒的顺序和在挂起时不需要拥有锁。这个抽象类被设计为作为一些可用原子 int 值来表示状态的同步器的基类。如果你有看过类似 CountDownLatch 类的源码实现,会发现其内部有一个继承了 AbstractQueuedSynchronizer 的内部类 Sync。可见 CountDownLatch 是基于 AQS 框架来实现的一个同步器。类似的同步器在 JUC 下还有不少。(eg. Semaphore)

AQS 用法

如上所述,AQS 管理一个关于状态信息的单一整数,该整数可以表现任何状态。比如, Semaphore 用它来表现剩余的许可数,ReentrantLock 用它来表现拥有它的线程已经请求了多少次锁;FutureTask 用它来表现任务的状态(尚未开始、运行、完成和取消)

作者:o__p__0
链接:https://www.jianshu.com/p/98220486426a
来源:简书
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。


🐶 你走,我不送你。你来,风雨无阻,我去接你。

评论
留下你的脚步
推荐阅读