java并发-volatile关键字
一. 内存模型
java每个线程具有独立的缓存
i = i + 1;
线程会把i读到缓存,然后i+1,然后存到主存,多线程时缓存不一致,但是内存中修改的内容是一份。
解决方式:
- 总线加锁:只有一个线程能够读主存该值
- 缓存一致性协议:比如MESI机制,通知线程这个缓存无效,在内存重新读取
二. 并发编程中的三个概念
原子性
一个操作或者多个操作 要么全部执行并且执行的过程不会被任何因素打断,要么就都不执行,类似事务
可见性
当多个线程访问同一个变量时,一个线程修改了这个变量的值,其他线程能够立即看得到修改的值
//线程1执行的代码
int i = 0;
i = 10;
 
//线程2执行的代码
j = i;
在缓存情况下,线程1修改的是线程1缓存中的值,如果没有立即写会内存,线程2读取的值是旧结果。
有序性
jvm会进行指令重排序,处理器在进行重排序时是会考虑指令之间的数据依赖性;
指令重排序不会影响单个线程的执行,但是会影响到线程并发执行的正确性。
三. Java内存模型
java内存模型规定:
- 有的变量都是存在主存当中
- 每个线程都有自己的工作内存
- 线程对变量的所有操作都必须在工作内存中进行,而不能直接对主存进行操作
- 每个线程不能访问其他线程的工作内存
原子性
对基本数据类型的变量的读取和赋值操作是原子性操作
x = 10;         //语句1
y = x;         //语句2
x++;           //语句3
x = x + 1;     //语句4
只有第一句是原子的:
- 语句1是直接将数值10赋值给x,也就是说线程执行这个语句的会直接将数值10写入到工作内存中。
- 语句2实际上包含2个操作,它先要去读取x的值,再将x的值写入工作内存,虽然读取x的值以及 将x的值写入工作内存 这2个操作都是原子性操作,但是合起来就不是原子性操作了。
- x++和 x = x+1包括3个操作:读取x的值,进行加1操作,写入新的值。
只有简单的读取、赋值(而且必须是将数字赋值给某个变量,变量之间的相互赋值不是原子操作)才是原子操作
可见性
Java提供了volatile关键字来保证可见性
当一个共享变量被volatile修饰时,它会保证修改的值会立即被更新到主存,当有其他线程需要读取时,它会去内存中读取新值
有序性
Java内存模型具备一些先天的,happens-before 原则
- 程序次序规则:一个线程内,按照代码顺序,书写在前面的操作先行发生于书写在后面的操作
- 锁定规则:一个unLock操作先行发生于后面对同一个锁额lock操作
- volatile变量规则:对一个变量的写操作先行发生于后面对这个变量的读操作
- 传递规则:如果操作A先行发生于操作B,而操作B又先行发生于操作C,则可以得出操作A先行发生于操作C
- 线程启动规则:Thread对象的start()方法先行发生于此线程的每个一个动作
- 线程中断规则:对线程interrupt()方法的调用先行发生于被中断线程的代码检测到中断事件的发生
- 线程终结规则:线程中所有的操作都先行发生于线程的终止检测,我们可以通过Thread.join()方法结束、Thread.isAlive()的返回值手段检测到线程已经终止执行
- 对象终结规则:一个对象的初始化完成先行发生于他的finalize()方法的开始
四. volatile
不同线程可见性:即一个线程修改了某个变量的值,这新值对其他线程来说是立即可见的
禁止指令重排
例子1:可见性
//线程1
boolean stop = false;
while(!stop){
    doSomething();
}
 
//线程2
stop = true;
线程1运行,利用线程2终止线程1
如果线程2把值更改之后,没哟及时写入缓存,这时线程1仍然在运行。
使用volatile关键字,会强制修改的值立即写入主存,并且使线程1中stop缓存失效。线程1读取会从主存重新读取。
例子2:无法保证原子性
public class Test {
    public volatile int inc = 0;
     
    public void increase() {
        inc++;
    }
     
    public static void main(String[] args) {
        final Test test = new Test();
        for(int i=0;i<10;i++){
            new Thread(){
                public void run() {
                    for(int j=0;j<1000;j++)
                        test.increase();
                };
            }.start();
        }
         
        while(Thread.activeCount()>1)  //保证前面的线程都执行完
            Thread.yield();
        System.out.println(test.inc);
    }
}
自增操作不是原子性操作,而且volatile也无法保证对变量的任何操作都是原子性的
可以使用synchronized、Lock、AtomicInteger的方式保证原子性
例子3:一定程度的有序性
volatile关键字禁止指令重排序有两层意思
- 当程序执行到volatile变量的读操作或者写操作时,在其前面的操作的更改肯定全部已经进行,且结果已经对后面的操作可见;在其后面的操作肯定还没有进行;
- 在进行指令优化时,不能将在对volatile变量访问的语句放在其后面执行,也不能把volatile变量后面的语句放到其前面执行。
//x、y为非volatile变量
//flag为volatile变量
 
x = 2;        //语句1
y = 0;        //语句2
flag = true;  //语句3
x = 4;         //语句4
y = -1;       //语句5
保证1,2句在3前,4,5句在3后
但是不保证1,2和4,5内部的顺序
例子4:一定程度的有序性
//线程1:
context = loadContext();   //语句1
inited = true;             //语句2
 
//线程2:
while(!inited ){
  sleep()
}
doSomethingwithconfig(context);
如果用volatile修饰inited,可以保证每个线程中,执行到inited时,loadContext()都执行完毕。
加入volatile开关键字,会多出一个lock前缀指令
lock前缀指令实际上相当于一个内存屏障,提供三个功能:
- 它确保指令重排序时不会把其后面的指令排到内存屏障之前的位置,也不会把前面的指令排到内存屏障的后面;即在执行到内存屏障这句指令时,在它前面的操作已经全部完成;
- 它会强制将对缓存的修改操作立即写入主存;
- 如果是写操作,它会导致其他CPU中对应的缓存行无效。
五. volatile使用场景
1)对变量的写操作不依赖于当前值
2)该变量没有包含在具有其他变量的不变式中
可以被写入 volatile 变量的这些有效值独立于任何程序的状态,包括变量的当前状态
状态标记量
//线程1
volatile boolean flag = false;
 
while(!flag){
    doSomething();
}
//线程2
public void setFlag() {
    flag = true;
}
volatile boolean inited = false;
//线程1:
context = loadContext();  
inited = true;            
 
//线程2:
while(!inited ){
sleep()
}
doSomethingwithconfig(context);
double check
双重检查的单例模式必须加volatile
class Singleton{
    private volatile static Singleton instance = null;
    private Singleton() {}
     
    public static Singleton getInstance() {
        if(instance==null) {
            synchronized (Singleton.class) {
                if(instance==null)
                    instance = new Singleton();
            }
        }
        return instance;
    }
}
 
                    
                
 
 
                
            
         浙公网安备 33010602011771号
浙公网安备 33010602011771号