java并发编程的艺术4之Java中的锁

1.Lock接口

Java SE5之后新增加的,与Synchronized功能类似,只是使用时需要显示的获取和释放锁。虽然它缺少了(通过synchronized块或者方法所提供的)隐式获取释放锁的便捷性,但却拥有了锁获取与释放的可操作性,可终端的获取锁以及超时获取锁等多种synchronized关键字所不具备的同步特性。

在finally块中释放锁,保证在获取到锁之后,最终能够被释放。

不需要将获取锁的过程卸载try块中,因为如果在获取锁(自定义锁的实现)时发生了异常,异常抛出的同时,也会导致锁的无故释放。

java并发编程的艺术4之Java中的锁

java并发编程的艺术4之Java中的锁

 2 队列同步器

 队列同步器(AbstractQueuedSynchronized ),简称同步器,是用来构建锁或者其他同步组件的基础框架。

主要使用方式是继承。在实现抽象方法时,避免不了修改同步器的状态。需要用到同步器的三个方法(getState(),getState(int newState)和compareAndSetState(int expect,int update))。推荐子类被定义为自定义同步组件的静态内部类。

同步器提供的模板基本上分为3类:独占式获取与释放同步状态、共享式获取与释放同步状态、查询同步队列中的等待线程情况。

队列同步器的实现:同步队列、独占是同步状态获取与释放、共享式同步状态获取与释以及超时获取同步状态等同步器的核心数据结构与模板方法。

2.1 同步队列

同步器依赖内部的同步队列(一个FIFO双向队列)来完成同步状态的管理。当前线程获取同步状态失败时,同步器会将当前线程及等待状态等信息构造成为一个节点(Node)并将其加入同步队列,同时会阻塞当前线程,当同步状态释放时,会把首节点中的线程唤醒,使其再次尝试获取同步状态。

节点:保存获取同步状态失败的县城引用、等待状态以及前驱和后继节点。

基于CAS设置尾节点:compareAndSetTail(Node expect,Node update),传递当前线程认为的尾节点和当前节点,只有设置成功之后,当前节点才正式与之前的尾节点建立关联。

2.2 独占式同步状态获取与释放

调用acquire(int args)方法获取同步状态,该方法对于中断不敏感。

看一看书上代码的解释。

整个流程的总结:在获取同步状态时,同步器维护一个同步队列,获取状态失败的线程都会被加入到队列中并在队伍中自旋;移出队列(或停止自旋)的条件时前驱节点为头节点并且成功获取了同步状态。在释放同步状态时,同步器调用tryRelease(int args)方法释放同步状态,然后唤醒头节点的后继节点。

2.3 共享式同步状态获取与释放

共享式获取与独占式获取最主要的区别在于同一时刻能否有多个线程同时获取到同步状态。

acquireShared(int args)方法共享地获取同步状态 ;tryReleaseShared(int args)方法尝试获取同步状态,返回值为int类型,当返回值大于等于0时,表示能够获取到同步状态。

releaseShared释放同步状态。

2.4 独占式超时获取同步状态

doAcquiredNanos(int arg,long nanosTimeout)方法:在指定的时间段内获取同步状态,如果获取到同步状态则返回true,否则返回false。

主要计算需要睡眠的时间间隔nanosTimeout,nanosTimeout-=now-lastTime。

该方法在自旋过程中,当节点的前驱节点为头节点时尝试获取同步状态,如果获取成功则从该方法返回,这个过程与独占式同步获取的过程类似,但是在同步状态获取失败的处理上有所不同。如果当前线程获取同步状态失败,则判断是否超时(nanosTimeout小于等于0),如果没有超时,重新计算超时时间间隔nanosTimeout,然后使当前线程等待nanosTimeout纳秒。

   nanosTimeout小于等于spinForTimeThreshold(1000纳秒)时,将不会使该线程进行超时等待,而是进入快速的自旋过程。因为非常短的超时等待无法做到十分精确,会让超时等待整体上表现不佳,所以在超时非常短的场景下,同步器会进入无条件的快速自旋。

2.5 自定义同步组件--TwinsLock

通过分析自己的需求对同步器进行继承重写。

3 重入锁

重入锁ReentrantLock,支持重进入的锁,表示该锁能够支持一个线程对资源的重复加锁。还支持获取锁时的公平和非公平性选择。

