synchronized与volatile
synchronized实现原理:
1、Java代码层面上,用于锁定一个代码块只能同时被一个线程访问;
public class synchronizedTest {
public void method(){
synchronized(this){
Object o = new Object();
}
}
}
2、虚拟机层面上,通过锁定和解锁某个监视器来同步变量的值;
synchronized锁定的区域,都会指向一个monitor对象(c++实现)的起始地址。在虚拟机规范上,synchronized是通过字节码指令monitorenter和monitorexit进行实现的,其中monitorenter指令是在编译后插入到同步代码块的开始位置,而monitorexit是插入到方法结束处和异常处。任何对象都有一个monitor与之关联,当一个对象持有了monitor之后,将处于锁定状态。
3、待完善…
synchronized实现过程:
synchronized实现过程需要用到java对象的知识,首先接受java对象相关内容。
java对象实例
首先引用该篇文章对象结构的讲解。java对象结构
我们在new一个对象的时候,该对象在堆中具体的结构分为三部分:
(对象头)+(实例数据)+(对齐填充)
对象头
HotSpot虚拟机的对象头包括两部分信息:
(1) markword
第一部分markword,用于存储对象自身的运行时数据,如哈希码(HashCode)、GC分代年龄、锁状态标志、线程持有的锁、偏向线程ID、偏向时间戳等。
(2) klass
对象头的另外一部分是klass类型指针,即对象指向它的类元数据的指针,虚拟机通过这个指针来确定这个对象是哪个类的实例.
(3) 数组长度(只有数组对象有)
如果对象是一个数组, 那在对象头中还必须有一块数据用于记录数组长度.
实例数据
程序代码的各种类型数据。
对齐填充
无实际意义,保证对象大小为8bit的整数倍。
对象头大小计算(64位系统)
开启压缩指针情况下,markword占8字节,klass占4字节,总共16字节(填充4字节);
关闭压缩指针情况下,markword占8字节,klass占8字节,总共16字节。
synchronized锁升级过程
无锁》》偏向锁》》轻量级锁》》重量级锁
MarkWord在锁升级过程中的结构变化:
偏向锁:当一个线程访问同步块并获取锁时,会在对象头和栈帧中的锁记录里存储锁偏向的线程ID,以后该线程在进入和退出同步块时不需要花费CAS操作来加锁和解锁,而只需简单的测试一下对象头的Mark Word里是否存储着指向当前线程的偏向锁,如果测试成功,表示线程已经获得了锁,如果测试失败,则需要再测试下Mark Word中偏向锁的标识是否设置成1(表示当前是偏向锁),如果没有设置,则使用CAS竞争锁,如果设置了,则尝试使用CAS将对象头的偏向锁指向当前线程。
轻量级锁:线程在执行同步块之前,JVM会先在当前线程的栈桢中创建用于存储锁记录的空间,并将对象头中的Mark Word复制到锁记录中,官方称为Displaced Mark Word。然后线程尝试使用CAS将对象头中的Mark Word替换为指向锁记录的指针。如果成功,当前线程获得锁,如果失败,表示其他线程竞争锁,当前线程便尝试使用自旋来获取锁。
锁升级具体过程:
(1)初期创建锁对象时候,没有任何线程来竞争,处于无锁状态;
(2)当有一个线程来竞争锁,优先使用偏向锁,表示锁对象偏爱这个线程,这个线程要执行这个锁关联的任何代码,不需要再做任何检查和切换,这种竞争不激烈的情况下,效率非常高。同时还需要将获得锁的线程id记录到锁对象的Mark Word中,下次可直接进入;
(3)当有两个线程来竞争锁,不再使用偏向锁,升级为轻量级锁。线程之间公平竞争,优先获取锁对象的线程优先执行代码,其余线程自旋等待(自旋耗费大量的cpu资源),详细过程如下:
a.锁对象撤销偏向模式,偏向锁标志位失效,升级为轻量级锁;
b.在当前线程(获取锁的线程)的帧栈中开辟一块锁记录空间(Lock Record),用于存储锁对象当前的Mark Word拷贝;
c.把锁对象的Mark Word更新为指向帧栈中Lock Record的指针
(4)当线程竞争的激烈程度过高时,jvm会把锁升级为重量级锁,获取锁的线程优先执行,其余线程放入一个队列等待。
更加详细讲解请参考:详细讲解
volatile实现原理
详细讲解
1、java代码层面,用来修饰Java变量
public class volatileTest {
private static volatile int count = 1;
public static void main(String[] args) {
count = 2;
System.out.println(count);
}
}
2、jvm字节码层面,通过ACC_VOLATILE标志来判断
3、待完善…
volatile的作用:
1、可见性
首先讲解java虚拟机的内存模型(JMM)
Java虚拟机规范中定义了一种Java内存 模型(Java Memory Model,即JMM)来屏蔽掉各种硬件和操作系统的内存访问差异,以实现让Java程序在各种平台下都能达到一致的并发效果。Java内存模型的主要目标就是定义程序中各个变量的访问规则,即在虚拟机中将变量存储到内存和从内存中取出变量这样的细节。
JMM中规定所有的变量都存储在主内存(Main Memory)中,每条线程都有自己的工作内存(Work Memory),线程的工作内存中保存了该线程所使用的变量的从主内存中拷贝的副本。线程对于变量的读、写都必须在工作内存中进行,而不能直接读、写主内存中的变量。同时,本线程的工作内存的变量也无法被其他线程直接访问,必须通过主内存完成。
整体线程模型如下:
当对volatile修饰的变量执行写操作后,JMM会把工作内存中的最新变量值强制刷新到主内存,并且写操作会导致其他线程中的缓存无效。这样边保证了可见性。
2、有序性
volatile修饰的变量,在生成字节码时,会在指令序列中添加“内存屏障”来禁止指令重排序。
LoadLoadBarrier
volatile 读操作
LoadStoreBarrier
StoreStoreBarrier
volatile 写操作
StoreLoadBarrier
总结
项目 | 原子性 | 可见性 | 有序性 | 实现原理 |
---|---|---|---|---|
synchronized | 是 | 是 | 是 | 通过锁的方式,会造成线程阻塞 |
volatile | 否 | 是 | 是 | 通过内存屏障的方式 |
需要注意,synchronized的有序性跟volatile的有序性是不同的:
synchronized 的有序性:是持有相同锁的两个同步块只能串行的进入,即被加锁的内容要按照顺序被多个线程执行,但是其内部的同步代码还是会发生重排序,使块与块之间有序可见。
volatile的有序性:是通过插入内存屏障来保证指令按照顺序执行。不会存在后面的指令跑到前面的指令之前来执行。是保证编译器优化的时候不会让指令乱序。