★
一,Volatile 使用
volatile是使用在成员上的,synchronized是使用在方法上,代码块上面的,他们功能差不多,加锁保证安全,但是volatile是一个轻量级锁。
他的基本特点:保证可见性,不保证原子性,禁止重排序。
当线程对volatile修饰的变量进行修改,其他线程马上会得知,所以能够保证其他线程获得最新的数据内容,volatile的作用是强制从公共堆栈中 【读取】 变量的值
public class ThreadA extends Thread{
volatile private boolean isRunning= true; //volatile修饰 标识符
public boolean isRunning() {
return isRunning;}
public void setRunning(boolean isRunning) {
this.isRunning = isRunning;}
// 运行方法
public void run() {
System.out.println("进入 run 了");
//等待标识符,如果该变量没有volatile修饰,那么它读取的是线程私有内存中的值,是不会感受到变化的
while(isRunning) {}
System.out.println("线程停止了!");
}
public static void main(String []args) {
try {
ThreadA a=new ThreadA();
a.start();
a.sleep(1000);
a.setRunning(false);
System.out.println("赋值为 false 应该停止了");
}catch(Exception e) {
e.printStackTrace();
}
}
}
如果不加volatile 关键字,那么这个程序是无法停止的,因为我们之前已经讲过线程的模型,它是把堆中的共享变量,复制一份到达自己的私有小内存中进行操作,我们的set这个值,它只是把共享变量中的值,换成了false,但是a线程中的私有内存里面的值还是true,所以会一直循环下去,
为什么加上volatile之后,这个变量的值,就会直接从共享内存中获取,这样就说明了,可见性(别人的修改,你是可以看到的)
非原子特性:(不具备同步特性,所以不具有原子性)
public class MyThread extends Thread{
volatile public static int count;
private static void addCount() {
for(int i=0;i<100;i++)
count++;
System.out.println("count= "+count);
}
public void run() {
addCount();
}
public static void main(String []args) {
MyThread[] bb=new MyThread[100];
for(int i=0;i<100;i++)
bb[i]=new MyThread();
for(int i=0;i<100;i++)
bb[i].start();
}
}
结果:
已经不是100整数了,解决办法就是给addCount()方法加上synchronized同步锁,但是如此降低锁的重量目的就无法达到,所以最好的办法是和原子类配合。
二,volatile的特点
class MyThread{
volatile long v;
public void set(long l) {
v=l;
}
public void getAndIncrement() {
v++;
}
public long get() {
return v;
}
}
上下 两种代码 是相等的:
class MyThread{
long v;
public synchronized void set(long l) {
v=l;
}
public void getAndIncrement() {
long temp=get();
temp+=1;
set(temp);
}
public synchronized long get() {
return v;
}
}
1.原子性:对于volatile来说,set,get是原子性的,而++这种复合操作是非原子性的。也就是操作的完整性,不能被打断,比如你在修改一个数据的时候,另外一个人在读取,这就打破了,有点事务的意思。(使用volatile修饰字段,多个线程并发操作,并不能保证线程安全,所以是轻量级的)
2.可见性:任意线程总能看到volatile这个变量最后的写入。在公共内存上的变量值修改了,对于线程来说,它能够看见感知,也就是可见性。
3.有序性:禁止进行指令重排序,我们输入的代码,会经历三次指令重排,进行优化。
如果在多线程环境中,指令的重排,要考虑指令之间的依赖性,但是对于各种重排是无法考虑到各种线程的先后执行顺序的(对它来说是单线程的)所以就会存在问题,所以我们需要进行加锁。
有问题的demo:
如果语句1,2换位置,那么对下面的代码操作就存在影响
加入内存屏障,(是cpu指令),在内存屏障之中的指令,就会禁止排序。
单例模式中,双端检测不一定线程安全,因为存在指令重排序的存在,所以要加入volatile禁止指令重排。对象在new的过程中,分为下面三个步骤,如果2,3指令重排,那么就会出现问题,也就是对象已经!=null,然后就有其他线程取读取这个对象,但是!这个对象还没有初始化,数据还没有准备好,所以就会导致线程安全问题!
三,volatile 原理
volatile的可见性是基于内存屏障实现的,内存屏障是一个cpu指令,有四个内存屏障指令。可见我上篇博客。
通过源码可知道,对volatile变量进行写操作,会加入一个Lock 前缀的指令,这个又和我上篇 “原子操作”博客内容有关(所以说,涉及底层的内容,都交叉进行的)
在处理器的原子操作中存在 总线锁定和 缓存锁定。缓存锁定存在“缓存一致性”协议,MESI。他们是共存的,缓存锁定是在总线锁定上的优化,没有淘汰谁的意思。
在lock 前缀指令中,处理器收到这个指令,就会把缓存行中的数据 回写到 内存中,让其他保存了这个数据的缓冲行无效(修改内存地址)。(问题来了,其他的处理品缓冲行如何知道自己的缓冲是否有效呢)
这里就退出了缓冲一致性协议。它通过一定的规则确保自己缓冲行中的数据是最新的,比如说:每个处理器会嗅探在总线上传播的数据来检查自己缓冲的值是否过期了,当处理器发现自己缓冲行对于的缓冲地址被修改,就会把
缓冲行设置为无效数据,需要重新读取
四,volatile有序性实现
happens-before原则:对一个volatile的写,早于任意后虚对这个volatile的读。(这个原则是给程序员看的,具体的实现,要到编译器的有序和处理器的有序中进行处理)//假设线程A执行writer方法,线程B执行reader方法
class VolatileExample {
int a = 0;
volatile boolean flag = false; //它是一个volatile变量,而且是一个标志变量
public void writer() {
a = 1; // 1 线程A修改共享变量
flag = true; // 2 线程A写volatile变量
}
public void reader() {
if (flag) { // 3 线程B读同一个volatile变量
int i = a; // 4 线程B读共享变量
……
}
}}
编译器中volatile的重排序规则:
总结:
1.当第二个操作是volatile写时,不管第一个操作是什么,都不能重排序。(确保volatile写之前的操作不被排到后面,造成脏读,因为volatile可能是一个标志变量)
2.当第一个操作是volatile读时,不管第二个操作是什么,都不能重排序,(确保标志位成立才操作后面的)
3.当一个操作是volatile写,第二个操作是volatile读,不能重排序。
处理器中的重排序规则:
1.每个volatile写操作的前面插入一个StoreStore屏障
2.每个volatile写操作的后面插入一个StoreLoad屏障
3.每个volatile读操作的后面插入一个LoadLoad屏障
4.每个volatile读操作的后面插入一个LoadStore屏障
内存屏障 | 说明 |
StoreStore 屏障 | 禁止上面的普通写和下面的 volatile 写重排序。 |
StoreLoad 屏障 | 防止上面的 volatile 写与下面可能有的 volatile 读/写重排序。 |
LoadLoad 屏障 | 禁止下面所有的普通读操作和上面的 volatile 读重排序。 |
LoadStore 屏障 | 禁止下面所有的普通写操作和上面的 volatile 读重排序。 |
写操作:
读操作:
参考链接: https://www.jianshu.com/p/157279e6efdb
https://www.jianshu.com/p/ccfe24b63d87(全面)
https://blog.csdn.net/jyxmust/article/details/76946283(volatile的内存语义)
https://www.zhihu.com/question/65372648(volatile与cas比较)