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

Java Collections 的同步机制

2021-03-185.0k 阅读

Java Collections 的同步机制

1. Java集合框架概述

在深入探讨Java Collections的同步机制之前,先简要回顾一下Java集合框架。Java集合框架是一个包含了各种数据结构和算法的体系,它为开发者提供了一系列用于存储和操作数据的类和接口。这些集合类大致可分为以下几类:

  • List:有序的集合,允许重复元素。例如ArrayListLinkedListArrayList基于数组实现,随机访问效率高;LinkedList基于链表实现,插入和删除操作效率高。
  • Set:不包含重复元素的集合。例如HashSetTreeSetHashSet基于哈希表实现,查找效率高;TreeSet基于红黑树实现,元素有序。
  • Map:存储键值对的集合,一个键最多映射到一个值。例如HashMapTreeMapHashMap基于哈希表实现,查找效率高;TreeMap基于红黑树实现,按键有序。

然而,在多线程环境下使用这些集合时,如果不进行适当的同步处理,可能会导致数据不一致、并发修改异常等问题。

2. 多线程环境下集合的问题

考虑以下简单的代码示例,在多线程环境下使用ArrayList

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

public class UnsynchronizedArrayListExample {
    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("Final size of list: " + list.size());
    }
}

在上述代码中,两个线程同时向ArrayList中添加元素。运行这段代码多次,你可能会发现每次输出的列表大小并不总是2000,这是因为在多线程环境下,多个线程同时访问和修改ArrayList,导致数据不一致。

3. 同步集合的方式

3.1 使用Collections.synchronizedXxx方法

Java提供了Collections类的静态方法来创建同步的集合。例如,要创建一个同步的List,可以使用Collections.synchronizedList方法:

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

public class SynchronizedArrayListExample {
    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();
        }

        synchronized (list) {
            System.out.println("Final size of list: " + list.size());
        }
    }
}

在上述代码中,通过Collections.synchronizedList创建了一个同步的List。并且在访问和修改list时,通过synchronized关键字同步块来确保线程安全。这里需要注意的是,在迭代同步集合时,也必须在同步块内进行,以避免并发修改异常。例如:

List<Integer> syncList = Collections.synchronizedList(new ArrayList<>());
// 添加元素到syncList
synchronized (syncList) {
    for (Integer num : syncList) {
        System.out.println(num);
    }
}

同样的方式,Collections类还提供了synchronizedSetsynchronizedMap等方法来创建同步的SetMap

3.2 使用ConcurrentHashMap等并发集合类

ConcurrentHashMap是Java提供的线程安全的哈希表实现,它与通过Collections.synchronizedMap创建的同步Map有所不同。ConcurrentHashMap允许多个线程同时读,并且支持高并发的写操作,它通过分段锁机制来实现这一点。

import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentMap;

public class ConcurrentHashMapExample {
    private static ConcurrentMap<String, Integer> map = new ConcurrentHashMap<>();

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

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

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

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

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

在上述代码中,ConcurrentHashMap在多线程环境下能够高效地处理并发的读写操作,而不需要像同步Map那样对整个Map进行加锁。ConcurrentHashMap在JDK 1.8之后,还引入了红黑树结构来提高查找和插入性能,尤其是在元素数量较多时。

除了ConcurrentHashMap,Java还提供了其他并发集合类,如ConcurrentSkipListMap(基于跳表实现的有序并发Map)、ConcurrentSkipListSet(基于跳表实现的有序并发Set)、CopyOnWriteArrayListCopyOnWriteArraySet

3.3 CopyOnWriteArrayListCopyOnWriteArraySet

CopyOnWriteArrayList是一种特殊的List实现,它在进行写操作(如添加、删除元素)时,会创建一个原数组的副本,在副本上进行操作,操作完成后再将原数组指向新的副本。读操作则直接读取原数组,因此读操作是线程安全的,并且不需要加锁,这使得读操作的性能非常高。但由于写操作需要复制数组,所以写操作的性能相对较低,适用于读多写少的场景。

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

public class CopyOnWriteArrayListExample {
    private static List<Integer> list = new CopyOnWriteArrayList<>();

    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("Final size of list: " + list.size());
    }
}

CopyOnWriteArraySet的原理与CopyOnWriteArrayList类似,它是线程安全的Set实现,同样适用于读多写少的场景。

4. 同步机制的性能分析

不同的同步集合方式在性能上有显著的差异。

4.1 Collections.synchronizedXxx创建的同步集合

通过Collections.synchronizedXxx方法创建的同步集合,在多线程环境下,每次访问和修改集合都需要获取整个集合的锁。这意味着在同一时间内,只有一个线程能够对集合进行操作,其他线程必须等待锁的释放。这种方式虽然保证了线程安全,但在高并发场景下,性能会受到严重影响,因为线程的竞争会导致大量的等待时间。

4.2 ConcurrentHashMap等并发集合类

ConcurrentHashMap通过分段锁机制,允许多个线程同时对不同的段进行操作,从而提高了并发性能。在JDK 1.8之后,ConcurrentHashMap进一步优化,使用CAS操作和红黑树结构,使得读写操作更加高效。相比通过Collections.synchronizedMap创建的同步MapConcurrentHashMap在高并发读写场景下具有明显的性能优势。

4.3 CopyOnWriteArrayListCopyOnWriteArraySet

