線程的建立很簡單,一般是內建Thread類或者實作Runnable接口,我就不細說了。然後,要牢記多線程的3大特性:
多線程的三個特性:原子性、可見性、有序性
原子性:是指一個操作是不可中斷的。即使是多個線程一起執行的時候,一個操作一旦開始,就不會被其他線程幹擾。比如,對于一個靜态全局變量int i,兩個線程同時對它指派,線程A給他指派為1,線程B給他指派為-1。那麼不管這兩個線程以何種方式。何種步調工作,i的值要麼是1,要麼是-1.線程A和線程B之間是沒有幹擾的。這就是原子性的一個特點,不可被中斷。
可見性:是指當一個線程修改了某一個共享變量的值,其他線程是否能夠立即知道這個修改。顯然,對于串行來說,可見性問題是不存在的。
有序性:在并發時,程式的執行可能會出現亂序。給人的直覺感覺就是:寫在前面的代碼,會在後面執行。有序性問題的原因是因為程式在執行時,可能會進行指令重排,重排後的指令與原指令的順序未必一緻。
而共享變量的寫操作出錯,最重要的是原子性,一般多線程的問題主要抓住這個。
線程安全問題
一般多線程程式設計都會遇到線程安全的問題,線程安全總體來說是因為多個線程競争共享資源造成的。比如:
public class Test{
private int num = 0;
public void add(int value){
this.num = this.num + value;
}
}
兩個線程分别加了2和3到count變量上,兩個線程執行結束後count變量的值應該等于5。如果兩個線程同時執行這個對象的add()方法,會造成這種現象:線程A先讀到num為0,此時恰好線程B也讀到num為0,然後A,B同時執行加2和加3的操作,如果A先指派num為2,然後B又指派num為3,會造成最後結果為3;或者反過來,造成num為2,使得最後的結果無法預料。
如果線程并沒有共享資源,那麼多線程執行的代碼是安全的,比如:
類方法中局部變量或者局部對象引用
public class Test{
public void add(int value){
int num = 0;
String a = new String("aa");
num = num + value;
}
}
還有一種安全的方法,就是每個線程都是執行同一個類不同對象的方法,雖然代碼相同,但是不同的對象空間,也不會出現問題,如servlet。
線程狀态
線程的狀态實作通過 Thread.State 常量類實作,有 6 種線程狀态:new(建立)、runnnable(可運作)、blocked(阻塞)、waiting(等待)、time waiting (定時等待)和 terminated(終止)。狀态轉換圖如下:
線程狀态流程大緻如下:
- 線程建立後,進入 new 狀态
- 調用 start 或者 run 方法,進入 runnable 狀态
- JVM 按照線程優先級及時間分片等執行 runnable 狀态的線程。開始執行時,進入 running 狀态
- 如果線程執行 sleep、wait、join,或者進入 IO 阻塞等。進入 wait 或者 blocked 狀态
- 線程執行完畢後,線程被線程隊列移除。最後為 terminated 狀态。
ThreadLocal
ThreadLocal與線程同步無關,它雖然提供了一種解決多線程環境下成員變量的問題,但是它并不是解決多線程共享變量的問題。
它的API介紹如下:
該類提供了線程局部 (thread-local) 變量。這些變量不同于它們的普通對應物,因為通路某個變量(通過其get 或 set 方法)的每個線程都有自己的局部變量,它獨立于變量的初始化副本。ThreadLocal執行個體通常是類中的 private static 字段,它們希望将狀态與某一個線程(例如,使用者 ID 或事務 ID)相關聯。
是以ThreadLocal與線程同步機制不同,線程同步機制是多個線程共享同一個變量,而ThreadLocal是為每一個線程建立一個單獨的變量副本,故而每個線程都可以獨立地改變自己所擁有的變量副本,而不會影響其他線程所對應的副本。可以說ThreadLocal為多線程環境下變量問題提供了另外一種解決思路。
ThreadLocal定義了四個方法:
- get():傳回此線程局部變量的目前線程副本中的值。
- initialValue():傳回此線程局部變量的目前線程的“初始值”。
- remove():移除此線程局部變量目前線程的值。
set(T value):将此線程局部變量的目前線程副本中的值設定為指定值。
除了這四個方法,ThreadLocal内部還有一個靜态内部類ThreadLocalMap,該内部類才是實作線程隔離機制的關鍵,get()、set()、remove()都是基于該内部類操作。ThreadLocalMap提供了一種用鍵值對方式存儲每一個線程的變量副本的方法,key為目前ThreadLocal對象,value則是對應線程的變量副本。
對于ThreadLocal需要注意的有兩點:
- ThreadLocal執行個體本身是不存儲值,它隻是提供了一個在目前線程中找到副本值得key。
-
是ThreadLocal包含在Thread中,而不是Thread包含在ThreadLocal中,有些小夥伴會弄錯他們的關系。
下圖是Thread、ThreadLocal、ThreadLocalMap的關系
ThreadLocal示例
package com.xushu.multi;
public class Test{
private static ThreadLocal<Integer> count = new ThreadLocal<Integer>(){
// 實作initialValue()
@Override
protected Integer initialValue() {
return 0; //這裡傳回了一個0
}
};
public int nextSeq(){
count.set(count.get() + 1);
return count.get();
}
private static class SeqThread implements Runnable{
private Test te;
SeqThread(Test te) {
this.te = te;
}
@Override
public void run() {
for(int i = 0; i < 3; i++){
System.out.println(Thread.currentThread().getName() + " seqCount :" + te.nextSeq());
}
}
}
public static void main(String[] args) {
Test te = new Test();
Thread t1 = new Thread(new SeqThread(te));
Thread t2 = new Thread(new SeqThread(te));
Thread t3 = new Thread(new SeqThread(te));
Thread t4 = new Thread(new SeqThread(te));
t1.start();
t2.start();
t3.start();
t4.start();
}
}
可以看出,每個線程都有自己的一個變量副本,是以從根本上避免了讀同一個變量。但是,如果在initialValue()方法中,如果return的是一個共有變量,那就是所有的線程都通路同一個變量了,是以ThreadLocal就失效了。
這篇文章有解析。ThreadLocal源碼解析
ThreadLocal雖然解決了這個多線程變量的複雜問題,但是它的源碼實作卻是比較簡單的。ThreadLocalMap是實作ThreadLocal的關鍵,我們先從它入手。
ThreadLocalMap
ThreadLocalMap其内部利用Entry來實作key-value的存儲,如下:
static class Entry extends WeakReference<ThreadLocal<?>> {
/** The value associated with this ThreadLocal. */
Object value;
Entry(ThreadLocal<?> k, Object v) {
super(k);
value = v;
}
}
從上面代碼中可以看出Entry的key就是ThreadLocal,而value就是值。同時,Entry也繼承WeakReference,是以說Entry所對應key(ThreadLocal執行個體)的引用為一個弱引用(關于弱引用這裡就不多說了,感興趣的可以關注這篇部落格
Java 理論與實踐: 用弱引用堵住記憶體洩漏)
ThreadLocalMap的源碼稍微多了點,我們就看兩個最核心的方法getEntry()、set(ThreadLocal> key, Object value)方法。
set(ThreadLocal> key, Object value)
private void set(ThreadLocal<?> key, Object value) {
ThreadLocal.ThreadLocalMap.Entry[] tab = table;
int len = tab.length;
// 根據 ThreadLocal 的散列值,查找對應元素在數組中的位置
int i = key.threadLocalHashCode & (len-1);
// 采用“線性探測法”,尋找合适位置
for (ThreadLocal.ThreadLocalMap.Entry e = tab[i];
e != null;
e = tab[i = nextIndex(i, len)]) {
ThreadLocal<?> k = e.get();
// key 存在,直接覆寫
if (k == key) {
e.value = value;
return;
}
// key == null,但是存在值(因為此處的e != null),說明之前的ThreadLocal對象已經被回收了
if (k == null) {
// 用新元素替換陳舊的元素
replaceStaleEntry(key, value, i);
return;
}
}
// ThreadLocal對應的key執行個體不存在也沒有陳舊元素,new 一個
tab[i] = new ThreadLocal.ThreadLocalMap.Entry(key, value);
int sz = ++size;
// cleanSomeSlots 清楚陳舊的Entry(key == null)
// 如果沒有清理陳舊的 Entry 并且數組中的元素大于了門檻值,則進行 rehash
if (!cleanSomeSlots(i, sz) && sz >= threshold)
rehash();
}
這個set()操作和我們在集合了解的put()方式有點兒不一樣,雖然他們都是key-value結構,不同在于他們解決散列沖突的方式不同。集合Map的put()采用的是拉鍊法,而ThreadLocalMap的set()則是采用開放定址法(具體請參考
散列沖突處理系列部落格)。掌握了開放位址法該方法就一目了然了。
set()操作除了存儲元素外,還有一個很重要的作用,就是replaceStaleEntry()和cleanSomeSlots(),這兩個方法可以清除掉key == null 的執行個體,防止記憶體洩漏。在set()方法中還有一個變量很重要:threadLocalHashCode,定義如下:
private final int threadLocalHashCode = nextHashCode();
從名字上面我們可以看出threadLocalHashCode應該是ThreadLocal的散列值,定義為final,表示ThreadLocal一旦建立其散列值就已經确定了,生成過程則是調用nextHashCode():
private static AtomicInteger nextHashCode = new AtomicInteger();
private static final int HASH_INCREMENT = 0x61c88647;
private static int nextHashCode() {
return nextHashCode.getAndAdd(HASH_INCREMENT);
}
nextHashCode表示配置設定下一個ThreadLocal執行個體的threadLocalHashCode的值,HASH_INCREMENT則表示配置設定兩個ThradLocal執行個體的threadLocalHashCode的增量,從nextHashCode就可以看出他們的定義。
private Entry getEntry(ThreadLocal<?> key) {
int i = key.threadLocalHashCode & (table.length - 1);
Entry e = table[i];
if (e != null && e.get() == key)
return e;
else
return getEntryAfterMiss(key, i, e);
}
由于采用了開放定址法,是以目前key的散列值和元素在數組的索引并不是完全對應的,首先取一個探測數(key的散列值),如果所對應的key就是我們所要找的元素,則傳回,否則調用getEntryAfterMiss(),如下:
private Entry getEntryAfterMiss(ThreadLocal<?> key, int i, Entry e) {
Entry[] tab = table;
int len = tab.length;
while (e != null) {
ThreadLocal<?> k = e.get();
if (k == key)
return e;
if (k == null)
expungeStaleEntry(i);
else
i = nextIndex(i, len);
e = tab[i];
}
return null;
}
這裡有一個重要的地方,當key == null時,調用了expungeStaleEntry()方法,該方法用于處理key == null,有利于GC回收,能夠有效地避免記憶體洩漏。
get()
- 傳回目前線程所對應的線程變量
public T get() {
// 擷取目前線程
Thread t = Thread.currentThread();
// 擷取目前線程的成員變量 threadLocal
ThreadLocalMap map = getMap(t);
if (map != null) {
// 從目前線程的ThreadLocalMap擷取相對應的Entry
ThreadLocalMap.Entry e = map.getEntry(this);
if (e != null) {
@SuppressWarnings("unchecked")
// 擷取目标值
T result = (T)e.value;
return result;
}
}
return setInitialValue();
}
首先通過目前線程擷取所對應的成員變量ThreadLocalMap,然後通過ThreadLocalMap擷取目前ThreadLocal的Entry,最後通過所擷取的Entry擷取目标值result。
getMap()方法可以擷取目前線程所對應的ThreadLocalMap,如下:
ThreadLocalMap getMap(Thread t) {
return t.threadLocals;
}
set(T value)
- 設定目前線程的線程局部變量的值。
public void set(T value) {
Thread t = Thread.currentThread();
ThreadLocalMap map = getMap(t);
if (map != null)
map.set(this, value);
else
createMap(t, value);
}
擷取目前線程所對應的ThreadLocalMap,如果不為空,則調用ThreadLocalMap的set()方法,key就是目前ThreadLocal,如果不存在,則調用createMap()方法建立一個,如下:
void createMap(Thread t, T firstValue) {
t.threadLocals = new ThreadLocalMap(this, firstValue);
}
initialValue()
- 傳回該線程局部變量的初始值。
protected T initialValue() {
return null;
}
該方法定義為protected級别且傳回為null,很明顯是要子類實作它的,是以我們在使用ThreadLocal的時候一般都應該覆寫該方法。該方法不能顯示調用,隻有在第一次調用get()或者set()方法時才會被執行,并且僅執行1次。
remove()
- 将目前線程局部變量的值删除。
public void remove() {
ThreadLocalMap m = getMap(Thread.currentThread());
if (m != null)
m.remove(this);
}
該方法的目的是減少記憶體的占用。當然,我們不需要顯示調用該方法,因為一個線程結束後,它所對應的局部變量就會被垃圾回收。
參考文獻
1.并發程式設計網