一、Java集合框架线程安全问题的起源

大家都知道,Java集合框架就像是一个大仓库,里面有各种各样装东西的容器,像 List、Set、Map 这些。在单线程环境下,这些容器用起来那是相当顺手,就跟咱们平时在家里收拾东西一样,想怎么放就怎么放,想怎么拿就怎么拿。

但是呢,一旦到了多线程环境,情况就不一样了。多线程就好比好多人同时在仓库里拿东西、放东西。要是大家都不管不顾地乱搞,那仓库不就乱套了嘛。Java 里的很多集合类,像 ArrayList、HashMap 这些,在设计的时候就没考虑到多线程的情况,所以在多线程环境下使用它们,就可能会出问题。

比如说,有两个线程同时往一个 ArrayList 里添加元素。线程 A 刚把元素放到位置 0,还没来得及更新列表的大小,线程 B 也来放元素了,它也以为位置 0 是空的,结果就把线程 A 放的元素给覆盖了,这就会导致数据丢失。

二、常见的线程不安全集合类及问题示例

2.1 ArrayList

我们来写个代码看看 ArrayList 在多线程环境下的问题。

// Java 技术栈
import java.util.ArrayList;
import java.util.List;

public class ArrayListThreadSafetyExample {
    public static void main(String[] args) throws InterruptedException {
        // 创建一个 ArrayList 集合
        List<Integer> list = new ArrayList<>();

        // 创建一个线程任务,向集合中添加元素
        Runnable task = () -> {
            for (int i = 0; i < 1000; i++) {
                list.add(i);
            }
        };

        // 创建两个线程
        Thread thread1 = new Thread(task);
        Thread thread2 = new Thread(task);

        // 启动线程
        thread1.start();
        thread2.start();

        // 等待两个线程执行完毕
        thread1.join();
        thread2.join();

        // 输出集合的大小
        System.out.println("List size: " + list.size());
    }
}

在这个例子里,我们创建了一个 ArrayList,然后启动两个线程往里面添加元素。正常情况下,两个线程各添加 1000 个元素,集合的大小应该是 2000。但实际上,运行这个程序,你会发现输出的结果可能小于 2000,这就是因为多线程操作导致的数据不一致问题。

2.2 HashMap

再看看 HashMap,它在多线程环境下也有类似的问题。

// Java 技术栈
import java.util.HashMap;
import java.util.Map;

public class HashMapThreadSafetyExample {
    public static void main(String[] args) throws InterruptedException {
        // 创建一个 HashMap 集合
        Map<Integer, Integer> map = new HashMap<>();

        // 创建一个线程任务,向集合中添加元素
        Runnable task = () -> {
            for (int i = 0; i < 1000; i++) {
                map.put(i, i);
            }
        };

        // 创建两个线程
        Thread thread1 = new Thread(task);
        Thread thread2 = new Thread(task);

        // 启动线程
        thread1.start();
        thread2.start();

        // 等待两个线程执行完毕
        thread1.join();
        thread2.join();

        // 输出集合的大小
        System.out.println("Map size: " + map.size());
    }
}

这个例子和上面的 ArrayList 类似,两个线程同时往 HashMap 里添加元素,也会出现数据不一致的问题,输出的结果可能小于 2000。

三、线程安全的集合类及解决方案

3.1 Vector 和 Hashtable

Java 里有一些线程安全的集合类,像 Vector 和 Hashtable。它们的实现方式很简单,就是在方法上加了 synchronized 关键字,保证同一时间只有一个线程能访问这些方法。

// Java 技术栈
import java.util.Vector;

public class VectorExample {
    public static void main(String[] args) throws InterruptedException {
        // 创建一个 Vector 集合
        Vector<Integer> vector = new Vector<>();

        // 创建一个线程任务,向集合中添加元素
        Runnable task = () -> {
            for (int i = 0; i < 1000; i++) {
                vector.add(i);
            }
        };

        // 创建两个线程
        Thread thread1 = new Thread(task);
        Thread thread2 = new Thread(task);

        // 启动线程
        thread1.start();
        thread2.start();

        // 等待两个线程执行完毕
        thread1.join();
        thread2.join();

        // 输出集合的大小
        System.out.println("Vector size: " + vector.size());
    }
}

在这个例子里,我们使用了 Vector 集合,它是线程安全的。运行这个程序,输出的结果就是 2000,说明没有出现数据不一致的问题。

3.2 Collections.synchronizedXxx 方法

除了 Vector 和 Hashtable,我们还可以使用 Collections 类的 synchronizedXxx 方法来把一个线程不安全的集合变成线程安全的。

// Java 技术栈
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;

