zoukankan      html  css  js  c++  java
  • JUC之集合中的线程安全问题

    集合线程安全问题

    JDK Version:9

    首先说下集合线程安全是什么:当多个线程对同一个集合进行添加和查询的时候,出现异常错误。

    复现例子:

    package com.JUC;
    
    import java.util.ArrayList;
    import java.util.List;
    import java.util.UUID;
    
    public class ListSecutity04 {
        public static void main(String[] args) {
            List<String> list = 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();
            }
        }
    }
    

    效果图:

    image-20211231201048683

    可以看到报ConcurrentModificationException并发修改异常;出现该错误的问题是,在ArrayList中的add方法没有加锁。

    查看其源码:

    public boolean add(E e) {
        modCount++;
        add(e, elementData, size);
        return true;
    }
    //----------------------------------------
    public void add(int index, E element) {
        rangeCheckForAdd(index);
        modCount++;
        final int s;
        Object[] elementData;
        if ((s = size) == (elementData = this.elementData).length)
            elementData = grow();
        System.arraycopy(elementData, index,
                         elementData, index + 1,
                         s - index);
        elementData[index] = element;
        size = s + 1;
    }
    //----------------------------------------
    private void add(E e, Object[] elementData, int s) {
        if (s == elementData.length)
            elementData = grow();
        elementData[s] = e;
        size = s + 1;
    }
    

    解决方式-:Vector和Conllections

    见名知意,标题的两种方法都是比较古老的方法,使用Vector是因为其add方法中加的sychronized关键字修饰的

    public synchronized boolean add(E e) {
        modCount++;
        add(e, elementData, elementCount);
        return true;
    }
    

    另一种方法是在Collection中的工具类synchronizedCollection(Collection<T> c)返回指定 collection 支持的同步(线程安全的)collection。

    反复执行测试,代码通过:

    package com.JUC;
    
    import java.util.*;
    
    public class ListSecutity04 {
        public static void main(String[] args) {
    
    //        List<String> list = new ArrayList<>();
    //        List<String> list = new Vector<>();
            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

    写时复制技术:

    List<String> list = new CopyOnWriteArrayList<>();
    

    其思想:

    在多线程的情况下,当对集合进行写的操作的时候,系统先将原来的内容(A)复制一份为(B),原来的内容(A)可以进行并发读,复制后的内容(B)写入新的内容,当内容写入完成后,A与B实现覆盖或者说合并。

    其中数组的定义我们使用的是volatile定义的,这样,每个线程可以实时的观察到数组的变化。

    private transient volatile Object[] array;
    final void setArray(Object[] a) {
        array = a;
    }
    

    add()方法源码:对lock对象加了锁

    final transient Object lock = new Object();
    
    public boolean add(E e) {
        synchronized (lock) {
            Object[] elements = getArray();  //获取原内容
            int len = elements.length;  
            Object[] newElements = Arrays.copyOf(elements, len + 1); //数组复制
            newElements[len] = e;  //添加新的内容
            setArray(newElements);  //覆盖原数组
            return true;
        }
    }
    

    CopyOnWriteArrayList最大特点,读写分离,最终一致。比synchronized悲观锁性能较好。缺点就是,复制需要占用内存,可能出现OOM的情况。

    与之类似线程不安全的集合有:HashMap,HashSet,其解决方法类似,在JUC中都有对应,

    image-20220101202732087

    我们也可以进行代码的编写,并进入源码简单分析一下:

    HashSet--CopyOnWriteArraySet

    首先查看HashSet中的add方法的源码:(线程不安全)

    public boolean add(E e) {
        return map.put(e, PRESENT)==null;
    }
    //---------map中
    public V put(K key, V value) {
        return putVal(hash(key), key, value, false, true);
    }
    

    从中可以看到,set集合底层使用的是map集合中的put方法,进入其方法中查看,是没有同步相关的代码修饰的。

    也可以看出来Set集合是无序且不重复的,set中传入的E最后被传入到map中作为key。

    然后查看下CopyOnWriteArraySet中add的源码:(线程安全)

    public boolean add(E e) {
        return al.addIfAbsent(e);  //CopyOnWriteArrayList<E> al = new CopyOnWriteArrayList<E>()
    }
    //-------------addIfAbsent---------------
    public boolean addIfAbsent(E e) {
        Object[] snapshot = getArray();
        return indexOf(e, snapshot, 0, snapshot.length) >= 0 ? false :
        addIfAbsent(e, snapshot);
    }
    //---------------------addIfAbsent------------------------
    private boolean addIfAbsent(E e, Object[] snapshot) {
        synchronized (lock) {
            Object[] current = getArray();
            int len = current.length;
            if (snapshot != current) {
                // Optimize for lost race to another addXXX operation
                int common = Math.min(snapshot.length, len);
                for (int i = 0; i < common; i++)
                    if (current[i] != snapshot[i]
                        && Objects.equals(e, current[i]))
                        return false;
                if (indexOf(e, current, common, len) >= 0)
                    return false;
            }
            Object[] newElements = Arrays.copyOf(current, len + 1);
            newElements[len] = e;
            setArray(newElements);
            return true;
        }
    

    从源码可以看到,set使用的是CopyOnWriteArrayList,最终添加的数据在addIfAbsent方法中进行了同步。

    HashMap--ConcurrentHashMap

    HashMap解决的方式就是ConcurrentHashMap,其源码中采用的也是synchronized修饰的,具体的添加的流程,代码没读懂,暂且不表。
    欢迎朋友分享下该部分的优质博客。

  • 相关阅读:
    准备活动
    几个很好的.Net开源框架
    windows 进程通信(使用DDE)(转)
    mysql error 1046 1064 1264 (ERROR大全)
    在版本库里建立版本
    20120206系统日志
    Cocos2dx项目从VS移植到Xcode中的配置
    用python解析JSON
    win10下vc++6.0的安装问题
    Python爬虫(一)抓取指定的页面
  • 原文地址:https://www.cnblogs.com/xbhog/p/15757278.html
Copyright © 2011-2022 走看看