Java语言规范第3版中对volatile的定义以下:java
- Java编程语言容许线程访问共享变量,为了确保共享变量能被准确和一致的更新,线程应该确保经过排他锁单独得到这个变量。
具备可见性、有序性,不具有原子性。编程
1.适用于对变量的写操做不依赖于当前值,对变量的读取操做不依赖于非volatile变量。 2.适用于读多写少的场景。数组
若是一个变量被声明为volatile,Java线程内存模型确保全部线程看到这个变量的值是一致的。缓存
volatile关键字修饰的共享变量进行写操做时在汇编代码中会多出lock行代码,经过查IA-32架构软件开发者手册可知,lock前缀的指令在多核处理器下会引起两件事: a.将当前处理器缓存行的数据写回到系统内存。b.这个写回内存的操做会使其余CPU缓存了该内存地址的数据无效。 volatile两条实现原则:安全
- a.Lock前缀指令会引发处理器缓存回写到内存。
LOCK#信号确保在声言该信号期间,处理器能够独占任何共享内存,也就是“锁总线”。目前较新的处理器中,LOCK#信号通常是“锁缓存”(若是访问的内存区域已经缓存在处理器内部,则不会声言LOCK#信号。相反,它会锁定这个内存区域并回写到内存,并使用缓存一致性机制来确保修改操做的原子性。此操做被称为:缓存锁定
),由于锁总线的开销比较大(锁住总线,致使其余CPU没法访问总线,不能访问总线就意味着不能访问系统内存)。多线程
- b.一个处理器的缓存回写到内存,会致使其余处理器的缓存失效。
处理器使用嗅探技术保证它的内部缓存、系统内存和其余处理的缓存数据在总线上保持一致。架构
synchronized被不少人称之为重量锁,通过Java SE 1.6优化后,有些状况下它已经不那么重了。synchronized实现同步的基础是:Java中每一个对象均可以做为锁。具体表现为:并发
当一个线程试图访问同步代码块时,必须首先得到锁,退出或异常时释放锁,那么锁到底在哪里?里面存储的信息有哪些呢?
app
JVM基于进入和退出Monitor对象来实现方法同步和代码块同步,代码块同步是使用monitorenter和monitorexit指令来实现的,而方法同步是使用另外一种方式实现,在JVM中并无详细说明,但方法同步一样可使用这两个指令来实现。编程语言
monitorenter指令是在编译后插入到同步代码块的开始位置,而monitorexit是插入到方法结束处和异常处,JVM要保证每一个monitorenter必须有对应的monitorexit与之配对。任何一个对象都有一个monitor与之关联,当一个monitor被持有后,它便处于锁定状态。线程执行到monitorenter指令时,会尝试获取该对象对应的monitor的全部权,即尝试获取该对象的锁。
synchronized用的锁是存在Java对象头里的。如对象为数组类型,则虚拟机使用3个字宽(Word)来存储对象头(比非数组类型多存储了一个数组长度),非数组类型使用2个字宽存储对象头。 |长度|内容|说明| |----|-----|------| |32/64bit | Mark Word| 存储对象的hashCode或锁信息| |32/64bit|Class Metadata Address|存储到对象类型数据的指针| |32/64bit|Array Length|数组的长度(若是对象为数组类型)|
Mark Word里默认存储对象的HashCode、分代年龄和锁标记位。
32位虚拟机默认存储结构
|锁状态|25bit|4bit|1bit是否偏向锁|2bit 锁标志位| |-----|-----|------|------|------| |无锁状态|对象的hashCode|对象分代年龄|0|01| 运行期间,Mark Word存储的数据会随着锁标志位的变化而变化。 64位虚拟机默认存储结构
|锁状态|25bit|31bit|1bit cms_free|4bit 分代年龄|1bit 是否偏向锁|2bit 锁标志位| |-----|-----|------|------|------|------|------| |无锁状态|unused|hashCode|||0|01| |偏向锁|ThreadID(54bit)Epoch(2bit)|||1|01|
锁一共四种状态:无锁状态,偏向锁状态,轻量级锁状态,重量级锁状态。 锁只能升级而不能降级,这么作的目的是为了提升得到锁和释放锁的效率。
HotSpot做者研究发现:锁不只存在多线程竞争,并且老是有同一线程屡次得到。为了让线程得到锁的代价更低而引入了偏向锁。
具体步骤:
偏向锁采用一种等到竞争才会释放锁的机制,因此当其余线程尝试竞争偏向锁时,持有偏向锁的线程才会释放锁。偏向锁的撤销,须要等到全局安全点(在这个时间点上没有正在执行的字节码)
a.轻量级锁加锁
线程执行同步块时,JVM会如今当前线程的栈帧中建立用于存储锁记录的空间,并将对象头中的Mark Word复制到锁记录中(官方称之为: Displaced Mark Word)。 而后线程尝试使用CAS将对象头中的Mark Word替换为指向锁记录的指针。若成功,则线程得到锁,若失败,表示其余线程竞争锁,当前线程尝试使用自旋获取锁。
b.轻量级锁解锁
解锁时,会使用CAS操做将Displaced Mark Word 替换回到对象头,若是成功,表示竞争没有发生。若是失败,表示当前锁存在竞争,锁就会膨胀成重量级锁。
由于自旋会消耗CPU,为了不无用的自旋,一旦线程升级为重量级锁,就不能再恢复到轻量级锁状态。当锁处于该状态下时,其余线程试图获取锁时,都会被阻塞,只有当持有该锁的线程释放锁后,才会唤醒这些线程。被唤醒的线程进行新一轮的“九子夺嫡”夺锁之争。
锁 | 优势 | 缺点 | 适用场景 |
---|---|---|---|
偏向锁 | 加锁解锁不需额外消耗,和执行非同步方法相比仅存在纳秒级的差距 | 线程间存在锁竞争,会带来额外锁撤销消耗 | 适用一个线程访问同步块场景 |
轻量级锁 | 竞争的线程不会阻塞,提升了程序相应速度 | 若是始终得不到锁竞争的线程,使用自旋会消耗CPU | 追求响应时间,同步块执行速度很是快 |
重量级锁 | 线程竞争不使用自旋,不会消耗CPU | 线程阻塞,响应时间缓慢 | 追求吞吐量,同步块执行速度较长 |
原子操做意为“不可中断的一个或一系列操做”。在多处理器上实现原子操做就变得有点复杂。
处理器基于对缓存加锁或总线加锁的方式来保证操做的原子性,当一个处理器读取一个字节时,其余处理器不能访问这个字节的内存地址。较新的处理器能保证单处理器对同一个缓存行进行16/32/64位操做是原子的,而对于较为复杂的内存操做,处理器是不能自动保证其操做的原子性的。好比,跨总线宽度,跨多个缓存行和跨页表访问。但处理器提供了总线锁和缓存锁这两个机制来保证复杂内存操做的原子性。
若是多个处理器同事对共享变量就行读改写操做(i++),那么共享变量就会被多个处理器同时操做,这样读改写的操做就不是原子的,操做完后共享变量的值会和指望值不一致。缘由是多个处理器同时出各自的缓存中读取变量,而后分别进行+1操做,再分别写入系统内存中。 而总线锁就是使用处理器提供的LOCK#信号,当一个处理器在总线上输出该信号时,其余处理器的请求将被阻塞,那么该处理器就能够独占内存。
缓存锁定:内存区域若是被缓存在处理器的缓存行中,而且在LOCK操做期间被锁定,那么当它执行锁操做回写到内存时,处理器不在总线上声言LOCK#信号,而是修改内部的内存地址,并容许它的缓存一致性机制来保证操做的原子性,由于缓存一致性机制会组织同时修改由两个以上处理器缓存的内存区域数据。当其余处理器回写已被锁定的缓存行时,会使缓存行无效。
1.操做数据不能被缓存或跨多个缓存行。此时会调用总线锁。 2.部分处理器不支持缓存锁定。如Pentium处理器和Intel 486。
在Java中能够经过锁和循环CAS的方式来实现原子操做。
JVM中的CAS操做利用了处理器提供的CMPXCHG指令实现的。自旋CAS实现的基本思路就是循环进行CAS操做直到成功为止。
从Java1.5开始,JDK的并发包提供了一些类来支持原子操做,如AtomicLong,AtomicBoolean,AtomicInteger。还提供了一些原子的自增1和自减1方法。
CAS原子操做的三大问题:
CAS操做值时,要检查值有没有发生变化,若是没有发生变化则更新,但若是变量原始值为A,变成了B,又变成了A。那么使用CAS检查时,会认为该值没有发生变化,但实际上却发生变化了。解决办法:在变量更新时增长版本号,更改一次版本号加1。Java1.5开始,新增了一个AtomicStampedReference来解决ABA问题。该类中的compareAndSet方法就是首先检查当前引用是否等于预期引用,而后检查当前标志是否等于预期标志,若相等,则以原子方式将该引用和该标志的值设置为给定的更新值。
自旋CAS长时间不成功,会给CPU带来很是大的执行开销。若JVM支持处理器的pause指令,则效率会有必定提高。pause指令做用:a.延迟流水线执行指令(de-pipeline),使CPU不会消耗过多的执行资源。b.避免退出循环时,因内存顺序冲突(Memory Order Violation)而引发CPU流水线被清空(CPU Pipeline Flush),从而提升CPU的执行效率。
解决方法:(1)使用锁。(2)把多个共享变量合并成一个共享变量来操做,好比i=2,j=a,合并为ij=2a。(3)从java1.5开始,JDK提供了AtomicReference类,能够将多个变量放在一个对象中来实现原子操做。
JVM内部的锁有偏向锁,轻量级锁,互斥锁。但除了偏向锁,JVM实现锁的方式都运用了循环CAS,线程进入和退出同步块时都使用CAS来获取锁和释放锁。
本节使用代码验证volatile和synchronized关键字的使用。 1.验证volatile具备可见性,不具有原子性。
public volatile int ivl = 0; public static void main(String[] args) { final App app = new App(); for (int i = 0; i < 20; i++) { new Thread() { @Override public void run() { for (int j = 0; j < 10000; j++) { app.doSome(); } } }.start(); } while (Thread.activeCount() > 1) { // 保证前面的线程都执行完成 Thread.yield(); } System.out.println(app.ivl); } /** * 自增方法 * * @return */ public int doSome() { return ivl++; } }
若是执行这段代码,会发现,每次输出的值都是不同的。并且始终都是小于20*10000的值。自增操做是不具有原子性的,具体缘由上面已经分析过。 若是要使获得的结果等于20*10000,可使用synchronized修饰doSome()方法,这样在处理器读取内存时加LOCK#指令,保证内存只能被一个处理器访问和修改。