JVM高效并发----线程安全与锁优化

目录

1. 概述

2. 线程安全

2.1 Java中的线程安全

2.2 线程安全的实现方法

3. 锁优化

3.1 自旋锁与自适应自旋

3.2 锁消除

3.3 锁粗化

3.4 轻量级锁

3.5 偏向锁

参考:


1. 概述

如何实现并发的正确性,如何实现高效性

2. 线程安全

2.1 Java中的线程安全

按照安全程度由强到弱:

  • 不可变

不可变的对象一定线程安全。

例如:String、枚举类型、java.lang.Number 的部分之类等

  • 绝对线程安全

达到“不管运行时环境如何,调用者带哦用不需要任何额外的同步操作”。

达到上述要求需要的代价很大甚至不切实际。

  • 相对线程安全

我们通常意义上所讲的线程安全。,需要保证这个多项单独的线程操作是安全的,我们在调用是不要用做额外的保障措施。

Vector、HashTable等

  • 线程兼容

本身不是线程安全的,通过在调用段正确使用同步手段来保证对象在不能够发环境中能够安全使用。

通常所说的线程不安全

  • 线程对立

无论调用段采取怎样的同步措施,都无法在多线程环境下并发使用的代码。

如Thread 类的suspend() 和resume()

2.2 线程安全的实现方法

  • 互斥同步(阻塞同步)

互斥是方法,同步是牧师

synchronized 关键字:

<> 编译后在同步快前后分别形成monitorenter 和monitorexit这两个字节码指令,每一个都需要一个reference 类型的参数来表明要锁定和解锁的对象。

<>重量级操作,在阻塞和唤醒线程时,都需要操作系统帮忙,需要从用户态到内核态

ReentrantLock 实现同步(重入锁)

<>等待可中断,可以放弃等待

<>公平锁 先来后到

<> 绑定多个条件。

  • 非阻塞同步

基于冲突检测的乐观并发检测:先进行操作,如果没有其他线程竞争共享数据,那么操作成功;如果有共享数据争用,在采取弥补措施(最常见的是不断重试,直到成功)

CAS:比较并交换(Compare-and-Swap,CAS)

ABA 问题:如果一个变量初次读取的时候是 A 值,它的值被改成了 B,后来又被改回为 A,那 CAS 操作就会误认为它从来没有被改变过。

  • 无同步方案

有一些代码天生就是线程安全的:

可重入代码:可以在代码执行的任何时刻中断它,转而去执行另外一段代码(包括递归调用它本身),而在控制权返回后,原来的程序不会出现任何错误。

线程本地存储: 如果一段代码中所需要的数据必须与其他代码共享,那就看看这些共享数据的代码是否能保证在同一个线程中执行。如果能保证,我们就可以把共享数据的可见范围限制在同一个线程之内,这样,无须同步也能保证线程之间不出现数据争用的问题。

栈封闭: 多个线程访问同一个方法的局部变量时,不会出现线程安全问题,因为局部变量存储在虚拟机栈中,属于线程私有的。

3. 锁优化

3.1 自旋锁与自适应自旋

  • 自旋锁,让线程执行一个忙循环,而不直接挂起。只适用于共享数据的锁定状态很短的场景。
  • 自适应的自旋锁:自适应意味着自旋的次数不再固定了,而是由前一次在同一个锁上的自旋次数及锁的拥有者的状态来决定。

3.2 锁消除

  • 主要依据:逃逸分析的数据支持。如果堆上的共享数据不可能逃逸出去被其它线程访问到,那么就可以把它们当成私有数据对待,也就可以将它们的锁进行消除。
  • 对于被检测出不可能存在竞争的共享数据的锁进行消除。

3.3 锁粗化

  • 如果一系列的连续操作都对同一个对象反复加锁和解锁,频繁的加锁操作就会导致性能损耗。
  • 如果虚拟机探测到有这样一串零碎的都对同一对象加锁,将会把加锁同步的范围扩大到整个操作序列的外部。

3.4 轻量级锁

  • 锁拥有了四个状态:无锁状态(unlocked)、偏向锁状态(biasble)、轻量级锁状态(lightweight locked)和重量级锁状态(inflated)。
  • HotSpot 虚拟机对象头的内存布局,这些数据被称为 Mark Word。其中 tag bits 对应了五个状态,这些状态在右侧的 state 表格中给出。

JVM高效并发----线程安全与锁优化_第1张图片

  • 轻量级锁是相对于传统的重量级锁而言,它使用 CAS 操作来避免重量级锁使用互斥量的开销。对于绝大部分的锁,在整个同步周期内都是不存在竞争的,因此也就不需要都使用互斥量进行同步,可以先采用 CAS 操作进行同步,如果 CAS 失败了再改用互斥量进行同步。
  • 轻量级锁提升性能的依据:对于绝大部分的锁,在整个同步周期内都是不存在竞争的。这是一个经验数据。
  • 加锁过程:

尝试获取一个锁对象,如果锁对象标记为 0 01,说明锁对象的锁未锁定(unlocked)状态。此时虚拟机在当前线程的虚拟机栈中创建 Lock Record,然后使用 CAS 操作将对象的 Mark Word 更新为 Lock Record 指针。如果 CAS 操作成功了,那么线程就获取了该对象上的锁,并且对象的 Mark Word 的锁标记变为 00,表示该对象处于轻量级锁状态。

如果 CAS 操作失败了,虚拟机首先会检查对象的 Mark Word 是否指向当前线程的虚拟机栈,如果是的话说明当前线程已经拥有了这个锁对象,那就可以直接进入同步块继续执行,否则说明这个锁对象已经被其他线程线程抢占了。如果有两条以上的线程争用同一个锁,那轻量级锁就不再有效,要膨胀为重量级锁。

JVM高效并发----线程安全与锁优化_第2张图片

  • 解锁过程:

解锁过程也是通过CAS操作来进行的,如果对象的Mark Word 仍然指向线程的锁记录,那就用CAS操作吧对象当前的Mark Word 和线程中复制的Displaced Mark Word替换回来,如果替换成功,整个同步过程就完成了;如果失败,说明有其他线程尝试获取该锁,那就释放锁,唤醒被挂起的线程。

3.5 偏向锁

  • 目的:消除数据在无竞争情况下的同步原语。
  • 偏:偏向锁的思想是偏向于让第一个获取锁对象的线程,这个线程在之后获取该锁就不再需要进行同步操作,甚至连 CAS 操作也不再需要。

当锁对象第一次被线程获得的时候,进入偏向状态,标记为 1 01。同时使用 CAS 操作将线程 ID 记录到 Mark Word 中,如果 CAS 操作成功,这个线程以后每次进入这个锁相关的同步块就不需要再进行任何同步操作。

当有另外一个线程去尝试获取这个锁对象时,偏向状态就宣告结束,此时撤销偏向(Revoke Bias)后恢复到未锁定状态或者轻量级锁状态

 

参考:

https://cyc2018.github.io/CS-Notes/#/notes/Java%20%E5%B9%B6%E5%8F%91?id=%E8%87%AA%E6%97%8B%E9%94%81

《深入理解Java虚拟机》

你可能感兴趣的:(JVM高效并发----线程安全与锁优化)