线程安全产生的原因以及解决方案


引言

线程安全问题,是我们在面试中遇见的最常见的有关线程的问题,所以熟练掌握线程安全问题是非常有必要的.

🍊线程安全产生的原因

🍊什么是线程安全

在操作系统中,因为线程的调度是随机的(抢占式执行),正是因为这中随机性,才会让代码中产生很多bug 如果认为是因为这样的线程调度才导致代码产生了bug,则认为线程是不安全的, 如果这样的调度,并没有让代码产生bug,我们则认为线程是安全的
这里的安全指代的是代码中有没有产生bug,与我们平常认为的安全是两种截然不同的概念,我们所熟知的安全是由黑客造成的,他们会不会侵入你的电脑💻,攻击你的计算机,这是我门不能够制止的,我们所要做的就是让代码不会产生bug.

🍊线程安全实例

栗子 🌰 :使用两个线程,对同一个整型变量进行自增操作,每个线程自增五万次,看最后的结果,代码如下

class Counter{
    int count = 0;
     public void increase(){
        count++;
    }
}
public class Demo1 {
    private static Counter counter = new Counter();
    public static void main(String[] args) throws InterruptedException {
        Thread thread1 = new Thread(()->{
            for (int i = 0; i < 50000; i++) {
                counter.increase();
            }
        });
        Thread thread2 = new Thread(()->{
            for (int i = 0; i < 50000; i++) {
                counter.increase();
            }
        });
        thread1.start();
        thread2.start();
        thread1.join();
        thread2.join();
        System.out.println(counter.count);
    }
}

运行结果如下

  1. 在这里插入图片描述
  2. 在这里插入图片描述
  3. 在这里插入图片描述
    从 ⬆️ 面的运行结果,我们不难发现,每次的运行结果都是不同的值都是在50000~100000之间的值,这是为什么呢?

在此,我们需要了解一下count++,是如何在CPU上运行的?

count++,实际上是三个CPU指令

请添加图片描述

  1. 上面的指令分别会在两个线程中执行,在执行中,各个线程的指令调度是随机的,若按照下面的顺序执行指令调度,则count的值会正确运算

请添加图片描述

  1. 下面的执行顺序也不会产生bug

请添加图片描述

  1. 但是,下列两种顺序则会产生bug

请添加图片描述

请添加图片描述

从以上的解析我门会发现,count在自增的过程中,两个进程在并发的执行过程中,count可能会发生重复自增,也就是说count自增了两次,但事实上只是自增了一次,而且因为线程调度中伴随着随机性,所以上述哪一种状况都可能发生,无法预测;这就产生了线程安全问题,也就是bug

🍊产生线程不安全的原因总结

  1. 线程是抢占式的,线程之间的调度充满随机性.[线程不安全的万恶之源,而且这是无法避免的,程序猿无可奈何😮‍💨]
  2. 多个线程对同一个变量进行修改~~(多个线程对不同的变量进行修改是没问题的,多个线程对同一个变量进行读操作也是没问题的),这是可以控制的,通过对代码的结构进行修改,使不同线程操作不同的变量
  3. 针对变量的操作不是原子性的,这就是上面的load->add->save等指令,这些操作是绑定的,原子性的,把多个指令看成一个.这需要加锁才能解决
  4. 内存可见性(编译器优化)
    这是很难理解的,我们需要举一个具体的🌰

请添加图片描述

编译器优化:这里我们简单的了解一下什么叫做编译器优化
看上面的截图,有线程t1,t2,t1执行的操作是频繁的读内存的数据t2则是基础的三连(读写存),t1在内存中频繁的读取是非常低效的,而t2又迟迟的不进行修改,t1读到的值又始终是一个值,因此 t1就会产生一个大胆的想法,直接从寄存器中读取数据,不在执行load了,这时t2,对数据进行修改了,但是t1无法读取,这就会让代码产生bug了
编译器优化产生的主要原因就是编译器不相信程序猿,认为程序猿瞎b写代码,代码都是粑粑💩,然后编译器在原逻辑不变的情况下,主观的对代码进行调整,从而提高代码的效率,这就会改变代码的原始目的,所以产生了bug.使线程变得不安全

对于这种情况我们可以使用synchronizedvolatile关键字解决,我们在此先不做详解介绍,一会在分析

内存可见性是编译器优化范围中的一个典型案例

  1. 指令重排序

指令重排序也是编译器优化的一种

请添加图片描述
系统不按照我们所给的命令执行程序,而是为了提高效率,改变指令的执行顺序,这样就会产生bug,这也是编译器认为程序猿瞎b写代码的案列(编译器优化)

🍉线程不安全的解决方案(加锁)

