ArrayList、HashSet、HashMap是线程不安全
1. ArrayList是线程不安全的,请编写一个不安全的案例并给出解决方案
1.1 ArrayList线程不安全
private static void NotSafe() {
List<String> list = new ArrayList<>();//ArrayList没锁,不安全
for (int i = 0; i <20 ; i++) {
new Thread(()->{
list.add(UUID.randomUUID().toString().substring(0,8));
System.out.println(list);
},String.valueOf(i)).start();
}
//java.util.Concurrent Modification Exception(字面意思:并发修改异常)
}
部分运行结果:
Exception in thread "14" Exception in thread "17"
java.util.ConcurrentModificationException
1.2 为什么会发生这种情况
并发正常修改缘由:
一个人正在写入,另一个人又来抢夺,导致数据不一致,并发修改异常。
1.3 三种解决办法
public class ContainerNotSateDemo {
public static void main(String[] args) {
// List<String> list = new ArrayList<>();//ArrayList没锁,不安全
//1. List<String> list = new Vector<>();
//使用辅助类
//2. List<String> list = Collections.synchronizedList(new ArrayList<>());
//3.写实复制,读写分离(适合读多写少的操作,内存消耗可能也比较大)就是读和写的容器不一样,同时写的话,前一个会加锁,导致后一个等待
List<String> list = new CopyOnWriteArrayList<>();
for (int i = 0; i <20 ; i++) {
new Thread(()->{
list.add(UUID.randomUUID().toString().substring(0,8));
System.out.println(list);
},String.valueOf(i)).start();
}
//java.util.ConcurrentModificationException并发修改异常
}
}
- 对于Vector的add方法添加了加了synchronized,所以保证了在多线程的安全
public synchronized boolean add(E e) {
modCount++;
ensureCapacityHelper(elementCount + 1);
elementData[elementCount++] = e;
return true;
}
-
使用辅助类Collections.synchronizedList()通过synchronized创建安全的ArrayLis。
-
CopyOnWriteArrayList.add方法:
CopyOnWrite容器即写时复制,往一个元素添加容器的时候,不直接往当前容器Object[]添加,而是先将当前容器Object[]进行copy,复制出一个新的容器Object[] newElements,让后新的容器添加元素,添加完元素之后,再将原容器的引用指向新的容器setArray(newElements),这样做可以对CopyOnWrite容器进行并发的读,而不需要加锁,因为当前容器不会添加任何元素,所以CopyOnWrite容器也是一种读写分离的思想,读和写不同的容器。
具体代码:
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();
}
}
2. HashSet是线程不安全的
问题和解决方法的代码和上方的类似,不过少了一种解决方法,只有两种。
private static void NotSafe1() {
Set<String> list=new HashSet<>();
// Set<String> list=Collections.synchronizedSet(new HashSet<>());
// Set<String> list=new CopyOnWriteArraySet<>();
for (int i = 0; i <20 ; i++) {
new Thread(()->{
list.add(UUID.randomUUID().toString().substring(0,8));
System.out.println(list);
},String.valueOf(i)).start();
}
}
了解一下HashSet
- HashSet的底层是HashMap,创建了一个底层容量大小为16,负载原子是0.75的对象(其负载原子的作用在于当容量大于16*0.75的时候,HashSet会自动进行扩容,16为一个不断可以不断更新的size())
public HashSet(Collection<? extends E> c) {
map = new HashMap<>(Math.max((int) (c.size()/.75f) + 1, 16));
addAll(c);
}
- HashSet的add()方法,只有一个变量,为底层HashMap中的key,而HashMap中的value则是HashSet类中中的一个present的常量对象,是一个恒定的。
public HashSet() {
map = new HashMap<>();
}
public boolean add(E e) {
return map.put(e, PRESENT)==null;
}
3. HashMap是线程不安全的
对比上方的内容,HashMap也有两种类似的可以实现线程安全的方法。
private static void NotSafe2() {
//Map<String, String> map = new HashMap<>();
// Map<String,String> map=new ConcurrentHashMap<>();
Map<String,String> map=Collections.synchronizedMap(new HashMap<>());
for (int i = 0; i <20 ; i++) {
new Thread(()->{
map.put(Thread.currentThread().getName(),
UUID.randomUUID().toString().substring(0,8));
System.out.println(map);
},String.valueOf(i)).start();
}
}
- 为什么HashMap是线程不安全的呢?
- 在多线程的情况下,两个线程同时去执行同一样操作,加入数put操作,有可能同时存储,导致数据的覆盖问题。
- 扩容的时候,可能会出现死循环的情况。原因:重新定位每一桶的下标,并采用头插法将元素迁移到新数组中,而头插法会将链表的顺序翻转,a、b两个线程同时进行扩容操作,会形成换链表。
来个面试题—Arraylist 与 LinkedList 区别?
来个面试题爽一下。