由浅入深全面分析乐观锁、悲观锁、MVCC

目录

1、什么是并发控制

1.1 数据库中的三种并发场景

1.2 并发控制的解释

1.3 乐观锁与悲观锁的澄清

2、什么是悲观锁(Pessimistic Lock)

2.1 悲观锁的概念

2.2 悲观锁的实现

2.3 悲观锁的分类

2.4 悲观锁的优缺点

3、什么是乐观锁(Optimistic Lock)

3.1 乐观锁的概念

3.2 乐观锁的实现思想:CAS

3.3 乐观锁的具体实现

3.3.1 方式1:使用数据版本号(version)实现

3.3.2 方式2:使用时间戳(timestamp)实现

3.4 乐观锁的优缺点

4、悲观锁和乐观锁的具体实现SQL

4.1 悲观锁的具体实现SQL

4.2 乐观锁的具体实现SQL

4.2.1 使用乐观锁带来的ABA问题

4.2.2 ABA问题的解决:version或者timestamp

5、简要介绍:多版本并发控制MVCC

5.1 什么是当前读和快照读

5.2 如何选择乐观锁和悲观锁

5.3 乐观锁、悲观锁、MVCC三者的关系(重要)

6、另:理解 CAS 底层

7、另:CAS 典型应用

7.1 支持计数功能 Demo 实现

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

7.2.1 方案1:synchronized 加锁

7.2.2 使用Atomic 原子类

8、另:CAS 性能优化

99、参考

1、什么是并发控制

1.1 数据库中的三种并发场景

在数据库中,并发场景主要有三种,分别是:

(1)读-读 并发不存在任何问题,也不需要并发控制。

(2)读-写 并发有隔离性问题,可能会遇到 脏读、不可重复读、幻读 问题。

(3)写-写 并发有更新丢失问题,比如第一类更新丢失、第二类更新丢失。

在MySQL中,如果没有做好并发控制,就有可能会出现上述问题。

1.2 并发控制的解释

那么,什么是并发控制呢?在程序中,如果出现并发访问同一数据的情况时,就需要保证在并发情况下数据的准确性,以此来确保当前用户和其他用户同时操作该数据时,所得到的结果和他自己单独操作时的结果是一样的。这种控制手段就叫做 并发控制(Concurrency Control)。并发控制的目的是:保证一个用户的工作不会对其他并发用户的工作产生不合理的影响。

常说的并发控制,一般都是和数据库管理系统(DBMS)有关。DBMS 中的并发控制的任务就是:确保在多个并发事务同时存取数据库中同一数据时,不破坏事务的隔离性、一致性和数据库的统一性。实现并发控制的主要手段,大致可以分为 乐观并发控制 和 悲观并发控制 两种。

1.3 乐观锁与悲观锁的澄清

(1)首先需要明确的是:无论是悲观锁还是乐观锁,都是人们定义出来的一种概念,它们本质上不是数据库中具体的锁的概念,可以认为是一种用来描述两种类别的锁的思想。所以,在有了乐观锁与悲观锁这样的设计分类之后,我们就可以通过这个分类对数据库中具体的锁进行分门别类。

(2)不过,数据库中的乐观锁更倾向于叫做 乐观并发控制(OCC),悲观锁更倾向于叫做 悲观并发控制(PCC)。此外,还有区别于乐观锁与悲观锁的另外一种控制叫做 MVCC,即多版本并发控制乐观锁比较适用于读多写少的情况(多读场景)。悲观锁比较适用于写多读少的情况(多写场景)。

(3)同时,也不要把乐观锁与悲观锁 和数据库中的行锁、表锁、共享锁、排他锁等具体的一些锁混为一谈,因为它们本身就不是同一个维度的东西。前者是一个锁的分类思想,可以将后者根据是否进行趋近于乐观锁与悲观锁的并发控制思想进行分类。

(4)其实,乐观锁与悲观锁的概念不仅仅存在于数据库领域,可以说,存在并发控制的场景几乎都有乐观锁与悲观锁的思想存在。比如在Java中像 tair 数据存储结构 等都有类似的概念,但是,不同领域的乐观锁与悲观锁的具体实现都不尽相同,要解决的问题也可能有所不同

