Java-并发编程--ThreadLocal、InheritableThreadLocal

1.ThreadLocal 作用

作用:为变量在线程中都创建副本,线程可访问自己内部的副本变量。该类提供了线程局部 (thread-local) 变量,访问这个变量(通过其 get 或 set 方法)的每个线程都有自己的局部变量,它独立于变量的初始化副本

原理:每个线程都有一个ThreadLocalMap类型变量 threadLocals。ThreadLocal的set()会在threadLocals中保存以ThreadLocal对象为key,以保存的变量为value的值,get()会获取该值

建议:

  • 将ThreadLocal变量定义成private static的,这样的话ThreadLocal的生命周期就更长,由于一直存在ThreadLocal的强引用,所以ThreadLocal也就不会被回收,也就能保证任何时候都能根据ThreadLocal的弱引用访问到Entry的value值,然后remove它,防止内存泄露
  • 每次使用完ThreadLocal,都调用它的remove()方法,清除数据。

2.ThreadLocal继承关系

3.源码走读

3.1.ThreadLocal.java
public class ThreadLocal<T> {//**每一个实例都有一个唯一的threadLocalHashCode,值为上一个实例的值加上0x61c88647//**作用是为了让哈希码能均匀的分布在2的N次方的数组里private final int threadLocalHashCode = nextHashCode();private static AtomicInteger nextHashCode = new AtomicInteger();private static final int HASH_INCREMENT = 0x61c88647;private static int nextHashCode() {return nextHashCode.getAndAdd(HASH_INCREMENT);}//**返回此线程局部变量的当前线程的“初始值”//**线程第一次使用get()方法时调用此方法,如果线程之前调用了set(T)方法,则不会对该线程再调用该方法//**通常,此方法对每个线程最多调用一次,但调用了remove(),则会再次调用此方法//**默认返回null,如果希望返回其它值,则须创建子类,并重写此方法,通常将使用匿名内部类完成此操作 protected T initialValue() {return null;}//**在java8,使用函数式编程的方式设置并返回当前线程变量的初始值,与上个方法功能相同//**示例:ThreadLocal<String> threadLocal = ThreadLocal.withInitial(() -> "test");public static <S> ThreadLocal<S> withInitial(Supplier<? extends S> supplier) {//**返回SuppliedThreadLocal对象,SuppliedThreadLocal是ThreadLocal的子类,//**重写的initialValue方法调用supplier的get方法做为当前线程变量的初始值return new SuppliedThreadLocal<>(supplier);}//**返回此线程局部变量的当前线程副本中的值,如果变量没有用于当前线程的值,则返回initialValue()的值public T get() {//**获取当前线程的实例Thread t = Thread.currentThread();//**获取当前线程中的ThreadLocalMap类型变量threadLocalsThreadLocalMap map = getMap(t);if (map != null) {//**从threadLocals中获取以this为key的Entry对象ThreadLocalMap.Entry e = map.getEntry(this);//**如果Entry对象不为空,则返回它的valueif (e != null) {@SuppressWarnings("unchecked")T result = (T)e.value;return result;}}//**如果threadLocals对象不为空或者Entry为空,则调用setInitialValue进行初始化return setInitialValue();}//**使用initialValue()的值初始化线程局部变量private T setInitialValue() {//**获取线程局部变量的初始值,默认为nullT value = initialValue();//**获取当前线程的实例Thread t = Thread.currentThread();//**获取当前线程中的ThreadLocalMap类型变量threadLocalsThreadLocalMap map = getMap(t);//**如果threadLocals不为空,设置以this为key,以value为值的Entry对象if (map != null)map.set(this, value);//**如果threadLocals为空,则进行初始化,并设置以this为key,以value为值的Entry对象elsecreateMap(t, value);return value;}//**设置线程局部变量的值public void set(T value) {Thread t = Thread.currentThread();ThreadLocalMap map = getMap(t);if (map != null)map.set(this, value);elsecreateMap(t, value);}//**移除此线程局部变量当前线程的值,如果随后调用get()方法,且没有调用set()设置值,则将调用initialValue()重新初始化值public void remove() {ThreadLocalMap m = getMap(Thread.currentThread());if (m != null)m.remove(this);}//**从线程实例中获取threadLocals对象ThreadLocalMap getMap(Thread t) {return t.threadLocals;}//**初始化线程t的threadLocals对象,并设置以this为key,以firstValue为值的Entry对象void createMap(Thread t, T firstValue) {t.threadLocals = new ThreadLocalMap(this, firstValue);}//**根据主线程中的ThreadLocalMap对象创建子线程的ThreadLocalMap对象static ThreadLocalMap createInheritedMap(ThreadLocalMap parentMap) {return new ThreadLocalMap(parentMap);}//**ThreadLocal对象不支持,在InheritableThreadLocal中实现T childValue(T parentValue) {throw new UnsupportedOperationException();}}
3.2.SuppliedThreadLocal.java
  • ithInitial方法使用Supplier对象创建SuppliedThreadLocal对象
  • 作用是为了在java8,支持使用函数式编程的方式设置并返回当前线程变量的初始值
static final class SuppliedThreadLocal<T> extends ThreadLocal<T> {private final Supplier<? extends T> supplier;SuppliedThreadLocal(Supplier<? extends T> supplier) {this.supplier = Objects.requireNonNull(supplier);}@Override//**重写的initialValue方法,调用supplier的get方法做为当前线程变量的初始值protected T initialValue() {return supplier.get();}
}
3.4.ThreadLocalMap.java
作用:

ThreadLocalMap是ThreadLocal的内部类。存放以ThreadLocal变量为key,以保存的变量为value的键值对

原理:
  • ThreadLocalMap内部以Entry[]做为存储,原始长度默认为16,当元素个数达到扩容阀值(数组长度的3/4)-扩容阀值/4,则自动扩容,扩容到上次长度的2倍。Entry[]的长度必须是2的倍数
  • Entry[]存储元素并不是按索引顺序存储,而是根据ThreadLocal进行计算存储位置,这样能实现根据ThreadLocal都能快速定位键值对,而不用遍历数组的每个元素
  • 计算方法:ThreadLocal.threadLocalHashCode & (Entry[].length - 1)计算,ThreadLocal每一个实例都有一个唯一的threadLocalHashCode,值为上一个实例的值加上0x61c88647,该算法可以生成均匀的分布在2的N次方数组里的下标
  • 如果计算的存储位置已经有元素,则会存放到下一个索引的位置,ThreadLocalMap会清理过期数据,并重新根据计算的存储位置重置,以保证尽可能减少和纠正此类问题
static class ThreadLocalMap {//**存放单个键值对的对象//**弱引用: 如果某个对象只有弱引用,那么gc会立即回收static class Entry extends WeakReference<ThreadLocal<?>> {Object value;Entry(ThreadLocal<?> k, Object v) {super(k);value = v;}}//**默认初始化的大小,必须是2的倍数private static final int INITIAL_CAPACITY = 16;//**真正存储数据的数组,长度必须是2的倍数private Entry[] table;//**ThreadLocalMap的大小,即上述Entry[]中存放元素的个数private int size = 0;//**自动扩容的阀值private int threshold; // Default to 0//**设置自动扩容的阀值,为设定长度的2/3private void setThreshold(int len) {threshold = len * 2 / 3;}//**下一个索引private static int nextIndex(int i, int len) {return ((i + 1 < len) ? i + 1 : 0);}//**上一个索引private static int prevIndex(int i, int len) {return ((i - 1 >= 0) ? i - 1 : len - 1);}//**创建ThreadLocalMap,并设置第一个键值对ThreadLocalMap(ThreadLocal<?> firstKey, Object firstValue) {//**根据默认初始化的大小初始化Entry[]table = new Entry[INITIAL_CAPACITY];//**根据threadlocal对象的threadLocalHashCode和Entry[]数组的长度计算存放的位置//**该算法可以生成均匀的分布在2的N次方数组里的下标//**每个键值对并不是按顺序存放Entry[]里面int i = firstKey.threadLocalHashCode & (INITIAL_CAPACITY - 1);//**把Entry对象放到指定位置table[i] = new Entry(firstKey, firstValue);//**设置ThreadLocalMap的大小,即Entry[]中存放元素的个数size = 1;//**设置自动扩容的阀值setThreshold(INITIAL_CAPACITY);}//**根据parentMap创建另一个parentMap,使用InheritableThreadLocal时,创建子线程时会调用private ThreadLocalMap(ThreadLocalMap parentMap) {Entry[] parentTable = parentMap.table;int len = parentTable.length;setThreshold(len);table = new Entry[len];for (int j = 0; j < len; j++) {Entry e = parentTable[j];if (e != null) {@SuppressWarnings("unchecked")ThreadLocal<Object> key = (ThreadLocal<Object>) e.get();if (key != null) {//**调用InheritableThreadLocal的childValue方法处理保存的对象Object value = key.childValue(e.value);Entry c = new Entry(key, value);int h = key.threadLocalHashCode & (len - 1);while (table[h] != null)h = nextIndex(h, len);table[h] = c;size++;}}}}//**根据threadlocal获取Entry对象private Entry getEntry(ThreadLocal<?> key) {//**计算下标int i = key.threadLocalHashCode & (table.length - 1);Entry e = table[i];//**如果对象存在,且key一样,则返回if (e != null && e.get() == key)return e;else  //**否则从指定索引的下一个索引开始查找return getEntryAfterMiss(key, i, e);}//**没有直接命中,则指定索引的下一个索引开始查找private Entry getEntryAfterMiss(ThreadLocal<?> key, int i, Entry e) {Entry[] tab = table;int len = tab.length;//**从指定索引开始遍历,直到数据为nullwhile (e != null) {ThreadLocal<?> k = e.get();//**如果数据存在则返回if (k == key)return e;//**threadlocal对象为空,删除过期数据if (k == null)//**删除过期数据expungeStaleEntry(i);//**i为下一个索引elsei = nextIndex(i, len);//**e为下一个索引的值e = tab[i];}//**没有数据不存在则返回nullreturn null;}//**根据threadlocal对象设置valueprivate void set(ThreadLocal<?> key, Object value) {Entry[] tab = table;int len = tab.length;//**计算存放的索引int i = key.threadLocalHashCode & (len-1);//**从指定索引开始遍历Entry[],直到数据为nullfor (Entry e = tab[i]; e != null; e = tab[i = nextIndex(i, len)]) {ThreadLocal<?> k = e.get();//**如果数据存在,则直接返回if (k == key) {e.value = value;return;}//**如果key为空,则替换当前索引的数据,并返回if (k == null) {replaceStaleEntry(key, value, i);return;}}//**设置指定索引的数据tab[i] = new Entry(key, value);int sz = ++size;//**如果没有数据需要清理并且数组长度大于了扩容阀值,则扩容if (!cleanSomeSlots(i, sz) && sz >= threshold)rehash();}//**根据key删除数据private void remove(ThreadLocal<?> key) {Entry[] tab = table;int len = tab.length;//**计算存放的索引int i = key.threadLocalHashCode & (len-1);//**从指定的索引开始遍历Entry[],直到数据为nullfor (Entry e = tab[i]; e != null; e = tab[i = nextIndex(i, len)]) {//**如果指定key存在,则删除指定数据if (e.get() == key) {e.clear();expungeStaleEntry(i);return;}}}//**替换指定索引的过期数据的private void replaceStaleEntry(ThreadLocal<?> key, Object value, int staleSlot) {Entry[] tab = table;int len = tab.length;Entry e;//**从指定索引往前找,找到过期数据的索引int slotToExpunge = staleSlot;for (int i = prevIndex(staleSlot, len); (e = tab[i]) != null; i = prevIndex(i, len))if (e.get() == null)slotToExpunge = i;//**从指定索引往后找for (int i = nextIndex(staleSlot, len); (e = tab[i]) != null; i = nextIndex(i, len)) {ThreadLocal<?> k = e.get();//**如果是数据的key等于指定的keyif (k == key) {//**替换它的valuee.value = value;//**把它的位置和指定索引的位置互换(把数据替换到计算索引的位置)tab[i] = tab[staleSlot];tab[staleSlot] = e;//**如果过期数据的的索引等于指定索引,则过期数据的索引为互换后的新索引if (slotToExpunge == staleSlot)slotToExpunge = i;cleanSomeSlots(expungeStaleEntry(slotToExpunge), len);return;}//**过期数据的索引if (k == null && slotToExpunge == staleSlot)slotToExpunge = i;}//**如果指定数据不存在,则创建新的数据tab[staleSlot].value = null;tab[staleSlot] = new Entry(key, value);//**如果有过时的条目,则清理if (slotToExpunge != staleSlot)cleanSomeSlots(expungeStaleEntry(slotToExpunge), len);}//**删除指定索引的过期数据,并返回数据为null的索引private int expungeStaleEntry(int staleSlot) {Entry[] tab = table;int len = tab.length;//**指定索引的数据置为null,数据减一(删除指定数据)tab[staleSlot].value = null;tab[staleSlot] = null;size--;Entry e;int i;//**从指定的索引的下一个数据开始循环遍历Entry[]数组,直到遇到null值for (i = nextIndex(staleSlot, len); (e = tab[i]) != null; i = nextIndex(i, len)) {ThreadLocal<?> k = e.get();//**如果key为空,Entry置为空,数据减一(删除指定数据)if (k == null) {e.value = null;tab[i] = null;size--;} else {//**重新计算存放的索引int h = k.threadLocalHashCode & (len - 1);//**如果新索引不等于原索引,则原索引数据置为nullif (h != i) {tab[i] = null;//**如果新的存放的索引有数据,则存放到新索引的下一个索引,直到没有数据为止while (tab[h] != null)h = nextIndex(h, len);tab[h] = e;}}}//**返回数据为null的索引return i;}//**从指定索引开始清理数据private boolean cleanSomeSlots(int i, int n) {boolean removed = false;Entry[] tab = table;int len = tab.length;do {i = nextIndex(i, len);Entry e = tab[i];if (e != null && e.get() == null) {n = len;removed = true;i = expungeStaleEntry(i);}} while ( (n >>>= 1) != 0);return removed;}//**删除过期数据并扩容private void rehash() {//**删除所有的过期数据expungeStaleEntries();//**数据量 >= 扩容阀值 - 扩容阀值 / 4,则扩容if (size >= threshold - threshold / 4)resize();}//**扩容private void resize() {Entry[] oldTab = table;int oldLen = oldTab.length;//**扩容为原来的2倍int newLen = oldLen * 2;Entry[] newTab = new Entry[newLen];int count = 0;//**把旧数据存放在新的Entry[]中for (int j = 0; j < oldLen; ++j) {Entry e = oldTab[j];if (e != null) {ThreadLocal<?> k = e.get();if (k == null) {e.value = null; // Help the GC} else {int h = k.threadLocalHashCode & (newLen - 1);while (newTab[h] != null)h = nextIndex(h, newLen);newTab[h] = e;count++;}}}//**计算新的扩容阀值setThreshold(newLen);size = count;table = newTab;}//**删除所有的过期数据private void expungeStaleEntries() {Entry[] tab = table;int len = tab.length;for (int j = 0; j < len; j++) {Entry e = tab[j];//**如果Entry不为空并且key为空(threadlocal对象为null)则为过期数据if (e != null && e.get() == null)expungeStaleEntry(j);}}}
3.5 Thread 、ThreadLocal、ThreadLocalMap关系图

Thread1 和Thread2 线程中的ThreadLocal 是相同的话,那么ThreadLocalMap 中Entry 下标位置都是 ThreadLocal 的hashcode & (len-1)的位置如不出现hash冲突的话则都是相同的。

3.6 InheritThreadLocal详解

原理和解析:

