5.Java中的锁

在Lock接口出现之前,Java程序是靠synchronized关键字实现锁功能的,而Java 1.5之后,JUC中新增了Lock接口(以及相关实现类)用来实现锁功能,它提供了与synchronized关键字类似的同步功能,只是在使用时需要显式地获取和释放锁。虽然它缺少了(通过synchronized块或方法所提供的)隐式获取释放锁的便捷性,但是却拥有了锁获取与释放的可操作性、可中断的获取锁以及超时获取锁等多种synchronized关键字所不具备的同步特性。

Lock的使用也很简单。

Lock lock = new ReentrantLock();
lock.lock();
try {
    // do sth
} finally {
    lock.unlock();
}

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

Lock接口提供的synchronized关键字所不具备的主要特性。

特性 描述
尝试非阻塞地获取锁 当前线程尝试获取锁,如果这一时刻锁没有被其他线程获取到,则成功获取并持有锁
能被中断地获取锁 与synchronized不同,获取到锁的线程能够响应中断,当获取到锁的线程被中断时,中断异常将会被抛出,同时锁会被释放
超时获取锁 在指定的截止时间之前获取锁,如果截止时间到了仍旧无法获取锁,则返回。

Lock是一个接口,它定义了锁获取与释放的基本操作。

方法名称 描述
void lock() 获取锁,调用该方法当前线程将会获取锁,当锁获得后,从该方法返回
void lockInterruptibly() throws InterruptedException 可中断地获取锁,和lock()方法的不同之处在于该方法会响应中断,即在锁的获取中可以中断当前线程
boolean tryLock() 尝试非阻塞的获取锁,调用该方法后立即返回,如果能够获取则返回true,否则返回false
boolean tryLock(long time,TimeUnit unit) throws InterruptedException 超时的获取锁,当前线程在以下3种情况下返回:1.当前线程在超时时间内获取了锁 2.当前线程在超时时间内被中断 3.超时时间结束,返回false
void unlock() 释放锁
Condition neCondition() 获取等待通知组件,该组件与当前的锁绑定,当前线程只有获取了锁,才能调用该组件的wait()方法,而调用后,当前线程将释放锁

AQS是用来构建锁或者其他同步组件的基础框架,它使用了一个int成员变量表示同步状态,通过内置的FIFO队列来完成资源获取线程的排队工作。

AQS的主要使用方式是继承,子类通过继承AQS并实现它的抽象方法来管理同步状态,在抽象方法的实现过程中免不了要对同步状态进行更改,这时就需要使用AQS提供的3个方法getState()setState(newState)compareAndSetState(int expect, int update)来进行操作,因为它们能够保证状态的改变是安全的。

子类被推荐定义为自定义同步组件的静态内部类,AQS自身没有实现任何同步接口,它仅仅定义了若干同步状态获取和释放的方法来供自定义同步组件使用,AQS既可以支持独占式地获取同步状态,也支持共享式地获取同步状态。

AQS是实现同步组件的关键,在同步组件的实现中聚合AQS,利用AQS实现锁的语义。

AQS的设计是基于模板方法模式的,也就是说,使用者需要继承AQS并重写指定的方法,随后将AQS组合在自定义同步组件的实现中,并调用AQS提供的模板方法,而这些模板方法将会调用使用者重写的方法。

重写AQS指定的方法时,需要使用AQS提供的如下3个方法来访问或修改同步状态:

  • getState():获取当前同步状态
  • setState(int newState):设置当前同步状态
  • compareAndSetState(int expect, int update):使用CAS设置当前状态,该方法能保证状态设置的原子性。

AQS可重写的方法与描述如下:

方法名称 描述
protected boolean tryAcquire(int arg) 独占式获取同步状态,实现该方法需要查询当前状态并判断同步状态十分符合预期,然后再进行CAS设置同步状态
protected boolean tryRelease(int arg) 独占式地释放同步状态,等待获取同步状态的线程将有机会获取同步状态
protected int tryAcquireShared(int arg) 共享式地获取同步状态,返回大于0的值,表示获取成功,反之,获取失败
protected boolean tryReleaseShared(int arg) 共享式释放同步状态
protected boolean isHeldExclusively() 当前同步器是否在独占模式下被线程占用,一般该方法表示是否被当前线程所独占

