「这是我参与11月更文挑战的第6天,活动详情查看:2021最后一次更文挑战」
ThreadLocal是什么
ThreadLocal提供线程局部变量。这些变量与正常的变量不同,因为每一个线程在访问ThreadLocal实例的时候(通过其get或set方法)都有自己的、独立初始化的变量副本。ThreadLocal实例通常是类中的私有静态字段,使用它的目的是希望将状态(例如,用户ID或事务ID)与线程关联起来。
由于CPU快过内存。所以会有L1,L2,L3三级缓存。
简单理解:就是线程自己在自己私有线程本地内存副本中,对共享变量副本的set,get的相关过程和内容。(图片可以参考JMM内存模型图片)
能干嘛
实现每一个线程都有自己专属的本地变量副本(自己用自己的变量,不和其他人共享,人人都有),不用加锁来维持。
解决了让每个线程绑定自己的值,通过使用get()和set()方法,获取默认值或其值更改为当前线程所存的副本的值从而避免线程安全问题。
以前,多个线程,但是同一时间只有一个线程能访问,要加锁
但是,现在每个人都可以拿到这支笔去写,自己用自己的,不用加锁,依上面。
例如
以前的sync关键字,一次锁一个,悲观锁
CAS,不加锁,用轮询一个一个操作,乐观锁
本质上,都是多对一个进行操作,抢资源。
而现在,不需要加锁,人手都有一份。
使用ThreadLocal
最好就是给ThreadLocal进行赋值,提供了initialValue和withInitial两个方法来赋值。
现在不采用initialValue方法,匿名内部类,冗长。建议使用withInitial()。
有五个主要方法get(),set(),remove()以及这里描述的两个方法。
//静态的方法public static <S> ThreadLocal<S> withInitial(Supplier<? extends S> supplier) {
return new SuppliedThreadLocal<>(supplier);
}
ThreadLocal<Integer> threadLocal = ThreadLocal.withInitial(() -> 0);
依据阿里手册,必须回收自定义的ThreadLocal变量,尤其在线程池的场景下,线程会经常被复用,如果不清理自定义的ThreadLocal变量,可能会影响后续的业务逻辑和造成内存泄漏等问题,所以要在代理中使用try-finally块进行回收。
int i=0;
ThreadLocal<Integer> threadLocal = ThreadLocal.withInitial(() -> 0);
public void sale(){
threadLocal.get();
i++;
threadLocal.set(i);
}
//不加锁,代码比较优雅public static void main(String[] args) {
Bank bank = new Bank();
new Thread(() -> {
try {
bank.sale();
}catch (Exception e){
e.printStackTrace();
}finally {
bank.threadLocal.remove();
//在下一次使用写的时候,就要将当前去除。
}
}).start();
}
SimpleDateFormat的线程安全问题
SimpleDateFormat是线程不安全类,一般不要定义为static变量,如果一定要使用static,必须加锁。或者使用DateUtils类(会造成日期数据混乱或乱码,有时候会抛乱七八糟的异常)
注意:时间工具类,写成静态的成员变量在多线程下,危险性很强。
解决办法:(优雅的写法)
class Bank{ public static final ThreadLocal<SimpleDateFormat> sdf = ThreadLocal.withInitial(() -> new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"));//使用ThreadLocal,分开的执行各自的
public static final Date DateTransfer(String date) throws ParseException {
sdf.get().parse(date);
}}
public class SingletonDemo {
public static void main(String[] args) {
new Thread(() -> {
try {
Bank.DateTransfer("2021-10-25 22:03:02");
} catch (ParseException e) {
e.printStackTrace();
}finally {
Bank.sdf.remove();//回收
}
}).start();
}}
在JDK8中,使用Instant代替Date,LocalDateTime代替Calendar,DateTimeFormatter代替SimpleDateFormat。
这样写出来的代码比较健壮和美观优雅。同时线程安全。
Thread,ThreadLocal和ThreadLocalMap
静态内部类:一个类中,含有一个static修饰的内部类。
Thread中含有ThreadLocal.ThreadLocalMap inheritableThreadLocals = null;
ThreadLocal类中含有ThreadLocalMap的静态内部类。static class ThreadLocalMap{}。
ThreadLocalMap中还含有一个静态内部类Entry。
ThreadLocalMap底层是key,value键值对。
总结:ThreadLocalMap实际上是一个以threadLocal实例为key,任意对象为value的Entry对象。
当我们为threadLocal变量赋值,实际上就是以当前threadLocal实例为key,值为value的Entry往这个threadLocalMap中存放。
Entry继承到了弱引用
static class Entry extends WeakReference<ThreadLocal<?>> { //WeakReference弱引用
/** The value associated with this ThreadLocal. */
Object value;
Entry(ThreadLocal<?> k, Object v) {
super(k);
value = v;
}}
JVM内部维护了一个线程版的Map(通过ThreadLocal对象的set方法,结果把ThreadLocal对象自己当做key,放进了ThreadLoalMap中),每个线程要用到这个T的时候,用当前的线程去Map里面获取,通过这样让每个线程都拥有了自己独立的变量,人手一份,竞争条件被彻底消除,在并发模式下是绝对安全的变量。
JVM四大引用——强引用,弱引用,虚引用,软引用
Reference:强引用
SoftReferce:软引用
WeakRefemce:弱引用
PhantomReference:虚引用
java使用finalize()方法在垃圾收集器将对象从内存清除出去之前做必要的清理工作(工作中不用)
强引用(默认模式)
当内存不足,JVM开始垃圾回收,对于强引用的对象,就算是出现了OOM也不会对该对象进行回收。
强引用是我们最常见的普通对象引用,只要还有强引用指向一个对象,就能表明对象还“活着”,垃圾收集器不会碰这种对象。
在 Java 中最常见的就是强引用,把一个对象赋给一个引用变量,这个引用变量就是一个强引用。当一个对象被强引用变量引用时,它处于可达状态,它是不可能被垃圾回收机制回收的,即使该对象以后永远都不会被用到JVM也不会回收。因此强引用是造成Java内存泄漏的主要原因之一。
对于一个普通的对象,如果没有其他的引用关系,只要超过了引用的作用域或者显式地将相应(强)引用赋值为 null,一般认为就是可以被垃圾收集的了(当然具体回收时机还是要看垃圾收集策略)。
软引用
软引用是一种相对强引用弱化了一些的引用,需要用java.lang.ref.SoftReference类来实现,可以让对象豁免一些垃圾收集。
对于只有软引用的对象来说,
当系统内存充足时它 不会 被回收,
当系统内存不足时它 会 被回收。
软引用通常用在对内存敏感的程序中,比如高速缓存就有用到软引用,内存够用的时候就保留,不够用就回收!
弱引用
弱引用需要用java.lang.ref.WeakReference类来实现,它比软引用的生存期更短,对于只有弱引用的对象来说,只要垃圾回收机制一运行,不管JVM的内存空间是否足够,都会回收该对象占用的内存。
虚引用
虚引用需要java.lang.ref.PhantomReference类来实现。
顾名思义,就是形同虚设,与其他几种引用都不同,虚引用并不会决定对象的生命周期。如果一个对象仅持有虚引用,那么它就和没有任何引用一样,在任何时候都可能被垃圾回收器回收,它不能单独使用也不能通过它访问对象,虚引用必须和引用队列 (ReferenceQueue)联合使用。
虚引用的主要作用是跟踪对象被垃圾回收的状态。 仅仅是提供了一种确保对象被 finalize以后,做某些事情的机制。 PhantomReference的get方法总是返回null,因此无法访问对应的引用对象。
其意义在于:说明一个对象已经进入finalization阶段,可以被gc回收,用来实现比finalization机制更灵活的回收操作。
换句话说,设置虚引用关联的唯一目的,就是在这个对象被收集器回收的时候收到一个系统通知或者后续添加进一步的处理。
虚引用在死之前会调用finalize(),虚引用在被回收之前会进入队列。
ThreadLocal如何避免内存泄漏
为什么要用弱引用
当强引用的对象死了之后(GC),该threadLocal就得作废。
public void function01(){
ThreadLocal tl = new ThreadLocal();
//line1
tl.set(2021);
//line2 tl.get();
line3}//line1新建了一个ThreadLocal对象,t1 是强引用指向这个对象;line2调用set()方法后新建一个Entry,通过源码可知Entry对象里的k是弱引用指向这个对象。
为什么源代码用弱引用?
当function01方法执行完毕后,栈帧销毁强引用 tl 也就没有了。但此时线程的ThreadLocalMap里某个entry的key引用还指向这个对象若这个key引用是强引用,就会导致key指向的ThreadLocal对象及v指向的对象不能被gc回收,造成内存泄漏;
若这个key引用是弱引用就大概率会减少内存泄漏的问题(还有一个key为null的雷)。使用弱引用,就可以使ThreadLocal对象在方法执行完毕后顺利被回收且Entry的key引用指向为null。
key为null的entry,原理解析
ThreadLocalMap使用ThreadLocal的弱引用作为key,如果一个ThreadLocal没有外部强引用引用他,那么系统gc的时候,这个ThreadLocal势必会被回收,这样一来,ThreadLocalMap中就会出现key为null的Entry,就没有办法访问这些key为null的Entry的value。
如果当前线程再迟迟不结束的话(比如正好用在线程池),这些key为null的Entry的value就会一直存在一条强引用链。 虽然弱引用,保证了key指向的ThreadLocal对象能被及时回收,但是v指向的value对象是需要ThreadLocalMap调用get、set时发现key为null时才会去回收整个entry、value。
因此弱引用不能100%保证内存不泄露。我们要在不使用某个ThreadLocal对象后,手动调用remoev方法来删除它(必须remove),尤其是在线程池中,不仅仅是内存泄露的问题,因为线程池中的线程是重复使用的,意味着这个线程的ThreadLocalMap对象也是重复使用的,如果我们不手动调用remove方法,那么后面的线程就有可能获取到上个线程遗留下来的value值,造成bug。
总结
- ThreadLocal并不解决线程间的共享数据问题
- ThreadLocal适用于变量在线程间间隔且在方法间共享的场景。
- ThreadLocal通过隐试在不同线程创建独立的实例副本避免了线程安全问题。
- 每个线程持有一个只属于自己专属的Map,并维护了ThreadLocal对象,Map只被他持有的线程访问,所以不存在线程安全问题和锁的问题。
- ThreadLocalMap的Entry对ThreadLocal的引用为弱引用,避免ThreadLocal对象无法被回收的问题,都会通过expungeStaleEntry,cleanSomeSlots,replaceStaleEntry这三个方法清理掉key为null的脏entry。