【JavaEE】并发编程(多线程)线程安全问题&内存可见性&指令重排序

这篇具有很好参考价值的文章主要介绍了【JavaEE】并发编程(多线程)线程安全问题&内存可见性&指令重排序。希望对大家有所帮助。如果存在错误或未考虑完全的地方,请大家不吝赐教,您也可以点击"举报违法"按钮提交疑问。

目录

第一个问题:什么是线程安全问题?

第二个问题:为什么会出现线程安全问题? 

第三个问题:如何解决多线程安全问题? 

第四个问题:产生线程不安全的原因有哪些? 

第五个问题:内存可见性问题及解决方案 

第六个问题:指令重排序问题?


第一个问题:什么是线程安全问题?

线程安全就是多线程访问时,采用了加锁机制,当一个线程访问该类的某个数据时,进行保护,其他线程不能进行访问直到该线程读取完,其他线程才可使用。不会出现数据不一致或者数据污染。 线程不安全就是不提供数据访问保护,有可能出现多个线程先后更改数据造成所得到的数据是脏数据。 

第二个问题:为什么会出现线程安全问题? 

出现线程安全的问题的根源其实是在于我们之前说过的多线程“抢占式执行,随机调度”的特性决定的。当我们在使用多线程进行编程的时候,是躲不过这一“万恶之源”的。我们只可以通过一些编程手段来解决这些线程安全的问题。

我们可以看一下下面这部分的代码。

(这是一个典型的多线程的线程安全问题,里面会出现脏数据,也就是多个线程对同一个变量进行更改的问题)

首先我们来看当我们写两个线程进行更改同一个变量的情况:

package Thread;

class Countsum{
    private static int count=0;
    public  void CountAdd(){
        count++;
    }
    public int getCount(){

        return count;
    }
}
public class ThreadDemo15 {
    public static void main(String[] args) {
        Countsum countsum=new Countsum();
        //第一个线程t1
        Thread t1 =new Thread(()->{

            for (int i = 0; i <50000; i++) {
                countsum.CountAdd();
            }

        });
        //第二个线程t2
        Thread t2=new Thread(()->{
            for(int i=0;i<50000;i++){
                countsum.CountAdd();
            }
        });
        //两个线程操作同一个变量
        t1.start();
        t2.start();
        //让t1,t2两个线程执行完,再执行main线程,这里让main线程阻塞
        try {
            t1.join();
        } catch (InterruptedException e) {
            throw new RuntimeException(e);
        }
        try {
            t2.join();
        } catch (InterruptedException e) {
            throw new RuntimeException(e);
        }
        //打印最后的结果,看和预期值10_0000是否一致。
        System.out.println(countsum.getCount());
    }


}

预期值:10_0000

第一次运行:64603【JavaEE】并发编程(多线程)线程安全问题&内存可见性&指令重排序 

第二次运行:73388

【JavaEE】并发编程(多线程)线程安全问题&内存可见性&指令重排序

第三次运行:75233

【JavaEE】并发编程(多线程)线程安全问题&内存可见性&指令重排序

每一次的结果都和预期值相差甚远。这就说明期间发生了脏读了,也揭示了线程的不安全性。

那么具体的过程是怎样变成这样的?

首先我们需要知道count++这个过程到底是怎么实现的。

我们从CPU的角度出发:count++主要是由三个指令实现的

1、(load)把内存中count的值加载到CPU的寄存器当中

2、(add)把寄存器中的数值加1

3、(save)把寄存器中的值放回到内存中,对原来的值进行覆盖。

我们画个示意图:

【JavaEE】并发编程(多线程)线程安全问题&内存可见性&指令重排序

同样,也正是因为这个过程需要多个步骤来进行实现,就使得多线程的“抢占式执行,随机调度”得以充分发挥作用了。我们都知道排列组合。在这10万次循环中,会有无数种排列的情况出现,所以基本上每一次的结果不会相等,但是不排除相等的情况。

这里我们就列举一种情况来进行说明即可:

比如这种情况:

【JavaEE】并发编程(多线程)线程安全问题&内存可见性&指令重排序

我们来分析一下这个过程:

假设初始值为0.

【JavaEE】并发编程(多线程)线程安全问题&内存可见性&指令重排序 如果是正常情况下结果应该是2,但是这里结果却是1。这就和上面的程序是一样的道理。

如果要得到正确结果应该是这种的步骤:

【JavaEE】并发编程(多线程)线程安全问题&内存可见性&指令重排序

就是像这样的能够得到正确的数据。 

第三个问题:如何解决多线程安全问题? 

答案:加锁 

那么java中加锁的方式有很多种,最常使用的是 synchronized 关键字。我们可以给上述代码的自增函数内部自增操作上加synchronized 关键字或者直接给自增的方法加上synchronized关键字就是加锁成功了。

加锁成功后在看一下程序:

package Thread;

