第 5 章 基础构建模块

本文深入探讨了并发编程的核心策略,包括使用委托、同步容器、并发容器及阻塞队列等技术提升多线程应用的效率与安全性。文章详细解析了线程安全类的设计原则,同步容器的局限性与复合操作的挑战,以及并发容器如ConcurrentHashMap和CopyOnWriteArrayList如何改善并发性能。此外,还介绍了阻塞队列在生产者-消费者模式中的应用,以及同步工具类如闭锁、FutureTask和信号量在协调线程控制流中的作用。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

             @@@  委托是创建线程安全类的一个最有效的策略:只需让现有的线程安全类管理所有的

             状态即可。

             @@@   Java 平台类库包含了丰富的并发基础构建模块,例如线程安全的容器类以及各种用于

             协调多个相互协作的线程控制流的同步工具类。

》》同步容器类

             @@@  同步容器类包括 Vector 和 Hashtable  等。这些同步的封装类是由

              Collections.synchronizedXxx  等工厂方法创建的。这些类实现线程安全的方式是:将它们的

              状态封装起来,并对每个公有方法都进行同步,使得每次只有一个线程能访问容器的状态。 

       ###  同步容器类的问题

             @@@  同步容器类是线程安全的,但在某些情况下可能需要额外的客户端加锁来保护复合操作。

             @@@  容器上常见的复合操作迭代(反复访问元素,直到遍历完容器中的所有元素)、

                                                            跳转(根据指定顺序找到当前元素的下一个元素)、

                                                            条件运算

             @@@  由于同步容器类要遵守同步策略,即支持客户端加锁,因此可能会创建一些新的操作,

              只要我们知道应该使用哪一个锁,那么这些新操作就与容器的其他操作一样都是原子操作。

             @@@ 示例:在使用客户端加锁的 Vector 上的复合操作

              ---------------------------------------------------------------------------------------------------------------------------------

                        public  static  Object  getLast( Vector  list ){

                                    synchronized ( list ) {

                                           int  lastIndex =  list.size( ) -1 ;

                                           return  list.get( lastIndex ) ;

                                    }

                         }

                        public  static  void  deleteLast( Vector  list ){

                                  synchronized ( list ){

                                         int  lastIndex  =  list.size( ) -1 ;

                                         return  list.remove( lastIndex ) ;

                                  }

                        }

              -------------------------------------------------------------------------------------------------------------------------------

             补充:(1)、在上面的代码的基础上,对  Vector  中的元素进行迭代时可能会抛出

                        ArrayIndexOutOfBoundsException  的异常。

              @@@ 我们可以通过在客户端加锁来解决不可靠迭代的问题,但要牺牲一些伸缩性。通过在

              迭代期间持有 Vector 的锁,可以防止其他线程在迭代期间修改 Vector 。然而,这同样会导致

              其他线程在迭代期间无法访问它,因此降低了并发性。

              ----------------------------------------------------------------------------------------------------------------------------------

                       示例:在客户端加锁的迭代

                       synchronized  ( vector ){

                                  for ( int  i = 0 ; i < vector.size( ) ; i++){

                                       doSomething( vector.get(i) ) ;

                                  }

                       }  

             -----------------------------------------------------------------------------------------------------------------------------------

       ###   迭代器与 ConcurrentModificationException

             @@@ 无论是直接迭代还是在 Java 5.0 引入的 for-each 循环语法中,对容器类进行迭代的标准

             方式都是  Iterator 。

                       在设计同步容器类的迭代器时并没有考虑到并发修改的问题,并且它们表现出的行为是

              “ 及时失败 ”的。这意味着,当它们发现容器在迭代过程中被修改时,就会抛出一个

                ConcurrentModificationException  异常。

              @@@  有时候开发人员并不希望在迭代期间对容器加锁。持有锁的时间越长,那么在锁上竞争

             就可能越激烈,如果许多线程都在等待锁被释放,那么将极大地降低吞吐量和 CPU 的利用率。

              @@@  如果不希望在迭代期间对容器加锁,那么一种替代方法就是 “ 克隆 ” 容器,并在副本上

              进行迭代。由于副本被封闭在线程内,因此其他线程不会在迭代期间对其进行修改,这样就避免

              了抛出 ConcurrentModificationException (在克隆过程中仍然需要对容器加锁)。

                         在克隆容器时存在显著的性能开销。这种方式的好坏取决于多个因素,包括容器的大小,

               在每个元素上执行的工作,迭代操作相对于其他操作的调用频率,以及在响应时间和吞吐量等

               方面的需求。

       ###  隐藏迭代器

              @@@   虽然加锁可以防止迭代器抛出 ConcurrentModificationException ,但你必须要记住所有

              对共享容器进行迭代的地方都需要加锁。实际情况要更加复杂,因为在某些情况下,迭代器会隐藏

              起来。

              @@@  标准容器的  toString  方法将迭代容器,并在每个元素上调用 toString 来生成容器内容的

              格式化表示

              @@@  如果状态与保护它的同步代码之间相隔越远,那么开发人员就越容易忘记在访问状态时使用

               正确的同步

              @@@  正如封装对象的状态有助于维持不变性条件一样,封装对象的同步机制同样有助于确保实施

               同步策略。

              @@@  容器的  hashCode  和 equals 等方法也会间接地执行迭代操作,当容器作为另一个容器的

               元素或键值时,就会出现这样的情况。同样, containsAll 、 removeAll 和 retainAll 等方法,以及

               把容器作为参数的构造函数,都会对容器进行迭代。所有这些间接的迭代器都可能抛出

               ConcurrentModificationException 

