JUC读书笔记(三)


本章将介绍Java并发包中与dang锁相关的API和组件,以及这些API和组件的使用方式和实现细节。内容只要围绕两个方面:

  • 使用,通过示例演示这些组件的使用方法以及详细介绍与锁相关的API;
  • 实现,通过分析源码来剖析实现细节,因为理解实现的细节方能更加得心应手且正确地使用这些组件。

Lock接口

Lock的使用方式:

Lock lock = new ReentranLock();
lock.lock();
try {
} finally {
    lock.unlock();
}

在finally块中释放锁,目的是保证获取到锁之后,最终能够被释放。
不要将获取锁的过程写在try块中,因为如果在获取锁(自定义锁的实现)时发生了异常,异常抛出的同时,也会导致锁无故释放。
Lock接口提供的synchronized关键字不具备的主要特性如表1-1所示。

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

Lock是一个接口,它定义了锁获取和释放的基本操作,Lock的API如表1-2所示。

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

Lock接口的实现基本都是通过聚合了一个同步器的子类来完成线程访问控制的。

队列同步器(AbstractQueuedSynchronizer)

  • 队列同步器是用来构建锁或者其他同步组件的基础框架,它使用了一个int成员变量表示同步状态,通过内置的FIFO队列来完成资源获取线程的派对工作。
  • 同步器的主要使用方式是继承,子类通过继承同步器并实现它的抽象方法来管理同步状态,在抽象方法的实现过程中免不了要对同步状态进行修改,这时就需要使用同步器提供的3个方法(getState()、setState(int newState)、和compareAndSetState(int expect, int update))来进行操作,因为它们能够保证状态的改变是安全的。
  • 子类被推荐定义为自定义同步组件的静态内部类,同步器自身没有实现任何同步接口,它仅仅是定义了若干通状态获取和释放的方法来供自定义同步组件使用,同步器既可以支持独占式地获取同步状态,也可以支持共享式地获取同步状态,这样就可以方便实现不同类型的同步组件(ReentranLock、ReentranReadWriteLock和CountDownLatch等)

锁是面向使用者的,它定义了使用者与锁交互的接口(比如可以允许两个线程并行访问),隐藏了实现细节;同步器面向的是锁的实现者,它简化了锁的实现方式,屏蔽了同步状态管理、线程的排队、等待与唤醒等底层操作。锁和同步器很好地隔离了使用者和实现者所需关注的领域。

代码同步器的接口和示例

同步器的设计是基于模板方法模式的,也就是说,使用者需要继承同步器并重写指定的方法,随后将同步器组合在自定义组件的实现中,并调用同步器提供的模板方法,而这些模板方法将会调用使用者重写的方法。
重写同步器指定的方法时,需要使用同步器提供的如下3个方法来访问或修改同步状态。

  • getState():获取当前状态
  • setState(int newState):设置当前同步状态
  • compareAndSetState(int expect, int update):使用CAS设置当前状态,该方法能够保证状态设置的原子性。
    同步器可重写的方法与描述如表1-3所示:
方法名称描述
protected boolean tryAcquire(int arg)独占获取同步状态,实现该方法需要查询当前状态并判断同步状态是否符合预期,然后再进行CAS设置同步状态
protected boolean tryRelease(int arg)独占式释放同步状态,等待获取同步状态的线程将有机会获取同步状态
protected int tryAcquireShared(int arg)共享式获取同步状态,返回大于等于0的值,表示获取成功,反之,获取失败
protected boolean tryRealeaseShared(int arg)共享式释放同步状态
protected boolean isHeldExclusively()当前同步器是否在独占模式下呗线程占用,一般该方法表示是否被当前线程所独占

实现自定义同步组件时,将会调用同步器提供的模板方法,这些(部分)模板方法与描述如表

