Java并发系列三《ReentrantLock》

目录

前言

一、ReentrantLock总体概述

二、源码分析

1.ReentrantLock#lock公平锁源码分析

① ReentrantLock的lock公平锁实现方法最终会到 tryAcquire ()方法

 ② 创建排队节点:addWaiter()

③ 以独占不间断模式获取已在队列中的线程:acquireQueued

2.ReentrantLock#unlock源码分析

3.公平锁、非公平锁的异同

总结



前言

本文通过逐步分析ReentrantLock的源码(Java8)来学习其原理。



一、ReentrantLock总体概述

...



二、源码分析


1.ReentrantLock#lock 公平锁源码分析

        逻辑:尝试获取锁,获取成功则直接退出;获取失败则将线程排队,排队后再中断线程。

        ReentrantLock的lock公平锁实现方法会调用到 acquire (int arg)方法,该方法又包含了 tryAcquire() 、addWaiter() 、acquireQueued() ,下面就逐步分析下这几个方法。

public final void acquire(int arg) {
    //获取锁成功:结束。
    //获取锁失败:开始排队。
    if (!tryAcquire(arg) &&
        acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
        selfInterrupt();
}

        tryAcquire():尝试获取锁。

        addWaiter() :线程排队。

        acquireQueued():独占不间断模式获取已在队列中的线程。

①尝试获取锁:tryAcquire()

该方法总体能概括为三步:

  • 第一步:获取当前线程的状态state。
  • 第二步:如果state为0,则当前线程还没有获取到锁。
  • 第三步:如果state不为0时,当前线程已经拥有独占访问权限(即不用排队等操作,已经拿到了锁)。

        获取到锁、没获取到锁之后的过程已经在下例源码中标出:

//尝试获取锁
    protected final boolean tryAcquire(int acquires) {
        final Thread current = Thread.currentThread();
        //获取当前线程的同步状态,state=0锁空闲,获取锁就会+1
        int c = getState();
        //没有获取到锁
        if (c == 0) {
            //如果当前线程之前没有排队线程则:1.通过CAS设置state
            if (!hasQueuedPredecessors() &&
                    compareAndSetState(0, acquires)) {
                //2.设置当前线程为拥有独占访问权限的线程并返回true。即获取锁成功。
                setExclusiveOwnerThread(current);
                return true;
            }
        }
        //当前线程拥有独占访问权限(即不用排队等操作,已经拿到了锁),直接设置state
        else if (current == getExclusiveOwnerThread()) {
            int nextc = c + acquires;
            if (nextc < 0)
                throw new Error("Maximum lock count exceeded");
            setState(nextc);
            return true;
        }
        return false;
    }

另外,再解释下 hasQueuedPredecessors 方法,该方法判断当前线程有没有前继线程。

首先我们想一下怎么样能说明该线程没有前继线程?

        1. 队列为空,如果队列为空说明压根没有初始化排队队列,谈何排队,那么当前线程必定是即将要获取锁的线程。

        2. 队列不为空存在Head节点,但是Head节点的后继节点next为null 或者 后继节点的线程就是当前的线程自己。Head节点的后继节点next为null 的意思就是队列只有Head节点还没有其他节点排队,那么当前线程必定是即将要获取锁的线程;后继节点的线程就是当前的线程自己 的意思就是当前线程是排在Head节点后面的第一个线程,那么理所当然当前线程必定是即将要获取锁的线程。

        下面看一下源代码:

public final boolean hasQueuedPredecessors() {
	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());
}

 ② 线程排队:addWaiter()

分析addWaiter时,我们先看一张图:

用这张图理解线程排队的场景,Head节点不关联线程,其他节点都关联一个线程。

addWaiter法方主要逻辑分为两大步:

  • 第一步:排队队列不为空,将当前节点node的prev指向尾节点tail,然后通过CAS将新节点node设置为尾节点,CAS成功后将原来尾节点的next指向当前节点node。(总的来说就是把新节点node排队为尾节点)
  • 第二步:排队队列为空或者第一步CAS失败,调用enq()方法,主要作用是将节点插入队列直至成功,必要时进行初始化。(第二步与第一步差不多,只是确保线程能排队成功,相当于兜底操作)