class Countsum{
    private static int count=0;
    public  void CountAdd(){
        synchronized (this){
            count++;
        }

    }
    public int getCount(){

        return count;
    }
}
public class ThreadDemo15 {
    public static void main(String[] args) {
        Countsum countsum=new Countsum();
        //第一个线程t1
        Thread t1 =new Thread(()->{

            for (int i = 0; i <50000; i++) {
                countsum.CountAdd();
            }

        });
        //第二个线程t2
        Thread t2=new Thread(()->{
            for(int i=0;i<50000;i++){
                countsum.CountAdd();
            }
        });
        //两个线程操作同一个变量
        t1.start();
        t2.start();
        //让t1,t2两个线程执行完,再执行main线程,这里让main线程阻塞
        try {
            t1.join();
        } catch (InterruptedException e) {
            throw new RuntimeException(e);
        }
        try {
            t2.join();
        } catch (InterruptedException e) {
            throw new RuntimeException(e);
        }
        //打印最后的结果,看和预期值10_0000是否一致。
        System.out.println(countsum.getCount());
    }


}

【JavaEE】并发编程(多线程)线程安全问题&内存可见性&指令重排序

这个结果就和我们的预期值一样了。

第四个问题:产生线程不安全的原因有哪些? 

1、线程是抢占式执行的,线程间的调度充满随机性。(线程不安全的根本原因)

2、多个线程对同一个变量进行修改操作。

3、针对变量的操作不是原子的,通过加锁操作就是把几个指令打包成一个原子的。

4、内存可见性。

 这里需要简单理解一下几个名词:

1)原子性  我们可以简单的理解为打包为一个整体 

第五个问题:内存可见性问题及解决方案 

2)内存可见性

内存可见性问题其实是编译器优化的结果。

我们这里以一个线程读取数字,一个线程修改数字为例:

t线程负责读取istrue的值,main线程负责修改istrue的值。

package Thread;

import java.util.Scanner;

public class ThreadDemo16 {
    public static int istrue=0;
    public static void main(String[] args) {
        Thread t=new Thread(()->{
            while(istrue==0){

            }
            System.out.println("t线程结束!");
        });
        t.start();
        Scanner scanner=new Scanner(System.in);
        System.out.println("请输入一个数字:");
        istrue=scanner.nextInt();
        System.out.println("main线程执行完毕");

    }
}

我们看一下执行结果:

【JavaEE】并发编程(多线程)线程安全问题&内存可见性&指令重排序

当我们输入一个5的时候,我们原来是应该让t线程结束的,然而main线程结束后,t线程却进入了死循环当中,也就是说,此时的istrue还是0,并没有得到修改。这到底是什么原因导致的呢?

原因是这样的:由于从内存中读是要比从寄存器中读慢很多的(好几个数量级吧大概) 这里的t线程需要不断的循环读取istrue的值,如果我们的main线程不做出修改,那么t线程读取到的值就一直是一样的值。于是编译器就可能会进行优化,让t线程直接从寄存器中读取数据,也就是省去了load的操作,这一大胆的行为使得后续我们对istrue进行的修改都无法让t线程感知到,也就是说修改失去了作用。所以t线程并不会终止。

那么如何解决内存可见性的问题呢?

1、使用synchronized 关键字。synchronized 不光能够保证原子性,同时也能够保证内存可见性。被synchronized 包裹起来的代码,编译器就不敢轻易做出上述假设,就相当于手动禁止了编译器的优化。

2、使用volatile关键字。volatile和原子性无关,但是能够保证内存可见性。使得编译器每次都要重新从内存中读取istrue的值。

方案一:使用volatile关键字(最常用)

 public static volatile int istrue=0;

【JavaEE】并发编程(多线程)线程安全问题&内存可见性&指令重排序

方案二: 使用synchronized关键字

有时候我们也可以使用一些别的操作,比如sleep啊等等的,不过这些不太可靠哈。

                while(istrue==0){
                    try {
                        Thread.sleep(1000);
                    } catch (InterruptedException e) {
                        throw new RuntimeException(e);
                    }

                }

【JavaEE】并发编程(多线程)线程安全问题&内存可见性&指令重排序

编译器优化总的来说还是比较玄学的!!! 

说到这,还有一个由编译器优化引发的问题!!! 

第六个问题:指令重排序问题?

指令重排序问题听着挺吓人,其实就是个排序问题罢了。 

指令重排序是编译器优化的结果,编译器会对我们写的代码进行重排序从而来提高编译的效率,但是有时候一旦发生指令重排序,就可能会使得程序与我们预期的结果不同了。(在单线程中指令的重排序不会产生太大的影响,但是在多线程中容易出现严重bug,需要多注意!)我们要保证逻辑不变,对顺序进行调整。(使用synchronized可以进行禁止指令的重排序)文章来源地址https://www.toymoban.com/news/detail-429806.html

到了这里,关于【JavaEE】并发编程(多线程)线程安全问题&内存可见性&指令重排序的文章就介绍完了。如果您还想了解更多内容,请在右上角搜索TOY模板网以前的文章或继续浏览下面的相关文章,希望大家以后多多支持TOY模板网!

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处: 如若内容造成侵权/违法违规/事实不符,请点击违法举报进行投诉反馈,一经查实,立即删除!

