天天看點

java threadlocal 并發_Java并發系列:ThreadLocal的用法和坑

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管理。

java threadlocal 并發_Java并發系列:ThreadLocal的用法和坑

圖檔來源:用了三年 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的用法和坑相關教程