ThreadLocal源码

news2024/11/15 10:05:25

介绍

ThreadLocal是一个线程的本地变量,也就意味着这个变量是线程独有的,是不能与其他线程共享的。这样就可以避免资源竞争带来的多线程的问题。

但是,这种解决多线程安全问题的方式和加锁方式(synchronized、Lock) 是有本质的区别的,区别如下所示:

1> 关于资源的管理

当资源是多个线程共享时,访问的时候可以通过加锁的方式,逐一访问资源。

ThreadLocal是每个线程都有一个资源副本,是不需要加锁的。

2> 关于实现方式

锁是通过时间换空间的做法。

ThreadLocal是通过空间换时间的做法。

由于使用场景的不同,我们可以选择不同的技术手段,关键还是要看你的应用场景对于资源的管理是需要多线程之间共享还是单线程内部独享。

说明

  • 它能让线程拥有了自己内部独享的变量
  • 每一个线程可以通过get、set方法去进行操作
  • 可以覆盖initialValue方法指定线程独享的值
  • 通常会用来修饰类里private static final的属性,为线程设置一些状态信息,例如user ID或者Transaction ID
  • 每一个线程都有一个指向threadLocal实例的弱引用,只要线程一直存活或者该threadLocal实例能被访问到,都不会被垃圾回收清理掉

常量&变量

    /**
     * ThreadLocals rely on per-thread linear-probe hash maps attached
     * to each thread (Thread.threadLocals and
     * inheritableThreadLocals).  The ThreadLocal objects act as keys,
     * searched via threadLocalHashCode.  This is a custom hash code
     * (useful only within ThreadLocalMaps) that eliminates collisions
     * in the common case where consecutively constructed ThreadLocals
     * are used by the same threads, while remaining well-behaved in
     * less common cases.
     * 对象私有常量
     */
    private final int threadLocalHashCode = nextHashCode();

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

    /**
     * The difference between successively generated hash codes - turns
     * implicit sequential thread-local IDs into near-optimally spread
     * multiplicative hash values for power-of-two-sized tables.
     * 魔数0x61c88647,利用一定算法实现了元素的完美散列  对应十进制=1640531527。
     */
    private static final int HASH_INCREMENT = 0x61c88647;

    /**
     * Returns the next hash code.
     * 获取下一个hashcode值
     */
    private static int nextHashCode() {
        // 获取并增加,原子操作
        return nextHashCode.getAndAdd(HASH_INCREMENT);
    }

构造方法


    /**
     * Returns the current thread's "initial value" for this
     * thread-local variable.  This method will be invoked the first
     * time a thread accesses the variable with the {@link #get}
     * method, unless the thread previously invoked the {@link #set}
     * method, in which case the {@code initialValue} method will not
     * be invoked for the thread.  Normally, this method is invoked at
     * most once per thread, but it may be invoked again in case of
     * subsequent invocations of {@link #remove} followed by {@link #get}.
     *
     * <p>This implementation simply returns {@code null}; if the
     * programmer desires thread-local variables to have an initial
     * value other than {@code null}, {@code ThreadLocal} must be
     * subclassed, and this method overridden.  Typically, an
     * anonymous inner class will be used.
     *
     * @return the initial value for this thread-local
     * 返回当前线程的这个线程局部变量的“初始值”  由子类重写该方法
     */
    protected T initialValue() {
        return null;
    }

    /**
     * Creates a thread local variable. The initial value of the variable is
     * determined by invoking the {@code get} method on the {@code Supplier}.
     *
     * @param <S> the type of the thread local's value
     * @param supplier the supplier to be used to determine the initial value
     * @return a new thread local variable
     * @throws NullPointerException if the specified supplier is null
     * @since 1.8
     * 创建线程局部变量
     */
    public static <S> ThreadLocal<S> withInitial(Supplier<? extends S> supplier) {
        return new SuppliedThreadLocal<>(supplier);
    }

    /**
     * Creates a thread local variable.
     * @see #withInitial(java.util.function.Supplier)
     */
    public ThreadLocal() {
    }

内部类

