作为Java开发者,集合框架是日常开发的“高频工具包”——从简单的列表存储,到高并发场景下的键值对操作,ArrayList、HashMap几乎贯穿所有业务代码。

但很多人只停留在“会用”层面,遇到这些问题就卡壳:

•多线程下用HashMap为什么会丢数据、抛异常?

•ArrayList和LinkedList,到底该怎么选才高效?

•ConcurrentHashMap是如何保证线程安全,还能提升并发性能的?

这篇文章避开晦涩理论,聚焦「面试高频考点+实战落地」,从底层实现、核心特性、线程安全三个维度,拆解4个最常用的集合类,搭配可直接复制运行的代码示例,帮你彻底吃透背后逻辑,面试不慌、开发不踩坑。

一、ArrayList & LinkedList:列表的两种核心实现(底层+对比)

列表是我们最常用的集合类型,用于存储有序元素,但ArrayListLinkedList的底层实现截然不同,导致它们的性能差异极大,选型错误会直接影响代码效率。

1. ArrayList:基于动态数组的快速访问王者

核心定位:读多写少、需要随机访问场景的首选,底层是「Object[] 动态数组」,JDK8后采用懒加载(首次添加元素才初始化数组)。

关键特性(必记):

初始容量:默认10,扩容机制为「原容量的1.5倍」(公式:newCapacity = oldCapacity + (oldCapacity >> 1)),扩容时会复制原数组元素到新数组;

访问效率:O(1) 高效,通过下标直接定位元素(比如list.get(1)直接获取);

增删效率:尾部增删快(O(1)),中间/头部增删慢(O(n)),需要移动后续所有元素。

