文档章节

可重入的独占锁——ReentrantLock源码分析

须臾之余
 须臾之余
发布于 07/20 16:23
字数 2277
阅读 1100
收藏 19
CAS

ReentrantLock面试题分析

1、ReentrantLock是怎么实现的?

2、ReentrantLock的公平锁和非公平锁是如何实现的?

1.ReentrantLock类图结构

从类图我们可以直观地了解到,ReentrantLock最终还是使用AQS来实现地,并且根据参数来决定其内部是一个公平🔒还是非公平锁🔒,默认是非公平锁🔒。

public ReentrantLock() {
    sync = new NonfairSync();
}

public ReentrantLock(boolean fair) {
    sync = fair ? new FairSync() : new NonfairSync();
}

其中Sync类直接继承自AQS,它的子类NonfairSync和FairSync分别实现了获取锁的非公平与公平策略。

如果读者对AQS还不了解的话,可以去看看我的这篇文章:抽象同步队列AQS——AbstractQueuedSynchronizer锁详解

在这里,AQS的state状态值表示线程获取该锁的可重入次数,在默认情况下,state的值为0表示当前锁没有被任何线程持有。当一个线程第一次获取该锁时,会尝试使用CAS设置state的值为1,

如果CAS成功则当前线程获取了该锁,然后记录该锁的持有者为当前线程。在该线程没用释放锁的情况下第二次获取该锁后,状态值被设置为2,这就是可重入次数。

在该线程释放锁时,会尝试使用CAS让状态值减1,如果减1后状态值为0,则当前线程释放该锁。

2.获取锁的主要方法

2.1 void lock()方法

lock()获取锁,其实就是把state从0变成n(重入锁可以累加)。实际调用的是sync的lock方法,分公平和非公平。

public void lock() {
    sync.lock();
}

在如上代码中,ReentrantLock的lock()委托给sync类,根据创建的ReentrantLock构造函数选择sync的实现是NonfairSync还是FairSync,先看看sync的子类NonfairSync(非公平锁🔒)的情况

final void lock() {
    if (compareAndSetState(0, 1))//CAS设置状态值为1
        setExclusiveOwnerThread(Thread.currentThread());//设置该锁的持有者为当前线程
    else //CAS失败的话
        acquire(1);//调用AQS的acquire方法,传递参数为1
}

下面是AQS的acquire的核心源码

public final void acquire(int arg) {
    if (!tryAcquire(arg) &&//调用ReentantLock重写tryAcquire方法
        acquireQueued(addWaiter(Node.EXCLUSIVE), arg))//tryAcquire返回false会把当前线程放入AQS阻塞队列
        selfInterrupt();
}

之前说过,AQS并没有提供可用的tryAcquire方法,tryAcquire方法需要子类自己定制化,所以这里代码会调用ReentantLock重写的tryAcquire方法。我们看下非公平锁🔒的实现

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) {//当前AQS状态为0,acquires参数传递默认为1,因为之前CAS失败,再次获取锁
        if (compareAndSetState(0, acquires)) {//CAS设置状态值为1
            setExclusiveOwnerThread(current);//设置该锁的持有者为当前的线程
            return true;
        }
    }
    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;//如果当前线程不是该锁的持有者,则返回false,然后会放入AQS阻塞队列
}

结束完非公平锁🔒的实现代码,回过头来看看非公平在这里是怎么体现的。首先非公平是说先尝试获取锁的线程并不一定比后尝试获取锁的线程优先获取锁🔒。

而是使用了抢夺策略。那么下面我们看看公平锁🔒是怎么实现公平的。

protected final boolean tryAcquire(int acquires) {
        final Thread current = Thread.currentThread();
        int c = getState();
        if (c == 0) {//当前AQS状态为0
            if (!hasQueuedPredecessors() &&//公平性策略,判断队列还有没有其它node,要保证公平
                compareAndSetState(0, acquires)) {//CAS设置状态
                setExclusiveOwnerThread(current);//设置获取锁的线程
                return true;
            }
        }
        else if (current == getExclusiveOwnerThread()) {//如果当前线程是该锁的持有者
            int nextc = c + acquires;//重入次数+1
            if (nextc < 0)
                throw new Error("Maximum lock count exceeded");
            setState(nextc);//重新设置锁的状态
            return true;
        }
        return false;
    }
}

如上代码所示,公平的tryAcquire策略与非公平的类似,不同之处在于,代码在设置CAS操作之前添加了hasQueuedPredecessors()方法,该方法是实现公平性的核心代码。代码如下

public final boolean hasQueuedPredecessors() {
   
    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());
}

2.2void lockInterruptibly()方法

该方法与lock()方法类似,不同在于对中断进行响应,如果当前线程在调用该方法时,其它线程调用了当前线程的interrupt()方法,则该线程抛出异常而返回

