线程安全的集合

简介: 日常coding,我们是不是经常用到ArrayList、HashSet、HashMap这样的集合?那你知不知道这些集合在多线程中是不安全的?

线程安全的集合

WangScaler: 一个用心创作的作者。

声明:才疏学浅,如有错误,恳请指正。

不安全的集合

日常coding,我们是不是经常用到ArrayList、HashSet、HashMap这样的集合?那你知不知道这些集合在多线程中是不安全的,举个例子。

package com.wangscaler.securecollection;
​
import java.util.*;
​
/**
 * @author WangScaler
 * @date 2021/8/5 15:25
 */
​
public class NotSafeCollection {
    public static void main(String[] args) {
​
        List<Integer> list = new ArrayList<>();
        int number = 3;
        Random random = new Random();
        for (int i = 0; i < number; i++) {
            new Thread(() -> {
                int randomNumber = random.nextInt(10) % 11;
                list.add(randomNumber);
            }, String.valueOf(i)).start();
        }
        while (Thread.activeCount() > 2) {
            Thread.yield();
        }
        list.forEach(System.out::println);
    }
}

你的本意可能是起三个线程去填充这个ArrayList集合,然而结果却总是超出意料,上述的代码执行的结果可能是null;null;3也可能是null;2;3,当然也有可能达到你的预期效果1,2,3。

为什么会出现这种情况呢?我们翻开源码

public boolean add(E e) {
    ensureCapacityInternal(size + 1);  // Increments modCount!!
    elementData[size++] = e;
    return true;
}

但是三行代码的执行的时候字节码如下

 0 aload_0
 1 aload_0
 2 getfield #284 <java/util/ArrayList.size : I>
 5 iconst_1
 6 iadd
 7 invokespecial #309 <java/util/ArrayList.ensureCapacityInternal : (I)V>
10 aload_0
11 getfield #287 <java/util/ArrayList.elementData : [Ljava/lang/Object;>
14 aload_0
15 dup
16 getfield #284 <java/util/ArrayList.size : I>
19 dup_x1
20 iconst_1
21 iadd
22 putfield #284 <java/util/ArrayList.size : I>
25 aload_1
26 aastore
27 iconst_1
28 ireturn

在程序执行的时候,线程是交替执行的。我们上面的例子有三个线程,分别是线程1、线程2、线程3。

看字节码 putfield #284 <java/util/ArrayList.size : I>是在aastore之前的,也就是说size写回主内存是在数组写回之前的。所以就有可能出现线程1数组还没写进去的时候,线程2就开始执行了,此时size已经是加一之后的了,所以此时线程2将新值保存到数组里,也就出现了null;2;3的情况。

HashMap也是线程不安全的,同样HashSet也是,因为HashSet的底层就是HashMap,话不多说上源码

public HashSet() {
    map = new HashMap<>();
}

那HashMap和HashSet的区别是啥呢?我们知道HashMap是键值对的形式,而HashSet的value值是固定的,源码如下。

private static final Object PRESENT = new Object();
public boolean add(E e) {
    return map.put(e, PRESENT)==null;
}

HashMap除了线程不安全,在jdk8之前HashMap扩容的时候还会产生死链的情况,

如何解决?

1、遗留的安全集合

  • Vector用于ArrayList
  • HashTable用于HashMap

    举例如下:将上述的List<Integer> list = new ArrayList<>();修改为List<Integer> list = new Vector<>(); 即可。为什么这个就可以解决问题呢?打开源码

    public synchronized boolean add(E e) {
        modCount++;
        ensureCapacityHelper(elementCount + 1);
        elementData[elementCount++] = e;
        return true;
    }

    是个同步方法,通过互斥锁使问题得到解决,但是在多线程中极大的影响效率,已经被弃用了。HashTable同样因为同步的问题,被弃用了。

2、Collections

通过Collections的修饰将不安全的集合变成安全的集合。

  • synchronizedList用于ArrayList
  • synchronizedMap用于HashMap
  • synchronizedSet用于HashSet

在原有不安全集合上包装了一个线程安全的类,来达到预期的效果。举例如下:将上述的List<Integer> list = new ArrayList<>();修改为List<Integer> list = Collections.synchronizedList(new ArrayList<>());即可解决。原理是什么呢?还是看源码

