Java 集合框架(List/Set/Map)的线程安全问题

Java 集合框架的线程安全问题

List线程安全问题

    1、先来一段代码

    @Test
    public void test() {
        List<Integer> ints = Arrays.asList(1, 2, 3, 4);
        ints.forEach(System.out::println);
    }

    上述代码是不存在线程安全问题的,但是在多线程环境中,List下的ArrayList、LinkedList,会出现线程安全问题。例如:

    public static void main(String[] args) {
        List<String> strList = new ArrayList<>();
        for (int i = 0; i < 3; i++) {
            new Thread(() -> {
                strList.add(UUID.randomUUID().toString().substring(0, 8));
                System.out.println(strList);
            }, String.valueOf(i)).start();
        }
    }

    运行结果出现安全问题
在这里插入图片描述
    当线程数增加到30出现异常

java.util.ConcurrentModificationException
	at java.util.ArrayList$Itr.checkForComodification(ArrayList.java:909)
	at java.util.ArrayList$Itr.next(ArrayList.java:859)
	at java.util.AbstractCollection.toString(AbstractCollection.java:461)
	at java.lang.String.valueOf(String.java:2994)
	at java.io.PrintStream.println(PrintStream.java:821)
	at com.example.juc.UnSafeDemo.lambda$main$0(UnSafeDemo.java:30)
	at java.lang.Thread.run(Thread.java:748)
public class UnSafeDemo {
    // 1、异常现象 java.util.ConcurrentModificationException
    // 2、导致原因
    //   多线程并发争抢同一个资源类,而且没有加锁
    // 3、解决方案
    //   重写ArrayList,自己加锁,是不可行的方案。
    //   List<String> strList = new Vector<>(); // 数据一致性可以保证但是访问效率下降
    //   List<String> strList = Collections.synchronizedList(new ArrayList<>());
    //   List<String> strList = new CopyOnWriteArrayList<>();
    // 4、优化建议
    //   高并发情况下可以使用JUC 下的包
    public static void main(String[] args) {
//        List<String> strList = new Vector<>(); // new ArrayList<>()
//        List<String> strList = Collections.synchronizedList(new ArrayList<>()); // new ArrayList<>()
        List<String> strList = new CopyOnWriteArrayList<>(); // new ArrayList<>()
        for (int i = 0; i < 30; i++) {
            new Thread(() -> {
                strList.add(UUID.randomUUID().toString().substring(0, 8));
                System.out.println(strList);
            }, String.valueOf(i)).start();
        }
    }

    CopyOnWriteArrayList :写时复制也可以称为读写分离,CopyOnWrite容器即写时复制容器,往一个容器中添加元素的时候,不是在当前容器的Object[]直接添加,而是先将当前数组进行copy,复制出新的 Object[] newElements然后在新的数组中添加元素到尾部,再将原有数组的引用指向新的数组,这样做的优势是该容器可以进行并发的读,不需要加锁。原因是当前容器不会添加任何元素,也是一种读写分离的思想。
     CopyOnWriteArrayList.add()方法源码解析。

    public boolean add(E e) {
    	// 获取一个可重入锁
        final ReentrantLock lock = this.lock;
        lock.lock();
        try {
            Object[] elements = getArray(); // 在新增之前先先复制一份
            int len = elements.length; // 获取当前数组的长度
            // 复制之前的数组且长度+1
            Object[] newElements = Arrays.copyOf(elements, len + 1);
            // 新增元素
            newElements[len] = e;
            // 将当前数组引用指向为新的数组
            setArray(newElements);
            return true;
        } finally {
            lock.unlock(); // 释放锁
        }
    }

Set线程不安全

    与CopyOnWriteArrayList 类似,JUC也提供了一个CopyOnWriteArraySet类,与HashSet不同的是它的底层也是数组,新增时会判断元素是否已经存在。

    public CopyOnWriteArraySet() {
        al = new CopyOnWriteArrayList<E>();
    }

    代码

    public static void hashSetNotSafe() {
//        Set<String> strSet = new HashSet<>();
//        Set<String> strSet = Collections.synchronizedSet(new HashSet<>());
        Set<String> strSet = new CopyOnWriteArraySet<>();
        for (int i = 0; i < 30; i++) {
            new Thread(() -> {
                strSet.add(UUID.randomUUID().toString().substring(0, 8));
                System.out.println(strSet);
            }).start();
        }
    }

    CopyOnWriteArraySet 新增元素逻辑源码分析 这里是调用的CopyOnWriteArrayList中的方法。

    public boolean add(E e) {
        return al.addIfAbsent(e);
    }

    public boolean addIfAbsent(E e) {
        Object[] snapshot = getArray();
        // 查询是否存在 存在返回false 不存在准备新增
        return indexOf(e, snapshot, 0, snapshot.length) >= 0 ? false :
            addIfAbsent(e, snapshot);
    }

