深入理解Java虚拟机笔记(十三)

线程安全与锁优化

线程安全

当多个线程访问一个对象时,如果不用考虑这些线程在运行环境下的调度和交替执行,也不需要进行额外的同步,或者在调用方进行任何其他的协调操作,调用这个对象的行为都可以获得正确的结果,那这个对象是线程安全的。

Java语言中的线程安全

安全程度由强至弱,共享数据分类:

共享数据类型安全性实现方式
不可变(Immutable)一定线程安全,只要一个不可变对象被正确构建出来(没出现this引用逃逸情况),外部可见状态用于不变基本数据类型,定义时使用final关键字修饰;对象,保证对象的行为不会对其状态发生任何影响,将对象中带有状态的变量声明为final
绝对线程安全不管运行环境如何,调用者都不需要任何额外的同步措施
相对线程安全对这个对象单独的操作是线程安全的,我们在调用时不需要做额外的保障措施,但对于一些特定顺序的连续调用,就可能需要在调用端使用额外的同步手段来保证调用的正确性Vector、HashTable、Collections的synchronizedCollection()方法包装的集合等
线程兼容对象本身并不是线程安全的,但可以通过在调用端正确地使用同步手段来保证对象在并发环境中可以安全地使用ArrayList、HashMap等
线程对立无论调用端是否采用了同步措施,都无法再多线程环境中并发使用地代码Thread类的uspend()和resume()方法,System.setIn()、System.setOut()、System.runFinalizersOnExit()等

线程安全的实现方式

实现方式原理具体方式Java实现手段缺陷
互斥同步(阻塞同步)同步是指多个线程并发访问共享数据时,保证共享数据在同一时刻只被一个或一些线程使用,互斥是实现同步的一种方式临界区,互斥量、信号量synchronized,java.util.concurrent包中的重入锁(RenentrantLock)线程阻塞和唤醒带来的性能问题,悲观的并发策略,无论并发数据是否真的出现竞争,都进行加锁、用户态内核态转换、维护锁计数器和检查是否有被阻塞的线程需要唤醒等操作
非阻塞同步乐观并发策略,先进行操作,如果没有其他线程争用共享数据,那操作就成功,如果有争用,产生冲突,就采用其他的补偿措施依赖硬件使操作和冲突检测具有原子性处理器指令有,测试并设置(Test-and-Set),获取并增加(Fetch-and-Increment),交换(Swap),比较并交换(Compare-and-Swap),加载链接/条件存储(Load-Linked/Store-Conditional)
无同步方案某些代码天生线程安全可重入代码(纯代码),线程本地存储

synchronized

原生语法层面的互斥锁。
synchronized关键字经过编译后,在同步块前后分别形成monitorenter和monitorexit两个字节码指令。
执行monitorenter指令时,首先尝试获取对象的锁,如果对象没被锁定,或者当前线程已经拥有了那个对象的锁,把锁的计数器加1,执行monitorexit时减1,计数器为0时,锁被释放。如果获取对象锁失败,当前线程要阻塞等待,直到对象锁被另一个线程释放为止。
synchronized同步块对同一线程可重入,不会自己把自己锁死;同步块在已进入的线程执行完之前,会阻塞后面其他线程的进入。
阻塞或唤醒需要从用户态转换到内核态。虚拟机优化,在阻塞之前自旋等待,避免频繁切换。

ReentrantLock

API层面的互斥锁,lock和unlock配合try/finally语句块完成。
高级功能:

  • 等待可中断:持有锁的线程长期不释放锁的时候,正在等待的线程可以选择放弃等待,改为处理其他事情。
  • 公平锁:多个线程等待同一个锁时,按申请锁的时间顺序依次获得锁。
  • 锁可以绑定多个条件:一个ReentrantLock对象可以同时绑定多个Condition对象。

CAS

3个操作数:内存位置V,旧的预期值A,新值B。
当且仅当V符合旧预期值A时,处理器用新值B更新V的值,否则不执行更新,无论是否更新了V的值,都会返回 V的旧值。
sun.misc.Unsafe类中,不是提供给用户程序调用的类,只有启动类加载器加载的Class才能访问。
访问方式:反射,J.U.C中的原子类
问题:ABA问题,一个变量V初次读取时时A值,在这期间被改成B值,后又被改回A,此时进行赋值检查到它仍是A值,误认为它从未被改变过。解决方式:带标记的原子引用类AtomicStampedReference

