1.了解java对象的存储结构
- 在学习synchronized 之前我们需要先了解一下 java对象包含了什么?如下图所示:
- 对象头
- mark word:主要用来标识对象的线程锁状态,另外可以配合GC、存放该对象的hashcode
- klass word:记录用于标识对象指向方法区中class信息的指针
- 数组长度:只有当对象是数组的时候才会有这部分
- 对象体
- 对象体是用于保存对象属性和值的部分
- 对齐字节
- 对齐跟数据在内存中的位置有关,需要字节对齐的根本原因在于CPU访问数据的效率问
2.锁的状态
- 刚刚我们已经了解到了mark word存储了线程锁的状态,接下来让我们看看Mark word在64位操作系统的表现形式,如下图所示:
- 偏向锁位(biased_lock)占有一个进制位,0表示对象没有偏向锁,1表示对象获取了偏向锁
- 锁标志位(lock)占有两个进制位,和biased_lock共同表示表示锁的状态
3.锁升级过程
- 锁升级过程:new -> 偏向锁 ->轻量级锁(无锁,自旋锁,自适应自旋) ->重量级锁 ,如下图所示:
- 当new object()的时候,没有任何线程来竞争,是无锁状态(锁=0 0 1)
- 当有一个线程来竞争时,先使用偏向锁,表示锁对象偏爱这个线程,线程在调用锁的这块代码,无需检查和切换,效率非常高
- 当两个线程开始竞争同一个锁,会先撤销偏向锁升级为轻量级锁。线程在自己的线程栈生成LockRecod,用CAS操作将markword设置为指向自己这个线程的LR的指针,设置成功者得到锁
- 当多个线程竞争同一个锁,导致更多wait,锁会升级为重量级锁。这个锁对象Mark Word再次发生变化,会指向一个监视器对象,这个监视器对象用集合的形式,来登记和管理排队的线程
4.synchronized是可重入锁
- 重入锁:重入次数必须记录,因为和解锁次数必须对应
- 因为继承,父类方法被sychronized修饰,子类在方法中调用super.该方法,所以可以使用父类的锁,所以是可重入的
5.锁消除(lock eliminate)和 锁粗化(lock coarsening)
- 锁消除
/**
* This method synchronizes on {@code this}, the destination
* object, but does not synchronize on the source ({@code sb}).
*
* @param sb the {@code StringBuffer} to append.
* @return a reference to this object.
* @since 1.4
*/
public synchronized StringBuffer append(StringBuffer sb) {
toStringCache = null;
super.append(sb);
return this;
}
- StringBuffer是线程安全的,是因为 append 方法被synchronized 修饰过,参考上述源码注释,我们可以看出源码写的是:synchronized只用于 append这个方法,不作用在sb这个引用上。 因此sb是不共享的资源,JVM会自动消除Stringbuffer 对象内部的锁
- 锁粗化
StringBuffer sb=new StringBuffer();
while(i < 50){
sb.append(str);
}
- JVM 会检测到这样一连串的操作都对同一个对象加锁(while 循环内 50次执行 append,没有锁粗化的就要进行 50次加锁/解锁),此时 JVM 就会将加锁的范围粗化到这一连串的操作的外部(比如 while 虚幻体外),使得这一连串操作只需要加一次锁即可。
6.为什么有自旋锁还需要重量级锁?
- 自旋锁是为了减少线程的阻塞,但是会占用cpu的资源,如果持有锁的线程需要执行的同步代码块时间很长,就会浪费大量的cpu资源
7.偏向锁是否一定比自旋锁效率高?
- 不一定,在知道有多线程竞争的情况下,偏向锁会涉及大量的锁撤销,效率低,这个时候使用自旋锁会更好一些
参考文档:
java对象结构与锁实现原理及MarkWord详解
欢迎大家关注我的微信公众号共同学习进步: