【JUC并发编程05】集合的线程安全
2022/1/31 17:40:51
本文主要是介绍【JUC并发编程05】集合的线程安全,对大家解决编程问题具有一定的参考价值,需要的程序猿们随着小编来一起学习吧!
文章目录
- 5 集合的线程安全
- 5.1 集合线程不安全演示
- 5.2 解决方案-Vector
- 5.3 解决方案-Collections
- 5.4 解决方案-CopyOnWriteArrayList
- 5.5 对上述三种方式的总结
- 5.6 HashSet的线程不安全
- 5.7 HashMap的线程不安全
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<>();
这篇关于【JUC并发编程05】集合的线程安全的文章就介绍到这儿,希望我们推荐的文章对大家有所帮助,也希望大家多多支持为之网!
- 2024-11-23增量更新怎么做?-icode9专业技术文章分享
- 2024-11-23压缩包加密方案有哪些?-icode9专业技术文章分享
- 2024-11-23用shell怎么写一个开机时自动同步远程仓库的代码?-icode9专业技术文章分享
- 2024-11-23webman可以同步自己的仓库吗?-icode9专业技术文章分享
- 2024-11-23在 Webman 中怎么判断是否有某命令进程正在运行?-icode9专业技术文章分享
- 2024-11-23如何重置new Swiper?-icode9专业技术文章分享
- 2024-11-23oss直传有什么好处?-icode9专业技术文章分享
- 2024-11-23如何将oss直传封装成一个组件在其他页面调用时都可以使用?-icode9专业技术文章分享
- 2024-11-23怎么使用laravel 11在代码里获取路由列表?-icode9专业技术文章分享
- 2024-11-22怎么实现ansible playbook 备份代码中命名包含时间戳功能?-icode9专业技术文章分享