关于 Builder 模式线程安全的疑问

2019-11-19 23:05:18 +08:00
 PonysDad
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 创建对象时,出现的线程不安全问题。

4047 次点击
所在节点    Java
10 条回复
billlee
2019-11-20 00:26:23 +08:00
并不能保证线程安全,要线程安全必须加 synchornized. 设计模式只能让代码变得更好看,减少编码上的人为错误。

Immutable 对象可以减少人为的编码错误 -> 构造 immutable 对象可能需要很多参数的构造函数 -> 很多参数的函数不好看 -> 用 builder pattern 可以变得更好看

仅此而已

有 named parameters 语法的 kotlin 和 scala 就不需要 builder pattern 了。
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 被多个线程访问,会互相覆盖,不能保证得到正确值。
xzg
2019-11-20 09:45:33 +08:00
首先你的两个线程是持有同一个 test 对象来分别调用 write 和 read 方法?如果是那肯定非线程安全,如果是持有 new 的两个 test 对象,那就没影响了
PonysDad
2019-11-20 13:46:57 +08:00
@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 已经初始化完毕。

不知道我的理解是否有错?
请不吝赐教。
PonysDad
2019-11-20 13:52:10 +08:00
obj = address;

补上编译后代码漏了上面一行。
PonysDad
2019-11-20 13:52:47 +08:00
这一行是接在
Address address = addressBuilder.build();
后面
billlee
2019-11-20 14:09:38 +08:00
@PonysDad #4 build() 对 id, streat 有数据依赖,不会被重排或者乱序发射的。这里不是指令重排的问题,是内存可见性的问题,一个线程对内存的写操作不一定能被另一个线程看到。
不需要考虑 builder pattern, 如果直接构造一个对象,把引用传递给另一个线程,不做线程同步,另一个线程可能看到的状态就是乱的。
可以去看一下 java memory model, 或者计算机结构里面关于 cache coherence 的内容。
vjnjc
2019-11-20 16:24:24 +08:00
@PonysDad #4 你举的例子不成立。
1 指令重排不会更改你的源码的顺序。
2 访问到未初始化的 reference 只会发生在这段代码会被 2 个 thread 并发执行的时候,你举的例子没有表现出这个条件
PonysDad
2019-11-20 21:48:23 +08:00
@billlee
一针见血。
我漏看了 return new Address(id, street);是传递两个值过去的,且一直在纠结这个构造函数 final 域的问题。
addressBuilder.id = 1L;
addressBuilder.street = "street 1";
只有这两句可以被重排。
剩下的是内存可见性问题。
PonysDad
2019-11-24 10:10:13 +08:00
@billlee @vjnjc

《 Effective Java 》中有一段这样的描述:
-----------------------------------------------------------------------------------------------------------------------------
不幸的是,JavaBeans 模式本身有严重的缺陷。由于构造方法在多次调用中被分割,所以在构造过程中 JavaBean
可能处于不一致的状态。该类没有通过检查构造参数参数的有效性来执行一致性的选项。在不一致的状态下尝试使用
对象可能会导致与包含 bug 的代码大相径庭的错误,因此很难调试。
-----------------------------------------------------------------------------------------------------------------------------
一直模拟出构造方法被割裂而导致的不一致。
不知道大家有没有一个很好例子?

这是一个专为移动设备优化的页面(即为了让你能够在 Google 搜索结果里秒开这个页面),如果你希望参与 V2EX 社区的讨论,你可以继续到 V2EX 上打开本讨论主题的完整版本。

https://www.v2ex.com/t/621184

V2EX 是创意工作者们的社区,是一个分享自己正在做的有趣事物、交流想法,可以遇见新朋友甚至新机会的地方。

V2EX is a community of developers, designers and creative people.

© 2021 V2EX