专栏首页Java进阶架构师【原创】Java并发编程系列15 | 重入锁ReentrantLock

【原创】Java并发编程系列15 | 重入锁ReentrantLock

写在前面

本文为何适原创并发编程系列第 15 篇,文末有本系列文章汇总。

AQS是java.util.concurrent包的核心基础组件,是实现Lock的基础。那么AQS是如何实现Lock的呢?

ReentrantLock是Lock中用到最多的,与synchronized具有相同的功能和内存语义,本文将从源码角度深入分析AQS是如何实现ReentrantLock的。

注:本文是在默认理解AQS原理基础上分析ReentrantLock的,建议读者先读懂上一篇AQS原理。

1. ReentrantLock使用

用法:

public class ReentrantLockDemo {
    private static ReentrantLock reentrantLock = new ReentrantLock();
    
    public void createOrder() {
        reentrantLock.lock();// 获取锁
        try {
            // 同步代码
        } finally {
            reentrantLock.unlock();// 释放锁
        }
    }
}

需要注意两点:

  1. synchronized同步块执行完成或者遇到异常是锁会自动释放,而lock必须调用unlock()方法释放锁。
  2. 为了保证在获取到锁之后,最终能够被释放,在finally块中释放锁。

使用举例:

synchronized文章中讲到的线程安全问题,代码如下:

public class ReentrantLockTest {
    public int inc = 0;

    public void increase() {
        inc++;
    }

    public static void main(String[] args) {
        final ReentrantLockTest test = new ReentrantLockTest();
        for (int i = 0; i < 10; i++) {
            new Thread() {
                public void run() {
                    for (int j = 0; j < 1000; j++)
                        test.increase();
                };
            }.start();
        }

        while (Thread.activeCount() > 1)
            // 保证前面的线程都执行完
            Thread.yield();
        System.out.println(test.inc);
    }
}

目的是得到test.inc=10000,但是因为线程安全问题,最终的结果总是小于10000。

使用synchronized解决办法是,用synchronized修饰increase()方法。同样可以使用重入锁解决,代码如下:

public class ReentrantLockTest {
    private ReentrantLock reentrantLock = new ReentrantLock();
    public int inc = 0;

    public void increase() {
        reentrantLock.lock();// 加锁
        inc++;
        reentrantLock.unlock();// 解锁
    }

    public static void main(String[] args) {
        final ReentrantLockTest test = new ReentrantLockTest();
        for (int i = 0; i < 10; i++) {
            new Thread() {
                public void run() {
                    for (int j = 0; j < 1000; j++)
                        test.increase();
                };
            }.start();
        }

        while (Thread.activeCount() > 1)
            // 保证前面的线程都执行完
            Thread.yield();
        System.out.println(test.inc);
    }
}

2. 类结构

public class ReentrantLock implements Lock, java.io.Serializable {
    private final Sync sync;
    abstract static class Sync extends AbstractQueuedSynchronizer {}
    static final class FairSync extends Sync {}
    static final class NonfairSync extends Sync {}
}

ReentrantLock用内部类Sync来管理锁,所以真正的获取锁和释放锁是由Sync的实现类来控制的。

Sync有两个实现,分别为NonfairSync(非公平锁)和FairSync(公平锁),以FairSync为例来讲解ReentrantLock,之后会专门分析公平锁和非公平锁。

3. 获取锁

ReentrantLock分为公平锁和非公平锁,本文以公平锁为例讲解,下一篇将详细介绍公平锁与非公平锁。本文的源码讲解方式依然是在代码中适当位置加入注释。

/**
 * 获取锁reentrantLock.lock()-->ReentrantLock.lock()
 */
public void lock() {
    sync.lock();
}

/**
 * ReentrantLock.lock()-->ReentrantLock.FairSync.lock()
 */
final void lock() {
    acquire(1);
}

/**
 * ReentrantLock.FairSync.lock()-->AbstractQueuedSynchronizer.acquire(int)
 * 很熟悉了吧,上一篇讲的AQS获取锁的方法
 * 1.当前线程通过tryAcquire()方法抢锁
 * 2.线程抢到锁,tryAcquire()返回true,完成。
 * 3.线程没有抢到锁,将当前线程封装成node加入同步队列,并将当前线程挂起,等待被唤醒之后再抢锁。
 */
