【Java虚拟机】线程安全与锁优化

前言

  站在计算机的角度去抽象、解决问题,是面向过程的编程思想;站在现实世界的角度去抽象、解决问题,是面向对象的编程思想。然而计算机世界与现实世界存在一些差异,必须让程序在计算机中正确无误的进行,然后实现高效,即保证并发的正确性和实现线程的安全性。

线程安全

一、定义

  1.线程安全
  当多个线程访问一个对象时,不考虑这些线程在运行时环境下的调度和交替执行,不需进行额外同步,或者在调用方法进行任何其他的协调操作,调用这个对象的行为都可以获得正确的结果,则这个对象是线程安全的。
  2.特征
  代码本身封装了所有必要的正确性保障手段(如互斥同步等),使调用者无需关心多线程的问题,也无需采取任何措施保证多线程的正确调用。

二、Java语言中的线程安全

  1.分类
  按照线程安全的“安全程度”由强至弱排序,将各种操作共享的数据分为5类:不可变、绝对线程安全、相对线程安全、线程兼容和线程对立
  2.具体分析
  • 不可变:不可变(Immutable)的对象一定是线程安全的,final关键字可以体现。
  • 绝对线程安全:定义严格,不管运行时环境如何,调用者都不需要任何额外的同步措施。
  • 相对线程安全:保证对这个对象单独的操作是线程安全的,调用时不需做额外的保障,如Vector、HashTable、Collections的synchronizedCollection()方法包装的集合等。
  • 线程兼容:对象本身不是线程安全的,通过调用端使用同步手段保证并发环境下,对象线程安全,如ArrayList、HashMap等。
  • 线程对立:无论调用端是否采取了同步措施,都无法在多线程环境下并发使用代码。如Thread类的suspend()、 resume()两个方法。

线程安全实现方法

  线程安全似乎是一件由代码如何编写来决定的事情,但虚拟机提供的同步和锁机制也有非常重要的作用,同时更偏重于锁机制。

一、互斥同步

  这是一种常见的并发正确性保障手段。
  1.概念
  同步指在多个线程并发访问共享数据时,同一个时刻只有一个线程使用共享数据;互斥是实现同步的一种手段,临界区、互斥量和信号量是实现互斥的主要方式。互斥是因,同步是果;互斥是方法,同步是目的。
  2.synchronized关键字
  synchronized关键字是实现互斥同步的基本手段,经过编译后,会在同步块前后形成monitorenter和monitorexit两个字节码指令,reference类型的参数致命要锁定和解锁的对象。
  synchronized同步块对同一条线程是重入的,同步块在已进入的线程执行完之前,会阻塞后面其他线程进入。
  Java线程映射到操作系统的原生线程上,要阻塞或唤醒一个线程,都需要操作系统帮忙完成,需要从用户态转换到核心态中,这会耗费很多的处理器时间,状态转换消耗的时间可能比用户代码执行时间还要长,因此synchronized是Java语言中一个重量级(Heavyweight)的操作。
  3.ReentrantLock重入锁
  ReentrantLock也具备线程重入特性,在代码写法上,表现为API层面的互斥锁,lock() 、unlock()配合try/finally语句块完成互斥同步,保证线程安全。
  增加了高级功能,主要有3个:等待可中断、可实现公平锁,以及可以绑定多个条件。
  4.synchronized与ReentrantLock的对比
  JDK1.5的单核和多核处理器情况下,两者的吞吐量对比图,多线程环境下synchronized的吞吐量下降得非常严重,而ReentrantLock则能基本保持在同一个比较稳定的水平上。JDK1.6及以上两者的性能基本持平,提倡在synchronized能实现需求时,优先考虑使用synchronized进行同步。

在这里插入图片描述
在这里插入图片描述
  互斥同步主要问题是进行线程阻塞和唤醒带来的性能问题,这种同步也叫阻塞同步(Blocking Synchronized),属于一种悲观的并发策略。

二、非阻塞同步

  非阻塞同步,是在硬件指令集发展后,基于冲突检测的乐观并发策略。
  1.常用的硬件指令
  测试并设置(Test-and-Set)
  获取并增加(Fetch-and-Increment)
  交换(Swap)
  比较并交换(Compare-and-Swap)
  加载链接/条件存储(Load-Linked/Store-Conditional)
  其中前3条是大多数指令集中的处理器指令,后面的两条是现代处理器新增的。CAS不能涵盖互斥同步的所有使用场景,存在“ABA”问题,大部分情况下ABA问题不会影响程序并发的正确性,如果要解决ABA问题,改用传统的互斥同步会比原子类更高效。
  2.使用CAS操作的方法
  在JDK1.5后,才有使用CAS操作的方法。sun.misc.Unsafe类中的compareAndSwapInt()和compareAndSwapLong等几个方法包装提供。
  3.反射手段或Java API间接使用(AtomicInteger类)

