Java 集合框架的线程安全问题
List线程安全问题
1、先来一段代码
@Test
public void test() {
List<Integer> ints = Arrays.asList(1, 2, 3, 4);
ints.forEach(System.out::println);
}
上述代码是不存在线程安全问题的,但是在多线程环境中,List下的ArrayList、LinkedList,会出现线程安全问题。例如:
public static void main(String[] args) {
List<String> strList = new ArrayList<>();
for (int i = 0; i < 3; i++) {
new Thread(() -> {
strList.add(UUID.randomUUID().toString().substring(0, 8));
System.out.println(strList);
}, String.valueOf(i)).start();
}
}
运行结果出现安全问题
当线程数增加到30出现异常
java.util.ConcurrentModificationException
at java.util.ArrayList$Itr.checkForComodification(ArrayList.java:909)
at java.util.ArrayList$Itr.next(ArrayList.java:859)
at java.util.AbstractCollection.toString(AbstractCollection.java:461)
at java.lang.String.valueOf(String.java:2994)
at java.io.PrintStream.println(PrintStream.java:821)
at com.example.juc.UnSafeDemo.lambda$main$0(UnSafeDemo.java:30)
at java.lang.Thread.run(Thread.java:748)
public class UnSafeDemo {
// 1、异常现象 java.util.ConcurrentModificationException
// 2、导致原因
// 多线程并发争抢同一个资源类,而且没有加锁
// 3、解决方案
// 重写ArrayList,自己加锁,是不可行的方案。
// List<String> strList = new Vector<>(); // 数据一致性可以保证但是访问效率下降
// List<String> strList = Collections.synchronizedList(new ArrayList<>());
// List<String> strList = new CopyOnWriteArrayList<>();
// 4、优化建议
// 高并发情况下可以使用JUC 下的包
public static void main(String[] args) {
// List<String> strList = new Vector<>(); // new ArrayList<>()
// List<String> strList = Collections.synchronizedList(new ArrayList<>()); // new ArrayList<>()
List<String> strList = new CopyOnWriteArrayList<>(); // new ArrayList<>()
for (int i = 0; i < 30; i++) {
new Thread(() -> {
strList.add(UUID.randomUUID().toString().substring(0, 8));
System.out.println(strList);
}, String.valueOf(i)).start();
}
}
CopyOnWriteArrayList
:写时复制也可以称为读写分离,CopyOnWrite容器即写时复制容器,往一个容器中添加元素的时候,不是在当前容器的Object[]直接添加,而是先将当前数组进行copy,复制出新的 Object[] newElements然后在新的数组中添加元素到尾部,再将原有数组的引用指向新的数组,这样做的优势是该容器可以进行并发的读,不需要加锁。原因是当前容器不会添加任何元素,也是一种读写分离的思想。
CopyOnWriteArrayList.add()
方法源码解析。
public boolean add(E e) {
// 获取一个可重入锁
final ReentrantLock lock = this.lock;
lock.lock();
try {
Object[] elements = getArray(); // 在新增之前先先复制一份
int len = elements.length; // 获取当前数组的长度
// 复制之前的数组且长度+1
Object[] newElements = Arrays.copyOf(elements, len + 1);
// 新增元素
newElements[len] = e;
// 将当前数组引用指向为新的数组
setArray(newElements);
return true;
} finally {
lock.unlock(); // 释放锁
}
}
Set线程不安全
与CopyOnWriteArrayList
类似,JUC也提供了一个CopyOnWriteArraySet
类,与HashSet不同的是它的底层也是数组,新增时会判断元素是否已经存在。
public CopyOnWriteArraySet() {
al = new CopyOnWriteArrayList<E>();
}
代码
public static void hashSetNotSafe() {
// Set<String> strSet = new HashSet<>();
// Set<String> strSet = Collections.synchronizedSet(new HashSet<>());
Set<String> strSet = new CopyOnWriteArraySet<>();
for (int i = 0; i < 30; i++) {
new Thread(() -> {
strSet.add(UUID.randomUUID().toString().substring(0, 8));
System.out.println(strSet);
}).start();
}
}
CopyOnWriteArraySet
新增元素逻辑源码分析 这里是调用的CopyOnWriteArrayList中的方法。
public boolean add(E e) {
return al.addIfAbsent(e);
}
public boolean addIfAbsent(E e) {
Object[] snapshot = getArray();
// 查询是否存在 存在返回false 不存在准备新增
return indexOf(e, snapshot, 0, snapshot.length) >= 0 ? false :
addIfAbsent(e, snapshot);
}
private static int indexOf(Object o, Object[] elements,
int index, int fence) {
if (o == null) { // 这个Set 允许存入一个 null
for (int i = index; i < fence; i++)
if (elements[i] == null)
return i;
} else {
for (int i = index; i < fence; i++)
// 元素最好重写equals方法,根据我们需求判断是否是同一个值
if (o.equals(elements[i]))
return i;
}
return -1;
}
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(); // 释放锁
}
}
Map线程安全问题
public static void hashMapNotSafe() {
// Map<String, String> map = new HashMap<>(); // 多线程可能会出现线程安全问题
// Map<String, String> map = new Hashtable<>(); // 可解决线程安全问题,但是处处加锁 读写效率较低
// Map<String, String> map = Collections.synchronizedMap(new HashMap<>()); // 利用Collections.synchronizedMap()方法,将HashMap转为线程安全
Map<String, String> map = new ConcurrentHashMap<>(); // JUC 提供的ConcurrentHashMap类,可解决线程安全问题
for (int i = 0; i < 30; i++) {
new Thread(() -> {
map.put(Thread.currentThread().getName(), UUID.randomUUID().toString().substring(0, 8));
System.out.println(map);
}).start();
}
}
ConcurrentHashMap
提供的put方法解析。CAS + synchronized 保证线程安全
/** Implementation for put and putIfAbsent */
final V putVal(K key, V value, boolean onlyIfAbsent) {
// ConcurrentHashMap 不允许key或value为null
if (key == null || value == null) throw new NullPointerException();
int hash = spread(key.hashCode()); // 获取hash值
int binCount = 0;
for (Node<K,V>[] tab = table;;) { // 多线程环境,一直循环,
Node<K,V> f; int n, i, fh;
if (tab == null || (n = tab.length) == 0)
//在 new ConcurrentHashMap时,并不直接初始化长度,而是在第一次put时初始化,达到懒加载的目的
tab = initTable();
else if ((f = tabAt(tab, i = (n - 1) & hash)) == null) {
// Node<K,V>[] 数组不为空,且当前位置上没有数据 将会以CAS的方式新增元素
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)
//该节点的hash值为Moved,说明当前节点是ForwardingNode,意味着有其他线程
//在进行扩容,则一起进行扩容操作
tab = helpTransfer(tab, f);
else {
V oldVal = null;
//加锁同步,针对首个节点进行加锁操作
synchronized (f) {
if (tabAt(tab, i) == f) { // 找到table表下标为i的节点
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); // 增加binCount容量,检查当前容量是否需要进行扩容
return null;
}
快乐小石头分享的 ConcurrentHashMap实现原理及源码分析
从ConcurrentHashMap的源码中我们可以学到很多并发控制的优秀思想。
(1)当前线程与其它线程发生竞争修改资源类时,可以循环CAS修改直到成功。
(2)当其他线程在修改,当前线程可以修改也可以不修改时,可以帮助其他线程修改,也可以让出CPU,循环等待。这样可以减少很多线程环境切换。