线程安全-概述

什么是线程安全问题?

概述

:::primary

① 详细讲述线程安全之前先来简单感受下什么是线程安全问题

② 感受下出现线程安全问题的大致需要什么样的条件,因为我发现在没有样例的情况怎么写都是不好写的

:::

  • 线程不安全示例:

    • [类提供对成员变量进行读写的方法,而多个线程同时调用类提供的该方法对成员变量递增 1000 次]{.aqua}

    • [最后得到结果会是我们预想的 2000 吗?还是会得到一个非常奇怪的结果? ]{.aqua}

      @Slf4j(topic = "c.syn")
      public class ThreadSynchronized{
      // 多线程使用的变量
      private int count;
      // 多线程控制变量的方法
      private void increment() {
      count++;
      }
      // 为了节省代码的篇幅, 仅列出对成员变量和相应的方法,多线程的代码请自己编写
      }
    • 如果你已经编写好多线程的代码,并且经过测试应该会发现结果可能每次都不一样

      线程不安全的示例

      +++danger 为什么会出现如此奇怪的结果?

      ① 每个线程对成员变量进行相加的过程中并不是简单地执行加法,而是执行了三种操作

      ② 每个线程首先会获取成员变量的值、成员变量加一、最后将结果更新给成员变量

      ③ 如果某个线程在执行完成员变量加一之后,因为线程上下文切换而导致无法执行时,此时的结果依然是原来的值

      ④ 其余线程获取到的成员变量的值就是原来的值,执行完后续两个操作之后,成员变量真正完成了加一的操作

      ⑤ 但是刚才没有执行完的线程并不知道这件事情,它继续将自己的运算得到的结果更新给成员变量

      ⑥ 这就导致最后明明执行两次加法,但是成员变量却只增加了一次

      +++

  • 线程不安全的条件

    • [存在可共享的变量]{.red}
    • [存在多线程并行或者并发地对共享变量进行读写]{.red}
  • 前言:

    • 只有确定共享变量之后,才能够分析是否会有线程安全问题,最终才能够确定同步方案
      1. 首先就需要了解如何确定共享变量,哪些变量才是共享变量
      2. 其次就需要分析线程安全问题是如何产生的
      3. 最后才会思考如何采用合适的同步方案解决线程安全问题

共享变量

  • 前提:

    • [变量是否共享都是以线程为单位的,而不是对象或者方法之类的]{.orange}
    • [共享变量是产生线程安全问题的必要条件]{.orange}
      • [线程安全问题的产生必然基于对共享变量的操作]{.pink}
      • [但是共享变量的存在不一定会导致线程安全问题的出现]{.pink}
  • 共享变量定义:[多线程可以同时访问的变量称为共享变量]{.grey}

  • 共享变量位置:[Java 虚拟机中的栈空间的变量都是线程私有的,堆空间中的对象都是线程共享的]{.red}

  • 共享变量实例:

    • 成员变量(实例变量 + 类变量):

      • 前提:[无论成员变量是基本数据类型还是引用类型都是 可能 共享的]{.orange}

        • 对象的成员变量都是分配在堆空间中的,而堆空间是所有线程都共享的区域
      • [如果成员变量在能够直接或者间接地被其他对象使用,那么这些成员变量都是可共享的变量]{.red}

        • 逸出的方式有非常多种,并不是每种逸出方式都可能造成线程安全的

        • 只能够代表这些变量可以被其他线程所访问到,也就是可共享

          public class SharedVariable
          {
          // ① 访问权限是公共的
          public int count;
          // ② 访问权限私有的基本数据类型
          private int number;
          // 其余线程可以通过方法获取私有成员变量的副本
          public int getNumber() {
          return number;
          }
          // ③ 访问权限私有的引用数据类型
          private StringBuilder sb;
          // 其余线程可以通过方法获取到私有成员变量的引用
          public StringBuilder getStringBuilder(){
          return sb;
          }

          // ④ 直接对成员变量的使用也是逸出, 不是只有返回成员变量才叫逸出
          public void increment(){
          number++;
          }
          public void setStringBuilder(StringBuilder sb){
          this.sb = sb;
          }

          // 总结:
          // ① ④ 两种方式都涉及到对成员变量的修改, 是很容易产生产生线程安全的
          // ② ③ 两种方式涉及到的知识成员变量的修改, 但是后者会产生线程安全问题
          }
    • 局部变量:

      • 前提:

        1. [基本数据类型的局部变量是 不可能 被共享的]{.orange}
          • 基本数据类型的局部变量是分配在栈空间中的
          • 每个线程访问该局部变量的时候都复制一份到自己的栈空间中
          • 每个线程的栈空间都是私有的,不存在共享变量的可能
        2. [引用类型的局部变量存在是 可能 共享的]{.aqua}
          • 每个线程访问局部变量的时候复制对象的引用到自己栈空间中
          • 每个线程的使用的引用指向的都是堆空间中相同的对象,所以存在共享的可能
      • [如果 引用类型 的局部变量能够直接或间接地被其他对象使用,那么这些局部变量都是可共享的变量]{.red}

        // ① 方法的持续调用, 导致局部变量被其他线程所引用
        // 个人感觉这种情况比较特殊, 但是既然存在那也写上来吧
        // 注:基本数据类型即使这么做也是没有用的, 因为 Java 是值传递, 每个线程都会拿到副本
        private void updateList(){
        List<Integer> list = new ArrayList<>();
        for (int i = 0; i < 10; i++) {
        // 此时的局部变量就已经逸出到其他线程中了, 可以被其他线程所使用
        add(list, i + 2);
        remove(list, i);
        }
        }
        // 一个线程可以给链表增加元素
        private void add(List<Integer> list, int number){
        new Thread(()->{
        list.add(number);
        }).start();
        }
        // 另一个线程可以给链表移除元素
        private void remove(List<Integer> list, int index){
        new Thread(()->{
        list.remove(index);
        }).start();
        }
        // ② 返回值
        private void getStringBuilder(){
        // 是否存在线程安全取决于调用者
        // 返回的对象如果只有一个线程使用, 那就有没有问题
        // 返回的对象如果有多个线程使用,那就有线程安全问题
        // 但是无论怎样, 这个对象都是可以被共享的
        return new StringBuilder();
        }
    • 总结:

      1. [只要变量不暴露给其他对象使用,那么无论变量是什么类型,它都是不可共享的]{.blue}
      2. [引用类型的变量通常都是可以共享的,基本数据类型的变量是成员变量是才可以共享]{.blue}
      3. [堆中的对象基本可以共享,取决于线程是否开放他,栈中的变量不可能共享]{.blue}