2、什么是悲观锁(Pessimistic Lock)

2.1 悲观锁的概念

在关系型数据库管理系统中,悲观并发控制【Pessimistic Concurrency Control,缩写是 “PCC”,又名“悲观锁” 】是一种并发控制的方法,又可以叫做悲观锁。

悲观锁指的是 采用一种持有悲观消极的态度,默认数据在被外界访问时,必然会产生冲突,所以 在数据处理的整个过程中都采用加锁的状态,保证同一时间 只有一个线程可以访问到数据,实现数据的排他性。

通常,数据库的悲观锁是利用数据库本身提供的锁机制去实现的。数据库的悲观并发控制可以解决【读-写冲突】和【写-写冲突】,也即是使用加锁的方式去解决。

2.2 悲观锁的实现

正如其名,悲观锁具有强烈的独占性和排他性。它指的是:在数据被外界修改时总是持保守态度(注意,这里的外界是指:包括本系统当前的其他事务,以及来自外部系统的事务处理)。因此,在整个数据处理过程中,都将数据处于锁定状态。

通常情况下,数据库的悲观锁就是利用数据库本身提供的锁去实现的(也只有数据库层提供的锁机制才能真正保证数据访问的排他性,否则,即使在本系统中实现了加锁机制,也无法保证外部系统不会并发的修改数据):

(1)当外界要访问某条数据时,那它就需要首先向数据库申请该数据的锁(某一种锁)。

(2)如果获锁成功,那它就可以操作该数据,在它操作数据期间,其他客户端就无法再操作该数据了。

(3)如果获锁失败,则代表同一时间已有其他客户端获得了该数据的锁,那它就必须等待其他客户端执行结束释放锁了。

(4)传统的关系型数据库(如MySQL)提供了非常多的锁,比如行锁、表锁、读锁、写锁等,都是在做操作数据之前先上锁。

2.3 悲观锁的分类

悲观锁可以分为两种:共享锁与排它锁。

(1)共享锁【shared locks】又称为 读锁,简称 S锁。顾名思义,共享锁就是指多个事务对于同一个数据可以共享一把锁,在多个事务中都能够访问到数据,但是,都是只能读不能修改。

(2)排他锁【exclusive locks】又称为 写锁,简称 X锁。顾名思义,排他锁就是不能与其他锁并存,如果一个事务获取了一个数据行的排他锁,那么,其他事务就不能再获取该行的其他任何锁,包括共享锁和排他锁,但是,获取排他锁的事务本身是可以对数据行进行读取和修改的。

2.4 悲观锁的优缺点

悲观并发控制 PCC,实际上是 “先取锁再访问” 的保守策略,为数据处理的安全性提供了保证。但是在效率方面,处理加锁的机制会让数据库产生额外的开销,还有增加产生死锁的风险。此外,还会降低并行性,如果一个事务锁定了某行数据,其他事务就必须等待该事务处理完毕且释放锁之后才可以处理那行数据。

优点:适合在读少写多的并发环境中使用,虽然无法维持非常高的性能,但是,在乐观锁无法提供更好的性能的前提下,可以做到数据的安全性。

缺点:加锁会增加系统开销,虽然能保证数据的安全,但是,数据处理的吞吐量低,不适合在读多写少的场合下使用。

3、什么是乐观锁(Optimistic Lock)

3.1 乐观锁的概念

在关系型数据库管理系统中,乐观并发控制【Optimistic Concurrency Control,缩写是“OCC”,又名“乐观锁”】也是一种并发控制的方法,又可以叫做乐观锁。

乐观锁,是相对于悲观锁而言的。乐观锁指的是 它假设认为即使在并发环境中,外界对数据的操作一般不会造成冲突,所以,并不会去加锁(所以乐观锁不是一把锁),而是在数据进行提交更新的时候,才会正式的对数据的冲突与否进行检测,如果检测发现冲突了,则让返回冲突信息,让用户决定如何去做下一步,比如说重试,直至成功为止;否则,则会直接更新数据。

数据库的乐观并发控制,要解决的是数据库并发场景下的写-写冲突,【旨在用无锁的方式去解决 多事务之间的写-写冲突】。乐观锁适用于读操作多写操作少的场景,这样可以提高程序的吞吐量。

