乐观锁(CAS)和悲观锁(synchronized)的详细介绍

1. 锁的定义

在代码中多个线程需要同时操作共享变量,这时需要给变量上把锁,保证变量值是线程安全的。
锁的种类非常多,比如:互斥锁、自旋锁、重入锁、读写锁、行锁、表锁等这些概念,总结下来就两种类型,乐观锁和悲观锁。

2.乐观锁

乐观锁就是持比较乐观态度的锁。在操作数据时非常乐观,认为别的线程不会同时修改数据,只有到数据提交的时候才通过一种机制来验证数据是否存在冲突。一般使用CAS算法实现。乐观锁适用于多读的应用类型,这样可以提高吞吐量。

3.悲观锁

比较悲观的锁,总是想着最坏的情况,每次去拿数据的时候都认为别人会修改,所以每次在拿数据的时候都会上锁,这样别人想拿这个数据就会阻塞直到它拿到锁。比如行锁,表锁等,读锁,写锁等,都是在做操作之前先上锁。在Java中,synchronized从偏向锁、轻量级锁到重量级锁,全是悲观锁。JDK提供的Lock实现类全是悲观锁。一般用于多写的场景。

4.CAS的介绍

CAS 即 Compare and Swap,它体现的一种乐观锁的思想,比如:多个线程要对一个共享的整型变量执行 +1 操作:

// 需要不断尝试
while (true) {
    int 旧值 = 共享变量; // 比如拿到了当前值 0
    int 结果 = 旧值 + 1; // 在旧值 0 的基础上增加 1 ,正确结果是 1

   /*这时候如果别的线程把共享变量改成了 5,本线程的正确结果 1 就作废了,这时候compareAndSwap 返回 false,重新尝试,
   直到:compareAndSwap 返回 true,表示本线程做修改的同时,别的线程没有干扰*/
    if( compareAndSwap ( 旧值, 结果 )) {
        // 成功,退出循环
    }
}

获取共享变量时,为了保证该变量的可见性,需要使用 volatile 修饰。结合 CAS 和 volatile 可以实现无锁并发,适用于竞争不激烈、多核 CPU 的场景下。CAS 底层依赖于一个 Unsafe 类来直接调用操作系统底层的 CAS 指令。

java.util.concurrent中提供了原子操作类,可以提供线程安全的操作,例如:AtomicInteger、 AtomicBoolean等,它们底层就是采用 CAS 技术 + volatile 来实现的。

java的内存模型(可见性,原子性,有序性)详细介绍_傻鱼爱编程的博客-CSDN博客这个里面原子性问题,用CAS技术解决方案如下:

    // 创建原子整数对象
    private static AtomicInteger i = new AtomicInteger(0);

    public static void main(String[] args) throws InterruptedException {
        Thread t1 = new Thread(() -> {
            for (int j = 0; j < 1000; j++) {
                i.getAndIncrement();  // 获取并且自增  i++
            }
        });

        Thread t2 = new Thread(() -> {
            for (int j = 0; j < 1000; j++) {
                i.getAndDecrement(); // 获取并且自减  i--
            }
        });

        t1.start();
        t2.start();
        t1.join();
        t2.join();
        System.out.println(i);
    }

5. synchronized的介绍

Java HotSpot 虚拟机中,每个对象都有对象头(包括 class 指针和 Mark Word)。Mark Word 平时存储这个对象的哈希码 、分代年龄,当加锁时,这些信息就根据情况被替换为 标记位 、 线程锁记录指针 、重量级锁指针 、线程ID 等内容。

JDK5引入了CAS原子操作,从JDK6开始对synchronized的实现机制进行了各种优化,包括使用JDK5引进的CAS自旋之外,还增加了自适应的CAS自旋、锁消除、锁粗化、偏向锁、轻量级锁(默认开启偏向锁)这些优化策略。由于此关键字的优化使得性能极大提高,同时语义清晰、操作简单、无需手动关闭,推荐在允许的情况下尽量使用此关键字。

JDK6以后锁主要存在四种状态,依次是:无锁状态(对象头中存储01)、偏向锁状态(对象头中存储线程id)、轻量级锁状态(对象头中存储00)、重量级锁状态(对象头中存储10),锁的升级是单向的。

5.1  偏向锁

Java6中引入了偏向锁来做优化:大多数情况下,锁不仅不存在多线程竞争,而且总是由同一线程多次获得,为了让线程获得锁的代价更低,引进了偏向锁。只有第一次使用 CAS 将线程 ID 设置到对象的 Mark Word 头,之后发现这个线程 ID 是自己的就表示没有竞争,不用重新 CAS。

偏向锁的缺点:
1. 撤销偏向需要将持锁线程升级为轻量级锁,这个过程中所有线程需要暂停(STW)
2. 访问对象的 hashCode 也会撤销偏向锁
3.撤销偏向和重偏向都是批量进行的,以类为单位,如果撤销偏向到达某个阈值,整个类的所有对象都会变为不可偏向的

开启偏向锁(默认开启):-XX:+UseBiasedLocking -XX:BiasedLockingStartupDelay=0
关闭偏向锁:-XX:-UseBiasedLocking

举个例子分析一下:

假设有两个方法同步块,利用同一个对象加锁

Object ob = new Object();
public void method1() {
    synchronized(ob) {
        // 同步块 A
        method2();
    }
}
public void method2() {
    synchronized(ob) {
        // 同步块 B
    }
}

解释过程如下: 

乐观锁(CAS)和悲观锁(synchronized)的详细介绍_第1张图片

 5.2 轻量级锁

若偏向锁失败,它会尝试使用轻量级锁的优化手段,此时Mark Word 的结构也变为轻量级锁的结构。如果一个对象虽然有多线程访问,但多线程访问的时间是错开的(也就是没有竞争),如果存在同一时间访问同一锁的场合,就会导致轻量级锁膨胀为重量级锁。

轻量级锁竞争的时候,用自旋进行了优化,如果当前线程自旋成功(即这时候持锁线程已经退出了同步块,释放了锁),这时当前线程就可以避免阻塞。在Java 6之后自旋锁是自适应的,比如对象刚刚的一次自旋操作成功过,那么认为这次自旋成功的可能性会高,就多自旋几次;反之,就少自旋甚至不自旋,总之,比较智能。自旋会占用 CPU 时间,单核 CPU 自旋就是浪费,多核 CPU 自旋才能发挥优势。Java 7 之后不能控制是否开启自旋功能。

举个例子分析一下:

假设有两个方法同步块,利用同一个对象加锁

Object ob = new Object();
public void method1() {
    synchronized(ob) {
        // 同步块 A
        method2();
    }
}
public void method2() {
    synchronized(ob) {
        // 同步块 B
    }
}

 解释过程如下: (每个线程都的栈帧都会包含一个锁记录的结构,内部可以存储锁定对象的 Mark Word)

乐观锁(CAS)和悲观锁(synchronized)的详细介绍_第2张图片

 5.3 重量锁 

 如果在尝试加轻量级锁的过程中,CAS 操作无法成功(经过自旋),这时一种情况就是有其它线程为此对象加上了轻量级锁(有竞争),这时需要进行锁膨胀,将轻量级锁变为重量级锁。

乐观锁(CAS)和悲观锁(synchronized)的详细介绍_第3张图片

 

你可能感兴趣的:(java,开发语言,jvm,tomcat)