面试又被问懵了吗?不如把ThreadLocal拆开了揉碎看看

3天冲刺Java实习面试:八股文高效复习全攻略 技术面试中,基础知识的考察往往从八股文开始,但面试官真正关注的并非死记硬背的结论,而是候选人对底层原理的理解程度。以HashMap为例,从数据结构到扩容机制,再到并发场景下的演变,背后牵涉哈希碰撞、加载因子、红黑树等一连串知识网络。同样,JVM内存模型、并发编程中的volatile与synchronized、Spring循环依赖的三级缓存等,都是高频考点。掌握这些核心概念的原理、技术价值与应用场景,能帮助求职者构建完整的知识骨架。本文整理了一套针对Java实习面试的3天冲刺方法,明确每日复习重点与表达技巧, 阅读详情

前言

1.为什么用 ThreadLocal?

所谓并发,就是有限资源需要应对远超资源的访问。解决问题的方法,要么增加资源应对访问;要么增加资源的利用率。 所以,相信这年头做开发的多多少少,都会那么几个“线程二三招”、“用锁五六式”。 那所带来的就是多线程访问下的并发安全问题。 共享变量的访问域跨越了原始的单线程,进入了千家万户的线程眼里。谁都可以用,谁都可以改,那不就打起来了吗? 因此,防止并发问题的最好办法,就是不要多线程访问(这科技水平倒退二十年~)。ThreadLocal 顾名思义,将一个变量限制为“线程封闭”:对象只被一个线程持有、访问、修改。

2.那到底什么是 ThreadLocal?

ThreadLocal 如果做到线程封闭,那固然是独木难支。它必然携手 Thread 为广大 Javaer 带来福音。 ThreadLocal 自己不是存储者,它只是 Thread 的搬运工。独有变量必然是存在 Thread 中的。一般项目中多定义多个 ThreadLocal,那相应的 Thread 必然也需要存储那么多独有变量。 既然解决了线程之间的访问干扰,那一个线程的访问干扰自然就不在话下了。Thread 维护了一个 ThreadLocalMap,以“key-value”的形式存储了独有变量;以 ThreadLocal 实例为 key,精准获取。

3.ThreadLocal 需要考虑哪些问题?

如果线程死亡了,那 ThreadLocalMap、ThreadLocal 及独有变量都会被销毁。

但是现在避免线程的重复创建与销毁,线程使用完都是放回线程池。而如果没有手动移除 ThreadLocalMap 的元素,即使当前线程退出,ThreadLocal 已不被线程方法栈持有,也依然无法被回收,从而造成内存泄漏。 所以 ThreadLocalMap.Entry 的 key(也就是 ThreadLocal)实际是弱引用。当没有其他强引用时,只要发生 GC,就会被回收,相当于这个时候 key 为 null。

这又产生了一个问题,key 被回收了, entry 和 value 可还是强引用呢,怎么办? ThreadLocalMap 已经考虑了这种情况,再调用 set()、get()、remove() 方法的时候,会清理掉 key 为 null 的记录。 所以人家设计是没有问题的,如果发生内存泄漏都是用的不对。 建议使用完 ThreadLocal方法后,最好手动调用remove()方法。

4.ThreadLocal 还需要考虑哪些问题?

随着业务场景的复杂化,变量的线程封闭固然解决了访问的问题,但是也给线程传递带来了难度。 线程之间的协作,带来了变量在两个线程之间安全传递的需要。需要人为处理这种传递,需要三个步骤:

  • 线程 1 取出变量;
  • 线程 1 安全传递变量、ThreadLocal(其实一般选择共享)给线程 2,当心逃逸。
  • 线程 2 放到当前线程的 ThreadLocal。 这个步骤是通用的,只要存在使用 hreadLocal并且需要线程传递时,必然少不了这三步。 JDK 为我们提供了“线程 2 是线程 1 创建出来时,独有变量传递给线程 2”的解决方法:InheritableThreadLocal,Thread 中也有专门为其服务的 ThreadLocalMap。

那我们明白,在线程池化的世面下,不会经常存在创建的场景,更多的是与已有线程的协作。 各家公司,其实也会为相关业务的 ThreadLocal 自研类库,去做到传递。 市面上解决通用场景的线程传递的类库就是 TransmittableThreadLocal。我也写过一篇该类库的源码解析:https://blog.youkuaiyun.com/jiangxiayouyu/article/details/105605116

源码解析

Thread

