JAVA-并发编程(二)
sschrodinger
2019/05/14
引用
《Java 并发编程的艺术》 方腾飞,魏鹏,程晓明 著
JAVA SE version-1.8 源码
理解 Condition 和 条件变量 by -PFF
Lock 接口
锁是用来控制多个线程访问共享资源的方式,一般来说,一个锁能够防止多个线程同时访问共享资源。在 Java 中,有两种方式获得锁,一种是使用 synchronized 关键字,该关键字会隐式地获取锁,更加灵活地另外一种方式是使用 Lock 接口,实现了比 synchronized 更加灵活地特性,比如说超时等特性,Lock 接口提供地 API 如下表:
方法名称 | 描述 |
---|---|
void lock() | 获取锁,调用该方法当前线程会获取锁,当获取锁之后,从该方法返回 |
void lockInterruptibly() throws InterruptException | 可中断地获取锁,和 lock 方法不同的是该方法会响应中断,即在锁的获取过程中可以中断该线程 |
boolean tryLock() | ... |
boolean tryLock(long time, TimeUnit unit) throws InterruptException | ... |
void unlock() | ... |
Condition newCondition() | 获取条件变量 |
需要注意的是,Lock 接口的 lock 方法对中断不敏感(同 synchronized 关键字),如果一个线程在等待锁的过程中被中断,仍然会继续等待锁,如下所示:
public class Demo {
static class MyRunnable implements Runnable {
@Override
public void run() {
System.out.println("waiting for thread lock");
// 2. 线程尝试获得锁
lock.lock();
System.out.println("get lock");
lock.unlock();
}
}
public static void main(String[] args) {
Thread thread = new Thread(new MyRunnable());
// 1. 主线程上锁
lock.lock();
thread.start();
// 3. 主线程对新线程进行中断
thread.interrupt();
}
private static Lock lock = new ReentrantLock();
// output:
// waiting for thread lock
}
如果需要用线程敏感的加锁方式,需要用如下的代码:
public class Demo {
static class MyRunnable implements Runnable {
@Override
public void run() {
System.out.println("waiting for thread lock");
try {
lock.lockInterruptibly();
System.out.println("get lock");
lock.unlock();
} catch (InterruptedException e) {
System.out.println("thread interrupted by others");
}
}
}
public static void main(String[] args) {
Thread thread = new Thread(new MyRunnable());
lock.lock();
thread.start();
thread.interrupt();
// lock.unlock();
}
private static Lock lock = new ReentrantLock();
}
条件变量
Lock 接口可以返回一个新的条件变量,条件变量是用来等待而不是用来上锁的。条件变量用来自动阻塞一个线程,直到某特殊情况发生为止。通常条件变量和互斥锁同时使用。
条件变量接口的定义如下:
public interface Condition {
/*
* 使当前线程释放锁,并进行等待
* 只有 4 种情况可以唤醒该线程:
* 1. 其他线程调用该条件变量的 signal 方法,而当前线程正好被选择需要唤醒
* 2. 其他线程调用该条件变量的 signalAll 方法
* 3. 该线程被中断
* 4. 虚假唤醒
*/
void await() throws InterruptedException;
/*
* 使当前线程释放锁,并进行等待
* 只有 4 种情况可以唤醒该线程:
* 1. 其他线程调用该条件变量的 signal 方法,而当前线程正好被选择需要唤醒
* 2. 其他线程调用该条件变量的 signalAll 方法
* 3. 虚假唤醒
*/
void awaitUninterruptibly();
/*
* 使当前线程释放锁,并进行等待
* 只有 4 种情况可以唤醒该线程:
* 1. 其他线程调用该条件变量的 signal 方法,而当前线程正好被选择需要唤醒
* 2. 其他线程调用该条件变量的 signalAll 方法
* 3. 该线程被中断
* 4. 虚假唤醒
*/
long awaitNanos(long nanosTimeout) throws InterruptedException;
boolean await(long time, TimeUnit unit) throws InterruptedException;
boolean awaitUntil(Date deadline) throws InterruptedException;
/**
* 唤醒该条件变量的一个等待线程
* 等待线程再从 await 返回前,必须获得锁
*/
void signal();
/**
* 唤醒该条件变量的所有等待线程
* 等待线程再从 await 返回前,必须获得锁
*/
void signalAll();
}
条件变量使用
条件变量一般来解决轮询-判断或者休眠-判断的问题。
条件变量的使用如 Object 类的 wait 和 notify 函数的使用。
有如下的场景:一个线程 T1 需要不断对共享 s 进行累加,另一个线程 T2 需要在 s 的值大于 100 时,将该变量重新置为 0,如果没有共享变量,代码如下:
int s = 0;
public static final Lock lock = new ReentrantLock();
// T1
public void run() {
lock.lock();
s++;
lock.unlock();
}
// T2
public void run() {
while(true) {
lock.lock();
if (s > 100) s = 0;
lock.unlock();
}
}
以上的代码浪费了大量的时间片,可以使用条件变量经典的等待/通知机制改进,等待/通知机制的编写范式如下:
获取锁;
while (条件状态不满足) {
线程挂起等待,直到条件满足通知;
}
临界区操作;
释放锁;
以上代码可以改造成:
int s = 0;
public static final Lock lock = new ReentrantLock();
public static final Contition condition = lock.newCOndition();
// T1
public void run() {
lock.lock();
s++;
if (s > 100) {
condition.singal();
}
lock.unlock();
}
// T2
public void run() {
while(true) {
lock.lock();
while(s < 100) {
condition.await();
}
if (s > 100) s = 0;
lock.unlock();
}
}
在 Java 库函数中,大量的使用了条件变量,如在 LinkedBlockingQueue 中,如果已经到最大容量,则 put() 需要等待,如果为空,则 take() 需要等待,部分代码如下:
/** Lock held by take, poll, etc */
private final ReentrantLock takeLock = new ReentrantLock();
/** Wait queue for waiting takes */
private final Condition notEmpty = takeLock.newCondition();
/** Lock held by put, offer, etc */
private final ReentrantLock putLock = new ReentrantLock();
/** Wait queue for waiting puts */
private final Condition notFull = putLock.newCondition();
public void put(E e) throws InterruptedException {
if (e == null) throw new NullPointerException();
// Note: convention in all put/take/etc is to preset local var
// holding count negative to indicate failure unless set.
int c = -1;
Node node = new Node(e);
final ReentrantLock putLock = this.putLock;
final AtomicInteger count = this.count;
putLock.lockInterruptibly();
try {
/*
* Note that count is used in wait guard even though it is
* not protected by lock. This works because count can
* only decrease at this point (all other puts are shut
* out by lock), and we (or some other waiting put) are
* signalled if it ever changes from capacity. Similarly
* for all other uses of count in other wait guards.
*/
while (count.get() == capacity) {
notFull.await();
}
enqueue(node);
c = count.getAndIncrement();
if (c + 1 < capacity)
notFull.signal();
} finally {
putLock.unlock();
}
if (c == 0)
signalNotEmpty();
}
public E take() throws InterruptedException {
E x;
int c = -1;
final AtomicInteger count = this.count;
final ReentrantLock takeLock = this.takeLock;
takeLock.lockInterruptibly();
try {
while (count.get() == 0) {
notEmpty.await();
}
x = dequeue();
c = count.getAndDecrement();
if (c > 1)
notEmpty.signal();
} finally {
takeLock.unlock();
}
if (c == capacity)
signalNotFull();
return x;
}
Lock 接口实现
同步队列器(AbstractQueuedSynchronizer
)是用来构建锁或者其他同步组建的基础框架。
它使用了一个 int
成员变量表示同步状态,通过内置的FIFO队列来完成资源获取线程的排队工作。
同步器的主要使用方式是继承,子类通过继承同步器并实现它的抽象方法来管理同步状态。
note
- 在抽象方法的实现过程中需要对同步状态进行更改,就需要同步器提供的三个方法
getState()
、setState(int newState)
、compareAndSetState(int expect, int update)
操作。这三个内置操作可以保证更改同步状态是安全的。- 同步器既可以实现独占式得获取同步状态,也可以支持共享式地获取同步状态,这样就可以方便实现不同类型地同步组件。(ReentrantLock、ReetrantReadWriteLock、CountDownLatch)
- 同步器地实现是基于模板方法的,即已经有了一个算法框架,只需要重写部分函数即可实现自己地逻辑。
队列同步器地接口与示例
同步器地设计是基于模板方法的,使用者需要继承同步器并重写指定的方法,随后将同步器组合在自定义同步组件地实现中,并调用同步器提供的模板方法,而这些模板方法将会调用使用者重写地方法。
重写同步器指定地方法时,需要使用 getState()
(获取同步状态)、setState(int newState)
(设置同步状态)、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(int arg) 方法 |
void acquireInterruptibly(int arg) | 独占式获取同步状态,如果当前线程获取同步状态成功,则该方法返回,否则进入同步队列等待,该方法将会调用重写的 tryAcquire(int arg) 方法,该方法相应中断,如果当前线程被中断,则该方法抛出 InterruptedException 并返回。 |
boolean tryAcquireNanos(int arg, long nanos) | 加入时间限制的acquire,如果在规定时间内没有获取到锁,返回False,反之,返回True。 |
void acquireShared(int arg) | 共享式地获取同步状态,如果当前线程未获取到同步状态就进入同步队列等待 |
void acquireSharedInterruptibly(int arg) | 同上,响应中断 |
boolean tryAcquireSharedNanos(int arg, long nanos) | 类比 tryAcquireNanos |
boolean release(int arg) | 独占式的释放同步状态,该方法会在释放之后将同步队列的第一个节点包含的线程唤醒。 |
boolean releaseShared(int arg) | 共享式的释放同步状态 |
collection |
获取同步队列上线程的集合。 |
AQS 通用模板
AQS 使用一个通用化的实现模式:
- 首先,申明一个共享变量为 volatile
- 使用 CAS 原子条件交换来实现线程之间的同步
我们看一个 AQS 的实现(ReentrantLock):
// 合并 AQS,Sync,NonFairSync 三个类
static final class NonfairSync extends Sync {
// 只有 state == 0时,才能获得锁
final void lock() {
if (compareAndSetState(0, 1))
setExclusiveOwnerThread(Thread.currentThread());
else
acquire(1);
}
protected final boolean tryAcquire(int acquires) {
return nonfairTryAcquire(acquires);
}
final boolean nonfairTryAcquire(int acquires) {
final Thread current = Thread.currentThread();
int c = getState();
if (c == 0) {
if (compareAndSetState(0, acquires)) {
//...
return true;
}
}
//...
}
// 只有 state == 0 时才会释放锁
protected final boolean tryRelease(int releases) {
int c = getState() - releases;
if (Thread.currentThread() != getExclusiveOwnerThread())
throw new IllegalMonitorStateException();
boolean free = false;
if (c == 0) {
free = true;
setExclusiveOwnerThread(null);
}
setState(c);
return free;
}
// 共享变量 state
// state == 0 代表可以获得锁
// state > 0 代表不能获得锁
private volatile int state;
}
队列实现分析
同步队列内部使用一个 FIFO 的双向队列来保证同步状态的管理,当当前线程获取同步队列失败时,同步器会将当前线程以及等待状态等信息构成一个节点并将其假如同步队列,节点的构造如下:
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;
static final int CANCELLED = 1;
static final int SIGNAL = -1;
static final int CONDITION = -2;
static final int PROPAGATE = -3;
volatile int waitStatus;
volatile Node prev;
volatile Node next;
volatile Thread thread;
Node nextWaiter;
//等待队列中的后继节点,如果当前节点是共享的,那么这个字段是一个 SHARED 常量
final boolean isShared() {
return nextWaiter == SHARED;
}
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 总共有 4 种状态:
- CANCELLED:该线程由于超时或中断等原因被取消,如果某线程进入了改状态,就会一直是该状态
- SIGNAL:后继节点处于等待状态,如果该节点释放了锁或被取消,需要通知后继节点
- CONDITION:使用在条件变量中,见条件变量,代表节点在等待队列中,当其他线程对 Condition 调用了 signal 方法,该节点会从等待队列转移到同步队列中,加入到对同步状态的获取中
- PROPAGATE:表示下一次共享式同步状态获取将被无条件传播下去
一个同步队列如上图所示:同步器提供了基于 CAS 的设置尾节点的方法:compareAndSetTail,保证尾节点的设置保持原子性和线程安全
通古比队列遵循 FIFO:首节点是获取同步状态成功的节点(获得锁的节点);首节点的线程在释放同步状态时,会唤醒后继节点;后继节点在获取同步状态成功后将自己设置为首节点
独占式同步状态的获取
我们来看当获取锁不成功时,如何加入一个节点到尾节点:
public final void acquire(int arg) {
if (!tryAcquire(arg) && acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
selfInterrupt();
}
可以看到,当 tryAcquire(arg)
不成功时,会调用 acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
方法。从函数名可以看出,是新键一个独占节点并将其加入到同步队列中等待。
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) {
// 自旋添加 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;
}
}
}
}
当创建成功后,就会进入 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())
// shouldParkAfterFailedAcquire 三个功能:
// 1. 如果前驱节点为 SINGAL 直接返回 ture
// 2. 如果前驱节点为 CANCEL,重整队列,跳过 node 之前所有的 cancel 的 node,更换前驱节点为 SIGNAL,返回 false
//3. 如果前驱节点为 0 或者 PROPAGATE,需要尝试将其更改为 SINGAL(主要是表注共享锁结束)
interrupted = true;
}
} finally {
if (failed)
cancelAcquire(node);
}
}
需要注意的是,同步队列的 head 节点是一个空节点( enq
函数初始化 head 节点时可证明),所以 如果 node 的前驱结点是 head 节点就可以进行上锁了。
note
- 在 parkAndCheckInterrupt 中,使用 park 函数暂停线程
- park 函数返回有三种情况:
- 另外一个线程调用了 unpark 方法
- 另外一个线程中断了该线程
- 另外一个线程在他调用 park 之前就调用了 unpark 方法
对于 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;
}
总体流程图如下:
共享式同步状态的获取
共享式同步锁可以和共享式同步锁兼容,却不能和独占式同步锁兼容,即,如果获得了独占式同步锁,就不能再次获得共享式同步锁;如果获得了共享式同步锁,可以再次获得共享式同步锁,但不能获得独占锁。
共享式获取锁和排他式类似,只是多了 setHeadAndPropagate
方法保证唤醒所有的 SHARED 模式的线程,代码如下:
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;
}
}
if (shouldParkAfterFailedAcquire(p, node) &&
parkAndCheckInterrupt())
interrupted = true;
}
} finally {
if (failed)
cancelAcquire(node);
}
}
通过调用 releaseShared 方法可以释放同步状态,如下:
public final boolean releaseShared(int arg) {
if (tryReleaseShared(arg)) {
doReleaseShared();
return true;
}
return false;
}
/**
* Release action for shared mode -- signals successor and ensures
* propagation. (Note: For exclusive mode, release just amounts
* to calling unparkSuccessor of head if it needs signal.)
*/
private void doReleaseShared() {
for (;;) {
Node h = head;
if (h != null && h != tail) {
int ws = h.waitStatus;
if (ws == Node.SIGNAL) {
if (!compareAndSetWaitStatus(h, Node.SIGNAL, 0))
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;
}
}
与独占式不同的是,一般 tryReleaseShared 必须确保可能多个线程同时调用并且安全释放,所以通过循环和 CAS 操作来保证。
独占式超时等待
独占式超时等待的获得锁过程如下:
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);
}
}
note
- 当 nanosTimeout 小于 spinForTimeoutThreshold 时,使用 LockSupport.parkNanos(this, nanosTimeout) 函数不能精准的控制时间,进入快速自旋,保证准确度
流程图如下:
条件变量接口
Condition 的实现分析
ConditionObject 是同步器 AQS 的内部类,包含一个等待队列,该队列是 Condition 对象等待/通知的关键。
等待队列
等待队列是一个 FIFO 队列,在 Object 的监视器模型上,一个对象拥有一个同步队列和一个等待队列,而并发包中的 Lock 拥有一个同步队列和多个等待队列。如下:
await 方法
ConditionPbject 的 await 方法如下:
public final void await() throws InterruptedException {
if (Thread.interrupted())
throw new InterruptedException();
Node node = addConditionWaiter();
int savedState = fullyRelease(node);
int interruptMode = 0;
while (!isOnSyncQueue(node)) {
LockSupport.park(this);
if ((interruptMode = checkInterruptWhileWaiting(node)) != 0)
break;
}
if (acquireQueued(node, savedState) && interruptMode != THROW_IE)
interruptMode = REINTERRUPT;
if (node.nextWaiter != null) // clean up if cancelled
unlinkCancelledWaiters();
if (interruptMode != 0)
reportInterruptAfterWait(interruptMode);
}
该方法总共分为 6 步:
- 如果当前线程 interrupt,抛出 InterruptedException 异常
- 将该线程构造成节点加入等待队列
- 通过调用 getState(fullyRelease 内部)获得当前同步状态
- 通过调用 release(fullyRelease 内部)释放同步状态,如果释放失败,抛出 IllegalMonitorStateException 异常
- 阻塞知道其他线程调用 signal 或者该线程被打断,如果被打断,抛出 InterruptedException 异常
- 通过 acquireQueued 重新等待获得同步状态
如下图:
note
- await 的步骤要求在调用 await 之前一定要获得同步状态(锁)
- 在没锁状态下调用 tryRelease,会导致 state = -1 ,这是不正确的状态
如下:
public class Demo {
public static void main(String[] args) throws InterruptedException {
Condition condition = lock.newCondition();
condition.await();
}
private static Lock lock = new ReentrantLock();
// output:
// Exception in thread "main" //java.lang.IllegalMonitorStateException
// at java.util.concurrent.locks.ReentrantLock$Sync.tryRelease(ReentrantLock.java:151)
// at java.util.concurrent.locks.AbstractQueuedSynchronizer.release(AbstractQueuedSynchronizer.java:1261)
// at java.util.concurrent.locks.AbstractQueuedSynchronizer.fullyRelease(AbstractQueuedSynchronizer.java:1723)
// at java.util.concurrent.locks.AbstractQueuedSynchronizer$ConditionObject.await(AbstractQueuedSynchronizer.java:2036)
}
signal 方法
调用 singal() 方法,会将唤醒在等待队列中等待时间最长的节点(首节点),在唤醒节点之前,会将节点移动到同步队列中。代码如下:
public final void signal() {
if (!isHeldExclusively())
throw new IllegalMonitorStateException();
Node first = firstWaiter;
if (first != null)
doSignal(first);
}
private void doSignal(Node first) {
do {
if ( (firstWaiter = first.nextWaiter) == null)
lastWaiter = null;
first.nextWaiter = null;
} while (!transferForSignal(first) &&
(first = firstWaiter) != null);
}
final boolean transferForSignal(Node node) {
/*
* If cannot change waitStatus, the node has been cancelled.
*/
if (!compareAndSetWaitStatus(node, Node.CONDITION, 0))
return false;
/*
* Splice onto queue and try to set waitStatus of predecessor to
* indicate that thread is (probably) waiting. If cancelled or
* attempt to set waitStatus fails, wake up to resync (in which
* case the waitStatus can be transiently and harmlessly wrong).
*/
Node p = enq(node);
int ws = p.waitStatus;
if (ws > 0 || !compareAndSetWaitStatus(p, ws, Node.SIGNAL))
LockSupport.unpark(node.thread);
return true;
}
使用 enq 方法安全的将其加入到尾节点,当前线程移动到同步队列后,当前线程再次使用 LockSupport 唤醒该节点的线程。
如下:
note
- 同样,该函数在调用前也需要获得锁,如果没有获得锁,检查 isHeldExclusively 失败,则会抛出 IllegalMonitorStateException 异常
内置锁
可重入锁 ReentrantLock
可重入锁就是支持重入的锁,他表示该锁能够支持一个线程对资源的重复加锁。主要是解决一个线程对同时加锁多次会被阻塞的问题。
实现可重入
实现可重入需要解决两个问题:
- 线程再次获取锁:锁需要去识别获取锁的线程是否为当前占据锁的线程,如果是,则可以再次成功获取锁
- 锁的最终释放:线程重复 n 次获取了锁,随后在第 n 次释放该锁后,其他线程能够得到锁,即需要对获取锁的次数进行自增和自减。
如下图所示,非公平锁的 tryAcquire 实现如下,发现其中增加了获得锁的线程记录和线程获得锁个数的自增变量 c,用来实现可重入的性质:
protected final boolean tryAcquire(int acquires) {
return nonfairTryAcquire(acquires);
}
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) // overflow
throw new Error("Maximum lock count exceeded");
setState(nextc);
return true;
}
return false;
}
protected final boolean tryRelease(int releases) {
int c = getState() - releases;
if (Thread.currentThread() != getExclusiveOwnerThread())
throw new IllegalMonitorStateException();
boolean free = false;
if (c == 0) {
free = true;
setExclusiveOwnerThread(null);
}
setState(c);
return free;
}
公平性的实现
当一个线程释放锁的瞬间,会有如下的线程竞争情况:
- 还未加入同步队列的线程在 acquire 函数中调用 tryAcquire 函数利用 CAS 竞争锁
- 同步队列的 head 节点在 acquireQueued 中调用 tryAcquire 函数利用 CAS 竞争锁
可能的函数调用栈如下:
// 线程 1(还未加入到 AQS 中)
/**
* compareAndSetState (竞争锁)
* /\
* AbstractQueuedSynchronizer.tryAcquire()
* /\
* AbstractQueuedSynchronizer.acquire()
* /\
* Lock.lock()
*/
// 线程 2(AQS 同步队列的头节点)
/**
* compareAndSetState (竞争锁)
* /\
* AbstractQueuedSynchronizer.tryAcquire()
* /\
* AbstractQueuedSynchronizer.acquireQueued()
* /\
* AbstractQueuedSynchronizer.tryAcquire()
* /\
* AbstractQueuedSynchronizer.acquire()
* /\
* Lock.lock()
*/
在同一时刻,如上的两个线程只有一个线程可以竞争到锁,如下图所示:
如果不是 AQS 队列中的头节点竞争到了锁,那么就好似其他线程进行了插队,这样就时不公平的锁。一般解决方案是线程在 tryAcquire() 时需要判断 AQS 中是否有元素,有元素的话不参与竞争,直接加入到 AQS 同步队列中。
观察公平的可重入锁,他的 tryAquire 函数如下:
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;
}
他和非公平锁的区别就在于增加了 hasQueuedPredecessors()
函数,该函数查询是否有任何线程等待的时间比当前线程长,可以理解成 AQS 同步队列中是否有等待的元素。
读写锁
读写锁使用“按位切割使用”的方式在一个整形变量上维护多种状态,如下:
|<------------------------------32 bits------------------------>|
-----------------------------------------------------------------
|0|0|0|0|0|0|0|0|0|0|0|0|0|0|0|1|0|0|0|0|0|0|0|0|0|0|0|0|0|0|1|1|
-----------------------------------------------------------------
| |
| read state | write state
\|/ |
|<-----------high 16 bits------>| |
--------------------------------- |
|0|0|0|0|0|0|0|0|0|0|0|0|0|0|0|1| |
--------------------------------- \|/
|<-----------low 16 bits--- --->|
---------------------------------
|0|0|0|0|0|0|0|0|0|0|0|0|0|0|1|1|
---------------------------------
以一个整数的高 16 位代表读状态,低 16 位代表写状态。
写锁的获取与释放
写锁是一个支持重入的排他锁,如果当前线程已经获取了写锁,则增加写状态,如果在获取写锁时,已经获取了读锁或者该线程不是已经获取写锁的线程,则需要等待。
读锁的获取与释放
读锁是一个支持重入的共享锁,如果当前线程已经获取了读锁,则增加读状态,如果在获取读锁时,其他线程已经获得了写锁,则需要等待,即如果当前线程获取了写锁,也可以获得读锁。
sychronized
synchronized 关键字可以用来修饰方法或者同步块,他确保多个线程在同一个时刻,只能有一个线程在方法或同步块中。
package test;
public class Synchronized {
public static void main(String[] args) {
// TODO Auto-generated method stub
synchronized (Synchronized.class) {
}
m();
}
public static synchronized void m() {
}
}
以上源码的部分汇编形式如下所示。
public static void main(java.lang.String[]);
descriptor: ([Ljava/lang/String;)V
flags: ACC_PUBLIC, ACC_STATIC
Code:
stack=2, locals=1, args_size=1
0: ldc #1 // class test/Synchronized
2: dup
3: monitorenter
4: monitorexit
5: invokestatic #16 // Method m:()V
8: return
LineNumberTable:
line 7: 0
line 10: 5
line 11: 8
LocalVariableTable:
Start Length Slot Name Signature
0 9 0 args [Ljava/lang/String;
public static synchronized void m();
descriptor: ()V
flags: ACC_PUBLIC, ACC_STATIC, ACC_SYNCHRONIZED
Code:
stack=0, locals=0, args_size=0
0: return
LineNumberTable:
line 15: 0
LocalVariableTable:
Start Length Slot Name Signature
在上面的信息中,对于同步块的使用实现了 monitorenter
和 monitorexit
指令,而同步方法则是依赖方法描述符上的 ACC_SYNCHRONIZED
来完成的。其本质都是对一个对象的监视器(monitor)进行获取,而这个过程是排他的,也就是同一时刻只能够有一个线程获取到 synchronized 所保护对象的监视器。
当这个对象由同步块或者这个对象的同步方法调用时,执行方法的线程必须先获取到该对象的监视器才能进入同步块或者同步方法,而没有获取到监视器的线程将会进入 BLOCKED 状态。
从上图可以看出,任意线程对 object 的访问(object 由 synchronized 所保护),都要首先获得 object 的监视器,如果获取失败,线程进入同步队列,线程状态变为 BLOCKED。当访问 object 的前驱释放了锁,则该操作会唤醒阻塞在同步队列中的线程,使其重新尝试对监视器的获取。
wait/notified 机制
定义如下变量。
- 等待持续时间:REMAINING = T
- 超时时间FUTURE = now + T
超时等待模式的伪代码如下:
public synchronized Object get(long mills) throws InterruptedException {
long future = System.currentTimeMillis() + mills;
long remaining = mills;
while((result == null) && remaining > 0) {
wait(remaining);
remaining = future - System.currentTimeMillis();
}
return result;
}
等待/通知机制
方法名称 | 描述 |
---|---|
notify() | 通知一个在对象上等待的线程,使其从wait()方法中返回,而返回的前提是该线程获取到了对象的锁 |
notifyAll() | 通知所有等待在该对象上的线程 |
wait() | 调用该方法的线程进入WAITING状态,只有等待另外线程的通知或被中断才会返回,调用wait()方法后,会释放对象的锁。 |
wait(long) | 等待n毫秒后强制返回 |
wait(long, int) | 可以达到纳米的细粒度控制 |
等待/通知机制,是指一个线程A调用了对象O的 wait()
方法进入等待状态,而另一个线程B调用了对象O的 notify()
或者 notifyAll()
方法,线程A收到通知后从对象O的 wait()
方法返回,进而执行后续操作。上述两个线程通过对象O来完成交互。
package test;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.concurrent.TimeUnit;
public class WaitNotify {
static boolean flag = true;
static Object lock = new Object();
public static void main(String[] args) throws Exception {
// TODO Auto-generated method stub
Thread waitThread = new Thread(new Wait(), "WaitThread");
waitThread.start();
TimeUnit.SECONDS.sleep(1);
Thread notifyThread = new Thread(new Notify(), "NotifyThread");
notifyThread.start();
}
static class Wait implements Runnable {
@Override
public void run() {
// TODO Auto-generated method stub
synchronized (lock) {
while (flag) {
try {
System.out.println(Thread.currentThread() + "flag is true. wait @ "
+ new SimpleDateFormat("HH:mm:ss").format(new Date()));
lock.wait();
} catch (InterruptedException e) {
// TODO: handle exception
}
}
System.out.println(Thread.currentThread() + "flag is false. running @ "
+ new SimpleDateFormat("HH:mm:ss").format(new Date()));
}
}
}
static class Notify implements Runnable {
@Override
public void run() {
// TODO Auto-generated method stub
synchronized (lock) {
System.out.println(Thread.currentThread() + "hold lock. notify @ "
+ new SimpleDateFormat("HH:mm:ss").format(new Date()));
lock.notifyAll();
flag = false;
SleepUtils.second(5);
}
synchronized (lock) {
System.out.println(Thread.currentThread() + "hold lock again. notify @ "
+ new SimpleDateFormat("HH:mm:ss").format(new Date()));
SleepUtils.second(5);
}
}
}
}
//output:
//Thread[WaitThread,5,main]flag is true. wait @ 21:24:26
//Thread[NotifyThread,5,main]hold lock. notify @ 21:24:27
//Thread[NotifyThread,5,main]hold lock again. notify @ 21:24:32
//Thread[WaitThread,5,main]flag is false. running @ 21:24:37
note
- 上述结果的3,4行结果可能互换。
- 使用wait()、notify()、notifyAll()方法时需要先对调用对象加锁。
- notify()、notifyAll()方法调用后,等待线程依旧不会从wait()中返回,除非调用notify()、notifyAll()的线程释放锁之后,等待线程才有机会返回
- notify() 方法将等待队列中的一个等待线程从等待队列中取出移到同步队列中,而notifyAll()方法则是将所有等待队列中的线程全部移到同步队列中,被移动的线程状态由WAITING变成BLOCKED。
下图是上述java代码的运行过程。
- WaitThread 首先获得了对象的锁。
- 调用wait()方法后,放弃了锁并进入了等待队列中。
- NotifyThread随之获得锁,并调用notify方法。
- notify系列方法将WaitThread从等待队列移动到同步队列中。
- NotifyThread释放了锁之后,WaitThread再次获取到锁并从wait()方法中返回。