用AI赚第一桶💰低成本搭建一套AI赚钱工具,源码可二开。 广告
## 构造方法非线程安全 当你在脑子里想象一个对象构造的过程,你会很容易认为这个过程是线程安全的。毕竟,在对象初始化完成前对外不可见,所以又怎会对此产生争议呢?确实,[Java 语言规范](https://docs.oracle.com/javase/specs/jls/se8/html/jls-8.html#jls-8.8.3) (JLS)自信满满地陈述道:“*没必要使构造器的线程同步,因为它会锁定正在构造的对象,直到构造器完成初始化后才对其他线程可见。*” 不幸的是,对象的构造过程如其他操作一样,也会受到共享内存并发问题的影响,只是作用机制可能更微妙罢了。 设想下使用一个 **static** 字段为每个对象自动创建唯一标识符的过程。为了测试其不同的实现过程,我们从一个接口开始。代码示例: ```java //concurrent/HasID.java public interface HasID { int getID(); } ``` 然后 **StaticIDField** 类显式地实现该接口。代码示例: ```java // concurrent/StaticIDField.java public class StaticIDField implements HasID { private static int counter = 0; private int id = counter++; public int getID() { return id; } } ``` 如你所想,该类是个简单无害的类,它甚至都没一个显式的构造器来引发问题。当我们运行多个用于创建此类对象的线程时,究竟会发生什么?为了搞清楚这点,我们做了以下测试。代码示例: ```java // concurrent/IDChecker.java import java.util.*; import java.util.function.*; import java.util.stream.*; import java.util.concurrent.*; import com.google.common.collect.Sets; public class IDChecker { public static final int SIZE = 100_000; static class MakeObjects implements Supplier<List<Integer>> { private Supplier<HasID> gen; MakeObjects(Supplier<HasID> gen) { this.gen = gen; } @Override public List<Integer> get() { return Stream.generate(gen) .limit(SIZE) .map(HasID::getID) .collect(Collectors.toList()); } } public static void test(Supplier<HasID> gen) { CompletableFuture<List<Integer>> groupA = CompletableFuture.supplyAsync(new MakeObjects(gen)), groupB = CompletableFuture.supplyAsync(new MakeObjects(gen)); groupA.thenAcceptBoth(groupB, (a, b) -> { System.out.println( Sets.intersection( Sets.newHashSet(a), Sets.newHashSet(b)).size()); }).join(); } } ``` **MakeObjects** 类是一个生产者类,包含一个能够产生 List\<Integer> 类型的列表对象的 `get()` 方法。通过从每个 `HasID` 对象提取 `ID` 并放入列表中来生成这个列表对象,而 `test()` 方法则创建了两个并行的 **CompletableFuture** 对象,用于运行 **MakeObjects** 生产者类,然后获取运行结果。 使用 Guava 库中的 **Sets.`intersection()` 方法,计算出这两个返回的 List\<Integer> 对象中有多少相同的 `ID`(使用谷歌 Guava 库里的方法比使用官方的 `retainAll()` 方法速度快得多)。 现在我们可以测试上面的 **StaticIDField** 类了。代码示例: ```java // concurrent/TestStaticIDField.java public class TestStaticIDField { public static void main(String[] args) { IDChecker.test(StaticIDField::new); } } ``` 输出结果: ``` 13287 ``` 结果中出现了很多重复项。很显然,纯静态 `int` 用于构造过程并不是线程安全的。让我们使用 **AtomicInteger** 来使其变为线程安全的。代码示例: ```java // concurrent/GuardedIDField.java import java.util.concurrent.atomic.*; public class GuardedIDField implements HasID { private static AtomicInteger counter = new AtomicInteger(); private int id = counter.getAndIncrement(); public int getID() { return id; } public static void main(String[] args) { IDChecker.test(GuardedIDField::new); } } ``` 输出结果: ``` 0 ``` 构造器有一种更微妙的状态共享方式:通过构造器参数: ```java // concurrent/SharedConstructorArgument.java import java.util.concurrent.atomic.*; interface SharedArg{ int get(); } class Unsafe implements SharedArg{ private int i = 0; public int get(){ return i++; } } class Safe implements SharedArg{ private static AtomicInteger counter = new AtomicInteger(); public int get(){ return counter.getAndIncrement(); } } class SharedUser implements HasID{ private final int id; SharedUser(SharedArg sa){ id = sa.get(); } @Override public int getID(){ return id; } } public class SharedConstructorArgument{ public static void main(String[] args){ Unsafe unsafe = new Unsafe(); IDChecker.test(() -> new SharedUser(unsafe)); Safe safe = new Safe(); IDChecker.test(() -> new SharedUser(safe)); } } ``` 输出结果: ``` 24838 0 ``` 在这里,**SharedUser** 构造器实际上共享了相同的参数。即使 **SharedUser** 以完全无害且合理的方式使用其自己的参数,其构造器的调用方式也会引起冲突。**SharedUser** 甚至不知道它是以这种方式调用的,更不必说控制它了。 同步构造器并不被java语言所支持,但是通过使用同步语块来创建你自己的同步构造器是可能的(请参阅附录:[并发底层原理](./Appendix-Low-Level-Concurrency.md),来进一步了解同步关键字—— `synchronized`)。尽管JLS(java语言规范)这样陈述道:“……它会锁定正在构造的对象”,但这并不是真的——构造器实际上只是一个静态方法,因此同步构造器实际上会锁定该类的Class对象。我们可以通过创建自己的静态对象并锁定它,来达到与同步构造器相同的效果: ```java // concurrent/SynchronizedConstructor.java import java.util.concurrent.atomic.*; class SyncConstructor implements HasID{ private final int id; private static Object constructorLock = new Object(); SyncConstructor(SharedArg sa){ synchronized (constructorLock){ id = sa.get(); } } @Override public int getID(){ return id; } } public class SynchronizedConstructor{ public static void main(String[] args){ Unsafe unsafe = new Unsafe(); IDChecker.test(() -> new SyncConstructor(unsafe)); } } ``` 输出结果: ``` 0 ``` **Unsafe**类的共享使用现在就变得安全了。另一种方法是将构造器设为私有(因此可以防止继承),并提供一个静态Factory方法来生成新对象: ```java // concurrent/SynchronizedFactory.java import java.util.concurrent.atomic.*; final class SyncFactory implements HasID{ private final int id; private SyncFactory(SharedArg sa){ id = sa.get(); } @Override public int getID(){ return id; } public static synchronized SyncFactory factory(SharedArg sa){ return new SyncFactory(sa); } } public class SynchronizedFactory{ public static void main(String[] args){ Unsafe unsafe = new Unsafe(); IDChecker.test(() -> SyncFactory.factory(unsafe)); } } ``` 输出结果: ``` 0 ``` 通过同步静态工厂方法,可以在构造过程中锁定 **Class** 对象。 这些示例充分表明了在并发Java程序中检测和管理共享状态有多困难。即使你采取“不共享任何内容”的策略,也很容易产生意外的共享事件。