您的当前位置:首页正文

多线程设计模式解读—Immutable Object(不可变对象

2022-06-15 来源:知库网

多线程设计模式解读—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
显示全文