SuppliedThreadLocal

    /**
     * An extension of ThreadLocal that obtains its initial value from
     * the specified {@code 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();
        }
    }

ThreadLocalMap

​ 数据结构仅仅是数组

​ 通过开放地址法来解决hash冲突的问题

​ Entry内部类中的key是弱引用,value是强引用

    /**
     * ThreadLocalMap is a customized hash map suitable only for
     * maintaining thread local values. No operations are exported
     * outside of the ThreadLocal class. The class is package private to
     * allow declaration of fields in class Thread.  To help deal with
     * very large and long-lived usages, the hash table entries use
     * WeakReferences for keys. However, since reference queues are not
     * used, stale entries are guaranteed to be removed only when
     * the table starts running out of space.
     */
    static class ThreadLocalMap {

        /**
         * The entries in this hash map extend WeakReference, using
         * its main ref field as the key (which is always a
         * ThreadLocal object).  Note that null keys (i.e. entry.get()
         * == null) mean that the key is no longer referenced, so the
         * entry can be expunged from table.  Such entries are referred to
         * as "stale entries" in the code that follows.
         */
        static class Entry extends WeakReference<ThreadLocal<?>> {
            /** The value associated with this ThreadLocal. */
            Object value;

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

        /**
         * The initial capacity -- MUST be a power of two.
         */
        private static final int INITIAL_CAPACITY = 16;

        /**
         * The table, resized as necessary.
         * table.length MUST always be a power of two.
         */
        private Entry[] table;

        /**
         * The number of entries in the table.
         */
        private int size = 0;

        /**
         * The next size value at which to resize.
         */
        private int threshold; // Default to 0

        /**
         * Set the resize threshold to maintain at worst a 2/3 load factor.
         * 阈值 = 容量 * 2/3 即负载因子为2/3
         */
        private void setThreshold(int len) {
            threshold = len * 2 / 3;
        }

        /**
         * Increment i modulo len.
         * 从指定的下标i开始,向后获取下一个位置的下标值
         */
        private static int nextIndex(int i, int len) {
            return ((i + 1 < len) ? i + 1 : 0);
        }

        /**
         * Decrement i modulo len.
         * 从指定的下标i开始,前向获取上一个位置的下标值。
         */
        private static int prevIndex(int i, int len) {
            return ((i - 1 >= 0) ? i - 1 : len - 1);
        }

        /**
         * Construct a new map initially containing (firstKey, firstValue).
         * ThreadLocalMaps are constructed lazily, so we only create
         * one when we have at least one entry to put in it.
         *  ThreadLocalMap构造函数
         */
        ThreadLocalMap(ThreadLocal<?> firstKey, Object firstValue) {
            //table数组的默认大小 16
            table = new Entry[INITIAL_CAPACITY];
            //插入数组的下标
            int i = firstKey.threadLocalHashCode & (INITIAL_CAPACITY - 1);
            //创建待插入的entry对象
            table[i] = new Entry(firstKey, firstValue);
            //设置数组table中entry元素的个数为1
            size = 1;
            //设置数组table的阈值
            setThreshold(INITIAL_CAPACITY);
        }

        /**
         * Construct a new map including all Inheritable ThreadLocals
         * from given parent map. Called only by createInheritedMap.
         *
         * @param parentMap the map associated with parent thread.
         */
        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) {
                        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++;
                    }
                }
            }
        }

        /**
         * Get the entry associated with key.  This method
         * itself handles only the fast path: a direct hit of existing
         * key. It otherwise relays to getEntryAfterMiss.  This is
         * designed to maximize performance for direct hits, in part
         * by making this method readily inlinable.
         *
         * @param  key the thread local object
         * @return the entry associated with key, or null if no such
         */
        private Entry getEntry(ThreadLocal<?> key) {
            // 计算entry table索引
            int i = key.threadLocalHashCode & (table.length - 1);
            Entry e = table[i];
            if (e != null && e.get() == key)
                return e;
            else
                // 当entry的值不存在时
                return getEntryAfterMiss(key, i, e);
        }

        /**
         * Version of getEntry method for use when key is not found in
         * its direct hash slot.
         *
         * @param  key the thread local object
         * @param  i the table index for key's hash code
         * @param  e the entry at table[i]
         * @return the entry associated with key, or null if no such
         */
        private Entry getEntryAfterMiss(ThreadLocal<?> key, int i, Entry e) {
            Entry[] tab = table;
            int len = tab.length;

            while (e != null) {
                ThreadLocal<?> k = e.get();
                if (k == key)
                    // 找到entry
                    return e;
                if (k == null)
                    // 移除过期条目
                    expungeStaleEntry(i);
                else
                    // 向下扫描
                    i = nextIndex(i, len);
                e = tab[i];
            }
            return null;
        }

        /**
         * Set the value associated with key.
         *
         * @param key the thread local object
         * @param value the value to be set
         */
        private void set(ThreadLocal<?> key, Object value) {

            // We don't use a fast path as with get() because it is at
            // least as common to use set() to create new entries as
            // it is to replace existing ones, in which case, a fast
            // path would fail more often than not.
            //table数组
            Entry[] tab = table;
            //table数组的长度
            int len = tab.length;
            //计算数组的下标  待插入entry的下标
            int i = key.threadLocalHashCode & (len-1);
            //通过哈希码和数组长度找到数组下标,从i开始往后寻找相等的ThreadLocal对象,没有就下一个index
            for (Entry e = tab[i];
                 e != null;
                 e = tab[i = nextIndex(i, len)]) {
                ThreadLocal<?> k = e.get();
                //如果是相同的ThreadLocal对象,则将value值覆盖更新
                if (k == key) {
                    e.value = value;
                    return;
                }
                //key为null,表示ThreadLocal对象已经被GC回收 (虚引用)
                if (k == null) {
                    //替换待清除的entry,并清除历史key = null 的垃圾数据
                    replaceStaleEntry(key, value, i);
                    return;
                }
            }
            //没有找到 创建新的entry 插入到i的索引位置
            tab[i] = new Entry(key, value);
            //插入完entry后,最新的table数组长度
            int sz = ++size;
            //如果超过阈值,就需要rehash
            if (!cleanSomeSlots(i, sz) && sz >= threshold)
                //对table数组和元素进行整理(扩容等操作)
                rehash();
        }

        /**
         * Remove the entry for key.
         */
        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.get() == key) {
                    //调用entry的clear方法  这里是其父类  抽象类Reference的方法
                    //将弱引用的对象置null,有利于GC回收内存
                    e.clear();
                    //清除陈旧数据
                    expungeStaleEntry(i);
                    return;
                }
            }
        }

        /**
         * Replace a stale entry encountered during a set operation
         * with an entry for the specified key.  The value passed in
         * the value parameter is stored in the entry, whether or not
         * an entry already exists for the specified key.
         *
         * As a side effect, this method expunges all stale entries in the
         * "run" containing the stale entry.  (A run is a sequence of entries
         * between two null slots.)
         *
         * @param  key the key
         * @param  value the value to be associated with key
         * @param  staleSlot index of the first stale entry encountered while
         *         searching for key.
         *                   替换待清除的entry
         */
        private void replaceStaleEntry(ThreadLocal<?> key, Object value,
                                       int staleSlot) {
            Entry[] tab = table;
            int len = tab.length;
            Entry e;

            // Back up to check for prior stale entry in current run.
            // We clean out whole runs at a time to avoid continual
            // incremental rehashing due to garbage collector freeing
            // up refs in bunches (i.e., whenever the collector runs).
            //以slotToExpunge为中轴,向左遍历到的第一个空位置和向右遍历遇到的第一个空位置 之间,
            // 最左的第一个为旧的entry的下标
            int slotToExpunge = staleSlot;
            //从staleSlot位置向前遍历,将旧的entry空间释放
            //如果所有的槽位都被占满了,一直循环,直到有空的位置
            for (int i = prevIndex(staleSlot, len);
                 (e = tab[i]) != null;
                 i = prevIndex(i, len))
                //空位置
                if (e.get() == null)
                    slotToExpunge = i;

            // Find either the key or trailing null slot of run, whichever
            // occurs first
            //从staleSlot位置向后遍历,
            //遇到与key相同的entry时,执行清除操作,返回,不再继续遍历
            for (int i = nextIndex(staleSlot, len);
                 (e = tab[i]) != null;
                 i = nextIndex(i, len)) {
                ThreadLocal<?> k = e.get();

                // If we find key, then we need to swap it
                // with the stale entry to maintain hash table order.
                // The newly stale slot, or any other stale slot
                // encountered above it, can then be sent to expungeStaleEntry
                // to remove or rehash all of the other entries in run.
                //找到相同的key,替换旧的值并且和前面那个过期的对象进行位置交换
                if (k == key) {
                    e.value = value;

                    tab[i] = tab[staleSlot];
                    tab[staleSlot] = e;

                    // Start expunge at preceding stale entry if it exists
                    //向左遍历没有找到旧的entry,将staleSlot位置的entry作为旧的
                    //向右遍历已经与旧的对象进行位置交换,待清理的位置为i
                    if (slotToExpunge == staleSlot)
                        slotToExpunge = i;
                    //清理旧数据
                    cleanSomeSlots(expungeStaleEntry(slotToExpunge), len);
                    return;
                }

                // If we didn't find stale entry on backward scan, the
                // first stale entry seen while scanning for key is the
                // first still present in the run.
                //slotToExpunge == staleSlot 向左遍历找到了空位置,那么会在向右遍历过程中寻找旧的entry
                if (k == null && slotToExpunge == staleSlot)
                    slotToExpunge = i;
            }

            // If key not found, put new entry in stale slot
            //将staleSlot位置的entry的value设置为GC可回收,
            tab[staleSlot].value = null;
            //在staleSlot位置创建一个新的entry
            tab[staleSlot] = new Entry(key, value);

            // If there are any other stale entries in run, expunge them
            //如果有过期的对象,进行清理
            if (slotToExpunge != staleSlot)
                cleanSomeSlots(expungeStaleEntry(slotToExpunge), len);
        }

        /**
         * Expunge a stale entry by rehashing any possibly colliding entries
         * lying between staleSlot and the next null slot.  This also expunges
         * any other stale entries encountered before the trailing null.  See
         * Knuth, Section 6.4
         *
         * @param staleSlot index of slot known to have null key
         * @return the index of the next null slot after staleSlot
         * (all between staleSlot and this slot will have been checked
         * for expunging).
         * 清除陈旧的entry
         */
        private int expungeStaleEntry(int staleSlot) {
            Entry[] tab = table;
            int len = tab.length;

            // expunge entry at staleSlot
            //设置下标staleSlot对应的entry可被GC回收
            //置空value
            tab[staleSlot].value = null;
            //置空entry
            tab[staleSlot] = null;
            //数组元素减1
            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();
                //key为null,清除对应槽位的元素 size减1
                if (k == null) {
                    e.value = null;
                    tab[i] = null;
                    size--;
                } else {
                    //key不为null,后面的元素向前移动,重新获得下标
                    int h = k.threadLocalHashCode & (len - 1);
                    if (h != i) {
                        //如果不在同一个位置,位置发生了改变,就将原来的旧位置entry = null
                        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开始往后遍历,直到找到空节点,插入
                            h = nextIndex(h, len);
                        tab[h] = e;
                    }
                }
            }
            return i;
        }

        /**
         * Heuristically scan some cells looking for stale entries.
         * This is invoked when either a new element is added, or
         * another stale one has been expunged. It performs a
         * logarithmic number of scans, as a balance between no
         * scanning (fast but retains garbage) and a number of scans
         * proportional to number of elements, that would find all
         * garbage but would cause some insertions to take O(n) time.
         *
         * @param i a position known NOT to hold a stale entry. The
         * scan starts at the element after i.
         *
         * @param n scan control: {@code log2(n)} cells are scanned,
         * unless a stale entry is found, in which case
         * {@code log2(table.length)-1} additional cells are scanned.
         * When called from insertions, this parameter is the number
         * of elements, but when from replaceStaleEntry, it is the
         * table length. (Note: all this could be changed to be either
         * more or less aggressive by weighting n instead of just
         * using straight log n. But this version is simple, fast, and
         * seems to work well.)
         *
         * @return true if any stale entries have been removed.
         * 旧的entry是否已被清除
         */
        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);
                }
                //n=n/2
            } while ( (n >>>= 1) != 0);
            return removed;
        }

        /**
         * Re-pack and/or re-size the table. First scan the entire
         * table removing stale entries. If this doesn't sufficiently
         * shrink the size of the table, double the table size.
         * 对table数组和元素进行整理
         */
        private void rehash() {
            //清除表中的陈旧数据
            expungeStaleEntries();

            // Use lower threshold for doubling to avoid hysteresis
            //清理完陈旧数据,如果仍然大于阈值的3/4,就执行扩容,
            if (size >= threshold - threshold / 4)
                //扩容
                resize();
        }

        /**
         * Double the capacity of the table.
         * 扩容  将table扩容2倍,并把老数据重新哈希散列进新的table
         */
        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) {
                        //如果key = null,将value也置为null,有助于GC回收,防止内存泄漏
                        e.value = null; // Help the GC
                    } else {
                        //key不为空
                        //重新hash计算旧entry在新的table数组中的位置
                        int h = k.threadLocalHashCode & (newLen - 1);
                        //如果该位置已经被其他entry占用,向右(后)查找空位 直到找到一个没有使用的位置
                        while (newTab[h] != null)
                            //h递增
                            h = nextIndex(h, newLen);
                        //在找到的第一个空节点塞入e
                        newTab[h] = e;
                        //计数++ 记录保存的元素个数
                        count++;
                    }
                }
            }
            //扩容后,设置新的table数组的阈值   newLen的2/3
            setThreshold(newLen);
            //设置ThreadLocalMap的元素个数
            size = count;
            //将新table赋值给ThreadLocalMap中的entry[] table
            table = newTab;
        }

        /**
         * Expunge all stale entries in the table.
         * 清除表中的陈旧条目
         */
        private void expungeStaleEntries() {
            Entry[] tab = table;
            int len = tab.length;
            //遍历
            for (int j = 0; j < len; j++) {
                Entry e = tab[j];
                //entry不为空,但是entry的key为null
                if (e != null && e.get() == null)
                    //删除该下标对应的陈旧的entry
                    expungeStaleEntry(j);
            }
        }
    }

