收藏多年的线程安全问题大全笔记(上篇)——{线程不安全的背后原理},笔记一生一起走,那些日子不再有

本篇会加入个人的所谓鱼式疯言

❤️❤️❤️鱼式疯言:❤️❤️❤️此疯言非彼疯言

而是理解过并总结出来通俗易懂的大白话,

小编会尽可能的在每个概念后插入鱼式疯言,帮助大家理解的.

🤭🤭🤭可能说的不是那么严谨.但小编初心是能让更多人能接受我们这个概念 !!!

在这里插入图片描述

引言

🦊🦊🦊🦊

在并发编程的广阔世界里,有一个隐匿的陷阱,它静静地潜伏在每一个试图并行处理任务的程序之中,那就是线程安全问题。想象一下,当多个线程同时访问并修改同一块内存区域时,犹如多辆马车在狭窄的小道上竞相前行,稍有不慎便可能引发混乱与冲突。本文将带您深入探索线程安全问题的本质,揭示其背后的原理与危害,并为您铺设一条通向安全并发编程的道路。准备好,让我们一起揭开线程安全的面纱,确保您的程序在并发的浪潮中稳健前行。

🦊🦊🦊🦊

前言

在前面文章的学习中, 我们认识到了

💞 💞

  1. 并发编程认识,和对于程序本身上效率的提高。

从中也更加明白

  1. 进程本质上就是 系统资源分配 的基本单位 , 比较 量, 开销较大。

  2. 线程则是 系统调度执行 的基本单位, 比较 量, 开销较小。

试想一下,并发编程虽然较好效率较高。 但同时也会产生相关的问题 , 从而导致我们程序出现 BUG

而这些问题我们称之为 线程安全问题 , 在本篇中小编带着小伙伴认识线程安全问题, 并用相对应的方案来 解决线程安全问题

需要回顾的小伙伴可以点击下面链接学习哦

进程与线程的零基础讲解

💞 💞

目录

  1. 线程安全问题的初识

  2. 线程安全问题产生的原因

  3. 线程安全问题的解决方案

  4. 锁的使用原理和方式

一. 线程安全问题的初识

线程安全问题是什么? 🤔 🤔 🤔 🤔

1. 线程安全问题的概念

线程安全问题是当我们进行 并发编程 时,多个线程同时执行一段代码,出现 BUG 的情况。

注意我们只有在并发编程中, 才有可能会出现 实际结果预期结果 不一致的情况,而这种情况,我们就就称为 线程安全问题

故言之, 我们 一个线程执行的串行编程 就不会出现这种情况。

那么这是什么原因呢? 为什么我们的串行编程不会出现BUG , 而我们的 并发编程就会出现相关的BUG 呢?

下面就让我们继续往下看看吧 💥 💥 💥 💥

二. 线程安全问题产生的原因

在讲解线程安全问题产生的原因之前, 小编想举个栗子,只有通过 实际的栗子 才能更好的说明,更好的让小伙伴理解 线程安全 的根源。

1. 举个栗子

class  synchronousDemo2 {



    public  static  int count =0;
    public static void main(String[] args) throws InterruptedException {
        Thread t1 =  new Thread(()-> {
            // 给线程1 加锁

                for (int i = 0; i < 10000000; i++) {
                    count++;
                }


        });

        Thread t2 = new Thread(()-> {
            // 给线程2 加锁

                for (int i = 0; i < 10000000; i++) {
                    count++;
                }

        });

//        先执行线程 1
        t1.start();

        t2.start();

        t2.join();
        t1.join();


        System.out.println(count);
    }


}

在这里插入图片描述

看到上述代码,小伙伴是否就很吃惊, 为啥两个线程同时运行, 都加上 10000000 次, 所以我们得到的应该是 20000000 才对,但是实际得到的结果却是 < 20000000 的数字。

这时我们就称为出现的 BUG 了,也就出现了 线程安全问题

2. 线程安全的原因

之前的文章我们学过, 在我们计算机中, 执行一次加法,操作系统要发出一系列的指令来对CPU进行 计算操作。

而我们加法的指令整体可以简单划分为:Load , add , save 的三个指令的执行

Load: 从内存中读取数据

add: 自增数据

save: 返回数据到内存中

那么当我们进行两次相加的时候

<1>. 串行执行

在这里插入图片描述

如果是 串行执行 的话,每个指令是 完整执行 的, 而 每个指令和下一个指令 也是衔接好执行 自增操作 的, 也就是三个指令之间也是相互配合, 完成 + 1 操作 的。

<2>. 并发执行

