美文网首页
多线程设计模式解读—Immutable Object(不可变对象

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

作者: 九九派 | 来源:发表于2018-09-02 17:30 被阅读36次

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

    相关文章

      网友评论

          本文标题:多线程设计模式解读—Immutable Object(不可变对象

          本文链接:https://www.haomeiwen.com/subject/iiafiftx.html