多线程高并发系列之ReentrantLock锁的实现原理:AQS

本来这篇想写并发容器的,但是我看了下api文档,没啥深度,没啥可写的,照着api文档的描述敲一遍代码也差不多就懂了。所以也就决定这一片记一下这两天学习的AQS原理。

大家不要一看到原理两字,心里就有点退堂鼓,以为很难的感觉。难道那不是人写的代码吗,他能写出来,你就写不出来吗,也就是一个人的逻辑,明白了他的逻辑不也就懂了代码原理了嘛。

初入博客,有许多描述不清楚、语句不通顺的地方。请提出指正!!!

开始

synchronized锁的实现,它是jvm关键字,直接转换成字节码命令。 ReentrantLock是一个普通的代码类,是java代码通过逻辑来实现锁的效果,也正因为是java代码实现的,所以在灵活度上要比sync锁强。 在讲解是,我会以非公平锁示例来描述他发生的过程,理解了非公平锁,公平锁也自然就理解了。我将以不同的情况来描述Rreentrantlock当时要做的操作。

1. 初始状态下,第一个线程访问:

public static void main(String[] args) {
		Lock rtLock =new ReentrantLock();
		try {
			rtLock.lock();
			TimeUnit.SECONDS.sleep(Integer.MAX_VALUE);
		} catch (InterruptedException e) {
			e.printStackTrace();
		}finally {
			rtLock.unlock();
		}
	}

断点运行这段代码,此时的状态:
在这里插入图片描述
可以看到rtLock中有4个字段:state、head、tail、exclusiveOwnerThread,
state表示的是锁的状态:0表示没有锁,0<state,则表示有锁或锁重入。
head和tail:表示的队列中的节点,Rtlock之所以能实现公平锁,就是因为队列的设计,下面的情节会说。
exclusiveOwnerThread:当前持有锁的线程.
这段是获取锁的源码,通过CAS算法去获取锁,我们这一小节是初始状态的前提下,所以CAS返回值肯定为true,然后设置当前持有锁的线程为当前线程。

final void lock() {
	if (compareAndSetState(0, 1))
		setExclusiveOwnerThread(Thread.currentThread());
	else
		acquire(1);
}

2. RtLock的重入锁 继第一步,我们在main方法中,稍加改动

在这里插入图片描述
看到了state变成了2,看源码是怎么操作的:

final void lock() {
	if (compareAndSetState(0, 1))
		setExclusiveOwnerThread(Thread.currentThread());
	else
		acquire(1);
}

还是这段源码,这次compareAndSetState的CAS返回结果肯定为false,所以执行else中的方法,跟进去:

