文章目录
AbstractQueuedSynchronizer ---- jdk1.8
锁的大基础LockSupport
LockSupport的park() unpark()对比于Thread的 suspend和resume有如下优势:
Thead.suspend和Thread.resume有两种死锁场景,其一是不释放锁,其二是suspend和resume的顺序反了,oracel的原文是:If the thread that would resume the target thread attempts to lock this monitor prior to calling resume, deadlock results. Such deadlocks typically manifest themselves as “frozen” processes.而park和unpark没有顺序问题引起的死锁,但是park和unpark同样有互持不会释放锁的问题。如果在同步代码块中运行还是可能导致死锁的。
LockSupport 的park/unpark方法
其实park/unpark的设计原理核心是“许可”。park是等待一个许可。unpark是为某线程提供一个许可。如果某线程A调用park,那么除非另外一个线程调用unpark(A)给A一个许可,否则线程A将阻塞在park操作上。
有一点比较难理解的,是unpark操作可以再park操作之前。也就是说,先提供许可。当某线程调用park时,已经有许可了,它就消费这个许可,然后可以继续运行。
这个类当中的一些重要方法
private static final long parkBlockerOffset;
// 阻塞方法
public static void park(Object blocker) {
Thread t = Thread.currentThread();
setBlocker(t, blocker); // 把当前线程阻塞到blockers上
UNSAFE.park(false, 0L);
setBlocker(t, null);
}
// 设定阻塞对象
private static void setBlocker(Thread t, Object arg) {
// Even though volatile, hotspot doesn't need a write barrier here.
UNSAFE.putObject(t, parkBlockerOffset, arg);
}
// 获得阻塞对象
public static Object getBlocker(Thread t) {
if (t == null)
throw new NullPointerException();
return UNSAFE.getObjectVolatile(t, parkBlockerOffset);
}
了解下AQS使用的数据结构,等待队列(双向链表)
AQS拥有一个同步队列和多个等待队列
AQS之同步器–线程安全神器
这个体系下的锁都是通过各自的内部类Sync extends AbstractQueuedSynchronizera来实现同步的。其提供了一些基本的lock、release方法来解决同步问题。
先了解个名词CLH
CLH锁即Craig, Landin, and Hagersten (CLH)
locks。CLH锁是一个自旋锁。能确保无饥饿性。提供先来先服务的公平性。CLH锁也是一种基于链表的可扩展、高性能、公平的自旋锁,申请线程仅仅在本地变量上自旋,它不断轮询前驱的状态,假设发现前驱释放了锁就结束自旋。
// AQS 中有一个深度释放重入锁的方法
final int fullyRelease(Node node) {
boolean failed = true;
try {
int savedState = getState();
if (release(savedState)) {
failed = false;
return savedState;
} else {
throw new IllegalMonitorStateException();
}
} finally {
if (failed)
node.waitStatus = Node.CANCELLED;
}
}
// 线程设置中断状态
static void selfInterrupt() {
Thread.currentThread().interrupt();
}
AQS之ConditionObject–阻塞神器
ConditionObject是很重要的一个成员,看看他的类图。首先它通过Node维护了一个双向队列见上图2。然后也维护了firstWaiter和lastWaiter,也提供了最基本的方法await和signal/signalAll。
// 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); // 最终的阻塞方法,看到没用的LockSupport.park,阻塞的条件是this,也就是当前的condition对象,调用await的线程都会被阻塞在该condition上
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);
}
private Node addConditionWaiter() {
Node t = lastWaiter;
// If lastWaiter is cancelled, clean out.队列中最后一个都不阻塞了,理应都移出队列
if (t != null && t.waitStatus != Node.CONDITION) {
unlinkCancelledWaiters();
t = lastWaiter;
}
Node node = new Node(Thread.currentThread(), Node.CONDITION); // 把当前线程构建成一个阻塞在condition上状态的一个阻塞节点
if (t == null)
firstWaiter = node; // 队列为空,加到队首
else
t.nextWaiter = node; // 否则加到队尾
lastWaiter = node;
return node;
}
// 至于定时阻塞都是通过自旋来等待时间结束的
// 唤醒线程
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;
}
ReentrantLock
一个重要的同步器 private final Sync sync; // 同步器是基于AQS的
通过构造方法来决定使用公平/非公平,默认是非公平
public ReentrantLock() {
sync = new NonfairSync();
}
abstract static class Sync extends AbstractQueuedSynchronizer
定义了两个AQS,一个是公平锁,一个是非公平锁
static final class NonfairSync extends Sync // 非公平锁
static final class FairSync extends Sync // 公平锁
公平锁
// 上锁 公平锁(static final class FairSync extends Sync)
final void lock() {
acquire(1); // 上来就以公平的方式去拿锁
}
public final void acquire(int arg) {
if (!tryAcquire(arg) &&
acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
selfInterrupt();// 如果acquireQueued的结果是当前线程已被中断 则中断当前线程
}
protected final boolean tryAcquire(int acquires) {
final Thread current = Thread.currentThread();
int c = getState();
if (c == 0) {
if (!hasQueuedPredecessors() && // 此方法逻辑:true:CLH队列中已经有线程在当前线程前排队;false:当前线程是head节点,或者队列empty。这正是公平与非公平的区别之处,公平是上来看看CLH中是否有比当前行程还早的排队线程,非公平是上来就尝试取锁
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;
}
/**
* @param mode Node.EXCLUSIVE for exclusive, Node.SHARED for shared
* @return the new node
*/
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;
}
}
}
}
/**
* 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)) { // 一般线程阻塞后由两种方式解除阻塞:1 别的线程调用interrupt();2 别的线程调用unpark()。如果是调用unpark() 则是其前驱节点触发的,所以会判断p==head,这也是公平性的保证。
setHead(node); // 拿到锁后把当前线程设置为head,
p.next = null; // help GC
failed = false;
return interrupted;
}
if (shouldParkAfterFailedAcquire(p, node) &&
parkAndCheckInterrupt()) // 如果当前线程在阻塞过程中被interrupted了,则返回中断标识
interrupted = true;
}
} finally {
if (failed)
cancelAcquire(node);
}
}
非公平锁
// 上锁 非公平锁(NonfairSync extends Sync)
final void lock() {
if (compareAndSetState(0, 1)) // 此处非公平第一次机会,上来就先抢一次锁
setExclusiveOwnerThread(Thread.currentThread());
else
acquire(1);// 直接没抢着再通过非公平方式去抢一次
}
public final void acquire(int arg) {
if (!tryAcquire(arg) &&
acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
selfInterrupt();
}
protected final boolean tryAcquire(int acquires) {
return nonfairTryAcquire(acquires);
}
// 非公平获取锁逻辑,上来就直接尝试获取锁,没获得就返回false
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;
}
看看代码比对区别一下:
综上:
公平锁:当线程来获取锁的时候优先看是否有在之之前排队的,有的话就排队,然后自旋等待获取锁(除非被中断)
非公平锁:当线程来获取锁的时候不管前面有没有其他线程排队,直接尝试拿锁,没拿到的话后续就跟公平锁一样了加入队列。
ReentrantLock中的tryLock是走的非公平锁逻辑。
ReentrantLock.unlock() 此方法完全继承自Sync的父类AQS
// 放锁
public final boolean release(int arg) {
if (tryRelease(arg)) {
Node h = head;
if (h != null && h.waitStatus != 0)
unparkSuccessor(h);
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);// 释放后把排它锁的持有者置为 null
}
setState(c); // 这里可以看到完全释放后的线程状态时 0 (none)
return free;
}
private void unparkSuccessor(Node node) {
/*
* 把head的线程的waitStatus设置为0(none)
*/
int ws = node.waitStatus;
if (ws < 0)
compareAndSetWaitStatus(node, ws, 0);
/*
* Thread to unpark is held in successor, which is normally
* just the next node. But if cancelled or apparently null,
* traverse backwards from tail to find the actual
* non-cancelled successor.
* 如果当前节点的next不为空且没有被cancel就唤醒它,如果不满足这个条件就从尾部开始一直倒推找到最靠近head的waitStatus为 SIGNAL = -1 | CONDITION = -2 | PROPAGATE = -3;的线程来唤醒它
*/
Node s = node.next;
if (s == null || s.waitStatus > 0) {
s = null;
for (Node t = tail; t != null && t != node; t = t.prev)
if (t.waitStatus <= 0)
s = t;
}
if (s != null)
LockSupport.unpark(s.thread);
}
等待队列
// ReentrantLock#newCondition
public Condition newCondition() {
return sync.newCondition();
}
// Sync#newCondition
final ConditionObject newCondition() {
return new ConditionObject(); // 到这里就是使用的AQS的等待队列
}
ReentrantReadWriteLock
可以看到读写锁中有两个Lock来分别控制读和写
读写锁也是基于AQS实现(Sync),读写锁也支持公平与非公平。
看看构造函数
public ReentrantReadWriteLock(boolean fair) {
sync = fair ? new FairSync() : new NonfairSync();
readerLock = new ReadLock(this); // new 一个读锁
writerLock = new WriteLock(this); // new 一个写锁
}
// 获取读写锁的方法
public ReentrantReadWriteLock.WriteLock writeLock() { return writerLock; }
public ReentrantReadWriteLock.ReadLock readLock() { return readerLock; }
ReadLock的lock方法
public void lock() {
sync.acquireShared(1); // 共享锁
}
WriteLock的lock方法
public void lock() {
sync.acquire(1); // 排它锁
}
BlockingQueue
这里的阻塞的一个直白解释:在需要的空间资源/对象资源不足时线程阻塞到此条件上,知道别的线程补给了相应的资源并唤醒阻塞线程。
java 默认实现了如下阻塞队列(AbstractQueue作为一个基础)
ArrayBlockingQueue 和 LinkedBlockingQueue
这两个都是继承自AbstractQueue和实现BlockingQueue
异同如下:
- 实现数据结构不同 ABQ是基于数组存储元素的 final Object[] items且实例化时务必指定capacity,不可扩展;LBQ是基于双向链表存储元素的,实例化时capacity不是必须的。
- ABQ数据结构是数组,因此是通过putIndex和takeIndex来标记进出元素的位置的;LBQ是通过队列的first和last来标记进出元素位置的。
- 两个都是基于非公平锁ReentrantLock来控制线程安全的,都是通过lock对应的双condition来阻塞/唤醒增删线程的。
PriorityBlockintQueue
要搞明白优先阻塞队列,先来看一看优先队列PriorityQueue
PriorityQueue的数据结构是一个最小堆/最大堆 ,这个是线程不安全的
/**
*Priority queue represented as a balanced binary heap: the two
*children of queue[n] are queue[2*n+1] and queue[2*(n+1)]
*/
transient Object[] queue;
然后看看它的构造方法
public PriorityQueue(int initialCapacity,
Comparator<? super E> comparator) {
// Note: This restriction of at least one is not actually needed,
// but continues for 1.5 compatibility
if (initialCapacity < 1)
throw new IllegalArgumentException();
this.queue = new Object[initialCapacity];
this.comparator = comparator; // 比较器,优先级的判定者,如果不传默认是自然顺序
}
优先核心逻辑,最小堆的维护
最小堆,是一种经过排序的完全二叉树,其中任一非终端节点的数据值均不大于其左子节点和右子节点的值。
private void heapify() {
for (int i = (size >>> 1) - 1; i >= 0; i--)//逆序是因为要确保所有的孩子与父节点都进行了比较,这里算出来的话正好是从最左边的第一个非叶子节点开始
siftDown(i, (E) queue[i]);// 每个节点处理完的时间复杂度与子树的高度线性相关(O(logn))
}
private void siftDown(int k, E x) {
if (comparator != null)
siftDownUsingComparator(k, x);
else
siftDownComparable(k, x);
}
// 维护最小堆:把父节点与左右子节点中的最小者交换,直到父节点比左右子节点都小为止
private void siftDownUsingComparator(int k, E x) {
int half = size >>> 1;
while (k < half) {
int child = (k << 1) + 1; // 左子节点脚标
Object c = queue[child]; // 临时变量指向左子节点,最终目的是要这个临时变量指向左右子节点中的最小者
int right = child + 1; // 右子节点脚标
if (right < size && // 右子节点脚标没越界
comparator.compare((E) c, (E) queue[right]) > 0) // 如果左子节点大于右子节点
c = queue[child = right];// 临时变量指向右子节点(最小的一个)
if (comparator.compare(x, (E) c) <= 0) // 父节点小于等于原左孩子的值,满足最小堆的规则,不需要处理
break;
queue[k] = c; // 否则把左右自己点中的最小者赋值给父节点
k = child;
}
queue[k] = x; // 左右子节点中最小者的位置放置父节点的值,其实就是跟父节点换位置
}
// 元素出列
public E poll() {
if (size == 0)
return null;
int s = --size;
modCount++;
E result = (E) queue[0]; // 从对顶出列(最小的)
E x = (E) queue[s];
queue[s] = null;
if (s != 0)
siftDown(0, x); // 然后把最后一个元素放到对顶后并进行最小堆维护。
return result;
}
// 扩容很简单,小于64的时候近乎翻倍,大于64增加50%
private void grow(int minCapacity) {
int oldCapacity = queue.length;
// Double size if small; else grow by 50%
int newCapacity = oldCapacity + ((oldCapacity < 64) ?
(oldCapacity + 2) :
(oldCapacity >> 1));
// overflow-conscious code
if (newCapacity - MAX_ARRAY_SIZE > 0)
newCapacity = hugeCapacity(minCapacity);
queue = Arrays.copyOf(queue, newCapacity);
}
PriorityBlockingQueue的优先级维护同PriorityQueue一致,PriorityBlockingQueue 只可能阻塞调用take方法的线程。
最大的区别有:
- PriorityBlockingQueue 通过ReentrantLock及其Condition添加了阻塞关系。
- PriorityBlockingQueue 的扩容是通过乐观锁CAS类进行的
// 由于是一个无界队列,所以添加元素的操作永远不会阻塞,这样put和offer就一样了(看上去跟BlockingQueue接口中声明的put方法不一致?因为无界,所以无法!)
public void put(E e) {
offer(e); // never need to block
}
public boolean offer(E e) {
if (e == null)
throw new NullPointerException();
final ReentrantLock lock = this.lock;
lock.lock();
int n, cap;
Object[] array;
while ((n = size) >= (cap = (array = queue).length))
tryGrow(array, cap);// 扩容:注意try这个用词
try {
Comparator<? super E> cmp = comparator;
if (cmp == null)
siftUpComparable(n, e, array);
else
siftUpUsingComparator(n, e, array, cmp);
size = n + 1;
notEmpty.signal();
} finally {
lock.unlock();
}
return true;
}
/**
* Tries to grow array to accommodate at least one more element
* (but normally expand by about 50%), giving up (allowing retry)
* on contention (which we expect to be rare). Call only while
* holding lock.
*
* @param array the heap array
* @param oldCap the length of the array
*/
private void tryGrow(Object[] array, int oldCap) {
lock.unlock(); // must release and then re-acquire main lock 这个地方为了更好的并发性释放了全局锁,下一步就通过乐观锁来扩容
Object[] newArray = null;
if (allocationSpinLock == 0 &&
UNSAFE.compareAndSwapInt(this, allocationSpinLockOffset,
0, 1)) { // cas乐观锁
try {
int newCap = oldCap + ((oldCap < 64) ?
(oldCap + 2) : // grow faster if small
(oldCap >> 1));
if (newCap - MAX_ARRAY_SIZE > 0) { // possible overflow
int minCap = oldCap + 1;// 扩容时至少得扩1个元素,如果连一个都没法扩了,就报OOM
if (minCap < 0 || minCap > MAX_ARRAY_SIZE)
throw new OutOfMemoryError();
newCap = MAX_ARRAY_SIZE;
}
if (newCap > oldCap && queue == array)
newArray = new Object[newCap];
} finally {
allocationSpinLock = 0;
}
}
if (newArray == null) // back off if another thread is allocating
Thread.yield();// CAS扩容失败,暂时放弃CPU执行权
lock.lock();// 再把前面释放的全局锁"抢"回来
if (newArray != null && queue == array) { // 扩容成功的话迁移元素
queue = newArray;
System.arraycopy(array, 0, newArray, 0, oldCap);
}
}
// 出列
public E take() throws InterruptedException {
final ReentrantLock lock = this.lock;
lock.lockInterruptibly();
E result;
try {
while ( (result = dequeue()) == null)
notEmpty.await(); // 阻塞
} finally {
lock.unlock();
}
return result;
}
// 堆顶出列,出去后还需要维护最小堆,在lock内执行的,所以线程安全
private E dequeue() {
int n = size - 1;
if (n < 0)
return null;
else {
Object[] array = queue;
E result = (E) array[0];
E x = (E) array[n];
array[n] = null;
Comparator<? super E> cmp = comparator;
if (cmp == null)
siftDownComparable(0, x, array, n);
else
siftDownUsingComparator(0, x, array, n, cmp);
size = n;
return result;
}
}
DelayQueue 延时队列
DelayQueue 实现的是 BlockingQueue接口,是阻塞的。 DelayQueue 使用了ReentrantLock处理同步问题,也是线程安全的。
DelayQueue 中的元素E extends Delayed。所以必须具备一个获取剩余延时时长的方法:long getDelay(TimeUnit unit);
使用场景:
DelayQueue使用场景
- 关闭空闲连接。
- 缓存过期自动清理。
- 任务超时处理。
下面从两方面简要分析一下:1 数据结构,2 take方法
- 1 数据结构: DelayQueue
是通过一个全局变量PriorityQueue来充当它的数据结构的,所以它里面的元素具有优先性。按照getDelay的值作为排序依据。 - 2 take方法
public E take() throws InterruptedException {
final ReentrantLock lock = this.lock;
lock.lockInterruptibly();
try {
for (;;) {
E first = q.peek(); // 堆顶元素,这个元素理应是最早到时的
if (first == null)
available.await();// 堆顶无资源,阻塞,等待其他线程添加资源
else {
long delay = first.getDelay(NANOSECONDS); // 堆顶有资源的话,获取该资源的剩余延迟时长,看是否是到时
if (delay <= 0)
return q.poll(); // 到时就弹出堆顶
first = null; // don't retain ref while waiting
// 以下为堆顶元素还未到期的处理逻辑,leader为等待的线程。
if (leader != null) // 等待线程不为空,说明之前有线程早就在等待资源到期
available.await(); // 有人在前面了,自己乖乖等着吧
else {
Thread thisThread = Thread.currentThread();
leader = thisThread; // 暂没有等待资源的线程,则让当前线程占据高地
try {
available.awaitNanos(delay); // 占据高地后 自己也自旋等待吧(不过在自旋的过程中它会自我唤醒)
} finally {
if (leader == thisThread) // 自旋完到时间后,要把高地让出来
leader = null;
}
}
}
}
} finally {
if (leader == null && q.peek() != null)
available.signal();
lock.unlock();
}
}