Java容器与容器安全
Java容器关系图
idea使用快捷键 ctrl+h 可以查看类之间的继承关系,以上根据类继承接口的关系所绘,其中标绿色的是接口,深蓝色的是线程不安全的实现类,浅蓝色的是线程安全的实现类。
List与List不安全
只看图中的四个List实现类,正常情况下使用ArrayList
List<String> list = new ArrayList<>();
list.add("a");
list.add("b");
list.add("c");
list.forEach(e-> System.out.println(e));
同样的,你也可以这样使用 LinkedList (LinkedList与ArrayList均是List的实现,不同的是ArrayList使用数组实现,支持随机访问,而LinkedList使用链表实现)
List<String> list = new LinkedList<>();
list.add("a");
list.add("b");
list.add("c");
list.forEach(e-> System.out.println(e));
上面的代码均会顺序打印 a,b,c 如下,无论运行100次还是10000次都是如此
a
b
c
在单线程情况下,该程序无论如何都不会出错,但在多线程情况下也如此吗?
我们来编写代码验证一下
List<String> list = new ArrayList<>();
for(int i = 0;i < 6;i++){
int finalI = i;
new Thread(()->{
list.add("thread"+ finalI);
System.out.println("线程"+finalI+list);
},"thread"+i
).start();
}
多试几次,会出现程序并发修改异常 ConcurrentModificationException ,为什么会出现这个错误呢,根据打印的错误栈追踪可以看到下列源码
if (modCount != expectedModCount)
throw new ConcurrentModificationException();
当修改次数与希望的修改次数不一致时则抛异常,即***多线程情况下该线程的期望修改次数与程序实际的修改次数不一致*** ,那么怎么解决这个问题呢
方法一:使用 Vector 类
List<String> list = new Vector<>();//修改容器为 Vector
for(int i = 0;i < 6;i++){
int finalI = i;
new Thread(()->{
list.add("thread"+ finalI);
System.out.println("线程"+finalI+list);
},"thread"+i
).start();
}
多次运行也不会出现异常,我们查看 Vector 类的 add 方法
public synchronized boolean add(E e) {
modCount++;
ensureCapacityHelper(elementCount + 1);
elementData[elementCount++] = e;
return true;
}
发现该方法给 synchronized 加了锁,保证了容器每次只有单个线程操作
方法二:使用 Collections 工具类
//通过 Collections的synchronizedList()方法来获得一个线程安全的List容器
List<String> list = Collections.synchronizedList(new ArrayList<>());
for(int i = 0;i < 6;i++){
int finalI = i;
new Thread(()->{
list.add("thread"+ finalI);
System.out.println("线程"+finalI+list);
},"thread"+i
).start();
}
我们查看 synchronizedList() 方法的源码
public static <T> List<T> synchronizedList(List<T> list) {
return (list instanceof RandomAccess ?
new SynchronizedRandomAccessList<>(list) :
new SynchronizedList<>(list));
}
发现该方法 return 部分是一个三元表达式,会查看传如的类是否支持随机访问,支持则返回一个支持随机访问的容器。
继续查看 add 方法
public void add(int index, E element) {
synchronized (mutex) {list.add(index, element);}
}
发现同样使用了synchronized 关键字
方法三:使用 CopyOnWriteArrayList
使用加锁固然简单,但会在频繁写入的时候出现访问慢的问题,那么有没有能在写入时候访问对象的呢
List<String> list = new CopyOnWriteArrayList<>();
for(int i = 0;i < 6;i++){
int finalI = i;
new Thread(()->{
list.add("thread"+ finalI);
System.out.println("线程"+finalI+list);
},"thread"+i
).start();
}
我们查看该容器的 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();
}
}
通过可重入锁来支持多个线程访问写入,先加锁,然后复制修改并写入原件;虽然该方法支持多个线程访问,但它每次写入都会完全复制一份原件,会消耗大量的空间
Set与Set不安全
Set<String> set = new HashSet<>();
//Set<String> set = new LinkedSet<>();
set.add("a");
set.add("a");
set.add("b");
set.forEach(e -> System.out.println(e));
HashSet,LinkedSet,TreeSet都是线程不安全的Set,因为他们底层实现是HashMap,LinkedMap,TreeMap,通过Map的key的不可重复的特性来实现的Set
同样在多线程的情况下
Set<String> set = new HashSet<>();
for(int i = 0;i < 100;i++){
new Thread(()->{
set.add(UUID.randomUUID().toString().substring(0,4));
System.out.println(set);
},"thread"+i
).start();
}
多次运行可能会出现 ConcurrentModificationException 的异常
解决方法,类比上面的 List 的解决方法
方法一:使用Collections工具类
Set<String> set = Collections.synchronizedSet(new HashSet<>());
for(int i = 0;i < 100;i++){
new Thread(()->{
set.add(UUID.randomUUID().toString().substring(0,4));
System.out.println(set);
},"thread"+i
).start();
}
查看synchronizedSet()方法的源码
public static <T> Set<T> synchronizedSet(Set<T> s) {
return new SynchronizedSet<>(s);
}
发现它会new一个线程安全的Set容器
方法二:使用CopyOnWriteArraySet
Set<String> set = new CopyOnWriteArraySet<>();
for(int i = 0;i < 100;i++){
new Thread(()->{
set.add(UUID.randomUUID().toString().substring(0,4));
System.out.println(set);
},"thread"+i
).start();
}
同样追踪源码,发现与上面的CopyOnWriteArrayList的实现方式一致
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与Map不安全
Map是一种 k,v 数据结构,初始化大小为16,扩容因子为0.75,当达到扩容因子设置的阈值(16*0.75=12)时,大小会扩容一倍(即从16扩容至32)
jdk1.7时HashMap的实现是经典的哈希表实现,即数组+链表;从1.8开始,实现变成了数组+链表+红黑树
Map<String,String> map = new HashMap<>();
//Map<String,String> map = new LinkedHashMap<>();
//Map<String,String> map = new TreeMap<>();
map.put("a","aaa");
map.put("b","bbb");
map.put("c","ccc");
System.out.println(map);
使用这几种Map运行上面的这段程序,永远不会出问题
老生常谈,多线程情况下
//HashMap TreeMap LinkedHashMap 均会出现ConcurrentModificationException的错误
Map<String,String> map = new LinkedHashMap<>();
for(int i = 0;i < 100;i++){
new Thread(()->{
map.put(UUID.randomUUID().toString().substring(0,2),UUID.randomUUID().toString().substring(0,2));
System.out.println(map);
},"thread"+i
).start();
}
运行这段代码会出现ConcurrentModificationException的错误
方法一:使用HashTable解决
Map<String,String> map = new Hashtable<>();
for(int i = 0;i < 100;i++){
new Thread(()->{
map.put(UUID.randomUUID().toString().substring(0,2),UUID.randomUUID().toString().substring(0,2));
System.out.println(map);
},"thread"+i
).start();
}
HashTable是一个线程安全的Map
public synchronized V put(K key, V value)
查看源码发现它使用了 synchronized 关键字来解决多线程访问的问题
方法二:使用ConcurrentHashMap解决
Map<String,String> map = new ConcurrentHashMap<>();
for(int i = 0;i < 100;i++){
new Thread(()->{
map.put(UUID.randomUUID().toString().substring(0,2),UUID.randomUUID().toString().substring(0,2));
System.out.println(map);
},"thread"+i
).start();
}
ConcurrentHashMap使用的是分段锁来保证多线程情况下可以并发的执行put操作