java锁机制ReentrantLock源码实例分析

2022-11-13 18:11:52 源码 实例 机制

一:简述

ReentrantLock是java.util.concurrent包中提供的一种锁机制,它是一种可重入,互斥的锁,ReentrantLock还同时支持公平和非公平两种实现。本篇文章基于java8,对并发工具ReentrantLock的实现原理进行分析。

二:ReentrantLock类图

三:流程简图

四:源码分析

我们以lock()方法和unlock()方法为入口对ReentrantLock的源码进行分析。

lock()源码分析:

ReentrantLock在构造构造方法创建对象的时候会根据构造函数传递的fair参数 创建不同的Sync对象(默认是非公平锁的实现),reentrantLock.lock()会调用sync.lock()。在Sync类中的lock()方法是一个抽象方法,具体实现分别在FairSync(公平)和NonfairSync(非公平)中。

    public ReentrantLock() {
        //默认是非公平锁
        sync = new NonfairSync();
    }
    public ReentrantLock(boolean fair) {
        sync = fair ? new FairSync() : new NonfairSync();
    }
    public void lock() {
        sync.lock();
    }

非公平实现:

       final void lock() {
            // state表示锁重入次数 0代表无锁状态
            //尝试抢占锁一次  利用cas替换state标志 替换成功代表抢占锁成功
            if (compareAndSetState(0, 1))
		//抢占成功将抢占到锁的线程设置为当前线程
                setExclusiveOwnerThread(Thread.currentThread());
            else
                acquire(1);
        }

公平锁实现:

	final void lock() {
            acquire(1);
        }

可以看出非公平锁在lock()方法开始会尝试cas抢占一次锁,也就是在这里会插队一次。然后都会调用acquire()方法。acquire()方法中调用了三个方法,tryAcquire(),addWaiter(),acquireQueued()三个方法,而这三个方法正是ReentrantLock加锁的核心方法,接下来我们会对这三个方法进行重点的分析。

public final void acquire(int arg) {
        if (!tryAcquire(arg) &&
            acquireQueued(addWaiter(node.EXCLUSIVE), arg))		
	//这里acquireQueued()是一步步将线程是否中断的标志传回来的 如果为true 那么代表线程被中断过 需要设置中断标志 交给客户端处理
	// 因为LockSupport.park()之后不会对中断进行响应 所以需要一步一步将中断标记传回来
            selfInterrupt();
        }

tryAcquire()方法

流程图:

tryAcquire()方法是抢占锁的方法,返回ture表示线程抢占到锁了,如果抢占到锁就什么都不做,直接执行同步代码,如果没有抢占到锁就需要将线程的信息保存起来,并且阻塞线程,也就是调用addWaiter()方法和acquireQueued()方法。

tryAcquire()也有公平和非公平锁两种实现。

公平锁实现:

	protected final boolean tryAcquire(int acquires) {
	    //获取当前线程
            final Thread current = Thread.currentThread();
	    //获取state的值
            int c = getState();
            if (c == 0) {
	    //如果state为0 代表现在是无锁状态 那么可以抢占锁
	    //公平锁这里多了一个判断 只有在AQS链表中不存在元素 才去尝试抢占锁 否则就去链表中排队
                if (!hasQueuedPredecessors() &&
                    compareAndSetState(0, acquires)) {
		//cas替换成功 那么就代表抢占锁成功了 将获取锁的线程设置为当前线程
                    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;
        }
    }

非公平锁实现:

protected final boolean tryAcquire(int acquires) {
            return nonfairTryAcquire(acquires);
    }
	final boolean nonfairTryAcquire(int acquires) {
            final Thread current = Thread.currentThread();
            int c = getState();
            if (c == 0) {
		//如果state为0 代表现在是无锁状态 非公平锁这里就直接尝试抢占锁
		// 公平锁这里多了一个判断 先去看AQS链表中有没有已经在等待的线程 有的话就不会尝试去抢占锁了,非公平锁这里没有这个判断,也就是这里允许插队(不公平)
                if (compareAndSetState(0, acquires)) {	
		//cas替换成功 那么就代表抢占锁成功了 将获取锁的线程设置为当前线程
                    setExclusiveOwnerThread(current);
                    return true;
                }
            }
	   //判断获取锁的线程是否是当前线程 如果是的话增加重入次数 (即增加state的值)
            else if (current == getExclusiveOwnerThread()) {
                int nextc = c + acquires;
                if (nextc < 0) // overflow
                    throw new Error("Maximum lock count exceeded");
                //重新设置state的值    
                setState(nextc);
                return true;
            }
            return false;
        }

接下来分析addWaiter()方法

addWaiter()

addWaiter()方法的作用就是将没有获取到锁的线程封装成一个Node对象,然后存储在AbstractQueuedSynchronizer这个队列同步器中(为了偷懒简称AQS)。我们先看一下Node对象的结构。

    static final class Node {
        static final Node SHARED = new Node();
        static final Node EXCLUSIVE = null;
        static final int CANCELLED =  1;
        static final int SIGNAL    = -1;
        static final int CONDITioN = -2;
        static final int PROPAGATE = -3;
        volatile int waitStatus;
        volatile Node prev;
        volatile Node next;
        volatile Thread thread;
        Node nextWaiter;
        final boolean isshared() {
            return nextWaiter == SHARED;
        }
        final Node predecessor() throws NullPointerException {
            Node p = prev;
            if (p == null)
                throw new NullPointerException();
            else
                return p;
        }
        Node() {    // Used to establish initial head or SHARED marker
        }
        Node(Thread thread, Node mode) {     // Used by addWaiter
            this.nextWaiter = mode;
            this.thread = thread;
        }
        Node(Thread thread, int waitStatus) { // Used by Condition
            this.waitStatus = waitStatus;
            this.thread = thread;
        }
    }

通过Node对象的结构我们可以看出这是一个双向链表的结构,保存了prev和next引用,thread成员变量用来保存阻塞的线程引用,并且node是有状态的,分别是CANCELLED,SIGNAL,CONDITION,PROPAGATE,其中ReentrantLock涉及到的状态就是SIGNAL(等待被唤醒),CANCELLED(取消)(由于相关性不强,其他的状态在后续文章用到的时候在讲吧)。而AQS又保存了链表的头结点head和尾结点tail,所以实际上AQS存储阻塞线程的数据结构是一 个Node双向链表。addWaiter()方法的作用就是将阻塞线程封装成Node并且将其保存在AQS的链表结构中。

流程图:

源码分析:

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;
	//如果AQS尾结点不为null 代表AQS链表已经初始化 尝试将构建好的节点添加到链表的尾部
        if (pred != null) {
            node.prev = pred;
	    //cas替换AQS的尾结点 
            if (compareAndSetTail(pred, node)) {
                pred.next = node;
                return node;
            }
        }
	//没有初始化调用enq()方法
        enq(node);
        return node;
    }
