详解-ReentrantLock-底层原理

151 阅读12分钟

ReentrantLock实现关系

Lock接口

在 Lock 接口出现之前,Java中的应用程序对于多线程的并发安全处理只能基于 synchronized 关键字来解决,但是 synchronized 他不灵活。Lock 的出现可以解决 synchronized在某些场景中的短板,它比synchronized更加灵活。

Lock 本质上是一个接口,它定义了释放锁和获得锁的抽象方法,定义成接口就意味着它定义了锁的一个标准规范,也同时意味着锁的不同实现。

  • 实现Lock接口的类有很多,以下为几个常见的锁实现
  1. ReentrantLock:表示重入锁,它是唯一一个实现了Lock接口的类。重入锁指的是线程在获得锁之后,再次获取该锁不需要阻塞,而是直接关联一次计数器增加重入次数
  2. ReentrantReadWriteLock:重入读写锁,它实现了ReadWriteLock接口,在这个类中维护了两个锁,一个是ReadLock,一个是WriteLock,他们都分别实现了Lock接口。读写锁是一种适合读多写少的场景下解决线程安全问题的工具,基本原则是:读和读不互斥、读和写互斥、写和写互斥。也就是说涉及到影响数据变化的操作都会存在互斥。
  3. StampedLock:stampedLock是JDK8引入的新的锁机制,可以简单认为是读写锁的一个改进版本,读写锁虽然通过分离读和写的功能使得读和读之间可以完全并发,但是读和写是有冲突的,如果大量的读线程存在,可能会引起写线程的饥饿。 stampedLock是一种乐观的读策略,使得乐观锁完全不会阻塞写线程。

Lock的接口方法

  1. void lock() // 如果锁可用就获得锁,如果锁不可用就阻塞直到锁释放
  2. void lockInterruptibly() // 和 lock()方法相似, 但阻塞的线程可中断,抛出 java.lang.InterruptedException异常
  3. boolean tryLock() // 非阻塞获取锁;尝试获取锁,如果成功返回true
  4. boolean tryLock(long timeout, TimeUnit timeUnit) //带有超时时间的获取锁方法
  5. void unlock() // 释放锁

sync

  • 锁的同步控制的基础,也就是说获取锁的方法。
  1. 公平锁:FairSync
  2. 非公平锁:NonfairSync

ReentrantLock重入锁详解

重入锁,表示支持重新进入的锁,也就是说,如果当前线程t1通过调用lock方法获取了锁之后,再次调用lock,是不会再阻塞去获取锁的,直接增加重试次数就行了。synchronized和ReentrantLock都是可重入锁。

  • 代码演示一下什么是重入锁

  • 我们假设 synchronized 不支持重入锁,那么一定会成为死锁。因为主线程调用demo()方法,已经拿到了该对象锁,而demo()中又调用了demo2()方法,而demo2()中又得拿该对象锁,而此时该锁已经被拿走了,所以demo2()方法等着demo()方法结束,他才能拿锁,而demo()方法又要执行完demo2()方法才能释放锁。现在死锁了!!!!

  • 因为 synchronized 支持重入锁所以他不会发生上面描述的问题。

ReentrantLock 的实现原理

我们知道锁的基本原理是,基于将多线程并行任务通过某一种机制实现线程的串行执行,从而达到线程安全性的目的。在 synchronized 中,我们分析了偏向锁、轻量级锁、乐观锁。基于乐观锁以及自旋锁来优化了 synchronized 的加锁开销,同时在重量级锁阶段,通过线程的阻塞以及唤醒来达到线程竞争和同步的目的。

那么在ReentrantLock中,也一定会存在这样的需要去解决的问题。就是在多线程竞争重入锁时,竞争失败的线程如何实现阻塞以及如何被唤醒的?

AQS

在 Lock 中,用到了一个同步队列 AQS,全称 AbstractQueuedSynchronizer,它 是一个同步工具也是Lock用来实现线程同步的核心组件。如果你搞懂了AQS,那么J.U.C中绝大部分的工具都能轻松掌握。

  • 可以看到 ReentrantLock 中的Sync属性继承了AQS接口,也就是用来实现线程的同步执行的方法实际上由AQS来完成。

AQS 的内部实现

AQS 队列内部维护的是一个 FIFO 的双向链表,这种结构的特点是每个数据结构都有两个指针,分别指向直接的后继节点和直接前驱节点。所以双向链表可以从任意一个节点开始很方便的访问前驱和后继。每个 Node 其实是由线程封装,当线程争抢锁失败后会封装成Node加入到ASQ队列中去;当获取锁的线程释放锁以后,会从队列中唤醒一个阻塞的节点(线程)。

Node的组成

AQS 的两种功能

从使用层面来说,AQS的功能分为两种:独占和共享

  1. 独占锁:每次只能有一个线程持有锁,比如前面给大家演示的ReentrantLock就是以独占方式实现的互斥锁。
  2. 共享锁:允许多个线程同时获取锁,并发访问共享资源,比如 ReentrantReadWriteLock。

ReentrantLock 的源码分析

  • 以ReentrantLock作为切入点,来看看在这个场景中是如何使用AQS来实现线程的同步的

ReentrantLock 的时序图

  • 调用ReentrantLock中的lock()方法,源码的调用过程我使用了时序图来展现。

  • 这个是 ReentrantLock 获取锁的入口
	public void lock() {     
		sync.lock(); 
	}

sync实际上是一个抽象的静态内部类,它继承了AQS来实现重入锁的逻辑,我们前面说过AQS是一个同步队列,它能够实现线程的阻塞以及唤醒,但它并不具备业务功能,所以在不同的同步场景中,会继承AQS来实现对应场景的功能。

Sync有两个具体的实现类

  1. NofairSync:表示可以存在抢占锁的功能,也就是说不管当前队列上是否存在其他线程等待,新线程都有机会抢占锁
  2. FailSync: 表示所有线程严格按照FIFO来获取锁
  • 以非公平锁为例,来看看lock中的实现
  1. 非公平锁和公平锁最大的区别在于,在非公平锁中我抢占锁的逻辑是,不管有 没有线程排队,我先上来cas去抢占一下
  2. CAS成功,就表示成功获得了锁
  3. CAS失败,调用acquire(1)走锁竞争逻辑
	final void lock() {
    		// 第一次试图插队,如果返回 true 说明此时锁为空
            if (compareAndSetState(0, 1))
            	//设置属性,表示该线程拿到了锁
                setExclusiveOwnerThread(Thread.currentThread());
            else
                acquire(1);
        }
  • CAS实现原理
    protected final boolean compareAndSetState(int expect, int update) {
        // See below for intrinsics setup to support this
        return unsafe.compareAndSwapInt(this, stateOffset, expect, update);
    }

通过 cas 乐观锁的方式来做比较并替换,这段代码的意思是,如果当前内存中的 state 的值和预期值 expect 相等,则替换为update。更新成功返回true,否则返 回 false 这个操作是原子的,不会出现线程安全问题,这里面涉及到 Unsafe 这个类的操作,以及涉及到state这个属性的意义。

