一、ArrayList遍历问题
1.当只有一个线程迭代遍历ArrayList时:边遍历边修改List元素会出现ConcurrenMdifyedException
正确方法可以采用迭代器遍历迭代器修改元素
2.当多个线程访问ArrayList时(如:一个线程在遍历,一个线程在删除元素):
想要线程安全的遍历可以采用:
1)CopyOnWriteArrayList(CopyOnWriteArrayList用于在遍历为主要操作的情况下替代ArrayList)
CopyOnWriteArrayList是java.util.concurrent包中的一个List的实现类。CopyOnWrite的意思是在写时拷贝,也就是如果需要对 CopyOnWriteArrayList的内容进行改变,首先会拷贝一份新的List并且在新的List上进行修改,最后将原List的引用指向新的List。使用CopyOnWriteArrayList可以线程安全地遍历,因为如果另外一个线程在遍历的时候修改List的话,实际上会拷贝出一个新的List上修改,而不影响当前正在被遍历的List。
public static void main(String[] args) {
// 初始化一个list,放入5个元素
final List<Integer> list = new CopyOnWriteArrayList<>();
for(int i = 0; i < 5; i++) {
list.add(i);
}
// 线程一:通过Iterator遍历List
new Thread(new Runnable() {
@Override
public void run() {
for(int item : list) {
System.out.println("遍历元素:" + item);
// 由于程序跑的太快,这里sleep了1秒来调慢程序的运行速度
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}).start();
// 线程二:remove一个元素
new Thread(new Runnable() {
@Override
public void run() {
// 由于程序跑的太快,这里sleep了1秒来调慢程序的运行速度
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
list.remove(4);
System.out.println("list.remove(4)");
}
}).start();
}
2)线程安全的List.forEach
List.forEach方法是Java 8新增的一个方法,主要目的还是用于让List来支持Java 8的新特性:Lambda表达式。
由于forEach方法是List的一个方法,所以不同于在List外遍历List,forEach方法相当于List自身遍历的方法,所以它可以自由控制是否线程安全。
我们看线程安全的Vector的forEach方法源码:
public synchronized void forEach(Consumer<? super E> action) {
...
}
public static void main(String[] args) {
// 初始化一个list,放入5个元素
final List<Integer> list = new Vector<>();
for(int i = 0; i < 5; i++) {
list.add(i);
}
// 线程一:通过Iterator遍历List
new Thread(new Runnable() {
@Override
public void run() {
list.forEach(item -> {
System.out.println("遍历元素:" + item);
// 由于程序跑的太快,这里sleep了1秒来调慢程序的运行速度
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
});
}
}).start();
// 线程二:remove一个元素
new Thread(new Runnable() {
@Override
public void run() {
// 由于程序跑的太快,这里sleep了1秒来调慢程序的运行速度
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
list.remove(4);
System.out.println("list.remove(4)");
}
}).start();
}
二、同步容器与并发容器
1.同步容器
同步容器的同步原理就是在方法上用 synchronized 修饰。那么,这些方法每次只允许一个线程调用执行。
由于被 synchronized 修饰的方法,每次只允许一个线程执行,其他试图访问这个方法的线程只能等待。显然,这种方式比没有使用 synchronized 的容器性能要差。
代表类:Vector、Stack、HashTable
2.并发容器
可以并发的进行修改、遍历等等操作
JDK 的 java.util.concurrent 包(即 juc)中提供了几个非常有用的并发容器。
CopyOnWriteArrayList - 线程安全的 ArrayList
CopyOnWriteArraySet - 线程安全的 Set,它内部包含了一个 CopyOnWriteArrayList,因此本质上是由 CopyOnWriteArrayList 实现的。
ConcurrentSkipListSet - 相当于线程安全的 TreeSet。它是有序的 Set。它由 ConcurrentSkipListMap 实现。
ConcurrentHashMap - 线程安全的 HashMap。采用分段锁实现高效并发。
ConcurrentSkipListMap - 线程安全的有序 Map。使用跳表实现高效并发。
ConcurrentLinkedQueue - 线程安全的无界队列。底层采用单链表。支持 FIFO。
ConcurrentLinkedDeque - 线程安全的无界双端队列。底层采用双向链表。支持 FIFO 和 FILO。
ArrayBlockingQueue - 数组实现的阻塞队列。
LinkedBlockingQueue - 链表实现的阻塞队列。
LinkedBlockingDeque - 双向链表实现的双端阻塞队列。
ConcurrentHashMap
要点
作用:ConcurrentHashMap 是线程安全的 HashMap。
原理:JDK6 与 JDK7 中,ConcurrentHashMap 采用了分段锁机制。JDK8 中,摒弃了锁分段机制,改为利用 CAS 算法。