我觉得在了解锁之前了解对象的内存布局是有必要的
![}9KCP%T1VXZLM]QHJ6MI{$D.png](https://ucc.alicdn.com/pic/developer-ecology/a3ac06a92ce1450dac1c496d5df475d1.png)
上图为普通对象的内存布局
markword | Klass pointer | data | padding |
---|---|---|---|
属于对象头部分包含hashCode、分代年龄、锁信息。总共占8个字节 | java miror镜像的地址,负责找到方法区中对象的一些信息。开启指针压缩的情况下占4个字节,不开启的情况占8个字节,默认开启 | 表示对象的实力数据 | 补齐操作,在64位计算机中每次读8个字节数据,当对象的大小正好为8的倍数时读取效率最高所以会有一个补齐操作 |
这几个中我们重点研究markword中的数据
锁状态 | markword中锁信息以外的内容 | 是否偏向 | 锁标志位 |
---|---|---|---|
无线程占用 | hashCode(只有调用过hashCode算法才有)、分代年龄 | 0 | 01 |
偏向锁 | 占用线程指针、分代年龄、Epoch | 1 | 01 |
轻量级锁 | lock Record地址 | 00 | |
重量级锁 | 互斥量指针 | 10 |
上面表格是当一个对象为不同锁时markword的存储占用情况,下面我们说说锁升级的过程
首先
一个对象当没有线程占用时为第一个状态,此时对象头中锁信息为无偏向锁标志位为01,
然后
如果此时一个线程需要锁住这个对象了,就会给当前对象一个偏向锁,偏向锁也可以叫无锁因为本身只是将线程ID写入对象头中以及改变一个是否偏向,
若此时
又有一个线程进来需要锁住这个对象,但是此时线程发现这个对象已经有线程占用了,这时两个线程会在自己线程栈中生成一个lock record的锁记录同时进行CAS操作,将这个锁记录的地址放入到markword中,哪个线程放进去了,哪个线程就占用了这个对象,其他线程继续进行CAS操作,这个操作称为自旋,自旋的用处主要是如果占用资源的线程很快就能结束那么就可以省去了线程休眠和唤醒的操作了,可以提高效率,但是当线程自旋次数超过一定阈值,或者自旋的线程超过一定的阈值,就会升级为重量级锁,这个叫做自适应自旋。
一旦
需要升级为重量级锁,虚拟机就会向操作申请一把重量级锁,此时cpu从用户态转化为内核态,去找一找有没有锁时空闲的,如果有将锁的地址写在markword上,这里的重量级锁就是mutex(互斥量)的一种结构,其余线程放入等待队列中,,轮到谁用谁出队。
**
说人话就是要上锁的对象是饮品店的饮料机,一个人刚来的时候只有他自己买,就可以直接做了(现在就是偏向锁),此时如果第二个人来了,也需要用这个饮料机,正好第一个人的还没开始做,这时就需要竞争,谁先抢到饮料机谁就先做,没抢到的等着(现在就是轻量级锁),等着的人多了,或者抢的次数多了,饮品店老板不高兴了,别抢了,都去后边排队吧,叫到谁谁过来拿来。
**
值得注意的是
偏向锁其实还是有很大缺点的,现在的大部分产品的并发量都很高,线程对资源的抢夺基本是必须的,所有对于偏向锁而言,加锁解锁的开销变得多余,还是有点有点影响性能的,我认为他的存在就是为了解决锁的重入的。这里其实有一个Epoch批量加锁解锁的东西。
还有就是锁升级后,hashCode还有分代年龄的东西会放入到线程栈中,还是可以访问的,因为markword 的内存有限。
关于锁的一些优化
锁的粗化
比如说
StringBuffer s = new StringBuffer();
for (int i = 0; i < 50; i++) {
s.append(i);
}
对于这个操作如果不对锁进行粗化就会有50次锁的加锁解锁所以虚拟机会自动优化为将锁放在for循环外边
锁的消除
public void synchornDel(){
StringBuffer s = new StringBuffer();
for (int i = 0; i < 50; i++) {
s.append(i);
}
}
对于上述代码,s对象并不会逃逸出该方法,更不会存在线程安全问题所以虚拟机就会对锁进行消除