实现自定义同步组件时,将会调用AQS的模板方法,部分模板方法和描述如下:

方法名称 描述
void acquire(int arg) 独占式获取同步状态,如果当前线程获取同步状态成功,则由该方法返回,否则,将会进入同步队列等待,该方法将会调用重写的tryAcquire(int arg)方法
void acquireInterruptibly(int arg) 与acquire(int arg)相同,但是该方法响应中断,当前线程未获取到同步状态而进入同步队列中,如果当前线程被中断,则该方法会抛出InterruptedException并返回
boolean tryAcquireNanos(int arg,long nanos) 在acquireInterruptibly(int arg)基础上增加了超时机制,如果当前线程在超时时间内没有获取到同步状态,那么将会返回false,如果获取到了返回true
void acquireShared(int arg) 共享式的获取同步状态,如果当前线程未获取到同步状态,将会进入同步队列等待,与独占式获取的主要区别是在同一时刻可以有多个线程获取到同步状态
void acquireSharedInterruptibly(int arg) 与acquireShared(int arg)相同,该方法响应中断
boolean tryAcquireSharedNanos(int arg,long nanos) 在acquireSharedInterruptibly(int arg)基础上增加了超时限制
boolean release(int arg) 独占式的释放同步状态,该方法会在释放同步状态之后,将同步队列中第一个节点包含的线程唤醒
boolean releaseShared(int arg) 共享式的释放同步状态
Collection getQueuedThreads() 获取等待在同步队列上的线程集合

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

同步队列中的节点Node用来保存获取同步状态失败的线程引用、等待状态以及前驱和后继节点,节点的属性类型与名称以及描述如下:

属性类型和名称 描述
int waitStatus 等待状态,包含如下状态:(1)CANCELLED,值为1,由于在同步队列中等待的线程等待超时或者被中断,需要从同步队列中取消等待,节点进入该状态将不会变化(2)SIGNAL,值为-1,后继节点的线程处于等待状态,而当前节点的线程如果释放了同步状态或取消了等待,将会通知后继节点,使后继节点的线程得以运行(3)CONDITION,值为-2,节点在等待队列中,节点线程等待在Condition上,当其他线程对Condition调用了signal()方法后,该节点将会从等待队列中装移到同步队列中,加入到对同步状态的获取中(4)PROPAGATE,值为-3,表示下一次共享式同步状态获取将会无条件地被传播下去(5)INITIAL,值为0,初始状态
Node prev 前驱节点,当节点加入同步队列时被设置(尾部添加)
Node next 后继节点
Node nextWaiter 等待队列中的后继节点。如果当前节点是共享的,那么这个字段将是一个SHARED常量,也就是说节点类型(独占和共享)和等待队列中的后继节点共用同一个字段
Thread thread 获取同步状态的线程

节点是构成同步队列的基础,AQS持有首节点head和尾节点tail,没有成功获取同步状态的线程将会成为节点加入该队列的尾部。

当一个线程成功地获取了同步状态(或锁),其他线程将无法获取到同步状态,转而被构造成为节点并加入同步队列中,这个加入队列的过程必须要保证线程安全,AQS提供了一个基于CAS的设置尾节点的方法:compareAndSetTail(Node expect, Node update),它需要传递当前线程认为的尾节点和当前节点,只有设置成功后,当前节点才正式与之前的尾节点建立关联。

同步队列遵循FIFO,首节点是获取同步状态成功的节点,首节点的线程在释放同步状态时,将会唤醒后继节点,而后继节点将会在获取同步状态成功时将自己设置为首节点。设置首节点是通过获取同步状态成功的线程完成的,由于只有一个线程能够成功获取到同步状态,因此设置头结点的方法并不需要使用CAS来保证,它只需要将首节点设置成为原首节点的后继节点并断开原首节点的next引用即可。

