JVM学习--(九)线程安全和锁优化

JVM学习–(九)线程安全和锁优化

一.概述

从以前流行的面向过程编程到现在的面向对象编程,面向对象编程极大的提升了现代软件开发的效率和规模,但是不可避免的是对象之间的相互切换,为了让程序维护的更好更好,我们必须引入“高效并发”来保证并发的正确性、安全性和效率性。

二.线程安全

比较宽泛的定义(没有什么可以操作性):如果一个对象可以被多个线程同时使用,那么它就是线程安全的。
比较严谨并且有操作性的定义:当多个线程同时访问同一个对象时,如果不用考虑这些线程在运行时环境下的调度和交替执行,也需要进行额外的同步操作,或者在调用方法进行任何其他的协调操作,调用这个对象的行为都可以获得正确的结果,那么就称这个对象是线程安全的。

Java语言中的线程安全

为了深入理解,可以把线程安全当做一个非真即假的二元排它项,根据安全程度从强到弱可以分成以下五类:不可变、绝对线程安全、相对线程安全、线程兼容和线程对立。
1.不可变
不可变的对象一定是线程安全的,无论是对象的方法实现还是方法的调用者,都不需要再进行任何的线程安全保障措施。例如final关键词就可以达到这种效果。
Java语言中如果多线程共享的数据是一个基本数据类型,那么定义时的final关键词就可以保证它的不变性,例如String以及Integer等都是通过final来保证不变性。
2.绝对线程安全
一个类要达到“不管运行时环境如何,调用者都不需要任何额外的同步措施”。Java的API中很多声明自己是线程安全的类大多数都不是绝对的线程安全。例如Vector中的get()、remove()方法都是synchronize的,但是不能保证顺序间也是原子性的。
3.相对线程安全
就是我们俗称的线程安全,相比于绝对线程安全,我们需要对于一些特定顺序的连续调用,我们需要调用额外的同步手段来保证调用的正确性。
4.线程兼容
指的是对象本身并不是线程安全的,但是可以通过在调用端正确地使用同步手段来保证对象在并发环境下可以安全地使用。
5.线程对立
指的是无论采取了什么同步措施,都无法在多线程环境中并发地使用代码。

线程安全的实现方法

实现代码线程安全和代码编写有很大的关系,但虚拟机提供的同步和锁机制也起到了至关重要的作用。
1.互斥同步
同步是指多个线程并发访问共享数据时,保证共享数据在同一时刻只被其中的一条线程所使用。互斥就是实现同步的一种手段,临界区、互斥量和信号量都是常见的互斥实现方式。因此互斥是因,同步是果;互斥是方法,同步是目的。
Java中最基本的互斥同步方法就是synchronized关键词,编译之后在同步块的前后会形成monitorenter和monitorexit。在使用的时候需要注意:1️⃣被synchronize修饰的同步块对于同一块线程来说都是可重入的,意味着同一线程反复进入同步块也不会出现自己把自己锁死的情况。2️⃣被synchronize修饰的同步块在持有锁的线程执行完毕并释放锁之前,会无条件地阻塞后面的进程的进入。
因此持有锁是一个重量级的操作。java的线程调换是通过内核线程来完成的,因此会陷入用户态和内核态的调用中,所以一般在不必要的场所会对其进行一定的优化操作。
除了synchronize之外,jdk5之后还提供了J.U.C包,其中的Lock接口成为了java的另一种全新的互斥同步手段。重入锁是Lock接口最常见的一种实现,在用法上与synchronize类似,不过相比多增加了三个新的功能:
1️⃣等待可中断:当持有锁的线程长期不释放的时候,正在等待的线程可以选择放弃,改做其他事情。
2️⃣公平锁:多个线程等待的时候,必须按照申请锁的时间顺序来依次获得锁。
3️⃣锁绑定多个条件:一个重入锁对象可以绑定多个Condition对象。如果和多于一个的条件关联的时候,可以直接添加Condition对象。
在JDK6之后优化,synchronize和重入锁性能上已经基本持平。虽然reentrantLock的性能高于Synchronize,但是还是推荐使用Synchronize,原因:
1️⃣synchronize更为简洁方便。
2️⃣Lock必须手动释放锁,但是synchronized的话即使出了异常,系统会自动的释放。
3️⃣虽然以前lock的性能优于synchronize,但是以后的优化都会优先考虑synchronize。

