Java 线程安全与 volatile 和 synchronize
前言
Java 多线程编码中,保证线程安全的实质是保证对数据操作的原子性,即一个线程对数据的操作能够及时的更新到其他使用该数据的线程中,这样就可以避免多个线程因为操作的数据值不一致而产生错误。
由于 Java 内存模型(JMM)规定,所有线程公用的数据保存在主内存中,而线程在使用时先从主内存中取到线程私有的工作内存中,之后再在使用完毕后同步到主内存中,在这过程中,如果其他线程也用到了该数据则可能会出现问题,因此在线程操作数据时需要考虑线程并发时操作数据的同步问题。
volatile
和synchronize
因此而生。
volatile
volatile
修饰的变量有两个特性:
- 变量对所有线程可见 普通变量则需要等线程操作完毕,将结果从工作内存写入到主内存中才可以被其他线程可见,volatile 修饰的变量会在修改后通知其他线程该变量已经被更改,从而让其他线程再去主内存中读取最新的值
- 禁止指令重排优化
volatile
修饰的变量执行效率和普通变量差别不大,其写操作因为要插入内存屏障,所以会稍微慢一些
需要注意的是:
由于 Java 运算的具体实现并非原子性的,故而虽然
volatile
修饰的变量在所有线程可见,但是并发下并不线程安全。Java 代码编译成 class 文件后可以看到,类似
c = c + 1
这样的语句,会被分为:读取c
的值;计算c+1
的值;将结果赋予c
这几步来完成。所以在此期间如果有其他的线程访问这段代码,就会发生冲突。Java 会通过指令重排来优化代码
指令重排 指对于变量的赋值会在定义该变量和使用该变量的值之间的任意位置执行,不一定和代码中的顺序一致
volatile
修饰的变量则会插入内存屏障,从而实现屏蔽指令重排的效果
synchronize
synchronize
实现的原理是锁定指定的对象(如果没有指定则锁定对应的类对象或 class 对象),然后阻塞其他线程进入(获取到该锁的线程可以多次重入)。
由于 Java 的线程实现是映射到系统线程的,阻塞和唤醒需要由系统内核完成,会消耗大量的时间,因此synchronize
是重量级操作。
JMM 与三个特征
JMM 的设计是围绕着原子性、可见性、有序性三个特征进行的。
原子性 JVM 中的
read,load,assign,use,store,write
操作和synchronize
可见性 一个线程更改了共享变量的值时,其余线程能够立即得知这个更改。通过
synchronize
,final
和volatile
保证。final
要保证可见性的前提是要被安全的构建出来,避免**“this 引用逃逸”**this 引用逃逸 对象还没有被构造完成,他的
this引用
就已经被发布出去了。在构造函数中生成内部类,由于内部类自动持有外部类的
this引用
,如果有对象在内部类语句之后构造,则就有可能发生“内部类访问这个对象时,该对象还没有构造完毕”的情况。有序性 通过
synchronize
,volatile
保证。
线程从内部观察时有序(线程内是串行的语义),线程外部观察是无序(由指令重排、工作内存与主内存同步延迟导致)
实现线程安全
实现线程安全有以下几种方法:
互斥同步(阻塞同步)
互斥同步的思想是:多个线程使用同一个共享数据时,保证同一时刻只能被一个线程使用
有两种途径:
synchronize
(原生语法层),优先使用ReentrantLock
重入锁(API 层),功能有:1.等待可中断(可以放弃等待)2.公平锁 多个线程申请锁时必须按照申请时间顺序获得锁 3.锁绑定多个条件
非阻塞同步
减少了阻塞/唤醒的耗时,在操作时进行 CAS(比较并交换),在冲突发生的时候不断尝试执行所需操作,直到执行成功。
但是有一个逻辑漏洞:如果在第一次操作失败到第二次再次尝试操作之间,其他线程对齐进行了操作但是该数据最终没有被变化,当第二次再次尝试时,其实已经被其他线程访问过了。
无同步方案
保证线程安全,不一定需要同步,当线程操作的数据不是共享数据时,即使不同步也是线程安全的。
- 可重入代码 指在代码执行的过程中,如果中断其运行并运行其他的线程,当再次返回继续执行该代码时不会影响到其执行结果的代码。这种代码一般没有用到堆中的公用资源。
- 线程本地存储 共享数据值存在于同一个线程中,如每个线程的 ThreadLocal 对象
锁优化
JDK1.6 以后,在 HotSpot 虚拟机上实现了许多锁优化技术:
自旋锁
实现阻塞同步时,阻塞和唤醒会很耗时,为了避免这种情况,可以先对其进行忙循环,如果还不行再去执行阻塞操作
自适应自旋 由 JVM 智能决定自旋次数
锁消除
JVM 会自动取出不必要的锁
锁粗化
如果一段代码中有连续的锁,则 JVM 会将这些锁合并为一个大锁
轻量级锁
轻量级锁消耗比传统锁机制小,会优先尝试使用轻量级锁,如果不行,在升级为互斥锁
大多数情况下会减少消耗,但如果存在锁竞争,则除了互斥锁的开销外,还有轻量级锁的开销
偏向锁
在无竞争的情况下消除同步
乐观锁
读取数据时默认该对象不会被其他对象更改而不加锁,每次写数据时对比当前值与持有值是否一致,一致时才去更新数据
参考资料
《深入理解 Java 虚拟机——JVM 高级特性与最佳实践》周志明