private Node enq(final Node node) {
	    //自旋
        for (;;) {
            Node t = tail;
	    //尾结点为空 说明AQS链表还没有初始化 那么进行初始化
            if (t == null) { // Must initialize
	    //cas 将AQS的head节点 初始化 成功初始化head之后,将尾结点也初始化
            //注意 这里我们可以看到head节点是不存储线程信息的 也就是说head节点相当于是一个虚拟节点
                if (compareAndSetHead(new Node()))
                    tail = head;
            } else {
		//尾结点不为空 那么直接添加到链表的尾部即可
                node.prev = t;
                if (compareAndSetTail(t, node)) {
                    t.next = node;
                    return t;
                }
            }
        }
    }

接下来分析acquireQueued()

acquireQueued()

acquireQueued()方法的作用就是利用Locksupport.park()方法将AQS链表中存储的线程阻塞起来。

流程图:

源码分析:

final boolean acquireQueued(final Node node, int arg) {
        boolean failed = true;
        try {
            boolean interrupted = false;
	    //进入自旋
            for (;;) {
		//获取当前节点的前一个节点
                final Node p = node.predecessor();
		// 如果前一个节点是head 而且再次尝试获取锁成功,将节点从AQS队列中去除 并替换head 同时返回中断标志
                if (p == head && tryAcquire(arg)) {
                    setHead(node);
                    p.next = null; // help GC
                    failed = false;
                    //注意 只有抢占到锁才会跳出这个for循环
                    return interrupted;
                }
                //去除状态为CANCELLED的节点 并且阻塞线程 线程被阻塞在这
                //注意 线程被唤醒之后继续执行这个for循环 尝试抢占锁 没有抢占到的话又会阻塞
                if (shouldParkAfterFailedAcquire(p, node) &&
                    parkAndCheckInterrupt())
                    interrupted = true;
            }
        } finally {
            if (failed)
                //如果失败 将node状态修改为CANCELLED
                cancelAcquire(node);
        }
    }

在acquireQueued()方法中有两个方法比较重要shouldParkAfterFailedAcquire()方法和parkAndCheckInterrupt()方法。

shouldParkAfterFailedAcquire()

private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) {
        int ws = pred.waitStatus;
        if (ws == Node.SIGNAL)
	    //如果节点是SIGNAL状态 不需要处理 直接返回
            return true;
        if (ws > 0) {
            
	   //如果节点状态>0 说明节点是取消状态 这种状态的节点需要被清除 用do while循环顺便清除一下前面的连续的、状态为取消的节点
            do {
                node.prev = pred = pred.prev;
            } while (pred.waitStatus > 0);
            pred.next = node;
        } else {
            
	    //正常的情况下 利用cas将前一个节点的状态替换为 SIGNAL状态 也就是-1
	    //注意 这样队列中节点的状态 除了最后一个都是-1 包括head节点
            compareAndSetWaitStatus(pred, ws, Node.SIGNAL);
        }
        return false;
    }

