一、ArrayList

1、初步小理解

默认值是多少?

(1)查看源码可知默认值是10,但是没找到初始化为10的代码?

  1. private static final Object[] DEFAULTCAPACITY_EMPTY_ELEMENTDATA = {};
  2. /**
  3. * Constructs an empty list with an initial capacity of ten.
  4. */
  5. public ArrayList() {
  6. this.elementData = DEFAULTCAPACITY_EMPTY_ELEMENTDATA;
  7. }

(2)查看新增方法,在calculateCapacity中可知默认的容量是10。

  1. public boolean add(E e) {
  2. ensureCapacityInternal(size + 1); // Increments modCount!!
  3. elementData[size++] = e;
  4. return true;
  5. }
  6. // 确定容量
  7. private void ensureCapacityInternal(int minCapacity) {
  8. ensureExplicitCapacity(calculateCapacity(elementData, minCapacity));
  9. }
  10. // 判断是否需要扩容
  11. private void ensureExplicitCapacity(int minCapacity) {
  12. modCount++;
  13. // 如果最小需要空间比elementData的内存空间要大,则需要扩容
  14. if (minCapacity - elementData.length > 0)
  15. //扩容
  16. grow(minCapacity);
  17. }
  18. // 计算出容量,小于10,则返回10(可知默认的容量是10)
  19. private static final int DEFAULT_CAPACITY = 10;
  20. private static int calculateCapacity(Object[] elementData, int minCapacity) {
  21. if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA) {
  22. return Math.max(DEFAULT_CAPACITY, minCapacity);
  23. }
  24. return minCapacity;
  25. }
  26. // 进行扩容 1.5倍
  27. private void grow(int minCapacity) {
  28. // 获取到ArrayList中elementData数组的内存空间长度
  29. int oldCapacity = elementData.length;
  30. // 扩容至原来的1.5倍
  31. int newCapacity = oldCapacity + (oldCapacity >> 1);
  32. // 再判断一下新数组的容量够不够,够了就直接使用这个长度创建新数组,
  33. // 不够就将数组长度设置为需要的长度
  34. if (newCapacity - minCapacity < 0)
  35. newCapacity = minCapacity;
  36. //若预设值大于默认的最大值检查是否溢出
  37. if (newCapacity - MAX_ARRAY_SIZE > 0)
  38. newCapacity = hugeCapacity(minCapacity);
  39. // 调用Arrays.copyOf方法将elementData数组指向新的内存空间时newCapacity的连续空间
  40. // 并将elementData的数据复制到新的内存空间
  41. elementData = Arrays.copyOf(elementData, newCapacity);
  42. }

由上述代码也可以看出,扩容是扩容到原来的1.5倍

为什么线程不安全

add方法没有加synchronized

线程不安全示例

  1. public class NoSafeDemo {
  2. public static void main(String[] args) {
  3. List<String> list = new ArrayList<>();
  4. for (int i = 0; i <= 30; i++) {
  5. new Thread(()->{
  6. list.add(UUID.randomUUID().toString().substring(0,9));
  7. System.out.println(list);
  8. }).start();
  9. }
  10. // 报错:java.util.ConcurrentModificationException 线程不安全常见的异常(并发修改异常)
  11. // 报错原因:并发争抢修改导致
  12. }
  13. }

报错位置如下,可知

  • modCount是ArrayList中的一个成员变量。它表示该集合实际被修改的次数
  • expectedModCount 是 ArrayList中的一个内部类——Itr中的成员变量。expectedModCount表示这个迭代器期望该集合被修改的次数。其值是在ArrayList.iterator方法被调用的时候初始化的。只有通过迭代器对集合进行操作,该值才会改变。

A线程 add走到checkForComodification()的时候,B线程刚add改变了modCount的值,就会出现modCount != expectedModCount

  1. final void checkForComodification() {
  2. if (modCount != expectedModCount)
  3. throw new ConcurrentModificationException();
  4. }
  5. private class Itr implements Iterator<E> {
  6. int cursor; // index of next element to return
  7. int lastRet = -1; // index of last element returned; -1 if no such
  8. // =========关注这里
  9. int expectedModCount = modCount;
  10. Itr() {}
  11. public boolean hasNext() {
  12. return cursor != size;
  13. }
  14. @SuppressWarnings("unchecked")
  15. public E next() {
  16. checkForComodification();
  17. int i = cursor;
  18. if (i >= size)
  19. throw new NoSuchElementException();
  20. Object[] elementData = ArrayList.this.elementData;
  21. if (i >= elementData.length)
  22. throw new ConcurrentModificationException();
  23. cursor = i + 1;
  24. return (E) elementData[lastRet = i];
  25. }

变成线程安全

1、使用方法Collections.synchronizedList()
  1. public class SafeDemo2 {
  2. public static void main(String[] args) {
  3. List<String> list = Collections.synchronizedList(new ArrayList<>());
  4. for (int i = 0; i <= 30; i++) {
  5. new Thread(()->{
  6. list.add(UUID.randomUUID().toString().substring(0,9));
  7. System.out.println(list);
  8. }).start();
  9. }
  10. }
  11. }

这个方法会返回一个新的同步list,如下。

  1. public static <T> List<T> synchronizedList(List<T> list) {
  2. return (list instanceof RandomAccess ?
  3. new SynchronizedRandomAccessList<>(list) :
  4. new SynchronizedList<>(list));
  5. }

该方法的新建和查询有一个互斥锁(mutex),每次只能要么查询要么新建

  1. public boolean add(E e) {
  2. synchronized (mutex) {return c.add(e);}
  3. }
  4. public E get(int index) {
  5. synchronized (mutex) {return list.get(index);}
  6. }

2、使用java.util.concurrent包中的CopyOnWriteArrayList,写时复制
  1. public class SafeDemo3 {
  2. public static void main(String[] args) {
  3. List<String> list = new CopyOnWriteArrayList<>();
  4. for (int i = 0; i <= 30; i++) {
  5. new Thread(()->{
  6. list.add(UUID.randomUUID().toString().substring(0,9));
  7. System.out.println(list);
  8. }).start();
  9. }
  10. }
  11. }

参考源码add()

  1. public boolean add(E e) {
  2. final ReentrantLock lock = this.lock;
  3. lock.lock();
  4. try {
  5. Object[] elements = getArray();
  6. int len = elements.length;
  7. Object[] newElements = Arrays.copyOf(elements, len + 1);
  8. newElements[len] = e;
  9. setArray(newElements);
  10. return true;
  11. } finally {
  12. lock.unlock();
  13. }
  14. }
  15. public E get(int index) {
  16. return get(getArray(), index);
  17. }

CopyOnWrite容器即写时复制容器,往一个容器中添加元素的时候,不直接往当前容器Object[]添加,而是先将当前容器进行copy,复制出一个新容器 Object[] newElements,新容器容量比原容器大1,将新元素放入新容器的最后,最后将原容器的引用指向新容器setArray(newElements);这样做的好处是可以对CopyOnWrite容器进行并发读,而不需要加锁,因为当前容器不会添加任何元素,所以CopyOnWrite容器也是一种读写分离的思想,读和写时不同的容器。写的时候加锁,保证线程安全。

注意和ArrayList增加元素的区别:ArrayList是先设置好一个 具备容量的数组,然后将元素放进去;而CopyOnWriteArrayList是直接在新的数组里添加元素,然后将老数组的引用指向新数组。