ThreadLocal原理分析--结合Spring事务
ThreadLocal
本文以JDK21为例子,其实大致方法和JDK8都一样。
1.基本介绍
ThreadLocal
是一个在多线程编程中常用的概念,不同编程语言中实现方式不同,但核心思想一致:为每个使用该变量的线程都提供一个独立的变量副本,每个线程都可以独立地改变自己的副本,而不会影响其他线程所对应的副本。
主要作用
- 线程安全:避免多线程共享变量时需要进行同步操作(如加锁),从而简化并发编程。
- 传递上下文:在同一个线程的不同方法中传递数据,避免显式传递参数。
它的几个API:
方法声明 | 描述 |
---|---|
ThreadLocal() | 创建ThreadLocal对象 |
public void set(T value) | 设置当前线程绑定的局部变量 |
public T get() | 获取当前线程绑定的局部变量 |
public void remove() | 移除当前线程绑定的局部变量 |
下面来简单使用一下:
public class SimpleLocalTest {private static ThreadLocal<String> threadLocal = new ThreadLocal<>();public static void main(String[] args) {threadLocal.set("main" + "变量");new Thread(() -> {// 在线程1中设置变量threadLocal.set("thread1" + "变量");try {Thread.sleep(1000);} catch (InterruptedException e) {e.printStackTrace();}// 在线程1中得到的仍然是该变量的值,并没有得到其他线程的值,达到了线程间数据隔离System.out.println(Thread.currentThread().getName() + " : " + threadLocal.get());threadLocal.remove(); // 删除掉}, "线程1").start();new Thread(() -> {threadLocal.set("thread2" + "变量"); // 同理try {Thread.sleep(1000);} catch (InterruptedException e) {e.printStackTrace();}System.out.println(Thread.currentThread().getName() + " : " + threadLocal.get());threadLocal.remove();}, "线程2").start();// 主线程的本地变量值System.out.println(Thread.currentThread().getName() + " : " + threadLocal.get());threadLocal.remove();}
}
2.对比Synchronized
ThreadLocal
和 Synchronized
(或其他同步机制)都用于处理多线程环境下的并发问题,但它们的核心思路和应用场景完全不同。
ThreadLocal | Synchronized |
---|---|
避免共享:为每个线程创建独立的变量副本,线程之间互不干扰。 | 控制共享:通过锁机制保证多线程对共享资源的有序访问。 |
空间换时间:每个线程单独存储数据,牺牲内存换取无锁的高效。 | 时间换空间:通过阻塞其他线程的访问,保证共享资源的安全性。 |
每个线程内部通过 ThreadLocalMap 存储自己的变量副本,键是 ThreadLocal 对象,值是变量值。 | 基于 JVM 内置锁(Monitor)实现,通过锁的获取和释放控制代码块或方法的访问权限。 |
通过 get() /set() 直接操作当前线程的局部变量,无需锁。 | 锁竞争时,未获取锁的线程会进入阻塞状态(或自旋),直到锁释放。 |
线程隔离:每个线程需要独立操作变量(如用户会话、数据库连接)。 | 共享资源保护:多个线程需要操作同一资源(如计数器、缓存)。 |
避免线程安全问题:通过隔离变量副本,无需同步(如 SimpleDateFormat )。 | 原子性保证:确保一段代码的原子执行(如余额扣减)。 |
性能敏感场景:避免锁竞争的开销(如线程池中的上下文传递)。 | 临界区保护:保护共享数据的读写一致性。 |
内存泄漏风险:若未调用 remove() ,线程池中的线程可能因 ThreadLocalMap 的强引用导致内存泄漏。 | 性能开销:锁竞争激烈时,线程阻塞和唤醒会带来性能损耗(尤其是重量级锁)。 |
无锁操作:get() /set() 直接操作线程私有数据,性能极高。 | 锁优化:JVM 对 synchronized 有锁升级机制(偏向锁→轻量级锁→重量级锁)。 |
- 两者可以结合使用:例如用
ThreadLocal
保存线程私有数据【数据隔离】,用Synchronized
保护共享状态【数据共享】。 - 现代框架中的典型应用:Spring 的事务管理通过
ThreadLocal
保存数据库连接
3.原理分析
首先从上面的ThreadLocal简单使用案例的方法来看看。
①set
// 首先是构造方法
public ThreadLocal() {} // 没啥好看的// 然后是set方法
public void set(T value) {//public static native Thread currentThread();这是个native方法//currentThread方法返回正在被执行的线程的信息。set(Thread.currentThread(), value);if (TRACE_VTHREAD_LOCALS) { // 这个就不用看了dumpStackIfVirtualThread();}
}
private void set(Thread t, T value) {ThreadLocalMap map = getMap(t); // 调用getMap方法if (map != null) {// key 是Threadmap.set(this, value); // 如果map不是null,就把kv设置进去} else {// 创建mapcreateMap(t, value);}
}// 返回Thread的threadLocals变量
ThreadLocalMap getMap(Thread t) {return t.threadLocals;
}void createMap(Thread t, T firstValue) {// 把Thread的这个变量初始化t.threadLocals = new ThreadLocalMap(this, firstValue);
}
// ThreadLocalMap的构造方法
ThreadLocalMap(ThreadLocal<?> firstKey, Object firstValue) {// 初始化哈希表,然后把第一个kv放进去table = new Entry[INITIAL_CAPACITY];int i = firstKey.threadLocalHashCode & (INITIAL_CAPACITY - 1);table[i] = new Entry(firstKey, firstValue);size = 1;setThreshold(INITIAL_CAPACITY);
}
经过上面的源码分析我们可以得出以下信息:
- 每个Thread对象里面都有一个threadLocals变量,它的类型是ThreadLocalMap;
- ThreadLocalMap是ThreadLocal的静态内部类
下面来简单看一下ThreadLocalMap(ThreadLocal的静态内部类)
static class ThreadLocalMap {static class Entry extends WeakReference<ThreadLocal<?>> {/** The value associated with this ThreadLocal. */Object value;Entry(ThreadLocal<?> k, Object v) {super(k);value = v;}}private static final int INITIAL_CAPACITY = 16;private Entry[] table;private int size = 0;private int threshold;// set// getEntry【返回hash索引位置上的对象Entry】private Entry getEntry(ThreadLocal<?> key) {int i = key.threadLocalHashCode & (table.length - 1);Entry e = table[i];if (e != null && e.refersTo(key))return e;elsereturn getEntryAfterMiss(key, i, e);}.......
}
可以把它看作为一个Map。到这里,set方法算是知道了,然后还大致知道了他们之间的关系,如下图:【不同线程之间的ThreadLocal他们的value是不一样的,达到了线程隔离的效果】
②get
public T get() {return get(Thread.currentThread());
}private T get(Thread t) {//getMap在面已经知道了ThreadLocalMap map = getMap(t);if (map != null) {ThreadLocalMap.Entry e = map.getEntry(this);if (e != null) {@SuppressWarnings("unchecked")T result = (T) e.value;return result;}}// 如果t.threadLocals == nullreturn setInitialValue(t);
}// 初始化
private T setInitialValue(Thread t) {T value = initialValue(); // 调用这个ThreadLocalMap map = getMap(t);if (map != null) {map.set(this, value);} else {createMap(t, value);}if (this instanceof TerminatingThreadLocal<?> ttl) {TerminatingThreadLocal.register(ttl);}if (TRACE_VTHREAD_LOCALS) {dumpStackIfVirtualThread();}return value;
}
/*
此方法的作用是返回该线程局部变量的初始值。
- 这个方法是一个延迟调用方法,从上面的代码我们得知,在set方法还未调用而先调用了get方法时才执行,并且仅执行1次。
- 这个方法直接返回一个null。
- 如果想要一个除null之外的初始值,可以重写此方法。(该方法是一个protected的方法,显然是为了让子类覆盖而设计的)
*/
protected T initialValue() {return null;
}
get方法就是首先拿到执行方法的线程是哪一个,然后从该线程的threadLocals(ThreadLocalMap)上以该Thread对象为key,拿到Entry的value值。
③remove
public void remove() {remove(Thread.currentThread());
}
private void remove(Thread t) {ThreadLocalMap m = getMap(t);if (m != null) {// 实际是ThreadLocalMap的remove方法m.remove(this);}
}
// 静态内部类ThreadLocalMap.java
private void remove(ThreadLocal<?> key) {Entry[] tab = table;int len = tab.length;int i = key.threadLocalHashCode & (len-1);for (Entry e = tab[i];e != null;e = tab[i = nextIndex(i, len)]) {if (e.refersTo(key)) {e.clear();expungeStaleEntry(i);return;}}
}
// 会调用这个
/*
主要用于清理因弱引用导致key为null 的过期Entry,从而避免内存泄漏-见下文
*/
private int expungeStaleEntry(int staleSlot) {。。。。
}
直接将ThrealLocal 对应的值从当前的Thread中的ThreadLocalMap中删除
④再说ThreadLocalMap
我们对于ThreadLocal的get、set或者是remove,本质上都是在操作ThreadLocaMap里面的Entry数组
static class ThreadLocalMap {static class Entry extends WeakReference<ThreadLocal<?>> {Object value;Entry(ThreadLocal<?> k, Object v) {super(k);value = v;}}
}
Entry将ThreadLocal作为Key【为弱引用】,值作为value保存,它继承自WeakReference. 这个弱引用是啥?前面还说了,可以把它看作为一个Map(ThreadLocalMap并没有实现Map接口),但是我们熟知的HashMap之类的,key可能是会冲突的,这里的ThreadLocalMap里面的key冲突了咋办呢?本节就来分析一下。
1) 冲突
发生冲突的时候,那么肯定是在set/get的时候把,我们看一下set方法
// 静态内部类ThreadLocalMap.java
private void set(ThreadLocal<?> key, Object value) {Entry[] tab = table;int len = tab.length;int i = key.threadLocalHashCode & (len-1);/*这里的nextIndex如下【说实话就是 i+1】private static int nextIndex(int i, int len) {return ((i + 1 < len) ? i + 1 : 0);}*/for (Entry e = tab[i]; e != null; e = tab[i = nextIndex(i, len)]) {if (e.refersTo(key)) { // 如果key相同e.value = value; // value直接覆盖return;}//如果当前位置是空的,就初始化一个Entry对象放在位置i上if (e.refersTo(null)) {replaceStaleEntry(key, value, i);return;}}// 找到了下标为null的位置tab[i] = new Entry(key, value); // 这个位置设置上key,valueint sz = ++size;if (!cleanSomeSlots(i, sz) && sz >= threshold)rehash();
}
根据上面的源码分析,我们得知,set的时候,根据ThreadLocal对象的hash值,定位到table中的位置i,然后判断该位置是否为空;如果key相同,直接覆盖旧值;如果是空的,初始化一个Entry对象放在位置i上;否则,就在i的位置上,往后一个一个找。【线性探测法】
再来看一下get方法:
// 静态内部类ThreadLocalMap.java
private Entry getEntry(ThreadLocal<?> key) {int i = key.threadLocalHashCode & (table.length - 1);Entry e = table[i];if (e != null && e.refersTo(key))return e;elsereturn getEntryAfterMiss(key, i, e);
}
private Entry getEntryAfterMiss(ThreadLocal<?> key, int i, Entry e) {Entry[] tab = table;int len = tab.length;// 相等就直接返回,不相等就继续+1查找,找到相等为止。while (e != null) {if (e.refersTo(key))return e;if (e.refersTo(null))expungeStaleEntry(i);elsei = nextIndex(i, len);e = tab[i];}return null;
}
上面的源码很简单吧。可以知道,ThreadLocal在hash冲突严重的时候,他的效率其实是不高的。
2) 弱引用
那么这个弱引用呢?说起这个,肯定会提到ThreadLocal老生常谈的内存泄漏问题了。
内存泄漏:【不会再被使用的对象或者变量占用的内存不能被回收,就是内存泄露。】
Memory overflow:内存溢出,没有足够的内存提供申请者使用。
Memory leak:内存泄漏是指程序中己动态分配的堆内存由于某种原因程序未释放或无法释放,造成系统内存的浪费,导致程序运行速度减慢甚至系统溃等严重后果。内存泄漏的堆积终将导致内存溢出。
在 Java 中,弱引用(Weak Reference) 是一种特殊的引用类型,它的核心特点是:当垃圾回收(GC)发生时,无论内存是否充足,弱引用指向的对象【对象必须仅被弱引用指向(没有任何强引用)】都会被回收。这与其他引用类型(如强引用、软引用、虚引用)有显著区别。下面通过对比不同引用类型,详细解释弱引用的特性及用途。
引用类型 | GC 行为 | 典型应用场景 | 实现类 |
---|---|---|---|
强引用 | 对象有强引用时,永远不会被回收(如果想取消强引用和某个对象之间的关联,可以显式地将引用赋值为null,这样可以使JVM在合适的时间就会回收该对象。) | 日常对象的使用(如 new Object() ) | 默认引用类型 |
弱引用 | 只要发生 GC,就会被回收 | 缓存、ThreadLocal 防内存泄漏 | WeakReference<T> |
软引用 | 内存不足时才会被回收 | 缓存(如图片缓存) | SoftReference<T> |
虚引用 | 无法通过虚引用访问对象,GC 后会收到通知 | 资源清理跟踪(如堆外内存释放) | PhantomReference<T> |
弱引用的回收时机由 GC 决定,无法精确控制。
在弄清楚上述的内存泄漏和弱引用问题之前,我们需要先知道ThreadLocal体系的对象存在哪里的,如下图:
Thread ThreadRef = new Thread(xx);就是强引用,下图中用实线连接起来的,ThreadLocal同理。
在 ThreadLocal
的使用中,内存泄漏的核心原因是 ThreadLocalMap
的 Entry 对 value 是强引用,而 Entry 的 key 是对 ThreadLocal
实例的弱引用。当 ThreadLocal
实例失去外部强引用时,GC 会回收 key(弱引用),key就为null了,但 value 仍被强引用保留在 Entry 中,若线程长期存活(如线程池线程),value 将无法被回收,导致内存泄漏。那么,什么时候ThreadLocal会失去外部的强引用呢?下面给出两个例子
// 下面两个例子中,线程一直存活
// 1.局部变量场景
public void processRequest() {ThreadLocal<User> userContext = new ThreadLocal<>(); // 局部变量userContext.set(currentUser);// ...业务逻辑...
}
// 另一个线程一直存活
Thread(() -> {processRequest();......
}).start();
/*
在上述场景中:
方法结束时,局部变量 userContext 的强引用被释放。
此时 ThreadLocal 实例仅被 ThreadLocalMap 的弱引用(Entry 的 key)指向。
下次 GC 发生时,ThreadLocal 实例的 key 被回收,Entry 变为 key=null, value=强引用。
若线程持续运行(如线程池线程),value 无法自动回收,导致泄漏。
*/// 2.实例变量所属对象被回收
public class Service {// 在这里,ThreadLocal作为了对象的实例变量private ThreadLocal<Connection> connHolder = new ThreadLocal<>(); public void execute() {connHolder.set(getConnection());// ...使用连接...}
}// 使用示例
void process() {Service service = new Service();service.execute();service = null; // Service 实例失去强引用,可能被回收
}
// 另一个线程一直存活
Thread(() -> {process();......
}).start();
总结一下,何时出现 “无外部强引用”?
- 当
ThreadLocal
实例不再被任何强引用直接或间接指向的时候:- 局部变量超出作用域。
- 所属对象实例被回收。
- 但线程仍存活(如线程池线程长期复用)。
我们平时开发都是使用的线程池,线程有的可能会一直存活。
为了避免出现上述情况,我们平时使用完成之后,尽量在业务结束并且不需要该线程本地变量的时候,给它remove掉。
通过上述分析,我们可以看到一个非常致命的条件,那就是线程存活的时间 大于了 ThreadLocal的强引用存活时间。如果说,ThreadLocal 和 Thread的生命周期一样长,即时我们不remove,一样不会内存泄漏的。( 如下图 )
ThreadLocal其实它有兜底措施的:【就是上面的remove方法里面调用的expungeStaleEntry
】
/*
在这些方法也会调用的
set()方法: 当插入新值时发现哈希冲突,且当前槽位的Entry已过期,触发清理流程
get()方法: 当查询Entry未命中(key 不匹配)时,触发清理以优化后续查询效率
*/
private int expungeStaleEntry(int staleSlot) {Entry[] tab = table;int len = tab.length;// 清理当前槽位tab[staleSlot].value = null;tab[staleSlot] = null;size--;// Rehash until we encounter nullEntry e;int i;for (i = nextIndex(staleSlot, len);(e = tab[i]) != null;i = nextIndex(i, len)) {ThreadLocal<?> k = e.get();if (k == null) {// 清理过期 Entrye.value = null;tab[i] = null;size--;} else {// 重新哈希有效 Entryint h = k.threadLocalHashCode & (len - 1);if (h != i) {tab[i] = null;while (tab[h] != null)h = nextIndex(h, len);tab[h] = e;}}}return i;
}
当 ThreadLocal
实例被垃圾回收(GC)后,其对应的 Entry
的 key
会变为 null
(弱引用特性),但 value
仍被强引用保留。expungeStaleEntry
会遍历哈希数组,将这些 key
为 null
的 Entry
的 value
置为 null
,并释放 Entry
对象本身,从而切断 value
的强引用链,帮助 GC 回收内存
删除的时候:
- 从指定位置开始向后遍历哈希数组,若发现
Entry
的key
为null
,则清除其value
并释放Entry
对象。 - 若
Entry
的key
有效,则重新计算其哈希值(k.threadLocalHashCode & (len - 1)
),检查是否需要调整位置以优化哈希分布
从上述分析可以看出:expungeStaleEntry
仅清理从指定位置开始的连续过期 Entry
,而非整个哈希表,因此无法完全避免内存泄漏;清理效果取决于 set
、get
等方法的调用频率,若线程长期不操作 ThreadLocal
,残留的 value
仍可能堆积。
那如果key是强引用呢?会出现上述内存泄漏问题吗?
为什么key设计成弱引用呢?
当 ThreadLocalMap
的键是 弱引用 时:
- 外部强引用
threadLocal
被置为null
后,键(弱引用)指向的ThreadLocal
对象仅被弱引用持有。 - GC 会回收
ThreadLocal
对象,并将对应的弱引用放入引用队列,key就为null了。 ThreadLocalMap
在下次操作(如get()
、set()
)时,会清理引用队列中的失效条目,释放值对象的内存。【就是我们说的兜底措施嘛】
如果 ThreadLocalMap
的键是 强引用,会发生:
threadLocal
变量被置为null
,但ThreadLocalMap
中的键仍强引用着ThreadLocal
对象。ThreadLocal
对象无法被 GC 回收,导致其对应的值(BigObject
)也无法被回收,即使线程可能长期存活(如线程池中的线程)。- 内存泄漏:无用的键值对会一直存在于
ThreadLocalMap
中
5.框架中的应用
这一节只看一下ThreadLocal在Spring事务中的应用。
在事务中,需要做到如下保证:
- 每个事务的执行需要通过数据源连接池获取到数据库的connetion。
- 为了保证所有的数据库操作都属于同一个事务,事务使用的连接必须是同一个,也就是在一个线程里面需要操作同一个连接
- 线程隔离:在多线程并发的情况下,每个线程都只能操作各自的connetion,不能使用其他线程的连接
在Spring事务的源码中:
DataSourceTransactionManager.java
@Override
protected void doBegin(Object transaction, TransactionDefinition definition) {DataSourceTransactionObject txObject = (DataSourceTransactionObject) transaction;Connection con = null;try {if (!txObject.hasConnectionHolder() ||txObject.getConnectionHolder().isSynchronizedWithTransaction()) {Connection newCon = obtainDataSource().getConnection();...txObject.setConnectionHolder(new ConnectionHolder(newCon), true);}txObject.getConnectionHolder().setSynchronizedWithTransaction(true);con = txObject.getConnectionHolder().getConnection();....// 手动开启一个事务if (con.getAutoCommit()) {txObject.setMustRestoreAutoCommit(true);...con.setAutoCommit(false);}prepareTransactionalConnection(con, definition);txObject.getConnectionHolder().setTransactionActive(true);int timeout = determineTimeout(definition);if (timeout != TransactionDefinition.TIMEOUT_DEFAULT) {txObject.getConnectionHolder().setTimeoutInSeconds(timeout);}// Bind the connection holder to the thread.if (txObject.isNewConnectionHolder()) {TransactionSynchronizationManager.bindResource(obtainDataSource(), txObject.getConnectionHolder());}}catch (Throwable ex) {if (txObject.isNewConnectionHolder()) {DataSourceUtils.releaseConnection(con, obtainDataSource());txObject.setConnectionHolder(null, false);}throw new CannotCreateTransactionException("Could not open JDBC Connection for transaction", ex);}
}
TransactionSynchronizationManager.java
:下面就把线程和ThreadLocal绑定起来了。
public abstract class TransactionSynchronizationManager {private static final ThreadLocal<Map<Object, Object>> resources =new NamedThreadLocal<>("Transactional resources");....public static void bindResource(Object key, Object value) throws IllegalStateException {// 这个其实是数据源dataSource对象Object actualKey = TransactionSynchronizationUtils.unwrapResourceIfNecessary(key);Assert.notNull(value, "Value must not be null");// 拿到当前线程的mapMap<Object, Object> map = resources.get();// 为空,初始化一下if (map == null) {map = new HashMap<>();resources.set(map);}Object oldValue = map.put(actualKey, value);if (oldValue instanceof ResourceHolder && ((ResourceHolder) oldValue).isVoid()) {oldValue = null;}if (oldValue != null) {throw new IllegalStateException("Already value [" + oldValue + "] for key [" + actualKey + "] bound to thread");}}// 清理remove操作@Nullableprivate static Object doUnbindResource(Object actualKey) {Map<Object, Object> map = resources.get();if (map == null) {return null;}Object value = map.remove(actualKey);// Remove entire ThreadLocal if empty...if (map.isEmpty()) {resources.remove();}// Transparently suppress a ResourceHolder that was marked as void...if (value instanceof ResourceHolder && ((ResourceHolder) value).isVoid()) {value = null;}return value;}
}
在上面源码中,每个线程里面的本地变量是一个map,map是以DateSource为key,ConnectionHolder为value。在一个系统可以有多个DataSource,connection又是由相应的DataSource得到的。所以ThreadLocal维护的是以DataSource作为key, 以ConnectionHolder为value的一个Map
总结一下,在开启事务的时候,绑定资源,Spring 事务管理器(如 DataSourceTransactionManager
)会调用 doBegin()
,获取数据库连接并绑定到 ThreadLocal
。
在执行sql的时候,MyBatis 通过 SpringManagedTransaction
获取当前事务的连接
// SqlSessionUtils.java
public static SqlSession getSqlSession(SqlSessionFactory sessionFactory, ExecutorType executorType,PersistenceExceptionTranslator exceptionTranslator) {.....SqlSessionHolder holder = (SqlSessionHolder) TransactionSynchronizationManager.getResource(sessionFactory);SqlSession session = sessionHolder(executorType, holder);if (session != null) {return session;}LOGGER.debug(() -> "Creating a new SqlSession");session = sessionFactory.openSession(executorType);registerSessionHolder(sessionFactory, executorType, exceptionTranslator, session);return session;
}// TransactionSynchronizationManager.java
@Nullable
public static Object getResource(Object key) {Object actualKey = TransactionSynchronizationUtils.unwrapResourceIfNecessary(key);return doGetResource(actualKey);
}
最后提交事务,相关清理工作… 就是remove那些了。
总流程如下所示:
// TransactionAspectSupport.java
@Nullable
protected Object invokeWithinTransaction(Method method, @Nullable Class<?> targetClass,final InvocationCallback invocation) throws Throwable {if (txAttr == null || !(ptm instanceof CallbackPreferringPlatformTransactionManager)) {// 1.绑定连接的ThreadLocalTransactionInfo txInfo = createTransactionIfNecessary(ptm, txAttr, joinpointIdentification);Object retVal;try {// 2.进去往下一步执行-- 执行sql【jdbcTempalte、sqlsessionTemplate....】retVal = invocation.proceedWithInvocation();}catch (Throwable ex) {// 异常回滚completeTransactionAfterThrowing(txInfo, ex);throw ex;}finally {// 3.清理工作cleanupTransactionInfo(txInfo);}if (retVal != null && vavrPresent && VavrDelegate.isVavrTry(retVal)) {// Set rollback-only in case of Vavr failure matching our rollback rules...TransactionStatus status = txInfo.getTransactionStatus();if (status != null && txAttr != null) {retVal = VavrDelegate.evaluateTryFailure(retVal, txAttr, status);}}commitTransactionAfterReturning(txInfo);return retVal;}
}
end.参考
留一个问题:ThreadLocal还有一个很经典的问题,那就是在父子线程中通信的问题了。
1.https://blog.csdn.net/qq_35190492/article/details/107599875
2.https://blog.csdn.net/u010445301/article/details/111322569
3.https://zhuanlan.zhihu.com/p/102571059
4.https://cloud.tencent.com/developer/article/2355282