锁升级(膨胀)过程
synchronize锁升级过程:jdk高版本之后对synchronize关键字进行了很多优化,其中一项就是锁升级,以前synchronize默认就是悲观锁,是在JVM层面上加锁的,加锁解锁的开销都比较大。所以引入了偏向锁、轻量级锁、重量级锁。
那么偏向锁、轻量级锁、重量级锁又是什么
偏向锁:我认为偏向锁的关键就是“偏”,偏向于第一个访问的线程。也就是说在无竞争的环境下,有一个线程访问的同步代码块,那么这个锁就会偏向这个线程,下次有线程访问的时候就会判断是不是之前访问过的线程访问,这样就会少一次cas的开销。因为第一次有线程访问同步代码块的时候会用cas把线程id写入mark word中。偏向锁会有一个延迟,程序刚启动的5s内不会出现偏向锁,这点在博主前面jol测试对象头中证明了这点,计算过hashcode值的对象不会加偏向锁,因为对象头没有空间放线程id了。
轻量级锁:轻量级锁体现轻量的点就在于自旋,如果线程访问轻量级锁的同步代码块,会cas判断线程id是否一致,不一致会自旋一定的时间一致cas,如果cas成功就还是轻量级锁。但一般都是失败的,然后轻量级锁就会升级为重量级锁。
重量级锁:jvm层面的两个标识,加锁解锁都会阻塞其他线程。
详细谈一下锁升级
简单的来说,在无竞争的时候sync使用偏向锁,如果偏向锁失败了(一个对象被不同的线程加锁了),就会升级为轻量级锁,如果有线程的竞争,就升级为重量级锁。
详细的说,偏向锁的标记被记录在了markwork里面,标志位为100,偏向锁的线程id也被记录在了对象头中,线程访问同步代码块的时候,就会用cas检查对象头记录的线程id是不是当前线程,如果不是,就cas把替换线程id,如果成功就获得偏向锁。如果是当前线程,也获得偏向锁。如果替换失败,就当偏向锁线程达到安全点的时候,升级为轻量级锁。偏向锁的意思就是偏向第一个访问的线程。
轻量级锁的线程信息存储在当前线程的栈帧中,并将对象头中的markword复制到锁记录中,然后线程尝试使用cas将对象头中的markword替换为指向锁记录的指针,如果成功,当前线程获得锁,如果失败,表示有其他线程竞争,自旋一定的次数后就会升级为重量级锁。重量级锁使除了拥有锁的线程以外的线程都阻塞。
再来说说synchronize的原理
Wait Set:那些调用wait方法被阻塞的线程放置在这里
contention List:竞争队列,所有请求锁的线程首先被放在竞争队列
entry list:contention中那些有资格的会被移入contention list
ondeck:任意时刻,最多有一个线程在竞争资源,该线程为ondeck
owner:当前获取到资源的线程
- JVM 每次从队列的尾部取出一个数据用于锁竞争候选者(OnDeck),但是并发情况下,ContentionList 会被大量的并发线程进行 CAS 访问,为了降低对尾部元素的竞争,JVM 会将一部分线程移动到 EntryList 中作为候选竞争线程。
- Owner 线程会在 unlock 时,将 ContentionList 中的部分线程迁移到 EntryList 中,并指定EntryList 中的某个线程为 OnDeck 线程(一般是最先进去的那个线程)。
- Owner 线程并不直接把锁传递给 OnDeck 线程,而是把锁竞争的权利交给 OnDeck,
OnDeck 需要重新竞争锁。这样虽然牺牲了一些公平性,但是能极大的提升系统的吞吐量,在JVM 中,也把这种选择行为称之为“竞争切换”。 - OnDeck 线程获取到锁资源后会变为 Owner 线程,而没有得到锁资源的仍然停留在 EntryList中。如果 Owner 线程被 wait 方法阻塞,则转移到 WaitSet 队列中,直到某个时刻通过 notify或者 notifyAll 唤醒,会重新进去 EntryList 中。
- 处于 ContentionList、EntryList、WaitSet 中的线程都处于阻塞状态,该阻塞是由操作系统来完成的(Linux 内核下采用 pthread_mutex_lock 内核函数实现的)。
- Synchronized 是非公平锁。 Synchronized 在线程进入 ContentionList 时,等待的线程会先尝试自旋获取锁,如果获取不到就进入 ContentionList,这明显对于已经进入队列的线程是不公平的,还有一个不公平的事情就是自旋获取锁的线程还可能直接抢占 OnDeck 线程的锁资源。
参考:https://blog.csdn.net/zqz_zqz/article/details/70233767 - 每个对象都有个 monitor 对象,加锁就是在竞争 monitor 对象,代码块加锁是在前后分别加上 monitorenter 和 monitorexit 指令来实现的,方法加锁是通过一个标记位来判断的
- synchronized 是一个重量级操作,需要调用操作系统相关接口,性能是低效的,有可能给线程加锁消耗的时间比有用操作消耗的时间更多。
- Java1.6,synchronized 进行了很多的优化,有适应自旋、锁消除、锁粗化、轻量级锁及偏向锁等,效率有了本质上的提高。在之后推出的 Java1.7 与 1.8 中,均对该关键字的实现机理做了优化。引入了偏向锁和轻量级锁。都是在对象头中有标记位,不需要经过操作系统加锁。
- 锁可以从偏向锁升级到轻量级锁,再升级到重量级锁。这种升级过程叫做锁膨胀;
- JDK 1.6 中默认是开启偏向锁和轻量级锁,可以通过-XX:-UseBiasedLocking 来禁用偏向锁。