Java并发容器类综述
0) 出现原因:
起初在对程序性能要求不高的年代,基本都是单线程编程,那个时候CPU基本是单核,随着计算机硬件技术的发展,出现了多核CPU,过去的单核编程已经不能更好的使用我们的CPU核心数目,加上对程序的速度性能的要求的提高,出现了并发编程技术. 单核程序的性能的瓶颈就是频繁的上下文切换,那么我们就可以通过减少上下文的切换来提高性能。让更多的CPU协同 异步工作.
1)发展过程:
Java在 Java5 添加的一个并发工具包。这个包包含了一系列能够让 Java 的并发编程变得更加简单轻松的类。在这之前,你需要自己手动去实现相关的工具类. 是以工具类的形式提供 包路径为 java.util.concurrent
提供的类:
- 阻塞队列 BlockingQueue
- 数组阻塞队列 ArrayBlockingQueue
- 延迟队列 DelayQueue
- 链阻塞队列 LinkedBlockingQueue
- 具有优先级的阻塞队列 PriorityBlockingQueue
- 同步队列 SynchronousQueue
- 阻塞双端队列 BlockingDeque
- 链阻塞双端队列 LinkedBlockingDeque
- 并发 Map ConcurrentMap
- 并发导航映射 ConcurrentNavigableMap
- 闭锁 ConutDownLatch
- 栅栏 CyclicBarrier
- 交换机 Exchanger
- 信号量 Semaphore
- 执行器服务 ExecutorService
- 线程池执行者 ThreadPoolExecutor
- 定时执行者服务 ScheduledExecutorService
- 使用 ForkJoinPool 进行分叉和合并
- 锁 Lock
- 读写锁 ReadWriteLock
- 原子性布尔 AtomicBoolean
- 原子性整型 AtomicInteger
- 原子性长整型 AtomicLong
- 原子性引用型 AtomicReference
我们选择常用的进行概述.
2)必要理论知识
悲观锁
总是假设最坏的情况,每次去拿数据的时候都认为别人会修改,所以每次在拿数据的时候都会上锁,这样别人想拿这个数据就会阻塞直到它拿到锁(共享资源每次只给一个线程使用,其它线程阻塞,用完后再把资源转让给其它线程)。传统的关系型数据库里边就用到了很多这种锁机制,比如行锁,表锁等,读锁,写锁等,都是在做操作之前先上锁。Java中synchronized和ReentrantLock等独占锁就是悲观锁思想的实现。
乐观锁
总是假设最好的情况,每次去拿数据的时候都认为别人不会修改,所以不会上锁,但是在更新的时候会判断一下在此期间别人有没有去更新这个数据,可以使用版本号机制和CAS算法实现。乐观锁适用于多读的应用类型,这样可以提高吞吐量,像数据库提供的类似于write_condition机制,其实都是提供的乐观锁。在Java中java.util.concurrent.atomic包下面的原子变量类就是使用了乐观锁的一种实现方式CAS实现的。
CAS是乐观锁技术,当多个线程尝试使用CAS同时更新同一个变量时,只有其中一个线程能更新变量的值,而其它线程都失败,失败的线程并不会被挂起,而是被告知这次竞争中失败,并可以再次尝试
3) 目前最新发展:
Java类库中出现的第一个关联的集合类是 Hashtable
,它是JDK 1.0的一部分。 Hashtable
提供了一种易于使用的、线程安全的、关联的map功能,这当然也是方便的。然而,线程安全性是凭代价换来的―― Hashtable
的所有方法都是同步的。 此时,无竞争的同步会导致可观的性能代价。 Hashtable
的后继者 HashMap
是作为JDK1.2中的集合框架的一部分出现的,它通过提供一个不同步的基类和一个同步的包装器 Collections.synchronizedMap
,解决了线程安全性问题.
但是: 其结果是尽管表面上这些程序在负载较轻的时候能够正常工作,但是一旦负载较重,它们就会开始抛出 NullPointerException
或 ConcurrentModificationException 异常
随后出现了 ConcurrentMap
ConcurrentMap 的实现
concurrent 包里面就一个类实现了 ConcurrentMap 接口
ConcurrentHashMap
ConcurrentHashMap
ConcurrentHashMap 和 HashTable 类很相似,但 ConcurrentHashMap 能提供比 HashTable 更好的并发性能。在你从中读取对象的时候,ConcurrentHashMap 并不会把整个 Map 锁住。此外,在你向其写入对象的时候,ConcurrentHashMap 也不会锁住整个 Map,它的内部只是把 Map 中正在被写入的部分锁定。
其实就是把 synchronized 同步整个方法改为了同步方法里面的部分代码。
另外一个不同点是,在被遍历的时候,即使是 ConcurrentHashMap 被改动,它也不会抛 ConcurrentModificationException。尽管 Iterator 的设计不是为多个线程同时使用。
使用例子:
public class ConcurrentHashMapExample {
public static void main(String[] args) {
// HashMap<String, String> map = new HashMap<>();
ConcurrentHashMap<String, String> map = new ConcurrentHashMap<>();
map.put("1", "a");
map.put("2", "b");
map.put("3", "c");
map.put("4", "d");
map.put("5", "e");
map.put("6", "f");
map.put("7", "g");
map.put("8", "h");
new Thread1(map).start();
new Thread2(map).start();
}
}
class Thread1 extends Thread {
private final Map map;
Thread1(Map map) {
this.map = map;
}
@Override
public void run() {
super.run();
try {
Thread.sleep(20);
} catch (InterruptedException e) {
e.printStackTrace();
}
map.remove("6");
}
}
class Thread2 extends Thread {
private final Map map;
Thread2(Map map) {
this.map = map;
}
@Override
public void run() {
super.run();
Set set = map.keySet();
for (Object next : set) {
System.out.println(next + ":" + map.get(next));
try {
Thread.sleep(10);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}复制代码
CopyOnWriteArrayList
在那些遍历操作大大地多于插入或移除操作的并发应用程序中,一般用
CopyOnWriteArrayList
类替代 ArrayList
。如果是用于存放一个侦听器(listener)列表,例如在AWT或Swing应用程序中,或者在常见的JavaBean中,那么这种情况很常见(相关的 CopyOnWriteArraySet
使用一个 CopyOnWriteArrayList
来实现 Set
接口) 。
原理实现:
如果您正在使用一个普通的
ArrayList
来存放一个侦听器列表,那么只要该列表是可变的,而且可能要被多个线程访问,您 就必须要么在对其进行迭代操作期间,要么在迭代前进行的克隆操作期间,锁定整个列表,这两种做法的开销都很大。当对列表执行会引起列表发生变化的操作时, CopyOnWriteArrayList
并不是为列表创建一个全新的副本,它的迭代器肯定能够返回在迭代器被创建时列表的状态,而不会抛出 ConcurrentModificationException
。在对列表进行迭代之前不必克隆列表或者在迭代期间锁 定列表,因为迭代器所看到的列表的副本是不变的。换句话说, CopyOnWriteArrayList
含有对一个不可变数组的一个可变的引用,因此,只要保留好那个引用,您就可以获得不可变的线程安全性的好处,而且不用锁 定列表。
4)目前最新应用情况:
以ConcurrentHashMap为例子:
jdk7版本
ConcurrentHashMap和HashMap设计思路差不多,但是为支持并发操作,做了一定的改进,ConcurrentHashMap引入Segment 的概念,目的是将map拆分成多个Segment(默认16个)。操作ConcurrentHashMap细化到操作某一个Segment。在多线程环境下,不同线程操作不同的Segment,他们互不影响,这便可实现并发操作
最新JDk8 实现:
jdk8版本的ConcurrentHashMap相对于jdk7版本,发送了很大改动,jdk8直接抛弃了Segment的设计,采用了较为轻捷的Node + CAS + Synchronized设计,保证线程安全。
ConcurrentHashMap结构图
看上图ConcurrentHashMap的大体结构,一个node数组,默认为16,可以自动扩展,扩展速度为0.75
private static finalint DEFAULT_CONCURRENCY_LEVEL = 16;
private static final float LOAD_FACTOR = 0.75f;
每一个节点,挂载一个链表,当链表挂载数据大于8时,链表自动转换成红黑树
static final int TREEIFY_THRESHOLD = 8;
部分代码:
public class ConcurrentHashMap<K,V> extends AbstractMap<K,V>
implements ConcurrentMap<K,V>, Serializable {
transient volatile Node<K,V>[] table;
}
static class Node<K,V> implements Map.Entry<K,V> {
final int hash;
final K key;
volatile V val;
volatile Node<K,V> next;
}
static final class TreeNode<K,V> extends Node<K,V> {
TreeNode<K,V> parent; // red-black tree links
TreeNode<K,V> left;
TreeNode<K,V> right;
TreeNode<K,V> prev; // needed to unlink next upon deletion
boolean red;
}
static final class TreeBin<K,V> extends Node<K,V> {
TreeNode<K,V> root;
volatile TreeNode<K,V> first;
volatile Thread waiter;
volatile int lockState;
}