  • 每个线程都还有另外一个ThreadLocalMap类型变量inheritableThreadLocals
  • InheritableThreadLocal重写了getMap和createMap方法,维护的不在是threadLocals,而是inheritableThreadLocals
  • 当主线程创建一个子线程的时候,会判断主线程的inheritableThreadLocals是否为空
  • 如果不为空,则会把inheritableThreadLocals的值传给子线程的inheritableThreadLocals,传送的逻辑是childValue实现的
private void init(ThreadGroup g, Runnable target, String name,long stackSize, AccessControlContext acc) {...//**获取主线程的实例Thread parent = currentThread();...//**如果主线的inheritableThreadLocals不为空if (parent.inheritableThreadLocals != null)//**根据主线程的inheritableThreadLocals创建子线程的inheritableThreadLocalsthis.inheritableThreadLocals = ThreadLocal.createInheritedMap(parent.inheritableThreadLocals);...
}

注意:

  • 因为传送逻辑是在创建子线程的时候完成的,子线程创建后,主线程在修改InheritableThreadLocal变量的值,是无法传给子线程的
  • 创建子线程完成后,原则上子线程和父线程中InheritableThreadLocal变量的值在没有关联,各自调用set/get/remove都只影响本线程中的值
  • 如果InheritableThreadLocal变量的值是引用类型,通过get方法获取到对象后,直接修改了该对象的属性,则父线程和子线程都会受影响

InheritableThreadLocal类重写了ThreadLocal的3个函数:

	/*** 该函数在父线程创建子线程,向子线程复制InheritableThreadLocal变量时使用*/protected T childValue(T parentValue) {return parentValue;}/*** 由于重写了getMap,操作InheritableThreadLocal时,* 将只影响Thread类中的inheritableThreadLocals变量,* 与threadLocals变量不再有关系*/ThreadLocalMap getMap(Thread t) {return t.inheritableThreadLocals;}/*** 类似于getMap,操作InheritableThreadLocal时,* 将只影响Thread类中的inheritableThreadLocals变量,* 与threadLocals变量不再有关系*/void createMap(Thread t, T firstValue) {t.inheritableThreadLocals = new ThreadLocalMap(this, firstValue);}

线程间传值实现原理

public class Thread implements Runnable {......(其他源码)/* * 当前线程的ThreadLocalMap,主要存储该线程自身的ThreadLocal*/ThreadLocal.ThreadLocalMap threadLocals = null;/** InheritableThreadLocal,自父线程集成而来的ThreadLocalMap,* 主要用于父子线程间ThreadLocal变量的传递* 本文主要讨论的就是这个ThreadLocalMap*/ThreadLocal.ThreadLocalMap inheritableThreadLocals = null;......(其他源码)
}

Thread类中包含 threadLocals 和 inheritableThreadLocals 两个变量,其中inheritableThreadLocals 即主要存储可自动向子线程中传递的ThreadLocal.ThreadLocalMap。
接下来看一下父线程创建子线程的流程,我们从最简单的方式说起:
用户创建Thread
Thread thread = new Thread();

