synchronized
简介
JVM会自动通过使用monitor来加锁和解锁,保证了同时只有一个线程可以执行指定代码,从而保证了线程安全,同时具有可重入和不可中断的性质。
用法
对象锁
-
方法锁
-
同步的代码块锁
类锁
- 静态的方法
- 指定锁为class对象
性质
可重入
不可中断
原理
源码
字节码:monitorenter moniterexit
jvm层级(hotspot实现)
InterpreterRuntime:: monitorenter方法
|
|
synchronizer.cpp
revoke_and_rebias
|
|
|
|
inflate方法:膨胀为重量级锁
synchronized最底层实现
查看汇编码(由于jvm在热点代码才使用jit,所以需要构造热点代码再执行,需要使用工具hsdis)
|
|
运行 java -XX:+UnlockDiagonositicVMOptions -XX:+PrintAssembly T
C1 Compile Level 1 (一级优化)
C2 Compile Level 2 (二级优化)
找到m() n()方法的汇编码,会看到lock comxchg .....
指令
lock addl 0 往某个寄存器上加个0 想当于没任何操作 空操作 这个实现的是内存屏障
对象头
synchronized优化的过程和markword息息相关,用markword中最低的三位代表锁状态,其中1位是偏向锁位,两位是普通锁位。
锁标志11 表示对象需要被回收
低位01区分不开,增加1位区分无锁和偏向锁
可以使用jol工具类查看,注意打印出来的内存布局采用的是大端存储
锁升级
无锁 - 偏向锁 - 轻量级锁 (自旋锁,自适应自旋)- 重量级锁
-
Object o = new Object() 锁 = 0 01 无锁态
-
默认情况在jvm启动时偏向锁也已启动,只不过偏向锁有个时延,默认是4秒
why?
因为JVM虚拟机自己有一些默认启动的线程,里面有好多sync代码,这些sync代码启动时就知道肯定会有竞争,如果使用偏向锁,就会造成偏向锁不断的进行锁撤销和锁升级的操作,效率较低。
可以通过设置
-XX:BiasedLockingStartupDelay=0
来消除延迟 -
如果设定
-XX:BiasedLockingStartupDelay=0
new Object () - > 101 偏向锁 ->线程ID为0 -> Anonymous BiasedLock 打开偏向锁,new出来的对象,默认就是一个可偏向匿名对象101
-
如果有线程上锁 上偏向锁,指的就是,把markword的线程ID改为自己线程ID的过程 偏向锁不可重偏向 批量偏向 批量撤销
如果偏向锁上来就重度竞争,耗时特别长,或者有wait操作,直接升级到重量级锁
-
如果有线程竞争
-
撤销偏向锁,升级轻量级锁
轻度竞争,当有了偏向锁后,又来了线程进行竞争,后面的线程使用cas的方式进行竞争,竞争成功后 将指向线程栈中的Lock Record 贴到指定空间
线程在自己的线程栈生成LockRecord ,用CAS操作将markword设置为指向自己这个线程的LR的指针,设置成功者得到锁
每个线程有自己的LockRecord在自己的线程栈上,用CAS去争用markword的LR的指针,指针指向哪个线程的LR,哪个线程就拥有锁
-
如果竞争加剧 竞争加剧:有线程超过10次自旋, -XX:PreBlockSpin, 或者自旋线程数超过CPU核数的一半, 1.6之后,加入自适应自旋 Adapative Self Spinning , JVM自己控制
自适应自旋锁意味着自旋的时间(次数)不再固定,而是由前一次在同一个锁上的自旋时间及锁的拥有者的状态来决定。如果在同一个锁对象上,自旋等待刚刚成功获得过锁,并且持有锁的线程正在运行中,那么虚拟机就会认为这次自旋也是很有可能再次成功,进而它将允许自旋等待持续相对更长的时间。如果对于某个锁,自旋很少成功获得过,那在以后尝试获取这个锁时将可能省略掉自旋过程,直接阻塞线程,避免浪费处理器资源。
升级重量级锁:-> 向操作系统申请资源,linux mutex , CPU从3级-0级系统调用,线程挂起,进入等待队列,等待操作系统的调度,然后再映射回用户空间
偏向锁
偏向锁 - markword 上记录当前线程指针,下次同一个线程加锁的时候,不需要争用,只需要判断线程指针是否同一个,所以,偏向锁是偏向加锁的第一个线程 。线程销毁,锁降级为无锁。
偏向锁由于有锁撤销的过程revoke,会消耗系统资源,所以,在锁争用特别激烈的时候,用偏向锁未必效率高。还不如直接使用轻量级锁。
自旋锁
自旋锁在 JDK1.4.2 中引入,使用 -XX:+UseSpinning 来开启。JDK 6 中变为默认开启,并且引入了自适应的自旋锁(适应性自旋锁)。
自适应自旋锁意味着自旋的时间(次数)不再固定,而是由前一次在同一个锁上的自旋时间及锁的拥有者的状态来决定。如果在同一个锁对象上,自旋等待刚刚成功获得过锁,并且持有锁的线程正在运行中,那么虚拟机就会认为这次自旋也是很有可能再次成功,进而它将允许自旋等待持续相对更长的时间。如果对于某个锁,自旋很少成功获得过,那在以后尝试获取这个锁时将可能省略掉自旋过程,直接阻塞线程,避免浪费处理器资源。
重量级锁
重量级锁依赖于操作系统的互斥量(mutex) 实现的,而操作系统中线程间状态的转换需要相对比较长的时间,所以重量级锁效率很低,但被阻塞的线程不会消耗CPU。
前面说到,每一个对象都可以当做一个锁,当多个线程同时请求某个对象锁时,对象锁会设置几种状态用来区分请求的线程:
|
|
当一个线程尝试获得锁时,如果该锁已经被占用,则会将该线程封装成一个ObjectWaiter
对象插入到Contention List的队列的队首,然后调用park
函数挂起当前线程。
当线程释放锁时,会从Contention List或EntryList中挑选一个线程唤醒,被选中的线程叫做Heir presumptive
即假定继承人,假定继承人被唤醒后会尝试获得锁,但synchronized
是非公平的,所以假定继承人不一定能获得锁。这是因为对于重量级锁,线程先自旋尝试获得锁,这样做的目的是为了减少执行操作系统同步操作带来的开销。如果自旋不成功再进入等待队列。这对那些已经在等待队列中的线程来说,稍微显得不公平,还有一个不公平的地方是自旋线程可能会抢占了Ready线程的锁。
如果线程获得锁后调用Object.wait
方法,则会将线程加入到WaitSet中,当被Object.notify
唤醒后,会将线程从WaitSet移动到Contention List或EntryList中去。需要注意的是,当调用一个锁对象的wait
或notify
方法时,如当前锁的状态是偏向锁或轻量级锁则会先膨胀成重量级锁。
各种锁的优缺点对比
用户空间锁 vs 重量级锁
- 偏向锁、自旋锁都是用户空间完成的
- 重量级锁需要向内核申请
为何会有偏向锁
synchronized方法,在多数情况下只有一个线程在运行。
为什么有自旋锁还需要重量级锁
自旋锁适合临界区执行时间比较短的情况,转圈次数比较少就能获得或者竞争的线程比较少。
自旋是消耗cpu资源的,如果锁的时间过长,或者自旋线程很多,cpu资源会被大量消耗。重量级锁有等待队列,所有拿不到锁的进入等待队列,不需要消耗资源。
锁降级
https://www.zhihu.com/question/63859501
其实,只被VMThread访问,降级也就没啥意义了。所以可以简单认为锁降级不存在!
降级只会在某些特定情况下发生比如gc,但是gc表明对象要被回收,此时降级没什么意义。
锁消除 lock eliminate
|
|
我们都知道 StringBuffer 是线程安全的,因为它的关键方法都是被 synchronized 修饰过的,但我们看上面这段代码,我们会发现,sb 这个引用只会在 add 方法中使用,不可能被其它线程引用(因为是局部变量,栈私有),因此 sb 是不可能共享的资源,JVM 会自动消除 StringBuffer 对象内部的锁。
锁粗化 lock coarsening
|
|
JVM 会检测到这样一连串的操作都对同一个对象加锁(while 循环内 100 次执行 append,没有锁粗化的就要进行 100 次加锁/解锁),此时 JVM 就会将加锁的范围粗化到这一连串的操作的外部(比如 while 循环体外),使得这一连串操作只需要加一次锁即可。
缺陷
不够灵活
试图获得锁时不能设定超时
不能中断一个正在试图获得锁的线程
加锁和释放的时机单一
每个锁仅有单一的条件(某个对象)
无法知道是否成功获取到锁
针对这些缺陷,lock锁能够有效解决:
1、 lock();//获取锁
2、 unlock();//释放锁
3、 tryLock();//判断锁是否可用。返回值为:boolean;
4、tryLock(time,TimeUnit);//在规定的时间内,如果未获得锁,则就放弃。第一项表示规定的时间;第二项表示设置时间的单位
synchronized vs Lock(CAS)
在高争用 高耗时的环境下synchronized效率更高 在低争用 低耗时的环境下CAS效率更高 synchronized到重量级之后是等待队列(不消耗CPU) CAS(等待期间消耗CPU)
一切以实测为准