目录
前言:
上一部分,我们一起学习了线程的基本概念,线程的状态,线程之间的通信,这一部分,我们来一起学习java中的锁
1. Lock接口
锁是用来控制多个线程访问共享资源的方式,在jdk1.5之前,java程序只能靠synchronized关键字来实现锁的功能,但早期的synchronized性能比较差,所以jdk1.5之后,并发包中增加了Lock接口和相关的实现类来实现锁的功能。Lock和synchronized不同的地方在与Lock需要显式的加锁和解锁,Lock锁可中断和可超时获取锁下面整理一下Lock接口具有的synchronized的没有的特性
特性 | 描述 |
---|---|
尝试非阻塞的获取锁 | 当前线程尝试获取锁,如果这一时刻没有被其他线程获取到,则成功获取并获取锁 |
能被中断地获取锁 | 获取到的锁可以响应中断,当获取到锁的线程被中断时,中断异常将被抛出,同时锁释放 |
超时锁获取 | 在指定的超时时间内获取锁,如果指定时间到了还没有获取到锁,直接返回 |
2. 队列同步器
队列同步器AbstractQueuedSynchronizer,简称AQS,是用来构建锁和其他同步组件的基础框架,同步器是实现锁的关键,同步器使用一个volatile的int变量state来表示同步状态,同步器是面向锁的实现着,简化了锁的实现方式。下面来看一下队列同步器的接口和示例
2.1 同步器的接口和示例
同步器的设计是基于模板方法设计模式的,使用者需要继承同步器并重写指定的方法,然后把同步器组合在自定义同步组件的实现中,调用同步器的模板方法,这些模板方法会调用使用者重写的方法。
同步器提供三个方法啊来访问或修改同步状态
setState(int newState) //设置当前同步状态
getState() //获取当前同步状态
compareAndSetState(int expect, int update)//使用CAS设置当前状态,该方法能保证状态设置的原子性
同步器可重写的方法如下表所示
方法名称 | 描述 |
---|---|
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() | 判断当前同步器是否在独占模式下被线程占用,一般该方法表示是否被当前现场占用 |
同步器提供的模板方法如下
方法名称 | 描述 |
---|---|
void acquire(int arg) | 独占式获取同步状态,如果当前线程获取同步状态成功,则返回,否则将会进入等待队列,该方法将会调用重写的tryAcquire()方法 |
void acquireInterruptibly(int arg) | 与acquire()相同,但这个方法响应中断,当前线程未获取到同步状态而进入同步队列中,如果当前线程被中断,则会抛出InterrupteedException并返回 |
boolean tryAcquireNanos(int arg, long nanosTimeout) | 在上一个方法上加了超时限制,如果当前线程在超时时间内没有获得同步状态,会返回false,如果获取到了就返回true |
boolean release(int arg) | 独占式释放同步状态,该方法会在释放同步状态后,将同步队列中第一个节点包含的线程唤醒 |
void acquireShared(int arg) | 共享式获取同步状态,如果当前线程未获得同步状态,则进入同步队列等待,同一时刻可以有多个线程获取到同步状态 |
void acquireSharedInterruptibly(int arg) | 和acquireShared()方法相同,此方法响应中断 |
boolean tryAcquireSharedNanos(int arg, long nanosTimeout) | 相比acquireShared(),增加了超时限制 |
boolean releaseShared(int arg) | 共享式释放同步状态 |
Collection<Thread> getQueuedThreads() | 获取在同步队列上等待的线程的集合 |
这些方法一共可以分为三类,独占式获取和释放同步状态,共享式获取和释放同步状态,查询同步队列中等待线程的情况。
现在我们用一个独占锁的示例来深入了解一下同步器的工作原理。独占锁,顾名思义就是在同一时刻只能有一个线程获取到锁,其他获取锁的线程将会被阻塞,进入同步队列中等待,直到获取锁的线程释放锁,后续线程才能获得这把锁。
public class Mutex implements Lock {
private static class Sync extends AbstractQueuedSynchronizer {
//是否处于占用状态
@Override
protected boolean isHeldExclusively() {
return getState() == 1;
}
//当状态为0时,获取锁
@Override
public boolean tryAcquire(int acquires) {
if (compareAndSetState(0, 1)) {
//设置独占线程为当前线程
setExclusiveOwnerThread(Thread.currentThread());
return true;
}
return false;
}
//释放锁,将同步状态置为0
@Override
protected boolean tryRelease(int release) {
if (getState() == 0) {
throw new IllegalMonitorStateException();
}
setExclusiveOwnerThread(null);
setState(0);
return true;
}
//返回一个Condition,每个condition包含一个condition队列
Condition newCondition() {
return new ConditionObject();
}
}
/**
* 接下来只要将操作代理到sync上即可
*/
private final Sync sync=new Sync();
@Override
public void lock() {
sync.acquire(1);
}
@Override
public void lockInterruptibly() throws InterruptedException {
sync.acquireInterruptibly(1);
}
@Override
public boolean tryLock() {
return sync.tryAcquire(1);
}
@Override
public boolean tryLock(long time, TimeUnit unit) throws InterruptedException {
return sync.tryAcquireNanos(1,unit.toNanos(time));
}
@Override
public void unlock() {
sync.release(1);
}
@Override
public Condition newCondition() {
return sync.newCondition();
}
}
上面代码中,Mutex是一个独占锁,它是一个自定义的同步组件,在同一时刻只能有一个线程获得锁。类中定义了一个静态内部类,继承了AQS同步器并实现了独占式获取和释放同步状态。用户在使用Mutex的时候不会直接和同步器的实现打交道,直接调用Mutex的方法即可。
2.2 同步器的实现分析
接下来我们深入了解一下同步器是如何完成线程同步的,主要探究同步队列、独占式同步状态的获取与释放,共享式同步状态的获取与释放,超时获取同步状态等同步器的核心数据结构和方法
1.同步队列
同步器中由一个同步队列 (FIFO的双向队列)来完成同步状态的管理,当前线程获取同步状态失败时,同步器会将当前线程以及等待状态等信息构造为一个Node对象并将它加入同步队列,同时会阻塞当前线程,同步状态释放时,会把首节点中的线程唤醒并再次让其获取同步状态。内部类Node的源码如下
static final class Node {
/** Marker to indicate a node is waiting in shared mode */
static final Node SHARED = new Node();
/** Marker to indicate a node is waiting in exclusive mode */
static final Node EXCLUSIVE = null;
/** waitStatus value to indicate thread has cancelled */
static final int CANCELLED = 1;
/** waitStatus value to indicate successor's thread needs unparking */
static final int SIGNAL = -1;
/** waitStatus value to indicate thread is waiting on condition */
static final int CONDITION = -2;
/**
* waitStatus value to indicate the next acquireShared should
* unconditionally propagate
*/
static final int PROPAGATE = -3;
/**
* Status field, taking on only the values:
* SIGNAL: The successor of this node is (or will soon be)
* blocked (via park), so the current node must
* unpark its successor when it releases or
* cancels. To avoid races, acquire methods must
* first indicate they need a signal,
* then retry the atomic acquire, and then,
* on failure, block.
* CANCELLED: This node is cancelled due to timeout or interrupt.
* Nodes never leave this state. In particular,
* a thread with cancelled node never again blocks.
* CONDITION: This node is currently on a condition queue.
* It will not be used as a sync queue node
* until transferred, at which time the status
* will be set to 0. (Use of this value here has
* nothing to do with the other uses of the
* field, but simplifies mechanics.)
* PROPAGATE: A releaseShared should be propagated to other
* nodes. This is set (for head node only) in
* doReleaseShared to ensure propagation
* continues, even if other operations have
* since intervened.
* 0: None of the above
*
* The values are arranged numerically to simplify use.
* Non-negative values mean that a node doesn't need to
* signal. So, most code doesn't need to check for particular
* values, just for sign.
*
* The field is initialized to 0 for normal sync nodes, and
* CONDITION for condition nodes. It is modified using CAS
* (or when possible, unconditional volatile writes).
*/
volatile int waitStatus;
/**
* Link to predecessor node that current node/thread relies on
* for checking waitStatus. Assigned during enqueuing, and nulled
* out (for sake of GC) only upon dequeuing. Also, upon
* cancellation of a predecessor, we short-circuit while
* finding a non-cancelled one, which will always exist
* because the head node is never cancelled: A node becomes
* head only as a result of successful acquire. A
* cancelled thread never succeeds in acquiring, and a thread only
* cancels itself, not any other node.
*/
volatile Node prev;
/**
* Link to the successor node that the current node/thread
* unparks upon release. Assigned during enqueuing, adjusted
* when bypassing cancelled predecessors, and nulled out (for
* sake of GC) when dequeued. The enq operation does not
* assign next field of a predecessor until after attachment,
* so seeing a null next field does not necessarily mean that
* node is at end of queue. However, if a next field appears
* to be null, we can scan prev's from the tail to
* double-check. The next field of cancelled nodes is set to
* point to the node itself instead of null, to make life
* easier for isOnSyncQueue.
*/
volatile Node next;
/**
* The thread that enqueued this node. Initialized on
* construction and nulled out after use.
*/
volatile Thread thread;
/**
* Link to next node waiting on condition, or the special
* value SHARED. Because condition queues are accessed only
* when holding in exclusive mode, we just need a simple
* linked queue to hold nodes while they are waiting on
* conditions. They are then transferred to the queue to
* re-acquire. And because conditions can only be exclusive,
* we save a field by using special value to indicate shared
* mode.
*/
Node nextWaiter;
/**
* Returns true if node is waiting in shared mode.
*/
final boolean isShared() {
return nextWaiter == SHARED;
}
/**
* Returns previous node, or throws NullPointerException if null.
* Use when predecessor cannot be null. The null check could
* be elided, but is present to help the VM.
*
* @return the predecessor of this node
*/
final Node predecessor() throws NullPointerException {
Node p = prev;
if (p == null)
throw new NullPointerException();
else
return p;
}
Node() { // Used to establish initial head or SHARED marker
}
Node(Thread thread, Node mode) { // Used by addWaiter
this.nextWaiter = mode;
this.thread = thread;
}
Node(Thread thread, int waitStatus) { // Used by Condition
this.waitStatus = waitStatus;
this.thread = thread;
}
}
下面来解释一下Node类中的属性
属性类型及名称 | 描述 |
---|---|
int waitStatus | 等待状态 有下列5中状态 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 | 获取同步状态的线程 |
Node节点是构成同步队列的基础,同步器有首节点和尾节点,没有成功获取同步状态的线程将会成为节点加入该队列的尾部,同步队列的结构图如下所示
同步器中包含了两个节点类型的引用,一个指向头节点,一个指向尾节点,当一个线程因无法获得同步状态而阻塞,然后被构造成节点要加入同步队列中,那么这个加入的过程必须要保证线程安全,所以同步器提供了一个CAS的设置尾结点的方法compareAndSetTail(Node expect,Node update),它需要两个参数分别是当前线程“认为”的尾结点和当前节点,只有设置成功后,当前节点才算真正的加入到队列中去。同步器将节点加入队列的过程如下图所示
同步队列为FIFO队列,遵循先进先出原则,首节点是获取同步状态成功的节点,首节点的线程在释放同步状态的时候会唤醒后继节点,后继节点在获取同步状态成功时会把自己设置为首节点,过程如下图所示
设置首节点是通过获取同步状态成功的过程来完成的,由于只有一个线程可以成功获得同步状态,所以设置头节点的方法不必使用CAS来保证,只需要把首节点设置为原首节点的后继节点并断开原首节点的next引用即可。
2. 独占式同步状态获取与释放
通过同步器的acquire(int arg)方法可以获取同步状态,该方法对中断不敏感,方法的源码如下
public final void acquire(int arg) {
if (!tryAcquire(arg) &&
acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
selfInterrupt();
}
上述代码主要完成了同步状态获取、节点构造,加入同步队列以及自旋等操作,代码首先调用tryAcquired来保证线程安全的获取同步状态,如果获取同步状态失败,则构造同步节点(独占式Node,EXCLUSIVE),并且通过addWaiter()方法将该节点加入到同步队列的尾部,最后调用acquireQueued()方法,使得该节点以“死循环”自旋的方式获取同步状态。如果获取不到则阻塞节点中的线程,而被阻塞线程的唤醒依靠前驱节点的出队或阻塞线程被中断来实现。下面来分析一下这些方法,首先是节点的构造以及加入同步队列
private Node addWaiter(Node mode) {
Node node = new Node(Thread.currentThread(), mode);
// Try the fast path of enq; backup to full enq on failure
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 = tail;
if (t == null) { // Must initialize
if (compareAndSetHead(new Node()))
tail = head;
} else {
node.prev = t;
if (compareAndSetTail(t, node)) {
t.next = node;
return t;
}
}
}
}
上面的代码使用compareAndSetTail()方法来确保节点能被线程安全添加,compareAndSetTail()底层是一个CAS的原子方法。而enq方法中,同步器通过“死循环”来保证节点的正确添加,在“死循环”中只有通过CAS将节点设置成尾节点后,才能返回,否则,当前线程将不断的进行尝试,enq方法将并发添加节点的请求通过CAS变得串行化了。
在下一个acquireQueued这个方法中,节点一旦进入了同步队列,就开始了自旋的过程,每个线程都在自己观察,当条件满足时,获取到了同步状态,就可以退出自旋,代码如下
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);
}
}
独占式同步状态获取流程如下
上图中,前驱节点为头节点且能获取同步状态的判断条件和线程进入等待状态是获取同步状态的自旋过程。
当前线程获取到同步状态且执行了相应的逻辑后,就要释放同步状态,使后续节点可以继续获取同步状态。通过调用release()方法可以释放同步状态,该方法在释放了同步状态后,会唤醒后继节点。方法源码如下
public final boolean release(int arg) {
if (tryRelease(arg)) {
Node h = head;
if (h != null && h.waitStatus != 0)
unparkSuccessor(h);
return true;
}
return false;
}
该方法执行时,会唤醒头节点的后继节点线程。
3. 共享式获取同步状态与释放
共享式和独占式获取的区别就在于是否能有多个线程同时获取到同步状态,调用同步器的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; // help GC
if (interrupted)
selfInterrupt();
failed = false;
return;//r大于0且当前节点的前驱是头节点才能返回
}
}
if (shouldParkAfterFailedAcquire(p, node) &&
parkAndCheckInterrupt())
interrupted = true;
}
} finally {
if (failed)
cancelAcquire(node);
}
}
可以看到,同步器用tryAcquireShared()方法来尝试获取同步状态,当返回值大于0时,表示获取到同步状态,所以,共享式获取同步状态自旋的退出条件也是tryAcquireShared()的返回值大于0,而且从doAcquireShared()方法中可以看到,要想从自旋中退出还要保证当前节点的前驱节点为头结点。
同样的,共享式的获取也需要释放同步状态,还是来看源码
public final boolean releaseShared(int arg) {
if (tryReleaseShared(arg)) {
doReleaseShared();
return true;
}
return false;
}
private void doReleaseShared() {
/*
* Ensure that a release propagates, even if there are other
* in-progress acquires/releases. This proceeds in the usual
* way of trying to unparkSuccessor of head if it needs
* signal. But if it does not, status is set to PROPAGATE to
* ensure that upon release, propagation continues.
* Additionally, we must loop in case a new node is added
* while we are doing this. Also, unlike other uses of
* unparkSuccessor, we need to know if CAS to reset status
* fails, if so rechecking.
*/
for (;;) {
Node h = head;
if (h != null && h != tail) {
int ws = h.waitStatus;
if (ws == Node.SIGNAL) {
if (!compareAndSetWaitStatus(h, Node.SIGNAL, 0)) //CAS
continue; // loop to recheck cases
unparkSuccessor(h);
}
else if (ws == 0 &&
!compareAndSetWaitStatus(h, 0, Node.PROPAGATE))
continue; // loop on failed CAS
}
if (h == head) // loop if head changed
break;
}
}
它和独占式释放方法的区别在于共享式的释放必须确保线程安全,所以一般是通过CAS来保证的
4. 独占式超时获取同步状态
同步器调用doAcquireNanos()方法来在指定的时间内获取同步状态,如果获取到返回true,获取失败则返回false,此方法具有响应中断的特性,我们知道在jdk1.5以前,当一个线程获取不到锁被阻塞在synchronized之外时,对该线程进行中断操作,此时该线程的中断标志位会被修改,但是线程依旧阻塞等待获取锁。在同步器中,acquireInterruptibly()这个方法如果在等待获取同步状态是被中断,会立刻付返回,并抛出InterruptedException异常。doAcquireNanos()这个方法可以看做acquireInterruptibly()方法的增加版,也就是增加了超时获取的特性,下面来一起看源码
private boolean doAcquireNanos(int arg, long nanosTimeout)
throws InterruptedException {
if (nanosTimeout <= 0L)
return false;
final long deadline = System.nanoTime() + nanosTimeout;
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;
}
nanosTimeout = deadline - System.nanoTime();
if (nanosTimeout <= 0L)
return false;
if (shouldParkAfterFailedAcquire(p, node) &&
nanosTimeout > spinForTimeoutThreshold)
LockSupport.parkNanos(this, nanosTimeout);
if (Thread.interrupted())
throw new InterruptedException();
}
} finally {
if (failed)
cancelAcquire(node);
}
}
独占式超时获取同步状态流程图如下
总结
这一部分,我们一起学习了同步器的概念和用法,分析了相关源码,下一部分我们来一起学习可重入锁和读写锁。