您当前的位置:首页 > 电脑百科 > 程序开发 > 编程百科

ThreadLocal:多线程环境下的神秘武器

时间:2023-10-19 13:25:46  来源:微信公众号  作者:码农本农

ThreadLocal是一个线程安全的,以线程为单位的数据传递工具。广泛应用于多层级数据传递。

1应用场景

ThreadLocal主要功能是跨层传递参数,比如,Controller层的数据需要在业务逻辑层使用时,除了利用方法的参数传递之外还可以使用ThreadLocal传递。

有时候我们需要从上层传递一个参数到下层的方法,但是下层的方法新增一个参数的话,会违背开闭原则,如果依赖此方法的上层比较多,那修改此方法必然会牵扯很多其他的代码也要改动(代码中难免会遇到这种不合理的代码)因此我们可以通过ThreadLocal来传递这个参数

另外,ThreadLocal在源码中经常被应用,例如,Spring MVC的RequestContextHolder的实现就是使用了ThreadLocal,cglib动态代理中也应用了ThreadLocal等等。

2基础应用

public final class OperationInfoRecorder {

private static final ThreadLocal<OperationInfoDTO> THREAD_LOCAL = new ThreadLocal<>();

    private OperationInfoRecorder() {
    }
    
    public static OperationInfoDTO get() {
        return THREAD_LOCAL.get();
    }
    
    public static void set(OperationInfoDTO operationInfoDTO) {
        THREAD_LOCAL.set(operationInfoDTO);
    }
    
    public static void remove() {
        THREAD_LOCAL.remove();
    }
    
}

//使用
OperationInfoRecorder.set(operationInfoDTO)
OperationInfoRecorder.get()

日常的代码书写中需要注意两点:

  • static确保全局只有一个保存OperationInfoDTO对象的ThreadLocal实例,并且可避免内存泄露;

  • final确保ThreadLocal的实例不可更改。防止被意外改变,导致放入的值和取出来的不一致。

3架构设计

先来看看ThreadLocal设计的巧妙之处,通过一段源码深入了解

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

跟到这里发现获取当前线程,当前线程参与进来了,进入createMap方法

void createMap(Thread t, T firstValue) {
      t.threadLocals = new ThreadLocalMap(this, firstValue);
}

此处实际上就是创建了一个ThreadLocalMap对象,赋值给当前线程的threadLocals属性。

我们去到Thread类中看看这个属性到底是什么

public class Thread implements Runnable {

ThreadLocal.ThreadLocalMap threadLocals = null;

ThreadLocal.ThreadLocalMap inheritableThreadLocals = null;

}

可见每个线程对象中都有两个属性,这两个属性都是ThreadLocalMap类型。

看到这里不难想象,ThreadLocal对外声称的数据线程隔离不过是把数据保存到了当前线程对象里面,自然是线程隔离以及线程安全了。

4数据结构

那么ThreadLocalMap和ThreadLocal是什么关系呢?

ThreadLocal:多线程环境下的神秘武器

如图:

ThreadLocalMap内部有一个Entry数组,这个数组中的每个元素都是一个key-value键值对,value是要存储的值,key是通过WeakReference包装的ThreadLocal对象的弱引用,弱引用会在每次垃圾回收的时候被回收。

在代码结构上ThreadLocalMap是ThreadLocal的静态内部类,真正负责存储数据的是ThreadLocalMap。

在应用上,ThreadLocal为ThreadLocalMap提供了对外访问的api,包括set,get,remove。同时ThreadLocal对象的引用又作为ThreadLocalMap中Entry元素的key。

既然是数组,插入数据的时候是怎么解决hash冲突呢?

ThreadLocalMap采用开放寻址法插入数据。就是如果发现hash冲突,就依次向后面的寻找一个空桶,直到找到为止,然后插入进去。

那么为什么使用开地址法?而不是像hash表一样使用链表法呢?

在开放寻址法中,所有的数据都存储在一个数组中,比起链表法来说,冲突的代价更高。所以,使用开放寻址法解决冲突的散列表,装载因子的上限不能太大。这也导致这种方法比链表法更浪费内存空间。但是反过看,链表法指针需要额外的空间,故当结点规模较小时,开放寻址法较为节省空间,而若将节省的指针空间用来扩大散列表的规模,可使装填因子变小,这又减少了开放寻址法中的冲突,从而提高平均查找速度。并且使用中很少有大量ThreadLocal对象的场景。

5源码解析

set方法解析

1.第一次set数据

