Java并发编程|第六篇:ReentrantLock 重入锁

Java并发编程|第六篇:ReentrantLock 重入锁

系列文章

前言

JUC >>>>>>>> java.util.concurrent.* 下面的工具类

重入锁源码中的很多知识点(AQS,CAS…),调用的方法(acquire(),tryAcquire(),enq(node)…),在JUC系列里面的其他工具类中都有多处使用;因此本文的学习非常重要,有助于更好的理解和学习JUC后面的源码分析系列。

场景

例子1

经典例子count++1000次

创建1000个线程,每个线程操作变量 count++
count 打印结果不确定的原因可以参考Java并发编程|第三篇:synchronized锁

public class ReentrantLockDemo implements Runnable {

    private static int count = 0;

    public static void main(String[] args) throws InterruptedException {
        for (int i = 0; i < 1000; i++) {
            new Thread(new ReentrantLockDemo()).start();
        }
        Thread.sleep(3000);
        System.out.println("count:" + count);
    }

    @Override
    public void run() {
            try {
                Thread.sleep(1);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            count++;
        }
}

执行结果:957(每次结果不确定)
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-Kn9Y7fQq-1576307556795)(C:\Users\Administrator\AppData\Roaming\Typora\typora-user-images\image-20191214150326796.png)]

使用synchronized关键字解决

当然有多种使用方法,本文用的是new object()对象作为lock的方式。

public class ReentrantLockDemo32 implements Runnable {

    private Object lock;

    public ReentrantLockDemo32(Object lock) {
        this.lock = lock;
    }

    private static int count = 0;

    public static void main(String[] args) throws InterruptedException {
        Object lock = new Object();
        for (int i = 0; i < 1000; i++) {
            new Thread(new ReentrantLockDemo32(lock)).start();
        }
        Thread.sleep(3000);
        System.out.println("count:" + count);
    }

    @Override
    public void run() {
        synchronized (lock) {
            try {
                Thread.sleep(1);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            count++;
        }
    }
}

执行结果:1000
在这里插入图片描述

使用ReentrantLock重入锁解决
public class ReentrantLockDemo3 implements Runnable {

    private static ReentrantLock lock = new ReentrantLock();

    private static int count = 0;

    public static void main(String[] args) throws InterruptedException {
        for (int i = 0; i < 1000; i++) {
            new Thread(new ReentrantLockDemo3()).start();
        }
        Thread.sleep(3000);
        System.out.println("count:" + count);
    }

    @Override
    public void run() {
        lock.lock();
        try {
            Thread.sleep(1);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        count++;
        lock.unlock();
    }
}

执行结果:1000
在这里插入图片描述

例子2

什么是重入?

同一个线程

  • 在demo()中获得lock锁
  • 在demo2()中再次获得同一把lock锁
重入锁的设计目的

调用 demo 方法获得了lock对象锁,然后在这个方法中再去调用demo2,demo2 中的存在同一把lock对象锁,这个时候如果当前线程因为无法获得 demo2 的lock对象锁而阻塞,就会产生死锁;重入锁的设计目的是避免线程的死锁。

public class ReentrantLockDemo {

    private Object lock;

    public ReentrantLockDemo(Object lock) {
        this.lock = lock;
    }

    public void demo() {//获取对象锁
        synchronized (lock) {
            System.out.println(Thread.currentThread().getName() + ":demo");
            demo2();
        }

    }

    public void demo2() {
        synchronized (lock) {//再次获取对象锁
            System.out.println(Thread.currentThread().getName() + ":demo2");
        }
    }


    public static void main(String[] args) {
        final Object lock = new Object();
        for (int i = 0; i < 10; i++) {
            new Thread(() ->
                    new ReentrantLockDemo(lock).demo()
                    , "Thread" + i).start();
        }
    }
}
使用ReentrantLock重入锁改写上面的例子

重入锁可以有效的防止死锁的产生

public class ReentrantLockDemo4 {

    private Lock lock;

    public ReentrantLockDemo4(Lock lock) {
        this.lock = lock;
    }
    
    public void demo() {//获取对象锁
        lock.lock();
        demo2();
        System.out.println(Thread.currentThread().getName() + ":demo");
        lock.unlock();
    }

