【JUC并发编程05】集合的线程安全

5 集合的线程安全

5.1 集合线程不安全演示

在学习集合线程安全之前,先来看一下为什么在多线程中会出现不安全。

以 ArrayList 为例,我们进入 ArrayList 源码,找到 add() 方法,源码如下

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

显然,add() 方法没有使用同步互斥,所以在多线程并发是,会出现线程异常,测试代码如下

public class SetUnsefertyTest {
    public static void main(String[] args) {
        // 创建ArrayList 集合
        ArrayList<String> list = new ArrayList<>();

        // 创建10个线程,往 list 中添加元素
        for (int i = 0; i < 10; i++) {
            new Thread(()->{
                // 向集合中添加内容
                list.add(UUID.randomUUID().toString().substring(0,8));
                // 从集合中取出内容
                System.out.println(list);
            },String.valueOf(i)).start();
        }
    }
}

会出现如下异常

在这里插入图片描述

解决该方法主要有三种,即使用这三个类:Vector、Collections、CopyOnWriteArrayList(常用)

5.2 解决方案-Vector

进入在Vector的底层实现,找到 add() 方法是线程安全的,源代码如下,可以发现 Vector 将 add() 加上同步关键字了

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

但是 Vector 用的不多,因为每次对添加的元素上锁,而且使用的是重量级锁synchronized是十分占用资源的,效率是十分低下的。其用法和 ArrayList 一样。

5.3 解决方案-Collections

进入 Collections 的底层,找到 synchronizedList(List list) 方法,源代码如下,synchronizedList(List list) 方法返回指定列表支持的同步(线程安全的)列表

public static <T> List<T> synchronizedList(List<T> list) {
    return (list instanceof RandomAccess ?
            new SynchronizedRandomAccessList<>(list) :
            new SynchronizedList<>(list));
}

static <T> List<T> synchronizedList(List<T> list, Object mutex) {
    return (list instanceof RandomAccess ?
            new SynchronizedRandomAccessList<>(list, mutex) :
            new SynchronizedList<>(list, mutex));
}

对 Collections 的使用如下

List<String> list = Collections.synchronizedList(new ArrayList<>());

5.4 解决方案-CopyOnWriteArrayList

这种方法用的最多。

CopyOnWriteArrayList涉及的底层原理为 写时复制技术

  • 读的时候并发(多个线程操作)
  • 写的时候独立,先复制相同的空间到某个区域,将其写到新区域,旧新合并,并且读新区域(每次加新内容都写到新区域,覆盖合并之前旧区域,读取新区域添加的内容)

进入 CopyOnWriteArrayList 底层,来看一下它是怎么实现的,其 add() 源代码如下

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

对 CopyOnWriteArrayList 的使用如下

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

5.5 对上述三种方式的总结

对比三者来看,Vector和Collections虽然也可以实现同步,但由于这两种方法在底层都使用了synchronized重量级锁,使其效率很低,所以对 ArrayList 的同步主要采用 CopyOnWriteArrayList

5.6 HashSet的线程不安全

HashSet 同时读写时也会出现 ConcurrentModificationException 异常

进入 HashSet 底层,来看一下它是怎么实现的,其 add() 源代码如下

public boolean add(E e) {
    return map.put(e, PRESENT)==null;
}

显然,他的问题和 ArrayList 一样,没有对 add(E e) 方法做同步处理

其解决方法与 CopyOnWriteArrayList 类似,在 JDK1.8 中,也有一个类叫做 CopyOnWriteArraySet ,其底层代码如下

public boolean add(E e) {
    // private final CopyOnWriteArrayList<E> al;
    return al.addIfAbsent(e);
}

通过 Debug 找到了对关键的一个函数,发现其实现方式与 CopyOnWriteArrayList 底层实现方式类似

// e 表示添加的元素
// snapshot 表示被复制的列表
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();
    }
}

对 CopyOnWriteArraySet 的使用如下

CopyOnWriteArraySet<String> list = new CopyOnWriteArraySet<>();

5.7 HashMap的线程不安全

HashMap 同时读写时一样会出现 ConcurrentModificationException 异常

进入 HashMap 底层,来看一下它是怎么实现的,其 put(K key, V value) 源代码如下

public V put(K key, V value) {
    return putVal(hash(key), key, value, false, true);
}

在 JDK1.8 中,也有一个类叫做 ConcurrentHashMap ,实现 HashMap 的同步问题,其底层代码如下

final V putVal(K key, V value, boolean onlyIfAbsent) {
    if (key == null || value == null) throw new NullPointerException();
    int hash = spread(key.hashCode());
    int binCount = 0;
    for (Node<K,V>[] tab = table;;) {
        Node<K,V> f; int n, i, fh;
        // 若为空,就初始化
        if (tab == null || (n = tab.length) == 0)
            tab = initTable();
        // 计算hash值,得到下标
        else if ((f = tabAt(tab, i = (n - 1) & hash)) == null) {
            // 传入参数
            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)
            tab = helpTransfer(tab, f);
        else {
            V oldVal = null;
            // 在这里加上了锁
            synchronized (f) {
                if (tabAt(tab, i) == f) {
                    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);
    return null;
}

对 ConcurrentHashMap 的使用如下

ConcurrentHashMap<String, String> map = new ConcurrentHashMap<>();
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

_之桐_

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值