目录

chen 的个人博客

VX:TiAmo151028
Phone:13403656751
Email:zxydczzs@gmail.com

X

悲观锁、乐观锁、自旋锁、读写锁、共享锁、排它锁、统一锁、分段锁。

悲观锁

1、定义
具有强烈的独占和排它特性,它指的是对数据被外界(包括本系统当前的其他食物,以及来自外部系统的事务处理)修改持保守态度,因此,在整个数据处理过程中,将数据处于锁定状态。
2、Java 悲观锁实现之 synchronized

synchronized 就是典型的悲观锁,相当于不管哪一个线程(例如线程 A),运行到这个方法时,都要检查有没有其他线程 B 或者 C、D 等正在用这个方法(或者该类的其他同步方法),有的话要等正在使用 synchronized 方法的线程 B 或者 C、D 运行完这个方法后再运行此线程 A,没有的话,锁定调用者,然后直接运行。

补充
 1/**
 2 * @author ZhangXiaoYu
 3 * @date 2021/5/13 9:25
 4 */
 5public class Thumbnail {
 6    Object obj = new Object();
 7    //synchronized修饰普通方法,锁对象是this对象。
 8    public synchronized void test(){
 9
10    }
11
12    //synchronized代码块,可以写入this,那么锁的也是当前对象,也可以自己new一个对象当作锁对象使用。
13    public void test1(){
14        synchronized (obj){
15
16        }
17    }
18
19    //synchronized修饰静态方法,锁是当前对象的class对象
20    public static synchronized void test2(){
21
22    }
23}

乐观锁

1、定义

总是认为不会产生并发问题,每次去取数据的时候总认为不会有其他线程对数据进行修改,因此不会上锁,但是在更新时会判断其他线程在这之前有没有对数据进行修改,一般会使用版本号机制或 CAS 操作实现。乐观锁适用于多读的应用类型,这样可以提高吞吐量,像数据库提供的类似于 write_condition 机制,其实就是提供的乐观锁。在 Java 中 java.util.concurrent.atomic 包下面的原子变量类就是使用了乐观锁的一种实现方式 CAS 实现的。

2、Java 乐观锁实现之 CAS 自旋锁

CAS(Compare And Swap 比较并且替换)是乐观所的一种实现方式,是一种轻量级锁,JUC 中很多工具类的实现就是基于 CAS 的。

3、CAS 是怎样实现线程安全的。

线程在读取数据时不加锁,在准备写回数据时,先去查询原值,操作的时候比较原值是否修改,若未被其他线程修改则写回,若已被修改,则重新执行读取流程。

例如:现在一个线程要修改数据库的 name,修改之前会先去数据库查 name 的值,发现 name=“张三”,拿到值之后准备将 name 修改为 name=“李四“。在修改之前先判断一下 name 还是不是张三了,如果不是,那么改为李四的操作就放弃,如果 name 还是原来的张三,就把 name 改为李四,至此一个流程结束。

4、如何处理 CAS 的 ABA 问题。

所谓的 ABA 问题拿上面的例子说明,如果我们在要修改为李四前,name=“张三”,这是,线程 B 将 name 修改为王五并提交数据库,线程 C 将 name 王五在修改为张三,那么此时我们在业务处理完之后发现 name 还是张三,这是我们要修改 name 为李四,但是中间其实是经历了两个线程的写数据的。也就是说,在这个过程中任何线程都没做错什么。但是值被改变了,线程 1 却没有办法发现。

处理方法:加标志位,例如新增一个字增的字段 version,每操作一次 version 就加一。

例如:现在数据库中 name=“张三”,我们将张三拿到后,同时将 version 拿到,例如 1,在线程 1 将张三修改为李四时,线程 2 将张三修改为了王五,并且 version 加一变为 2,线程 3 将王五又改回了张三,并且 version 加一变为 3,此时线程 1 回来后判断此时张三还是张三,但是 version 对不上,所以操作驳回,不进行修改的操作。

自旋锁

1、定义

对,没错,CAS 就是自旋锁,上述已经大概介绍了一下自旋锁,下面来深入了解一下。

首先 CAS 就是根据乐观锁的设计思想来实现的,在取数据的时候,判断一下再次期间是否有人修改,如果没有修改,则直接使用。

CAS 有三个操作数,即“内存值 V“,旧的操作数“a”,新的操作数“b“。当我们需要更新 V 值为 b 时,首先我们判断 V 值是否和我们之前的所见值 a 相同,若相同则将 V 赋值为 b,若不同,则什么都不做,是一种非阻塞算法(non-blocking algorithm),在 Java 中可以通过锁和循环 CAS 的方式来实现原子操作。

2、实现

Java 中 java.util.concurrent.atomic 包相关类就是 CAS 的实现,通过自旋转 CAS 来尝试获得锁。