CopyOnWriteArrayListCopyOnWriteArraySet由于读操作不需要加锁,读性能非常高。但写操作时需要复制数组,这带来了较大的开销。因此,它们适用于读操作远远多于写操作的场景。如果写操作频繁,不断地复制数组会导致内存消耗增加和性能下降。

5. 选择合适的同步集合

在实际应用中,选择合适的同步集合取决于具体的需求和场景。

  • 读多写少的场景:如果应用程序主要是读取集合中的数据,而写操作较少,可以选择CopyOnWriteArrayListCopyOnWriteArraySet。对于Map类型,可以考虑使用ConcurrentHashMap,因为它的读性能也很出色,并且支持一定程度的并发写操作。
  • 读写均衡的场景:当读写操作频率相对均衡时,ConcurrentHashMap是一个不错的选择,它能够在保证线程安全的前提下,提供较好的并发性能。对于ListSet,如果需要有序性,可以考虑使用ConcurrentSkipListSetConcurrentSkipListMap
  • 写多读少的场景:在写操作频繁的情况下,通过Collections.synchronizedXxx方法创建的同步集合可能是一个选择,虽然性能相对较低,但实现简单。不过,如果性能要求较高,可能需要自行设计更细粒度的锁机制来优化写操作的性能。

6. 迭代器与同步集合

在使用同步集合的迭代器时,需要特别注意线程安全问题。

6.1 同步集合的迭代器

对于通过Collections.synchronizedXxx方法创建的同步集合,迭代器操作必须在同步块内进行,以避免ConcurrentModificationException异常。例如:

List<Integer> syncList = Collections.synchronizedList(new ArrayList<>());
// 添加元素到syncList
synchronized (syncList) {
    for (Integer num : syncList) {
        System.out.println(num);
    }
}

这是因为在迭代过程中,如果其他线程对集合进行了修改,就会导致迭代器的状态与集合的实际状态不一致,从而抛出异常。

6.2 并发集合类的迭代器

ConcurrentHashMap等并发集合类的迭代器具有弱一致性。这意味着迭代器在创建时会获取集合的一个“快照”,在迭代过程中,即使集合被其他线程修改,迭代器也不会抛出ConcurrentModificationException异常,而是会尽力返回反映最近修改的结果。例如:

ConcurrentMap<String, Integer> map = new ConcurrentHashMap<>();
map.put("key1", 1);
map.put("key2", 2);

map.forEach((key, value) -> {
    System.out.println(key + ": " + value);
    map.put("key3", 3); // 在迭代过程中修改map
});

上述代码在迭代ConcurrentHashMap的过程中对其进行了修改,由于迭代器的弱一致性,不会抛出异常,并且能尽量反映最新的修改。

7. 自定义同步集合

在某些情况下,可能需要自定义同步集合以满足特定的需求。

7.1 基于现有集合实现

可以基于现有的集合类,通过添加同步机制来实现自定义同步集合。例如,下面是一个简单的自定义同步List实现:

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

public class CustomSynchronizedList<T> {
    private List<T> list = new ArrayList<>();

    public synchronized void add(T element) {
        list.add(element);
    }

    public synchronized T get(int index) {
        return list.get(index);
    }

    public synchronized int size() {
        return list.size();
    }
}

在上述代码中,通过在方法上使用synchronized关键字,对List的操作进行了同步,从而实现了线程安全。

7.2 使用ReentrantLock实现更灵活的同步

ReentrantLock提供了比synchronized关键字更灵活的同步控制。可以使用ReentrantLock来实现自定义同步集合,例如:

import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.locks.ReentrantLock;

public class CustomReentrantLockList<T> {
    private List<T> list = new ArrayList<>();
    private ReentrantLock lock = new ReentrantLock();

    public void add(T element) {
        lock.lock();
        try {
            list.add(element);
        } finally {
            lock.unlock();
        }
    }

    public T get(int index) {
        lock.lock();
        try {
            return list.get(index);
        } finally {
            lock.unlock();
        }
    }

    public int size() {
        lock.lock();
        try {
            return list.size();
        } finally {
            lock.unlock();
        }
    }
}

ReentrantLock允许更细粒度的控制,例如可以设置锁的公平性,以及使用tryLock方法尝试获取锁而不阻塞。

8. 总结同步机制的要点

  • 理解多线程问题:在多线程环境下使用Java集合时,数据不一致和并发修改异常是常见问题,需要通过同步机制来解决。
  • 同步方式选择:根据应用场景选择合适的同步集合方式,如Collections.synchronizedXxx、并发集合类(ConcurrentHashMap等)或CopyOnWriteArrayList等。
  • 性能考量:不同的同步集合在性能上有差异,读多写少、读写均衡和写多读少的场景应选择不同的同步方式以优化性能。
  • 迭代器处理:同步集合的迭代器操作需要注意线程安全,并发集合类的迭代器具有弱一致性特点。
  • 自定义同步:在必要时,可以通过基于现有集合添加同步机制或使用ReentrantLock等工具来自定义同步集合。

通过深入理解和合理应用Java Collections的同步机制,开发者能够在多线程环境下高效、安全地使用集合类,构建健壮的应用程序。无论是开发大型企业级应用还是小型的多线程工具,对同步机制的掌握都是至关重要的。希望本文的内容能帮助你更好地理解和运用Java集合的同步机制,在实际开发中避免多线程相关的问题,提高程序的稳定性和性能。