并发编程中的三个概念
- 原子性:一个操作和多个操作,要么全部执行完,要不全部不执行。
- 可见性:多个线程同时访问一个变量时,一个线程修改了变量值,其他线程能够立即看见。
- 有序性:程序按照代码先后顺序执行。JVM在编译时会发生指令重排序。
想要程序正确的执行,必须保证程序的原子性、可见性和有序性。否则程序执行出错。
JAVA内存模型规定所有变量存储在主存中(类似与物理内存),每个线程有自己的工作内存(类似于高速缓存),线程只能操作自己的工作内存,不能对主存进行操作,每个线程不能访问其他线程的工作内存。
JAVA中对三性的保护:
原子性:在JAVA中对基本数据类型变量的读取和赋值是原子操作。
x = 10;//1
y = x;//2
x++;//3
x = x+1;//4
以上只有1是原子操作,该局直接将10赋值给x,也就是线程将数值10写入到工作内存中。
语句2有两个操作,1.读取x的值,2.写入到工作内存。
3和4有三个操作,1.读取x值,2.对值加一,3.写入到工作内存。
另外,在32位平台下对64位数据的读取和赋值是两个操作。但最新的JVM已经保证了该操作为原子操作。
可见性:volatile关键字可以保证变量的可见性。被volatile修饰的变量修改后会立即写入到内存中,其他线程读取时需要去内存中读取。普通变量在修改后,写入内存中的时间是不确定的。
另外,sychronized和lock也可以保证变量的可见性。他们可以保证只有一个线程同时访问同步代码,并在释放锁之前将修改的变量值刷新到内存中。
有序性:JAVA允许编译器和处理器对指令进行重排序,重排序不会影响到单线程的执行,但是会影响到多线程的执行,这里存在一个happens-before原则,如果虚拟机不能通过该原则推出指令的次序,那么虚拟机可以随意对他们进行重排序。
Volatile关键字:
一个变量被volatile修饰,那么这个变量:
- 是可见的,一个线程修改了该变量的值,其他线程是可见的。
- 禁止进行指令重排序。
volatile无法保证操作的原子性:
public class test {
public volatile int inc = 0;
public void increase() {
inc++;
}
public static void main(String[] args) throws InterruptedException {
test t = new test();
for(int i=0;i<10;i++){
new Thread(){
public void run() {
for(int j=0;j<1000;j++)
t.increase();
};
}.start();
}
Thread.sleep(2000);
System.out.println(t.inc);
}
}
上面的代码发现:每次执行玩完成后,输出的inc不为10000,而是小于10000的不同的值,产生这种情况的执行顺序如下:
- 线程1取出inc的值,被阻塞。
- 线程2取出inc的值,进行+1,被阻塞(没有把增加1后的inc值赋值给inc)。
- 线程1被唤醒,inc+1,赋值,同步到主存,线程1结束。
- 线程2唤醒,将inc的值赋值给inc,同步到主存。(此部不需要读取缓存中的inc值,尽管此时线程2中的inc缓存行已经无效)
volatile禁止指令重排序,可以在一定程度上保证程序的有序性:当程序执行到volatile变量时,在其前面的操作已经全部执行,并且更改更新到内存中,结果对后面的操作可见,在其后面的操作没有进行。
volatile实现机制:
在加入了volatile关键字时,会多出一个lock前缀指令,相对于一个内存屏障,它有三个功能:
- 指令重排序不会把屏障之前的指令排到屏障之后,也不会把屏障之后的指令排到屏障之前。
- 强制对缓存的修改立即写入到主存。
- 如果是写操作,会导致其他cpu对应的缓存行无效。