ThreadLocal提供线程局部变量。这些变量与正常的变量不同,因为每一个线程在访问ThreadLocal实例的时候(通过其get或set方法)都有自己的、独立初始化的变量副本。ThreadLocal实例通常是类中的私有静态字段,使用它的目的是希望将状态(例如,用户ID或事务ID)与线程关联起来。
实现每一个线程都有自己专属的本地变量副本(自己用自己的变量不麻烦别人,不和其他人共享,人人有份,人各一份),主要解决了让每个线程绑定自己的值,通过使用get()和set()方法,获取默认值或将其值更改为当前线程所存的副本的值从而避免了线程安全问题。
需求:5个销售卖房子,集团高层只关心销售总量的准确统计数
package site.zhourui.juc.theadLocal;
import java.util.Random;
import java.util.concurrent.TimeUnit;
class House
{
int saleCount=0;
public synchronized void saleHouse() {
++saleCount;
}
}
/**
* 需求1:5个销售卖房子,集团高层只关心销售总量的准确统计数
*/
public class ThreadLocalDemo {
public static void main(String[] args)
{
House house = new House();
for (int i = 1; i <=5; i++) {
new Thread(() -> {
int size = new Random().nextInt(5) + 1;//模拟每个销售售卖的房子数量不同
System.out.println(size);
for (int j = 0; j <size; j++) {
house.saleHouse();
}
},String.valueOf(i)).start();
}
try { TimeUnit.MILLISECONDS.sleep(10); } catch (InterruptedException e) { e.printStackTrace(); }
System.out.println(Thread.currentThread().getName()+"\t共计卖出多少套:" +house.saleCount);
}
}
执行结果:
即我们用synchronized加锁的方式来解决并发问题
需求2:5个销售卖完随机数房子,各自独立销售额度,自己业绩按提成走,分灶吃饭,各个销售自己动手,丰衣足食
package site.zhourui.juc.theadLocal;
import java.util.Random;
import java.util.concurrent.TimeUnit;
class House
{
int saleCount=0;
public synchronized void saleHouse() {
++saleCount;
}
// ThreadLocal integerThreadLocal = new ThreadLocal(){
// @Override
// protected Integer initialValue() {
// return 0;
// }
// };
ThreadLocal<Integer> saleVolume = ThreadLocal.withInitial(() -> 0);
public void setVolumeByThreadLocal(){
saleVolume.set(1+saleVolume.get());
}
}
/**
* 需求1:5个销售卖房子,集团高层只关心销售总量的准确统计数
*/
public class ThreadLocalDemo {
public static void main(String[] args)
{
House house = new House();
for (int i = 1; i <=5; i++) {
new Thread(() -> {
int size = new Random().nextInt(5) + 1;//模拟每个销售售卖的房子数量不同
for (int j = 0; j <size; j++) {
house.setVolumeByThreadLocal();
house.saleHouse();
}
System.out.println(Thread.currentThread().getName()+"\t号销售卖出:"+house.saleVolume.get());
},String.valueOf(i)).start();
}
try { TimeUnit.MILLISECONDS.sleep(10); } catch (InterruptedException e) { e.printStackTrace(); }
System.out.println(Thread.currentThread().getName()+"\t共计卖出多少套:"+house.saleCount);
}
}
执行结果:
不加锁同样也可以解决线程安全问题
注意:
创建ThreadLocal有两种方式:
- new ThreadLocal<>()
- 这种方式需要以匿名内部类的方式重写initialValue方法初始化值
- ThreadLocal.withInitial(() -> 0);
- 这种方式直接用lambda表达式的方式初始化值
- 并且withInitial是静态方法
总结:ThreadLocal类与Synchonized对象锁的异同
- 相同点:ThreadLocal和Synchonized都用于解决多线程并发访问
- 不同点:
- synchronized是利用锁的机制,使变量或代码块在某一时该只能被一个线程访问,用于在多个线程间通信时能够获得数据共享。
- ThreadLocal为每一个线程都提供了变量的副本,使得每个线程在某一时间访问到的并不是同一个对象,这样就隔离了多个线程对数据的数据共享。
- ThreadLocal总结:
- 因为每个 Thread 内有自己的实例副本且该副本只由当前线程自己使用
- 既然其它 Thread 不可访问,那就不存在多线程间共享的问题。
- 统一设置初始值,但是每个线程对这个值的修改都是各自线程互相独立的
package site.zhourui.juc.theadLocal;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
class MyData{
ThreadLocal<Integer> threadLocalField=ThreadLocal.withInitial(()->0);
public void add(){
threadLocalField.set(1+ threadLocalField.get());
}
}
public class ThreadLocalDemo2 {
public static void main(String[] args) {
MyData myData = new MyData();
ExecutorService fixedThreadPool = Executors.newFixedThreadPool(3);
try {
for (int i = 0; i < 10; i++) {
fixedThreadPool.submit(()->{
Integer before = myData.threadLocalField.get();
myData.add();
Integer after = myData.threadLocalField.get();
System.out.println(Thread.currentThread().getName()+"\t"+"before:"+before+"\t"+"after:"+after);
});
}
}catch (Exception e){
e.printStackTrace();
}finally {
fixedThreadPool.shutdown();
}
}
}
执行结果:
如果ThreadLocal在线程复用的情况下执行完成后不进行remove,那么当该线程再次拿到任务时,上一次的ThreadLocal还在该线程中,出现如下效果,数字越来越大,最后极端情况线程会爆
package site.zhourui.juc.theadLocal;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
class MyData{
ThreadLocal<Integer> threadLocalField=ThreadLocal.withInitial(()->0);
public void add(){
threadLocalField.set(1+ threadLocalField.get());
}
}
public class ThreadLocalDemo2 {
public static void main(String[] args) {
MyData myData = new MyData();
ExecutorService fixedThreadPool = Executors.newFixedThreadPool(3);
try {
for (int i = 0; i < 10; i++) {
fixedThreadPool.submit(()->{
try {
Integer before = myData.threadLocalField.get();
myData.add();
Integer after = myData.threadLocalField.get();
System.out.println(Thread.currentThread().getName()+"\t"+"before:"+before+"\t"+"after:"+after);
}finally {
myData.threadLocalField.remove();
}
});
}
}catch (Exception e){
e.printStackTrace();
}finally {
fixedThreadPool.shutdown();
}
}
}
执行结果:
每次执行完成线程都会清除掉ThreadLocal
- threadLocalMap实际上就是一个以threadLocal实例为key,任意对象为value的Entry对象。
- 当我们为threadLocal变量赋值,实际上就是以当前threadLocal实例为key,值为value的Entry往这个threadLocalMap中存放
- JVM内部维护了一个线程版的Map
(通过ThreadLocal对象的set方法,结果把ThreadLocal对象自己当做key,放进了ThreadLoalMap中),每个线程要用到这个T的时候,用当前的线程去Map里面获取,通过这样让每个线程都拥有了自己独立的变量,人手一份,竞争条件被彻底消除,在并发模式下是绝对安全的变量。
不再会被使用的对象或者变量占用的内存不能被回收,就是内存泄露。
ThreadLocalMap 中有一个静态内部类Entry ,而这个类继承了弱引用,那么这个类的实例对象就是弱引用的
ThreadLocalMap从字面上就可以看出这是一个保存ThreadLocal对象的map(其实是以它为Key),不过是经过了两层包装的ThreadLocal对象:
- 第一层包装是使用 WeakReference
> 将ThreadLocal对象变成一个弱引用的对象; - 第二层包装是定义了一个专门的类 Entry 来扩展 WeakReference
>:
具体介绍可以看我的jvm笔记3.6章节,介绍的比较全面
当内存不足,JVM开始垃圾回收,对于强引用的对象,==就算是出现了OOM也不会对该对象进行回收,死都不收。==因此强引用是造成Java内存泄漏的主要原因之一。
对于一个普通的对象,如果没有其他的引用关系,只要超过了引用的作用域或者显式地将相应(强)引用赋值为 null,一般认为就是可以被垃圾收集的了(当然具体回收时机还是要看垃圾收集策略)。
软引用是一种相对强引用弱化了一些的引用,需要用java.lang.ref.SoftReference类来实现,可以让对象豁免一些垃圾收集。
软引用通常用在对内存敏感的程序中,比如高速缓存就有用到软引用,内存够用的时候就保留,不够用就回收!
弱引用需要用java.lang.ref.WeakReference类来实现,它比软引用的生存期更短,
对于只有弱引用的对象来说,只要垃圾回收机制一运行,不管JVM的内存空间是否足够,都会回收该对象占用的内存。
GCRoots和四大引用小总结
强引用
,就会导致key指向的ThreadLocal对象及v指向的对象不能被gc回收,造成内存泄漏;弱引用
就大概率会减少内存泄漏的问题(但还有一个key为null的雷)。使用弱引用,就可以使ThreadLocal对象在方法执行完毕后顺利被回收且Entry的key引用指向为null。原因:
当我们为threadLocal变量赋值,实际上就是当前的Entry(threadLocal实例为key,值为value)往这个threadLocalMap中存放。Entry中的key是弱引用,当threadLocal外部强引用被置为null(tl=null),那么系统 GC 的时候,根据可达性分析,这个threadLocal实例就没有任何一条链路能够引用到它,这个ThreadLocal势必会被回收,这样一来,ThreadLocalMap中就会出现key为null的Entry,就没有办法访问这些key为null的Entry的value,如果当前线程再迟迟不结束的话,这些key为null的Entry的value就会一直存在一条强引用链:Thread Ref -> Thread -> ThreaLocalMap -> Entry -> value永远无法回收,造成内存泄漏。
当然,如果当前thread运行结束,threadLocal,threadLocalMap,Entry没有引用链可达,在垃圾回收的时候都会被系统进行回收。
但在实际使用中我们有时候会用线程池去维护我们的线程,比如在Executors.newFixedThreadPool()时创建线程的时候,为了复用线程是不会结束的,所以threadLocal内存泄漏就值得我们小心
解决方案:
此后我们调用get,set或remove方法时,就会尝试删除key为null的entry,可以释放value对象所占用的内存。
虽然弱引用,保证了key指向的ThreadLocal对象能被及时回收,但是v指向的value对象是需要ThreadLocalMap调用get、set时发现key为null时才会去回收整个entry、value,因此弱引用不能100%保证内存不泄露。我们要在不使用某个ThreadLocal对象后,手动调用remove
方法来删除它,尤其是在线程池中,不仅仅是内存泄露的问题,因为线程池中的线程是重复使用的,意味着这个线程的ThreadLocalMap对象也是重复使用的,如果我们不手动调用remove方法,那么后面的线程就有可能获取到上个线程遗留下来的value值,造成bug。
从前面的set,getEntry,remove方法看出,在threadLocal的生命周期里,针对threadLocal存在的内存泄漏的问题,都会通过expungeStaleEntry
,cleanSomeSlots
,replaceStaleEntry
这三个方法清理掉key为null的脏entry。
判断Entry的key值是否为空,为空即调用expungeStaleEntry
方法清空为key为null的value
清空key值和value值
set(T value)–>set(ThreadLocal> key, Object value)–>replaceStaleEntry–>cleanSomeSlots–>expungeStaleEntry
get()–>getEntry–>getEntryAfterMiss–>expungeStaleEntry
remove(ThreadLocal> key)–>expungeStaleEntry
总结:
ThreadLocal threadLocalField=ThreadLocal.withInitial(()->初始化值);
建议吧ThreadLocal修饰为static
ThreadLocal能实现了线程的数据隔离,不在于它自己本身,而在于Thread的ThreadLocalMap所以,ThreadLocal可以只初始化一次,只分配一块存储空间就足以了,没必要作为成员变量多次被初始化。
用完记得手动remove