state 是 AQS 中的一个属性,它在不同的实现中所表达的含义不一样,对于重入锁的实现来说,表示一个同步状态。它有两个含义的表示

  1. 当 state = 0时,表示无锁状态
  2. 当 state > 0 时,表示已经有线程获得了锁,也就是 state = 1,但是因为ReentrantLock允许重入,所以同一个线程多次获得同步锁的时候,state会递增,比如重入5次,那么state=5。而在释放锁的时候,同样需要释放5次直到 state = 0 其他线程才有资格获得锁。
  • 了解一下CAS 的 C++ 源码
	UNSAFE_ENTRY(jboolean, Unsafe_CompareAndSwapInt(JNIEnv *env, jobject 	unsafe, jobject obj, jlong offset,jint e, jint x))   
	UnsafeWrapper("Unsafe_CompareAndSwapInt");   
	oop p = JNIHandles::resolve(obj);  //将Java对象解析成JVM的oop(普通对象指针),   jint* addr = (jint *) 
	index_oop_from_field_offset_long(p, offset); //根据对象p和地址偏移量找到地址   
	return (jint)(Atomic::cmpxchg(x, addr, e)) == e; //基于cas比较并替换, x表示需要更新的值,addr表示state 在内存中的地址,e表示预期值 UNSAFE_END 
  • 如果成功则设置属性
    protected final void setExclusiveOwnerThread(Thread thread) {
    	//exclusiveOwnerThread 属性设置为当前线程
        //exclusiveOwnerThread:独占模式同步的当前所有者
        exclusiveOwnerThread = thread;
    }
  • 如果失败则进入 accquire 方法
    public final void acquire(int arg) {
        if (!tryAcquire(arg) &&
            acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
            selfInterrupt();
    }

acquire 是 AQS 中的方法,如果 CAS 操作未能成功,说明 state 已经不为 0,此时继续acquire(1)操作

  1. 通过tryAcquire尝试获取独占锁,如果成功返回true,失败返回false
  2. 如果tryAcquire失败,则会通过addWaiter方法将当前线程封装成Node添加到AQS队列尾部
  3. acquireQueued,将Node作为参数,通过自旋去尝试获取锁。
  • tryAcquire() 是一个钩子方法。这个方法的作用是尝试获取锁,如果成功返回true,不成功返回 false 它是重写 AQS 类中的 tryAcquire 方法,
        protected final boolean tryAcquire(int acquires) {
            return nonfairTryAcquire(acquires);
        }
  • nonfairTryAcquire() 方法
  1. 获取当前线程,判断当前的锁的状态
  2. 如果state=0表示当前是无锁状态,通过cas更新state状态的值
  3. 当前线程是属于重入,则增加重入次数
	final boolean nonfairTryAcquire(int acquires) {
    		//拿到当前执行的线程
            final Thread current = Thread.currentThread();
            //拿到 state 的值,如果锁此时不被占用返回0,被占用返回不为0
            int c = getState();
            //判断state是否位0
            if (c == 0) {
            	//第二次试图插队,acquires的值为1
                if (compareAndSetState(0, acquires)) {
                	//当前线程拿到锁
                    setExclusiveOwnerThread(current);
                    //返回true
                    return true;
                }
            }
            //如果当前线程已经拿到锁了,并且又尝试获取锁
            else if (current == getExclusiveOwnerThread()) {
            	//state的值+1,代表重入次数
                int nextc = c + acquires;
                //如果 < 0(不理解什么场景会出现),抛出异常
                if (nextc < 0) // overflow
                    throw new Error("Maximum lock count exceeded");
                //更新state值
                setState(nextc);
                //返回true
                return true;
            }
            //没拿到锁返回 false
            return false;
        }

当 tryAcquire 方法获取锁失败以后,则会先调用 addWaiter 将当前线程封装成 Node 入参 mode 表示当前节点的状态,传递的参数是 Node.EXCLUSIVE,表示独占状态。意味着重入锁用到了AQS的独占锁功能。

  1. 将当前线程封装成Node。
  2. 当前链表中的 tail 节点是否为空,如果不为空,则通过 cas 操作把当前线程的 node添加到AQS队列。
  3. 如果为空或者cas失败,调用enq将节点添加到AQS队列。
    private Node addWaiter(Node mode) {
    	//把当前线程封装成一个Node节点
        Node node = new Node(Thread.currentThread(), mode);
        //tail 是 AQS 中表示同比队列队尾的属性,默认是 null 
        Node pred = tail;
        //如果尾节点不为null
        if (pred != null) {
        	//设置该Node节点的前置节点为尾节点
            node.prev = pred;
            //通过 cas 把 node 加入到 AQS 队列,也就是设置为 tail
            if (compareAndSetTail(pred, node)) {
            	//此时的尾节点已经改变了
            	//设置上一个尾节点的下一个节点为该Node节点
                pred.next = node;
                //返回该线程节点
                return node;
            }
        }
        //如果为null,也就是线程第一次想加入 AQS 队列,对 AQS 队列进行初始化
        enq(node);
        //返回当前线程的节点
        return node;
    }
    // enq 就是通过自旋操作把当前节点加入到队列中 
	private Node enq(final Node node) {
    	//自旋操作
        for (;;) {
        	//拿到尾节点
            Node t = tail;
            //如果尾节点为null
            if (t == null) { // Must initialize
            	//通过 CAS 操作创建一个Node节点并赋给head属性
                if (compareAndSetHead(new Node()))
                	//尾节点设置为head(头)节点,此时头尾属性都指向同一个节点
                    tail = head;
            } else {
            	//尾节点不为null,当前线程的Node节点的前置节点指向头节点
                node.prev = t;
                //通过 CAS 操作把当前节点添加进 AQS 队列  
                if (compareAndSetTail(t, node)) {
                	//头节点的下一个节点指向当前线程的Node节点
                    t.next = node;
                    //返回的是头节点,也就是那个创建的空节点
                    return t;
                }
            }
        }
    }

图解分析

假设3个线程来争抢锁,那么截止到enq方法运行结束之后,或者调用addwaiter方法结束后,AQS中的链表结构图

AQS.acquireQueued 通过 addWaiter 方法把线程添加到链表后,会接着把 Node 作为参数传递给 acquireQueued 方法,去竞争锁

  1. 获取当前节点的prev节点
  2. 如果prev节点为head节点,那么它就有资格去争抢锁,调用tryAcquire抢占锁
  3. 抢占锁成功以后,把获得锁的节点设置为 head,并且移除原来的初始化 head 节点
  4. 如果获得锁失败,则根据waitStatus决定是否需要挂起线程
  5. 最后,通过cancelAcquire取消获得锁的操作
	final boolean acquireQueued(final Node node, int arg) {
    	//设置 failed 标识
        boolean failed = true;
        try {
        	//设置 interrupted 标识
            boolean interrupted = false;
            //自旋操作,此时所有的等待线程都会阻塞在这里
            for (;;) {
            	//拿到该线程的Node节点的 prev 指向的节点
                final Node p = node.predecessor();
                //如果p是头节点 && 拿到锁成功
                if (p == head && tryAcquire(arg)) {
                	//获取锁成功,则把头节点的下一个节点设置为头节点
                    setHead(node);
                    //把原 head 节点从链表中移除                 
                    p.next = null; // help GC             
                    failed = false;
                    //返回false,结束自旋
                    return interrupted;
                }
                //给线程设置一个标识
                if (shouldParkAfterFailedAcquire(p, node) &&
                	//判断是否有过中断操作
                    parkAndCheckInterrupt())
                    //并且设置当前线程的 interrupted 属性为 true,代表不能被中断
                    interrupted = true;
            }
        } finally {
            if (failed)
                cancelAcquire(node);
        }
    }

shouldParkAfterFailedAcquire

如果ThreadA的锁还没有释放的情况下,ThreadB和ThreadC来争抢锁肯定是会失败,那么失败以后会调用shouldParkAfterFailedAcquire方法 Node 有5种状态

  1. CANCELLED:在同步队列中等待的线程等待超时或被中断,需要从同步队列中取消该Node的结点, 其结点的waitStatus为CANCELLED,即结束状态,进入该状态后的结点将不会再变化。
  2. SIGNAL:只要前置节点释放锁,就会通知标识为SIGNAL状态的后续节点的线程
  3. CONDITION:和Condition有关系,
  4. PROPAGATE:共享模式下,PROPAGATE状态的线程处于可运行状态
  5. 默认状态:

这个方法的主要作用是,通过 Node 的状态来判断,ThreadA 竞争锁失败以后是否应该被挂起。

  1. 如果ThreadA的pred节点状态为SIGNAL,那就表示可以放心挂起当前线程
  2. 通过循环扫描链表把CANCELLED状态的节点移除
  3. 修改pred节点的状态为SIGNAL,返回false. 返回false时,也就是不需要挂起,返回true,则需要调用 parkAndCheckInterrupt 挂起当前线程
  private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) {
 		//拿到前置节点的 waitStatus   
        int ws = pred.waitStatus;
        //如果前置节点为SIGNAL状态,返回true
        if (ws == Node.SIGNAL)
            return true;
        //ws 大于 0,意味着 prev 节点取消了排队,直接移除这个节点就行 
        if (ws > 0) {
            do {
                node.prev = pred = pred.prev;
              //相当于: pred=pred.prev; node.prev=pred; 
            } while (pred.waitStatus > 0);
            //这里采用循环,从双向列表中移除 CANCELLED 的节点 
            pred.next = node;
        } else {
        	//利用 cas 设置 prev 节点的状态为 SIGNAL(1)
            compareAndSetWaitStatus(pred, ws, Node.SIGNAL);
        }
        // 返回 false
        return false;
    }

