线程安全

文章讨论了线程安全的重要性,分析了多线程环境下导致线程不安全的原因,如抢占式执行的不确定性、多个线程修改同一变量导致的数据不一致、原子性问题以及内存可见性问题。通过示例代码解释了synchronized和volatile关键字在确保线程安全中的作用,synchronized提供互斥访问保证原子性,volatile确保内存可见性但不解决原子性问题。最后提到了指令重排序对线程安全的影响。
摘要由CSDN通过智能技术生成

目录

1.抢占式执行

2.多个线程修改同一个变量

3.原子性

synchronized

4.内存可见性

volatile

 5.指令重排序


什么是线程安全?

如果在多线程环境下的运行结果符合预期,即跟在单线程下的结果一致,那么就说这个程序是线程安全的.

为什么会引入线程安全呢?这是因为多线程环境下会出bug.

又是什么造成的线程不安全呢?根本原因是线程调度不确定(无序/随机).

 下面我们从多个角度分析一些造成线程不安全的主要原因:

1.抢占式执行

多线程执行的顺序是不确定的,随机且无序的.

 


 

2.多个线程修改同一个变量

 下面我们来看一个代码:

我们创建两个线程,每个线程循环 1w 次,累加变量 count 的值,count 默认值为 0,

class Counter{
    public int count=0;
    public void add(){
            count++;
    }
}

public class TestDemo{
    public static void main(String[] args) throws InterruptedException {
        Counter counter=new Counter();
        Thread t1=new Thread(()->{
            int i=0;
            while(i<10000)
            {
                counter.add();
                i++;
            }
        });

        Thread t2=new Thread(()->{
            int i=0;
            while(i<10000){
                counter.add();
                i++;
            }
        });

        t1.start();
        t2.start();
        t1.join();
        t2.join();
        System.out.println(counter.count);

    }
}

此时,我们当然希望代码的运行结果是20000,可是它真的是20000吗?下面我们一起来看一下:

显而易见,与我们的预期不符,那么,是什么造成的的呢?这里涉及到一个概念,多个线程修改同一个变量时,会产生线程不安全.

此时这个 counter.count 是一个多个线程都能访问到的 " 共享数据.

3.原子性

什么是原子性,我们把代码想象成一个房间,每个线程都是进入这个房间的人.当你没有作出任何说明的情况下,是不是别人也可以进入这个房间,此时这个房间失去原子性了.

也是结合上一段代码,对Counter.count进行++操作的代码分三步:

  1. load:那内存数据读取到CPU寄存器上;
  2. add:把寄存器中的值进行++(更新);
  3. save:把寄存器中的值再写到寄存器中去.

由于线程的执行是无序的,那么t1,t2的执行顺序也是不一定的:

下面我们通过两种执行方式,就能看出上述代码执行结果不是20000的原因了:

第一种: 

 

 

 此时按照这一种顺序,得到的count是符合预期的.

第二种:

 此时,我们可以看到count的值不是2,而是1.所以没有达到我们想要的结果.

由于我们线程就是无序执行的,所以,当t1线程现在进行++操作时,必须要保证t2无法对同一个变量count进行++操作.我们如何区解决呢?这里涉及到锁!!!引入一个关键字synchronized.当我们t1线程进行++操作,对其进行加锁,此时t2也想加锁,就必须阻塞等待.等到t1线程释放锁了为止.

 

synchronized

synchronized 会起到互斥效果 , 某个线程执行到某个对象的 synchronized 中时 , 其他线程如果也执行到
同一个对象 synchronized 就会 阻塞等待 .
进入 synchronized 修饰的代码块 , 相当于 加锁
退出 synchronized 修饰的代码块 , 相当于 解锁 

 对上述代码的add方法加锁,进入方法内部,相当于对当前对象进行加锁.

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

此时,我们再来看一下运行结果:

 

此时就是线程安全啦!!!!!! 

 我们分析一些为什么?

一个线程上了锁,其它线程只能等待.相当于一个人上厕所,其它人排队等着上厕所.

在此时,相当于我们t1对counter对象进行加锁时,如果t2也尝试对counter对象进行加锁,就不能加上,会造成阻塞等待.当t1完成++后,释放锁,t2才能获取到锁.

所以,t2拿到的是t1已经对count++后得到的值,再进行++结果是2,符合预期.
 

注:阻塞等待的概念:

         针对每一把锁, 操作系统内部都维护了一个等待队列. 当这个锁被某个线程占有的时候, 其他线程尝试进行加锁, 就加不上了, 就会阻塞等待, 一直等到之前的线程解锁之后, 由操作系统唤醒一个新的线程, 再来获取到这个锁.

如果多个线程尝试对同一个对象进行加锁,就会产生锁竞争.

 


 

4.内存可见性

可见性指, 一个线程对共享变量值的修改,能够及时地被其他线程看到. 多线程环境下,编译器对于代码优化,产生了误判,从而引起了bug.

 

每个线程都有自己的工作内存,当线程要读取一个共享变量时,会将主内存的值放到工作内存中(相当于一个副本),再从工作内存中读取数据 ;当线程要修改一个共享变量时,会先修改工作内存中的值,再同步到主内存中去.

由于每个线程都有自己的工作内存,当线程t1修改了t1的工作内存中的值,但并未同步到t2的工作内存,t2也尝试修改共享变量的值.但t2的工作内存中的值还是未修改的值,此时再进行++,最后同步到主内存的共享变量的值将不是我们预期的.

1->2   

             2

1->2

 

解决方案就是引入volatile关键字

volatile

首先,创建两个线程 t1 和 t2
t1 中包含一个循环, 这个循环以 flag == 0 为循环条件.
t2 中从键盘读入一个整数, 并把这个整数赋值给 flag.
预期当用户输入非 0 的值的时候, t1 线程结束.
class Counter2{
    public int flag=0;
}
public class TestDemo9 {
    public static void main(String[] args) {
        Counter2 counter=new Counter2();
        Thread t1=new Thread(()->{
            while(counter.flag==0){

            }
            System.out.println("循环结束!!!");
        });

        t1.start();

        Thread t2=new Thread(()->{
            Scanner scanner=new Scanner(System.in);
            System.out.println("请输入一个整数:");
            counter.flag=scanner.nextInt();
        });

        t2.start();
    }
}

此时,当输入一个非0值时,循环没有结束.

这是因为t1读的时自己的工作内存的值,t2修改的是自己的工作内存的值,并未同步到t1.

此时,我们对代码进行修改,引入volatile关键字:

 

class Counter2{
    public volatile int flag=0;
}

 

注意:volatile不能解决原子问题 

我们对第一个代码t1,t2对一个变量的count进行++的count加上volatile

class Counter{
    public volatile int count=0;
    public void add(){
            count++;
    }
}

 还是无法保证结果.此时,我们还是要引入synchronized:

synchronized既可以保证代码可见性,又可以保证原子性.

 


 

 5.指令重排序

当实例化一个对象时,我们是不是可以分成三步

  1. 创建内存
  2. 调用构造方法
  3. 将内存地址赋给引用

此时创建内存可以想象成买房,调用构造方法可以看成装修,将内幕才能地址赋给引用可以看成拿钥匙.

而2,3顺序是可以进行交换的.如果我们进行了指令重排序,3在2之前.是不是就是我们拿到钥匙的时候,房子是一个毛坯房;当我们后续想调用里面的方法(想坐沙发时),发现里面什么都没有.

解决方法:volatile.

总结

祝大家顺顺利利健健康康,下次见!!!

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值