ThreadLocal、Thread、ThreadLocalMap、Entry之间的关系

image-20230531190833007

每个Thread线程都有一个ThreadLocal,

ThreadLocalMap是ThreadLocal的一个静态内部类

ThreadLocalMap又有一个静态内部类Entry

threadLocalMap实际上有一个以threadLocal实例为key,任意缓存对象为value的Entry对象数组。
当我们为threadLocal变量赋值,实际上就是以当前threadLocal实例为key,值为value的Entry往这个threadLocalMap中存放

一个Thread最多只有一个ThreadLocalMap,ThreadLocalMap底层是一个Entry数组,

但是一个Thread可以有多个ThreadLocal,一个ThreadLocal对应一个变量数据,封装成Entry存到ThreadLocalMap中,所以就有多个Entry。

常用方法

get

image-20230531200415802

    /**
     * Returns the value in the current thread's copy of this
     * thread-local variable.  If the variable has no value for the
     * current thread, it is first initialized to the value returned
     * by an invocation of the {@link #initialValue} method.
     *
     * @return the current thread's value of this thread-local
     */
    public T get() {
        // 获得当前线程
        Thread t = Thread.currentThread();
        // 从当前线程中获得ThreadLocalMap对象
        ThreadLocalMap map = getMap(t);
        if (map != null) {
            // 获得对应entry对象
            ThreadLocalMap.Entry e = map.getEntry(this);
            if (e != null) {
                @SuppressWarnings("unchecked")
                T result = (T)e.value;
                // 不为空,获取value正常返回
                return result;
            }
        }
        // 当ThreadLocalMap不存在时 说明还未初始化,初始化ThreadLocalMap并返回
        return setInitialValue();
    }
  • 获取当前线程Thread对象,进而获取此线程对象中维护的ThreadLocalMap对象。
  • 判断当前的ThreadLocalMap是否存在,如果存在,则以当前的ThreadLocal 为 key,调用ThreadLocalMap中的getEntry方法获取对应的存储实体 e。找到对应的存储实体 e,获取存储实体 e 对应的 value值,即为我们想要的当前线程对应此ThreadLocal的值,返回结果值。
  • 如果不存在,则证明此线程没有维护的ThreadLocalMap对象,调用setInitialValue方法进行初始化。返回setInitialValue初始化的值。