3.2 乐观锁的实现思想:CAS

数据库中的乐观锁,并不是利用数据库本身提供的锁机制去实现的。其实,数据库中乐观锁的具体实现几乎就跟Java中乐观锁采用的CAS算法思想一致,所以,我们可以从CAS算法中学习到数据库乐观锁的设计。

CAS,全称为 Compare and Swap,比较并且交换,它是系统的指令集,整个CAS操作是一个原子操作,是不可分割的。从具体的描述上,我们可以这么看CAS操作:

(1)CAS指令需要3个操作数:分别是【内存位置V】、【旧预期值A】和【新值B】。

(2)CAS指令的执行过程是:CAS指令执行时,当我们读取的 内置位置V 的现值等于 旧预期值A 时,处理器才会用新值B 去更新内置位置V 的现值;否则,处理器就不执行更新。

放到代码层次上去理解 【 i = 2;   i++; 】 就是说:

(1)首先,线程1 从内存位置V 中读取到了值,保存并作为旧预期值A(A=2)。

(2)然后,因为 i 要进行 i++ 操作,系统会比较内存位置V 的现值跟旧预期值A 进行比较:现值 =?A

(3)如果相等,B = i++ = 3 ,新值B 就会对内存位置V 进行更新,所以,内存位置V 的值就从旧值2 变成了 新值3。

(4)如果不相等,则说明有其他的线程已经修改过了内存位置V 的值,比如 线程2 在线程1 修改 i 的值之前就更新了 i 的值,所以线程1 更新变量 i 会失败。但是,线程1 不会挂起,而是返回失败状态,等待调用线程决定是否重试或其他操作(通常会重试直到成功)。

数据库中的乐观锁实现也类似上述代码层面的实现。

3.3 乐观锁的具体实现

通常,乐观锁的实现有两种,但它们的内在思想都是CAS思想的设计。

3.3.1 方式1:使用数据版本号(version)实现

使用数据版本号(version)实现,这是乐观锁最常用的一种实现方式。

什么是数据版本号呢?就是在表中增加一个字段作为该记录的版本标识,比如叫 version,每次对该记录的写操作都会让 version + 1。

使用数据版本号(version)实现的具体过程:当我们读取了数据(当然也包括数据版本 version),做出更新,要提交的时候,就会拿 之前取得的version 去跟数据库中的 version现值 作比较,看是否一致。如果一致,则代表这个时间段并没有其他并发线程也修改过这个数据,更新成功,同时 version + 1;如果不一致,则代表在这个时间段内,该记录已经被其他并发线程修改过了, 会认为是过期数据,返回冲突信息,让用户决定下一步动作,比如重试(重新读取最新数据,再来一遍更新过程)。

update table set num = num + 1 , version = version + 1 
where version = #{version} and id = #{id} ;

3.3.2 方式2:使用时间戳(timestamp)实现

使用时间戳(timestamp)实现,这是乐观锁另外一种常用的实现方式。

也即:在表中增加一个字段,名称无所谓,比如叫 update_time, 字段类型使用时间戳(timestamp)。

使用时间戳的方式来实现乐观锁,其原理和方式1 一致:也是在更新提交的时检查当前数据库中数据的时间戳和自己更新前 取到的时间戳是否一致,如果一致,则代表这段时间内没有冲突,更新成功,同时会把时间戳更新为当前时间;否则,就是该时间段内有其他并发线程已经更新过数据了,则会返回冲突信息,等待用户的下一步动作。

update table set num = num + 1 ,update_time = unix_timestamp(now()) 
where id = #{id} and update_time = #{updateTime} ;

但是,我们需要注意,想要实现乐观锁思想,我们必须要保证 CAS 多个操作的原子性:即 获取数据库中数据的版本、拿数据库的数据版本与之前拿到的数据版本的比较、以及更新数据 等这几个操作的执行必须是连贯执行,具有复合操作的原子性。所以,如果是数据库的SQL,那么我们就要保证这些SQL操作处于同一个事务中。

3.4 乐观锁的优缺点

乐观并发控制(OCC),相信事务之间的数据竞争(data race)的概率比较小,因此,读取数据之后尽可能直接把业务做下去,直到提交修改的时候才去检查是否出现数据冲突,所以不会产生任何锁和死锁的现象。

