JAVA多线程并发编程——基础(下)

对象的组合


设计线程安全的类

包括一下三个要素:

1、找出构成对象状态的所有变量。

2、找出约束状态变量的不变性条件。

3、建立对象状态的并发访问策略。

要分析对象的状态,首先从对象的域开始。如果对象中所有的域都是基本类型状态的变量,那么这些域将构成对象的全部状态


收集同步需求

要确保类的线程安全行,就需要确保它的不变性条件不会在并发访问的情况下被破坏,这就需要对其状态进行判断。

如果不了解对象的不变性条件与后验条件,那么就不能确保线程安全性。要满足在状态变量的有效值或状态在转让上的各种约束条件,就需要借助原子性与封装性。

依赖状态的操作

类的不变性条件与后验条件约束了在对象上有那些状态和状态转换是有效的。在某些对象的方法中还包含一些基于状态的先验条件。

在单线程程序中,如果某个操作无法满足先验条件,那么就只能失败。但是在并发程序中,先验条件可能会由于其它线程执行的操作而变成真。在并发程序中要一直等到先验条件为真,然后再执行该操作。


实例封闭

如果某对象不是线程安全的,那么可以通过多种技术使其在多线程程序中安全地使用。你可以确保该对象只能由单个线程访问(线程封闭),或者通过一个锁来保护对该对象的所有访问。

封装简化了线程安全类的实现过程,他提供了一种实例封闭机制。通常也简称为“封闭”。通过将封闭机制与合适的枷锁策略结合起来,可以确保以线程安全的方式来是使用非线程安全的对象。

将数据封装在对象内部,可以将数据的访问权限在对象的方法上,从而更容易确保线程在访问数据时总能保持正确的锁。

封闭机制更易于构造线程安全的类,因为当封闭类的状态时,在分析类的线程安全性时就无须检查整个程序。

java监视器模式

java的监视器模式仅仅是一种编写代码的约定,对于任何一种锁对象,只要自始至终都使用该锁对象,都可以用来保护对象的状态。

客户端加锁机制

客户端加锁机制与扩展类机制有许多共同点,二者都是将派生类的行为与基类的实现耦合在一起,正如扩展会破坏实现的封装性,客户端加锁同样会破坏同步策略的封装性。



基础构建模块


同步容器类

同步容器类包括Vector和Hashtable,二者是早期JDK的一部分,此外还包括JDK1.2种添加的一些功能相似的类,这些同步的封装器类是由Collections.sysnchronizedXXX等工厂方法创建的。这些类实现线程安全的方式是:将它们的状态封装起来,并对每个公有方法都进行同步,使得每次只有一个线程能访问容器的状态。

同步容器类的问题

同步容器类都是线程安全的,但在某些情况下可能需要额外的客户端加锁老来保护复合操作。容器上常见的复合操作包括:迭代、跳转、以及条件运算。在同步容器类种,这些复合操作在没有客户端加锁的情况下仍然是线程安全的,但当其他线程并发地修改容器,它们可能会出线意外之料的行为。

因为同步容器类要遵从同步策略,即客户端加锁。对于迭代可能出现的问题,我们可以通过获得自身容器类的锁来确保操作的原子性。也可以使用客户端加锁来解决不可靠的迭代问题,但是需要牺牲一些伸缩性,然而这样会导致其他线程在迭代期间无法访问它,因此降低了并发性。

虽然加锁可以防止迭代器抛出异常,但你必须要记住所有对共享容器进行迭代的地方都需要加锁。实际情况要更复杂。


并发容器

java5.0提供了多种并发容器类来改进同步容器的性能。同步容器将所有对容器状态的访问都串行化,以实现它的线程安全行。这种方法的代价是严重降低并发性,当多个线程竞争容器的锁时,吞吐量会降低。通过并发容器来代替同步容器,可以极大地提高伸缩性并降低风险。


ConcurrentHashMAP

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

与HsahMap一样,concurrentHashMap也是一个基于散列的Map,但它使用了一种完全不同的加锁策略来提供更高的并发性和伸缩性。concurrentHashMap并不是将每个办法都在同一个锁上同步并使得每次只能有一个线程访问容器,而是使用一种粒度更细的加锁机制来实现更大程度的共享,这种机制称为分段锁。在这种机制中,任意数量的读取线程可以并发地访问Map,执行读取操作的线程和执行写入操作的线程可以并发地访问Map,并且一定数量的写入线程可以并发地修改Map,ConcurrentHashMap带来的结果是,在并发访问环境下将实现更高的吞吐量,而在单线程环境中只损失非常小性能。

ConcurrentHashMap与其他并发容器一起增加了同步容器类:他们提供的迭代器不会抛出异常,因此不需要在迭代过程中对容器加锁。


额外原子的Map操作

由于ConcurrentHashMap不能被加锁来执行独占访问,因此我们无法使用客户端加锁来创建新的原子操作。但是,一些常见的复合操作,例如”若没有则添加“ 、”若相等则移除“’、”若想等则退换“等,都已经实现为原子操作并且在ConcurrentMap的接口中声明。