根据上文我们可以了解到,线程不安全的原因主要分为三种

  1. 线程间的调度是随机的,这是我们无法改变的,这也是线程不安全的万恶之源,对于这种随机性,我们也我可奈何
    2.多个线程对同一个变量进行操作,并且这些调度指令不是捆绑在一起的,这也会产生线程不安全
    3.内存可见性(编译器优化)

我们主要对第2,3条原因进行干涉,这就需要加锁的方法

🍉加锁的概念

加锁

加锁就是把线程上锁🔒,这个线程所有的资源,方法,指令都被上了锁,在这个过程中其他的进程无法在对这里的资源进行修改操作,只能在上了锁的资源解了锁之后,才可以访问,也就是将多线程并发变为了单线程串行,降低了运行效率,但也确保了线程的安全性

那么,讲到这里就会有人问,那么,上了锁之后,并发编程不就是很鸡肋了吗?
这是不对的!!!

🍉加锁的好处与意义

到这里,我们已经知道了加锁就是为了确保线程的安全性,但是与其这样,还不如单线程串行编程呢
我们要知道,在一个进程中,由若干个线程,并不是每一个线程都需要加锁的,一般来说,只有最后的那几个线程,才需要加锁,来确保线程安全,其他的线程不需要加锁
这样既保证了,代码执行的效率,也确保了线程的安全性

🍉怎么进行加锁

🍉 synchronized关键字

synchronized翻译过来是同步的意思!!!
在计算机中,同步这个词,在不同的环境中所代表的含义不同
多线程中同步的含义代表互斥,也就是加锁的概念,但是在其他的环境中,如在IO或网络编程中,同步和线程就没有关系了,在这里表示的是消息的发送方,如何获取到消息

1. 使用关键字synchronized****(一定要记住怎么读和拼写!!!)

使用🌰

class Counter{
    int count = 0;
    synchronized public void increase(){
        count++;
    }
}
public class Demo1 {
    private static Counter counter = new Counter();
    public static void main(String[] args) throws InterruptedException {
        Thread thread1 = new Thread(()->{
            for (int i = 0; i < 50000; i++) {
                counter.increase();
            }
        });
        Thread thread2 = new Thread(()->{
            for (int i = 0; i < 50000; i++) {
                counter.increase();
            }
        });
        thread1.start();
        thread2.start();
        thread1.join();
        thread2.join();
        System.out.println(counter.count);
    }
}

运行结果

请添加图片描述

我们发现加上了synchronized后,数据正确,也就是说线程安全了,synchronized既能保证指令的原子性,也能保证内存可见性
被synchronized包装起来的代码编译器不会轻易的进行优化

  1. 如何使用synchronized
    synchronized本质操作就是修改了Object对象中的对象头里的一个标记

🍉1.直接修饰普通方法,也就相当于把锁对象指定为this了,具体操作如下

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

🍑 2.把synchronized加到代码块上,如果是针对某个代码块加锁,就需要手动制定
锁对象:针对哪个对象加锁

class Counter{
    int count = 0;
     public void increase(){
         synchronized(this){
             count++;
         }
    }
}

🍌 3.把synchronized加到静态方法中
所谓的静态方法更准确的叫法是:类方法,普通方法更严谨的叫法是:实例方法

synchronized public static void func(){}

相当于

public static void func(){
         synchronized (Counter.class){
             
         }
     }

以上就是synchronized的使用方法!!!(单词一定要会读,会拼)

🍉volatile关键字

volatile的作用和synchronized差不多,它也禁止了编译器的优化,但是与synchronized不同的是,volatile不能保证指令的原子性

class Counter{
    volatile int count = 0;
      public void increase(){
             count++;
    }
     public static void func(){
         synchronized (Counter.class){

         }
     }
}
public class Demo1 {
    private static Counter counter = new Counter();
    public static void main(String[] args) throws InterruptedException {
        Thread thread1 = new Thread(()->{
            for (int i = 0; i < 50000; i++) {
                counter.increase();
            }
        });
        Thread thread2 = new Thread(()->{
            for (int i = 0; i < 50000; i++) {
                counter.increase();
            }
        });
        thread1.start();
        thread2.start();
        thread1.join();
        thread2.join();
        System.out.println(counter.count);
    }
}

请添加图片描述
从上面的代码中,我们得知,volatile不能保证线程的安全性.

🍉synchronized与volatile的区别

他俩本质没什么区别,都是Java的关键字,功能也相同,都是对线程进行加锁,保证线程的安全性,只不过synchronized的能保证指令的原子性

那么,有人会问,既然synchronized的功能更全,直接无脑用就好了?
❌ 这是不对的!!!

synchronized的使用时需要付出代价的,一旦使用了synchronized,就很容易使线程阻塞,也就是说代码的执行效率会大大降低,虽说保证了线程的安全性,但是效率也变得更低了
volatile则不会让线程阻塞,效率也相对的提高

所以说具体问题具体分析,需要知道你需要干什么,在选择相应的关键字来加锁

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值