public Class Thread implements Runnable {

    //与此线程有关的 ThreadLocal 值。由 ThreadLocal 类维护
    ThreadLocal.ThreadLocalMap threadLocals = null;
	// 与此线程有关的 InheritableThreadLocal 值。由 InheritableThreadLocal 类维护
    ThreadLocal.ThreadLocalMap inheritableThreadLocals = null;

ThreadLocalMap 是 ThreadLocal 的内部类,是定制的 Map 实现。 初始值都为 null,只有当第一次调用对应ThreadLocal的 get 或 set 时,才会初始化。

ThreadLcoal

ThreadLcoal 只有一个默认的无参构造函数。实际的初始化逻辑,都在第一次调用 get 或 set 时。

######get()

由于是类似懒加载的形式,所以 get 中涉及到ThreadLocalMap的创建以及初始值设置。

public T get() {
    Thread t = Thread.currentThread();
    // 获取线程的 map, 为啥要抽取方法呢?就是为了扩展之前提到的 InheritableThreadLocal
    ThreadLocalMap map = getMap(t);
    if (map != null) {
    // 已经 set 过
    ThreadLocalMap.Entry e = map.getEntry(this);
    if (e != null) {
    @SuppressWarnings("unchecked")
    // 走到这里没有 Entry 的情况:remove 以后
    T result = (T)e.value;
    return result;
    }
    }
    // 未 set 过的第一次 get (map == null)
    // 或 set 过, 但是 remove 了 (map != null && e == null)
    return setInitialValue();
}

private T setInitialValue() {
    // 获取指定初始值, 默认是 null
    // 可以通过 withInitial(Supplier<? extends S> supplier) 工厂方法来创建指定初始化值的 ThreadLocal
    T value = initialValue();
    Thread t = Thread.currentThread();
    ThreadLocalMap map = getMap(t);
    if (map != null) {
        map.set(this, value);
    } else {
        // ThreadLocalMap 未初始化
        createMap(t, value);
    }
    if (this instanceof TerminatingThreadLocal) {
        // 处理一个特殊子类的逻辑
        TerminatingThreadLocal.register((TerminatingThreadLocal<?>) this);
    }
    return value;
}   
void createMap(Thread t, T firstValue) {
    t.threadLocals = new ThreadLocalMap(this, firstValue);
}
ThreadLocalMap getMap(Thread t) {
    return t.threadLocals;
}

指定初始值的工厂构造方法

// 如果以下情况下的第一次 get, 判断 map 的 entry 为 null下
// 1.从未 set 过;
// 2.remove 过后
protected T initialValue() {
	return null;
}

默认初始值是 null。 可以通过以下工厂方法,获取一个指定初始化逻辑的 ThreadLocal。

public static <S> ThreadLocal<S> withInitial(Supplier<? extends S> supplier) {
    return new SuppliedThreadLocal<>(supplier);
}

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
    protected T initialValue() {
        return supplier.get();
    }
}

set() & remove()

set() 有点像 setInitialValue(),只不过一个是初始值,一个是指定值。

两个方法其实本身都简单,主要依赖于 ThreadLocalMap的操作。

public void set(T value) {
    Thread t = Thread.currentThread();
    ThreadLocalMap map = getMap(t);
    if (map != null) {
        map.set(this, value);
    } else {
        createMap(t, value);
    }
}
public void remove() {
    ThreadLocalMap m = getMap(Thread.currentThread());
    if (m != null) {
        m.remove(this);
    }
}

ThreadLocalMap

  • 这个类是 ThreadLocal 的内部类,是包私有的。
  • key 的 hashcode 是自定义的增长值。
  • key 是 WeakReference 的。

Entry

可以看到 key 就是 ThreadLocal,肯定不为空,但也是弱引用的。

也就是说,当 key 为 null 时,说明 ThreadLocal 已经被回收了,对应的 Entry 就应该被清除了。

static class Entry extends WeakReference<ThreadLocal<?>> {
    /** The value associated with this ThreadLocal. */
    Object value;

    Entry(ThreadLocal<?> k, Object v) {
        super(k);
        value = v;
    }
}

预设值

  • 初始容量为 16,扩容翻倍。所以容量一定为 2 的 n 次幂。
  • 负载因子是 2/3。
  • 初始化时,应该是第一次设置值,或来源于 ThreadLocalMap。所以算得上饿汉式加载。
private static final int INITIAL_CAPACITY = 16;
private Entry[] table;
private int size = 0;
private int threshold; // Default to 0
private void setThreshold(int len) {
    threshold = len * 2 / 3;
}

构造函数