public void set(T value) {
        Thread t = Thread.currentThread();
        ThreadLocalMap map = getMap(t);
        if (map != null)
            map.set(this, value);
        else
            createMap(t, value);
    }
    
    void createMap(Thread t, T firstValue) {
        t.threadLocals = new ThreadLocalMap(this, firstValue);
    }
    
    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);
        }
        
        

第一次set数据比较简单,线程中尚未初始化ThreadLocalMap,需要先初始化,初始化步骤如下:

  1. 声明数组
  2. 计算下标
  3. 给对应数组下标赋值
  4. 设置当前数组长度size
  5. 数组长度计算扩容因子Threshold

1.非第一次set数据

private void set(ThreadLocal<?> key, Object value) {

            Entry[] tab = table;
            int len = tab.length;
            int i = key.threadLocalHashCode & (len-1);

            for (Entry e = tab[i];
                 e != null;
                 e = tab[i = nextIndex(i, len)]) {
                ThreadLocal<?> k = e.get();

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

                if (k == null) {
                    replaceStaleEntry(key, value, i);
                    return;
                }
            }

            tab[i] = new Entry(key, value);
            int sz = ++size;
            if (!cleanSomeSlots(i, sz) && sz >= threshold)
                rehash();
        }

上面的代码步骤如下:

  1. 计算下标

  2. 如果当前下标无数据,直接进入4。

  3. 如果当前下标有数据,则从当前下标开始向后遍历,每遍历一次,i++

    3.1  如果当前下标桶中的Entry对象的k和需要保存的key相同,直接更新,结束

    3.2  如果当前下标桶中的Entry对象的k和需要保存的key不相同,且k不为空,不处理

    3.3  如果当前下标桶中的Entry对象的k为空,说明当前Entry对象已经失效无用,需要进行进一步处理

    3.4  进入replaceStaleEntry方法,结束

  4. 如果到现在没有结束方法,则创建Entry赋值给下标i对应的桶,注意这里的i不一定是最开始值了。

 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();
                if (k == key) {
                    e.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);
        }

replaceStaleEntry方法是对set过程中遇到的失效Entry做进一步处理,replaceStaleEntry代码中执行步骤如下:

1. 从当前下标为staleSlot的地方向左遍历,直到找到第一个空桶停止遍历,此时slotToExpunge=staleSlot,或者直到找到第一个非空桶且Entry对象的key为空为止,此时slotToExpunge为当前桶下标。此处可能说的有点绕,但是相信自己看代码就能明白。

2. 从当前下标为staleSlot的地方向右遍历,此遍历的目的是为了查看右侧是否存在key相同的Entry,如果有,就更新value,并且和staleSlot下标对应的桶中的失效Entry交换位置,如果没有就直接更新staleSlot下标的桶。

这里为什么不直接更新staleSlot下标对应的桶呢?

因为Entry数组插入的时候如果遇到hash冲突(即两个key计算出的下标相同),直接是依次插到后面一个空桶,如果再后来的数据插入的时候发现对应下标的桶已经被占用,这种情况也是向后一个空桶插入。因此可以知道,不直接更新而是向后遍历查看key是否相等,就类似于hash表插入的时候发生hash冲突后对链表的遍历查找。只不过多了一个为止交换。

3. 每一次插入完成,就要执行expungeStaleEntry方法和cleanSomeSlots方法,这个两个方法都是失效清理方法。

expungeStaleEntry方法为探测式清理,从给定开始的下标开始向右遍历,直到第一个空桶为止

private int expungeStaleEntry(int staleSlot) {
            Entry[] tab = table;
            int len = tab.length;
            tab[staleSlot].value = null;
            tab[staleSlot] = null;
            size--;
            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;
        }

还记得这个变量吗slotToExpunge,这个变量的值是向左遍历得到的第一个Entry失效的桶的下标。

此方法做的事情就是从这个下标开始向右把失效的Entry全部清除,而把没有失效的Entry重新计算下标,重新按照开放地址法放到数组中。直到第一个空桶停止遍历。并且把当前遍历到的桶的下标返回。

我们先来总结下这个过程的几个关键点

  • 向左遍历到第一个空桶的位置。

  • 向右遍历的过程中清除失效Entry,重hash有效Entry,直到遍历到第一个空桶为止。

那么为什么这么做呢?

首先,之所以只操作两个空桶之间的元素,是因为两个空桶之间的元素都和当前key计算的下标有关系(有可能是hash冲突造成的临近元素),操作这一部分数据可以保证与当前key相关的元素都能得到失效处理。