public final void acquire(int arg) {
    if (!tryAcquire(arg) &&
        acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
        selfInterrupt();
}

/**
 * ReentrantLock.FairSync.tryAcquire(int)
 * 实现了AQS的抢锁方法,抢锁成功返回true
 * 获取锁成功的两种情况:
 * 1.没有线程占用锁,且AQS队列中没有其他线程等锁,且CAS修改state成功。
 * 2.锁已经被当前线程持有,直接重入。
 */
protected final boolean tryAcquire(int acquires) {
    final Thread current = Thread.currentThread();
    int c = getState();// AQS的state (FairSync extends Sync extends AQS)
    if (c == 0) {// state==0表示当前没有线程占用锁
        if (!hasQueuedPredecessors() && // AQS同步队列中没有其他线程等锁的话,当前线程可以去抢锁,此方法下文有详解
            compareAndSetState(0, acquires)) {// CAS修改state,修改成功表示获取到了锁
            setExclusiveOwnerThread(current);// 抢锁成功将AQS.exclusiveOwnerThread置为当前线程
            return true;
        }
    }
    else if (current == getExclusiveOwnerThread()) {
        /*
         * AQS.exclusiveOwnerThread是当前线程,表示锁已经被当前线程持有,这里是锁重入
         * 重入一次将AQS.state加1
         */
        int nextc = c + acquires;
        if (nextc < 0)
            throw new Error("Maximum lock count exceeded");
        setState(nextc);
        return true;
    }
    return false;
}

/**
 * AbstractQueuedSynchronizer.hasQueuedPredecessors()
 * 判断AQS同步队列中是否还有其他线程在等锁
 * 返回true表示当前线程不能抢锁,需要到同步队列中排队;返回false表示当前线程可以去抢锁
 * 三种情况:
 * 1.队列为空不需要排队, head==tail,直接返回false
 * 2.head后继节点的线程是当前线程,就算排队也轮到当前线程去抢锁了,返回false
 * 3.其他情况都返回true,不允许抢锁
 */
public final boolean hasQueuedPredecessors() {
    Node t = tail;
    Node h = head;
    Node s;
    return h != t && // head==tail时队列是空的,直接返回false
        ((s = h.next) == null || s.thread != Thread.currentThread());// head后继节点的线程是当前线程,返回false
}

  • 最终获取到锁的标志就是sync.state>0sync.exclusiveOwnerThread==当前线程
  • 判断锁的状态也是通过sync.state的值和sync.exclusiveOwnerThread来判断。

四、释放锁

/**
 * 释放锁reentrantLock.unlock()-->ReentrantLock.unlock()
 */
public void unlock() {
    sync.release(1);
}

/**
 * ReentrantLock.unlock()-->AbstractQueuedSynchronizer.release(int)
 * 同样是上一篇AQS中的释放锁方法
 * 释放锁成功之后,唤醒head的后继节点next,next节点被唤醒后再去抢锁。
 */
public final boolean release(int arg) {
    if (tryRelease(arg)) {
        Node h = head;
        if (h != null && h.waitStatus != 0)
            unparkSuccessor(h);
        return true;
    }
    return false;
}

/**
 * AbstractQueuedSynchronizer.release(int)-->ReentrantLock.Sync.tryRelease(int)
 * 释放重入锁。只有锁彻底释放,其他线程可以来竞争锁才返回true
 * 锁可以重入,state记录锁的重入次数,所以state可以大于1
 * 每执行一次tryRelease()将state减1,直到state==0,表示当前线程彻底把锁释放
 */
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;
}

5. 如何实现重入

  1. 线程T获取到锁,AQS.state=1,AQS.exclusiveOwnerThread置为线程T。
  2. 线程T没释放锁之前再次调用lock()加锁,判断AQS.exclusiveOwnerThread==线程T,就可以直接执行不会阻塞,此时AQS.state加1。
  3. 此时线程T再次调用lock()加锁,继续重入,AQS.state再加1,此时state==2。
  4. 线程T执行完部分同步代码,调用unlock()解锁,AQS.state减1,此时state==1,线程T还持有该锁,其他线程还无法来竞争锁。
  5. 线程T执行完所有同步代码,调用unlock()解锁,AQS.state减1,此时state==0,线程将锁释放,允许其他线程来竞争锁。

state用于记录线程状态:state==0,没有线程占用该锁;state==1,一个线程持有该锁;state==n,一个线程持有该锁且重入了n次。

重入锁实现重入过程