ReadWriteLock仅定义了获取读锁和写锁的两个方法,即readLock()方法和writeLock()方法,而其实现ReentrantReadWriteLock除了接口方法之外,还提供了一些便于外界监视器内部工作状态的方法。

方法名称 描述
int getReadLockCount() 返回当前读锁被获取的次数。该次数不等于获取读锁的线程数,因为有线程重入
int getReadHoldCount() 返回当前线程获取读锁的次数。该方法在Java 6中加入到ReentrantReadWriteLock中,使用ThreadLocal保存当前线程获取的次数,这也使得Java 6的实现变得更加复杂
boolean isWriteLocked() 判断写锁是否被获取
int getWriteHoldCount() 返回当前写锁被获取的次数

读写锁同样依赖自定义同步器来实现同步功能,而读写状态就是其同步器的同步状态。在ReentrantLock中自定义同步器的同步状态表示一个锁被一个线程重复获取的次数,而读写锁的自定义同步器需要在同步状态(一个int变量)上维护多个读线程和一个写线程的状态。如果在一个int变量上维护多种状态,就一定需要“按位切割使用”这个变量,读写锁将变量切分成了两个部分,高16位表示读,低16位表示写。假设当前同步状态是S,写状态等于S&0x0000FFFF,读状态等于S>>>16(无符号补0右移16位)。当写状态增加1时,等于S+1,当读状态增加1时,等于S+(1<<16),也就是s+0x00010000。根据状态的划分能得出一个推论:s不等于0时,当写状态(s&0x0000ffff)等于0时,则读状态(s>>>16)大于0,即读锁已被获取。

如果存在读锁,则写锁不能被获取,原因在于:读写锁要确保写锁的操作对读锁可见,如果允许读锁在已被获取的情况下对写锁的获取,那么正在运行的其他读线程就无法感知到当前写线程的操作。因此只有等待其他读线程都释放了读锁,写锁才能被当前线程获取,而写锁一旦被获取,则其他读写线程的后续访问均被阻塞。

锁降级是指把持住(当前拥有的)写锁,再获取到读锁,随后释放先前拥有的写锁的过程。锁降级中读锁的获取是否必要呢?答案是必要的。主要是为了保证数据的可见性,如果当前不获取读锁而是直接释放写锁,假设此时另一个线程T获取了写锁并修改了数据,那么当前线程无法感知线程T的数据更新。如果当前线程获取读锁,即遵循锁降级的步骤,则线程T将会被阻塞,直到当前线程使用数据并释放读锁之后,线程T才能获取写锁进行数据更新。

LockSupport定义了一组以park开头的方法用来阻塞当前线程,以及unpark(Thread thread)方法来唤醒一个被阻塞的线程。

方法名称 描述
void park() 阻塞当前线程,如果调用unpark(Thread thread)方法或者当前线程被中断,才能从park()方法返回
void parkNanos(long nanos) 阻塞当前线程,最长不超过nanos纳秒,返回条件在park()的基础上增加了超时返回
void parkUntil(long deadline) 阻塞当前线程,直到deadline时间
void unpark(Thread thread) 唤醒处于阻塞状态的线程thread

Java6中,LockSupport增加了park(Object blocker)、parkNanos(Object blocker,long nanos)和parkUntil(Object blocker, long deadline)3个方法,用于实现阻塞当前线程的功能,其中参数blocker是用来标识当前线程在等待的对象,该对象主要用于问题排查和系统监控。

对比项 Object Monitor Methods Condition
前置条件 获取对象的锁 调用Lock.lock()获取锁,调用lock.newCondition()获取Condition对象
调用方式 直接调用如obj.wait() 直接调用如condition.await();
等待队列个数 一个 多个
当前线程释放锁并进入等待状态 支持 支持
当前线程释放锁并进入等待状态,在等待状态中不响应中断 不支持 支持
当前线程释放锁并进入超时等待状态 支持 支持
当前线程释放锁并进入等待状态到将来某个时刻 不支持 支持
唤醒等待队列中的一个线程 支持 支持
唤醒等待队列中的全部线程 支持 支持

Condition的部分方法以及描述

