变量的线程安全问题
临界区: 多个线程对共享资源的读写操作时发生指令交错,就会出现问题,一段代码如果出现对共享资源的多线程读写操作,这段代码称为临界区
竞态条件: 多个线程在临界区内执行,由于代码的执行序列不同而导致结果无法预测,称之为发生了竞态条件
1 成员变量和静态变量是否是线程安全的
-
如果没有共享,则是线程安全的
-
如果被共享了,根据他们的状态是否能够改变,分为两种状态
① 只有读操作,则线程安全
② 有读写操作则这段代码是临界区,需要考虑线程安全
2 局部变量
-
局部变量是线程安全的
-
局部变量引用的对象不一定
① 如果该对象没有逃离方法的作用范围,则是线程安全的
② 如果逃离了方法的作用范围,则需要考虑线程安全
class ThreadSafe {
public final void method1(int loopNumber) {
ArrayList<String> list = new ArrayList<>();
for (int i = 0; i < loopNumber; i++) {
method2(list);
method3(list);
}
}
public void method2(ArrayList<String> list) {
list.add("1");
}
public void method3(ArrayList<String> list) {
list.remove(0);
}
}
class ThreadSafeSubClass extends ThreadSafe {
@Override
public void method3(ArrayList<String> list) {
new Thread(() -> {
list.remove(0);
}).start();
}
}
public class Test {
public static void main(String[] args) {
ThreadSafeSubClass t = new ThreadSafeSubClass();
for(int i = 0; i<100; i++){
new Thread(()->{
t.method1(100);
}).start();
}
}
}
我们对调用子类的method1方法,但是子类重写了method3方法,在method3新起了线程,也就是说我们把成员变量的ArrayList的引用给暴露给了另外一个线程,出现了线程安全问题
这也就给了我们一个启示:
方法的访问修饰符实际上可以保护我们的线程安全问题
3 几个共享问题
多个线程会共享使用其中的userService,在service里面,有对count的改变操作,也就是临界区,所以会有线程安全问题
也会有线程安全问题
4. 从Java内存模型的方面分析一下线程安全问题
4.1 先来看一下jvm内存模型
可以看到类的class文件
会被类加载器加载到jvm
中,再由执行引擎执行,而对于jvm的内存结构
如下
PC程序计数器
- 每个线程对应有一个程序计数器。
- 各线程的程序计数器是
线程私有的
,互不影响,是线程安全的。 - 程序计数器记录线程正在执行的
内存地址
,以便被中断线程恢复执行时再次按照中断时的指令地址继续执行
Java栈JavaStack(虚拟机栈JVM Stack)
-
每个线程会对应一个
Java栈
,每个Java栈由若干栈帧组成
,每个方法
对应一个栈帧
; -
栈帧在方法运行时,创建并入栈;方法执行完,该栈帧弹出栈帧中的元素作为该方法返回值,该栈帧被清除;
-
栈顶的栈帧叫
活动栈
,表示当前执行的方法,才可以被CPU执行; -
线程请求的栈深度大于虚拟机所允许的深度,将抛出
StackOverflowError
异常; -
栈扩展时无法申请到足够的内存,就会抛出
OutOfMemoryError
异常;
方法区
-
方法区是Java堆的永久区(
PermanetGeneration
) -
方法区存放了要加载的
类的信息
(名称、修饰符等)、类中的静态常量
、类中定义为final类型的常量·、
类中的Field信息、
类中的方法信息` -
方法区是被Java线程共享的
-
方法区要使用的内存超过其允许的大小时,会抛出
OutOfMemoryError
:PremGen space
的错误信息。
常量池
- 常量池是方法区的一部分
本地方法栈
- 本地方法栈和Java栈所发挥的作用非常相似,区别不过是Java栈为JVM执行Java方法服务,而本地方法栈为JVM执行Native方法服务