LockSupport工具类
java.util.concurrent.locks.LockSupport
是个工具类,主要作用是挂起和唤醒线程,该工具类是创建锁和其他同步类的基础。
LockSupport
类与每个使用它的线程都会关联一个许可证,在默认情况下调用LockSupport
类的方法的线程是不持有许可证的。
LockSupport
是使用Unsafe
类实现的。
几个主要函数:
void park()方法
/**
Disables the current thread for thread scheduling purposes unless the permit is available.
*/
public static void park(Object blocker) {
Thread t = Thread.currentThread();
setBlocker(t, blocker);
UNSAFE.park(false, 0L);
setBlocker(t, null);
}
在其他线程调用unpark(Thread thread)
并且将当前线程作为参数时,调用park()
方法而被阻塞的线程会返回。
如果其他线程调用了阻塞线程的interrupt()
方法,设置了中断标志或者线程被虚假唤醒,则阻塞线程也会返回。
所以,调用park
方法时最好也使用循环条件判断方式。
void unpark(Thread thread)方法
当一个线程调用unpark时:
1、参数thread线程没有持有与LockSupport类关联的许可证,则让thread线程持有
2、如果thread之前因调用park而被挂起,则该线程会被唤醒
3、如果thread之前没有调用park,则调用unpark后,再调用park,其会立即返回
parkNanos(long nanos)方法
public static void parkNanos(long nanos) {
if (nanos > 0)
UNSAFE.park(false, nanos);
}
如果没有拿到许可证,则调用线程会被挂起nanos时间后修改为自动返回。
park方法还支持带有blocker参数的方法park(Object blocker)
:
public static void park(Object blocker) {
Thread t = Thread.currentThread();
setBlocker(t, blocker);
UNSAFE.park(false, 0L);
setBlocker(t, null);
}
当线程在没有持有许可证的情况下调用park(Object blocker)
,这个blocker
对象会被记录到该线程内部。
使用诊断工具可以观察线程被阻塞的原因,诊断工具时通过调用getBlocker(Thread t)
方法来获取blocker对象的。所以jdk推荐使用带有blocker参数的park方法,并且blocker被设置为this,这样当在打印线程堆栈排查问题时就能知道是哪个类被阻塞了。
LockSupport.park(this);
抽象同步队列AQS概述
AQS——锁的底层支持
AQS(AbstractQueuedSynchronizer),是实现同步器的基础组件,并发包中锁的底层就是使用AQS实现的。
1、AQS是一个FIFO的双向队列。内部通过节点 head和tail记录队首和队尾元素,队列元素的类型为Node。
static final class Node {
//标记该线程是获取共享资源时被阻塞挂起后放入AQS队列的
static final Node SHARED = new Node();
//标记该线程是获取独占资源时被阻塞挂起后放入AQS队列的
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;
// 记录当前线程的等待状态:SIGNAL、CANCELLED、CONDITION、PROPAGATE
volatile int waitStatus;
volatile Node prev;
volatile Node next;
volatile Thread thread;
........
}
2、在AQS中维持了一个单一的状态信息state
/**
* The synchronization state.
*/
private volatile int state;
protected final int getState() {
return state;
}
protected final void setState(int newState) {
state = newState;
}
对于ReentrantLock
实现来说,state可以用来表示当前线程获取锁的可重入次数;
对于读写锁ReentrantReadWriteLock
来说,sate的高16位表示读状态,即获取该读锁的次数,低16位表示获取该写锁的线程的可重入次数;
对于semaphore
来说,state用来表示当前可用信号的个数;
对于CountDownlatch
来说,state用来表示计数器当前的值
3、AQS有个内部类ConditionObject
,用来结合锁实现线程同步。
-
ConditionObject
可以直接访问AQS对象内部的变量,如state状态值和AQS队列。 -
ConditionObject
是条件变量,每个条件变量对应一个条件队列(单向链表队列),其用来存放调用条件变量的await方法后被阻塞的线程。
/** First node of condition queue. */
private transient Node firstWaiter;
/** Last node of condition queue. */
private transient Node lastWaiter;
4、对于AQS来说,线程同步的关键是对状态值state进行操作
根据state是否属于一个线程,操作state的方式分为独占方式和共享方式。
独占方式:
acquire(int arg)
acquireInterruptibly(int arg)
boolean release(int arg)
共享方式:
void acquireShared(int arg)
void acquireSharedInterruptibly(int arg)
boolean releaseShared(int arg)
使用独占方式获取的资源是与具体线程绑定的,即如果一个线程获取到了资源,就会标记是这个线程获取到了,其他线程再尝试操作state获取资源时就会发现当前该资源不是自己持有的,获取失败后被阻塞。
如独占锁ReentrantLock的实现,当一个线程获取了ReentrantLock的锁后,在AQS内部会首先使用CAS操作把state状态值从0变为1,然后设置当前锁的持有者位当前线程,当该线程再次获取锁时发现他就是锁的持有者,则会把状态值从1变为2,也就是设置可重入次数。而当另一个线程获取锁时会被放入AQS阻塞队列后挂起
共享方式的资源与具体线程是不相关的,当多个线程去请求资源时通过CAS方式竞争获取资源,当一个线程获取到了资源后,另外一个线程再次获取资源时如果当前资源还能满足它的需要,则当前线程只需要使用CAS方式进行获取即可。
比如Semaphore信号量,当一个线程通过acquire方法获取信号量时,会首先看当前信号量个数是否满足需要,不满足则把当前线程放入阻塞队列,如果满足则通过自旋CAS获取信号量
class Semaphore
public void acquire() throws InterruptedException {
sync.acquireSharedInterruptibly(1);
}
在独占方式下,获取与释放资源的流程如下
获取资源:
public final void acquire(int arg) {
if (!tryAcquire(arg) &&
acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
selfInterrupt();
}
1、使用tryAcquire
方法尝试获取资源
2、具体是设置状态变量state的值,成功则直接返回,失败则将当前线程封装为Node.EXCLUSIVE
的Node节点后插入到AQS阻塞队列的尾部,并调用LockSupport.park(this);
方法挂起自己
释放资源:
public final boolean release(int arg) {
if (tryRelease(arg)) {
Node h = head;
if (h != null && h.waitStatus != 0)
unparkSuccessor(h);
return true;
}
return false;
}
1、尝试使用tryRelease(arg)释放资源
2、调用LockSupport.unpark(thread);激活AQS队列里面被阻塞的一个线程
3、被激活的线程使用tryAcquire尝试,看当前状态变量state的值能否满足自己的需要,满足则该线程被激活,否则还是被放入AQS队列并被挂起
AQS的入队操作:
1、当一个线程获取锁失败后该线程会被转换为Node节点,然后就会使用enq(final Node node)
方法将该节点插入到AQS的阻塞队列。
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;
}
}
}
}
AQS——条件变量的支持
正如notify和wait方法,是配合synchronized内置锁实现线程间同步的基础设施一样,条件变量的signal
和await
方法也是用来配合锁(使用AQS实现的锁)实现线程间同步的基础设施。不同之处是synchronized只能与一个共享变量的notify和wait方法实现同步,而AQS的一个锁可以对应多个条件变量。
当线程调用条件变量的await()
方法时(必须先调用锁的lock()方法获取锁):
1、在内部会构造一个类型为Node.CONDITION的node节点
2、将该节点插入到条件队列的末尾
3、该线程释放锁并被阻塞挂起
当另一个线程调用条件变量的signal
方法时(必须先调用锁的lock()方法获取锁):
1、在内部会把条件队列里面队头的一个线程节点从队列里面移除
2、将该线程节点放入AQS的阻塞队列里面
3、激活这个线程
注意:1、AQS只提供了ConditionObject
的实现,并没有提供newCondition
函数,该函数用来new一个ConditionObject
对象。需要由AQS的子类来提供newCondition
函数。
2、当多个线程同时调用lock.lock()方法获取锁时,只有一个线程获取到了锁,其他线程会被转换为Node节点插入到lock锁对应的AQS阻塞队列里面,并做自旋CAS尝试获取锁。
一个锁对应一个AQS阻塞队列,对应多个条件变量,每个条件变量有自己的一个条件队列。
独占锁ReentrantLock的原理
读写锁ReentrantReadWriteLock的原理
JDK8中新增的StampedLock锁探究
-
StampedLock
是并发包里面 JDK8 版本新增的 一个锁 - 该锁提供了三种模式的读写控制,当调用获取锁的系列函数时,会返回一个 long 型的变量,我们称之为戳记 (stamp), 这个戳记代表了锁的状态 。
- 其中 try 系列获取锁的函数,当获取锁失败后会返回为 0 的 stamp值
- 当调用释放锁和转换锁的方法时需要传入获取锁时返回的stamp值。
StampedLock
提供3种读写模式的锁:
写锁writeLock:排它锁/独占锁
悲观读锁readLock:共享锁
乐观读锁tryOptimisticRead
这三种锁在一定条件下可以进行相互转换。
StampedLock
的读写锁都是不可重入锁。当多个线程同时尝试获取读锁和写锁时,谁先获取锁没有一定的规则,是随机的。并且该锁不是直接实现Lock
或ReadWriteLock
接口,而是在内部自己维护了一个双向阻塞队列。
《Java并发编程之美》