一,示例
说明:创建两个线程,t1线程用来判断定义的flag变量是否等于0(等于0的话进入循环什么都不做),t2线程用来输入一个变量来修改flag的值;我们想要通过t2线程修改flag变量的值来达到跳出t1线程的循环的作用。
代码如下:
import java.util.Scanner;
class Counter {
public int flag = 0;
}
public class ThreadDemo5 {
public static void main(String[] args) {
Counter counter = new Counter();
//t1线程用来进行判断
Thread t1 = new Thread(() -> {
while (counter.flag == 0) {
}
});
//t2线程用来修改flag变量的值
Thread t2 = new Thread(() -> {
Scanner scanner = new Scanner(System.in);
System.out.println("请输入一个整数:");
counter.flag = scanner.nextInt();
});
t1.start();
t2.start();
}
}
通过结果我们考可以发现当我们将flag的值改为1时,t1线程并没有结束?这是为什么呢?
下面我们来主要探讨这个现象的主要原因并给出解决方案!
二,内存可见性问题
2.1 线程安全问题的原因:
1.抢占式执行,随机调度(根本原因,由操作系统内核决定,无法改变)
2.因为单个进程下的多个线程是资源共享的,所以多个线程修改同一个变量时会线程不安全
多个线程修改不同的变量 没事
一个线程修改同一个变量 没事
多个线程读取同一个变量 没事
3.原子性
如果修改操作是原子性的,就不会有线程安全问题
如果修改操作是非原子性的就很大概率出现线程安全问题(上述示例的add操作就是非原子性 的,一个add操作分成了三个指令去执行)
所以我们去避免线程安全的主要手段就是将非原子性的操作变成原子性---->加锁
4.内存可见性问题
当一个线程在读取数据,一个线程在修改数据时就会出现线程安全问题(也就是常说的脏读问 题)
5.指令重排序(本质上是代码出现了bug)
2.2 内存可见性问题
1.上述示例的原因
上述示例也是一个典型的线程不安全的例子,造成这个现象的主要原因就是上述线程安全原因的第四点——内存可见性问题;
对于while(counter.flag == 0)这条语句用汇编来理解可以看成两个操作,第一步:load;把内存中flag的值读取到寄存器中;第二步:cmp;把寄存器中的值和0作比较来判断是否进入循环。由于load的执行速度相对于cmp来说很慢,再加上反复load所读取的值都是一样的,于是编译器开始进行优化(觉得没有人再改flag变量了),只读取一次造成修改后的flag的值没有及时被读取成功。
2.什么是内存可见性问题?
一个线程针对一个变量进行读操作,另一个线程针对同一个变量进行写操作,此时读到的值不一定是修改之后的值,这就是内存可见性问题(上述t1线程读flag变量,t2线程修改flag变量);内存可见性问题最根本的原因就是编译器优化!
三,解决内存可见性问题(volatile关键字)
volatile关键字专门是用来修饰变量,不可以修饰方法,只需要在可能会发生改变的变量前加上volatile关键字即可,针对上述示例加上volatile代码之后:
import java.util.Scanner;
class Counter {
public volatile int flag = 0;
}
public class ThreadDemo5 {
public static void main(String[] args) {
Counter counter = new Counter();
//t1线程用来进行判断
Thread t1 = new Thread(() -> {
while (counter.flag == 0) {
//System.out.println("hello thread");
}
});
//t2线程用来修改flag变量的值
Thread t2 = new Thread(() -> {
Scanner scanner = new Scanner(System.in);
System.out.println("请输入一个整数:");
counter.flag = scanner.nextInt();
});
t1.start();
t2.start();
}
}
此时输入整数1之后发现整个进程随之结束;
当对flag变量加上volatile关键字之后,系统就会认为整个变量是异变的,就不会再进行编译器优化了,此时t1线程就可以感知到t2线程对flag变量的修改了(强制读取内存,虽然执行效率减低了一点,但是代码的准确性提高了)
四,volatile的原理
1. 改变线程工作内存中 volatile 变量副本的值2. 将改变后的副本的值从工作内存刷新到主内存
1. 从主内存中读取 volatile 变量的最新值到线程的工作内存中2. 从工作内存中读取 volatile 变量的副本
总结:
1.volatile会强制读写内存,编译器此时不会进行优化,可以感知到线程对变量的修改;
2. volatile和synchronized都是解决线程安全问题的关键字,但是synchronized是针对使得原子性问题的关键字,而volatile是针对内存可见性问题的关键字。