英语阅读 21周 Java Concurrency: CopyOnWrite

259 阅读3分钟

Java Concurrency: CopyOnWrite 本篇文章主要讲述Java并发的CopyOnWrite机制

问题复现

文章首先解释在何种情况之下使用CopyOnWrite。例如以下问题

public class MutableAddress {
    private volatile String street;
    private volatile String city;
    private volatile String phoneNumber;
    public MutableAddress(String street, String city, String phoneNumber) {
        this.street = street;
        this.city = city;
        this.phoneNumber = phoneNumber;
    }
    public String getStreet() {
        return street;
    }
    public String getCity() {
        return city;
    }
    public void updatePostalAddress(String street ,String city ) {
        this.street = street;
        this.city = city;
    }
    @Override
    public String toString() {
        return "street=" + street + 
        ",city=" + city + 
        ",phoneNumber=" + phoneNumber;
    }
}

测试代码,通过Interleave 注解将函数进行分片处理(vmlens.com/)

public class ConcurrencyTestReadWrite {
  private final MutableAddress address = new MutableAddress("E. Bonanza St." 
    , "South Park" , "456 77 99");
  private String readAddress;
  @Interleave(ConcurrencyTestReadWrite.class)
  private void updatePostalAddress() {
    address.updatePostalAddress("Evergreen Terrace" , "Springfield");
   }
  @Interleave(ConcurrencyTestReadWrite.class)
  private void read() {
    readAddress = address.toString();
  } 
  @Test
  public void test() throws InterruptedException {
   Thread first  = new Thread( () ->    {  updatePostalAddress();  } ) ;
   Thread second = new Thread( () ->   {  read();  } ) ;
   first.start();
   second.start();
   first.join();
   second.join();   
   assertTrue(  "readAddress:" + readAddress  ,  
    readAddress.equals(
    "street=E. Bonanza St.,city=South Park,phoneNumber=456 77 99")  || 
    readAddress.equals(
    "street=Evergreen Terrace,city=Springfield,phoneNumber=456 77 99") );   
  }
}

实际输出代码,打出来信息只更新一半

java.lang.AssertionError: readAddress:
    street=Evergreen Terrace,city=South Park,phoneNumber=456 77 99

通过测试后结果查看,更新操作被拆分两次,到之后该问题

CopyOnWrite

使用CopyOnWrite 技术可以解决以上问题。该技术在写入时候复制对象,修改之后再重新发布出来。复制和发布过程不受到其他干扰。 第一:用永恒不变对象代表当前地址

public class AddressValue {
    private final String street;
    private final String city;
    private final String phoneNumber;
    public AddressValue(String street, String city, 
                String phoneNumber) {
        super();
        this.street = street;
        this.city = city;
        this.phoneNumber = phoneNumber;
    }
    public String getStreet() {
        return street;
    }
    public String getCity() {
        return city;
    }
    public String getPhoneNumber() {
        return phoneNumber;
    }
}

第二:变化对象来执行CopyOnWrite技术

public class AddressUsingCopyOnWrite {
    private volatile AddressValue addressValue;
    private final Object LOCK = new Object();
    @Override
    public String toString() {
        AddressValue local = addressValue;
        return "street=" + local.getStreet() +
        ",city=" + local.getCity() + 
        ",phoneNumber=" + local.getPhoneNumber();
    }
    public AddressUsingCopyOnWrite(String street, String city, String phone) {
        this.addressValue = new AddressValue( street,  city,  phone);
    }
    public void updatePostalAddress(String street ,String city ) {
        synchronized(LOCK){
            addressValue = new AddressValue(  
            street,  city,  addressValue.getPhoneNumber() );
        }
    }
    public void updatePhoneNumber( String phoneNumber) {
        synchronized(LOCK){
            addressValue = new AddressValue(  
            addressValue.getStreet(), addressValue.getCity(),  phoneNumber );
        }   
    }
}

要注意toString也先把对象赋值给本地对象,而不是以下逻辑

public String toStringNotThreadSafe() {
    return "street=" + addressValue.getStreet() + 
    ",city=" + addressValue.getCity() + 
    ",phoneNumber=" + addressValue.getPhoneNumber();
}

以上逻辑也存在过程中变化可能性,运行结果为

java.lang.AssertionError: readAddress:
    street=E. Bonanza St.,city=Springfield,phoneNumber=456 77 99

看看执行结果图,三次toString()与更新逻辑也是混合在一起,也会导致问题

写操作加入同步锁

如果写入操作不加入同步锁会出现什么问题?例如以下代码

public void updatePostalAddress(String street ,String city ) {
            addressValue = new AddressValue(  street,  city,  
                addressValue.getPhoneNumber() );
}
public void updatePhoneNumber( String phone) {
            addressValue = new AddressValue(  addressValue.getStreet(),  
                addressValue.getCity(),  phone );
}

结果,没有错误

[INFO] BUILD SUCCESS

为了进一步说明问题,我们使用另外一个测试逻辑,通过不同线程同时更新地址

public class ConcurrencyTestTwoWrites {
   private final AddressUsingCopyOnWriteWithoutSynchronized address = 
    new AddressUsingCopyOnWriteWithoutSynchronized("E. Bonanza St." 
    , "South Park" , "456 77 99"); 
  @Interleave(ConcurrencyTestTwoWrites.class)
  private void updatePostalAddress() {
   address.updatePostalAddress("Evergreen Terrace" , "Springfield");
  }
  @Interleave(ConcurrencyTestTwoWrites.class)
  private void updatePhoneNumber() {
   address.updatePhoneNumber("99 55 2222");
  } 
  @Test
  public void test() throws InterruptedException {
   Thread first  = new Thread( () -> {  updatePostalAddress();} ) ;
   Thread second = new Thread( () -> {  updatePhoneNumber();  } ) ;
   first.start();
   second.start();
   first.join();
   second.join(); 
   assertEquals(  "street=Evergreen Terrace,
   city=Springfield,phoneNumber=99 55 2222" , 
   address.toString() );
  }
}

运行结果为

org.junit.ComparisonFailure: 
    expected:<...ngfield,phoneNumber=[99 55 2222]> 
    but was:<...ngfield,phoneNumber=[456 77 99]>

一个线程会覆盖另一个线程的更新,从而导致竞争条件。通过同步避免该问题。

总结

CopyOnWrite允许我们以线程安全的方式更新类。 这种技术的主要优点是读取线程永远不会阻塞,写入线程只会被其他写入线程阻塞。 使用此技术时,请确保在读取时始终使用局部变量,在写入时始终使用同步锁。