疑问:为何这段Java代码被标注为非线程安全?
为什么这段代码是非线程安全的?
嘿,我来帮你搞清楚这个问题~你之所以困惑,是因为误以为调用getList()的线程会自动互斥执行,但Java里普通方法默认没有任何线程同步机制,这就导致了代码里的竞态条件问题。
核心问题:竞态条件导致重复初始化
我们来模拟一下两个线程同时调用getList()的场景:
- 线程1进入
getList(),检查到list == null,准备执行list = new ArrayList<>(),但还没完成引用赋值(这个操作在JVM里不是原子的,分为创建对象、赋值引用两步); - 此时线程2也进入
getList(),因为线程1还没把新对象的引用赋值给list,所以线程2看到的list仍然是null; - 线程2也会执行
list = new ArrayList<>(),最终list会被两个线程先后赋值两次,导致两个线程可能拿到不同的ArrayList实例(看哪个线程先完成赋值)。
你可以运行这段代码多次,大概率会看到两个线程输出的identityHashCode不一样——这就是非线程安全的直接表现:本该只初始化一次的list被创建了多次,破坏了我们预期的“单例”行为。
你的误解:线程不会自动互斥
Java中,只有当方法被synchronized修饰,或者手动加锁时,才会保证同一时间只有一个线程进入方法/临界区。你的getList()是普通方法,多个线程可以同时进入执行,自然会出现上述的竞态问题。
修复方案
这里给你几种常见的修复方式,都能解决线程安全问题:
方案1:给getList()加synchronized修饰
最简单直接的方式,让整个方法成为同步方法,同一时间只有一个线程能进入:
public class MyClass { private List<String> list; public static void main (String[] args) throws InterruptedException { MyClass obj = new MyClass(); Thread thread1 = new Thread(() -> { System.out.println("thread1 : " + System.identityHashCode(obj.getList())); }); Thread thread2 = new Thread(() -> { System.out.println("thread2 : " + System.identityHashCode(obj.getList())); }); thread1.start(); thread2.start(); } // 加synchronized保证线程互斥 private synchronized List<String> getList () { if (list == null) { list = new ArrayList<>(); } return list; } }
方案2:双重检查锁定(DCL)+ volatile
如果担心synchronized方法的性能开销(虽然这个场景下影响很小),可以用双重检查锁定,只在初始化阶段加锁:
public class MyClass { // volatile修饰,禁止指令重排,保证多线程下的可见性 private volatile List<String> list; public static void main (String[] args) throws InterruptedException { MyClass obj = new MyClass(); Thread thread1 = new Thread(() -> { System.out.println("thread1 : " + System.identityHashCode(obj.getList())); }); Thread thread2 = new Thread(() -> { System.out.println("thread2 : " + System.identityHashCode(obj.getList())); }); thread1.start(); thread2.start(); } private List<String> getList () { if (list == null) { // 第一次无锁检查,避免每次都加锁 synchronized (this) { if (list == null) { // 加锁后再次检查,防止竞态 list = new ArrayList<>(); } } } return list; } }
注意:这里必须给
list加volatile修饰,否则JVM的指令重排可能导致线程拿到未完全初始化的ArrayList实例。
方案3:静态内部类实现延迟加载
这是一种更优雅的线程安全单例方式,利用Java类加载机制天然保证线程安全:
public class MyClass { // 静态内部类,只有当getList()调用时才会加载 private static class ListHolder { private static final List<String> INSTANCE = new ArrayList<>(); } public static void main (String[] args) throws InterruptedException { MyClass obj = new MyClass(); Thread thread1 = new Thread(() -> { System.out.println("thread1 : " + System.identityHashCode(obj.getList())); }); Thread thread2 = new Thread(() -> { System.out.println("thread2 : " + System.identityHashCode(obj.getList())); }); thread1.start(); thread2.start(); } private List<String> getList () { return ListHolder.INSTANCE; } }
内容的提问来源于stack exchange,提问作者James
相关产品推荐
相关产品推荐

