Java并發系列:ThreadLocal的用法和坑
Java并發系列:ThreadLocal的用法和坑
本文主要講解:
ThreadLocal的用法
多線程競争同一個變量
同一個線程無需顯式調用
ThreadLocal的原理
資料結構
set、get 和 remove方法
ThreadLocal的問題
線程不安全的場景
記憶體溢出問題
延伸:強弱虛軟引用
本文源碼位址:E01_ThreadLocal, 歡迎star我的Github
ThreadLocal的用法
多線程競争同一個變量
當我們使用線程池來複用線程的時候,對于同一個變量的競争使用,一般會導緻線程安全問題,是以建議放入到ThreadLocal中使用。
public class RightThreadLocalDemo {
public static ExecutorService tpool = Executors.newFixedThreadPool(10);
@Test//一千個線程
public void ThreadLocalDemo_right() throws InterruptedException {
for (int i = 0; i < 1000; i++) {
int fi = i;
tpool.submit(() -> {
String date = new RightThreadLocalDemo().date(fi);
System.out.println(Thread.currentThread().getName() + ": " + date);
});
}
Thread.sleep(100);
tpool.shutdown();
}
public String date(int second) {
Date date = new Date(1000 * second);
SimpleDateFormat df = datefmt1.get();
//此處證明了ThreadLocal即使是static 對象,其線上程中也不僅僅是一個,而是以副本的形式存在于線程中
System.out.println(Thread.currentThread().getName()+"========"+System.identityHashCode(df));
return df.format(date);
}
//此處是建立 ThreadLocal
public static ThreadLocal datefmt1 =
ThreadLocal.withInitial(() -> new SimpleDateFormat("mm:ss"));
}
注意到一個問題:為什麼ThreadLocal可以寫在很多地方,比如寫在不同的類中,用的時候确在另一個類或者方法裡面,但是依舊是線程安全的?
因為ThreadLocal是屬于線程的,使用了 Thread.currentThread() 來擷取目前線程。
public void set(T value) {
Thread t = Thread.currentThread();
ThreadLocalMap map = getMap(t);
if (map != null)
map.set(this, value);
else
createMap(t, value);
}
同一個線程無需顯式調用
當一個線程需要經曆很多類的很多方法,一般是将需要的對象當作參數,每一步都帶下去,此時可以使用ThreadLocal,就像web裡面的session一下,做到随取随用。
public class RightThreadLocalDemo2 {
static ThreadLocal tl = new ThreadLocal<>();
@Test
public void deal(String nn){
new Service1().service(nn);
}
public static ExecutorService tpool = Executors.newFixedThreadPool(10);
public static void main(String[] args) throws InterruptedException {
for (int i = 0; i < 100; i++) {
int fg = i;
tpool.submit(()->{
new Service1().service("ljfirst---"+fg);
});
}
//Thread.sleep(100);
tpool.shutdown();
}
}
class Service1{
public void service(String n){
User u = new User(n);
RightThreadLocalDemo2.tl.set(u);
new Service2().service();
}
}
class Service2{
public void service(){
User u = RightThreadLocalDemo2.tl.get();
System.out.println(Thread.currentThread().getName()+" ---Service2().service(): "+u.name);
new Service3().service();
}
}
class Service3{
public void service(){
User u = RightThreadLocalDemo2.tl.get();
System.out.println(Thread.currentThread().getName()+" ---Service3().service(): "+u.name);
new Service4().service();
}
}
class Service4{
public void service(){
User u = RightThreadLocalDemo2.tl.get();
System.out.println(Thread.currentThread().getName()+" ---Service4().service(): "+u.name);
//不用的時候記得remove
RightThreadLocalDemo2.tl.remove();
}
}
class User{
String name ;
public User(String name){
this.name = name;
}
}
ThreadLocal的原理
資料結構
由下圖可以看出ThreadLocal其實是一個Entry,每個ThreadLocal組成一個Entry的數組,被ThreadLocalMap管理。
圖檔來源:用了三年 ThreadLocal 今天才弄明白其中的道理
set、get 和 remove方法
set方法
public void set(T value) {
// 擷取目前線程
Thread t = Thread.currentThread();
// 從 Thread 中擷取 ThreadLocalMap
ThreadLocalMap map = getMap(t);
if (map != null)
map.set(this, value);
else
createMap(t, value);
}
ThreadLocalMap getMap(Thread t) {
return t.threadLocals;
}
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();
}
remove方法
在這裡插入代碼片public void remove() {
ThreadLocalMap m = getMap(Thread.currentThread());
if (m != null)
m.remove(this);
}
上述提及的三個方法,隻是表象,需要看具體的實作,還是進ThreadLocalMap中看。源碼解析來自于:ThreadLocal有沒有記憶體洩漏?源碼給你安排得明明白白
ThreadLocal的問題
線程不安全的場景
在上述樣例中,展示了使用線程池複用線程的方法,但是複用線程的同時,ThreadLocal沒有被清除,也會被複用,是以造成污染。
public class threadlocal複用污染 {
public static ExecutorService tpool = Executors.newFixedThreadPool(10);
@Test
public void test() {
Thread t = new Thread(()->{
Tools.tl.set("bbb");
System.out.println(Thread.currentThread().getName() + ":======" + Tools.tl.get());
//這句話不加會導緻後續的線程複用時,threadlocal也被複用,是以造成線程不安全
Tools.tl.remove();
});
tpool.submit(t);
Thread t1 = new Thread(()->{
Tools.dd();
System.out.println(Thread.currentThread().getName() + ":======" + Tools.tl.get());
});
tpool.submit(t1);
for (int i = 0; i < 100; i++) {
tpool.submit(() -> {
System.out.println(Thread.currentThread().getName() + Tools.tl.get());
});
}
tpool.shutdown();
}
}
class Tools {
static ThreadLocal tl = new ThreadLocal<>();
public static void dd() {
tl.set("aaaa");
}
}
記憶體溢出問題
如果ThreadLocal在使用後,不删除,雖然ThreadLocalMap的key會被清空(因為它是弱引用),但是其Value并不會,在高并發場景下,很容易出現OOM。
public class ThreadLocal記憶體洩漏 {
public static void main(String[] args) {
for (int i = 0; i < 100; i++) {
new Task().calc(10);
//通過下方的Evaluate Expression 可以看出在80前後回收了 線程裡面的内容
//但是僅僅回收了map 的 key(目前的ThreadLocal),并不是回收Value。
//是以存在記憶體溢出的問題。
if (i == 80) {
System.gc();
}
}
}
static class Task {
ThreadLocal value;
public int calc(int i) {
value = new ThreadLocal();
value.set((value.get() == null ? 0 : value.get()) + i);
return value.get();
}
}
}
延伸:強弱虛軟引用
強引用:
類似于Student s = new Student();這裡的s就是一個強引用,當線程、對象消失,或者手動s=null;就在恰當的時間,被GC掉。
弱引用:
當一個對象同時被強、弱引用指向時,它不會被回收,但是當強引用消失,那麼弱引用就會在恰當的時間,被GC掉,有種狐假虎威的感覺。
下方代碼的Entry,線上程銷毀,或者線程池銷毀的時候,将被GC掉。
弱引用的特點是不管記憶體是否足夠,隻要發生GC,都會被回收
static class Entry extends WeakReference> {
Object value;
Entry(ThreadLocal> k, Object v) {
super(k);
value = v;
}
}
虛引用:
在NIO中,就運用了虛引用管理堆外記憶體
軟引用:
軟引用就是把對象用SoftReference包裹一下,當我們需要從軟引用對象獲得包裹的對象,隻要get一下就可以了。
當記憶體不足,會觸發JVM的GC,如果GC後,記憶體還是不足,就會把軟引用的包裹的對象給幹掉,也就是隻有在記憶體不足,JVM才會回收該對象
SoftReferencestudentSoftReference=new SoftReference(new Student());
Student student = studentSoftReference.get();
System.out.println(student);
強軟弱虛的驗證代碼:強軟弱虛
參考部落格:強軟弱虛引用
Java并發系列:ThreadLocal的用法和坑相關教程