一、Volatile关键字
在内存模型上,堆内存用来存储对象和基本数据类型的备份,称之为主内存,将栈内存中存储的变量的部分内存,称之为本地内存(工作内存),这个就是 JMM模型的内容。
1、Java线程对于所有的变量操作(读取,赋值)都是在自己的工作内存中进行的,线程是不直接读取主内存中的变量。
2、不同线程无法直接访问对方的工作内存中的变量。
3、线程间变量的传递主要是通过主内存来完成。
主内存和工作内存具体的交互协议,即一个变量从主内存拷贝到工作内存,如果从工作内存同步到主内存的是有具体的交互操作:
主要分为8种操作:Lock(锁定)、unlock(解锁)、read(读取)、load(载入)、use(使用)、assign(赋值)、store(存储)、write(写入)。
加入volatile之后, 在汇编层面(非字节码 产生的.class )在对应的汇编语句前加了“#Lock”,当B线程修改本地内存变量操作时:
1、本地内存B将变量副本修改为最新值,并立即将最新值回写到主内存上,通过总线将A线程的变量副本的标识置为无效。
2、当A线程来访问变量的本地副本时,先检测标志位为无效时,A线程会从主内存拷贝数据到本地内存副本上。
volatile特征
保证了内存可见性
volatile修饰的变量(本地内存:java虚拟机栈/寄存器)不会被缓存在寄存器,变量在本地内存(虚拟机栈线程私有的空间),一旦变量修改会立即回写至主内存,每一个线程访问主内存上的数据是最新的变量结果,如果已经有线程存在本地副本,即在回写时有效标志位会失效,从而是存储副本的线程能够进入主内存获取最新数据。
禁止指令重排序
Java内存模型不会对volatile指令进行重排序,从而保证对volatile变量的执行顺序,永远按照顺序出现的顺序执行。
重排序是语句happen-before法则,法则之一规定“对volatile字段的写入操作happen-before与每一个后续的同一个字段的读操作”。
注意:volatile字段只能够满足并发特征中的可见性、(有序性),不能保证原子性,也不能保证线程安全。
volatile只能修饰变量,对基本类型的数据起作用。
volatile修饰对象是否起作用?
对对象不起作用,只能对对象的地址空间进行可见,即地址如果发生改变,其他线程能够立即感知,但是对象本身的属性发生改变,volatile是不能保证其他下线程立即感知。
Volatile工作原理
《深入理解Java虚拟机》关于volatile描述:
”观察加入volatile关键字和没有加入volatile关键字所产生的汇编语言,加入了volatile关键字时,会多出一个Lock前缀指令。“
Lock前缀指定实际是相当于一个内存屏障(内存栅栏),内存屏障存在的作用主要有3点:
1、他保证指定重排序之后内存栅栏之后的指定不会到内存栅栏之前,内存栅栏之前的指定不会进入栅栏之后。
2、它会强制对缓存(工作内存)中的数据立即写回主内存。
3、如果是写操作,他会立即导致其他CPU的对应的缓存立即无效。
如果volatile当前修饰的是一个变量
1、变量值从主内存(在堆中)加载load到本地内存(虚拟机栈的栈帧中)。
2、之后,线程对该变量的操作就不在和主内存打交道/不在联系,直接使用本地内存的副本数据,如果主内存中或副本的数据发生任何变化,如果不互相联系,则导致主内存和副本数据不一致的的问题。
3、在volatile修饰的变量在某个线程中发生改变,基于volatile的的特征:立即将该变量的修改写回主内存,并且其他CPU上对应的缓存是会立即失效。
Synchronized关键字
Synchronized的使用
Synchronized添加到代码块
//修饰代码块
public void test1(Object o) {
synchronized (o) {
//doing
}
}
synchronized如果锁的是某一个obj的对象,实际上作用在代码块上。
synchronized添加在普通的方法上
//修饰普通方法
public synchronized void test2() {
//doing
}
Synchronized加在普通方法上,锁的是对象实例。
多个线程来竞争时,那个线程获取了该对象实例那个线程调用的方法才能继续执行。
假如存在两个线程,同时拥有该对象实例,一个线程调用test2方法,一个线程调用test4方法,两个方法可以同时执行吗?
答:不能,
test2和test4方法都是Synchronized修饰的普通方法,Synchronized加在普通方法上,锁的是对象实例。一个对象实例时,一个线程获取,首先占有当前对象实例,然后调用该对象的方法,两个线程同时来要执行,哪一个线程先获取对象实例,则才能调用对应方法,另一个线程只能等占有的线程释放掉锁之后才能继续获取对象实例进行执行方法。
举个例子假如demo2线程抢先获取对象实例,则可以执行test2方法,同时demo4线程因为竞争对象实例synchronizeDemo失败,而需要等待demo2线程执行结束才能执行。
Synchronized添加到静态方法上
//修饰静态方法
public synchronized static void test3() {
//doing
}
如果Synchronized加在静态方法上,锁的就是当前的class实例。
Synchronized的特点
Synchronized修饰的方法或代码块,在同一时刻JVM只能允许一个线程访问,Synchronized通过锁机制来完成同一时刻只能一个线程访问(临界区)。
并发编程中,Synchronized的锁机制可以做到原子性,可见性,有序性。
synchronized原理
通过Javac命令将代码编程生成字节码文件.class文件
通过javap -v XXX.class反编译字节码
修饰代码块:
通过上面大致可以看到,修饰方法在字节码层面上flag中通过ACC_SYNCHRONIZED标志,修饰代码块时使用monitorenter和monitorexit来完成。
无论使用以上两种那种方式,本质上都是对一个对象的监视器(monitor)进行获取,而对于这个监视器的获取是排他的,也就是同一时刻只有一个线程可以获取到由Synchronized所保护的对象的监视器。
通过上图可以知道,任何线程的访问对象,都需要首先来获取对象的监视器(monitor),如果获取监视器成功,则可以访问该对象,一旦有线程成功获取monitor对象,其他的线程则获取失败,线程会进入到BLocking状态,将获取不成功的线程放入到队列中,成功访问的线程在Monitor监视器。
Monitor排他性的实现是需要借助操作系统所提供的锁来实现(mutux)。
Synchronize的使用场景
考虑在以下场景下线程是否安全?为什么?
场景1:两个线程同时访问同一个对象的同步方法
分析:两个线程来访问同一个对象锁,所以会相互等待,是线程安全的。
两个线程同时访问同一个对象的同步方法,是线程安全的。
场景2:两个线程同时访问两个对象的同步方法
这是一种锁失效的情况,访问两个对象的同步方法,那个线程分别持有两个对象的同步方法,所以线程之间树互不受限,加锁的目的是为了多个线程竞争同一把锁,而这个不存在多个线程竞争同一把锁,而是分别持有一把锁。
两个线程同时访问两个对象的同步方法,是线程不安全的。
如何解决锁失效问题,只需要将方法用static修饰,这样就形成类锁,多个实例竞争同一把类锁,可以做到线程安全。
场景3:两个线程同时访问(一个或者两个)对象的静态同步方法
和上面场景2的锁实现的解决方案是同一个问题。
两个线程同时访问(一个或者两个)对象的静态同步方法,是线程安全的。
场景4:两个线程分别同时访问(一个或者两个)对象的同步方法和非同步方法
是线程不安全的。
public class Condition implements Runnable {
@Override
public void run() {
if (Thread.currentThread().getName().equals("Thread-0")){
//执行同步方法
test1();
}
if (Thread.currentThread().getName().equals("Thread-1")) {
//执行非同步方法
test2();
}
}
//同步方法
public synchronized void test1() {
System.out.println("线程名:"+Thread.currentThread().getName()+" 线程开始");
try {
Thread.sleep(5000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("线程名:"+Thread.currentThread().getName()+" 线程结束");
}
//非同步方法
public void test2() {
System.out.println("线程名:"+Thread.currentThread().getName()+" 线程开始");
try {
Thread.sleep(5000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("线程名:"+Thread.currentThread().getName()+" 线程结束");
}
public static void main(String[] args) {
Condition condition = new Condition();
Thread t1 = new Thread(condition);
Thread t2 = new Thread(condition);
t1.start();
t2.start();
}
}
执行结果:
由上图可知,两个线程是并行执行的,所以是线程不安全的。
volatile和synchronized的区别
相同点:
volatile与synchronized都用于保证多线程中数据的安全。
区别:
(1)volatile修饰的变量,不会被缓存在寄存器,变量在本地内存(虚拟机栈线程私有的空间),一旦变量修改会立即回写至主内存,每一个线程访问主内存上的数据是最新的变量结果,如果已经有线程存在本地副本,即在回写时有效标志位会失效,从而是存储副本的线程能够进入主内存获取最新数据。
而synchronized则是锁住当前方法或对象,同一时刻只有一个线程能够访问当前对象或方法。
(2)volatile只能修饰变量,对基本类型的数据起作用,而synchronized可用在变量和方法中。
(3)volatie仅能实现变量的可见性,有序性,无法保证变量操作的原子性,也不能保证线程安全。Synchronized的锁机制可以做到原子性,可见性,有序性。