专注Java教育14年 全国咨询/投诉热线:444-1124-454
赢咖4LOGO图
始于2009,口口相传的Java黄埔军校
首页 hot资讯 高并发应用的设计原则和模式

高并发应用的设计原则和模式

更新时间:2021-08-13 10:17:14 来源:赢咖4 浏览731次

1.概述

在本教程中,我们将讨论随着时间的推移建立的一些设计原则和模式,以构建高度并发的应用程序。

但是,值得注意的是,设计并发应用程序是一个广泛而复杂的主题,因此没有任何教程可以声称对其进行了详尽的处理。我们将在这里介绍一些经常使用的流行技巧!

2.并发基础

在我们继续之前,让我们花一些时间了解基础知识。首先,我们必须澄清我们对所谓的并发程序的理解。如果多个计算同时发生,我们指的是一个程序是并发的。

现在,请注意我们已经提到了同时发生的计算——也就是说,它们同时进行。但是,它们可能会或可能不会同时执行。理解差异很重要,因为同时执行的计算被称为并行。

(1)如何创建并发模块?

了解我们如何创建并发模块很重要。有很多选择,但我们将在这里重点介绍两个流行的选择:

进程:进程是一个正在运行的程序的实例,它与同一台机器上的其他进程隔离。机器上的每个进程都有自己独立的时间和空间。因此,通常不可能在进程之间共享内存,它们必须通过传递消息进行通信。

线程:另一方面,线程只是进程的一部分。一个程序中可以有多个线程共享相同的内存空间。但是,每个线程都有唯一的堆栈和优先级。线程可以是本地的(由操作系统本地调度)或绿色的(由运行时库调度)。

(2)并发模块如何交互?

如果并发模块不必通信,这是非常理想的,但通常情况并非如此。这产生了两种并发编程模型:

共享内存:在这个模型中,并发模块通过在内存中读写共享对象来交互。这通常会导致并发计算的交错,从而导致竞争条件。因此,它可能不确定地导致不正确的状态。

消息传递:在此模型中,并发模块通过通信通道相互传递消息来进行交互。在这里,每个模块按顺序处理传入的消息。由于没有共享状态,编程相对容易,但这仍然无法摆脱竞争条件!

(3)并发模块如何执行?

摩尔定律在处理器的时钟速度方面遇到瓶颈已经有一段时间了。相反,由于我们必须成长,我们开始将多个处理器集成到同一芯片上,通常称为多核处理器。但是,听到超过 32 个内核的处理器的情况并不常见。

现在,我们知道单个内核一次只能执行一个线程或一组指令。但是,进程和线程的数量可以分别为数百和数千。那么,它究竟是如何运作的呢?这是操作系统为我们模拟并发的地方。操作系统通过时间切片来实现这一点——这实际上意味着处理器在线程之间频繁、不可预测且不确定地切换。

3. 并发编程中的问题

当我们开始讨论设计并发应用程序的原则和模式时,首先了解典型问题是什么是明智的。

在很大程度上,我们在并发编程方面的经验涉及使用具有共享内存的本机线程。因此,我们将重点关注由此产生的一些常见问题:

互斥(Synchronization Primitives):交错线程需要对共享状态或内存进行独占访问,以保证程序的正确性。共享资源的同步是一种流行的实现互斥的方法。有多种同步原语可供使用——例如,锁、监视器、信号量或互斥锁。但是,互斥编程很容易出错,并且通常会导致性能瓶颈。有几个与此相关的经过充分讨论的问题,例如deadlock 和 livelock。

上下文切换(重量级线程):每个操作系统都有对进程和线程等并发模块的原生支持,尽管有所不同。如前所述,操作系统提供的一项基本服务是通过时间切片调度线程以在有限数量的处理器上执行。现在,这实际上意味着线程在不同状态之间频繁切换。在这个过程中,他们当前的状态需要被保存和恢复。这是一项直接影响整体吞吐量的耗时活动。

4. 高并发设计模式