public final void acquire(int arg) {
		//tryAcquire获取锁,若tryAcquire获取锁失败,则进入
		//acquireQueued方法
        if (!tryAcquire(arg) &&
            acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
            selfInterrupt();
    }
//先跟进去tryAcquire方法
protected final boolean tryAcquire(int acquires) {
		return nonfairTryAcquire(acquires);
	}
final boolean nonfairTryAcquire(int acquires) {
            final Thread current = Thread.currentThread();//当前线程
            int c = getState();//此时锁的状态
            //锁重入,则c肯定不为0,执行else if逻辑
			if (c == 0) {
                if (compareAndSetState(0, acquires)) {
                    setExclusiveOwnerThread(current);
                    return true;
                }
            }
			/**
			*我们在上面说明了,有个表示当前持有锁的线程的字段,
			getExclusiveOwnerThread为得到当前持有锁的线程,
			*判断当前线程与此时持有锁的线程比较是否相等,相等则表示为
			同一个线程,则state加1,返回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;
        }

3. 此时处于上锁状态时,又新来一线程获取锁

在这里插入图片描述

waitStatus在AQS中有5个状态:

	/** waitStatus value to indicate successor's thread needs unparking */
	翻译:waitStatus value to 指示后续线程需要断开连接
        static final int SIGNAL    = -1;
	/** waitStatus value to indicate thread has cancelled */
	翻译:waitStatus value to 指示线程已取消
        static final int CANCELLED =  1;
	/** waitStatus value to indicate thread is waiting on condition */
	翻译:waitStatus value to 指示线程正在等待条件
        static final int CONDITION = -2;
	/** waitStatus value to indicate the next acquireShared should
         * unconditionally propagate*/
	翻译:waitStatus value to 指示下一个acquireShared应无条件传播
        static final int PROPAGATE = -3;
为0时,则以上都不是。
  1. 我们看到head(队列头部)的thread为空、waitstatus为-1以及next指向了下一个节点,thread为空则说明head是AQS创建的一个空节点,只不过,为啥要这么做呢?—程序之所以创建一个空的head节点,就是为了能准确的确定当前节点是否为有效队列中的第一个节点。
  2. tail(队列尾部)的waitstatus为0,上一个节点为head,下一个节点为空,thread为新来的线程。
    记住这张图的信息,然后我们开始看源码:
    继第2步,tryAcquire获取锁方法肯定返回false,然后“!false”为true,则执行acquireQueued(addWaiter(Node.EXCLUSIVE), arg),
    嵌套了一个方法addWaiter,我们进去:
private Node addWaiter(Node mode) {
        Node node = new Node(Thread.currentThread(), mode);//创建一个独占锁的node节点,Node.EXCLUSIVE表示独占锁
		//队列中的尾部节点,由于这个线程是新来的,队列还未创建,所以tail现在肯定为空。
        Node pred = tail;
        //pred为空,则跳过
		if (pred != null) {
            node.prev = pred;//原来队列中的尾部节点,现在变成新节点的上一个节点
            if (compareAndSetTail(pred, node)) {//通过cas算法,加入队列中
                pred.next = node;//原来尾部节点的下一个节点为当前节点
                return node;
            }
        }
        enq(node);//加入队列,进入enq
        return node;
    }

	private Node enq(final Node node) {
        for (;;) {//相当于while(true),最好的写法是for(;;)
            //第一次循环,tail为空,则执行if方法体,第2次循环不为空,
            //则执行else方法体,
			//这里说的第一次循环tail为空,是基于这个新来的线程是第二
			//个竞争锁的线程,
			//如果第3个线程来了,第一个线程还未释放锁,那这个tail肯定
			//不为空。所以不要混淆
			Node t = tail;
            if (t == null) {
           		 //新建一个节点,通过cas算法放入head中。
                if (compareAndSetHead(new Node()))
                	//将头部又赋值给尾部,说明该队列是个循环列表,然
                	//后进行第2次循环
                    tail = head;
            } else {
			//这一步就是给tail重新赋值,将新线程的信息赋值给tail,然
			//后循环结束
                node.prev = t;
                if (compareAndSetTail(t, node)) {
                    t.next = node;
                    return t;
                }
            }
        }
    }

结束,我们看到addWaiter方法就是做了队列的创建,以及将新来的线程加入到创建的队列中,一个特点就是head节点是空的,有效的线程从队列中的第2个元素开始。然后,我们看acquireQueued方法:

	final boolean acquireQueued(final Node node, int arg) {
        boolean failed = true;
        try {
            boolean interrupted = false;
            for (;;) {
           		 //现在队列中虽然有两个节点,但是现在“真正有效”的节点
           		 //只有tail节点,则p为head节点
                final Node p = node.predecessor();
                //现在第一个线程还未释放锁,则p==head为true,
                //tryAcquire(arg)为false,跳过第一个if,执行第二个if
				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);
        }
    }

	 private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) {
        int ws = pred.waitStatus;
		//刚才看到了图片上head的waitStatus为-1,所以返回true
        if (ws == Node.SIGNAL)
            return true;
        if (ws > 0) {
            do {
                node.prev = pred = pred.prev;
            } while (pred.waitStatus > 0);
            pred.next = node;
        } else {
            compareAndSetWaitStatus(pred, ws, Node.SIGNAL);
        }
        return false;
    }
	private final boolean parkAndCheckInterrupt() {
        LockSupport.park(this);
        return Thread.interrupted();
    }

看到源码,我们得到的信息是,去当前节点,上一个节点的waitStatus枚举值,由枚举值决定返回值,然后,parkAndCheckInterrupt方法,是直接将当前线程中断,那么这个if (shouldParkAfterFailedAcquire(p, node) &&parkAndCheckInterrupt())干的就是:由当前节点的上一个节点的状态来决定当前节点是否需要被中断。在这里我想说明一下:我看到了很多博客中说for(;;)是在自旋。确实是自旋,但是与jvm中对锁进行优化时进行的自旋锁有锁不同,jvm中的自旋锁,是为了防止阻塞导致的性能下降而引入的,而这里描述的自旋,仅仅只是自旋,并没有关于性能的优化设计,在这里,如果需要阻塞,就会阻塞。我也因为当时理解错误,找了好久的资料。
对于CLH队列,只是记录了线程的id,在需要唤醒的时候,根据这个id唤醒它,否则根据这个id找到这个线程后让他中断。

4. 看到了AQS的队列操作,再来看看解锁吧

解锁很简单,我不会在这里再那么多话,请自行分析代码

	public void unlock() {
        sync.release(1);
    }
	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);
            }
            setState(c);
            return free;
        }
	//唤醒head节点下一个节点
	private void unparkSuccessor(Node node) {
        int ws = node.waitStatus;
        if (ws < 0)
            compareAndSetWaitStatus(node, ws, 0);
        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);
    }

这是解锁,没啥好说的,可以说一下的就是在解锁之后,程序又去唤醒了head节点的下一个节点。节点在获取锁之后,将当前节点移出队列,就是if (p == head && tryAcquire(arg))中的逻辑。
**在这里,被唤醒的节点,从被中断的位置开始执行,也就是再次开始执行循环for(;;)体,则if (p == head && tryAcquire(arg))有可能成功(因为是非公平锁,所以有可能成功),通过自旋。**这里的自旋与sync锁的优化自旋不同,sync的自旋的设计是不想线程阻塞耗费性能,这里的自旋只是单纯的循环获取锁而已。

总结:当锁已经被占用的时候,非公平锁,会首先去尝试获取锁,如果获取锁失败,则乖乖的去排队。CLH队列,遵循FIFO规则,也就是,只要你入队了,那么你只能被按照公平锁的方式执行。而公平锁与非公平锁在代码上的不同就是在获取锁的时候,先去查看队列中是否有node节点,如果有,那就乖乖排队去,没有就直接获取锁。
另外,aqs又可以实现条件队列来操作对应的线程,详见博客
这只是reentrantlock中的很小的一部分,但是基本的流程逻辑也差不多就这样了,其他的方法多是对一些逻辑方法的强化等。
如果有时间真的可以好好读一读。
CLH队列:是Craig,Landin and Hagersten三个人发明的,所以叫CLH队列

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值