JAVA代码编译后会变成java字节码,字节码会被类加载器加载到JVM中,JVM执行字节码,最终需要转化为汇编指令在CPU上执行,Java中所使用的并发机制依赖于JVM的实现和CPU的指令。
Volatile的应用volatile在多处理器中保证了共享变量的可见性,可见性的意思是当一个线程修改一个共享变量时,另一个线程能读到这个修改的值。如果volatile使用恰当,它比synchroized的使用和执行成本更低,因为它不会引起线程上下文的切换和调度。
volatile是如何确保可见性使用volatile修饰的变量在翻译的汇编指令中会在前边加上lock指令,
加了改指令则会在多核处理器引发两件事情
- 将当前处理器缓存行的数据写会到系统内存
- 这个写会内存的操作会使在其他CPU里缓存了改内存地址的数据无效。
为了提高处理速度,处理器不直接和内存进行通信,而是先将系统内存的数据读到内部缓存(L1,L2或其他)后再进行操作,但操作完不知道何时会写到内存。
对于单处理器来说,如果声明了volatile的变量进行写操作,JVM就会向处理器发送一条Lock前缀的指令,将这个变量所在缓存行的数据写回到系统内存。
对于多处理器来说,就要遵守缓存一致性协议,每个处理器通过嗅探在总线上传播的数据来检查自己缓存的值是不是过期,当处理器发现自己缓存行对应的内存地址被修改,就会将当前处理器的缓存行设置为无效状态,如果处理器要对这个数据进行修改操作,会重新从系统内存中把数据读到处理器缓存中
著名的JAVA并发编程大师Doug lea在JDK7的并发包里新增一个队列集合类linkedTransferQueue,它在使用volatile变量时,用一种追加字节码的方式来优化队列出队和入队的性能。
追加字节码是如何优化性能的?
答:对于yingteer酷睿i7,酷睿,Atom和NetBurst等高速缓冲行是64个字节宽,不支持部分填充缓存行,这意味着队列的头节点和尾节点都不足64字节的话,处理器会将它们都读到同一个高速缓存行中,在多处理器下每个处理器都会缓存同样的头,尾节点,当一个处理器试图修改头节点时,会将整个缓存行锁定,而队列的出队和入队操作需要不断修改头尾节点,严重影响入队和出队效率。
是不是使用volatile变量都应该追加到64字节?
答:不是的。缓存行非64字节宽的处理器和共享变量不会被频繁写的情况下,不应该追加字节码。
JDK1.6之后对synchroized进行优化引入了偏向锁,轻量级锁以及锁的存储结构和升级过程。
synchroized锁的三种方式
- 普通的同步方法。锁是当前实例对象。
- 对于静态同步方法,锁是当前类的Class对象
- 对于同步方法块,锁是Synchroized括号里配置的对象
Synchroized在JVM中的实现原理
JVM基于进入Monitor对象来实现方法同步和代码块同步,两者都可以通过monitorenter和monitorexit指令实现。
monitorenter指令在编译后插入到同步代码块的开始位置,而monitorexit是插入到方法结束处和异常处。
Synchroized用的锁是存储在java对象头里的,如果对象是数据类型,则虚拟机使用3个字宽存储对象头,如果对象是非数组类型,则使用2字宽存储对象头。
JAVA对象头里的Mark Word里默认存储对象的HashCode,分代年龄和锁标志位。存储结构如下
-
偏向锁
引入偏向锁的原因:大多数情况下,锁不仅不存在多线程竞争,而且总是由同一线程多次获得,为了让线程获取锁的代码更低而引入了偏向锁。
偏向锁是基于一种等到竞争出现才释放锁的机制,即只要有多个线程操作共享变量,偏向锁则会撤销。
偏向锁的关闭:-XX:-UseBiasedLocking=false,程序默认会进入轻量级锁状态 -
轻量级锁
线程在执行同步代码块之前,JVM会先在当前线程的栈帧中创建用于存储锁记录的空间,并将对象头的Mark Word复制到锁记录中,官方称为Displaced Mark Word 然后线程尝试使用CAS将对象头中的Mark Word替换为指向锁记录的指针,如果成功,当前线程获取锁,如果失败表示其他线程竞争锁,当前线程变尝试使用自旋来获取锁。
解锁,轻量级解锁时,会使用原子的CAS操作将Displaced Mark Word替换回到对象头,如果成功,则表示没有竞争发生。如果失败,表示当前锁存在竞争,锁会膨胀成重量级锁。 -
锁的优缺点对比
| 锁 | 优点 | 缺点 | 适用场景 |
|---|---|---|---|
| 偏向锁 | 加锁和解锁不需要额外的消耗,和执行非同步方法相比仅存在纳秒级的差距 | 如果线程存在锁竞争,则会带来额外的锁撤销的消耗 | 适用于只有一个线程访问同步块的场景 |
| 轻量级锁 | 竞争的锁不会阻塞,提高了程序的相应速度 | 如果始终得不到锁竞争的线程,则会使用自旋消耗CPU | 追求响应时间,同步块执行速度非常快 |
| 重量级锁 | 线程竞争不使用自旋,不会消耗CPU | 线程阻塞,响应时间缓慢 | 追求吞吐量,同步块执行速度比较长 |
Java使用锁和循环cas的方式来实现原子操作的
CAS操作的三大问题
- ABA问题,如果在同步代码块中将A先变成B,经过一系列操作后把B再变为A,这样是不合法的,于是JDK的Atomic包里提供了一个类AtomicStampedReference来解决ABA问题,于是A->B->A,就变成了1A->2B->3A
- 循环时间开销大,如果CAS长时间不成功,回给cpu带来非常大的执行开销
- 只能保证一个共享变量的原子操作。