1.3getEntry

        /**
         * Get the entry associated with key.  This method
         * itself handles only the fast path: a direct hit of existing
         * key. It otherwise relays to getEntryAfterMiss.  This is
         * designed to maximize performance for direct hits, in part
         * by making this method readily inlinable.
         *
         * @param  key the thread local object
         * @return the entry associated with key, or null if no such
         */
        private Entry getEntry(ThreadLocal<?> key) {
            // 计算entry table索引
            int i = key.threadLocalHashCode & (table.length - 1);
            Entry e = table[i];
            if (e != null && e.get() == key)
                return e;
            else
                // 当entry的值不存在时
                return getEntryAfterMiss(key, i, e);
        }

getEntryAfterMiss

        /**
         * Version of getEntry method for use when key is not found in
         * its direct hash slot.
         *
         * @param  key the thread local object
         * @param  i the table index for key's hash code
         * @param  e the entry at table[i]
         * @return the entry associated with key, or null if no such
         */
        private Entry getEntryAfterMiss(ThreadLocal<?> key, int i, Entry e) {
            Entry[] tab = table;
            int len = tab.length;

            while (e != null) {
                ThreadLocal<?> k = e.get();
                if (k == key)
                    // 找到entry
                    return e;
                if (k == null)
                    // 移除过期条目
                    expungeStaleEntry(i);
                else
                    // 向下扫描
                    i = nextIndex(i, len);
                e = tab[i];
            }
            return null;
        }
expungeStaleEntry
        /**
         * Expunge a stale entry by rehashing any possibly colliding entries
         * lying between staleSlot and the next null slot.  This also expunges
         * any other stale entries encountered before the trailing null.  See
         * Knuth, Section 6.4
         *
         * @param staleSlot index of slot known to have null key
         * @return the index of the next null slot after staleSlot
         * (all between staleSlot and this slot will have been checked
         * for expunging).
         * 清除陈旧的entry
         */
        private int expungeStaleEntry(int staleSlot) {
            Entry[] tab = table;
            int len = tab.length;

            // expunge entry at staleSlot
            //设置下标staleSlot对应的entry可被GC回收
            //置空value
            tab[staleSlot].value = null;
            //置空entry
            tab[staleSlot] = null;
            //数组元素减1
            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();
                //key为null,清除对应槽位的元素 size减1
                if (k == null) {
                    e.value = null;
                    tab[i] = null;
                    size--;
                } else {
                    //key不为null,后面的元素向前移动,重新获得下标
                    int h = k.threadLocalHashCode & (len - 1);
                    if (h != i) {
                        //如果不在同一个位置,位置发生了改变,就将原来的旧位置entry = null
                        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开始往后遍历,直到找到空节点,插入
                            h = nextIndex(h, len);
                        tab[h] = e;
                    }
                }
            }
            return i;
        }

