惯性聚合 高效追踪和阅读你感兴趣的博客、新闻、科技资讯
阅读原文 在惯性聚合中打开

推荐订阅源

V
V2EX
C
Check Point Blog
博客园_首页
B
Blog
D
Docker
U
Unit 42
量子位
I
InfoQ
有赞技术团队
有赞技术团队
Martin Fowler
Martin Fowler
GbyAI
GbyAI
L
LangChain Blog
云风的 BLOG
云风的 BLOG
博客园 - Franky
美团技术团队
T
The Blog of Author Tim Ferriss
阮一峰的网络日志
阮一峰的网络日志
月光博客
月光博客
Vercel News
Vercel News
Recent Announcements
Recent Announcements
雷峰网
雷峰网
大猫的无限游戏
大猫的无限游戏
小众软件
小众软件
Google DeepMind News
Google DeepMind News

博客园 - xinghebuluo

在云主机上部署OpenClaw并集成微信插件:从零到上手完整指南 mongodb 数据块迁移的源码分析 mongodb 数据块的迁移流程介绍 Guava Cache源码浅析 mongodb在双活(主备)机房的部署方案和切换方案设计 mongodb oplog日志详解和格式分析 Redis之ziplist源码分析 Redis之quicklist源码分析 mongodb中出现_id字段重复记录的排查笔记 Redis LRU源码分析 JAVA中int转string及String.valueOf()的使用 String,StringBuilder和StringBuffer的特点和使用场景 mongodb的TTL索引介绍(超时索引) JBoss AS7(Application Server 7)的Standalone模式和Domain模式 mongodb 配置均衡器的运行窗口 mongodb添加延时节点 AFNetworking 遇到错误 Code=-1016 "Request failed: unacceptable content-type: text/plain" 使用cocoa捕获dock栏中的“退出”事件,解决qt开发的应用程序退出异常的问题 【Grails 代理Proxy设置】部署Grails遇到Error Resolve error obtaining dependencies:错误,及解决方法
ReentrantLock的原理解析
xinghebuluo · 2018-03-12 · via 博客园 - xinghebuluo

重入锁(ReentrantLock)是一种可重入无阻塞的同步机制。性能同synchronized接近(老版本jdk中性能很差)。

下面重点看下常用的lock()和unlock()方法的实现原理。

lock()

首先看下源代码

    public void lock() {
        sync.lock();  // 有公平同步和非公平同步两种机制
    }

它的实现很简单,调用了一行sync的lock()方法,由于sync有两种实现方式:公平同步和非公平同步,默认是非公平同步,继续看代码:

final void lock() {
            if (compareAndSetState(0, 1))  // CAS机制,如果处于无锁状态,就直接锁定。lock锁中维护一个计数,大于0表示加锁了,值表示重入加锁次数
                setExclusiveOwnerThread(Thread.currentThread());  // 设置锁被本线程持有,有什么用呢?用于可重入时检查使用
            else    
                acquire(1);  // 如果锁引用计数不是0,说明已经上锁,检查是否可以重入
}

 继续看"acquire(1)"的实现:

    
    public final void acquire(int arg) {
        if (!tryAcquire(arg) && // 判断是否可以重入,即持有锁的线程是否是当前线程,如果是就把锁引用计数加1,返回lock成功
            acquireQueued(addWaiter(Node.EXCLUSIVE), arg)) // 否则尝试把自己挂起
            selfInterrupt();
    }
   // 再次尝试加锁
    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) {  // 为0,说明没有锁了
                if (compareAndSetState(0, acquires)) {  // 尝试加锁
                    setExclusiveOwnerThread(current);
                    return true;
                }
            }
            else if (current == getExclusiveOwnerThread()) { // 持有锁的正是当前线程,那么就把锁引用计数加1,加锁成功
                int nextc = c + acquires;
                if (nextc < 0) // overflow
                    throw new Error("Maximum lock count exceeded");
                setState(nextc);
                return true;
            }
            return false;
        }

上面看到tryAcquire中会再次尝试加锁,或者如果持有锁的是当前线程,则把锁引用计数加1,返回加锁成功。

否则执行addWaiter,看下代码:

private Node addWaiter(Node mode) {
        Node node = new Node(Thread.currentThread(), mode);
        // Try the fast path of enq; backup to full enq on failure
        Node pred = tail;  // 增加一个等待node带队尾
        if (pred != null) {
            node.prev = pred;
            if (compareAndSetTail(pred, node)) {
                pred.next = node;
                return node;
            }
        }
       // 前面使用CAS方式设置失败,则进入enq,循环使用CAS方式把Node加入队尾
        enq(node);
        return node;
    }
  // 把node加入队尾
 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;
                }
            }
        }
    }
