将复合操作使用同步锁控制,控制执行复合操作每个时刻只有一个线程
简介
volatile是Java虚拟机提供的轻量级锁,在一些场景上可以用来替换synchronized锁,而且效率更高,比如多个线程共享某个变量的值时候,要求一个线程修改这个值之后,立刻对其他线程可见
特性
可见性
一个线程修改了共享变量的值,对另一个线程是能够立刻看到共享变量的修改的
根据volatile的happen before 原则
如果一个volatile的写happen-before对一个volatile的读,那么对volatile的写操作的更改对后面的volatile的读操作是可见的
验证
public class Service {
private boolean flag =true ;
public void test(){
while(flag){
}
System.out.println("跳出while循环执行......");
}
public void cancel(){
flag = false;
}
public static void main(String[] args) throws InterruptedException {
final Service s = new Service();
Thread t = new Thread(new Runnable() {
@Override
public void run() {
s.test();
}
});
t.start();
Thread.sleep(1000);
s.cancel();
System.out.println("已经将flag赋值为false.....");
}
}
配置: 代码在-server模式jvm运行,eclipse中设置-server模式:
运行结果: 线程t不会输出 跳出死循环这句。。。
原因:在主线程中,将flag改为false,线程t仍然是死循环中。因为对线程t而言,flag的修改它看不到
修改:将flag 加上 volatile关键字,再次运行代码
运行结果: 线程t结束死循环...
不具有原子性
原子性,是指一个操作不能被CPU中断。volatile线程不安全的地方,主要体现在对volatile修饰的变量进行复合操作是不具有原子性的,常见的例子就是i++问题。(当然,volatile的读或写操作是原子性)
i++ 操作在内存中其实三个操作:
- 从主内存中读取i到工作内存中
- 工作内存中对i自增后,赋值给一个临时变量
- 将临时变量刷新回主内存中
在多个线程,执行i++操作的时候,可能存在线程A读取i值,线程B随后读取一样的值,线程A自增后,将值写会到主内存中,之后,被线程B自增的值覆盖了,这就线程不安全了
验证
public class Service {
private volatile int i = 1 ;
public void incre(){
for(int j =0;j<100;j++){
this.i++;
}
System.out.println("i的值:"+i);
}
public int get(){
return i;
}
public static void main(String[] args) throws InterruptedException {
final Service service = new Service();
for(int i=0;i<100;i++){
Thread t = new Thread(new Runnable() {
@Override
public void run() {
service.incre();
}
});
t.start();
}
}
}
运行结果: 9990
解决volatile复合操作不具有原子性
将复合操作使用同步锁控制,控制执行复合操作每个时刻只有一个线程
public synchronized void incre(){
for(int j =0;j<100;j++){
this.i++;
}
}
使用并发包中的Atomic类操作
Atomic类操作复合运算的原理的核心,运用了CAS算法
举例来说,类似i++操作,使用AtomicInteger 类的incrementAndGet自增的话,就是不断调用comparseAndSet方法直到更新成功。
对于 compareAndSet方法,有三个参数,主内存的值,本地内存的值,期望更新的值,只有当主内存的值和本地内存的值一致时,才将期望值更新到主内存中,并且返回true,否则返回false。
public final int incrementAndGet() {
for (;;) {
int current = get();
int next = current + 1;
if (compareAndSet(current, next))
return next;
}
}
public final boolean compareAndSet(int expect, int update) {
return unsafe.compareAndSwapInt(this, valueOffset, expect, update);
}
禁止指令重排
jmm对于volatile变量,会禁止特定的编译器或者处理器重排序
- 当第二个操作是volatile写时,不管第一个操作是什么,都不能重排序。这个确保volatile写之前的操作都不能被重排序到volatile写后面
- 当第一个操作是volatile读时,不管第二个操作是什么,都不能重排序
- 当第一个操作是volatile写,第二个是volatile读时,不能重排序
而禁止重排序,是通过内存屏障来实现的,具体如下:
1. 每一个volatile写前面加上一个storeStore屏障
加上这个内存屏障可以保证:前面的所有普通写操作对任意处理器可见,将屏障前的普通写在volatile写之前,刷新到主内存中
2. 每一个volatile 写后面加上一个storeload屏障,禁止后面的volatile读写操作重排序
3. 每一个volatile 读后面加上一个loadLoad屏障, 禁止下面的所有的普通读和上面的volatile读重排序
4. 每一个volatile 读后面加上一个loadstore屏障,禁止下面的所有普通写和上面的volatile读重排序
有什么用?
以双重检查的单例模式为例
package cn.bing.singleton;
/**
* 双重检查锁定延迟加载
* @author Administrator
*
*/
public class SingleTonDoubleLock {
private static volatile SingleTonDoubleLock instance;
private SingleTonDoubleLock() {}
public static SingleTonDoubleLock getInstance() {
if(instance==null) {//1
synchronized (SingleTonDoubleLock.class) {//2
if(instance==null)
instance = new SingleTonDoubleLock();
}
}
return instance;
}
}
假设线程A和线程B调用getInstance方法,线程A获取到类锁,进入2,创建对象
对象的创建分为3步骤
- 给对象分配内存空间
- 对象初始化
- 将引用指向对象的内存空间
jvm可能对上面的指定进行重排序,线程B看到另一个线程的执行顺序都是乱序的,可能是1,3,2的顺序
此时,线程B执行到1,看到instance的地址不为空(由于重排序,可能对象还没有初始化),直接就返回了地址,但是此时对象还没有被初始化
解决: 声明变量是volatile类型,当第二个操作是volatile写时,不管第一个操作是什么,都不能重排序。这个确保volatile写之前的操作都不能被重排序到volatile写后面,因此,保证另一个线程看到引用赋值的时候,一定对象是初始化了的
注意:
如果存在两个线程执行一个相同的方法,对线程本身,是顺序执行的,看别的线程执行是乱序的。
注意
volatile类型用来一般修饰单个变量的读写,如果碰到多个变量需要共享时候,可以将多个变量封装为一个对象