7.5 锁: 锁原语 CAS
1.锁原语 CAS(CompareAndSwap:比较并交换)
CAS(V,E,N)
V(Value)表示要更新的目标变量
E(Expected)表示预期值(操作前的原始值)
N(New)表示新值
解析:
步骤 1:Compare 比较:如果 V 目标变量值等于预期 E 值(Value==Expected)
步骤 2:Swap 交换:则将 V 预期值设置为新值 N;如果 V 值和 E 值不同,什么都不做。
CAS 是一种系统原语,原语的执行必须是连续的,在执行过程中不允许被中断。
2.Java 通过 CAS 原语在对象头中修改 Mark Word 实现加锁
|------------------------------------------------------------------------------------|---------
| Mard Word (64bits) | 锁状态
|------------------------------------------------------------------------------------|---------
| unused:25 | identity_hashcode:31 | unused:1 | age:4 |biased_lock:0 | lock:01 | 正常
|------------------------------------------------------------------------------------|----------
| thread:54 | epoch:2 | unused:1| age:4 |biased_lock:1 | lock:01 | 偏向锁
|-------------------------------------------------------------------------------------|---------
| ptr_to_lock_record:62 | lock:00 | 轻量级锁
|-------------------------------------------------------------------------------------|---------
| ptr_to_heavyweight_monitor:62 | lock:10 | 重量级锁
|-------------------------------------------------------------------------------------|----------
解析:synchronized: 被同步
1.单方法<==>synchronized(this): 线程要在当前 this 对象上被同步。方法本身变成临界区。
2.多个方法上使用 synchronized,多个方法都要在当前 this 实例对象上被同步
3.检查:先检查当前实例对象上的同步锁,是否已经被其他线程标记。
如果被标记,说明该同步锁已经被其他线程获取,当前线程被阻塞,无法进入当前方法,等待线程释放同步锁。
如果没有被标记,说明没有其他线程争用该同步锁,当前线程标记同步锁为当前线程所有,即可进入该方法。
示例: class A {
public synchronized void f(){ }
public synchronized void g(){ }
}
线程 1 执行 f,线程 2 执行 g, 线程 1 和线程 2 同时执行。
如果线程 1 首先标记当前实例 A 对象的为线程 1 所有,则线程 2 被阻塞,不能执行 g 函数。
lock:显式锁。锁创建临界区。线程在锁上保持同步。
CAS 加锁解析: CAS 原语 CAS(V,E,N)加锁: 场景:sychronized 请求同步。
CAS 操作是在 CPU 上执行的,不阻塞。
V:Mard word(64bits),内存栅栏圈起来的 64 个二进制位
E:读出的 64 比特位(期望的正常原始值)
N:加偏向锁的值。设置偏向锁标志位 biased_lock:1,写入线程 ID:thread:54.
线程 54 获取当前对象锁,其他线程 CAS 操作尝试抢占锁时,发现已经被抢占,不能进入临界区。
锁竞争: 但是其他线程要进入临界区,仍然是需要锁的。所以其他线程申请锁竞争,目的:得到这把锁。怎么得到这把锁呢?
如果当前对象已经加了偏向锁,但是其他线程还在竞争这把锁。在 Java 里面,就会锁升级。
偏向锁升级为轻量级锁:多个线程同时争夺偏向锁。偏向锁升级为轻量级锁。设置锁标记 lock:00,表示轻量级锁。
假设线程 2,期望线程 1 释放锁。线程 1 释放锁后,又被线程 3 抢占。线程 2 仍然没有得到锁。
执行了几次 CAS 后,仍然不能把自己的锁记录写入到 Mard Word 里面去。
CAS(V,E,N):写入轻量级锁;
V:Mard word(64bits),内存栅栏圈起来的 64 个二进制位
E:读出的 64 比特位(期望的偏向锁状态值)
N:指向锁记录(ptr_to_lock_record)值。设置锁标志位 lock:1(轻量级锁)
轻量级锁升级重量级锁:多个线程争夺轻量级锁。轻量级升级为重量级锁。设置锁标记 lock:10,表示重量级锁。
假设线程 2,期望线程 1 释放锁。线程 1 释放锁后,又被线程 3 抢占。线程 2 仍然没有得到轻量级锁。
执行了几次 CAS 后,仍然不能把自己的锁记录写入到 Mard Word 里面去。
CAS(V,E,N):写入重量级锁;
V:Mard word(64bits),内存栅栏圈起来的 64 个二进制位
E:读出的 64 比特位(期望的轻量级锁状态值)
N:指向重量级锁记录(ptr_to_heavyweight_monitor)值。
重量级锁:重量级====>线程会被阻塞。 对比:(CAS 操作在 CPU 上执行,不阻塞)
升级为重量级锁之后,不再进行 CAS 操作,释放 CPU。
ptr_to_heavyweight_monitor 值指向一个监视器(Monitor),监视器指向队列。
线程 2 进入阻塞状态,把自己的线程 ID 记录到监视器的队列中。等待其他线程释放锁,按照队列的方式排队等待锁。
监视器(Monitor)会按照队列一个一个唤醒线程。线程重新设置锁,进入临界区。
偏向锁:指一段同步代码一致被一个线程所访问,那么该线程会自动获取锁,降低获取锁的代价。
轻量级锁:指当锁是偏向锁时,被另一个线程所访问,偏向锁就会升级为轻量级锁,
其他线程会通过自旋(CAS)的形式尝试获取锁,不会阻塞,提高性能。
重量级锁:当锁是轻量级锁时,另一个线程虽然自旋,单自旋不会一直持续下去,当自旋到一定次数时,还没读取到锁,就会进入阻塞。
该锁膨胀为重量级锁,重量级锁会让其他申请的线程进入阻塞,性能降低。
3.多 CPU 情况下的锁
解析:CAS 操作。CAS(V,E,N). 示例解析 i++,初始值 i=0。
线程 1:读取 V 初始值,值为 0. “++”操作结果为 1,回写到主存。主存结果为 1.
线程 2:读取 V 初始值,值为 0. “++”操作结果为 1,回写到主存。主存结果为 1.
线程 3:读取 V 初始值,值为 0. “++”操作结果为 1,回写到主存。主存结果为 1.
丢失更新,怎么处理?
4.总线锁 VS 缓存锁
硬件或者操作系统支持:
总线锁:使用处理器的 LOCK#信号,当一个处理器在总线上输出此信号时,其他处理器的请求将被阻塞(影响性能),该处理器独占内存。
缓存锁:是指内存区域如果被缓存在处理器的缓存行中,并且在 LOCK 操作期间被锁定,那么当他执行锁操作回写到内存时,处理器不再总线上声言 LOCK#信号,而是修改内部的内存地址,并允许他的缓存一致性机制来保证操作的原子性,因为缓存一致性机制会阻止同时修改由两个以上处理器缓存的内存区域数据,当其他处理器回写已经锁定的缓存数据时,会使缓存行失效。
5.公平锁 VS 非公平锁
公平锁:多个线程按照申请锁的顺序来获取锁。重量级锁:是公平锁。
非公平锁:多个线程获取锁的顺序并不是按照申请锁的顺序,有可能后申请的线程比先申请的线程有限获取锁,可能会造成饥饿现象。
CAS 非公平锁。
锁升级:轻量级==>重量级。 非公平===>公平
6.可重入锁
某个线程已经多得锁,可以再次获取锁,而不会出现死锁。
class A {
public synchronized void f(){ g(); }
public synchronized void g(){ }
}
线程 1 执行 f,如果锁不可重入,g 函数需要获得锁,而等待线程 1 释放锁。出现自己等待自己的情况出现===>死锁。
g 需要的锁,与 f 函数所需要的的锁,是同一个锁,允许锁重入。避免死锁,提高锁性能。
7.独享锁/互斥锁 共享锁 读写锁
独享锁/互斥锁:该锁只能被一个线程所持有
共享锁:该锁可以被多个线程所持有。信号量,有共享数目限制,非无限共享。
读写锁:多个读线程之间并不互斥,而写线程则要求与任何线程互斥。
8.乐观锁 VS 悲观锁
悲观锁:对同一个数据的并发操作,一定是会发生修改的,哪怕没有修改,也会认为修改。因此对于同一个数据的并发操作,悲观的采用加锁形式,不加锁的并发操作一定会出问题。---先加锁,后操作。
乐观锁:对同一个数据的并发操作,是不会发生修改的。在更新数据的时候,检查是否已经被修改过,如果修改过,就放弃。---先不加锁,先处理,如果数据没有被修改,就修改成功;如果被修改了,丢弃当前修改结果,就检查数据版本,重试修改。
9.分段锁
分段锁目的:细化锁粒度,当操作不需要更新真个数组的时候,就仅仅针对数组的一段进行加锁操作。
JDK ConcurrentHashMap 是通过分段锁的形式来实现高效并发的。
10.自旋锁
自旋锁:尝试获取锁的线程不会立即阻塞,而是采用循环(CAS)的方式去尝试获取锁,好处:减少线程上下文切换的消耗,缺点:循环会消耗 CPU。
评论