》》并发容器

              @@@ 同步容器将所有对容器状态的访问都串行化,以实现它们的线程安全性。这种方法的代价是

              严重降低并发性,当多个线程竞争容器的锁时,吞吐量就严重降低。

              @@@  并发容器是针对多个线程并发访问设计的

              -------- 在 Java  5.0 中增加了  ConcurrentHashMap ,用来替代同步且基于散列的 Map

                      CopyOnWriteArrayList  , 用于在遍历操作为主要操作的情况下代替同步的 List

                      在新的 ConcurrentMap  接口中增加了对一些常见复合操作的支持,例如 “ 若没有则添加 ” 、

                       替换以及有条件删除等。

               @@@   通过并发容器来代替同步容器,可以极大地提高伸缩性并降低风险

               @@@    Java 5.0 增加了两种新的容器类型:

               --------  Queue :   用来临时保存一组等待处理的元素

               --------  BlockingQueue : 扩展了  Queue , 增加了可阻塞的插入和获取等操作。(有阻塞队列)

                        补充:

                        在 “ 生产者---消费者 ” 设计模式中,阻塞队列是非常有用的

               @@@   Java 6.0 也引入了 ConcurrentSkipListMap  和  ConcurrentSkipListSet , 分别作为同步

               的 SortedMap 和  SortedSet 的并发替代品(例如用 synchronizedMap  包装的 TreeMap  或

               TreeSet )

       ###   ConcurrentHashMap

               @@@  同步容器类在执行每个操作期间都持有一个锁。

               @@@  ConcurrentHashMap  并不是将每个方法都在同一个锁上同步并使得每次只能有一个线程

                访问容器,而是使用一种粒度更细的加锁机制来实现更大程度的共享,这种机制称为分段锁(

                Lock  Striping)

                @@@   ConcurrentHashMap  带来的结果是,在并发访问环境下将实现更高的吞吐量,而在单线程

               环境中只损失非常小的性能。

                @@@  ConcurrentHashMap 与其他并发容器一起增强了同步容器类:它们提供的迭代器不会抛出

               ConcurrentModificationException  ,因此不需要在迭代过程中对容器加锁

                @@@  ConcurrentHashMap  中没有实现对 Map 加锁以提供独占访问

       ###   额外的原子 Map 操作

                @@@ 一些常见的复合操作,例如 “ 若没有则添加 ” 、 “ 若相等则移除 ” 、 “ 若相等则替换 ” 等,

                都已经实现为原子操作并且在 ConcurrentMap   的接口中声明

       ###   CopyOnWriterArrayList

                @@@ CopyOnWriterArrayList 用于替代 List , 在某些情况下它提供了更好的并发性能,并且在迭

               代期间不需要对容器进行加锁或复制。(类似地,CopyOnWriterArraySet 的作用是替代同步 Set )

                @@@  " 写入时复制(Copy-On-Write)"容器的迭代器保留一个指向底层基础数组的引用,这个

                数组当前位于迭代器的起始位置,由于它不会被修改,因此在对其进行同步时只需要确保数组内容

                的可见性。

                         每当修改容器时都会复制底层数组, 这需要一定的开销。

                @@@   仅当迭代操作远远多于其他操作时,才应该使用 “ 写入时复制 ” 容器

