局部变量与成员变量的线程安全分析
前言
观看黑马程序员全面深入学习java并发编程遇到了疑问,进行总结,同时发现了一篇文章写的也不错,是根据视频进行的总结。https://blog.csdn.net/m0_37989980/article/details/111400237
线程安全分析
成员变量和静态变量是否线程安全?
-
如果它们没有共享,则线程安全
-
如果它们被共享了,根据它们的状态是否能够改变,又分两种情况
-
如果只有读操作,则线程安全
-
如果有读写操作,则这段代码是临界区,需要考虑线程安全
-
局部变量是否线程安全?
-
局部变量是线程安全的
-
但局部变量引用的对象则未必
-
如果该对象没有逃离方法的作用访问,它是线程安全的
-
如果该对象逃离方法的作用范围,需要考虑线程安全
-
成员变量示例
static final int THREAD_NUMBER = 2;
static final int LOOP_NUMBER = 200;
public static void main(String[] args) {
ThreadUnsafe test = new ThreadUnsafe();
for (int i = 0; i < THREAD_NUMBER; i++) {
new Thread(() -> {
test.method1(LOOP_NUMBER);
}, "Thread" + i).start();
}
}
class ThreadUnsafe {
ArrayList<String> list = new ArrayList<>();
public void method1(int loopNumber) {
for (int i = 0; i < loopNumber; i++) {
// { 临界区, 会产生竞态条件
method2();
method3();
// } 临界区
}
}
private void method2() {
list.add("1");
}
private void method3() {
list.remove(0);
}
}
报错分析
因为list是成员变量,无论哪个线程中的 method2 引用的都是同一个堆中的 list 成员变量
此时list存在并发问题,因为多个Thread使用的同一个list,在并发情况下add操作可能被覆盖,导致remove的比add的多从而报错
局部变量示例
class ThreadSafe {
public final void method1(int loopNumber) {
ArrayList<String> list = new ArrayList<>();
for (int i = 0; i < loopNumber; i++) {
method2(list);
method3(list);
}
}
private void method2(ArrayList<String> list) {
list.add("1");
}
private void method3(ArrayList<String> list) {
list.remove(0);
}
}
分析
list 是局部变量,每个线程调用时会创建其不同实例,没有共享,所以list不存在并发问题
而 method2 的参数是从 method1 中传递过来的,与 method1 中引用同一个对象
继承父类线程不安全示例
对上方代码的method3进行重写后,出现线程不安全
class ThreadSafeSubClass extends ThreadSafe{
@Override
public void method3(ArrayList<String> list) {
new Thread(() -> {
list.remove(0);
}).start();
}
}
报错分析
对父类的method3进行重写后,method3新开线程操作list,所以每个thread中method1线程和method2是主线程,到了method3新开线程,method2和method3是不同的线程共享list,存在线程安全问题
然而list是局部变量,每个method1的thread不共享list,为什么还会不安全?
本来猜测是不是多线程并发导致的list的add出现覆盖问题,但是局部变量的list是线程私有,不该有问题,于是我把主函数就设了一个线程,加大了method1中循环次数,发现仍然有问题,错误锁定在了method2和method3的执行顺序问题
因为在method1中的for循环,由于method3新开了线程,所以这里method2和method3没有防止指令重排,执行顺序会出问题。
在上一个局部变量示例中,没对method3进行重写,我对线程进行了输出,1,0交错打印
public void method2(ArrayList<String> list) {
System.out.println((Thread.currentThread() + "1"));
}
public void method3(ArrayList<String> list) {
System.out.println((Thread.currentThread() + "0"));
}
在重写之后,我发现for循环并不是按顺序执行的,可能method3会在method2之前执行,这就导致了remove发生错误
这里我猜测是因为原本的局部变量示例method2和method3是在同一个线程中,需根据as-if-serial语义,进行了指令重排,保证method3在method2之后执行
而重写method3之后,method2和method3在不同线程,所以可能导致method3被指令重排到之前执行,出现问题
public void method3(ArrayList<String> list) {
new Thread(() -> {
System.out.println((Thread.currentThread() + "0"));
}).start();
}
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-yUXLLMGS-1617846620121)(https://i.loli.net/2021/04/08/1SQpaOPHfRNrG58.png)]
完整代码
package com.apollo.day02;
import java.util.ArrayList;
public class demo1 {
public static void main(String[] args) {
ThreadSafeSubClass test = new ThreadSafeSubClass();
for(int i = 0 ; i < 1; i++){
new Thread(()->{
test.method1(20000);
}, "Thread" + (i + 1)).start();
}
}
}
class ThreadSafe {
public 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");
System.out.println((Thread.currentThread() + "1"));
}
public void method3(ArrayList<String> list) {
// list.remove(0);
System.out.println((Thread.currentThread() + "0"));
}
}
class ThreadSafeSubClass extends ThreadSafe{
@Override
public void method3(ArrayList<String> list) {
new Thread(() -> {
// list.remove(0);
System.out.println((Thread.currentThread() + "0"));
}).start();
}
}
final和private的必要性
根据上一个例子,我们可以用final禁止类被继承,防止子类重写方法搞事情,也可以直接把方法设为private禁止重写。
方法访问修饰符带来的思考: 如果把上述例子2中的method2和method3 的方法修改为public 会不会导致线程安全问题;
分情况:
情况1:有其它线程调用 method2 和 method3
只修改为public修饰,此时不会出现线程安全的问题, 即使线程2调用method2/3方法, 给2/3方法传过来的list对象也是线程2调用method1方法时,传递给method2/3的list对象, 不可能是线程1调用method1方法传的对象。
情况2:在情况1 的基础上,为ThreadSafe 类添加子类,子类覆盖method2 或 method3方法,即如下所示: 从这个例子可以看出 private 或 final 提供【安全】的意义所在,请体会开闭原则中的【闭】
如果改为public, 此时子类可以重写父类的方法, 在子类中开线程来操作list对象, 此时就会出现线程安全问题:
子类和父类共享了list对象 如果改为private, 子类就不能重写父类的私有方法, 也就不会出现线程安全问题;
所以所private修饰符是可以避免线程安全问题. 所以如果不想子类, 重写父类的方法的时候, 我们可以将父类中的方法设置为private,
final修饰的方法, 此时子类就无法影响父类中的方法了!