Java实现锁的方式

阻塞:ReentrantLock,Synchronized
非阻塞:CAS+自旋

Jav实现monitor

是利用Object的wait/notify/notifyall,每个Object都是一个monitor,或者说java的每一个对象都是一个监视器

synchronized的锁变化

  • 偏向锁,可能在进入同步块中不存在竞争,那就会偏向某个线程,那这个线程重复进入当前同步块的时候,不会重复加锁解锁的操作,可以直接进入。
  • 轻量级锁,线程间存在轻微的竞争(线程交替执行,临界区逻辑简单的情况下)。一个线程刚开始在竞争锁的时候,没竞争上,会进行一次CAS,如果CAS成功则获得锁。CAS失败会膨胀变成重量级锁。
  • 重量级锁,存在线程竞争激烈场景,膨胀期间会创建一个monitor对象

synchronized锁优化

偏向锁批量重偏向&批量撤销

当只有一个线程反复进入同步块时,偏向锁带来的性能开销基本可以忽略,但是当有其他线程尝试获得锁时,就需要等到safe point时,再将偏向锁撤销为无锁状态或升级为轻量级,会消耗一定的性能,所以在多线程竞争频繁的情况下,偏向锁不仅不能提高性能,还会导致性能下降。于是,就有了批量重偏向与批量撤销的机制

批量重偏向

当撤销偏向锁阈值超过 20 次后,jvm 会这样觉得,我是不是偏向错了,于是会在给这些对象加锁时重新偏向至加锁线程,重偏向会重置对象 的 Thread ID。

  1. public static void main(String[] args) throws InterruptedException {
  2. //延时产生可偏向对象
  3. Thread.sleep(5000);
  4. // 创建一个list,来存放锁对象
  5. List<Object> list = new ArrayList<>();
  6. // 线程1
  7. new Thread(() -> {
  8. for (int i = 0; i < 50; i++) {
  9. // 新建锁对象
  10. Object lock = new Object();
  11. synchronized (lock) {
  12. list.add(lock);
  13. }
  14. }
  15. try {
  16. //为了防止JVM线程复用,在创建完对象后,保持线程thead1状态为存活
  17. Thread.sleep(100000);
  18. } catch (InterruptedException e) {
  19. e.printStackTrace();
  20. }
  21. }, "thead1").start();
  22. //睡眠3s钟保证线程thead1创建对象完成
  23. Thread.sleep(3000);
  24. System.out.println("打印thead1,list中第20个对象的对象头:");
  25. System.out.println((ClassLayout.parseInstance(list.get(19)).toPrintable()));
  26. System.out.println("打印thead1,list中第50个对象的对象头:");
  27. System.out.println((ClassLayout.parseInstance(list.get(49)).toPrintable()));
  28. // 线程2
  29. new Thread(() -> {
  30. for (int i = 0; i < 40; i++) {
  31. Object obj = list.get(i);
  32. synchronized (obj) {
  33. if (i >= 15 && i <= 21 || i >= 38) {
  34. System.out.println("thread2-第" + (i + 1) + "次加锁执行中\t" +
  35. ClassLayout.parseInstance(obj).toPrintable());
  36. }
  37. }
  38. if (i == 17 || i == 19) {
  39. System.out.println("thread2-第" + (i + 1) + "次释放锁\t" +
  40. ClassLayout.parseInstance(obj).toPrintable());
  41. }
  42. }
  43. try {
  44. Thread.sleep(100000);
  45. } catch (InterruptedException e) {
  46. e.printStackTrace();
  47. }
  48. }, "thead2").start();
  49. LockSupport.park();
  50. }

Thread1创建的50个线程都是偏向锁。
深入理解synchronized(二) - 图1
Thread2

  • 前18次偏向锁撤销,变成轻量级锁

深入理解synchronized(二) - 图2

  • 第19次后执行了批量重偏向

深入理解synchronized(二) - 图3

批量撤销

当撤销偏向锁阈值超过 40 次后,jvm 会认为不该偏向,于是整个类的所有对象都会变为不可偏向的(轻量级锁),新建的对象也是不可偏向的。
注意:时间-XX:BiasedLockingDecayTime=25000ms范围内没有达到40次,撤销次数清为0,重新计时

偏向锁批量重偏向&批量撤销总结

  • 批量重偏向和批量撤销是针对类的优化,和对象无关
  • 偏向锁重偏向一次之后不可再次重偏向
  • 当某个类已经触发批量撤销机制后,JVM会默认当前类产生了严重的问题,剥夺了该类的新实例对象使用偏向锁的权利

自旋优化

重量级锁竞争的时候,还可以使用自旋来进行优化,如果当前线程自旋成功(即这时候持锁线程已经退出了同步块,释放了锁),这时当前线程就可以避免阻塞。
自旋的目的是为了减少线程挂起的次数,尽量避免直接挂起线程

锁粗化

假设一系列的连续操作都会对同一个对象反复加锁及解锁,甚至加锁操作是出现在循环体中的,即使没有出现线程竞争,频繁地进行互斥同步操作也会导致不必要的性能损耗。如果JVM检测到有一连串零碎的操作都是对同一对象的加锁,将会扩大加锁同步的范围(即锁粗化)到整个操作序列的外部。

  1. StringBuffer buffer = new StringBuffer();
  2. public void append(){
  3. buffer.append("aaa").append(" bbb").append(" ccc");
  4. }

上述代码每次调用 buffer.append 方法都需要加锁和解锁,如果JVM检测到有一连串的对同一个对象加锁和解锁的操作,就会将其合并成一次范围更大的加锁和解锁操作,即在第一次append方法时进行加锁,最后一次append方法结束后进行解锁。

锁消除

锁消除即删除不必要的加锁操作。锁消除是Java虚拟机在JIT编译期间,通过对运行上下文的扫描,去除不可能存在共享资源竞争的锁,通过锁消除,可以节省毫无意义的请求锁时间

逃逸分析

逃逸分析,是一种可以有效减少Java 程序中同步负载和内存堆分配压力的跨函数全局数据流分析算法。通过逃逸分析,Java Hotspot编译器能够分析出一个新的对象的引用的使用范围从而决定是否要将这个对象分配到堆上。逃逸分析的基本行为就是分析对象动态作用域.

使用逃逸分析,编译器可以对代码做如下优化:
1.同步省略或锁消除(Synchronization Elimination)。如果一个对象被发现只能从一个线程被访问到,那么对于这个对象的操作可以不考虑同步。
2.将堆分配转化为栈分配(Stack Allocation)。如果一个对象在子程序中被分配,要使指向该对象的指针永远不会逃逸,对象可能是栈分配的候选,而不是堆分配。
3.分离对象或标量替换(Scalar Replacement)。有的对象可能不需要作为一个连续的内存结构存在也可以被访问到,那么对象的部分(或全部)可以不存储在内存,而是存储在CPU寄存器中。