📌 同步与异步,阻塞与非阻塞详解
这四个概念经常在计算机科学和软件开发中出现,特别是在并发编程和系统设计中。理解它们的差异对于编写高效、可扩展的代码至关重要。让我们详细探讨每个概念:
1. 📜 同步 (Synchronous)
- 📍 定义 当一个操作开始后,必须等待该操作完成后,才能开始下一个操作。
- 📍 特点
- 调用者主动等待操作的完成。
- 结果是立即可知的。
- 代码逻辑通常更简单直接。
- 📍 示例
- 从数据库中读取数据。
- HTTP请求。
2. 📜 异步 (Asynchronous)
- 📍 定义 一个操作的开始和结束是分离的,不需要等待完成。
- 📍 特点
- 调用者不必等待,可以继续其他操作。
- 结果可能立即返回,也可能稍后通过回调、事件或其他机制返回。
- 可能需要复杂的控制逻辑或错误处理。
- 📍 示例
- JavaScript中的AJAX请求。
- Java中的
Future
或CompletableFuture
.
3. 📜 阻塞 (Blocking)
- 📍 定义 当某个操作不能立即完成时,会使调用者等待,直到该操作完成。
- 📍 特点
- 调用者不能继续执行,直到阻塞操作完成。
- 在多线程环境中,其他线程可能仍然继续执行。
- 📍 示例
- 读取磁盘文件,如果文件还没准备好,操作会阻塞。
- 在没有数据可用的情况下,从网络套接字读取。
4. 📜 非阻塞 (Non-blocking)
- 📍 定义 即使操作不能立即完成,也不会阻止调用者继续执行。
- 📍 特点
- 调用者可以继续执行其他操作,不必等待。
- 如果操作不能立即完成,通常会返回一个状态,告知调用者稍后重试。
- 📍 示例
- 非阻塞I/O操作。
- 数据库的轮询查询。
⚠️ 注意
- 同步和异步通常描述的是操作的完成时间与其开始时间的关系。
- 阻塞和非阻塞通常描述的是操作的开始是否会暂停调用者的执行。
在设计系统或写代码时,选择合适的同步/异步和阻塞/非阻塞策略是关键。选择的策略会影响性能、可扩展性以及代码的复杂性。
🌟 CAS (Compare-And-Swap) 详解
-
📖 定义
- CAS是一种无锁算法。它是一种原子操作,用于在多线程环境下管理变量的并发操作。
- CAS操作包括三个参数:一个内存位置(V)、预期原值(A)和新值(B)。
- 如果内存位置V的值与预期原值A相匹配,则将该位置的值更新为B,否则不做任何操作。
- 典型的CAS操作返回一个布尔值或者之前的值,以指示操作是否成功。
-
🖥️ 工作原理
-
- 📜 从内存位置V读取当前值。
-
- 🔍 比较当前值与预期值A。
-
- 💡 如果值相等,使用新值B更新内存位置V的值。
-
- 🔄 如果不相等,可能需要重新尝试,或者返回失败。
-
-
💥 优势
- 🚫 无锁 CAS不需要传统的锁机制来实现并发控制。
- 🚀 性能 在某些场景下,尤其是低竞争的环境中,CAS比锁具有更好的性能。
- 🔒 线程安全 CAS提供了一种方法,可以确保数据在多线程环境中的安全性。
-
🚧 限制和问题
-
- 🔄 自旋 如果不成功,可能需要多次尝试。在高竞争的环境中,可能会导致大量的自旋。
-
- 📛 ABA问题 如果V的值在A和B之间变化,然后又回到了A,CAS会认为它从未改变过。这是一个潜在的问题,可能需要额外的机制来处理。
-
- 🔍 只能保证一个共享变量的原子操作 对于多个共享变量,CAS无法做到原子性。
-
-
🔧 Java中的实现
- 📚 Java的
java.util.concurrent.atomic
包为多种变量类型(如AtomicInteger
、AtomicLong
等)提供了CAS操作。 - 💻 在Java中,CAS是通过原生方法(如
Unsafe.compareAndSwapInt
)实现的。
- 📚 Java的
⚠️ 注意 尽管CAS提供了一个无锁的并发解决方案,但它并不总是最佳选择。在某些场景下,传统的锁或其他并发工具可能更为适合。
🌟 使用Concurrent
集合
在Java中,java.util.concurrent
包提供了一系列线程安全的集合类,这些集合类被设计用于支持高并发操作,而无需使用外部同步。以下是一些常用的Concurrent集合:
1. 📂 List
- 📜
CopyOnWriteArrayList
:- 常用方法
add(E e)
: 向列表的末尾添加一个元素。addIfAbsent(E e)
: 如果列表中尚未存在该元素,则添加该元素。remove(Object o)
: 从列表中移除第一个出现的指定元素。get(int index)
: 返回列表中指定位置的元素。set(int index, E element)
: 替换列表中指定位置的元素。size()
: 返回列表中的元素数量。
- 特点
- 读操作无锁,写操作会创建数组的新副本。
- 适用于读多写少的场景。
- 写操作的开销较大,因为每次写都需要复制整个数组。
- 结构
- 基于数组。
- 原理
- 采用“写时复制”策略,即当列表被修改时(如添加、删除元素),会创建该列表的一个新副本。
- 使用场景及示例 读操作远多于写操作的高并发场景。实时读取配置信息。
- 实时读取配置信息
class ConfigManager { private final CopyOnWriteArrayList<String> configItems = new CopyOnWriteArrayList<>(); public void addConfig(String config) { configItems.add(config); } public List<String> getConfigs() { return configItems; } } ConfigManager manager = new ConfigManager(); manager.addConfig("DATABASE_URL=jdbc:mysql://localhost:3306/mydb"); List<String> currentConfig = manager.getConfigs();
- 实时读取配置信息
- 常用方法
2. 📂 Set
- 📜
CopyOnWriteArraySet
:- 常用方法
add(E e)
: 向集合中添加一个元素,如果集合已经包含此元素,则不进行任何操作。remove(Object o)
: 从集合中移除一个元素。contains(Object o)
: 判断集合是否包含指定的元素。size()
: 返回集合中的元素数量。
- 特点 与
CopyOnWriteArrayList
类似。 - 结构 基于
CopyOnWriteArrayList
。 - 原理 使用“写时复制”策略。
- 使用场景及示例 与
CopyOnWriteArrayList
类似,但不需要保持元素的顺序。- 注册监听器或订阅者
class EventDispatcher { private final CopyOnWriteArraySet<EventListener> listeners = new CopyOnWriteArraySet<>(); public void registerListener(EventListener listener) { listeners.add(listener); } public void dispatchEvent(Event event) { for (EventListener listener : listeners) { listener.handleEvent(event); } } }
- 注册监听器或订阅者
- 常用方法
- 📜
ConcurrentSkipListSet
:- 常用方法
add(E e)
: 将指定元素添加到此集合。remove(Object o)
: 从集合中移除指定元素的单个实例(如果存在)。contains(Object o)
: 判断集合是否包含指定的元素。first()
: 返回此集合中的第一个(最低)元素。last()
: 返回此集合中的最后一个(最高)元素。
- 特点
- 线程安全。
- 提供排序。
- 结构 跳表。
- 原理 使用跳表数据结构,每个节点都有多个指针指向不同的层。
- 使用场景及示例 需要排序的线程安全的Set。
- 排行榜应用
class Scoreboard { private final ConcurrentSkipListSet<PlayerScore> scores = new ConcurrentSkipListSet<>(); public void addScore(PlayerScore score) { scores.add(score); } public Set<PlayerScore> getTopScores(int n) { return scores.headSet(new PlayerScore("", n)); } }
- 排行榜应用
- 常用方法
3. 📂 Queue
- 📜
ConcurrentLinkedQueue
:- 常用方法
offer(E e)
: 将指定的元素添加到此队列的尾部。poll()
: 检索并移除此队列的头部,如果此队列为空,则返回 null。peek()
: 检索但不移除此队列的头部,如果此队列为空,则返回 null。
- 特点
- 非阻塞。
- 线程安全。
- 结构 基于链接节点。
- 原理 使用无锁算法实现。
- 使用场景及示例 高并发场景中的非阻塞队列。
- 无阻塞任务队列
class TaskQueue { private final ConcurrentLinkedQueue<Task> tasks = new ConcurrentLinkedQueue<>(); public void addTask(Task task) { tasks.offer(task); } public Task fetchTask() { return tasks.poll(); } }
- 无阻塞任务队列
- 常用方法
- 📜
LinkedBlockingQueue
:- 常用方法
offer(E e)
: 将指定的元素添加到此队列的尾部,如果可以立即执行而不需要等待。take()
: 检索并移除此队列的头部,如有必要,等待元素变得可用。put(E e)
: 将指定的元素添加到此队列的尾部,如有必要,等待空间变得可用。
- 特点
- 阻塞。
- 可选的容量。
- 结构 基于链接节点。
- 原理 使用内部锁和条件来实现阻塞操作。
- 使用场景及示例 生产者-消费者模型中的任务队列。
- 生产者-消费者模型中的任务队列
class ProducerConsumerExample { private final LinkedBlockingQueue<Item> queue = new LinkedBlockingQueue<>(10); class Producer extends Thread { public void run() { while (true) { Item item = produceItem(); queue.put(item); } } } class Consumer extends Thread { public void run() { while (true) { Item item = queue.take(); consumeItem(item); } } } }
- 生产者-消费者模型中的任务队列
- 常用方法
- 📜
ArrayBlockingQueue
:- 常用方法
offer(E e)
: 将指定的元素添加到此队列的尾部,如果可以立即执行而不需要等待。take()
: 检索并移除此队列的头部,如有必要,等待元素变得可用。put(E e)
: 将指定的元素添加到此队列的尾部,如有必要,等待空间变得可用。
- 特点
- 有界。
- 阻塞。
- 结构 基于数组。
- 原理 使用内部锁和条件来实现阻塞操作。
- 使用场景 固定大小的任务队列。
- 📍 使用场景及示例
- 固定大小的任务队列
class TaskManager { private final ArrayBlockingQueue<Task> taskQueue = new ArrayBlockingQueue<>(100); public void submitTask(Task task) throws InterruptedException { taskQueue.put(task); // 将任务放入队列,如果队列已满,这将阻塞 } public Task fetchTask() throws InterruptedException { return taskQueue.take(); // 获取任务,如果队列为空,这将阻塞 } } TaskManager manager = new TaskManager(); manager.submitTask(new Task()); Task task = manager.fetchTask();
- 固定大小的任务队列
- 常用方法
⚠️ 注意 ArrayBlockingQueue
的容量是固定的,一旦创建,就不能更改。添加元素时,如果队列已满,操作将阻塞,直到队列中有可用空间。同样,如果尝试从空队列中取出元素,操作将阻塞,直到队列中有可用元素。
- 📜
SynchronousQueue
:- 常用方法
put(E e)
: 将指定的元素添加到此队列,等待另一个线程来接收它。take()
: 检索并移除此队列的头部,如有必要,等待元素变得可用。
- 特点
- 无存储空间。
- 用于线程间的数据传输。
- 结构 不存储元素。
- 原理 一个线程的插入操作会等待另一个线程的删除操作,反之亦然。
- 使用场景及示例 线程池。
- 线程之间的数据交换
class DataExchanger { private final SynchronousQueue<Data> dataQueue = new SynchronousQueue<>(); class DataProducer extends Thread { public void run() { Data data = produceData(); dataQueue.put(data); } } class DataConsumer extends Thread { public void run() { Data data = dataQueue.take(); processData(data); } } }
- 线程之间的数据交换
- 常用方法
- 📜
DelayQueue
:- 常用方法
offer(E e)
: 将指定的元素添加到此队列的尾部。take()
: 检索并移除此队列的头部,如有必要,等待元素变得可用。
- 特点
- 无界。
- 元素有延迟时间。
- 结构 基于
PriorityQueue
。 - 原理 只有当元素的延迟时间到达时,它才能从队列中被获取。
- 使用场景及示例 任务调度,定时任务。
- 任务调度
class TaskScheduler { private final DelayQueue<DelayedTask> taskQueue = new DelayQueue<>(); public void scheduleTask(DelayedTask task) { taskQueue.put(task); } class Worker extends Thread { public void run() { while (true) { DelayedTask task = taskQueue.take(); task.execute(); } } } }
- 任务调度
- 常用方法
4. 📂 Deque (双端队列)
- 📜
ConcurrentLinkedDeque
:- 常用方法
offerFirst(E e)
: 在此双端队列的前面插入指定的元素。offerLast(E e)
: 在此双端队列的末尾插入指定的元素。pollFirst()
: 检索并移除此双端队列的第一个元素,如果此双端队列为空,则返回 null。pollLast()
: 检索并移除此双端队列的最后一个元素,如果此双端队列为空,则返回 null。
- 特点
- 非阻塞。
- 线程安全。
- 结构 基于链接节点。
- 原理 使用无锁算法实现。
- 使用场景及示例 双端操作的非阻塞队列。
- 双向任务队列
class TaskDeque { private final ConcurrentLinkedDeque<Task> tasks = new ConcurrentLinkedDeque<>(); public void addTaskToFront(Task task) { tasks.offerFirst(task); } public void addTaskToEnd(Task task) { tasks.offerLast(task); } public Task fetchTaskFromFront() { return tasks.pollFirst(); } public Task fetchTaskFromEnd() { return tasks.pollLast(); } }
- 双向任务队列
- 常用方法
- 📜
LinkedBlockingDeque
:- 常用方法
putFirst(E e)
: 在此双端队列的前面插入指定的元素,如有必要,等待空间变得可用。putLast(E e)
: 在此双端队列的末尾插入指定的元素,如有必要,等待空间变得可用。takeFirst()
: 检索并移除此双端队列的第一个元素,如有必要,等待元素变得可用。takeLast()
: 检索并移除此双端队列的最后一个元素,如有必要,等待元素变得可用。
- 特点
- 阻塞。
- 可选的容量。
- 结构 基于链接节点。
- 原理 使用内部锁和条件来实现阻塞操作。
- 使用场景及示例 双端操作的有界任务队列。
- 生产者-消费者模型中的双端任务队列
class ProducerConsumerDequeExample { private final LinkedBlockingDeque<Item> deque = new LinkedBlockingDeque<>(10); class Producer extends Thread { public void run() { while (true) { Item item = produceItem(); deque.putFirst(item); } } } class Consumer extends Thread { public void run() { while (true) { Item item = deque.takeLast(); consumeItem(item); } } } }
- 生产者-消费者模型中的双端任务队列
- 常用方法
5. 📂 Map
- 📜
ConcurrentHashMap
:- 常用方法
put(K key, V value)
: 将指定的值与此映射中的指定键关联。get(Object key)
: 返回与指定键关联的值,如果此映射中不包含该键的映射关系,则返回 null。remove(Object key)
: 如果存在,则移除此映射中与键关联的映射关系。
- 特点
- 高并发。
- 线程安全。
- 结构 哈希表。
- 原理 使用分段锁技术。
- 使用场景及示例 大规模、高并发的缓存。
- 缓存实现
class CacheManager { private final ConcurrentHashMap<String, Object> cache = new ConcurrentHashMap<>(); public void putToCache(String key, Object value) { cache.put(key, value); } public Object getFromCache(String key) { return cache.get(key); } }
- 缓存实现
- 常用方法
- 📜
ConcurrentSkipListMap
:- 常用方法
put(K key, V value)
: 将指定的值与此映射中的指定键关联。get(Object key)
: 返回与指定键关联的值,如果此映射中不包含该键的映射关系,则返回 null。remove(Object key)
: 如果存在,则移除此映射中与键关联的映射关系。firstKey()
: 返回此映射中当前的第一个(最低)键。lastKey()
: 返回此映射中当前的最后一个(最高)
- 特点
- 线程安全。
- 提供排序。
- 结构 跳表。
- 原理 使用跳表数据结构。
- 使用场景及示例 需要排序的线程安全的Map。
- 排行榜应用
class ScoreboardMap { private final ConcurrentSkipListMap<String, Integer> scores = new ConcurrentSkipListMap<>(); public void addScore(String playerName, int score) { scores.put(playerName, score); } public int getScore(String playerName) { return scores.get(playerName); } }
- 排行榜应用
- 常用方法
⚠️ 注意 这些Concurrent集合的设计目标是提供更高的并发性能,特别是在高并发、读多写少的场景中。在使用这些集合时,应考虑它们的特性和适用场景,以确保选择最合适的数据结构。
6. 📂 Atomic
🔵 AtomicBoolean
-
📍 常用方法
get()
: 返回当前值。set(boolean newValue)
: 设置新值。compareAndSet(boolean expect, boolean update)
: 如果当前值 == 预期值,则原子地设置为新值。
-
📍 特点
- 原子更新布尔值。
- 无锁。
-
📍 结构 内部使用一个
volatile int
来表示布尔值。 -
📍 原理 利用 CPU 提供的原子指令来实现原子操作。
-
📍 使用场景及示例
-
控制一次性初始化(例如单例模式中的懒加载):
class Singleton { // 使用AtomicBoolean来标记是否已经初始化 private static final AtomicBoolean isInitialized = new AtomicBoolean(false); private static Singleton instance; public static Singleton getInstance() { // 首次检查,提高效率 if (!isInitialized.get()) { synchronized(Singleton.class) { // 双重检查锁定,确保只初始化一次 if (instance == null && !isInitialized.get()) { instance = new Singleton(); isInitialized.set(true); } } } return instance; } }
-
作为开关或标志来控制多线程中的操作:
class TaskRunner extends Thread { // 使用AtomicBoolean作为运行标志 private final AtomicBoolean shouldRun = new AtomicBoolean(true); @Override public void run() { // 当标志为true时,继续执行任务 while (shouldRun.get()) { // 执行相关任务... } } // 设置标志为false来停止任务 public void stopTask() { shouldRun.set(false); } }
-
🔵 AtomicInteger
-
📍 常用方法
get()
set(int newValue)
getAndIncrement()
getAndDecrement()
compareAndSet(int expect, int update)
-
📍 特点
- 原子更新整数值。
- 无锁。
-
📍 结构 内部使用一个
volatile int
。 -
📍 原理 利用 CPU 提供的原子指令来实现原子操作。
-
📍 使用场景及示例
-
作为计数器,例如跟踪在线用户数或监控指标:
class UserManager { // 使用AtomicInteger跟踪在线用户数 private final AtomicInteger onlineUsersCount = new AtomicInteger(0); // 用户登录时增加计数 public void userLoggedIn() { onlineUsersCount.incrementAndGet(); } // 用户注销时减少计数 public void userLoggedOut() { onlineUsersCount.decrementAndGet(); } // 获取当前在线用户数 public int getOnlineUsersCount() { return onlineUsersCount.get(); } }
-
控制资源的访问数量:
class ResourceLimiter { // 使用AtomicInteger跟踪资源的使用数量 private final AtomicInteger resourceCount = new AtomicInteger(0); private static final int LIMIT = 10; // 尝试使用资源 public boolean tryUseResource() { while (true) { int current = resourceCount.get(); if (current >= LIMIT) { return false; // 资源已达上限 } if (resourceCount.compareAndSet(current, current + 1)) { return true; // 资源使用成功 } } } // 释放资源 public void releaseResource() { resourceCount.decrementAndGet(); } }
-
🔵 AtomicLong
- ⚠️ 注意: 和
AtomicInteger
类似,只是它操作的是长整数。 - 📍 使用场景及示例
-
作为大数字的计数器:
class TransactionManager { // 使用AtomicLong跟踪交易ID private final AtomicLong transactionId = new AtomicLong(0); // 获取下一个交易ID public long getNextTransactionId() { // 自增并返回新的交易ID return transactionId.incrementAndGet(); } }
-
跟踪系统中的累计值,如总计请求量:
class RequestMetrics { // 使用AtomicLong跟踪系统的总请求数量 private final AtomicLong totalRequests = new AtomicLong(0); // 每次请求时增加计数 public void logRequest() { totalRequests.incrementAndGet(); } // 获取当前的总请求数量 public long getTotalRequests() { return totalRequests.get(); } }
-
🔵 AtomicReference
- 📍 常用方法
get()
set(V newValue)
compareAndSet(V expect, V update)
- 📍 特点
- 原子更新引用类型。
- 无锁。
- 📍 结构 内部使用一个
volatile V
来表示引用。 - 📍 原理 利用 CPU 提供的原子指令来实现原子操作。
- 📍 使用场景及示例
-
在无锁算法中替换对象的引用:
class ConfigManager<T> { // 使用AtomicReference存储配置对象 private final AtomicReference<T> configReference = new AtomicReference<>(); // 更新配置 public void updateConfig(T newConfig) { configReference.set(newConfig); } // 获取当前的配置 public T getConfig() { return configReference.get(); } }
-
作为缓存策略的一部分,例如跟踪最近的对象:
class Cache<K, V> { private final Map<K, AtomicReference<V>> cacheMap = new ConcurrentHashMap<>(); // 设置缓存项 public void set(K key, V value) { cacheMap.computeIfAbsent(key, k -> new AtomicReference<>()).set(value); } // 获取缓存项 public V get(K key) { AtomicReference<V> ref = cacheMap.get(key); return ref == null ? null : ref.get(); } }
-
🔵 AtomicIntegerArray, AtomicLongArray, AtomicReferenceArray
- ⚠️ 注意: 这些类为数组中的元素提供了原子操作。
- 它们的常用方法与单个原子类的方法类似,但都带有一个数组索引参数。
- 📍 使用场景及示例
- 管理一个固定大小的原子变量集合:
class Scoreboard { // 使用AtomicIntegerArray跟踪每个玩家的得分 private final AtomicIntegerArray scores; public Scoreboard(int playerCount) { scores = new AtomicIntegerArray(playerCount); } // 增加玩家的得分 public void addScore(int playerIndex, int delta) { scores.addAndGet(playerIndex, delta); } // 获取玩家的得分 public int getScore(int playerIndex) { return scores.get(playerIndex); } }
- 管理一个固定大小的原子变量集合:
🔵 AtomicMarkableReference
- 📍 常用方法
get()
: 返回当前引用和标记。set(V newReference, boolean newMark)
: 设置新引用和标记。compareAndSet(V expectedReference, V newReference, boolean expectedMark, boolean newMark)
: 如果当前引用和标记与预期的引用和标记匹配,则原子地设置新引用和标记。
- 📍 使用场景及示例
- 解决 ABA 问题:
class Stack<T> { private static class Node<T> { final T item; Node<T> next; Node(T item) { this.item = item; } } private final AtomicStampedReference<Node<T>> top = new AtomicStampedReference<>(null, 0); public void push(T item) { Node<T> newNode = new Node<>(item); int[] stampHolder = new int[1]; while (true) { Node<T> currentTop = top.get(stampHolder); newNode.next = currentTop; if (top.compareAndSet(currentTop, newNode, stampHolder[0], stampHolder[0] + 1)) { break; } } } public T pop() { int[] stampHolder = new int[1]; while (true) { Node<T> currentTop = top.get(stampHolder); if (currentTop == null) return null; if (top.compareAndSet(currentTop, currentTop.next, stampHolder[0], stampHolder[0] + 1)) { return currentTop.item; } } } }
- 解决 ABA 问题:
🔵 AtomicStampedReference
- ⚠️ 注意: 和
AtomicMarkableReference
类似,只是它的标记是一个整数,而不是一个布尔值。