目录

大橙子

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

X

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

悲观锁

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

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

补充
/**
 * @author ZhangXiaoYu
 * @date 2021/5/13 9:25
 */
public class Thumbnail {
    Object obj = new Object();
    //synchronized修饰普通方法,锁对象是this对象。
    public synchronized void test(){

    }

    //synchronized代码块,可以写入this,那么锁的也是当前对象,也可以自己new一个对象当作锁对象使用。
    public void test1(){
        synchronized (obj){

        }
    }

    //synchronized修饰静态方法,锁是当前对象的class对象
    public static synchronized void test2(){

    }
}

乐观锁

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://www.dczzs.com/articles/2021/09/08/1631070399566.html