方法名称描述
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 acqureShared(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()获取等待在同步队列上的线程集合

同步器提供的模板方法基本分为三类:

  • 独占式获取与释放同步状态
  • 共享式获取与释放同步状态
  • 查询同步队列中的等待线程情况

队列同步器的实现分析

同步队列

同步器依赖内部的同步队列(一个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获取同步状态的线程

节点是构成同步队列(等待队列)的基础,同步器拥有首节点(head)和尾节点(tail),没有成功获取同步状态的线程将会成为节点加入该队列的尾部。
同步队列遵循FIFO,首节点是获取同步状态成功的节点,首节点的线程在释放同步状态时,将会唤醒后继节点,而后继节点将会在获取同步状态成功时将自己设置为首节点。

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

通过调用同步器的acquire(int arg)方法可以获取同步状态,该方法对中断不敏感,也就是由于线程获取同步状态失败后进入同步队列中,后续对线程进行中断操作时,线程不会从同步队列中移出,该方法代码如代码清单5-3所示。

public final void acquire(int arg){
	if(!tryAcquire(arg) && acquireQueued(addWaiter(Node.EXCLUSIVE), arg)){
		selfInterrupt();
}

上述代码主要完成了同步状态获取、节点构造、加入同步队列以及在同步队列中自旋等待的相关工作,其主要逻辑是:首先调用自定义同步器实现的tryAcquire(int arg)方法,该方法保证线程安全的获取同步状态,如果同步状态获取失败,则构造同步节点(独占式Node.EXCLUSIVE,同一时刻只能有一个线程成功获取同步状态)并通过addWaiter(Node node)方法将该节点加入到同步队列的尾部,最后调用acquireQueued(Node node, int arg)方法,使得该节点以“死循环”的方式获取同步状态。如果获取不到则阻塞节点中的线程,而被阻塞线程的唤醒主要依靠前驱节点的出队或阻塞线程被中断来实现。
节点的构造以及加入同步队列如代码清单所示:

private Node addWaiter(Node node){
	Node node = new Node(Thread.currentThread(), mode);
	// 快速尝试在尾部添加
	Node pred = tail;
	if (pred != null){
		node.prev = pred;
		if(compareAndSetTail(pred, node)){
			pred.next = node;
			return node;
		}
	}
	enq(node);
	return node;
}
private Node enq(final Node node){
	for(;;){
		Node t = fail;
		if(t == null) {
			if(compareAndSetHead(new Node()))
				tail = head;
		} else {
			node.prev = t;
			if(compareAndSetTail(t, node)){
				t.next = node;
				return t;
			}
		}
	}
}

上述代码通过使用compareAndSetTail(Node expect, Node update)方法来确保节点能够被线程安全添加。
在enq(fina Node node)方法中,同步器通过“死循环”来保证节点的正确添加,在“死循环”中只有通过CAS将节点设置成尾节点之后,当前线程才能从该方法返回,否则,当前线程不断地尝试设置。enq(fina Node node)方法将并发添加节点的请求通过CAS变得“串行化”了。
节点进入同步队列后,就进入了一个自旋的过程,每个节点(或者说每个线程)都在自省地观察,当条件满足获取到了同步状态,就可以从这个自旋过程中退出,否则依旧停留在这个自旋过程中(并会阻塞节点的进程)。自旋过程如代码清单所示:

final boolean acquireQueued(final Node node, int arg){
	boolean failed = true;
	try{
		boolean interrupted = false;
		for(;;){
			final Node p = node.predecessor();
			if(p == head && tryAcquire(arg)){
				setHead(node);
				p.next = null; //help GC
				failed = false;
				return interrupted;
			}
			if(shouldParkAfterFailedAcquire(p, node) && parkAndCheckInterrupt())
				interrupted = true;
		}
	} finally {
		if(failed)
			cancelAcquire(node);
	}
}

在acquireQueued(final Node node, int arg)方法中,当前线程在“死循环”中尝试获取同步状态,而只有前驱节点是节点才能够尝试获取同步状态,这是为什么?原因有两个:

  • 头节点是成功获取到同步状态的节点,而头节点的线程释放了同步状态之后,将会唤醒其后继节点,后继节点的线程被唤醒之后需要检查自己的前驱节点是否是头节点。
  • 维护同步队列的FIFO原则。

通过调用同步器的release(int arg)方法可以释放同步状态,该方法在释放了同步状态之后,会唤醒其后继节点(进而使后继节点重新尝试获取同步状态)。该方法代码如代码清单所示:

public final boolean release(int arg){
	if(tryRelease(arg)){
		Node h = head;
		if(h != null && h.waitStatus != 0)
			unparkSuccessor(h);
		return true;
	}
	return false;
}

unparkSuccessor(Node node)方法使用LockSupport来唤醒处于等待状态的线程。

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

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

共享式获取与独占式获取最主要的区别在于同一时刻能否有多个线程同时获取到同步状态。以文件的读写为例,如果一个程序在对文件进行读操作,那么这一时刻对于该文件的写操作均被阻塞,而读操作能够同时进行。写操作要求对资源的独占式访问,而读操作可以是共享式访问。
即共享式访问资源时,其他共享式的访问都被允许,只有独占式访问被阻塞,当独占式访问资源时,同一时刻其他访问均被阻塞。
acquireShared(int arg)代码如代码清单所示:

public final void acquireShared(int arg){
	if(tryAcquireShared(arg) < 0)
		doAcquireShared(arg);
}
private void doAcquireShared(int arg){
	final Node node = addWaiter(Node.SHARED);
	boolean failed = true;
	try{
		boolean interrupted = false;
		for(;;){
			final Node p = node.predecessor();
			if(p == head){
				int r = tryAcquireShared(arg);
				if(r >= 0){
					setHeadAndPropagate(node, r);
					p.next = null;
					if(interrupted)
						selfInterrupt();
					failed = false;
					return;
				}
			}
			if(shouldParkAfterFailedAcquire(p, node) && parkAndCheckInterrupt())
				interrupted = true;
		}
	} finally{
		if(failed)
			cacelAcquire(node);
	}
}

在acquireShared(int arg)方法中,同步器调用tryAcquireShared(int arg)方法尝试获取同步状态,truAcquireShared(int arg)方法返回值为int类型,当返回值大于等于0时,表示能够获取到同步状态。因此,在共享式获取的自旋过程中,成功获取到同步状态并退出自旋的条件就是tryAcquireShared(int arg)方法返回值大于等于0。可以看到,在doAcquireShared(int arg)方法的自旋过程中,如果当前节点的前驱为头节点时,尝试获取同步状态,如果返回值大于等于0,表示该次获取同步状态成功并从自旋过程中退出。
共享式释放同步状态调用releaseShared(int arg)方法,如代码清单所示:

public final boolean releaseShared(int arg){
	if(tryReleased(arg)){
		doReleaseShared();
		return true;
	}
	return false;
}

该方法在释放同步状态之后,将会唤醒后续处于等待状态的节点。对于能够支持多个线程同时访问的并发组件(比如Semaphore),它和独占式主要区别在于tryReleaseShared(int arg)方法必须确保同步状态(或者资源数)线程安全释放,一般是通过循环和CAS来保证的,因为释放同步状态的操作同时来自多个线程。

独占式超时获取同步状态

通过调用同步器的doAcquireNanos(int arg,long nanosTimeout)方法可以超时获取同步状态,即在指定时间段内获取同步状态,如果获取到同步状态则返回true,否则返回false。该方法提供了传统Java同步操作所不具备的特性。
doAcquireNanos的代码如代码清单所示:

private boolean adAcquireNanos(int arg, long nanosTimeout) throws InterruptedException{
	long lastTime = System.nanoTime();
	final Node node = addWaiter(Node.EXCLUSIVE);
	boolean failed = true;
	try{
		for(;;){
			final Node p = node.predecessor();
			if(p == head && tryAcquire(arg)){
				setHead(node);
				p.next = null;// help GC
				failed = false;
				return true;
			}
			if(nanosTimeout <= 0)
				return false;
			if(shouldParkAfterFailedAcquire(p,node) && nanosTimeout > spinForTimeoutThreshold)
				LockSupport.parkNanos(this, nanosTimeout);
			long now = System.nanoTime();
			// 计算时间,当前时间now减去睡眠之前的时间lastTime得到已经睡眠的时间data,然后被原有时间nanosTimeout减去,得到了还应该睡眠的时间
			nanosTimeout -= now - lastTime;
			lastTime = now;
			if(Thread.interrupted())
				throw new InterruptedException();
		}
	} finally{
		if(failed)
			cancelAcquire(node);
	}
}

该方法在自旋过程中,当节点的前驱节点为头节点时尝试获取同步状态,如果获取成功则从方法返回,这个过程和独占式获取的过程类似,但是在同步状态获取失败的处理上有所不同。如果当前线程获取同步状态失败,则判断是否超时(nanoTimeout小于等于0表示已经超时),如果没有超时,重新计算超时间隔nanosTimeout,然后使当前线程等待nanosTimeout纳秒(当已到设置的超时时间,该线程会从LockSupoort.parkNanos(Object blocker, long nanos)方法返回)。
如果nanosTimeout小于等于spinForTimeoutThreshold(1000纳秒)时,将不会使该线程进行超时等待,而是进入快速的自旋过程。原因在于,非常短的超时等待无法做到十分精确,如果这时再进行超时等待,相反会让nanosTimeout的超时从整体上表现的反而不精确。因为,在超时非常短的场景下,同步器会进入无条件地快速自旋。

重入锁

重入锁ReentrantLock,顾名思义,就是支持重进入的锁,它表示该锁能够支持一个线程对资源的重复加锁。除此之外,该锁的还支持获取锁时的公平和非公平性选择。

实现重进入

重进入是指任意线程在获取到锁之后能够再次获取该锁而不会被锁所阻塞,该特性的实现需要解决以下两个问题。

  1. 线程再次获取锁。锁需要去识别获取锁的线程是否为当前占据锁的线程,如果是,则再次成功获取。
  2. 锁的最终释放。线程重复n次获取了锁,随后在第n次释放该锁后,其他线程能够获取到该锁。锁的最终释放要求锁对于获取进行增数自增,计数表示当前锁被重复获取的次数,当锁被释放时,计数自减,当计数等于0时表示锁已经成功释放。

ReentrantLock是通过组合自定义同步器来实现锁的获取与释放,以非公平性(默认的)实现为例,获取同步状态的代码如代码清单所示:

final boolean nonfairTryacquire(int acquires){
	final Thread current = Thread.currentThread();
	int c = getState();
	if(c == 0){
		if(compareAndSetState(0, acquires)){
			setExclusiveOwnerThread(current);
			return true;
		}
	} else if (current == getExclusiveOwnerThread()){
		int nextc = c + acquires;
		if(nextc < 0)
			throw new Error("Maximum lock count exceeded");
		setState(nextc);
		return true;
	}
	return false;
}

该方法增加了再次获取同步状态的处理逻辑:通过判断当前线程是否为获取锁的线程来决定获取操作是否成功,如果是获取锁的线程再次请求,则将同步状态值进行增加并返回true,表示获取同步状态成功。
成功获取锁的现场再次获取锁,只是增加了同步状态值,这也就要求ReentrantLock在释放同步状态时减少同步状态值,该方法的代码如代码清单所示:

protected final boolean tryRelease(int releases){
	int c = getState() - release;
	if(Thread.currentThread() != getExclusiveOwnerThread())
		throw new IllegalMonitorStateException();
	boolean free = false;
	if (c == 0) {
		free = true;
		setExclusiveOwnerThread(null);
	}
	setState(c);
	return free;
}	

如果该锁被获取了n次,那么前(n-1)次tryRelease(int releases)方法必须返回false,而只有同步状态完全释放了,才能返回true。可以看到,该方法将同步状态是否为0作为最终释放的条件,当同步状态为0时,将占有线程设置为null,并返回true,表示释放成功。

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

公平锁的获取顺序遵循FIFO原则。对于非公平锁,只要CAS设置同步状态成功,则表示当前线程获取了锁,而公平锁则不同,如代码清单所示:

protected final boolean tryAcquire(int acquires){
	final Thread current = Thread.currentThread();
	int c = getState();
	if(c == 0){
		if(!hasQueuedPredecessors() && compareAndSetState(0, acquires)){
			setExclusiveOwnerThread(current);
			return true;
		}
	} else if(current == getExclusiveOwnerThread()){
		int nextc = c + acquires;
		if(nextc < 0)
			throw new Error("Maximum lock count exceeded");
		setState(nextc);
		return true;
	}
	return false;
}

该方法与nofairTryAcquire(int acquires)比较,唯一不同的位置为判断条件多了hasQueuedPredecessors()方法,即加入了同步队列中当前节点是否有前驱节点的判断,如果该方法返回true,则表示有线程比当前线程更早地请求获取锁,因此需要等待前驱线程获取并释放锁之后才能继续获取锁。

公平性锁保证了锁的获取按照FIFO原则,而代价是进行大量的线程切换。非公平性锁虽然可能造成线程“饥饿”,但极少的线程切换,保证了其更大的吞吐量。

读写锁

读写锁在同一时刻可以允许多个读线程访问,但是在写线程访问时,所有的读线程和其他写线程均被阻塞。读写锁维护了一对锁,一个读锁和一个写锁,通过分离读锁和写锁,使得并发性相比一般的排他锁有了很大提升。
ReentrantReadWriteLock提供的特性如表所示:

特性说明
公平性选择支持非公平(默认)和公平的锁获取方式,吞吐量还是非公平优于公平
重进入该锁支持重进入,以读写线程为例:读线程在获取了读锁之后,能够再次获取读锁。而写线程在获取了写锁之后能够再次获取写锁,同时也可以获取读锁
锁降级遵循获取写锁、获取读锁再释放写锁的次序,写锁能够降级成为读锁

读写锁的接口

方法名称描述
int getReadLockCount()返回当前读锁被获取的次数。该次数不等于获取读锁的线程数,例如,仅一个线程,它连续获取(重进入)了n次读锁,那么占据读锁的线程数是1,但该方法返回n
int getReadHoldCount()返回当前线程获取读锁的次数。该方法在Java6中加入到ReentrantReadWriteLock中,使用ThreadLocal保存当前线程获取的次数,这也使得Java6的实现变得更加复杂
boolean isWriteLocked()判断写锁是否被获取
int getWriteHoldCount()返回当前写锁被获取的次数

读写锁的实现分析

接下来分析ReentrantReadWriteLock的实现,主要包括:读写状态的设计、写锁的获取与释放、读锁的获取与释放以及锁降级(以下没有特别说明读写锁均可认为是ReentrantReadWriteLock)。

读写状态的设计

读写锁同样依赖自定义同步器来实现同步功能,而读写状态就是其同步器的同步状态。ReentrantLock中自定义同步器的实现,同步状态表示锁被一个线程重复获取的次数,而读写锁的自定义同步器需要在同步状态(一个整型变量)上维护多个读线程和一个写线程的状态,使得该状态的设计成为读写锁实现的关键。

如果在一个整型变量上维护多种状态,就一定需要“按位切割使用”这个变量,读写锁将变量切分成了两个部分,高16位表示读,低16位表示写。通过位运算可以迅速确定读和写各自的状态:假设当前同步状态为S,写状态等于:S & 0x0000FFFF(将高16位全部抹去),读状态等于S>>>16(无符号补0右移16位)。当写状态增加1时,等于S+1,当读状态增加1时,等于S+(1<<16),也就是S+0x00010000。
状态的划分可以得出一个结论:S不等于0时,当写状态等于0(S & 0x0000FFFF)等于0时,则读状态(S>>>16)大于0,即读锁已经被获取。

写锁的获取与释放

写锁是一个支持重进入的排它锁。如果当前线程已经获取了写锁,则增加写状态。如果当前线程在获取锁时,读锁已经被获取(读状态不为0)或者该线程不是已经获取写锁的线程,则当前线程进入等待状态。获取写锁的代码如代码清单所示:

protected final boolean tryAcquire(int acquires){
	Thread current = Thread.currentThread();
	int c = getState();
	int w = exclusiveCount(c);
	if(c != 0) {
		//存在读锁或者当前获取线程不是已经获取写锁的线程
		if(w == 0 || current != getExclusiveOwnerThread())
			return false;
		if(w + exclusiveCount(acquires) > MAX_COUNT)
			throw new Error("Maximum lock count exceeded");
		setState(c + acquires);
		return true;
	}
	if(writeShouldBlock() || !compareAndSetState(c, c + acquires)){
		return false;
	}
	setExclusiveOwnerThread(current);
	return true;
}

该方法除了重入条件(当前线程为获取了写锁的线程)之外,增加了一个读锁是否存在的判断。如果存在读锁,则写锁不能被获取,原因在于:读写锁要确保写锁的操作对读锁是否可见,如果允许读锁在已被获取的情况下对写锁的获取,那么正在运行的其他读线程就无法感知到当前写线程的操作。因此,只有等待其他读线程都释放了读锁,写锁才能被当前线程获取,而当前线程一旦获取,则其他读写线程的后续访问均被阻塞。写锁的释放与ReentrantLock的释放过程基本类似,每次释放均减少写状态,当写状态为0时表示写锁已被释放,从而等待的读写线程能够访问读写锁,同时前次写线程对后续读写线程可见。

读锁的获取与释放

ReentrantReadWitreLock的tryAcquireShared实现:

protected final int tryAcquireShared(int unused){
	for(;;){
		int c = getState();
		int nextc = c + (1 << 16);
		if(nextc < c)
			throw new Error("Maximum lock count exceeded");
		if(exclusiveCount(c) != 0 && owner != Thread.currentThread())
			return -1;
		if(compareAndSetState(c, nextc))
			return 1;
	}
}

在tryAcquireShared(int unused)方法中,如果其他线程已经获取了写锁,则当前线程获取读锁失败,进入等待状态。如果当前线程获取了写锁或者写锁为被获取,则当前线程(线程安全,依靠CAS保证)增加读状态,成功获取读锁。
读锁的每次释放(线程安全的,可能有多个读线程同时释放读锁)均减少读状态,减少的值是(1<<16)。

锁降级

锁降级指的是写锁降级成为读锁。如果当前线程拥有写锁,然后将其释放,最后再获取读锁,这种分段完成的过程不能称之为锁降级。锁降级是指把持住(当前拥有的)写锁,再获取到读锁,随后释放(当前拥有的)写锁的过程。
因为数据不常变化,所以多个线程可以并发地进行数据处理,当数据变更后,如果当前线程感知到数据变化,则进行数据的准备工作,同时其他处理现场均被阻塞,知道当前线程完成数据的准备工作,如代码清单所示:

public void processData(){
	readLock.lock();
	if(!update){
		//必须先释放读锁
		readLock.unlock();
		//锁降级从写锁获取到开始
		writeLock.lock();
		try{
			if(!update){
				//准备数据的流程(略)
				update = true;
			}
			readLock.lock();
		} finally{
			writeLock.unlock();
		}
		//锁降级完成,写锁降级为读锁
	}
	try{
		//使用数据的流程(略)
	} finally {
		readLock.unlock();
	}
}

上述示例中,当数据发生变更后,update变量(布尔类型且volatile修饰)被设置为false,此时所有访问processData()方法的线程都能感知到变化,但只有一个线程能够获取到写锁,其他线程会被阻塞在读锁和写锁的lock()方法上。当前线程获写锁完成数据准备之后,再获取读锁,随后释放写锁,完成锁降级。
锁降级为了保证数据的可见性,必须先获取读锁再释放写锁,否则另外一个线程获取了写锁并修改了数据,当前线程就无法感知到数据更新。如果当前线程获取读锁,遵循了锁降级的步骤,试图获取写锁的线程就会被阻塞,直到当前线程使用数据并释放读锁之后,其他线程才能获取写锁进行数据更新。
ReentrantReadWriteLock不支持锁升级(把持读锁、获取写锁,最后释放读锁的操作)。目的也是为了保证数据可见性,如果读锁已被多个线程获取,其中任意线程成功获取了写锁并更新了数据,则其更新对其他获取到读锁的线程是不可见的。

LockSupport工具

LockSupport提供的阻塞和唤醒方法

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

Condition接口

Object的监视器方法与Condition接口的对比

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

Condition接口与示例

Condition定义了等待、通知两种类型的方法,当前线程调用这些方法时,需要提前获取到Condition对象关联的锁。Condition是由Lock对象(调用Lock对象的newCondition()方法)创建出来的,换句话说,Condition是依赖Lock对象的。
使用方式如代码清单所示:

Lock lock = new ReentrantLock();
Condition condition = lcok.newCondition();

public void conditionWait() throws InterruptedException{
	lock.lock();
	try{
		condition.await();
	} finally{
		lock.unlock();
	}
}

public void conditionSignal() throws InterruptedException{
	lock.lock();
	try{
		condition.signal();
	} finally{
		lock.unlock();
	}
}

一般都会将Condition对象作为成员变量。当调用await()方法后,当前线程会释放锁并在此等待,而其他线程调用Condition对象的signal()方法,通知当前线程后,当前线程才从await()方法返回,并且在返回前已经获取了锁。
Condition定义的(部分)方法以及描述如表5-13所示。

方法名称描述
void await() throws InterruptedException当前线程进入等待状态直到被通知(signal)或中断,当前线程将进入运行状态并从await()返回的情况,包括:1、其他调用该Condition的signal()或signalAll()方法,当前线程被选中唤醒;2、其他线程(调用interrupt()方法)中断当前线程;3、如果当前等待线程从await()方法返回,那么表明该线程已经获取了Condition对象所对应的锁
void awaitUninterruptibly()当前线程进入等待状态直到被通知,从方法名称上可以看出该方法对中断不敏感
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必须通过Lock的newCondition()方法。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值