优点:在读多写少的并发场景下,可以避免数据库加锁的开销,提高Dao层的响应性能。其实很多情况下,我们的ORM工具都有带有乐观锁的实现,所以,这些方法不一定需要我们人为去实现。

缺点:在读少写多的并发场景下,也就是说 在写操作竞争激烈的情况下,会导致CAS多次重试,冲突频率过高,导致开销比悲观锁更高。

4、悲观锁和乐观锁的具体实现SQL

4.1 悲观锁的具体实现SQL

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

(1)在对记录进行修改之前,先尝试为该记录加上排他锁(exclusive locks)。

(2)如果加锁失败,则说明该记录正在被其他事务修改,那么当前查询可能要阻塞等待或者抛出异常。具体的响应方式需要由开发者根据实际业务场景决定。

(3)如果加锁成功,那么本事务就可以对记录做修改了,事务 完成后就会解锁。

(4)在本事务操作数据期间,如果有其他对该记录做修改或加排他锁的事务操作,都会等待解锁或直接抛出异常。

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

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

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

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

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

上面提到,使用 select ... for update  会把数据对应的索引项给锁住,不过需要注意MySQL中一些锁的级别,MySQL InnoDB 默认行级锁。行级锁都是基于索引的,如果一条 SQL 语句用不到索引,则本次查询不会使用行级锁,会退化成使用表级锁把整张表锁住,这点需要注意!

4.2 乐观锁的具体实现SQL

乐观锁的实现,不需要借助数据库本身的锁机制。乐观锁的实现方式,主要就是两个步骤:在提交修改时,冲突检测和数据更新。比较典型的就是 CAS机制(Compare and Swap)。

CAS,即比较并交换,它是解决多线程并发情况下使用锁造成性能损耗的一种代替方案。CAS 操作包含三个操作数 —— 内存位置(V)、旧预期值(A) 和 新值(B)。

CAS的执行流程:如果内存位置(V)的现值 与 旧预期值(A) 相匹配,那么处理器会自动将内存位置(V)的现值 更新为 新值(B)。否则,处理器不做任何操作。

CAS机制 有效地说明了 “ 我认为内存位置(V)的现值 应该等于 旧预期值(A)。如果确实等于,则使用新值(B) 替换内存位置(V)的现值;否则,不要更改 内存位置(V)的现值。”

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

当多个线程尝试使用 CAS机制 同时更新同一个变量时,只有其中一个线程能够成功更新变量的值,而其它线程都更新失败,失败的线程并不会被挂起,而是被告知在这次竞争中失败,并可以再次重试。比如前面的扣减库存问题,通过乐观锁可以实现如下:

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

4.2.1 使用乐观锁带来的ABA问题

以上更新语句存在一个比较严重的问题,即 ABA问题

(1)比如说,线程2 从数据库中取出库存数 3,这个时候线程1 也从数据库中取出库存数 3。

(2)然后,线程1 进行了一些操作使库存数从3 变成了 2,紧接着,线程1 又将库存数从2 变成了 3。这个时候,线程1 进行 CAS 操作发现数据库中的库存数仍然是 3,然后线程1 操作成功。

(3)尽管线程1 的 CAS 操作成功,但是,这并不代表这个过程就是没有问题的。

这个过程出现的问题是:库存数在线程1中经过了两个过程的变化,第一个是线程1把库存数从3减少到了2,第二个是线程1把库存数从2增加到了3。这两个变化过程,其实在具体的业务场景中都代表着具体的含义,并不是简单的数字变化过程。

4.2.2 ABA问题的解决:version或者timestamp

一个比较好的解决办法,就是通过一个单独的可以顺序递增的 version 字段。优化如下:

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

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

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

以上 SQL 语句中,如果用户下单数为 1,则通过 quantity - 1 > 0 的方式进行乐观锁控制。在执行过程中,会在一次原子操作中查询一遍 quantity 的值,并将其扣减掉 1。

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

5、简要介绍:多版本并发控制MVCC