   /*** Allocates a new {@code Thread} object. This constructor has the same* effect as {@linkplain #Thread(ThreadGroup,Runnable,String) Thread}* {@code (null, null, gname)}, where {@code gname} is a newly generated* name. Automatically generated names are of the form* {@code "Thread-"+}<i>n</i>, where <i>n</i> is an integer.*/public Thread() {init(null, null, "Thread-" + nextThreadNum(), 0);}/*** 默认情况下,设置inheritThreadLocals可传递*/private void init(ThreadGroup g, Runnable target, String name,long stackSize) {init(g, target, name, stackSize, null, true);}/*** 初始化一个线程.* 此函数有两处调用,* 1、上面的 init(),不传AccessControlContext,inheritThreadLocals=true* 2、传递AccessControlContext,inheritThreadLocals=false*/private void init(ThreadGroup g, Runnable target, String name,long stackSize, AccessControlContext acc,boolean inheritThreadLocals) {......(其他代码)if (inheritThreadLocals && parent.inheritableThreadLocals != null)this.inheritableThreadLocals =ThreadLocal.createInheritedMap(parent.inheritableThreadLocals);......(其他代码)}

可以看到,采用默认方式产生子线程时,inheritThreadLocals=true;若此时父线程inheritableThreadLocals不为空,则将父线程inheritableThreadLocals传递至子线程

ThreadLocal.createInheritedMap
让我们继续追踪createInheritedMap:

    static ThreadLocalMap createInheritedMap(ThreadLocalMap parentMap) {return new ThreadLocalMap(parentMap);}/*** 构建一个包含所有parentMap中Inheritable ThreadLocals的ThreadLocalMap* 该函数只被 createInheritedMap() 调用.*/private ThreadLocalMap(ThreadLocalMap parentMap) {Entry[] parentTable = parentMap.table;int len = parentTable.length;setThreshold(len);// ThreadLocalMap 使用 Entry[] table 存储ThreadLocaltable = new Entry[len];// 逐一复制 parentMap 的记录for (int j = 0; j < len; j++) {Entry e = parentTable[j];if (e != null) {@SuppressWarnings("unchecked")ThreadLocal<Object> key = (ThreadLocal<Object>) e.get();if (key != null) {// 可能会有同学好奇此处为何使用childValue,而不是直接赋值,// 毕竟childValue内部也是直接将e.value返回;// 个人理解,主要为了减轻阅读代码的难度Object value = key.childValue(e.value);Entry c = new Entry(key, value);int h = key.threadLocalHashCode & (len - 1);while (table[h] != null)h = nextIndex(h, len);table[h] = c;size++;}}}}

从ThreadLocalMap可知,子线程将parentMap中的所有记录逐一复制至自身线程

InheritableThreadLocal主要用于子线程创建时,需要自动继承父线程的ThreadLocal变量,方便必要信息的进一步传递。

public class Test {private static List getList(String param) {List rst = new ArrayList<>();rst.add(param);return rst;}private static final InheritableThreadLocal<List> threadLocal = new InheritableThreadLocal<>();public static void test(Consumer<InheritableThreadLocal<List>> consumer) throws InterruptedException {threadLocal.set(getList("test"));Thread child = new Thread(() -> {while (!Thread.currentThread().isInterrupted()){try {TimeUnit.MILLISECONDS.sleep(1);} catch (InterruptedException e) {throw new RuntimeException(e);}System.out.println("子线程中threadLocal的值:" + threadLocal.get());}});System.out.println("主线程中threadLocal的值:" + threadLocal.get());child.start();TimeUnit.MILLISECONDS.sleep(1);consumer.accept(threadLocal);System.out.println("主线程中threadLocal的值:" + threadLocal.get());TimeUnit.MILLISECONDS.sleep(3);child.interrupt();}public static void main(String[] args) throws InterruptedException {//**创建子线程完成后,主线程调用set方法修改值,不会影响到子线程test(local -> local.set(getList("test1")));System.out.println("===========================");//**保存list对象时,通过get方法获取,然后修改list的值,则会影响到子线程test(local -> local.get().set(0, "test2"));}
}//**执行结果
主线程中threadLocal的值:[test]子线程中threadLocal的值:[test]主线程中threadLocal的值:[test1]子线程中threadLocal的值:[test]===========================主线程中threadLocal的值:[test]子线程中threadLocal的值:[test]主线程中threadLocal的值:[test2]子线程中threadLocal的值:[test2]
3.7 TransmittableThreadLocal详解

用于解决使用线程池缓存线程的组件的情况下传递ThreadLocal
使用场景:分布式跟踪系统,应用容器或上下层框架跨应用代码给下层SDK传递信息,日志收集系统上下文,

源码分析:

TransmittableThreadLocal 继承自 InheritableThreadLocal,这样可以在不破坏ThreadLocal 本身的情况下,使得当用户利用 new Thread() 创建线程时仍然可以达到传递InheritableThreadLocal 的目的。

public class TransmittableThreadLocal<T> extends InheritableThreadLocal<T>{......}

TransmittableThreadLocal相比于InheritableThreadLocal比较关键的一点是引入了holder变量,这样就不必对外暴露Thread中的inherittablethreadlocals变量保存ThreadLocalMap的封装性

// 理解holder,需注意如下几点:
// 1、holder 是 InheritableThreadLocal 变量;
// 2、holder 是 static 变量;
// 3、value 是 WeakHashMap;
// 4、深刻理解 ThreadLocal 工作原理;
private static InheritableThreadLocal<Map<TransmittableThreadLocal<?>, ?>> holder =new InheritableThreadLocal<Map<TransmittableThreadLocal<?>, ?>>() {@Overrideprotected Map<TransmittableThreadLocal<?>, ?> initialValue() {return new WeakHashMap<>();}@Overrideprotected Map<TransmittableThreadLocal<?>, ?> childValue(Map<TransmittableThreadLocal<?>, ?> parentValue) {return new WeakHashMap<>(parentValue);}};
// 调用 get() 方法时,同时将 this 指针放入 holder
public final T get() {T value = super.get();if (null != value) {addValue();}return value;
}
void addValue() {if (!holder.get().containsKey(this)) {holder.get().put(this, null); // WeakHashMap supports null value.}
}
// 调用 set() 方法时,同时处理 holder 中 this 指针
public final void set(T value) {super.set(value);if (null == value) { // may set null to remove valueremoveValue();} else {addValue();}
}
void removeValue() {holder.get().remove(this);
}

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/751506.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

CleanMyMac X2024免费绿色版安装包下载

在当今这个数字化时代&#xff0c;我们的生活和工作中离不开电脑&#xff0c;尤其是对于Mac用户而言&#xff0c;更是需要一个轻巧、快捷的解决方案来保持电脑的高效运转。CleanMyMac X正是为此而生&#xff0c;它将帮助您清理Mac中的垃圾文件、优化系统性能&#xff0c;让您的…

Python电梯楼层数字识别

程序示例精选 Python电梯楼层数字识别 如需安装运行环境或远程调试&#xff0c;见文章底部个人QQ名片&#xff0c;由专业技术人员远程协助&#xff01; 前言 这篇博客针对《Python电梯楼层数字识别》编写代码&#xff0c;代码整洁&#xff0c;规则&#xff0c;易读。 学习与应…

STM32的简单介绍

STM32是一种基于ARM Cortex-M内核的32位微控制器&#xff0c;由意法半导体公司开发和生产。STM32具有丰富的外设和功能&#xff0c;适用于各种应用场合&#xff0c;如工业控制、消费电子、物联网、人机交互等。STM32的优势包括低功耗、高性能、高可靠性、易于开发等。STM32的系…

嵌入式学习39-程序创建数据库及查找

1.sqlite3_open int sqlite3_open( const char *filename, /* Database filename (UTF-8) */ sqlite3 **ppDb /* OUT: SQLite db handle */ ); 功能: 打开 数据库文件(创建一个数据库连接) 参数: filename: …

【Linux】基础 IO(文件描述符)-- 详解

一、前言 1、文件的宏观理解 文件在哪呢&#xff1f; 从广义上理解&#xff0c;键盘、显示器、网卡、声卡、显卡、磁盘等几乎所有的外设都可以称之为文件&#xff0c;因为 “Linux 下&#xff0c;一切皆文件”。 从狭义上的理解&#xff0c;文件在磁盘&#xff08;硬件&#…

【博士每天一篇文献-综述】Brain network communication_ concepts, models and applications

阅读时间&#xff1a;2023-12-1 1 介绍 年份&#xff1a;2023 作者&#xff1a;Caio Seguin&#xff0c;Olaf Sporns印第安纳大学心理与脑科学系 期刊&#xff1a; nature reviews neuroscience 引用量&#xff1a;33 中文翻译参考&#xff1a;https://swarma.org/?p44524 …

深度学习pytorch——Tensor维度变换(持续更新)

view()打平函数 需要注意的是打平之后的tensor是需要有物理意义的&#xff0c;根据需要进行打平&#xff0c;并且打平后总体的大小是不发生改变的。 并且一定要谨记打平会导致维度的丢失&#xff0c;造成数据污染&#xff0c;如果想要恢复到原来的数据形式&#xff0c;是需要…

力扣细节题:字符串中的最大奇数

奇数只要找到第一位是奇数的即可&#xff0c;不是找单个数字 //即从最低位开始&#xff0c;找到第一位为奇数的位 //然后之前的就是需要的数字char * largestOddNumber(char * num){int i strlen(num) - 1;while(i > 0){if((num[i] - 0) % 2 1)break;i--;}//先找到低位开…

Spring Boot中application配置文件的生效顺序

Spring Boot的一个重要特性就是它的自动配置&#xff0c;这一特性在很大程度上依赖于名称为application的配置文件。本文将详细介绍在Spring Boot中&#xff0c;这些配置文件的加载顺序以及每份文件的应用范围。 文章目录 配置文件的种类配置文件的加载顺序配置文件的环境切换 …

Win10系统使用IIS服务搭建WebDAV网站结合内网穿透公网访问本地文件

文章目录 推荐1. 安装IIS必要WebDav组件2. 客户端测试3. cpolar内网穿透3.1 打开Web-UI管理界面3.2 创建隧道3.3 查看在线隧道列表3.4 浏览器访问测试 4. 安装Raidrive客户端4.1 连接WebDav服务器4.2 连接成功4.2 连接成功总结&#xff1a; 推荐 前些天发现了一个巨牛的人工智能…

Python爬虫与数据可视化源码免费领取

引言 作为一名在软件技术领域深耕多年的专业人士&#xff0c;我不仅在软件开发和项目部署方面积累了丰富的实践经验&#xff0c;更以卓越的技术实力获得了&#x1f3c5;30项软件著作权证书的殊荣。这些成就不仅是对我的技术专长的肯定&#xff0c;也是对我的创新精神和专业承诺…

Docker 哲学 - 容器操作 -cp

1、拷贝 容器绑定的 volume的 数据&#xff0c;到指定目录 2、匿名挂载 volume 只定义一个数据咋在容器内的path&#xff0c;docker自动生成一个 sha256 的key作为 volume 名字。这个 sha256 跟 commitID 一致都是唯一的所以 &#xff0c;docker利用这个机制&#xff0c;可以…

AI大浪潮,怎能少了国产HBM内存?

据有关报道显示&#xff0c;武汉新芯半导体制造有限公司&#xff08;XMC&#xff09;正在启动一项专注于开发和生产高带宽内存&#xff08;HBM&#xff09;的项目。 HBM作为一种关键的DRAM类型&#xff0c;对于人工智能&#xff08;AI&#xff09;和高性能计算&#xff08;HPC&…

Python自动获取指定上市公司的所有财务数据(资产负债表,利润表,现金流量表)

案例背景 很多经管类同学找财务数据都很困难&#xff0c;去找一个个查找特定的公司&#xff0c;然后又要去同花顺或者东方财富网一年一年的去查看报表&#xff0c;一年一年的数据一个个填入...太慢了。 tushare能获取金融数据的接口&#xff0c;他有资产负债表&#xff0c;利…

upload-labs第一关

上一篇文章中搭建好了upload-labs环境&#xff0c;接下来进行第一关的尝试&#xff0c;我也是第一次玩这个挺有意思。 1、第一关的界面是这样的先不看其他的源码&#xff0c;手动尝试下试试。 2、写一个简单的php一句话木马 3、直接上传&#xff0c;提示必须要照片格式的文…

HarmonyOS(鸿蒙)ArcUI组件

方舟开发框架&#xff08;简称ArkUI&#xff09;为HarmonyOS应用的UI开发提供了完整的基础设施&#xff0c;包括简洁的UI语法、丰富的UI功能&#xff08;组件、布局、动画以及交互事件&#xff09;&#xff0c;以及实时界面预览工具等&#xff0c;可以支持开发者进行可视化界面…

蓝桥杯单片机快速开发笔记——矩阵键盘

一、原理分析 二、思维导图 三、示例框架 定义了四个位控制变量&#xff0c;用于控制键盘扫描时的行列信号。 在Scan_Keys()函数中&#xff0c;首先设置行列信号&#xff0c;将其中一个行信号置为0&#xff0c;另一个行信号置为1&#xff0c;同时将列信号置为1&#xff0c;用于…

【Redis知识点总结】(四)——如何保证缓存与数据库中的数据一致性

Redis知识点总结&#xff08;四&#xff09;——如何保证缓存与数据库中的数据一致性 更新缓存删除缓存先删除缓存后更新数据库先更新数据库后删除缓存 使用canal总结 面试会经常遇到这种问题&#xff1a;你们如何保证缓存与数据库中的数据一致性&#xff1f;或者是&#xff1a…

*波动数列c++

题目 输入样例&#xff1a; 4 10 2 3输出样例&#xff1a; 2样例解释 两个满足条件的数列分别是2 4 1 3和7 4 1 -2。 思路 上来先理解题意&#xff0c;本题求的是“长度为n 总和为s的……数列的数目”。 假设第一项为x&#xff0c;增加 a 或者减少 b用di表示&#xff0c;…

【Spring Boot 源码学习】深入应用上下文初始化器实现

《Spring Boot 源码学习系列》 深入应用上下文初始化器实现 一、引言二、往期内容三、主要内容3.1 spring-boot 子模块中内置的实现类3.1.1 ConfigurationWarningsApplicationContextInitializer3.1.2 ContextIdApplicationContextInitializer3.1.3 DelegatingApplicationConte…