可重入代码(纯代码)

可以在代码执行的任何时刻中断它,转而去执行另外一段代码,而在控制权返回后,原来的程序不会出现任何错误,所有的可重入的代码都是线程安全的,但并非所有线程安全的代码都是可重入的。
特征:不依赖存储在堆上的数据和公用的系统资源、用到的状态量都由菜属中传入、不调用非可重入的方法等。
判断原则:如果一个方法的返回结果使可以预测的,只要输入了相同的数据,就能返回相同的结果,那它就满足可重入性的要求,线程安全。

线程本地存储

如果一段代码中的数据必须与其他代码共享,如果这些共享数据的代码能保证在同一个线程中执行,就可以把共享数据的可见范围限制在同一个线程之内,无需同步也能保证线程之间不出现数据争用问题。
应用场景:消费队列、Thread-per-Request
实现方式:java.lang.ThreadLocal类,每个线程的Thread对象都有一个ThreadLocalMap对象,存储了以threadLocalHashCode为键,以本地线程变量为值的K-V值对。

锁优化

自旋锁

后面请求锁的线程执行忙循环
优点:避免线程切换的开销
缺点:如果锁占用时间长,浪费处理器资源

自适应自旋

自旋时间不再固定,由前一次在同一个锁上的自旋时间及锁的拥有者状态决定,如果在同一个锁对象上自选等待刚成功获得过锁,并且持有锁的线程正在运行,虚拟机认为这次自旋也很可能成功,允许自选等待持续相对更长的时间,如果对于某个锁,自旋很少成功获得,以后获取这个锁将可能省略自旋。

锁清除

虚拟机即时编译器在运行时,对一些代码上要求同步,但被检测到不可能存在共享数据竞争的锁进行清除。
判定依据:逃逸分析的数据支持,如果判断在一段代码中,堆上的所有数据都不会逃逸出去从而被其他线程访问到,那就可以把它们当成栈上数据来对待。

锁粗化

如果虚拟机探测到有一串零碎的操作都对同一个对象加锁,将会把加锁同步的范围扩展到整个操作序列的外部。

轻量级锁

在没有多线程竞争的前提下,减少传统的重量级锁适用操作系统互斥量产生的性能消耗。
加锁过程:

  1. 如果对象没被锁定(对象头信息mark word中锁标志位为01),虚拟机首先在当前线程的栈帧中建立一个名为锁记录(lock record)的空间,存储锁对象目前的mark word的拷贝(displaced mark word)
  2. 虚拟机用CAS操作尝试将对象的mark word更新为指向lock record的指针。如果更新成功,这个线程获得了该对象的锁,对象mark word的锁标志位变为00
  3. 如果更新失败,检查对象的mark word是否指向当前线程的栈帧,如果指向说明当前线程已拥有这个对象的锁,直接进入同步块继续执行,否则说明这个锁对象被其他线程抢占了。如果有两条以上的线程争用同一个锁,膨胀为重量级锁,锁标志位变为10,mark word存储指向重量级锁(互斥量)的指针,后面等待锁的线程阻塞。
    解锁过程:
    如果对象的mark word仍指向线程的锁记录,用CAS操作把对象当前的mark word和线程中复制的displaced mark word替换回来,如果替换成功,同步过程完成,如果替换失败,说明有其他线程尝试过获取该锁,在释放锁的同时,唤醒被挂起的线程。
    优点:如果没有竞争,轻量级锁使用CAS操作避免了使用互斥量的开销
    缺点:如果过存在锁竞争,除了互斥量的开销外,还额外发生了CAS操作

偏向锁

消除数据在无竞争情况下的同步原语
锁偏向于第一个获得它的线程,如果在接下来的执行中,该锁没有被其他线程获取,则持有偏向锁的线程永远不需要再进行同步。
加锁过程:锁对象第一次被线程获取时,虚拟机将对象头中的标志位设为01,即偏向模式,使用CAS操作把获得这个锁的线程ID记录在对象的mark word中,如果CAS成功,持有偏向锁的线程以后每次进入这个锁相关的同步块时,虚拟机都可以不再进行任何同步操作。当有另一个线程尝试获取这个锁时,偏向模式宣告结束,根据锁对象目前是否被锁定,撤销偏向后恢复到未锁定(01)或轻量级锁定(00)状态。
在这里插入图片描述
优点:提高带有同步但无竞争的程序性能


版权声明:本文为buxiangqumingzi原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接和本声明。