Lock源码解析第一篇
公平锁加锁过程
ReetrantLock 实现 Lock接口,在这个ReetrantLock类中维护了一个对象:
private final Sync sync;
这个Sync extends AbstractQueuedSynchronized 这个AbstractQueuedSynchronized 就是我们平时说的AQS
AQS 里面有三个元素很重要: 同步队列
private transient volatile Node head; //队列头
private transient volatile Node tail; //队列尾
private volatile int state; //锁状态,加锁成功则为1, 重入为+1 解锁则为0
public class Node{
volatile Node prev;
volatile Node next;
volatile Thread thread;
}
//Lock 接口,里面这么多方法的
public interface Lock {
void lock();
void lockInterruptibly() throws InterruptedException;
boolean tryLock();
boolean tryLock(long time, TimeUnit unit) throws InterruptedException;
void unlock();
Condition newCondition();
}
//先来写个小例子
//我们经常用的lock.lock
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
public class AQSDemo {
public static void main(String[] args) {
//lock.lock() 一定要写在try模块的外面
//因为一旦写在里面,获取到了锁,一抛异常,走finally就是释放锁
//finally 这个东西一定要写,保证锁 100% 释放
//ReentrantLock 它实现的就是上面的Lock接口
Lock lock = new ReentrantLock();
lock.lock();
try{
Thread.sleep(10000);
}catch (Exception e){
lock.unlock();33
}
}
}
//我假设有三个线程: 1、2、3 来解释一下公平锁的流程,然后带着这个流程去看源码解析
//AQS其实就是维护了锁的资源,state
//第一个线程来了,可以直接占用这个资源,表名已经上锁成功
//第二个线程来了,第二个线程肯定是要竞争锁资源的,但是由于此时锁资源被第一个线程占用,一直在无限循环
//第三个线程来了,第三个线程就不会竞争资源了,为什么呢?第二个都没有获取到锁,第三个就没有资格去竞争
// Doug Lea在第三个线程这边做了一个很有意思的东西,信号量(不要把它当成线程中的那个信号量,不是同一个概念)
//shouldParkAfterFailedAcquire() 待着疑问去看这个方法
//ReentrantLock 实现了接口Lock
public class ReentrantLock implements Lock, java.io.Serializable {
//这个里面有两种锁, 公平锁 非公平锁
public void lock() {
sync.lock();
}
//公平锁
static final class FairSync extends Sync {
final void lock() {
acquire(1);
}
}
//非公平锁
static final class NonfairSync extends Sync {
final void lock() {
if (compareAndSetState(0, 1)){
setExclusiveOwnerThread(Thread.currentThread());
}else{
acquire(1);
}
}
}
}
此篇,我只说一下公平锁。 @author Doug Lea 写线程 优化线程的超级大佬,先膜拜一下
//才一开始这个方法的调用点儿, acquire(1); arg == 1 1代表加锁成功
public final void acquire(int arg) {
//if条件是:
//先要尝试获取锁tryAcquire(arg), 如果获取锁失败了,就执行后面的acquireQueued(加入等待队列)
if (!tryAcquire(arg) && acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
selfInterrupt();
}
//公平锁
//讲述的是 尝试获取锁的,然后说明一下 acquireQueued(addWaiter(Node.EXCLUSIVE)
static final class FairSync extends Sync {
private static final long serialVersionUID = -3000897897090466540L;
final void lock() {
acquire(1);
}
/**
* Fair version of tryAcquire. Don't grant access unless
* recursive call or no waiters or is first.
*/
protected final boolean tryAcquire(int acquires) {
//拿到当前线程
final Thread current = Thread.currentThread();
//获取一下加锁的状态 state 这个值是在AQS里面维护的
//这个是获取当前锁资源的状态,看看是否已经上锁成功 1已经加锁 0还未加锁
//第一个线程进来,这个c肯定是0,未加锁
//那么,它就应该尝试去加锁
//总结一句话:getState()这个获取的不是某个线程的状态,是AQS的状态,是资源的状态,
//线程都是通过CAS来改变这个状态。 lock加锁 都是抢占这个AQS资源的
int c = getState();
if (c == 0) {
//我们首先得判断,当前线程来加锁,是否有等待队列,为什么这么说?
//谁知道当前线程是第一个线程还是第N个线程,资源只能由一个线程占有,
//其它的线程怎么办?既然是公平锁,就排队吧,因此有了一个等待队列的概念
//如果,没有等待队列,那么就直接用compareAndSetState(0, acquires) 修改状态
//把c从0修改成1,证明当前线程加锁成功
//这个等待队列怎么判断有没有正在等待的值?
if (!hasQueuedPredecessors() && compareAndSetState(0, acquires)) {
//表示加锁成功,独占资源了,独占锁
setExclusiveOwnerThread(current);
return true;
}
}
//重入锁,文章一开始我有说过重入锁 +1的概念
//因此,Lock支持重入锁的
else if (current == getExclusiveOwnerThread()) {
int nextc = c + acquires;
if (nextc < 0)
throw new Error("Maximum lock count exceeded");
setState(nextc);
return true;
}
return false;
}
}
//这个方法就是上面判断是否有等待队列(等待加锁的线程)的方法
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 这个s是什么东西?
//我翻译了一下这个英文解释,最后一句话的理解 the current thread is first in queue
//当前线程是队列中的第一个,我就是把这个s当成当前线程了 能解释的通
Node s;
//h != t 队列头 不等于 队列尾
//一开始,h是等于t的 为什么?这个是个空队列啊,都是null
// 所以一开始刚进来的时候,h != t 为false,不执行&&后面的代码了,直接返回false(我不需要排队了,直接去 // 获取锁)
//(s = h.next) == null 队尾的下一个元素为null
// s.thread != Thread.currentThread()
//(s = h.next) == null 判断空节点有没有下一个节点 == null 没有下一个节点
return h != t && ((s = h.next) == null || s.thread != Thread.currentThread());
}
//acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
//如果尝试加锁失败了,就要放入队列中去等待了,在这里我要详细说明这个过程
private Node addWaiter(Node mode) {
//把线程放到队列里,不是放线程,而是把线程封装到Node里面
Node node = new Node(Thread.currentThread(), mode);
// Try the fast path of enq; backup to full enq on failure
Node pred = tail;
//当前队列尾巴节点不为null 证明已经有队列了,直接放到当前队列中tail的下一个位置就好了
//一开始进来的时候,队列都没有,pred肯定是null的,不走这个if逻辑
if (pred != null) {
node.prev = pred;
if (compareAndSetTail(pred, node)) {
pred.next = node;
return node;
}
}
//第一个线程进来放入到等待队列,尾结点肯定是null,不走上面的逻辑
enq(node);
return node;
}
// 还没有队列的时候,就要放入元素,怎么放入呢? 就是下面的这个逻辑
//先虚拟出来一个队列 然后把当前Node放入到虚拟队列后面 CAS转换一下位置
//就把当前Node放入到了队列中了,大神的这种思路很厉害的 CAS保证了原子性,不会出现脏数据
//这边有个问题就是:在虚拟队列的时候为什么要 new Node() 这么一个空节点
//这个会在解锁的时候用到,你就耐着性子往下看!!!!!
private Node enq(final Node node) {
for (;;) {
Node t = tail;
//这个时候t == null 证明还没有这个队列,所以我们先要虚拟出来一个队列
//通俗点儿,就是,我们还没有new队列呢
if (t == null) { // Must initialize
//这里就是虚拟处理一个队列 new了一个队列
//这里肯定就有疑问了?if了,else怎么走?
//看看这个 for(;;) 这个东西类似于 while(true)相当于一个无限循环
//当前线程封装的Node 第二次循环 肯定就会进 else了
if (compareAndSetHead(new Node()))
tail = head;
} else {
//当前Node的前一个节点是初始化的new Node() new Node()--Node(当前)
node.prev = t;
//compareAndSetTail 位置换一下 Node -- new Node()
if (compareAndSetTail(t, node)) {
t.next = node;
return t;
}
}
}
}
//由上面知道,这块代码由于尝试加锁失败,并且放入到了等待队列中,才会走到这里的
//Doug Lea 这个逼很优秀的,
//进来线程,尝试加锁,加锁失败,放入到队列中,放入到队列后干什么呢?
//我就要判断我上一个节点是什么状态,如果上一个节点处于park状态,那么我也得park(等待),
//如果上一个节点不是park状态,就得看我在不在队列的头部了,判断我是不是在第二个(因为第一个永远是空)
final boolean acquireQueued(final Node node, int arg) {
boolean failed = true;
try {
boolean interrupted = false;
//加锁,又是一个 while(true)
for (;;) {
//node.predecessor() 拿当前节点的上一个节点
final Node p = node.predecessor();
//如果上一个节点是头部 p == head
//如果上一个节点不是头部 就是走下面的if逻辑了,头部都park了,我也得park 公平锁嘛
//tryAcquire(arg) 又是同样的加锁的过程,不再写了,上面已经写过了
if (p == head && tryAcquire(arg)) {
setHead(node);
p.next = null; // help GC
failed = false;
return interrupted;
}
//这个if就是当前节点的上一个节点不是head,我就得park了 看后面的解释
//这个是通过当前节点得到上一个节点设置为 可以获取锁的状态 并把自己睡眠了(不能获取锁资源的)
if (shouldParkAfterFailedAcquire(p, node) && parkAndCheckInterrupt())
interrupted = true;
}
} finally {
if (failed)
cancelAcquire(node);
}
}
volatile int waitStatus;
static final int SIGNAL = -1;
//这段代码写的就很精彩了,我得认真写一下
private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) {
int ws = pred.waitStatus; // 默认是 0
//ws默认0
//第二次缓存进来就返回这个true了
//具体的,看这个代码块最下面的解释
if (ws == Node.SIGNAL){
return true;
}
if(ws > 0){
do {
node.prev = pred = pred.prev;
} while (pred.waitStatus > 0);
pred.next = node;
}else{
//ws默认0 当前线程直接走这里
//修改状态了,把上一个节点的状态直接修改为 -1 直接返回false了
//问题在于,这个方法是在上一个无限循环中调用的,第一次返回了false
//那么,它还要第二次循环的,第一次把waitStatus从0改成了-1
//第二次,就直接返回true了
compareAndSetWaitStatus(pred, ws, Node.SIGNAL);
}
return false;
}
非公平锁加锁过程
//非公平锁
// 非公平锁和公平锁有一个很大的不一样就是,
//非公平锁 线程一来就先竞争资源,没有抢到,就去排队了了
static final class NonfairSync extends Sync {
final void lock() {
if (compareAndSetState(0, 1)){
setExclusiveOwnerThread(Thread.currentThread());
}else{
acquire(1);
}
}
}
总结:
公平锁和非公平锁:一朝排队永远排队