parkAndCheckInterrupt()

private final boolean parkAndCheckInterrupt() {
  //挂起当前线程 并且返回中断标志  LockSupport.park(thread) 会调用UNSAFE.park()方法将线程阻塞起来(是一个native方法)
        LockSupport.park(this);
        return Thread.interrupted();
    }

到这里lock()方法也就分析完了 接下来我们分析unlock()方法

unlock()方法源码分析:

流程图:

reentrantLock的unlock()方法会调用sync的release()方法。

public void unlock() {
	    //每次调用unlock 将state减1
        sync.release(1);
}

release()方法有两个方法比较重要,tryRelease()方法和unparkSuccessor(),tryRelease()方法计算state的值,看线程是否已经彻底释放锁(这是因为ReentrantLock是支持重入的),如果已经彻底释放锁那么需要调用unparkSuccessor()方法来唤醒线程,否则不需要唤醒线程。

public final boolean release(int arg) {
	//只有tryRelease返回true 说明已经释放锁 需要将阻塞的线程唤醒 否则不需要唤醒别的线程
        if (tryRelease(arg)) {
            Node h = head;
	//如果头结点不为空 而且状态不为0 代表同步队列已经初始化 且存在需要唤醒的node
	//注意 同步队列的头结点相当于是一个虚拟节点  这一点我们可以在构建节点的代码中很清楚的知道
	//并且在shouldParkAfterFailedAcquire方法中 会把head节点的状态修改为-1
	//如果head的状态为0 那么代表队列中没有需要被唤醒的元素 直接返回true
            if (h != null && h.waitStatus != 0)
		//唤醒头结点的下一个节点
                unparkSuccessor(h);
            return true;
        }
        return false;
    }

tryRelease()

protected final boolean tryRelease(int releases) {
	     //减少重入次数
            int c = getState() - releases;
	    //如果获取锁的线程不是当前线程 抛出异常
            if (Thread.currentThread() != getExclusiveOwnerThread())
                throw new IllegalMonitorStateException();
            boolean free = false;
	    //如果state为0 说明已经彻底释放了锁 返回true
            if (c == 0) {
                free = true;
		//将获取锁的线程设置为null
                setExclusiveOwnerThread(null);
            }
	    //重置state的值
            setState(c);
	    //如果释放了锁 就返回true 否则返回false
            return free;
        }

unparkSuccessor()

private void unparkSuccessor(Node node) {
        //获取头结点的状态 将头结点状态设置为0 代表现在正在有线程被唤醒 如果head状态为0 就不会进入这个方法了
        int ws = node.waitStatus;
        if (ws < 0)
            //将头结点状态设置为0
            compareAndSetWaitStatus(node, ws, 0);
        
	//唤醒头结点的下一个状态不是cancelled的节点 (因为头结点是不存储阻塞线程的)
        Node s = node.next;
	//当前节点是null 或者是cancelled状态
        if (s == null || s.waitStatus > 0) {
            s = null;
	 //从aqs链表的尾部开始遍历 找到离头结点最近的 不为空的 状态不是cancelled的节点 赋值给s 这里为什么从尾结点开始遍历而是头结点 应该是因为添加结点的时候是先初始化结点的prev的, 从尾结点开始遍历 不会出现prve没有赋值的情况 
            for (Node t = tail; t != null && t != node; t = t.prev)
                if (t.waitStatus <= 0)
                    s = t;
        }
        if (s != null)
	    //调用LockSupport.unpark()唤醒指定的线程
            LockSupport.unpark(s.thread);
    }	

五:总结

最后总结一下,有以下几点需要注意:

1.ReentrantLock有公平锁和非公平锁两种实现,其实这两种实现的差别只有两点,第一是在lock()方法开始的时候非公平锁会尝试cas抢占锁插一次队, 第二是在tryAcquire()方法发现state为0的时候,非公平锁会抢占一次锁,而公平锁会判断AQS链表中是否存在等待的线程,没有等待的线程才会去抢占锁。

2.AQS存储阻塞线程的数据结构是一个双向链表的结构,而且它是遵循先进先出的,因为它是从头结点的下一个节点开始唤醒,而添加新的节点的时候是添加到链表的尾部,所以AQS同时也是一个队列的数据结构。

3.线程被唤醒之后会继续执行acquireQueued()方法,因为它是阻塞在acquireQueued()方法的for循环中的,唤醒后尝试去获取锁,获取成功就会将节点从AQS中删除并跳出for循环,否则又会继续阻塞。(获取锁失败的原因就是因为有人插队啊。。也就是非公平锁导致的)。

以上就是java锁机制ReentrantLock源码实例分析的详细内容,更多关于java锁机制ReentrantLock的资料请关注其它相关文章!

相关文章