【多线程】(基础二)

本文详细介绍了多线程环境下的线程安全问题,包括操作系统随机调度、非原子性操作、内存可见性和指令重排序等因素导致的不安全因素。通过示例代码展示了线程不安全的情况,并探讨了解决方案,如使用`synchronized`关键字和`volatile`变量。同时,讲解了主内存与工作内存的概念,以及`wait()`、`notify()`在控制线程执行顺序中的作用。
摘要由CSDN通过智能技术生成

线程安全问题

多线程带来的风险

线程安全问题:在操作系统的随机调度下 ,多个线程的并发执行会产生多种可能,可能会产生BUG

package thread;

class Test{
   
     int a;
     public void func(){
   
         a++;
     }
}

public class Demo8 {
   
    public static Test test = new Test();

    public static void main(String[] args) throws InterruptedException {
   
        Runnable runnable1 = new Runnable() {
   
            @Override
            public void run() {
   
                for (int i = 0; i < 50000; i++) {
   
                    test.func();
                }
            }
        };
        Thread thread1 = new Thread(runnable1);

        Runnable runnable2 = new Runnable() {
   
            @Override
            public void run() {
   
                for (int i = 0; i < 50000; i++) {
   
                    test.func();
                }
            }
        };
        Thread thread2 = new Thread(runnable2);

        thread1.start();
        thread2.start();
        thread1.join();
        thread2.join();
        System.out.println(test.a);
    }
}

  • 我们使用两个线程对同一个变量分别自增了5000次,按理来说最后结果应该是10000,下面我们来看结果

image-20220801203826775

image-20220801203839614

  • 可以看出运行多次结果都不相同,不是10000,原因是:

a++; 这一行代码对应了三条机器指令:

从内存读取数据到CPU(load),在CPU寄存器中完成加法运算(add),把运算结果放到内存中(save)

那由于两个线程的执行是调度器随机调度的,所以在某一时刻,这两个线程可能都在CPU的不同核心上运行(并行执行),也可能只有一个在CPU上执行,也可能俩线程都没在CPU上执行。如果俩线程并行执行那就可能会出问题,下面具体来看:

image-20220801212243325

  • 如果线程在执行时有这样的时间关系,也就是线程1从Load到Save的时间段内又有线程2的++操作穿插进来了,那么两次Load读到的值是一样的,假如都Load了个0,则Save了1,那这两个++操作的执行效果其实是和一次++一样的,明明是两次自增操作,结果却只增了一次,这就是典型的线程不安全。
  • 那其实也有可能线程1和2的++操作是完全串行化的,那两次自增操作,结果就会加2
  • 极端情况下线程1和线程2中的所有++操作都如上图一样,那最终的值就是5000,如果线程1和线程2是完全串行化的,那最终结果就是10000,所以最后的值范围是5000~10000之间

线程不安全的原因

操作系统随机调度

操作系统随机调度/抢占式执行,是造成线程安全问题的罪魁祸首,这个是操作系统的调度器的逻辑,我们是无法改变的

多个线程修改同一个变量

image-20220802103248291

在多线程带来的风险演示的代码中test.a在堆区,各个线程共享,如果多个线程同时修改同一个变量,就有可能造成多线程带来的风险中的bug

非原子性

有些修改操作不是原子的(不可分割的最小单位),比如上面的++操作,就对应了三个指令,这就不是原子的,而 =(赋值)就对应了一条机器指令,就是原子的。

内存可见性

可见性指, 一个线程对共享变量值的修改,能够及时地被其他线程看到

在下面这样的一个场景中:一个线程读,一个线程写就比较容易引发内存可见性问题

image-20220802112959056

线程1在频繁的读和判断,如果中间线程2突然写了一下内存(线程1和线程2都是针对同一个数据),那线程2写完之后,线程1就能立刻读出变化了的内存数据,从而让判断出现变化。

但是程序在运行中,可能会出现优化,这个优化可能是编译器的优化,也可能是JVM的优化,也可能是操作系统的优化,下面具体来看一下如何优化的:

image-20220802113827342

Load是读内存,Test是在寄存器中做判断,所以这个Load操作就比Test消耗时间多得多,那既然线程1频繁读内存操作都是读取到的同一个值,又耗时这么高,那所以JVM就做出优化:不再重复的从内存中读了,直接复用第一次从内存读到寄存器中的值进行每次的判断

那一优化就可能会出现问题:假如判断过程中,线程2执行了一次写操作,把这个共享的数据给改了,那线程1是感知不到内存数据的变化的,线程1还是用的寄存器中的值做判断,没有从内存中读数据,那就不能及时的做出相应的反应,这就是内存可见性问题(内存被修改,但是读不到,看不见),但是对于单线程就没有这样的问题,单线程中,你去写内存,CPU中执行的是写的指令,没有执行判断的指令,然后写完之后,再去判断,就从内存读数据再判断,判断和写肯定是有先后关系的,但是多线程就是并发的关系,写的时候依然可以进行判断

用volatile就可以解决这个内存可见性问题,让某个变量不要优化

指令重排序

指令重排序是对代码的执行顺序进行调整,以提升运行速度,也是编译器/JVM/操作系统的一种优化,但是在多线程的环境下,就可能会产生BUG

比如 Test test = new Test(); 可以分为三个指令 1.创建内存空间,2.往这个内存空间上构造一个对象,3.test引用这块内存空间的地址 ,而2和3是可以调换顺序的,在单线程下调换顺序是没啥影响的,但是如果在多线程下:

如果按照2,3的顺序执行,在另一个线程下获取到的test就是一个有效地址,如果按照3,2执行,那获取到的test就可能是一个无效地址(地址所对应的内存空间中没有

指令重排序也是可以用volatile避免这种问题

synchronized

synchronized作用

synchronized的意思是使同步

synchronized 关键字可以给某个对象加锁,以保证原子性,具体给哪个对象加锁,下面再分析

package thread;

class Test{
   
     int a;
     public synchronized void func(){
   
         a++;
     }
}

public class Demo8 {
   
    public static Test test = new Test();

    public static void main(String[] args) throws InterruptedException {
   

        Runnable runnable1 = new Runnable() {
   
            @Override
            public void run() {
   
                for (int i = 0; i < 50000; i++) {
   
                    test.func();
                }
            }
        };
        Thread thread1 = new Thread(runnable1);

        Runnable runnable2 = new Runnable() {
   
            @Override
            public void run() {
   
                for (int i = 0; i < 50000; i++) {
   
                    test.func();
                }
            }
        };
        Thread thread2 = new Thread(runnable2);

        thread1.start();
        thread2
  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值