    public void demo2() {
        lock.lock();//再次获取对象锁
        System.out.println(Thread.currentThread().getName() + ":demo2");
        lock.unlock();
    }

    public static void main(String[] args) {
        final ReentrantLock lock = new ReentrantLock();
        for (int i = 0; i < 10; i++) {
            new Thread(() ->
                    new ReentrantLockDemo4(lock).demo()
                    , "Thread" + i).start();
        }
    }
}

1.类图分析

在这里插入图片描述

  • ReentrantLock 实现了 Lock 接口

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 接口出现之前,Java 中的应用程序对于多线程的并发安全处理只能基于synchronized 关键字来解决。但是 synchronized 在有些场景中会存在一些短板,也就是它并不适合于所有的并发场景。但是在 Java5 以后,Lock 的出现可以解决synchronized 在某些场景中的短板,它比 synchronized 更加灵活。

  • Sync实际上是 ReentrantLock 中的一个抽象的静态内部类,它继承了AQS(AbstractQueuedSynchronizer)来实现重入锁的逻辑

  • Sync有两个具体的实现

    • NonfairSync(非公平锁):表示可以存着抢占锁的功能,不管当前线程是有存着其他线程等待,新线程都有机会抢占锁
    • FairSync(公平锁):表示所有线程严格按照 FIFO(先进先出) 来获取锁

2.预备知识

2.1 什么是AQS

在 Lock 中,用到了一个同步队列 AQS,全称 AbstractQueuedSynchronizer,它是一个同步工具也是 Lock 用来实现线程同步的核心组件。

AQS 的两种功能

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

  • 独占锁,每次只能有一个线程持有锁,ReentrantLock 就是以独占方式实现的互斥锁

  • 共享锁,允许多个线程同时获取锁,并发访问共享资源 , 比如ReentrantReadWriteLock

AQS内部实现

AQS 队列内部维护的是一个 FIFO 的双向链表,这种结构的特点是每个数据结构都有两个指针,分别指向上一个节pre节点和下一个next节点;从链表的任意一个节点开始访问都很方便,每个Node节点其实都是由线程封装,当线程竞争锁失败之后会封装成Node节点 加入到 AQS 的队列中;当线程释放锁会从队列中唤醒一个阻塞的节点(线程)。
在这里插入图片描述

2.2 CAS实现原理

  • C:compare 比较

  • A:and

  • S:swap 替换

我们以java.util.concurrent.locks.AbstractQueuedSynchronizer#compareAndSetState来分析

    protected final boolean compareAndSetState(int expect, int update) {
        // See below for intrinsics setup to support this
        return unsafe.compareAndSwapInt(this, stateOffset, expect, update);
    }

unsafe中的compareAndSwapInt方法是一个native的方法,有兴趣可以去研究openjdkc++源码

public final native boolean compareAndSwapInt(Object var1, long var2, int var4, int var5);

这段代码的意思是,通过cas乐观锁的方式来做比较并替换,如果当前内存中state的值和预期值expect相等,则替换为update值。如果更新成功返回true,否则返回false

这个操作是原子的,不会出现线程安全问题,这里面涉及到unsafe这个类的操作,以及state属性的一样。

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

    • state=0 ,表示无锁状态
    • state>=1,表示有锁状态,比如state=1,但是因为ReentrantLock允许重入,所有同一个线程多次获得同步锁时,state会递增,比如state=5;而在释放锁的时候,同样需要依次释放锁直到state=0时,其他线程才能获取锁。
  • Unsafe 类

Unsafe 类是在 sun.misc 包下,不属于 Java 标准。但是很多 Java 的基础类库,包括一些被广泛使用的高性能开发库都是基于 Unsafe 类开发的,比如 Netty、Hadoop、Kafka 等。

Unsafe 可认为是 Java 中留下的后门,提供了一些低层次操作,如直接内存访问、线程的挂起和恢复、CAS、线程同步、内存屏障。

而 CAS 就是 Unsafe 类中提的一个原子操作,第一个参数为需要改变的对象,第二个为偏移量(即之前求出来的 headOffset 的值),第三个参数为期待的值,第四个为更新后的值整个方法的作用是如果当前时刻的值等于预期值 var4 相等,则更新为新的期望值 var5,如果更新成功,则返回 true,否则返回 false。

3.ReentrantLock 源码分析

3.1 加锁流程

lock.lock() 时序图

在这里插入图片描述

lock()入口

ReentrantLock.lock() 方法是锁的入口方法

这里以NonfairSync的实现为例

