進程:每個進程都有獨立的代碼和數據空間(進程上下文),進程間的切換會有較大的開銷,一個進程包含1--n個線程。(進程是資源分配的最小單位)
線程:同一類線程共享代碼和數據空間,每個線程有獨立的運行棧和程序計數器(PC),線程切換開銷小。(線程是cpu調度的最小單位)
線程和進程一樣分爲五個階段:創建、就緒、運行、阻塞、終止。
多進程是指操作系統能同時運行多個任務(程序)。
多線程是指在同一程序中有多個順序流在執行。
線程對象是可以產生線程的對象。比如在Java平臺中Thread對象,Runnable對象。線程,是指正在執行的一個指點令序列。在java平臺上是指從一個線程對象的start()開始,運行run方法體中的那一段相對獨立的過程。相比於多進程,多線程的優勢有:
(1)進程之間不能共享數據,線程可以;
(2)系統創建進程需要爲該進程重新分配系統資源,故創建線程代價比較小;
(3)Java語言內置了多線程功能支持,簡化了java多線程編程。
一、創建線程和啓動
(1)繼承Thread類創建線程類
通過繼承Thread類創建線程類的具體步驟和具體代碼如下:
• 定義一個繼承Thread類的子類,並重寫該類的run()方法;
• 創建Thread子類的實例,即創建了線程對象;
• 調用該線程對象的start()方法啓動線程。
class SomeThead extends Thraad {
public void run() {
//do something here
}
}
public static void main(String[] args){
SomeThread oneThread = new SomeThread();
步驟3:啓動線程:
oneThread.start();
}
(2)實現Runnable接口創建線程類(推薦使用)
通過實現Runnable接口創建線程類的具體步驟和具體代碼如下:
• 定義Runnable接口的實現類,並重寫該接口的run()方法;
• 創建Runnable實現類的實例,並以此實例作爲Thread的target對象,即該Thread對象纔是真正的線程對象。
class SomeRunnable implements Runnable {
public void run() {
//do something here
}
}
Runnable oneRunnable = new SomeRunnable();
Thread oneThread = new Thread(oneRunnable);
oneThread.start();
(3)通過Callable和Future創建線程
通過Callable和Future創建線程的具體步驟和具體代碼如下:
• 創建Callable接口的實現類,並實現call()方法,該call()方法將作爲線程執行體,並且有返回值。
• 創建Callable實現類的實例,使用FutureTask類來包裝Callable對象,該FutureTask對象封裝了該Callable對象的call()方法的返回值。
• 使用FutureTask對象作爲Thread對象的target創建並啓動新線程。
• 調用FutureTask對象的get()方法來獲得子線程執行結束後的返回值其中,Callable接口(也只有一個方法)定義如下:
public interface Callable {
V call() throws Exception;
}
步驟1:創建實現Callable接口的類SomeCallable(略);
步驟2:創建一個類對象:
Callable oneCallable = new SomeCallable();
步驟3:由Callable創建一個FutureTask對象:
FutureTask oneTask = new FutureTask(oneCallable);
註釋: FutureTask是一個包裝器,它通過接受Callable來創建,它同時實現了 Future和Runnable接口。
步驟4:由FutureTask創建一個Thread對象:
Thread oneThread = new Thread(oneTask);
步驟5:啓動線程:
oneThread.start();
注意:start()方法的調用後並不是立即執行多線程代碼,而是使得該線程變爲可運行態(Runnable),什麼時候運行是由操作系統決定的。
多線程程序是亂序執行。因此,只有亂序執行的代碼纔有必要設計爲多線程。
Thread.sleep()方法調用目的是不讓當前線程獨自霸佔該進程所獲取的CPU資源,以留出一定時間給其他線程執行的機會。
實際上所有的多線程代碼執行順序都是不確定的,每次執行的結果都是隨機的。
Thread和Runnable的區別
如果一個類繼承Thread,則不適合資源共享。但是如果實現了Runable接口的話,則很容易的實現資源共享。
總結:
實現Runnable接口比繼承Thread類所具有的優勢:
1):適合多個相同的程序代碼的線程去處理同一個資源
2):可以避免java中的單繼承的限制
3):增加程序的健壯性,代碼可以被多個線程共享,代碼和數據獨立
4):線程池只能放入實現Runable或callable類線程,不能直接放入繼承Thread的類
提醒一下大家:main方法其實也是一個線程。在java中所以的線程都是同時啓動的,至於什麼時候,哪個先執行,完全看誰先得到CPU的資源。
二、線程的生命週期
1、新建狀態
用new關鍵字和Thread類或其子類建立一個線程對象後,該線程對象就處於新生狀態。處於新生狀態的線程有自己的內存空間,通過調用start方法進入就緒狀態(runnable)。
注意:不能對已經啓動的線程再次調用start()方法,否則會出現Java.lang.IllegalThreadStateException異常。
2、就緒狀態
處於就緒狀態的線程已經具備了運行條件,但還沒有分配到CPU,處於線程就緒隊列(儘管是採用隊列形式,事實上,把它稱爲可運行池而不是可運行隊列。因爲cpu的調度不一定是按照先進先出的順序來調度的),等待系統爲其分配CPU。等待狀態並不是執行狀態,當系統選定一個等待執行的Thread對象後,它就會從等待執行狀態進入執行狀態,系統挑選的動作稱之爲“cpu調度”。一旦獲得CPU,線程就進入運行狀態並自動調用自己的run方法。
提示:如果希望子線程調用start()方法後立即執行,可以使用Thread.sleep()方式使主線程睡眠一夥兒,轉去執行子線程。
3、運行狀態
處於運行狀態的線程最爲複雜,它可以變爲阻塞狀態、就緒狀態和死亡狀態。
處於就緒狀態的線程,如果獲得了cpu的調度,就會從就緒狀態變爲運行狀態,執行run()方法中的任務。如果該線程失去了cpu資源,就會又從運行狀態變爲就緒狀態。重新等待系統分配資源。也可以對在運行狀態的線程調用yield()方法,它就會讓出cpu資源,再次變爲就緒狀態。
注: 當發生如下情況是,線程會從運行狀態變爲阻塞狀態:
①、線程調用sleep方法主動放棄所佔用的系統資源
②、線程調用一個阻塞式IO方法,在該方法返回之前,該線程被阻塞
③、線程試圖獲得一個同步監視器,但更改同步監視器正被其他線程所持有
④、線程在等待某個通知(notify)
⑤、程序調用了線程的suspend方法將線程掛起。不過該方法容易導致死鎖,所以程序應該儘量避免使用該方法。
當線程的run()方法執行完,或者被強制性地終止,例如出現異常,或者調用了stop()、desyory()方法等等,就會從運行狀態轉變爲死亡狀態。
4、阻塞狀態
處於運行狀態的線程在某些情況下,如執行了sleep(睡眠)方法,或等待I/O設備等資源,將讓出CPU並暫時停止自己的運行,進入阻塞狀態。
在阻塞狀態的線程不能進入就緒隊列。只有當引起阻塞的原因消除時,如睡眠時間已到,或等待的I/O設備空閒下來,線程便轉入就緒狀態,重新到就緒隊列中排隊等待,被系統選中後從原來停止的位置開始繼續運行。
阻塞狀態(Blocked):阻塞狀態是線程因爲某種原因放棄CPU使用權,暫時停止運行。直到線程進入就緒狀態,纔有機會轉到運行狀態。阻塞的情況分三種:
(一)、等待阻塞:運行的線程執行wait()方法,JVM會把該線程放入等待池中。(wait會釋放持有的鎖)
(二)、同步阻塞:運行的線程在獲取對象的同步鎖時,若該同步鎖被別的線程佔用,則JVM會把該線程放入鎖池中。
(三)、其他阻塞:運行的線程執行sleep()或join()方法,或者發出了I/O請求時,JVM會把該線程置爲阻塞狀態。當sleep()狀態超時、join()等待線程終止或者超時、或者I/O處理完畢時,線程重新轉入就緒狀態。(注意,sleep是不會釋放持有的鎖)
5、死亡狀態
當線程的run()方法執行完,或者被強制性地終止,就認爲它死去。這個線程對象也許是活的,但是,它已經不是一個單獨執行的線程。線程一旦死亡,就不能復生。 如果在一個死去的線程上調用start()方法,會拋出java.lang.IllegalThreadStateException異常。
三、線程管理
Java提供了一些便捷的方法用於會線程狀態的控制。具體如下:
1、線程睡眠——sleep
如果我們需要讓當前正在執行的線程暫停一段時間,並進入阻塞狀態,則可以通過調用Thread的sleep方法。
注:
(1)sleep是靜態方法,最好不要用Thread的實例對象調用它,因爲它睡眠的始終是當前正在運行的線程,而不是調用它的線程對象,它只對正在運行狀態的線程對象有效。如下面的例子:
public class Test1 {
public static void main(String[] args) throws InterruptedException {
System.out.println(Thread.currentThread().getName());
MyThread myThread=new MyThread();
myThread.start();
myThread.sleep(1000);//這裏sleep的就是main線程,而非myThread線程
Thread.sleep(10);
for(int i=0;i<100;i++){
System.out.println("main"+i);
}
}
}
(2)Java線程調度是Java多線程的核心,只有良好的調度,才能充分發揮系統的性能,提高程序的執行效率。但是不管程序員怎麼編寫調度,只能最大限度的影響線程執行的次序,而不能做到精準控制。因爲使用sleep方法之後,線程是進入阻塞狀態的,只有當睡眠的時間結束,纔會重新進入到就緒狀態,而就緒狀態進入到運行狀態,是由系統控制的,我們不可能精準的去幹涉它,所以如果調用Thread.sleep(1000)使得線程睡眠1秒,可能結果會大於1秒。
2、線程讓步——yield
yield()方法和sleep()方法有點相似,它也是Thread類提供的一個靜態的方法,它也可以讓當前正在執行的線程暫停,讓出cpu資源給其他的線程。但是和sleep()方法不同的是,它不會進入到阻塞狀態,而是進入到就緒狀態。yield()方法只是讓當前線程暫停一下,重新進入就緒的線程池中,讓系統的線程調度器重新調度器重新調度一次,完全可能出現這樣的情況:當某個線程調用yield()方法之後,線程調度器又將其調度出來重新進入到運行狀態執行。
實際上,當某個線程調用了yield()方法暫停之後,優先級與當前線程相同,或者優先級比當前線程更高的就緒狀態的線程更有可能獲得執行的機會,當然,只是有可能,因爲我們不可能精確的干涉cpu調度線程。用法如下:
public class Test1 {
public static void main(String[] args) throws InterruptedException {
new MyThread("低級", 1).start();
new MyThread("中級", 5).start();
new MyThread("高級", 10).start();
}
}
class MyThread extends Thread {
public MyThread(String name, int pro) {
super(name);// 設置線程的名稱
this.setPriority(pro);// 設置優先級
}
@Override
public void run() {
for (int i = 0; i < 30; i++) {
System.out.println(this.getName() + "線程第" + i + "次執行!");
if (i % 5 == 0)
Thread.yield();
}
}
}
注:關於sleep()方法和yield()方的區別如下:
①、sleep方法暫停當前線程後,會進入阻塞狀態,只有當睡眠時間到了,纔會轉入就緒狀態。而yield方法調用後 ,是直接進入就緒狀態,所以有可能剛進入就緒狀態,又被調度到運行狀態。
②、sleep方法聲明拋出了InterruptedException,所以調用sleep方法的時候要捕獲該異常,或者顯示聲明拋出該異常。而yield方法則沒有聲明拋出任務異常。
③、sleep方法比yield方法有更好的可移植性,通常不要依靠yield方法來控制併發線程的執行。
3、線程合併——join
線程的合併的含義就是將幾個並行線程的線程合併爲一個單線程執行,應用場景是當一個線程必須等待另一個線程執行完畢才能執行時,Thread類提供了join方法來完成這個功能,注意,它不是靜態方法。
從上面的方法的列表可以看到,它有3個重載的方法:
void join()
當前線程等該加入該線程後面,等待該線程終止。
void join(long millis)
當前線程等待該線程終止的時間最長爲 millis 毫秒。 如果在millis時間內,該線程沒有執行完,那麼當前線程進入就緒狀態,重新等待cpu調度
void join(long millis,int nanos)
等待該線程終止的時間最長爲 millis 毫秒 + nanos 納秒。如果在millis時間內,該線程沒有執行完,那麼當前線程進入就緒狀態,重新等待cpu調度
A線程調用了B線程的join方法,那麼A等待B執行完畢之後再執行(A釋放CPU執行權)
示例:主線程讓子線程執行完畢再執行
public class ThreadDemo {
public static void main(String[] args) throws InterruptedException {
Thread thread = new Thread(() -> {
for (int i = 0; i < 60; i++) {
System.out.println("子線程i:" + i);
}
});
thread.start();
thread.join();
for (int i = 0; i < 10; i++) {
System.out.println("主線程i:" + i);
}
System.out.println("主線程執行完畢");
}
}
觀察輸出發現:子線程打印完59,纔開始主線程的打印
4、設置線程的優先級
每個線程執行時都有一個優先級的屬性,優先級高的線程可以獲得較多的執行機會,而優先級低的線程則獲得較少的執行機會。與線程休眠類似,線程的優先級仍然無法保障線程的執行次序。只不過,優先級高的線程獲取CPU資源的概率較大,優先級低的也並非沒機會執行。
每個線程默認的優先級都與創建它的父線程具有相同的優先級,在默認情況下,main線程具有普通優先級。
注:Thread類提供了setPriority(int newPriority)和getPriority()方法來設置和返回一個指定線程的優先級,其中setPriority方法的參數是一個整數,範圍是1~·0之間,也可以使用Thread類提供的三個靜態常量:
MAX_PRIORITY =10
MIN_PRIORITY =1
NORM_PRIORITY =5
public class Test1 {
public static void main(String[] args) throws InterruptedException {
new MyThread("高級", 10).start();
new MyThread("低級", 1).start();
}
}
class MyThread extends Thread {
public MyThread(String name,int pro) {
super(name);//設置線程的名稱
setPriority(pro);//設置線程的優先級
}
@Override
public void run() {
for (int i = 0; i < 100; i++) {
System.out.println(this.getName() + "線程第" + i + "次執行!");
}
}
}
注:雖然Java提供了10個優先級別,但這些優先級別需要操作系統的支持。不同的操作系統的優先級並不相同,而且也不能很好的和Java的10個優先級別對應。所以我們應該使用MAX_PRIORITY、MIN_PRIORITY和NORM_PRIORITY三個靜態常量來設定優先級,這樣才能保證程序最好的可移植性。
5、守護線程
在Java程序中,有主線程和GC線程(用於回收垃圾),主線程死亡後,GC線程也會死亡,同時銷燬,這種和主線程一起銷燬的線程就是守護線程。
非守護線程:線程的狀態和主線程無關
守護線程使用的情況較少,但並非無用,舉例來說,JVM的垃圾回收、內存管理等線程都是守護線程。還有就是在做數據庫應用時候,使用的數據庫連接池,連接池本身也包含着很多後臺線程,監控連接個數、超時時間、狀態等等。調用線程對象的方法setDaemon(true),則可以將其設置爲守護線程。守護線程的用途爲:
• 守護線程通常用於執行一些後臺作業,例如在你的應用程序運行時播放背景音樂,在文字編輯器裏做自動語法檢查、自動保存等功能。
• Java的垃圾回收也是一個守護線程。守護線的好處就是你不需要關心它的結束問題。例如你在你的應用程序運行的時候希望播放背景音樂,如果將這個播放背景音樂的線程設定爲非守護線程,那麼在用戶請求退出的時候,不僅要退出主線程,還要通知播放背景音樂的線程退出;如果設定爲守護線程則不需要了。
setDaemon方法的詳細說明:
public final void setDaemon(boolean on) 將該線程標記爲守護線程或用戶線程。當正在運行的線程都是守護線程時,Java 虛擬機退出。
該方法必須在啓動線程前調用。 該方法首先調用該線程的 checkAccess 方法,且不帶任何參數。這可能拋出 SecurityException(在當前線程中)。
參數:
on - 如果爲 true,則將該線程標記爲守護線程。
拋出:
IllegalThreadStateException - 如果該線程處於活動狀態。
SecurityException - 如果當前線程無法修改該線程。
注:JRE判斷程序是否執行結束的標準是所有的前臺執線程行完畢了,而不管後臺線程的狀態,因此,在使用後臺縣城時候一定要注意這個問題。
6、正確結束線程
Thread.stop()、Thread.suspend、Thread.resume、Runtime.runFinalizersOnExit這些終止線程運行的方法已經被廢棄了,使用它們是極端不安全的!想要安全有效的結束一個線程,可以使用下面的方法:
• 正常執行完run方法,然後結束掉;
• 控制循環條件和判斷條件的標識符來結束掉線程。
class MyThread extends Thread {
int i=0;
boolean next=true;
@Override
public void run() {
while (next) {
if(i==10)
next=false;
i++;
System.out.println(i);
}
}
}
四、線程同步
java允許多線程併發控制,當多個線程同時操作一個可共享的資源變量時(如數據的增刪改查),將會導致數據不準確,相互之間產生衝突,因此加入同步鎖以避免在該線程沒有完成操作之前,被其他線程的調用,從而保證了該變量的唯一性和準確性。
1、線程安全問題:
當多個線程共享同一個全局變量,做寫的操作時候,會發生線程安全問題。
模擬線程安全問題:車站賣票經典案例
public class ThreadDemo implements Runnable {
//一共有一百張票
private int count = 100;
@Override
public void run() {
while (count > 0) {
try {
Thread.sleep(100);
sale();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
private void sale() {
if (count > 0) {
System.out.println(Thread.currentThread().getName() + "出售第" + (100 - count + 1) + "張票");
count--;
}
}
public static void main(String[] args) {
ThreadDemo threadDemo = new ThreadDemo();
Thread t1 = new Thread(threadDemo, "窗口1");
Thread t2 = new Thread(threadDemo, "窗口2");
t1.start();
t2.start();
}
}
觀察輸出發現:很多票重複出售
2、線程安全問題解決:
1.在sale方法上使用synchronized關鍵字,即有synchronized關鍵字修飾的方法。由於java的每個對象都有一個內置鎖,當用此關鍵字修飾方法時,內置鎖會保護整個方法。在調用該方法前,需要獲得內置鎖,否則就處於阻塞狀態。
原理:當線程進入該方法時候會自動獲取鎖,一旦某線程獲取了鎖,其他線程就會等待,等到執行完畢該線程代碼,釋放鎖
缺點:降低程序效率,每次執行該方法都需要進行判斷。
private synchronized void sale() {
if (count > 0) {
System.out.println(Thread.currentThread().getName() + "出售第" + (100 - count + 1) + "張票");
count--;
}
}
2.使用同步代碼塊
注: synchronized關鍵字也可以修飾靜態方法,此時如果調用該靜態方法,將會鎖住整個類
即有synchronized關鍵字修飾的語句塊。被該關鍵字修飾的語句塊會自動被加上內置鎖,從而實現同步。
注:同步是一種高開銷的操作,因此應該儘量減少同步的內容。通常沒有必要同步整個方法,使用synchronized代碼塊同步關鍵代碼即可。
public class ThreadDemo implements Runnable {
//一共有一百張票
private int count = 100;
private final Object object = new Object();
@Override
public void run() {
while (count > 0) {
try {
Thread.sleep(100);
sale();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
private void sale() {
synchronized (object) {
if (count > 0) {
System.out.println(Thread.currentThread().getName() + "出售第" + (100 - count + 1) + "張票");
count--;
}
}
}
public static void main(String[] args) {
ThreadDemo threadDemo = new ThreadDemo();
Thread t1 = new Thread(threadDemo, "窗口1");
Thread t2 = new Thread(threadDemo, "窗口2");
t1.start();
t2.start();
}
}
觀察輸出:問題解決。
注意:如果寫成這樣還是存在問題
public static void main(String[] args) {
ThreadDemo threadDemo1 = new ThreadDemo();
ThreadDemo threadDemo2 = new ThreadDemo();
Thread t1 = new Thread(threadDemo1, "窗口1");
Thread t2 = new Thread(threadDemo2, "窗口2");
t1.start();
t2.start();
}
這時候需要給全局變量加上static關鍵字:共享同一個鎖
private static int count = 100;
private static final Object object = new Object();
3、使用特殊域變量(volatile)實現線程同步
• volatile關鍵字爲域變量的訪問提供了一種免鎖機制;
• 使用volatile修飾域相當於告訴虛擬機該域可能會被其他線程更新;
• 因此每次使用該域就要重新計算,而不是使用寄存器中的值;
• volatile不會提供任何原子操作,它也不能用來修飾final類型的變量。
public class SynchronizedThread {
class Bank {
private volatile int account = 100;
public int getAccount() {
return account;
}
/**
* 用同步方法實現
*
* @param money
*/
public synchronized void save(int money) {
account += money;
}
/**
* 用同步代碼塊實現
*
* @param money
*/
public void save1(int money) {
synchronized (this) {
account += money;
}
}
}
class NewThread implements Runnable {
private Bank bank;
public NewThread(Bank bank) {
this.bank = bank;
}
@Override
public void run() {
for (int i = 0; i < 10; i++) {
// bank.save1(10);
bank.save(10);
System.out.println(i + "賬戶餘額爲:" +bank.getAccount());
}
}
}
/**
* 建立線程,調用內部類
*/
public void useThread() {
Bank bank = new Bank();
NewThread new_thread = new NewThread(bank);
System.out.println("線程1");
Thread thread1 = new Thread(new_thread);
thread1.start();
System.out.println("線程2");
Thread thread2 = new Thread(new_thread);
thread2.start();
}
public static void main(String[] args) {
SynchronizedThread st = new SynchronizedThread();
st.useThread();
}
注:多線程中的非同步問題主要出現在對域的讀寫上,如果讓域自身避免這個問題,則就不需要修改操作該域的方法。用final域,有鎖保護的域和volatile域可以避免非同步的問題。
4、使用重入鎖(Lock)實現線程同步
在JavaSE5.0中新增了一個java.util.concurrent包來支持同步。ReentrantLock類是可重入、互斥、實現了Lock接口的鎖,它與使用synchronized方法和快具有相同的基本行爲和語義,並且擴展了其能力。ReenreantLock類的常用方法有:
ReentrantLock() : 創建一個ReentrantLock實例
lock() : 獲得鎖
unlock() : 釋放鎖
注:ReentrantLock()還有一個可以創建公平鎖的構造方法,但由於能大幅度降低程序運行效率,不推薦使用
//只給出要修改的代碼,其餘代碼與上同
class Bank {
private int account = 100;
//需要聲明這個鎖
private Lock lock = new ReentrantLock();
public int getAccount() {
return account;
}
//這裏不再需要synchronized
public void save(int money) {
lock.lock();
try{
account += money;
}finally{
lock.unlock();
}
}
}
五、線程通信
1、藉助於Object類的wait()、notify()和notifyAll()實現通信
線程執行wait()後,就放棄了運行資格,處於凍結狀態;
線程運行時,內存中會建立一個線程池,凍結狀態的線程都存在於線程池中,notify()執行時喚醒的也是線程池中的線程,線程池中有多個線程時喚醒第一個被凍結的線程。
notifyall(), 喚醒線程池中所有線程。
注: (1) wait(), notify(),notifyall()都用在同步裏面,因爲這3個函數是對持有鎖的線程進行操作,而只有同步纔有鎖,所以要使用在同步中;
(2) wait(),notify(),notifyall(), 在使用時必須標識它們所操作的線程持有的鎖,因爲等待和喚醒必須是同一鎖下的線程;而鎖可以是任意對象,所以這3個方法都是Object類中的方法。
單個消費者生產者例子如下:
class Resource{ //生產者和消費者都要操作的資源
private String name;
private int count=1;
private boolean flag=false;
public synchronized void set(String name){
if(flag)
try{wait();}catch(Exception e){}
this.name=name+"---"+count++;
System.out.println(Thread.currentThread().getName()+"...生產者..."+this.name);
flag=true;
this.notify();
}
public synchronized void out(){
if(!flag)
try{wait();}catch(Exception e){}
System.out.println(Thread.currentThread().getName()+"...消費者..."+this.name);
flag=false;
this.notify();
}
}
class Producer implements Runnable{
private Resource res;
Producer(Resource res){
this.res=res;
}
public void run(){
while(true){
res.set("商品");
}
}
}
class Consumer implements Runnable{
private Resource res;
Consumer(Resource res){
this.res=res;
}
public void run(){
while(true){
res.out();
}
}
}
public class ProducerConsumerDemo{
public static void main(String[] args){
Resource r=new Resource();
Producer pro=new Producer(r);
Consumer con=new Consumer(r);
Thread t1=new Thread(pro);
Thread t2=new Thread(con);
t1.start();
t2.start();
}
}//運行結果正常,生產者生產一個商品,緊接着消費者消費一個商品。
但是如果有多個生產者和多個消費者,上面的代碼是有問題,比如2個生產者,2個消費者,運行結果就可能出現生產的1個商品生產了一次而被消費了2次,或者連續生產2個商品而只有1個被消費,這是因爲此時共有4個線程在操作Resource對象r, 而notify()喚醒的是線程池中第1個wait()的線程,所以生產者執行notify()時,喚醒的線程有可能是另1個生產者線程,這個生產者線程從wait()中醒來後不會再判斷flag,而是直接向下運行打印出一個新的商品,這樣就出現了連續生產2個商品。
爲了避免這種情況,修改代碼如下:
class Resource{
private String name;
private int count=1;
private boolean flag=false;
public synchronized void set(String name){
while(flag) /*原先是if,現在改成while,這樣生產者線程從凍結狀態醒來時,還會再判斷flag.*/
try{wait();}catch(Exception e){}
this.name=name+"---"+count++;
System.out.println(Thread.currentThread().getName()+"...生產者..."+this.name);
flag=true;
this.notifyAll();/*原先是notity(), 現在改成notifyAll(),這樣生產者線程生產完一個商品後可以將等待中的消費者線程喚醒,否則只將上面改成while後,可能出現所有生產者和消費者都在wait()的情況。*/
}
public synchronized void out(){
while(!flag) /*原先是if,現在改成while,這樣消費者線程從凍結狀態醒來時,還會再判斷flag.*/
try{wait();}catch(Exception e){}
System.out.println(Thread.currentThread().getName()+"...消費者..."+this.name);
flag=false;
this.notifyAll(); /*原先是notity(), 現在改成notifyAll(),這樣消費者線程消費完一個商品後可以將等待中的生產者線程喚醒,否則只將上面改成while後,可能出現所有生產者和消費者都在wait()的情況。*/
}
}
public class ProducerConsumerDemo{
public static void main(String[] args){
Resource r=new Resource();
Producer pro=new Producer(r);
Consumer con=new Consumer(r);
Thread t1=new Thread(pro);
Thread t2=new Thread(con);
Thread t3=new Thread(pro);
Thread t4=new Thread(con);
t1.start();
t2.start();
t3.start();
t4.start();
}
}
2、使用Condition控制線程通信
jdk1.5中,提供了多線程的升級解決方案爲:
(1)將同步synchronized替換爲顯式的Lock操作;
(2)將Object類中的wait(), notify(),notifyAll()替換成了Condition對象,該對象可以通過Lock鎖對象獲取;
(3)一個Lock對象上可以綁定多個Condition對象,這樣實現了本方線程只喚醒對方線程,而jdk1.5之前,一個同步只能有一個鎖,不同的同步只能用鎖來區分,且鎖嵌套時容易死鎖。
class Resource{
private String name;
private int count=1;
private boolean flag=false;
private Lock lock = new ReentrantLock();/*Lock是一個接口,ReentrantLock是該接口的一個直接子類。*/
private Condition condition_pro=lock.newCondition(); /*創建代表生產者方面的Condition對象*/
private Condition condition_con=lock.newCondition(); /*使用同一個鎖,創建代表消費者方面的Condition對象*/
public void set(String name){
lock.lock();//鎖住此語句與lock.unlock()之間的代碼
try{
while(flag)
condition_pro.await(); //生產者線程在conndition_pro對象上等待
this.name=name+"---"+count++;
System.out.println(Thread.currentThread().getName()+"...生產者..."+this.name);
flag=true;
condition_con.signalAll();
}
finally{
lock.unlock(); //unlock()要放在finally塊中。
}
}
public void out(){
lock.lock(); //鎖住此語句與lock.unlock()之間的代碼
try{
while(!flag)
condition_con.await(); //消費者線程在conndition_con對象上等待
System.out.println(Thread.currentThread().getName()+"...消費者..."+this.name);
flag=false;
condition_pro.signqlAll(); /*喚醒所有在condition_pro對象下等待的線程,也就是喚醒所有生產者線程*/
}
finally{
lock.unlock();
}
}
}
3、使用阻塞隊列(BlockingQueue)控制線程通信
BlockingQueue是一個接口,也是Queue的子接口。BlockingQueue具有一個特徵:當生產者線程試圖向BlockingQueue中放入元素時,如果該隊列已滿,則線程被阻塞;但消費者線程試圖從BlockingQueue中取出元素時,如果隊列已空,則該線程阻塞。程序的兩個線程通過交替向BlockingQueue中放入元素、取出元素,即可很好地控制線程的通信。
BlockingQueue提供如下兩個支持阻塞的方法:
(1)put(E e):嘗試把Eu元素放如BlockingQueue中,如果該隊列的元素已滿,則阻塞該線程。
(2)take():嘗試從BlockingQueue的頭部取出元素,如果該隊列的元素已空,則阻塞該線程。
BlockingQueue繼承了Queue接口,當然也可以使用Queue接口中的方法,這些方法歸納起來可以分爲如下三組:
(1)在隊列尾部插入元素,包括add(E e)、offer(E e)、put(E e)方法,當該隊列已滿時,這三個方法分別會拋出異常、返回false、阻塞隊列。
(2)在隊列頭部刪除並返回刪除的元素。包括remove()、poll()、和take()方法,當該隊列已空時,這三個方法分別會拋出異常、返回false、阻塞隊列。
(3)在隊列頭部取出但不刪除元素。包括element()和peek()方法,當隊列已空時,這兩個方法分別拋出異常、返回false。
BlockingQueue接口包含如下5個實現類:
ArrayBlockingQueue :基於數組實現的BlockingQueue隊列。
LinkedBlockingQueue:基於鏈表實現的BlockingQueue隊列。
PriorityBlockingQueue:它並不是保準的阻塞隊列,該隊列調用remove()、poll()、take()等方法提取出元素時,並不是取出隊列中存在時間最長的元素,而是隊列中最小的元素。
它判斷元素的大小即可根據元素(實現Comparable接口)的本身大小來自然排序,也可使用Comparator進行定製排序。
SynchronousQueue:同步隊列。對該隊列的存、取操作必須交替進行。
DelayQueue:它是一個特殊的BlockingQueue,底層基於PriorityBlockingQueue實現,不過,DelayQueue要求集合元素都實現Delay接口(該接口裏只有一個long getDelay()方法),
DelayQueue根據集合元素的getDalay()方法的返回值進行排序。
import java.util.concurrent.ArrayBlockingQueue;
import java.util.concurrent.BlockingQueue;
public class BlockingQueueTest{
public static void main(String[] args)throws Exception{
//創建一個容量爲1的BlockingQueue
BlockingQueue<String> b=new ArrayBlockingQueue<>(1);
//啓動3個生產者線程
new Producer(b).start();
new Producer(b).start();
new Producer(b).start();
//啓動一個消費者線程
new Consumer(b).start();
}
}
class Producer extends Thread{
private BlockingQueue<String> b;
public Producer(BlockingQueue<String> b){
this.b=b;
}
public synchronized void run(){
String [] str=new String[]{
"java",
"struts",
"Spring"
};
for(int i=0;i<9999999;i++){
System.out.println(getName()+"生產者準備生產集合元素!");
try{
b.put(str[i%3]);
sleep(1000);
//嘗試放入元素,如果隊列已滿,則線程被阻塞
}catch(Exception e){System.out.println(e);}
System.out.println(getName()+"生產完成:"+b);
}
}
}
class Consumer extends Thread{
private BlockingQueue<String> b;
public Consumer(BlockingQueue<String> b){
this.b=b;
}
public synchronized void run(){
while(true){
System.out.println(getName()+"消費者準備消費集合元素!");
try{
sleep(1000);
//嘗試取出元素,如果隊列已空,則線程被阻塞
b.take();
}catch(Exception e){System.out.println(e);}
System.out.println(getName()+"消費完:"+b);
}
}
}