线程安全问题

:::primary

① 这里会简述代码中常见的线程安全问题,至于这些线程安全问题究竟是如何造成的,稍后再讲

② 这样排版的主要原因是因为,没有对内存模型的认知,是根本不可能深入理解可见性这个概念的

③ 所以造成原子性、可见性、有序性问题的原因都会在 Java 内存模型中讲述

:::

基本内容

  • 示例:先来看看什么样的是线程安全问题

    • 测试代码
    • 测试结果
  • 线程安全定义:

    • 严谨的定义:
      1. 多个线程并发或者并行访问共享变量的时候
      2. 无论运行时采用何种调度方式安排线程的执行,并且不需要任何额外的同步代码
      3. 多线程访问共享变量的结果都能够表现出正确的行为,那么就认为是线程安全的
    • 通俗的定义:[多线程按照任意顺序对共享资源进行访问都能够得到正确的结果,就认为是线程安全]{.red}
  • 线程安全问题:多线程访问共享变量的结果或者说正确性得不到保证时,就认为出现了线程安全问题

    • 多线程访问共享变量的方式会直接决定是否会产生线程安全问题

    • 访问方式:读与写

      • [多线程 同时读取共享变量 不会造成线程安全问题]{.red}

        • 解释:同时读取共享变量的行为显然是无论如何打乱,执行的结果一定是符合预期的
      • [多线程 同时读写或者同时写入 共享变量都是会造成线程安全问题的]{.red}

        • 解释:同时的读写或者同时写入的行为被打乱,那么执行的结果是非常有可能不同的

        • 举例:如果某个线程正在对共享变量做递增操作,修改完成之后还没来得及更新变量

          ​ 另外一个线程也获取到共享变量并对其进行递增操作,修改完成之后进行更新

          ​ 这样就会导致明明变量递增了两次,但是最终只增加了一

  • 临界区:[线程访问共享变量的代码块称为临界区]{.red}

  • 竞态条件:[多线程访问共享变量的结果取决于特定的顺序的时候,就认为发生了竞态条件]{.red}

原子性问题

  • 定义:

  • 测试代码

可见性问题

  • 定义:

有序性问题

:::primary

:::

线程安全等级

不可变

  • 定义:[不可变的对象或者变量都是不会存在任何线程安全性问题的]{.red}

  • 不可变的变量的线程安全:确保变量的线程安全

  • 不可变对象的线程安全

    • [不可变对象只能够代表对象的引用是不可以改变的,但是对象内部的成员变量依旧可以访问并且改变]{.red}

    • 也就是说只能够保证引用的线程安全,对象内部的线程安全是没有保证的,需要对象自行保证

      :::danger

      ① 最简单的方式就是将对象内部的所有变量都声明为不可变

      ② 类声明为不可变只能够代表该类无法被继承,而不是代表线程安全的

      :::

    不可变性
  • 细节:

    • JDK 5 之后才能够保证不可变对象没有线程安全问题
    • [不可变对象需要在没有发生 this 引用逃逸 的情况下被正确的构建出来才是线程安全的]{.pink}

绝对线程安全

  • 定义:[严格满足线程安全的定义也就是绝对线程安全]{.pink}
  • 细节:绝对线程安全几乎很难做到,Java 提供的大多数线程安全类并不是绝对线程安全的

相对线程安全

  • 定义:
    • 线程单次调用对象中的某个方法时是线程安全的,不需要提供任何同步措施
    • 但是线程组合调用多个线程的方法时需要采用一定的同步措施
  • 细节:相对线程安全就是通常意义上指的线程安全,Java 提供的大多数线程安全类都是相对线程安全的

线程兼容

  • 定义:线程调用的对象不具备任何确保线程安全的措施,需要在调用端提供完善的同步措施

线程对立

  • 定义:[无论线程调用的对象是否采取了同步措施,都没有办法让线程并行或者并发调用该对象]{.red}
  • 例子:suspend()resume() 是线程对立的方法
    • 两个线程分别调用 suspend()resume() 方法是不现实的
    • 前者用于挂起线程,后者用于恢复线程,这两个方法同时执行显然是不合理的
  • 细节:Java 天生就支持多线程,所以会产生线程对立的方法都被废弃掉了(此前应该提过几个已经被废弃的方法)
Author: Fuyusakaiori
Link: http://example.com/2021/10/28/juc/多线程基础/线程安全-概述/
Copyright Notice: All articles in this blog are licensed under CC BY-NC-SA 4.0 unless stating additionally.