synchronized

75 阅读3分钟

本质:底层是通过monitorEnter 和moniterExit 指令实现的

javap反编译加了synchorize 的代码 ,看字节码 monitorEnter 同步代码块 monitorExit(释放锁)

image.png 多了一个monitorexit 就是异常情况下释放锁 加锁的理解: 当我们调用monitorEnter的时候,会向操作系统申请一个操作系统的Monitor锁,这个锁底层是C++实现的。比如线程1执行了monitorEnter(LOCK),这个LOCK是java对象。先找到java对象,用java对象头记录monitor锁的内存地址。加锁过程主要靠Monitor里面的owner 属性,owner属性表示有没有主人。初始时为NULL表示当前没有任何线程拥有该monitor record, 当线程成功拥有该锁后保存线程唯一标识,当锁被释放时又设置为NULL,比如,这时候来了一个Thread2 ,重复上面的步骤。判断owner有没有主人,如果有主人,此时会先自旋几次,自旋期间看看Thread1是否能释放,如果不能释放,他就会进入一个阻塞队列EntryList,状态也会从运行状态变为阻塞状态,再来一个Thread3也是这样。当线程1执行monitorexit的时候,会将owner属性置为null。他还要做一件事情,就是唤醒阻塞队列中的线程。可以从队列头,也可以从队列尾,这个不纠结。比如唤醒了Thread2。这时候Thread2将owner置为自己。thread3仍然等待

image.png

相关的内存屏障

就是加了内存屏障,就是monitorEnter 和monitorExit中间的这些代码不要跑出去,中间的一些读写操作不会重排序到代码块外面去

image.png

锁优化 为什么要锁优化?

重量级锁

1.6之前只有重量级锁,当发生竞争时候,就会向操作系统申请monitor互斥锁,因为monitor互斥锁性能比较差,用户态和内核态切换导致性能差。所以1.6开始进行了优化。什么优化呢?

轻量级锁

从轻量级锁开始。如果线程加锁、解锁时间上刚好是错开的,这时候就可以使用轻量级锁,只是使用 cas 尝试将对象头替换为该线程的锁记录地址,如果 cas 失败,会锁重入或触发重量级锁升级.

偏向锁(稍微复杂一点)

打个比方,轻量级锁就好比用课本占座,线程每次占座前还得比较一下,课本是不是自己的(cas),频繁 cas 性能也会受到影响而偏向锁就好比座位上已经刻好了线程的名字,线程【专用】这个座位,比 cas 更为轻量但是一旦其他线程访问偏向对象,那么比较麻烦,需要把座位上的名字擦去,这称之为偏向锁撤销,锁也升级为轻量级锁.偏向锁撤销也属于昂贵的操作,怎么减少呢,JVM 会记录这一类对象被撤销的次数,如果超过了 20 这个阈值,下次新线程访问偏向对象时,就不用撤销了,而是刻上新线程的名字,这称为重偏向.如果撤销次数进一步增加,超过 40 这个阈值,JVM 会认为这一类对象不适合采用偏向锁,会对它们禁用偏向锁,下次新建对象会直接加轻量级锁 (默认)偏向锁(一个线程用)->轻量锁(不同线程无竞争)有竞争升级为->重量级锁

synchronized 更为重量,申请锁、锁重入都要发起系统调用,频繁调用性能会受影响
synchronized 如果无法获取锁时,线程会陷入阻塞,引起的线程上下文切换成本高
虽然做了一系列优化,但轻量级锁、偏向锁都是针对无数据竞争场景的
如果数据的原子操作时间较长,仍应该让线程阻塞,无锁适合的是短频快的共享数据修改操作主要用于计数\器、停止标记、或是阻塞前的有限尝试