本文包含内容
- volatile 实现原理
- synchronized 实现原理及应用
- synchronized 锁升级过程
- java 如何实现原子操作
- 锁与CAS
- CAS常见问题与解决
volatile 实现原理
定义
java语言规范第三版中对volatile定义如下:
Java编程语言允许线程访问共享变量,为了确保共享变量能准确和一致地更新,线程应该确保通过排他锁单独获得这个变量。
如果一个字段被声明成volatile,java线程内存模型确保所有线程看到这个变量的值是一致的。
实现原理
大家都知道,为了提高运行速度,处理器不会直接与内存通信,而是通过内部高速缓存(L1、L2、或其他)间接与内存通信,在高速缓存中操作后的缓存数据写回到主存的时间是不一定的。在JVM执行字节码过程中,对于声明了volatile关键字的变量,当线程执行了写操作后,JVM会向处理器发送一条Lock前缀的指令,将这个变量所在的缓存行数据写回到系统内存。
但是,光写回到主存是不够的,因为其他处理器的缓存值仍然没有更新。在多处理器下,为了保证各处理器的缓存是一致的,就会实现缓存一致性协议,每个处理器通过嗅探在总线上传播的数据来检查自己缓存的值是不是过期了,当处理器发现自己的缓存行对应的内存地址被修改,就会将当前处理器的缓存行设置为无效状态,这就是缓存失效 当处理器对这个数据进行修改操作的时候,会重新到主存中进行加载,这样就实现了 内存可见 。
其他特性
volatile除了保证修饰变量的内存可见性之外,还有一个特性是可以防止JVM的指令重排优化,这里暂时不打算展开,有时间补上。
synchronized 实现原理及应用
除了volatile,Java并发编程中使用较多的还有synchronized关键字。
jdk1.6之前,人们称它为“重量级锁”,因为它的实现方式是操作系统级别的,锁从内核态到用户态的相互转换比较耗费性能。
接下来详解jdk1.6之后,为了减少获得锁与释放锁带来的性能消耗而引入的偏向锁和轻量级锁,以及锁的存储结构和升级过程。
利用synchronized实现同步的基础:java中每一个对象都可以作为锁,具体表现为以下三种形式:
- 对于普通同步方法,锁的是当前实例对象
- 对于静态同步方法,锁的是当前类的类对象(Class对象)
- 对于同步代码块,锁住的是synchronized括号中所指定的对象
实现原理
从JVM规范中可以看到:JVM基于进入和退出Monitor对象来实现方法同步和代码块同步,代码块同步时使用monitorenter
和monitorexit
指令实现。
monitorener
指令是在编译后插入到同步代码块的开始位置,而monitorexit
是插入到同步代码块结束处或者异常处。线程执行到monitorenter
指令时,将会尝试获取对象所对应的monitor的所有权,即尝试获得对象的锁。
synchronized 锁升级过程
在jdk1.6中 synchronized锁一共有四种状态,分别为“无锁状态”、“偏向锁状态”、“轻量级锁”、“重量级锁状态"。几种状态只能升级不能降级。
偏向锁:大多数情况下,锁不仅不存在多线程竞争,甚至总是由同一个线程持有,为了适应这种情况引入了偏向锁,当线程访问同步块获得锁时,会在对象头和栈帧处记录锁偏向的线程ID,之后这个线程进入同步块时不再需要CAS操作来加锁和解锁,只需简单测试对象头mark word中是否存储着指向该线程的偏向锁。
如果测试成功,则线程重入,如果测试失败,先验证对象头偏向锁标识是否为1,如果不是1,则CAS竞争锁,如果是1,则尝试使用CAS将对象头的偏向锁指向当前线程。
轻量级锁:当竞争出现时,持有偏向锁的线程会释放锁,释放锁时,会等到全局安全点(这个时间点上没有执行的字节码)。释放结束后锁会升级为轻量级锁,此时线程会尝试使用CAS持有锁,如果失败,将会使用自旋的方式重试获得锁。
若当前只有一个等待线程,则该线程将通过自旋进行等待。但是当自旋超过一定的次数时,轻量级锁便会升级为重量级锁(锁膨胀)。
另外,当一个线程已持有锁,另一个线程在自旋,而此时又有第三个线程来访时,轻量级锁也会升级为重量级锁(锁膨胀)。
java 如何实现原子操作
原子本意是”不能被进一步分割的最小粒子“,java中的原子操作是指”对变量的一系列操作,要么全部成功,要么全部失败“,并发编程中,只有保证一些操作的原子性,才不至于产生错误的结果
处理器如何处理
- 通过总线锁保证操作原子性
使用处理器提供的一个Lock#信号,当一个处理器在总线上输出此信号时,其他处理器请求将被阻塞住,那么该处理器独享内存,使用总线锁的开销是很大的,因为在锁定期间,其他处理器不能操作其他内存地址的数据。 - 使用缓存锁保证操作原子性
很好理解,当内存内某处理器使用缓存锁锁住内存某区域地址,那么处理器将不能同时缓存该缓存行
JAVA中的实现机制
Java中通过 锁 和 CAS 来实现原子操作。
通过锁可以实现同一时间只有一个线程操作数据,自然保证了操作安全,这里重点说一下CAS 。
自旋操作通过与预期值比较来判断是否可以进行结果写入,当预期不符时,将重新执行并再次判断,直到写入成功。
CAS 实现原子操作的三大问题
- ABA问题
如果一个值原来是A,变成了B,然后又变成了A,在CAS看来这个变量没有发生变化,而实际上却变化了,解决思路是通过版本号来判断是否发生了改变。从jdk1.5开始,Atomic包中提供了AtomicStampedReference来解决ABA问题。 - 循环时间长,开销大
很显然,自旋时间长,会给cpu带来非常大的执行开销,解决此问题的途径是JVM支持处理器提供的pause指令,这里暂时不作展开。 - 只能保证一个共享变量的原子操作
CAS只能保证单个变量操作的原子性,从jdk1.5开始,jdk提供了AtomicReference类来保证引用对象之间的原子性,就可以把多个变量放在一个对象里来进行CAS操作。
小结
通过本文浅显的介绍,可以了解volatile、synchronized和原子操作的实现原理。java中大部分容器和框架都依赖此部分基础知识,相信会对大家今后的并发编程有一定帮助。