线程安全的问题和解决方法

java-code: java代码管理 (gitee.com)

文章目录

一、线程安全是什么?

二、为啥会产生线程安全问题?

1.count++问题

 2.由于内存的可见性导致线程不安全

3.由于指令重排序导致的线程不安全

三、如何避免线程安全问题

总结


一、线程安全是什么?

一个程序在单线程情况下没有问题,但是拿到多线程运行就会产生一些bug

二、为啥会产生线程安全问题?

1.线程调度的随机性(根本原因)(抢占式执行)

2.多个线程对一个变量进行修改

3.修改操作不是原子的

4.内存的可见性

5.指令重排序

举例:

1.count++问题

 class add{
    public static int count=0;
     public  void add(){
        count++;
    }
    public int getcount(){
        return count;
    }
}
public class ThreadDemo {
    public static void main(String[] args) throws InterruptedException {
        add coun=new add();
        Thread t=new Thread(()->{
            for (int i = 0; i <50000; i++) {
                coun.add();
            }

        });
        Thread t2=new Thread(()->{
            for (int i = 0; i <50000; i++) {
                coun.add();
            }
        });
        t.start();
        t2.start();
        t.join();
        t2.join();
        System.out.println(coun.getcount());
    }
}

在这个多线程程序中,我们的期望应该两个线程各自加了五万次,最后输出count应该是100000,但是实际运行出来

结果并不是100000,这就是多线程导致的线程不安全问题,具体是为什么呢?

这里我们可以把count++这一步分为三个cpu指令,分别是

load:从内存中拿出来

add:CPU运算结果

save:将结果写回内存

由于线程的调度是随机的,所以你无法确定到底是先执行那一句??这就导致三个cpu指令会有很多不同的排序,产生不可预料的BUG。(这里简单举例几种)

 

此时就拿中间的举例,t1线程从内存中读取count为0,然后给cpu++得到1,但是并不是立即写会内存,此时t2线程读取内存的count也为0,add++得到1,然后写回内存,此时count为1了,但是t1线程再次写会,结果也是1,这就出现了加两次只实现了一次的BUG,这一切都是由于线程的调度是抢占式运行的!!(还有修改操作不是原子性)

 2.由于内存的可见性导致线程不安全

import java.util.Scanner;

public class ThreadDemo2 {
     public  static  int flag=0;
    public static void main(String[] args) throws InterruptedException {
        Thread t1=new Thread(()->{
            while (flag==0){

            }
            System.out.println("循环结束");
        });
        Thread t2=new Thread(()->{
            Scanner scanner=new Scanner(System.in);
            flag=scanner.nextInt();
        });
        t1.start();
        t2.start();
    }
}

这个程序的预期结果是:线程t1和t2一起并发执行,t1一直在循环,当t2线程输入一个非0的数,就会改变flag,从而使t1线程停下来。

但是运行之后我们可以看到,线程并没有停下来,这就是内存的可见性导致的线程不安全问题

 具体是因为:我们的编译器都有一些自动优化的功能,在where这一句可以分为两个cpu指令

load:从内存中读取数据

cmp:从寄存器中比较是否为0     此时load的开销是远远大于cmp的

因为线程执行的是非常快的,当我们的编译器发现,load开销非常大,并且每次读取的都是同一个结果,编译器就会自动的把load优化掉,这样就导致,只有第一次load的值,后面再对load的值修改,线程也不会再读取到了,线程就只cmp不load了,所以就导致了后面我们修改了flag线程也并没有结束。

 总而言之就是:由于编译器的优化导致的BUG,这些优化对单线程可能没什么问题,有些时候多线程就会产生问题

编译器优化:能够只能的调整你的代码的实行逻辑,保证程序结果不变的情况下,通过加减语句,通过语句变换等操作,让你的代码执行效率大大提升

3.由于指令重排序导致的线程不安全

volatile还有个效果:禁止指令重排序,指令重排序也是编译器的优化,但是可能也会导致线程不安全。

 

指令重排序:调整代码执行的顺序,让程序更高效,前提也是保证整体逻辑不变~

三、如何避免线程安全问题

1.java中有锁来解决修改操作不是原子性:

public  void add(){
         synchronized (this){
        count++;
         }
    }

java中使用synchronized来给线程加锁

一旦某个线程/操作加锁之后,其他的线程再想上锁,就不能直接上锁,必须阻塞等待,一直等到拿到锁的线程释放了才能加锁(多个线程抢占式加锁,不管先后)

java使用代码块来修饰,在java中加锁和解锁都是自动的,进入代码块加锁,出了代码块就会自动解锁

这里的this是锁对象,如果两个线程给一个锁对象加锁,此时就会出现“锁竞争”问题,一个线程先拿到锁,另一个线程就得阻塞等待。

注意:()里填什么都是,只要是个Object的实例就行(内置类型不行),具体填什么,看你给谁上锁,如果两个线程对一个对象加锁就会有锁竞争,不同对象就不会有锁竞争

注意join和加锁的区别:加锁是只有加锁的那一块逻辑,这一块逻辑变成串行,其他的逻辑还是可以并发的,join是整个线程完全串行,效率上来说加锁还是比join高效很多

此时再次运行就可以得到正确的结果了

2.使用volatile来让编译器优化失效(指令重排序也是编译器优化,使用volatile也可以解决)

使用volatile修饰需要的变量,就可以让编译器对这个变量不在优化,就可以达到每次都load,都从内存中读取flag。


总结

介绍了三个多线程安全问题和JAVA 是如何解决多线程安全问题的,谢谢老铁支持~

 

  • 3
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 3
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值