ThreadLocalMap(ThreadLocal<?> firstKey, Object firstValue) {
    table = new Entry[INITIAL_CAPACITY];
    int i = firstKey.threadLocalHashCode & (INITIAL_CAPACITY - 1);
    table[i] = new Entry(firstKey, firstValue);
    size = 1;
    setThreshold(INITIAL_CAPACITY);
}

ThreadLocal {
    /**
     * 人为设置的 hash code 分布. 对于在相同线程中使用连续构造的 ThreadLocal, 可以有效避免冲突.
     * 因为是可以预见的场景, 仅在 ThreadLocalMap 中使用.
     */
    private final int threadLocalHashCode = nextHashCode();

    /**
     * The next hash code to be given out. Updated atomically. Starts at
     * zero.
     */
    private static AtomicInteger nextHashCode =  new AtomicInteger();

    private static final int HASH_INCREMENT = 0x61c88647;
    private static int nextHashCode() {
        return nextHashCode.getAndAdd(HASH_INCREMENT);
    }
}

每个 ThreadLocal 对象都有一个 hash 值 threadLocalHashCode,每初始化一个 ThreadLocal 对象,hash 值就增加一个固定的大小 0x61c88647。这个东西比较讲究,有兴趣可以自行研究一下。

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;
         // 开放定址法: 索引位置 + 1
         e = tab[i = nextIndex(i, len)]) {
        ThreadLocal<?> k = e.get();

        if (k == key) {
            e.value = value;
            return;
        }

        if (k == null) {
            // key 为空, 说明 对应的 ThreadLocal 已经回收了.
            // 可以复用当前位置.
            // 有两种情况:1. entry 存在, 在这个过时位置的后面. 所以需要置换到这个位置
            // 2.不存在, 直接放到这个位置
            replaceStaleEntry(key, value, i);
            // 因为是替换, 所以size 要么不变,要么减少。
            return;
        }
    }
	
    // 没找到已存在的, 也没找到可以替换的过时. 则直接新建
    tab[i] = new Entry(key, value);
    int sz = ++size;
    if (!cleanSomeSlots(i, sz) && sz >= threshold)
        // 如果没有清除过时 entry, 并且超过阈值. 则进行先尝试缩小,不行则扩容
        rehash();
}

类中定义了两个方法用于开放定址法的查找:增量为 1。

private static int prevIndex(int i, int len) {
    return ((i - 1 >= 0) ? i - 1 : len - 1);
}

private static int nextIndex(int i, int len) {
    return ((i + 1 < len) ? i + 1 : 0);
}

replaceStaleEntry()

replaceStaleEntry() 比较复杂。一是需要清除过时 entry,二是开放定址法要保证所计算出的索引值后面的元素连续性。

所以,replaceStaleEntry() 会检查当前可替换位置的前后最近的两个空档之间所有的过时 entry。

其次,如果是 key 已存在过时位置的后面,那原有位置替换后会留出空档,需要后面的 entry 都往前挪一位(空档前的)。

private void replaceStaleEntry(ThreadLocal<?> key, Object value,
                               int staleSlot) {
    Entry[] tab = table;
    int len = tab.length;
    Entry e;

    int slotToExpunge = staleSlot;
    // 1.往前查找第一个空档后的最小过时
    for (int i = prevIndex(staleSlot, len);
         (e = tab[i]) != null;
         i = prevIndex(i, len))
        if (e.get() == null)
            slotToExpunge = i;


    // 往前查找第一个空档前的 key 或 最大过时
    for (int i = nextIndex(staleSlot, len);
         (e = tab[i]) != null;
         i = nextIndex(i, len)) {
        ThreadLocal<?> k = e.get();

        // 找到对应的 entry
        if (k == key) {
            e.value = value;
            // 2.将key 与原位置的过时替换
            tab[i] = tab[staleSlot];
            tab[staleSlot] = e;

            if (slotToExpunge == staleSlot)
                // 3.如果前面都没有过时的话,那这个区间的第一个过时就是原来的staleSlot, 现在的 i
                slotToExpunge = i;
            // 4.清理过时, 挪移 entry
            cleanSomeSlots(expungeStaleEntry(slotToExpunge), len);
            return;
        }

        // 5.如果前面没有空槽, 且有新的过时, 则重新标记第一个过时.(因为staleSlot一定会被替换成不过时的,到时候就不是第一个过时点了)
        if (k == null && slotToExpunge == staleSlot)
            slotToExpunge = i;
    }
	// 6.直接替换
    tab[staleSlot].value = null;
    tab[staleSlot] = new Entry(key, value);

    // slotToExpunge == staleSlot, 说明当前区间只有这个过时, 已经被替换了, 所以不需要再进行清除
    if (slotToExpunge != staleSlot)
        // key 本不在, 且前或后存在其他的过时
        // 7.清理过时, 挪移 entry
        cleanSomeSlots(expungeStaleEntry(slotToExpunge), len);
}