三、无同步方案

  如果一个方法不涉及共享数据,就不需要同步措施保证正确性,一些代码本身是线程安全的,主要有下面两类:
  1.可重入代码
  如果一个方法的返回结果可以预测,输入相同的数据,返回相同的结果,它就满足可重入性要求,即线程安全。
  2.线程本地存储
  共享数据的可见范围限制在同一个线程内,无须同步保证线程间的数据争用后的安全。java.lang.ThreadLocal实现线程本地存储功能。

锁优化

  高效并发是从JDK1.5到JDK1.6的一个重要改进,实现了各种锁优化技术,使得线程之间更高效共享数据,解决共享数据竞争问题,如适应性自旋(Adaptive Spinning)、锁消除(Lock Elimination)、锁粗化(Lock Coarsening)、轻量级锁(Lightweight Locking)和偏向锁(Biased Locking)等。

一、自旋锁与自适应自旋

  1.自旋锁
  有一个以上的处理器,两个或以上的线程同时执行,后面请求锁的线程不放弃处理器的执行时间,执行一个忙循环(自旋),以便持有锁的线程很快释放锁而获取到锁。
  2.自适应的自旋锁
  自旋时间由前一次在同一个锁上的自旋时间及锁的拥有者的状态来决定。
  3.总结
  自旋锁在JDK1.6中默认开启,参数-XX:+UseSpinning开启。在锁被占用的时间短的情况下,适合使用自旋等待,自旋次数默认值是10次,参数-XX:PreBlockSpin控制。
  JDK1.6引入自适应自旋,程序运行和性能监控信息的完善,可以帮助虚拟机对程序锁的状况预测更准确。

二、锁消除

  锁消除在虚拟机即时编译器运行时,对一些代码要求同步,但被检测到不存在共享数据竞争的锁进行消除。
  1.判断依据
  逃逸分析下,判断一段代码中,堆上所有数据都不会逃逸出去,不能被其他线程访问到,则认为他们是线程私有,当做栈上数据对待,同步加锁就无必要。
  2.例子
  String是不可变类,对字符串连接操作总是生成新的String对象进行。JDK1.5之前,StringBuffer对象的append()操作来处理;JDK1.5及以后,会使用StringBuilder对象的append()操作处理。我们发现,StringBuffer.append()中都有一个同步块,锁里面的代码不会逃逸出去,其他线程无法访问到它。因此,在即时编译后,代码中会忽略所有同步而直接执行了。

三、锁粗化

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

四、轻量级锁

  在没有多线程竞争的前提下,使用轻量级锁来减少传统的重量级锁使用操作系统互斥量产生的性能消耗。
  1.虚拟机对象的内存布局
  分为两部分:一部分是存储对象自身的运行时数据,如hashCode、GC分代年龄等,官方称之为“Mark Word”;另一部分是存储指向方法区对象类型数据的指针;若是数组对象,还会有一个额外部分存储数组长度。
  2.轻量级锁执行过程
  (1)未锁定:代码进入同步块时,若同步对象未被锁定(锁标志位01),虚拟机首先在当前线程的栈帧中建立一个名为锁记录(Lock Record)的空间,存储锁对象目前Mark Word的拷贝(Displaced Mark Word),如下图13-3;
  (2)轻量级锁:虚拟机使用CAS操作将对象的Mark Word更新为指向Lock Record的指针,更新动作成功,线程则拥有了该对象锁,对象Mark Word的锁标志位为00,对象处于轻量级锁定状态,如图13-4;
  (3)重量级锁:若CAS更新操作失败,虚拟机首先检查对象的Mark Word是否指向当前线程的栈帧,是则当前线程拥有此对象锁,继续执行同步块代码;否则锁对象被其他线程抢占,两条以上线程争用同一个锁,轻量级锁将膨胀为重量级锁,锁标志状态值为10,Mark Word中存储的是指向重量级锁(互斥量)的指针,后面等待所的线程进入阻塞状态。

在这里插入图片描述

在这里插入图片描述

五、偏向锁

  JDK1.6引入的一项锁优化,在无竞争的情况下把整个同步都消除掉,连CAS操作都不用做。
  1.特点
  偏向于第一个获得偏向锁的线程,执行过程中,如果其他线程没有获取该锁,则当前持有偏向锁的线程不会进行同步。
  2.过程
  默认启用偏向锁,-XX:+UseBiasedLocking,当锁对象第一次被线程获取时,虚拟机将把对象头中标志位设为01,偏向模式。使用CAS操作把获取到偏向锁的线程ID记录在对象的Mark Word中;
  CAS操作成功,持有偏向锁的线程每次进入此锁相关的同步块时,不进行任何同步操作(如Locking、Unlocking及对Mark Word的Update等);
  其他线程尝试获取偏向锁时,偏向模式结束。
  3.偏向锁、轻量级锁的状态转换
  根据锁对象目前是否处于被锁定状态,撤销偏向(Remove Bias)后恢复到未锁定(标志位01)或轻量级锁定(标志位00)的状态,后续的同步操作如轻量级锁执行过程。

在这里插入图片描述

小结

  线程安全的概念和分类、同步实现的方式及虚拟机底层运作原理,一系列锁优化措施实现高效并发,这些知识是高级程序员必备知识之一。
感谢您的访问!
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章