前言:在多线程环境中,集合类的线程安全性是保证数据一致性和避免并发冲突的关键。Java 提供了多种线程安全集合类,它们在不同的并发场景中有着各自的优缺点。
✨✨✨这里是秋刀鱼不做梦的BLOG
✨✨✨想要了解更多内容可以访问我的主页秋刀鱼不做梦-CSDN博客
在正式开始讲解之前,先让我们看一下本文大致的讲解内容:
目录
(2)使用Collections.synchronizedList(new ArrayList())
1.线程安全的集合类简介
在开始学习Java中的线程安全的集合类之前,先让我们了解一下什么是Java中的线程安全的集合类:
在多线程编程中,线程安全性是一个关键问题,尤其是当多个线程同时访问和修改同一个集合对象时。线程安全的集合类能够确保在并发环境下的操作不会引发数据不一致或不可预期的行为,在 Java 中,线程安全的集合类通常是通过同步机制(如 synchronized 关键字或显式的锁)来实现的,这些集合类保证了在多线程环境下数据访问的正确性。
Java 提供了一些内置的线程安全集合类,包括但不限于 Vector、Hashtable、ConcurrentHashMap、CopyOnWriteArrayList 等,这些集合类的设计原则是要保证在多线程并发访问时,不会发生数据损坏或冲突,因此其内部常常采用了同步控制或分段锁机制来保证线程安全。
线程安全集合的设计原则
在多线程环境下设计集合类时,通常需要解决的一个问题是并发访问的同步控制。例如,多个线程在同一时刻访问集合时,必须保证只有一个线程能够修改集合的内容,而其他线程只能进行读取。因此我们常用的策略包括:
- 同步控制:通过 synchronized 关键字或其他同步机制确保同一时刻只有一个线程能操作集合对象。
- 分段锁(Segment Locks):对集合进行分段,每段独立加锁,从而减少锁竞争,提高并发性能。
- 无锁设计:在某些高性能场景下,通过无锁机制如乐观锁来实现线程安全。
至此,我们就大致的了解了Java中的线程安全的集合类了!
2.多线程环境下使用 ArrayList 的方式
ArrayList 是 Java 中最常用的集合类之一,它在单线程环境中非常高效,因为其能够动态扩展数组的大小,并且提供常数时间复杂度的随机访问,然而,在多线程环境下,ArrayList 本身并不是线程安全的,意味着多个线程并发地对同一个 ArrayList 进行修改或访问时,可能会发生竞态条件,导致数据不一致或数组越界等问题。因此,在多线程环境中使用 ArrayList 时,需要采取额外的措施来保证线程安全。
——下面我们将详细探讨三种常用的ArrayList类的线程安全解决方案:
(1)直接使用 ArrayList、
(2)通过Collections.synchronizedList创建线程安全列表,
(3)使用 CopyOnWriteArrayList。
(1)直接使用 ArrayList
在多线程环境中,ArrayList 并不是线程安全的类,如果多个线程并发访问并修改同一个 ArrayList,可能会导致数据不一致或抛出异常(例如 ConcurrentModificationException)。为了确保线程安全,可以通过使用 synchronized 关键字对访问 ArrayList 的代码块或方法进行同步。
——以下为一个使用synchronized关键字来保证ArrayList类使用安全的例子:
import java.util.ArrayList;
public class SynchronizedArrayList {
public static void main(String[] args) {
ArrayList<Integer> list = new ArrayList<>();
// 创建多个线程并发操作 ArrayList
Thread thread1 = new Thread(() -> {
synchronized (list) {
for (int i = 0; i < 5; i++) {
list.add(i);
System.out.println("Thread1 added: " + i);
}
}
});
Thread thread2 = new Thread(() -> {
synchronized (list) {
for (int i = 5; i < 10; i++) {
list.add(i);
System.out.println("Thread2 added: " + i);
}
}
});
Thread thread3 = new Thread(() -> {
synchronized (list) {
for (int i = 0; i < list.size(); i++) {
System.out.println("Thread3 reads: " + list.get(i));
}
}
});
// 启动线程
thread1.start();
thread2.start();
try {
// 等待线程1和线程2完成
thread1.join();
thread2.join();
} catch (InterruptedException e) {
e.printStackTrace();
}
// 启动读取线程
thread3.start();
}
}
这里我们对上述的代码进行分析一下:
synchronized 关键字作用:
每次操作 list 的时候,都显式加锁 list 本身。
确保同一时刻只有一个线程可以访问或修改 list。
线程安全性:
线程 1 和线程 2 并发添加元素时,由于 synchronized 的限制,不会导致数据竞争问题。
线程 3 在读取元素时也进行了同步,确保不会在添加元素未完成时读取。
线程执行顺序:
使用 synchronized 会导致线程按照加锁的顺序执行,从而保证数据操作的安全。
当然,我们在使用synchronized关键字来保证ArrayList类在多线程的情况下的线程安全也是有一些注意事项的,以下为一些注意事项:
同步开销:
- 使用 synchronized 会降低程序的并发性能,因为在锁释放之前其他线程会被阻塞。
锁的粒度:
- 如果需要更精细的控制,可以选择同步不同的代码块,而非整个方法或多个操作。
可替代方案:
- 使用 Collections.synchronizedList 方法,直接获得线程安全的 ArrayList。
这样,我们就大致的了解了在多线程下我们如果使用synchronized关键字来保证ArrayList类在多线程的情况下的线程安全
(2)使用Collections.synchronizedList(new ArrayList())
除了上述直接使用synchronized关键字来保证ArrayList类在多线程的情况下的线程安全的方式之外,Java 还提供了 Collections.synchronizedList 方法,此方法可以将一个非线程安全的 list(如 ArrayList)转换为线程安全的 list,该方法通过对所有访问操作进行同步,确保在多线程环境下对 ArrayList 的操作是线程安全的。
实现原理:
—— Collections.synchronizedList 方法会返回一个包装后的线程安全 list,该列表会将对每个操作的访问进行同步(加锁),确保在多线程环境下只有一个线程可以同时访问该列表的方法。
以下为使用Collections.synchronizedList 方法的一个案例:
import java.util.*;
public class SynchronizedArrayListExample {
public static void main(String[] args) {
List<Integer> list = new ArrayList<>();
List<Integer> synchronizedList = Collections.synchronizedList(list);
// 启动多个线程向 synchronizedList 中添加元素
Thread thread1 = new Thread(() -> {
for (int i = 0; i < 1000; i++) {
synchronizedList.add(i);
}
});
Thread thread2 = new Thread(() -> {
for (int i = 1000; i < 2000; i++) {
synchronizedList.add(i);
}
});
thread1.start();
thread2.start();
// 等待线程结束
try {
thread1.join();
thread2.join();
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("Synchronized list size: " + synchronizedList.size());
}
}
这样我们就了解了使用 Collections.synchronizedList(new ArrayList()) 来保证ArrayList类的线程安全了。
(3)使用CopyOnWriteArrayList
CopyOnWriteArrayList是一种线程安全的 list 实现,它基于写时复制(Copy-On-Write,简称 COW)策略实现,它的设计思想是:在修改集合时,不直接修改原集合,而是将原集合复制一份,然后在复制的集合上进行修改。这种方式可以避免在读取操作时加锁,因为读取操作总是对原集合进行,而写操作则只对副本集合进行。
实现原理:
——CopyOnWriteArrayList在进行写操作时,会创建一个新的副本,然后对副本进行修改,修改完成后将新的副本替换掉原集合,因此,读取操作不需要加锁,多个线程可以并发地执行读操作。写操作会导致集合的复制,因此可能会引入一定的性能开销。
以下为使用CopyOnWriteArrayList方法的一个案例:
import java.util.concurrent.*;
public class CopyOnWriteArrayListExample {
public static void main(String[] args) {
List<Integer> list = new CopyOnWriteArrayList<>();
// 启动多个线程向 CopyOnWriteArrayList 中添加元素
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("CopyOnWriteArrayList size: " + list.size());
}
}
这样我们就了解了使用CopyOnWriteArrayList来保证ArrayList类的线程安全了。
从上边我们学习了直接使用 ArrayList、通过 Collections.synchronizedList 创建线程安全列表、使用CopyOnWriteArrayList三种方式来保证在多线程的情况下使用ArrayList类的线程安全,那么读者可能会发问,那我在不同的情况下,我到底选择哪个方式来保证ArrayList类的线程安全呢?
——这里我们对上述三种方式进行对比分析一下:
【1】ArrayList的直接使用
直接使用 ArrayList 时,多个线程同时访问或修改集合内容可能会导致数据不一致的问题,因此不推荐在多线程环境下直接使用 ArrayList,除非我们能够对其进行外部同步控制。
优缺点分析:
- 优点: 实现简单,不需要额外的同步机制,性能高(如果没有并发问题)。
- 缺点: 如果多个线程并发访问或修改同一个 ArrayList,会导致数据不一致或其他异常行为,必须使用外部同步。
【2】Collections.synchronizedList(new ArrayList())
Collections.synchronizedList(new ArrayList())将 ArrayList 转换为线程安全的集合,保证在并发环境下对集合的操作不会出错。它通过加锁的方式保证线程安全,每次访问都会加锁,这样避免了数据不一致的问题。
优缺点分析:
- 优点: 使用简单,通过 synchronizedList 可以让 ArrayList 变为线程安全的。
- 缺点: 由于对每个操作都进行同步,因此在高并发环境下可能导致性能瓶颈,尤其是大量写操作时,锁竞争可能导致性能下降。
【3】CopyOnWriteArrayList
CopyOnWriteArrayList是一种基于写时复制(Copy-On-Write)机制的线程安全 list 实现。在多线程环境下,读取操作非常高效,因为它不需要加锁,而写操作则会复制整个集合,产生额外的性能开销。因此,它非常适用于读多写少的场景。
优缺点分析:
- 优点:
- 读操作不需要加锁,读操作的性能非常高。
- 避免了锁竞争,可以在高并发读场景下提供良好的性能。
- 缺点:
- 写操作可能产生较高的性能开销,因为每次写操作都会复制整个集合。
- 如果写操作频繁,性能会受到影响,不适合写多读少的场景。
所以我们这里给出总结:
- 如果是单线程应用或者操作非常简单,直接使用 ArrayList 可能就足够了。
- 如果在多线程环境下需要保证线程安全,可以使用 Collections.synchronizedList(new ArrayList()),但要注意,它的性能可能在高并发环境下有所下降。
- 如果在多线程环境下频繁进行读操作,且写操作相对较少,CopyOnWriteArrayList 是一个非常好的选择,它能够保证高效的读操作,但要注意在写多的场景下性能可能会降低。
至此我们就学会了在多线程中如何使用ArrayList了!
3.多线程环境使用队列
在多线程编程中,队列是一种非常重要的集合类型,尤其是在生产者-消费者模型中。Queue 接口在 Java 中有多种实现,其中有一些是线程安全的,能够在并发环境下保证元素的正确访问。
(1)ConcurrentLinkedQueue的使用
ConcurrentLinkedQueue是一种线程安全的无界队列,它基于非阻塞算法实现,不需要加锁来保证线程安全。它通过 CAS(Compare-And-Swap)机制来确保数据的正确性,适用于高并发场景。
Queue<Integer> queue = new ConcurrentLinkedQueue<>();
queue.offer(1);
queue.offer(2);
Integer value = queue.poll(); // 非阻塞获取元素
这种队列适合用于需要频繁入队和出队操作的场景,尤其是对性能要求较高时。它能够避免传统队列实现中的锁竞争问题,提供更高的并发性能。
(2)BlockingQueue的使用
对于需要阻塞操作的场景(例如生产者-消费者问题),BlockingQueue是一个非常实用的接口。ArrayBlockingQueue 和 LinkedBlockingQueue 都是 BlockingQueue 的实现,并且它们本身是线程安全的。
BlockingQueue<Integer> blockingQueue = new ArrayBlockingQueue<>(10);
blockingQueue.put(1); // 如果队列已满,当前线程将阻塞
Integer value = blockingQueue.take(); // 如果队列为空,当前线程将阻塞
BlockingQueue的阻塞操作非常适合在多线程环境中进行任务分配,避免了轮询和手动同步的问题。
至此,我们就大致的了解了如何在多线程中使用队列了!
4.多线程环境使用哈希表
哈希表(Hashtable)是一种基于哈希算法的键值对存储结构。在多线程环境中,多个线程同时对同一哈希表进行操作时,需要保证线程安全。Java 提供了两种主要的线程安全哈希表实现:Hashtable 和 ConcurrentHashMap。尽管这两者都能保证线程安全,但它们的实现机制、性能特征及适用场景有所不同。
(1)Hashtable
Hashtable 是 Java 早期的线程安全哈希表实现,它通过对所有方法进行同步(即加锁)来保证线程安全。这种实现确保了在多线程环境下对哈希表的访问是互斥的,然而,由于同步的粒度是整个对象,因此它的性能在高并发场景下往往较差,尤其是当多个线程同时访问 Hashtable 时,所有的读写操作都会被串行化执行,造成较高的锁竞争。
以下为使用Hashtable的案例:
import java.util.*;
public class HashtableExample {
public static void main(String[] args) {
// 创建一个线程安全的 Hashtable
Hashtable<Integer, String> hashtable = new Hashtable<>();
// 启动多个线程同时对 Hashtable 进行操作
Thread thread1 = new Thread(() -> {
for (int i = 0; i < 1000; i++) {
hashtable.put(i, "Value" + i);
}
});
Thread thread2 = new Thread(() -> {
for (int i = 1000; i < 2000; i++) {
hashtable.put(i, "Value" + i);
}
});
thread1.start();
thread2.start();
// 等待线程结束
try {
thread1.join();
thread2.join();
} catch (InterruptedException e) {
e.printStackTrace();
}
// 打印 Hashtable 的大小
System.out.println("Hashtable size: " + hashtable.size());
}
}
这里我们对Hashtable的优缺点进行分析一下:
优点:
- Hashtable 在并发访问时通过对所有方法加锁保证了线程安全,确保了数据一致性。
缺点:
- 性能瓶颈:由于所有方法都加锁,多个线程并发访问 Hashtable 时会发生锁竞争,导致性能显著下降。特别是在读多写少的场景下,所有线程都需要等待锁的释放,影响并发性能。
- 较老的实现:Hashtable 是 Java 1.0 中引入的,随着 Java 的发展,它已经不再是推荐的线程安全哈希表实现。现代 Java 开发中,ConcurrentHashMap 是更为常用的线程安全哈希表实现。
(2)ConcurrentHashMap
ConcurrentHashMap 是 Java 中现代化的线程安全哈希表实现。它采用了分段锁机制(Segment Locking),将哈希表划分为多个段,每个段内部独立加锁,从而减少锁竞争,提高并发性能,与 Hashtable 不同,ConcurrentHashMap 支持更高效的并发读写操作,适用于读多写少的场景。
以下为使用ConcurrentHashMap的案例:
import java.util.concurrent.*;
public class ConcurrentHashMapExample {
public static void main(String[] args) {
// 创建一个线程安全的 ConcurrentHashMap
ConcurrentHashMap<Integer, String> concurrentMap = new ConcurrentHashMap<>();
// 启动多个线程同时对 ConcurrentHashMap 进行操作
Thread thread1 = new Thread(() -> {
for (int i = 0; i < 1000; i++) {
concurrentMap.put(i, "Value" + i);
}
});
Thread thread2 = new Thread(() -> {
for (int i = 1000; i < 2000; i++) {
concurrentMap.put(i, "Value" + i);
}
});
thread1.start();
thread2.start();
// 等待线程结束
try {
thread1.join();
thread2.join();
} catch (InterruptedException e) {
e.printStackTrace();
}
// 打印 ConcurrentHashMap 的大小
System.out.println("ConcurrentHashMap size: " + concurrentMap.size());
}
}
这里我们也对ConcurrentHashMap的优缺点进行分析一下:
优点:
- 性能高:ConcurrentHashMap 采用了分段锁机制,即将哈希表分为多个段(通常是 16 个段,每个段都有自己的锁),这样多个线程可以同时操作不同段的元素,减少了锁竞争。因此,ConcurrentHashMap 提供了比 Hashtable 更高的并发性能。
- 更细粒度的锁控制:对于写操作,ConcurrentHashMap 可以只锁定需要修改的段,而不是整个表。这样即使多个线程同时进行写操作,也不会造成像 Hashtable 那样的性能瓶颈。
- 高效的并发读写操作:对于读操作,ConcurrentHashMap 无需加锁,因此读操作非常高效。对于写操作,只有在修改单个段的数据时才会加锁,进一步提高了并发性能。
缺点:
- 空间开销:由于采用了分段锁机制,ConcurrentHashMap 的内部结构相对复杂,需要更多的内存来管理这些段。因此,相比 Hashtable,ConcurrentHashMap 的内存开销较大。
- 不支持
null
键和值:ConcurrentHashMap 不允许存储null
键或值,这与 Hashtable 相同,但与 HashMap 不同。在某些场景下,如果需要存储null
值,可能需要使用其他数据结构。
在当今社会的开发中使用Hashtable来确保多线程情况下的使用哈希表的线程安全已经不在常见,现在都是使用ConcurrentHashMap了!
以上就是本篇文章的全部内容了~~~