局部变量与成员变量的线程安全分析

局部变量与成员变量的线程安全分析

前言

观看黑马程序员全面深入学习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的多从而报错

image-20210408085806689

局部变量示例
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 中引用同一个对象
image-20210408090418156

继承父类线程不安全示例

对上方代码的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"));
    }

image-20210408091447937

在重写之后,我发现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)]

image-20210408091838411
image-20210408091527102

完整代码
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修饰的方法, 此时子类就无法影响父类中的方法了!

评论 4
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值