       final void lock() {
            //通过cas来实现原子性,返回true表示,获取lock成功
            //如果第一个线程A访问 state默认状态为0 cas操作后变为1
            //state锁标记 0:无锁状态 >=1:有锁状态 
            if (compareAndSetState(0, 1))
                setExclusiveOwnerThread(Thread.currentThread());
            else
                acquire(1);
        }

线程A访问,CAS成功获取锁

CAS:compareAndSetState(0, 1)成功返回true

  • 设置 state=0 -> state=1
  • 设置setExclusiveOwnerThread = 当前线程A
    在这里插入图片描述
acquire()

线程B再访问时,CAS失败会走else中的逻辑acquire(1);

acquire逻辑如下

    public final void acquire(int arg) {
        if (!tryAcquire(arg) &&
            acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
            selfInterrupt();
    }
tryAcquire()

第一:tryAcquire(arg),这里的nonfairTryAcquire是非公平锁的实现

  • 尝试获取独占锁,如果成功返回 true,失败返回 false
  • 如果当前执行线程和之前获取锁的线程是同一个线程,增加重入次数
        final boolean nonfairTryAcquire(int acquires) {
            //假设当前线程是线程B
            final Thread current = Thread.currentThread();
            //再次获取lock的状态
            int c = getState();
            //如果为0 表示无锁 继续走之前的获取锁的逻辑
            if (c == 0) {
                if (compareAndSetState(0, acquires)) {
                    setExclusiveOwnerThread(current);//设置当前获得锁的线程
                    return true;
                }
            }
            //如果线程A的lock还没有释放 而且当前线程B=线程A 表示是同一个线程
            //重入锁的概念,增加重入次数
            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;
        }

如果第一方法tryAcquire返回false表示是不同线程,继续走后面的逻辑acquireQueued(addWaiter(Node.EXCLUSIVE), arg)

addWaiter ()

addWaiter 方法将当前线程封装成 Node节点 添加到 AQS 队列尾部

  • addWaiter(Node.EXCLUSIVE)
    • Node 是 AQS 中的一个双向链表结构
    • EXCLUSIVE表示该node为独占节点
    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;
        //第二次 线程B进来不走这段逻辑 因为tail尾节点 为null
        if (pred != null) {//如果尾节点(为线程B)不为空 线程C进来
            node.prev = pred;//设置当前节点的上一个节点为尾节点(线程B节点)
            if (compareAndSetTail(pred, node)) {//设置尾节点为线程C
                pred.next = node;//设置线程B节点的下一个节点为线程C节点
                return node;
            }
        }
        enq(node);
        return node;
    }
enq(node)

竞争锁失败的Node(线程)添加到 AQS 链表中

