自学内容网 自学内容网

线程安全问题

目录

一、线程安全问题

二、线程安全问题原因

修改共享数据

原子性

内存可见性

指令重排序


一、线程安全问题

多线程带给我们效率提升的同时,也为我们带来了风险-线程安全,因为多线程的抢占式执行,带来的随机性。
我们想使用两个线程将一个变量同时增加5000次

class Num{
    public int num;
    public void add() {
        this.num++;
    }
}
public class ThreadDemo {
    public static void main(String[] args) {
        Num num = new Num();
        //创建两个线程,分别对调用5000次add()
        Thread t1 = new Thread(() -> {
            for (int i = 0; i < 5000; i++) {
                num.add();
            }
        });
        Thread t2 = new Thread(() -> {
            for (int i = 0; i < 5000; i++) {
                num.add();
            }
        });
        //启动线程
        t1.start();
        t2.start();
        try {
            //等待线程结束
            t1.join();
            t2.join();
        } catch (InterruptedException e) {
            throw new RuntimeException(e);
        }
        System.out.println("num = "+num.num);
    }
}

++操作本质上要分为三步
1.先把内存中的值读取到CPU的寄存器中 load指令
2.把CPU寄存器里的数值+1操作 add指令
3.把结果值保存到内存中 save指令

此时两个线程的并发执行,就相当于两组load add save指令并发执行。所以这两组指令的执行顺序存在了许多可能性。
这里我举一个线程安全的例子和线程不安全的例子。

二、线程安全问题原因

最根本的原因: 抢占式执行,随机调度

修改共享数据

我们上述线程代码之所以不安全,因为涉及到我们两个线程同时去修改一个相同的变量。
一个线程修改一个变量,安全
多个线程读取同一个变量,安全
多个线程修改多个不同的变量,安全

原子性

什么是原子性?

我们把一段代码想象成一个房间,每个线程就是要进入这个房间的人。如果没有任何机制保证,A进入房间之后,还没有出来;B 是不是也可以进入房间,打断 A 在房间里的隐私。这个就是不具备原子性的。那我们应该如何解决这个问题呢?是不是只要给房间加一把锁,A 进去就把门锁上,其他人是不是就进不来了。这样就保证了这段代码的原子性了。

不保证原子性会给多线程带来什么问题?

一个线程正在对一个变量操作,中途其他线程插进来了,对这个操作造成了打断,可能会造成结果的错误。这和线程的抢占式调度有关,如果不是抢占式,就算不是原子性,也问题不大。

内存可见性

ava内存模型(JMM): java虚拟机规定了java内存模型。
目的: 屏蔽各种硬件和操作系统的内存访问差异,实现java程序在各平台下都达成一致的并发效果

1.线程之间的共享变量存在 主内存 (Main Memory).
2.每一个线程都有自己的 “工作内存” (Working Memory) .
3.当线程要读取一个共享变量的时候, 会先把变量从主内存拷贝到工作内存, 再从工作内存读取数据.
4.当线程要修改一个共享变量的时候, 也会先修改工作内存中的副本, 再同步回主内存.
一旦线程t1修改了变量,但未来得及同步,对应t2读取工作内部的值就可能出现错误。

这个时候代码就会出现问题。

为什么要整这么多内存?

其实没有这么多内存,只能java中进行了"抽象"的叫法。
主内存: 硬件角度的内存
工作内存: CPU寄存器 和 高度缓冲器Cache

为什么要复制来复制去?

因为在内存读取速度相比于工作内存的读取速度慢了好几个数量级,但是工作内存有十分小(而且还贵),所以就得不断地复制,将急需的一些东西放在里面。

指令重排序

指令重排序实际上也是编译器优化,简单的来说,就是我们把一个东西写的太烂了,JVM.CPU指令集会对其进行优化。
编译器对于指令重排序的前提是 “保持逻辑不发生变化”. 这一点在单线程环境下比较容易判断, 但是在多线程环境下就没那么容易了, 多线程的代码执行复杂程度更高, 编译器很难在编译阶段对代码的执行效果进行预测, 因此激进的重排序很容易导致优化后的逻辑和之前不等价


原文地址:https://blog.csdn.net/hhj25802580/article/details/143781693

免责声明:本站文章内容转载自网络资源,如本站内容侵犯了原著者的合法权益,可联系本站删除。更多内容请关注自学内容网(zxcms.com)!