目录
1. 同步容器
2. 并发容器
3. HashMap(1.7和1.8介绍)
4. ConcurrentHashMap
5. 一些问题引发的思考
1. 同步容器
普通的集合类ArrayList、LinkedList、HashMap等等是线程不安全的
在多线程共同操作这些类对象时,会发生竞争条件或者操作异常。如果我们需要在多线程下使用这些集合类,就需要显示地进行同步操作(用synchronized关键字等对集合类的操作代码加同步锁),如果不这样做,可以使用Java提供的同步容器,同步容器默认是线程安全的,对它的操作都是已经默认加了同步锁的
同步容器主要包括两类
- Vector、Stack、HashTable
- Vector实现了List接口,Vector底层是一个数组,其对于数组的各种操作和ArrayList几乎一样,唯一不同的在于大部分线程不安全的方法都加了
synchronized
关键字去限定
- Stack底层也是一个数字,它继承于Vector类,很多方法也用
synchronized
关键字加了锁 - HashTable实现了Map接口,它的实现原理几乎和HashMap一样。但是HashTable对很多方法都加了
synchronized
关键字进行限定
- Vector实现了List接口,Vector底层是一个数组,其对于数组的各种操作和ArrayList几乎一样,唯一不同的在于大部分线程不安全的方法都加了
- Collections工具类中提供的同步集合类
Collections类是一个工具类,相当于Arrays类对于Array的支持,Collections类中提供了大量对集合或者容器进行排序、查找的方法。它还提供了几个静态方法来创建同步容器类:
Modifier and Type | Method | Description |
---|---|---|
static < T> Collection< T> | synchronizedCollection(Collection< T> c) | 返回由指定collection支持的同步(线程安全)collection。 |
static < T> List< T> | synchronizedList(List< T> list) | 返回由指定列表支持的同步(线程安全)列表 |
static <K,V> Map<K,V> | synchronizedMap(Map<K,V> m) | 返回由指定映射支持的同步(线程安全)映射 |
static < T> Set< T> | synchronizedSet(Set< T> s) | 返回由指定set支持的同步(线程安全)set |
static <K,V> SortedMap<K,V> | synchronizedSortedMap(SortedMap<K,V> m) | 返回由指定有序映射支持的同步(线程安全)有序映射 |
static < T> SortedSet< T> | synchronizedSortedSet(SortedSet< T> s) | 返回由指定有序set支持的同步(线程安全)有序set |
2. 并发容器
同步容器是通过synchronized
关键字对线程不安全的操作进行加锁来保证线程安全的,其原理是使得多线程轮流获取同步锁进行对集合的操作,所以性能有所下降
JUC提供了多种并发容器,以:在原有集合的拷贝上进行操作,用修改后的集合替换原集合的方式来达到并发且安全地使用集合类的目的
根据接口的类型,主要有以下四种接口,其它具体的容器均是这些接口的继承和实现类:
- Queue类型: 阻塞队列BlockingQueue、非阻塞队列ConcurrentLinkedQueue
- Map类型: ConcurrentMap
- Set类型: ConcurrentSkipListSet、CopyOnWriteArraySet
- List类型: CopyOnWriteArrayList
3. HashMap(1.7和1.8介绍)
3.1 HashMap
HashMap根据键的hashCode值存储数据,大多数情况下可以直接定位到它的值,因为具有很快的访问速度,但遍历顺序却是不确定的。HashMap最多只允许一条记录的键为null,允许多条记录的值为null。HashMap非线程安全,即任一时刻可以有多个线程同时写HashMap,可能会导致数据的不一致。如果需要满足线程安全,可以用Collections的synchronizedMap方法使HashMap具有线程安全的能力,或者使用ConcurrentHashMap
3.1.1 Java7实现HashMap结构图
HashMap 里面是一个数组,然后数组中每个元素是一个单向链表。上图中,每个绿色的实体是嵌套类 Entry 的实例,Entry 包含四个属性:key, value, hash 值和用于单向链表的 next。
- capacity: 当前数组容量,始终保持2^n,可以扩容,扩容后数组大小为当前的2倍
- loadFactor: 负载因子,默认为0.75
- threhold: 扩容的阈值,等于capacity * loadFactor
3.1.2 Java8实现
Java8对HashMap进行了一些修改,最大的不同就是利用了红黑树,所以其由数组+链表+红黑树组成
根据Java7 HashMap的介绍,我们知道在查询的时候,根据hash值我们能够很快定位到数组的具体下标,但是之后的话,需要顺着链表一个个比较下去才能找到我们需要的,时间复杂度取决于链表的长度,为O(N)。为了降低这部分的开销,在Java8中,当链表中的元素超过了8个以后,会将链表转换为红黑树,在这些位置进行查找的时候可以降低时间复杂度为O(logN)
3.1.3 为什么HashMap是线程不安全的?
- 同时put碰撞导致数据丢失
- 同时put扩容导致数据丢失
- 多个线程同时进行扩容的时候会造成链表循环,即死循环造成的CPU100%(仅JDK7及其以前存在)
4. ConcurrentHashMap
4.1 Segment段
ConcurrentHashMap和HashMap思路差不多,但是因为它支持并发操作,所以要复杂一些。整个ConcurrentHashMap由一个个Segment组成,Segment代表"部分"或"一段"的意思,所以很多地方都会将其描述为分段锁。
4.2 线程安全(Segment继承ReentrantLock加锁)
ConcurrentHashMap是一个Segment数组,Segment通过继承ReentrantLock来进行加锁,所以每次需要加锁的操作锁住的是一个segment,这样只要保证每个Segment是线程安全的,也就实现了全局的线程安全。
4.3 Java7 ConcurrentHashMap结构图
4.4 Java8 ConcurrentHashMap结构图
Java8引入了红黑树
5. 一些问题引发的思考
问题1: 为什么不用Collections.synchronizedMap(),而用ConcurrentHashMap?
答: Collections.synchronizedMap()是线程安全的,但是它是通过使用一个全局的锁来同步不同线程间的并发访问,因此会带来较大的性能问题
问题2: 组合操作导致ConcurrentHashMap也不是线程安全的?
本质上ConcurrentHashMap是线程安全的,只是我们的操作失误而导致错误,例如组合操作
/**
* 描述: 组合操作并不保证线程安全
*/
public class OptionsNotSafe implements Runnable {
private static ConcurrentHashMap<String, Integer> scores = new ConcurrentHashMap<String, Integer>();
public static void main(String[] args) throws InterruptedException {
scores.put("小明", 0);
Thread t1 = new Thread(new OptionsNotSafe());
Thread t2 = new Thread(new OptionsNotSafe());
t1.start();
t2.start();
t1.join();
t2.join();
// 两个线程分别进行1000次+操作,结果值小于2000
System.out.println(scores);
}
@Override
public void run() {
for (int i = 0; i < 1000; i++) {
Integer score = scores.get("小明");
Integer newScore = score + 1;
scores.put("小明", newScore);
}
}
}
如何避免?采用replace方法(思想类似cas)
@Override
public void run() {
for (int i = 0; i < 1000; i++) {
while(true) {
Integer score = scores.get("小明");
Integer newScore = score + 1;
boolean b = scores.replace("小明", score, newScore);
// 不断进行类似cas操作,直到成功退出进行下一轮
if(b) {
break;
}
}
}
}