MK
摩柯社区 - 一个极简的技术知识社区
AI 面试

Java并发集合类与传统集合的比较

2022-07-103.5k 阅读

Java集合体系概述

在深入探讨Java并发集合类与传统集合的比较之前,先简要回顾一下Java集合体系。Java集合框架为开发者提供了一组丰富的数据结构,用于存储和操作数据。它主要分为两大类:Collection和Map。

Collection

Collection是一个接口,它是List、Set和Queue接口的父接口。

  • List:有序且允许重复元素的集合,如ArrayList和LinkedList。ArrayList基于数组实现,适合随机访问;LinkedList基于链表实现,适合频繁的插入和删除操作。
  • Set:无序且不允许重复元素的集合,典型实现有HashSet和TreeSet。HashSet基于哈希表实现,查询效率高;TreeSet基于红黑树实现,可对元素进行排序。
  • Queue:用于存储等待处理的元素,通常遵循FIFO(先进先出)原则,常见实现有PriorityQueue等。

Map

Map用于存储键值对,一个键最多映射到一个值。常见的实现类有HashMap、TreeMap和Hashtable。HashMap基于哈希表,允许null键和null值;TreeMap基于红黑树,可按键排序;Hashtable是线程安全的,不允许null键和null值。

传统集合的线程安全问题

传统的Java集合类,如ArrayList、HashMap等,在多线程环境下使用时,会出现线程安全问题。这是因为这些类的方法并非线程安全的,当多个线程同时访问和修改这些集合时,可能会导致数据不一致、丢失更新等问题。

示例1:ArrayList在多线程环境下的问题

import java.util.ArrayList;
import java.util.List;

public class ArrayListThreadSafetyExample {
    private static List<Integer> list = new ArrayList<>();

    public static void main(String[] args) {
        Thread thread1 = new Thread(() -> {
            for (int i = 0; i < 1000; i++) {
                list.add(i);
            }
        });

        Thread thread2 = new Thread(() -> {
            for (int i = 1000; i < 2000; i++) {
                list.add(i);
            }
        });

        thread1.start();
        thread2.start();

        try {
            thread1.join();
            thread2.join();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

        System.out.println("List size: " + list.size());
    }
}

在上述代码中,两个线程同时向ArrayList中添加元素。由于ArrayList不是线程安全的,最终输出的列表大小可能不是2000,因为在多线程操作过程中可能会出现数据竞争。

示例2:HashMap在多线程环境下的问题

import java.util.HashMap;
import java.util.Map;

public class HashMapThreadSafetyExample {
    private static Map<Integer, String> map = new HashMap<>();

    public static void main(String[] args) {
        Thread thread1 = new Thread(() -> {
            for (int i = 0; i < 1000; i++) {
                map.put(i, "Value" + i);
            }
        });

        Thread thread2 = new Thread(() -> {
            for (int i = 1000; i < 2000; i++) {
                map.put(i, "Value" + i);
            }
        });

        thread1.start();
        thread2.start();

        try {
            thread1.join();
            thread2.join();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

        System.out.println("Map size: " + map.size());
    }
}

同样,HashMap在多线程环境下也会出现问题。多个线程同时对HashMap进行插入操作时,可能会导致哈希表的结构损坏,从而引发异常或数据丢失。

传统集合的线程安全解决方案

为了在多线程环境下安全地使用传统集合,可以采取以下几种方法。

使用Collections.synchronizedXXX方法

Java的Collections类提供了一些静态方法,如synchronizedListsynchronizedMap等,用于创建线程安全的集合包装。

示例3:使用Collections.synchronizedList

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

public class SynchronizedListExample {
    private static List<Integer> list = Collections.synchronizedList(new ArrayList<>());