代码示例:ArrayList核心特性演示(可直接运行)

    import java.util.ArrayList;
    import java.lang.reflect.Field;
    
    public class ArrayListDemo {
        public static void main(String[] args) {
            // 1. 初始化ArrayList(懒加载,此时底层数组未初始化)
            ArrayList<String> list = new ArrayList<>();
    
            // 2. 尾部添加元素(高效,无需移动元素)
            list.add("Java");
            list.add("Python");
            list.add("Go");
            System.out.println("尾部添加后:" + list); // 输出:[Java, Python, Go]
    
            // 3. 随机访问(O(1),直接通过下标定位)
            String element = list.get(1);
            System.out.println("下标1的元素:" + element); // 输出:Python
    
            // 4. 中间插入元素(低效,需移动后续元素)
            list.add(1, "C++");
            System.out.println("中间插入后:" + list); // 输出:[Java, C++, Python, Go]
    
            // 5. 查看底层数组容量(通过反射演示,直观看到扩容机制)
            try {
                Field field = ArrayList.class.getDeclaredField("elementData");
                field.setAccessible(true); // 打破封装,获取私有数组
                Object[] array = (Object[]) field.get(list);
                System.out.println("当前底层数组容量:" + array.length); // 初始容量10,未扩容时输出10
            } catch (Exception e) {
                e.printStackTrace();
            }
        }
    }

    2. LinkedList:基于双向链表的增删能手

    核心定位:增删频繁(尤其是中间/头部)场景的首选,底层是「双向链表」,无固定容量限制。

    关键特性(必记):

    底层结构:由Node节点组成,每个节点包含 prev(前驱节点)、next(后继节点)、item(存储元素);

    扩容机制:无扩容操作,新增元素只需创建新节点,修改前后节点的指针即可;

    访问效率:O(n) 低效,随机访问(比如list.get(5))需从头/尾遍历到目标位置;

    增删效率:O(1) 高效,只需修改节点指针,无需移动大量元素。

    代码示例:LinkedList 核心特性演示(可直接运行)

      import java.util.LinkedList;
      public class LinkedListDemo {
          
      public static void main(String[] args) {
              
        LinkedList<String> list = new LinkedList<>();
                     
        // 1. 头部添加元素(高效,仅修改头指针)       
        list.addFirst("B");
              
        list.addFirst("A");
                      
        // 2. 尾部添加元素
        list.addLast("C");
              
        System.out.println("初始链表:" + list); // 输出:[A, B, C]
                      
        // 3. 中间插入元素(高效,仅修改前后节点指针)       
        list.add(2, "D");
              
        System.out.println("中间插入后:" + list); // 输出:[A, B, D, C]     
              
        // 4. 头部/尾部删除元素(高效)
        list.removeFirst();
              
        list.removeLast();
              
         System.out.println("删头尾后:" + list); // 输出:[B, D]        
              
         // 5. 随机访问(低效,需遍历)        
         String elem = list.get(1);
              
         System.out.println("下标1的元素:" + elem); // 输出:D
          
        }
      }

      3. 重点:两者的线程安全性分析

      核心结论:ArrayListLinkedList 均为线程不安全!

      原因:底层操作(addremoveget)未加锁,多线程并发修改时,会出现「数据丢失」「并发修改异常(ConcurrentModificationException)」等问题。

      代码示例:ArrayList 线程不安全演示(复现场景)

      import java.util.ArrayList;
      import java.util.concurrent.CountDownLatch;
      public class ArrayListUnsafeDemo {
          
      // 共享的ArrayList(多线程共同修改)    
      private static final ArrayList<Integer> list = new ArrayList<>();
          
      // 计数器:等待10个线程全部执行完毕    
      private static final CountDownLatch latch = new CountDownLatch(10);
          
      public static void main(String[] args) throws InterruptedException {
              
       // 启动10个线程,每个线程添加1000个元素       
       for (int i = 0; i < 10; i++) {
                  
          new Thread(() -> {
                      
            for (int j = 0; j < 1000; j++) {
                          
              list.add(j);
                      
            }
                      
             latch.countDown(); // 线程执行完毕,计数器减1
                  
           }).start();
              
      }
                      
       latch.await(); // 等待所有线程执行完成        
       // 理想大小:10*1000=10000,实际会小于10000(数据丢失),甚至抛出异常        
       System.out.println("最终集合大小:" + list.size());
          
       }
      }

      线程安全替代方案(实战推荐)

      CopyOnWriteArrayList(首选):写时复制容器,新增/删除时复制一份新数组,修改完成后替换原数组,读操作无锁,「读多写少」场景(如配置缓存)性能极佳;

      Vector(不推荐):ArrayList的线程安全版本,所有方法加synchronized(锁整个对象),并发性能极低,仅兼容老代码时使用。

      二、HashMap & ConcurrentHashMap:哈希表的线程安全对决

      哈希表用于存储键值对(key-value),是Java开发中最常用的集合之一,HashMap是单线程首选,但多线程场景下必须用ConcurrentHashMap,两者的底层差异和线程安全机制是面试高频考点。

      1. HashMap:单线程高效,多线程踩坑重灾区JDK8底层)

      核心定位:单线程/无并发修改场景的键值对存储首选,底层是「数组+链表+红黑树」(解决哈希冲突)。

      关键特性(必记):

      存储结构:数组(桶)链表(哈希冲突时挂载元素),当链表长度≥8且数组容量≥64时,链表转为红黑树(查询效率从O(n)提升到O(logn));

      哈希计算:hash(key) = (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16),再通过(n-1) & hash确定元素在数组中的下标;

      扩容机制:容量(默认16)达到阈值(容量×负载因子0.75)时,扩容为原容量的2倍;

      线程安全:完全不安全,多线程并发put、扩容时,会出现「数据丢失、数据覆盖、链表成环(死循环)」等问题。

      代码示例:HashMap 线程不安全演示

        import java.util.HashMap;
        import java.util.concurrent.CountDownLatch;
         
        public class HashMapUnsafeDemo {
            // 共享的HashMap
            private static final HashMap<Integer, Integer> map = 
              new HashMap<>();
            private static final CountDownLatch latch =
              new CountDownLatch(10);
         
            public static void main(String[] args)
            throws InterruptedException {
                // 10个线程,每个线程put 1000个键值对
                for (int i = 0; i < 10; i++) {
                    int threadId = i;
                    new Thread(() -> {
                        for (int j = 0; j < 1000;j++) {
                            // 键唯一:threadId*1000 + j,避免主动覆盖
                            map.put(threadId * 1000 +j, j);
                        }
                        latch.countDown();
                    }).start();
                }
                
                latch.await();
                // 理想大小:10000,实际会小于10000(数据丢失)
                System.out.println("最终Map大小:" + map.size());
            }
        }

        2. ConcurrentHashMap:高并发场景的安全首选JDK8底层)

        核心定位:多线程并发读写键值对的首选,底层和HashMap一致(数组+链表+红黑树),但通过优化锁机制,实现高效线程安全。

        关键特性(必记,面试重点):

        线程安全机制(JDK8):摒弃JDK7分段锁,采用「CAS + 桶级synchronized锁」,仅锁定当前操作的桶(数组元素),不影响其他桶的操作,并发性能大幅提升;

        无锁操作:新增元素时,先通过CAS尝试插入,失败后再对当前桶加锁,减少锁竞争;

        扩容机制:支持多线程协助扩容,提升扩容效率,避免单线程扩容阻塞;

        细节:不允许null/值(HashMap允许1null键),避免多线程场景下的歧义。

        代码示例:ConcurrentHashMap 线程安全演示

          import java.util.concurrent.ConcurrentHashMap;
          import java.util.concurrent.CountDownLatch;
          
          public class ConcurrentHashMapSafeDemo {
              // 共享的ConcurrentHashMap
              private static final ConcurrentHashMap<Integer, Integer&gt; map = new ConcurrentHashMap<>();
              private static final CountDownLatch latch = new CountDownLatch(10);
          
              public static void main(String[] args) throws InterruptedException {
                  // 10个线程,每个线程put 1000个键值对
                  for (int i = 0; i < 10; i++) {
                      int threadId = i;
                      new Thread(() -> {
                          for (int j = 0; j < 1000; j++) {
                              map.put(threadId * 1000 + j, j);
                          }
                          latch.countDown();
                      }).start();
                  }
          
                  latch.await();
                  // 稳定输出10000,无数据丢失、无异常
                  System.out.println("最终Map大小:" + map.size());
              }
          }

          3. HashMap vs ConcurrentHashMap 核心对比(面试必背)

          对比维度

          HashMap

          ConcurrentHashMap

          线程安全

          不安全

          安全

          锁机制

          无锁

          CAS + 桶级synchronized

          扩容方式

          单线程扩容

          多线程协助扩容

          null/

          允许(1null键)

          不允许

          并发性能

          高(单线程),多线程下极差

          高(多线程,锁粒度小)

          三、实战选型指南(直接套用,避免踩坑)

          结合前面的底层和性能分析,整理了日常开发中最常用的选型场景,直接对照使用即可:

          1. 列表选型(ArrayList/LinkedList

          读多写少、需要随机访问(比如分页查询结果存储)→ ArrayList

          增删频繁(比如队列、栈,频繁在头部/中间操作)→ LinkedList

          多线程场景 → CopyOnWriteArrayList(读多写少首选),避免用Vector

          2. 哈希表选型(HashMap/ConcurrentHashMap

          单线程、无并发修改(比如本地缓存、临时键值对存储)→ HashMap

          多线程并发读写(比如分布式缓存本地副本、全局计数器)→ ConcurrentHashMap

          低并发线程安全场景(不推荐)→ Collections.synchronizedMap(new HashMap<>())(锁整个对象,效率低)。

          四、总结(面试速记版)

          1. 列表核心:ArrayList(数组)快速访问,LinkedList(双向链表)快速增删,两者均线程不安全,多线程用CopyOnWriteArrayList

          2. 哈希表核心:HashMap(数组+链表+红黑树)单线程高效,线程不安全;ConcurrentHashMapCAS+桶级锁实现安全,高并发首选;

          3. 选型关键:先判断是否有并发需求,再根据读多写少还是增删频繁选择具体实现。

          Logo

          汇聚全球AI编程工具,助力开发者即刻编程。

          更多推荐