SynchronizedList(List<E> list) {
    super(list);
    this.list = list;
}
 public E get(int index) {
            synchronized (mutex) {return list.get(index);}
        }
        public E set(int index, E element) {
            synchronized (mutex) {return list.set(index, element);}
        }
        public void add(int index, E element) {
            synchronized (mutex) {list.add(index, element);}
        }
        public E remove(int index) {
            synchronized (mutex) {return list.remove(index);}
        }

在所有的方法上加了synchronized修饰,从而达到同步的效果。

3、JUC

  • Bloacking

    • ArrayBlockingQueue
    • LinkedBlockingQueue
    • LinkedBlockingDeque
    • ...
  • CopyOnWrite

    • CopyOnWriteArrayList对应ArrayList
    • CopyOnWriteArraySet用于HashSet,底层还是CopyOnWriteArrayList
  • Concurrent(推荐使用,弱一致性。)

    • ConcurrentHashMap用于HashMap

      只能保证一个操作是原子的,比如先检查key在不在(get),不在再添加(put)两个操作无法保证原子性,应该使用computeIfAbsent()

    • ConcurrentSkipListMap
    • ConcurrentSkipListSet
    • ...

举例如下:将上述的List<Integer> list = new ArrayList<>();修改为List<Integer> list = new CopyOnWriteArrayList<>();

适合读多写少的场景。看下源码

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

在写入的时候加锁,并复制一份,将新加入的写进新数组,最终把新数组写回原资源,从而保证数据的原子性。这个过程中只是给增加方法加锁,不影响读的操作。

结语

多线程中同步方法大大影响了工作效率,所以ConcurrentHashMap通过volatile结合自旋锁的方式,广受大家喜爱,同样也是面试官常问的题目之一,值得大家好好去读一下源码。

目录
相关文章
|
3月前
|
存储 安全 Java
【Java集合类面试二十五】、有哪些线程安全的List?
线程安全的List包括Vector、Collections.SynchronizedList和CopyOnWriteArrayList,其中CopyOnWriteArrayList通过复制底层数组实现写操作,提供了最优的线程安全性能。
|
8天前
|
安全 Java
Java多线程集合类
本文介绍了Java中线程安全的问题及解决方案。通过示例代码展示了使用`CopyOnWriteArrayList`、`CopyOnWriteArraySet`和`ConcurrentHashMap`来解决多线程环境下集合操作的线程安全问题。这些类通过不同的机制确保了线程安全,提高了并发性能。
|
5月前
|
安全 Java
java线程之List集合并发安全问题及解决方案
java线程之List集合并发安全问题及解决方案
909 1
|
6月前
|
安全 Java 开发者
【JAVA】哪些集合类是线程安全的
【JAVA】哪些集合类是线程安全的
|
3月前
|
安全 Java
【Java集合类面试十三】、HashMap如何实现线程安全?
实现HashMap线程安全的方法包括使用Hashtable类、ConcurrentHashMap,或通过Collections工具类将HashMap包装成线程安全的Map。
|
3月前
|
Java
【Java集合类面试十二】、HashMap为什么线程不安全?
HashMap在并发环境下执行put操作可能导致循环链表的形成,进而引起死循环,因而它是线程不安全的。
|
3月前
|
安全 算法 Java
【Java集合类面试二】、 Java中的容器,线程安全和线程不安全的分别有哪些?
这篇文章讨论了Java集合类的线程安全性,列举了线程不安全的集合类(如HashSet、ArrayList、HashMap)和线程安全的集合类(如Vector、Hashtable),同时介绍了Java 5之后提供的java.util.concurrent包中的高效并发集合类,如ConcurrentHashMap和CopyOnWriteArrayList。
【Java集合类面试二】、 Java中的容器,线程安全和线程不安全的分别有哪些?
|
3月前
|
安全 算法 Java
17 Java多线程(线程创建+线程状态+线程安全+死锁+线程池+Lock接口+线程安全集合)(下)
17 Java多线程(线程创建+线程状态+线程安全+死锁+线程池+Lock接口+线程安全集合)
77 6
|
3月前
|
存储 安全 Java
17 Java多线程(线程创建+线程状态+线程安全+死锁+线程池+Lock接口+线程安全集合)(中)
17 Java多线程(线程创建+线程状态+线程安全+死锁+线程池+Lock接口+线程安全集合)
86 5
|
3月前
|
存储 安全 Java
17 Java多线程(线程创建+线程状态+线程安全+死锁+线程池+Lock接口+线程安全集合)(上)
17 Java多线程(线程创建+线程状态+线程安全+死锁+线程池+Lock接口+线程安全集合)
84 3