乐趣区

关于java:synchronized-优化手段之锁膨胀机制

synchronized 在 JDK 1.5 之前性能是比拟低的,在那时咱们通常会抉择应用 Lock 来代替 synchronized。然而这个状况在 JDK 1.6 时就产生了扭转,JDK 1.6 中对 synchronized 进行了各种优化,性能也失去了大幅的晋升,这也是目前版本中还能常常见到 synchronized 身影的重要起因之一。当然除了性能之外,synchronized 的应用也十分便当,这也是它风行的重要起因。

在泛滥优化计划中,锁收缩机制是晋升 synchronized 性能最无利的伎俩之一(其余优化计划咱们前面再讲),本文咱们重点来看什么是锁收缩?以及锁收缩的各种细节。

注释

在 JDK 1.5 时,synchronized 须要调用监视器锁(Monitor)来实现,监视器锁实质上又是依赖于底层的操作系统的 Mutex Lock(互斥锁)实现的,互斥锁在进行开释和获取的时候,须要从用户态转换到内核态,这样就造成了很高的老本,也须要较长的执行工夫,这种依赖于操作系统 Mutex Lock 实现的锁咱们称之为“重量级锁”。

什么是用户态和内核态?

用户态(User Mode):当过程在执行用户本人的代码时,则称其处于用户运行态。
内核态(Kernel Mode):当一个工作(过程)执行零碎调用而陷入内核代码中执行时,咱们就称过程处于内核运行态,此时处理器处于特权级最高的内核代码中执行。

为什么分内核态和用户态?

假如没有内核态和用户态之分,程序就能够随便读写硬件资源了,比方随便读写和分配内存,这样如果程序员一不小心将不适当的内容写到了不该写的中央,很可能就会导致系统解体。

而有了用户态和内核态的辨别之后,程序在执行某个操作时会进行一系列的验证和测验之后,确认没问题之后才能够失常的操作资源,这样就不会放心一不小心就把零碎搞坏的状况了,也就是 有了内核态和用户态的辨别之后能够让程序更加平安的运行,但同时两种状态的切换会导致肯定的性能开销。

锁收缩

在 JDK 1.6 时,为了解决获取锁和开释锁带来的性能耗费,引入了“偏差锁”和“轻量级锁”的状态,此时 synchronized 的状态总共有以下 4 种:

  1. 无锁
  2. 偏差锁
  3. 轻量级锁
  4. 重量级锁

锁的级别依照上述先后顺序顺次降级,咱们把这个降级的过程称之为“锁收缩”。

PS:到当初为止,锁的降级是单向的,也就是说只能从低到高降级(无锁 -> 偏差锁 -> 轻量锁锁 -> 重量级锁),不会呈现锁降级的状况。

锁收缩为什么能优化 synchronized 的性能?当咱们理解了这些锁状态之后天然就会有答案,上面咱们一起来看。

1. 偏差锁

HotSpot 作者通过钻研实际发现,在大多数状况下,锁不存在多线程竞争,总是由同一线程屡次取得的,为了让线程取得锁的代价更低,于是就引进了偏差锁。

偏差锁(Biased Locking)指的是,它会偏差于第一个拜访锁的线程,如果在运行过程中,同步锁只有一个线程拜访,不存在多线程争用的状况,则线程是不须要触发同步的,这种状况下会给线程加一个偏差锁。

偏差锁执行流程

当一个线程拜访同步代码块并获取锁时,会在对象头的 Mark Word 里存储锁偏差的线程 ID,在线程进入和退出同步块时不再通过 CAS 操作来加锁和解锁,而是检测 Mark Word 里是否存储着指向以后线程的偏差锁,如果 Mark Word 中的线程 ID 和拜访的线程 ID 统一,则能够间接进入同步块进行代码执行,如果线程 ID 不同,则应用 CAS 尝试获取锁,如果获取胜利则进入同步块执行代码,否则会将锁的状态降级为轻量级锁。

偏差锁的长处

偏差锁是为了在无多线程竞争的状况下,尽量减少不必要的锁切换而设计的,因为锁的获取及开释要依赖屡次 CAS 原子指令,而偏差锁只须要在置换线程 ID 的时候执行一次 CAS 原子指令即可。