方法名称 描述
void await() throws InterruptedException 当前线程进入等待状态直到被通知signal或中断,当前线程将进入运行状态且从await()返回的情况包括:其他线程调用该condition的signal()或signalAll()方法,而当前线程被选中唤醒。其他线程调用interrupt()方法中断当前线程。如果当前等待线程从await()返回,那么表明线程已经获得了Condition对应的锁
void awaitInterruptibly() 当前线程进入等待状态直到通知,从方法名称上可以看出该方法对中断不敏感
long awaitNanos(long nanosTimeout) throws InterruptedException 当前线程进入等待状态直到被通知、中断或者超时。返回值表示剩余的时间,如果在nanosTimeout纳秒之前被唤醒,那么返回值就是(nanosTimeout - 实际耗时)。如果返回值是0或者负数,那么可以认定已经超时了
boolean awaitUntil(Date deadline) throws InterruptedException 当前线程进入等待状态直到被通知、中断或者到某个时间。如果没有到指定时间就被通知,方法返回true,否则,表示到了指定时间,方法返回false
void signal() 唤醒一个等待在Condition上的线程,该线程从等待方法返回前必须获得与Condition相关联的锁
void signalAll() 唤醒所有等待在Condition上的线程,能够从等待方法返回的线程必须获得与Condition相关联的锁

每个Condition对象都包含着一个队列(等待队列),该队列是Condition对象实现等待/通知功能的关键。等待队列是一个FIFO队列,在队列中的每个节点都包含了一个线程引用,该线程就是在Condition对象上等待的线程,如果一个线程调用了Condition.await()方法,那么该线程就会释放锁、构造成节点加入等待队列并进入等待状态。事实上,节点的定义复用了AQS中节点的定义,也就是说同步队列和等待队列中节点类型都是AQS的静态内部类AbstractQueuedSynchronizer.Node

一个Condition包含一个等待队列,Condition拥有首节点(firstWaiter)和尾节点(lastWaiter)。当前线程调用Condition.await()方法,将会以当前线程构造节点,并将节点从尾部加入等待队列。Condition拥有首尾节点的引用,而新增节点只需要将原有的尾节点nextWaiter指向它,并且更新尾节点即可。上述节点引用更新的过程并没有使用CAS保证,原因在于调用await()方法的线程必定是获取了锁的线程,也就是说该过程是由锁来保证线程安全的。

在Object的监视器模型上,一个对象拥有一个同步队列和等待队列,而JUC中的Lock(更确切的说是AQS)拥有一个同步队列和多个等待队列。

调用Condition的await()相关方法,会使当前线程进入等待队列并释放锁,同时线程状态变为等待状态。当从await()方法返回时,当前线程一定获取了Condition相关联的锁。如果从队列(同步队列和等待队列)的角度看await()方法,当调用await()方法时,相当于同步队列的首节点(获取了锁的节点)移动到Condition的等待队列中。调用await()方法的线程,也就是成功获取了锁的线程(同步队列的首节点),该方法会将当前线程构造成节点并加入等待队列中,然后释放同步状态,唤醒同步队列中的后继节点,然后当前线程会进入等待状态。当等待队列中的节点被唤醒,则唤醒节点的线程开始尝试获取同步状态。如果不是通过其他线程调用Condition.signal()方法唤醒,而是对等待队列进行中断,则会抛出InterruptedException。

调用Condition的signal()方法,将会唤醒在等待队列中等待时间最长的节点(首节点),在唤醒节点之前,会将节点移到同步队列中。被唤醒后的线程,将从await()方法中的while循环中退出(isOnSyncQueue(Node node)方法返回true,节点已经在同步队列中),进而调用AQS的acquireQueued()方法加入到获取同步状态的竞争中。成功获取同步状态(或者说锁)之后,被唤醒的线程将从先前调用await()方法返回,此时该线程已经成功地获取了锁。

Condition的signalAll()方法,相当于对等待队列中的每个节点均执行一次signal()方法,效果就是将等待队列中所有节点全部移动到同步队列中,并唤醒每个节点的线程。

results matching ""

    No results matching ""