java-并发-并发容器(3)

本文深入分析了ConcurrentHashMap和CopyOnWriteArrayList两种并发容器的工作原理,包括ConcurrentHashMap的线程安全实现及其在JDK 1.8中的改进,以及CopyOnWriteArrayList的写时复制机制,探讨了它们在不同应用场景下的优缺点。

同样注意内层的第一个for循环,里面有语句int c = segments[i].count; 但是c却从来没有被使用过,即使如此,编译器也不能做优化将这条语句去掉,因为存在对volatile变量count的读取,这条语句存在的唯一目的就是保证segments[i].modCount读取到几乎最新的值。关于containsValue方法的其它部分就不分析了,它和size方法差不多。

跨段方法中还有一个isEmpty()方法,其实现比size()方法还要简单,也不介绍了。最后简单地介绍下迭代方法,如keySet(), values(), entrySet()方法,这些方法都返回相应的迭代器,所有迭代器都继承于Hash_Iterator类(提交时居然提醒我不能包含sh It,只得加了下划线),里实现了主要的方法。其结构是:

abstract class Hash_Iterator{  
    int nextSegmentIndex;  
    int nextTableIndex;  
    HashEntry<K,V>[] currentTable;  
    HashEntry<K, V> nextEntry;  
    HashEntry<K, V> lastReturned;  
}  

nextSegmentIndex是段的索引,nextTableIndex是nextSegmentIndex对应段中中hash链的索引,currentTable是nextSegmentIndex对应段的table。调用next方法时主要是调用了advance方法:

final void advance() {  
    if (nextEntry != null && (nextEntry = nextEntry.next) != null)  
        return;  

    while (nextTableIndex >= 0) {  
        if ( (nextEntry = currentTable[nextTableIndex--]) != null)  
            return;  
    }  

    while (nextSegmentIndex >= 0) {  
        Segment<K,V> seg = segments[nextSegmentIndex--];  
        if (seg.count != 0) {  
            currentTable = seg.table;  
            for (int j = currentTable.length - 1; j >= 0; --j) {  
                if ( (nextEntry = currentTable[j]) != null) {  
                    nextTableIndex = j - 1;  
                    return;  
                }  
            }  
        }  
    }  
}  

不想再多介绍了,唯一需要注意的是跳到下一个段时,一定要先读取下一个段的count变量。

这种迭代方式的主要效果是不会抛出ConcurrentModificationException。一旦获取到下一个段的table,也就意味着这个段的头结点在迭代过程中就确定了,在迭代过程中就不能反映对这个段节点并发的删除和添加,对于节点的更新是能够反映的,因为节点的值是一个volatile变量。
结束语

ConcurrentHashMap是一个支持高并发的高性能的HashMap实现,它支持完全并发的读以及一定程度并发的写。ConcurrentHashMap的实现也是很精巧,充分利用了最新的JMM规范,值得学习,却不值得模仿。
1.8 的版本,与JDK6的版本有很大的差异。实现线程安全的思想也已经完全变了,它摒弃了Segment(锁段)的概念,而是启用了一种全新的方式实现,利用CAS算法。它沿用了与它同时期的HashMap版本的思想,底层依然由“数组”+链表+红黑树的方式思想,但是为了做到并发,又增加了很多辅助的类,例如TreeBin,Traverser等对象内部类。CAS算法实现无锁化的修改值的操作,他可以大大降低锁代理的性能消耗。这个算法的基本思想就是不断地去比较当前内存中的变量值与你指定的一个变量值是否相等,如果相等,则接受你指定的修改的值,否则拒绝你的操作。因为当前线程中的值已经不是最新的值,你的修改很可能会覆盖掉其他线程修改的结果。这一点与乐观锁,SVN的思想是比较类似的。

List类型的CopyOnWriteArrayList

