多线程设计模式解读—Immutable Object(不可变对象)模式
前面讲了Producer-Consumer模式,它有许多变种,我们以后会讲。我们将接着了解另外一个分支的设计模式,前面所讲的所有的模式,都是要用到锁的,而锁是会带来一些额外的开销和问题的,那么能不能不通过锁,实现多线程环境下的线程安全呢?其中一个思路就是通过Immutable Object(不可变对象)模式。它使用对外可见的不可变对象,天生具有线程安全的“基因”。因为与多线程的原子性、可见性相关的问题(如失效数据、丢失更新操作、对象处于不一致状态等)都与多线程试图同时访问同一个可变状态相关,若对象状态不可变,那这些问题也就不存在了。
不可变对象的条件:
- 对象创建以后其状态就不能修改
-
对象的所有域都是final类型
-
对象是正确创建的(对象创建期间,this引用没有逸出)
构造不可变对象建议:
- 类声明为final类型,字段可见性设置为private,这样可以防止子类修改其字段值。
- 字段声明为final字段,这样字段被赋值一次,就不会再被赋值。同时保证了字段引用的对象的初始化安全。
- 不存在setter方法,且确保字段引用的实例未变化。
示例代码实例如下:
public final class ImmutableCustomer {
private final String name;
private final String address;
public ImmutableCustomer(String name, String address) {
this.name = name;
this.address = address;
}
public String getName() {
return name;
}
public String getAddress() {
return address;
}
@Override
public String toString() {
return "[ ImmutableCustomer: name = " + name + ", address = " + address + " ]";
}
}
public class OpeCustomerThread extends Thread {
private ImmutableCustomer immutableCustomer;
public OpeCustomerThread(ImmutableCustomer person) {
this.immutableCustomer = person;
}
@Override
public void run() {
while (true) {
System.out.println(Thread.currentThread().getName() + " "
+ immutableCustomer);
}
}
}
public class Main {
public static void main(String[] args) {
ImmutableCustomer alice = new ImmutableCustomer("Alice", "Alaska");
alice = new ImmutableCustomer("Ace", "Alaska");
new OpeCustomerThread(alice).start();
new OpeCustomerThread(alice).start();
new OpeCustomerThread(alice).start();
}
}
jdk中的CopyOnWriteArrayList也使用了该模式,它是ArrayList的线程安全变体,其中所有变更操作(添加,设置等)都是通过创建底层数组的新副本来实现的(实际上,array的元素是可以被替换的,这是一个事实不可变对象,即对象从技术上而言未满足不可变对象的严格定义,是可变,但其状态在安全发布后不会再改变了)。这需要一定开销,但是当遍历操作远比变更频繁时,它可能比其他方法更有效。它不需要加锁就可以排除并发线程之间的干扰。迭代器不会抛出ConcurrentModificationException。自迭代器创建后,迭代器无需考虑后期修改操作带来的影响。
源码片段如下:
public class CopyOnWriteArrayList<E>
implements List<E>, RandomAccess, Cloneable, java.io.Serializable {
private static final long serialVersionUID = 8673264195747942595L;
/** 锁保护所有的变更操作 */
final transient ReentrantLock lock = new ReentrantLock();
/** array,只能通过getArray/setArray访问 */
private transient volatile Object[] array;
/**
* 获取array
*/
final Object[] getArray() {
return array;
}
/**
* 设置array.
*/
final void setArray(Object[] a) {
array = a;
}
/**
* 添加特定元素到list
*/
public boolean add(E e) {
final ReentrantLock lock = this.lock;
lock.lock();
try {
Object[] elements = getArray();
int len = elements.length;
Object[] newElements = Arrays.copyOf(elements, len + 1);
newElements[len] = e;
setArray(newElements);
return true;
} finally {
lock.unlock();
}
}
/**
* 移除特定位置的元素
*/
public E remove(int index) {
final ReentrantLock lock = this.lock;
lock.lock();
try {
Object[] elements = getArray();
int len = elements.length;
E oldValue = get(elements, index);
int numMoved = len - index - 1;
if (numMoved == 0)
setArray(Arrays.copyOf(elements, len - 1));
else {
Object[] newElements = new Object[len - 1];
System.arraycopy(elements, 0, newElements, 0, index);
System.arraycopy(elements, index + 1, newElements, index,
numMoved);
setArray(newElements);
}
return oldValue;
} finally {
lock.unlock();
}
}
/**
* 返回的迭代器提供构造迭代器时list状态的快照。遍历迭代器时不需要同步。 迭代器不支持remove方法。
*/
public Iterator<E> iterator() {
return new COWIterator<E>(getArray(), 0);
}
}
从对以往CopyOnWriteArrayList使用,我们可以总结使用不可变对象模式需要注意的地方:
1、当变更操作比较频繁时,会在状态变化时不断创建替换新的不可变对象,这会加重GC的负担和系统开销,应该谨慎使用。
2、CopyOnWriteArrayList中array的元素是可以被替换的,访问其中的元素需要避免外部代码修改其状态,这里的迭代器不支持remove方法。类似的情况,如我们返回HashMap类型的对象时,需要做好防御性复制:
Collections.unmodifiableMap(deepCopy(map))
欢迎扫码关注公众号java达人:
drjava