关于 Builder 模式线程安全的疑问 - V2EX
V2EX = way to explore
V2EX 是一个关于分享和探索的地方
现在注册
已注册用户请  登录
PonysDad
V2EX    Java

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

  •  
  •   PonysDad 2019-11-19 23:05:18 +08:00 4505 次点击
    这是一个创建于 2164 天前的主题,其中的信息可能已经有所发展或是发生改变。
    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 创建对象时,出现的线程不安全问题。

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

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

    仅此而已

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

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

    《 Effective Java 》中有一段这样的描述:
    -----------------------------------------------------------------------------------------------------------------------------
    不幸的是,JavaBeans 模式本身有严重的缺陷。由于构造方法在多次调用中被分割,所以在构造过程中 JavaBean
    可能处于不一致的状态。该类没有通过检查构造参数参数的有效性来执行一致性的选项。在不一致的状态下尝试使用
    对象可能会导致与包含 bug 的代码大相径庭的错误,因此很难调试。
    -----------------------------------------------------------------------------------------------------------------------------
    一直模拟出构造方法被割裂而导致的不一致。
    不知道大家有没有一个很好例子?
    关于     帮助文档     自助推广系统     博客     API     FAQ     Solana     951 人在线   最高记录 6679       Select Language
    创意工作者们的社区
    World is powered by solitude
    VERSION: 3.9.8.5 32ms UTC 21:18 PVG 05:18 LAX 14:18 JFK 17:18
    Do have faith in what you're doing.
    ubao msn snddm index pchome yahoo rakuten mypaper meadowduck bidyahoo youbao zxmzxm asda bnvcg cvbfg dfscv mmhjk xxddc yybgb zznbn ccubao uaitu acv GXCV ET GDG YH FG BCVB FJFH CBRE CBC GDG ET54 WRWR RWER WREW WRWER RWER SDG EW SF DSFSF fbbs ubao fhd dfg ewr dg df ewwr ewwr et ruyut utut dfg fgd gdfgt etg dfgt dfgd ert4 gd fgg wr 235 wer3 we vsdf sdf gdf ert xcv sdf rwer hfd dfg cvb rwf afb dfh jgh bmn lgh rty gfds cxv xcv xcs vdas fdf fgd cv sdf tert sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf shasha9178 shasha9178 shasha9178 shasha9178 shasha9178 liflif2 liflif2 liflif2 liflif2 liflif2 liblib3 liblib3 liblib3 liblib3 liblib3 zhazha444 zhazha444 zhazha444 zhazha444 zhazha444 dende5 dende denden denden2 denden21 fenfen9 fenf619 fen619 fenfe9 fe619 sdf sdf sdf sdf sdf zhazh90 zhazh0 zhaa50 zha90 zh590 zho zhoz zhozh zhozho zhozho2 lislis lls95 lili95 lils5 liss9 sdf0ty987 sdft876 sdft9876 sdf09876 sd0t9876 sdf0ty98 sdf0976 sdf0ty986 sdf0ty96 sdf0t76 sdf0876 df0ty98 sf0t876 sd0ty76 sdy76 sdf76 sdf0t76 sdf0ty9 sdf0ty98 sdf0ty987 sdf0ty98 sdf6676 sdf876 sd876 sd876 sdf6 sdf6 sdf9876 sdf0t sdf06 sdf0ty9776 sdf0ty9776 sdf0ty76 sdf8876 sdf0t sd6 sdf06 s688876 sd688 sdf86