目录
[一. 悲观锁和乐观锁](#一. 悲观锁和乐观锁)
[二. 重量级锁和轻量级锁](#二. 重量级锁和轻量级锁)
[三. 挂起等待锁和自旋锁](#三. 挂起等待锁和自旋锁)
[四. 公平锁和非公平锁](#四. 公平锁和非公平锁)
[五. 可重入锁和不可重入锁](#五. 可重入锁和不可重入锁)
[六. 读写锁](#六. 读写锁)
[七. synchronized](#七. 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 提供了++读写锁的功能++.
java
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锁的代码 , 编译器就会对我们加锁的代码做出判定, 如果判断得到这里没有必要加锁, 就会自动把这里的锁消除掉.
九. 锁粗化
在说锁粗化之前, 我们首先得明确一个概念: "锁的粒度". --> 一个锁保护的代码范围越广 , 那么这个锁的粒度就越粗 ; 一个锁保护的代码范围越小 , 那么这个锁的粒度就越细 .
那么锁的粗化, 就是把多个"细粒度"的锁, 合成粗粒度的锁. 当编译器检测到*++一系列连续的操作都在对同一个锁进行加锁和解锁时, 会将这些操作合并为一个更大的锁区域, 从而减少锁的开销.++*
锁粗化的优点: 减少了对锁的获取和释放次数, 降低了上下文切换和调度的开销, 减少了线程因频繁获取和释放锁而产生的竞争, 提高了程序的吞吐量。