对应的非并发容器:ArrayList
目标:代替Vector、synchronizedList
原理:利用高并发往往是读多写少的特性,对读操作不加锁,对写操作,先复制一份新的集合,在新的集合上面修改,然后将新集合赋值给旧的引用,并通过volatile 保证其可见性,当然写操作的锁是必不可少的了。
CopyOnWriteArrayList采用写入时复制的方式避开并发问题。这其实是通过冗余和不可变性来解决并发问题,在性能上会有比较大的代价,但如果写入的操作远远小于迭代和读操作,那么性能就差别不大了。
应用它们的场合通常在数组相对较小,并且遍历操作的数量大大超过可变操作的数量时,这种场合应用它们非常好。它们所有可变的操作都是先取得后台数组的副本,对副本进行更改,然后替换副本,这样可以保证永远不会抛出ConcurrentModificationException移除。
CopyOnWriteArrayList容器是Collections.synchronizedList(List list)的替代方案,CopyOnWriteArrayList在某些情况下具有更好的性能,考虑读远大于写的场景,如果把所有的读操作进行加锁,因为只有一个读线程能够获得锁,所以其他的读线程都必须等待,大大影响性能。CopyOnWriteArrayList称为“写时复制”容器,就是在多线程操作容器对象时,把容器复制一份,这样在线程内部的修改就与其他线程无关了,而且这样设计可以做到不阻塞其他的读线程。从JDK1.5开始Java并发包里提供了两个使用CopyOnWrite机制实现的并发容器,它们是CopyOnWriteArrayList和CopyOnWriteArraySet。
CopyOnWriteArrayList源码
先说说CopyOnWriteArrayList容器的实现原理:简单地说,就是在需要对容器进行操作的时候,将容器拷贝一份,对容器的修改等操作都在容器的拷贝中进行,当操作结束,再把容器容器的拷贝指向原来的容器。这样设计的好处是实现了读写分离,并且读读不会发生阻塞。下面的源码是CopyOnWriteArrayList的add方法实现:

public void add(int index, E element) {
        final ReentrantLock lock = this.lock;
        lock.lock();
        try {
            Object[] elements = getArray();
            int len = elements.length;
            if (index > len || index < 0)
                throw new IndexOutOfBoundsException("Index: "+index+
                                                    ", Size: "+len);
            Object[] newElements;
            int numMoved = len - index;
            //1、复制出一个新的数组
            if (numMoved == 0)
                newElements = Arrays.copyOf(elements, len + 1);
            else {
                newElements = new Object[len + 1];
                System.arraycopy(elements, 0, newElements, 0, index);
                System.arraycopy(elements, index, newElements, index + 1,
                                 numMoved);
            }
            //2、把新元素添加到新数组中
            newElements[index] = element;
            //3、把数组指向原来的数组
            setArray(newElements);
        } finally {
            lock.unlock();
        }
    }

上面的三个步骤实现了写时复制的思想,在读数据的时候不会锁住list,因为写操作是在原来容器的拷贝上进行的。而且,可以看到,如果对容器拷贝修改的过程中又有新的读线程进来,那么读到的还是旧的数据。读的代码如下

public E get(int index) {
        return get(getArray(), index);
    }

final Object[] getArray() {
       return array;
   }

CopyOnWrite并发容器用于读多写少的并发场景。比如白名单,黑名单,商品类目的访问和更新场景
从CopyOnWriteArrayList的实现原理可以看到因为在需要容器对象的时候进行拷贝,所以存在两个问题:内存占用问题和数据一致性问题。

内存占用问题

因为需要将原来的对象进行拷贝,这需要一定的开销。特别是当容器对象过大的时候,因为拷贝而占用的内存将增加一倍(原来驻留在内存的对象仍然在使用,拷贝之后就有两份对象在内存中,所以增加了一倍内存)。而且,在高并发的场景下,因为每个线程都拷贝一份对象在内存中,这种情况体现得更明显。由于JVM的优化机制,将会触发频繁的Young GC和Full GC,从而使整个系统的性能下降。

数据一致性问题

CopyOnWriteArrayList不能保证实时一致性,因为读线程在将引用重新指向原来的对象之前再次读到的数据是旧的,所以CopyOnWriteArrayList只能保证最终一致性。因此在需要实时一致性的厂几个CopyOnWriteArrayList是不能使用的
CopyOnWriteArrayList适用于读多写少的场景
在并发操作容器对象时不会抛出ConcurrentModificationException,并且返回的元素与迭代器创建时的元素是一致的
容器对象的复制需要一定的开销,如果对象占用内存过大,可能造成频繁的YoungGC和Full GC
CopyOnWriteArrayList不能保证数据实时一致性,只能保证最终一致性。
CopyOnWriteArrayList容器是Collections.synchronizedList(List list)的替代方案,CopyOnWriteArrayList在某些情况下具有更好的性能,考虑读远大于写的场景,如果把所有的读操作进行加锁,因为只有一个读线程能够获得锁,所以其他的读线程都必须等待,大大影响性能。CopyOnWriteArrayList称为“写时复制”容器,就是在多线程操作容器对象时,把容器复制一份,这样在线程内部的修改就与其他线程无关了,而且这样设计可以做到不阻塞其他的读线程。从JDK1.5开始Java并发包里提供了两个使用CopyOnWrite机制实现的并发容器,它们是CopyOnWriteArrayList和CopyOnWriteArraySet。
CopyOnWriteArrayList容器使用示例