5、7 由于是清理过时,后面再详细说明。

区间是当前过时位置staleSlot前后第一个空位所组成的范围,即下图两个空白格子之间。

我们根据区间的不同情况,做了图例说明。

key 存在:

面试又被问懵了吗?不如把ThreadLocal拆开了揉碎看看

 

key 不存在:

面试又被问懵了吗?不如把ThreadLocal拆开了揉碎看看

 

rehash()

当 set() 完,数量到达阈值,是先尝试能不能删掉一些过时的。如果删无可删,或者删完之后达不到标准,则扩容。

注意的是,这个标准不是之前的 threshold,而是 3/4 threshold,避免滞后性。

private void rehash() {
    // 对整个数组进行扫描,清理.
    // 而不像替换那步, 只扫描区间
    expungeStaleEntries();

    // Use lower threshold for doubling to avoid hysteresis
    if (size >= threshold - threshold / 4)
        resize();
}

private void resize() {
    Entry[] oldTab = table;
    int oldLen = oldTab.length;
            // 翻倍扩容
    int newLen = oldLen * 2;
    Entry[] newTab = new Entry[newLen];
    int count = 0;

    for (Entry e : oldTab) {
        if (e != null) {
            ThreadLocal<?> k = e.get();
            if (k == null) {
                // 发现过时, 则抛弃
                e.value = null; // Help the GC
            } else {
                // 重新 hash
                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];
        if (e != null && e.get() == null)
            expungeStaleEntry(j);
    }
}

expungeStaleEntry()

从上面的分析可以看到,该方法应用在 replaceStaleEntry 和 expungeStaleEntries。

replaceStaleEntry是对区间进行处理, expungeStaleEntries是对全数组。所以expungeStaleEntry(int)就是上述处理的一个子集。这样理解下来,就是清理指定位置到下一个空位之间的过时 entry,包含指定位置:[index, indexOf(first null))。

  • index 一定是一个过时元素的位置。
  • 既然过时的会被清除,那中间就会留出空位。开放定址法是要求连续的,所以重新计算索引来放置。
  • 注意:保留的 key 是重新计算索引, 而不是简单地往前挪一位。
    • 这是因为清除区间的过时,是在某个 key 与运算出的起始索引之前。
    • 而 key 刚好在这个索引上,简单往前挪一位,下次查找可能就找不到了。
    • 因为要求连续性地从头遍历到尾,一旦中间出现空位,就找不到了。
private int expungeStaleEntry(int staleSlot) 
    Entry[] tab = table;
    int len = tab.length;

    // expunge entry at staleSlot
    // 明确当前位置一定是过时的, 先直接清理掉
    tab[staleSlot].value = null;
    tab[staleSlot] = null;
    size--;

    // Rehash until we encounter null
    Entry e;
    int i;
    // 开始遍历直到遇到第一个空位
    for (i = nextIndex(staleSlot, len);
         (e = tab[i]) != null;
         i = nextIndex(i, len)) {
        ThreadLocal<?> k = e.get();
        if (k == null) {
            // 清理过时
            e.value = null;
            tab[i] = null;
            size--;
        } else {
            // 因为前面留出空档, 所以后面的元素都要重新计算索引, 以望填补空位
            int h = k.threadLocalHashCode & (len - 1);
            if (h != i) {
                tab[i] = null;

                // Unlike Knuth 6.4 Algorithm R, we must scan until
                // null because multiple entries could have been stale.
                while (tab[h] != null)
                    h = nextIndex(h, len);
                tab[h] = e;
            }
        }
    }
    // 返回遇到的第一个空位
    return i;
}

cleanSomeSlots()

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];
        // 没扫到任何过时,共扫描 log2(n) 个槽;
        if (e != null && e.get() == null) {
            // 上述期间扫到过时,则将该区间遍历:
            // 然后基于区间终点,重新扫描 log2(length);
            // 如果扫到,重复上面;
            // 如果一直重复,最终扫描了全数组。
            n = len;
            removed = true;
            i = expungeStaleEntry(i);
        }
    } while ( (n >>>= 1) != 0);
    return removed;
}

