一、显式锁

Java 5.0之前,协调共享对象的访问时可用机制的只有synchronized和volatile,Java 5.0之后增加了ReentrantLock,这个是当内置加锁机制不适用时,作为一种可选择的高级功能。

1.Lock与ReentrantLock

Lock提供了一种无条件的、可轮询的、定时的一级可中断的锁获取操作,所有加锁和解锁方式都是显式的。

ReentrantLock实现了Lock接口,并提供了与synchronized相同的互斥性和内存可见性。ReentrantLock支持在Lock接口中定义的所有的获取锁模式,并且对比synchronized还为处理锁的不可用性提供了更高的灵活性。

ReentrantLock一个不能完全替代synchronized的原因就是,当程序的执行控制离开被保护的代码块时,不会自动清除锁,需要在finally块中释放锁。

  • 轮询锁与定时锁
    可轮询锁和可定时锁的锁获取模式具有更完善的错误恢复机制。在实现一个具有时间限制的操作时,使用定时锁可以根据剩余时间提供一个时限,如果操作还不能在指定的时间内给出结果,那么就会使程序提前结束。
  • 可中断的锁获取操作
    可中断的锁获取操作能在可取消的操作中使用加锁,因此当需要实现一个定时和可中断的锁获取操作时,可以使用tryLock方法。
  • 非块结构的加锁
    在内置锁中,锁的获取和释放等操作都是基于代码块的,释放锁的操作总是与获取锁的操作处于同一个代码块中,而不考虑控制权是如何退出该代码块。自动的锁释放操作简化了对程序的分析。还有一些更灵活的加锁规则,例如锁分段技术等。

2.性能考虑因素

如果有越多的资源被耗费在锁的管理和调度上,那么应用程序得到的资源就越少。锁的实现方式越好,就需要越少的系统调用和上下文切换,在共享内存总线上内存同步通信量也越少。性能也是一个不断变化的指标,此时和彼时通过测试基准测出来的结果可能都存在交大的差异。

3.公平性

ReentrantLock的构造函数中提供了两种公平性选择:公平锁,线程按照它们发出请求的顺序来获得锁,但在非公平的锁上,当一个线程请求非公平的锁时,如果在发生请求的同时该锁的状态变为可用,那么这个线程将跳过队列中所有的等待线程并获得这个锁。

并不是在所有情况下都会期望公平性,例如公平性将由于挂起线程和恢复线程时存在的开销而极大的降低性能。大多数情况下,非公平锁的性能要高于公平锁。不必要的话不要为公平性付出代价。主要原因在于恢复一个被挂起的线程与该线程真正开始运行之间存在着严重的延迟,这期间的延时甚至可以给第三个线程使用完资源,这种插队的情况可以带来更大的吞吐量。

4.在synchronized和ReentrantLock之间选择

ReentrantLock在加锁和内存上提供的语义和内置锁相同,此外还提供一些功能,包括定时的锁等待、可中断的锁等待、公平性、以及非块结构的加锁。

除了上述的功能外还是应该优先使用synchronized,因为内置锁更加简洁紧凑,不用在finally块中考虑unlock。

5.读-写锁

互斥锁是一种过于强硬的加锁规则,较于保守的加锁方式同样也避免了“读/读”冲突,如果程序中大多数访问操作都是读操作,可以稍微放宽加锁需求,提升程序性能。

上述情况就可以使用到读/写锁:一个资源可以被多个读操作访问,或者被一个写操作访问,并且两者不能同时进行。

读写锁在多处理器系统上被频繁读取的数据结构中能提升性能,而在其他情况下,却比独占锁性能要略差一点,因为复杂度更高。


二、构建自定义同步工具

类库包含了许多存在状态依赖的类,例如FutureTask、Semaphore和BlockingQueue等。

1.状态依赖性的管理

  • 将前提条件的失败传递给调用者
  • 通过轮询与休眠来实现简单的阻塞
  • 条件队列,一组线程能够通过某中方式来等待特定的条件成真

2.使用条件队列

使用条件队列涉及以下相关内容

  • 条件谓词
    是某个操作成为状态依赖操作的前提条件
  • 唤醒
  • 丢失信号
  • 通知

3.显式的Condition对象

一个Condition和一个Lock关联在一起,就像一个条件队列和一个内置锁相关联一样,Condition使得在每个锁上可存在多个等待、条件等到可以是可中断的或不可中断的、基于时限的等待,以及公平的或非公平的队列操作。

在Condition对象中,与wait、notify和notifyAll方法对应的分别是await、signal和signalAll。但是,Condition对Object进行了扩展,因而它也包含wait和notify方法。

4.Synchronized剖析

ReentrantLock和Semaphore这两个接口之间存在很多共同点,基于两者可以相互实现对方。它们在实现时都是用了一个共同的基类,即AbstractQueuedSynchronizer(AQS),AQS是一个用于构建锁和同步器的框架,基于它的还有CountDownLatch、ReentrantReadWriteLock、SynchronousQueue和FutureTask。

5.AbstractQueuedSynchronizer

大部分情况不会直接使用AQS,标准同步器类的集合能够满足绝大多数情况的需求。

基于AQS构建的同步器类中,基本的操作包括各种形式的获取和释放操作。AQS负责管理同步器类中的状态,在同步器类中还可以自己管理一些额外的状态变量。获取操作可以是独占操作也可以是一个非独占操作。

  • ReentrantLock
    独占的获取方式。
  • Semaphore与CountDownLatch
    将AQS的同步状态用于保存当前可用许可的数量。
  • FutureTask
    AQS同步状态用来保存任务的状态,Future.get的语义非常类似于闭锁的语义:如果发生了某个时间,那么线程就可以恢复执行。
  • ReentrantReadWriteLock
    不同于ReadWriteLock中一个读取锁一个写入锁,ReentrantReadWriteLock中单个AQS子类将同时管理读取加锁和写入加锁。AQS内部维护一个等待线程队列,其中记录了某个线程请求的是独占访问还是共享访问。

Ref

Briangoetz. Java并发编程实战[M]. 机械工业出版社, 2012.