JVM学习笔记八:线程安全与锁优化

线程的实现

线程的实现分为内核线程实现、用户线程实现、混合实现。

  1. 内核实现就是直接由操作系统内核支持的线程,各种操作,如创建、销毁、同步都需要进行系统调用,需要在用户态和内核态中来回切换,代价较高,另外内核线程需要消耗一定的内核资源,所以一个系统支持的线程数量是有限的。
  2. 用户线程是由用户态自己实现的,线程管理的开销很低,理论上也支持更大的线程规模,缺点是实现复杂。
  3. 混合模式 java线程的实现在jdk1.2之前是采用用户线程实现的、但是在1.2之后切换为内核线程实现。

Java线程调度

线程调度分为协同式和抢占式

协同式:是指线程的执行时间由线程本身控制,线程把自己的工作执行完之后,要主动通知系统切换到其他线程上。

抢占式:是指线程的执行时间由系统调度分配,但是线程可以让出执行时间。

线程优先级

Java设置了10个级别的线程优先级,级别越高,越有机会被CPU分配工作时间,但是由于java的线程是采用内核线程实现的,线程调度最终还是依赖于各平台的内核线程调度,虽然各平台都有线程优先级的机制,但是级别的设定不一定与Java一一对应,在Solaris中有2的32次方种,在windows中有7种,这种比java优先级种类少的平台上,有可能java不同的线程优先级对应windows上同一种优先级。

线程状态转换

java定义了5种线程状态:新建New、运行Runing、无限期等待Waiting、有限期等待Timed Waiting、阻塞Blocked、Terminated结束。

线程安全

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

线程安全的实现方法

互斥同步

临界区Critical Section、互斥量 Mutex、信号量 Semaphore都是主要的互斥实现方式。Java最基本的互斥同步手段就是synchronize,如果是同步代码块,JVM会在同步快的前后生成monitorenter和monitorexit这两个字节码指令,这两个字节码指令需要一个reference类型的参数来指明需要锁定和解锁的对象,如果使用的是被修饰的实例方法,那么会隐式的使用实例对象作为所对象,如果是静态方法,则使用类型对象作为锁对象。

另外JVM规定,执行monitorenter指令时,首先尝试获取对象锁,如果成功或者已经拥有了那个对象的锁(可重入锁),把锁的计数加1,相应的,执行monitorexit时计数器减1。当计数器为0时,锁就释放。如果获取对象锁失败,那当前线程就要阻塞等待,直到拥有锁的线程释放为止。

可重入锁 ReentrantLock

顾名思义,它是可以多次获取同一个对象的锁的,代码写法与synchronize有区别,除了能提供synchronize的功能外还可以提供其他三种功能:等待可中断、可实现公平锁(按照申请锁的时间来依次获得锁,如:synchronize是非公平的)、以及锁可以绑定多个条件。

非阻塞同步

互斥同步是一种悲观的并发策略,总是认为只要不去做正确的同步就会出现问题,无论是否真的出现竞争,它都会进行加锁,在冲突不多的情况下,这很浪费执行性能,因此一种基于冲突检测的乐观并发策略产生,乐观的先进行操作,如果成功了,就顺利执行,如果共享数据存在争用,产生了冲突,那就再采取其他的补偿措施(最常见的补偿措施就是不断重试,知道成功为止),这种乐观的并发策略不需要把线程挂起,因此这种同步操作称为飞阻塞同步。乐观并发策略需要更底层的硬件支持,因为我们需要冲突检测和操作执行这两个步骤具备原子性,如果再使用互斥同步来保证就没有意义了(鸡生蛋还是蛋生鸡的问题),我们需要的是CPU一个指令来完成这两个操作,这类常用指令有:

  1. 测试并设置 Test-and-Set
  2. 获取并增加 Fetch-and-Increment
  3. 交换 Swap
  4. 比较并交换 Compare-and-Swap
  5. 加载链接/条件存储 Load-Linked/Store-Conditional,下文称LL/SC

