多线程如何设计?一对多/多对一/多对多

二、14个多线程设计模式

参考原文:https://www.cnblogs.com/rainbowbridge/p/17443503.html

single Thread 模式

一座桥只能通过一个人

Single Thread模式是一种单线程设计模式,即在一个应用程序中只有一个主线程、一个事件循环,对外只提供唯一的入口点、唯一的出口点,所有操作都由这个线程来完成。该模式的核心思想是,通过限制应用程序中的线程数目来简单化设计、提高可维护性和可测试性,同时降低程序达到系统资源上限的风险。
Single Thread模式适用于一些要求简单且易于维护和测试的应用程序,例如游戏、事件处理系统等。在这些应用程序中,存在着大量的异步操作,对于线程安全的控制不当会带来大量的开销。Single Thread模式也可以被认为是一种面向事件的编程范式。
Single Thread模式的一个典型实现是Node.js,其中所有的请求都由事件循环处理,所有I/O操作都非阻塞形式完成,所有的应用程序只有一个主线程。在单线程应用程序中,同步调用卡住线程并且阻碍了整个应用程序的执行,而异步调用在指定的时候执行回调函数,释放这些函数的线程,从而接受更多的请求。
需要注意的是,在使用Single Thread模式时,需要注意避免单点故障的发生。如果一个线程出现崩溃,整个应用程序就无法正常工作。此外,由于所有操作都在同一个线程中执行,如果一个操作执行时间较长,就会阻塞其他操作,影响应用程序的性能。

Immutable(不可变的)模式

想破坏也破坏不了
Immutable(不可变的)模式是一种设计模式,它鼓励创建不可变的对象,即一旦对象被创建,则不能再修改其状态。在Immutable模式中,所有的属性都是final或者只读的,并且没有任何修改属性的方法。因此,创建的对象是线程安全的,可以共享并且不会出现竞态条件。
该模式的核心思想是,通过不可变的对象来避免状态的修改,从而提高应用程序的可靠性。Immutable模式适用于那些要求高可靠性的应用程序,如多线程环境、分布式系统、函数式编程等。
在使用Immutable模式时,需要注意以下几点:

不可变对象应该是线程安全的,即多个线程可以同时访问对象,不会出现竞态条件。
不可变对象应该是可序列化的,即可以将对象转换为字节流,存储在磁盘或者通过网络传输。
不可变对象应该提供有效的equals和hashCode方法,以便于比较两个对象是否相等。

需要注意的是,使用Immutable模式会增加一些代码的复杂度,因为需要为每个属性编写getter方法。另外,如果有需要改变对象的状态,就需要创建一个新的对象,而不是在原有的对象上进行修改。虽然这会增加内存的开销,但是会减少线程同步的开销,提高应用程序的性能和可靠性。

guarded suspension,执行前对共享资源有条件的执行

等我准备好哦
Guarded Suspension是一种并发模式,也称为Polling模式,它用于解决多线程环境下的资源竞争问题。在这种模式中,线程必须先获取某个条件下的锁(Guarded Object,又称监控对象)后,才能执行进一步操作。
该模式的核心思想是,当线程需要访问/修改一个共享变量时,首先需要检查一个特定的条件(Guarded Condition)是否成立,如果条件不成立,则线程必须等待,直到条件成立后才能继续执行。这种等待状态称为Guarded Suspension。Guarded Object通常是一个共享变量,线程在访问Guarded Object时需要获得对它的锁,从而避免多个线程同时对同一个变量进行操作。
Guarded Suspension模式通常用于生产者-消费者的场景中,其中多个线程同时读写共享的缓冲区。当缓冲区满时,生产者需要等待一个“有空位置”的条件;而当缓冲区为空时,消费者需要等待一个“有数据”的条件,以避免数据的读写冲突。
Guarded Suspension模式需要注意线程安全和死锁的问题,因此设计合理的条件(Guarded Condition)和监控对象(Guarded Object)是非常重要的。

Balking 模式,执行时检查条件,是否撤销

