本文参考了《深入理解java虚拟机》锁优化章节,尚硅谷JUC,黑马多线程视频,再增加了一些自己的理解,绝不是胡诌的哦。
文章目录
- 1. 自旋锁是什么?
- 2. 锁消除
- 3. 轻量级锁
- 3.1 对象头
- 3.2 轻量级锁的加锁过程
- 3.3 轻量级锁的解锁过程
- 4. 重量级锁
- 5. 偏向锁
- 6. 补充:关于HashCode
1. 自旋锁是什么?
这其实是在jdk4引入的手段,一般来讲加锁解锁会涉及到cpu从内核态到用户态之间的切换,而如果某个线程持有锁的时间很短,那么我们只需要稍微让cpu空转一下,即一个执行一小段时间的while循环,结束后发现线程可以获取锁了,那么就减去了切换的开销。
这确实是个好主意,并且现在是多核时代,如果对于单核cpu,那么cpu空转会白白浪费性能,但是多核cpu,一两个cpu空转等待这个开销是值得的,在jdk6以后,自旋锁升级为了自适应自旋锁,jvm会自己去判断该不该进行自旋,这方面其实都不太需要我们考虑了。
2. 锁消除
这里用了深入理解java虚拟机中的例子。
在jdk5中下面代码会被转化成下面的代码。
我们知道StringBuffer是线程安全的集合,本身append方法就是使用synchronized
修饰的,这就造成了一个方法里有多个锁块,而程序其实会对其进行优化,将局部方法中的同步代码块进行消除,以提高程序的执行效率。
3. 轻量级锁
它并不是用来代替重量级锁的,而是在没有锁竞争的情况下,减少重量级锁使用操作系统互斥量带来的性能开销的。
所以关键在于,如果锁竞争比较激烈,轻量级锁反而会有很大的开销,核心就在于它内部的自旋,jvm做的聪明的一点在于,如果处于竞争激烈的情况下,那么就会升级为重量级锁。
那我们详细来讲讲吧,首先我们需要一些前置知识。
3.1 对象头
复习下对象头吧,对象头主要有两部分,对象标记和类元信息。
对象标记指的就是Mark Word,有以下内容,其中标志位和状态,指针都比较重要。
而类元信息存储的是方法区对象类型数据的指针。补充下这张图。
3.2 轻量级锁的加锁过程
首先一个对象没有被锁定时,它的锁标志位是01,当代码快要进入同步代码块时,会在线程栈帧中新建了一个名为Lock Record
锁记录的空间,用来存储Mark Word
的拷贝,官方把这玩意称之为Disokaced Mark Word
,如下所示。
此时虚拟机将使用CAS将对象中的Mark Word
更新为锁记录的指针,如果更新成功,就代表线程拥有了这个对象的锁,
下面是更新成功的情况,对象头的Mark Word
的锁标志位也同时被更新为00了。
但上面只是介绍了从无锁->轻量级锁的过程。
而如果对象已经持有锁了呢?
那么就分为两种情况,对象Mark Word是否指向当前的线程栈帧?
如果是,说明是一个线程多次进入同一把锁,也就是重入的情况,会再添加一条锁记录作为重入的技术,而后直接进入执行代码即可。
如果不是,说明存在竞争情况,如下所示,线程0已经加了轻量级锁,而线程1想要再加,CAS失败,进入锁膨胀的情况。
Object对象申请重量级锁Monitor,这个是操作系统的锁,而后Mark Word指向了Monitor的地址,同时线程1进入EntryList阻塞等待线程0释放。
3.3 轻量级锁的解锁过程
解锁同样是通过CAS操作,也分为几种情况。
1.重入解锁成功,如果有取值为null的记录,表示有重入的情况,去掉该锁记录,并将重入计数-1。
2.正常解锁成功,没有竞争情况,且锁记录没有为null的记录,此时会通过CAS将Mark Word中的记录更新,对象头重置为无锁00状态。
3.解锁失败,此时表明存在竞争,膨胀为重量级锁。还是这张图,本想将Object中Mark Word的内容还原,但CAS失败,因为内容已经变成monitor的地址了此时要将owner置为null 唤醒在EntryList中的线程1。
4. 重量级锁
如果线程2进入了同步代码块,那么就进入的java对象还会关联一个Monitor对象。
其中Owner指向了线程2,而其他竞争的线程345则进入EntryList中等待,其中EntryList是一个双向链表,并且345之间存在竞争,是非公平的。
再来说说图重点WAITING。
线程01判断条件是否满足,不满足调用wait方法进入waiting队列中等待,当线程2释放锁 owner为null时,blocked队列就开始竞争锁了,而释放锁时如果调用了notify/notifyAll方法 就会将单个或所有线程都放入到blocked队列中竞争锁。
5. 偏向锁
首先要明白偏向锁的几个特性。
为什么要引入偏向锁?
为了弥补轻量级锁多次cas比较带来的性能损耗,偏向锁是完全无竞争,连CAS操作都省略掉了,很多情况下只有一个线程反复进入同步代码快时,就很有用了。
而一旦发生了竞争情况,偏向锁就会升级,这也是在所难免的,值得一提的是,在jdk15后就废弃了偏向锁,但我们现在还是用的上的。
什么时候会升级为偏向锁?
其实默认会在4秒后开启偏向锁看,所以一般情况下4秒钟内的对象默认是轻量级锁,有两个办法可以改变。
1.延迟参数设置为0,即-XX:BiasedLockingStartupDelay=0
2.延迟4秒后 再创建对象
我们一般讲的是将延迟参数设置为0的情况。
偏向锁的加锁很简单,在无锁的情况下,会将对象头中的偏向模式设置为1,标志位变为01,简称101,同时使用CAS操作将该线程的ID记录到对象头中。
有锁的情况下,只需要判断对象头中的线程ID是否一致即可,一致无需任何其他操作,直接往下执行。
有一种批量重偏向的情况,有以下情况,线程1循环进入同步代码快,此时对象头的锁id为线程1的id,过了一会线程2循环记录同步代码块,但这是没有竞争的情况下,所以不会一下子的改变对象头的id。
可以看下下面代码,t1线程先是30次加锁,每个对象放入集合,每个对象的mark word都为当前线程的id。
t2线程加锁获取对象,打印对象内容。
这里截取部分内容,两张图可以对比一下,发现在t2线程第20次循环时,对象的记录结果和第19次的三次打印是不同的,即第20次第一段代码打印的是指向线程1的对象头,第二次加锁后就变成了指向线程2的对象头了,后面的结果也是一样。
得出结论,当撤销偏向锁阈值超过 20 次后,jvm 会这样觉得,我是不是偏向错了呢,于是会在给这些对象加锁时重新偏向至
加锁线程。
不一致的情况,说明存在竞争了,此时需要撤销偏向锁。
撤销偏向锁有以下情况。
1.当有其它线程使用偏向锁对象时,会将偏向锁升级为轻量级锁,竞争会导致锁升级。
2.使用hashcode时,会了保证hashcode的一致性,会直接膨胀为重量级锁。
3.在代码中使用到了wait/notify时,也会膨胀为重量级锁。
4.批量撤销的情况,比如偏向锁t1线程占用 后面改为t2占用,等撤销达到第40次后,jvm发现撤销次数太多了,可能就不该偏向 ,所以就算新创建的对象都是无锁的,进锁块直接就是轻量级锁,不会有偏向锁了。(这种情况有空补下代码吧)
6. 补充:关于HashCode
下面总结了几种情况,在调用hashcode时,不同加锁情况下的变化。