3.3、变量的线程安全分析

变量的线程安全分析

成员变量和静态变量是否线程安全?

  • 如果它们没有共享,则线程安全
  • 如果它们被共享了,根据它们的状态是否能够改变,又分两种情况
    • 如果只有读操作,则线程安全
    • 如果有读写操作,则这段代码是临界区,需要考虑线程安全

局部变量是否线程安全

  • 局部变量是线程安全的
  • 单局部变量引用的对象则未必
    • 如果该对象没有逃离方法的作用访问,它是线程安全的
    • 如果该对象逃离方法的作用范围,需要考虑线程安全

局部变量线程安全分析

public static void test1() {
	int i = 10; 
	i++;
}

每个线程调用test1()方法时局部变量i,会在每个线程的栈帧内存中被创建多份,因此不存在共享

public static void test1();
 descriptor: ()V
 flags: ACC_PUBLIC, ACC_STATIC
 Code:
 stack=1, locals=1, args_size=0
 0: bipush 10
 2: istore_0
 3: iinc 0, 1
 6: return
 LineNumberTable:
 line 10: 0
 line 11: 3
 line 12: 6
 LocalVariableTable:
 Start Length Slot Name Signature
 3 4 0 i I

如图
在这里插入图片描述
局部变量的引用稍有不同
先看一个成员变量的例子

public class Test2 {


    public static void main(String[] args) {
        ThreadUnsafe threadUnsafe = new ThreadUnsafe();
        for (int i = 0; i < 2; i++) {
            new Thread(() -> {
                threadUnsafe.method1(200);
            }, "t" + 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);
    }

}

其中一种情况是,如果线程2还未add,线程1 remove就会报错:

Exception in thread "t1" java.lang.IndexOutOfBoundsException: Index: 0, Size: 0
	at java.util.ArrayList.rangeCheck(ArrayList.java:657)
	at java.util.ArrayList.remove(ArrayList.java:496)
	at sync.test.ThreadUnsafe.method3(Test2.java:39)
	at sync.test.ThreadUnsafe.method1(Test2.java:30)
	at sync.test.Test2.lambda$main$0(Test2.java:15)
	at java.lang.Thread.run(Thread.java:748)

具体原因是:

new Thread(() -> {
    list.add("1");        // 时间1. 会让内部 size ++
    list.remove(0); // 时间3. 再次 remove size-- 出现角标越界
}, "t1").start();

new Thread(() -> {
    list.add("2");        // 时间1(并发发生). 会让内部 size ++,但由于size的操作非原子性,  size 本该是2,但结果可能出现1
    list.remove(0); // 时间2. 第一次 remove 能成功, 这时 size 已经是0
}, "t2").start();

分析:

  • 无论哪个线程中的method2引用的都是同一个对象中的list成员变量
  • method3与method2分析相同

在这里插入图片描述
将list修改为局部变量

public class Test2 {


    public static void main(String[] args) {
        ThreadUnsafe threadUnsafe = new ThreadUnsafe();
        for (int i = 0; i < 2; i++) {
            new Thread(() -> {
                threadUnsafe.method1(200);
            }, "t" + i).start();
        }
    }
}


class ThreadUnsafe {

//    ArrayList<String> list = new ArrayList<>();

    public 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是局部变量,每个线程调用时会创建不同的实例,没有共享
  • 而method2的参数是从method1中传递过来的,与method1中引用同一个对象
  • method3同理

方法访问修饰符带来的思考,如果把method2和method3的方法修改为public会不会有线程安全问题?

  • 情况1:有其他线程调用method2和method3
  • 情况2:在情况1的基础上,为ThreadSafe类添加子类,子类覆盖method2和method3方法

即:


class ThreadUnsafe {

//    ArrayList<String> list = new ArrayList<>();

    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");
    }

    public void method3(ArrayList<String> list) {
        list.remove(0);
    }

}

class ThreadUnsafeSub extends ThreadUnsafe {

    @Override
    public void method3(ArrayList<String> list) {
        new Thread(() -> {
            list.remove(0);
        }).start();
    }
}

从这个例子可以看出private或final提供【安全】的意义所在,请体会开闭原则中的【闭】

常见的线程安全类

  • String
  • integer
  • StringBuffer
  • Random
  • Vector
  • hashtable
  • java.util.concurrent包下的类

这里说他们是线程安全的是指,多个线程调用他们同一个实例的某个方法时,是线程安全的,也可以理解为

 public static void main(String[] args) {
        Hashtable<String, String> hashtable = new Hashtable<>();
        new Thread(() -> {
            hashtable.put("1", "1");
        }).start();
        new Thread(() -> {
            hashtable.put("2", "2");
        }).start();
    }
  • 他们的每个方法是原子的
  • 注意他们多个方法的组合不是原子的

线程安全类方法的组合

分析下面代码是否线程安全?

Hashtable table = new Hashtable();
// 线程1,线程2
if( table.get("key") == null) {
 table.put("key", value);
}

在这里插入图片描述

不可变类线程安全性

String、Integer等都是不可变类,因为其内部的状态不可以改变,因此他们的方法都是线程安全的
String的replace,substring等方法【可以】改变值,那么这些方法是如何保证线程安全的呢?

public class Immutable {

    private int value = 0;

    public Immutable(int value) {
        this.value = value;
    }

    public int getValue() {
        return this.value;
    }

    public Immutable add(int v) {
        return new Immutable(this.value + v);
    }

}

实例分析

暂略

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值