守護線程
使用者線程即運行在前台的線程,而守護線程是運行在背景線程。 守護線程作用是為其他前台線程的運行提供便利服務,而且僅在普通、非守護線程仍然運行時才需要,比如記憶體回收線程就是一個守護線程。當VM檢測僅剩一個守護線程,而使用者線程都已經退出運行時,VM就會退出,因為沒有如果沒有了被守護這,也就沒有繼續運行程式的必要了。如果有非守護線程仍然存活,VM就不會退出。
守護線程並非只有虛擬機器內部提供,使用者在編寫程式時也可以自己設定守護線程。使用者可以用Thread的setDaemon(true)方法設定當前線程為守護線程。
雖然守護線程可能非常有用,但必須小心確保其他所有非守護線程消亡時,不會由於它的終止而產生任何危害。因為你不可能知道在所有的使用者線程退出運行前,守護線程是否已經完成了預期的服務任務。一旦所有的使用者線程退出了,虛擬機器也就退出運行了。 因此,不要在守護線程中執行商務邏輯操作(比如對資料的讀寫等)。、
另外有幾點需要注意: setDaemon(true)必須在調用線程的start()方法之前設定,否則會跑出IllegalThreadStateException異常。 在守護線程中產生的新線程也是守護線程。 不要認為所有的應用都可以分配給守護線程來進行服務,比如讀寫操作或者計算邏輯。 線程阻塞
線程可以阻塞於四種狀態: 當線程執行Thread.sleep()時,它一直阻塞到指定的毫秒時間之後,或者阻塞被另一個線程打斷; 當線程碰到一條wait()語句時,它會一直阻塞到接到通知(notify())、被中斷或經過了指定毫秒時間為止(若制定了逾時值的話 線程阻塞與不同I/O的方式有多種。常見的一種方式是InputStream的read()方法,該方法一直阻塞到從流中讀取一個位元組的資料為止,它可以無限阻塞,因此不能指定逾時時間; 線程也可以阻塞等待擷取某個對象鎖的排他性存取權限(即等待獲得synchronized語句必須的鎖時阻塞)。
並非所有的阻塞狀態都是可中斷的,以上阻塞狀態的前兩種可以被中斷,後兩種不會對中斷做出反應。 synchronized
在並發編程中,多線程同時並發訪問的資源叫做臨界資源,當多個線程同時訪問對象並要求操作相同資源時,分割了原子操作就有可能出現資料的不一致或資料不完整的情況,為避免這種情況的發生,我們會採取同步機制,以確保在某一時刻,方法內只允許有一個線程。
採用synchronized修飾符實現的同步機制叫做互斥鎖機制,它所獲得的鎖叫做互斥鎖。每個對象都有一個monitor(鎖標記),當線程擁有這個鎖標記時才能訪問這個資源,沒有鎖標記便進入鎖池。任何一個對象系統都會為其建立一個互斥鎖,這個鎖是為了分配給線程的,防止打斷原子操作。每個對象的鎖只能分配給一個線程,因此叫做互斥鎖。 關於同步機制擷取互斥鎖 如果同一個方法內同時有兩個或更多線程,則每個線程有自己的局部變數拷貝。 類的每個執行個體都有自己的對象層級鎖。當一個線程訪問執行個體對象中的synchronized同步代碼塊或同步方法時,該線程便擷取了該執行個體的對象層級鎖,其他線程這時如果要訪問synchronized同步代碼塊或同步方法,便需要阻塞等待,直到前面的線程從同步代碼塊或方法中退出,釋放掉了該對象層級鎖。 訪問同一個類的不同執行個體對象中的同步代碼塊,不存在阻塞等待擷取對象鎖的問題,因為它們擷取的是各自執行個體的對象層級鎖,相互之間沒有影響。 持有一個對象層級鎖不會阻止該線程被交換出來,也不會阻塞其他線程訪問同一樣本對象中的非synchronized代碼。當一個線程A持有一個對象層級鎖(即進入了synchronized修飾的代碼塊或方法中)時,線程也有可能被交換出去,此時線程B有可能擷取執行該對象中代碼的時間,但它只能執行非同步代碼(沒有用synchronized修飾),當執行到同步代碼時,便會被阻塞,此時可能線程規劃器又讓A線程運行,A線程繼續持有對象層級鎖,當A線程退出同步代碼時(即釋放了對象層級鎖),如果B線程此時再運行,便會獲得該對象層級鎖,從而執行synchronized中的代碼。 持有對象層級鎖的線程會讓其他線程阻塞在所有的synchronized代碼外。例如,在一個類中有三個synchronized方法a,b,c,當線程A正在執行一個執行個體對象M中的方法a時,它便獲得了該對象層級鎖,那麼其他的線程在執行同一執行個體對象(即對象M)中的代碼時,便會在所有的synchronized方法處阻塞,即在方法a,b,c處都要被阻塞,等線程A釋放掉對象層級鎖時,其他的線程才可以去執行方法a,b或者c中的代碼,從而獲得該對象層級鎖。 **使用synchronized(obj)同步語句塊,可以擷取指定對象上的對象層級鎖。**obj為對象的引用,如果擷取了obj對象上的對象層級鎖,在並發訪問obj對象時時,便會在其synchronized代碼處阻塞等待,直到擷取到該obj對象的對象層級鎖。當obj為this時,便是擷取當前對象的對象層級鎖。 類層級鎖被特定類的所有樣本共用,它用於控制對static成員變數以及static方法的並發訪問。具體用法與對象層級鎖相似。 互斥是實現同步的一種手段,臨界區、互斥量和訊號量都是主要的互斥實現方式。synchronized關鍵字經過編譯後,會在同步塊的前後分別形成monitorenter和monitorexit這兩個位元組碼指令。根據虛擬機器規範的要求,在執行monitorenter指令時,首先要嘗試擷取對象的鎖,如果獲得了鎖,把鎖的計數器加1,相應地,在執行monitorexit指令時會將鎖計數器減1,當計數器為0時,鎖便被釋放了。由於synchronized同步塊對同一個線程是可重新進入的,因此一個線程可以多次獲得同一個對象的互斥鎖,同樣,要釋放相應次數的該互斥鎖,才能最終釋放掉該鎖。 volatile
在JDK1.2之前,Java的記憶體模型實現總是從主存(即共用記憶體)讀取變數,是不需要進行特別的注意的。而隨著JVM的成熟和最佳化,現在在多線程環境下volatile關鍵字的使用變得非常重要。
在當前的Java記憶體模型下,線程可以把變數儲存在本地記憶體(比如機器的寄存器)中,而不是直接在主存中進行讀寫。這就可能造成一個線程在主存中修改了一個變數的值,而另外一個線程還繼續使用它在寄存器中的變數值的拷貝,造成資料的不一致。
要解決這個問題,就需要把變數聲明為volatile(也可以使用同步),這就指示JVM,這個變數是不穩定的,每次使用它都到主存中進行讀取。一般說來,多任務環境下,各任務間共用的變數都應該加volatile修飾符。
Volatile修飾的成員變數在每次被線程訪問時,都強迫從共用記憶體中重讀該成員變數的值。而且,當成員變數發生變化時,強迫線程將變化值回寫到共用記憶體。這樣在任何時刻,兩個不同的線程總是看到某個成員變數的同一個值。
volatile是一種稍弱的同步機制,在訪問volatile變數時不會執行加鎖操作,也就不會執行線程阻塞,因此volatilei變數是一種比synchronized關鍵字更輕量級的同步機制。
使用建議:在兩個或者更多的線程需要訪問的成員變數上使用volatile。當要訪問的變數已在synchronized代碼塊中,或者為常量時,沒必要使用volatile。
由於使用volatile屏蔽掉了JVM中必要的代碼最佳化,所以在效率上比較低,因此一定在必要時才使用此關鍵字。 記憶體可見度
加鎖(synchronized同步)的功能不僅僅局限於互斥行為,同時還存在另外一個重要的方面:記憶體可見度。我們不僅希望防止某個線程正在使用對象狀態而另一個線程在同時修改該狀態,而且還希望確保當一個線程修改了對象狀態後,其他線程能夠看到該變化。而線程的同步恰恰也能夠實現這一點。
內建鎖可以用於確保某個線程以一種可預測的方式來查看另一個線程的執行結果。為了確保所有的線程都能看到共用變數的最新值,可以在所有執行讀操作或寫操作的線程上加上同一把鎖。
考慮如下代碼
public class MutableInteger { private int value; public int get(){ return value; } public void set(int value){ this.value = value; } }
以上代碼中,get和set方法都在沒有同步的情況下訪問value。如果value被多個線程共用,假如某個線程調用了set,那麼另一個正在調用get的線程可能會看到更新後的value值,也可能看不到。
通過對set和get方法進行同步,可以使MutableInteger成為一個安全執行緒的類,如下
public class SynchronizedInteger { private int value; public synchronized int get(){ return value; } public synchronized void set(int value){ this.value = value; } }
對set和get方法進行了同步,加上了同一把對象鎖,這樣get方法可以看到set方法中value值的變化,從而每次通過get方法取得的value的值都是最新的value值。 記憶體可見的兩種方法比較:加鎖和volatile變數
加鎖和volatile變數的區別 volatile變數是一種稍弱的同步機制在訪問volatile變數時不會執行加鎖操作,因此也就不會使執行線程阻塞,因此volatile變數是一種比synchronized關鍵字更輕量級的同步機制。 從記憶體可見度的角度看,寫入volatile變數相當於退出同步代碼塊,而讀取volatile變數相當於進入同步代碼塊。 在代碼中如果過度依賴volatile變數來控制狀態的可見度,通常會比使用鎖的代碼更脆弱,也更難以理解。僅當volatile變數能簡化代碼的實現以及對同步策略的驗證時,才應該使用它。一般來說,用同步機制會更安全些。 加鎖機制(即同步機制)既可以確保可見度又可以確保原子性,而volatile變數只能確保可見度,原因是聲明為volatile的簡單變數如果當前值與該變數以前的值相關,那麼volatile關鍵字不起作用,也就是說如下的運算式都不是原子操作:“count++”、“count = count+1”。
若且唯若滿足以下所有條件時,才應該使用volatile變數: 對變數的寫入操作不依賴變數的當前值,或者你能確保只有單個線程更新變數的值。 該變數沒有包含在具有其他變數的不變式中。 Runnable和Thread實現多線程的區別
Java中實現多線程有兩種方法:繼承Thread類、實現Runnable介面,在程式開發中只要是多線程,肯定永遠以實現Runnable介面為主,因為實現Runnable介面相比繼承Thread類有如下優勢: 可以避免由於Java的單繼承特性而帶來的局限; 增強程式的健壯性,代碼能夠被多個線程共用,代碼與資料是獨立的; 適合多個相同程式碼的線程區處理同一資源的情況。 賣票執行個體
繼承Thread實現
class MyThread extends Thread{ private int ticket = 5; public void run(){ for (int i=0;i<10;i++) { if(ticket > 0){ System.out.println("ticket = " + ticket--); } } } } public class ThreadDemo{ public static void main(String[] args){ new MyThread().start(); new MyThread().start(); new MyThread().start(); } }
結果
Thread-2 sold ticket = 5 Thread-1 sold ticket = 5 Thread-0 sold ticket = 5 Thread-0 sold ticket = 4 Thread-1 sold ticket = 4 Thread-2 sold ticket = 4 Thread-1 sold ticket = 3 Thread-0 sold ticket = 3 Thread-1 sold ticket = 2 Thread-2 sold ticket = 3 Thread-1 sold ticket = 1 Thread-0 sold ticket = 2 Thread-0 sold ticket = 1 Thread-2 sold ticket = 2 Thread-2 sold ticket = 1
從結果中可以看出,每個線程單獨賣了5張票,即獨立地完成了買票的任務,但實際應用中,比如火車站售票,需要多個線程去共同完成任務,在本例中,即多個線程共同買5張票。
實現Runnable介面實現
/** * Created by roger on 2016/8/16. */ public class TestRunnable implements Runnable { private int ticket = 5; @Override public void run() { for(int i=0; i<20; i++){ if(ticket > 0){ System.out.println(Thread.currentThread().getName() + " sold ticket = " + ticket--); } } } public static void main(String[] args){ /*new Thread(new TestRunnable()).start(); new Thread(new TestRunnable()).start(); new Thread(new TestRunnable()).start();*/ TestRunnable testRunnable = new TestRunnable(); new Thread(testRunnable).start(); new Thread(testRunnable).start(); new Thread(testRunnable).start(); } }
結果
Thread-1 sold ticket = 4 Thread-2 sold ticket = 3 Thread-0 sold ticket = 5 Thread-2 sold ticket = 1 Thread-1 sold ticket = 2
從結果中可以看出,三個線程一共賣了5張票,即它們共同完成了買票的任務,實現了資源的共用。
更多內容請參考Java多線程基礎(2)