
5 集合的线程安全
5.1 集合线程不安全演示5.2 解决方案-Vector5.3 解决方案-Collections5.4 解决方案-CopyOnWriteArrayList5.5 对上述三种方式的总结5.6 HashSet的线程不安全5.7 HashMap的线程不安全
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 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 staticList synchronizedList(List list) { return (list instanceof RandomAccess ? new SynchronizedRandomAccessList<>(list) : new SynchronizedList<>(list)); } static List synchronizedList(List list, Object mutex) { return (list instanceof RandomAccess ? new SynchronizedRandomAccessList<>(list, mutex) : new SynchronizedList<>(list, mutex)); }
对 Collections 的使用如下
List5.4 解决方案-CopyonWriteArrayListlist = Collections.synchronizedList(new ArrayList<>());
这种方法用的最多。
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 的使用如下
List5.5 对上述三种方式的总结list = new CopyOnWriteArrayList<>();
对比三者来看,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 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 的使用如下
CopyOnWriteArraySet5.7 HashMap的线程不安全list = new CopyOnWriteArraySet<>();
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[] tab = table;;) {
Node 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(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 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 pred = e;
if ((e = e.next) == null) {
pred.next = new Node(hash, key,
value, null);
break;
}
}
}
else if (f instanceof TreeBin) {
Node p;
binCount = 2;
if ((p = ((TreeBin)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 的使用如下
ConcurrentHashMapmap = new ConcurrentHashMap<>();
欢迎分享,转载请注明来源:内存溢出
微信扫一扫
支付宝扫一扫
评论列表(0条)