在这里插入图片描述

虽然每个独立的指令是完整执行的,但是指令和指令之间的配合是 不完整

其原因就在并发执行的一个本质的特点:

随机调度抢占执行

本质就是 指令抢占去执行 ,从而导致 指令执行的不确定 ,造成了 自增 时, 三条配合的指令不能完全连续执行下来,导致指令和指令之间相互错综重复的执行, 造成 不可预期的BUG

鱼式疯言

思考一下:

  • 想上面的两个线程计数都累加10000000 , 如果出现线程安全问题,有没有最终的总和 可能 == < 10000000== ,答案是 完全有可能的

因为这种 线程中 随机调度,抢占执行 , 其实情况是 很复杂的, 而小编上面作的图也只是其中 一种情况 , 真正在 线程随机调度的执行 的过程中, 其实是 很复杂的过程

一句话概括:

串行执行时 有序的 ,并发执行是 随机调度,抢占执行的 ,更容易出现 BUG

竟然小伙伴们都知道知道并发执行产生BUG的原因啦,那我们该怎么解决这个问题 ???

请移步下一节观赏哦 ❣️ ❣️ ❣️ ❣️

三. 线程安全问题的解决方案

1. 方案的引入

关于线程安全问题

小编在这里还得补充产生线程安全的主要因素有哪些 :

  1. 随机调度,抢占执行 [内因]
  1. 不同的线程 针对 同一个变量 进行 修改 操作
  1. 修改操作是 非 “原子” 性 。

像上面的栗子我们已经很清楚的表明了 随机调度,抢占执行 的 内因的影响。

但更需要向小伙伴们解释的是 2 和 3

<1> . 不同线程对同一变量进行修改操作

这里小伙伴们需要引起注意的是

  • 首先 , 我们需要注意的是在不同线程才是大前提, 如果是在同一线程,这种是不会出现线程安全问题的

  • 其次, 一定是需要对 同一个变量 ,如果是不要变量的话的,也不会出现线程安全问题

  • 最后就是修改操作, 一定是修改操作 ,如果是 访问读取操作 的话,也不会设计指令相互配合 去影响的问题。

  • 综上所得,上面的三种情况是 缺一不可 , 如果 缺少一个不会出现线程安全问题 的。

那么有小伙伴们问了,我们进行把这个原因都规避掉,不就解决问题了嘛。

这个也没错,是很有道理的, 但是我们 Java程序员是不用这套方案的 ,具体情况小编会在下文重点说明。

<2>. 修改操作,“非”原子性

关于这个原子性的概念

小编在 数据库事务中,重点讲解到

数据库事务的零基础讲解

小编在这里就简单说明下:

原子性本身就是一种不可拆分的整体性,好比化学中的原子是不能再拆分的。

在上面的栗子中,指令自身是 原子性 ,但是这个 自增的修改操作 ,是需要 三个指令协调完成的 , 而 这三个指令又是独立工作的,所以是可拆分的,就称不上原子性, 就为非“原子性” 。

故在 并发编程 中,就会因为 程序的随机调配,抢占执行 的原因,出现 指令直接交错重复的连续调用

而我们 Java程序员 是重点 利用这个原因来 解决线程安全问题

2. 修改方案

<1>. 方案一

针对 不同变量 的方式进行修改。

对于这个方案, 主要的问题有两个

  1. 如果是需要使用同一个变量的情景下, 我们可能就不适用

  2. 如果只是单纯的读取数据,我们就不需要用两个变量来读取

故上述问题,方案一不可取。

<2>. 方案二

针对非"原子性" 的修改操作。

我们可以采取的策略就是把他们打包成 一个原子来执行

所以我们Java中就引入了 synchronized关键字 来进行 打包原子性

这个关键字的作用就是把 一个线程中 需要 进行的部分 会产生线程安全的代码先执行, 而同时 另一个线程 进行 等待 , 当这个线程中的代码 执行完了 ,另一个线程再 进行执行

四. 锁的使用原理和方式

1. synchronized关键字的原理讲解

synchronized 的本质上是提供 一把锁

在这里插入图片描述

当第一个滑稽老铁用 synchronized 把厕所在这里插入图片描述锁上的时候,第一个滑稽就可以 先执行他的操作 ,让第二个在门外等的滑稽老铁就需要 阻塞等待不可以执行该操作, 直到第一个滑稽老铁 用好这个厕所,自动把锁 打开之后, 第二个滑稽老铁才能执行 它的代码 。 这样就可以保证该代码中的指令 有序的执行 ,从而变成一个 原子性的修改操作 的代码。

