当前位置:网站首页>ThreadLocal会用可不够
ThreadLocal会用可不够
2022-07-07 08:22:00 【HGW689】
文章目录
小编最近找实习,有被问到ThreadLocal底层源码,知道个大概掰扯了一下,好再成功收到OF(如果对远去哪公司感兴趣的小朋友可以私信小编要面试题哟)~
1、ThreadLocal-简介
官网介绍
这个类提供线程局部变量。 这些变量与其正常的对应方式不同,因为访问一个的每个线程(通过其get
或set
方法)都有自己独立初始化的变量副本。 ThreadLocal
实例通常是希望将状态与线程关联的类中的私有静态字段(例如,用户ID或事务ID)。
通俗点说
通俗点说就是ThreadLocal中填充的变量属于当前线程,该变量对其他线程而言是隔离的,也就是说该变量是当前线程独有的变量。ThreadLocal为变量在每个线程中都创建了一个副本,那么每个线程可以访问自己内部的副本变量。
ThreadLoal 变量,线程局部变量,同一个 ThreadLocal 所包含的对象,在不同的 Thread 中有不同的副本。这里有几点需要注意:
- 因为每个 Thread 内有自己的实例副本,且该副本只能由当前 Thread 使用。这是也是 ThreadLocal 命名的由来。
- 既然每个 Thread 有自己的实例副本,且其它 Thread 不可访问,那就不存在多线程间共享的问题。
ThreadLocal 提供了线程本地的实例。它与普通变量的区别在于,每个使用该变量的线程都会初始化一个完全独立的实例副本。ThreadLocal 变量通常被private static修饰。当一个线程结束时,它所使用的所有 ThreadLocal 相对的实例副本都可被回收。
总的来说
总的来说,ThreadLocal 适用于每个线程需要自己独立的实例且该实例需要在多个方法中被使用,也即变量在线程间隔离而在方法或类间共享的场景。
- 线程并发:在多线程并发的场景下
- 传递数据:通过ThreadLocal在同一个线程,不同组件中传递公共变量
- 线程隔离:每个线程的变量都是独立的,不会互相影响
2、ThreadLocal-基本使用
在使用之前,我们先来认识几个ThreadLocal的常用方法:
方法声明 | 描述 |
---|---|
ThreadLocal() | 创建ThreadLocal对象 |
public void set(T value) | 将当前线程的此线程局部变量的副本设置为指定的值。 |
public T get() | 返回当前线程的此线程局部变量的副本中的值。 |
public void remove() | 删除此线程局部变量的当前线程的值。 |
protected T initialValue() | 返回此线程局部变量的当前线程的“初始值”。 |
public static <S> ThreadLocal<S> withInitial(Supplier<? extends S> supplier) | 创建线程局部变量。 |
class MyData {
ThreadLocal<Integer> threadLocalField = ThreadLocal.withInitial(()->0);
public void add() {
threadLocalField.set(1 + threadLocalField.get());
}
}
public class ThreadLocalDemo01 {
public static void main(String[] args) {
MyData myData = new MyData();
for (int i = 0; i < 5; i++) {
int finalI = i;
new Thread(()->{
myData.add();
System.out.println(Thread.currentThread().getName() + "线程获取域值:" + myData.threadLocalField.get());
},String.valueOf(i)).start();
}
}
}
以上就实现了多线程情况下,每个线程独立域值且线程之间不影响。
3、ThreadLocal 与 synchronized的区别
以上的例子想必大家在思考这样通过synchronized不也可以实现嘛?确实,不过他们两的区别如下:
虽然ThradLocal模式 与 synchronized 关键字都用于处理多线程并发访问变量的问题,不过两者处理问题的角度和思路不同。
synchronized | ThreadLocal | |
---|---|---|
原理 | 同步机制采用 “以时间换空间” 的方式,只提供一份变量,让不同线程排队访问 | ThreadLocal采用“以空间换时间”的方式,为每一个线程都提供了一份变量的副本,从而实现同时访问而互不干扰 |
侧重点 | 多个线程之间访问资源的同步 | 多线程中让每个线程之间的数据互相隔离 |
总之,两者对应的业务场景是不一样的,虽然使用ThreadLocal和synchronized都能解决问题,但是使用ThreadLocal更为合适,因为这样可以使程序拥有更高的并发性。
4、ThreadLocal-阿里巴巴开发手册
根据阿里巴巴开发手册中表述:
必须回收自定义的 ThreadLocal 变量,尤其在线程池场景下,线程经常会被复用,如果不清理自定义的 ThreadLocal 变量,可能会影响后续业务逻辑和造成内存泄露等问题。尽量在代理中使用 try-finally 块进行回收。
class MyData {
ThreadLocal<Integer> threadLocalField = ThreadLocal.withInitial(()->0);
public void add() {
threadLocalField.set(1 + threadLocalField.get());
}
}
当我们在线程池的情况下不进行清理自定义ThreadLocal变量时,其执行结果如何?
我们发现在线程池的情况下线程是复用的,每次用完之后若不清空恢复到原始状态,就会影响后续业务逻辑。
5、ThreadLocal-的内部结构
常见的误解
想必大家许多人会和我最初的想法是一致的~
每个 ThreadLocal 都创建一个 Map
,然后用线程作为 Map
的 key,要存储的局部变量作为 Map
的 value,从而达到各个线程的局部变量隔离的效果。这是最简单的设计方法,JDK最早的 ThreadLocal 确实是这样设计的,但现在早已不是了。
现在的设计
但是,JDK后面优化了设计方案,在JDK8中 ThreadLocal 的设计是:
每个 Thread 维护一个 ThreadLocalMap,这个Map的 key 是 ThreadLocal 实例本身,value 才是真正要存储的值 object。
具体的过程是这样的:
- 每个Thread线程内部都有一个Map(ThreadLocalMap)
- Map里面存储ThreadLocal对象(key) 和 线程的变量副本(value)
- Thread内部的Map是由ThreadLocal维护的,由ThreadLocal负责向map获取和设置线程的变量值
- 对于不同的线程,每次获取副本值是,别的线程并不能获取到当前线程的副本值,形成了副本的隔离,互不干扰。
这样设计方法的好处是:
- 每个Map存储的Entry数量减少
- 当Thread销毁的时候,ThreadLocalMap也会随之销毁,减少内存的使用
6、ThreadLocal -之底层源码分析(重头戏)
6.1、ThreadLocal基本结构
首先我们通过 Thread、ThreadGroup、ThreadLocal 的关系来入手底层~
首先是 Thread类中维护了一个ThreadLocalMap,ThreadLocal类中包含了一个ThreadLocalMap静态内部类。
通过以上分析,我们了解到ThreadLocal的操作时机上是围绕着ThreadLocalMap展开的,ThreadLocalMap的源码相对比较复杂,我们从以下三个方面进行讨论~
6.1.1、ThreadLocalMap 基本结构
ThreadLocalMap 是 ThreadLocal 的静态内部类,没有实现Map接口,用独立的方式实现了Map的功能,其内部的Entry也是独立实现。类图大致如下:
1、 成员变量
/** * 初始容量 -- 必须是2的整次幂。 */
private static final int INITIAL_CAPACITY = 16;
/** * 存放数据的table,Entry类的定义在下面分析 * 同样,数组长度必须是2的整次幂 */
private Entry[] table;
/** * 数组里面entry的个数,可以用于判断table当前使用量是否超过阈值 */
private int size = 0;
/** * 进行扩容的阈值,表使用量大于它的时候进行扩容 */
private int threshold; // Default to 0
跟 HashMap 类似,INITIAL_CAPACITY代表这个Map的初始容量;table 是一个Entry类型的数组,用于存储数据;size 代表表中的存储数目;threshold代表需要扩容时对应 size 的阈值。
2、存储结构-Entry
/** * Entry 继承 WeakReference(弱引用),并且用ThreadLocal作为key。 * 如果key为null(entry.get() == null),意味着key不再被引用,因此这时候entry也可以从table中清除 */
static class Entry extends WeakReference<ThreadLocal<?>> {
/** The value associated with this ThreadLocal. */
Object value;
Entry(ThreadLocal<?> k, Object v) {
super(k);
value = v;
}
}
在ThreadLocalMap中,也是用Entry来保存k-v存储结构的,不过Entry中key只能是ThreadLocal对象,这点在构造方法中已经限定死了。
另外,Entry继承WeakReference,也就是key(ThreadLocal)是弱引用,其目的是将ThreadLocal对象的生命周期和线程生命周期解绑。
6.1.2、弱引用和内存泄漏
我们在使用ThreadLocal的过程中会发现有内存泄漏的情况发生,会有同学猜测这个内存泄漏跟Entry中使用了弱引用的key有关系。这里理解其实是不对的。
首先我们需要知道内存泄漏指的是什么呢?
- Memory overflow:内存溢出,没有足够的内存提供申请者使用。
- Memory leak:内存泄漏是指程序中已动态分配的对内存由于某种原因程序未释放或无法释放,造出系统内存的浪费,导致程序运行速度减慢甚至系统崩溃等严重后果。内存泄漏的堆积终将导致内存溢出。
Java中的引用有四种类型:强、软、弱、虚。
- 强引用(Strong Reference):无论任何情况下,只要强引用关系存在,垃圾收集器就不会回收这个对象。最传统的“引用定义”,是指在程序代码之中普遍存在的引用赋值,即类似 Object obj = new Object() 这种引用关系。
- 软引用(Soft Reference):如果内存不足,进行回收。在系统将要发生内存溢出之前,将会把这些对象列入回收范围之中进行第二次回收。如果这次回收后还没有足够的内存,才会抛出内存溢出异常。
- 弱引用(Weak Reference):被弱引用关联的对象只能生存到下一次垃圾收集之前。当垃圾收集器工作时,无论内存空间是否足够,都会回收掉被弱引用关联的对象。
- 虚引用(Phantom Reference):在任何时候都可能被垃圾回收器回收。一个对象是否有虚引用的存在,完全不会对其生存时间构成影响,也无法通过虚引用来获得一个对象的实例。为一个对象设置虚引用关联的唯一目的就是能在这个对象被收集器会收时收到一个系统通知。
假设ThreadLocalMap中的key使用了强引用,那么会出现内存泄漏吗?
- 假设在业务代码中使用完 ThreadLocal,threadLocal Ref 被回收了。
- 但是因为 threadLocalMap 的Entry 强引用了 threadLocal,造成 threadLocal无法被回收。
- 在没有手动删除这个Entry以及CurrentThread依然运行的前提下,始终有强引用链 threadRef->currentThread->threadLocalMap->entry,Entry就不会被回收(Entry中包括了ThreadLocal实例和value),导致Entry内存泄漏。
也就是说,如果ThreadLocalMap中的key使用了强引用,是无法完全避免内存泄漏的。
那么ThreadLocalMap中的key使用了弱引用,会出现内存泄漏吗?
- 同样假设在业务代码中使用完 ThreadLocal,threadLocal Ref 被回收了。
- 由于 ThreadLocalMap 只持有 ThreadLocal 的弱引用,没有任何强引用指向 threadlocal实例,所以threadlocal就可以顺利被gc回收,此时Entry中的key=null。
- 但是在没有手动删除这个Entry以及CurrentThread依然运行的前提下,也存有强引用链 threadRef->currentThread->threadLocalMap->entry->value,value就不会被回收,而这块value永远不会被访问到了,导致value内存泄漏。
也就是说,如果ThreadLocalMap中的key使用了弱引用,也有可能内存泄漏。
那出现内存泄漏的真实原因呢?
通过以上分析我们就会发现,内存泄漏的发生跟ThreadLocalMap中的key是否使用弱引用是没有关系的。那么内存泄漏的真正原因是什么呢?
细心的同学会发现,在以上两种内存泄漏的情况中,都有两个前提:
- 没有手动删除这个Entry
- CurrentThread依然执行
第一点很好理解,只要在使用完ThreadLocal,调用其remove方法删除对应的Entry,就能避免内存泄漏。
第二点稍微复杂一点,由于ThreadLocalMap是Thread的一个属性,被当前线程所引用,所以它的生命周期跟Thread一样长。那么在使用完ThreadLocal的使用,如果当前Thread也随之执行结束,ThreadLocalMap自然也会被gc回收,从根源上避免了内存泄漏。
综上,ThreadLocal内存泄漏的根源是:由于ThreadLocalMap的生命周期跟Thread一样长,如果没有手动删除对应key就会导致内存泄漏。
为什么使用弱引用
无论ThreadLocalMap中的key使用那种类型引用都无法完全避免内存泄漏,跟使用弱引用没有关系。
也就是说,只要记得在使用完ThreadLocal及时的调用remove,无论key是强运用还是弱引用都不会有问题。那么为什么key要用弱引用呢?
我们知道避免内存泄漏有两种方式:
使用完ThreaadLocal,调用其remove方法删除对应的Entry
使用完ThreadLocal,当前Thread也随之运行结束
相对第一种方式,第二种方式显然更不好控制,特别是使用线程池的时候,线程是复用的。
也就是说,只要记得在使用完ThreadLocal及时的调用remove,无论key是强引用还是弱引用都不会有问题。那么为什么key要用弱引用呢?
事实上,在ThreadLocalMap中的set/getEntry方法中,会对key为null (即ThreadLocal为null) 进行判断,如果为null的话,那么是会对value置为null的。
这就意味着使用完ThreadLocal,CurrentThread依然运行的前提下,就算忘记调用remove方法,弱引用比强引用可以多一层屏障:弱引用的ThreadLocal会被回收,对应的value在下一次ThreadLocalMap调用set,get,remove中的任意方法的时候会被清除,从而避免内存泄漏。
6.2、核心方法源码分析
接下来看看 initialValue
、set
、get
、remove
四个方法的源码~
6.2.1、set方法源码探究
/** * 设置此线程局部变量的当前线程副本 * @param value 要存储在该线程本地的当前线程副本中的值。 */
public void set(T value) {
// 获取当前线程对象
Thread t = Thread.currentThread();
// 获取次线程对象中维护的 ThreadLocalMap对象
ThreadLocalMap map = getMap(t);
// 判断map是否存在
if (map != null) {
// 存在则调用map.set设置此实体entry
map.set(this, value);
} else {
// 当先线程Thread 不存在ThreadLocalMap对象,则调用createMap进行不存在ThreadLocalMap对象的初始化,并将 t(当前线程)和value(t对应的值)作为第一个entry存放至ThreadLocalMap中
createMap(t, value);
}
}
/** * 获取与ThreadLocal * @param 当前线程 * @return 当前线程对应维护的ThreadLocalMap */
ThreadLocalMap getMap(Thread t) {
return t.threadLocals;
}
/** * 创建当前线程Thread对应维护的ThreadLocalMap * @param 当前线程 * @param 存放到map中第一个entry的值 */
void createMap(Thread t, T firstValue) {
t.threadLocals = new ThreadLocalMap(this, firstValue);
}
代码执行流程如下:
- 首先获取当前线程,并根据当前线程获取一个Map
- 如果获取的Map不为空,则将参数设置到Map中(当前ThreadLocal的引用作为key)
- 如果map为空,则给线程创建 Map,并设置初始值
接下来我们来探究一下元素的存放过程:当调用set()方法时如果map为空,则会调用 createMap(Thread t, T firstValue)
方法给线程创建 ThreadLocalMap ,并设制初始值。该方法调用了 ThreadLocalMap 的构造方法~
构造方法
ThreadLocalMap(ThreadLocal<?> firstKey, Object firstValue)
/** * firstKey:本ThreadLocal实例(this) * firstValue:要保存的线程本地变量 */
ThreadLocalMap(ThreadLocal<?> firstKey, Object firstValue) {
// 初始化table,创建长度为16的 Entry数组
table = new Entry[INITIAL_CAPACITY];
// 计算索引(即在Entry数组中的存放位置)
int i = firstKey.threadLocalHashCode & (INITIAL_CAPACITY - 1);
// 在指定索引的位置创建一个Entry存放键值对
table[i] = new Entry(firstKey, firstValue);
// 记录Map中键值对的个数,目前存放第一个entry,故为1
size = 1;
// 设置阈值
setThreshold(INITIAL_CAPACITY);
}
构造函数首先创建一个长度为16的Entry数组,然后计算出firstKey对应的索引,然后将key,value封装成Entry存储在table数组中,并设置size和threshold。
重点分析:int i = firstKey.threadLocalHashCode & (INITIAL_CAPACITY - 1);
该语句解决了Hash冲突问题。
1、关于firstKey.threadLocalHashCode
private final int threadLocalHashCode = nextHashCode();
private static int nextHashCode() {
return nextHashCode.getAndAdd(HASH_INCREMENT);
}
// AtomicInteger 是一个提供原子操作的Intger类,通过线程安全的方式操作加减,适合高并发情况下的使用
private static AtomicInteger nextHashCode = new AtomicInteger();
// 特殊的hash值
private static final int HASH_INCREMENT = 0x61c88647;
这里定义了一个 AtomicInteger 类型(该方法有关介绍于本人CAS机制 博客),每次获取当前值并加上 HASH_INCREMENT,HASH_INCREMENT = 0x61c88647
,这个值跟斐波那契数列(黄金分割数)有关,其主要目的就是为了让哈希码能均匀的分布在2的n次方的数组里,也就是Entry[] table中,这样做可以尽量避免hash冲突。
2、关于 & (INITIAL_CAPACITY - 1)
计算hash的时候里面采用了 hashCode & (size-1)
的算法,这相当于取模运算 hashCode%size 的一个更高效的实现。正是因为这种算法,我们要求size必须是2的整次幂,这也能保证在索引不越界的前提下,使得hash发生冲突的次数减小。
ThreadLocalMap中的set方法
ok分析完map的构造方法,我们再来探究一下map的set方法。
private void set(ThreadLocal<?> key, Object value) {
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)]) {
ThreadLocal<?> k = e.get();
// ThreadLocal 对应的 key 存在,直接覆盖之前的值
if (k == key) {
e.value = value;
return;
}
// key为null,但是值不为null,说明之前 ThreadLocal 对象已经被回收了,当前数组下标的Entry是一个陈旧(stale)的元素
if (k == null) {
// 用新元素替换陈旧的元素,这个方法进行了不少的垃圾清理动作,防止内存泄漏
replaceStaleEntry(key, value, i);
return;
}
}
// ThreadLocal对应的key不存在并且没有找到陈旧的元素,则在空元素的位置创建一个新的Entry
tab[i] = new Entry(key, value);
int sz = ++size;
/** * cleanSomeSlots 用于清除那些 e.get()==null的元素 * 这种数据key关联的对象已经被回收,所以这个Entry(table[index])可以被置null。 * 如果没有清除任何entry,并且当前使用量达到了负载因子所定义(长度的2/3),那么进行rehash(执行一次全表的扫描清除工作) */
if (!cleanSomeSlots(i, sz) && sz >= threshold)
rehash();
}
- 首先根据key计算出索引i,然后查找i位置上的Entry。
- 若是Entry已经存在并且key等于传入的key,直接覆盖原有value值。
- 若是Entry存在,但是key为null,则调用 replaceStaleEntry 来更换这个key为空的Entry。
- 不断循环检测,直到遇到为null的地方,这时候要是还没在循环过程中return,那么就在这个null的位置新建一个Entry,并且插入,同时size增加1。
最后调用 cleanSomeSlots ,清理key为null的Entry,最后返回是否清理了Entry,接下来再判断sz是否>=thresg达到了rehash的条件,达到的话就会调用rehash函数执行一次全表的扫描清理。
再来谈谈 ThreadLocalMap 使用 线性探测法 来解决哈希冲突的。
该方法一次探测下一个地址,直到有空的地址后插入,若整个空间都找不到空余的地址,则产生溢出。
比如说:假设当前table长度为16,也就是说如果计算出来的key的索引为14,如果table[14]上已经有值并且其key与当前key 不一致,那么就发生了hash冲突,这个时候将14+1得到15,取table[15]进行判断,这个时候如果还是冲突会回到0,取table[0],以此类推,直到可以插入。可以把Entry[] table看成一个环形数组。
6.2.2、get方法源码探究
/** * 返回此线程局部变量的当前线程副本中的值 * @return 返回当前线程对应此ThreadLocal的值 */
public T get() {
// 获取当前线程对象
Thread t = Thread.currentThread();
// 获取此线程对象中维护的ThreadLocalMap对象
ThreadLocalMap map = getMap(t);
// 如果此map存在
if (map != null) {
// 以当前的ThreadLocal 为key,调用getEntry获取对应的存储实体e
ThreadLocalMap.Entry e = map.getEntry(this);
// 对e进行判空
if (e != null) {
@SuppressWarnings("unchecked")
// 获取存储实体 e 对应的 value值,并返回
T result = (T)e.value;
return result;
}
}
//1.map不存在,表示此线程没有维护的ThreadLocalMap对象,
//2.map存在,但是没有与当前ThreadLocal关联的entry
return setInitialValue();
}
/** * 初始化 * @return 初始化后的值 */
private T setInitialValue() {
// 调用initialValue获取初始化的值,此方法可以被子类重写,如果不重写默认返回null
T value = initialValue();
// 获取当前线程对象
Thread t = Thread.currentThread();
// 获取此线程对象中维护的ThreadLocalMap对象
ThreadLocalMap map = getMap(t);
if (map != null) {
// 存在则调用map.set设置此实体entry
map.set(this, value);
} else {
// 当先线程Thread 不存在ThreadLocalMap对象,则调用createMap进行不存在ThreadLocalMap对象的初始化,并将 t(当前线程)和value(t对应的值)作为第一个entry存放至ThreadLocalMap中
createMap(t, value);
}
if (this instanceof TerminatingThreadLocal) {
TerminatingThreadLocal.register((TerminatingThreadLocal<?>) this);
}
//返回设置的值value
return value;
}
代码执行流程如下:
- 首先获取当前线程,根据当前线程获取一个Map
- 如果获取的Map不为空,则在Map中以ThreadLocal的引用作为key来在Map中获取对应Entry e,否则转到 4
- 如果e不为null,则返回e .value,否则转到D
- map为空或e为空,则通过initialValue()函数获取初始值value,然后用ThreadLocal的引用和value作为firstKey和firstValue创建一个新的Map。
总结:先获取当前线程的 ThreadLocalMap 变量,如果存在则返回值,不存在则创建并返回初始者。
6.2.3、remove方法源码探究
/** * 删除当前线程中保存的ThreadLocal对应的实体entry */
public void remove() {
// 获取当前线程对象中维护的ThreadLocalMap对象
ThreadLocalMap m = getMap(Thread.currentThread());
// 如果此map存在
if (m != null) {
// 存在则调用map.remove,删除以当前threadLocal为key对应的实体entry
m.remove(this);
}
}
代码执行流程如下:
- 首先获取当前线程,并根据当前线程获取一个Map
- 如果获取的Map不为空,则移除当前ThreadLocal对象对应的entry
6.2.4、initialValue方法源码探究
/** * 返回当前线程对应的ThreadLocal的初始值。 */
protected T initialValue() {
return null;
}
此方法的第一次调用发生在:当线程通过get方法访问此线程的ThreadLocal值时。除非线程先调用了set方法,在这种情况下,initialValue 才不会被这个线程调用。
通常情况下,每个线程最多调用一次这个方法。
这个方法仅仅简单的返回null,如果程序员想ThreadLocal线程局部变量有一个除null以外的初始值,必须通过子类继承 ThreadLocal 的方式去重写此方法,通常,可以通过匿名内部类的方式实现。如本篇博客中的demo~
此方法的作用是 返回该线程局部变量的初始值。
- 这个方法是一个延迟调用方法,从上面的代码我们得知,在set方法还未调用了get方法时才执行,并且仅执行一次。
- 这个方法缺省实现直接返回一个null。
- 如果想要一个除null之外的初始值,可以重写此方法。(我们可以看到该方法被 protected修饰,显然是为了让子类覆盖而设计的)
7、ThreadLocal总结
说了这么多 ThreadLocal,给大家小总结一下吧~
- ThreadLocal 并不解决线程间共享数据的问题
- ThreadLocal 适用于变量在线程间隔离且在方法间共享的场景
- ThreadLocal 通过隐式的在不同线程内创建独立实例副本避免了实例线程安全的问题
- 每个线程持有一个只属于自己的专属Map并维护了ThreadLocal对象与具体实例的映射,该Map由于只被持有它的线程访问,故不存在线程安全以及锁的问题。
- ThreadLocalMap 的 Entry 对 ThreadLocal 的引用为弱引用,避免了ThreadLocal对象无法被回收的问题
- 都会通过expungeStaleEntry,cleanSomeSlots,replaceStaleEntry 这三个方法回收键为 null 的 Entry对象的值(即为具体实例) 以及Entry对象本身从而防止内存泄漏,属于安全加固的方法。
边栏推荐
- String formatting
- 施努卡:机器视觉定位技术 机器视觉定位原理
- STM32 ADC和DMA
- The variables or functions declared in the header file cannot be recognized after importing other people's projects and adding the header file
- Smart city construction based on GIS 3D visualization technology
- 搭建物联网硬件通信技术几种方案
- 宁愿把简单的问题说一百遍,也不把复杂的问题做一遍
- Study summary of postgraduate entrance examination in July
- XML configuration file parsing and modeling
- Inno setup packaging and signing Guide
猜你喜欢
基于HPC场景的集群任务调度系统LSF/SGE/Slurm/PBS
Trajectory planning for multi-robot systems: Methods and applications 综述阅读笔记
原型与原型链
The Hal library is configured with a general timer Tim to trigger ADC sampling, and then DMA is moved to the memory space.
openinstall与虎扑达成合作,挖掘体育文化产业数据价值
【STM32】STM32烧录程序后SWD无法识别器件的问题解决方法
1321:【例6.3】删数问题(Noip1994)
电表远程抄表拉合闸操作命令指令
Programming features of ISP, IAP, ICP, JTAG and SWD
Socket通信原理和实践
随机推荐
Postman interface test VII
嵌入式工程师如何提高工作效率
MCU与MPU的区别
【HigherHRNet】 HigherHRNet 详解之 HigherHRNet的热图回归代码
HDU-2196 树形DP学习笔记
宁愿把简单的问题说一百遍,也不把复杂的问题做一遍
ORM -- database addition, deletion, modification and query operation logic
Postman interface test III
反射效率为什么低?
[higherhrnet] higherhrnet detailed heat map regression code of higherhrnet
【剑指Offer】42. 栈的压入、弹出序列
Download Text, pictures and ab packages used by unitywebrequest Foundation
When there are pointer variable members in the custom type, the return value and parameters of the assignment operator overload must be reference types
一文讲解单片机、ARM、MUC、DSP、FPGA、嵌入式错综复杂的关系
Study summary of postgraduate entrance examination in September
Kotlin实现微信界面切换(Fragment练习)
IIC基本知识
The story of Plato and his three disciples: how to find happiness? How to find the ideal partner?
Some properties of leetcode139 Yang Hui triangle
移动端通过设置rem使页面内容及字体大小自动调整