》》阻塞队列和生产者---消费者模式

                @@@ 阻塞队列提供了可阻塞的 put 和 take 方法,以及支持定时的 offer 和 poll 方法

                @@@ 阻塞队列支持生产者--消费者这种设计模式。

                ----------   生产者----消费者模式能简化开发过程,因为它消除了生产者类和消费者类之间

                         的代码依赖性,此外,该模式还将生产数据的过程和使用数据的过程解耦开来以简化

                         工作负载的管理,因为这两个过程在处理数据的速率上有所不同。

                @@@  BlockingQueue  简化了生产者--消费者设计的实现过程,它支持任意数量的生产者和

                 消费者。一种最常见的生产者---消费者设计模式就是线程池工作队列的组合,在 Executor

                 任务执行框架中就体现了这种模式

                @@@  阻塞队列简化了消费者程序的编码,因为 take 操作会一直阻塞直到有可用的数据。

                @@@  在一些情况下,需要调整生产者线程数量消费者线程数量之间的比率,从而实现

                更高的资源利用率(例如,在“ 网页爬虫【Web Crawler】”    或在其他应用程序中,有无穷的

                工作需要完成)。

                @@@  阻塞队列中的 put 方法的阻塞特性极大地简化了生产者的编码。

                @@@  阻塞队列提供了一个 offer  方法,如果数据项不能被添加到队列中,那么将返回一个

                 失败状态。这样你就能够创建更多灵活的策略来处理负荷过载的情况,例如减轻负载 , 将多

                 余的工作项序列化写入磁盘,减少生产者线程的数量,或者通过某种方式来抑制生产者线程。

                @@@   在构建高可靠的应用程序时,有界队列是一种强大的资源管理工具:它们能抑制并

                防止产生过多的工作项,使应用程序在负荷过载的情况下变得更加健壮

                @@@  应该尽早地通过阻塞队列在设计中构建资源管理机制--------这将事情做的越早,就越容易。

                在许多情况下,阻塞队列能使这项工作更加简单,如果阻塞队列并不完全符合设计需求,那么还

                可以通过信号量(Semaphore)来创建其他的阻塞数据结构。

                @@@ PriorityBlockingQueue 是一个按优先级排序的队列,当你希望按照某种顺序而不是 FIFO

                来处理元素时,这个队列是非常有用的。正如其他有序的容器一样,PriorityBlockingQueue  既

                可以根据元素的自然顺序来比较元素(如果它们实现了 Comparable 方法),也可以使用

                Comparator 来比较。

       ###   示例:桌面搜索

                @@@  生产者----消费者模式能带来许多性能优势。生产者和消费者可以并发执行。如果一个是

                I / O 密集型, 另一个是 CPU密集型,那么并发执行的吞吐率要高于串行执行的吞吐率。

       ###  串行线程封闭

                @@@  在 java.util.concurrent  中实现的各种阻塞队列都包含了足够的内部同步机制,从而安全地

                将对象从生产者线程发布到消费者线程。

                @@@   对于可变对象生产者--消费者这种设计与阻塞队列一起,促进了串行线程封闭,从而将

                对象所有权从生产者交付给消费者

                           线程封闭对象只能由单个线程拥有,但可以通过安全地发布该对象来 “ 转移 ” 所有权。在

                转移所有权之后,也只有另一个线程能获得这个对象的访问权限,并且发布对象的线程不会再访问

                它。这种安全的发布确保了对象状态对于新的所有者来说是可见的,并且由于最初的所有者不会再

                 访问它,因此对象将被封闭在新的线程中。新的所有者线程可以对该对象做任意修改,因此它具有

                 独占的访问权。

                           对象池利用了串行线程封闭,将对象 “ 借给 ”一个请求线程。

       ###  双端队列与工作密取

                @@@   Java 6 增加了两种容器类型:Deque(双端队列)-----------------------> 扩展了 Queue

                                                                       BlockingDeque------------------>扩展了 BlockingQueue

                具体的实现包括: ArrayDeque  和  LinkedBlockingDeque

                @@@   阻塞队列适用于生产者---消费者模式

                            --------  在生产者--消费者模式中,所有消费者有一个共享的工作队列

                             双端队列适用于工作密取(Work  Stealing)

                            --------  在工作密取中,每个消费者都有各自的双端队列。

                            --------   如果一个消费者完成了自己双端队列中的全部工作,那么它可以从其他消费者

                                    双端队列末尾秘密地获取工作。

                            --------  当工作者线程需要访问另一个队列时,它会从队列的尾部而不是从队列的头部

                                   获取工作,因此进一步降低了队列上的竞争程度。

               @@@    工作密取非常适用于既是消费者也是生产者问题-------当执行某个工作时可能导致出现

               更多的工作。