领支付宝红包 赞助服务器费用

相关文章

  • JavaEE之多线程编程:4. 线程安全(重点!!!)

    下面我们来举个例子: 我们大家都知道,在单线程中,以下的代码100%是正确的。 但是,两个线程,并发的进行上述循环,此时逻辑可能就出现问题了。 上述这样的情况就是非常典型的线程安全问题。这种情况就是bug!! 只要实际结果和预期的结果不符合,就一定是bug。 想

    2024年01月25日
    浏览(42)
  • 【Java并发编程】变量的线程安全分析

    1.成员变量和静态变量是否线程安全? 如果他们没有共享,则线程安全 如果被共享: 只有读操作,则线程安全 有写操作,则这段代码是临界区,需要考虑线程安全 2.局部变量是否线程安全 局部变量是线程安全的 当局部变量引用的对象则未必 如果给i对象没有逃离方法的作用

    2024年02月08日
    浏览(56)
  • 【JavaEE】多线程之线程安全(synchronized篇),死锁问题

    线程安全问题 观察线程不安全 线程安全问题的原因  从原子性入手解决线程安全问题 ——synchronized synchronized的使用方法  synchronized的互斥性和可重入性 死锁 死锁的三个典型情况  死锁的四个必要条件  破除死锁 在前面的章节中,我们也了解到多线程为我们的程序带来了

    2024年02月01日
    浏览(58)
  • 关于并发编程与线程安全的思考与实践

    作者:京东健康 张娜 并发编程的意义是充分的利用处理器的每一个核,以达到最高的处理性能,可以让程序运行的更快。而处理器也为了提高计算速率,作出了一系列优化,比如: 1、硬件升级:为平衡CPU 内高速存储器和内存之间数量级的速率差,提升整体性能,引入了多

    2024年02月03日
    浏览(64)
  • java八股文面试[多线程]——并发三大特性 原子 可见 顺序

        AutomicInteger :  volatile + CAS 总线LOCK  MESI 两个协议 TODO volatile的可见性和禁止重排序是怎么实现的: DCL场景:  new操作会在字节码层面生成两个步骤: 分配内存、调用构造器 然后把引用赋值给singleton 不加volatile则会发生指令重排,可能得到不完整的对象 知识来源: 【并

    2024年02月11日
    浏览(53)
  • JUC并发编程-集合不安全情况以及Callable线程创建方式

    1)List 不安全 ArrayList 在并发情况下是不安全的 解决方案 : 1.Vector 2.Collections.synchonizedList() 3. CopyOnWriteArrayList 核心思想 是,如果有 多个调用者(Callers)同时要求相同的资源 (如内存或者是磁盘上的数据存储),他们 会共同获取相同的指针指向相同的资源 , 直到某个调用者

    2024年01月23日
    浏览(49)
  • 关于并发编程与线程安全的思考与实践 | 京东云技术团队

    作者:京东健康 张娜 并发编程的意义是充分的利用处理器的每一个核,以达到最高的处理性能,可以让程序运行的更快。而处理器也为了提高计算速率,作出了一系列优化,比如: 1、硬件升级:为平衡CPU 内高速存储器和内存之间数量级的速率差,提升整体性能,引入了多

    2024年02月07日
    浏览(70)
  • 【JavaEE面试题(九)线程安全问题的原因和解决方案】

    大家观察下是否适用多线程的现象是否一致?同时尝试思考下为什么会有这样的现象发生呢? 原因是 1.load 2. add 3. save 注意:可能会导致 小于5w 想给出一个线程安全的确切定义是复杂的,但我们可以这样认为: 如果多线程环境下代码运行的结果是符合我们预期的,即在单线

    2024年02月16日
    浏览(47)
  • 【Java|多线程与高并发】线程安全问题以及synchronized使用实例

    Java多线程环境下,多个线程同时访问共享资源时可能出现的数据竞争和不一致的情况。 线程安全一直都是一个令人头疼的问题.为了解决这个问题,Java为我们提供了很多方式. synchronized、ReentrantLock类等。 使用线程安全的数据结构,例如ConcurrentHashMap、ConcurrentLinkedQueue等

    2024年02月09日
    浏览(47)
  • 线程中并发安全问题(Sychronized关键字的底层原理)

    Sychronized的底层原理 ​ sychronized 对象锁采用互斥方式让同一时刻至多只有一个线程能持有对象锁,其他线程想获取这个对象锁只能被阻塞。 Monitor Sychronized的底层实现Monitor。 WaitSet:关联调用了wait方法的线程,用于存储处于等待状态的线程。 EntryList:关联了没有获得

    2024年02月16日
    浏览(40)

觉得文章有用就打赏一下文章作者

支付宝扫一扫打赏

博客赞助

微信扫一扫打赏

请作者喝杯咖啡吧~博客赞助

支付宝扫一扫领取红包,优惠每天领

二维码1

领取红包

二维码2

领红包