文章目录
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<>();