现在,我们了解了并发编程的基础知识和其中的常见问题,是时候了解一些避免这些问题的常见模式了。我们必须重申,并发编程是一项需要大量经验的艰巨任务。因此,遵循一些既定的模式可以使任务更容易。

(1)基于 Actor 的并发

我们将讨论的关于并发编程的第一个设计称为 Actor 模型。这是一个并发计算的数学模型,基本上把一切都当作一个参与者。参与者可以相互传递消息,并且可以根据消息做出本地决策。这是由 Carl Hewitt 首次提出的,并启发了许多编程语言。

Scala 并发编程的主要结构是actor。Actor 是 Scala 中的普通对象,我们可以通过实例化Actor类来创建它们。此外,Scala Actors 库提供了许多有用的 actor 操作:

class myActor extends Actor {
    def act() {
        while(true) {
            receive {
                // Perform some action
            }
        }
    }
}

在上面的示例中,在无限循环中调用receive方法会挂起 actor,直到消息到达。消息到达后,从参与者的邮箱中删除,并采取必要的行动。

参与者模型消除了并发编程的一个基本问题——共享内存。Actors 通过消息进行通信,每个 Actor 依次处理来自其专用邮箱的消息。但是,我们通过线程池执行actor。我们已经看到原生线程可能是重量级的,因此数量有限。

当然,这里还有其他模式可以帮助我们——我们稍后会介绍这些!

(2)基于事件的并发

基于事件的设计明确解决了原生线程的生成和操作成本高的问题。基于事件的设计之一是事件循环。事件循环与事件提供程序和一组事件处理程序一起工作。在此设置中,事件循环阻塞事件提供者,并在到达时将事件分派给事件处理程序。

基本上,事件循环只不过是一个事件调度器!事件循环本身可以仅在单个本机线程上运行。那么,事件循环中到底发生了什么?让我们以一个非常简单的事件循环的伪代码为例:

while(true) {
    events = getEvents();
    for(e in events)
        processEvent(e);
}

基本上,我们的事件循环所做的就是不断寻找事件,并在找到事件时处理它们。该方法非常简单,但它获得了事件驱动设计的好处。

使用这种设计构建并发应用程序可以更好地控制应用程序。此外,它还消除了多线程应用程序的一些典型问题,例如死锁。

JavaScript 实现事件循环以提供异步编程。它维护一个调用堆栈来跟踪要执行的所有函数。它还维护一个事件队列,用于发送新函数进行处理。事件循环不断检查调用堆栈并从事件队列中添加新函数。所有异步调用都被分派到 Web API,通常由浏览器提供。

事件循环本身可以在单个线程上运行,但 Web API 提供单独的线程。

(3)非阻塞算法

在非阻塞算法中,一个线程的挂起不会导致其他线程的挂起。我们已经看到,我们的应用程序中只能有有限数量的本机线程。现在,阻塞线程的算法显然会显着降低吞吐量 并阻止我们构建高度并发的应用程序。

非阻塞算法总是利用底层硬件提供的比较和交换原子原语。这意味着硬件会将内存位置的内容与给定值进行比较,并且仅当它们相同时才会将值更新为新的给定值。这可能看起来很简单,但它有效地为我们提供了一个原子操作,否则将需要同步。

这意味着我们必须编写使用这种原子操作的新数据结构和库。这为我们提供了大量的多种语言的无等待和无锁实现。Java 有几个非阻塞数据结构,如AtomicBoolean、AtomicInteger、AtomicLong和AtomicReference。

考虑一个应用程序,其中多个线程试图访问相同的代码:

boolean open = false;
if(!open) {
    // Do Something
    open=false;
}

显然,上面的代码不是线程安全的,它在多线程环境中的行为是不可预测的。我们在这里的选择是将这段代码与锁同步或使用原子操作:

