【并发编程JUC】变量的线程安全分析

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

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

局部变量是否线程安全?

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

1 局部变量分析

测试代码:

@Slf4j(topic = "test4")
public class test4 {
    public static void test() {
        int i = 10;
        i++;
    }
}

字节码(先编译代码,再反编译:javap -v test4.class)

public static void test();
  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 18: 0
      line 19: 3
      line 20: 6
    LocalVariableTable:
      Start  Length  Slot  Name   Signature
          3       4     0     i   I

每个线程调用test()方法时,局部变量i会在每个线程的栈帧内存中创建多份,因此不存在共享。
在这里插入图片描述

2 成员变量与局部变量对比分析

ThreadUnsafe类有成员变量list和三个方法,method2方法是向list中添加元素,method3是从list中移除元素。

当两个线程共同作用同一个list对象时,会出现以下情况:线程1添加元素没有写进内存时被线程2中断,线程2添加元素并写进内存,之后线程1再写进内存,两次添加相当于只添加了一次,之后再移除两次,就会发生错误。

public class TestThreadSafe {
    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+1)).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); }
}

报错IndexOutOfBoundsException

Exception in thread "Thread1" java.lang.IndexOutOfBoundsException: Index: 0, Size: 0
	at java.util.ArrayList.rangeCheck(ArrayList.java:659)
	at java.util.ArrayList.remove(ArrayList.java:498)
	at Chapter4.ThreadUnsafe.method3(TestThreadSafe.java:39)
	at Chapter4.ThreadUnsafe.method1(TestThreadSafe.java:34)
	at Chapter4.TestThreadSafe.lambda$main$0(TestThreadSafe.java:23)
	at java.lang.Thread.run(Thread.java:748)

分析:

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

在这里插入图片描述
当把list成成员变量改成局部变量后:

public class TestThreadSafe {
    static final int THREAD_NUMBER = 2;
    static final int LOOP_NUMBER = 200;

    public static void main(String[] args) {
        ThreadSafe test = new ThreadSafe();
        for (int i = 0; i < THREAD_NUMBER; i++) {
            new Thread(() -> {
                test.method1(LOOP_NUMBER);
            }, "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);
        }
    }

    private void method2(ArrayList<String> list) { list.add("1"); }
    private void method3(ArrayList<String> list) { list.remove(0); }
}

分析:

  • list是局部变量,每个线程调用时会创建其不同实例,没有共享
  • 而method2的参数是从method1中传递过来的,与method1中引用同一个对象
  • method3的参数分析与method2相同
  • method2和method3都是私有方法,操作list的方法只有这两个。
    在这里插入图片描述

3 局部变量暴露引用

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

  • 情况1:有其它线程调用method2和method3
  • 情况2:在情况1的基础上,为ThreadSafe类添加子类,子类覆盖method2或method3方法,即
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"); }
    public void method3(ArrayList<String> list) { list.remove(0); }
}

将method2和method3的权限修饰符改为public,此时仍线程安全。因为,在调用method2和method3是,操作的list并不是在method1内的list。

添加子类ThreadSafeSubClass继承ThreadSafe,并重写method3方法,在其内部创建线程来操作list。

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

此时会出现线程安全问题,因为method1创建list对象操作list,调用method3时会创建另一个线程操作这个list,多个线程共享list,将出现线程安全问题。

从例子可以看出private和final提供【安全】的意义所在。

private修饰的方法不能被重写。
final防止子类覆盖方法。

4 常见线程安全类

  • String
  • Integer
  • StringBuffer
  • Random
  • Vector
  • Hashtable
  • java.util.concurrent包下的类

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

  • 它们的每个方法是原子的
  • 但注意它们多个方法的组合不是原子的,见后面分析

5 线程安全类方法的组合

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

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

可能的执行顺序如下:
在这里插入图片描述

理想情况下,可能只有一个线程进行了 put;但也有可能一个线程put完,又被另一个线程put覆盖。覆盖前面的情况,丢失了更新内容。

线程安全类中每个方法都能保证原子性,但是组合不能保证原子性。如果想让组合保证原子性,需要在组合的外面加一个保护。

6 不可变线程安全性

String、Integer 等都是不可变类,因为其内部的状态(属性)不可以改变(只能读,不能改),因此它们的方法都是线程安全的

但,String 有replace, substring 等方法可以改变值,那么这些方法又是如何保证线程安全的呢?

查看String的源码,其中的substring方法创建了一个新的字符串,并不是对原字符串的修改。

