Merge pull request #356 from OrientationJump/master

Fix issue #112:翻译小节:构造器非线程安全
This commit is contained in:
LingCoder
2020-01-18 07:47:15 +08:00
committed by GitHub

View File

@@ -2226,7 +2226,238 @@ public class DiningPhilosophers {
<!-- Constructors are not Thread-Safe -->
## 构造函数非线程安全
当你在脑子里想象一个对象构造的过程你会很容易认为这个过程是线程安全的。毕竟在对象初始化完成前没人能见到这个对象所以又怎么会产生对于这个对象的争议呢确实java语言规范(JLS)自信满满地陈述道:“没有必要使构造器同步,因为它会锁定正在构造的对象,而这通常会使得该对象直到其所有构造器完成所有工作后,才对其他线程可见。”
不幸的是,对象构造过程像其他任何事物一样容易受到共享内存并发问题的影响,只是作用机制可能更微妙罢了。
考虑使用静态字段为每个对象自动创建唯一标识符的过程。为了测试其不同的实现过程,我们从一个接口开始:
```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);
}
}
/* Output:
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);
}
}
/* Output:
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));
}
}
/* Output:
24838
0
*/
```
在这里SharedUser构造器实际上共享了相同的参数。即使SharedUser以完全无害且合理的方式使用其自己的参数其构造器的调用方式也会引起冲突。SharedUser甚至不知道它是以这种方式调用的更不必说控制它了。
同步构造器并不被java语言所支持但是通过使用同步语块来创建你自己的同步构造器是可能的请参阅附录Low-Level Concurrency来进一步了解同步关键字——synchronized。尽管JLSjava语言规范这样陈述道“……它会锁定正在构造的对象”但这并不是真的——构造器实际上只是一个静态方法因此同步构造器实际上会锁定该类的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));
}
}
/* Output:
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));
}
}
/* Output:
0
*/
```
通过同步静态工厂方法可以在构造过程中锁定Class对象。
这些示例充分表明了在并发Java程序中检测和管理共享状态有多困难。即使您采取“不共享任何内容”的策略也很容易产生意外的共享事件。
<!-- Effort, Complexity,Cost -->
## 复杂性和代价