第二章-Java并发机制的底层实现原理
2.1 volatile的应用
- volatile的定义与实现原理
Java编程语言允许线程访问共享变量,为了确保共享变量能被准确和一致地更新,线程应该确保通过排他锁单独获得这个变量。Java语言提供了volatile,在某些情况下比锁要更加方便。如果一个字段被声明成volatile,Java线程内存模型确保所有线程看到这个变量的值是一致的。
-
那么如何确保所所有线程看到这个volatile修饰的变量是一致的呢?
我们将下列代码通过工具获取JIT编译器生成的汇编指令
public volatile Singleton instance = new Singleton();
如下
0x01a3de1d: movb $0×0,0×1104800(%esi);0x01a3de24: lock addl $0×0,(%esp);
我们会发现如果使用volatile修饰的变量会多出上面这一条指令,Lock前缀的指令在多核处理器下会引发了两件事情
- 1、将当前处理器缓存行的数据写回到系统内存。
- 2、这个写回内存的操作会使在其他CPU里缓存了该内存地址的数据无效。
这个过程图片引用大白话聊聊Java并发面试问题之volatile到底是什么?【石杉的架构笔记】
看到这里,你或许会有一点疑问,如果两个线程都带着data=7要同时刷主存那还不是会值丢失?
volatile为了确保同步所以也是会锁的噢~那锁什么呢?在上面指令中出现的lock前缀会发出信号,根据CPU的不同会使用不同的方法。- 1 锁住总线:使其他CPU不能访问总线,不能访问总线就意味着不能访问系统内
存。(在最近的处理器里,LOCK#信号一般不锁总线,而是锁缓存,毕竟锁总线开销的比较大) - 2 缓存锁定:它会锁定这块内存区域的缓存并回写到内存,并使用缓存一致性机制来确保修改的原子性,缓存一致性机制会阻止同时修改由两个以上处理器缓存的内存区域数据。
2.2 synchronized的实现原理与应用
先来看看和volatial的比较
- volatile关键字是线程同步的轻量级实现,所以volatile性能肯定比synchronized关键字要好。但是volatile关键字只能用于变量而synchronized关键字可以修饰方法以及代码块。synchronized关键字在JavaSE1.6之后进行了主要包括为了减少获得锁和释放锁带来的性能消耗而引入的偏向锁和轻量级锁以及其它各种优化之后执行效率有了显著提升,实际开发中使用 synchronized 关键字的场景还是更多一些。
- 多线程访问volatile关键字不会发生阻塞,而synchronized关键字可能会发生阻塞
- volatile关键字能保证数据的可见性,但不能保证数据的原子性。synchronized关键字两者都能保证。
- volatile关键字主要用于解决变量在多个线程之间的可见性,而 synchronized关键字解决的是多个线程之间访问资源的同步性。
-
使用方法有三种形式
- 对于普通同步方法,锁是当前实例对象。
- 对于静态同步方法,锁是当前类的Class对象。
- 对于同步方法块,锁是synchonized括号里配置的对象。
-
那么synchonized的原理和volatile有什么不同呢?下面的原理内容来自公众号
SnailClimb
同样的来一段代码- ① synchronized 同步语句块的情况
public class SynchronizedDemo { public void method() { synchronized (this) { System.out.println("synchronized 代码块"); } } }
通过 JDK 自带的 javap 命令查看 SynchronizedDemo 类的相关字节码信息:首先切换到类的对应目录执行 javac SynchronizedDemo.java 命令生成编译后的 .class 文件,然后执行javap -c -s -v -l SynchronizedDemo.class 之后会出现
从上面我们可以看出:
synchronized 同步语句块的实现使用的是 monitorenter 和 monitorexit 指令,其中 monitorenter 指令指向同步代码块的开始位置,monitorexit 指令则指明同步代码块的结束位置。 当执行 monitorenter 指令时,线程试图获取锁也就是获取 monitor(monitor对象存在于每个Java对象的对象头中,synchronized 锁便是通过这种方式获取锁的,也是为什么Java中任意对象可以作为锁的原因) 的持有权.当计数器为0则可以成功获取,获取后将锁计数器设为1也就是加1。相应的在执行 monitorexit 指令后,将锁计数器设为0,表明锁被释放。如果获取对象锁失败,那当前线程就要阻塞等待,直到锁被另外一个线程释放为止。
- ② synchronized 修饰方法的的情况,同样使用上述方法。
public class SynchronizedDemo2 { public synchronized void method() { System.out.println("synchronized 方法"); } }
synchronized 修饰的方法并没有 monitorenter 指令和 monitorexit 指令,取得代之的确实是
ACC_SYNCHRONIZED 标识,该标识指明了该方法是一个同步方法,JVM 通过该ACC_SYNCHRONIZED 访问标志来辨别一个方法是否声明为同步方法,从而执行相应的同步调用。
2.2.2 锁的升级与对比
1 偏向锁
Mark Word Java对象头中存储对象的hashCode或锁信息等
大多数情况下,锁不仅不存在多线程竞争,而且总是由同一线程多次获得,为了让线程获得锁的代价更低而引入了偏向锁。当一个线程访问同步块并获取锁时,会在对象头和栈帧中的锁记录里存储锁偏向的线程ID,以后该线程在进入和退出同步块时不需要进行CAS操作来加锁和解锁,只需简单地测试一下对象头的Mark Word里是否存储着指向当前线程的偏向锁。如果测试成功,表示线程已经获得了锁。如果测试失败,则需要再测试一下Mark Word中偏向锁的标识是否设置成1(表示当前是偏向锁):如果没有设置,则使用CAS竞争锁;如果设置了,则尝试使用CAS将对象头的偏向锁指向当前线程。
我的理解,如有错误希望指正,共同进步
if(锁的对象头中的Mark Word中是否存有指向当前线程的偏向锁){
线程获得该锁
} else{
if(锁是否设置了偏向锁)//查看Mark Word中偏向锁的标识是否设置为 1(如果为 1 表示已经设置){
//尝试撤销偏向锁
1.先暂停拥有偏向锁的线程
2.判断线程是否存活
if(线程是否存活){
1.要么重新偏向其他线程
2.恢复无锁
3.锁升级
}else{
将对象头设置成无锁状态
}
} else {
使用CAS竞争锁
}
}
- 偏向锁在
Java 6
和Java 7
里是默认启用的,但是它在应用程序启动几秒钟之后才激活,如
有必要可以使用JVM参数来关闭延迟:-XX:BiasedLockingStartupDelay=0
。如果你确定应用程
序里所有的锁通常情况下处于竞争状态,可以通过JVM参数关闭偏向锁:-XX:- UseBiasedLocking=false
,那么程序默认会进入轻量级锁状态。
2 轻量级锁
第一次接触是在深入理解Java虚拟机中
在并发的情况下分配内存,其中的一种解决方案是采用CAS配上失败重试保证更新操作的原子性。
- CAS是乐观锁的一种实现方式。所谓的乐观锁就是每次不加锁,而是假设没有冲突的去完成某种操作,如果因为冲突失败就重试,知道成功为止。
给出书中的流程图
2.3 原子操作的实现原理
- Java中如何实现原子操作
- 个人感觉就是上述的volatile的锁总线和锁内存 + CAS实现
- 1.只有volatile只能确保可见不能确保原子,就像volatile不能确保多线程下 i ++ 的正确
- 2.只有CAS没有volatile,如果没法保证比较值的可见性,比较一个错误的值就没有意义了
- CAS的不足
- 1 ABA问题(
使用版本号解决
) - 2 循环时间问题(
Java 1.6中的自适应自旋锁不知是不是类似的解决方法?
) - 3 只能保证一个共享遍历的原子操作(
使用原子引用
)
- 1 ABA问题(
- 个人感觉就是上述的volatile的锁总线和锁内存 + CAS实现
如果有错希望指出,共同进步,谢谢!