Java并发编程实战--笔记
概述
进程的通信:套接字,信号处理器,共享内存,信号量,文件
线程安全性
线程安全: 当多个线程访问某个类时,不管运行时环境采用何种调度方式或者这些线程将如何交替执行,并且在主调代码中不需要任何额外的同步或者协同,这个类都能表现成正确的行为,那么就称这个类时线程安全的。
无状态对象一定是线程安全的。
无状态对象值的是:既不包含任何域,也不包含任何对其他类中域的引用。计算过程中的临时状态仅存在于线程栈上的局部变量中,并且只能由正在执行的线程访问。
原子性
假定有两个操作A和B,如果从执行A的线程来看,当另一个线程执行B的时候,要么将B全部执行完,要么完全不执行B,那么A和B对彼此来说是原子的。原子操作时指对于访问同一个状态的所有操作(包括操作本身)来说,这个操作是以原子方式执行的。
竞态条件
当某个计算的正确性取决于多个线程的而交替执行时序时,就会发生竞态条件。
最常见的竞态条件类型是“先检查后执行”操作,即通过一个可能失效的观测结果来决定下一步的动作。
延迟初始化中的竞态条件
延迟初始化的目的是将对象的初始化操作推迟到实际被使用时才进行,同时要确保只被初始化一次。
与大多数并发错误一样,竞态条件并不总是会产生错误,还需要某种不恰当的执行时序。然而,竞态条件也可能导致严重的问题。
复合操作
“先检查后执行”与“读取-修改-写入”都是复合操作。
加锁机制是java确保原子性的内置机制。还有一种方法是使用现有的线程安全类。如java.util.concurrent.atomic中的原子变量类。
例如用AtomicLong代替long作为计数器。
加锁机制
要保持状态的一致性,就需要在单个原子操作中更新所有相关的状态变量。
内置锁
java提供一种内置的锁机制来支持原子性:同步代码块(Synchronized Block)。同步代码块包括两个部分:一个作为锁的对象引用,一个作为由这个锁保护的代码块。以关键字synchronized来修饰的方法就是一种横跨整个方法体的同步代码块,其中同步代码块的锁就是方法所调用的对象。
|
|
每个java对象都可以用作一个实现同步的锁,这些锁被称为内置锁(Intrinsic Lock)或者监视器锁(Monitor Lock)。线程进入同步代码块之前会自动获得锁,退出时自定释放。java的内置锁相当于互斥锁,最多一个线程可以持有。
但是这种方法会涉及性能问题。
重入
某个线程请求由其他线程持有的锁时候发出请求的线程就会阻塞。但是某个线程试图获得一个已经由自己持有的锁,请求会成功,即“重入”。
重入的一种实现方法是为每个锁关联一个获取计数值和一个所有者线程,当计数值为0,这个锁就被认为不被任何线程持有,同一个线程请求一次,计数值减1。
某种程度上避免了自己请求自己出现死锁的问题。
用锁来保护状态
锁能使保护的路径代码以串行形式来访问,可以实现对共享状态的独占访问。
对于可能被多个线程同时访问的可变状态变量,在访问它时都需要持有同一个锁,在这种情况下,我们称变量是由这个锁保护的。
对象的内置锁与其状态之间并没有内在的关联。虽然大多数类都将内置锁用作一种有效的加锁机制,但对象的域并不一定要通过内置锁来保护。当获取与对象关联的锁时,并不能阻止其他线程访问该对象,某个线程在获得对象的锁之后只能阻止其他线程获得同一个锁。之所以每个对象都有一个内置锁,是为了免去显示地创建锁对象。你需要自行构造加锁协议或者同步策略来实现对共享状态的安全访问,并且在程序中自始至终的使用它们。
每个方法采用同步并不能保证线程安全,因为原子操作之间可能还是存在竞态条件,此外还可能导致活跃性问题(Liveness)或者性能问题(Performance).
活跃性与性能
通过缩小同步代码块的作用范围可以容易做到既确保并发性又维护线程安全性。要确保同步代码块不能过小,并且不要将本来是原子的操作拆分到多个代码块中。应该尽量将不影响共享状态且执行时间较长的操作从同步代码块中分离出去,从而在这些操作的执行中,其他线程可以访问共享状态。
要判断同步代码块的合理大小,需要在各种设计需求与性能之间进行权衡。包括安全性(这个需求必须得到满足)、简单性和性能。有时候,在简单性与性能之间会发生冲突,但通常能平衡。
另外加锁时间过长可能带来活跃性和性能问题。
对象的共享
同步和共享和发布对象形成了构建线程安全类以及通过java.util.concurrent类库来构建并发应用程序的重要基础。
同步代码块和同步方法可以确保以原子的方式执行操作,但是一种常见的误区是认为关键字synchronized只能用于实现原子性或者确定“临界区(Critical Section)”。同步还有另一个重要方面:没存可见性(Memory Visibility)。我们不仅希望防止某个线程正在使用对象状态而另一个线程在同步修改该状态,并且希望确保当一个线程修改了对象状态后,其他线程能够看到发生的状态变换。如果没有同步就无法实现,可以通过显式的同步或者类库中的内置同步来保证对象被安全的发布。
可见性
在没有同步的情况下,编译器处理器以及运行时等都可能对操作的执行顺序进行一些意想不到的调整。在缺乏足够同步的多线程程序中,要想对内存操作的执行顺序进行判断,几乎无法得出正确的结论。
失效数据
get和set操作都必须实现同步。否则get可能取得失效值。
非原子的64位操作
当线程在没有同步的情况下读取变量,可能会得到一个失效值,但至少这个值是由之前的某个线程设置的值,而不是一个随机值。这种安全性保证也被称作最低安全性(out-of-the-air-safty)。
最低安全性适用于绝大多数变量,但是存在一个例外:非volatile类型的64位数值变量double和long。java内存模型要求,变量的读取和操作和写入都必须是原子操作,但是对于非volatile的long和double变量,jvm允许将64位的读写操作分解为两个32位的操作。
多线程使用long double需要关键字volatile或者锁。
加锁与可见性
加锁的含义不仅仅局限于互斥行为,还包括内存可见性,为了确保所有的线程都能看到共享变量的最新值,所有执行读操作或者写操作的线程都必须在同一个锁上同步。
Volatile变量
稍弱的同步机制,确保将变量的更新操作通知到其他线程。把变量声明为volatile类型后,编译器与运行时都会注意到这个变量时共享的,因此不会将该变量上的操作与其他内存操作一起重排序。volatile变量不会被缓存在寄存器或者其他对处理器不可见的地方,因此在读取volatile变量时总会返回最新写入的值。
访问volatile变量时不会执行加锁操作,也不会执行线程阻塞,因此volatile变量是一种比synchronized关键字更轻量级的同步机制。
写入volatile变量相当于退出同步代码块,读取volatile变量相当于进入同步代码块,但是不能过度依赖volatile。
仅当volatile变量能简化代码的实现以及对同步策略的验证时,才能使用它们。如果在验证争取性时需要对可见性进行复杂判断,那么就不要使用volatile变量。volatile变量的正确性使用包括:确保自身状态的可见性,确保所引用对象的状态的可见性以及标志一些重要程序生命周期时间的发生(如初始化或者关闭)。
加锁机制能确保可见性和原子性,而volatile变量只能确保可见性。
例如自增操作的原子性volatile不能保证。
当且仅当满足以下所有条件是,才应该使用volatile变量;
- 对变量的写入操作不依赖变量的当前值,或者你能确保只有单个线程更新变量的值
- 该变量不会与其他状态变量一起纳入不变性条件中
- 在访问变量时不需要加锁
发布与溢出
发布publish一个对象的意思是,使对象能够在当前作用域之外的代码中使用。
例如:
- 将一个对象的引用保存早其他代码可以访问的地方
- 在某一个非私有的方法中返回该引用
- 将引用传递到其他类的方法中
- 发布一个内部的类实例
当某个不该发布的对象被发布的时候称为逸出escape。
内部类的this引用会在构造函数中逸出。当内部类实例发布的时候,在外部封装的类实例也逸出了。当且仅当对象的构造函数返回时,对象才处于可预测的和一致的状态。
不要在构造过程中使用this引用逸出
在构造过程中使用this引用逸出的一个常见错误是,在构造函数中启动一个线程。当对象在其构造函数中创建一个线程的时候,无论是显式创建(通过将它传给构造函数)还是隐式创建(由于Thread或者runnable是该对象的而一个内部类) this引用都会被新创建的线程共享。在对象尚未完全构造之前,新的线程就可以看见他。在构造函数中创建线程最好不要立即启动,而是通过一个start或者initialize方法来启动。在构造函数中调用一个可改写的实例方法(既不是私有也不是final)同样会导致this引用在构造过程中溢出。
如果香皂构造函数中注册一个事件监听器或者启动线程,可以使用一个私有的构造函数和一个公共的工厂方法,从而避免不正确的构造过程。
线程封闭
一种避免使用同步的方法就是不共享数据,仅在单线程内访问数据,称作线程封闭。
java语言及核心库提供了一些机制来帮助维持线程的封闭性,例如局部变量和ThreadLocal类,但即使突刺,程序员仍然需要负责确保封闭在线程中的对象不会从线程中逸出。
Ad-hoc线程封闭
ad-hoc线程封闭是指,维护线程封闭性的职责完全由程序实现来承担。
非常脆弱,没有任何一种语言特性如可见性修饰符或者局部变量能将对象封闭到目标线程上。通常使用是为特定子系统实现一个单线程子系统。少用,用栈封闭或者ThreadLocal。
栈封闭
线程封闭的特例,只能通过局部变量才能访问对象。
ThreadLocal类
使线程中某个值与保存值的对象关联起来,提供了get与set等访问接口与方法,这些方法为每个使用该变量的线程都存有一份独立的副本,因此get总是返回由当前执行线程在调用set时设置的最新值。
ThreadLocal对象通常用于防止对可变的单实例变量(singleton)或全局变量进行共享。
例如将JDBC的链接保存都ThreadLocak对象中,每个线程都拥有自己的链接。
当某个频繁执行的操作需要一个临时对象例如缓冲区而又希望避免在每次执行时都重新分配该临时对象就可以使用这项技术。
假设你需要将一个单线程应用程序移植到多线程环境中,通过将共享的全局变量转换为ThreadLocal对象,可以维持线程安全性。
在实现应用程序框架时大量采用了ThreadLocal。
ThreadLocal变量类似于全局变量,它能降低代码的可重用性,并在类之间引入隐含的耦合变量,因此在使用的时候要格外小心。
不变性
满足同步需求的另一种方法是使用不可变对象(Immutable Object).
不可变对象一定是线程安全的
不可变对象不等于将对象中所有的域都声明为final;类型,即时对象中所有的域都是final类型,这个兑现也仍然是可变的,因为在final类型的域中可以保存对可变对象的引用。
不变性条件如下:
当满足以下条件时,对象才是不可变的:
- 对象创建以后其状态不能修改 - 对象的所有域都是final类型(有特例,如String) - 对象时正确创建的(即在对象创建期间,this引用没有逸出)
“不可变对象”与“不可变的对象引用”之间存在差异。保存在不可变对象中的程序状态仍然可以更新,即通过一个保存新状态的实例来替换原有的而不可变对象。
final域
final类型的域是不能求改的,final域能确保初始化过程的安全性,从而可以不受限制的访问不可变对象,并在共享这些对象时候无需同步。
正如“除非需要更高的可见性,否则应将所有的域声明为私有域”是一个良好的编程习惯
“除非需要某个域是可变的,否则应该将其声明为final域”也是一个良好的编程习惯。
示例:使用Volatile类型来发布不可变对象
对于在访问和更新多个相关变量时出现的竞争条件问题,可以通过将这些变量全部保存在一个不可变对象中来消除。如果是一个可变对象,那么就必须使用锁来确保原子性。如果是一个不可变对象,那么当线程获得了该对象的引用后,就不必担心另一个线程会修改对象的状态。如果要更新对象,那么可以创建一个新的容器对象,但其他使用原有对象的线程仍然会看到对象处于一致的状态。
安全发布
不正确的发布:正确的对象被破坏
不能指望一个尚未被完全创建的对象拥有完整性。某个观察该对象的线程将看到对象处于不一致的状态,然后看到对象的状态突然发生变化,即使线程在对象发布后还没修改过。
不可变对象与初始化安全性
由于不可变对象是一种非常重要的对象,因此java内存模型为不可变对象的共享提供了一种特殊的初始化安全性保证。
为了维持初始化安全性,必须满足不可变性的所有需求:状态不可更改,所有域都是final,正确构造过程。
任何线程都可以在不需要额外同步的情况下安全的访问不可变对象,即时在发布这些对象时没有同步。
这种保证还将延伸到正确创建对象中所有的final类型的域。在没有额外同步的情况下,也可以安全的访问final类型的域。然而,如果final类型的与指向的是可变对象,那么在访问这些于所指向的对象的状态时仍需要同步。
安全发布的常用模式
可变对象必须通过安全的方式来发布,即发布和使用该对象的线程时都必须使用同步。
要安全的发布一个对象,对象的引用以及对象的状态必须同时对其他线程课件。一个正确构造的对象可以通过以下方式来安全地发布:
- 在静态初始化函数中初始化一个对象引用
- 将对象的引用保存到volatile类型的域或者AtomicReference对象中
- 将对象的引用保存到某个正确构造对象的funal域中
- 将对象的引用保存到一个由锁保护的域中(或者线程安全容器内部)
将对象放到某个线程安全的容器内部如Vector或者synchronizedList时,满足最后一条需求。线程安全库的容器类提供了以下的安全发布保证:
- 通过将一个键或者值当如到HashTable、synchronizedMap或者ConcurrentMap中,可以安全地将它发布给任何从这些容器中访问它的线程(无论是直接访问还是通过迭代器访问)。
- 通过将某个元素放入到Vector、CopyOnWriteArrayList、synchronizedList或synchronizedSet中,可以将该元素安全地发布到任何从这些容器中访问该元素的进程。
- 通过将某个元素放入到BlockingQueue或者ConcurrentLinkedQueue中,可以将该元素安全地发布到人格从这些队列中访问该元素的进程。
- 类库中其他数据传递机制,如Future和Exchangeer同样能实现安全发布。
通常要发布一个讲台构造的对象,最简单安全的方式是使用静态的初始化器:
public static Holder holder = new Holder(42);
静态初始化器由jvm在类的初始化阶段执行。由于jvm内部存在同步机制,因此通过这种方式初始化的任何对象都可以被安全地发布。
事实不可变对象
如果对象在发布后不会再改吧,足以保证所有访问都是安全的。
如果对象从技术上看是可变的,但其在发布后不会再改变,那么把这种对象称作“事实不可变对象”。
在没有额外同步的情况下,任何线程都可以安全地使用被安全发布的事实不可变对象。
可变对象
可变对象必须在发布对象时使用同步,并且在每次对象访问时同样需要使用同步来确保后继修改操作的可见性。
对象的发布需求取决于它的可变性:
- 不可变对象可以通过任意机制来发布
- 事实不可变对象必须通过安全方式来发布
- 可变对象必须通过安全方式来发布,并且必须是线程安全的或者由某个锁保护起来。
安全地共享对象
当发布一个对象时,必须明确的说明对象的访问方式。
在并发程序使用和共享对象时,可以使用一些实用的策略,如:
- 线程封闭
线程封闭的对象只能由一个线程拥有,对象被封闭在该线程中,并且只能由这个
线程修改。- 只读共享
在没有额外同步的情况下,共享的只读对象可以由多个线程并发访问。但是任何线程
都不能修改。共享的对象包括不可变对象和事实不可变对象。- 线程安全共享
线程安全的对象在其内部实现同步,因此多个线程可以通过对象的公有接口进行访问
而不需要进一步的同步。- 保护对象
被保护的对象只能通过持有特定的锁来访问。保护的对象包括封装在其他线程安全的
对象中的对象,以及已发布的并且由某个特定锁保护的对象。
对象的组合
设计线程安全的类
尽管可以将程序所有状态保存在公有的静态域,但是与将状态封装的程序相比,安全性更难保证。
在设计线程安全类的过程中,需要包含以下三个基本要素:
- 找出构成对象状态的所有变量
- 找出约束状态变量的不变性条件
- 建立对象状态的并发访问管理策略
同步策略定义了如何在不违背对象不变条件或者后验条件的情况下对其状态访问操作进行协同。同步策略规定了如何将不可变性、线程封闭与加锁机制等结合起来维护线程安全性,并且规定了哪些变量由哪些锁来保护。要确保开发人员可以对这个类进行分析和维护,必须将同步策略写为正式文档。
收集同步需求
不可变条件判断状态是有效的还是无效的。
后延条件判断状态迁移是否是有效的。
由于不可变性条件以及后延条件在状态以及状态转换上施加了各种约束,因此就需要额外的同步与封装。如果这些状态是无效的,那么必须对底层的状态变量进行封装,否则客户代码可能会使对象处于无效状态;
如果某个操作中存在无效的状态转换,那么该操作必须是原子的。另外如果类中没有施加这种约束就可以放宽封装性或者序列化等要求,一遍获得更高的灵活性或性能。
依赖状态的操作
基于状态的先验条件,如删除元素前队列必须处于“非空”。
如果某个操作中包含有基于状态的先验条件,那么这个操作就成为依赖状态的操作。
实现某个等待先验条件为真时才执行需要通过现有库中类如阻塞队列或者信号量来实现依赖状态的行为。
状态的所有权
许多情况下,所有权与封装性总是相互关联的:对象封装它所拥有的状态,反之也成了,即对它封装的状态拥有所有权。状态变量的拥有者可以决定何种加锁协议维护变量状态的完整性。
然而发布某个独享的引用就不在拥有独占权,最多是“共享控制权”。
容器类通常表现出一种“所有权分离”的形式。其中容器类具有其自身的状态,而客户代码则拥有容器中各个对象的状态。
实例封闭
封装简化了线程安全类的实现过程,提供了一种实例封闭的机制。
将数据封装在对象内部,可以将数据的访问限制在对象的方法删,从而更容易确保线程
在访问数据的时候总能持有正确的锁。
被封闭的对象一定不能超过既定的作用域。如对象可以封闭在类的一个实例(作为类的一个私有成员)中,或者封闭在某个作用域内(作为一个局部变量),再或者封闭在线程内(例如单个线程中方法传递而不是多个线程共享对象)。
java的类库中有很多线程封闭的实例。其中有些类的唯一用途就是将非线程安全的类转化为线程安全的列。一些基本的容器是非线程安全的,例如ArrayList和HashMap.。但类库提供了包装工厂方法(例如Collections.synchronizedList及其类似方法)。使得这些非线程安全的类可以在多线程环境中安全的使用。
将本该被封闭对象发布出去会破坏封闭性。
封闭机制更易于构造线程安全的类,因为放封闭类的状态是,在分析类的线程安全性时就无需检测整个实例。
java监听器模式
遵循java监听器模式的对象会把对象的所有可变状态都封装起来,并由对象自己的内置锁来保护。
许多类都使用监听器模式如Vector、HashTable。
监听器模式仅仅是一种编写代码的约定,对于任何一种锁对象,只要自始至终都用该锁对象,都可以用来保护对象的状态。
线程安全性的委托
某些情况下,通过多个线程安全类组合而成的类是线程安全的(各个状态之间不存在耦合关系,彼此独立);某些情况下,只是一个良好的开端。
委托失效时
如果某个类有复合操作,仅靠委托是不足以实现线程安全的。在这种情况下,这个类必须提供自己的加锁机制来保证这些复合操作都是原子操作,除非整个复合操作都可以委托给状态变量。
如果一个类是由多个独立且线程安全的状态变量组成,并且在所有操作中都不包含无效的状态转换,那么可以将线程安全性委托给底层的状态变量。
发布底层的状态变量
如果一个状态变量时线程安全的,并且没有任何不变性条件来约束它的值,在变量的操作上也不存在任何不允许的状态转换,那么就可以安全地发布这个变量。
现有的线程安全类中添加功能
要添加一个新的原子操作,最安全的方法是修改原始的类,但这通常无法做到。因为你可能无法访问或者修改类的源代码。修改的时候还需要理解代码中的同步策略,这样增加的功能才能和原有的设计保持一致。
另一种方法是扩展这个类。但是并非所有的类都将状态向子类公开,因此也不合适。
扩展方法比直接添加更脆弱,因为现有的额同步策略实现被分布到多个单独维护的源代码文件中。若底层类改变了同步策略选择不同的锁,子类会被破坏。
客户端加锁机制
第三种策略是扩展类的功能,但并不扩展类本身,而是将扩展代码放入一个“辅助类”中。
将同步策略文档化
在文档总说明了客户代码需要了解的线程安全性保证,以及代码维护人员需要了解的同步策略。
基础构建模块
同步容器类
Vector和HashTable,这些同步的封装是由Collections.synchronizedXxx等工厂方法创建的。这些类实现线程安全的方式是:将它们的状态封装起来,并对每个共有方法都进行同步,使得每次只有一个线程能访问容器的状态。
同步容器类的问题
同步容器类都是线程安全的,但是在某些情况下需要额外的客户端加锁来保护复合操作。
常见的复合操作:
- 迭代(反复访问元素,知道遍历完容器中所有元素)
- 跳转(根据指定顺序找到当前元素的下一个元素)
- 条件运算(例如若没有则添加)
在同步容器类中,这些复合操作在没有客户端加锁的情况下仍然是线程安全的,但是当其他线程并发地修改容器时,可能会表现出意料之外的行为。
迭代器与ConcurrentModificationExpection
迭代器是即时失败的fail-fast,意味着当他们发现容器在迭代过程中被修改的时候就会抛出一个ConcurrentModificationException异常。
如果不希望迭代期间对容器加锁,另一种替代方法是克隆容器,并在副本上进行迭代。
隐藏迭代器
并发容器
通过并发起来代替同步容器,可以极大地提高伸缩性并降低风险
- HashMap: ConcurrentHashMap
- List: CopyOnWriteArrayList
- Queue:ConcurrentLinkedQueue,BlockingQueue
- SortedMap: ConcurrentSkipListMap
- SortedSet:ConcurrentSkipListSet
ConcurrentHashMap
采用的是分段锁(Lock Striping)的加锁机制。
任意数量的读取线程可以并发的访问Map,执行读取操作的线程和执行写入操作的线程可以并发的访问map,并且一定数量的写入线程可以并发地修改Map。ConcurrentHashMap带来的结果是,在并发环境下将实现更高的吞吐量,而在单线程环境中只损失非常小的性能。
ConcurrentHashMap与其他并发容器一起增强了同步容器类:提供的迭代器不会抛出异常,不需要再迭代过程对容器加锁。返回的迭代器具有弱一致性,并非即时失败。可以容忍并发地修改,当创建迭代器时会遍历已有的元素,并可以(但是不保证)在迭代器被构造后将修改操作反映给容器。
但是例如size和isEmpty等返回只能返回估计值。因此这些操作的需求被弱化了,以换取对其他更重要操作的性能优化,包括get、put、containsKey和remove等。
ConcurrentHashMap没有实现对map加锁提供独占访问,但是HashTable和synchronizedMap实现了。但是签字比后者有更多优势更上劣势。只有当引用程序需要加锁Map进行独占访问时候才放弃ConcurrentHashmap。
额外的原子map操作
“若没有则添加”“若相等则移除”“若相等则替换”都已经实现为原子操作并在ConcurrentMap接口中声明。
CopyOnWriteArrayList
代替同步List,在迭代期间不需要加锁或者复制。(类似的CopyOnWriteSet作用是替代同步Set)。
“CopyOnWrite”写入时复制容器的线程安全性在于,只要正确的发布一个事实不可变的对象,那么在访问该对象时候就不再需要进一步的同步。每次修改的时候,都会创建并重新发布一个新的容器副本,从而实现可变性。
写入时复制的容器迭代器保留一个指向底层基础数组的引用,这个数组当前位于迭代器的起始位置,由于它不会被修改,因此在对其进行同步的时候只需要确保数组内容的可见性。
显然开销很大,仅当迭代操作远大于修改操作的时候,才使用“写入时复制”。这个准则很好的描述了很多事件通知系统。
阻塞队列和生产者消费者模式
阻塞队列BlockingQueue提供了可阻塞的put和take方法,以及支持定时的offer和poll方法。如果队列已经满了,那么put方法可以将阻塞直到有空间可用;如果队列为空,那么take方法将会阻塞直到有元素可用。队列可以是有界的也可以是无界的,无界永远不会阻塞。
阻塞队列支持生产者-消费者这种设计模式,这种方式能简化开花,消除生产者和消费者之间的代码依赖性,将生产数据和使用数据解耦来简化工作负载管理。
一种最常见的生产者消费者设计模式就是线程池与工作队列的组合,在Executor任务执行框架中采用。
在构建高可靠的应用程序时,有界队列是一种强大的资源管理工具:能抑制并防止产生过多的工作项,使应用程序在负荷过载的情况向变得更加健壮。
应该尽早通过阻塞队列在设计者构建资源管理机制。如果阻塞队列不能完全符合设计需求,还可以通过信号量来创建其他的阻塞数据结构。
BlockingQueue的实现由LinkedBlockingQueue和ArrayBlockingQueue。分别与LinkedList和ArrayList类似但具有更好并发性能。还有PriorityBlockingQueue和SynchronousQueue。SynchronousQueue不是一个真正队列,不为为队列中元素维护存储空间,维护的是一组线程,这些线程在等待着把元素加入或者移出队列。而且可以直接交付工作。因此put和take会一致阻塞直到另一个线程已经准备好直接交付。
仅当有足够多的消费者,并且总是有一个消费者准备好交付的时候才适合同步队列。
串行线程封闭
java.util.concurrent包含足够同步机制,从而安全的将对象从生产者线程发布到消费者线程。
对于可变对象,生产者-消费者与阻塞队列一起,促进了串行线程封闭。
对象池利用了串行线程封闭,将对象“借给”一个请求线程。
我们也可以用其他发布机制来传递可变对象的所有权,但必须保证只有一个线程能接受被转移的对象。阻塞队列简化了这个操作,除此之外,还可以通过ConcurrentMap原子方法 remoce或者AtomicReference原子方法compareAndSet完成。
双端队列与工作密取
正如阻塞队列适用于生产者-消费者模型,双端队列适用于工作密取(Work Stealing).
前者所有消费者一个共享队列,后者每个消费者有各自的双端队列。如果一个消费者完成自己队列中全部工作,可以从其他消费者队列末尾秘密的获取工作。
工作密取极大较少了竞争,适用于既是生产者又是消费者的问题–当执行某个工作可能产生更多工作的问题。
阻塞方法与中断方法
等待IO,等待获得锁,等待从Thread.sleep中等都会线程发生阻塞,并通常处于某种阻塞状态(BLOCKED,WAITING或者TIMED_WAITING)。
Thread提供interrupt查询线程是否已经中断或者直接中断。
抛出InterruptedException异常时候可以传递这个异常或者恢复中断。
同步工具类
阻塞队列不仅能作为保存对象的容器还能协调生产者和消费者等线程之间的控制了。
同步工具类是根据自身状态来协调线程的控制流。除了阻塞队列还有信号量Semaphore、栅栏Barrier以及闭锁latch。
这些同步工具类都包含一些特定的结构化属性:封装了一些状态,这些状态将决定执行同步工具类的线程还是继续执行还是等待,此外还提供一些方法对状态进行操作,以及另一些方法用于高效等待同步工具类进入预期状态。
闭锁
相当于一扇门。
FutureTask
也可以做闭锁,表示的计算是通过Callable实现。
信号量
用来控制同时访问某个特定资源的数量或者同时执行某个指定操作的数量,还可以实现某种资源池或者对容器施加边界。
栅栏
闭锁来启动一组相关的操作后者等待一组相关的操作结束,是一次性对象,一旦进入终止状态就不能被重置。
栅栏是所有线程必须同时到达栅栏位置才能继续执行,用于实现一些协议如约定时间集合。
构建高效且可伸缩的结果缓存
第一部分小结
- 可变状态是至关重要的。可变状态越少,越容易保证线程安全。
- 尽量将域声明为final类型,除非需要他们是可变的
- 不可变对象一定是线程安全的
- 封装有助于管理复杂性
- 用锁来保护每个可变变量
- 保护同一个不变性条件中所有变量时,要使用同一个锁
- 执行符合操作期间要持有锁
- 如果多个线程访问一个可变变量时没有同步机制,那么程序会出现问题。
- 不要故作聪明推断出不需要同步
- 在设计过程中考虑线程安全,或者在文档中明确指出不是线程安全的
- 将同步策略文档化