专栏首页Java识堂ReentrantLock实现原理

ReentrantLock实现原理

前言

建议和上一篇分享结合着看:深入理解AbstractQueuedSynchronizer

先举个例子,下面程序输出始终是5000,可以用ReentrantLock来保证线程安全

@ThreadSafe
public class CountTest {

    public static int count = 0;
    public static Lock lock = new ReentrantLock();

    public static void main(String[] args) {

        ExecutorService service = Executors.newCachedThreadPool();
        for (int i = 0; i < 5; i++) {
            service.execute(() -> {
                //加锁
                lock.lock();
                try {
                    for (int j = 0; j < 1000; j++) {
                        count++;
                    }
                } finally {
                    //释放锁
                    lock.unlock();
                }
            });
        }
        service.shutdown();
        try {
            TimeUnit.SECONDS.sleep(2);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println("count = " + count);
    }
}

源码

基于jdk1.8.0_20,先看UML图

Lock接口中定义了对锁的各种操作

public interface Lock {

    //不响应中断的获取锁
    void lock();

    //响应中断的获取锁
    void lockInterruptibly() throws InterruptedException;

    //尝试非阻塞的获取锁,true为获取到锁,false为没有获取到锁
    boolean tryLock();

    //超时获取锁,以下情况会返回:时间内获取到了锁,时间内被中断,时间到了没有获取到锁
    boolean tryLock(long time, TimeUnit unit) throws InterruptedException;

    //释放锁
    void unlock();

    //创建一个condition
    Condition newCondition();
}

ReentrantLock在AQS基础上,又扩展出来非公平锁和公平锁,那么它现在有4种功能,各种操作分别在NonfairSync和FairSync这两个静态内部类中实现

  1. 响应中断的非公平锁
  2. 不响应中断的非公平锁
  3. 响应中断的公平锁
  4. 不响应中断的公平锁

AQS有一个state变量,在不同子类中有不同的含义,在ReentrantLock中表示锁的状态

  • status的值表示加锁的次数,无锁时值为0,第一次加锁将status设置为1,由于ReentrantLock是可重入锁,当持有锁的线程是当前线程时,即可加锁,加锁一次,将status的值加1
  • 每解锁一次将status的个数减1,当stauts的值为0,其他线程可以获得锁

ReentrantLock只有一个成员变量Sync,Sync相当于一个代理类,具体的实现在子类中定义

private final Sync sync;

ReentrantLock类有两个构造函数,默认是非公平锁。当传入的参数是true时为公平锁,是false为非公平锁

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

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

不响应中断的非公平锁

//ReentrantLock
public void lock() {
   sync.lock();
}
//NonfairSync
final void lock() {
   //这就是不公平的地方,上来直接通过CAS尝试将status的值从0设置为1
   if (compareAndSetState(0, 1))
       //设置成功则将持有锁的线程设置为当前线程
       setExclusiveOwnerThread(Thread.currentThread());
   else
       acquire(1);
}
//AbstractQueuedSynchronizer(这个方法已经在上一篇分享中分析了)
public final void acquire(int arg) {
   if (!tryAcquire(arg) &&
       acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
       selfInterrupt();
}
//NonfairSync(重写了AbstractQueuedSynchronizer的方法)
//尝试获取锁
protected final boolean tryAcquire(int acquires) {
   return nonfairTryAcquire(acquires);
}
//Sync
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");
        //设置state的值
        setState(nextc);
        return true;
    }
    return false;
}
//AbstractQueuedSynchronizer(锁被当前线程持有,直接设置即可)
protected final void setState(int newState) {
    state = newState;
}

来个小插曲,尝试非阻塞的获取锁直接调用的就是nonfairTryAcquire方法

public boolean tryLock() {
   return sync.nonfairTryAcquire(1);
}

接着看释放锁,公平锁和非公平锁用的都是这个方法

