乐观锁、悲观锁
相关文章参考:
概念
乐观锁
乐观锁不会上锁,只是在执行更新的时候判断一下在此期间别人是否修改了数据:如果别人修改了数据则放弃操作,否则执行操作。
乐观锁本身不会上锁。但是可以与加锁操作合作实现一些业务需求
悲观锁
悲观锁总是假设最坏的情况,认为共享资源每次被访问的时候就会出现问题(比如共享数据被修改),所以每次在获取资源操作的时候都会上锁,这样其他线程想拿到这个资源就会阻塞直到锁被上一个持有者释放。也就是说,共享资源每次只给一个线程使用,其它线程阻塞,用完后再把资源转让给其它线程。
实现
悲观锁的实现
像 Java 中synchronized和ReentrantLock等独占锁就是悲观锁思想的实现。
1 | public void performSynchronisedTask() { |
高并发的场景下,激烈的锁竞争会造成线程阻塞,大量阻塞线程会导致系统的上下文切换,增加系统的性能开销。并且,悲观锁还可能会存在死锁问题(线程获得锁的顺序不当时),影响代码的正常运行
乐观锁的实现
CAS算法
CAS 的全称是 Compare And Swap(比较与交换)
- CAS操作包括了3个操作数:
- 需要读写的内存位置(V)
- 进行比较的预期值(A)
- 拟写入的新值(B)
- 操作逻辑:如果内存位置V的值等于预期的A值,则将该位置更新为新值B,否则不进行任何操作。(通常情况CAS是自旋的,操作不成功会一直重试)
- CAS 是一个原子操作,底层依赖于一条 CPU 的原子指令。(其原子性由硬件层面保证)
示例(java7):1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43public class Test {
//value1:线程不安全
private static int value1 = 0;
//value2:使用乐观锁
private static AtomicInteger value2 = new AtomicInteger(0);
//value3:使用悲观锁
private static int value3 = 0;
private static synchronized void increaseValue3() {
value3++;
}
public static void main(String[] args) throws Exception {
//开启1000个线程,并执行自增操作
for (int i = 0; i < 1000; ++i) {
new Thread(new Runnable() {
public void run() {
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
value1++;
value2.getAndIncrement();
increaseValue3();
}
}).start();
}
//查看活跃线程 ,因守护线程的原因[基于工具问题windows:idea run 启动用 >2,debug 用>1]
while (Thread.activeCount() > 2) {
//Thread.currentThread().getThreadGroup().list();
Thread.yield();//让出cpu
}
//打印结果
Thread.sleep(1000);
System.out.println("线程不安全:" + value1);
System.out.println("乐观锁(AtomicInteger):" + value2);
System.out.println("悲观锁(synchronized):" + value3);
}
}因为版本太老,分析自行看第一条借鉴文章,总的来说就是乐观与悲观锁都保证了线程安全
版本号机制
- 版本号机制的基本思路是在数据中增加一个字段
version,表示该数据的版本号,每当数据被修改,版本号加1。 - 常见的思路就是当版本号已经被A更新而B访问原来的版本号,就不会进行操作
乐观锁和悲观锁优缺点和适用场景
- 功能限制 与悲观锁相比,乐观锁适用的场景受到了更多的限制,无论是CAS还是版本号机制。
- 例如,CAS只能保证单个变量操作的原子性,当涉及到多个变量时,CAS是无能为力的,而
synchronized则可以通过对整个代码块加锁来处理。 - 再比如版本号机制,如果
query的时候是针对表1,而update的时候是针对表2,也很难通过简单的版本号来实现乐观锁。
- 例如,CAS只能保证单个变量操作的原子性,当涉及到多个变量时,CAS是无能为力的,而
- 竞争激烈程度 如果悲观锁和乐观锁都可以使用,那么选择就要考虑竞争的激烈程度:
- 当竞争不激烈 (出现并发冲突的概率小)时,乐观锁更有优势,因为悲观锁会锁住代码块或数据,其他线程无法同时访问,影响并发,而且加锁和释放锁都需要消耗额外的资源。
- 当竞争激烈(出现并发冲突的概率大)时,悲观锁更有优势,因为乐观锁在执行更新时频繁失败,需要不断重试,浪费CPU资源。
CAS的一些缺点
ABA问题
- 假设有两个线程——线程1和线程2,两个线程按照顺序进行以下操作:
1
2
3
4(1)线程1读取内存中数据为A;
(2)线程2将该数据修改为B;
(3)线程2将该数据修改为A;
(4)线程1对数据进行CAS操作 - 在第(4)步中,由于内存中数据仍然为A,因此CAS操作成功,但实际上该数据已经被线程2修改过了。这就是ABA问题。
- 但是在某些场景下,ABA却会带来隐患,例如栈顶问题:一个栈的栈顶经过两次(或多次)变化又恢复了原值,但是栈可能已发生了变化。
- 方案:比较有效的方案是引入版本号,内存中的值每发生一次变化,版本号都+1
高竞争下的开销问题
- 在并发冲突概率大的高竞争环境下,如果CAS一直失败,会一直重试,CPU开销较大。
- 针对这个问题的一个思路是引入退出机制,如重试次数超过一定阈值后失败退出。
- 当然,更重要的是避免在高竞争环境下使用乐观锁。
功能限制
- CAS的功能是比较受限的,例如CAS只能保证单个变量(或者说单个内存值)操作的原子性,这意味着:
- 原子性不一定能保证线程安全,例如在Java中需要与
volatile配合来保证线程安全; - 当涉及到多个变量(内存值)时,CAS也无能为力
- 除此之外,CAS的实现需要硬件层面处理器的支持,在Java中普通用户无法直接使用,只能借助atomic包下的原子类使用,灵活性受到限制。
- 原子性不一定能保证线程安全,例如在Java中需要与
本博客所有文章除特别声明外,均采用 CC BY-NC-SA 4.0 许可协议。转载请注明来源 Blog of Sof!
评论






