并发编程的三个特性
1原子性
int等不大于32位的基本类型的操作都是具有原子性,对于long和double变量,把它们作为2个原子性的32位值来对待,而不是一个原子性的64位值,
这样将一个long型的值保存到内存的时候,可能是2次32位的写操作,
2个竞争线程想写不同的值到内存的时候,可能导致内存中的值是不正确的结果
分析下列语句的是否为原子操作
i =666;//原子性, 线程执行这个语句时,直接将数值666写入到工作内存中
i = j;//看起来也是原子性的,但是它实际上涉及两个操作,先去读j的值,再把j的值写入工作内存,两个操作分开都是原子操作,但是合起来就不满足原子性了
i = i+1; //非原子性
2 可见性
当一个线程修改了共享变量的值时,其他线程能够立即得知这个修改
JMM通过在变量修改后将新值同步回主内存,在变量读取前从主内存刷新变量值这种依赖主内存作为传递媒介的方式来实现可见性的,无论是普通变量还是volatile变量都是如此。
volatile变量,保证新值能立即同步回主内存,以及每次使用前立即从主内存刷新,所以我们说volatile保证了多线程操作变量的可见性
synchronized和Lock也能够保证可见性,线程在释放锁之前,会把共享变量值都刷回主存。final也可以实现可见性
3 有序性
CPU重排序包括
1. 指令并行重排序
2. 内存系统重排序
JMM定义: 如果在本线程内观察,所有的操作都是有序的;如果在一个线程中,观察另一个线程,所有的操作都是无序的.
后半句的意思即 允许编译器和处理器对指令进行重排序, 会影响到多线程并发执行的正确性
而前半句意思就是as-if-serial的语义,即不管怎么重排序(编译器和处理器为了提高并行度),(单线程)程序的执行结果不会被改变
即重排序对单线程没有影响,而在多线程情况下可能会影响程序的正确性
例子
bool flag = false;
int b = 0;public void read() {
b = 1; //1
flag = true; //2
}public void add() {
if (flag) { //3
int sum =b+b; //4
System.out.println("bb sum is"+sum);
}
}
单线程无论情况下 无论b=1 先执行还是flag=true先执行程序都是有序的.而在多线程情况下二者的执行顺序将使得结果不确定.
使用volatile修改上面的例子并分析
volatile bool flag = false;
int b = 0;public void read() {
b = 1; //1
flag = true; //2
}public void add() {
if (flag) { //3
int sum =b+b; //4
System.out.println("bb sum is"+sum);
}
}
按 happens-before原则分析:
1. flag加上volatile关键字,那就禁止了指令重排,也就是1 happens-before 2了
2. 根据volatile变量规则,2 happens-before 3
3. 由程序次序规则,得出 3 happens-before 4
4. 由传递性,得出1 happens-before 4,因此输出sum=2
可以给变量加上volatile关键字,来保证有序性。当然,也可以通过synchronized和Lock来保证有序性。synchronized和Lock保证某一时刻是只有一个线程执行同步代码,相当于是让线程顺序执行程序代码了,自然就保证了有序性
除此之外,java语言中有一个 先行发生原则( happens-before ) 来保证JMM的有序性。
1. 程序次序规则
在一个线程内,按照控制流顺序,书写在前面的操作先行发生于书写在后面的操作
2. 管程锁定规则
一个unLock操作先行发生于后面对同一个锁额lock操作
3. volatile变量规则
对一个变量的写操作先行发生于后面对这个变量的读操作
4. 线程启动规则
Thread对象的start()方法先行发生于此线程的每个一个动作
5. 线程终止规则
线程中所有的操作都先行发生于线程的终止检测,我们可以通过Thread.join()方法结束、Thread.isAlive()的返回值手段检测到线程已经终止执行
6. 线程中断规则
对线程interrupt()方法的调用先行发生于被中断线程的代码检测到中断事件的发生
7. 对象终结规则
一个对象的初始化完成先行发生于他的finalize()方法的开始
8. 传递性
如果操作A先行发生于操作B,而操作B又先行发生于操作C,则可以得出操作A先行发生于操作C
Volatile原理
volatile变量,用来确保将变量的更新操作通知到其他线程。当把变量声明为volatile类型后,编译器与运行时都会注意到这个变量是共享的,因此不会将该变量上的操作与其他内存操作一起重排序。volatile变量不会被缓存在寄存器或者对其他处理器不可见的地方,因此在读取volatile类型的变量时总会返回最新写入的值。
首先来看一个例子
在线程t1当中定义了一个布尔型变量stop,然后试图在线程t2当中改变它的值使线程t1跳出死循环.
public class VolatileTest1 {
public static void main(String[] args) throws InterruptedException {
Task task = new Task();Thread t1 = new Thread(task, "线程t1");
Thread t2 = new Thread(new Runnable() {
@Override
public void run() {
try {
Thread.sleep(1000);
System.out.println("开始通知线程停止");
task.stop = true; //修改stop变量值。
} catch (InterruptedException e) {
e.printStackTrace();
}}
}, "线程t2");
t1.start(); //开启线程t1
t2.start(); //开启线程t2
Thread.sleep(1000);
}
}class Task implements Runnable {
boolean stop = false;
int i = 0;@Override
public void run() {
long s = System.currentTimeMillis();
while (!stop) {
i++;
}
System.out.println("线程退出" + (System.currentTimeMillis() - s));
}
}
运行结果
开始通知线程停止 //无法通知到线程1
现在 对这个程序做一点改变,在共享变量stop前以volatile修饰
package VolatileTest;
public class VolatileTest1 {
public static void main(String[] args) throws InterruptedException {
Task task = new Task();Thread t1 = new Thread(task, "线程t1");
Thread t2 = new Thread(new Runnable() {
@Override
public void run() {
try {
Thread.sleep(1000);
System.out.println("开始通知线程停止");
task.stop = true; //修改stop变量值。
} catch (InterruptedException e) {
e.printStackTrace();
}}
}, "线程t2");
t1.start(); //开启线程t1
t2.start(); //开启线程t2
Thread.sleep(1000);
}
}class Task implements Runnable {
volatile boolean stop = false;
int i = 0;@Override
public void run() {
long s = System.currentTimeMillis();
while (!stop) {
i++;
}
System.out.println("线程退出" + (System.currentTimeMillis() - s));
}
}
运行结果
开始通知线程停止
线程退出1001
可见 这次线程t2成功拿到了线程1的stop变量,可见stop变量对线程t2是可见的.
声明变量是 volatile 的,JVM 保证了每次读变量都从内存中读,跳过 CPU cache 这一步.
当一个变量定义为 volatile 之后,将具备两种特性:
1.保证此变量对所有的线程的可见性,这里的“可见性”,如本文开头所述,当一个线程修改了这个变量的值,volatile 保证了新值能立即同步到主内存,以及每次使用前立即从主内存刷新。
2.禁止指令重排序优化。有volatile修饰的变量,赋值后多执行了一个“load addl $0x0, (%esp)”操作,这个操作相当于一个内存屏障(指令重排序时不能把后面的指令重排序到内存屏障之前的位置),只有一个CPU访问内存时,并不需要内存屏障;(什么是指令重排序:是指CPU采用了允许将多条指令不按程序规定的顺序分开发送给各相应电路单元处理)。
内存屏障
实际上volatile保证可见性和禁止指令重排都跟内存屏障有关,我们编译volatile相关代码看看, 下面以一个DCl单例模式为例来反编译代码学习内存屏障的相关知识.
public class Singleton {
private volatile static Singleton instance;
private Singleton (){}
public static Singleton getInstance() {
if (instance == null) {
synchronized (Singleton.class) {
if (instance == null) {
instance = new Singleton();
}
}
}
return instance;
}
}
DCL单例模式(Double Check Lock,双重检查锁)比较常用,它是需要volatile修饰的
编译这段代码后,观察有volatile关键字和没有volatile关键字时的instance所生成的汇编代码发现,有volatile关键字修饰时,会多出一个lock addl $0x0,(%esp),即多出一个lock前缀指令
0x01a3de0f: mov $0x3375cdb0,%esi ;...beb0cd75 33
; {oop('Singleton')}
0x01a3de14: mov %eax,0x150(%esi) ;...89865001 0000
0x01a3de1a: shr $0x9,%esi ;...c1ee09
0x01a3de1d: movb $0x0,0x1104800(%esi) ;...c6860048 100100
0x01a3de24: lock addl $0x0,(%esp) ;...f0830424 00
;*putstatic instance
; -
Singleton::getInstance@24
lock指令相当于一个内存屏障,它保证以下这几点:
1.重排序时不能把后面的指令重排序到内存屏障之前的位置
2.将本处理器的缓存写入内存
3.如果是写入动作,会导致其他处理器中对应的缓存无效。
内存屏障保证前面的指令先执行,所以这就保证了禁止了指令重排啦,同时内存屏障保证缓存写入内存和其他处理器缓存失效,这也就保证了可见性
volatile 性能:
volatile 的读性能消耗与普通变量几乎相同,但是写操作稍慢.
常见面试题
1. 谈谈volatile的特性
保证变量对所有线程的可见性 ,禁止指令重排,不保证原子性
2. volatile的内存语义
1. 当写一个 volatile 变量时,JMM 会把该线程对应的本地内存中的共享变量值刷新到主内存
2. 当读一个 volatile 变量时,JMM 会把该线程对应的本地内存置为无效。线程接下来将从主内存中读取共享变量
3. 并发编程的3大特性
原子性
可见性
有序性
4. 什么是内存可见性,什么是指令重排序?
可见性就是指当一个线程修改了共享变量的值时,其他线程能够立即得知这个修改
指令重排是指JVM在编译Java代码的时候,或者CPU在执行JVM字节码的时候,对现有的指令顺序进行重新排序
5. volatile是如何解决java并发中可见性的问题
底层是通过内存屏障实现的,volatile能保证修饰的变量后,可以立即同步回主内存,每次使用前立即先从主内存刷新最新的值
6. volatile如何防止指令重排
内存屏障
Java内存的保守策略
在每个volatile写操作的前面插入一个StoreStore屏障。
在每个volatile写操作的后面插入一个StoreLoad屏障。
在每个volatile读操作的后面插入一个LoadLoad屏障。
在每个volatile读操作的后面插入一个LoadStore屏障。
volatile保证在重排序时不能把内存屏障后面的指令重排序到内存屏障之前的位置
7. volatile可以解决原子性嘛?为什么?
不可以
原子性需要synchronzied或者lock保证
8. volatile底层的实现机制
volatile如何保证可见性和禁止指令重排,需要讲到内存屏障
9. volatile和synchronized, threadlocal 的区别
volatile修饰的是变量,synchronized一般修饰代码块或者方法
volatile保证可见性、禁止指令重排,但是不保证原子性;synchronized可以保证原子性
volatile不会造成线程阻塞,synchronized可能会造成线程的阻塞,所以后面才有锁优化