public void lockInterruptibly() throws InterruptedException {
    sync.acquireInterruptibly(1);
}
public final void acquireInterruptibly(int arg)
        throws InterruptedException {
    if (Thread.interrupted())//如果当前线程被中断,则直接抛出异常
        throw new InterruptedException();
    if (!tryAcquire(arg))//尝试获取资源
        doAcquireInterruptibly(arg);//调用AQS可被中断的方法
}

2.3 boolean tryLock()方法

尝试获取锁,如果当前锁没用被其它线程持有,则当前线程获取该锁并返回true,否则返回false。注意,该方法不会引起当前线程阻塞

public boolean tryLock() {
    return sync.nonfairTryAcquire(1);
}
final boolean nonfairTryAcquire(int acquires) {
    final Thread current = Thread.currentThread();
    int c = getState();
    if (c == 0) {
        if (compareAndSetState(0, acquires)) {
            setExclusiveOwnerThread(current);
            return true;
        }
    }
    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()方法代码类似,所以tryLock()使用的是非公平策略。

2.4 boolean tryLock(long timeout, TimeUnit unit)方法

尝试获取锁,与tryLock()的不同之处在于,它设置了超时时间,如果超时时间到了,没用获取到锁,则返回false,以下是相关代码

public boolean tryLock(long timeout, TimeUnit unit)
        throws InterruptedException {
    return sync.tryAcquireNanos(1, unit.toNanos(timeout));//调用AQS的tryAcquireNanos方法
}
public final boolean tryAcquireNanos(int arg, long nanosTimeout)
        throws InterruptedException {
    if (Thread.interrupted())
        throw new InterruptedException();
    return tryAcquire(arg) ||
        doAcquireNanos(arg, nanosTimeout);
}
private boolean doAcquireNanos(int arg, long nanosTimeout)
        throws InterruptedException {
    if (nanosTimeout <= 0L)
        return false;
    final long deadline = System.nanoTime() + nanosTimeout;
    final Node node = addWaiter(Node.EXCLUSIVE);
    boolean failed = true;
    try {
        for (;;) {
            final Node p = node.predecessor();
            if (p == head && tryAcquire(arg)) {
                setHead(node);
                p.next = null; // help GC
                failed = false;
                return true;
            }
            nanosTimeout = deadline - System.nanoTime();
            if (nanosTimeout <= 0L)
                return false;
            if (shouldParkAfterFailedAcquire(p, node) &&
                nanosTimeout > spinForTimeoutThreshold)
                LockSupport.parkNanos(this, nanosTimeout);
            if (Thread.interrupted())
                throw new InterruptedException();
        }
    } finally {
        if (failed)
            cancelAcquire(node);
    }
}

3 释放锁相关方法

3.1 void unlock()方法

尝试获取锁,如果当前线程持有锁,则调用该方法会让该线程持有的AQS状态值减1,如果减1后当前状态值为0,则当前线程会释放该锁,否则仅仅减1而已。

如果当前线程没用持有该锁而调用了该方法则会抛出异常,代码如下:

public void unlock() {
    sync.release(1);
}
public final boolean release(int arg) {
    if (tryRelease(arg)) {
        Node h = head;
        if (h != null && h.waitStatus != 0)
            unparkSuccessor(h);
        return true;
    }
    return false;
}

protected final boolean tryRelease(int releases) {
    int c = getState() - releases;//AQS状态值减1
    if (Thread.currentThread() != getExclusiveOwnerThread())
        throw new IllegalMonitorStateException();
    boolean free = false;
    if (c == 0) {//如果当前可重入次数为0,则清空锁持有线程
        free = true;
        setExclusiveOwnerThread(null);
    }
    setState(c);//设置可重入次数为原始值减1
    return free;
}

4.案例介绍

下面使用ReentrantLock来实现一个简单的线程安全的list集合

public class ReentrantLockList {
    //线程不安全的list
    private ArrayList<String>arrayList=new ArrayList<>();
    //独占锁
    private volatile ReentrantLock lock=new ReentrantLock();

    //添加元素
    public void add(String e){
        lock.lock();
        try {
            arrayList.add(e);
        }finally {
            lock.unlock();
        }
    }
    //删除元素
    public void remove(String e){
        lock.lock();
        try {
            arrayList.remove(e);
        }finally {
            lock.unlock();
        }
    }
    //获取数据
    public String get(int index){
        lock.lock();
        try {
            return arrayList.get(index);
        }finally {
            lock.unlock();
        }
    }
}

如上代码在操作arrayList元素前进行加锁保证同一时间只有一个线程可用对arrayList数组进行修改,但是也只能一个线程对arrayList进行访问。

如图,假如线程Thread-1,Thread-2,Thread-3同时尝试获取独占锁ReentrantLock,加上Thread-1获取到了🔒,则Thread-2和Thread-3就会被转换为Node节点并放入ReentrantLock对应的AQS阻塞队列,而后阻塞挂起。

如图,假设Thread-1获取锁后调用了对应的锁创建的条件变量1,那么Thread-1就会释放获取到的🔒,然后当前线程就会被转换为Node节点插入条件变量1的条件队列。由于Thread-1释放了🔒,所以阻塞到AQS队列里面的

Thread-2和Thread-3就会有机会获取到该锁,假如使用的是公平性策略,那么者时候Thread-2会获取到锁,从而从AQS队列里面移除Thread-2对应的Node节点。

小结:

本章介绍了ReentrantLock的实现原理,ReentrantLock的底层使用AQS实现的可重入独占锁。在这里AQS状态值为0表示当前🔒空闲,为大于1的值则说明该🔒已经被占用了。

该🔒内部有公平与非公平实现,默认情况下是非公平的实现,另外,由于该锁的独占锁,所以某一时刻只有一个线程可以获取到该🔒。

本文参考书籍

Java并发编程之美

 

© 著作权归作者所有

须臾之余
粉丝 119
博文 59
码字总数 160700
作品 0
吉安
程序员
私信 提问
并发编程之ReentrantLock

简介 在java中,实现锁有2种方式,一个是synchronized,一个是Lock,今天我们讲讲可重入锁ReentrantLock,ReentrantLock相对于传统的synchronized,优势在于提供了更加灵活的方式使用锁,比如...

熊小飞呀
2018/09/06
20
0
ReentrantLock(重入锁)功能详解和应用演示

1. ReentrantLock简介 jdk中独占锁的实现除了使用关键字synchronized外,还可以使用ReentrantLock。虽然在性能上ReentrantLock和synchronized没有什么区别,但ReentrantLock相比synchronized而...

takumiCX
2018/07/19
0
0
Java Lock接口分析之ReentantReadWriteLock

ReentantReadWriteLock读写锁,在读线程多余写线程的并发环境中能体现出优异的性能,相比于synchronized与ReentrantLock这种独占式锁的模型,ReentantReadWriteLock采用独占式写锁与共享式读...

我爱春天的毛毛雨
2018/09/21
52
0
Java 重入锁 ReentrantLock 原理分析

1.简介 可重入锁自 JDK 1.5 被引入,功能上与关键字类似。所谓的可重入是指,线程可对同一把锁进行重复加锁,而不会被阻塞住,这样可避免死锁的产生。ReentrantLock 的主要功能和 synchroniz...

Java-老刘
2018/05/08
70
0
并发九:ReentrantReadWriteLock实现分析

ReentrantReadWriteLock ReentrantReadWriteLock具有ReentrantLock的特性,支持重入和公平性设置,但是对读写进行了分离。 读操作采用共享锁,写操作采用独占锁,即一个资源可以有多个线程同...

wangjie2016
2018/04/14
0
0

没有更多内容

加载失败,请刷新页面

加载更多

Executor线程池原理与源码解读

线程池为线程生命周期的开销和资源不足问题提供了解决方 案。通过对多个任务重用线程,线程创建的开销被分摊到了多个任务上。 线程实现方式 Thread、Runnable、Callable //实现Runnable接口的...

小强的进阶之路
昨天
6
0
maven 环境隔离

解决问题 即 在 resource 文件夹下面 ,新增对应的资源配置文件夹,对应 开发,测试,生产的不同的配置内容 <resources> <resource> <directory>src/main/resources.${deplo......

之渊
昨天
8
0
详解箭头函数和普通函数的区别以及箭头函数的注意事项、不适用场景

箭头函数是ES6的API,相信很多人都知道,因为其语法上相对于普通函数更简洁,深受大家的喜爱。就是这种我们日常开发中一直在使用的API,大部分同学却对它的了解程度还是不够深... 普通函数和...

OBKoro1
昨天
7
0
轻量级 HTTP(s) 代理 TinyProxy

CentOS 下安装 TinyProxy yum install -y tinyproxy 启动、停止、重启 # 启动service tinyproxy start# 停止service tinyproxy stop# 重启service tinyproxy restart 相关配置 默认...

Anoyi
昨天
2
0
Linux创建yum仓库

第一步、搞定自己的光盘 #创建文件夹 mkdir -p /media/cdrom #挂载光盘 mount /dev/cdrom /media/cdrom #编辑配置文件使其永久生效 vim /etc/fstab 第二步,编辑yun源 vim /ect yum.repos.d...

究极小怪兽zzz
昨天
6
0

没有更多内容

加载失败,请刷新页面

加载更多

返回顶部
顶部