》》阻塞方法和中断方法

               @@@     线程可能会阻塞或暂停执行,原因有多种:

                              -------  等待 I / O 操作结束

                              ------- 等待获得一个锁

                             -------- 等待从  Thread.sleep 方法中醒来

                             -------- 等待另一个线程的计算结果

              @@@    当线程阻塞时,它通常被挂起,并处于某种阻塞状态(BLOCKED 、WAITING 或

               TIMED_WAITING )。

                           当某个外部事件发生时,线程被置回 RUNNABLE  状态,并可以再次被调度执行。

             @@@   BlockingQueue 的 put 和 take 等方法会抛出受检查异常(Checked  Exception)

              InterruptedException 。 当某个方法抛出  InterruptedException  时,表示该方法是一个阻塞

              方法,如果这个方法被中断,那么它将努力提前结束阻塞状态。

             @@@   Thread  提供了  interrupt  方法,用于中断线程或查询线程是否已经被中断。每个线程

             都有一个布尔类型的属性,表示线程的中断状态,当中断线程时将设置这个状态。

             @@@   中断是一种协作机制。

                         --------   最常用中断的方式就是取消这个操作。

                         --------    方法对中断请求的响应度越高,就越容易及时取消那些执行时间很长的操作

             @@@    当在代码中调用了一个将抛出 InterruptedException  异常的方法时,你自己的方法也会

             变成一个阻塞方法,并且必须要处理对中断的响应。对于库代码来说,有两种基本选择:

              ------ 传递 InterruptedException 

                          避开这个异常通常是最明智的策略------只需把 InterruptedException  传递给方法的调用者。

              传递 InterruptedException  的方法包括,根本不捕获该异常,或者捕获该异常,然后在执行某种简单

              的清理工作后再次抛出这个异常。

              ------ 恢复中断

                          有时候不能抛出 InterruptedException  ,例如当代码是 Runable  的一部分时。在这些情况下,

              必须捕获  InterruptedException  , 并通过调用当前线程上的 interrupt 方法恢复中断状态,这样在调用

              栈中更高层的代码将看到引发了一个中断。

              补充:

                          只有在一种特殊的情况中才能屏蔽中断,即对 Thread 进行扩展,并且能控制调用栈上所有更高

              层的代码