鱼式疯言

    1. 对于上述 synchronized 的操作, 有小伙伴肯定会认为,先执行一个线程后执行另外一个线程 , 这不是就是串行执行吗? 那还能是 并发执行 吗?
  • 是哒, 但小伙伴一定要注意一个点就是, 这里整体上还是 并发执行 , 只用 synchronized 加锁 的部分代码是 串行执行
  • 虽然这里有一部分的代码是串行执行, 相比于全部的并发执行, 效率是会 减慢很多 , 但是 相比于 全部都是串行执行 来说, 效率已经增大了很多了
  • 必要的串行执行是为了解决 线程安全问题防止出现 BUG , 这些是很有必要的。
  1. 操作系统的底层理解

在这里插入图片描述

2. 锁的实际运用

public class synchronousDemo1 {


    /**
     * 解决线程问题的方案:
     *
     * 一. synchronized 锁对象修饰代码块
     *
     * 1、 synchronized  同时 -> 互斥
     * 同时给需要解决的问题进行加锁
     *  加锁时不需要引入的对象是什么
     *  只需要关注需要的线程是否一致
     *
     *
     *  对象不一致时,就会导致一边锁门
     *  一边破窗而入
     * 还是会导致线程安全的问题
     *
     */

    static int count =0;


    private static Object locker = new Object();

    public static void main(String[] args) throws InterruptedException {
        /**
         *
         *  从而在前面  {  开始对对象加锁
         *
         * 后面 } 进行解锁
         *
         */



        Thread t1 =  new Thread(()-> {
            // 给线程1 加锁
            synchronized(locker) {
                for (int i = 0; i < 100000; i++) {
                    count++;
                }
            }

        });

        Thread t2 = new Thread(()-> {
            // 给线程2 加锁
            synchronized(locker) {
                for (int i = 0; i < 100000; i++) {
                    count++;
                }
            }
        });



        t1.start();
        t2.start();

        t1.join();
        t2.join();


        System.out.println(count);
    }
}

在这里插入图片描述

是的,这里用 synchronized 来进行 加锁 , 就可以保证 线程安全

使用事项:

  1. ()内放的是锁对象,对象只要是 Object类 或者 Object 的子类 都可,不可以是一个 基本类型的变量
  1. { } 内存放的需要打包成 原子性 的代码, 对该代码进行该 对象加锁 ,就能保证一方 线程执行 ,一方 线程阻塞 的现象。 并且, { 就意味着加锁 , 到 } 结束 ,就意味着 =解锁

3. 锁的错误使用

<1>. 问题一

public class synchronousDemo1 {


    /**
     * 解决线程问题的方案:
     *
     * 一. synchronized 锁对象修饰代码块
     *
     * 1、 synchronized  同时 -> 互斥
     * 同时给需要解决的问题进行加锁
     *  加锁时不需要引入的对象是什么
     *  只需要关注需要的线程是否一致
     *
     *
     *  对象不一致时,就会导致一边锁门
     *  一边破窗而入
     * 还是会导致线程安全的问题
     *
     */

    static int count =0;

    public static void main(String[] args) throws InterruptedException {
        /**
         *
         *  从而在前面  {  开始对对象加锁
         *
         * 后面 } 进行解锁
         *
         */


        Thread t1 =  new Thread(()-> {
            // 给线程1 加锁
            synchronized(synchronousDemo1.class) {
                for (int i = 0; i < 100000; i++) {
                    count++;
                }
            }

        });

        Thread t2 = new Thread(()-> {
            // 给线程2 加锁
            synchronized(new String()) {
                for (int i = 0; i < 100000; i++) {
                    count++;
                }
            }
        });



        t1.start();
        t2.start();

        t1.join();
        t2.join();


        System.out.println(count);
    }
}

在这里插入图片描述

易错点

  1. 这里我们需要重点注意的是, 当 synchronized不同对象加锁 时, 这里的操作是不可效的

是的, 只有 竞争同一把锁 ,这里的 阻塞效果才能执行

在这里插入图片描述

好比有两个房间,这里就 不会出现阻塞等待 的情况, 就和平常的 并发执行 就是一样的, 仍然会有 线程安全 的问题。

<2>. 问题二

public class synchronousDemo1 {




    static int count =0;


    private static Object locker = new Object();

