一、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。同时,在使用线程安全的集合时,要注意锁的粒度和迭代器的使用,避免出现数据不一致和异常的问题。
评论