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