实现锁同步机制,我们一般可以通过
(1) 自旋死循环实现缺点也很明显 空耗cpu
(2) 通过自旋加 当前线程调用yied() 释放cpu进入就绪状态,当然这样当下次仍有可能会被选择执行
(3) 通过自旋加 sleep() 让当前线程进入休眠状态 释放cpu , 这样似乎很完美,但是由于持有锁的线程执行时间不确定,可能造成当前线程的频繁唤醒
(4) 通过 自旋加 Locksurport.park() 方法实现锁同步,当线程释放锁时通过 Locksurport.unprk(t1) 唤醒等待线程
那么 jdk是如何做的呢?我们来看 ReentrantLock 是如何实现加锁的
public void lock() {
sync.lock();
}
那么这个sync是个什么, 何时初始化? 我们发现这个 sync 变量其实就是 AQS(AbstractQueuedSynchronizer) 他有两个实现类,公平锁和非公平锁 (AQS 通过维护一个类似与 CLS的双向链表的队列来实现公平加锁机制 )
通过源码我们发现其实 AQS 也是通过 对 状态 state 字段的值来控制锁的获得与释放,当获得锁state 值就 加 1,当释放锁或者说退出同步代码时state值减1当减到0时锁释放了
final void lock() {
acquire(1);
}
public void unlock() {
sync.release(1);
}
public final void acquire(int arg) {
if (!tryAcquire(arg)
&& acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
selfInterrupt();
}
tryAcquire() 去尝试获得锁,首先会拿到当前线程,获得state 的值,state 为int 当第一个线程来拿锁时 state 为默认值0,然后会判断AQS的等待队列是否有线程在等待,第一个线程拿锁时队列是不存在的还没初始化 所以 hasQueuedPredecessors()返回false 然后直接 compareAndSetState 获得锁。那么这个hasQueuedPredecessors又是怎么实现的呢?
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;
}
其实也很简单 当第一个线程过来是 tail 和 head 多为null 所以 h != t 直接返回false 逻辑与 就不往后比较直接返回false, 当后续线程过来拿锁时 tail 和 head 一定不相同,下面我们就来分析为什么。
public final boolean hasQueuedPredecessors() {
// The correctness of this depends on head being initialized
// before tail and on head.next being accurate if the current
// thread is first in queue.
Node t = tail; // Read fields in reverse initialization order
Node h = head;
Node s;
return h != t &&
((s = h.next) == null || s.thread != Thread.currentThread());
}
当后续线程拿锁时 ! tryAcquire(arg) && acquireQueued(addWaiter(Node.EXCLUSIVE), arg) 方法tryAquire 尝试拿锁失败,执行 acquireQueued 关键就在 addWaiter方法,首先会创建好一个node放在内存中,判断 队列尾是否为空,第二个线程拿锁tail 为空,执行 enq方法 又是一个自旋 首先判断 队列尾是否为空 第二个线程过来 肯定为空,jdk 就创建了一个 空的 node 通过无参构造 其中的属性 均为设置。然后设置为head ,当死循环再次执行时队列尾就不为null,当前(t2)node 的 prev 设置为 tail 然后 cas设置 tail 即将 当前(t2)节点设置为tail 将 前面节点的 next 指向 node(t2)返回。
总结:enq 方法 第一进入死循环是初始化队列 创建一个空的node节点 将 head 和 tail 均指向 改空的节点,第二次进入时设置 队列尾部为 当前节点(t2)
enq 第一次进入
enq 方法第二次进入
当第三个线程过来时,tail 肯定不为空,直接将 node(t3)的 prev 指向 队列尾,cas 设置tail 为当前 node3, 前置节点的next 指向 当前 node3直接返回.
private Node addWaiter(Node mode) {
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 = 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;
}
}
}
}
Node() { // Used to establish initial head or SHARED marker
}
第二个线程 获取锁进入队列等待之前,会再次 获取其前置节点 判断一下自己是否是队列中第一个等待的线程即其前置节点为head p==head 会在尝试一次获取锁,失败就 调用 park 进入队列等待
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);
}
}
上面说的是公平锁,现在说下非公平锁,非公平锁与公平锁的区别:非公平锁会在竞争锁时开始首先就强制的去cas设置锁并不关心等待队列是否为空,当抢锁失败会会再次判读一下锁是否处于自由状态,如果处于自由状态 即 state == 0 再次 cas 强制取锁,失败则加入等待队列之后的入队与公平锁一样。
总结:非公平锁无视等待队列,两次cas强制取锁失败后 加入等待队列排队取锁