二、可運行狀態:當線程有資格運行,但調度程序尚未把它選定爲運行線程時線程所處的狀態。當start()方法調用時,線程首先進入可運行狀態。在線程運行以後或者從阻塞、等待或睡眠狀態回來後,也返回到可運行狀態。
四、等待/阻塞/睡眠狀態:這是線程有資格運行時它所處的狀態。實際上這個三狀態組合爲一種,其共同點是:線程仍舊是活的,可是當前沒有條件運行。換句話說,它是可運行的,可是若是某件事件出現,他可能返回到可運行狀態。
五、死亡態:當線程的run()方法完成時就認爲它死去。這個線程對象也許是活的,可是,它已經不是一個單獨執行的線程。線程一旦死亡,就不能 復生。 若是在一個死去的線程上調用start()方法,會拋出java.lang.IllegalThreadStateException異常。
2、阻止線程執行
對於線程的阻止,考慮一下三個方面,不考慮IO阻塞的狀況:
睡眠;
等待;
由於須要一個對象的鎖定而被阻塞。
一、睡眠
Thread.sleep(long millis)和Thread.sleep(long millis, int nanos)靜態方法強制當前正在執行的線程休眠(暫停執行),以「減慢線程」。當線程睡眠時,它入睡在某個地方,在甦醒以前不會返回到可運行狀態。當睡 眠時間到期,則返回到可運行狀態。
線程睡眠的緣由:線程執行太快,或者須要強制進入下一輪,由於Java規範不保證合理的輪換。
睡眠的實現:調用靜態方法。
try {
Thread.sleep(123);
} catch (InterruptedException e) {
e.printStackTrace();
}
睡眠的位置:爲了讓其餘線程有機會執行,能夠將Thread.sleep()的調用放線程run()以內。這樣才能保證該線程執行過程當中會睡眠。
注意:
一、線程睡眠是幫助全部線程得到運行機會的最好方法。
二、線程睡眠到期自動甦醒,並返回到可運行狀態,不是運行狀態。sleep()中指定的時間是線程不會運行的最短期。所以,sleep()方法不能保證該線程睡眠到期後就開始執行。
三、sleep()是靜態方法,只能控制當前正在運行的線程。
二、線程的優先級和線程讓步yield()
線程的讓步是經過Thread.
yield()來實現的。yield()方法的做用是:暫停當前正在執行的線程對象,並執行其餘線程。
要理解yield(),必須瞭解線程的優先級的概念。線程老是存在優先級,優先級範圍在1~10之間。JVM線程調度程序是基於優先級的搶先調度機制。在大多數狀況下,當前運行的線程優先級將大於或等於線程池中任何線程的優先級。但這僅僅是大多數狀況。
注意:當設計多線程應用程序的時候,必定不要依賴於線程的優先級。由於線程調度優先級操做是沒有保障的,只能把線程優先級做用做爲一種提升程序效率的方法,可是要保證程序不依賴這種操做。
當線程池中線程都具備相同的優先級,調度程序的JVM實現自由選擇它喜歡的線程。這時候調度程序的操做有兩種可能:一是選擇一個線程運行,直到它阻塞或者運行完成爲止。二是時間分片,爲池內的每一個線程提供均等的運行機會。
設置線程的優先級:線程默認的優先級是建立它的執行線程的優先級。能夠經過setPriority(int newPriority)更改線程的優先級。例如:
Thread t = new MyThread();
t.setPriority(8);
t.start();
線程優先級爲1~10之間的正整數,JVM從不會改變一個線程的優先級。然而,1~10之間的值是沒有保證的。一些JVM可能不能識別10個不一樣的值,而將這些優先級進行每兩個或多個合併,變成少於10個的優先級,則兩個或多個優先級的線程可能被映射爲一個優先級。
線程默認優先級是5,Thread類中有三個常量,定義線程優先級範圍:
static int MAX_PRIORITY
線程能夠具備的最高優先級。
static int MIN_PRIORITY
線程能夠具備的最低優先級。
static int NORM_PRIORITY
分配給線程的默認優先級。
三、Thread.yield()方法
Thread.yield()方法做用是:暫停當前正在執行的線程對象,並執行其餘線程。
yield()應該作的是讓當前運行線程回到可運行狀態,以容許具備相同優先級的其餘線程得到運行機會。所以,使用yield()的目的是讓相 同優先級的線程之間能適當的輪轉執行。可是,實際中沒法保證yield()達到讓步目的,由於讓步的線程還有可能被線程調度程序再次選中。
結論:yield()從未致使線程轉到等待/睡眠/阻塞狀態。在大多數狀況下,yield()將致使線程從運行狀態轉到可運行狀態,但有可能沒有效果。
四、join()方法
Thread的非靜態方法join()讓一個線程B「加入」到另一個線程A的尾部。在A執行完畢以前,B不能工做。例如:
Thread t = new MyThread();
t.start();
t.join();
另外,join()方法還有帶超時限制的重載版本。 例如t.join(5000);則讓線程等待5000毫秒,若是超過這個時間,則中止等待,變爲可運行狀態。
線程的加入join()對線程棧致使的結果是線程棧發生了變化,固然這些變化都是瞬時的。下面給示意圖:
小結
到目前位置,介紹了線程離開運行狀態的3種方法:
一、調用Thread.sleep():使當前線程睡眠至少多少毫秒(儘管它可能在指定的時間以前被中斷)。
二、調用Thread.yield():不能保障太多事情,儘管一般它會讓當前運行線程回到可運行性狀態,使得有相同優先級的線程有機會執行。
三、調用join()方法:保證當前線程中止執行,直到該線程所加入的線程完成爲止。然而,若是它加入的線程沒有存活,則當前線程不須要中止。
除了以上三種方式外,還有下面幾種特殊狀況可能使線程離開運行狀態:
一、線程的run()方法完成。
二、在對象上調用wait()方法(不是在線程上調用)。
三、線程不能在對象上得到鎖定,它正試圖運行該對象的方法代碼。
四、線程調度程序能夠決定將當前運行狀態移動到可運行狀態,以便讓另外一個線程得到運行機會,而不須要任何理由。
Java線程:線程的同步與鎖
1、同步問題提出
線程的同步是爲了防止多個線程訪問一個數據對象時,對數據形成的破壞。
例如:兩個線程ThreadA、ThreadB都操做同一個對象Foo對象,並修改Foo對象上的數據。
public
class Foo {
private
int x = 100;
public
int getX() {
return x;
}
public
int fix(
int y) {
x = x - y;
return x;
}
}
public
class MyRunnable
implements Runnable {
private Foo foo =
new Foo();
public
static
void main(String[] args) {
MyRunnable r =
new MyRunnable();
Thread ta =
new Thread(r,
"Thread-A");
Thread tb =
new Thread(r,
"Thread-B");
ta.start();
tb.start();
}
public
void run() {
for (
int i = 0; i < 3; i++) {
this.fix(30);
try {
Thread.sleep(1);
}
catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() +
" : 當前foo對象的x值= " + foo.getX());
}
}
public
int fix(
int y) {
return foo.fix(y);
}
}
運行結果:
Thread-A : 當前foo對象的x值= 40
Thread-B : 當前foo對象的x值= 40
Thread-B : 當前foo對象的x值= -20
Thread-A : 當前foo對象的x值= -50
Thread-A : 當前foo對象的x值= -80
Thread-B : 當前foo對象的x值= -80
Process finished with exit code 0
從結果發現,這樣的輸出值明顯是不合理的。緣由是兩個線程不加控制的訪問Foo對象並修改其數據所致。
若是要保持結果的合理性,只須要達到一個目的,就是將對Foo的訪問加以限制,每次只能有一個線程在訪問。這樣就能保證Foo對象中數據的合理性了。
在具體的Java代碼中須要完成一下兩個操做:
把競爭訪問的資源類Foo變量x標識爲private;
同步哪些修改變量的代碼,使用synchronized關鍵字同步方法或代碼。
2、同步和鎖定
一、鎖的原理
Java中每一個對象都有一個內置鎖
當程序運行到非靜態的synchronized同步方法上時,自動得到與正在執行代碼類的當前實例(this實例)有關的鎖。得到一個對象的鎖也稱爲獲取鎖、鎖定對象、在對象上鎖定或在對象上同步。
當程序運行到synchronized同步方法或代碼塊時才該對象鎖才起做用。
一個對象只有一個鎖。因此,若是一個線程得到該鎖,就沒有其餘線程能夠得到鎖,直到第一個線程釋放(或返回)鎖。這也意味着任何其餘線程都不能進入該對象上的synchronized方法或代碼塊,直到該鎖被釋放。
釋放鎖是指持鎖線程退出了synchronized同步方法或代碼塊。
關於鎖和同步,有一下幾個要點:
1)、只能同步方法,而不能同步變量和類;
2)、每一個對象只有一個鎖;當提到同步時,應該清楚在什麼上同步?也就是說,在哪一個對象上同步?
3)、沒必要同步類中全部的方法,類能夠同時擁有同步和非同步方法。
4)、若是兩個線程要執行一個類中的synchronized方法,而且兩個線程使用相同的實例來調用方法,那麼一次只能有一個線程可以執行方 法,另外一個須要等待,直到鎖被釋放。也就是說:若是一個線程在對象上得到一個鎖,就沒有任何其餘線程能夠進入(該對象的)類中的任何一個同步方法。
5)、若是線程擁有同步和非同步方法,則非同步方法能夠被多個線程自由訪問而不受鎖的限制。
6)、線程睡眠時,它所持的任何鎖都不會釋放。
7)、線程能夠得到多個鎖。好比,在一個對象的同步方法裏面調用另一個對象的同步方法,則獲取了兩個對象的同步鎖。
8)、同步損害併發性,應該儘量縮小同步範圍。同步不但能夠同步整個方法,還能夠同步方法中一部分代碼塊。
9)、在使用同步代碼塊時候,應該指定在哪一個對象上同步,也就是說要獲取哪一個對象的鎖。例如:
public int fix(int y) {
synchronized (this) {
x = x - y;
}
return x;
}
固然,同步方法也能夠改寫爲非同步方法,但功能徹底同樣的,例如:
public synchronized int getX() {
return x++;
}
與
public int getX() {
synchronized (this) {
return x;
}
}
效果是徹底同樣的。
3、靜態方法同步
要同步靜態方法,須要一個用於整個類對象的鎖,這個對象是就是這個類(XXX.class)。
例如:
public static synchronized int setName(String name){
Xxx.name = name;
}
等價於
public static int setName(String name){
synchronized(Xxx.class){
Xxx.name = name;
}
}
4、若是線程不能不能得到鎖會怎麼樣
若是線程試圖進入同步方法,而其鎖已經被佔用,則線程在該對象上被阻塞。實質上,線程進入該對象的的一種池中,必須在哪裏等待,直到其鎖被釋放,該線程再次變爲可運行或運行爲止。
當考慮阻塞時,必定要注意哪一個對象正被用於鎖定:
一、調用同一個對象中非靜態同步方法的線程將彼此阻塞。若是是不一樣對象,則每一個線程有本身的對象的鎖,線程間彼此互不干預。
二、調用同一個類中的靜態同步方法的線程將彼此阻塞,它們都是鎖定在相同的Class對象上。
三、靜態同步方法和非靜態同步方法將永遠不會彼此阻塞,由於靜態方法鎖定在Class對象上,非靜態方法鎖定在該類的對象上。
四、對於同步代碼塊,要看清楚什麼對象已經用於鎖定(synchronized後面括號的內容)。在同一個對象上進行同步的線程將彼此阻塞,在不一樣對象上鎖定的線程將永遠不會彼此阻塞。
5、什麼時候須要同步
在多個線程同時訪問互斥(可交換)數據時,應該同步以保護數據,確保兩個線程不會同時修改更改它。
對於非靜態字段中可更改的數據,一般使用非靜態方法訪問。
對於靜態字段中可更改的數據,一般使用靜態方法訪問。
若是須要在非靜態方法中使用靜態字段,或者在靜態字段中調用非靜態方法,問題將變得很是複雜。已經超出SJCP考試範圍了。
6、線程安全類
當一個類已經很好的同步以保護它的數據時,這個類就稱爲「線程安全的」。
即便是線程安全類,也應該特別當心,由於操做的線程是間仍然不必定安全。
舉個形象的例子,好比一個集合是線程安全的,有兩個線程在操做同一個集合對象,當第一個線程查詢集合非空後,刪除集合中全部元素的時候。第二個 線程也來執行與第一個線程相同的操做,也許在第一個線程查詢後,第二個線程也查詢出集合非空,可是當第一個執行清除後,第二個再執行刪除顯然是不對的,因 爲此時集合已經爲空了。
看個代碼:
public
class NameList {
private List nameList = Collections.synchronizedList(
new LinkedList());
public
void add(String name) {
nameList.add(name);
}
public String removeFirst() {
if (nameList.size() > 0) {
return (String) nameList.remove(0);
}
else {
return
null;
}
}
}
public
class Test {
public
static
void main(String[] args) {
final NameList nl =
new NameList();
nl.add(
"aaa");
class NameDropper
extends Thread{
public
void run(){
String name = nl.removeFirst();
System.out.println(name);
}
}
Thread t1 =
new NameDropper();
Thread t2 =
new NameDropper();
t1.start();
t2.start();
}
}
雖然集合對象
private List nameList = Collections.synchronizedList(new LinkedList());
是同步的,可是程序還不是線程安全的。
出現這種事件的緣由是,上例中一個線程操做列表過程當中沒法阻止另一個線程對列表的其餘操做。
解決上面問題的辦法是,在操做集合對象的NameList上面作一個同步。改寫後的代碼以下:
public
class NameList {
private List nameList = Collections.synchronizedList(
new LinkedList());
public
synchronized
void add(String name) {
nameList.add(name);
}
public
synchronized String removeFirst() {
if (nameList.size() > 0) {
return (String) nameList.remove(0);
}
else {
return
null;
}
}
}
這樣,當一個線程訪問其中一個同步方法時,其餘線程只有等待。
7、線程死鎖
死鎖對Java程序來講,是很複雜的,也很難發現問題。當兩個線程被阻塞,每一個線程在等待另外一個線程時就發生死鎖。
仍是看一個比較直觀的死鎖例子:
public
class DeadlockRisk {
private
static
class Resource {
public
int value;
}
private Resource resourceA =
new Resource();
private Resource resourceB =
new Resource();
public
int read() {
synchronized (resourceA) {
synchronized (resourceB) {
return resourceB.value + resourceA.value;
}
}
}
public
void write(
int a,
int b) {
synchronized (resourceB) {
synchronized (resourceA) {
resourceA.value = a;
resourceB.value = b;
}
}
}
}
假設read()方法由一個線程啓動,write()方法由另一個線程啓動。讀線程將擁有resourceA鎖,寫線程將擁有resourceB鎖,二者都堅持等待的話就出現死鎖。
實際上,上面這個例子發生死鎖的機率很小。由於在代碼內的某個點,CPU必須從讀線程切換到寫線程,因此,死鎖基本上不能發生。
可是,不管代碼中發生死鎖的機率有多小,一旦發生死鎖,程序就死掉。有一些設計方法能幫助避免死鎖,包括始終按照預約義的順序獲取鎖這一策略。已經超出SCJP的考試範圍。
8、線程同步小結
一、線程同步的目的是爲了保護多個線程反問一個資源時對資源的破壞。
二、線程同步方法是經過鎖來實現,每一個對象都有切僅有一個鎖,這個鎖與一個特定的對象關聯,線程一旦獲取了對象鎖,其餘訪問該對象的線程就沒法再訪問該對象的其餘同步方法。
三、對於靜態同步方法,鎖是針對這個類的,鎖對象是該類的Class對象。靜態和非靜態方法的鎖互不干預。一個線程得到鎖,當在一個同步方法中訪問另外對象上的同步方法時,會獲取這兩個對象鎖。
四、對於同步,要時刻清醒在哪一個對象上同步,這是關鍵。
五、編寫線程安全的類,須要時刻注意對多個線程競爭訪問資源的邏輯和安全作出正確的判斷,對「原子」操做作出分析,並保證原子操做期間別的線程沒法訪問競爭資源。
六、當多個線程等待一個對象鎖時,沒有獲取到鎖的線程將發生阻塞。
七、死鎖是線程間相互等待鎖鎖形成的,在實際中發生的機率很是的小。真讓你寫個死鎖程序,不必定好使,呵呵。可是,一旦程序發生死鎖,程序將死掉。
Java線程:線程的交互
線程交互是比較複雜的問題,SCJP要求不很基礎:給定一個場景,編寫代碼來恰當使用等待、通知和通知全部線程。
1、線程交互的基礎知識
SCJP所要求的線程交互知識點須要從
java.lang.Object的類的三個方法來學習:
void notify()
喚醒在此對象監視器上等待的單個線程。
void notifyAll()
喚醒在此對象監視器上等待的全部線程。
void wait()
致使當前的線程等待,直到其餘線程調用此對象的 notify() 方法或 notifyAll() 方法。
固然,wait()還有另外兩個重載方法:
void wait(long timeout)
致使當前的線程等待,直到其餘線程調用此對象的 notify() 方法或 notifyAll() 方法,或者超過指定的時間量。
void wait(long timeout, int nanos)
致使當前的線程等待,直到其餘線程調用此對象的 notify() 方法或 notifyAll() 方法,或者其餘某個線程中斷當前線程,或者已超過某個實際時間量。
以上這些方法是幫助線程傳遞線程關心的時間狀態。
關於等待/通知,要記住的關鍵點是:
必須從同步環境內調用wait()、notify()、notifyAll()方法。線程不能調用對象上等待或通知的方法,除非它擁有那個對象的鎖。
wait()、notify()、notifyAll()都是Object的實例方法。與每一個對象具備鎖同樣,每一個對象能夠有一個線程列表,他 們等待來自該信號(通知)。線程經過執行對象上的wait()方法得到這個等待列表。從那時候起,它再也不執行任何其餘指令,直到調用對象的 notify()方法爲止。若是多個線程在同一個對象上等待,則將只選擇一個線程(不保證以何種順序)繼續執行。若是沒有線程等待,則不採起任何特殊操 做。
下面看個例子就明白了:
public
class ThreadA {
public
static
void main(String[] args) {
ThreadB b =
new ThreadB();
//啓動計算線程
b.start();
//線程A擁有b對象上的鎖。線程爲了調用wait()或notify()方法,該線程必須是那個對象鎖的擁有者
synchronized (b) {
try {
System.out.println(
"等待對象b完成計算。。。");
//當前線程A等待
b.wait();
}
catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(
"b對象計算的總和是:" + b.total);
}
}
}
public
class ThreadB
extends Thread {
int total;
public
void run() {
synchronized (
this) {
for (
int i = 0; i < 101; i++) {
total += i;
}
//(完成計算了)喚醒在此對象監視器上等待的單個線程,在本例中線程A被喚醒
notify();
}
}
}
等待對象b完成計算。。。
b對象計算的總和是:5050
Process finished with exit code 0
千萬注意:
當在對象上調用wait()方法時,執行該代碼的線程當即放棄它在對象上的鎖。然而調用notify()時,並不意味着這時線程會放棄其鎖。若是線程榮然在完成同步代碼,則線程在移出以前不會放棄鎖。所以,只要調用notify()並不意味着這時該鎖變得可用。
2、多個線程在等待一個對象鎖時候使用notifyAll()
在多數狀況下,最好通知等待某個對象的全部線程。若是這樣作,能夠在對象上使用notifyAll()讓全部在此對象上等待的線程衝出等待區,返回到可運行狀態。
下面給個例子:
public
class Calculator
extends Thread {
int total;
public
void run() {
synchronized (
this) {
for (
int i = 0; i < 101; i++) {
total += i;
}
}
//通知全部在此對象上等待的線程
notifyAll();
}
}
public
class ReaderResult
extends Thread {
Calculator c;
public ReaderResult(Calculator c) {
this.c = c;
}
public
void run() {
synchronized (c) {
try {
System.out.println(Thread.currentThread() +
"等待計算結果。。。");
c.wait();
}
catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread() +
"計算結果爲:" + c.total);
}
}
public
static
void main(String[] args) {
Calculator calculator =
new Calculator();
//啓動三個線程,分別獲取計算結果
new ReaderResult(calculator).start();
new ReaderResult(calculator).start();
new ReaderResult(calculator).start();
//啓動計算線程
calculator.start();
}
}
運行結果:
Thread[Thread-1,5,main]等待計算結果。。。
Thread[Thread-2,5,main]等待計算結果。。。
Thread[Thread-3,5,main]等待計算結果。。。
Exception in thread
"Thread-0" java.lang.IllegalMonitorStateException: current thread not owner
at java.lang.Object.notifyAll(Native Method)
at threadtest.Calculator.run(Calculator.java:18)
Thread[Thread-1,5,main]計算結果爲:5050
Thread[Thread-2,5,main]計算結果爲:5050
Thread[Thread-3,5,main]計算結果爲:5050
Process finished with exit code 0
運行結果代表,程序中有異常,而且屢次運行結果可能有多種輸出結果。這就是說明,這個多線程的交互程序還存在問題。到底是出了什麼問題,須要深刻的分析和思考,下面將作具體分析。
實際上,上面這個代碼中,咱們指望的是讀取結果的線程在計算線程調用notifyAll()以前等待便可。 可是,若是計算線程先執行,並在讀取結果線程等待以前調用了notify()方法,那麼又會發生什麼呢?這種狀況是可能發生的。由於沒法保證線程的不一樣部 分將按照什麼順序來執行。幸運的是當讀取線程運行時,它只能立刻進入等待狀態----它沒有作任何事情來檢查等待的事件是否已經發生。 ----所以,若是計算線程已經調用了notifyAll()方法,那麼它就不會再次調用notifyAll(),----而且等待的讀取線程將永遠保持 等待。這固然是開發者所不肯意看到的問題。
所以,當等待的事件發生時,須要可以檢查notifyAll()通知事件是否已經發生。
線程休眠的目的是使線程讓出CPU的最簡單的作法之一,線程休眠時候,會將CPU資源交給其餘線程,以便能輪換執行,當休眠必定時間後,線程會甦醒,進入準備狀態等待執行。
線程休眠的方法是Thread.sleep(long millis) 和Thread.sleep(long millis, int nanos) ,均爲靜態方法,那調用sleep休眠的哪一個線程呢?簡單說,哪一個線程調用sleep,就休眠哪一個線程。