基础知识之一:锁的类型
锁从宏观上分类,分为悲观锁与乐观锁。
乐观锁
乐观锁是一种乐观思想,即认为读多写少,遇到并发写的可能性低,每次去拿数据的时候都认为别人不会修改,所以不会上锁,但是在更新的时候会判断一下在此期间别人有没有去更新这个数据,采取在写时先读出当前版本号,然后加锁操作(比较跟上一次的版本号,如果一样则更新),如果失败则要重复读-比较-写的操作。
java中的乐观锁基本都是通过CAS操作实现的,CAS是一种更新的原子操作,比较当前值跟传入值是否一样,一样则更新,否则失败。
悲观锁
悲观锁是就是悲观思想,即认为写多,遇到并发写的可能性高,每次在读写数据的时候都会上锁,这样别人想读写这个数据就会block直到拿到锁。java中的悲观锁就是Synchronized,AQS框架下的锁则是先尝试cas乐观锁去获取锁,获取不到,才会转换为悲观锁,如RetreenLock。
基础知识之二:java线程阻塞的代价
java的线程是映射到操作系统原生线程之上的,如果要阻塞或唤醒一个线程就需要操作系统介入,需要在户态与核心态之间切换,这种切换会消耗大量的系统资源,因为用户态与内核态都有各自专用的内存空间,专用的寄存器等,用户态切换至内核态需要传递给许多变量、参数给内核,内核也需要保护好用户态在切换时的一些寄存器值、变量等,以便内核态调用结束后切换回用户态继续工作。
1.如果线程状态切换是一个高频操作时,这将会消耗很多CPU处理时间;
2.如果对于那些需要同步的简单的代码块,获取锁挂起操作消耗的时间比用户代码执行的时间还要长,这种同步策略显然非常糟糕的。
synchronized会导致争用不到锁的线程进入阻塞状态,所以说它是java语言中一个重量级的同步操纵,被称为重量级锁,为了缓解上述性能问题,JVM从1.5开始,引入了轻量锁与偏向锁,默认启用了自旋锁,他们都属于乐观锁。
明确java线程切换的代价,是理解java中各种锁的优缺点的基础之一。
markword
在介绍java锁之前,先说下什么是markword,markword是java对象数据结构中的一部分,要详细了解java对象的结构可以点击这里,这里只做markword的详细介绍,因为对象的markword和java各种类型的锁密切相关;
markword数据的长度在32位和64位的虚拟机(未开启压缩指针)中分别为32bit和64bit,它的最后2bit是锁状态标志位,用来标记当前对象的状态,对象的所处的状态,决定了markword存储的内容,如下表所示:
状态 | 标志位 | 存储内容 | 特点 |
---|---|---|---|
未锁定 | 01 | 对象哈希码、对象分代年龄 | |
轻量级锁定 | 00 | 指向锁记录的指针 | 竞争线程数量少,锁持有时间短,能通过自旋获取锁 |
膨胀(重量级锁定) | 10 | 执行重量级锁定的指针 | 竞争线程数量多,锁持有时间长 |
GC标记 | 11 | 空(不需要记录信息) | |
可偏向 | 01 | 偏向线程ID、偏向时间戳、对象分代年龄 | 只有一个线程在重复请求锁 |
32位虚拟机在不同状态下markword结构如下图所示:
前面提到了java的4种锁,他们分别是重量级锁、自旋锁、轻量级锁和偏向锁,
不同的锁有不同特点,每种锁只有在其特定的场景下,才会有出色的表现,java中没有哪种锁能够在所有情况下都能有出色的效率,引入这么多锁的原因就是为了应对不同的情况;
前面讲到了重量级锁是悲观锁的一种,自旋锁、轻量级锁与偏向锁属于乐观锁,所以现在你就能够大致理解了他们的适用范围,但是具体如何使用这几种锁呢,就要看后面的具体分析他们的特性;
轻量级锁
加锁过程:发现MarkWord已经偏向其他线程,并且锁还没有释放(持有偏向锁的线程处于同步块?),此时把偏向锁升级成轻量级锁。在线程的栈帧里创建一条锁记录(LockRecord),把MarkWord拷贝进去(DisplayedMarkWord),然后修改MarkWord指向锁记录。接着自旋。如果自旋之后还不能获取锁,则膨胀成重量级锁,即修改MarkWord指向monitor对象(互斥变量?)。
轻量级锁的加锁过程:发现MarkWord已经偏向其他线程,但是锁已经释放,则撤销偏向锁,把MarkWord修改成无锁,接着获取偏向锁。(疑问:偏向锁不能自己解锁?)
轻量级锁的解锁过程:通过CAS操作把MarkWord改写成DisplacedMarkWord,也就是恢复到偏向锁。如果修改失败则说明已经是重量级锁了。
Monitor的同步队列包括:
竞争队列ContentionList: 等待锁的所有线程。是一个双向列表,新线程(并发)通过CAS操作放入头部,锁的Owner(单线程)从尾部取线程放入EntryList
候选队列EntryList: Owner把一个线程移入OnDeck
OnDeck: 同一时刻只有一个线程在竞争锁
Owner: 持有锁的线程
WaitSet: 调用了wait()的线程
偏向锁升级成轻量级锁
- 线程A请求锁,发现对象的MarkWord是无锁状态,尝试CAS设置为偏向锁状态,并写入线程A的ID
- 线程B也来请求锁,发现MarkWord已经是偏向锁状态,检查线程A是否存在
- 如果此时线程A已经不存在
- 将MarkWord设置为无锁状态(?)
- 尝试CAS设置为偏向锁状态,并写入线程B的ID
- 如果此时线程A存在
- 暂停线程A
- 在线程A的栈帧中创建锁记录(Lock Record)
- 将MarkWord复制到该锁记录中
- 尝试CAS更新MarkWord,指向该锁记录
- 更新锁记录的Owner指向MarkWord(?)
- 设置MarkWord为轻量级锁状态
- 此时MarkWord与DisplacedMarkWord存储了相同的内容(?)
- 继续执行线程A
- 线程B自旋来获取锁
轻量级锁膨胀成重量级锁
- 线程A栈帧的锁记录已经复制了MarkWord,并且MarkWord指向了该锁记录
- 线程B来请求锁,发现MarkWord已经是轻量级锁,尝试自旋(?)
- 线程B自旋之后还是获取不到锁(?)
- 更新MarkWord,指向重量级锁(Mutex Lock)(?)
- 设置MarkWord为重量级锁状态
- 阻塞线程B
- 线程A尝试CAS用DisplacedMarkWord替换当前的MarkWord,CAS失败
- 释放锁
- 唤醒阻塞的线程
作者:金舜径
链接:https://www.jianshu.com/p/c2089d096552
来源:简书
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。