- 常见的锁策略
- 悲观锁
- 乐观锁
- 读写锁
- 轻量级锁、重量级锁
- 自旋锁
- 公平锁和非公平锁
- 可重入锁 vs 不可重入锁
- synchronized是什么锁呢?
常见的锁策略
锁策略不仅仅限制于Java;其它锁相关的也是会涉及这些策略;这些特性主要是在实现锁的时候运用的。虽然我们的工作可能就是把轮子拼装成一辆车;不涉及造一个轮子。但是背后的原理学习还是有利于我们对知识的理解和深入;能初步的认识。
悲观锁
定义:假设在并发环境中都会发生冲突,因此在访问共享资源之前先获取锁。统统阻塞;得先获取锁。
乐观锁
定义:假设在并发环境中都不会发生冲突,因此直接进行操作,而在更新时检查是否有其他线程干扰。如果发现并发冲突了,则让返回用户错误的信息,让用户决定如何去做。
1:悲观与乐观区别?
预测锁竞争激励程度;然后背后的处理是不同的。
2:乐观锁如何检查是否有其它线程干扰呢?
乐观锁处理:是在进行操作(读或写)时不显式加锁,而是通过某种机制(如版本号、时间戳、CAS操作等)在更新时检查是否有其他线程干扰。
假设:设我们需要多线程修改 “用户账户余额”;设当前余额为 100. 引入一个版本号 version, 初始值为 1. 规定 “提交版本必须大于记录当前版本才能执行更新余额”
读写锁
区分读操作和写操作,允许多个线程同时读取共享资源,但只有一个线程可以进行写操作。
1:为什么能对读写锁区分呢?
一个线程对于数据的访问, 主要存在两种操作: 读数据和写数据.
两个线程都只是读一个数据, 此时并没有线程安全问题. 直接并发的读取即可.
两个线程都要写一个数据, 有线程安全问题.
一个线程读另外一个线程写, 也有线程安全问题.
2:Java 标准库提供了 ReentrantReadWriteLock 类, 实现了读写锁.
ReentrantReadWriteLock.ReadLock 类表示一个读锁. 这个对象提供了 lock / unlock 方法进行
加锁解锁.
ReentrantReadWriteLock.WriteLock 类表示一个写锁. 这个对象也提供了 lock / unlock 方法进
行加锁解锁.
3:读锁跟写锁的区别
-
读锁(共享锁):
a:多线程读取时不互斥:多个线程可以同时获得读锁,同时读取共享资源,不互斥。
b:适用于读多写少场景:在读多写少的场景中,多个线程可以同时读取共享资源,提高并发性能。
c:阻塞写操作:当一个线程持有读锁时,其他线程若想获得写锁,就会被阻塞,直到所有的读锁都被释放。
d:不阻塞读操作:读锁不会阻塞其他读操作,因为多个线程可以同时持有读锁。 -
写锁(排它锁):
a:互斥访问:写锁是独占锁,同一时刻只允许一个线程持有写锁,用于保护对共享资源的写操作。
b:适用于写多读少场景:在写多读少的场景中,写锁能够确保只有一个线程能够修改共享资源,保证数据的一致性。
c:阻塞其他写操作和读操作:当一个线程持有写锁时,其他线程若想获得读锁或写锁,都会被阻塞,直到写锁被释放。
d:不允许陈旧的读操作:写锁的引入确保在写操作时不会有其他读或写操作,防止产生陈旧的读操作结果。
轻量级锁、重量级锁
因为锁是操作系统对CPU的原子指令实现mutex 互斥锁;我们JVM 基于操作系统提供的互斥锁, 实现了 synchronized 和 ReentrantLock 等关键字和类.;但是基于mutex 进行封装;但是不限于这些工作。
轻量级锁:轻量级锁是为了在多线程竞争不激烈的情况下提高性能而设计的。不太依赖操作系统的mutex;不容易引发线程的调度;线程状态切换也是要开销的。
重量级锁:重量级锁是为了在多线程竞争激烈的情况下保证数据的正确性而设计的。重度依赖操作系统mutex;很容易引发线程的调度;线程状态切换也是要开销的。
自旋锁
按之前的方式,线程在抢锁失败后进入阻塞状态,放弃 CPU,需要过很久才能再次被调度;大部分情况下,虽然当前抢锁失败,但过不了很久,锁就会被释放。没必要就放弃 CPU. 这个时候就可以使用自旋锁来处理这样的问题。
自旋锁一种忙等的状态;如果获取锁失败, 立即再尝试获取锁, 无限循环, 直到获取到锁为止. 第一次获取锁失败, 第二次的尝试会在极短的时间内到来.;一旦锁被其他线程释放, 就能短时间获取到锁.
伪代码:
自旋锁是一种典型的轻量级锁的实现方式:
优点: 没有放弃 CPU, 不涉及线程阻塞和调度, 一旦锁被释放, 就能第一时间获取到锁.
缺点: 如果锁被其他线程持有的时间比较久, 那么就会持续的消耗 CPU 资源. (而挂起等待的时候是
不消耗 CPU 的)
公平锁和非公平锁
假设三个线程 A, B, C.。A 先尝试获取锁, 获取成功. 然后 B 再尝试获取锁, 获取失败, 阻塞等待; 然后
C 也尝试获取锁, C 也获取失败, 也阻塞等待.
公平锁: 遵守 “先来后到”. B 比 C 先来的. 当 A 释放锁的之后, B 就能先于 C 获取到锁.
非公平锁: 不遵守 “先来后到”. B 和 C 都有可能获取到锁.
操作系统调度上随机的;可以看做默认是非公平锁;想实现公平锁, 就需要依赖额外的数据结构, 来记录线程们的先后顺序.
可重入锁 vs 不可重入锁
可重入锁的字面意思是“可以重新进入的锁”,即允许同一个线程多次获取同一把锁。
Java里只要以Reentrant开头命名的锁都是可重入锁,而且JDK提供的所有现成的Lock实现类,包括
synchronized关键字锁都是可重入的。而 Linux 系统提供的 mutex 是不可重入锁
synchronized是什么锁呢?
synchronized是悲观锁也是乐观锁:
初始使用乐观锁策略. 当发现锁竞争比较频繁的时候, 就会自动切换成悲观锁策略.
Synchronized 不是读写锁
synchronized是轻量级锁也是重量级锁;如果锁冲突比较严重, 就会变成重量级锁。和悲观乐观锁对锁的预测程度是不同。
synchronized 是非公平锁:基于操作系统的 mutex实现
synchronized 是可重入锁:通过记录持有锁的线程身份和计数器记录加锁次数。 如果发现当前加锁
的线程就是持有锁的线程, 则直接计数自增。要所有的锁释放才能被别人获取到;即计数器为0。