如何理解Java并发容器J.U.C

95次阅读
没有评论

共计 7757 个字符,预计需要花费 20 分钟才能阅读完成。

本篇文章为大家展示了如何理解 Java 并发容器 J.U.C,内容简明扼要并且容易理解,绝对能使你眼前一亮,通过这篇文章的详细介绍希望你能有所收获。

J.U.C 是 java.util.concurrent 的简写, 里面提供了很多线程安全的集合。

CopyOnWriteArrayList 介绍

CopyOnWriteArrayList 相比于 ArrayList 是线程安全的, 字面意思是写操作时复制。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(); // 释放锁
 }
 }

从上面的源码中得到 CopyOnWriteArrayList 的 add 操作是在加锁的保护下完成的。加锁是为了多线程对 CopyOnWriteArrayList 并发 add 时, 复制多个副本, 把数据搞乱。

public E get(int index) { return get(getArray(), index);
}

以上代码显示 get 是没有加锁的

如果出现并发 get, 会有以下 3 中情况。

如果写操作未完成,那么直接读取原数组的数据;

如果写操作完成,但是引用还未指向新数组,那么也是读取原数组数据;

如果写操作完成,并且引用已经指向了新的数组,那么直接从新数组中读取数据。

CopyOnWriteArrayList 多线程代码演示。

