深入理解Java虚拟机13–线程安全与锁优化
一、线程安全
当多个线程同时访问一个对象时,如果不用考虑这些线程在运行时环境下的调度和交替执行,也不需要进行额外的同步,或者在调用方进行任何其他的协调操作,调用这个对象的行为都可以获得正确的结果,那就称这个对象是线程安全的
二、Java语言中的线程安全
1.不可变
在Java语言里面不可变对象一定是线程安全的,无论是对象的方法实现还是方法的调用者,都不需要在进行任何线程安全保护措施
final关键字带来的可见性
只要一个不可变的对象被正确的构建出来(即没有发生this引用逃逸),那外部的可见状态永远都不会改变,永远都不会看到它在多个线程之中处于不一致的状态。“不可变带来的安全性是最直接、最纯粹的”
final变量要么在定义的时候就设置好初始值,要么在构造函数中设置初始值,final修饰的变量不能有set方法
Java语言中,如果多线程共享的是一个基本数据类型,那么只要在定义时使用final关键字修饰它就可以保证它是不可变的。如果是一个对象,保证对象行为不影响自己的状态的途径有很多种,最简单的一种就是把对象里面带有状态的变量都申明为final
String和所有的包装类型都是Final修饰的
AtomicInteger和AtomicLong则是可变的,因为Atomic保证了操作的原子性是CAS的
三、线程安全的实现方法
1.互斥同步
互斥同步是一种最常见也是最主要的并发正确性保障手段。同步是指在多个线程并发访问共享数据时,保证共享数据在同一时刻只被一条线程使用。而互斥是实现同步的一种手段,临界区、互斥量和信号量都是常见的互斥实现方式。因此互斥是因,同步是果,互斥是方法,同步是目的。
在Java里面,最基本的互斥同步手段就是synchronized关键字,这是一种结构块的同步语法。synchronized关键字经过Javac编译后,会在同步块前后分别形成monitorenter和monitorexit这两个字节码指令。如果Java源码中的synchronized明确指定了对象参数,那就以对象的引用作为refrence;如果没有明确指定,那将根据synchronized修饰的方法类型(如实例方法或类方法),来决定是取代码所在对象实例还是取类型对应Class对象来作为线程要持有的锁
被synchronized修饰的同步块对同一线程来说是可重入的。这意味着同一线程反复进入同步块也不会出现把自己锁死的情况
被synchronized修饰的同步块在持有锁的线程执行完毕并释放之前,会无条件地阻塞后面其他线程的进入。这意味着无法像处理某些数据库中的锁那样,强制已获取锁的线程释放锁;也无法强制正在等待锁的线程中断或超时退出
synchronized
缺点
- 1.持有锁是是一个重量级操作,线程切换耗费资源和时间
- 2.无法强制线程释放锁
- 3.无法强制等待锁的进程中断或退出
- 4.非公平锁
虚拟机本身对synchronized的优化
- 1.自旋
- 2.后面讲到的锁优化
可重入性是指一条线程能够反复进入被它直接持有锁的同步块的特性,即锁关联的计数器,如果持有锁的线程再次获得它,则将计数器的值加一,每次释放锁时计数器的值减一,当计数器为零时,才能真正释放锁
ReentrantLock
重入锁(ReentrantLock)是Lock接口最常见的一种实现,顾名思义,它与synchronized一样是可重入的。ReentrantLock与synchronized相比增加了一些高级功能,只要有以下三项:
- 1.等待可中断
- 2.可实现公平锁
- 3.可以绑定多个条件
如果要基于等待可中断、可实现公平锁及可以绑定多个条件三个条件考虑,ReentrantLock是一个很好的选择,JDK6之后加入了大量对synchronized的优化(偏向锁、轻量级锁、重量级锁),性能已经不在是选择synchronized或者ReentrantLock的决定因素。
面试:synchronized和ReentrantLock的选择
1.回答synchronized的特点和缺点
2.回答ReentrantLock的特点
3.回答锁优化(偏向锁、轻量级锁、CAS、自旋、重量级锁)
Java中的ReentrantLock和synchronized两种锁定机制的对比
Lock接口详解
2.非阻塞同步
互斥同步说面临的主要问题是进行线程阻塞和唤醒所带来的性能开销,因此这种同步也被称为阻塞同步。从解决问题的方式上看,互斥同步是属于一种悲观的并发策略,其总是认为只要不去做正确的同步措施(例如加锁),那就肯定会出现问题,无论共享的数据时候真的出现竞争,它都会进行加锁。
非阻塞同步:基于冲突检测的乐观锁并发策略,通俗的说就是不管风险,先进行操作,如果没有其他线程争用共享的数据,那就直接成功了;如果共享的数据的确被争用,产生了冲突,那在进行其他的补救措施,最常用的补救措施是不断进行重试,直到出现没有竞争的共享数据为止。
CAS(比较并交换)
3.无同步方案
ThreadLocal
synchronized的实现原理
1.对象头
2.Monotor
3.对象头是锁优化的基础
三、锁优化
适应性自旋、锁消除、锁膨胀、轻量级锁、偏向锁
自旋锁与自适应自旋
- 自旋:如果物理机器有一个以上的处理器或者处理器核心,能让两个或者以上的线程同时并行执行,我们就可以让后面请求锁的那个线程“稍等一会”,但不放弃处理器执行时间,看看持有锁的线程是否很快就会释放锁。为了让线程等待,我们只须让线程执行一个忙循环(自旋),这项技术就是所谓的自旋锁
- 自适应自旋:自适应意味着自旋时间不再是固定的了,而是由前一次在同一个锁上自旋时间及锁的拥有者的状态来决定的。如果在同一个锁对象上,自旋等待刚刚成功获得锁,并且持有锁的线程正在运行中,那么虚拟机就会认为这次自旋也很有可能再次成功,进而允许自旋等待时间持续相对更长时间。另一方面,如果对于某个锁,自旋很少成功获得过锁,那在以后要获得这个锁时将有可能直接省略掉自旋过程,以避免浪费处理器资源
锁消除
锁消除的主要判断依据来源于逃逸分析的数据支持,如果判断到一段代码中,在堆上的所有数据都不会逃逸出去被其他线程访问到,那就可以把它们当作栈上数据对待,认为它们是线程私有的,同步加锁自然就无需再进行
锁粗化
如果一些列的连续操作都对同一个对象反复加锁和解锁,甚至加锁操作是出现在循环体之中的,那即使没有使用线程竞争,频繁地进行互斥同步操作也会导致不必要的性能损耗,如SpringBuilder::append,如果虚拟机探测到有这样一串零碎的操作都对同一个对象加锁,将会把加锁同步范围扩展(粗化)到整个操作序列的外部
偏向锁
如果锁轻量级锁是在无竞争的情况下使用CAS操作去消除同步使用的互斥量,那偏向锁就是在无竞争的情况下把整个同步都消除,连CAS操作都不去做了。
偏向锁会偏向于第一个获取它的线程,如果在接下来的执行过程中,该锁一直没有被其他线程获取,则持有偏向锁的线程将永远不需要再进行同步
当锁对象第一次被线程获取的时候,虚拟机将会把对象头中的标志位设置为“01”,把偏向模式设置为“1”,表示进入偏向模式。同时使用CAS操作把获取到这个锁的线程ID记录在Mark Word之中。如果CAS操作成功,持有偏向锁的线程以后每次进入这个锁相关的同步块时,虚拟机都可以不再进行任何同步操作(例如加锁、解锁及对Mark Work的更新操作等)
一旦出现另一个线程尝试获取锁的情况,偏向模式就马上宣告结束。