什么是线程安全问题?
概述
:::primary
① 详细讲述线程安全之前先来简单感受下什么是线程安全问题
② 感受下出现线程安全问题的大致需要什么样的条件,因为我发现在没有样例的情况怎么写都是不好写的
:::
线程不安全示例:
[类提供对成员变量进行读写的方法,而多个线程同时调用类提供的该方法对成员变量递增 1000 次]{.aqua}
[最后得到结果会是我们预想的 2000 吗?还是会得到一个非常奇怪的结果? ]{.aqua}
public class ThreadSynchronized{
// 多线程使用的变量
private int count;
// 多线程控制变量的方法
private void increment() {
count++;
}
// 为了节省代码的篇幅, 仅列出对成员变量和相应的方法,多线程的代码请自己编写
}如果你已经编写好多线程的代码,并且经过测试应该会发现结果可能每次都不一样
+++danger 为什么会出现如此奇怪的结果?
① 每个线程对成员变量进行相加的过程中并不是简单地执行加法,而是执行了三种操作
② 每个线程首先会获取成员变量的值、成员变量加一、最后将结果更新给成员变量
③ 如果某个线程在执行完成员变量加一之后,因为线程上下文切换而导致无法执行时,此时的结果依然是原来的值
④ 其余线程获取到的成员变量的值就是原来的值,执行完后续两个操作之后,成员变量真正完成了加一的操作
⑤ 但是刚才没有执行完的线程并不知道这件事情,它继续将自己的运算得到的结果更新给成员变量
⑥ 这就导致最后明明执行两次加法,但是成员变量却只增加了一次
+++
线程不安全的条件
- [存在可共享的变量]{.red}
- [存在多线程并行或者并发地对共享变量进行读写]{.red}
前言:
- 只有确定共享变量之后,才能够分析是否会有线程安全问题,最终才能够确定同步方案
- 首先就需要了解如何确定共享变量,哪些变量才是共享变量
- 其次就需要分析线程安全问题是如何产生的
- 最后才会思考如何采用合适的同步方案解决线程安全问题
- 只有确定共享变量之后,才能够分析是否会有线程安全问题,最终才能够确定同步方案
共享变量
前提:
- [变量是否共享都是以线程为单位的,而不是对象或者方法之类的]{.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;
}
// 总结:
// ① ④ 两种方式都涉及到对成员变量的修改, 是很容易产生产生线程安全的
// ② ③ 两种方式涉及到的知识成员变量的修改, 但是后者会产生线程安全问题
}
局部变量:
前提:
- [基本数据类型的局部变量是 不可能 被共享的]{.orange}
- 基本数据类型的局部变量是分配在栈空间中的
- 每个线程访问该局部变量的时候都复制一份到自己的栈空间中
- 每个线程的栈空间都是私有的,不存在共享变量的可能
- [引用类型的局部变量存在是 可能 共享的]{.aqua}
- 每个线程访问局部变量的时候复制对象的引用到自己栈空间中
- 每个线程的使用的引用指向的都是堆空间中相同的对象,所以存在共享的可能
- [基本数据类型的局部变量是 不可能 被共享的]{.orange}
[如果 引用类型 的局部变量能够直接或间接地被其他对象使用,那么这些局部变量都是可共享的变量]{.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();
}
总结:
- [只要变量不暴露给其他对象使用,那么无论变量是什么类型,它都是不可共享的]{.blue}
- [引用类型的变量通常都是可以共享的,基本数据类型的变量是成员变量是才可以共享]{.blue}
- [堆中的对象基本可以共享,取决于线程是否开放他,栈中的变量不可能共享]{.blue}
线程安全问题
:::primary
① 这里会简述代码中常见的线程安全问题,至于这些线程安全问题究竟是如何造成的,稍后再讲
② 这样排版的主要原因是因为,没有对内存模型的认知,是根本不可能深入理解可见性这个概念的
③ 所以造成原子性、可见性、有序性问题的原因都会在 Java 内存模型中讲述
:::
基本内容
示例:先来看看什么样的是线程安全问题
- 测试代码
- 测试结果
线程安全定义:
- 严谨的定义:
- 多个线程并发或者并行访问共享变量的时候
- 无论运行时采用何种调度方式安排线程的执行,并且不需要任何额外的同步代码
- 多线程访问共享变量的结果都能够表现出正确的行为,那么就认为是线程安全的
- 通俗的定义:[多线程按照任意顺序对共享资源进行访问都能够得到正确的结果,就认为是线程安全]{.red}
- 严谨的定义:
线程安全问题:多线程访问共享变量的结果或者说正确性得不到保证时,就认为出现了线程安全问题
多线程访问共享变量的方式会直接决定是否会产生线程安全问题
访问方式:读与写
[多线程 同时读取共享变量 不会造成线程安全问题]{.red}
- 解释:同时读取共享变量的行为显然是无论如何打乱,执行的结果一定是符合预期的
[多线程 同时读写或者同时写入 共享变量都是会造成线程安全问题的]{.red}
解释:同时的读写或者同时写入的行为被打乱,那么执行的结果是非常有可能不同的
举例:如果某个线程正在对共享变量做递增操作,修改完成之后还没来得及更新变量
另外一个线程也获取到共享变量并对其进行递增操作,修改完成之后进行更新
这样就会导致明明变量递增了两次,但是最终只增加了一
临界区:[线程访问共享变量的代码块称为临界区]{.red}
竞态条件:[多线程访问共享变量的结果取决于特定的顺序的时候,就认为发生了竞态条件]{.red}
原子性问题
定义:
测试代码
可见性问题
- 定义:
有序性问题
:::primary
:::
线程安全等级
不可变
定义:[不可变的对象或者变量都是不会存在任何线程安全性问题的]{.red}
不可变的变量的线程安全:确保变量的线程安全
不可变对象的线程安全
[不可变对象只能够代表对象的引用是不可以改变的,但是对象内部的成员变量依旧可以访问并且改变]{.red}
也就是说只能够保证引用的线程安全,对象内部的线程安全是没有保证的,需要对象自行保证
:::danger
① 最简单的方式就是将对象内部的所有变量都声明为不可变
② 类声明为不可变只能够代表该类无法被继承,而不是代表线程安全的
:::
细节:
- JDK 5 之后才能够保证不可变对象没有线程安全问题
- [不可变对象需要在没有发生 this 引用逃逸 的情况下被正确的构建出来才是线程安全的]{.pink}
绝对线程安全
- 定义:[严格满足线程安全的定义也就是绝对线程安全]{.pink}
- 细节:绝对线程安全几乎很难做到,Java 提供的大多数线程安全类并不是绝对线程安全的
相对线程安全
- 定义:
- 线程单次调用对象中的某个方法时是线程安全的,不需要提供任何同步措施
- 但是线程组合调用多个线程的方法时需要采用一定的同步措施
- 细节:相对线程安全就是通常意义上指的线程安全,Java 提供的大多数线程安全类都是相对线程安全的
线程兼容
- 定义:线程调用的对象不具备任何确保线程安全的措施,需要在调用端提供完善的同步措施
线程对立
- 定义:[无论线程调用的对象是否采取了同步措施,都没有办法让线程并行或者并发调用该对象]{.red}
- 例子:
suspend()
和resume()
是线程对立的方法- 两个线程分别调用
suspend()
和resume()
方法是不现实的 - 前者用于挂起线程,后者用于恢复线程,这两个方法同时执行显然是不合理的
- 两个线程分别调用
- 细节:Java 天生就支持多线程,所以会产生线程对立的方法都被废弃掉了(此前应该提过几个已经被废弃的方法)