其中后两条是现代处理器新增的,功能上是相似的,都可以达到我们的目的,IA64、x86指令集中有cmpxchg指令完成CAS功能,在sparc-TSO也有casa指令实现,而在ARM和PowerPC架构下,则需要使用一对ldres/strex指令来完成LL/SC功能。

CAS的作用与应用

CAS需要三个操作数,分别是待修改的变量的地址、旧的期望值、新值,CAS指令执行时,当且仅当变量的值等于期望值时,才会更新变量为新值,否则不进行更新,但无论是否进行了更新,都会将变量的旧值作为结果返回。

JDK中的CAS由sun.misc.Unsafe提供,包括compareAndSwapInt()和compareAndSwapLong(),在JVM内部对这些方法的调用做了特殊处理,即使编译出来的结果就是一条平台相关的CAS指令。但是由于Unsafe类被限制为只有启动类加载器Bootstrap ClassLoader加载的Class才能调用它,所以我们只能使用Jdk提供的其他API间距的使用它,如JUC包里的Atomic系列类。

虽然CAS能够提供不错的性能,但是它存在一个漏洞,即它只能根据当前的值判断变量有没有被修改,但是变量在这期间有没有被修改就无从得知了,这个漏洞被称为CAS操作的“ABA”问题,JUC包为了解决这个问题,提供了一个带有标记的原子引用类,“AtomicStampedReference”,它可以通过控制变量值的版本来保证CAS的正确性。

无同步方案

包括可重入代码和线程本地存储 ThreadLocal

锁优化

包括适应性自旋锁、锁消除、锁粗化、轻量级锁、偏向锁

自旋锁是指当前一个线程已经获得锁,后一个线程稍等一会,但不放弃处理器的执行时间,等待有锁的线程释放锁,为了让线程等待,让线程执行一个忙循环(自旋)。JDK1.6默认开启了自旋锁,自旋锁的默认自旋次数为10次,用户可以使用参数-XX:PreBlockSpin来指定。所谓适应性自旋锁,是由JVM来自动调节自旋次数,上一次成功后,使用更长的自旋时间,如果失败了就相应减少时间,甚至取消自旋。

锁清除

通过逃逸分析,检测到代码不可能存在冲突,就会在即时编译阶段对锁进行消除。

锁粗化

原则上锁范围越小越好,但是对于一个区域内对同一对象连续获取释放锁,或循环获取释放锁,是性能的浪费,在这种情况下,虚拟机会将锁同步的范围粗化到整个操作序列外部。

轻量级锁

轻量级锁是在没有多线程竞争的前提下,减少传统重量级锁使用操作系统互斥量产生的性能消耗。在代码进入同步代码块的时候,如果同步对象没有被锁定(锁标记位为01),虚拟机首先在当前线程的栈帧中建立一个名为锁记录Lock Record的空间,用于存储对象目前的Mark Word的拷贝,然后虚拟机将使用CAS操作尝试将对象的MarkWord更新为Lock Record的指针,如果更新成功,这个线程就拥有了这个对象的锁,锁标记位转变为00,即表示此对象处于轻量级锁定状态。如果更新操作失败了,虚拟机首先会检查对象的Mark World是否指向栈帧,如果是说明当前线程拥有了这个对象的锁,那就可以直接进入同步代码块继续执行,否则说明其他线程已经枪占了这个对象的锁,如果有两条以上的线程争同一个锁,那轻量级锁不再有效,要膨胀为重量级锁,锁的标志位状态值变为10,Mark World中存贮的就是指向重量级锁的指针,后续等待锁的线程也要进入阻塞状态。

偏向锁

目的是消除在无竞争条件下的同步原语,比起轻量级锁的CAS操作,偏向锁完全消除了同步,前提是只针对第一个获得它的线程,如果接下来没有其他线程获取,则持有偏向锁的线程将永远不需要再进行同步。

但是如果程序中大多数的锁总是被多个线程访问,那偏向锁就是多余的,有时候使用参数-XX:-UseBiasedLocking来禁止偏向锁优化反而可以提高性能。

参考资料

本文参考:《深入理解Java虚拟机》

原文地址:https://www.cnblogs.com/MinnieChang/p/7350423.html