自学内容网 自学内容网

并发编程中常见的锁策略

目录

一. 悲观锁和乐观锁

二. 重量级锁和轻量级锁

三. 挂起等待锁和自旋锁

四. 公平锁和非公平锁

五. 可重入锁和不可重入锁

六. 读写锁

七. synchronized

八. 锁消除

九. 锁粗化


一. 悲观锁和乐观锁

1. 乐观锁: 乐观锁 在加锁时, 假设出现锁冲突的概率不大 --> 接下来围绕加锁做的工作就更少.

2. 悲观锁: 悲观锁 在加锁时, 假设出现锁冲突的概率很大 --> 接下来围绕加锁做的工作就更多.

[注]: synchronized这个锁是"自适应锁". 它在初始情况下是乐观的, 预估出现锁冲突的概率不大. 但是会统计锁冲突的次数, 当所冲突的次数达到一定值之后, 就会从乐观锁转变为悲观锁.

二. 重量级锁和轻量级锁

1. 重量级锁: 加锁的开销比较大, 围绕加锁要做的工作更多.

        (一般来说, 悲观锁都是重量级的).

2. 轻量级锁: 加锁的开销比较小, 围绕加锁要做的工作更少.

        (一般来说, 乐观锁都是轻量级的).

三. 挂起等待锁和自旋锁

1. 挂起等待锁: 挂起等待锁 就是悲观锁(重量级锁)的一种典型实现.

挂起等待锁采用的等待策略是 "挂起等待", 等待过程中释放CPU资源, 这样的话就不用一直占用CPU等待锁资源, 可以让出CPU资源做别的事情了. 如果锁资源释放, 会以某种方式通知该线程.

2. 自旋锁: 自旋锁 就是乐观锁(轻量级锁)的一种典型实现.

自旋锁采用的等待策略是 "忙等", 等待过程中不会释放CPU资源, 等待过程中一直不停地检测锁是否被释放, 如果释放, 就有机会立即获取锁资源.

四. 公平锁和非公平锁

1. 公平锁: 公平锁确保 锁的获取是按照线程请求的顺序进行的. (即: 最先请求的线程会最先获取到锁).

2. 非公平锁: 非公平锁不能保证 锁的获取是按照线程锁请求的顺序进行的. (当锁被释放时, 任何线程都有机会获得锁, 即使是刚开始尝试获取锁的线程).

[注]: synchronized就属于非公平锁, 当synchronized的锁资源释放后, 等待锁资源的n个线程就会重新竞争, 下一个是哪个线程拿到锁是不确定的.

五. 可重入锁和不可重入锁

1. 可重入锁: 可重入锁是指同一个线程可以多次获取同一把锁. 如果某线程已持有锁, 那么当该线程再次尝试获取该锁资源时 (再次进入由这个锁保护的代码块) , 不会发生阻塞.

(可重入锁可以防止死锁的发生,因为同一个线程可以重复获取已经持有的锁)

2. 不可重入锁: 不可重入锁是指如果某线程已经持有锁, 那么它不能再次获取这个锁, 否则就会导致死锁.

java中也对可重入锁进行了封装. java中用 ReentrantLock 这个类来实现可重入锁.

3. ReentrantLock 和 synchronized的区别

(1) synchronized 是关键字, 而ReentrantLock 是java标准库中的一个类.

(2) synchronized 通过代码块加锁解锁, 而ReentrantLock通过 lock() 和 unlock() 实现加锁解锁.

(3) synchronized 没有 try-lock 这样的锁风格, 当加锁失败的时候, 就会阻塞等待, 等待锁资源释放;  而ReentrantLock提供了 try-lock 这样的锁风格, 当加锁失败的时候, 不会阻塞等待, 而是直接返回一个返回值, 通过返回值来表示加锁成功还是失败.

 (3) synchronized 一定是非公平锁, 无法修改;  而 ReentrantLock 默认为非公平锁, 但是可以通过给构造方法传入参数来把 ReentrantLock 设定成公平锁.

(4) synchronized 的等待和唤醒都是通过wait() -- notify() 来完成的;  而ReentrantLock提供了功能更强的"等待--通知"机制, 基于Condition类实现. 