AtomicBoolean open = new AtomicBoolean(false);
if(open.compareAndSet(false, true) {
    // Do Something
}

正如我们所见,使用像AtomicBoolean这样的非阻塞数据结构可以帮助我们编写线程安全的代码,而不会沉迷于锁的缺点!

5.编程语言支持

我们已经看到有多种方法可以构建并发模块。虽然编程语言确实有所作为,但主要是底层操作系统如何支持这一概念。然而,由于本机线程支持的基于线程的并发在可扩展性方面遇到了新的障碍,我们总是需要新的选择。

实施我们在上一节中讨论的一些设计实践确实证明是有效的。但是,我们必须记住,它确实使编程变得复杂。我们真正需要的是能够提供基于线程并发的强大功能而又不会带来不良影响的东西。

我们可用的一种解决方案是绿色线程。绿色线程是由运行时库调度的线程,而不是由底层操作系统本地调度的线程。虽然这并不能解决基于线程的并发中的所有问题,但在某些情况下它确实可以为我们提供更好的性能。

现在,除非我们选择使用的编程语言支持它,否则使用绿色线程并非易事。并非每种编程语言都有这种内置支持。此外,我们松散地称为绿色线程的东西可以通过不同的编程语言以非常独特的方式实现。让我们看看其中一些可供我们使用的选项。

(1)Go 中的 Goroutine

Go 编程语言中的Goroutine是轻量级线程。它们提供可以与其他函数或方法同时运行的函数或方法。Goroutines非常便宜,因为它们在堆栈大小中仅占用几千字节,从.

最重要的是,goroutines 与较少数量的本地线程复用。此外,goroutine 使用通道相互通信,从而避免访问共享内存。我们几乎得到了我们需要的一切,然后你猜怎么着——什么都不做!

(2)Erlang 中的进程

在Erlang 中,每个执行线程都称为一个进程。但是,这与我们目前讨论的过程不太一样!Erlang 进程是轻量级的,内存占用小,创建和处理速度快,调度开销低。

在幕后,Erlang 进程只不过是运行时处理调度的函数。此外,Erlang 进程不共享任何数据,它们通过消息传递相互通信。这就是我们首先称这些“过程”的原因!

(3)Java 中的 Fibers(提案)

Java 并发的故事一直在不断演变。Java 确实支持绿色线程,至少对于 Solaris 操作系统,一开始是这样。但是,由于超出本教程范围的障碍,这已停止。

从那时起,Java 中的并发就是关于本机线程以及如何巧妙地使用它们!但出于显而易见的原因,我们可能很快就会在 Java 中拥有一个新的并发抽象,称为纤程。Project Loom提议将 continuation 与Fiber一起引入,这可能会改变我们在 Java 中编写并发应用程序的方式!

这只是对不同编程语言中可用内容的先睹为快。其他编程语言尝试处理并发性的方式要有趣得多。

此外,值得注意的是,在设计高并发应用程序时,上一节中讨论的设计模式的组合以及对类似绿色线程的抽象的编程语言支持可能非常强大。

6.高并发应用

现实世界的应用程序通常有多个组件通过网络相互交互。我们通常通过互联网访问它,它由多种服务组成,如代理服务、网关、Web 服务、数据库、目录服务和文件系统。

这种情况下如何保证高并发?让我们探索其中的一些层以及构建高度并发应用程序的选项。

正如我们在上一节中看到的,构建高并发应用程序的关键是使用那里讨论的一些设计概念。我们需要为工作选择合适的软件——那些已经包含了其中一些实践的软件。

(1)网络层

Web 通常是用户请求到达的第一层,这里不可避免地需要提供高并发性。让我们看看有哪些选项:

Node(也称为 NodeJS 或 Node.js)是一个基于 Chrome 的 V8 JavaScript 引擎构建的开源、跨平台 JavaScript 运行时。Node 在处理异步 I/O 操作方面工作得很好。Node 做得这么好的原因是因为它在单个线程上实现了一个事件循环。事件循环在回调的帮助下异步处理所有阻塞操作,如 I/O。

nginx是一个开源 Web 服务器,我们通常将其用作反向代理以及其他用途。nginx 提供高并发的原因是它使用异步的、事件驱动的方法。nginx 在单个线程中与主进程一起运行。主进程维护执行实际处理的工作进程。因此,工作进程并发地处理每个请求。

(2)应用层

在设计应用程序时,有几个工具可以帮助我们构建高并发。让我们来看看我们可以使用的一些库和框架:

Akka是一个用 Scala 编写的工具包,用于在 JVM 上构建高并发和分布式应用程序。Akka 处理并发的方法基于我们之前讨论过的 actor 模型。Akka 在参与者和底层系统之间创建了一个层。该框架处理创建和调度线程、接收和分派消息的复杂性。

Project Reactor 是一个反应库,用于在 JVM 上构建非阻塞应用程序。它基于 Reactive Streams 规范,专注于高效的消息传递和需求管理(背压)。Reactor 操作符和调度器可以维持消息的高吞吐率。几个流行的框架提供了 reactor 实现,包括 Spring WebFlux 和 RSocket。

Netty是一个异步的、事件驱动的网络应用程序框架。我们可以使用 Netty 来开发高并发协议服务器和客户端。Netty 利用NIO,它是 Java API 的集合,通过缓冲区和通道提供异步数据传输。它为我们提供了几个优势,例如更好的吞吐量、更低的延迟、更少的资源消耗以及最小化不必要的内存复制。

(3)数据层

最后,没有数据的应用程序是不完整的,数据来自持久存储。当我们讨论与数据库相关的高并发时,大部分焦点仍然放在 NoSQL 系列上。这主要是由于 NoSQL 数据库可以提供线性可扩展性,但在关系变体中很难实现。让我们来看看数据层的两个流行工具:

Cassandra是一种免费的开源 NoSQL 分布式数据库,可在商品硬件上提供高可用性、高可扩展性和容错性。但是,Cassandra 不提供跨多个表的 ACID 事务。因此,如果我们的应用程序不需要强一致性和事务,我们可以从 Cassandra 的低延迟操作中受益。

Kafka是一个分布式流媒体平台。Kafka 将记录流存储在称为主题的类别中。它可以为记录的生产者和消费者提供线性水平可扩展性,同时提供高可靠性和持久性。分区、副本和代理是它提供大规模分布式并发的一些基本概念。

(4)缓存层

好吧,现代世界中没有任何以高并发为目标的 Web 应用程序能够承受每次访问数据库的代价。这让我们选择一个缓存——最好是一个可以支持我们高并发应用程序的内存缓存:

Hazelcast 是一个分布式,云友好的,内存中的对象存储和计算引擎,支持多种数据结构,如地图,设置,列表,多重映射, RingBuffer和HyperLogLog。它具有内置复制并提供高可用性和自动分区。

Redis 是一种内存数据结构存储,我们主要用作缓存。它提供了一个具有可选持久性的内存键值数据库。支持的数据结构包括字符串、散列、列表和集合。Redis 具有内置复制并提供高可用性和自动分区。如果我们不需要持久性,Redis可以为我们提供一个功能丰富、网络化、性能卓越的内存缓存。

当然,在我们追求构建高度并发的应用程序的过程中,我们几乎没有触及可用内容的皮毛。重要的是要注意,除了可用的软件之外,我们的需求还应该指导我们创建合适的设计。其中一些选项可能合适,而其他选项可能不合适。

而且,我们不要忘记,还有更多可能更适合我们要求的选项。

以上就是赢咖4小编介绍的"高并发应用的设计原则和模式",希望对大家有帮助,想了解更多可查看Java赢咖4在线学习。赢咖4在线学习教程,针对没有任何Java基础的读者学习,让你从入门到精通,主要介绍了一些Java基础的核心知识,让同学们更好更方便的学习和了解Java编程,感兴趣的同学可以关注一下。

提交申请后,顾问老师会电话与您沟通安排学习

免费课程推荐 >>
技术文档推荐 >>