深入了解jvm-2Edition-线程安全与锁优化
1、什么是线程安全
Brian Goetz 在 《Java Concurrency In Practice》中定义的线程安全为:
“当多线程访问一个对象时,无论这些线程在运行时环境下以何种方式调度和交替执行,
在使用对象时,都不需要任何额外的同步,就可以得到正确的行为和结果,那么这个对象就是线程安全的。”
Java中的线程安全可分为:
1、不可变
对象被安全构造(安全发布)后状态永不改变,那么永远是线程安全的。
2、绝对线程安全
满足Brian Goetz 的线程安全的定义。
3、相对线程安全
只保证对对象单独的操作是线程安全的,复合操作还是要进行同步。
4、线程兼容
对象本身不是线程安全的,但是可以通过调用端使用同步来保证安全使用。
5、线程对立
无论是否采取同步,都无法安全并发使用的代码。
如:Thread.suspend() 和 Thread.resume() 有死锁风险。
2、线程安全实现方法
1、互斥同步-阻塞同步
通过使用互斥来保证同步。
临界区、互斥量、信号量。
synchronized同步代码块:
可重入、阻塞或唤醒需要操作系统完成。非公平,只能绑定一个唤醒条件。
ReentrantLock:
可重入,等待可中断、可实现公平、可绑定多个唤醒条件。
因为jdk对synchronized做了优化,能用synchronized实现需求时尽量使用synchronized。
2、非阻塞同步
阻塞同步是一种悲观的并发策略,认为只要没有正确的同步措施,就一定会出现问题。
非阻塞同步则是乐观的,基于冲突检测,没有冲突就算成功,有冲突就解决冲突(重新试)。
乐观并发策略需要硬件帮助,因为要保证操作和检测冲突是原子的。
常用的原子指令有:
Test-and-Set;
Fetch-and-Increment;
Swap;
Compare-and-Swap;
Load-Linked / Store-Conditional。
import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicInteger; public class ConcurrencyWithAtomic { private static final int THREADS_COUNT=20; public static AtomicInteger race=new AtomicInteger(0); public static void increase(){ race.incrementAndGet(); } public static void main(String[] args) throws InterruptedException { ExecutorService executorService= Executors.newCachedThreadPool(); for(int i=0; i<THREADS_COUNT; i++) { executorService.execute(()->{ for(int j=0; j<THREADS_COUNT; j++) increase(); }); } while(!executorService.awaitTermination(1000, TimeUnit.MILLISECONDS)) executorService.shutdown(); System.out.println(race); } }
其中incrementAndGet的源码为:
/** * Atomically increments the current value, * with memory effects as specified by {@link VarHandle#getAndAdd}. * * <p>Equivalent to {@code addAndGet(1)}. * * @return the updated value */ public final int incrementAndGet() { return U.getAndAddInt(this, VALUE, 1) + 1; } /** * Atomically adds the given value to the current value of a field * or array element within the given object {@code o} * at the given {@code offset}. * * @param o object/array to update the field/element in * @param offset field/element offset * @param delta the value to add * @return the previous value * @since 1.8 */ @IntrinsicCandidate public final int getAndAddInt(Object o, long offset, int delta) { int v; do { v = getIntVolatile(o, offset); } while (!weakCompareAndSetInt(o, offset, v, v + delta)); return v; }
可以看到,getAndAddInt(Object o, long offset, int delta) 方法是实现了栈封闭的。
3、无同步方案
可重入代码,栈封闭;
ThreadLocal实现线程封闭。
3、锁优化
1、旋转锁和自适应锁
由于在挂起和恢复线程时,需要陷入内核,因此开销较大。
因此,选择在锁持续时间短时,采用忙等待的形式。
自适应锁就是虚拟机会自动设置忙等待的循环次数。
2、锁消除
由逃逸分析数据支持,在实现了栈封闭时,就不需要锁了。
这时候就可以将锁消除掉。
3、锁粗化
我们写代码时,为了减少同步操作的数量,总是习惯将同步代码块缩小。
但是,如果同步代码块小而密集的时候,频繁的进行互斥操作会导致性能损耗。
这是,虚拟机会将同步代码块的范围扩大。
4、轻量级锁
在没有多线程竞争的时候,减少互斥产生的性能消耗。
HotSpot虚拟机对象头结构:
对象头分为两部分,一部分用于存贮对象自身的运行时数据(HashCode、GC Age)。
另一部分用于存储指向方法区中对应的元数据的引用。
第一部分也被称为Mark Word,HotSpot虚拟机Mark Word采用了非固定的数据结构。
在每种结构下,都有一个标志位。
结构和标志位一起确定了对象的状态。



在线程进入同步代码块时,如果此时对象没有被锁定,
虚拟机将首先在线程的栈帧中建立一个锁记录空间(Lock Record),用于保存对象的当前Mark Word的拷贝。
然后,虚拟机使用CAS操作将对象Mark Word的值更新为指向锁记录空间的指针。
如果更新成功,那么该对象就被轻量级锁定了,Mark Word的标志位变为00。
如果更新失败,检查对象的Mark Word是否指向当前线程,是,则可进入同步代码块。
否,则说明对象已经被其他线程锁定了。
此时,出现了两个以上的线程争用同一个锁,轻量级锁不再起效,要膨胀为重量级锁。
锁标记的标记位要变为 10。
释放锁的时候,也要通过CAS操作来完成。如果替换失败,要释放锁的同时,唤醒被挂起的线程。
偏向锁:
偏向锁会偏向于第一个获得它的线程,如果在接下来的执行过程中,该锁没有被其他线程获取,
则持有锁的线程不需要再进行同步。
一旦有线程尝试获取这个锁,偏向模式就结束。


浙公网安备 33010602011771号