六. 读写锁

读写锁是一种用于解决多线程环境中读操作和写操作之间冲突的锁机制. 读写锁可以提高并发程序的性能, 尤其是在读操作远多于写操作的场景中.

1. 分类:

(1) 读锁: 多个线程可以同时持有读锁.

(2) 写锁: 写锁是"排他"的, 即任何时候只能有一个线程持有写锁, 并且在此期间不能有其他线程进行读操作或者写操作.

2. 特点:

(1) 读读共享: 多个线程可以同时获取读锁.

(2) 读写互斥: 读操作和写操作不能同时进行. (如果某线程持有写锁, 那么其他线程无法获取读锁或者写锁)

(3) 写写互斥: 写操作不能同时进行. (如果某线程持有写锁, 那么其他线程不能获取写锁)

3. 实现方式

java中,  ReadWriteLock 接口 它的实现类 ReentrantReadWriteLock 提供了读写锁的功能.

import java.util.concurrent.locks.ReadWriteLock;
import java.util.concurrent.locks.ReentrantReadWriteLock;
public class Demo33 {

    public class ReadWriteLockExample {
        private final ReadWriteLock readWriteLock = new ReentrantReadWriteLock(); //创建一个读写锁对象
        private int data = 0; // 共享数据

        // 读操作
        public int read() {
            readWriteLock.readLock().lock(); //调用读锁
            try {
                // 执行读操作
                return data;
            } finally {
                readWriteLock.readLock().unlock();
            }
        }

        // 写操作
        public void write(int value) {
            readWriteLock.writeLock().lock(); //调用写锁
            try {
                // 执行写操作
                data = value;
            } finally {
                readWriteLock.writeLock().unlock();
            }
        }
    }

}

上述代码中, read操作获取读锁, 允许多个线程同时获取数据; write操作获取写锁, 确保在写入或修改数据时没有其他线程进行读写操作.

七. synchronized

1. synchronized锁的特点

(1) 悲观乐观 --> 自适应

(2) 重量轻量 --> 自适应

(3) 挂起等待 / 自旋 --> 自适应

(4) 是非公平锁

(5) 是可重入锁

(6) 不是读写锁

2. synchronized的加锁过程

synchronized的加锁过程, 实际上是一个"锁升级"的过程.

使用synchronized加锁, 刚开始, synchronized会处于"偏向锁"的状态, 即只是 "做个标记" , 但不会真正加锁. 然后, 如果出现锁竞争的话,  "偏向锁" 会升级到 "轻量级锁",  之后, 线程进一步统计锁竞争的次数和频率, 当达到一定程度的时候, "轻量级锁" 就会升级到 "重量级锁".

(synchronized加锁过程:  无锁 --> 偏向锁 --> 轻量级锁 --> 重量级锁)

[注]: 偏向锁, 并不会真的加锁, 而只是"做一个标记", 标记的过程, 开销很小, 非常的轻量和高效.

八. 锁消除

"锁消除"机制 是编译器的自动优化策略. 比如我们写了一个带synchronized锁的代码, 编译器就会对我们加锁的代码做出判定, 如果判断得到这里没有必要加锁, 就会自动把这里的锁消除掉.

九. 锁粗化

在说锁粗化之前, 我们首先得明确一个概念: "锁的粒度".  -->  一个锁保护的代码范围越广, 那么这个锁的粒度就越粗;  一个锁保护的代码范围越小, 那么这个锁的粒度就越细.

那么锁的粗化, 就是把多个"细粒度"的锁, 合成粗粒度的锁. 当编译器检测到一系列连续的操作都在对同一个锁进行加锁和解锁时, 会将这些操作合并为一个更大的锁区域, 从而减少锁的开销.

锁粗化的优点: 减少了对锁的获取和释放次数, 降低了上下文切换和调度的开销, 减少了线程因频繁获取和释放锁而产生的竞争, 提高了程序的吞吐量。


原文地址:https://blog.csdn.net/2301_80313139/article/details/143709670

免责声明:本站文章内容转载自网络资源,如本站内容侵犯了原著者的合法权益,可联系本站删除。更多内容请关注自学内容网(zxcms.com)!