多线程并发 之 并发集合

前言

在 JUC 中有几个重要的并发集合,这里简单记录一下对它们的认识。

CopyOnWriteArrayList

这个容器是为了ArrayList线程不安全设计的。我们都知道在java容器中ArrayList是线程不安全的,而vector是线程安全的。那么针对线程安全和不安全来说,这两个容器应该是够用了,为什么还要出现一个CopyOnWriteArrayList这个容器呢?CopyOnWriteArrayList是线程安全的,那么它较vector有哪些优势呢?看一下vector的add和get源码:

1
2
3
4
5
6
7
8
9
10
11
12
13
public synchronized boolean add(E e) {
modCount++;
ensureCapacityHelper(elementCount + 1);
elementData[elementCount++] = e;
return true;
}

public synchronized E get(int index) {
if (index >= elementCount)
throw new ArrayIndexOutOfBoundsException(index);

return elementData(index);
}

可以看到对于vector的add和get方法都被synchronize修饰了,这就说明了当我在读取数据的时候,其他线程也是不能获取的,所有的读和写都会被阻塞,性能太差了。

再来看CopyOnWriteArrayList中的源码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
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();
}
}

首先看add的方法。简单的概括就是,在添加数据的时候,系统会复制一个新的Array,然后添加的时候往新的Array添加,当然这个新的Array由Lock确保其add线程安全,注意原来的Array没有加锁,get读取的是旧的Array,所以可以保证get不会同步。再来看get方法:

1
2
3
4
5
6
7
public E get(int index) {
return get(getArray(), index);
}

private E get(Object[] a, int index) {
return (E) a[index];
}

get操作是没有加锁的,所以性能会很快。copyOnWriterArrayList通过消耗空间的方式来提高系统的并发性,但是这样浪费了空间,容易造成minor Gc ,严重的话会造成full gc。所以要根据情况来选择使用

ConcurrentHashMap

同样的ConcurrentHashMap是为了解决HashMap线程不安全的问题,但是你们有会疑惑,HashTable不是已经解决了HashMap线程不安全的问题了吗?为什么还需要ConcurrentHashMap呢?先来看一下HashTable的get和put操作:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
public synchronized V put(K key, V value) {
// Make sure the value is not null
if (value == null) {
throw new NullPointerException();
}

// Makes sure the key is not already in the hashtable.
Entry<?,?> tab[] = table;
int hash = key.hashCode();
int index = (hash & 0x7FFFFFFF) % tab.length;
@SuppressWarnings("unchecked")
Entry<K,V> entry = (Entry<K,V>)tab[index];
for(; entry != null ; entry = entry.next) {
if ((entry.hash == hash) && entry.key.equals(key)) {
V old = entry.value;
entry.value = value;
return old;
}
}

addEntry(hash, key, value, index);
return null;
}

public synchronized V get(Object key) {
Entry<?,?> tab[] = table;
int hash = key.hashCode();
int index = (hash & 0x7FFFFFFF) % tab.length;
for (Entry<?,?> e = tab[index] ; e != null ; e = e.next) {
if ((e.hash == hash) && e.key.equals(key)) {
return (V)e.value;
}
}
return null;
}

Hashtable的synchronized是针对整张Hash表的,即每次读写都是锁住整张表让线程独占,这个与Vector有点像,性能太差。而ConcurrentHashMap允许多个修改操作并发进行,其关键在于使用了锁分离技术:首先将数据分成一段一段的存储,然后给每一段数据配一把锁,当一个线程占用锁访问其中一个段数据的时候,其他段的数据也能被其他线程访问。

ConcurrentHashMap默认将hash表分为16个桶,诸如get、put、remove等常用操作只锁住当前需要用到的桶。这样,原来只能一个线程进入,现在却能同时有16个写线程执行,并发性能的提升是显而易见的。