public class SynchronizedListExample {
    public static void main(String[] args) throws InterruptedException {
        // 创建一个 ArrayList 集合
        List<Integer> list = new ArrayList<>();
        // 使用 Collections.synchronizedList 方法将其转换为线程安全的集合
        List<Integer> synchronizedList = Collections.synchronizedList(list);

        // 创建一个线程任务,向集合中添加元素
        Runnable task = () -> {
            for (int i = 0; i < 1000; i++) {
                synchronizedList.add(i);
            }
        };

        // 创建两个线程
        Thread thread1 = new Thread(task);
        Thread thread2 = new Thread(task);

        // 启动线程
        thread1.start();
        thread2.start();

        // 等待两个线程执行完毕
        thread1.join();
        thread2.join();

        // 输出集合的大小
        System.out.println("Synchronized list size: " + synchronizedList.size());
    }
}

在这个例子里,我们把一个 ArrayList 变成了线程安全的集合,使用起来就跟 Vector 差不多,也能保证数据的一致性。

3.3 ConcurrentHashMap

ConcurrentHashMap 是 Java 里专门为多线程环境设计的 Map 集合。它采用了分段锁的机制,比 Hashtable 的性能要好很多。

// Java 技术栈
import java.util.concurrent.ConcurrentHashMap;

public class ConcurrentHashMapExample {
    public static void main(String[] args) throws InterruptedException {
        // 创建一个 ConcurrentHashMap 集合
        ConcurrentHashMap<Integer, Integer> map = new ConcurrentHashMap<>();

        // 创建一个线程任务,向集合中添加元素
        Runnable task = () -> {
            for (int i = 0; i < 1000; i++) {
                map.put(i, i);
            }
        };

        // 创建两个线程
        Thread thread1 = new Thread(task);
        Thread thread2 = new Thread(task);

        // 启动线程
        thread1.start();
        thread2.start();

        // 等待两个线程执行完毕
        thread1.join();
        thread2.join();

        // 输出集合的大小
        System.out.println("ConcurrentHashMap size: " + map.size());
    }
}

在这个例子里,我们使用了 ConcurrentHashMap,它在多线程环境下能很好地保证数据的一致性,而且性能也不错。

四、应用场景

4.1 多线程数据共享场景

在很多多线程的应用程序中,需要多个线程共享同一个集合。比如说,一个电商系统里,有多个线程同时处理订单,这些订单信息可能会存储在一个集合里。这时候就需要使用线程安全的集合,否则就会出现数据不一致的问题。

4.2 高并发场景

在高并发的场景下,像网站的访问量很大,有很多用户同时访问服务器。服务器需要处理大量的请求,这些请求的数据可能会存储在集合里。使用线程安全的集合可以保证数据的正确性,避免出现数据丢失或者错误的情况。

五、技术优缺点

5.1 Vector 和 Hashtable

优点:使用起来很简单,只需要创建对象就可以了,不需要额外的操作。 缺点:性能比较低,因为它们使用了 synchronized 关键字,同一时间只能有一个线程访问,会导致线程阻塞。

5.2 Collections.synchronizedXxx 方法

优点:可以把现有的线程不安全的集合变成线程安全的,不需要重新创建集合。 缺点:性能也不是很高,因为也是使用了 synchronized 关键字。

5.3 ConcurrentHashMap

优点:性能高,采用了分段锁的机制,多个线程可以同时访问不同的段,提高了并发性能。 缺点:实现比较复杂,对于一些简单的应用场景可能有点大材小用。

六、注意事项

6.1 锁的粒度

在使用线程安全的集合时,要注意锁的粒度。如果锁的粒度过大,会导致性能下降;如果锁的粒度过小,可能会出现数据不一致的问题。比如说,在使用 Collections.synchronizedXxx 方法时,要注意对整个集合加锁,而不是对集合的某个元素加锁。

6.2 迭代器的使用

在多线程环境下,使用迭代器遍历集合时要小心。如果在遍历的过程中,其他线程修改了集合的结构,可能会抛出 ConcurrentModificationException 异常。所以,在使用迭代器时,最好使用线程安全的迭代器,或者在遍历的过程中对集合加锁。

七、文章总结

Java 集合框架在单线程环境下用起来很方便,但在多线程环境下,很多集合类会出现线程安全问题。为了解决这些问题,Java 提供了一些线程安全的集合类,像 Vector、Hashtable、ConcurrentHashMap 等,还可以使用 Collections.synchronizedXxx 方法把线程不安全的集合变成线程安全的。

在选择使用哪种线程安全的集合时,要根据具体的应用场景和性能要求来决定。如果对性能要求不高,可以使用 Vector 和 Hashtable;如果性能要求比较高,可以使用 ConcurrentHashMap。同时,在使用线程安全的集合时,要注意锁的粒度和迭代器的使用,避免出现数据不一致和异常的问题。