    public static void main(String[] args) {
        Thread thread1 = new Thread(() -> {
            for (int i = 0; i < 1000; i++) {
                synchronized (list) {
                    list.add(i);
                }
            }
        });

        Thread thread2 = new Thread(() -> {
            for (int i = 1000; i < 2000; i++) {
                synchronized (list) {
                    list.add(i);
                }
            }
        });

        thread1.start();
        thread2.start();

        try {
            thread1.join();
            thread2.join();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

        System.out.println("List size: " + list.size());
    }
}

在上述代码中,通过Collections.synchronizedList创建了一个线程安全的ArrayList。在对列表进行操作时,需要手动同步,以确保线程安全。

使用Hashtable

Hashtable是一个线程安全的Map实现,它的方法都使用synchronized关键字进行同步。

示例4:使用Hashtable

import java.util.Hashtable;
import java.util.Map;

public class HashtableExample {
    private static Map<Integer, String> map = new Hashtable<>();

    public static void main(String[] args) {
        Thread thread1 = new Thread(() -> {
            for (int i = 0; i < 1000; i++) {
                map.put(i, "Value" + i);
            }
        });

        Thread thread2 = new Thread(() -> {
            for (int i = 1000; i < 2000; i++) {
                map.put(i, "Value" + i);
            }
        });

        thread1.start();
        thread2.start();

        try {
            thread1.join();
            thread2.join();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

        System.out.println("Map size: " + map.size());
    }
}

虽然Hashtable保证了线程安全,但由于它对所有方法都进行同步,在高并发环境下性能较低。

Java并发集合类概述

为了解决传统集合在多线程环境下的性能和线程安全问题,Java提供了并发集合类,这些类位于java.util.concurrent包中。

并发List

  • CopyOnWriteArrayList:这是一个线程安全的List实现,它在修改操作(如添加、删除元素)时,会创建一个底层数组的副本,然后在副本上进行操作,最后将原数组引用指向新的副本。这种方式保证了读操作的高效性,因为读操作不需要加锁,但写操作相对较慢,因为需要复制数组。

并发Set

  • CopyOnWriteArraySet:基于CopyOnWriteArrayList实现的线程安全的Set,它保证了元素的唯一性,并且读操作性能较高。
  • ConcurrentSkipListSet:基于跳表实现的线程安全的有序Set,适合在需要对元素进行排序并且在多线程环境下使用的场景。

并发Map

  • ConcurrentHashMap:这是一个线程安全的高效Map实现。在Java 8之前,它使用分段锁技术,允许多个线程同时访问不同的段,从而提高并发性能。在Java 8及之后,它采用了CAS(Compare - And - Swap)和synchronized相结合的方式,进一步提升了性能。
  • ConcurrentSkipListMap:基于跳表实现的线程安全的有序Map,适用于需要按键排序的多线程场景。

CopyOnWriteArrayList与ArrayList比较

线程安全性

  • ArrayList:非线程安全,在多线程环境下需要手动同步,否则会出现数据竞争问题。
  • CopyOnWriteArrayList:线程安全,读操作不需要加锁,写操作通过复制数组来保证线程安全。

性能

  • 读性能:CopyOnWriteArrayList在读操作上性能较高,因为读操作直接访问数组,不需要加锁。而ArrayList在多线程环境下,如果不进行同步,读操作可能会读到不一致的数据。
  • 写性能:ArrayList的写操作性能通常较高,因为它直接在原数组上进行操作。而CopyOnWriteArrayList的写操作需要复制数组,开销较大,所以写性能相对较低。

示例5:CopyOnWriteArrayList与ArrayList性能对比

import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.CopyOnWriteArrayList;

public class ListPerformanceComparison {
    private static final int THREADS = 10;
    private static final int ITERATIONS = 100000;