》》同步工具类

              @@@  同步工具类可以是任何一个对象,只要它根据自身的状态来协调线程的控制流。阻塞队列可以作为

              同步类工具,其他类型的同步工具类还包括信号量(Semaphore)栅栏(Barrier)以及闭锁(Latch)

              @@@   所有的同步工具类都包含一个特定的结构化属性:它们封装了一些状态,这些状态将决定执行

              同步工具类的线程是继续执行还是等待,此外还提供了一些方法对状态进行操作,以及另一些方法用于

              高效地等待同步工具类进入到预期状态。

       ###  闭锁

              @@@   闭锁是一种同步工具,可以延迟线程的进度直到其到达终止状态。

              @@@   闭锁的作用相当于一扇门:在闭锁到达结束状态之前,这扇门是关闭的,并且没有任何线程能通过,

              当到达结束状态时,这扇门会打开并允许所有的线程通过。当闭锁到达结束状态后,将不会再改变状态,因

              此这扇门将永远保持打开状态。闭锁可以用来确保某些活动直到其他活动都完成后才继续执行。

              @@@   CountDownLatch 是一种灵活的闭锁实现,可以使一个或多个线程等待一组事件发生。

               ----------- 闭锁状态包括一个计数器,该计数器被初始化为一个正数,表示需要等待的事件数量

               ----------- countDown  方法递减计数器,表示有一个事件已经发生了,而 await 方法等待计数器达到零,这

                           表示所有需要等待的事件都已经发生。

               ----------- 如果计数器的值非零,那么 await 会一直阻塞直到计数器为零,或者等待中的线程中断,或者等待

                           超时。

       ###   FutureTask

              @@@   FutureTask  也可以用做闭锁。

              @@@   FutureTask  表示的计算是通过 Callable  来实现的,相当于一种可生成结果的  Runable ,并且可以

               处于以下 3 种状态:等待运行(Waiting  to run )

                                            正在运行(Running)

                                            运行完成(Completed)

              @@@    " 执行完成 " 表示计算的所有可能结束方式,包括正常结束由于取消而结束由于异常而结束等。

                         当 FutureTask 进入完成状态后,它会永远停止在这个状态上。

              @@@    FutureTask.get 的行为取决于任务的状态。如果任务已经完成,那么 get 会立即返回结果,否则 get

                        将阻塞直到任务进入完成状态,然后返回结果或者抛出异常。

               @@@   FutureTask  将计算结果从执行计算的线程传递到获取这个结果的线程,而 FutureTask  的规范确保

                        了这种传递过程能实现结果的安全发布。

              @@@   FutureTask 在 Executor 框架中表示异步任务,此外还可以用来表示一些时间较长的计算,这些计算

                       可以再使用计算结果之前启动。

       ###   信号量

              @@@    计数信号量(Counting  Semaphore)用来控制同时访问某个特定资源的操作数量,或者同时执行

               某个指定操作的数量。计数信号量还可以用来实现某种资源池,或者对容器施加边界。

              @@@     Semaphore 中管理着一组虚拟的许可(permit,许可的初始化数量可通过构造函数来指定。

              在执行操作时可以首先获得许可(只要还有剩余的许可),并在使用以后释放许可

               @@@    计数信号量的一种简化形式是二值信号量,即初始值为 1 的 Semaphore  。二值信号量可以用做

              互斥体(mutex) ,并具备不可重入的加锁语义:谁拥有这个唯一的许可,谁就拥有了互斥锁。

                @@@    Semaphore  可以用于实现资源池,例如数据库连接池

              @@@    在构造阻塞对象池时,一种更简单的方法是使用  BlockingQueue  来保存池的资源。

              @@@    可以使用  Semaphore  将任何一种容器变成有界阻塞容器。

       ###   栅栏

              @@@    闭锁是一次性对象,一旦进入终止状态,就不能被重置。(而栅栏可以被重置,以便下次使用

              @@@     栅栏和闭锁的关键区别:

               -------------    所有线程必须同时到达栅栏位置,才能继续执行。

                                闭锁用于等待事件,而栅栏用于等待其他线程

               -------------     栅栏用于实现一些协议,例如几个家庭在某个地方集合:“ 所有人 6:00 在麦当劳碰头,

                              到了以后等其他人,之后再讨论下一步要做的事情 ”

              @@@   CyclicBarrier 可以使一定数量的参与方反复地在栅栏位置汇集,它在并行迭代算法中非常

               有用:这种算法通常将一个问题拆分成一系列相互独立的子问题。

              @@@   CyclicBarrier  可以使你将一个栅栏操作传递给构造函数,这是一个 Runnable ,当成功通过

               栅栏时会(在一个子线程任务中)执行它,但在阻塞线程被释放之前是不能执行的。

              @@@ 另一种形式的栅栏是  Exchanger ,它是一种两方(Two-Party)栅栏,各方在栅栏位置上交换

               数据。当两方执行不对称的操作时, Exchanger  会非常有用。

                        当两个线程对象通过  Exchanger  交换对象时, 这种交换就把这两个对象安全地发布给另一方。

              @@@  数据交换的时机取决于应用程序的响应需求。

》》构建高效且可伸缩的结果缓存

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

小达人Fighting

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值