MVCC,全称是 Multi-Version Concurrency Control,多版本并发控制。MVCC是一种并发控制的方法,一般情况下,在数据库管理系统中实现对数据库的并发访问。参考 MVCC-来自于百度MVCC 在MySQL InnoDB 存储引擎中的实现,主要是为了提高数据库并发性能,用更好的方式去处理【读-写冲突】:做到即使有读-写冲突时,也能做到不加锁,非阻塞并发读。

5.1 什么是当前读和快照读

什么是MySQL InnoDB存储引擎下的当前读和快照读?

当前读:像 【 select ... lock in share mode (共享锁)、select ... for update (排它锁)、update / insert / delete (排它锁) 】这些SQL操作都是一种当前读。为什么叫当前读?就是因为它读取的是记录的最新版本,读取时还要保证其他并发事务不能修改当前记录,会对读取的记录进行加锁。当前读,实际上是一种加锁的操作,是悲观锁的实现。

快照读:像【 不加锁的 select ... 操作 】就是快照读,即不加锁的非阻塞读。快照读的前提 是隔离级别不是串行隔离级别,串行隔离级别下的快照读会退化成当前读。之所以出现快照读的情况,是基于提高并发性能的考虑,快照读的实现是基于多版本并发控制 即 MVCC,可以认为 MVCC 是行锁的一个变种,但是,它在很多情况下,避免了加锁操作,降低了开销。既然是基于多版本并发控制,那么 快照读可能读到的并不一定是数据的最新版本,而有可能是之前的历史版本。说白了,快照读就是MVCC思想在MySQL中的具体的非阻塞读功能的实现,整个MVCC多版本并发控制的目的 就是为了实现【读-写冲突不加锁】,提高并发读写性能,而这个读指的就是快照读。

5.2 如何选择乐观锁和悲观锁

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

(1)响应效率:如果Dao层需要非常高的响应速度,尤其是读多写少的场景下,那我们就可以采用乐观锁,降低数据库锁的开销,提高并发量。

(2)冲突频率:如果冲突频率非常高,那么我们可以采用悲观锁,保证成功率。毕竟如果冲突频率高,乐观锁会需要多次重试才能成功,代价可能会大大增加。

(3)重试代价:如果重试代价大,比如说重试过程的代码执行非常耗时,那么此时就不建议使用乐观锁了,还不如直接上悲观锁来保证成功率。

乐观锁,如果有人在你之前更新了,你的更新应当是被拒绝的,可以让用户重新操作。悲观锁,则会等待前一个更新完成之后,本更新才可以做。

所以,我们总结如下:

(1)在【读多写少】的场景下,CAS竞争没这么激烈的时候,我们可以采用乐观锁策略,降低数据库加锁的开销,提高数据库并发性能。

(2)在【读少写多】的场景下,因为会产生大量的CAS竞争,且重试成本较高的情况下,我们就不建议采用乐观锁策略了,还是直接使用悲观锁的数据库加锁。

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

5.3 乐观锁、悲观锁、MVCC三者的关系(重要)

(1)悲观并发控制(PCC),是一种用来解决【读-写冲突】和【写-写冲突】的加锁并发控制,为每个操作都加锁,同一时间下,只有获得该锁的事务才能有权利对该数据进行操作,没有获得锁的事务只能等待其他事务释放锁。所以,可以解决 脏读、幻读、不可重复读、第一类更新丢失、第二类更新丢失的问题。

(2)乐观并发控制(OCC),是一种用来解决【写-写冲突】的无锁并发控制,它认为事务之间的数据争用没有那么多,所以先进行修改,在提交事务前,检查一下事务开始后,有没有数据修改被提交,如果没有就提交,如果有就放弃并重试。乐观并发控制类似于自旋锁。乐观并发控制适用于低数据争用、写冲突比较少的场景。无法解决脏读、幻读、不可重复读,但是可以解决更新丢失类问题。

(3)多版本并发控制(MVCC),是一种用来解决【读-写冲突】的无锁并发控制,也就是为事务分配单向增长的时间戳,为每个修改都保存一个版本,版本与事务的时间戳相关联,读操作只读该事务开始前的数据库的快照。 这样,在读操作时就不用阻塞写操作,写操作也不用阻塞读操作。不仅可以提高并发性能,还可以解决脏读、幻读、不可重复读等事务问题,更新丢失类问题除外。