//ReentrantLock
public void unlock() {
   sync.release(1);
}
//AbstractQueuedSynchronizer(上一篇分享已经分析)
public final boolean release(int arg) {
   if (tryRelease(arg)) {
       Node h = head;
       if (h != null && h.waitStatus != 0)
           unparkSuccessor(h);
       return true;
   }
   return false;
}
//Sync(重写了AbstractQueuedSynchronizer的方法)
//尝试释放锁
protected final boolean tryRelease(int releases) {
    //当前加锁的次数-释放的次数
    int c = getState() - releases;
    //当前线程不是持有锁的线程
    if (Thread.currentThread() != getExclusiveOwnerThread())
        throw new IllegalMonitorStateException();
    boolean free = false;
    if (c == 0) {
        //为true时表示已经释放锁了
        free = true;
        //设置持有锁的线程为null
        setExclusiveOwnerThread(null);
    }
    setState(c);
    return free;
}

不响应中断的公平锁

这几步都一样

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

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

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

从这开始不一样了

//FairSync
protected final boolean tryAcquire(int acquires) {
    final Thread current = Thread.currentThread();
    int c = getState();
    if (c == 0) {
        //这就是公平的地方,线程是按照FIFO顺序来执行的
        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;
}
//当前线程前面有等待线程,返回true
//当前线程位于队列的头部或者队列为空,返回false
public final boolean hasQueuedPredecessors() {
    //AQS内部的FIFO队列的头节点
    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());
}

如果head=tail,则表示FIFO队列为空,如刚开始head和tail都为null,返回false 如果head!=tail,并且head的next为空时,则FIFO队列不为空

有两种情况会导致h的next为空

  • 当前线程进入hasQueuedPredecessors的同时,另一个线程已经更改了tail,但还没有将head的next指向tail
  • 当前线程将head赋给h后,head被另一个线程移除队列,导致h的next为空,这种情况说明锁已经被占用
//ReentrantLock->enq
private Node enq(final Node node) {
    for (;;) {
        Node t = tail;
        if (t == null) { // Must initialize
            if (compareAndSetHead(new Node()))
                tail = head;
        } else {
            node.prev = t;
            if (compareAndSetTail(t, node)) {
                //第一种情况在这行字中间
                t.next = node;
                return t;
            }
        }
    }
}
//ReentrantLock->acquireQueued
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)) {
                //第二种情况在这里,p.next=null
                setHead(node);
                p.next = null; 
                failed = false;
                return interrupted;
            }
            if (shouldParkAfterFailedAcquire(p, node) &&
                parkAndCheckInterrupt())
                interrupted = true;
        }
    } finally {
        if (failed)
            cancelAcquire(node);
    }
}

后记

最后再说一下公平锁和非公平锁,举个例子

public class SyncTest {

    public static Lock lock = new ReentrantLock();

    public static void main(String[] args) {

        Thread[] threads = new Thread[10];
        for (int i = 0; i < 10; i++) {
            threads[i] = new Thread(new Runnable() {
                @Override
                public void run() {
                    System.out.println(Thread.currentThread().getName() + " 开始运行");
                    testMethod();
                }
            });
        }
        for (int i = 0; i < 10; i++) {
            threads[i].start();
        }
    }

    public static void testMethod() {
        lock.lock();
        try {
            System.out.println(Thread.currentThread().getName() + " 获得锁");
        } finally {
            lock.unlock();
        }
    }
}

执行结果有时如下

Thread-0 开始运行
Thread-2 开始运行
Thread-0 获得锁
Thread-3 开始运行
Thread-1 开始运行
Thread-2 获得锁
Thread-4 开始运行
Thread-4 获得锁
Thread-5 开始运行
Thread-3 获得锁
Thread-6 开始运行
Thread-6 获得锁
Thread-1 获得锁
Thread-9 开始运行
Thread-7 开始运行
Thread-5 获得锁
Thread-8 开始运行
Thread-9 获得锁
Thread-7 获得锁
Thread-8 获得锁

