1、Synchronized的性能变化
1)在Java早期版本中:
在Java早期版本中 :synchronized属于重量级锁,效率低下 ,因为监视器锁(monitor)是依赖于底层的操作系统的MutexLock来实现的 。 挂起线程和恢复线程都需要转⼊内核态去完成 ,阻塞或唤醒 ⼀个Java线程需要操作系统切换CPU状态来 完成 ,这种状态切换需要耗费处理器时间。
2)Java 6之后
Java 6之后 :为了减少获得锁和释放锁所带来的性能消耗,引⼊了轻量级锁和偏向锁 。这时会有个逐步升级的过程,别⼀开始就捅到重量级锁 。
3)为什么每⼀个对象都可以成为⼀个锁?
1.Java对象是天⽣的Monitor ,每⼀个Java对象都有成为Monitor的潜质,因为在Java的设计中 ,每⼀个Java对象⾃打娘胎⾥出来就带了⼀把看不⻅的锁,它叫做Monitor锁 。 2.Monitor的本质是依赖于底层操作系统的Mutex Lock实现 ,操作系统实现线程之间的切换 ,需要从⽤户态到内核态的转换 ,成本⾮常⾼。
4)Mutex Lock
Monitor是在jvm底层实现的 ,底层代码是c++。本质是依赖于 底层操作系统的Mutex Lock实现 ,操作系统实现线程之间的 切换 ,需要从⽤户态到内核态的转换 ,状态转换需要耗费很多的处理器时间成本⾮常⾼。 所以synchronized是Java语⾔中 的⼀个重量级操作 。
5)synchronized锁升级说明
是由对象头中的Mark Word ,根据锁标志位的不同⽽被复⽤及锁升级策略 。
2、⽆锁
public class MyObject {
public static void main ( String [ ] args) {
Object o = new Object ( ) ;
System . out. println ( "10进制hash码:" + o. hashCode ( ) ) ;
System . out. println ( "16进制hash码:" + Integer . toHexString ( o. hashCode ( ) ) ) ;
System . out. println ( "2进制hash码:" + Integer . toBinaryString ( o. hashCode ( ) ) ) ;
System . out. println ( ClassLayout . parseInstance ( o) . toPrintable ( ) ) ;
}
}
3、偏向锁 单个线程多次访问
1)主要作⽤:
当⼀段同步代码 ,⼀直被同⼀个线程多次访问 ,由于只有⼀个线程那么该线程在后续访问时便会⾃动获得锁 (偏向锁) 。 偏向锁为了解决只有在一个线程执行同步时提高性能 。
2)再看64位标记图
3)偏向锁的理论 (什么是偏向锁?)
在实际应用运行过程中发 现,“锁总是同一个线程持有,很少发生竞争 ”,也就是说锁总是被第一个占用他的线程拥有 ,这个线程就是锁的偏向线程。 那么只需要在第一次拥有该锁的时候,记录下偏向线程ID 。这样偏向线程就一直持有着锁 。(后续这个线程进入和退出 这段加了同步锁的代码块时,不需要再次加锁和释放锁 。而是直接比较对象头里面是否存储了指向当前线程的偏向锁 ) 如果相等 表示偏向锁是偏向于当前线程的 ,就不需要再尝试获得锁了 ,直到竞争发生才释放锁 。以后每次同步,检查锁的偏向线程ID与当前线程ID是否一致 ,如果一致就直接进入同步 。无需每次加锁解锁都去CAS更新对象头 。如果自始至终使用锁的线程只有一个,很明显偏向锁几乎没有额外开销,性能极高 。假如不一致 意味着发生了竞争 ,锁已经 不是总偏向于同一个线程了 ,这时候可能需要升级变为轻量级锁 ,才能保证线程间公平竞争锁 。偏向锁只有遇到其他线程尝试竞争偏向锁时,持有偏向锁的线程才会释放锁 ,线程是不会主动释放偏向锁 的。
4)偏向锁的实现细节?
1.一个synchronized方法被一个线程抢到了锁时 ,那这个方法所在的对象就会在其所在的Mark Word中将偏向锁修改状态位 ,同时还会设置偏向锁持有线程指针指向当前线程 。 2.若该线程再次访问同一个synchronized方法时 ,该线程只需去对象头的Mark Word 中去判断一下是否有偏向锁指向本身的ID ,无需再进入Monitor去竞争对象了 。
5)对于如上的3,4进⾏细化
步骤:
1.偏向锁 的操作不⽤直接捅到操作系统 ,不涉及⽤户到内核转换 ,不必要直接升级为最⾼级 ,我们以⼀个account对象的“对象头”为例, 2.假如有⼀个线程执⾏到synchronized代码块 的时候,JVM使⽤CAS操作把线程指针ID记录到Mark Word当中 ,并修改偏向标示 ,表示当前线程就获得该锁 。锁状态变成偏向锁 (通过CAS修改对象头⾥的锁标志位),字⾯意思是“偏向于第⼀个获得它的线程”的锁。执⾏完同步代码块后,线程并不会主动释放偏向锁 。 3.这时线程获得了锁,可以执⾏同步代码块。当该线程第⼆次到达同步代码块时 会判断此时持有锁的线程是否还是⾃⼰ (持有锁的线程ID也在对象头 ⾥),JVM通过account对象的Mark Word判断:如果当前线程ID还在,说明还持有着这个对象的锁,就可以继续进⼊临界区⼯作 。由于之前没有释放锁,这⾥也就不需要重新加锁。 如果⾃始⾄终使⽤锁的线程只有⼀个,很明显偏向锁⼏乎没有额外开销,性能极⾼。 4.结论: JVM不⽤和操作系统协商设置Mutex(争取内核) ,它只需要记录下线程ID就表示⾃⼰获得了当前锁 ,不⽤操作系统接⼊ 。 上述就是偏向锁: 如果没有其他线程竞争的时候 ,⼀直偏向当前线程,当前线程可以⼀直执⾏ 。
6)重要参数说明
实际上偏向锁在JDK1.6之后是默认开启的 ,但是启动时间有延迟 。 所以需要添加参数==-XX:BiasedLockingStartupDelay=0==,让其在程序启动时⽴刻启动 。
7)偏向锁的撤销概述
偏向锁使⽤⼀种等到竞争出现才释放锁的机制 ,只有当其他线程竞争锁时,持有偏向锁的原来线程才会被撤销。撤销需要等待全局安全点(该时间点上没有字节码正在执⾏) ,同时检查持有偏向锁的线程是否还在执⾏ 。
8)偏向锁的撤销细节梳理
1.第⼀个线程正在执⾏synchronized⽅法(处于同步块) ,它还没有执⾏完 ,其它线程来抢夺 ,该偏向锁会被取消掉,并出现锁升级 ,但是轻量级锁还是由原持有偏向锁的线程持有 ,继续执⾏其同步代码,⽽正在竞争的线程会进⼊⾃旋等待 获得该轻量级锁。 2.第⼀个线程执⾏完成synchronized⽅法(退出同步块) ,则将对象头设置成⽆锁状态并撤销偏向锁 ,重新偏向 (我的理解是,其实如果线程A执⾏完毕,如果不再去竞争,那么就会重新设置线程B为偏向锁 ;如果线程A继续竞争,那么就会CAS⾃旋 也就升 级到了轻量级锁)。
9)偏向锁的撤销图解
4、轻量级锁 多个线程竞争
1)主要作用/目的/升级时机(本质就是自旋锁)
主要目的:
有线程来参与锁的竞争 ,但是获取锁的冲突时间极短 。轻量级锁是为了在线程近乎交替执行同步块时提高性能 。 在没有大量多线程竞争 的前提下,通过CAS减少重量级锁使用操作系统互斥量产生的性能消耗 ,说白了尽量自旋实在不行再阻塞 。 升级时机: (偏向锁升级轻量级锁)
当关闭偏向锁功能 或多线程竞争偏向锁 会导致偏向锁升级为轻量级锁 。
2)再看64位标记图
3)轻量级锁的获取细节
①. 举例说明:
4)⾃旋达到⼀定次数和程度
1.java6之前(了解) :默认启⽤,默认情况下⾃旋的次数是10次 ,-XX:PreBlockSpin=10来修改或者⾃旋线程数超过cpu核数⼀半。 2.Java6之后 :⾃适应 (⾃适应意味着⾃旋的次数不是固定不变的 ),⽽是根据:同⼀个锁上⼀次⾃旋的时间 和拥有锁线程的状态来决定 。
5)轻量锁与偏向锁的区别和不同
1.递进关系: 争夺偏向锁失败时 ,则偏向锁升级为轻量级锁 ,⾃旋尝试抢占锁。 2.释放锁的时机不同: 轻量级锁每次退出同步块都需要释放锁 ,⽽偏向锁是在竞争发⽣时才释放锁 。
5、重锁 会有⽤户态、内核态切换
出现的场景 :当有⼤量的线程参与锁的竞争 ,冲突性很⾼ 。
6、各种锁优缺点、synchronized锁升级和实现原理
1)各个锁的优缺点的对⽐
2)synchronized锁升级过程总结
1.⼀句话,就是先⾃旋,不⾏再阻塞 。 2.实际上是把之前的悲观锁(重量级锁) 变成 在⼀定条件下偏向锁 以及==轻量级(⾃旋锁CAS)==的形式。 3.synchronized在修饰⽅法和代码块在字节码上实现⽅式有很⼤差异 ,但是内部实现 还是基于对象头的MarkWord来实现的 。 4.JDK1.6之前 synchronized使⽤的是重量级锁 ,JDK1.6之后 进⾏了优化,拥有了⽆锁->偏向锁->轻量级锁->重量级锁的 升级过程 ,⽽不是⽆论什么情况都使⽤重量级锁 。
3)偏向锁、轻量级锁、重量级锁总结
1.偏向锁 :适⽤于单线程适⽤ 的情况,在不存在锁竞争的时候 进⼊同步⽅法/代码块则使⽤偏向锁。 2.轻量级锁:适⽤于竞争较不激烈的情况 (这和乐观锁的使⽤范围类似), 存在竞争时升级为轻量级锁,轻量级锁采⽤的是⾃旋锁, 如果同步⽅法/代码块执⾏时间很短的话 ,采⽤轻量级锁虽然会占⽤cpu资源 但是相对⽐使⽤重量级锁还是更⾼效 。 3.重量级锁: 适⽤于竞争激烈的情况 ,如果同步⽅法/代码块执⾏时间很⻓ ,那么使⽤轻量级锁⾃旋带来的性能消耗就⽐使⽤重量级锁更严重 ,这时候就需要升级为重量级锁。
7、锁消除
1)什么是锁消除?
从JIT⻆度看相当于⽆视它 ,synchronized (o)不存在了,这个锁对象并没有被共⽤扩散到其它线程使⽤ ,极端的说就是根本没有加这个锁对象的底层机器码,消除了锁的使⽤ 。
public class LockClearUPDemo {
static Object objectLock = new Object ( ) ;
public void m1 ( ) {
Object o = new Object ( ) ;
synchronized ( o) {
System . out. println ( "---hello LockClearUPDemo" + "\t" + o. hashCode ( ) + "\t" + objectLock. hashCode ( ) ) ;
}
}
}
8、锁粗化
1)什么是锁粗化?
假如方法中首尾相接,前后相邻的都是同一个锁对象 ,那JIT编译器就会把这几个synchronized块合并成一个大块 ,加粗加大范围,一次申请锁使用即可 ,避免次次的申请和释放锁,提升了性能 。
public class LockBigDemo
{
static Object objectLock = new Object ( ) ;
public static void main ( String [ ] args)
{
new Thread ( ( ) -> {
synchronized ( objectLock) {
System . out. println ( "11111" ) ;
}
synchronized ( objectLock) {
System . out. println ( "22222" ) ;
}
synchronized ( objectLock) {
System . out. println ( "33333" ) ;
}
} ,"a" ) . start ( ) ;
new Thread ( ( ) -> {
synchronized ( objectLock) {
System . out. println ( "44444" ) ;
System . out. println ( "55555" ) ;
System . out. println ( "66666" ) ;
}
} ,"a" ) . start ( ) ;
}
}
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30