java 数据结构与并发之 --- List (并发篇)

         ArrayList , Vector 和 LinkedList继承至AbstractList, Collections中的UnmodifiableList 和 SynchronizedList都继承至List。他们都实现了List的接口里的功能,但是在并发编程的时候,各自的表现是不一样的。ArrayList 和 LinkedList的所有方法都没有进行进行同步或者加锁,所以在多线程的场合,多个线程共同使用这些类的实例,在有修改的情况下是不安全的,而且有可能导致抛出异常。局部方法中使用是没有问题的,即使在并发环境下,局部方法调用只在方法内的周期内有效的对象。 

        关于多线程下的安全,Vector ,Collections.UnmodifiableList 和  Collections.SynchronizedList 分别使用不同的方式实现了多线程下的安全的考虑。Collections.UnmodifiableList 为非可变List,即它把传入的List 进行了包装,对所有的查询方法进行直接使用内部List方法,针对List会修改内部结构的接口都抛出异常,使用这种方式来告诉使用者不能调用本List的有修改内部结构的方法,从而对只需要查询而防止修改的情况下的并发有很好的效果。Vector 内的所有公有方法都使用synchronized关键字,确保这些方法在被调用的时候都是串行的,所以在并发环境下逻辑不会出现错乱等情况。 Collections.SynchronizedList 使用传递过来的List在包装List方法的基础上,针对内部的一个Ojbect变量进行加锁,所有的方法内的逻辑都需要获得这个实例的锁才能进行继续。这两个类都实现了对并发环境下针对公开方法或者公开方法的逻辑进行加锁来实现了并发下的安全,但是要注意的是Collections.SynchronizedList传入的List如果暴露给了不同的线程进行修改,是无法保证串行的,仍然有安全风险,所以使用的时候要进行设计如何只保留包装过的SynchronizedList。由于是并发环境,有些特殊的环境只保证方法粒度的同步是无法保证业务的安全的,比如哪个对象是否存在然后针对,然后再判断是进行修改还是插入操作,这个逻辑里的两个方法虽然都是串行的,但是在并发场景下,两个方法之间可能插入了线程方法的逻辑,所以仍然需要针对特殊逻辑进行同步化处理。另外并发环境下 使用 for (int i=0; i<n; i++) 或者 for ... in 等方式遍历也是不安全的操作,因为很容易出现空指针等问题,而AbstractList里的实现的Iterator和ListIterator都会进行安全检查进行快速失败,Collections.SynchronizedList依赖于内部传入List的实现。所以遍历的情况都需要考虑针对 List对象加锁才能进行,而且for 方式的遍历不能针对数据进行删除,会导致空指针(非并发情况也一样)。如果不想加锁就是要遍历当时时刻的一个基本情况,可以考虑 使用 toArray方法,针对创建出的Array进行遍历数据情况。

        以上是几种List的实现的情况,而java 1.5 的并发包中加入了 CopyOnWriteArrayList ,它是为了提升在修改较少,查询较多的情况下,又要求保证数据并发安全的情况下的一种方案。 它的核心就是只针对所有修改操作进行加锁,并进行数据的copy,然后把新的数据赋予内部数据引用,内部数据 使用了 private volatile transient Object[] array;  其中 volatile 来保证赋值操作是多线程下可见的,原子的操作。CopyOnWriteArrayList之所以在特定环境下能极高提高性能,就是使用了非必要方法(如 读取方法)进行了无锁话,只针对需要对内部结构进行修改的方法进行了加锁。

        CopyOnWriteArrayList 的 get, size,contains, indexof 等获取或者判定方法都没有进行加锁处理,如以下contains方法:

public boolean contains(Object o) {
        Object[] elements = getArray();
        return indexOf(o, elements, 0, elements.length) >= 0;
    }

        其中代码里可 Object[] elements = getArray(); 基本上可以在每个方法中见到, 这样做的原因就是为了保证方法运行时获取到的数据是当时List当时的数据,并且在整个方法过程中保证数据是一致的,而不是变化的,这样也保证了程序的稳定和快速运行。而在对应的 set,add*, remove* 等方法中都对数据结构进行了加锁处理,如以下 remove方法:

 public E remove(int index) {
 final ReentrantLock lock = this.lock;
 lock.lock();
 try {
     Object[] elements = getArray();
     int len = elements.length;
     Object oldValue = 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 (E)oldValue;
 } finally {
     lock.unlock();
 }
    }

        锁使用 ReentrantLock进行加锁,更加轻量级,关于为什么使用 final ReentrantLock lock = this.lock; 这个为了代码的更加安全和极度的性能追求了,可见java源码中很多新代码的质量越来越高。从这段代码我们可以看出CopyOnWriteArrayList数据结构的基本思想,也就是在不修改原数据的情况下对产生一个新的数据,然后通过组装成需要的数据,再通过赋值操作(=)来完成新数据代替旧的数据。为什么这样就可以做到获取方法不需要加锁,这个道理非常简单,就是所有的获取方法通过Object[] elements = getArray();得到的是当时的数据结构,虽然getArray()并没有对数据进行copy(如果每次进行数据copy将是非常的不划算的设计),但是它把当时数据的引用也复制给了局部变量。而当时修改方法完成修改的时候,这时候修改数据的那些方式不是修改原来数据结构而是通过利用原数据结构产生新的数据结构并且组装数据,完成后把实例变量的引用指向新的结构。这样就保证了修改后的数据在并发的同时也不会污染到当时的获取数据的方法。

        我们知道性能上CopyOnWriteArrayList在某些特定场景下性能是得到了提升,我们继续分析 CopyOnWriteArrayList 在并发环境下和其他数据结构的异同,使用 for (int i=0; i<n; i++) 或者 for ... in 等方式遍历对CopyOnWriteArrayList仍然是不安全的遍历方式,因为它同样只能保证数据结构在一个调用方法时候的不变,不是一个逻辑流程多个方法下的不变。CopyOnWriteArrayList也使用了自己实现的Iterator和ListIterator:

 private static class COWIterator<E> implements ListIterator<E> {
        /** Snapshot of the array **/
        private final Object[] snapshot;
        /** Index of element to be returned by subsequent call to next.  */
        private int cursor;
        private COWIterator(Object[] elements, int initialCursor) {
            cursor = initialCursor;
            snapshot = elements;
        }
        public boolean hasNext() {
            return cursor < snapshot.length;
        }
        public boolean hasPrevious() {
            return cursor > 0;
        }
        public E next() {
     if (! hasNext())
                throw new NoSuchElementException();
     return (E) snapshot[cursor++];
        }
        public E previous() {
     if (! hasPrevious())
                throw new NoSuchElementException();
     return (E) snapshot[--cursor];
        }
        public int nextIndex() {
            return cursor;
        }
        public int previousIndex() {
            return cursor-1;
        }
        /**
         * Not supported. Always throws UnsupportedOperationException.
         * @throws UnsupportedOperationException always; <tt>remove</tt>
         *         is not supported by this iterator.
         */
        public void remove() {
            throw new UnsupportedOperationException();
        }
        /**
         * Not supported. Always throws UnsupportedOperationException.
         * @throws UnsupportedOperationException always; <tt>set</tt>
         *         is not supported by this iterator.
         */
        public void set(E e) {
            throw new UnsupportedOperationException();
        }
        /**
         * Not supported. Always throws UnsupportedOperationException.
         * @throws UnsupportedOperationException always; <tt>add</tt>
         *         is not supported by this iterator.
         */
        public void add(E e) {
            throw new UnsupportedOperationException();
        }
    }

        可以看出COWIterator提供了遍历的基本方法,但是却把针对可以修改的方法进行了异常抛出,因为COWIterator遍历的数据结构也是调用的那一时刻已经确定的数据结构,而如果可以调用修改数据结构的方法的话,必须保证当前的数据结构是List类本身的数据结构,而由于CopyOnWriteArrayList特殊实现,是不对此进行保证的,所以对这些修改数据结构的方法进行了抛出异常。那么对于CopyOnWriteArrayList来说遍历的最好方式仍然是Iterator和toArray获取数组进行操作。然后对于需要修改的场景如何解决,必要的时候仍然需要加锁处理,当然加锁又影响了性能,好在CopyOnWriteArrayList也提供了一些如 addIfAbsent, addAllAbsent等更具有组合条件的方法,提高原子操作能力。同时需要指出的是在并发数据结构中一些技巧需要更换思维方式,如判断后操作的方法是不安全的,先操作后判定也许是一种解决技巧(还要看具体场景)。同时需要再多线程运行的业务场景下多考虑其安全性以及如何更好的提升性能,将是需要详细思考的东西。

        


转载于:https://my.oschina.net/u/1993946/blog/299650

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值