文章目录
- [1. 为什么会出现线程安全问题](#1. 为什么会出现线程安全问题)
- [2. synchronized 解决方案](#2. synchronized 解决方案)
-
- [2.1 线程八锁](#2.1 线程八锁)
- [3. `变量`的线程安全分析](#3.
变量
的线程安全分析) -
- [3.1 局部变量线程安全分析](#3.1 局部变量线程安全分析)
- [3.2 常见线程安全类](#3.2 常见线程安全类)
1. 为什么会出现线程安全问题
一段代码块内如果存在对共享资源的多线程读写操作,称这段代码块为临界区
共享资源:
- 多个线程读共享资源其实也没有问题
- 在多个线程对共享资源读写操作时发生指令交错,就会出现问题
2. synchronized 解决方案
为了避免临界区的竞态条件发生,有多种手段可以达到目的:
- 阻塞式的解决方案:synchronized,Lock
- 非阻塞式的解决方案:原子变量
synchronized,俗称的【对象锁】,它采用互斥的方式让同一时刻至多只有一个线程能持有【对象锁】,其它线程再想获取这个【对象锁】时就会阻塞住。这样就能保证拥有锁
的线程可以安全的执行临界区内的代码,不用担心线程上下文切换
如果多个线程要实现对共享资源的访问,用synchronized锁对这个资源的操作,synchronized 必须锁同一个对象或者同一个类。
2.1 线程八锁
3. 变量
的线程安全分析
成员变量 和静态变量是否线程安全?
- 如果它们没有共享,则线程安全
- 如果它们被共享了,根据它们的状态是否能够改变,又分两种情况
- 如果只有读操作,则线程安全
- 如果有读写操作,则这段代码是
临界区
,需要考虑线程安全
局部变量是否线程安全?
- 局部变量是线程安全的
- 但局部变量引用的对象则未必
- 如果该对象没有逃离方法的作用范围,它是线程安全的
- 如果该对象逃离方法的作用范围,需要考虑线程安全
3.1 局部变量线程安全分析
变量有两种类型,值引用和对象引用。
1. 值引用 :
每个线程调用 test1() 方法时局部变量 i,会在每个线程的栈帧内存中被创建多份,因此不存在共享
2. 对象引用 :list在本案例中不是局部变量,而是全局共享的。
分析:
- 无论哪个线程中的 method2 和method3 引用的都是同一个对象中的 list ,多个线程访问共享变量又有读和写操作会出现线程安全问题。
解决线程安全问题:将 list 修改为局部变量
思考:为 ThreadSafe 类添加子类,当 method2 或 method3 方法用public修饰,会不会出现线程安全问题呢?:会
上面图片为正确的写法不会出现线程安全问题,但是当我们将method2和method3方法用public修饰的话,那么当一个子类继承时如果重写了method2和method3,也有可能导致线程安全的问题,比如重写方法中又开了一个线程然后对同一个共享资源进行操作,这时旧线程和这个新线程就会因为对同一个共享资源进行操作而产生线程安全问题。(但是用private修饰则不会重写,不会覆盖父类中的方法,这时是子类定义的一个新的方法)所以用private修饰,final修饰也是防止公共方法避免受到子类的影响(避免子类重写覆盖)。 private 或 final 提供【安全】的意义,开闭原则中的闭。
3.2 常见线程安全类
- String
- Integer
- StringBuffer
- Random
- Vector
- Hashtable
- java.util.concurrent 包下的类(JUC)
这里说它们是线程安全的是指,多个线程调用它们同一个实例的某个方法时,是线程安全的。
比如下面的例子线程安全:
==但是:==单个方法线程安全,但是组合不安全,原因是Hashtable()中的get和put方法都用了synchronized修饰,但只能保证get操作的原子性或者put方法的原子性,但是不同的针对不同的对象,synchronized只能约束同一个,导致线程安全问题。
不可变类线程安全性 :
String、Integer 等都是不可变类,因为其内部的状态不可以改变,因此它们的方法都是线程安全的:只能读不能改,改的话都是创建一个新的对象,包含了改后的值。