可见性
可见性:是指当一个线程对共享变量进行了修改,那么另外的线程可以立即看到修改后的最新值。
案例演示:一个线程A根据 boolean 类型的标记 flag,while死循环;另一个线程B改变这个flag变量的值;那么线程A并不会停止循环。
/** 案例演示: 一个线程对共享变量的修改,另一个线程不能立即得到最新值*/
public class Test01Visibility {
// 多个线程都会访问的数据,我们称为线程的共享数据
private static boolean flag = false;
public static void main(String[] args) throws InterruptedException {
// t1线程不断的来读取run共享变量的取值
Thread t1 = new Thread(() -> {
while (flag) {
}
});
t1.start();
Thread.sleep(1000);
//t2线程对该共享变量的取值进行修改
Thread t2 = new Thread(() -> {
flag = false;
System.out.println("时间到,线层2设置为false");
});
t2.start();
// 可以观测得到t2线程对 flag 共享变量的修改,t1线程并不能够读取到更改了之后的值,导致程序不能停止;
// 这就出现了可见性问题
}
}
解决可见性:
Ⅰ. 在共享变量前面加上volatile关键字修饰;
Q:为什么volatile关键字能保证可见性?
volatile 的底层实现原理是内存屏障(Memory Barrier),保证了对 volatile 变量的写指令后会加入写屏障,对 volatile 变量的读指令前会加入读屏障。
- 写屏障(sfence)保证在写屏障之前的,对共享变量的改动,都同步到主存当中;
- 读屏障(lfence)保证在读屏障之后,对共享变量的读取,加载的是主存中最新数据;
Ⅱ. 在死循环内写一个 synchronized 同步代码块,因为synchronized 同步时会对应 JMM 中的 lock 原子操作,lock 操作会刷新工作内存中的变量的值,得到共享内存(主内存)中最新的值,从而保证可见性。
Q:为什么synchronized 同步代码块能保证可见性?
synchronized 同步的时候会对应8个原子操作当中的 lock 与 unlock 这两个原子操作,lock操作执行时该线程就会去主内存中获取到共享变量最新值,刷新工作内存中的旧值,从而保证可见性。
Thread t1 = new Thread(() -> {
while (flag) {
synchronized (obj) { // 死循环内加一个同步代码块
}
}
});
t1.start();
// 或者
Thread t1 = new Thread(() -> {
while (flag) {
// 输出语句也能保证可见性?
// 因为PrintStream.java中的println(boolean x)方法中也使用到了synchronized,synchronized 能保证可见性
System.out.println();
}
});
t1.start();
小结:
可见性(Visibility):是指当一个线程对共享变量进行了修改,那么另外的线程可以立即看到修改后的最新值。
synchronized 和 volatile都可以保证可见性,但缺点是 synchronized 锁属于重量级操作,性能相对更低。
原子性
原子性(Atomicity): 在一次或多次操作中,要么所有的操作都执行,并且不会受其他因素干扰而中断,要么所有的操作都不执行;
案例演示:5个线程各执行1000次i++操作:
public class Test01Atomicity {
private static int number = 0;
public static void main(String[] args) throws InterruptedException {
// 5个线程都执行1000次 i++
Runnable increment = () -> {
for (int i = 0; i < 1000; i++) {
number++;
}
}; // 5个线程
ArrayList<Thread> ts = new ArrayList<>();
for (int i = 0; i < 5; i++) {
Thread t = new Thread(increment);
t.start();
ts.add(t);
}
for (Thread t : ts) {
t.join();
}
/* 最终的效果即,加出来的效果不是5000,可能会少于5000
那么原因就在于 i++ 并不是一个原子操作
下面会通过java反汇编的方式来进行演示和分析,这个 i++ 其实有4条指令 */
System.out.println("number = " + number);
}
}
Idea 中找到target目录,找到当前java文件的字节码.class文件,该目录下打开cmd,输入javap -p -v xxx.class,得到字节码指令,其中,number++对应的字节码指令为:
9: getstatic #18 // Field number:I 获取静态变量的值
12: iconst_1 // 准备一个常量1
13: iadd // 让静态变量和1做相加操作
14: putstatic #18 // Field number:I 把相加后的结果赋值给静态变量
number++是由四条字节码指令组成的,那么在一个线程下是没有问题的,但如果是放在多线程的情况下就有问题,比如线程 A 在执行 13: iadd 前,CPU又切换到另外一个线程B,线程 B 执行了 9: getstatic,就会导致两次 number++,但实际上只加了1。
这个问题的原因就在于让两个线程来进行操作number++, 而number++的字节码指令又是多条指令(4条指令),其中一个线程执行到一半时,CPU又切换到另外一个线程,另外一个线程来执行,读取到的值依然跟另一个线程一样, 即第二个线程干扰了第一个线程的执行从而导致执行结果的错误,没有保证原子性。
解决原子性:
synchronized 可以保证 number++ 的原子性。synchronized 能够保证在同一时刻最多只有一个线程执行该段代码,已保证并发安全的效果。
synchronized(obj){ number++;}
加了 synchronized 同步代码块后,每次运行的结果都是 5000.
Idea 中找到 target 目录,找到当前java文件的字节码.class文件,该目录下打开cmd,输入javap -p -v xxx.class,得到字节码指令,其中,num++对应的字节码指令还是中间的四条,不过上下新增了几条指令:
详情可关注 synchronized
14: monitorenter
15: getstatic #18 // Field number:I
18: iconst_1
19: iadd
20: putstatic #18 // Field number:I
23: aload_124: monitorexit
小结:
原子性(Atomicity): 在一次的操作或多次操作中,要么所有的操作全部都得到了执行并且不会受到任何因素的干扰而中断,要么所有的操作都不执行。
原子性可以通过 synchronized 同步代码块或 ReentrantLock 来解决。或者使用原子类解决原子性问题:
import java.util.concurrent.atomic.AtomicInteger;
public class VolatileDemo1 {
//原子类的int
private volatile static AtomicInteger num = new AtomicInteger();
public static void add(){
num.getAndIncrement();//+1方法 使用到了CAS
}
public static void main(String[] args) {
for(int i=0;i<20;++i){
new Thread(() -> {
for(int j=0;j<1000;++j){
add();
}
}).start();
}
while(Thread.activeCount() > 2){//判断当前执行的线程是否大于2
Thread.yield();//继续执行没有执行完的线程
}
System.out.println(num);//现在就是20000
}
}
有序性
有序性(Ordering):是指程序代码在执行过程中的先后顺序,由于java在编译器以及运行期的优化,导致了代码的执行顺序未必就是开发者编写代码的顺序。
Q:为什么要重排序?
一般会认为编写代码的顺序就是代码最终的执行顺序,那么实际上并不一定是这样的,为了提高程序的执行效率,java在编译时和运行时会对代码进行优化(JIT即时编译器),会导致程序最终的执行顺序不一定就是编写代码时的顺序。重排序 是指 编译器 和 处理器 为了优化程序性能 而对 指令序列 进行 重新排序 的一种手段;
解决有序性:
Ⅰ. 可以使用 synchronized 同步代码块来保证有序性;
Q:synchronized保证有序性的原理是?
加了synchronized,依然会发生指令重排序(可以看看DCL单例模式),只不过,由于存在同步代码块,可以保证只有一个线程执行同步代码块当中的代码,也就能保证有序性。
Ⅱ. 除了可以使用synchronized来进行解决,还可以给共享变量加volatile关键字来解决有序性问题。
volatile如何保证有序性的?
- 写屏障会确保指令重排序时,不会将写屏障之前的代码排在写屏障之后;
- 读屏障会确保指令重排序时,不会将读屏障之后的代码排在读屏障之前;
总结
synchronized 可以保证原子性、有序性和可见性,而 volatile 只能保证有序性和可见性;
synchronized 是个重量级锁,应尽量少使用;