package com.rumenz.task;
import java.util.List;
import java.util.concurrent.*;

 public static Integer clientTotal=5000;  public static Integer threadTotal=200;  private static List integer  list=new CopyOnWriteArrayList();  public static void main(String[] args) throws Exception{ ExecutorService executorService = Executors.newCachedThreadPool();  final Semaphore semaphore=new Semaphore(threadTotal);  final CountDownLatch countDownLatch=new CountDownLatch(clientTotal);  for (int i = 0; i   clientTotal; i++) {  final Integer j=i;  executorService.execute(()- {  try{ semaphore.acquire();  update(j);  semaphore.release();  }catch (Exception e){ e.printStackTrace();  }  countDownLatch.countDown();  });
 private static void update(Integer j) { list.add(j);  } //size:5000

CopyOnWriteArrayList 使用场景

由于在 add 的时候需要拷贝原数组, 如果原数组内容比较多, 比较大, 可能会导致 young gc 和 full gc。

不能用于实时读的场景, 像拷贝数组, 新增元素都需要时间, 所以调用 get 操作后, 有可能得到的数据是旧数据, 虽然 CopyOnWriteArrayList 能做到最终一致性, 但是没有办法满足实时性要求。

CopyOnWriteArrayList 适合读多写少的场景, 比如白名单,黑名单等场景

CopyOnWriteArrayList 由于 add 时需要复制数组, 所以不适用高性能的互联网的应用。

CopyOnWriteArraySet 介绍

public CopyOnWriteArraySet() { al = new CopyOnWriteArrayList e}

CopyOnWriteArraySet 底层是用 CopyOnWriteArraySet 来实现的。可变操作 (add,set,remove 等) 都需要拷贝原数组进行操作, 一般开销很大。迭代器支持 hasNext(),netx()等不可变操作, 不支持可变的 remove 操作, 使用迭代器速度很快, 并且不会与其它线程冲突, 在构造迭代器时, 依赖不变的数组快照。

CopyOnWriteArraySet 多线代码演示

package com.rumenz.task;
import java.util.List;
import java.util.Set;
import java.util.concurrent.*;

 public static Integer clientTotal=5000;  public static Integer threadTotal=200;  private static Set integer  set=new CopyOnWriteArraySet();  public static void main(String[] args) throws Exception{ ExecutorService executorService = Executors.newCachedThreadPool();  final Semaphore semaphore=new Semaphore(threadTotal);  final CountDownLatch countDownLatch=new CountDownLatch(clientTotal);  for (int i = 0; i   clientTotal; i++) {  final Integer j=i;  executorService.execute(()- {  try{ semaphore.acquire();  update(j);  semaphore.release();  }catch (Exception e){ e.printStackTrace();  }  countDownLatch.countDown();  });
 private static void update(Integer j) { set.add(j);  } //size:5000

CopyOnWriteArraySet 使用场景

适用于 set 大小一般很小,读操作远远多于写操作的场景

ConcurrentSkipListSet

public ConcurrentSkipListSet() { m = new ConcurrentSkipListMap e,object}

ConcurrentSkipListSet e 是 jdk6 新增的类, 支持自然排序, 位于 java.util.concurrent。ConcurrentSkipListSet e 都是基于 Map 集合的, 底层由 ConcurrentSkipListMap 实现。

在多线程环境下,ConcurrentSkipListSet e 的 add,remove,contains 是线程安全的。但是对于批量操作 addAll,removeAll,containsAll 并不能保证原子操作, 所以是线程不安全的, 原因是 addAll,removeAll,containsAll 底层调用的还是 add,remove,contains 方法, 在批量操作时, 只能保证每一次的 add,remove,contains 是原子性的(即在进行 add,remove,contains, 不会被其它线程打断), 而不能保证每一次批量操作都不会被其它线程打断, 因此在 addAll、removeAll、retainAll 和 containsAll 操作时,需要添加额外的同步操作。

public boolean addAll(Collection !--? extends E--  c) {
 boolean modified = false;
 for (E e : c)
 if (add(e))
 modified = true;
 return modified;
public boolean removeAll(Collection !--?--  c) { Objects.requireNonNull(c);
 boolean modified = false;
 Iterator !--?--  it = iterator();
 while (it.hasNext()) { if (c.contains(it.next())) { it.remove();
 modified = true;
 }
 }
 return modified;
public boolean containsAll(Collection !--?--  c) { for (Object e : c)
 if (!contains(e))
 return false;
 return true;
}

ConcurrentSkipListSet 代码演示

package com.rumenz.task;
import java.util.ArrayList;
import java.util.List;
import java.util.Set;
import java.util.concurrent.*;

 public static Integer clientTotal=5000;  public static Integer threadTotal=200;  private static Set integer  set= new ConcurrentSkipListSet();  public static void main(String[] args) throws Exception{ ExecutorService executorService = Executors.newCachedThreadPool();  final Semaphore semaphore=new Semaphore(threadTotal);  final CountDownLatch countDownLatch=new CountDownLatch(clientTotal);  for (int i = 0; i   clientTotal; i++) {  final Integer j=i;  executorService.execute(()- {  try{ semaphore.acquire();  update(j);  semaphore.release();  }catch (Exception e){ e.printStackTrace();  }  countDownLatch.countDown();  });  }  countDownLatch.await();  executorService.shutdown();  System.out.println(size: +set.size());  }
 private static void update(Integer r) { set.add(r);  } //size:5000

ConcurrentHashMap

ConcurrentHashMap 中 key 和 value 都不允许为 null,ConcurrentHashMap 针对读操作做了大量的优化。在高并发场景很有优势。

在多线程环境下, 使用 HashMap 进行 put 操作会引起死循环, 导致 CPU 利用率到 100%, 所以在多线程环境不能随意使用 HashMap。原因分析:HashMap 在进行 put 的时候, 插入的元素超过了容量就会发生 rehash 扩容, 这个操作会把原来的元素 hash 到新的扩容新的数组, 在多线程情况下, 如果此时有其它线程在进行 put 操作, 如果 Hash 值相同, 可能出现在同一数组下用链表表示, 造成闭环, 导致 get 的时候出现死循环, 所以是线程不安全的。

HashTable 它是线程安全的, 它涉及到多线程的操作都 synchronized 关键字来锁住整个 table, 这就意味着所有的线程都在竞争同一把锁, 在多线程环境下是安全的, 但是效率很低。

HashTable 有很多的优化空间,锁住整个 table 这么粗暴的方法可以变相的柔和点,比如在多线程的环境下,对不同的数据集进行操作时其实根本就不需要去竞争一个锁,因为他们不同 hash 值,不会因为 rehash 造成线程不安全,所以互不影响,这就是锁分离技术,将锁的粒度降低,利用多个锁来控制多个小的 table,多线程访问容器里不同数据段的数据时,线程间就不会存在锁竞争,从而可以有效的提高并发访问效率,这就是 ConcurrentHashMapJDK1.7 版本的核心思想。

ConcurrentHashMap 代码演示案例

package com.rumenz.task;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.*;

 public static Integer clientTotal=5000;  public static Integer threadTotal=200;  private static Map integer,integer  map=new ConcurrentHashMap integer,integer  public static void main(String[] args) throws Exception{ ExecutorService executorService = Executors.newCachedThreadPool();  final Semaphore semaphore=new Semaphore(threadTotal);  final CountDownLatch countDownLatch=new CountDownLatch(clientTotal);  for (int i = 0; i   clientTotal; i++) {  final Integer j=i;  executorService.execute(()- {  try{ semaphore.acquire();  update(j);  semaphore.release();  }catch (Exception e){ e.printStackTrace();  }  countDownLatch.countDown();  });  }  countDownLatch.await();  executorService.shutdown();  System.out.println(size: +map.size());  }  private static void update(Integer j) { map.put(j, j);  } //size:5000

ConcurrentSkipListMap

ConcurrentSkipListMap 内部使用 SkipList 结构实现。跳表是一个链表, 但是通过跳跃式的查找方式使得插入, 读取数据时的时间复杂度变成 O(log n)。

跳表(SkipList): 使用空间换时间的算法, 令链表的每个结点不仅记录 next 结点位置,还可以按照 level 层级分别记录后继第 level 个结点。

如何理解 Java 并发容器 J.U.C

ConcurrentSkipListMap 代码案例

package com.rumenz.task;
import java.util.Map;
import java.util.concurrent.*;

 public static Integer clientTotal=5000;  public static Integer threadTotal=200;  private static Map integer,integer  map=new ConcurrentSkipListMap ();  public static void main(String[] args) throws Exception{ ExecutorService executorService = Executors.newCachedThreadPool();  final Semaphore semaphore=new Semaphore(threadTotal);  final CountDownLatch countDownLatch=new CountDownLatch(clientTotal);  for (int i = 0; i   clientTotal; i++) {  final Integer j=i;  executorService.execute(()- {  try{ semaphore.acquire();  update(j);  semaphore.release();  }catch (Exception e){ e.printStackTrace();  }  countDownLatch.countDown();  });
 private static void update(Integer j) { map.put(j, j);  } //size:5000

ConcurrentHashMap 与 ConcurrentSkipListMap 的对比

ConcurrentHashMap 比 ConcurrentSkipListMap 性能要好一些。

ConcurrentSkipListMap 的 key 是有序的,ConcurrentHashMap 做不到。

ConcurrentSkipListMap 支持高并发, 它的时间复杂度是 log(N), 和线程数无关, 也就是说任务一定的情况下, 并发的线程越多,ConcurrentSkipListMap 的优势就越能体现出来。

上述内容就是如何理解 Java 并发容器 J.U.C,你们学到知识或技能了吗?如果还想学到更多技能或者丰富自己的知识储备,欢迎关注丸趣 TV 行业资讯频道。

正文完
 
丸趣
版权声明:本站原创文章,由 丸趣 2023-08-25发表,共计7757字。
转载说明:除特殊说明外本站除技术相关以外文章皆由网络搜集发布,转载请注明出处。
评论(没有评论)