深入了解synchronized(二)锁优化策略

777_yL

  • synchronized原理进阶
    • 轻量锁
    • 锁膨胀
    • 自旋优化
    • 锁消除
    • 锁粗化
    • 偏向锁
    • 轻量锁、重量锁、偏向锁的区别

synchronized原理进阶

JDK 6 对 synchronized 做了很多优化,引入了自适应自旋、锁消除、锁粗化、偏向锁和轻量级锁等提高锁的效率,锁一共有 4 个状态,级别从低到高依次是:无锁、偏向锁、轻量级锁和重量级锁,状态会随竞争情况升级。锁可以升级但不能降级,这种只能升级不能降级的锁策略是为了提高锁获得和释放的效率。

轻量锁

如果一个对象虽然有多线程要加锁,但因加锁的时间错开(也就是没有竞争)我们可以用轻量锁来优化。
轻量级锁对使用者是透明的,即语法仍然是 synchronized

 public  static void method1(){
     
        synchronized (obj){
     
            method2();
        }
    }
 public static  void method2(){
     
        synchronized (obj){
     

        }
    }

原理:
在代码进行到同步代码时,如果同步对象没有被锁定,虚拟机会在当前线程的栈帧中建立锁记录(Lock Record),记录锁记录地址和对锁对象markword的拷贝。然后虚拟机用CAS(Compare and Swap) 将锁记录地址与对象头中的markword交换,如果交换成功,则标志该线程拥有了锁,锁标志位置为00,并处于轻量级锁状态。

深入了解synchronized(二)锁优化策略_第1张图片
深入了解synchronized(二)锁优化策略_第2张图片
深入了解synchronized(二)锁优化策略_第3张图片
如果cas操作失败,有两种情况

  • 至少有一个线程与当前线程竞争,虚拟机检查对象的Mark Word是否指向当前线程的栈桢,如果是表明当前线程已经拥有了锁。即为情况2。否则说明锁对象已经被其他线程抢占,如果出现两个线程正争用同一个锁,轻量级锁就会失效,将会进行锁膨张。

  • 当前线程再次访问当前锁的同步代码块,执行了synchronized锁重入,那么再添加一条锁记录作为重入的计数。
    深入了解synchronized(二)锁优化策略_第4张图片

  • 当退出 synchronized 代码块(解锁时)如果有取值为 null 的锁记录,表示有重入,这时重置锁记录,表示重入计数减一。

  • 当退出 synchronized 代码块(解锁时)锁记录的值不为 null,这时使用 cas 将 Mark Word 的值恢复给对象头

    成功,则解锁成功
    失败,说明轻量级锁进行了锁膨胀或已经升级为重量级锁,进入重量级锁解锁流程

锁膨胀

如果在尝试加轻量级锁的过程中,CAS 操作无法成功,这时一种情况就是有其它线程为此对象加上了轻量级锁(有竞争),这时需要进行锁膨胀,将轻量级锁变为重量级锁。
深入了解synchronized(二)锁优化策略_第5张图片
深入了解synchronized(二)锁优化策略_第6张图片

自旋优化

同步对性能最大的影响是阻塞,挂起和恢复线程的操作都需要转入内核态完成。许多应用上共享数据的锁定只会持续很短的时间,为了这段时间去挂起和恢复线程并不值得。如果机器有多个处理器核心,我们可以让后面请求锁的线程稍等一会,但不放弃处理器的执行时间,看看持有锁的线程是否很快会释放锁。为了让线程等待只需让线程执行一个忙循环,这项技术就是自旋锁。
自旋锁在 JDK1.4 就已引入,默认关闭,在 JDK6 中改为默认开启。自旋不能代替阻塞,虽然避免了线程切换开销,但要占用处理器时间,如果锁被占用的时间很短,自旋的效果就会非常好,反之只会白白消耗处理器资源。如果自旋超过了限定的次数仍然没有成功获得锁,就应挂起线程,自旋默认限定次数是10

锁消除

锁消除指即时编译器对检测到不可能存在共享数据竞争的锁进行消除。主要判定依据来源于逃逸分析,如果判断一段代码中堆上的所有数据都只被一个线程访问,就可以当作栈上的数据对待,认为它们是线程私有的而无须同步。

锁粗化

原则需要将同步块的作用范围限制得尽量小,只在共享数据的实际作用域中进行同步,这是为了使等待锁的线程尽快拿到锁。但如果一系列的连续操作都对同一个对象反复加锁和解锁,甚至加锁操作是出现在循环体之内的,即使没有线程竞争也会导致不必要的性能消耗。因此如果虚拟机探测到有一串零碎的操作都对同一个对象加锁,将会把同步的范围扩展到整个操作序列的外部。

偏向锁

轻量级锁在没有竞争的时,每次重入仍然需要CAS操作,造成锁开销。JDK6中引入偏向锁来做进一步优化。偏向锁是为了在没有竞争的情况下减少锁开销,锁会偏向于第一个获得它的线程,如果在执行过程中锁一直没有被其他线程获取,则持有偏向锁的线程将不需要进行同步。
实现方式
当锁对象第一次被线程获取时,虚拟机会将对象头中的偏向模式设为 1,同时使用 CAS 把获取到锁的线程 ID 记录在对象的 Mark Word 中。如果 CAS 成功,持有偏向锁的线程以后每次进入锁相关的同步块都不再进行任何同步操作。一旦有其他线程尝试获取锁,偏向模式立即结束,根据锁对象是否处于锁定状态决定是否撤销偏向,后续同步按照轻量级锁那样执行。
深入了解synchronized(二)锁优化策略_第7张图片
根据上图所知:
一个对象创建时:

  • 如果开启了偏向锁(默认时开启的),则MarkWord地址默认后三位为101,这时还没有线程获得锁,即线程IDthread、epoch、unused都为0;
  • 偏向锁是默认是延迟的,不会在程序启动时立即生效,如果想避免延迟,可以加 VM 参数
    -XX:BiasedLockingStartupDelay=0 来禁用延迟
  • 如果没有开启偏向锁,那么对象创建后,markword 值为 0x01 即最后 3 位为 001,这时它的 hashcode、age 都为
    0,第一次用到 hashcode 时才会赋值

轻量锁、重量锁、偏向锁的区别

  • 偏向锁的优点是加解锁不需要额外消耗,和执行非同步方法比仅存在纳秒级差距,缺点是如果存在锁竞争会带来额外锁撤销的消耗,适用只有一个线程访问同步代码块的场景。
  • 轻量级锁的优点是竞争线程不阻塞,程序响应速度快,缺点是如果线程始终得不到锁会自旋消耗 CPU,适用追求响应时间、同步代码块执行快的场景。
  • 重量级锁的优点是线程竞争不使用自旋不消耗CPU,缺点是线程会阻塞,响应时间慢,适应追求吞吐量、同步代码块执行慢的场景。

你可能感兴趣的:(java并发编程,多线程,并发编程,数据库,java,面试)