文章目录

  • 1.集合类不安全之并发修改异常
  • 2.集合类不安全之写时复制
  • 方案三:采用JUC里面的方法(推荐)
  • 3.集合类不安全之Set
  • 4.集合类不安全之Map


1.集合类不安全之并发修改异常

我们知道arrayList是线程不安全的,请编写一个不安全的案例以及给出解决方案

import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.UUID;
import java.util.Vector;

public class ArrayListNotSafeDemo {
	public static void main(String[] args) {
        List<String> list = new ArrayList<>();
        //List<String> list = new Vector<>();
        //List<String> list = Collections.synchronizedList(new ArrayList<>());

        for (int i = 0; i < 30; i++) {
            new Thread(() -> {
                list.add(UUID.randomUUID().toString().substring(0, 8));
                System.out.println(list);
            }, String.valueOf(i)).start();
        }
	}
}

上述程序会抛java.util.ConcurrentModificationException

Java代码审查不安全的json反序列化 java util random不安全_java


解决方案

方案一:Vector

第一种方法,就是不用ArrayList这种不安全的List实现类,而采用Vector,线程安全的
关于Vector如何实现线程安全的,而是在方法上加了锁,即synchronized

Java代码审查不安全的json反序列化 java util random不安全_ci_02


这样就每次只能够一个线程进行操作,所以不会出现线程不安全的问题,但是因为加锁了,导致并发性基于下降。

方案二:Collections.synchronized()

采用Collections集合工具类,在ArrayList外面包装一层 同步 机制

2.集合类不安全之写时复制

上一节程序导致抛java.util.ConcurrentModificationException的原因解析
先观察下抛错打印栈堆信息:

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.lun.collection.ArrayListNotSafeDemo.lambda$0(ArrayListNotSafeDemo.java:20)
	at java.lang.Thread.run(Thread.java:748)

可看出toString(),Itr.next(),Itr.checkForComodification()后抛出异常,那么看看它们next(),checkForComodification()源码:

public class ArrayList<E> extends AbstractList<E>
        implements List<E>, RandomAccess, Cloneable, java.io.Serializable{
    
    ...
    
	private class Itr implements Iterator<E> {
        int cursor;       // index of next element to return
        int lastRet = -1; // index of last element returned; -1 if no such
        int expectedModCount = modCount;//modCount在AbstractList类声明

        Itr() {}

        ...

        @SuppressWarnings("unchecked")
        public E next() {
            checkForComodification();
			...
        }

        final void checkForComodification() {
            if (modCount != expectedModCount)
                throw new ConcurrentModificationException();//<---异常在此抛出
        }
    }
    
    
    public boolean add(E e) {
        ensureCapacityInternal(size + 1);  // Increments modCount!!
        elementData[size++] = e;
        return true;
    }
    
    private void ensureCapacityInternal(int minCapacity) {
        ensureExplicitCapacity(calculateCapacity(elementData, minCapacity));
    }

    private void ensureExplicitCapacity(int minCapacity) {
        modCount++;//添加时,修改了modCount的值

        // overflow-conscious code
        if (minCapacity - elementData.length > 0)
            grow(minCapacity);
    }
    
	...
}
public abstract class AbstractList<E> extends AbstractCollection<E> implements List<E> {

    ...

    protected transient int modCount = 0;
    
    ...
}

modCount具体详细说明如下:

Java代码审查不安全的json反序列化 java util random不安全_java_03


此列表在结构上被修改的次数。结构修改是那些改变列表大小的修改,或者以其他方式扰乱它,以致正在进行的迭代可能会产生不正确的结果。

该字段由迭代器和列表迭代器方法返回的迭代器和列表迭代器实现使用。如果此字段的值意外更改,则迭代器(或列表迭代器)将抛出 ConcurrentModificationException 以响应 next、remove、previous、set 或 add 操作。这提供了快速失败行为,而不是在迭代期间面对并发修改时的非确定性行为。

综上所述,假设线程A将通过迭代器next()获取下一元素时,从而将其打印出来。但之前,其他某线程添加新元素至list,结构发生了改变,modCount自增。当线程A运行到checkForComodification(),expectedModCount是modCount之前自增的值,判定modCount != expectedModCount为真,继而抛出ConcurrentModificationException。

