线程安全问题及解决方案(下)

Java-线程安全问题及解决方案

  • 1.示例
    • 示例代码
    • 原因分析
    • 解决方案
  • 2.示例
  • 结尾

影响线程安全问题的因素有很多
包括但不限于:

  • 内存可见性
  • 指令重排序

本篇将通过实例对上述原因进行讲解

1.示例

示例代码

import java.util.Scanner;

public class Test {
    public static int n = 0;

    public static void main(String[] args) {
        Thread t1 = new Thread(() -> {
           while(n == 0) {
               //空着
           }
            System.out.println("线程结束运行");
        },"这是t1线程");

        Thread t2 = new Thread(() -> {
            Scanner scan = new Scanner(System.in);
            System.out.println("请输入一个不等于0的数字终止线程:");
            n = scan.nextInt();
        });

        t1.start();
        t2.start();
    }
}

代码描述:

  • 上述代码启动了 t1,t2两个线程
  • t1线程的while()循环体内部是空着的
  • 通过t2线程修改n的值让while()的循环条件不满足,从而让线程t1结束运行

那么这样做能否成功呢?
答案是不能

运行结果
线程安全问题及解决方案(下)_第1张图片
线程安全问题及解决方案(下)_第2张图片

通过运行结果我们看到 t1线程仍然处于运行状态


原因分析

线程安全问题及解决方案(下)_第3张图片

注意这里的while(n==0)

此处需要执行2个步骤

  • 1从内存读取数据n到寄存器
  • 2比较寄存器中的值是否等于0

小知识
访问速度:寄存器>内存>硬盘

由于寄存器的访问速度大于内存,且循环体内部是空着的.
每次从内存读取n的值到寄存器,读取的结果是相同的(由于循环体是空着的,所以几乎不占用时间执行循环体里的操作,所以在较短的时间内读取了多次n的值,发现n的值还是0).
读取操作(内存)相对于比较操作(寄存器)是一个比较大的时间开销,编译器就默认帮我们进行了优化
这也就解释了为什么 t2线程修改n的值t1线程没能停下来

上述现象可以解释为内存可见性的缘故

所谓内存可见性,就是多线程环境下,编译器对代码进行了优化,产生了误判,从而引起了bug


那么如果循环体里不是空着的, t1线程是不是就会停下来了呢?
答案是对的

运行结果
线程安全问题及解决方案(下)_第4张图片

线程安全问题及解决方案(下)_第5张图片

线程安全问题及解决方案(下)_第6张图片

当循环体非空时,读取操作就不再被看作是一个比较大的时间开销,编译器也就不再帮忙进行优化了


解决方案

当循环体为空时,可以加入volatile避免编译器进行优化
直接访问工作内存(寄存器), 速度非常快, 但是可能出现数据不一致的情况.
加上 volatile , 强制读写内存. 速度是慢了, 但是数据变的更准确了
也就是说,volatile 能保证内存可见性

完整代码

public class Test2 {
    volatile public static int n = 0;

    public static void main(String[] args) {
        Thread t1 = new Thread(() -> {
           while(n == 0) {
               //空着
               /*try {
                   Thread.sleep(1000);
               } catch (InterruptedException e) {
                   e.printStackTrace();
               }
               System.out.println("循环体非空");*/
           }
            System.out.println("线程结束运行");
        },"这是t1线程");

        Thread t2 = new Thread(() -> {
            Scanner scan = new Scanner(System.in);
            System.out.println("请输入一个不等于0的数字终止线程:");
            n = scan.nextInt();
        });

        t1.start();
        t2.start();

    }
}

运行结果

线程安全问题及解决方案(下)_第7张图片

2.示例

volatile还有另外一个作用,禁止指令重排序

那么什么是指令重排序呢

举个栗子

有一天
你的女朋友让你去超时帮她分别买(1)薯片(2)旺仔牛奶(3)QQ糖(4)曲奇饼干

线程安全问题及解决方案(下)_第8张图片

这时你选择的路线是入口–>(1)薯片–>(4)曲奇饼干–>(3)QQ糖–>(2)旺仔牛奶–>出口
编译器就会帮你进行优化(指令重排序)(执行顺序入口–>(1)薯片–>(2)旺仔牛奶–>(4)曲奇饼干->(3)QQ糖–>出口)

此时如果加入volatile就可以让编译器不再帮你进行优化

结尾

创作不易,如果对您有帮助,希望您能点个免费的赞
大家有什么不太理解的,可以私信或者评论区留言,一起加油

你可能感兴趣的:(JavaEE,java,开发语言,java-ee)