一、基础知识:
1. 原子性
1.1.概念:要么全部执行,要么全部不执行,
1.2.操作实现
2.重排序
顾名思义,对于你写的程序并一定会按照你写的顺序执行,系统会对你的程序重新排序运行;重排序通常是编译器或运行时环境为了优化程序性能而采取的对指令进行重新排序执行的一种手段。重排序分为两类:编译器重排序和运行期重排序,分别对应编译时和运行时环境 。
对于单线程来说,重排序不会对结果产生任何影响,但是对于并发多线程来说就会产生许多问题。
3.内存屏障(Memory Barriers)
通过内存屏障指令来禁止特定类型的处理器重排序,volatile就包含有这个指令,分类如下所示:
二、Java内存模型(JMM)实现的 final重排序规则
三、Volatile关键字
特性:保证可见性,一致性(保证单一的读/写的原子性),顺序性(写-读建立的happens-before关系)
1.保证可见性: i.将当前处理器缓存行的数据写回到系统内存
ii.这个写回的内存操作会使其他CPU里缓存了该内存地址的数据无效。
如下:当线程A写入主内存后,让线程B本地内存x值无效,让线程B重新去主内存读取数据
2. 一致性(保证单一的读/写的原子性 )
例如:
volatile int i=1;
i =2 ; // 写操作,原子性
int j=i; //读操作,原子性
i++; //这个不是,没有达到原子性,包含三个操作,读取i的值,然后进行加1操作,然后进行写入内存。
3.顺序性(写-读建立的happens-before关系)
在volatile字段读或者写的语句中,它前面的数据必定发生在当前语句之前,前面的语句可以发生重排序;同理,在volatile字段操作之后也同样必须发生在这个语句之后,后面的不会管他们的重排序情况。
例如:
int a,b,c ;
volatile int v1=1;
volatile int v2 =2;
void readAndWrite(){
a =1; //1
b=2; //2
c=3; //3
int i =v1; //4 第一个volatile读
int j =v2; //5 第二个volatile读
a = i+j; // 6 普通写
b = i+j; //7
c = i+j; //8
v1=i+1; //9 第一个volatile写
v2=j* 2; //10 第二个volatile写
}
如上所示:1,2,3可能发生重排序,但是在4的时候,一定会保证1,2,3都是执行了的。同理5,6,都是volatile字段操作都会保证前面的都是执行了的不会让一块发生重排序;同理6,7,8可能发生重排序;
另外上面的volatile都是单一的读,写,所以都是单一的保证了原子性
4.双重检查锁定和延迟初始化
使用volatile实现单例模式
如下:
Pubulic Class Singleton {
private volatile static Singleton instance; //1 这里用volatile申明
public static Singleton getInstance() {
if (null == instance) {
synchronized (Singleton.class) {
if (null == instance) {
instance = new Singleton (); //2 这里有三个操作,可能会被重排序
}
}
}
return instance;
}
}
如上所示,单例的引用采用volatile申明,就是为了避免2处的重排序
问题解析:
instance = new Singleton() 实际是由下面三步完成的。
memory=allocate(); 1. //分配对象内存空间
ctorInstance(memory); 2.//初始化对象
instance=memory; 3.//设置instance指向刚分配的内存
上面2,3可能会被重排序,如果重排序后,当A执行了线程执行到了3(2还没有执行),B线程获取这个单例,判断不为空(已经有指向内存空间了),但实际上并没有初始化,这里访问的也就是没有初始化的对象。如下图两个线程访问顺序:
解决办法:
1>: 禁止重排序 所以通过把instance用volatile申明,那么当前就会禁止重排序就会避免这个问题
2>: 通过类初始化完成,即饿汉式单例模式(类加载使这个重排序对外隐藏)
5.volatile使用条件
您只能在有限的一些情形下使用 volatile 变量替代锁。要使 volatile 变量提供理想的线程安全,必须同时满足下面两个条件:
- 对变量的写操作不依赖于当前值,例如自增环境下不行 i++;
- 该变量没有包含在具有其他变量的不变式中。
6.volatile使用场景
1.volatile特别适合于状态标记量
例如: volatile boolean flag = true;
2.双重检查锁定-单例模式使用,如上所示
其他场景可以参考:https://www.ibm.com/developerworks/cn/java/j-jtp06197.html
四、CAS实现
在jdk5以前,java基本只有靠synchronized关键字来保证同步,这会导致需要同步的对象被加上独占锁,也就是我们常说的悲观锁。悲观锁存在一些问题,典型的如:
1.在多线程竞争下,加锁和释放锁会导致比较多的上下文切换和调度延时,引起性能问题。
2.一个线程持有锁会导致其他所有需要此锁的线程挂起
与悲观锁对应的是乐观锁,乐观锁在处理某些场景的时候有更好的表现,所谓乐观锁就是认为在并发场景下大部分时候都不会产生冲突,因此每次读写数据读不加锁而是假设没有冲突去完成某项操作,如果因为冲突失败就重试,直到成功为止。乐观锁用到的机制就是CAS。
CAS操作包含三个操作数——内存位置(V),预期原值(A)和新值(B)。如果内存位置的值与预期原值相匹配,那么处理器将会自动将该位置值更新为新值,否则,不做任何操作。无论哪种情况,它都会在CAS指令之前返回该位置的值。
通过以上定义我们知道CAS其实是有三个步骤的
1.读取内存中的值
2.将读取的值和预期的值比较
3.如果比较的结果符合预期,则写入新值
现在的CPU都支持“读-比较-修改”原子操作,也就是一个cpu在执行这个操作的时候,是绝对不会被其他线程中断的,但是多cpu环境就不一定了。可能在cpu1执行完比较操作准备修改的时候,另一块cpu2火速完成了一次“读-比较-修改”操作从而让内存中的值发生变化。而此时cpu1再写入明显就不对了。并且这个场景也并没有违背该命令的原子性。
解决这个问题的答案其实也很简单,那就是就是volatile。
在上面的场景中,解决问题的关键就是volatile的一致性,volitile的写操作是安全的,因为他在写入的时候lock声言会锁住cpu总线导致其他CPU不能访问内存(现在多用缓存一致性协议,处理器嗅探总线上传播的数据来判断自己缓存的值是否过期),所以,写入的时候若其他cpu修改了内存值,那么写入会失败。上面的问题中,由于cpu1的CAS指令执行一半的时候cpu2火速修改了变量的值,因此这就让该变量在所有cpu上的缓存失效,cpu1在进行写入操作的时候,也会发现自己的缓存失效,那么CAS操作就会失败(在java的automicinteger中,会不停的CAS直到成功)。所以即使是在多cpu多线程环境下,CAS机制依然能够保证线程的安全。
但是CAS也并非完美的,CAS存在3个问题,ABA问题,循环时间长的话开销大(也就是说多冲突环境下乐观锁的重试消耗大),以及只能保证一个共享变量的原子操作,本文就不再详细讨论了。