cleanSomeSlots 一般在新增一个元素或删除另一个旧元素(不是 remove,而是 set 时刚好删掉另一个过时的后),进行扫描或清除。

起始位置是一个元素不是过时的索引,是扫描完一个区间后的终点(空位)或新增元素的位置。

终点的话,因为使用的是对数扫描,是两个极端情况的平衡:

  • 没扫到任何过时,共扫描 log2(n) 个槽;
  • 上述期间扫到过时,则将该区间遍历;
    • 然后基于区间终点,重新扫描 log2(length);
    • 如果扫到,重复上面;
    • 如果一直重复,最终扫描了全数组。

get & remove

get:

  • 如果直接找到,则返回;
  • 如果没有,在开放定址法的增量下,遍历查找。而这个过程,还需要兼职清除区间内的过时(expungeStaleEntry(int))。

remove:

找到指定的 key, 清除完,同样兼职清除一下区间内的。

内存泄漏

经过上述的分析,由于 key 也就是 ThreadLocal 在 Entry 中是 WeakReference 的。

ThreadLocal 在没有外部强引用时,发生 GC 的话,ThreadLocalMap的弱引用将不会影响回收。

那相当于 Entry 中的 key = null,可是 Entry 和 Value 都是强引用,是无法跟随着 key 一起被销毁的。

想想 ThreadLocal 的作用,当 ThreadLocal 都被销毁了,那 key-value 的存储就没有意义了。

如果等到兼职任务去清除过时,也是存在时间差的,在 value 是大对象的时候,也是较为麻烦的。

所以建议:

当使用完退出时,最好使用ThreadLocal.remove()方法将该变量主动移除。

InheritableThreadLocal

当线程 2 是从 线程 1 创建的时候,可以指定是否从线程 1 继承 ThreadLocal。当然,前提是线程 1 使用了可以被继承的 InheritableThreadLocal。

private Thread(ThreadGroup g, Runnable target, String name,
               long stackSize, AccessControlContext acc,
               boolean inheritThreadLocals) {
    ...... 省略
    Thread parent = currentThread();
    // parent.inheritableThreadLocals 不为空, 要当前线程必须使用 InheritableThreadLocal 
    if (inheritThreadLocals && parent.inheritableThreadLocals != null)
        this.inheritableThreadLocals =
            ThreadLocal.createInheritedMap(parent.inheritableThreadLocals);
    ...... 省略
}

ThreadLocal :

static ThreadLocalMap createInheritedMap(ThreadLocalMap parentMap) {
    return new ThreadLocalMap(parentMap);
}
// 只用于 createInheritedMap
private ThreadLocalMap(ThreadLocalMap parentMap) {
    Entry[] parentTable = parentMap.table;
    int len = parentTable.length;
    setThreshold(len);
    table = new Entry[len];

    for (Entry e : parentTable) {
        if (e != null) {
            ThreadLocal<Object> key = (ThreadLocal<Object>) e.get();
            if (key != null) {
                // 从父类那计算子类值, 默认是一样的
                Object value = key.childValue(e.value);
                Entry c = new Entry(key, value);
                int h = key.threadLocalHashCode & (len - 1);
                while (table[h] != null)
                    // hash 冲突处理方法是,开放定址法
                    h = nextIndex(h, len);
                table[h] = c;
                size++;
            }
        }
    }
}

InheritableThreadLocal:

可以看到使用 InheritableThreadLocal,操作的 Thread的变量是不同于 ThreadLocal。

刚好对应了上面创建 Thread,继承父线程的 inheritableThreadLocals。

public class InheritableThreadLocal<T> extends ThreadLocal<T> {
    
    protected T childValue(T parentValue) {
        // 从父类值计算子类值, 可以重写
        return parentValue;
    }
    
    ThreadLocalMap getMap(Thread t) {
        // 获取的 map 不同
       return t.inheritableThreadLocals;
    }

    void createMap(Thread t, T firstValue) {
        // 使用的 map 不同
        t.inheritableThreadLocals = new ThreadLocalMap(this, firstValue);
    }
}
Java秋招八股文:从死记硬背到融会贯通的面试实战指南 Java后端面试考察的核心从来不是死记硬背,而是对技术原理与工程实践的融会贯通。面对校招与秋招,很多候选人卡在同样的环节:知识点背得滚瓜烂熟,一旦被追问底层实现就难以招架。从JVM内存模型与垃圾回收,到并发编程中synchronized锁升级、volatile可见性,再到HashMap底层结构与线程池参数设计,这些高频考点背后都指向一套可推导的思考逻辑。理解集合框架的设计权衡、Spring IOC/AOP的代理机制、MySQL索引与Redis缓存的应用场景,才能在面试现场把“背过”转化为“讲清”。从构建Ja 阅读详情