1.4setInitialValue

    /**
     * Variant of set() to establish initialValue. Used instead
     * of set() in case user has overridden the set() method.
     *
     * @return the initial value
     * 设置初始化值
     */
    private T setInitialValue() {
        //该方法默认返回null,用户可以自定义
        T value = initialValue();
        Thread t = Thread.currentThread();
        ThreadLocalMap map = getMap(t);
        if (map != null)
            //如果map不为null,设置初始值value
            map.set(this, value);
        else
            //如果map为null,则创建一个map,设置初始化value
            createMap(t, value);
        return value;
    }

1.4.5createMap

    /**
     * Create the map associated with a ThreadLocal. Overridden in
     * InheritableThreadLocal.
     *
     * @param t the current thread
     * @param firstValue value for the initial entry of the map
     *     创建ThreadLocalMap并赋值
     */
    void createMap(Thread t, T firstValue) {
        t.threadLocals = new ThreadLocalMap(this, firstValue);
    }
ThreadLocalMap
        /**
         * Construct a new map initially containing (firstKey, firstValue).
         * ThreadLocalMaps are constructed lazily, so we only create
         * one when we have at least one entry to put in it.
         *  ThreadLocalMap构造函数
         */
        ThreadLocalMap(ThreadLocal<?> firstKey, Object firstValue) {
            //table数组的默认大小 16
            table = new Entry[INITIAL_CAPACITY];
            //插入数组的下标
            int i = firstKey.threadLocalHashCode & (INITIAL_CAPACITY - 1);
            //创建待插入的entry对象
            table[i] = new Entry(firstKey, firstValue);
            //设置数组table中entry元素的个数为1
            size = 1;
            //设置数组table的阈值
            setThreshold(INITIAL_CAPACITY);
        }
setThreshold
        /**
         * Set the resize threshold to maintain at worst a 2/3 load factor.
         * 阈值 = 容量 * 2/3 即负载因子为2/3
         */
        private void setThreshold(int len) {
            threshold = len * 2 / 3;
        }

set

image-20230531200602746

    /**
     * Sets the current thread's copy of this thread-local variable
     * to the specified value.  Most subclasses will have no need to
     * override this method, relying solely on the {@link #initialValue}
     * method to set the values of thread-locals.
     *
     * @param value the value to be stored in the current thread's copy of
     *        this thread-local.
     */
    public void set(T value) {
        Thread t = Thread.currentThread();
        ThreadLocalMap map = getMap(t);
        if (map != null)
            //调用ThreadLocalMap的set方法 赋值
            map.set(this, value);
        else
            //map为空,创建新的ThreadLocalMap
            createMap(t, value);
    }
  • 获取当前线程Thread对象,进而获取此线程对象中维护的ThreadLocalMap对象。
  • 判断当前的ThreadLocalMap是否存在:
  • 如果存在,则调用map.set设置此实体entry。
  • 如果不存在,则调用createMap进行ThreadLocalMap对象的初始化,并将此实体entry作为第一个值存放至ThreadLocalMap中。

1.3set

ThreadLocalMap.java

        /**
         * Set the value associated with key.
         *
         * @param key the thread local object
         * @param value the value to be set
         */
        private void set(ThreadLocal<?> key, Object value) {

            // We don't use a fast path as with get() because it is at
            // least as common to use set() to create new entries as
            // it is to replace existing ones, in which case, a fast
            // path would fail more often than not.
            //table数组
            Entry[] tab = table;
            //table数组的长度
            int len = tab.length;
            //计算数组的下标  待插入entry的下标
            int i = key.threadLocalHashCode & (len-1);
            //通过哈希码和数组长度找到数组下标,从i开始往后寻找相等的ThreadLocal对象,没有就下一个index
            for (Entry e = tab[i];
                 e != null;
                 e = tab[i = nextIndex(i, len)]) {
                ThreadLocal<?> k = e.get();
                //如果是相同的ThreadLocal对象,则将value值覆盖更新
                if (k == key) {
                    e.value = value;
                    return;
                }
                //key为null,表示ThreadLocal对象已经被GC回收 (虚引用)
                if (k == null) {
                    //替换待清除的entry,并清除历史key = null 的垃圾数据
                    replaceStaleEntry(key, value, i);
                    return;
                }
            }
            //没有找到 创建新的entry 插入到i的索引位置
            tab[i] = new Entry(key, value);
            //插入完entry后,最新的table数组长度
            int sz = ++size;
            //如果超过阈值,就需要rehash
            if (!cleanSomeSlots(i, sz) && sz >= threshold)
                //对table数组和元素进行整理(扩容等操作)
                rehash();
        }