Mark Word 扩大常识:内存布局

在 HotSpot 虚拟机中,对象在内存中存储的布局能够分为以下 3 个区域:

  1. 对象头(Header)
  2. 实例数据(Instance Data)
  3. 对齐填充(Padding)

对象头中又蕴含了:

  1. Mark Word(标记字段):咱们的偏差锁信息就是存储在此区域的
  2. Klass Pointer(Class 对象指针)

对象在内存中的布局如下:

在 JDK 1.6 中默认是开启偏差锁的,能够通过“-XX:-UseBiasedLocking=false”命令来禁用偏差锁。

2. 轻量级锁

引入轻量级锁的目标是在没有多线程竞争的前提下,缩小传统的重量级锁应用操作系统 Mutex Lock(互斥锁)产生的性能耗费。如果应用 Mutex Lock 每次获取锁和开释锁的操作都会带来用户态和内核态的切换,这样零碎的性能开销是很大的。

当敞开偏差锁或者多个线程竞争偏差锁时就会导致偏差锁降级为轻量级锁,轻量级锁的获取和开释都通过 CAS 实现的,其中锁获取可能会通过肯定次数的自旋来实现。

注意事项

须要强调一点:轻量级锁并不是用来代替重量级锁的,它的本意是在没有多线程竞争的前提下,缩小传统的重量级锁应用产生的性能耗费。轻量级锁所适应的场景是线程交替执行同步块的状况,如果同一时间多个线程同时拜访时,就会导致轻量级锁收缩为重量级锁。

3. 重量级锁

synchronized 是依赖监视器 Monitor 实现办法同步或代码块同步的,代码块同步应用的是 monitorenter 和 monitorexit 指令来实现的,monitorenter 指令是在编译后插入到同步代码块的开始地位,而 monitorexit 是插入到办法完结处和异样处的,任何对象都有一个 Monitor 与之关联,当且一个 Monitor 被持有后,它将处于锁定状态。

如以下加锁代码:

public class SynchronizedToMonitorExample {public static void main(String[] args) {
        int count = 0;
        synchronized (SynchronizedToMonitorExample.class) {for (int i = 0; i < 10; i++) {count++;}
        }
        System.out.println(count);
    }
}

当咱们将上述代码编译成字节码之后,它的内容是这样的:

从上述后果能够看出,在 main 办法的执行中多个 monitorenter 和 monitorexit 的指令,由此可知 synchronized 是依赖 Monitor 监视器锁实现的,而监视器锁又是依赖操作系统的互斥锁(Mutex Lock),互斥锁在每次获取和开释锁时,都会带来用户态和内核态的切换,这样就减少了零碎的性能开销。

总结

synchronized 在 JDK 1.6 时优化了其性能,在一系列优化的伎俩中,锁收缩是晋升 synchronized 执行效率的要害伎俩之一,锁收缩指的是 synchronized 会从无锁状态、到偏差锁、到轻量级锁,最初到重量级锁的过程。重量级之前的所有状态在绝大数状况下能够大幅的晋升 synchronized 的性能。

本系列举荐文章

  1. 并发第一课:Thread 详解
  2. Java 中用户线程和守护线程区别这么大?
  3. 深刻了解线程池 ThreadPool
  4. 线程池的 7 种创立形式,强烈推荐你用它 …
  5. 池化技术达到有多牛?看了线程和线程池的比照吓我一跳!
  6. 并发中的线程同步与锁
  7. synchronized 加锁 this 和 class 的区别!
  8. volatile 和 synchronized 的区别
  9. 轻量级锁肯定比重量级锁快吗?
  10. 这样终止线程,居然会导致服务宕机?
  11. SimpleDateFormat 线程不平安的 5 种解决方案!
  12. ThreadLocal 不好用?那是你没用对!
  13. ThreadLocal 内存溢出代码演示和起因剖析!
  14. Semaphore 自白:限流器用我就对了!
  15. CountDownLatch:别浪,等人齐再团!
  16. CyclicBarrier:人齐了,司机就能够发车了!

关注公号「Java 中文社群」查看更多有意思、涨常识的 Java 并发文章。

退出移动版