parkAndCheckInterrupt

使用LockSupport.park挂起当前线程编程WATING状态 Thread.interrupted,返回当前线程是否被其他线程触发过中断请求,也就是 thread.interrupt(); 如果有触发过中断请求,那么这个方法会返回当前的中断标识 true,并且对中断标识进行复位标识已经响应过了中断请求。如果返回true,意味着在acquire方法中会执行selfInterrupt()。

    private final boolean parkAndCheckInterrupt() {
    	//把当前线程阻塞,简单理解成wait()方法即可,所有的线程都被阻塞在此处
        LockSupport.park(this);
        //返回中断标识
        return Thread.interrupted();
    }
  • 图解分析

通过acquireQueued方法来竞争锁,如果ThreadA还在执行中没有释放锁的话,意味着ThreadB和ThreadC只能挂起了。

  • 最后如果有中断操作则执行 selfInterrupt() 方法。
    static void selfInterrupt() {
    	// 中断线程
        Thread.currentThread().interrupt();
    }
  • 解释一下在 AQS 中断操作的意思。

举个例子:假如A和B在公司上班,A和B需要合作完成一个项目,但是只有一个电脑,那么A和B只能排队使用电脑完成自己的任务,A先去敲代码,B此时没事干就去休假了。但是公司想要把B辞退,现在辞退不了啊,因为B去休假了不在公司,所以公司就记录一下辞退B的任务。B休假结束会公司了,公司发现有一个辞退任务,ok,此时再把B辞退。