不需要就算了
Balking模式是一种并发模式,用于解决在并发编程中的某些情况下,为避免冲突而导致线程阻塞和等待的问题。在这种模式下,当线程发现正在进行的操作已经失去了意义或者已经完成时,它将撤回或跳过当前操作,从而避免不必要的等待和阻塞。
该模式的核心思想是,在进行某项操作之前,线程需要先检查某些条件是否成立,如果条件不成立,那么线程将放弃执行该操作。当线程发现正在进行的操作已经没有意义或者已经完成时,它将停止执行,从而避免不必要的等待和阻塞。
Balking模式通常用于一些无法立即获得结果的操作,比如文件写入、网路传输等等。如果操作执行成功,那么线程将继续执行下一步操作;如果操作失败,那么线程将放弃当前操作,直接返回;如果操作正在进行中,那么线程将立即停止,在后续时间重新检查。
Balking模式需要注意线程安全的问题,同时也需要注意不要出现不必要的放弃和跳过操作,否则可能会导致一些操作未执行,从而造成数据不一致等问题。

produer-consumer 模式

生产者消费者模式
生产者-消费者模式是一种经典的并发模式,用于解决在多线程环境下生产者和消费者之间的数据交换问题。在该模式中,一个或多个生产者将数据存储到共享的缓冲区中,一个或多个消费者从中取走数据进行处理,缓冲区起到生产者与消费者之间的桥梁作用。
该模式的核心思想是,生产者和消费者在处理数据时需要共享同一个缓冲区。当缓冲区已满时,生产者需要等待直到有足够的空间将数据放入缓冲区。同理,当缓冲区为空时,消费者需要等待直到有数据可供取出。基于这种协作方式,生产者和消费者能够安全、高效地进行数据交换。
针对生产者-消费者模式,有许多不同的实现方式。其中一种常见的方式是通过线程间的等待和唤醒机制实现数据交换和同步。具体而言,可以使用wait(),notify()和notifyAll()等线程方法,在生产者和消费者之间控制缓冲区的读写动作,从而实现数据交换和同步。可以使用队列、栈等数据结构来作为缓冲区,也可以手动实现一个线程安全的缓冲区。
需要注意的是,生产者-消费者模式需要解决并发环境下的同步和互斥问题,因此需要采取一些措施来保证多个线程之间的数据交换和共享缓冲区的安全性。另外,为了避免“饥饿”和“死锁”等问题,也需要合理地分配和调度线程资源,保障生产者和消费者的运行效率。

read-writer 模式

大家一起读没有问题,但是读的时候不要些哦
读写器模式是一种并发模式,用于管理读写锁,目的是提高多线程环境下读写操作的并发性。在读写器模式中,读操作不会互斥,因此多个线程可以同时进行读操作,而写操作需要互斥,因此只能有一个线程进行写操作。
该模式的核心思想是,在读操作与写操作之间加入读写锁,保证写操作互斥,而读操作则最大化并发性。读写锁的关键点是,在读操作时允许多个线程同时获取读锁,只在写操作时不允许其他读写锁访问缓存数据。
读写器模式适用于读操作多,写操作少的应用场景,例如高性能缓存系统、搜索引擎系统等。
需要注意的是,在使用读写器模式时,必须正确地处理读写锁的互斥性,否则可能会导致死锁、竞争条件、饥饿等问题。此外,在涉及到数据共享时,必须考虑线程安全的问题,使用适当的线程同步机制。

thread-per-message 模式

