我們知道,執行緒的不安全問題,主要是由於多執行緒併發讀取一個變數而引起的,那麼有沒有一種辦法可以讓一個變數是執行緒獨有的呢,這樣不就可以解決執行緒安全問題了麼。其實JDK已經為我們提供了ThreadLocal這個東西。
◆
ThreadLocal基本使用
◆
當使用ThreadLocal維護變數時,ThreadLocal為每個使用該變數的執行緒提供獨立的變數副本,所以每一個執行緒都可以獨立地改變自己的副本,而不會影響其它執行緒所對應的副本。
ThreadLocal 的主要方法有這麼幾個:
| initialValue 初始化
set 賦值
get 取值
remove 清空複製程式碼
|
下面來看一個簡單的使用程式碼示例:
複製程式碼
| public class ThreadLocalDemo {
private static ThreadLocal<Integer> threadLocal = new ThreadLocal<Integer>() {
@Override
public Integer initialValue() {
return 0;
}
};
static class ThreadDemo implements Runnable {
@Override
public void run() {
for (int i = 0; i < 1000; i++) {
threadLocal.set(threadLocal.get() + 1);
}
System.out.println("thread :" + Thread.currentThread().getId() + " is" + threadLocal.get());
}
}
public static void main(String[] args) {
for (int i = 0; i < 10; i++) {
new Thread(new ThreadDemo()).start();
}
}
}複製程式碼
|
上方程式碼使用了10個執行緒迴圈對一個threadLocal的值進行一千次的加法,如果我們不知道ThreadLocal的原理的話我們可能會覺得最後列印的值一定是1000、2000、3000。。10000或者是執行緒不安全的值。
但是如果你執行這段程式碼你會發現最後列印的都是1000。
◆
ThreadLocal原理剖析
◆
現在我們來看一下ThreadLocal是如何實現為每個執行緒單獨維護一個變數的呢。
先來看一下初始化方法。
複製程式碼
| protected T initialValue() {
return null;
}複製程式碼
|
initialValue 預設是返回空的,所以為了避免空指標問題重寫了這個方法設定了預設返回值為0,但是呢,雖然這個方法好像是設定預設值的,但是還沒有生效,具體請接著往下看。
複製程式碼
| public void set(T value) {
Thread t = Thread.currentThread();
ThreadLocalMap map = getMap(t);
if (map != null)
map.set(this, value);
else
createMap(t, value);
}複製程式碼
|
我們可以看到set方法首先會獲取當前執行緒,然後通過一個getMap方法獲取了ThreadLocalMap,接著來看一下這個map是怎麼來的呢。
123複製程式碼
| ThreadLocalMap getMap(Thread t) {
return t.threadLocals;
}複製程式碼
|
這個map是在Thread類維護的一個map,下方是Thread類維護的此變數。預設這個map是空的。
1複製程式碼
| ThreadLocal.ThreadLocalMap threadLocals = null;複製程式碼
|
接著往下看程式碼,如果獲取的時候map不為空,則通過set方法把Thread類的threadLocals變數更新。如果是第一次建立的時候則初始化Thread的threadLocals變數。
下方是createMap的程式碼:
123複製程式碼
| void createMap(Thread t, T firstValue) {
t.threadLocals = new ThreadLocalMap(this, firstValue);
}複製程式碼
|
接下來看個get方法就比較容易理解了。
12345678910111213複製程式碼
| 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();
}複製程式碼
|
注意關注最後的一個return,看到呼叫的這個方法名我們就可以發現這個ThreadLocal的初始化原來是當第一呼叫get方法時如果還沒有被set的時候才會去獲取initialValue 方法的返回值。
12345678910複製程式碼
| private T setInitialValue() {
T value = initialValue();
Thread t = Thread.currentThread();
ThreadLocalMap map = getMap(t);
if (map != null)
map.set(this, value);
else
createMap(t, value);
return value;
}複製程式碼
|
◆
使用ThreadLocal最應該注意的事項
◆
首先來看一下執行緒退出的辦法:
123456789101112複製程式碼
| private void exit() {
if (group != null) {
group.threadTerminated(this);
group = null;
}
target = null;
threadLocals = null;
inheritableThreadLocals = null;
inheritedAccessControlContext = null;
blocker = null;
uncaughtExceptionHandler = null;
}複製程式碼
|
我們看到當執行緒結束的時候上方第7行會把ThreadLocal的值製為空,這個東西本身是沒問題的。但是,如果你是使用的執行緒池,這個問題可就大了!!!
要知道執行緒池裡的執行緒執行完一個任務之後緊接著下一個,這中間執行緒可不會結束,下一個任務獲得Thread的值可是上一個任務的遺留資料。
下面是這個問題的示例程式碼:
12345678910111213141516171819202122複製程式碼
| private static ThreadLocal<Integer> threadLocal = new ThreadLocal<Integer>() {
@Override
public Integer initialValue() {
return 0;
}
};
static class ThreadDemo implements Runnable {
@Override
public void run() {
for (int i = 0; i < 1000; i++) {
threadLocal.set(threadLocal.get() + 1);
}
System.out.println("thread :" + Thread.currentThread().getId() + " is" + threadLocal.get());
//threadLocal.remove();
}
}
public static void main(String[] args) {
ExecutorService executorService= Executors.newFixedThreadPool(5);
for (int i = 0; i < 10; i++) {
executorService.submit(new Thread(new ThreadDemo()));
}
}複製程式碼
|
執行這段程式碼你就會發現同樣的操作線上程池裡已經得不到一樣的結果了。想要解決這種問題也很簡單,只需要把ThreadLocal的值線上程執行完清空就可以了。把第14行註釋的程式碼放開再執行以下你就明白了。
◆
InheritableThreadLocal
◆
其實ThreadLocal還有一個比較強大的子類InheritableThreadLocal,它呢可以把父執行緒生成的變數傳遞給子執行緒。
下面來看一下程式碼示例:
123456789101112131415161718複製程式碼
| public class InheritableThreadLocalDemo { private static InheritableThreadLocal<Integer> inheritableThreadLocal = new InheritableThreadLocal<Integer>(); static class ThreadDemo implements Runnable { @Override public void run() { for (int i = 0; i < 1000; i++) { inheritableThreadLocal.set(inheritableThreadLocal.get() + 1); } System.out.println("thread :" + Thread.currentThread().getId() + " is" + inheritableThreadLocal.get()); } } public static void main(String[] args) { inheritableThreadLocal.set(24); for (int i = 0; i < 10; i++) { new Thread(new ThreadDemo()).start(); } }}複製程式碼
|
執行程式碼會發現程式輸出全是1024,這就是因為InheritableThreadLocal吧在主執行緒設定的值24傳遞到了那10個子執行緒中。
◆
InheritableThreadLocal原理剖析
◆
接下來我們來看一下InheritableThreadLocal為什麼可以實現這種功能呢。
InheritableThreadLocal是ThreadLocal的子類,
與ThreadLocal相同的set方法
12345678複製程式碼
| public void set(T value) { Thread t = Thread.currentThread(); ThreadLocalMap map = getMap(t); if (map != null) map.set(this, value); else createMap(t, value); }複製程式碼
|
不同點是InheritableThreadLocal重寫了createMap方法,將值賦值給了執行緒的inheritableThreadLocals變數。
123複製程式碼
| void createMap(Thread t, T firstValue) { t.inheritableThreadLocals = new ThreadLocalMap(this, firstValue); }複製程式碼
|
再跟進去Thread類的原始碼看inheritableThreadLocals變數你會發現:我去,這不是跟Threadlocal一樣麼,同樣初始值為null,執行緒退出的時候清空。沒錯,就是這樣的。也就是說它其實也是一個執行緒私有的變數,ThreadLocal的功能它是都有的。
那麼它又是怎麼把父執行緒的變數傳遞到子執行緒的呢?
接著看Thread的構造方法
123複製程式碼
| public Thread() { init(null, null, "Thread-" + nextThreadNum(), 0);}複製程式碼
|
一路追蹤init方法你會看見這段程式碼:
12345678910111213141516171819202122232425262728293031323334353637383940複製程式碼
| private void init(ThreadGroup g, Runnable target, String name, long stackSize, AccessControlContext acc) { if (name == null) { throw new NullPointerException("name cannot be null"); } this.name = name; Thread parent = currentThread(); SecurityManager security = System.getSecurityManager(); if (g == null) { if (security != null) { g = security.getThreadGroup(); } if (g == null) { g = parent.getThreadGroup(); } } g.checkAccess(); if (security != null) { if (isCCLOverridden(getClass())) { security.checkPermission(SUBCLASS_IMPLEMENTATION_PERMISSION); } } g.addUnstarted(); this.group = g; this.daemon = parent.isDaemon(); this.priority = parent.getPriority(); if (security == null || isCCLOverridden(parent.getClass())) this.contextClassLoader = parent.getContextClassLoader(); else this.contextClassLoader = parent.contextClassLoader; this.inheritedAccessControlContext = acc != null ? acc : AccessController.getContext(); this.target = target; setPriority(priority); if (parent.inheritableThreadLocals != null) this.inheritableThreadLocals = ThreadLocal.createInheritedMap(parent.inheritableThreadLocals); this.stackSize = stackSize; tid = nextThreadID(); }複製程式碼
|
仔細觀察倒數第5行到倒數第二行你就明白了。
本文所有原始碼github.com/shiyujun/sy…