ReentrantLock 的 unLock() 操作

    public void unlock() {
    	//调用 AQS 的release()方法
        sync.release(1);
    }
    public final boolean release(int arg) {
    	//如果释放锁成功
        if (tryRelease(arg)) {
        	//拿到此时的头节点
            Node h = head;
            //如果不为null && 且节点状态 != 0
            if (h != null && h.waitStatus != 0)
                //调用unparkSuccessor()方法,唤醒某个线程
                unparkSuccessor(h);
            return true;
        }
        return false;
    }

ReentrantLock.tryRelease

这个方法可以认为是一个设置锁状态的操作,通过将state状态减掉传入的参数值(参数是1),如果结果状态为0,就将排它锁的Owner设置为null,以使得其它的线程有机会进行执行。在排它锁中,加锁的时候状态会增加 1(当然可以自己修改这个值),在解锁的时候减掉1,同一个锁,在可以重入后,可能会被叠加为2、 3、 4这些值,只有unlock() 的次数与 lock()的次数对应才会将 Owner 线程设置为空,而且也只有这种情况下才会返回true。

	protected final boolean tryRelease(int releases) {
    		//拿到此时 State属性 - 1 的值 
            int c = getState() - releases;
            //如果当前线程不为拿到锁的线程则报错
            if (Thread.currentThread() != getExclusiveOwnerThread())
                throw new IllegalMonitorStateException();
            //定义 free 标识
            boolean free = false;
            //如果为0,标识释放锁成功
            if (c == 0) {
                free = true;
                //设置拿到锁的线程为null
                setExclusiveOwnerThread(null);
            }
            //更新 state 的值
            setState(c);
            //返回释放锁的结果 true/false
            return free;
        }

unparkSuccessor

private void unparkSuccessor(Node node) {
	    //拿到头节点的状态
        int ws = node.waitStatus;
        if (ws < 0)
        	//使用 CAS 操作设置头节点的状态为 0
            compareAndSetWaitStatus(node, ws, 0);
        //拿到头节点的下一个节点
        Node s = node.next;
        //如果下一个节点为null || 下一个节点的状态 > 0
        if (s == null || s.waitStatus > 0) {
            s = null;
            //通过从尾部节点开始扫描,找到距离 head 最近的一个 waitStatus<=0 的节点 
            /**
             * 为什么从尾部遍历,因为在插入 AQS 队列的时候,先修改的尾节点,在设置上一个尾节点的next指向新的尾节点。
             * 如果在这个过程中,发生了锁的释放,有可能会发生next -> null 的情况
             */
            for (Node t = tail; t != null && t != node; t = t.prev)
                if (t.waitStatus <= 0)
                    s = t;
        }
        if (s != null)
        	//唤醒线程(简单理解为notify()方法)
            LockSupport.unpark(s.thread);
    }
  • 图解分析

  1. 设置新head节点的prev=null
  2. 设置原head节点的next节点为null
  3. 执行被唤醒的线程的方法

最后再简单看一下公平锁

锁的公平性是相对于获取锁的顺序而言的,如果是一个公平锁,那么锁的获取顺序 就应该符合请求的绝对时间顺序,也就是 FIFO。

 	static final class FairSync extends Sync {
        private static final long serialVersionUID = -3000897897090466540L;

        final void lock() {
          	//非公平锁在获取锁的时候,会先通过CAS进行抢占,而公平锁则不会。
            acquire(1);
        }
  • 公平锁的 tryAcquire 也有点不同

他判断了同步队列中当前节点是否有前驱节点的判断,如果该方法返回true,则表示有线程比当前线程更早地请求获取锁,因此需要等待前驱线程获取并释放锁之后才能继续获取锁。

	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;
        }