这项工作就交给你了
Thread-per-message模式,又称为Actor模式,是一种并发编程模式,用于管理多线程环境中的消息和事件处理。在该模式中,每个消息或事件处理都启动一个新的线程(或轻量级进程、协程等),这些线程相互独立,通过消息传递进行通信。
该模式的核心思想是,每个消息或事件都有自己的消息处理线程,消息处理线程独立运行,互不影响。当消息/事件发生时,应用程序向线程池中提交一个任务,线程池中有一个或多个工作线程,它们将承担消息处理的工作。消息队列管理器负责将消息分发到线程池中。
Thread-per-message模式适用于事件驱动的应用程序,其中每个事件处理程序都需要独立运行,并且这些处理程序的执行时间不可预测,或者处理程序之间相互独立,没有直接依赖关系。该模式在分布式系统和网络编程中广泛应用,如分布式计算、Web应用程序、游戏开发等。
需要注意的是,在使用Thread-per-message模式时,必须谨慎处理线程安全,以避免数据竞争和锁竞争等问题。此外,线程池的最大线程数必须根据处理程序的性质进行合理的调整,以避免服务器资源的浪费和过度消耗。

worker Thread 模式

工作没有来就一直等,工作来了就干活
Worker Thread模式是一种并发模式,用于将任务分配给多个工作线程进行处理。在该模式中,一个主线程将任务队列中的任务分配给多个工作线程,每个工作线程独立执行任务,并将结果返回给主线程。
该模式的核心思想是,通过将大量的计算或处理工作分配给多个工作线程进行处理,从而减少主线程的开销,提高应用程序的性能。Worker Thread模式适用于那些需要处理大量繁重工作的应用程序,如图像处理、数据处理等。
需要注意的是,Worker Thread模式必须正确地管理多个线程之间的同步问题。为了避免竞态条件和死锁,必须使用适当的同步机制来管理任务队列和结果队列。此外,在设计Worker Thread模式时,还应该考虑线程调度的问题,因为线程调度的机制对于整个应用程序的性能和可靠性具有重要影响。
一种常见的实现方式是使用线程池和任务队列,主线程将任务提交到任务队列中,线程池中的工作线程从任务队列中取出任务并执行。在处理完任务之后,结果会传递给主线程。线程池中的线程数应该根据应用程序的需求进行调整,以达到最佳的性能和可靠性。

Future 模式

先给你提货单
Future模式是一种基于异步编程的设计模式,将异步的操作转化为同步的操作,允许在等待异步操作完成时执行其他操作。在该模式中,一个异步操作的结果被封装在一个Future对象中,可以在后续的代码中被访问和使用。
该模式的核心思想是,通过对异步操作的结果进行封装并提供Future对象,使得调用方可以在不阻塞主线程的情况下等待异步操作完成,并在结果可用时立即执行后续操作。Future模式适用于那些包含大量耗时操作或网络请求的应用程序,如Web应用程序、游戏开发等。
需要注意的是,在使用Future模式时,必须正确地管理异步操作和Future对象之间的关系。因为Future对象可能会被多个线程共享,并且可能会在异步操作完成前被访问,所以必须使用适当的同步机制来保证线程安全。
在Java中,Future模式由java.util.concurrent包提供支持。它提供了两个接口:Future和CompletionStage。Future接口表示一个异步计算的结果,可以被访问和取消。而CompletionStage接口则是一个可完成的异步操作,允许以串行和并行的方式组合多个异步操作。
需要注意的是,虽然Future模式提供了一种方便的处理异步操作的方式,但是它并不能解决所有的异步编程问题。在实际应用中,异步操作的错误处理、异常传递等问题也需要考虑到。

Two-phase-termination 模式

先收拾房间再睡觉
Two-phase-termination模式是一种设计模式,用于实现一种规范的线程(或进程)的终止机制。在该模式中,终止操作分为两个阶段:第一个阶段通知线程需要终止,并等待线程完成当前任务,第二个阶段等待线程真正的终止,包括资源的释放、日志的记录、异常的处理等操作。
该模式的核心思想是,通过将线程的终止过程分为两个阶段来保证线程能够安全地退出并执行完成必要的清理工作。Two-phase-termination模式适用于那些要求可靠的线程终止机制的应用程序,如服务器、桌面应用程序、嵌入式系统等。
需要注意的是,在使用Two-phase-termination模式时,需要正确地设计和使用终止协议,并避免出现死锁、竞态等问题。另外,当线程退出的原因是由于处理异常或错误时,需要处理与异常相关的问题,以防止异常传播和资源泄漏。
在Java中,Two-phase-termination模式可以通过Java的Executor框架和ShutdownHook机制来实现。Java中的Executor框架提供了一个线程池和一组API来执行异步任务。通过使用Executor框架的线程池,可以更容易地管理线程的生命周期并实现二阶段终止。而ShutdownHook机制则允许线程在Java虚拟机关闭时执行一些清理工作,如关闭文件、释放网络连接等。