    private Node enq(final Node node) {
        for (;;) {//自旋
            Node t = tail;
            //如果tail节点为空
            if (t == null) { // Must initialize
                //初始化一个head节点,设置tail节点 = head节点
                if (compareAndSetHead(new Node()))
                    tail = head;
            } else {
                //表示AQS队列中已经存在节点 
                //设置当前节点的上一个节点为tail节点
                node.prev = t;
                if (compareAndSetTail(t, node)) {//cas设置node节点为tail节点
                    t.next = node;//cas成功 设计tail节点的下一个节点为node
                    return t;
                }
            }
        }
    }

线程B进来,cas失败,第一步 初始化一个head节点 并设置 head = tail = new Node()
在这里插入图片描述

第二步:线程B自旋没有获取到锁 addWaiter 添加到链表的尾部

第三步:线程C进来,cas失败,自旋没有获取到锁 addWaiter 添加到链表的尾部
在这里插入图片描述

通过 addWaiter 将线程加到 链表 之后,继续走acquireQueued(addWaiter(Node.EXCLUSIVE), arg)把 Node 作为参数传递给acquireQueued 方法,去竞争锁

Node 的一些状态
    static final class Node {
        ...
        /** waitStatus value to indicate thread has cancelled */
        static final int CANCELLED =  1;
        /** waitStatus value to indicate successor's thread needs unparking */
        static final int SIGNAL    = -1;
        /** waitStatus value to indicate thread is waiting on condition */
        static final int CONDITION = -2;
        /**
         * waitStatus value to indicate the next acquireShared should
         * unconditionally propagate
         */
        static final int PROPAGATE = -3;
        ...
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) {
        boolean failed = true;
        try {
            boolean interrupted = false;
            for (;;) {//自旋
                //获取当前节点的上一个节点
                final Node p = node.predecessor();
                //如果上一个节点是头节点,继续争抢锁
                if (p == head && tryAcquire(arg)) {
                    //线程A 已经释放锁
                    //线程B 抢占锁成功以后,把获得锁的线程B节点设置为 head
                    setHead(node);
                    //移除原来的初始化 head节点
                    p.next = null; // help GC
                    failed = false;
                    return interrupted;
                }
                //线程A 可能还没释放锁,使得 线程B 在执行 tryAcquire 时会返回 false
                if (shouldParkAfterFailedAcquire(p, node) &&
                    parkAndCheckInterrupt())
                    interrupted = true;//并且返回当前线程在等待过程中有没有中断过。
            }
        } finally {
            //取消获得锁的操作
            if (failed)
                cancelAcquire(node);
        }
    }
shouldParkAfterFailedAcquire()

如果在争抢锁失败之后,是否要挂起线程。SIGNAL = -1

    private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) {
        int ws = pred.waitStatus;
        //如果上一个节点的状态为-1 但是ws此时的状态为0
        if (ws == Node.SIGNAL)
            /*
             * This node has already set status asking a release
             * to signal it, so it can safely park.
             */
            return true;
        if (ws > 0) {
            //如果ws>0 状态表示 CANCELLED 1
            /*
             * Predecessor was cancelled. Skip over predecessors and
             * indicate retry.
             */
            //无限循环去移除掉该节点
            do {
                node.prev = pred = pred.prev;
            } while (pred.waitStatus > 0);
            pred.next = node;
        } else {
            //所以走最后一个逻辑 将节点的pred节点 状态 修改为 SIGNAL
            /*
             * waitStatus must be 0 or PROPAGATE.  Indicate that we
             * need a signal, but don't park yet.  Caller will need to
             * retry to make sure it cannot acquire before parking.
             */
            compareAndSetWaitStatus(pred, ws, Node.SIGNAL);
        }
        return false;
    }

经过两次循环,线程B进入和线程C进入,都将pred节点的状态修改为SIGNAL(-1),最后的状态如下
在这里插入图片描述

parkAndCheckInterrupt
    private final boolean parkAndCheckInterrupt() {
        //挂起当前线程
        LockSupport.park(this);
        return Thread.interrupted();//返回当前线程的中断状态,并复位当前线程
    }

java.util.concurrent.locks.LockSupport#park(java.lang.Object)

    public static void park(Object blocker) {
        Thread t = Thread.currentThread();
        setBlocker(t, blocker);
        UNSAFE.park(false, 0L);
        setBlocker(t, null);
    }

在这里插入图片描述

3.2 锁的释放流程

lock.unlock() 时序图

在这里插入图片描述

线程A执行完,执行lock.unlock()方法释放锁

    public void unlock() {
        //释放
        sync.release(1);
    }
release()
    public final boolean release(int arg) {
        //尝试释放锁
        if (tryRelease(arg)) {
            //获取AQS中的head节点
            Node h = head;
            //如果此时的头节点为初始化的节点而且waitStatus = -1(SIGNAL)
            if (h != null && h.waitStatus != 0)//如果 head 节点不为空并且状态!=0.调用 unparkSuccessor(h)唤醒后续节点
                unparkSuccessor(h);
            return true;
        }
        return false;
    }
tryRelease()

这个方法可以认为是一个设置锁状态的操作,通过将 state 状态减掉传入的参数值(releases是 1),直到state状态为0,才返回true ; 因为在tryAcquire()方法中(如果同一个线程多次lock.lock() 获取锁)state增加重入次数,所以只有线程对应的lock.unlock(),当state依次递减为0时,才会将对应的Owner线程设置为null,最后才会返回true。