// 加入队尾后,再执行acquireQueued(即
acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
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)) { // 再次尝试加锁,防止有线程把锁释放了
                    setHead(node);
                    p.next = null; // help GC
                    failed = false;
                    return interrupted;
                }
                if (shouldParkAfterFailedAcquire(p, node) && // 检查是否可以把自己挂起,会设置一个状态值
                    parkAndCheckInterrupt())  // 把自己挂起
                    interrupted = true;
            }
        } finally {
            if (failed)
                cancelAcquire(node);
        }
    }

 上面的代码,重点看下这两个方法:

shouldParkAfterFailedAcquire
 1 private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) {
 2         int ws = pred.waitStatus;  // 第一次进来 ws是0,是初始值
 3         if (ws == Node.SIGNAL)   // 是否已经设置为等待唤醒状态,如果是,就可以挂起了
 4             /*
 5              * This node has already set status asking a release
 6              * to signal it, so it can safely park.
 7              */
 8             return true;
 9         if (ws > 0) {
10             /*
11              * Predecessor was cancelled. Skip over predecessors and
12              * indicate retry.
13              */
14             do {
15                 node.prev = pred = pred.prev;
16             } while (pred.waitStatus > 0);
17             pred.next = node;
18         } else { // 设置自己的状态为Node.SIGNAL,但本次不允许挂起。下次再进来的时候,就可以返回true,并可以挂起了
19             /*
20              * waitStatus must be 0 or PROPAGATE.  Indicate that we
21              * need a signal, but don't park yet.  Caller will need to
22              * retry to make sure it cannot acquire before parking.
23              */
24             compareAndSetWaitStatus(pred, ws, Node.SIGNAL);
25         }
26         return false;
27     }
parkAndCheckInterrupt
1     private final boolean parkAndCheckInterrupt() {
2         LockSupport.park(this);  // 挂起自己
3         return Thread.interrupted();
4     }
再看下park的实现:
1     public static void park(Object blocker) {
2         Thread t = Thread.currentThread();
3         setBlocker(t, blocker);  // 设置要挂起的线程,和挂起使用的对象
4         unsafe.park(false, 0L); // 挂起,jdk内置的挂起方法
5         setBlocker(t, null);      // 唤醒后,取消挂起的对象
6     }

再看下setBlocker:

1     private static void setBlocker(Thread t, Object arg) {
2         // Even though volatile, hotspot doesn't need a write barrier here.
3         unsafe.putObject(t, parkBlockerOffset, arg);
4     }  // 设置了阻塞线程和使用的阻塞对象
 
 

 从上面代码可以看到,lock的主要流程有:

1. 检查锁引用计数,如果为0,表示可以锁定,就使用CAS方式把当前Thread对象设置为持有锁的对象,并把锁引用计数加1.

2. 检查锁引用计数,如果大于0,需要:

    i. 检查当前持有锁的线程对象是否和本线程是同一个,如果是就对锁引用计数加1,加锁成功,这里体现了"可重入"特性。

    ii. 否则,创建一个等待的node对象并加入到等待链接的队尾,然后调用系统的unsafe.park方法,把自己挂起。

 Unlock

unlock就比较简单了,下面看下代码:

    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;
    }

这里面重点有两个地方,一个解锁,另外一个唤醒其他等待的线程。

先看解锁:

 1 // 索引用计数减releases,实现了重入
 2 protected final boolean tryRelease(int releases) {
 3             int c = getState() - releases;
 4             if (Thread.currentThread() != getExclusiveOwnerThread())
 5                 throw new IllegalMonitorStateException();
 6             boolean free = false;
 7             if (c == 0) {  // 如果锁引用计数为0,说明是无锁状态了,需要把持有锁的线程变量置为空,并返回true
 8                 free = true;
 9                 setExclusiveOwnerThread(null);
10             }
11             setState(c);
12             return free;
13         }

再看唤醒其他线程代码:

 1 // 当前面方法返回true,说明当前处于无锁状态了,这时候就可以唤醒其他的等待线程了
 2 private void unparkSuccessor(Node node) {
 3         /*
 4          * If status is negative (i.e., possibly needing signal) try
 5          * to clear in anticipation of signalling.  It is OK if this
 6          * fails or if status is changed by waiting thread.
 7          */
 8         int ws = node.waitStatus;
 9         if (ws < 0)
10             compareAndSetWaitStatus(node, ws, 0);
11 
12         /*
13          * Thread to unpark is held in successor, which is normally
14          * just the next node.  But if cancelled or apparently null,
15          * traverse backwards from tail to find the actual
16          * non-cancelled successor.
17          */
18         Node s = node.next;
19         if (s == null || s.waitStatus > 0) {
20             s = null;
21             for (Node t = tail; t != null && t != node; t = t.prev)
22                 if (t.waitStatus <= 0)  // 找到等待的线程
23                     s = t;
24         }
25         if (s != null)
26             LockSupport.unpark(s.thread);  // 唤醒线程
27     }

相对来说,unlock就简单许多了,两步:1.解锁,减引用计数。2.唤醒其他线程。