thread specific storage模式

一个线程一个储物柜
Thread Specific Storage(线程专属存储)模式是一种设计模式,用于在多线程环境下处理线程特定的数据。在该模式中,每个线程都有自己的数据存储区域,并且可以安全且独立地访问和修改数据。这个存储区域通常被称为线程本地存储(Thread Local Storage,TLS)。
该模式的核心思想是,通过在每个线程中保留线程特定的数据,避免在多线程环境中出现竞争和锁定等问题,提高并发性能。Thread Specific Storage模式适用于那些需要在多线程环境中处理线程特定数据的应用程序,如用户会话管理、全局配置等。
需要注意的是,在使用Thread Specific Storage模式时,需要注意内存管理和线程调度等问题。线程本地存储的对象如果过多或过大,会占用大量内存,影响应用程序的性能和可靠性。另外,在使用线程本地存储时,需要注意线程调度的问题,谨防出现死锁,避免出现线程显式终止时无法释放存储资源的情况。
在Java中,Thread Specific Storage模式可以通过ThreadLocal类来实现。ThreadLocal类为每个线程提供了一个独立的变量副本,线程之间互不干扰。通过ThreadLocal的get()和set()方法,可以获取和设置线程本地存储中的值。同时,Java还提供了InheritableThreadLocal类,允许子线程访问父线程的线程本地存储数据。

active Object 模式

接受异步消息的主动对象
Active Object模式是一种并发模式,用于将对象的方法调用异步化、可扩展化并且可并发执行。在该模式中,被调用的对象是一个活动对象(Active Object),并且它的方法调用被封装成一个消息或请求,然后放置到一个特殊的队列中等待执行。
该模式的核心思想是,将对象的方法调用分离为异步调用和同步调用两个阶段,以提高应用程序的性能和可扩展性。Active Object模式适用于那些需要处理繁重或阻塞操作的应用程序,如网络应用程序、多媒体应用程序等。
需要注意的是,在使用Active Object模式时,需要正确地处理并发访问和资源共享的问题。由于消息队列可能被多个线程同时访问,因此需要使用同步机制来保证线程安全。另外,在设计Active Object模式时,还需要考虑如何处理异常和错误,以及如何管理和调度活动对象的生命周期。
在Java中,Active Object模式可以通过Java并发包中的一些类来实现,如ExecutorService、Future和CompletionService。通过这些类,可以将任务分离为异步操作和同步操作两个阶段,并且可以非常方便地管理和调度活动对象的生命周期。

多线程与高并发

并发容器

ConcurrentHashMap:ConcurrentHashMap是一个线程安全的HashMap,它支持高并发的读和写,是Java中最常用的并发Map实现之一。
CopyOnWriteArrayList:CopyOnWriteArrayList是一个线程安全的ArrayList,它通过对数据进行复制来保证线程安全,在写操作时会对数据进行复制并进行写操作,读操作则不需要进行复制。
BlockingQueue:BlockingQueue是一个阻塞队列,它可以在队列为空或队列已满时,暂停生产者线程或消费者线程的执行,从而避免了数据竞争和资源浪费。
ConcurrentLinkedQueue:ConcurrentLinkedQueue是一个非阻塞的队列,它可以支持高并发的并发访问,并且性能优于BlockingQueue。
PriorityQueue:PriorityQueue是一种优先队列,它可以实现按照优先级排序的队列。它可以用于解决某些求最小值或最大值的算法问题。
ConcurrentSkipListMap和ConcurrentSkipListSet:ConcurrentSkipListMap和ConcurrentSkipListSet是一种高并发的跳表,它可以支持高效的并发读写操作。

