大家好,我是大圣,好久不见。
在我们上次的探讨中,我们深入了解了并发、并行和高并发这三个核心概念,它们都在我们的现代计算环境中扮演着关键的角色,使得系统能够更加高效地满足大量的请求。
在这篇文章中,我们将进一步探索多线程和异步编程的魅力,了解并发读写的挑战。
多线程和异步编程
实现高并发的方法有:多线程和多进程、负载均衡、缓存技术、数据库优化、异步处理、分布式系统等。
下面我们来详细说一下多线程和异步编程。
多线程
定义
多线程是一种允许单个程序创建多个并行执行流(线程)的技术。这些线程可以并发执行,每个线程都有自己的一套寄存器、程序计数器和栈,但它们会共享同一进程中的其他资源,如代码、数据和文件。
现代操作系统和多核 CPU 使得真正的线程并行执行成为可能,这意味着多个线程可以在不同的 CPU 核上同时执行。对于单核 CPU,操作系统通过时间片切换技术使得各个线程轮流执行,给人一种“并行”的错觉。
举例理解
上面这种专业的定义,理解起来比较晦涩,大家可以看下面我举得这个例子:
想象一下,你在一条单车道的路上驾驶,这条路代表了单线程。无论有多少车,它们都必须一个接一个地行驶,无法超车或并行行驶。但如果这是一条多车道的高速公路,每条车道都像一个线程,多辆车可以并行行驶,增加了整体的吞吐量。
多线程如何实现高并发?
举例理解
想象一下你进入了一个餐厅。这个餐厅只有一个服务员,不管餐厅里有多少客人,这个服务员都需要一个一个去服务。这种情况下,如果有 10 个客人同时进来,他们必须排队等待。这就像一个单线程的系统,无法处理高并发。
现在,假设餐厅决定雇佣更多的服务员。当 10 个客人同时进入餐厅,有 10 个服务员,每个服务员都可以为一个客人服务,所有的客人都能同时得到服务,没有等待时间。这就是多线程的方式,每个服务员都代表一个线程。
专业解释
在计算机领域,当有大量的用户请求到来时,如果只有一个执行线程,那么请求就需要排队等待。但如果系统采用了多线程技术,每个请求都可以由一个单独的线程来处理,从而实现真正的并行处理,大大提高了系统的并发处理能力。
这就是多线程如何帮助实现高并发的原理。通过多线程,系统能够同时处理多个任务,而不是顺序执行,从而大大提高了整体的执行效率和响应速度。
同样,在 Java 程序中,如果我们面临大量的用户请求或任务,而我们只有一个线程来处理这些请求,那么它们将会一个接一个地被处理。但是,如果我们使用多线程,那么我们可以并行地处理多个请求,大大提高了应对高并发的能力。
Java 代码实现
Java 提供了丰富的库和工具,如 Thread 类、ExecutorService 和 ThreadPoolExecutor 等,来帮助开发者方便地创建和管理线程。如下列方式:
ExecutorService executor = Executors.newFixedThreadPool(5); // 创建一个固定大小的线程池
for (int i = 0; i < 10; i++) {
Runnable worker = new MyRunnable();
executor.execute(worker);
}
executor.shutdown(); // 关闭线程池
总的来说,多线程允许程序同时执行多个任务,从而增加了程序的并发处理能力。当面临高并发的情况时,合理地使用多线程可以帮助我们提高系统的响应速度和吞吐量。
但是多线程去实现高并发的话,当多个线程去同时修改一个值的时候,这里就会出现并发写的问题。这个我们在后面会详细说。
异步编程
定义
异步编程是一种程序设计方法,它允许任务能够独立于主程序运行,这意味着您可以继续执行其他任务,而不必等待该任务完成。
举例理解异步编程
考虑一个简单的场景:你有一个应用,需要从数据库中读取数据、从网络上下载文件和写入日志文件。在传统的同步编程中,你可能会先读取数据库,等待数据返回后,再下载文件,下载完成后,再写入日志。但在高并发的环境下,这种等待将造成资源的浪费。
但如果使用异步编程,你可以在启动数据库查询后,立即开始文件下载;在这两个操作在后台运行的同时,你还可以写入日志。所有操作都没有相互阻塞。
异步编程的思想
1)非阻塞: 主线程启动一个异步任务后,不会傻傻地等待,而是可以继续执行其他任务。
2)事件驱动: 当异步任务完成时,会触发一个事件通知主线程。
3)回调函数: 一般与事件配合,异步任务完成后由主线程调用。
异步编程实现高并发
异步编程通过允许任务在后台执行,使得主线程可以处理更多的任务。这样,在高并发的环境下,系统能够更有效地使用资源,处理更多的请求。
Java 代码实现
在 Java 中,CompletableFuture 是处理异步编程的常用工具。下面是一个简单的示例:
import java.util.concurrent.CompletableFuture;
public class AsyncExample {
public static void main(String[] args) {
// 创建一个异步任务
CompletableFuture<Void> future = CompletableFuture.runAsync(() -> {
try {
Thread.sleep(2000); // 模拟一个长时间的操作
System.out.println("异步任务完成");
} catch (InterruptedException e) {
e.printStackTrace();
}
});
System.out.println("主线程继续执行");
// 等待异步任务完成
future.join();
System.out.println("主线程结束");
}
}
在上述代码中,我们创建了一个异步任务,该任务仅仅是等待 2 秒然后打印出消息。而主线程在启动异步任务后,不必等待它完成,而是继续执行。这是一个简单的示例,但它展示了异步编程如何允许主线程和其他任务并发执行。
并发写与并发读
并发写
定义
并发写(Concurrent Writes)是指在同一时间段内,多个客户端或线程试图同时写入(或更新)数据库或文件系统中的同一个数据项或资源。
为了更好的理解并发写的概念,大家可以看下面例子:
场景 1:没有冲突的并发写
时间点 1:客户端 1 开始编辑文档,他正在编写第 1 段。
时间点 2:客户端 2 开始编辑文档,但他正在编辑第 3 段(不是第 1 段)。
时间点 3:客户端 1 保存他对第 1 段的更改。
时间点 4:客户端 2 保存他对第 3 段的更改。
在这个情况下,即使两个客户端在同一时间段内编辑文档,也没有问题,因为他们编辑的是文档的不同部分,没有产生冲突。
场景 2:产生冲突的并发写
时间点 1:客户端 1 开始编辑文档,他正在编写第 1 段。
时间点 2:客户端 2 也开始编辑文档,他也正在编辑第 1 段。
时间点 3:客户端 1 保存他对第 1 段的更改。
时间点 4:客户端 2 尝试保存他对第 1 段的更改,但系统提示他第 1 段已被更改,让他选择是覆盖更改还是合并更改。
在这个情况下,两个客户端在同一时间段内编辑了文档的同一部分,产生了写冲突。
所以,“同一时间段内的并发写”可能会根据写操作是否影响到同一数据项而导致冲突或不冲突。希望这个例子能帮助你理解“同一时间段”这一概念和并发写可能出现的情况。
小结
在多用户或多线程的环境中,这是很常见的现象,但它也带来了一些问题和挑战,主要包括:
数据一致性问题、竞态条件。
为了解决这些问题和挑战,通常会使用一系列技术和机制来控制并发写,包括:锁机制、事务管理、乐观并发控制等。
具体怎么解决并发写出现冲突的问题,我们在锁和 MVCC 部分会详细说的。
并发读
定义
并发读是指多个客户端或线程在同一时间段内尝试读取数据库、文件系统或其他共享资源中的相同数据或资源。
举例理解
想象一个图书馆的情境:假如有一本非常受欢迎的新书。多个读者(线程)同时想要读这本书(数据)。他们都可以同时坐下来阅读书中的内容,而不会影响到其他读者的阅读体验。
并发读思想
1)无害性:读操作本身不修改数据,所以理论上,多个线程同时读取同一资源不会造成冲突或数据不一致。
2)高效:多个线程可以并发地读取数据,从而提高系统的响应速度和吞吐量。
实现高并发
并发读可以提高系统的并发性,因为:
1)读操作可以被同时发起和执行,不需要等待其他读操作完成。
2)读操作不会阻塞其他操作(除非涉及到某些锁定策略)。
Java 代码实现
以下是一个简单的 Java 示例,展示了使用 ReentrantReadWriteLock 来支持并发读。
import java.util.concurrent.locks.ReadWriteLock;
import java.util.concurrent.locks.ReentrantReadWriteLock;
public class ConcurrentData {
private int data;
private final ReadWriteLock rwLock = new ReentrantReadWriteLock();
public int readData() {
rwLock.readLock().lock(); // 使用读锁
try {
// 读取数据
return data;
} finally {
rwLock.readLock().unlock();
}
}
public void writeData(int value) {
rwLock.writeLock().lock(); // 使用写锁
try {
this.data = value;
} finally {
rwLock.writeLock().unlock();
}
}
}
public class Main {
public static void main(String[] args) {
ConcurrentData sharedData = new ConcurrentData();
// 启动多个线程进行并发读
for (int i = 0; i < 5; i++) {
new Thread(() -> {
System.out.println(Thread.currentThread().getName() + " reads: " + sharedData.readData());
}).start();
}
}
}
在这个例子中,ConcurrentData 类使用了 ReentrantReadWriteLock 来允许多个线程并发读取数据,但当数据被写入时,其他线程(无论读或写)都会被阻塞,直到写操作完成。
总结
本文我们说了多线程、异步编程、并发写、并发读的相关知识。有什么说的不对的地方欢迎各位小伙伴与我私聊讨论。
下一篇文章我会继续说,解决并发读写用到的锁和 MVCC 等知识,让大家对并发编程有一个全局的认识。
可能有小伙伴觉得光了解这些概念没有用,其实是非常有用的。大家不用着急,再下下篇文章中我会从一个大数据框架的源码去给大家解剖,来说明熟练掌握这些并发知识是非常有必要的,我们拭目以待。