可以看到开始运行的顺序和获得锁的顺序是不一致的

将lock成员变量改为如下

public static Lock lock = new ReentrantLock(true);

执行结果有时如下

Thread-0 开始运行
Thread-2 开始运行
Thread-1 开始运行
Thread-3 开始运行
Thread-4 开始运行
Thread-6 开始运行
Thread-0 获得锁
Thread-5 开始运行
Thread-2 获得锁
Thread-7 开始运行
Thread-1 获得锁
Thread-9 开始运行
Thread-8 开始运行
Thread-3 获得锁
Thread-4 获得锁
Thread-6 获得锁
Thread-5 获得锁
Thread-7 获得锁
Thread-9 获得锁
Thread-8 获得锁

可以看到开始运行的顺序和获得锁的顺序是一致的,但是这并不是绝对的,假设线程B调用tryAcquire失败后,并在调用addWaiter之前,线程A释放了锁,且线程C判断到锁空闲,进入hasQueuedPredecessors返回false(等待队列为空),最终C比B先获取到锁,因此公平锁并不是绝对公平的

非公平的锁的效率高于公平锁的效率,是因为在恢复一个被挂起的线程与该线程真正运行之间存在着严重的延迟,假设线程A持有一个锁,并且线程B请求这个锁。由于锁被A持有,因此B将被挂起。当A释放锁时,B将被唤醒,因此B会再次尝试获取这个锁。与此同时,如果线程C也请求这个锁,那么C很可能会在B被完全唤醒之前获得、使用以及释放这个锁。这样就是一种双赢的局面:B获得锁的时刻并没有推迟,C更早的获得了锁,并且吞吐量也提高了。

本文分享自微信公众号 - Java识堂(erlieStar),作者:李立敏

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

原始发表时间:2018-05-02

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

我来说两句

0 条评论
登录 后参与评论

相关文章

  • 大白话带你认识JVM

    如果在文中用词或者理解方面出现问题,欢迎指出。此文旨在提及和而不深究,但会尽量效率地把知识点都抛出来

    Java识堂
  • Java中的>>,>>>和<<

    我们都知道对于有符号数据类型,二进制最左端的数字为符号位,0代表正,1代表负,这里先介绍几个概念

    Java识堂
  • HashMap实现原理

    HashMap的主干是一个数组,假设我们有3个键值对dnf:1,cf:2,lol:3,每次放的时候会根据hash函数来确定这个键值对应该放在数组的哪个位置,即i...

    Java识堂
  • 并发基础(三): java线程优先级小试牛刀

    好好学java
  • Java线程(八):锁对象Lock-同步问题更完美的处理方式

            Lock是java.util.concurrent.locks包下的接口,Lock 实现提供了比使用synchronized 方法和语句可获得的...

    高爽
  • 异步函数async await在wpf都做了什么?

    首先我们知道async await 异步函数本质是状态机,我们通过反编译工具dnspy,看看反编译的两段代码是否有不同之处:

    ryzenWzd
  • java多线程|创建线程的各种方式

    本网站记录了最全的各种JavaDEMO ,保证下载,复制就是可用的,包括基础的, 集合的, spring的, Mybatis的等等各种,助力你从菜鸟到大牛,记得...

    微笑的小小刀
  • Redis 设计 --- 高效数据结构实现剖析

    即使有链表来处理键冲突,但是当节点数量远远大于 size 时,如果不扩充哈希表规模,请自行想象。这也是 rehash 的存在意义,笔者认为这也是 redis 扩...

    Arbiter
  • 在java中notify和notifyAll的区别

    notify()和notifyAll()以及wait()方法用于线程间的通信。通过调用wait()方法进入WaitSet的线程会一直处于WAITING状态,直到...

    冬天里的懒猫
  • 线程的补充

    ThreadLocal让线程有自己的局部变量,其中重要的方法有:set(),get(),remove()

    晚上没宵夜

扫码关注云+社区

领取腾讯云代金券