    private static int indexOf(Object o, Object[] elements,
                               int index, int fence) {
        if (o == null) { // 这个Set 允许存入一个 null
            for (int i = index; i < fence; i++)
                if (elements[i] == null)
                    return i;
        } else {
            for (int i = index; i < fence; i++)
            	// 元素最好重写equals方法,根据我们需求判断是否是同一个值
                if (o.equals(elements[i]))
                    return i;
        }
        return -1;
    }

    private boolean addIfAbsent(E e, Object[] snapshot) {
        final ReentrantLock lock = this.lock; // 获取锁
        lock.lock(); // 加锁
        try {
            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] && eq(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;
        } finally {
            lock.unlock(); // 释放锁
        }
    }

Map线程安全问题

    public static void hashMapNotSafe() {
//        Map<String, String> map = new HashMap<>(); // 多线程可能会出现线程安全问题
//        Map<String, String> map = new Hashtable<>(); // 可解决线程安全问题,但是处处加锁 读写效率较低
//        Map<String, String> map = Collections.synchronizedMap(new HashMap<>()); // 利用Collections.synchronizedMap()方法,将HashMap转为线程安全
        Map<String, String> map = new ConcurrentHashMap<>(); // JUC 提供的ConcurrentHashMap类,可解决线程安全问题
        for (int i = 0; i < 30; i++) {
            new Thread(() -> {
                map.put(Thread.currentThread().getName(), UUID.randomUUID().toString().substring(0, 8));
                System.out.println(map);
            }).start();
        }
    }

    ConcurrentHashMap提供的put方法解析。CAS + synchronized 保证线程安全

/** Implementation for put and putIfAbsent */
    final V putVal(K key, V value, boolean onlyIfAbsent) {
    	// ConcurrentHashMap 不允许key或value为null
        if (key == null || value == null) throw new NullPointerException();
        int hash = spread(key.hashCode()); // 获取hash值
        int binCount = 0;
        for (Node<K,V>[] tab = table;;) { // 多线程环境,一直循环,
            Node<K,V> f; int n, i, fh;
            if (tab == null || (n = tab.length) == 0)
            	//在 new ConcurrentHashMap时,并不直接初始化长度,而是在第一次put时初始化,达到懒加载的目的
                tab = initTable(); 
            else if ((f = tabAt(tab, i = (n - 1) & hash)) == null) {
            	// Node<K,V>[] 数组不为空,且当前位置上没有数据 将会以CAS的方式新增元素
                if (casTabAt(tab, i, null,
                             new Node<K,V>(hash, key, value, null)))
                    break;                   // no lock when adding to empty bin
            }
            else if ((fh = f.hash) == MOVED)
            	//该节点的hash值为Moved,说明当前节点是ForwardingNode,意味着有其他线程
            	//在进行扩容,则一起进行扩容操作
                tab = helpTransfer(tab, f);
            else {
                V oldVal = null;
                //加锁同步,针对首个节点进行加锁操作
                synchronized (f) {
                    if (tabAt(tab, i) == f) { //  找到table表下标为i的节点
                        if (fh >= 0) { // 正常节点
                            binCount = 1;
                            for (Node<K,V> e = f;; ++binCount) {// 无线循环 相当于自旋
                                K ek;
                                if (e.hash == hash &&
                                    ((ek = e.key) == key ||
                                     (ek != null && key.equals(ek)))) {
                                    oldVal = e.val;
                                    if (!onlyIfAbsent)
                                        e.val = value;
                                    break;
                                }
                                Node<K,V> pred = e;
                                if ((e = e.next) == null) { // 遍历至最后一个节点
                                    pred.next = new Node<K,V>(hash, key,
                                                              value, null); // 尾插法插入一个新节点
                                    break;
                                }
                            }
                        }
                        else if (f instanceof TreeBin) { // 判断节点类型是否是红黑树类型
                            Node<K,V> p;
                            binCount = 2;
                            if ((p = ((TreeBin<K,V>)f).putTreeVal(hash, key,
                                                           value)) != null) {
                                oldVal = p.val;
                                if (!onlyIfAbsent)
                                    p.val = value;
                            }
                        }
                    }
                }
                if (binCount != 0) {
                    if (binCount >= TREEIFY_THRESHOLD)
                        treeifyBin(tab, i);
                    if (oldVal != null)
                        return oldVal;
                    break;
                }
            }
        }
        addCount(1L, binCount); // 增加binCount容量,检查当前容量是否需要进行扩容
        return null;
    }

    快乐小石头分享的 ConcurrentHashMap实现原理及源码分析
    从ConcurrentHashMap的源码中我们可以学到很多并发控制的优秀思想。
    (1)当前线程与其它线程发生竞争修改资源类时,可以循环CAS修改直到成功。
    (2)当其他线程在修改,当前线程可以修改也可以不修改时,可以帮助其他线程修改,也可以让出CPU,循环等待。这样可以减少很多线程环境切换。

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值