Java
java.util
Arrays
HashSet
TreeSet
Deque
ArrayDeque
ArrayList
LinkedBlockingDeque
Map
HashMap
HashTable
TreeMap
LinkedHashMap
ComputeIfAbsent 在jdk8下的死锁场景
synchronized的锁升级过程
Volatile 关键字
redis 中的Lua脚本
AQS - 从干饭角度解析
ConcurrentHashMap
本文档使用 MrDoc 发布
-
+
首页
synchronized的锁升级过程
## 1.用法 synchronized可用来给对象和方法或者代码块加锁,当它锁定一个方法或者一个代码块的时候,同一时刻最多只有一个线程执行这段代码。 synchronized关键字经过Javac编译后,会在同步块的前后分别形成monitorenter和monitorexit两个字节码指令,参数为一个reference类型的参数变量。锁的java对象根据下面规则判断: - 作用于实例方法,当前实例加锁,进入同步代码前要获得当前实例的锁; - 作用于静态方法,当前类加锁,进去同步代码前要获得当前类对象的锁; - 作用于代码块,对括号里配置的对象加锁。 ## 2.实现原理 synchronized用的锁存在Java对象头里,Java对象头里的Mark Word默认存储对象的HashCode、分代年龄和锁标记位。在运行期间,Mark Word里存储的数据会随着锁标志位的变化而变化。32位JVM的Mark Word可能变化存储为以下5种数据:  锁一共有四种状态,级别从低到高依次是:无锁状态、偏向锁状态、轻量级锁状态和重量级锁状态,这几个状态随着竞争情况逐渐升级。为了提高获得锁和释放锁的效率,锁可以升级但不能降级,意味着偏向锁升级为轻量级锁后不能降级为偏向锁。 ### 2.1 偏向锁 当我们创建一个对象时,该对象的部分Markword关键数据如下。 | bitFiled | 是否偏向锁 | 锁标志 | | --- | --- | -- | | hash| 0 | 1 | 从图中可以看出,偏向锁的标志位是“01”,状态是“0”,表示该对象还没有被加上偏向锁。(“1”是表示被加上偏向锁)。该对象被创建出来的那一刻,就有了偏向锁的标志位,这也说明了所有对象都是可偏向的,但所有对象的状态都为“0”,也同时说明所有被创建的对象的偏向锁并没有生效。 不过,当线程执行到临界区(critical section)时,此时会利用CAS(Compare and Swap)操作,将线程ID插入到Markword中,同时修改偏向锁的标志位。 > 所谓临界区,就是只允许一个线程进去执行操作的区域,即同步代码块。CAS是一个原子性操作 此时的Mark word的结构信息如下: | bitFiled | | 是否偏向锁 | 锁标志 | | --- | -- |--- | -- | | threadId| epoch| 1 | 01 | 此时偏向锁的状态为“1”,说明对象的偏向锁生效了,同时也可以看到,哪个线程获得了该对象的锁。 偏向锁是jdk1.6引入的一项锁优化,其中的“偏”是偏心的偏。它的意思就是说,这个锁会偏向于第一个获得它的线程,在接下来的执行过程中,假如该锁没有被其他线程所获取,没有其他线程来竞争该锁,那么持有偏向锁的线程将永远不需要进行同步操作。也就是说:在此线程之后的执行过程中,如果再次进入或者退出同一段同步块代码,并不再需要去进行**加锁**或者**解锁**操作,而是会做以下的步骤: 1. Load-and-test,也就是简单判断一下当前线程id是否与Markword当中的线程id是否一致. 2. 如果一致,则说明此线程已经成功获得了锁,继续执行下面的代码. 3. 如果不一致,则要检查一下对象是否还是可偏向,即“是否偏向锁”标志位的值。 4. 如果还未偏向,则利用CAS操作来竞争锁,也即是第一次获取锁时的操作。 **释放锁** 偏向锁的释放采用了一种只有竞争才会释放锁的机制,线程是不会主动去释放偏向锁,需要等待其他线程来竞争。偏向锁的撤销需要等待全局安全点(安全点市处理器不分配时间,这个特殊的位置保存了线程上下文的全部信息)。其步骤如下: 1. 暂停拥有偏向锁的线程,判断锁对象石是否还处于被锁定状态; 2. 撤销偏向锁,恢复到无锁状态或者轻量级锁的状态; 3. 安全点会导致stw(stop the word),导致性能下降,这种情况下应当禁用; 4. 查看停顿–安全点停顿日志 要查看安全点停顿,可以打开安全点日志,通过设置JVM参数 -XX:+PrintGCApplicationStoppedTime 会打出系统停止的时间, 添加-XX:+PrintSafepointStatistics -XX:PrintSafepointStatisticsCount=1 这两个参数会打印出详细信息,可以查看到使用偏向锁导致的停顿,时间非常短暂,但是争用严重的情况下,停顿次数也会非常多; **注意:安全点日志不能一直打开**: 安全点日志默认输出到stdout,一是stdout日志的整洁性,二是stdout所重定向的文件如果不在/dev/shm,可能被锁。 对于一些很短的停顿,比如取消偏向锁,打印的消耗比停顿本身还大。 安全点日志是在安全点内打印的,本身加大了安全点的停顿时间。 所以安全日志应该只在问题排查时打开。 如果在生产系统上要打开,再再增加下面四个参数: -XX:+UnlockDiagnosticVMOptions -XX: -DisplayVMOutput -XX:+LogVMOutput -XX:LogFile=/dev/shm/vm.log 打开Diagnostic(只是开放了更多的flag可选,不会主动激活某个flag),关掉输出VM日志到stdout,输出到独立文件,/dev/shm目录(内存文件系统)。 此日志分三部分: 第一部分是时间戳,VM Operation的类型 第二部分是线程概况,被中括号括起来 | total | initially_running |wait_to_block| | --- | --- | -- | | 安全点里的总线程数 |安全点时开始时正在运行状态的线程数 |在VM Operation开始前需要等待其暂停的线程数 | 第三部分是到达安全点时的各个阶段以及执行操作所花的时间,其中最重要的是vmop | spin | block |sync|cleanup|vmop| | --- | --- | -- | -- | -- | | 等待线程响应safepoint号召的时间 |暂停所有线程所用的时间 |等于 spin+block,判断安全点耗时 |清理所用时间|真正执行VM Operation的时间| 可见,那些很多但又很短的安全点,全都是RevokeBias, 高并发的应用会禁用掉偏向锁。 **jvm开启/关闭偏向锁** 开启偏向锁:-XX:+UseBiasedLocking -XX:BiasedLockingStartupDelay=0 关闭偏向锁:-XX:-UseBiasedLocking ### 2.2 轻量级锁 轻量级锁的目标是,减少无实际竞争情况下,使用重量级锁产生的性能消耗,包括系统调用引起的内核态与用户态切换、线程阻塞造成的线程切换等。 顾名思义,轻量级锁是相对于重量级锁而言的。使用轻量级锁时,不需要申请互斥量,会创建一个新的栈帧Lock Record(Lock Record:JVM检测到当前对象是无锁状态,则会在当前线程的栈帧中创建一个名为LOCKRECOD表空间用于拷贝Mark word中的数据),然后将Mark Word中的部分字节用CAS的方式更新指向该栈帧。如果更新成功,则轻量级锁获取成功,记录锁状态为轻量级锁;否则,说明已经有线程获得了轻量级锁,目前发生了锁竞争(不适合继续使用轻量级锁),接下来膨胀为重量级锁。 当然,由于轻量级锁天然瞄准不存在锁竞争的场景,如果存在锁竞争但不激烈,仍然可以用自旋锁优化,自旋失败后再膨胀为重量级锁。 自旋锁(while不断尝试获取锁,在竞争不频繁且锁存活较短时可用,可以避免内核态和用户态的切换)的目标是降低线程切换的成本。如果锁竞争激烈,我们不得不依赖于重量级锁,让竞争失败的线程阻塞;如果完全没有实际的锁竞争,那么申请重量级锁都是浪费的。 轻量级锁的缺点:同自旋锁相似:如果锁竞争激烈,那么轻量级将很快膨胀为重量级锁,那么维持轻量级锁的过程就成了浪费。 ### 2.3 重量级锁 轻量级锁膨胀之后,就升级为重量级锁了。重量级锁是依赖对象内部的monitor锁来实现的,而monitor又依赖操作系统的MutexLock(互斥锁)来实现的,所以重量级锁也被成为互斥锁。 当轻量级所经过锁撤销等步骤升级为重量级锁之后,它的Markword部分数据大体如下 | bitFiled | 锁标志 | | --- | -- | | 指向信号量的指针| 10 | 为什么说重量级锁开销大呢 主要是,当系统检查到锁是重量级锁之后,会把等待想要获得锁的线程进行阻塞,被阻塞的线程不会消耗cup。但是阻塞或者唤醒一个线程时,都需要操作系统来帮忙,这就需要从用户态转换到内核态,而转换状态是需要消耗很多时间的,有可能比用户执行代码的时间还要长。 这就是说为什么重量级线程开销很大的。 互斥锁(重量级锁)也称为阻塞同步、悲观锁 ## 3. 总结 偏向所锁,轻量级锁都是乐观锁,重量级锁是悲观锁。 一个对象刚开始实例化的时候,没有任何线程来访问它的时候。它是可偏向的,意味着,它现在认为只可能有一个线程来访问它,所以当第一个 线程来访问它的时候,它会偏向这个线程,此时,**对象持有偏向锁**。偏向第一个线程,这个线程在修改对象头成为偏向锁的时候使用CAS操作,并将 对象头中的ThreadID改成自己的ID,之后再次访问这个对象时,只需要对比ID,不需要再使用CAS在进行操作。 一旦有第二个线程访问这个对象,因为偏向锁不会主动释放,所以第二个线程可以看到对象时偏向状态,这时表明在这个对象上已经存在竞争了,检查原来持有该对象锁的线程是否依然存活,如果挂了,则可以将对象变为无锁状态,然后重新偏向新的线程,如果原来的线程依然存活,则马上执行那个线程的操作栈,检查该对象的使用情况,如果仍然需要持有偏向锁,则**偏向锁升级为轻量级锁的**。如果不存在使用了,则可以将对象回复成无锁状态,然后重新偏向。 轻量级锁认为竞争存在,但是竞争的程度很轻,一般两个线程对于同一个锁的操作都会错开,或者说稍微等待一下(自旋),另一个线程就会释放锁。但是当自旋超过一定的次数,或者一个线程在持有锁,一个在自旋,又有第三个来访时,**轻量级锁膨胀为重量级锁**,重量级锁使除了拥有锁的线程以外的线程都阻塞,防止CPU空转 作者:奋斗小周 链接:https://www.jianshu.com/p/88631590b1d9 著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
寒烟濡雨
2021年9月6日 13:13
转发文档
收藏文档
上一篇
下一篇
手机扫码
复制链接
手机扫一扫转发分享
复制链接
关于 MrDoc
觅思文档MrDoc
是
州的先生
开发并开源的在线文档系统,其适合作为个人和小型团队的云笔记、文档和知识库管理工具。
如果觅思文档给你或你的团队带来了帮助,欢迎对作者进行一些打赏捐助,这将有力支持作者持续投入精力更新和维护觅思文档,感谢你的捐助!
>>>捐助鸣谢列表
微信
支付宝
QQ
PayPal
Markdown文件
分享
链接
类型
密码
更新密码