replaceStaleEntry

        /**
         * Replace a stale entry encountered during a set operation
         * with an entry for the specified key.  The value passed in
         * the value parameter is stored in the entry, whether or not
         * an entry already exists for the specified key.
         *
         * As a side effect, this method expunges all stale entries in the
         * "run" containing the stale entry.  (A run is a sequence of entries
         * between two null slots.)
         *
         * @param  key the key
         * @param  value the value to be associated with key
         * @param  staleSlot index of the first stale entry encountered while
         *         searching for key.
         *                   替换待清除的entry
         */
        private void replaceStaleEntry(ThreadLocal<?> key, Object value,
                                       int staleSlot) {
            Entry[] tab = table;
            int len = tab.length;
            Entry e;

            // Back up to check for prior stale entry in current run.
            // We clean out whole runs at a time to avoid continual
            // incremental rehashing due to garbage collector freeing
            // up refs in bunches (i.e., whenever the collector runs).
            //以slotToExpunge为中轴,向左遍历到的第一个空位置和向右遍历遇到的第一个空位置 之间,
            // 最左的第一个为旧的entry的下标
            int slotToExpunge = staleSlot;
            //从staleSlot位置向前遍历,将旧的entry空间释放
            //如果所有的槽位都被占满了,一直循环,直到有空的位置
            for (int i = prevIndex(staleSlot, len);
                 (e = tab[i]) != null;
                 i = prevIndex(i, len))
                //空位置
                if (e.get() == null)
                    slotToExpunge = i;

            // Find either the key or trailing null slot of run, whichever
            // occurs first
            //从staleSlot位置向后遍历,
            //遇到与key相同的entry时,执行清除操作,返回,不再继续遍历
            for (int i = nextIndex(staleSlot, len);
                 (e = tab[i]) != null;
                 i = nextIndex(i, len)) {
                ThreadLocal<?> k = e.get();

                // If we find key, then we need to swap it
                // with the stale entry to maintain hash table order.
                // The newly stale slot, or any other stale slot
                // encountered above it, can then be sent to expungeStaleEntry
                // to remove or rehash all of the other entries in run.
                //找到相同的key,替换旧的值并且和前面那个过期的对象进行位置交换
                if (k == key) {
                    e.value = value;

                    tab[i] = tab[staleSlot];
                    tab[staleSlot] = e;

                    // Start expunge at preceding stale entry if it exists
                    //向左遍历没有找到旧的entry,将staleSlot位置的entry作为旧的
                    //向右遍历已经与旧的对象进行位置交换,待清理的位置为i
                    if (slotToExpunge == staleSlot)
                        slotToExpunge = i;
                    //清理旧数据
                    cleanSomeSlots(expungeStaleEntry(slotToExpunge), len);
                    return;
                }

                // If we didn't find stale entry on backward scan, the
                // first stale entry seen while scanning for key is the
                // first still present in the run.
                //slotToExpunge == staleSlot 向左遍历找到了空位置,那么会在向右遍历过程中寻找旧的entry
                if (k == null && slotToExpunge == staleSlot)
                    slotToExpunge = i;
            }

            // If key not found, put new entry in stale slot
            //将staleSlot位置的entry的value设置为GC可回收,
            tab[staleSlot].value = null;
            //在staleSlot位置创建一个新的entry
            tab[staleSlot] = new Entry(key, value);

            // If there are any other stale entries in run, expunge them
            //如果有过期的对象,进行清理
            if (slotToExpunge != staleSlot)
                cleanSomeSlots(expungeStaleEntry(slotToExpunge), len);
        }
nextIndex和prevIndex
        /**
         * Increment i modulo len.
         * 从指定的下标i开始,向后获取下一个位置的下标值
         */
        private static int nextIndex(int i, int len) {
            return ((i + 1 < len) ? i + 1 : 0);
        }

        /**
         * Decrement i modulo len.
         * 从指定的下标i开始,前向获取上一个位置的下标值。
         */
        private static int prevIndex(int i, int len) {
            return ((i - 1 >= 0) ? i - 1 : len - 1);
        }
cleanSomeSlots
        /**
         * Heuristically scan some cells looking for stale entries.
         * This is invoked when either a new element is added, or
         * another stale one has been expunged. It performs a
         * logarithmic number of scans, as a balance between no
         * scanning (fast but retains garbage) and a number of scans
         * proportional to number of elements, that would find all
         * garbage but would cause some insertions to take O(n) time.
         *
         * @param i a position known NOT to hold a stale entry. The
         * scan starts at the element after i.
         *
         * @param n scan control: {@code log2(n)} cells are scanned,
         * unless a stale entry is found, in which case
         * {@code log2(table.length)-1} additional cells are scanned.
         * When called from insertions, this parameter is the number
         * of elements, but when from replaceStaleEntry, it is the
         * table length. (Note: all this could be changed to be either
         * more or less aggressive by weighting n instead of just
         * using straight log n. But this version is simple, fast, and
         * seems to work well.)
         *
         * @return true if any stale entries have been removed.
         * 旧的entry是否已被清除
         */
        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);
                }
                //n=n/2
            } while ( (n >>>= 1) != 0);
            return removed;
        }
rehash
        /**
         * Re-pack and/or re-size the table. First scan the entire
         * table removing stale entries. If this doesn't sufficiently
         * shrink the size of the table, double the table size.
         * 对table数组和元素进行整理
         */
        private void rehash() {
            //清除表中的陈旧数据
            expungeStaleEntries();

            // Use lower threshold for doubling to avoid hysteresis
            //清理完陈旧数据,如果仍然大于阈值的3/4,就执行扩容,
            if (size >= threshold - threshold / 4)
                //扩容
                resize();
        }
expungeStaleEntries
        /**
         * Expunge all stale entries in the table.
         * 清除表中的陈旧条目
         */
        private void expungeStaleEntries() {
            Entry[] tab = table;
            int len = tab.length;
            //遍历
            for (int j = 0; j < len; j++) {
                Entry e = tab[j];
                //entry不为空,但是entry的key为null
                if (e != null && e.get() == null)
                    //删除该下标对应的陈旧的entry
                    expungeStaleEntry(j);
            }
        }
resize
        /**
         * Double the capacity of the table.
         * 扩容  将table扩容2倍,并把老数据重新哈希散列进新的table
         */
        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) {
                        //如果key = null,将value也置为null,有助于GC回收,防止内存泄漏
                        e.value = null; // Help the GC
                    } else {
                        //key不为空
                        //重新hash计算旧entry在新的table数组中的位置
                        int h = k.threadLocalHashCode & (newLen - 1);
                        //如果该位置已经被其他entry占用,向右(后)查找空位 直到找到一个没有使用的位置
                        while (newTab[h] != null)
                            //h递增
                            h = nextIndex(h, newLen);
                        //在找到的第一个空节点塞入e
                        newTab[h] = e;
                        //计数++ 记录保存的元素个数
                        count++;
                    }
                }
            }
            //扩容后,设置新的table数组的阈值   newLen的2/3
            setThreshold(newLen);
            //设置ThreadLocalMap的元素个数
            size = count;
            //将新table赋值给ThreadLocalMap中的entry[] table
            table = newTab;
        }

remove

image-20230531200732361

    /**
     * Removes the current thread's value for this thread-local
     * variable.  If this thread-local variable is subsequently
     * {@linkplain #get read} by the current thread, its value will be
     * reinitialized by invoking its {@link #initialValue} method,
     * unless its value is {@linkplain #set set} by the current thread
     * in the interim.  This may result in multiple invocations of the
     * {@code initialValue} method in the current thread.
     *
     * @since 1.5
     */
     public void remove() {
         ThreadLocalMap m = getMap(Thread.currentThread());
         if (m != null)
             //调用ThreadLocalMap的remove删除
             m.remove(this);
     }

