zoukankan      html  css  js  c++  java
  • fail-fast机制

    先了解一些词语

    volatile:volatile的本意是“易变的”。volatile关键字是一种类型修饰符,用它声明的类型变量表示可以被某些编译器未知的因素更改,比如:操作系统、硬件或者其它线程等。遇到这个关键字声明的变量,编译器对访问该变量的代码就不再进行优化,从而可以提供对特殊地址的稳定访问。当要求使用volatile 声明的变量的值的时候,系统总是重新从它所在的内存读取数据,即使它前面的指令刚刚从该处读取过数据。而且读取的数据立刻被保存。volatile 指出 i是随时可能发生变化的,每次使用它的时候必须从i的地址中读取。对于volatile类型的变量,系统每次用到他的时候都是直接从对应的内存当中提取,而不会利用cache当中的原有数值,以适应它的未知何时会发生的变化。

    fail-fast 机制是java集合(Collection)中的一种错误机制。当多个线程对同一个集合的内容进行操作时,就可能会产生fail-fast事件。
    例如:当某一个线程A通过iterator去遍历某集合的过程中,若该集合的内容被其他线程所改变了;那么线程A访问集合时,就会抛出ConcurrentModificationException异常,产生fail-fast事件。在详细介绍fail-fast机制的原理之前,先通过一个示例来认识fail-fast。

    import java.util.*;
    import java.util.concurrent.*;
    
    /*
     * @desc java集合中Fast-Fail的测试程序。
     *
     *   fast-fail事件产生的条件:当多个线程对Collection进行操作时,若其中某一个线程通过iterator去遍历集合时,该集合的内容被其他线程所改变;则会抛出ConcurrentModificationException异常。
     *   fast-fail解决办法:通过util.concurrent集合包下的相应类去处理,则不会产生fast-fail事件。
     *
     *   本例中,分别测试ArrayList和CopyOnWriteArrayList这两种情况。ArrayList会产生fast-fail事件,而CopyOnWriteArrayList不会产生fast-fail事件。
     *   (01) 使用ArrayList时,会产生fast-fail事件,抛出ConcurrentModificationException异常;定义如下:
     *            private static List<String> list = new ArrayList<String>();
     *   (02) 使用时CopyOnWriteArrayList,不会产生fast-fail事件;定义如下:
     *            private static List<String> list = new CopyOnWriteArrayList<String>();
     *
     * @author skywang
     */
    public class FastFailTest {
    
        private static List<String> list = new ArrayList<String>();
        //private static List<String> list = new CopyOnWriteArrayList<String>();
        public static void main(String[] args) {
        
            // 同时启动两个线程对list进行操作!
            new ThreadOne().start();
            new ThreadTwo().start();
        }
    
        private static void printAll() {
            System.out.println("");
    
            String value = null;
            Iterator iter = list.iterator();
            while(iter.hasNext()) {
                value = (String)iter.next();
                System.out.print(value+", ");
            }
        }
    
        /**
         * 向list中依次添加0,1,2,3,4,5,每添加一个数之后,就通过printAll()遍历整个list
         */
        private static class ThreadOne extends Thread {
            public void run() {
                int i = 0;
                while (i<6) {
                    list.add(String.valueOf(i));
                    printAll();
                    i++;
                }
            }
        }
    
        /**
         * 向list中依次添加10,11,12,13,14,15,每添加一个数之后,就通过printAll()遍历整个list
         */
        private static class ThreadTwo extends Thread {
            public void run() {
                int i = 10;
                while (i<16) {
                    list.add(String.valueOf(i));
                    printAll();
                    i++;
                }
            }
        }
    
    }
    View Code

    运行结果
    运行该代码,抛出异常java.util.ConcurrentModificationException!即,产生fail-fast事件!

    结果说明
    (01) FastFailTest中通过 new ThreadOne().start() 和 new ThreadTwo().start() 同时启动两个线程去操作list。
        ThreadOne线程:向list中依次添加0,1,2,3,4,5。每添加一个数之后,就通过printAll()遍历整个list。
        ThreadTwo线程:向list中依次添加10,11,12,13,14,15。每添加一个数之后,就通过printAll()遍历整个list。
    (02) 当某一个线程遍历list的过程中,list的内容被另外一个线程所改变了;就会抛出ConcurrentModificationException异常,产生fail-fast事件。

    fail-fast解决办法

    fail-fast机制,是一种错误检测机制。它只能被用来检测错误,因为JDK并不保证fail-fast机制一定会发生。若在多线程环境下使用fail-fast机制的集合,建议使用“java.util.concurrent包下的类”去取代“java.util包下的类”。
    所以,本例中只需要将ArrayList替换成java.util.concurrent包下对应的类即可。

    fail-fast原理

    产生fail-fast事件,是通过抛出ConcurrentModificationException异常来触发的。
    那么,ArrayList是如何抛出ConcurrentModificationException异常的呢?

    我们知道,ConcurrentModificationException是在操作Iterator时抛出的异常。我们先看看Iterator的源码。ArrayList的Iterator是在父类AbstractList.java中实现的。代码如下:

    package java.util;
    
    public abstract class AbstractList<E> extends AbstractCollection<E> implements List<E> {
    
        ...
    
        // AbstractList中唯一的属性
        // 用来记录List修改的次数:每修改一次(添加/删除等操作),将modCount+1
        protected transient int modCount = 0;
    
        // 返回List对应迭代器。实际上,是返回Itr对象。
        public Iterator<E> iterator() {
            return new Itr();
        }
    
        // Itr是Iterator(迭代器)的实现类
        private class Itr implements Iterator<E> {
            int cursor = 0;
    
            int lastRet = -1;
    
            // 修改数的记录值。
            // 每次新建Itr()对象时,都会保存新建该对象时对应的modCount;
            // 以后每次遍历List中的元素的时候,都会比较expectedModCount和modCount是否相等;
            // 若不相等,则抛出ConcurrentModificationException异常,产生fail-fast事件。
            int expectedModCount = modCount;
    
            public boolean hasNext() {
                return cursor != size();
            }
    
            public E next() {
                // 获取下一个元素之前,都会判断“新建Itr对象时保存的modCount”和“当前的modCount”是否相等;
                // 若不相等,则抛出ConcurrentModificationException异常,产生fail-fast事件。
                checkForComodification();
                try {
                    E next = get(cursor);
                    lastRet = cursor++;
                    return next;
                } catch (IndexOutOfBoundsException e) {
                    checkForComodification();
                    throw new NoSuchElementException();
                }
            }
    
            public void remove() {
                if (lastRet == -1)
                    throw new IllegalStateException();
                checkForComodification();
    
                try {
                    AbstractList.this.remove(lastRet);
                    if (lastRet < cursor)
                        cursor--;
                    lastRet = -1;
                    expectedModCount = modCount;
                } catch (IndexOutOfBoundsException e) {
                    throw new ConcurrentModificationException();
                }
            }
    
            final void checkForComodification() {
                if (modCount != expectedModCount)
                    throw new ConcurrentModificationException();
            }
        }
    
        ...
    }
    View Code

    从中,我们可以发现在调用 next() 和 remove()时,都会执行 checkForComodification()。若 “modCount 不等于 expectedModCount”,则抛出ConcurrentModificationException异常,产生fail-fast事件。

    要搞明白 fail-fast机制,我们就要需要理解什么时候“modCount 不等于 expectedModCount”!
    从Itr类中,我们知道 expectedModCount 在创建Itr对象时,被赋值为 modCount。通过Itr,我们知道:expectedModCount不可能被修改为不等于 modCount。所以,需要考证的就是modCount何时会被修改。

    接下来,我们查看ArrayList的源码,来看看modCount是如何被修改的。

    package java.util;
    
    public class ArrayList<E> extends AbstractList<E>
            implements List<E>, RandomAccess, Cloneable, java.io.Serializable
    {
    
        ...
    
        // list中容量变化时,对应的同步函数
        public void ensureCapacity(int minCapacity) {
            modCount++;
            int oldCapacity = elementData.length;
            if (minCapacity > oldCapacity) {
                Object oldData[] = elementData;
                int newCapacity = (oldCapacity * 3)/2 + 1;
                if (newCapacity < minCapacity)
                    newCapacity = minCapacity;
                // minCapacity is usually close to size, so this is a win:
                elementData = Arrays.copyOf(elementData, newCapacity);
            }
        }
    
    
        // 添加元素到队列最后
        public boolean add(E e) {
            // 修改modCount
            ensureCapacity(size + 1);  // Increments modCount!!
            elementData[size++] = e;
            return true;
        }
    
    
        // 添加元素到指定的位置
        public void add(int index, E element) {
            if (index > size || index < 0)
                throw new IndexOutOfBoundsException(
                "Index: "+index+", Size: "+size);
    
            // 修改modCount
            ensureCapacity(size+1);  // Increments modCount!!
            System.arraycopy(elementData, index, elementData, index + 1,
                 size - index);
            elementData[index] = element;
            size++;
        }
    
        // 添加集合
        public boolean addAll(Collection<? extends E> c) {
            Object[] a = c.toArray();
            int numNew = a.length;
            // 修改modCount
            ensureCapacity(size + numNew);  // Increments modCount
            System.arraycopy(a, 0, elementData, size, numNew);
            size += numNew;
            return numNew != 0;
        }
       
    
        // 删除指定位置的元素 
        public E remove(int index) {
            RangeCheck(index);
    
            // 修改modCount
            modCount++;
            E oldValue = (E) elementData[index];
    
            int numMoved = size - index - 1;
            if (numMoved > 0)
                System.arraycopy(elementData, index+1, elementData, index, numMoved);
            elementData[--size] = null; // Let gc do its work
    
            return oldValue;
        }
    
    
        // 快速删除指定位置的元素 
        private void fastRemove(int index) {
    
            // 修改modCount
            modCount++;
            int numMoved = size - index - 1;
            if (numMoved > 0)
                System.arraycopy(elementData, index+1, elementData, index,
                                 numMoved);
            elementData[--size] = null; // Let gc do its work
        }
    
        // 清空集合
        public void clear() {
            // 修改modCount
            modCount++;
    
            // Let gc do its work
            for (int i = 0; i < size; i++)
                elementData[i] = null;
    
            size = 0;
        }
    
        ...
    }
    View Code

    从中,我们发现:无论是add()、remove(),还是clear(),只要涉及到修改集合中的元素个数时,都会改变modCount的值。

    接下来,我们再系统的梳理一下fail-fast是怎么产生的。步骤如下:
    (01) 新建了一个ArrayList,名称为arrayList。
    (02) 向arrayList中添加内容。
    (03) 新建一个“线程a”,并在“线程a”中通过Iterator反复的读取arrayList的值
    (04) 新建一个“线程b”,在“线程b”中删除arrayList中的一个“节点A”。
    (05) 这时,就会产生有趣的事件了。
           在某一时刻,“线程a”创建了arrayList的Iterator。此时“节点A”仍然存在于arrayList中,创建arrayList时,expectedModCount = modCount(假设它们此时的值为N)。
           在“线程a”在遍历arrayList过程中的某一时刻,“线程b”执行了,并且“线程b”删除了arrayList中的“节点A”。“线程b”执行remove()进行删除操作时,在remove()中执行了“modCount++”,此时modCount变成了N+1
    “线程a”接着遍历,当它执行到next()函数时,调用checkForComodification()比较“expectedModCount”和“modCount”的大小;而“expectedModCount=N”,“modCount=N+1”,这样,便抛出ConcurrentModificationException异常,产生fail-fast事件。

    至此,我们就完全了解了fail-fast是如何产生的!
    即,当多个线程对同一个集合进行操作的时候,某线程访问集合的过程中,该集合的内容被其他线程所改变(即其它线程通过add、remove、clear等方法,改变了modCount的值);这时,就会抛出ConcurrentModificationException异常,产生fail-fast事件。

    上面,说明了“解决fail-fast机制的办法”,也知道了“fail-fast产生的根本原因”。接下来,聊聊并发-Java中的Copy-On-Write容器

    Copy-On-Write简称COW,是一种用于程序设计中的优化策略。其基本思路是,从一开始大家都在共享同一个内容,当某个人想要修改这个内容的时候,才会真正把内容Copy出去形成一个新的内容然后再改,这是一种延时懒惰策略。从JDK1.5开始Java并发包里提供了两个使用CopyOnWrite机制实现的并发容器,它们是CopyOnWriteArrayList和CopyOnWriteArraySet。CopyOnWrite容器非常有用,可以在非常多的并发场景中使用到。

    什么是CopyOnWrite容器

    CopyOnWrite容器即写时复制的容器。通俗的理解是当我们往一个容器添加元素的时候,不直接往当前容器添加,而是先将当前容器进行Copy,复制出一个新的容器,然后新的容器里添加元素,添加完元素之后,再将原容器的引用指向新的容器。这样做的好处是我们可以对CopyOnWrite容器进行并发的读,而不需要加锁,因为当前容器不会添加任何元素。所以CopyOnWrite容器也是一种读写分离的思想,读和写不同的容器。

    CopyOnWriteArrayList的实现原理

    在使用CopyOnWriteArrayList之前,我们先阅读其源码了解下它是如何实现的。以下代码是向ArrayList里添加元素,可以发现在添加的时候是需要加锁的,否则多线程写的时候会Copy出N个副本出来

    public boolean add(T 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();
    
        }
    
    }
    
    final void setArray(Object[] a) {
        array = a;
    }
    View Code

    读的时候不需要加锁,如果读的时候有多个线程正在向ArrayList添加数据,读还是会读到旧的数据,因为写的时候不会锁住旧的ArrayList。

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

    CopyOnWrite的应用场景

    CopyOnWrite并发容器用于读多写少的并发场景。比如白名单,黑名单,商品类目的访问和更新场景,假如我们有一个搜索网站,用户在这个网站的搜索框中,输入关键字搜索内容,但是某些关键字不允许被搜索。这些不能被搜索的关键字会被放在一个黑名单当中,黑名单每天晚上更新一次。当用户搜索时,会检查当前关键字在不在黑名单当中,如果在,则提示不能搜索。实现代码如下:

    package com.ifeve.book;
    
    import java.util.Map;
    
    import com.ifeve.book.forkjoin.CopyOnWriteMap;
    
    /**
     * 黑名单服务
     *
     * @author fangtengfei
     *
     */
    public class BlackListServiceImpl {
    
        private static CopyOnWriteMap<String, Boolean> blackListMap = new CopyOnWriteMap<String, Boolean>(
                1000);
    
        public static boolean isBlackList(String id) {
            return blackListMap.get(id) == null ? false : true;
        }
    
        public static void addBlackList(String id) {
            blackListMap.put(id, Boolean.TRUE);
        }
    
        /**
         * 批量添加黑名单
         *
         * @param ids
         */
        public static void addBlackList(Map<String,Boolean> ids) {
            blackListMap.putAll(ids);
        }
    
    }
    View Code

    代码很简单,但是使用CopyOnWriteMap需要注意两件事情:

    1. 减少扩容开销。根据实际需要,初始化CopyOnWriteMap的大小,避免写时CopyOnWriteMap扩容的开销。

    2. 使用批量添加。因为每次添加,容器每次都会进行复制,所以减少添加次数,可以减少容器的复制次数。如使用上面代码里的addBlackList方法。

    CopyOnWrite的缺点

    CopyOnWrite容器有很多优点,但是同时也存在两个问题,即内存占用问题和数据一致性问题。所以在开发的时候需要注意一下。

    内存占用问题。因为CopyOnWrite的写时复制机制,所以在进行写操作的时候,内存里会同时驻扎两个对象的内存,旧的对象和新写入的对象(注意:在复制的时候只是复制容器里的引用,只是在写的时候会创建新对象添加到新容器里,而旧容器的对象还在使用,所以有两份对象内存)。如果这些对象占用的内存比较大,比如说200M左右,那么再写入100M数据进去,内存就会占用300M,那么这个时候很有可能造成频繁的Yong GC和Full GC。之前我们系统中使用了一个服务由于每晚使用CopyOnWrite机制更新大对象,造成了每晚15秒的Full GC,应用响应时间也随之变长。频繁的GC是因为修改CopyOnWriteArrayList里大量的元素造成的。两份对象内存是指修改前和修改后两个元素内存。

    针对内存占用问题,可以通过压缩容器中的元素的方法来减少大对象的内存消耗,比如,如果元素全是10进制的数字,可以考虑把它压缩成36进制或64进制。或者不使用CopyOnWrite容器,而使用其他的并发容器,如ConcurrentHashMap

    数据一致性问题。CopyOnWrite容器只能保证数据的最终一致性,不能保证数据的实时一致性。所以如果你希望写入的的数据,马上能读到,请不要使用CopyOnWrite容器。

  • 相关阅读:
    Leetcode 121. Best Time to Buy and Sell Stock
    Leetcode 120. Triangle
    Leetcode 26. Remove Duplicates from Sorted Array
    Leetcode 767. Reorganize String
    Leetcode 6. ZigZag Conversion
    KMP HDU 1686 Oulipo
    多重背包 HDU 2844 Coins
    Line belt 三分嵌套
    三分板子 zoj 3203
    二分板子 poj 3122 pie
  • 原文地址:https://www.cnblogs.com/ccgjava/p/6347425.html
Copyright © 2011-2022 走看看