您好,登錄后才能下訂單哦!
本篇內容介紹了“Java并發編程中如何實現線程之間的共享和協作”的有關知識,在實際案例的操作過程中,不少人都會遇到這樣的困境,接下來就讓小編帶領大家學習一下如何處理這些情況吧!希望大家仔細閱讀,能夠學有所成!
用處
Java支持多個線程同時訪問一個對象或者對象的成員變量
關鍵字synchronized可以修飾方法或者以同步塊的形式來進行使用
它主要確保多個線程在同一個時刻,只能有一個線程處于方法或者同步塊中
它保證了線程對變量訪問的可見性和排他性(原子性、可見性、有序性),又稱為內置鎖機制。
對象鎖和類鎖
對象鎖是用于對象實例方法,或者一個對象實例上的
類鎖是用于類的靜態方法或者一個類的class對象上的
類的對象實例可以有很多個,但是每個類只有一個class對象,所以不同對象實例的對象鎖是互不干擾的,但是每個類只有一個類鎖
注意的是,其實類鎖只是一個概念上的東西,并不是真實存在的,類鎖其實鎖的是每個類的對應的class對象
類鎖和對象鎖之間也是互不干擾的。
最輕量的同步機制,保證可見性,不保證原子性
volatile保證了不同線程對這個變量進行操作時的可見性,即一個線程修改了某個變量的值,這新值對其他線程來說是立即可見的。
volatile最適用的場景:只有一線程寫,多個線程讀的場景
ThreadLocal 和 Synchonized 都用于解決多線程并發訪問。
可是ThreadLocal與synchronized有本質的差別:
synchronized是利用鎖的機制,使變量或代碼塊在某一時該僅僅能被一個線程訪問。
而ThreadLocal為每個線程都提供了變量的副本,使得每個線程在某一時間訪問到的并非同一個對象,這樣就隔離了多個線程對數據的數據共享。
Spring的事務就借助了ThreadLocal類。
Spring會從數據庫連接池中獲得一個connection,然會把connection放進ThreadLocal中,也就和線程綁定了,事務需要提交或者回滾,只要從ThreadLocal中拿到connection進行操作。
以JDBC為例,正常的事務代碼可能如下:
dbc = new DataBaseConnection();//第1行
Connection con = dbc.getConnection();//第2行
con.setAutoCommit(false);// //第3行
con.executeUpdate(...);//第4行
con.executeUpdate(...);//第5行
con.executeUpdate(...);//第6行
con.commit();第7行
上述代碼,可以分成三個部分:
事務準備階段:第1~3行
業務處理階段:第4~6行
事務提交階段:第7行
不管我們開啟事務還是執行具體的sql都需要一個具體的數據庫連接。
開發應用一般都采用三層結構,我們的Service會調用一系列的DAO對數據庫進行多次操作,那么,這個時候我們就無法控制事務的邊界了,因為實際應用當中,我們的Service調用的DAO的個數是不確定的,可根據需求而變化,而且還可能出現Service調用Service的情況。
如果不使用ThreadLocal,如何讓三個DAO使用同一個數據源連接呢?我們就必須為每個DAO傳遞同一個數據庫連接,要么就是在DAO實例化的時候作為構造方法的參數傳遞,要么在每個DAO的實例方法中作為方法的參數傳遞。
Connection conn = getConnection();
Dao1 dao1 = new Dao1(conn);
dao1.exec();
Dao2 dao2 = new Dao2(conn);
dao2.exec();
Dao3 dao3 = new Dao3(conn);
dao3.exec();
conn.commit();
為了讓這個數據庫連接可以跨階段傳遞,又不顯式的進行參數傳遞,就必須使用別的辦法。
Web容器中,每個完整的請求周期會由一個線程來處理。因此,如果我們能將一些參數綁定到線程的話,就可以實現在軟件架構中跨層次的參數共享(是隱式的共享)。而JAVA中恰好提供了綁定的方法–使用ThreadLocal。
結合使用Spring里的IOC和AOP,就可以很好的解決這一點。
只要將一個數據庫連接放入ThreadLocal中,當前線程執行時只要有使用數據庫連接的地方就從ThreadLocal獲得就行了。
void set(Object value)
設置當前線程的線程局部變量的值。
public Object get()
該方法返回當前線程所對應的線程局部變量。
public void remove()
將當前線程局部變量的值刪除,目的是為了減少內存的占用,該方法是JDK 5.0新增的方法。
需要指出的是,當線程結束后,對應該線程的局部變量將自動被垃圾回收
所以顯式調用該方法清除線程的局部變量并不是必須的操作,但它可以加快內存回收的速度。
protected Object initialValue()
返回該線程局部變量的初始值
該方法是一個protected的方法,顯然是為了讓子類覆蓋而設計的。
這個方法是一個延遲調用方法,在線程第1次調用get()或set(Object)時才執行,并且僅執行1次。
ThreadLocal中的缺省實現直接返回一個null。
public final static ThreadLocal RESOURCE = new ThreadLocal()
RESOURCE代表一個能夠存放String類型的ThreadLocal對象。
此時不論任何一個線程能夠并發訪問這個變量,對它進行寫入、讀取操作,都是線程安全的。
public class ThreadLocal<T> {
//get方法,其實就是拿到每個線程獨有的ThreadLocalMap
//然后再用ThreadLocal的當前實例,拿到Map中的相應的Entry,然后就可以拿到相應的值返回出去。
//如果Map為空,還會先進行map的創建,初始化等工作。
public T get() {
//先取到當前線程,然后調用getMap方法獲取對應線程的ThreadLocalMap
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();
}
// Thread類中有一個 ThreadLocalMap 類型成員,所以getMap是直接返回Thread的成員
ThreadLocalMap getMap(Thread t) {
return t.threadLocals;
}
// ThreadLocalMap是ThreadLocal的靜態內部類
static class ThreadLocalMap {
ThreadLocalMap(ThreadLocal<?> firstKey, Object firstValue) {
// 用數組保存 Entry , 因為可能有多個變量需要線程隔離訪問,即聲明多個 ThreadLocal 變量
table = new Entry[INITIAL_CAPACITY];
int i = firstKey.threadLocalHashCode & (INITIAL_CAPACITY - 1);
// Entry 類似于 map 的 key-value 結構
// key 就是 ThreadLocal, value 就是需要隔離訪問的變量
table[i] = new Entry(firstKey, firstValue);
size = 1;
setThreshold(INITIAL_CAPACITY);
}
...
}
//Entry內部靜態類,它繼承了WeakReference,
//總之它記錄了兩個信息,一個是ThreadLocal<?>類型,一個是Object類型的值
static class Entry extends WeakReference<ThreadLocal<?>> {
/** The value associated with this ThreadLocal. */
Object value;
Entry(ThreadLocal<?> k, Object v) {
super(k);
value = v;
}
}
//getEntry方法則是獲取某個ThreadLocal對應的值
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);
}
//set方法就是更新或賦值相應的ThreadLocal對應的值
private void set(ThreadLocal<?> key, Object value) {
...
}
...
}
public class Thread implements Runnable {
/* ThreadLocal values pertaining to this thread. This map is maintained
* by the ThreadLocal class. */
ThreadLocal.ThreadLocalMap threadLocals = null;
...
}
創建對象 Object o = new Object();
這個o,我們可以稱之為對象引用,而new Object()我們可以稱之為在內存中產生了一個對象實例。
當 o=null 時,只是表示o不再指向堆中Object的對象實例,不代表這個對象實例不存在了。
指在程序代碼之中普遍存在的,類似“Object obj=new Object()
這類的引用,只要強引用還存在,垃圾收集器永遠不會回收掉被引用的對象實例。
用來描述一些還有用但并非必需的對象。
對于軟引用關聯著的對象,在系統將要發生內存溢出異常之前,將會把這些對象實例列進回收范圍之中進行第二次回收。如果這次回收還沒有足夠的內存,才會拋出內存溢出異常。
在JDK 1.2之后,提供了SoftReference類來實現軟引用。
用來描述非必需對象的,但是它的強度比軟引用更弱一些,被弱引用關聯的對象實例只能生存到下一次垃圾收集發生之前。
當垃圾收集器工作時,無論當前內存是否足夠,都會回收掉只被弱引用關聯的對象實例。
在JDK 1.2之后,提供了WeakReference類來實現弱引用。
也稱為幽靈引用或者幻影引用,它是最弱的一種引用關系。
一個對象實例是否有虛引用的存在,完全不會對其生存時間構成影響,也無法通過虛引用來取得一個對象實例。
為一個對象設置虛引用關聯的唯一目的就是能在這個對象實例被收集器回收時收到一個系統通知。
在JDK 1.2之后,提供了PhantomReference類來實現虛引用。
將堆內存大小設置為-Xmx256m
啟用一個線程池,大小固定為5個線程
//5M大小的數組
private static class LocalVariable {
private byte[] value = new byte[1024*1024*5];
}
// 創建線程池,固定為5個線程
private static ThreadPoolExecutor poolExecutor
= new ThreadPoolExecutor(5,5,1, TimeUnit.MINUTES,new LinkedBlockingQueue<>());
//ThreadLocal共享變量
private ThreadLocal<LocalVariable> data;
@Override
public void run() {
//場景1:不執行任何有意義的代碼,當所有的任務提交執行完成后,查看內存占用情況,占用 25M 左右
//System.out.println("hello ThreadLocal...");
//場景2:創建 數據對象,執行完成后,查看內存占用情況,與場景1相同
//new LocalVariable();
//場景3:啟用 ThreadLocal,執行完成后,查看內存占用情況,占用 100M 左右
ThreadLocalOOM obj = new ThreadLocalOOM();
obj.data = new ThreadLocal<>();
obj.data.set(new LocalVariable());
System.out.println("update ThreadLocal data value..........");
//場景4: 加入 remove(),執行完成后,查看內存占用情況,與場景1相同
//obj.data.remove();
//分析:在場景3中,當啟用了ThreadLocal以后確實發生了內存泄漏
}
場景1:
首先任務中不執行任何有意義的代碼,當所有的任務提交執行完成后,可以看見,我們這個應用的內存占用基本上為25M左右
場景2:
然后我們只簡單的在每個任務中new出一個數組,執行完成后我們可以看見,內存占用基本和場景1相同
場景3:
當我們啟用了ThreadLocal以后,執行完成后我們可以看見,內存占用變為了100多M
場景4:
我們加入一行代碼 obj.data.remove(); ,再執行,看看內存情況,可以看見,內存占用基本和場景1相同。
場景分析:
這就充分說明,場景3,當我們啟用了ThreadLocal以后確實發生了內存泄漏。
通過對ThreadLocal的分析,我們可以知道每個Thread 維護一個 ThreadLocalMap,這個映射表的 key 是 ThreadLocal實例本身,value 是真正需要存儲的 Object,也就是說 ThreadLocal 本身并不存儲值,它只是作為一個 key 來讓線程從 ThreadLocalMap 獲取 value。
仔細觀察ThreadLocalMap,這個map是使用 ThreadLocal 的弱引用作為 Key 的,弱引用的對象在 GC 時會被回收。
圖中的虛線表示弱引用。
當把threadlocal變量置為null以后,沒有任何強引用指向threadlocal實例,所以threadlocal將會被gc回收
這樣一來,ThreadLocalMap中就會出現key為null的Entry,就沒有辦法訪問這些key為null的Entry的value
如果當前線程再遲遲不結束的話,這些key為null的Entry的value就會一直存在一條強引用鏈:Thread Ref -> Thread -> ThreaLocalMap -> Entry -> value,而這塊value永遠不會被訪問到了,所以存在著內存泄露。
可以通過Debug模式,查看變量 poolExecutor->workers->0->thread->threadLocals,會發現線程的成員變量 threadLocals 的 size=1,map 中存放了一個 referent=null, value=data對象
只有當前thread結束以后,current thread就不會存在棧中,強引用斷開,Current Thread、Map value將全部被GC回收。
最好的做法是在不需要使用ThreadLocal變量后,都調用它的remove()方法,清除數據。
場景3分析:
在場景3中,雖然線程池里面的任務執行完畢了,但是線程池里面的5個線程會一直存在直到JVM退出,我們set了線程的localVariable變量后沒有調用localVariable.remove()方法,導致線程池里面的5個線程的threadLocals變量里面的new LocalVariable()實例沒有被釋放。
從表面上看內存泄漏的根源在于使用了弱引用。為什么使用弱引用而不是強引用?下面我們分兩種情況討論:
key 使用強引用:對ThreadLocal對象實例的引用被置為null了,但是ThreadLocalMap還持有這個ThreadLocal對象實例的強引用,如果沒有手動刪除,ThreadLocal的對象實例不會被回收,導致Entry內存泄漏。
key 使用弱引用:對ThreadLocal對象實例的引用被被置為null了,由于ThreadLocalMap持有ThreadLocal的弱引用,即使沒有手動刪除,ThreadLocal的對象實例也會被回收。value在下一次ThreadLocalMap調用set,get,remove都有機會被回收。
比較兩種情況,我們可以發現:由于ThreadLocalMap的生命周期跟Thread一樣長,如果都沒有手動刪除對應key,都會導致內存泄漏,但是使用弱引用可以多一層保障。
因此,ThreadLocal內存泄漏的根源是:
由于ThreadLocalMap的生命周期跟Thread一樣長,如果沒有手動刪除對應key就會導致內存泄漏,而不是因為弱引用。
總結:
JVM利用設置ThreadLocalMap的Key為弱引用,來避免內存泄露。
JVM利用調用remove、get、set方法的時候,回收弱引用。
當ThreadLocal存儲很多Key為null的Entry的時候,而不再去調用remove、get、set方法,那么將導致內存泄漏。
使用線程池 + ThreadLocal時要小心,因為這種情況下,線程是一直在不斷的重復運行的,從而也就造成了value可能造成累積的情況。
錯誤使用ThreadLocal導致線程不安全:
仔細考察ThreadLocal和Thead的代碼,我們發現ThreadLocalMap中保存的其實是對象的一個引用,這樣的話,當有其他線程對這個引用指向的對象實例做修改時,其實也同時影響了所有的線程持有的對象引用所指向的同一個對象實例。
這也就是為什么上面的程序為什么會輸出一樣的結果:5個線程中保存的是同一個Number對象的引用,因此它們最終輸出的結果是相同的。
正確的用法是讓每個線程中的ThreadLocal都應該持有一個新的Number對象。 線程間的協作
線程之間相互配合,完成某項工作;
比如一個線程修改了一個對象的值,而另一個線程感知到了變化,然后進行相應的操作;
前者是生產者,后者就是消費者,這種模式隔離了“做什么”(what)和“怎么做”(How);
常見的方法是讓消費者線程不斷地循環檢查變量是否符合預期在while循環中設置不滿足的條件,如果條件滿足則退出while循環,從而完成消費者的工作。
存在如下問題:
1)難以確保及時性;
2)難以降低開銷。如果降低睡眠的時間,比如休眠1毫秒,這樣消費者能更加迅速地發現條件變化,但是卻可能消耗更多的處理器資源,造成了無端的浪費。
是指一個線程A調用了對象O的wait()方法進入等待狀態,而另一個線程B調用了對象O的notify()或者notifyAll()方法,線程A收到通知后從對象O的wait()方法返回,進而執行后續操作。
上述兩個線程通過對象O來完成交互,而對象上的wait()和notify/notifyAll()的關系就如同開關信號一樣,用來完成等待方和通知方之間的交互工作。
notify():
通知一個在對象上等待的線程,使其從wait方法返回,而返回的前提是該線程獲取到了對象的鎖,沒有獲得鎖的線程重新進入WAITING狀態。
notifyAll():
通知所有等待在該對象上的線程。
wait():
調用該方法的線程進入 WAITING狀態,只有等待另外線程的通知或被中斷才會返回.需要注意,調用wait()方法后,會釋放對象的鎖。
wait(long):
超時等待一段時間,這里的參數時間是毫秒,也就是等待長達n毫秒,如果沒有通知就超時返回;
wait (long,int):
對于超時時間更細粒度的控制,可以達到納秒;
等待方遵循如下原則:
1.獲取對象的鎖
2.循環里判斷條件是否滿足,如果條件不滿足,那么調用對象的wait()方法,被通知后仍要檢查條件。
條件滿足則執行對應的邏輯。
synchronized(對象){
while(條件不滿足){
對象.wait();
}
對應的邏輯
}
通知方遵循如下原則:
1.獲取對象的鎖。
2.改變條件。
3.通知所有等待在對象上的線程。
synchronized(對象){
改變條件
對象.notifyAll();
}
在調用wait()、notify()系列方法之前,線程必須要獲得該對象的對象級別鎖,即只能在同步方法或同步塊中調用wait() 方法、notify()系列方法;
進入wait() 方法后,當前線程釋放鎖,在從wait() 返回前,線程與其他線程競爭重新獲得鎖,執行notify()系列方法的線程退出synchronized代碼塊的時候后,他們就會去競爭。
如果其中一個線程獲得了該對象鎖,它就會繼續往下執行,在它退出synchronized代碼塊,釋放鎖后,其他的已經被喚醒的線程將會繼續競爭獲取該鎖,一直進行下去,直到所有被喚醒的線程都執行完畢。
notify() 和 notifyAll() 應該用誰?
盡量用 notifyAll()
謹慎使用notify(),因為notify()只會喚醒一個線程,我們無法確保被喚醒的這個線程一定就是我們需要喚醒的線程;
調用場景:
調用一個方法時等待一段時間(一般來說是給定一個時間段),如果該方法能夠在給定的時間段之內得到結果,那么將結果立刻返回,反之,超時返回默認結果。
假設等待時間段是T,那么可以推斷出在當前時間now+T之后就會超時
等待持續時間:REMAINING=T ;
超時時間:FUTURE=now+T ;
客戶端獲取連接的過程被設定為等待超時的模式,也就是在1000毫秒內如果無法獲取到可用連接,將會返回給客戶端一個null。
設定連接池的大小為10個,然后通過調節客戶端的線程數來模擬無法獲取連接的場景。
通過構造函數初始化連接的最大上限,通過一個雙向隊列來維護連接,調用方需要先調用fetchConnection(long)方法來指定在多少毫秒內超時獲取連接,當連接使用完成后,需要調用releaseConnection(Connection)方法將連接放回線程池
調用yield() 、sleep()、wait()、notify()等方法對鎖有何影響?
yield() 、sleep()被調用后,都不會釋放當前線程所持有的鎖。
調用wait()方法后,會釋放當前線程持有的鎖,而且當前被喚醒后,會重新去競爭鎖,鎖競爭到后才會執行wait方法后面的代碼。
調用notify()系列方法后,對鎖無影響,線程只有在synchronized同步代碼執行完后才會自然而然的釋放鎖,所以notify()系列方法一般都是synchronized同步代碼的最后一行。
“Java并發編程中如何實現線程之間的共享和協作”的內容就介紹到這里了,感謝大家的閱讀。如果想了解更多行業相關的知識可以關注億速云網站,小編將為大家輸出更多高質量的實用文章!
免責聲明:本站發布的內容(圖片、視頻和文字)以原創、轉載和分享為主,文章觀點不代表本網站立場,如果涉及侵權請聯系站長郵箱:is@yisu.com進行舉報,并提供相關證據,一經查實,將立刻刪除涉嫌侵權內容。