总结

重入锁实现同步过程:

  1. 线程1调用lock()加锁,判断state=0,所以直接获取到锁,设置state=1 exclusiveOwnerThread=线程1。
  2. 线程2调用lock()加锁,判断state=1 exclusiveOwnerThread=线程1,锁已经被线程1持有,线程2被封装成节点Node加入同步队列中排队等锁。此时线程1执行同步代码,线程2阻塞等锁。
  3. 线程1调用unlock()解锁,判断exclusiveOwnerThread=线程1,可以解锁。设置state减1,exclusiveOwnerThread=null。state变为0时,唤醒AQS同步队列中head的后继节点,这里是线程2。
  4. 线程2被唤醒,再次去抢锁,成功之后执行同步代码。

线程最终获取到锁的标志就是AQS.state>0AQS.exclusiveOwnerThread==当前线程

Lock和AQS很好的隔离了使用者和实现者所需关注的领域。

  • Lock是面向使用者,定义了与使用者交互的接口,隐藏了实现细节;
  • AQS是面向Lock的实现者,实现了同步状态的管理,线程的排队,等待和唤醒等底层操作。

参考资料

  1. 《Java并发编程之美》
  2. 《Java并发编程实战》
  3. 《Java并发编程的艺术》

本文分享自微信公众号 - java进阶架构师(java_jiagoushi),作者:何适

原文出处及转载信息见文内详细说明,如有侵权,请联系 yunjia_community@tencent.com 删除。

原始发表时间:2020-03-05

本文参与腾讯云自媒体分享计划,欢迎正在阅读的你也加入,一起分享。

我来说两句

0 条评论
登录 后参与评论

相关文章

  • 论如何优雅的使用和理解线程池

    平时接触过多线程开发的童鞋应该都或多或少了解过线程池,之前发布的《阿里巴巴 Java 手册》里也有一条:

    java进阶架构师
  • 【原创】Java并发编程系列11 | 线程调度

    之前发过,但是因为之前忘记标记原创,没办法收录在【并发编程专题】里面,作为强迫症的我,必须要重发一次。本文为第 11 篇,前面几篇没看过的,可以在文末找到前几篇...

    java进阶架构师
  • 【原创】Java并发编程系列16 | 公平锁与非公平锁

    上一篇提到重入锁 ReentrantLock 支持两种锁,公平锁与非公平锁。那么这篇文章就来介绍一下公平锁与非公平锁。

    java进阶架构师
  • 【JavaScript】吃饱了撑的系列之JavaScript模拟多线程并发

    最近,明学是一个火热的话题,而我,却也想当那么一回明学家,那就是,把JavaScript和多线程并发这两个八竿子打不找的东西,给硬凑了起来,还写了一个并发库co...

    外婆的彭湖湾
  • Java并发编程实战系列11之性能与可伸缩性Performance and Scalability

    线程可以充分发挥系统的处理能力,提高资源利用率。同时现有的线程可以提升系统响应性。 但是在安全性与极限性能上,我们首先需要保证的是安全性。 11.1 对性能的...

    JavaEdge
  • 一台 Java 服务器可以跑多少个线程?

    打出jstack文件,通过IBM Thread and Monitor Dump Analyzer for Java工具查看如下:

    Java技术栈
  • 打通 Java 任督二脉 —— 并发数据结构的基石

    每一个 Java 的高级程序员在体验过多线程程序开发之后,都需要问自己一个问题,Java 内置的锁是如何实现的?最常用的最简单的锁要数 ReentrantLoc...

    老钱
  • 【转】Java并发的AQS原理详解

    每一个 Java 的高级程序员在体验过多线程程序开发之后,都需要问自己一个问题,Java 内置的锁是如何实现的?最常用的最简单的锁要数 ReentrantL...

    一枝花算不算浪漫
  • Java 经典问题

    switch语句后的控制表达式只能是short、char、int、long整数类型和枚举类型,不能是float,double和boolean类型。String类...

    好好学java
  • Java多线程编程-(5)-线程间通信机制的介绍与使用(温馨提示:图文较多,建议Wiff下打开)

    我们知道线程是操作系统中独立的个体,但是这个单独的个体之间没有一种特殊的处理方式使之成为一个整体,线程之间没有任何交流和沟通的话,他就是一个个单独的个体,不足以...

    Java后端技术

扫码关注云+社区

领取腾讯云代金券