CAS

CAS(Compare And Swap,比较并交换)是一种无锁(lock-free)算法,用于实现多线程环境下的原子操作。CAS操作包括三个操作数:内存位置、期望值和新值。如果内存位置的值与期望值相同,则将该位置的值设置为新值。否则,操作失败。
CAS的原理是通过比较内存位置的值和期望值是否相等来判断当前内存位置是否被其他线程修改,如果没有被修改,则用新值更新该位置的值,否则返回失败。CAS操作的效率比加锁操作高很多,因为CAS不会阻塞线程。
在Java中,CAS操作由java.util.concurrent.atomic包下的一些类实现,如AtomicBoolean、AtomicInteger、AtomicLong等。这些类可以保证原子性,并且通过CAS原理实现线程安全的操作,避免使用锁等机制。
需要注意的是,在使用CAS时,由于操作的过程是非阻塞的,如果存在数据竞争问题或线程数量过多等问题,则可能会导致CAS操作失败,从而需要重新尝试。因此,需要恰当地选择CAS的使用场景,并合理处理操作失败的情况。

线程池

  1. corePoolSize:核心线程数,指该线程池中保持活跃状态的线程数,除非设置了allowCoreThreadTimeOut的值为true,否则线程池中的线程数至少为corePoolSize。

  2. maximumPoolSize:最大线程数,指该线程池中允许的最大线程数,当活跃线程数达到maximumPoolSize时,如果任务队列已经满了,则会根据定义的拒绝策略对任务进行处理。

  3. keepAliveTime:线程空闲时间,当线程处于空闲状态超过keepAliveTime时,多余的线程会被销毁,只保留corePoolSize个线程。

  4. unit:线程空闲时间的单位,可以是毫秒、秒等。

  5. workQueue:任务队列,用于保存等待执行的任务,常用的队列有ArrayBlockingQueue、LinkedBlockingQueue、PriorityBlockingQueue、SynchronousQueue等。

  6. threadFactory:线程工厂,用于创建新线程,常用的线程工厂有DefaultThreadFactory、CustomThreadFactory等。

  7. handler:拒绝策略,当任务队列已满时,根据拒绝策略对任务进行处理,常用的拒绝策略有AbortPolicy、CallerRunsPolicy、DiscardPolicy、DiscardOldestPolicy等。

安全性 共享资源的
生存性 死锁、饥饿和活锁

  • 21
    点赞
  • 22
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
多线程中共用一个数据变量时,需要注意以下几点: 1. 线程安全性:确保对共享变量的访问是线程安全的,即多个线程同时访问该变量不会导致不确定的结果或数据损坏。可以使用锁机制(如互斥锁、读写锁)或其他同步机制(如信号量、条件变量)来保证线程安全。 2. 原子操作:如果多个线程需要对同一个变量进行修改操作,确保这些操作是原子的,即不会被其他线程打断。常见的原子操作有原子赋值、原子加减等,可以使用原子操作库或锁来实现。 3. 内存可见性:确保多个线程对共享变量的修改对其他线程可见。在多核处理器中,每个线程可能在不同的核心上执行,每个核心都有自己的缓存。为了保证内存可见性,可以使用volatile关键字或显式的内存屏障来禁止编译器和处理器对指令进行重排序或缓存优化。 4. 数据一致性:当多个线程对共享变量进行读写操作时,需要确保数据的一致性,避免出现脏读、写覆盖等问题。可以使用同步机制(如锁)来保证每次只有一个线程可以对共享变量进行修改。 5. 死锁:当多个线程同时获取多个资源,并且按照特定的顺序获取资源时,可能会发生死锁。为了避免死锁,需要合理地设计资源获取的顺序,并确保每个线程在获取资源时不会长时间占用,可以使用超时机制或者避免嵌套锁的使用。 总之,在多线程中共用一个数据变量时,需要综合考虑线程安全性、原子操作、内存可见性、数据一致性和死锁等方面的问题,以确保多线程程序的正确性和可靠性。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值