串行线程封闭

对于可变对象,生产者—消费者这种设计与阻塞队列一起,促进了串行线程封闭,从而将对象所有权从生产者交付给消费者。线程封闭对象只能由单个线程拥有,但可以通过安全地发布该对象来”转移“所有权。在转移所有权后,也只有另一个线程能获得这个对象的访问权限,并且发布对象的线程不会访问它。这种安全的发布确保了对象将被封闭在新的线程中。新的所有者线程可以对该对象做任意修改,因为它具有独占的访问权。


双端队列与工作密取 

java6增加了两种容器类型,Deque(发音为"deck")和BlockingDeque,他们分别对Queue和BlockingQueue进行了扩展。Deque是一个双端队列,实现了在队列头和队列尾的高效插入和移除。具体实现包括ArrayDeque和LinkedBlockingDeque。

正如阻塞队列适用于生产者——消费者模,双端队列同样适用于另一种相关模式,即工作密取。在生产者——消费者设计中,所有消费者有一个共享的队列,而在工作密取设计中,每个消费者都有各自的双端队列。如果一个消费完成了自己双端队列中的全部工作,那么它可以从其它消费者双端队列末尾秘密地获取工作。密取工作模式比较传统的生产者——消费者模式更具有更高的可伸缩性,这是因为工作者线程不会在单个共享的任务队列上发生竞争。在大多数的时候,他们都只是访问自己的双端队列,而极大地减少了竞争。当工作者线程需要访问另一个队列时,他会从队列的尾部而不是从头访问获取工作,因此进一步降低 了队列上的竞争程度。


阻塞方法于中断方法

线程可能会阻塞或者暂停执行,原因有多种:等待I/o操作结束,等待获得一个锁,等待从Thread.sleep方法中醒来,或是等待另一个线程的计算结果。当线程阻塞时,它通常被挂起,并处于某种阻塞状态(blocked、waiting、或timed_waiting)。阻塞操作与执行时间很长的普通操作的差别在于,被阻塞的线程必须等待某个不受它控制的事件发生后才能继续执行。

Thread提供了interrupt方法,用于中断线程或者查询线程是否已经被中断。每个线程都有一个布尔类型的属性,表示线程的中断状态,当中断线程时设置这个状态。

中断是一种协作机制。一个线程不能强制其它线程停止正在执行的操作而去执行其它的操作。

当代码中调用了 一个将抛出interruptedException异常的方法时,你自己的方法也就变成了一个阻塞方法,并且必须要处理对中断的响应。对于库代码而言,有两种基本选择:

传递interruptedExceptioninterruptedException传递给方法的调用者。包括:根本不捕获这个异常。或者捕获这个异常,然后在执行某种简单的清理工作后再次抛出这个异常。

恢复中断:有时候不能抛出interruptedException这个异常必须捕获。可以通过调用当前线程上的inrerrupt方法恢复中断状态,这样在调用栈中更高层的代码将看到引发了一个中断。


同步工具类

同步工具类可以是任何一个对象,只要它根据其自身的状态来协调线程的控制流。阻塞队列可以作为同步工具类,其它类型的同步工具类还包括信号量(Semaphore)、栅栏(barrier)以及闭锁。在平台类库中还包含其它一些同步工具类的类,如果这些都还无法满足需要,可以创建自己的同步工具类。

闭锁

是一种同步工具类,作用相当于一扇门:在闭锁到达结束状态之前,这扇门一直是关闭的,并且没有任何线程能通过,当到达结束状态时,这扇门会打开并允许所有的线程通过。当闭锁到达结束状态后,将不会再改变状态,因此这扇门将永远保持打开状态。闭锁可以用来确保某些活动知道其他活动都完成后才继续执行。

信号量

计数信号量(Counting Semaphore)用来控制同时访问某个特定资源的操作数量,或者同时执行某个指定操作的数量。计数信号量还可以用来实现某种资源池,或者对容器施放边界。

Semaphore可以用于实现资源池,例如数据库连接池。也可以是同Semaphore将任何一种容器变为有界阻塞容器




基础总结

1.可变状态是至关重要的,所有的并发问题都可以归结为如何协调对并发状态的访问。可变状态越少,就越容易确保线程安全性。

2.尽量将域声明为final类型,除非需要它们是可变的。

3.不可变对象一定是线程安全的。

不可变对象能极大降低并发程序的复杂性。他们更为简单和安全,可以任意共享而无须加锁或保护复制等机制

4.封装有助于管理复杂性

将数据封装在对象中,更易于维持不变性条件;将同步机制封装在对象中,更易于遵循同步策略。

5.用锁来保护每个可变变量。

6.当保护同一个不变性条件中的所有变量时,要使用同一个锁。

7.在执行复合操作期间,要持有锁。

8.如果从多个线程中访问同一个可变变量时没有同步机制,那么程序就会出现问题。

9.不要故作聪明地推断出不需要使用同步。

10.在设计过程中考虑线程安全,或者在文档中明确地指出它不是线程安全的。

11.将同步策略文档化。




你可能感兴趣的:(自己写的笔记)