        protected final boolean tryRelease(int releases) {
            //每次释放 重入次数-1
            int c = getState() - releases;
            if (Thread.currentThread() != getExclusiveOwnerThread())
                throw new IllegalMonitorStateException();
            boolean free = false;
            //当重入次数为0 表示无锁状态
            if (c == 0) {
                free = true;
                //设置独占线程为null
                setExclusiveOwnerThread(null);
            }
            //重新设置锁的状态
            setState(c);
            return free;
        }
unparkSuccessor()
    private void unparkSuccessor(Node node) {
        //此时传入的头节点的状态为-1
        int ws = node.waitStatus;
        if (ws < 0)
            compareAndSetWaitStatus(node, ws, 0);//将节点的状态替换为0 无锁状态
        //拿到头节点的next节点 为 线程B 节点
        Node s = node.next;
        //如果节点为空或者状态>0(>0表示cancelled) 
        if (s == null || s.waitStatus > 0) {
            s = null;
            //从尾部遍历
            for (Node t = tail; t != null && t != node; t = t.prev)
                if (t.waitStatus <= 0)//找到状态小于等于0的节点 single状态
                    s = t;
        }
        if (s != null)//线程B节点 不为null 唤醒线程B
            LockSupport.unpark(s.thread);
    }

唤醒之后,线程B 会接着执行之前被阻塞的位置执行。

为何要从尾部遍历?

原因是enq()中的else后面的这一段代码

    private Node enq(final Node node) {
        for (;;) {//自旋
            Node t = tail;
            //如果tail节点为空
            if (t == null) { // Must initialize
                //初始化一个head节点,设置tail节点 = head节点
                if (compareAndSetHead(new Node()))
                    tail = head;
            } else {
                //表示AQS队列中已经存在节点 
                //设置当前节点的上一个节点为tail节点
                node.prev = t;
                if (compareAndSetTail(t, node)) {//cas设置node节点为tail节点
                    t.next = node;//cas成功 设计tail节点的下一个节点为node
                    return t;
                }
            }
        }
    }

新加入AQS队列的节点先执行node.prev = t;后执行compareAndSetTail(t, node)成功之后才会设置原tail节点的next节点为node节点;如果在这个中间有其他线程释放锁,从前往后遍历,由于tail节点的next = node这一步还没有执行,会漏洞这一个节点,但是从后往前遍历就没有这个问题。

原来阻塞的线程重新开始执行

AQS.acquireQueued()这个方法重新开始执行,因为程序在这个方法里面的parkAndCheckInterrupt()方法中阻塞线程,所以唤醒此方法的自旋开始。

...此处省略N行代码   
        for (;;) {//自旋
               //获取当前节点的上一个节点
               final Node p = node.predecessor();
               //如果上一个节点是头节点,继续争抢锁
              if (p == head && tryAcquire(arg)) {
                   //线程A 已经释放锁
                   //线程B 抢占锁成功以后,把获得锁的线程B节点设置为 head
                   setHead(node);
                   //移除原来的初始化 head节点
                   p.next = null; // help GC
                   failed = false;
                   return interrupted;
               }
  • 自旋直到当前Node的上一个节点为head节点

  • tryAcquire(arg) 竞争锁成功

  • 设置新的头节点为当前Node 线程B

    private void setHead(Node node) {
        head = node;//设置AQS的head节点为node
        node.thread = null;//设置node的线程为null
        node.prev = null;//设置node节点的上一个节点为null
    }
  • 设置原来的head节点的next节点为null
    在这里插入图片描述

通过这种方式将原来的头节点从AQS队列中剔除

3.3 公平锁和非公平锁的区别

lock方法

公平锁 FairSync

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

非公平锁 NonfairSync

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

非公平锁上来,先通过CAS来抢占锁

tryAcquire方法

公平锁 FairSync

公平锁先进行了!hasQueuedPredecessors()的判断,判断AQS的前驱节点(第一个线程封装的节点)是否是当前线程

而非公平锁 NonfairSync 直接通过CAS来抢占锁

        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;
        }
    }
hasQueuedPredecessors()

判断在AQS队列中head节点的next节点是否为null

如果不为null,这个节点是否等于当前线程节点

    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;
        return h != t &&
            ((s = h.next) == null || s.thread != Thread.currentThread());
    }

4.参考

腾讯课堂->咕泡学院->mic老师->并发编程基础

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值