【JUC 并发】锁分类介绍

宏观上来分类,锁可以分为乐观锁与悲观锁。注意,这里说的的锁可以是数据库中的锁,也可以是 JavaPython 等开发语言中的锁技术。

一、乐观锁与悲观锁

宏观上来分类,锁可以分为乐观锁与悲观锁。注意,这里说的的锁可以是数据库中的锁,也可以是 JavaPython 等开发语言中的锁技术。

1.1 悲观锁

悲观锁是一种悲观思想,即认为写多,遇到并发写的可能性高。悲观锁在持有数据的时候总会把资源或者数据锁住,这样其他线程想要请求这个资源的时候就会阻塞,直到等到悲观锁把资源释放为止。

悲观锁的实现往往依靠数据库本身的锁功能实现。

  • 常见悲观锁

    1. 传统的关系型数据库里边就用到了很多这种锁机制,比如行锁表锁读锁写锁等,都是在做操作之前先上锁
    2. Java 中典型的悲观锁就是 SynchronizedReentrantLock 等独占锁(排他锁)。

1.2 乐观锁

乐观锁是一种乐观思想,即认为读多写少,遇到并发写的可能性低。它总认为资源数据不会被别人所修改,所以读取不会上锁。但是在进行写操作的时候会判断当前数据是否被修改过。

乐观锁的实现方案一般来说有两种:版本号机制 和 CAS 实现 。

  • 常见乐观锁

    数据库中的共享锁就是一种乐观锁。

乐观锁和悲观锁并不是一种真实存在的锁,而是一种设计思想。

1.3 乐观锁的实现方式

乐观锁一般有两种实现方式:采用版本号机制CAS(Compare-and-Swap)算法实现。

  • 版本号机制

版本号机制是在数据表中加上一个 version 字段来实现的,表示数据被修改的次数,当执行写操作并且写入成功后,version = version + 1,当线程 A 要更新数据时,在读取数据的同时也会读取 version 值,在提交更新时,若刚才读取到的 version 值为当前数据库中的 version 值相等时才更新,否则重试更新操作,直到更新成功。

  • CAS 算法

CAS 是一种的无锁算法,即在不使用锁的情况下实现多线程之间的变量同步,也就是在没有线程被阻塞的情况下实现变量的同步,所以也叫非阻塞同步(Non-blocking Synchronization)

  • CAS 中涉及三个要素:

    • 需要读写的内存值 V
    • 进行比较的值 A
    • 拟写入的新值 B

当且仅当预期值 A 和内存值 V 相同时,将内存值 V 修改为 B,否则什么都不做。

1.4 乐观锁的缺点

  • ABA 问题

该说的是,如果一个变量第一次读取的值是 A,准备好需要对 A 进行写操作的时候,发现值还是 A。那么这种情况下,能认为 A 的值没有被改变过吗?

可以是由 A -> B -> A 的这种情况,但是它只相信它看到的,因为第一次读取的值和目前的值相等,所以它就觉得没有变过。

  • 循环开销大

我们知道乐观锁在进行写操作的时候会判断是否能够写入成功,如果写入不成功将触发等待 -> 重试机制,进行等待重试的锁,它不适用于长期获取不到锁的情况,而且,循环对于性能开销比较大。

1.5 两种锁的使用场景

  1. 一般来说,悲观锁不仅会对写操作加锁,还会对读操作加锁,所以它的性能比较低,但是写比较多的情况下需要使用悲观锁;
  2. 相对而言,乐观锁用于读多写少的情况,即很少发生冲突的场景,这样可以省去锁的开销,增加系统的吞吐量。

二、公平锁与非公平锁

根据线程获取锁的抢占机制,锁可以分为公平锁和非公平锁。

  • 公平锁:表示线程获取锁的顺序是按照线程请求锁的时间早晚来决定的,也就是最早请求锁的线程将最早获取到锁;

  • 非公平锁:在运行时闯入,也就是先来不一定先得。

在没有公平性需求的前提下尽量使用非公平锁,因为公平锁会带来性能开销。

三、独占锁和共享锁

根据锁只能被单个线程持有还是能被多个线程共同持有,锁可以分为独占锁和共享锁。

  • 独占锁:保证任何时候都只有一个线程能得到锁
  • 共享锁:可以同时由多个线程持有,它允许一个资源可以被多线程同时进行操作。

四、可重入锁和不可重入锁

当一个线程要获取一个被其他线程持有的独占锁时,该线程会被阻塞,那么当一个线程再次获取它自己已经获取的锁时是否会被阻塞呢?

  1. 如果不被阻塞,那么我们说该锁是可重入的,也就是只要该线程获取了该锁,那么可以无限次数(严格来说是有限次数)地进入被该锁锁住的代码。
  2. 反之,如果被阻塞,说明该锁只能获取一次,即为不可重入锁。

可重入锁的原理

  1. 在锁内部维护一个线程标示,用来标示该锁目前被哪个线程占用,然后关联一个计数器。一开始计数器值为 0,说明该锁没有被任何线程占用;
  2. 当一个线程获取了该锁时,计数器的值会变成 1,这时其他线程再来获取该锁时会发现锁的所有者不是自己而被阻塞挂起;
  3. 但是当获取了该锁的线程再次获取锁时发现锁拥有者是自己,就会把计数器值加 + 1,当释放锁后计数器值 - 1
  4. 当计数器值为 0 时,锁里面的线程标示被重置为 null,这时候被阻塞的线程会被唤醒来竞争获取该锁。

五、其他锁

5.1 自旋锁

自旋锁是指当一个线程在获取锁的时候,如果锁已经被其它线程获取,那么该线程将循环等待,然后不断的判断锁是否能够被成功获取,直到获取到锁才会退出循环。

5.2 偏向锁/轻量级锁/重量级锁

JVM 为了提高锁的获取与释放效率,而做了以下优化

  • 分化锁的状态:

    • 无锁状态
    • 偏向锁状态
    • 轻量级锁状态
    • 重量级锁状态

说明:

  1. 锁的状态是通过对象监视器在对象头中的字段来表明的。
  2. 四种状态会随着竞争的情况逐渐升级,而且是不可逆的过程,即不可降级。
  • 偏向锁

    偏向锁是指一段同步代码一直被一个线程所访问,那么该线程会自动获取锁。降低获取锁的代价。

  • 轻量级锁

    轻量级锁是指当锁是偏向锁的时候,被另一个线程所访问,偏向锁就会升级为轻量级锁,其他线程会通过自旋的形式尝试获取锁,不会阻塞,提高性能。

  • 重量级锁

    重量级锁是指当锁为轻量级锁的时候,另一个线程虽然是自旋,但自旋不会一直持续下去,当自旋一定次数的时候,还没有获取到锁,就会进入阻塞,该锁膨胀为重量级锁。重量级锁会让其他申请的线程进入阻塞,性能降低。

5.3分段锁

分段锁其实是一种锁的设计,并不是具体的一种锁

我们一般有三种方式降低锁的竞争程度

  1. 减少锁的持有时间;
  2. 降低锁的请求频率;
  3. 使用带有协调机制的独占锁,这些机制允许更高的并发性。

在某些情况下我们可以将锁分解技术进一步扩展为一组独立对象上的锁进行分解,这成为分段锁。

例如:对于ConcurrentHashMap而言,其并发的实现就是通过分段锁的形式来实现高效的并发操作。

更多 Java 笔记,详见【Java 知识笔记本】,欢迎提供想法建议。

参考文章

Van wechat
最新文章,欢迎您扫一扫上面的微信公众号!
-------------    本文结束  感谢您的阅读    -------------