ReentrantLock及源码分析
简单介绍
我们知道
ReentrantLock
的直译就是可重入锁,那么可重入肯定是它的特性之一了。在详细分析它之前我们先得搞清楚
ReentrantLock
到底是一个什么样子的锁,这对我们理解它有很大的帮助
可重入锁和非可重入锁
什么是可重入锁,非可重入锁?
可重入锁又名递归锁,是指在同一个线程在外层方法获取锁的时候,再进入该线程的内层方法会自动获取锁(前提锁对象得是同一个对象或者class),不会因为之前已经获取过还没释放而阻塞。
ReentrantLock
和synchronized
都是可重入锁,可重入锁的一个优点是可一定程度避免死锁。
我们用一个例子来分析下:
public class Example{
public synchronized void doSomething() {
System.out.println("方法1执行...");
doOtherthing();
}
public synchronized void doOtherthing() {
System.out.println("方法2执行...");
}
}
上面的两个方法都被synchronized
关键字修饰,并且doSomething()
中调用了doOtherthing()
。因为synchronized
是可重入的,且锁已经在doSomething
中获取过,因此doOtherthing
的时候就可以直接获取该锁。
如果是不可重入锁,那么doOtherthing
之前需要等待doSomething
持有的锁释放,而doSomething
需要等待doOtherthing
执行完成,结果就是形成了循环依赖,出现死锁。
公平锁和非公平锁
什么是公平锁,非公平锁?
-
公平锁
公平锁是指多个线程按照申请锁的顺序来获取锁,线程直接进入队列中排队,队列中的第一个线程才能获得锁。
公平锁的优点是等待锁的线程不会饿死。
缺点是整体吞吐效率相对非公平锁要低,等待队列中除第一个线程以外的所有线程都会阻塞,CPU唤醒阻塞线程的开销比非公平锁大。
-
非公平锁
非公平锁是多个线程加锁时直接尝试获取锁,获取不到才会到等待队列的队尾等待。但如果此时锁刚好可用,那么这个线程可以无需阻塞直接获取到锁,所以非公平锁有可能出现后申请锁的线程先获取锁的场景。
非公平锁的优点是可以减少唤起线程的开销,整体的吞吐效率高,因为线程有几率不阻塞直接获得锁,CPU不必唤醒所有线程。
缺点是处于等待队列中的线程可能会造成饥饿,或者等很久才会获得锁。
后面我们讲
ReentrantLock
源码的时候就会把重点放在公平与非公平锁策略的区别上
独享锁和共享锁
什么是独享锁,共享锁?
-
独享锁
独享锁也叫排他锁,是指该锁一次只能被一个线程所持有。
如果线程T对数据A加上排它锁后,则其他线程不能再对A加任何类型的锁。获得排它锁的线程即能读数据又能修改数据。
synchronized
和Lock
的实现类ReentrantLock
就是互斥锁。 -
共享锁
共享锁是指该锁可被多个线程所持有。
如果线程T对数据A加上共享锁后,则其他线程只能对A再加共享锁,不能加排它锁。获得共享锁的线程只能读数据,不能修改数据。
Lock
的另一个实现类ReadWriteLock
,其读锁就是共享锁,而写锁却是独享锁。
独享锁与共享锁也是通过AQS
来实现的,通过实现不同的方法,来实现独享或者共享。
于是通过上面的分析我们知道了
ReentrantLock
是独享锁,可重入锁,悲观锁(虽然上面没说但它确实是)并且可以自定义策略实现公平锁或非公平锁
ReentrantLock分析
什么是ReentrantLock?
什么是ReentrantLock?
ReentrantLock
是 java.util.concurrent
(J.U.C)包中的锁。
我们可以把它分类成是「JUC锁」。
使用示例:
public class LockExample {
private Lock lock = new ReentrantLock();
public void run() {
lock.lock();
try {
for (int i = 0; i < 10; i++) {
System.out.print(i + " ");
}
} finally {
lock.unlock(); // 确保释放锁,从而避免发生死锁。
}
}
}
测试ReentrantLock的上锁效果:
@Test
public void testLock(){
LockExample lockExample = new LockExample();
ExecutorService executorService = Executors.newCachedThreadPool();
for (int i = 0; i < 3; i++) {
executorService.execute(lockExample::run);
}
}
测试结果:
0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9
ReentrantLock和synchronized关键字的锁实现的区别?
-
锁的实现
synchronized
是 JVM 实现的,而ReentrantLock
是 JDK 实现的。 -
性能
新版本 Java 对
synchronized
进行了很多优化,例如自旋锁等,synchronized
与ReentrantLock
大致相同。 -
等待可中断
当持有锁的线程长期不释放锁的时候,正在等待的线程可以选择放弃等待,改为处理其他事情。
ReentrantLock
可中断,而synchronized
不行。 -
公平锁
公平锁是指多个线程在等待同一个锁时,必须按照申请锁的时间顺序来依次获得锁。
synchronized
中的锁是非公平的,ReentrantLock
默认情况下也是非公平的,但是也可以是公平的。 -
锁绑定多个条件
一个
ReentrantLock
可以同时绑定多个Condition
对象。
什么时候使用ReentrantLock?
什么时候用ReentrantLock?什么时候用synchronized?
除非需要使用 ReentrantLock
的高级功能,否则优先使用 synchronized
。
因为 synchronized
是 JVM
实现的一种锁机制,JVM
原生地支持它,而 ReentrantLock
不是所有的 JDK 版本都支持。并且使用 synchronized
不用担心没有释放锁而导致死锁问题,因为 JVM
会确保锁的释放。
ReentrantLock源码分析
类的继承关系
ReentrantLock
实现了Lock
接口,Lock
接口中定义了lock
与unlock
相关操作,并且还存在newCondition
方法,表示生成一个条件。
public class ReentrantLock implements Lock, java.io.Serializable
类的内部类
ReentrantLock
类内部总共存在Sync
、NonfairSync
、FairSync
三个类,NonfairSync
与FairSync
类继承自Sync
类,Sync
类继承自AbstractQueuedSynchronizer
抽象类,关系图如下:
-
Sync类源码分析
我们添加锁和释放锁的大部分操作实际上都是在
Sync
中完成的源码以及分析如下:
abstract static class Sync extends AbstractQueuedSynchronizer { // 序列号 private static final long serialVersionUID = -5179523762034025860L; // 获取锁 abstract void lock(); // 非公平方式获取 final boolean nonfairTryAcquire(int acquires) { // 当前线程 final Thread current = Thread.currentThread(); // 获取状态 int c = getState(); if (c == 0) { // 表示没有线程正在竞争该锁 if (compareAndSetState(0, acquires)) { // 比较并设置状态成功,状态0表示锁没有被占用 // 设置当前线程独占 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; } // 试图在共享模式下获取对象状态,此方法应该查询是否允许它在共享模式下获取对象状态,如果允许,则获取它 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; } // 判断资源是否被当前线程占有 protected final boolean isHeldExclusively() { // While we must in general read state before owner, // we don't need to do so to check if current thread is owner return getExclusiveOwnerThread() == Thread.currentThread(); } // 新生一个条件 final ConditionObject newCondition() { return new ConditionObject(); } // 返回资源的占用线程 final Thread getOwner() { return getState() == 0 ? null : getExclusiveOwnerThread(); } // 返回状态 final int getHoldCount() { return isHeldExclusively() ? getState() : 0; } // 资源是否被占用 final boolean isLocked() { return getState() != 0; } // 自定义反序列化逻辑 private void readObject(java.io.ObjectInputStream s) throws java.io.IOException, ClassNotFoundException { s.defaultReadObject(); setState(0); // reset to unlocked state } }
Sync
类的方法和作用如下:-
lock
锁定,抽象方法。并未实现,留给具体子类实现
-
nonfairTryAcquire
非公平方式获取锁
-
tryRelease
试图在共享模式下获取对象状态,此方法应该查询是否允许它在共享模式下获取对象状态,如果允许,则获取它
-
isHeldExclusively
判断资源是否被当前线程占有
-
newCondition
新生一个条件
-
getOwner
返回占有资源的线程
-
getHoldCount
返回状态
-
isLocked
资源是否被占用
-
readObject
自定义反序列化逻辑
-
-
NonfairSync类源码分析
NonfairSync
类继承了Sync
类,表示采用非公平策略获取锁,其实现了Sync
类中抽象的lock
方法,源码如下:// 非公平锁 static final class NonfairSync extends Sync { // 版本号 private static final long serialVersionUID = 7316153563782823691L; // 获得锁 final void lock() { if (compareAndSetState(0, 1)) // 比较并设置状态成功,状态0表示锁没有被占用 // 把当前线程设置独占了锁 setExclusiveOwnerThread(Thread.currentThread()); else // 锁已经被占用,或者set失败 // 以独占模式获取对象,忽略中断 acquire(1); } protected final boolean tryAcquire(int acquires) { return nonfairTryAcquire(acquires); } }
从
lock
方法的源码可知,每一次都尝试获取锁,而并不会按照公平等待的原则进行等待,让等待时间最久的线程获得锁。 -
FairSync类
FairSync
类也继承了Sync
类,表示采用公平策略获取锁,其实现了Sync
类中的中抽象lock
方法,源码如下:// 公平锁 static final class FairSync extends Sync { // 版本序列化 private static final long serialVersionUID = -3000897897090466540L; final void lock() { // 以独占模式获取对象,忽略中断 acquire(1); } // 尝试公平获取锁 protected final boolean tryAcquire(int acquires) { // 获取当前线程 final Thread current = Thread.currentThread(); // 获取状态 int c = getState(); if (c == 0) { // 状态为0 if (!hasQueuedPredecessors() && compareAndSetState(0, acquires)) { // 不存在已经等待更久的线程并且比较并且设置状态成功 // 设置当前线程独占 setExclusiveOwnerThread(current); return true; } } else if (current == getExclusiveOwnerThread()) { // 状态不为0,即资源已经被线程占据 // 下一个状态 int nextc = c + acquires; if (nextc < 0) // 超过了int的表示范围 throw new Error("Maximum lock count exceeded"); // 设置状态 setState(nextc); return true; } return false; } }
跟踪
lock
方法的源码可知:当资源空闲时,它总是会先判断sync
队列(AbstractQueuedSynchronizer
中的数据结构)是否有等待时间更长的线程,如果存在,则将该线程加入到等待队列的尾部,实现了公平获取原则。其中,
FairSync
类的lock
的方法调用链如下(这里只给出了主要的方法):可以看出**
FairSync
的逻辑是只要资源被其它线程占用,该线程就会添加到sync
队列的尾部,而不会先尝试获取资源**。这也是和Nonfair
最大的区别**Nonfair
每一次都会尝试去获取资源**,如果此时该资源恰好被释放,则会被当前线程获取,这就造成了不公平的现象,当获取不成功,再加入队列尾部。
类的属性
public class ReentrantLock implements Lock, java.io.Serializable {
// 序列号
private static final long serialVersionUID = 7373984872572414699L;
// 同步队列
private final Sync sync;
}
-
sync
ReentrantLock
类的sync
非常重要,对ReentrantLock
类的操作大部分都直接转化为对Sync
和AbstractQueuedSynchronizer
类的操作。
类的构造函数
-
ReentrantLock()
public ReentrantLock() { // 默认非公平策略 sync = new NonfairSync(); }
默认是采用的非公平策略获取锁
-
ReentrantLock(boolean)
public ReentrantLock(boolean fair) { sync = fair ? new FairSync() : new NonfairSync(); }
可以传递参数确定采用公平策略或者是非公平策略,参数为true表示公平策略,否则,采用非公平策略:
ReentranLock核心函数分析
通过分析ReentrantLock
的源码,可知对其操作都转化为对Sync
对象的操作
由于Sync
继承了AQS
,所以基本上都可以转化为对AQS
的操作。如将ReentrantLock
的lock
函数转化为对Sync
的lock
函数的调用,而具体会根据采用的策略(如公平策略或者非公平策略)而调用到Sync
的不同子类。
所以可知,在ReentrantLock
的背后,是AQS
对其服务提供了支持。
公平与非公平锁策略
上面讲了这么多理论后我们再用一个
ReentrantLock
实现公平锁的实例来追踪方法调用链
公平锁策略测试
我们封装一个Thread
class MyThread extends Thread {
private Lock lock;
public MyThread(String name, Lock lock) {
super(name);
this.lock = lock;
}
public void run () {
lock.lock();
try {
System.out.println(Thread.currentThread() + " running");
try {
Thread.sleep(500);
} catch (InterruptedException e) {
e.printStackTrace();
}
} finally {
lock.unlock();
}
}
}
在测试方法中给它的lock
成员注入ReentrantLock
的实例,并且策略选用公平锁策略
@Test
public void testFairLock() throws InterruptedException {
Lock lock = new ReentrantLock(true);
MyThread t1 = new MyThread("t1", lock);
MyThread t2 = new MyThread("t2", lock);
MyThread t3 = new MyThread("t3", lock);
t1.start();
t2.start();
t3.start();
Thread.sleep(5000L);
}
运行结果(某一次):
Thread[t1,5,main] running
Thread[t2,5,main] running
Thread[t3,5,main] running
根据这么一次结果我们来分析源码执行流程:
-
t1线程执行
lock.lock
方法下图给出了该方法的调用链(主要方法)
最主要的方法都集中在如下方法及方法体中:
它需要为当前试图获取锁的线程判断锁的
state
(同步状态)如果当前线程之前没有在排队等待锁的线程且通过
CAS
设置state
成功,也就是获取锁成功,则将当前线程设置为独占线程由调用流程可知,t1线程成功获取了资源,可以继续执行
-
t2线程执行
lock.lock
我们知道
AQS
的Node
内部类结构如下:且知道
waitStatus
的值和对应含义如下:枚举 含义 0 当一个Node被初始化时的默认值 CANCELLED 为1,表示线程获取锁的请求已经取消了 CONDITION 为-2,表示节点在等待队列中,节点线程等待唤醒 PROPAGATE 为-3,当前线程处在SHARED情况下,该字段才会使用 SIGNAL 为-1,表示线程已经准备好了,就等资源释放了 下图给出了方法调用中的主要方法:
主要方法集中在:
我们由于这一次加锁
tryAcquire
会因为t2并不是当前锁的独占线程而返回false
,于是会执行 && 连接的后面的方法addWaiter
会将t2放到等待锁队列的尾巴,acquireQueued
会遍历队列,给所有的节点赋上正确的waitStatus
,并且给没能成功获取锁的线程调用LockSupport.park
,让它进入等待状态。由上图可知,最后的执行结果是t2线程会被禁止,因为调用了
LockSupport.park
。 -
t3线程执行
lock.lock
下图给出了方法调用的主要方法:
承接上面,t1没有释放锁,t2排队在t1后面,t3则被排在t2后面,然后根据各个被调用方法的逻辑赋上正确的状态,和调用
LockSupport.park
由上图可知,最后的结果是t3线程会被禁止,因为调用了
LockSupport.park
。 -
t1线程调用了
lock.unlock
下图给出了方法调用的主要方法:
tryRelease
中我们主要尝试将当前独占线程消除,将同步状态设置为初始值如上图所示,最后,head的状态会变为0,t2线程会被
unpark
,即t2线程可以继续运行。此时t3线程还是被禁止。 -
t2获得cpu资源,继续运行,由于t2之前被
park
了,现在需要恢复之前的状态下图给出了方法调用中的主要方法:
在
setHead
函数中会将head设置为之前head的下一个结点,并且将pre域与thread域都设置为null,在acquireQueued
返回之前,sync queue
就只有两个结点了。 -
t2执行
lock.unlock
,下图给出了方法调用中的主要方法。由上图可知,最终unpark t3线程,让t3线程可以继续运行。
-
t3线程获取cpu资源,恢复之前的状态,继续运行。
最终达到的状态是sync queue中只剩下了一个结点,并且该节点除了状态为0外,其余均为null。
-
t3执行lock.unlock
下图给出了方法调用中的主要方法
最后的状态和之前的状态是一样的,队列中有一个空节点,头节点为尾节点均指向它。
非公平锁策略
我们刚才重点讲了公平锁策略,接下来我们用对比这两种策略在源码层面的不同来说明非公平锁策略:
上图左为公平锁的加锁源码,右为非公平锁的加锁源码。
通过上图中的源代码对比,我们可以明显的看出公平锁与非公平锁的lock()
方法唯一的区别就在于公平锁在获取同步状态时多了一个限制条件:hasQueuedPredecessors()
。
该方法上面讲公平锁策略的时候已经提及,是AQS
中的方法:
public final boolean hasQueuedPredecessors() {
Node h, s;
if ((h = head) != null) {
if ((s = h.next) == null || s.waitStatus > 0) {
s = null; // traverse in case of concurrent cancellation
for (Node p = tail; p != h && p != null; p = p.prev) {
if (p.waitStatus <= 0)
s = p;
}
}
if (s != null && s.thread != Thread.currentThread())
return true;
}
return false;
}
它的工作就是找到当前打算获取锁的线程之前是否有已经在等待的线程,如果有则返回true
,如果没有则返回false
该判断加在加锁之前也就满足了我们要的「公平锁按照排队顺序获取锁」的需求了。
ReentrantLock可重入锁实现
之前我们说过ReentrantLock和synchronized都是重入锁,那么我们通过重入锁ReentrantLock以及非可重入锁NonReentrantLock的源码来对比分析一下为什么非可重入锁在重复调用同步资源时会出现死锁。
ReentrantLock在源码层面如何实现可重入锁?
ReentrantLock
和NonReentrantLock
都继承父类AQS
,其父类AQS
中维护了一个同步状态state来计数重入次数,state初始值为0。
-
加锁
获取锁先判断,如果当前线程已经是占有锁的线程,则state值+1,并且返回true。
-
释放锁
释放锁时也是先判断当前线程是否是已经占有锁的线程,然后再判断status。如果status等于0,才真正的释放锁。
NonReentrantLock的实现为什么是非可重入锁?
-
加锁
非重入锁是直接尝试获取锁
-
释放锁
释放锁时直接将state设置成0
ReentrantLock使用
基本使用
我们用一个累加的例子来测试ReentLock的使用
我们编写一个Counter
类,并且实现并发不安全的add
,应用synchronized
的addWithSynchronized
和应用ReentrantLock
的addWithReentrantLock
public static class Counter{
private final Lock lock=new ReentrantLock();
private int count;
public int getCount(){
return this.count;
}
public void add(){
++count;
}
public void addWithReentrantLock(){
lock.lock();
try{
++count;
}finally {
lock.unlock();
}
}
public void addWithSynchronized(){
synchronized(this){
++count;
}
}
}
我们按照如下测试方法,依次测试几个方法
@Test
public void testCounter() throws InterruptedException {
Counter counter=new Counter();
ExecutorService executorService=Executors.newFixedThreadPool(10);
int taskCount=10000;
CountDownLatch countDownLatch=new CountDownLatch(taskCount);
for (int i=0;i<taskCount;i++){
executorService.execute(()->{
counter.addWithReentrantLock();
countDownLatch.countDown();
});
}
countDownLatch.await();
System.out.println(counter.getCount());
}
测试结果如下:
// add方法
9987
// addWithReentrantLock
10000
// addWithSynchronized
10000
所以我们知道ReentrantLock
可以按照上面的方式来锁住临界区,替代synchroinzed
ReentrantLock和synchronized在使用上的异同?
而我们要注意,synchronized
是Java语言层面提供的语法,所以我们不需要考虑异常,而ReentrantLock
是Java代码实现的锁,我们就必须先获取锁,然后在finally
中正确释放锁。
和synchronized
不同的是,ReentrantLock
可以尝试获取锁:
if (lock.tryLock(1, TimeUnit.SECONDS)) {
try {
doSomething()
} finally {
lock.unlock();
}
}
doOtherthing();
上面的代码在尝试获取锁(lock.tryLock
)的时候,最多等待1秒。如果1秒后仍未获取到锁,tryLock()
返回false
,程序就可以做一些额外处理,而不是无限等待下去。
所以,使用ReentrantLock
比直接使用synchronized
更安全,线程在tryLock()
失败的时候不会导致死锁。
Condition
什么是Condition?
synchronized
可以配合wait
和notify
实现线程在条件不满足时等待,条件满足时唤醒,用ReentrantLock
我们怎么编写wait
和notify
的功能呢?
答案是使用Condition
对象来实现wait
和notify
的功能。
刚好我在博客绕不过的并发编程--Java线程API中有写过wait
和notify
的例子:
@Test
public void testThreadWait() throws InterruptedException{
Object office=new Object();
Thread fish=new Thread(()->{
synchronized (office){
log.debug("我先摸了,老板来了叫我。");
try {
office.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
log.debug("老板来了,好好工作!");
}
},"鱼鱼");
Thread mo=new Thread(()->{
log.debug("收到收到");
synchronized (office) {
log.debug("别摸鱼了,老板来了!");
office.notify();
}
},"摸摸");
log.debug("开始");
fish.start();
mo.start();
}
运行结果:
17:30:20.302 [main] DEBUG com.dyh.TestReentrantLock - 开始
17:30:20.302 [鱼鱼] DEBUG com.dyh.TestReentrantLock - 我先摸了,老板来了叫我。
17:30:20.302 [摸摸] DEBUG com.dyh.TestReentrantLock - 收到收到
17:30:20.306 [摸摸] DEBUG com.dyh.TestReentrantLock - 别摸鱼了,老板来了!
17:30:20.306 [鱼鱼] DEBUG com.dyh.TestReentrantLock - 老板来了,好好工作!
我们通过ReentrantLock
来改造:
@Test
public void testCondition() throws InterruptedException {
Lock lock=new ReentrantLock();
Condition condition=lock.newCondition();
Thread fish=new Thread(()->{
lock.lock();
try {
log.debug("我先摸了,老板来了叫我。");
condition.await();
log.debug("老板来了,好好工作!");
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
lock.unlock();
}
},"鱼鱼");
Thread mo=new Thread(()->{
log.debug("收到收到");
lock.lock();
try{
log.debug("别摸鱼了,老板来了!");
condition.signalAll();
}finally {
lock.unlock();
}
},"摸摸");
log.debug("开始");
fish.start();
mo.start();
Thread.sleep(1000L);
}
运行结果和上面是一样的,说明我们完成了改造:
17:30:20.302 [main] DEBUG com.dyh.TestReentrantLock - 开始
17:30:20.302 [鱼鱼] DEBUG com.dyh.TestReentrantLock - 我先摸了,老板来了叫我。
17:30:20.302 [摸摸] DEBUG com.dyh.TestReentrantLock - 收到收到
17:30:20.306 [摸摸] DEBUG com.dyh.TestReentrantLock - 别摸鱼了,老板来了!
17:30:20.306 [鱼鱼] DEBUG com.dyh.TestReentrantLock - 老板来了,好好工作!
Condition的API解释
使用Condition
时,引用的Condition
对象必须从Lock
实例的newCondition()
返回,这样才能获得一个绑定了Lock
实例的Condition
实例。
Condition
提供的await()
、signal()
、signalAll()
原理和synchronized
锁对象的wait()
、notify()
、notifyAll()
是一致的,并且其行为也是一样的:
await()
会释放当前锁,进入等待状态;signal()
会唤醒某个等待线程;signalAll()
会唤醒所有等待线程;- 唤醒线程从
await()
返回后需要重新获得锁。
此外,和tryLock()
类似,await()
可以在等待指定时间后,如果还没有被其他线程通过signal()
或signalAll()
唤醒,可以自己醒来:
if (condition.await(1, TimeUnit.SECOND)) {
// 被其他线程唤醒
} else {
// 指定时间内没有被其他线程唤醒
}
小结
本篇我们主要基于ReentrantLock
的源码来分析了它是一个什么样的锁。并且在最后通过和synchronized
加锁的例子进行类比,了解了ReentrantLock
该如何使用。
当然ReentrantLock
主要的方法最终都是在调用AQS
,这方面的内容我将在后面的博客中详细解释。
本篇参考:
转载自:https://juejin.cn/post/7169603738489847821