方案三:采用JUC里面的方法(推荐)

CopyOnWriteArrayList:写时复制,主要是一种读写分离的思想
写时复制,CopyOnWrite容器即写时复制的容器,往一个容器中添加元素的时候,不直接往当前容器Object[]添加,而是先将Object[]进行copy,复制出一个新的容器object[] newElements,然后新的容器Object[] newElements里添加原始,添加元素完后,在将原容器的引用指向新的容器 setArray(newElements);这样做的好处是可以对copyOnWrite容器进行并发的读 ,而不需要加锁,因为当前容器不需要添加任何元素。所以CopyOnWrite容器也是一种读写分离的思想,读和写不同的容器

就是写的时候,把ArrayList扩容一个出来,然后把值填写上去,在通知其他的线程,ArrayList的引用指向扩容后的

public class CopyOnWriteArrayList<E>
    implements List<E>, RandomAccess, Cloneable, java.io.Serializable {

    /** The array, accessed only via getArray/setArray. */
    private transient volatile Object[] array;
    
    final Object[] getArray() {
        return array;
    }

    final void setArray(Object[] a) {
        array = a;
    }
    
    ...
    
	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();
        }
    }
    
    ...
    
    public String toString() {
        return Arrays.toString(getArray());
    }
    
    ...
}

查看底层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();
        }

首先需要加锁

final ReentrantLock lock = this.lock;
lock.lock();

然后在末尾扩容一个单位

Object[] elements = getArray();
int len = elements.length;
Object[] newElements = Arrays.copyOf(elements, len + 1);

然后在把扩容后的空间,填写上需要add的内容

newElements[len] = e;

最后把内容set到Array中

3.集合类不安全之Set

HashSet也是非线性安全的。(HashSet内部是包装了一个HashMap的)

HashSet底层结构

同理HashSet的底层结构就是HashMap

Java代码审查不安全的json反序列化 java util random不安全_ci_04


但是为什么我调用 HashSet.add()的方法,只需要传递一个元素,而HashMap是需要传递key-value键值对?

首先我们查看hashSet的add方法

public boolean add(E e) {
        return map.put(e, PRESENT)==null;
    }

我们能发现但我们调用add的时候,存储一个值进入map中,只是作为key进行存储,而value存储的是一个Object类型的常量,也就是说HashSet只关心key,而不关心value
HashMap线程不安全
同理HashMap在多线程环境下,也是不安全的

import java.util.Collections;
import java.util.HashSet;
import java.util.Set;
import java.util.UUID;
import java.util.concurrent.CopyOnWriteArraySet;

public class SetNotSafeDemo {
	public static void main(String[] args) {
		
		Set<String> set = new HashSet<>();
		//Set<String> set = Collections.synchronizedSet(new HashSet<>());
		//Set<String> set = new CopyOnWriteArraySet<String>();
		
        for (int i = 0; i < 30; i++) {
            new Thread(() -> {
                set.add(UUID.randomUUID().toString().substring(0, 8));
                System.out.println(set);
            }, String.valueOf(i)).start();
        }
	}	}

解决方法:
①Collections.synchronizedSet(new HashSet<>())
②CopyOnWriteArraySet<>()(推荐)

4.集合类不安全之Map

import java.util.Collections;
import java.util.HashMap;
import java.util.Hashtable;
import java.util.Map;
import java.util.UUID;
import java.util.concurrent.ConcurrentHashMap;

public class MapNotSafeDemo {

	public static void main(String[] args) {
        Map<String, String> map = new HashMap<>();
//        Map<String, String> map = Collections.synchronizedMap(new HashMap<>());
//		Map<String, String> map = new ConcurrentHashMap<>();
//		Map<String, String> map = new Hashtable<>();
        for (int i = 0; i < 30; i++) {
            new Thread(() -> {
                map.put(Thread.currentThread().getName(), UUID.randomUUID().toString().substring(0, 8));
                System.out.println(map);
            }, String.valueOf(i)).start();
        }

	}

}

解决方法:

  1. HashTable
  2. Collections.synchronizedMap(new HashMap<>())
  3. ConcurrencyMap<>()(推荐)