然后就是小范围的失效操作,避免大量数据参与,可以提高性能。

最后是可以使得rehash后的数据距离正确的位置更近一些,能提高整个散列表的查询性能。

同时这个方法会在set,get,remove,resize方法中反复使用,因此不能大规模扫描。

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;
        }

cleanSomeSlots方法为启发式清理,从给定开始的下标开始向右遍历log2n个位置,对遍历过程中失效元素调用expungeStaleEntry方法,目的也是在不影响性能的基础上尽可能的多的把失效的元素清除。

get方法解析

 public T get() {
        Thread t = Thread.currentThread();
        ThreadLocalMap map = getMap(t);
        if (map != null) {
            ThreadLocalMap.Entry e = map.getEntry(this);
            if (e != null) {
                @SuppressWarnings("unchecked")
                T result = (T)e.value;
                return result;
            }
        }
        return setInitialValue();
    }
    
 private Entry getEntry(ThreadLocal<?> key) {
            int i = key.threadLocalHashCode & (table.length - 1);
            Entry e = table[i];
            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;

            while (e != null) {
                ThreadLocal<?> k = e.get();
                if (k == key)
                    return e;
                if (k == null)
                    expungeStaleEntry(i);
                else
                    i = nextIndex(i, len);
                e = tab[i];
            }
            return null;
        }   
    

get方法要比set方法简单,逻辑步骤如下

  1. 计算下标,通过下标获取元素
  2. 对比下标对应桶中元素的key和要查询k是否相等,如果相等直接返回
  3. 如果key不相等,就会走这个getEntryAfterMiss方法

getEntryAfterMiss方法就是从当前坐标开始向后检查key是否相等,相等的直接返回,如果失效,就调用expungeStaleEntry做失效处理,如果没有找到就返回null。

remove方法解析

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) {
                    e.clear();
                    expungeStaleEntry(i);
                    return;
                }
            }
        }

remove方法就更加简单了,遍历找到key相等的元素,进行删除,顺便在当前坐标位置开始调用expungeStaleEntry进行失效处理

扩容解析

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

            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;
        }

扩容机制也比较简单,在扩容前会先调用expungeStaleEntry进行一次失效处理,这此失效处理是在坐标0开始,失效处理结束后如果size >= threshold - threshold / 4,那就进行扩容

扩容步骤

  1. 声明新的数组,是原来数据的2倍
  2. 遍历原来的数组,对元素进行重hash计算下标,然后放入新的数组中
  3. 遍历过程中如果遇到失效的元素,value置为空
  4. 重置size,重置table,重新计算扩容因子threshold,(len * 2 / 3)

6ThreadLocal的问题

内存泄露

在ThreadLocalMap中使用WeakReference包装后的ThreadLocal对象作为key,也就是说这里对ThreadLocal对象为弱引用。当ThreadLocal对象在ThreadLocalMap引用之外,再无其他引用的时候能够被垃圾回收

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

垃圾回收后,Entry对象的key变为null,value还被引用着,既然key为null,那么value就不可能再被应用,但是因为value被Entry引用着,Entry被ThreadLocalMap引用着,ThreadLocalMap被Thread引用着,因此线程不结束,那么该回收的内存就会一直回收不了。

很容易出问题的情况就是我们在使用线程池的时候,线程池中的线程都是重复利用的,这时候使用threadLocal存放一些数据的话,如果在线程结束的时候没有显示的做清除处理,就有可能会出现内存泄露问题,甚至导致业务逻辑出现问题。所以在使用线程池的时候需要特别注意在代码运行完后显式的去清空设置的数据,如果用自定义的线程池同样也会遇到这样的问题。此时需要在finally代码块显式清除threadLocal中的数据。

当然对于内存泄露问题,ThreadLocalMap也是做了相关处理的,通过上面的源码知道ThreadLocalMap在get和set以及remove的时候,都会相应的做一次探测式清理操作,但是我们也说了这种清除是小范围的,是不能100%保证能够清理干净的。

我们可以通过以下两种方式来避免这个问题:

把ThreadLocal对象声明为static,这样ThreadLocal成为了类变量,生命周期不是和对象绑定,而是和类绑定,延长了声明周期,避免了被回收;

在使用完ThreadLocal变量后,手动remove掉,防止ThreadLocalMap中Entry一直保持对value的强引用。导致value不能被回收。

threadlocal的继承性

threadlocal不支持继承性:也就是说,同一个ThreadLocal变量在父线程中被设置值后,在子线程中是获取不到的。

