• Day12 尚硅谷JUC——集合的线程安全


    我是大白(●—●),这是我开始学习记录大白Java软件攻城狮晋升之路的第十二天。今天学习的是【尚硅谷】大厂必备技术之JUC并发编程

    一、List集合线程不安全演示

    这里以ArrayList集合为例,首先看ArrayList的add()方法,底层没有synchronized修饰,因此会导致线程不安全的问题。

        /**
         * Appends the specified element to the end of this list.
         *
         * @param e element to be appended to this list
         * @return true (as specified by {@link Collection#add})
         */
        public boolean add(E e) {
            ensureCapacityInternal(size + 1);  // Increments modCount!!
            elementData[size++] = e;
            return true;
        }
    

    例子:

    public class ArrayListDemo {
        public static void main(String[] args) {
            //创建ArrayList集合
            ArrayList<String> list = new ArrayList<>();
    
            for (int i = 0; i < 30; i++) {
                new Thread(() ->{
                    //向集合添加内容
                    list.add(UUID.randomUUID().toString().substring(0,8));
                    //从集合获取内容
                    System.out.println(list);
                }, String.valueOf(i)).start();
            }
        }
    }
    

    运行之后的结果将会看到抛出并发修改异常ConcurrentModificationException
    image.png

    原因分析

    由控制台输出可以知道在打印list的数据时,出现的异常。
    ArrayList继承AbstractList并继承AbstractCollection,因此继承其toString()方法,由此可以看出问题是出在了Iterator的next()方法中

    //  String conversion
    
    /**
     * Returns a string representation of this collection.  The string
     * representation consists of a list of the collection's elements in the
     * order they are returned by its iterator, enclosed in square brackets
     * ("[]").  Adjacent elements are separated by the characters
     * ", " (comma and space).  Elements are converted to strings as
     * by {@link String#valueOf(Object)}.
     *
     * @return a string representation of this collection
     */
    public String toString() {
        Iterator<E> it = iterator();
        if (! it.hasNext())
            return "[]";
    
        StringBuilder sb = new StringBuilder();
        sb.append('[');
        for (;;) {
            E e = it.next();
            sb.append(e == this ? "(this Collection)" : e);
            if (! it.hasNext())
                return sb.append(']').toString();
            sb.append(',').append(' ');
        }
    }
    

    继续分析ArrayList中实现的next()方法。会发现有一个checkForComodification(),其作用是用来检查集合是否被修改过,如果修改过就抛出ConcurrentModificationException异常。
    另外也可以看到i如果大于等于集合数据的长度,也会抛出ConcurrentModificationException异常,这里的i是迭代器的cursor,表示下一个要访问的元素的索引。

    public E next() {
        checkForComodification();
        int i = cursor;
        if (i >= size)
            throw new NoSuchElementException();
        Object[] elementData = ArrayList.this.elementData;
        if (i >= elementData.length)
            throw new ConcurrentModificationException();
        cursor = i + 1;
        return (E) elementData[lastRet = i];
    }
    

    接下来不考虑第二种情况,看一下 checkForComodification() 方法。可以看到modCount和expectedModCount不一致的情况下就会抛出异常。其中expectedModCount表示对ArrayList修改次数的期望值,它的初始值为modCount。

    modCount 是在 ArrayList 中赋值的,并且初始值为 0,在 add 和 remove 的时候(修改元素的时候)会增加 1。

    final void checkForComodification() {
        if (modCount != expectedModCount)
            throw new ConcurrentModificationException();
    }
    
    public boolean add(E e) {
        ensureCapacityInternal(size + 1);  // Increments modCount!!
        elementData[size++] = e;
        return true;
    }
    
    private void ensureCapacityInternal(int minCapacity) {
        ensureExplicitCapacity(calculateCapacity(elementData, minCapacity));
    }
    
    private void ensureExplicitCapacity(int minCapacity) {
        modCount++; //add方法执行到这里会对modCount进行+1操作
    
        // overflow-conscious code
        if (minCapacity - elementData.length > 0)
            grow(minCapacity);
    }
    

    因此就可以得出结果,是当A线程执行add()方法后再进行输出时已经对expectedModCount 赋值为当前的modCount并且正准备执行checkForComodification()方法,另一个B线程此时也正在执行add()方法,并对modCount进行了加1操作,就导致了此时ArrayList对象的modCountexpectedModCount不相等并抛出异常。

    二、解决方案——Vector

    对于以前版本的JDK来说,对于集合的并发线程不安全的处理方式是使用Vector集合,他底层的add()方法和remove()方法都是有synchronized修饰。因此调用add方法的时候会造成线程阻塞,因此来解决并发不安全问题,但是并发效率较低,不推荐使用。

        public synchronized boolean add(E e) {
            modCount++;
            ensureCapacityHelper(elementCount + 1);
            elementData[elementCount++] = e;
            return true;
        }
    
        public synchronized boolean removeElement(Object obj) {
            modCount++;
            int i = indexOf(obj);
            if (i >= 0) {
                removeElementAt(i);
                return true;
            }
            return false;
        }
    

    例子:

    public class VectorDemo {
        public static void main(String[] args) {
            //创建ArrayList集合
            Vector<String> list = new Vector<>();
    
            for (int i = 0; i < 100; i++) {
                new Thread(() ->{
                    //向集合添加内容
                    list.add(UUID.randomUUID().toString().substring(0,8));
                    //从集合获取内容
                    System.out.println(list);
                }, String.valueOf(i)).start();
            }
        }
    }
    

    三、解决方案——Collentions

    Collentions是集合的工具类,它里面有一个_synchronizedList()_方法,其可以返回由指定列表支持的同步(线程安全的)列表来保证线程安全。

    public class ArrayListDemo {
        public static void main(String[] args) {
            List<String> list = Collections.synchronizedList(new ArrayList<>());
            for (int i = 0; i < 100; i++) {
                new Thread(() ->{
                    //向集合添加内容
                    list.add(UUID.randomUUID().toString().substring(0,8));
                    //从集合获取内容
                    System.out.println(list);
                }, String.valueOf(i)).start();
            }
        }
    }
    

    四、解决方案——CopyOnWriteArrayList

    CopyOnWriteArrayList利用的是写时复制技术。
    image.png
    写时复制技术的核心思想是:如果有多个呼叫者(callers)同时要求相同资源,他们会共同取得相同的指标指向相同的资源,直到某个呼叫者(caller)尝试修改资源时,系统才会真正复制一个副本(private copy)给该呼叫者,以避免被修改的资源被直接察觉到,这过程对其他的呼叫只都是通透的(transparently)。此作法主要的优点是如果呼叫者并没有修改该资源,就不会有副本(private copy)被建立。
    这个过程对其他的调用者是透明的(transparently)。

    查看CopyOnWriteArrayList源码的add()方法,会发现首先进行上锁,然后copy数组,并在新数组中添加元素并合并数组。

        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();
            }
        }
    

    五、HashSet线程不安全

    public class HashSetDemo {
        public static void main(String[] args) {
            //创建ArrayList集合
            Set<String> set = new HashSet<>();
    
            for (int i = 0; i < 30; i++) {
                new Thread(() ->{
                    //向集合添加内容
                    set.add(UUID.randomUUID().toString().substring(0,8));
                    //从集合获取内容
                    System.out.println(set);
                }, String.valueOf(i)).start();
            }
        }
    }
    

    同样的也会抛出ConcurrentModificationException异常

    解决方案

    类似ArrayList,针对HashSet的线程不安全的解决办法是使用CopyOnWriteArraySet集合。

    HashMap线程不安全

    由于HashSet的底层就是由HashMap实现的

    public class HashSet<E>
        extends AbstractSet<E>
        implements Set<E>, Cloneable, java.io.Serializable
    {
        static final long serialVersionUID = -5024744406713321676L;
    
        private transient HashMap<E,Object> map;
    
        ...
    }
    

    因此使用HashMap时也是会出现ConcurrentModificationException异常

    解决方案(ConcurrentHashMap)

    public class HashMapDemo {
        public static void main(String[] args) {
            //创建ArrayList集合
            Map<String,String> map = new HashMap<>();
    
            for (int i = 0; i < 30; i++) {
                new Thread(() ->{
                    //向集合添加内容
                    map.put(UUID.randomUUID().toString().substring(0,8));
                    //从集合获取内容
                    System.out.println(map);
                }, String.valueOf(i)).start();
            }
        }
    }
    
  • 相关阅读:
    浅谈C++|类的继承篇
    Node.js 的 CommonJS & ECMAScript 标准用法
    数据结构 - 2(顺序表10000字详解)
    Java疫苗预约小程序线上疫苗预约系统
    2022年全国部分省市跨境电商交易规模汇总
    简谈FPGA设计中系统运行频率计算方法与组合逻辑的层级
    【Mac】Lightroom Classic 2024(LrC 2024中文版) v13.1安装教程
    第7章 面向对象基础-下(内部类)
    3D 打印机 G 代码命令:完整列表和教程
    Python之使用finally代码块释放资源
  • 原文地址:https://blog.csdn.net/qq2632246528/article/details/126963236