多线程、异步编程、并发读写 新认识

2023年 10月 7日 84.4k 0

大家好,我是大圣,好久不见。

在我们上次的探讨中,我们深入了解了并发、并行和高并发这三个核心概念,它们都在我们的现代计算环境中扮演着关键的角色,使得系统能够更加高效地满足大量的请求。

在这篇文章中,我们将进一步探索多线程和异步编程的魅力,了解并发读写的挑战。

多线程和异步编程

实现高并发的方法有:多线程和多进程、负载均衡、缓存技术、数据库优化、异步处理、分布式系统等。
下面我们来详细说一下多线程和异步编程。

多线程

定义

多线程是一种允许单个程序创建多个并行执行流(线程)的技术。这些线程可以并发执行,每个线程都有自己的一套寄存器、程序计数器和栈,但它们会共享同一进程中的其他资源,如代码、数据和文件。

现代操作系统和多核 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 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  {
                System.out.println(Thread.currentThread().getName() + " reads: " + sharedData.readData());
            }).start();
        }
    }
}

在这个例子中,ConcurrentData 类使用了 ReentrantReadWriteLock 来允许多个线程并发读取数据,但当数据被写入时,其他线程(无论读或写)都会被阻塞,直到写操作完成。

总结

本文我们说了多线程、异步编程、并发写、并发读的相关知识。有什么说的不对的地方欢迎各位小伙伴与我私聊讨论。

下一篇文章我会继续说,解决并发读写用到的锁和 MVCC 等知识,让大家对并发编程有一个全局的认识。

可能有小伙伴觉得光了解这些概念没有用,其实是非常有用的。大家不用着急,再下下篇文章中我会从一个大数据框架的源码去给大家解剖,来说明熟练掌握这些并发知识是非常有必要的,我们拭目以待。

本文由博客一文多发平台 OpenWrite 发布!

相关文章

JavaScript2024新功能:Object.groupBy、正则表达式v标志
PHP trim 函数对多字节字符的使用和限制
新函数 json_validate() 、randomizer 类扩展…20 个PHP 8.3 新特性全面解析
使用HTMX为WordPress增效:如何在不使用复杂框架的情况下增强平台功能
为React 19做准备:WordPress 6.6用户指南
如何删除WordPress中的所有评论

发布评论