相关推荐

瑞吉外卖项目深度复盘:从零跑通到面试加分

在Java后端开发的学习路径中,以真实业务项目为载体的实战训练,是连接基础语法与企业级开发的关键桥梁。通过一个贯穿多角色、多状态的业务闭环,开发者可以系统掌握Spring Boot、MyBatis-Plus、Redis等主流技术的工程化应用。原理上,项目围绕登录态拦截、分类建模、购物车合并、订单状态流转、缓存一致性等核心场景展开,帮助理解从Session到缓存、从单表CRUD到复杂事务的设计演进。其技术价值在于:既能训练规范的Controller-Service-Mapper分层能力,又能借公共字段填充、异

weixin_33483304的博客 296

把 ThreadLocal 拆开揉碎了看看

前言 1.为什么用 ThreadLocal? 所谓并发,就是有限资源需要应对远超资源的访问。解决问题的方法,要么增加资源应对访问;要么增加资源的利用率。 所以,相信这年头做开发的多多少少,都会那么几个“线程二三招”、“用锁五六式”。 那所带来的就是多线程访问下的并发安全问题。 共享变量的访问域跨越了原始的单线程,进入了千家万户的线程眼里。谁都可以用,谁都可以改,那不就打起来了吗? 因此,防止并发问题的最好办法,就是不要多线程访问(这科技水平倒退二十年~)。ThreadLocal 顾名思义,将一个变量限制为“

游戈程序员 179

CATIA高效设计:自定义快捷键全攻略

本文详细介绍了如何在CATIA中自定义快捷键以提升设计效率。通过减少鼠标依赖、优化工作流程和减轻手腕疲劳,合理设置快捷键可显著提高建模速度。文章提供了从基础设置到高级配置的全流程指南,包括解决冲突、推荐方案及外设扩展建议,帮助用户快速掌握CATIA高效设计技巧。

weixin_42648844的博客 588

3、ThreadLocal详解

原文:原文出处   ThreadLocal即线程局部变量或线程本地存储,是一种多线程间并发访问变量的解决方案。与synchoronized与Lock等枷锁的方式不同,ThreadLocal完全不提供锁,而是使用空间换时间的手段,为每个线程提供变量的独立副本,以保障线程安全,因此它不是数据共享的解决方案。在高并发量或者锁竞争激烈的情况下,使用ThreadLocal可以在一定程度上减少锁的竞争,减少CPU开销。   举个栗子:在一个简单的单体服务架构中,用户通过浏览器访问web服务器,登陆之后从服务器获取用户信

sky_20131213的博客 1659

simulink可控励磁同步电机模型分析

他励同步电机模型分析

qq_26681609的博客 5552

实战解析:利用Python requests库高效抓取京东商品评论并构建结构化数据集

本文详细介绍了如何利用Python requests库高效抓取京东商品评论并构建结构化数据集。通过分析京东评论接口、处理反爬机制以及数据解析存储,帮助开发者快速获取电商数据,适用于竞品监控、用户画像构建等商业场景。

weixin_42526415的博客 838

JDK1.8源码阅读(5)--ThreadLocal/Enum/Throwable

JDK1.8源码阅读(4)– 一.ThreadLocal类 类图: 这个类并没有继承其他的类或接口。 private final int threadLocalHashCode = nextHashCode(); 原始种子,由内置种子计算而来,用来生成均匀的索引 一个线程可以有多个ThreadLocal实例,各实例之内的原始种子值不相同 一个ThreadLocal实例也可被多个线程共享,此时多个线...

jacobbbbbbbb的博客 452

转录组测序(RNA-seq)的数据如何分析?

经过富集分析后,我们就可以从富集到的GO term或者Pathway中寻找关键基因,通过一个GO term和Pathway里面的基因往往小于100个,至此我们就又将上千条基因的范围缩小到了100条以内。对GO term或者Pathway里面的基因进行范围的缩小也有两个方法,一种方法是根据基因的注释或者文献来找到GO term或者Pathway里面的关键基因(2-3个),另一种方法需要有相应的表型数据,与GO term或者Pathway里面基因进行相关性分析,找出与表型高度相关的基因。

weixin_55372631的博客 1万+

【Java原理系列】 Java ThreadLocal 原理源码详解