但是父线程设置上下文就无法被子线程获取吗?当然不是,thread类除了提供了threadLocals,还提供了inheritableThreadLocals,InheritableThreadLocal继承了ThreadLocal,这个类中的父线程的值就可以在子线程中获取到。此类重写了ThreadLocal的三个方法。

public class InheritableThreadLocal<T> extends ThreadLocal<T> {
    public InheritableThreadLocal() {
    }
    protected T childValue(T var1) {
        return var1;
    }
    ThreadLocalMap getMap(Thread var1) {
        return var1.inheritableThreadLocals;
    }
    void createMap(Thread var1, T var2) {
        var1.inheritableThreadLocals = new ThreadLocalMap(this, var2);
    }
}

此类是如何实现子线程获取父线程保存的值的呢?下面代码是thread类的源码,在创建一个线程时,thread初始化的innt方法中会去判断父线程的inheritThreadLocals中是否有值,如果有,直接赋值给子线程

if (inheritThreadLocals && parent.inheritableThreadLocals != null)
    this.inheritableThreadLocals =
        ThreadLocal.createInheritedMap(parent.inheritableThreadLocals);

InheritableThreadLocals的使用方式

private static final ThreadLocal<OperationInfoDTO> THREAD_LOCAL = new InheritableThreadLocals <OperationInfoDTO>();

 



