public class Address {
private final Long id;
private final String street;
Address(Long id, String street) {
this.id = id;
this.street = street;
}
public static AddressBuilder builder() {
return new AddressBuilder();
}
public static class AddressBuilder {
private Long id;
private String street;
AddressBuilder() {
}
public AddressBuilder id(Long id) {
this.id = id;
return this;
}
public AddressBuilder street(String street) {
this.street = street;
return this;
}
public Address build() {
return new Address(id, street);
}
public String toString() {
return "Address.AddressBuilder(id=" + this.id + ", street=" + this.street + ")";
}
}
}
//测试类
public class Test {
Address obj;
public void write() {
Address address = Address.builder()
.id(1L)
.street("street 1")
.build();
obj = address;
}
public void read() {
Long id = obj.id;
}
}
现在,线程 A 调用 write 方法,创建 Address 实例并赋值给 obj,线程 B 调用 read 方法,读取 id 值。 这样子应该不是线程安全的吧,即使 id 有 final 修饰。
现在疑惑是,使用 Builder 模式创建对象一定是线程安全的吗?
虽然理论每次调用 build 方法会创建一个新实例,各线程之间不共享该实例也就不会出现并发问题。 但是,使用 Java Bean 也可以这样子
Address address = new Address();
address.setId(99L);
address.setStreet("Street 2");
每个线程都创建一个新实例啊,即使指令重排也没有影响啊。
但是 Effective Java 中又强调使用 Builder 模式可以规避 Jave Bean 创建对象时,出现的线程不安全问题。
1
billlee 2019-11-20 00:26:23 +08:00 1
并不能保证线程安全,要线程安全必须加 synchornized. 设计模式只能让代码变得更好看,减少编码上的人为错误。
Immutable 对象可以减少人为的编码错误 -> 构造 immutable 对象可能需要很多参数的构造函数 -> 很多参数的函数不好看 -> 用 builder pattern 可以变得更好看 仅此而已 有 named parameters 语法的 kotlin 和 scala 就不需要 builder pattern 了。 |
2
vjnjc 2019-11-20 00:29:25 +08:00
用 Address address = new Address(id, "street", "val3")能保证线程安全,
用 builder 也能保证。 但 address.setId(99L); address.setStreet("Street 2"); 不能保证线程安全,举个例子 setId()这个 api 被多个线程访问,会互相覆盖,不能保证得到正确值。 |
3
xzg 2019-11-20 09:45:33 +08:00
首先你的两个线程是持有同一个 test 对象来分别调用 write 和 read 方法?如果是那肯定非线程安全,如果是持有 new 的两个 test 对象,那就没影响了
|
4
PonysDad OP @billlee @vjnjc
我感觉用 builder pattern 构造 immutable 对象也不是线程安全的。 ```java Address address = Address.builder() .id(1L) .street("street 1") .build(); ``` 可能编译后(指令重排)如下: ```java AddressBuilder addressBuilder = new AddressBuilder(); Address address = addressBuilder.build(); addressBuilder.id = 1L; addressBuilder.street = "street 1"; ``` 这时候,线程 B 可能读取到 address 实例未初始化的值。 但是如果使用构造函数实例化,final 内存模型能保证 address 已经初始化完毕。 不知道我的理解是否有错? 请不吝赐教。 |
5
PonysDad OP obj = address;
补上编译后代码漏了上面一行。 |
6
PonysDad OP |
7
billlee 2019-11-20 14:09:38 +08:00 1
@PonysDad #4 build() 对 id, streat 有数据依赖,不会被重排或者乱序发射的。这里不是指令重排的问题,是内存可见性的问题,一个线程对内存的写操作不一定能被另一个线程看到。
不需要考虑 builder pattern, 如果直接构造一个对象,把引用传递给另一个线程,不做线程同步,另一个线程可能看到的状态就是乱的。 可以去看一下 java memory model, 或者计算机结构里面关于 cache coherence 的内容。 |
8
vjnjc 2019-11-20 16:24:24 +08:00
@PonysDad #4 你举的例子不成立。
1 指令重排不会更改你的源码的顺序。 2 访问到未初始化的 reference 只会发生在这段代码会被 2 个 thread 并发执行的时候,你举的例子没有表现出这个条件 |
9
PonysDad OP @billlee
一针见血。 我漏看了 return new Address(id, street);是传递两个值过去的,且一直在纠结这个构造函数 final 域的问题。 addressBuilder.id = 1L; addressBuilder.street = "street 1"; 只有这两句可以被重排。 剩下的是内存可见性问题。 |
10
PonysDad OP @billlee @vjnjc
《 Effective Java 》中有一段这样的描述: ----------------------------------------------------------------------------------------------------------------------------- 不幸的是,JavaBeans 模式本身有严重的缺陷。由于构造方法在多次调用中被分割,所以在构造过程中 JavaBean 可能处于不一致的状态。该类没有通过检查构造参数参数的有效性来执行一致性的选项。在不一致的状态下尝试使用 对象可能会导致与包含 bug 的代码大相径庭的错误,因此很难调试。 ----------------------------------------------------------------------------------------------------------------------------- 一直模拟出构造方法被割裂而导致的不一致。 不知道大家有没有一个很好例子? |