📖Chapter02 ——volatile 关键字
📖 对象及变量的并发访问
所需掌握的知识点
- synchronized 对象监视器为 Object 时的使用方法
- synchronized 对象监视器为 Class 时的使用方法
- 非线程安全问题如何出现
- 关键字 volatile 的主要作用
- 关键字 volatile 与 synchronized 的区别及使用情况
📑 2.3 volatile 关键字
volatile 使用上具有以下3个特性
- 可见性 —— B 线程能够马上看到 A 线程 更改的数据
- 原子性 —— 体现在赋值原子性
- 禁止代码重排序
🔖 2.3.1 可见性测试
⭐️ 单线程出现死循环
代码示例
/***
* @author: Alascanfu
* @date : Created in 2022/6/29 19:06
* @description:
* @modified By: Alascanfu
**/
public class PrintStringTest {
static class PrintString{
private boolean isContinuePrint = true;
public boolean isContinuePrint(){
return isContinuePrint;
}
public void setContinuePrint(boolean isContinuePrint){
this.isContinuePrint = isContinuePrint;
}
public void printStringMethod(){
try {
while (isContinuePrint == true ){
System.out.println("run printStringMethod threadName = "
+ Thread.currentThread().getName());
Thread.sleep(1000);
}
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
public static void main(String[] args) {
PrintString printString = new PrintString();
printString.printStringMethod();
System.out.println("我要停止它 ! stopThread = " +
Thread.currentThread().getName());
printString.setContinuePrint(false);
}
}
运行结果
- 停不下来的原因主要就是 main 线程一直在处理 while() 循环,导致程序不能继续执行后面的代码,解决的办法当然是用多线程技术
⭐️ 使用多线程解决死循环
修改代码
/***
* @author: Alascanfu
* @date : Created in 2022/6/29 19:06
* @description:
* @modified By: Alascanfu
**/
public class PrintStringTest {
static class PrintString implements Runnable{
private boolean isContinuePrint = true;
public boolean isContinuePrint(){
return isContinuePrint;
}
public void setContinuePrint(boolean isContinuePrint){
this.isContinuePrint = isContinuePrint;
}
public void printStringMethod(){
try {
while (isContinuePrint == true ){
System.out.println("run printStringMethod threadName = "
+ Thread.currentThread().getName());
Thread.sleep(1000);
}
} catch (InterruptedException e) {
e.printStackTrace();
}
}
@Override
public void run() {
printStringMethod();
}
}
public static void main(String[] args) {
PrintString printString = new PrintString();
Thread thread = new Thread(printString);
thread.start();
System.out.println("我要停止它 ! stopThread = " +
Thread.currentThread().getName());
printString.setContinuePrint(false);
}
}
运行结果
⭐️ 使用多线程有可能出现死循环
示例代码
/***
* @author: Alascanfu
* @date : Created in 2022/6/29 19:39
* @description:
* @modified By: Alascanfu
**/
public class RunThread extends Thread{
private boolean isRunning = true ;
public boolean isRunning(){
return isRunning;
}
public void setRunning(boolean isRunning){
this.isRunning = isRunning;
}
@Override
public void run() {
super.run();
System.out.println("进入 run 了");
while (isRunning == true ){
}
System.out.println("线程 被停止了 !");
}
public static void main(String[] args) {
try {
RunThread thread = new RunThread();
thread.start();
;
Thread.sleep(1000);
thread.setRunning(false);
System.out.println("已经赋值为 false");
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
运行结果
-
程序运行出现了死循环进程且未进行销毁
-
线程被停止永远不会被执行
❓ 是什么原因导致的死循环呢?
-
在启动线程时,因为变量
private boolean isRunning = true ;
分别存储在公共内存及线程的私有内存当中,线程运行后在 线程的私有内存中取得 isRunning 的值一直是 true。 -
而代码 “thread.setRunning(false);” 虽然被执行,却是将公共内存中的 isRunning 变量改成 false。
-
这个问题就是私有内存中的值和公共内存中的值是不同步造成的,可以通过 volatile 关键字来解决,volatile 的主要作用就是当线程访问 isRunning 变量时,强制地从公共内存中取值。
⭐️ 使用 volatile 关键字解决多线程出现的死循环
示例代码
/***
* @author: Alascanfu
* @date : Created in 2022/6/29 19:39
* @description:
* @modified By: Alascanfu
**/
public class RunThread extends Thread{
private volatile boolean isRunning = true ;
public boolean isRunning(){
return isRunning;
}
public void setRunning(boolean isRunning){
this.isRunning = isRunning;
}
@Override
public void run() {
super.run();
System.out.println("进入 run 了");
while (isRunning == true ){
}
System.out.println("线程 被停止了 !");
}
public static void main(String[] args) {
try {
RunThread thread = new RunThread();
thread.start();
;
Thread.sleep(1000);
thread.setRunning(false);
System.out.println("已经赋值为 false");
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
运行结果
线程具有私有内存
读取公共内存
⭐️ synchronized 代码块也具有增加可见性作用
synchronized 关键字可以使多个线程访问同一个资源,具有同步性,也可以使线程私有内存中的变量与公共内存中的变量同步。——可见性
示例代码
/***
* @author: Alascanfu
* @date : Created in 2022/6/29 19:39
* @description:
* @modified By: Alascanfu
**/
public class RunThread extends Thread{
private boolean isRunning = true ;
public boolean isRunning(){
return isRunning;
}
public void setRunning(boolean isRunning){
this.isRunning = isRunning;
}
@Override
public void run() {
super.run();
System.out.println("进入 run 了");
while (isRunning == true ){
synchronized (this){
}
}
System.out.println("线程 被停止了 !");
}
public static void main(String[] args) {
try {
RunThread thread = new RunThread();
thread.start();
Thread.sleep(1000);
thread.setRunning(false);
System.out.println("已经赋值为 false");
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
运行结果
📚 小结
synchronized 关键字会把私有内存中的数据同公共内存同步,使私有内存中的数据和公共内存中的数据一致。
🔖 2.3.2 原子性与非原子性的测试
在 32 位 JDK 与 64 位 JDK版本中,long 与 double 64位的数据类型是否实现赋值写原子性有着不同。
📚 volatile 关键字最知名的缺点是 不支持运算原子性,也就是多个线程对用 volatile 修饰的变量 i 执行 i--
或 i++
操作还是会被分解成三步,造成非线程安全问题。
⭐️ volatile 操作是非原子性的
volatile 关键字不支持 i ++ 运算原子性,使用多线程执行 volatile int i ++ 赋值操作是非原子性的 , i – 操作的行为也是一样的。
示例代码
/***
* @author: Alascanfu
* @date : Created in 2022/6/29 22:25
* @description:
* @modified By: Alascanfu
**/
public class MyThread extends Thread {
public static volatile int count;
private static void addCount() {
for (int i = 0; i < 100; i++) {
count++;
}
System.out.println("count = " + count);
}
@Override
public void run() {
super.run();
addCount();
}
public static void main(String[] args) {
MyThread[] myThreads = new MyThread[100];
for (int i = 0; i < 100; i++) {
myThreads[i] = new MyThread();
}
for (int i = 0; i < 100; i++) {
myThreads[i].start();
}
}
}
运行结果
📚 小结:
- volatile 关键字主要是在多个线程中可以感知实例变量被更改了,并且可以获得最新的值时使用,增加了可见性时使用。
- volatile 关键字提示线程每次从公共内存中去读取变量,而不是从私有内存中去读取,这样就保证了同步数据的可见性。
值得注意的是:如果修改实例变量中的数据,比如 i++ 则这样的操作其实并不是一个原子操作,也就是非线程安全的
📚 总结:volatile 保证数据在线程之间的可见性,但不保证同步性。
⭐️ 使用 Atomic 原子类进行 i++ 操作实现原子性
原子操作是不能分割的整体,没有其他线程能够中断或检查正在原子操作中的变量。一个原子(atomic) 类型就是一个原子操作可用的类型,它可以在没有锁的情况下做到线程安全(thread-safe)
示例代码
import java.util.concurrent.atomic.AtomicInteger;
/***
* @author: Alascanfu
* @date : Created in 2022/6/29 22:52
* @description: Atomic Integer Test
* @modified By: Alascanfu
**/
public class AddCountThread extends Thread{
private AtomicInteger count = new AtomicInteger(0);
@Override
public void run() {
for (int i = 0 ; i < 10000 ; i++){
System.out.println(count.incrementAndGet());
}
}
public static void main(String[] args) {
AddCountThread countThreadService = new AddCountThread();
Thread t1 = new Thread(countThreadService);
t1.start();
Thread t2 = new Thread(countThreadService);
t2.start();
Thread t3 = new Thread(countThreadService);
t3.start();
Thread t4 = new Thread(countThreadService);
t4.start();
Thread t5 = new Thread(countThreadService);
t5.start();
}
}
运行结果
⭐️ 逻辑混乱与解决方案
及时在有逻辑性的情况下,原子类的输出结果也具有随机性。
📚 方法和方法之间的调用是非原子的,此时可以通过同步解决该问题。
🔖 2.3.3 禁止代码重排序的测试
什么是重排序?
在 Java 程序运行时,JIT(Just-In-Time Compiler , 即时编译器)为了优化程序的运行,可以动态地改变程序代码运行的顺序。
A 代码 - 重耗时
B 代码 - 轻耗时
C 代码 - 重耗时
D 代码 - 轻耗时
在多线程的环境当中,JIT 有可能进行代码重排,重排后的代码顺序可能如下:
B 代码 - 轻耗时
D 代码 - 轻耗时
A 代码 - 重耗时
C 代码 - 重耗时
- 这样做的主要原因是在 CPU 流水线中 这 4 个指令 同时执行的 , 轻耗时的代码 在很大程度上会先执行完,以让出 CPU 流水线资源 供其他指令使用, 所以代码重排是为了追求更高的程序运行效率。
- 重排序发生在没有依赖关系时
- volatile 关键字可以禁止 代码重排序 。
比如代码如下
A 变量的操作
B 变量的操作
volatile Z 变量的操作
C 变量的操作
D 变量的操作
1️⃣ AB 可以重排序
2️⃣ CD 可以重排序
3️⃣ AB 不可以重排到 Z 的后面
4️⃣ CD 不可以重排到 Z 的前面
📚简单来说:变量 Z 是一道屏障,是一堵 Z 变量之前或之后的代码不可以跨域 Z 变量。synchronized 关键字也具有同样的特性。
⭐️ volatile 关键字之前的代码可以重排
⭐️ volatile 关键字之后的代码可以重排
⭐️ volatile 关键字之前的代码不可以重排到volatile之后
⭐️ volatile 关键字之后的代码不可以重排到volatile之前
⭐️ synchronized 关键字之前的代码不可以重排到 synchronized 之后
⭐️ synchronized 关键字之后的代码不可以重排到 synchronized 之前
🔖 总结
synchronized 关键字的主要作用:保证同一时刻,只有一个线程可以执行某一个方法或者某一个代码块。 synchronized 可以修饰方法以及代码块,随着 JDK 的版本升级 , synchronized 在执行效率上得到了很大的提升。它包括了可见性、原子性和禁止代码重排序。
volatile关键字的主要作用是让其他线程可以看到最新的值,volatile 只可以修饰变量。它也包括三个特征:可见性、操作非原子性、禁止代码重排序。
⭐️ synchronized 与 volatile 的使用场景
1️⃣ 当想实现一个变量的值被更改,而其他线程能取到最新的值时,就要对变量使用 volatile 。
2️⃣ 如果多个线程对同一个对象中的同一个实例变量进行写操作,为了避免出现非线程安全问题,就要使用 synchronized。