3.1 实现重进入

重进入:线程在获取到锁后能够再次获取该锁而不被堵塞,该特性的实现需要以下两个问题:

  1. 线程再次获取锁
    1. 锁需要识别获取锁的线程是否为当前占据锁的线程,如果是,则能够再次获取锁
  2. 锁的最终释放
    1. 要求锁对于当前线程获取锁的次数进行计数,锁被释放时,计数自减,当计数等于0时表示锁已经成功释放。

ReentrantLock通过组合自定同步器来实现锁的获取与释放。

3.2 公平与非公平获取锁的区别

如果一个锁是公平的,那么锁的获取顺序就应该符合请求的绝对时间顺序,也就是FIFO。

公平锁tryAcquire与非公平锁nonfairAcquire方法进行比较,区别在于判断位置多了一个hasQueuedPredecessors方法,即加入了同步队列中当前节点是否有前驱节点的判断,如果该方法返回true,则表示线程比当前线程更早地请求获取锁,所以需要等待前驱线程获取并释放锁之后才能继续获取锁。

非公平性锁地开销更小,切换次数较少。

4 读写锁

之前的锁(mutex和ReentranLock)都是排他锁,同一时刻只允许一个线程访问,而读写锁在同一时刻可以允许多个读线程访问,但在写线程访问时,所有的读线程和其他写线程均被堵塞。

读写锁维护了一对锁,一个读锁和一个写锁。

实现是:ReentrantReadWriteLock,有三个特性:公平性选择,重进入,锁降级。

4.1 读写锁的接口与示例

ReadWriteLock仅定义了获取读锁和写锁的两个方法,ReadLock()和WriteLock()方法,其实现ReentrantReadWriteLock除了接口方法,还提供了一些便于外界控制其内部工作状态的方法。

4.2 读写锁的实现分析

ReentrantReadWriteLock的实现主要包括:读写状态的设计,写锁的获取与释放,读锁的获取与释放以及锁降级

4.2.1 读写状态的设计

读写锁依赖自定义同步器来实现同步功能,读写状态就是其同步器的同步状态。同步状态标识锁被一个线程重复获取的次数。读写锁需要在同步状态(一个整型变量)上维护多个读线程和一个写线程的状态,这个状态的设计是读写锁实现的关键。

‘按位切割使用’这个整型变量。读写锁将变量切分成了两个部分,高16位表示读,低16位表示写。

4.2.2 写锁的获取与释放

支持重进入,如果当前线程在获取写锁时,读状态不为0(读锁已经被获取)或者该线程不是已经获取写锁的线程,则当前线程进入等待状态。

释放与ReentrantLock类似

4.2.3 读锁的获取与释放

支持重进入,如果当前线程已经获取了读锁,则增加读状态。如果当前线程在获取读锁时,写锁已被其他线程获取,则进入等待状态。一次读状态减少的值是:1<<16

4.2.4 锁降级

锁降级指的是写锁降级成为读锁,是指把持住当前拥有的写锁,再获取到读锁,随后释放先前拥有的写锁的过程。RentrantReadWriteLock不支持锁升级,目的也是保证数据可见性。

5 LockSupport工具

LockSupport定义了一组公共静态方法,这些方法提供了最基本的线程阻塞和唤醒功能,而LockSupport也成为构建同步组件的基础工具。LockSupport定义了以park开头的一系列方法来阻塞当前线程,unpark方法唤醒被阻塞线程。

6 Condition接口

任何一组Java对象,都拥有一组监视器方法,主要有wait(),wait(long timeout),notify()以及notifyAll()方法,这些方法与synchronized同步关键字配合,可以实现等待/通知模式。Condition接口也提供了类似Object的监视器方法,与Lock配合可以实现等待/通知模式,但是这两者在使用方式以及功能特性上还是有差别的。

java并发编程的艺术4之Java中的锁

6.1 Condition接口与示例

Condition依赖Lock对象,定义了等待/通知两种类型的方法。使用方法比较简单,需要注意在调用方法前获取锁。

signal() 唤醒一个等待在Condition上的线程,该线程从等待方法返回之前必须获得与Condition相关联的锁。

6.2 Condition的实现分析

6.2.1.等待队列

6.2.2 等待

6.2.3 通知