    public static void main(String[] args) {
        List<Integer> arrayList = new ArrayList<>();
        List<Integer> copyOnWriteArrayList = new CopyOnWriteArrayList<>();

        long startTime = System.currentTimeMillis();
        Thread[] arrayListThreads = new Thread[THREADS];
        for (int i = 0; i < THREADS; i++) {
            arrayListThreads[i] = new Thread(() -> {
                for (int j = 0; j < ITERATIONS; j++) {
                    synchronized (arrayList) {
                        arrayList.add(j);
                    }
                }
            });
            arrayListThreads[i].start();
        }
        for (Thread thread : arrayListThreads) {
            try {
                thread.join();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
        long endTime = System.currentTimeMillis();
        System.out.println("ArrayList write time: " + (endTime - startTime) + " ms");

        startTime = System.currentTimeMillis();
        Thread[] copyOnWriteArrayListThreads = new Thread[THREADS];
        for (int i = 0; i < THREADS; i++) {
            copyOnWriteArrayListThreads[i] = new Thread(() -> {
                for (int j = 0; j < ITERATIONS; j++) {
                    copyOnWriteArrayList.add(j);
                }
            });
            copyOnWriteArrayListThreads[i].start();
        }
        for (Thread thread : copyOnWriteArrayListThreads) {
            try {
                thread.join();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
        endTime = System.currentTimeMillis();
        System.out.println("CopyOnWriteArrayList write time: " + (endTime - startTime) + " ms");

        startTime = System.currentTimeMillis();
        for (int i = 0; i < ITERATIONS; i++) {
            synchronized (arrayList) {
                arrayList.get(i);
            }
        }
        endTime = System.currentTimeMillis();
        System.out.println("ArrayList read time: " + (endTime - startTime) + " ms");

        startTime = System.currentTimeMillis();
        for (int i = 0; i < ITERATIONS; i++) {
            copyOnWriteArrayList.get(i);
        }
        endTime = System.currentTimeMillis();
        System.out.println("CopyOnWriteArrayList read time: " + (endTime - startTime) + " ms");
    }
}

通过上述代码的性能测试,可以明显看到CopyOnWriteArrayList在读操作上的优势以及写操作上的劣势。

CopyOnWriteArraySet与HashSet比较

线程安全性

  • HashSet:非线程安全,在多线程环境下需要额外同步机制来保证线程安全。
  • CopyOnWriteArraySet:线程安全,内部基于CopyOnWriteArrayList实现,读操作无锁,写操作通过复制数组保证线程安全。

性能

  • 读性能:CopyOnWriteArraySet在读操作上具有优势,无需加锁。HashSet在多线程环境下如果不进行同步,读操作可能获取到不一致数据。
  • 写性能:HashSet的写操作性能通常优于CopyOnWriteArraySet,因为后者写操作要复制数组。

示例6:CopyOnWriteArraySet与HashSet性能对比

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

public class SetPerformanceComparison {
    private static final int THREADS = 10;
    private static final int ITERATIONS = 100000;

    public static void main(String[] args) {
        Set<Integer> hashSet = new HashSet<>();
        Set<Integer> copyOnWriteArraySet = new CopyOnWriteArraySet<>();

        long startTime = System.currentTimeMillis();
        Thread[] hashSetThreads = new Thread[THREADS];
        for (int i = 0; i < THREADS; i++) {
            hashSetThreads[i] = new Thread(() -> {
                for (int j = 0; j < ITERATIONS; j++) {
                    synchronized (hashSet) {
                        hashSet.add(j);
                    }
                }
            });
            hashSetThreads[i].start();
        }
        for (Thread thread : hashSetThreads) {
            try {
                thread.join();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
        long endTime = System.currentTimeMillis();
        System.out.println("HashSet write time: " + (endTime - startTime) + " ms");

        startTime = System.currentTimeMillis();
        Thread[] copyOnWriteArraySetThreads = new Thread[THREADS];
        for (int i = 0; i < THREADS; i++) {
            copyOnWriteArraySetThreads[i] = new Thread(() -> {
                for (int j = 0; j < ITERATIONS; j++) {
                    copyOnWriteArraySet.add(j);
                }
            });
            copyOnWriteArraySetThreads[i].start();
        }
        for (Thread thread : copyOnWriteArraySetThreads) {
            try {
                thread.join();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
        endTime = System.currentTimeMillis();
        System.out.println("CopyOnWriteArraySet write time: " + (endTime - startTime) + " ms");

        startTime = System.currentTimeMillis();
        for (int i = 0; i < ITERATIONS; i++) {
            synchronized (hashSet) {
                hashSet.contains(i);
            }
        }
        endTime = System.currentTimeMillis();
        System.out.println("HashSet read time: " + (endTime - startTime) + " ms");

        startTime = System.currentTimeMillis();
        for (int i = 0; i < ITERATIONS; i++) {
            copyOnWriteArraySet.contains(i);
        }
        endTime = System.currentTimeMillis();
        System.out.println("CopyOnWriteArraySet read time: " + (endTime - startTime) + " ms");
    }
}

此性能测试代码展示了CopyOnWriteArraySet和HashSet在读写操作上的性能差异。

ConcurrentHashMap与HashMap比较

线程安全性

  • HashMap:非线程安全,多线程环境下可能出现数据竞争和结构损坏。
  • ConcurrentHashMap:线程安全,采用CAS和synchronized等技术保证多线程环境下的安全访问。

性能

  • 读性能:两者在读性能上都比较高,ConcurrentHashMap在Java 8之后采用无锁化的读操作,性能进一步提升。HashMap在单线程环境下读性能良好,但多线程下需要同步。
  • 写性能:在高并发写操作场景下,ConcurrentHashMap性能明显优于HashMap。HashMap在多线程写时需要同步整个Map,而ConcurrentHashMap采用分段锁(Java 8前)或更细粒度的锁机制,允许多个线程同时写不同部分。

示例7:ConcurrentHashMap与HashMap性能对比

import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;

public class MapPerformanceComparison {
    private static final int THREADS = 10;
    private static final int ITERATIONS = 100000;

    public static void main(String[] args) {
        Map<Integer, String> hashMap = new HashMap<>();
        Map<Integer, String> concurrentHashMap = new ConcurrentHashMap<>();

        long startTime = System.currentTimeMillis();
        Thread[] hashMapThreads = new Thread[THREADS];
        for (int i = 0; i < THREADS; i++) {
            hashMapThreads[i] = new Thread(() -> {
                for (int j = 0; j < ITERATIONS; j++) {
                    synchronized (hashMap) {
                        hashMap.put(j, "Value" + j);
                    }
                }
            });
            hashMapThreads[i].start();
        }
        for (Thread thread : hashMapThreads) {
            try {
                thread.join();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
        long endTime = System.currentTimeMillis();
        System.out.println("HashMap write time: " + (endTime - startTime) + " ms");

        startTime = System.currentTimeMillis();
        Thread[] concurrentHashMapThreads = new Thread[THREADS];
        for (int i = 0; i < THREADS; i++) {
            concurrentHashMapThreads[i] = new Thread(() -> {
                for (int j = 0; j < ITERATIONS; j++) {
                    concurrentHashMap.put(j, "Value" + j);
                }
            });
            concurrentHashMapThreads[i].start();
        }
        for (Thread thread : concurrentHashMapThreads) {
            try {
                thread.join();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
        endTime = System.currentTimeMillis();
        System.out.println("ConcurrentHashMap write time: " + (endTime - startTime) + " ms");

        startTime = System.currentTimeMillis();
        for (int i = 0; i < ITERATIONS; i++) {
            synchronized (hashMap) {
                hashMap.get(i);
            }
        }
        endTime = System.currentTimeMillis();
        System.out.println("HashMap read time: " + (endTime - startTime) + " ms");

        startTime = System.currentTimeMillis();
        for (int i = 0; i < ITERATIONS; i++) {
            concurrentHashMap.get(i);
        }
        endTime = System.currentTimeMillis();
        System.out.println("ConcurrentHashMap read time: " + (endTime - startTime) + " ms");
    }
}

从性能测试结果可以看出ConcurrentHashMap在高并发场景下的优势。

ConcurrentSkipListMap与TreeMap比较

线程安全性

  • TreeMap:非线程安全,在多线程环境下需要同步。
  • ConcurrentSkipListMap:线程安全,基于跳表结构,能在多线程环境下提供安全的有序Map操作。

性能

  • 读性能:在多线程读操作下,ConcurrentSkipListMap性能较好,因为它采用无锁或细粒度锁机制。TreeMap在多线程下如果不同步,读操作可能获取到不一致数据。
  • 写性能:ConcurrentSkipListMap在高并发写操作上性能优于TreeMap,TreeMap在多线程写时需要同步整个Map,而ConcurrentSkipListMap通过更细粒度的锁机制提高并发性能。

示例8:ConcurrentSkipListMap与TreeMap性能对比

import java.util.TreeMap;
import java.util.concurrent.ConcurrentSkipListMap;

public class SkipListMapPerformanceComparison {
    private static final int THREADS = 10;
    private static final int ITERATIONS = 100000;

    public static void main(String[] args) {
        TreeMap<Integer, String> treeMap = new TreeMap<>();
        ConcurrentSkipListMap<Integer, String> concurrentSkipListMap = new ConcurrentSkipListMap<>();

        long startTime = System.currentTimeMillis();
        Thread[] treeMapThreads = new Thread[THREADS];
        for (int i = 0; i < THREADS; i++) {
            treeMapThreads[i] = new Thread(() -> {
                for (int j = 0; j < ITERATIONS; j++) {
                    synchronized (treeMap) {
                        treeMap.put(j, "Value" + j);
                    }
                }
            });
            treeMapThreads[i].start();
        }
        for (Thread thread : treeMapThreads) {
            try {
                thread.join();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
        long endTime = System.currentTimeMillis();
        System.out.println("TreeMap write time: " + (endTime - startTime) + " ms");

        startTime = System.currentTimeMillis();
        Thread[] concurrentSkipListMapThreads = new Thread[THREADS];
        for (int i = 0; i < THREADS; i++) {
            concurrentSkipListMapThreads[i] = new Thread(() -> {
                for (int j = 0; j < ITERATIONS; j++) {
                    concurrentSkipListMap.put(j, "Value" + j);
                }
            });
            concurrentSkipListMapThreads[i].start();
        }
        for (Thread thread : concurrentSkipListMapThreads) {
            try {
                thread.join();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
        endTime = System.currentTimeMillis();
        System.out.println("ConcurrentSkipListMap write time: " + (endTime - startTime) + " ms");

        startTime = System.currentTimeMillis();
        for (int i = 0; i < ITERATIONS; i++) {
            synchronized (treeMap) {
                treeMap.get(i);
            }
        }
        endTime = System.currentTimeMillis();
        System.out.println("TreeMap read time: " + (endTime - startTime) + " ms");

        startTime = System.currentTimeMillis();
        for (int i = 0; i < ITERATIONS; i++) {
            concurrentSkipListMap.get(i);
        }
        endTime = System.currentTimeMillis();
        System.out.println("ConcurrentSkipListMap read time: " + (endTime - startTime) + " ms");
    }
}

通过以上性能测试代码,可以直观地看到ConcurrentSkipListMap和TreeMap在多线程环境下的性能差异。

选择合适的集合类

在实际开发中,选择使用传统集合还是并发集合类,需要根据具体的应用场景来决定。

  • 单线程环境:在单线程环境下,传统集合类通常具有更好的性能,因为它们没有额外的线程安全开销。例如,ArrayList和HashMap在单线程应用中是很好的选择。
  • 多线程读多写环境:如果应用场景是多线程同时进行大量的读和写操作,并发集合类是更好的选择。如ConcurrentHashMap适用于多线程读写的Map场景,CopyOnWriteArrayList适用于读多写少的List场景。
  • 多线程读少写环境:对于读多写少的多线程场景,CopyOnWriteArraySet和CopyOnWriteArrayList等基于写时复制的集合类能提供较好的性能,因为读操作无锁。
  • 有序集合需求:如果需要有序的集合,并且在多线程环境下使用,ConcurrentSkipListSet和ConcurrentSkipListMap是不错的选择,它们能保证元素的有序性和线程安全。

在选择集合类时,不仅要考虑线程安全性,还要综合考虑性能、内存消耗等因素,以确保应用程序的高效运行。通过对Java并发集合类与传统集合类的深入比较,开发者能够根据实际需求做出更合适的选择。