`ThreadLocal` 的原理是基于线程内部的 `ThreadLocalMap` 实现的。在 `Thread` 类中有一个名为 `threadLocals` 的成员变量,它是一个 `ThreadLocalMap` 对象,用于存储每个线程的线程局部变量。`ThreadLocalMap` 是一个自定义的哈希表,其键为 `ThreadLocal` 对象,值为对应线程的变量副本。

wang2leee的博客 234

洛奇 服务器文件,MabinogiG20S2 [洛奇G20服务端]

Code:Titles:>set_title /id: /add /remove Parameters for titles>set_title /id:60001 /add add devcat title>set_title /id:60001 /remove remove devcat title===========================...

weixin_30813385的博客 4307

WOFOST模型参数调优实战:从‘跑不通’到‘预测准’的避坑指南

本文详细介绍了WOFOST模型参数调优的实战技巧,帮助用户从模型报错诊断到参数敏感性分析,再到土壤-作物参数联调策略,逐步解决‘跑不通’到‘预测准’的问题。通过Python和PCSE工具,提供参数扰动测试模板和关键参数优先级矩阵,助力作物模型的高效校准与预测。

weixin_32597009的博客 623

学习笔记:Java 并发编程(补)ThreadLocal

文章内容:ThreadLocal 中 ThreadLocalMap 的数据结构,以及二者的关系;ThreadLocal 中的 key 为什么是是弱引用;ThreadLocal 中的内存泄露问题的原因;在 ThreadLocal 的最后要加 remove() 方法的原因。

做笔记,记录学习中遇到的问题。 977

R3LIVE项目实战(3) —双目相机与激光雷达 lidar_camera_calib联合标定

lidar_camera_calib是一个在无目标环境中,用于高分辨率LiDAR(例如Livox)和摄像机之间准确的外部标定工具。我们的算法可以在室内和室外场景中运行,并且只需要场景中的边缘信息。如果场景适合,我们可以实现类似于或甚至超过基于目标的方法的像素级准确度。我们使用标定的外部参数给点云上色,并与实际图像进行比较。A和C是点云的局部放大视图。B和D是与A和C中的点云对应的摄像机图像的部分。

几度春风里的博客 4059

图解八股文面试网:把高频技术题画明白

技术面试中,八股文常被诟病,但其背后是计算机领域的基础共识。理解并结构化表达这些知识,比死记硬背更重要。认知负荷理论表明,图形化呈现能显著降低信息处理难度,将散点知识转化为流程与结构图,有助于深刻理解HashMap红黑树转换阈值、TCP三次握手等经典问题。对于求职者和面试官,图解提供了一种高效的知识组织与表达方式。一个开源图解面试网由此而生,覆盖Java集合、并发、JVM以及操作系统等高频考点,辅助读者从原理到应用构建完整技术认知。

weixin_34111790的博客 398

服务器取证——服务器基础知识

服务器基础知识

记录并分享学习安全的知识点.. 2482

ET2012新版免狗免安装绿色软件

ET2012新版免狗免安祖昂绿色软件。,新界面,新视觉,

从死背到讲透:技术面试八股文高效训练法

技术面试中,进程与线程、HashMap原理、TCP握手等基础概念常被当作“八股题”考核,但单纯死记硬背往往难以应对面试官的深度追问。真正高效的方式是理解底层原理,并通过“一题三问”将孤立知识点串联成知识网。从JVM内存模型到MySQL索引选择,本质上都在考察候选人的思维深度与工程落地能力。掌握可复制的训练方法,如90分钟单题训练、三段式答题法、模拟面试复盘,能显著提升表达的逻辑性与抗压性,让八股真正成为面试加分项。无论是应对跳槽、实习还是晋升答辩,这些方法都能帮助程序员建立系统化的知识体系,从“背答案”进阶

weixin_34277853的博客 335

蓝牙模块HM-10手册

蓝牙模块HM-10手册,该手册对蓝牙模块HM-10进行了详细的介绍和使用方法以及一些AT指令。

Java中高级面试八股文:知识主线与每日刷题实战指南

在Java后端开发与面试准备中,基础知识体系是否扎实,往往决定了候选人能否从容应对大厂的多轮技术面。面试官通过高频考点快速评估技术深度,背后考察的是对JVM、并发编程、集合原理、Spring、MySQL、Redis及分布式等核心机制的真正理解。掌握基础知识的主线逻辑,比机械背诵答案更重要。 HashMap底层为什么引入红黑树、线程池参数如何设置、索引为何使用B+树,这些问题背后都对应一套完整的原理链条。只有将零散知识点串联成知识网络,把“每日刷几道”转化为有节奏的精刷与复盘,才能在中高级面试中准确表达技术思