Tags:ThreadLocal   点击:()  评论:()
声明:本站部分内容及图片来自互联网,转载是出于传递更多信息之目的,内容观点仅代表作者本人,不构成投资建议。投资者据此操作,风险自担。如有任何标注错误或版权侵犯请与我们联系,我们将及时更正、删除。
▌相关推荐
ThreadLocal:多线程环境下的神秘武器
ThreadLocal是一个线程安全的,以线程为单位的数据传递工具。广泛应用于多层级数据传递。1应用场景ThreadLocal主要功能是跨层传递参数,比如,Controller层的数据需要在业务逻辑...【详细内容】
2023-10-19  Search: ThreadLocal  点击:(283)  评论:(0)  加入收藏
了解ThreadLocal,这一篇文章就够了
作者 | 蔡柱梁审校 | 重楼一、前言很多 Java 开发一般都是做中台较多,并发编程使用的不多。因此,对 ThreadLocal 不太熟悉,所以笔者这里想让大家了解它,知道它是用来干什么的。...【详细内容】
2023-09-04  Search: ThreadLocal  点击:(323)  评论:(0)  加入收藏
一文看懂Java中的ThreadLocal源码和注意事项
一、ThreadLocal的原理ThreadLocal是一个非常重要的类,它为每个线程提供了一个独立的变量副本。因此,每个线程都可以独立地访问和修改该变量,而不会影响其他线程的访问。这种机...【详细内容】
2023-04-12  Search: ThreadLocal  点击:(194)  评论:(0)  加入收藏
来说说ThreadLocal内存溢出问题
前言上次有个小伙伴问我,说他面试的时候,被问到ThreadLocal内存溢出问题,没有回答出来;那我们今天就来了解一下ThreadLocal。ThreadLocal介绍多线程在访问同一个变量时会产生线...【详细内容】
2021-06-17  Search: ThreadLocal  点击:(489)  评论:(0)  加入收藏
ThreadLocal原理及使用场景大揭秘
是什么ThreadLocal从名字上看好像是一个Thread,其实并不是,它是Therad的局部变量的维护类。作用是让变量私有化(为每个Thread提供变量的副本),以此来实现线程间变量的隔离。比如...【详细内容】
2021-01-14  Search: ThreadLocal  点击:(314)  评论:(0)  加入收藏
一文搞懂 ThreadLocal 原理
当多线程访问共享可变数据时,涉及到线程间同步的问题,并不是所有时候,都要用到共享数据,所以就需要线程封闭出场了。数据都被封闭在各自的线程之中,就不需要同步,这种通过将数据封...【详细内容】
2020-07-29  Search: ThreadLocal  点击:(290)  评论:(0)  加入收藏
ThreadLocal源码探析
闲谈ThreadLocal前面在我的GitHub仓库 V-LoggingTool 中有简单的使用过ThreadLocal,主要用在了切面类中,功能上需要取到前置增强拦截到的用户信息暂存,执行到后置增强时从该Thr...【详细内容】
2020-07-05  Search: ThreadLocal  点击:(322)  评论:(0)  加入收藏
FastThreadLocal 原理分析
FastThreadLocal 作用与JDK 原生的ThreadLocal功能是一样的,FastThreadLocal 持有指定类的对象,可以保证每个线程都持有一个唯一实例,每个线程持有实例都只在本线程内使用,所以不会有并发问题。但它的访问速度更快,顾名思...【详细内容】
2019-08-28  Search: ThreadLocal  点击:(1109)  评论:(0)  加入收藏
▌简易百科推荐
即将过时的 5 种软件开发技能!
作者 | Eran Yahav编译 | 言征出品 | 51CTO技术栈(微信号:blog51cto) 时至今日,AI编码工具已经进化到足够强大了吗?这未必好回答,但从2023 年 Stack Overflow 上的调查数据来看,44%...【详细内容】
2024-04-03    51CTO  Tags:软件开发   点击:(5)  评论:(0)  加入收藏
跳转链接代码怎么写?
在网页开发中,跳转链接是一项常见的功能。然而,对于非技术人员来说,编写跳转链接代码可能会显得有些困难。不用担心!我们可以借助外链平台来简化操作,即使没有编程经验,也能轻松实...【详细内容】
2024-03-27  蓝色天纪    Tags:跳转链接   点击:(12)  评论:(0)  加入收藏
中台亡了,问题到底出在哪里?
曾几何时,中台一度被当做“变革灵药”,嫁接在“前台作战单元”和“后台资源部门”之间,实现企业各业务线的“打通”和全域业务能力集成,提高开发和服务效率。但在中台如火如荼之...【详细内容】
2024-03-27  dbaplus社群    Tags:中台   点击:(8)  评论:(0)  加入收藏
员工写了个比删库更可怕的Bug!
想必大家都听说过删库跑路吧,我之前一直把它当一个段子来看。可万万没想到,就在昨天,我们公司的某位员工,竟然写了一个比删库更可怕的 Bug!给大家分享一下(不是公开处刑),希望朋友们...【详细内容】
2024-03-26  dbaplus社群    Tags:Bug   点击:(5)  评论:(0)  加入收藏
我们一起聊聊什么是正向代理和反向代理
从字面意思上看,代理就是代替处理的意思,一个对象有能力代替另一个对象处理某一件事。代理,这个词在我们的日常生活中也不陌生,比如在购物、旅游等场景中,我们经常会委托别人代替...【详细内容】
2024-03-26  萤火架构  微信公众号  Tags:正向代理   点击:(10)  评论:(0)  加入收藏
看一遍就理解:IO模型详解
前言大家好,我是程序员田螺。今天我们一起来学习IO模型。在本文开始前呢,先问问大家几个问题哈~什么是IO呢?什么是阻塞非阻塞IO?什么是同步异步IO?什么是IO多路复用?select/epoll...【详细内容】
2024-03-26  捡田螺的小男孩  微信公众号  Tags:IO模型   点击:(8)  评论:(0)  加入收藏
为什么都说 HashMap 是线程不安全的?
做Java开发的人,应该都用过 HashMap 这种集合。今天就和大家来聊聊,为什么 HashMap 是线程不安全的。1.HashMap 数据结构简单来说,HashMap 基于哈希表实现。它使用键的哈希码来...【详细内容】
2024-03-22  Java技术指北  微信公众号  Tags:HashMap   点击:(11)  评论:(0)  加入收藏
如何从头开始编写LoRA代码,这有一份教程
选自 lightning.ai作者:Sebastian Raschka机器之心编译编辑:陈萍作者表示:在各种有效的 LLM 微调方法中,LoRA 仍然是他的首选。LoRA(Low-Rank Adaptation)作为一种用于微调 LLM(大...【详细内容】
2024-03-21  机器之心Pro    Tags:LoRA   点击:(12)  评论:(0)  加入收藏
这样搭建日志中心,传统的ELK就扔了吧!
最近客户有个新需求,就是想查看网站的访问情况。由于网站没有做google的统计和百度的统计,所以访问情况,只能通过日志查看,通过脚本的形式给客户导出也不太实际,给客户写个简单的...【详细内容】
2024-03-20  dbaplus社群    Tags:日志   点击:(4)  评论:(0)  加入收藏
Kubernetes 究竟有没有 LTS?
从一个有趣的问题引出很多人都在关注的 Kubernetes LTS 的问题。有趣的问题2019 年,一个名为 apiserver LoopbackClient Server cert expired after 1 year[1] 的 issue 中提...【详细内容】
2024-03-15  云原生散修  微信公众号  Tags:Kubernetes   点击:(6)  评论:(0)  加入收藏
站内最新
站内热门
站内头条