    public static void main(String[] args) throws InterruptedException {
        /**
         *
         *  从而在前面  {  开始对对象加锁
         *
         * 后面 } 进行解锁
         *
         */



        Thread t1 =  new Thread(()-> {
            // 给线程1 加锁
            synchronized(locker) {
                for (int i = 0; i < 100000; i++) {
                    count++;
                }
            }

        });

        Thread t2 = new Thread(()-> {
            // 给线程2 加锁

                for (int i = 0; i < 100000; i++) {
                    count++;
                }

        });



        t1.start();
        t2.start();

        t1.join();
        t2.join();


        System.out.println(count);
    }
}

在这里插入图片描述

  1. 还需要注意的是,一方用 synchronized 加锁 , 另外一方就必须也要加上synchronized 加锁,否则就有可能下面一样破窗而入,造成线程安全 问题。

在这里插入图片描述

鱼式疯言

一句话总结

  1. 保证 编程安全, 用 synchronized 加锁 ,不关注 对象是什么 , 更关注是否对多个线程同时 加上相同对象的锁
  1. 其实本质上还是在竞争锁对象,谁先竞争到 锁对象 ,谁就 先执行 , 另一方就阻塞。

4. 锁的其他使用方式

<1>. 修饰成员方法

/**
 *
 * synchronized 也可以用来修饰 成员方法 和 静态方法。
 *
 */


class  SynchronizedDemo {

    public static int count =0;

   static class  Counter {

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

      // 上面的方法等效于 直接用this 来引用对象

       public  void  countAdd1() {

//         this 调用的对象相同
           synchronized(this) {
               count++;
           }

       }



    }

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

       Counter counter= new Counter();
        Thread t1= new Thread(()->{

            for (int i = 0; i < 50000; i++) {

                counter.countAdd();

            }

        });

        Thread t2= new Thread(()->{

            for (int i = 0; i < 50000; i++) {
                counter.countAdd();
            }

        });


        t1.start();
        t2.start();


        t1.join();
        t2.join();

        System.out.println(count);

    }
}

在这里插入图片描述

鱼式疯言



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


这两个方法其实本质上是 一样的 ,只是 写的方式不一样 ,都是 针对调用他的对象进行加锁操作

       public  void  countAdd1() {

//         this 调用的对象相同
           synchronized(this) {
               count++;
           }

       }

<2>. 修饰静态方法(类方法)

/**
 *
 * synchronized 也可以用来修饰 成员方法 和 静态方法。
 *
 */


class  SynchronizedDemo {

    public static int count =0;




       // 修饰静态方法
       synchronized  public static   void  countAdd3() {
           count++;
       }


       // 修饰静态方法时的等效效果
       public static   void  countAdd4() {
           synchronized (SynchronizedDemo.class) {
                 count++;
           }
       }
    }

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

       Counter counter= new Counter();
        Thread t1= new Thread(()->{

            for (int i = 0; i < 50000; i++) {

                Counter.countAdd4();

            }

        });

        Thread t2= new Thread(()->{

            for (int i = 0; i < 50000; i++) {
                Counter.countAdd4();
            }

        });


        t1.start();
        t2.start();


        t1.join();
        t2.join();

        System.out.println(count);

    }
}

在这里插入图片描述

鱼式疯言

  1. 注意点一
   // 修饰静态方法
   synchronized  public static   void  countAdd3() {
       count++;
   }

两者的本质原理是相同的,只是写法不同, 本身都是给 类对象 进行加锁操作

   // 修饰静态方法时的等效效果
   public static   void  countAdd4() {
       synchronized (SynchronizedDemo.class) {
             count++;
       }
   }
  1. 偷懒写法

给小伙伴们来点偷懒的写法, 类名. class ==> 类对象

当我们便捷的使用同一个对象进行 加锁操作 时,就可以使用 类对象 进行加锁。

总结

  • 线程安全问题的初识: 认识到了线程安全本质上是因为并发执行导致出现了不可预期和实际结果不同的BUG 。

  • 线程安全问题产生的原因: 熟悉了由于并发执行的随机调度,抢占执行的特点而导致指令和指令之间的交错的重复连续执行,出现了线程安全问题。

  • 线程安全问题的解决方案: 利用synchronized 把争夺锁对象,让一方线程执行,一方线程阻塞, 表现出是把非原子性的看成是原子性, 让指令有序执行,解决了线程安全问题。

  • 锁的使用原理和方式: synchronized 锁的使用是针对同一对象,而不关注对象本身来进行加锁的操作。

如果觉得小编写的还不错的咱可支持 三连 下 (定有回访哦) , 不妥当的咱请评论区 指正

希望我的文章能给各位宝子们带来哪怕一点点的收获就是 小编创作 的最大 动力 💖 💖 💖

在这里插入图片描述

评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

邂逅岁月

感谢干爹的超能力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值