1.3remove

ThreadLocalMap.java

        /**
         * Remove the entry for key.
         */
        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.get() == key) {
                    //调用entry的clear方法  这里是其父类  抽象类Reference的方法
                    //将弱引用的对象置null,有利于GC回收内存
                    e.clear();
                    //清除陈旧数据
                    expungeStaleEntry(i);
                    return;
                }
            }
        }

答疑

ThreadLocal内存泄漏问题

为什么会出现内存泄漏

image-20210908111735622

ThreadLocalMap从字面上就可以看出这是一个保存ThreadLocal对象的map(其实是以它为Key),不过是经过了两层包装的ThreadLocal对象:
(1)第一层包装是使用 WeakReference<ThreadLocal<?>> 将ThreadLocal对象变成一个弱引用的对象; (2)第二层包装是定义了一个专门的类 Entry 来扩展 WeakReference

每个Thread对象维护着一个ThreadLocalMap的强引用
ThreadLocalMap是ThreadLocal的内部类,用Entry来进行存储
调用ThreadLocal的set()方法时,实际上就是往ThreadLocalMap设置值,key是ThreadLocal对象,值Value是传递进来的对象
调用ThreadLocal的get()方法时,实际上就是从ThreadLocalMap获取值,key是ThreadLocal对象
ThreadLocal本身并不存储值,它只是自己作为一个key来让线程从ThreadLocalMap获取value,正因为这个原理,所以ThreadLocal能够实现“数据隔离”,获取当前线程的局部变量值,不受其他线程影响

为什么要用弱引用?不用如何?

public void function01()
{
    ThreadLocal tl = new ThreadLocal<Integer>();    //line1
    tl.set(2021);                                   //line2
    tl.get();                                       //line3
}

line1新建了一个ThreadLocal对象,t1 是强引用指向这个对象;
line2调用set()方法后新建一个Entry,通过源码可知Entry对象里的k是弱引用指向这个对象。

image-20210908152746626

为什么源代码用弱引用?
当function01方法执行完毕后,栈帧销毁强引用 tl 也就没有了。但此时线程的ThreadLocalMap里某个entry的key引用还指向这个对象
若这个key引用是强引用,就会导致key指向的ThreadLocal对象及v指向的对象不能被gc回收,造成内存泄漏;
若这个key引用是弱引用就大概率会减少内存泄漏的问题(还有一个key为null的问题)。

使用弱引用,就可以使ThreadLocal对象在方法执行完毕后顺利被回收 ,此时Entry的key引用就指向为null。

因为map是允许存在空key的,那如何回收这些entry呢?

此后我们调用get,set或remove方法时,就会尝试删除key为null的entry,可以释放value对象所占用的内存。

总结

根据源码,我们可以发现 get,set或remove方法都可能触发清理方法expungeStaleEntry(),所以正常情况下是不会有内存泄漏的。

但是如果我们没有调用get和set的时候就会可能面临着内存泄漏。

退一步说,就算我们没有调用get和set和remove方法,线程结束的时候,也就没有强引用再指向ThreadLocal中的ThreadLocalMap了,这样ThreadLocalMap和里面的元素也会被回收掉。

但是有一种危险是,如果线程是线程池的,在线程执行完代码的时候并没有结束,只是归还给线程池,这个时候ThreadLocalMap和里面的元素是不会回收掉的,可能导致内存泄漏。

养成好习惯在使用的时候调用remove(),加快垃圾回收,避免内存泄漏。

参考: ThreadLocal源码
github:ThreadLocal源码

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

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

相关文章

过来人建议:强烈安利本科生都去学Java编程!现在转行还不晚!

为什么标题这么说呢&#xff0c;因为Java的工资香啊&#xff0c;刚入门的小白就有 1w&#xff0c;转正后更多&#xff0c;而且越老越吃香&#xff01; 我们学一门技术在身&#xff0c;走到哪里都不怕&#xff0c;java是技术岗&#xff0c;不带销售性质&#xff0c;加班要分公司…

unity与oculus quest开发设置流程

目录 预准备quest项目的构建PC运行参考文章 版本&#xff1a; unity&#xff1a;2021 adb&#xff08;保证用USB连接PC和头显的时候能允许调试和数据访问&#xff09; macOS 当USB连接之后&#xff0c;需要带上头显将是否允许数据调试和访问的对话框选择允许。 文件中assets放…

2.6 Hello World 及简单语法规则

2.6 Hello World 及简单语法规则 新建文件夹&#xff0c;用于存放代码 新建一个Java文件 早期我们只是会建立txt文本文件&#xff0c;或者word&#xff0c;Java文件的后缀名是.java 新建Hello.java文件 // 输入Java代码public class Hello{public static void main(String[…

Vue3 小兔鲜:Pinia入门

Vue3 小兔鲜&#xff1a;Pinia入门 Date: May 11, 2023 Sum: Pinia概念、实现counter、getters、异步action、storeToRefs保持响应式解构 什么是Pinia Pinia 是 Vue 的专属状态管理库&#xff0c;可以实现跨组件或页面共享状态&#xff0c;是 vuex 状态管理工具的替代品&…

PHP7 连接数据库 MySQL8.0 报错 Call to undefined function mysqli_connect() 的解决方法

前提 如题所示使用的是PHP7的版本&#xff0c;连接MYSQL8.0数据库遇到的问题&#xff0c;提供我个人的解决办法。 我的报错是&#xff1a;Call to undefined function mysqli_connect() 首先明确 保证PHP与MySQL可以正常使用 访问localhost:80【或者你设置的端口号】是有页面…

shell编程之SNAT与DNAT的应用

