多线程 - java 中各种锁

乐观锁 悲观锁

这不是一种具体的锁,是一个广义的概念,可以认为所有的锁都是悲观或乐观的,java 中绝大部分锁都是悲观锁(synchronized、ReentrantLock等);也有乐观锁(原子类的递增、读写锁的读锁),java 的乐观锁都是 cas 实现的

乐观锁 顾名思义,老是假设自己在操作数据的同时一定会有别的数据也来操作,所以自己获取数据的时候要加锁,防止别的线程拿到这份数据
常见的实现有 synchronized、Lock的实现类、数据库更新删除和查询的 for update
特点:先加锁可以保证写操作时数据正确

悲观锁 老是觉得别的线程不会同时来操作数据,所以不会添加锁,只是再将要执行更改的时候进行判断,如果数据没发生更改就执行,如果发生了更改就 报错或重试
常见的方案有数据库的 version 机制,cas自旋锁
特点:不加锁的特点能够使其读操作的性能大幅提升

自旋锁(CAS)

乐观锁的基础,java 中的乐观锁是通过 CAS 实现的,Compare-and-Swap 或 Compare-and-Set,整个过程中没有加锁释放锁的步骤,所以也叫无锁(这种叫法并不合适,但是这样叫的人大有人在,准确的叫法应该是自旋锁)

原理是:如果要改变变量 flag 的值,比如要从 1 改成 2,如果当前 flag 的值是 1(flag 没变过),才改成 2,如果不是 1 就不改

ABA 问题:上面的例中,如果 flag 改成2,再改成 1,其实是变过的,但是最终表现的没有变过

java 中的乐观锁底层是 Unfase 类的 native 方法实现的,具体表现在各种原子类,比如 AtomicBoolean,AtomicLong,AtomicInteger
java 使用 AtomicStampedReference 解决原子类的 ABA 问题,原理是其内部维护了一个 Pair 对象,里面放了版本信息。其实解决 ABA 问题的原理就和数据库的乐观锁一样

公平锁 非公平锁

多个线程访问同一个锁,如果是公平锁,严格根据线程获取锁的先后顺序进行排队,先到先得;非公平锁就是后排队但是可能先得到锁(插队)

公平锁:线程一启动就进入 FIFO 队列,严格按照先后顺序获取到锁

非公平锁:线程启动时会尝试直接获取锁(插队),如果获取成功意味着插队成功。如果插队失败进入 FIFO 队列,这时和公平锁就一样了
对于公平锁,已经有别的线程获取到锁了,别的线程就要排队,等持有锁的线程释放锁然后唤醒一个等待中的线程。如果是非公平的,可能插队,直接就执行了,少唤醒的开销,所以 非公平锁吞吐量更高
如果一直都是后来的线程插队成功,队列里面的线程一直没机会被唤醒,就锁饿死了,所以 公平锁能缓解锁饥饿

java 中 synchronized 是非公平锁,ReentrantLock 默认是非公平锁,也可以是非公平锁(构造方法传入 true)

实现原理:通过一个 FIFO 队列来实现锁排队,公平锁就是严格按照排队先后顺序,持有锁的线程释放锁,唤醒队列中第一个线程;非公锁启动时判断是否能获取到锁,如果获取到就不排队了直接执行,如果没获取到锁就进入队列

重入锁 不可重入锁

在一个方法调用链中,如果外层方法获取了锁,内层方法自动也获取到该锁(前提是同一个锁,就不用再次获取锁),这就是可重入锁,反之就是不可重入锁,不可重入锁很容易造成死锁

public class Widget {
    // doSomething 获取到锁
    public synchronized void doSomething() {
        System.out.println("方法1执行...");
        // 如果是不可重入锁,doOthers 就要等 doSomething 释放锁,但是 doSomeing 没有执行完不会释放
        doOthers();
    }
    public synchronized void doOthers() {
        System.out.println("方法2执行...");
    }
}

目前 jdk1.8 没有已经实现了的不可重入锁(想想也是,太容易死锁了),ynchronized、Lock的实现类 都是可重入锁
实现原理是 AQS

读写锁

类似数据库的 S 和 X 锁,是一堆锁(上面说的这些锁,都是一个锁,不管读还是写,都要先拿到锁才能操作),也叫共享锁和排他锁/独占锁
多个线程能同时获取读锁,但是不能获取写锁(当读的场景多的时候写锁可能会饥饿)
只有一个线程能获取写锁,当写锁被持有时,别的线程不能获取读锁
java 的 ReentrantReadAndWriteLock 是已经实现了的读写锁
实现原理同 ReentrantLock 一样,也是 AQS

锁升级/锁膨胀

指的是 1.6 以后 synchronized 的优化方案,1.6 及之前一开始就是重量级锁,1.6 之后先是偏向锁,然后是 自旋锁(轻量级锁),最后是重量级锁
升级过程:

  • 代码压根不考虑并发(没有 synchronized 同步方法和同步代码块),这是当前对象是 无锁 状态
  • 使用了 synchronized,假设是同步方法,这时当前对象就从无锁变成有锁了,但是 synchronized 方法执行完不会释放锁,因为后续获取锁的线程可能还是当前线程(偏向第一次获取锁的线程,简言之就是先假设没有线程竞争),锁升级为 偏向锁
  • 当有别的线程参与竞争锁的时候,锁升级为 自旋锁(轻量级锁),cas 不会有加锁释放锁的操作,所以效率很高,所以 jvm 就让后面的线程自旋来获取锁
  • 一个线程持有锁,别的线程自旋,占着cpu做不了任何有效的业务操作,这就是忙等(busy-waiting),短时间还好(默认自旋10次,使用-XX:PreBlockSpin来更改),时间长了肯定也不合理,所以升级为 重量级锁
posted @ 2023-05-24 16:53  黄光跃  阅读(144)  评论(0编辑  收藏  举报