//线程排队
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;
}

enq()方法主要做两件事情

  • 1. 初始化,CAS设置一个头节点。
  • 2. 同addWaiter一样,排队操作,区别是会一直尝试直至成功。
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()

acquireQueued() 主要做两件事情:

  • 第一步:判断如果当前线程的prev是head节点并尝试获取锁。当前线程的prev是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)) {
                //头节点指向node节点,node的thread、prev属性为null,即node成为新的头节点
                setHead(node);
                //原来的头节点没用了,设置next引用为null等待GC回收
                p.next = null; // help GC
                failed = false;
                return interrupted;
            }
            //队列中有排队线程或者 if 中获取锁失败,则设置前一个节点的waitStatus=-1
            //并且park当前线程,中断线程
            if (shouldParkAfterFailedAcquire(p, node) &&
                parkAndCheckInterrupt())
                interrupted = true;
        }
    } finally {
        if (failed)
            cancelAcquire(node);
    }
}

这里需要说明一下 waitStatus 属性

        默认waitStatus = 0,线程排队后节点的 waitStatus = 0

        waitStatus = -1 代表后继(下一个)线程需要解停,即需要 unpark 。对应源码中的 SIGNAL

        waitStatus = 1 代表线程已取消 ,对应源码中的 CANCELLED

         线程释放锁的时候会将 waitStatus 设置为 0

④ 操作waitStatus状态进行park:shouldParkAfterFailedAcquire

        该方法主要是 ① 处理已经取消的节点,② 通过park标记被(或即将)阻塞的节点

private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) {
        int ws = pred.waitStatus;
        if (ws == Node.SIGNAL)
            /* 前继节点还在等待触发,还没当前节点的什么事,所以当前节点可以被park*/
            return true;
        if (ws > 0) {
            /* 前继节点被取消需要从同步队列中删除。
               若跳过后的节点还是为CANCELLED ,还需要重复上述步骤
            */
            do {
                node.prev = pred = pred.prev;
            } while (pred.waitStatus > 0);
            pred.next = node;
        } else {
            /* 
               标记被(或即将)阻塞的节点
               如果waitStatus=0或者PROPAGATE,则将waitStatus设置为 -1
             */
            compareAndSetWaitStatus(pred, ws, Node.SIGNAL);
        }
        return false;
    }

2.ReentrantLock#unlock源码分析

释放锁的逻辑比较简单,尝试释放锁成功后就unpark头节点后面的线程。

unpark的操作:从tail节点开始找出队列中排在队列最前面的需要释放锁的线程,进行unpark 。

public final boolean release(int arg) {
    //尝试释放锁
	if (tryRelease(arg)) {
		Node h = head;
		if (h != null && h.waitStatus != 0)
            //释放锁成功后进行unpark线程操作
			unparkSuccessor(h);
		return true;
	}
	return false;
}

private void unparkSuccessor(Node node) {
        
	int ws = node.waitStatus;
	if (ws < 0)
		compareAndSetWaitStatus(node, ws, 0);

    
	Node s = node.next;
    第一个排队节点为空或者此节点的waitStatus>0,即CANCELLED==1的状态
	if (s == null || s.waitStatus > 0) {
		s = null;
        /* 从tail节点开始找出队列中需要释放锁的线程,进行unpark */
		for (Node t = tail; t != null && t != node; t = t.prev)
			if (t.waitStatus <= 0)
				s = t;
	}
	if (s != null)
		LockSupport.unpark(s.thread);
}

3.公平锁、非公平锁的异同









总结

该篇内容主要总结了ReentrantLock的源码内容,进而分析ReentrantLock在获取锁以及释放锁时的原理。以及ReentrantLock公平锁和非公平锁的异同

内容补充中...

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值