总的来说,MVCC的出现就是数据库不满用悲观锁去解决读-写冲突问题 因性能不高而提出的解决方案,所以在数据库中,我们可以形成两个组合:

(1)MVCC + 悲观锁:MVCC解决 读-写冲突,悲观锁解决 写-写冲突。
(2)MVCC + 乐观锁:MVCC解决 读-写冲突,乐观锁解决 写-写冲突。

这2种组合的方式,就可以最大程度的提高数据库并发性能,并解决【读-写冲突】和【写-写冲突】导致的问题。【参考:乐观锁和MVCC的区别】

6、另:理解 CAS 底层

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

(1)首先,每个线程都会先获取当前内存值,接着走一个原子的 CAS 操作。原子的意思就是:这个 CAS 操作一定是自己完整执行完的,不会被别人打断。

(2)然后 CAS 操作里,会比较一下当前内存值是不是本线程中一开始获取到的那个值。如果是,说明没人修改过这个值,可以对其 设置成累加 1 之后的一个新值。

(3)同理,如果有人在执行 CAS 的时候,发现本线程中一开始获取到的那个值跟当前内存值不一样,则会导致 CAS 操作失败。失败之后,进入一个无限循环,再次获取当前内存值,接着执行 CAS 操作,这就是自旋的过程

7、另:CAS 典型应用

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

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

7.1 支持计数功能 Demo 实现

public class Increment {
    private int count = 0;
    public void add() {
        count++;
    }
}

在并发环境下对 count 进行自增运算是不安全的,为什么不安全以及如何解决这个问题呢?因为 count++ 不是原子操作,而是三个原子操作的组合:

(1)读取内存中的 count 值赋值给局部变量 temp;

(2)执行 temp+1 操作;

(3)将 temp 赋值给 count。

所以,如果两个线程同时执行 count++ 的话,不能保证线程1按照顺序完整的执行完上述三步后 线程2才开始执行。

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

7.2.1 方案1:synchronized 加锁

synchronized 加锁,同一时间只允许一个线程能成功加锁,其他线程需要等待锁,这样就不会出现 count 计数不准确的问题了:

public class Increment {
    private int count = 0;
    public synchronized void add() {
        count++;
    }
}

但是,引入 synchronized 关键字 会造成多个线程排队的问题,相当于让各个线程串行化了,一个接一个的排队、加锁、处理数据、释放锁,下一个再进来。同一时间只有一个线程执行,这样的锁有点“重量级”了。    这类似于悲观锁的实现,需要获取这个资源,就给它加锁,别的线程都无法访问该资源,直到操作完后释放对该资源的锁。虽然随着 Java 版本更新,也对 synchronized 做了很多优化,但是处理这种简单的累加操作,仍然显得“太重了”。

7.2.2 使用Atomic 原子类

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

//import java.util.concurrent.atomic.AtomicInteger;
public static void main(String[] args) {
    public static AtomicInteger count = new AtomicInteger(0);
    public static void increase() {
        count.incrementAndGet();
    }
}

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

8、另:CAS 性能优化

从CAS机制的执行流程图可以看出来,大量的线程同时并发修改一个 AtomicInteger,可能有很多线程会不停的自旋,进入一个无限重复的循环中。

这些线程不停地获取值,然后发起 CAS 操作,但是发现这个值被别人改过了,于是再次进入下一个循环,获取值,发起 CAS 操作又失败了,再次进入下一个循环。在大量线程高并发更新 AtomicInteger 的时候,这种问题可能会比较明显,导致大量线程空循环,自旋转,性能和效率都不是特别好。那么如何优化呢?

Java8 有一个新的类,LongAdder,它就是尝试使用分段 CAS 以及自动分段迁移的方式来大幅度提升多线程高并发执行 CAS 操作的性能。

99、参考

(1)https://www.jianshu.com/p/58589a7dc5c6

(2)https://www.jianshu.com/p/98220486426a

(3)https://blog.csdn.net/cmm0401/article/details/115791191

(4)https://blog.csdn.net/cmm0401/article/details/115655095

(5)https://blog.csdn.net/SnailMann/article/details/88388829

 

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值