前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >刨析线程的安全问题

刨析线程的安全问题

作者头像
东边的大西瓜
发布2022-05-05 12:21:36
3730
发布2022-05-05 12:21:36
举报
文章被收录于专栏:吃着西瓜学Java

刨析线程的安全问题

什么是线程安全问题?

认识线程安全前需要先引入与线程安全密不可分的一个概念:共享资源。

所谓的共享资源,就是一个资源被多个线程所共同持有或访问。

在早期我们刚接触编程的时候,首先学习的就是相应的编程语法,和基础的程序设计(算法),刷刷oj。这时我们所编写的代码程序还都处于单线程的顺序执行时期,这时我们所写的程序肯定是线程安全的,过渡到多线程环境下也一样,线程安全就是指在写这些程序时我们不需要去额外的考虑线程的调度和交替执行,也不用去做额外的同步,我们就可以获得正确的结果。

相对的线程安全问题就是指,在多线程环境下,读写一个共享资源,由于没有任何的同步措施,导致结果错误或者脏数据等不可遇见的问题。

什么是Java指令重排序?

Java内存模型允许编译器和处理器对指令重排序以提高运行性能,但是只会对不存在数据依赖性的指令重排。

由于这一特性,所以在单线程情况下并不会影响最终的结果。但是在多线程情况下由于指令的重排可能会出现与预期结果不一致等问题:

代码语言:javascript
复制
public class ResetProblem extends Thread {
    private static int num = 0;
    private static boolean flag = false;
    @Override
    public void run() {
        while (!Thread.currentThread().isInterrupted()){
            System.out.println(num+" "+flag); //①
       }
    }
    static class WThread extends Thread{
        @Override
        public void run() {
            num = 1;  //②
            flag = true; //③
        }
    }

    public static void main(String[] args) throws InterruptedException {
        ResetProblem resetProblem = new ResetProblem();
        resetProblem.start();
        WThread writethread = new WThread();
        writethread.start();
        Thread.sleep(1000);
        resetProblem.interrupt();
    }

}

如上述代码,我们理想化的执行顺序是②->③->①,但是由于因为操作①②①并不存在相互依赖关系,为了优化会发生1指令重排,所以在多线程的环境下可能实际的执行顺序为②->①->③,导致结果并不是预计的(1,true),而是(0,true);

什么是共享变量的内存可见性问题?

Java内存模型规定了所有的变量都存储在主存中,当线程使用变量时,会将内存中的变量复制到自己的工作内存中,线程对变量的所有操作(读写等)都必须在工作内存中进行,而不 能直接对主存进行操作。

基于这种规定,那么我们现在假设有两个线程A,B共享一个变量X,X初始值为0,会出现什么情况呢?

  1. 首先线程A和线程B都获取共享变量X,并将变量X=0复制到自己各自的工作空间中;
  2. 过程①线程A修改X的值使得X=1,并刷新到主存中,使主存的X=1
  3. 过程②线程B需要修改X的值了,但是由于其工作内存中已存在X变量,那么就会优先获取工作内存中的X=0,而不是主存中实际上已被修改的X=1;

线程B获取到的变量值,并不是已被线程A修改后的变量值,也就是说线程B写入的值对线程A不可见,这就是共享变量的内存不可见问题。

什么是原子性操作?

在原子刚被发现的时候被认为是不可分割的,尽管已经发现了比原子更小的中子、质子和夸克。所以我们对原子性操作的定义就是,执行的一系列操作要么全部执行,要么全部不执行,是不可分割的操作。 J a v a 虚 拟 机 规 定 lock、unlock、read、load、use、assign、store、write这八种操作是原子性的操作。

举两个简单的例子:

  1. 下面是一个赋值语句,该语句是原子操作; int i = 1;
  2. i++就不是一个原子操作,因为这个看上去只要一段代码就能实现的计数器操作,需要读-改-写三个步骤,而且在每个步骤之间都有被打断的可能;
  • 读取
  • 改写(增加)
  • 写入

如下图所示看一下,在多线程情况下i++操作可能发生的安全问题:

  1. 线程A首先拿到i=1,在进行i+=1的时候,切换为线程B,线程B拿到i=1;
  2. 由线程B切换为线程A,并进行i+=1操作,切换为线程B,线程B进行i+=1操作,并写入i=2;
  3. 最后由线程B切换为线程A,并进行最后的写入操作,写入i=2;

所以说在这种情况下,就算线程A和线程B的共享内存可见,由于该操作不是原子操作,也不能保证线程的安全性。

常见的线程安全问题

访问共享资源

代码语言:javascript
复制
    public static int i = 0;
    public static void main(String[] args) throws InterruptedException {
        Runnable r = ()->{
            for (int j = 0; j < 100000; j++) {
                i++;
            }
        };
        Thread thread1 = new Thread(r);
        Thread thread2 = new Thread(r);
        thread1.start();
        thread2.start();
        thread1.join();
        thread2.join();
        System.out.println(i);
    }

我们的预期值是200000,但是实际确是140901,而且每次执行结果都不一样。

依赖时许的操作

代码语言:javascript
复制
if(map.containsKey(key)){
     map.remove(key);
}

线程A先进入if语句中,之后切换线程为线程B,线程B进入if语句中并执行的remove操作,然后切换为线程A,但是实际上线程B已经进行该操作,所以此处会导致线程不安全问题。

下篇我们来聊一聊,如何解决线程的安全问题,和synchronized、volatile关键字

本文参与 腾讯云自媒体同步曝光计划,分享自微信公众号。
原始发表:2021-03-05,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 吃着西瓜学Java 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 刨析线程的安全问题
    • 什么是线程安全问题?
      • 什么是Java指令重排序?
        • 什么是共享变量的内存可见性问题?
          • 什么是原子性操作?
            • 常见的线程安全问题
              • 访问共享资源
              • 依赖时许的操作
          领券
          问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档