码迷,mamicode.com
首页 > 编程语言 > 详细

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

时间:2020-11-25 12:16:19      阅读:4      评论:0      收藏:0      [点我收藏+]

标签:queue   架构   可见性   new t   没有   控制   ++   细节   monitor   

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

收录于话题
#进阶架构师 | 并发编程专题
12个

点击上方“java进阶架构师”,选择右上角“置顶公众号”
20大进阶架构专题每日送达
技术图片

写在前面


本文为何适原创并发编程系列第 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();// 释放锁
        }
    }
}

需要注意两点:
synchronized同步块执行完成或者遇到异常是锁会自动释放,而lock必须调用unlock()方法释放锁。
为了保证在获取到锁之后,最终能够被释放,在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>0且sync.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. 如何实现重入


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

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

技术图片

重入锁实现重入过程

总结


重入锁实现同步过程:
线程1调用lock()加锁,判断state=0,所以直接获取到锁,设置state=1 exclusiveOwnerThread=线程1。
线程2调用lock()加锁,判断state=1 exclusiveOwnerThread=线程1,锁已经被线程1持有,线程2被封装成节点Node加入同步队列中排队等锁。此时线程1执行同步代码,线程2阻塞等锁。
线程1调用unlock()解锁,判断exclusiveOwnerThread=线程1,可以解锁。设置state减1,exclusiveOwnerThread=null。state变为0时,唤醒AQS同步队列中head的后继节点,这里是线程2。
线程2被唤醒,再次去抢锁,成功之后执行同步代码。
线程最终获取到锁的标志就是AQS.state>0且AQS.exclusiveOwnerThread==当前线程。
Lock和AQS很好的隔离了使用者和实现者所需关注的领域。

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

《Java并发编程之美》
《Java并发编程实战》
《Java并发编程的艺术》

并发系列文章汇总


【原创】01|开篇获奖感言
【原创】02|并发编程三大核心问题
【原创】03|重排序-可见性和有序性问题根源
【原创】04|Java 内存模型详解
【原创】05|深入理解 volatile
【原创】06|你不知道的 final
【原创】07|synchronized 原理
【原创】08|synchronized 锁优化
【原创】09|基础干货
【原创】10|线程状态
【原创】11|线程调度
【原创】13|LockSupport
【原创】14|AQS源码分析
———— e n d ————
微服务、高并发、JVM调优、面试专栏等20大进阶架构师专题请关注公众号【Java进阶架构师】后在菜单栏查看。
回复【架构】领取架构师视频一套。
技术图片

你的“在看”,就是给我最好的赞赏^_^

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

标签:queue   架构   可见性   new t   没有   控制   ++   细节   monitor   

原文地址:https://blog.51cto.com/15009303/2552798

(0)
(0)
   
举报
评论 一句话评论(0
登录后才能评论!
© 2014 mamicode.com 版权所有  联系我们:gaon5@hotmail.com
迷上了代码!