CAS 自旋锁适用于锁使用者保持锁时间比较短的情况中,因为自旋锁使用者一般保持锁的时间很短,所以才选择自旋而不是睡眠。

3、CAS 实现原子操作存在的三大问题
3.1、ABA 问题

因为 CAS 在进行操作的时候,总是需要比较新的操作数和旧的操作数,如果相同则更新,但是如果新的操作数经过两次修改之后返回原来的值,那么就出现了 ABA 问题。解决问题的方法就是增加一个版本号,不仅仅通过检查值得变化来确定是否更新。

3.2、循环时间开销大

自旋 CAS 如果长时间不成功,会给 CPU 带来非常大的执行开销。如果 JVM 能支持处理器提供的 pause 指令那么效率会有一定的提升。pause 指令有两个作用,第一它可以延迟流水线执行指令(de-pipiline),使 CPU 不会消耗过多的执行资源,延迟的时间取决于具体实现的版本,在一些处理器上延迟时间是零。第二它可以避免在退出循环的时候因内存顺序冲突(memory order violation)而引起 CPU 流水线被清空(CPU pipeline flush),从而提高 CPU 的执行效率。

3.3、只能保证一个共享变量的原子操作

解决的方法:把多个共享变量合并成一个共享变量。AtomicReference 类来保证引用对象之间的原子性。

使用锁实现原子操作。

锁的机制保障了只有获得锁的线程才能够操作锁定的内存区域。

除了偏向锁,JVM 都使用了循环 CAS 来获取锁。

读写锁

1、定义

读写锁分了两种情况,一种是读时的锁,一种是写时的锁,它允许多个线程同时读共享变量,但是只允许一个线程写共享变量,当然写共享变量的时候也会堵塞读的操作,这样在读的时候就不会互斥,提高读的效率。

2、实现

**ReadWriteLock:**实现类有 ReentrantReadWriteLock,接口有两个方法,readLock()和 writeLock(),两个方法分别返回读锁和写锁对象,返回值类型时 Lock 类型,因此返回对象的用法和 ReentrantLock 的用法相同。

3、分析
3.1、获取顺序

非公平模式(默认): 当以非公平模式初始化时,读锁和写锁的获取的顺序是不确定的。非公平锁主张竞争获取,可能会延缓一个或多个读或写线程,但是会比公平锁有更高的吞吐量。

公平模式: 当以公平模式初始化时,线程将以队列的顺序获取锁。当当前线程释放锁后,等待时间最长的写锁线程就会被分配写锁;或者有一组读线程等待时间比写线程长,那么这组读线程组将会被分配读锁。当有写线程持有写锁或者有等待的写线程时,一个尝试获取公平的读锁(非重入)的线程就会阻塞,这个线程直到等待时间最长的写锁获得锁并释放掉锁后才能获取到读锁。

3.2、可重入

允许读锁和写锁可重入,写锁可以获得读锁,读锁不能获得写锁。

3.3、锁降级

允许写锁降低为读锁。

3.4、中断锁的获取

在读锁和写锁的获取过程中支持中断操作。

3.5、支持 Condition

写锁支持 Condition 实现。

3.6、监控

提供确定锁是否被持有等辅助方法。

4、总结

当分析 ReentrantReadWriteLock 时,或者说分析内部使用 AQS 实现的工具类时,需要明白的就是 AQS 的 state 代表的是什么。ReentrantLockReadWriteLock 中的 state 同时表示写锁和读锁的个数。为了实现这种功能,state 的高 16 位表示读锁的个数,低 16 位表示写锁的个数。AQS 有两种模式:共享模式和独占模式,读写锁的实现中,读锁使用共享模式;写锁使用独占模式;另外一点需要记住的是,当有读锁时,写锁就不能获得;而当有写锁时,除了获得写锁的这个线程可以获得读锁外,其他线程不能获取到读锁。

共享锁&排它锁

1、定义

这里不再赘述,就是上面所述的读锁和写锁。

统一锁&分段锁

这里有个粒度的概念,统一锁就是将若干个粒度小的锁合并成为一把大锁,分段锁其实是一种锁的设计,并不是具体的一种锁,对于 ConcurrentHashMap 而言,其并发的实现就是通过分段锁的形式来实现高效的并发操作。当需要 put 的时候,并不是对整个 hashmap 进行加锁,而是先通过 hashcode 来知道它要放在哪一个分段中,然后对这个分段进行加锁,所以当多线程 put 的时候,只要不是放在一个分段中,就实现了真正的并行的插入。分段锁的设计目的是细化锁的粒度,当操作不需要更新整个数组的时候,就仅仅针对数组中的一项进行加锁操作。


标题:悲观锁、乐观锁、自旋锁、读写锁、共享锁、排它锁、统一锁、分段锁。
作者:zzzzchen
地址:https://dczzs.com/articles/2021/09/08/1631070399566.html