SNAT与DNAT的应用 一、SNAT的介绍1.SNAT概述2.SNAT源地址转换过程二、SNAT转换 三、DNAT的介绍1.DNAT概述2.DNAT转换前提条件 四、DNAT的转换五、防火墙规则的备份和还原六、tcpdump抓包工具的运用 一、SNAT的介绍 SNAT&#xff08;SNAT&#xff09;一般指源地址转换 1.SNAT概…

VMware安装Centos7图形化GUI系统全过程

1、打开vmware&#xff0c;点击文件然后新建虚拟机 2、然后自定义直接下一步 3、下一步 4、这里我们稍后安装操作系统&#xff0c;继续下一步 5、随后选择Centos7 64位&#xff0c;继续下一步 6、选择你所需要安装的虚拟机存放的位置&#xff0c;虚拟机名字看自己来设置&#x…

docker-安装minio集群

目录 1.服务器列表 2.NTP时间同步 1.服务端 minio_01配置 2.客户端配置-minio_02、minio_03、minio_04配置 3.Minio集群 1.在对应的主机执行对应的语句 2.创建文件夹 3.创建容器 4.访问 5.查看集群 4.nginx代理集群192.168.1.3 1.nginx安装 2.访问代理 1.服务器列…

[web]关于过滤器Filter

前言 为了避免在没有登录的情况下也可以通过路径访问到所有页面&#xff0c; 如果在每一个需要请求的资源里面都通过Session去判断有没有登录的用户对象就需要书写大量的判断代码&#xff0c;此时就可以用到Filter过滤器 作用 让请求的某些资源在之前或之后经过过滤器&#xff…

uni-app路由配置使用和页面跳转传参

uni-app路由配置使用和页面跳转传参 uni-app路由配置使用和页面跳转传参 文章目录 uni-app路由配置使用和页面跳转传参前言一、组件式路由跳转传参二、函数式路由跳转传参总结 前言 UNI-APP学习系列之路由配置使用和页面跳转传参 一、组件式路由跳转传参 组件式路由跳转 示例…

Java设计模式(一)

系列文章目录 单一职责原则 接口隔离原则 依赖倒转原则 文章目录 系列文章目录前言一、单一职责原则1.单一职责原则注意事项和细节 二、接口隔离原则1.接口隔离原则基本介绍(Interface Segregation Principle)2.应传统方法的问题和使用接口隔离原则改进 三、依赖倒转原则1.依赖…

【Linux】——调试器-gdb的使用

序言&#xff1a; 本期&#xff0c;我将带领大家学习的关于linux下的调试器gdb的使用&#xff0c;废话不多说跟着我一起去看看吧&#xff01;&#xff01; 目录 前言 &#xff08;一&#xff09;背景介绍 1、debug模式和release模式 2、为什么Release不能调试但DeBug可以调…

nDreams CEO:是时候打破市场对VR游戏的错误认知了

自Quest系列头显问世以来&#xff0c;VR游戏市场仿佛被注入了一剂强心针&#xff0c;发展速度远超Rift时代。从Quest 1到现在&#xff0c;已经过去4年左右&#xff0c;现在VR游戏市场又走到了哪呢&#xff1f;在GDC 2023上&#xff0c;资深VR游戏工作室和发行商nDreams公布了一…

django-vue-admin-pro 使用

地址&#xff1a; GitHub - dvadmin-pro/django-vue-admin-pro 一、准备工作 Python > 3.8.0 (推荐3.9版本) nodejs > 14.0 (推荐最新) Mysql > 5.7.0 (可选&#xff0c;默认数据库sqlite3&#xff0c;推荐8.0版本) Redis(可选&#xff0c;最新版)项目运行及部署 |…

Android crash 流程详解(一):JE

源码基于&#xff1a;Android R 0. 前言 App crash(全称为 Application crash)&#xff0c;又分 java crash 和 native crash&#xff0c;又称 java layer exception(JE) 和 native layer exception(NE)。对于 crash 在开发过程中或多或少都会遇到&#xff0c;本文将整理总结 …

Shell脚本攻略:shell函数应用

目录 一、理论 1.shell函数 2.函数传参 3.函数变量的作用范围 4.递归 5.函数位置变量与脚本位置变量区别 6.创建库 二、实验 1.实验一 一、理论 1.shell函数 &#xff08;1&#xff09;概念 将命令序列按格式写在一起&#xff0c;可方便重复使用命令序列。 ① 避免…

JetBrains的Java集成开发环境IntelliJ 2023版本在Win10系统的下载与安装配置教程

目录 前言一、IntelliJ 安装二、使用配置总结 前言 IntelliJ IDEA Ultimate是一款功能强大的Java集成开发环境&#xff08;IDE&#xff09;。它提供了丰富的功能和工具&#xff0c;可以帮助开发人员更高效地编写、调试和部署Java应用程序。 IntelliJ IDEA Ultimate的主要特点…

Benewake(北醒) 快速实现TFmini-Plus-IIC与电脑通信的操作说明

目录 1. 概述2. 测试准备2.1 工具准备2.2通讯协议转换 3. IIC通讯测试3.1 引脚说明3.2 测试步骤3.2.1 TFmini-Plus-IIC 与 PC 建立连接3.2.2 获取测距值3.2.3 更改 slave 地址 1. 概述 通过本文档的概述&#xff0c;能够让初次使用测试者快速了解测试 IIC 通信协议需要的工具以…

48. 旋转图像

48. 旋转图像 C代码&#xff1a; void rotate(int** matrix, int matrixSize, int* matrixColSize){int m matrixSize;int n matrixColSize[0];int arr[m*n];int arrTop 0;memset(arr, 0, sizeof(int) * m * n);for (int i 0; i < m; i) {for (int j 0; j < n; j) …

【Springcloud】分布式搜索elasticsearch

文章目录 1、ElasticSearch 1、ElasticSearch 先看下翻译&#xff1a; elasticsearch是一款非常强大的开源搜索引擎&#xff0c;可以帮助我们从海量数据中快速找到需要的内容 项目在运行的时候会产生海量的日志信息&#xff0c;而elasticsearch结合kibana、Logstash、Beats&am…