weixin_30252709的博客 406

复分析 阿尔福斯(Ahlfors) 第三版答案 英文

答案分为十章,有部分习题没有答案,全英文复分析阿尔福斯(Ahlfors) 第三版答案

VB摄像头监控

该VB模块可对摄像头捕获的画面进行录像等,可在创建的窗口进行控制

图解八股文面试网:用可视化方式高效备战程序员面试

在程序员技术面试中,如何高效掌握JVM、并发编程、MySQL、Redis等核心知识点,是许多求职者关注的焦点。传统的纯文本资料往往信息密度高、逻辑关系复杂,难以形成长期记忆。图解作为一种直观的知识呈现方式,将复杂的原理、流程与结构转化为可视图表,有效降低认知负担,帮助学习者从“背题”走向“看懂题”。这种可视化方法不仅适用于面试冲刺,也适合系统化梳理计算机基础知识。本文从图解技术的价值出发,介绍一个开源的原创图解八股文面试项目,覆盖操作系统、网络、数据库、中间件等高频考点,并结合部署实践、内容创作方法论与二次

weixin_34034670的博客 381

Android开发面试通关指南:从底层原理到性能优化全解析

在Android系统架构中,Handler消息机制与Binder进程通信构成了应用与系统服务交互的基石。理解其原理,包括Looper、MessageQueue与Binder驱动的一次拷贝机制,是开发者解决线程切换与IPC问题的关键。同时,性能优化能力已成为衡量高级工程师的重要标准,借助R8代码混淆、CPU Profiler火焰图等工具,开发者可以精准定位卡顿、缩减包体积并提升安全性。这些技术广泛应用于App启动加速、列表流畅度优化、多端适配等实际场景。无论是初级还是资深岗位,面试考察的正是这些基础知识的深度

weixin_33795743的博客 430

百度2019校招移动软研面试题剖析:基础原理与高分答题框架

在技术面试中,数据结构、操作系统与Android核心机制构成了移动开发岗位的考察基石。无论是HashMap的哈希冲突处理、Handler的消息循环,还是TCP三次握手、进程线程调度,这些基础原理看似独立,实则共同指向候选人的设计能力与工程思维。掌握底层机制不仅有助于理解框架演进与技术选型,更能指导日常开发中的性能优化与崩溃排查。对于准备大厂校招或跳槽的移动端工程师而言,从基础概念出发,结合真实项目分析答题框架,是快速提升面试通过率的有效路径。本文以百度2019校招移动软研方向问答题合集为样本,拆解各类题目的

weixin_34060299的博客 363

Android校招笔试:从Handler到性能优化,面试官到底在考什么?

在Android开发领域,校招笔试考察的往往不只是知识点记忆,更是工程直觉与源码理解能力。以消息机制为例,Handler、Looper与MessageQueue的协作原理,决定了开发者能否真正解决线程通信和界面卡顿问题;而Binder作为进程间通信的基石,则贯穿了AMS、WMS等系统服务的调用链。理解了这些核心机制,才能进一步深入启动优化、内存治理、ANR排查等性能优化场景。技术选型与原理认知,最终都会落到实际业务中图片加载、列表流畅度、网络框架设计等工程实践上。从基础概念到系统原理,再到应用场景,这条学习

weixin_34199335的博客 311

酷家乐后端笔试题深度解析:Java集合、JVM与缓存一致性考点全梳理

Java后端开发的核心能力,往往体现在对基础组件原理的深度理解上。HashMap在并发场景下的线程安全问题、JVM运行时数据区与OOM排查链路、Spring Boot自动配置机制、MySQL索引结构与Redis缓存一致性方案,都是技术笔试中反复出现的高频考点。理解这些知识背后的设计原理,不仅能帮助开发者在限时答题时快速定位题目意图,更能指导实际项目中的性能优化与架构选型。无论是准备校招笔试,还是系统排查自身技术盲区,掌握集合框架、JVM内存模型、缓存一致性等关键知识,都是构建完整后端知识体系的重要环节。本文

weixin_34094282的博客 252
上一篇: 微服务架构开发实战:基于容器的部署与发布微服务,30K有望了
下一篇: 不怕面试再问HashMap,一次彻底地梳理(原理+手写实现)
爱玛士
博客等级 码龄7年 720粉丝 253原创
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

个

红包个数最小为10个

元

红包金额最低5元

当前余额3.43元 前往充值 >
需支付:10.00元
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付元
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值