2.非阻塞同步
互斥同步的主要问题就是线程阻塞和唤醒的开销,因此也被称为“阻塞同步”,解决方案来看,这属于一种“悲观锁”:认为只要不去正确的做同步措施,那么肯定会出问题。随着硬件指令集的发展,我们已经有了另外一个选择:基于冲突检测的乐观并发策略,通俗来讲就是先运行,当出现故障再想方法补救,最常用的补偿方法是不断地重试,直到出现没有竞争的共享数据为止。这种乐观并发策略不再需要把线程阻塞挂起,因此这种同步操作被称为非阻塞同步。
乐观并发策略需要“硬件指令集的发展”,因为我们必须要求操作和冲突检测这两个步骤具备原子性。我们只可以通过硬件来实现,硬件保证了从语义上看起来需要多次操作的行为可以通过一条处理器指令就能完成,这类指令常用的是:
①测试并设置②获取并增加③交换④比较并交换(CAS)⑤加载链接/条件储存(LL/SC)
4,5两条是现代处理器新增的,这两条的目的和功能也是类似的。下面以CAS指令为例来进行分析:
CAS指令需要三个操作数,分别是内存位置(V)、旧的预期值(A)以及准备设置的新值(B )。当V符合A时,处理器才会用B来更新V的值,否则就不执行更新操作。但是无论是否更新了V的值,都会返回V的旧值。jdk9之前只有java类库可以使用CAS,例如J.U.C包中的整数原子类。
CAS并不能涵盖互斥同步的所有使用场景,例如它存在一个逻辑漏洞,称为“ABA”问题。

3.无同步方案
要保证线程安全并非一定需要进行阻塞或者阻塞同步,同步与线程安全之间并没有必要的联系。如果保证一个方法本来就不涉及共享数据,那么自然不需要任何同步措施来保证其确定性。因此有些代码天生就是安全的,例如:
1️⃣可重入代码。又被称为纯代码,指的是代码执行的任何阶段中断它转而运行其他的代码,而在控制权返回后也不会对结果产生什么影响。所有的可重入代码都是线程安全的,但是并不是所有线程安全的代码都是可重入的。比较简单的判定规则:如果一个方法的返回结果是可以预测的,只要输入相同的数据,就能返回相同的结果,那么它就满足可重入的要求。
2️⃣线程本地存储。如果一个线程中的数据必须和其他代码共享,那么就看看这些共享数据的代码是否能保证在同一个线程中执行。如果能够保证,就可以把共享数据的可见性放在一个线程中,这样就可以保证是线程安全的。

三.锁优化

JDK5到JDK6有一个重要的改进项,就是对锁进行了很多优化,这些技术都是为了提高程序的运行速度。

自旋锁与自适应自旋

很多时候锁定状态维持的时间很短,如果为了这段时间去挂起或者恢复线程十分不值得。可以通过让等待的锁执行一个忙循环(自旋)而不是将其阻塞,这就是自旋锁。
JDK6中加入自适应的自旋锁,意味着自旋的时间不是固定的,虚拟机会根据运行情况来设定自旋次数的阈值。

锁消除

指的是虚拟机即时编译器在运行时,对一些代码要求同步,但是对被检测到不可能存在共享数据竞争的锁进行消除。主要判断依据是逃逸分析的数据支持。

锁粗化

原则上是要求同步块越小越好,但是一系列对于同一对象反复的加锁解锁的过程中,频繁的进行互斥同步操作会导致不必要的性能损耗。因此可以考虑将锁粗化。

轻量级锁

JDK6后加入的新型锁机制,“轻量级”是相对于使用操作系统互斥量的传统锁而言的。但是轻量级锁并不能代替重量级锁,设计的初衷是在没有多线程竞争的前提下,减少传统的重量级锁使用操作系统互斥量的性能消耗。
hotspot虚拟机中对象头分为两部分,第一部分储存对象自身的运行时数据,例如哈希码、GC分代年龄等。这部分是实现轻量级锁和偏向锁的关键。另一部分用来储存指向方法区对象类型的指针。在32位的虚拟机中对象在没有被锁定的情况下,2个比特用来存储锁标志位,还有1个比特固定为0,除了未被锁定的情况,还有轻量级锁定、重量级锁定、GC标记等。
工作过程:代码即将进入同步块的时候,如果这个同步对象没有被锁定(01状态),虚拟机首先将在当前线程的栈帧中建立一个名为锁记录的空间,用来存储锁对象目前的MarkWord的拷贝,然后虚拟机将CAS操作尝试把对象的MarkWord更新为LockRecord的指针,如果成功了就代表着线程拥有这个对象的锁,并且对象锁标志位转变为“00”,表示此对象处于轻量级锁定。如果更新操作失败了,就意味着至少存在一条线程和当前线程竞争取得该对象的锁。这时候比对对象的markword是否指向当前线程的栈帧,如果是那么线程已经拥有这个对象的锁;否则说明了这个锁已经被其他线程所占用了。
需要注意的是如果两个线程以上争同一个锁,轻量级锁就不再有效,必须膨胀为重量级锁。
解锁过程也是一样,通过CAS操作,如果对象的markword仍然指向线程的锁记录,就把对象当前的markword和线程复制的DisplacedMark替换回来。
轻量级锁能够提升同步性能的依据是“对于绝大部分的锁,在整个同步周期中都是不存在竞争的”,如果不存在竞争,轻量级锁通过CAS操作成功避免了互斥量的开销,但是如果存在竞争,反而会比重量级锁更慢。

偏向锁

目的是消除数据在无竞争情况下的同步原语,进一步提高程序的运行性能。它的意思是这个锁会偏向于第一个获得它的过程,如果在接下来的执行过程中,该锁一直没有被其他的线程获取,则持有偏向锁的线程将永远不需要同步。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值