public String substring(int beginIndex) {
    if (beginIndex < 0) {
        throw new StringIndexOutOfBoundsException(beginIndex);
    }
    int subLen = value.length - beginIndex;
    if (subLen < 0) {
        throw new StringIndexOutOfBoundsException(subLen);
    }
    return (beginIndex == 0) ? this : new String(value, beginIndex, subLen);
}

7 实例分析

例1
在这里插入图片描述
Servlet运行在Tomcat环境下,只有一个实例,会被tomcat的多个线程共享,因此其中的成员变量都会存在共享问题。

  • Map map:不是线程安全的,非线程安全类,HashTable是线程安全的。
  • String S1:线程安全,因为它是不可变类。
  • final String S2:同样线程安全。
  • Date D1:不是线程安全的,非线程安全类。
  • final Date D2:Date的对象D2的引用值不能变,但日期里的属性(年月日)可以改变,不是线程安全的。对象的引用不可变不代表对象的状态不可变。

例2
在这里插入图片描述
在这里插入图片描述
从底向上分析:

  • DAO没有成员变量,即使多个线程访问,也没有要修改的状态,所以是线程安全的。
  • Connection是方法内的局部变量,即使有多线程,其他线程访问不到(每个线程都会单独创建一个Connection),也是线程安全的。
  • Service里面包含私有的成员变量,但没有其他的地方可以修改它,它是不可变的,所以它是线程安全的。

例3
在这里插入图片描述
此时Connection为DAO的局部变量,Servelet只有一份,所以Service只有一份,所以DAO只有一份,所以DAO肯定会被多个线程共享,所以其中的变量会被多个线程共享。所以,不是线程安全的。

比如,线程1刚刚创建Connection,还没用呢,线程2就拿着Connection使用,使用后close,那么机会出问题了。

例4
在这里插入图片描述
与例4相比,Dao相同,但Service不同。在每次调用Service时,会重新创建一个新的Dao,没有将Dao当成Service的成员变量。

Dao为Service内方法的局部变量,每个新线程都会创建一个新的Dao,新的Dao会创建一个新的Connection,所以不会出现线程安全问题。但是这种方式不太好。

例5
在这里插入图片描述
虽然SimpleDateFormat是剧本变量,但是它可通过抽象方法foo暴露给外面。它的子类可能做了一些不恰当的方法。

其中foo的行为是不确定的,可能导致不安全的发生,被称之为外星方法
在这里插入图片描述
不想暴露给外面的方法或属性就设置为finalString的实现就是这样做的。

8 习题

卖票

@Slf4j(topic = "ExerciseSell")
public class ExerciseSell {
    public static void main(String[] args) throws InterruptedException {
        // 模拟多人买票
        TicketWindow window = new TicketWindow(1000);

        // 卖出票数统计
        List<Integer> amountList = new Vector<>();

        // 所有线程的结合
        List<Thread> threadList = new ArrayList<>();

        // 卖票
        for (int i = 0; i < 100; i++) {
            Thread thread = new Thread(() -> {
                // 买票
                int amount = window.sell(randomAmount());
                try {
                    Thread.sleep(2000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                amountList.add(amount);
            });
            threadList.add(thread);
            thread.start();
        }

        // 等待线程运行结束
        for (Thread thread : threadList) {
            thread.join();
        }
        // 统计卖出票数和剩余票数
        log.debug("余票:{}", window.getCount());
        log.debug("卖出票数:{}", amountList.stream().mapToInt(i->i).sum());
    }

    // Random 为线程安全
    static Random random = new Random();

    // 随机1-5
    public static int randomAmount() {
        return random.nextInt(5) + 1;
    }
}

/**
 * 售票窗口
 */
class TicketWindow {
    private int count;

    public TicketWindow(int count) {
        this.count = count;
    }

    public int getCount() {
        return count;
    }

    public int sell(int amount) {
        if (count >= amount) {
            count -= amount;
            return amount;
        } else {
            return 0;
        }
    }
}

多运行几次可能会产生错误:出售的票数 + 剩余票数 != 最初票数

找出临界区,临界区:对共享资源的多线程读写操作

卖票window里的count是一个共享变量

int amount = window.sell(randomAmount());

这也是一个共享变量,但他是线程安全的。

amountList.add(amount);

所以只要保证这个卖票这个动作涉及的共享变量是线程安全的即可,添加synchronized

public synchronized int sell(int amount) {
    if (count >= amount) {
        count -= amount;
        return amount;
    } else {
        return 0;
    }
}
  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

望天边星宿

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值