import java.text.DateFormat;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.List;
import java.util.concurrent.CopyOnWriteArrayList;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.atomic.AtomicLong;

/**
 * Created by rhwayfun on 16-4-8.
 */
public class CopyOnWriteArrayListDdemo {

    /**
     * 内容编号
     */
    private static AtomicLong contentNum;

    /**
     * 日期格式器
     */
    private static DateFormat format;

    /**
     * 线程池
     */
    private final ExecutorService threadPool;

    public CopyOnWriteArrayListDdemo() {
        contentNum = new AtomicLong();
        format = new SimpleDateFormat("HH:mm:ss");
        threadPool = Executors.newFixedThreadPool(10);
    }

    public void doExec(int num) throws InterruptedException {
        List<String> list = new CopyOnWriteArrayList<>();
        for (int i = 0; i < num; i++){
            list.add(i,"main-content-" + i);
        }
        //5个写线程
        for (int i = 0; i < 5; i++){
            threadPool.execute(new Writer(list,i));
        }
        //启动10个读线程
        for (int i = 0; i < 10; i++){
            threadPool.execute(new Reader(list));
        }
        //关闭线程池
        threadPool.shutdown();
    }

    /**
     * 写线程
     *
     * @author rhwayfun
     */
    static class Writer implements Runnable {

        private final List<String> copyOnWriteArrayList;
        private int i;

        public Writer(List<String> copyOnWriteArrayList,int i) {
            this.copyOnWriteArrayList = copyOnWriteArrayList;
            this.i = i;
        }

        @Override
        public void run() {
            copyOnWriteArrayList.add(i,"content-" + contentNum.incrementAndGet());
            System.out.println(Thread.currentThread().getName() + ": write content-" + contentNum.get()
                    + " " +format.format(new Date()));
            System.out.println(Thread.currentThread().getName() + ": remove " + copyOnWriteArrayList.remove(i));
        }
    }

    static class Reader implements Runnable {

        private final List<String> list;

        public Reader(List<String> list) {
            this.list = list;
        }

        @Override
        public void run() {
            for (String s : list) {
                System.out.println(Thread.currentThread().getName() + ": read " + s
                        + " " +format.format(new Date()));
            }
        }
    }

    public static void main(String[] args) throws InterruptedException {
        CopyOnWriteArrayListDdemo demo = new CopyOnWriteArrayListDdemo();
        demo.doExec(5);
    }
}

首先启动5个写线程,再启动10个读线程,运行该程序发现并没有出现异常,所以使用写时复制容器效率很高。代码的运行结果如下:
先说说CopyOnWriteArrayList容器的实现原理:简单地说,就是在需要对容器进行操作的时候,将容器拷贝一份,对容器的修改等操作都在容器的拷贝中进行,当操作结束,再把容器容器的拷贝指向原来的容器。这样设计的好处是实现了读写分离,并且读读不会发生阻塞。下面的源码是CopyOnWriteArrayList的add方法实现:

public void add(int index, E element) {
        final ReentrantLock lock = this.lock;
        lock.lock();
        try {
            Object[] elements = getArray();
            int len = elements.length;
            if (index > len || index < 0)
                throw new IndexOutOfBoundsException("Index: "+index+
                                                    ", Size: "+len);
            Object[] newElements;
            int numMoved = len - index;
            //1、复制出一个新的数组
            if (numMoved == 0)
                newElements = Arrays.copyOf(elements, len + 1);
            else {
                newElements = new Object[len + 1];
                System.arraycopy(elements, 0, newElements, 0, index);
                System.arraycopy(elements, index, newElements, index + 1,
                                 numMoved);
            }
            //2、把新元素添加到新数组中
            newElements[index] = element;
            //3、把数组指向原来的数组
            setArray(newElements);
        } finally {
            lock.unlock();
        }
    }

上面的三个步骤实现了写时复制的思想,在读数据的时候不会锁住list,因为写操作是在原来容器的拷贝上进行的。而且,可以看到,如果对容器拷贝修改的过程中又有新的读线程进来,那么读到的还是旧的数据。读的代码如下:

public E get(int index) {
        return get(getArray(), index);
    }
    final Object[] getArray() {
        return array;
    }
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符  | 博主筛选后可见
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值