??一個任務通常就是一個程序,每個運行中的程序就是一個進程。當一個程序運行時,內部可能包含了多個順序執行流,每個順序執行流就是一個線程。
??
進程
定義:
??當一個程序進入內存運行時,即變成一個進程。進程是處于運行過程中的程序,并且具有一定的獨立功能,進程是系統進行資源分配和調度的一個獨立單位。
進程的特點:
獨立性:是系統獨立存在的實體,擁有自己獨立的資源,有自己私有的地址空間。在沒有經過進程本身允許的情況下,一個用戶的進程不可以直接訪問其他進程的地址空間。
動態性:進程與程序的區別在于:程序只是一個靜態的指令集合,而進程是一個正在系統中活動的指令集和,進程中加入了時間的概念。進程具有自己的生命周期和不同的狀態,這些都是程序不具備的。
并發性:多個進程可以在單個處理器上并發執行,多個進程之間不會相互影響。
??
并行性和并發性
??并行:指在同一時刻,有多條指令在多個處理上同時執行。(多核同時工作)
??并發:指在同一時刻只能有一條指令執行,但多個進程指令被快速輪換執行,使得在宏觀上具有多個進程同時執行的效果。(單核在工作,單核不停輪詢)
??
線程
??多線程擴展了多進程的概念,使得同一個進程可以同時并發處理多個任務。
??線程(Thread)也被成為輕量級的進程,線程是進程執行的單元,線程在程序中是獨立的、并發的執行流
??當進程被初始化后,主線程就被創建了。絕大數應用程序只需要有一個主線程,但也可以在進程內創建多條的線程,每個線程也是相互獨立的。
??一個進程可以擁有多個線程,一個線程必須有一個父進程。
??線程可以擁有自己的堆棧、自己的程序計數器和自己的局部變量,但不擁有系統資源,它與父進程的其他線程共享該進程所擁有的全部資源,因此編程更加方便。
??線程是獨立運行的,它并不知道進程中是否還有其他的線程存在。線程的執行是搶占式的,即:當前運行的線程在任何時候都有可能被掛起,以便另外一個線程可以運行。
??一個線程可以創建和撤銷另一個線程,同一個進程中多個線程之間可以并發執行。
??線程的調度和管理由進程本身負責完成。
??歸納而言:操作系統可以同時執行多個任務,每個任務就是進程;進程可以同時執行多個任務,每個任務就是線程
??
多線程的優點:
進程之間不能共享內存,但線程之間共享內存非常容易
系統創建進程要為該進程重新分配系統資源,但創建線程的代價則小得多。因此多線程實現多任務并發比多線程的效率高。
Java語言內置了多線程功能支撐,簡化了多線程的編程。
??
??
線程的創建和啟動
一、繼承Thread類創建線程類
步驟:
① 定義Thread類的子類,并重寫該類的run()方法,該run()方法的方法體就代表了線程需要完成的任務,稱為線程執行體
② 創建Thread子類的實例,即創建了線程對象
③ 調用線程對象的start()方法來啟動該線程
示例:
// 通過繼承Thread類來創建線程類
public class FirstThread extends Thread
{
private int i ;
// 重寫run方法,run方法的方法體就是線程執行體
public void run()
{
for ( ; i < 100 ; i++ )
{
// 當線程類繼承Thread類時,直接使用this即可獲取當前線程
// Thread對象的getName()返回當前該線程的名字
// 因此可以直接調用getName()方法返回當前線程的名
System.out.println(getName() + " " + i);
}
}
public static void main(String[] args)
{
for (int i = 0; i < 100; i++)
{
// 調用Thread的currentThread方法獲取當前線程
System.out.println(Thread.currentThread().getName() + " " + i);
if (i == 20)
{
// 創建、并啟動第一條線程
new FirstThread().start();
// 創建、并啟動第二條線程
new FirstThread().start();
}
}
}
}
注意點:
① 當Java程序開始運行后,程序至少會創建一個主線程,main()方法的方法體代表主線程的線程執行體
② 當線程類繼承Tread類時,直接使用this即可以獲取當前線程
③ 繼承Thread類創建線程類,多個線程之間無法共享線程類的實例變量
??
二、實現Runnable接口創建線程類
步驟:
① 定義Runnable接口的實現類,并重寫該接口的run()方法
② 創建Runnable實現類的實例,并以此實例作為Thread的target來創建Tread對象,該Tread對象才是真正的線程對象
// 通過實現Runnable接口來創建線程類
public class SecondThread implements Runnable
{
private int i ;
// run方法同樣是線程執行體
public void run()
{
for ( ; i < 100 ; i++ )
{
// 當線程類實現Runnable接口時,
// 如果想獲取當前線程,只能用Thread.currentThread()方法。
System.out.println(Thread.currentThread().getName() + " " + i);
}
}
public static void main(String[] args) {
for (int i = 0; i < 100; i++)
{
System.out.println(Thread.currentThread().getName() + " " + i);
if (i == 20)
{
SecondThread st = new SecondThread(); // ①
// 通過new Thread(target , name)方法創建新線程
new Thread(st , "新線程1").start();
new Thread(st , "新線程2").start();
}
}
}
}
注意點:
① 實現Runnable接口創建線程類,必須通過Thread.currentThread()方法來獲得當前線程對象
② 實現Runnable接口創建線程類,多個線程可以共享線程類的實例變量
??
三、使用Callable和Future創建線程
Callable接口提供了一個call()方法,call()方法比run()方法更強大:
① call()方法可以由返回值
② call()方法可以聲明拋出異常
步驟:
① 創建Callable接口的實現類,并實現call()方法,該call()方法作為線程執行體,且該call()方法有返回值
② 使用FutureTask類來包裝Callable對象,該FutureTask對象封裝了該Callable對象的call()方法的返回值
③ 調用FutureTask對象的get()方法獲得子線程執行結束的返回值
示例:
import java.util.concurrent.Callable;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.FutureTask;
//使用Callable接口和Future來創建線程
public class ThreadFuture {
//拋出異常
public static void main(String[] args) throws InterruptedException, ExecutionException {
//創建FutureTask對象,包裝 Callable接口實例
FutureTask<Integer> task = new FutureTask<Integer>((Callable<Integer>)()->{
int sum = 0;
for(int i = 0;i<100;i++){
System.out.println(Thread.currentThread().getName()+":"+i);
sum += i;
}
//注意看這里有返回值
return sum;
});
//使用task作為 Thread類的target 來創建一個線程
Thread instance = new Thread(task);
//啟動線程
instance.start();
//sleep一段時間,讓上面的線程執行完畢
Thread.sleep(1000);
//這里可以調用task.get() 獲取上面的那個線程的返回值
System.out.println("線程返回值:"+task.get());
}
}
??
??
創建線程三種方式的對比:
實現Runnable接口、Callable接口創建線程
優點:
①實現的是接口,還可以繼承其他類
② 多個線程可以共享同一個target對象,適合多個相同的線程來處理同一份資源的情況
缺點:
① 編程稍微復雜
② 獲取當前線程必須用Thread.currentThread()方法來獲得
繼承Tread類創建線程
優點:
①編程簡單
② 獲取當前線程,可以直接使用this來獲得
缺點:
① 已經繼承了Thread類,不能繼承其他類
??
??
線程的生命周期
線程的生命周期要經歷新建(New)、就緒(Runnable)、運行(Running)、阻塞(Blocke)和死亡(Dead)5種狀態。
尤其是當線程啟動以后,它不可能一直“霸占”著CPU獨自運行,所以CPU需要在多條線程之間切換,于是線程狀態也會多次在運行、阻塞之間切換。
1、新建和就緒狀態
當程序使用new
關鍵字創建了一個線程之后,該線程就處于新建狀態,此時它僅僅由Java虛擬機為其分配內存,并且初始化其成員變量的值。此時的線程對象沒有表現出任何線程隊動態特征,程序也不會執行線程的線程執行體。
當線程對象調用了start()
方法之后,該線程處于就緒狀態,Java虛擬機會為其創建方法調用棧和程序計數器,處于這個狀態中的線程并沒有開始運行,只是表示該線程可以運行了,至于該線程何時開始運行,取決于JVM里線程調度器的調度。
tips:
啟動線程使用
start()
方法,而不是run()
方法,如果調用run()
方法,則run()
方法立即就會被執行,而且在run()
方法返回之前,其他線程無法并發執行,也就是說,如果直接調用線程對象的run()
方法,系統把線程對象當成一個普通對象,而run()
方法也是一個普通方法,而不是線程執行體。如果直接調用線程對象的
run()
方法,則run()
方法里不能直接通過getName()
方法來獲得當前執行線程的名字,而是需要使用Thread.currentThread()
方法先獲得當前線程,再調用線程對象的getName()
方法來獲得線程的名字。啟動線程的正確方法是調用Thread
對象的start()
方法,而不是直接調用run()
方法,否則就變成單線程程序了。調用了線程的
run()
方法之后,該線程已經不再處于新建狀態,不要再次調用線程對象的start()
方法。
2、運行和阻塞狀態
如果處于就緒狀態的線程獲得了CPU,開始執行run()
方法的線程執行體,則該線程處于運行狀態。
但線程不可能一直處于運行狀態,它在運行過程中會被中斷,從而進入一個阻塞的狀態
當發生如下情況時,線程將會進入阻塞狀態:
1、線程調用sleep()
方法主動放棄所占用的處理器資源。
2、線程調用了一個阻塞式IO方法,在該方法返回之前,該線程被阻塞。
3、線程試圖獲得一個同步監視器,但該同步監視器正被其他線程所持有。
4、線程在等待某個通知(notify)。
5、程序調用了線程的suspend()
方法將該線程掛起。但這個方法容易導致死鎖,所以應該盡量避免使用該方法。
針對上面幾種情況,當發生如下特定的情況時可以解除上面的阻塞,讓該線程重新進入就緒狀態。
1、調用sleep()
方法的線程經過了指定時間。
2、線程調用的阻塞式IO方法已經返回。
3、 線程成功地獲得了試圖取得的同步監視器。
4、 線程正在等待某個通知時,其他線程發出了一個通知。
5、處于掛起狀態的線程被調用了resume()
恢復方法。
從圖中可以看出,線程從阻塞狀態只能進入就緒狀態,無法直接進入運行狀態。
而就緒和運行狀態之間的轉換通常不受程序控制,而是由系統線程調度所決定。
當處于就緒狀態的線程獲得處理器資源時,該線程進入運行狀態;當處于運行狀態的線程失去處理器資源時,該線程進入就緒狀態。
但有一個方法例外,調用yield()
方法可以讓運行狀態的線程轉入就緒狀態。
線程死亡
線程會以如下三種方式結束,結束后就處于死亡狀態。
run()
或call()
方法執行完成,線程正常結束。線程拋出一個未捕獲的
Exception
或Error
。直接調用該線程的
stop()
方法來結束該線程——該方法容易導致死鎖,通常不推薦使用。
tips:
1、當主線程結束時,其他線程不受任何影響,并不會隨之結束。一旦子線程啟動起來后,它就擁有和主線程相同的地位,它不會受主線程的影響。
2、為了測試某個線程是否已經死亡,可以調用線程對象的isAlive()
方法,當線程處于就緒、運行、阻塞三種狀態時,該方法將返回true
;當線程處于新建、死亡兩種狀態時,該方法將返回false
。
3、不要試圖對一個已經死亡的線程調用start()
方法使它重新啟動,死亡就是死亡,該線程將不可再次作為線程執行。在線程已經死亡的情況下再次調用start()
方法將會引發IIIegalThreadException
異常。
4、不能對死亡的線程調用start()
方法,程序只能對新建狀態的線程調用start()方法,對新建的線程兩次調用start()方法也是錯誤的,會引發IIIegalThreadStateException
異常。
??
控制線程
1、join
線程
Thread
提供了讓一個線程等待另一個線程完成的方法:join()
方法。當在某個程序執行流中調用其他線程的join()
方法時,調用線程將被阻塞,直到被join()
方法加入的join
線程執行完為止。
join()
方法通常由使用線程的程序調用,以將大問題劃分成許多小問題,每個小問題分配一個線程。當所有的小問題都得到處理后,再調用主線程來進一步操作。
代碼示例:
public class JoinThread extends Thread
{
// 提供一個有參數的構造器,用于設置該線程的名字
public JoinThread(String name)
{
super(name);
}
// 重寫run()方法,定義線程執行體
public void run()
{
for (int i = 0; i < 100 ; i++ )
{
System.out.println(getName() + " " + i);
}
}
public static void main(String[] args)throws Exception
{
// 啟動子線程
new JoinThread("新線程").start();
for (int i = 0; i < 100 ; i++ )
{
if (i == 20)
{
JoinThread jt = new JoinThread("被Join的線程");
jt.start();
// main線程調用了jt線程的join()方法,main線程必須等jt執行結束才會向下執行
jt.join();
}
System.out.println(Thread.currentThread().getName() + " " + i);
}
}
}
2、后臺線程
有一種線程,它是在后臺運行的,它的任務是為其他的線程提供服務,這種線程被稱為“后臺線程(Daemon Thread)”,又稱為“守護線程”或“精靈線程”。JVM的垃圾回收線程就是典型的后臺線程。
后臺線程有個特征:如果所有的前臺線程都死亡,后臺線程會自動死亡。
調用Thread
對象的setDaemon(true)
方法可將指定線程設置成后臺線程。
tips:
1、Thread
類還提供了一個isDaemon()
方法,用于判斷指定線程是否為后臺線程。
2、前臺線程創建的子線程默認是前臺線程,后臺線程子線程默認是后臺線程。
3、前臺線程死亡后,JVM會通知后臺線程死亡,但從它接收指令到做出響應,需要一定時間。
而且要將某個線程設置為后臺線程,必須在該線程啟動之前設置,也就是說,setDaemon(true)
必須在start()
方法之前調用,否則會引發llegalThreadStateException
異常。
3、線程睡眠:sleep
如果需要讓當前正在執行的線程暫停一段時間,并進入阻塞狀態,則可以通過調用Thread
類的靜態sleep()
方法來實現。
4、線程讓步yield()
yield()
方法是一個和sleep()
方法有點相似的方法,它也是Thread
類提供的一個靜態方法,它也可以讓當前正在執行的線程暫停,但它不會阻塞該線程,它只是將該線程轉入就緒狀態。
yield()
只是讓當前線程暫停一下,讓系統的線程調度器重新調度一次,完全可能的情況是:當某個線程調用了yield()
方法暫停之后,線程調度器又將其調度出來重新執行。
實際上,當某個線程調用了yield()
方法暫停之后,只有優先級與當前線程相同,或者優先級比當前線程更高的處于就緒狀態的線程才會獲得執行的機會。
關于sleep()
方法和yield()
方法的區別如下
sleep()
方法暫停當前線程后,會給其他線程執行機會,不會理會其他線程的優先級;但yield()
方法只會給優先級相同,或優先級更高的線程執行機會。sleep()
方法會將線程轉入阻塞狀態,直到經過阻塞時間才會轉入就緒狀態;而yield()
不會將線程轉入阻塞狀態,它只是強制當前線程進入就緒狀態。因此完全有可能某個線程調用yield()
方法暫停之后,立即再次獲得處理器資源被執行。sleep()
方法聲明拋出了InterruptedException
異常,所以調用sleep()
方法時要么捕捉該異常,要么顯式聲明拋出該異常;而yield()
方法則沒有聲明拋出任何異常。sleep()
方法比yield()
方法有更好的可移植性,通常不建議使用yield()
方法來控制并發線程的執行。
5、改變線程優先級
通過Thread
類提供的setPriority(int newPriority)
、getPriority()
方法來設置和返回指定線程的優先級。
setPriority()
方法的參數可以是一個整數,范圍是1~10之間,也可以使用Thread
類的如下三個靜態常量。
MAXPRIORITY:其值是10。
MIN PRIORITY:其值是1。
NORM_PRIORITY:其值是5。
??
線程同步
為了解決多個線程訪問同一個數據時,會出現問題,因此需要進行線程同步。就像前面介紹的文件并發訪問,當有兩個進程并發修改同一個文件時就有可能造成異常。
1、同步代碼塊
為了解決線程同步問題,Java的多線程支持引入了同步監視器來解決這個問題,使用同步監視器的通用方法就是同步代碼塊。同步代碼塊的語法格式如下:
synchronized(obj)
{.....
//此處的代碼就是同步代碼塊
}
上面語法格式中synchronized后括號里的obj就是同步監視器,上面代碼的含義是:線程開始執行同步代碼塊之前,必須先獲得對同步監視器的鎖定。
任何時刻只能有一個線程可以獲得對同步監視器的鎖定,當同步代碼塊執行完成后,該線程會釋放對該同步監視器的鎖定。
通常推薦使用可能被并發訪問的共享資源充當同步監視器,代碼示例如下:
public class DrawThread extends Thread
{
// 模擬用戶賬戶
private Account account;
// 當前取錢線程所希望取的錢數
private double drawAmount;
public DrawThread(String name , Account account
, double drawAmount)
{
super(name);
this.account = account;
this.drawAmount = drawAmount;
}
// 當多條線程修改同一個共享數據時,將涉及數據安全問題。
public void run()
{
// 使用account作為同步監視器,任何線程進入下面同步代碼塊之前,
// 必須先獲得對account賬戶的鎖定——其他線程無法獲得鎖,也就無法修改它
// 這種做法符合:“加鎖 → 修改 → 釋放鎖”的邏輯
synchronized (account)
{
// 賬戶余額大于取錢數目
if (account.getBalance() >= drawAmount)
{
// 吐出鈔票
System.out.println(getName()
+ "取錢成功!吐出鈔票:" + drawAmount);
try
{
Thread.sleep(1);
}
catch (InterruptedException ex)
{
ex.printStackTrace();
}
// 修改余額
account.setBalance(account.getBalance() - drawAmount);
System.out.println("\t余額為: " + account.getBalance());
}
else
{
System.out.println(getName() + "取錢失敗!余額不足!");
}
}
// 同步代碼塊結束,該線程釋放同步鎖
}
}
??
2、同步方法
同步方法就是使用synchronized
關鍵字來修飾某個方法,則該方法稱為同步方法。
對于synchronized
修飾的實例方法(非static方法)而言,無須顯式指定同步監視器,同步方法的同步監視器是this
,也就是調用該方法的對象。
通過使用同步方法可以非常方便地實現線程安全的類,線程安全的類具有如下特征。
該類的對象可以被多個線程安全地訪問。
每個線程調用該對象的任意方法之后都將得到正確結果。
每個線程調用該對象的任意方法之后,該對象狀態依然保持合理狀態。
代碼示例:
public class Account
{
// 封裝賬戶編號、賬戶余額兩個成員變量
private String accountNo;
private double balance;
public Account(){}
// 構造器
public Account(String accountNo , double balance)
{
this.accountNo = accountNo;
this.balance = balance;
}
// accountNo的setter和getter方法
public void setAccountNo(String accountNo)
{
this.accountNo = accountNo;
}
public String getAccountNo()
{
return this.accountNo;
}
// 因此賬戶余額不允許隨便修改,所以只為balance提供getter方法,
public double getBalance()
{
return this.balance;
}
// 提供一個線程安全draw()方法來完成取錢操作
public synchronized void draw(double drawAmount)
{
// 賬戶余額大于取錢數目
if (balance >= drawAmount)
{
// 吐出鈔票
System.out.println(Thread.currentThread().getName()
+ "取錢成功!吐出鈔票:" + drawAmount);
try
{
Thread.sleep(1);
}
catch (InterruptedException ex)
{
ex.printStackTrace();
}
// 修改余額
balance -= drawAmount;
System.out.println("\t余額為: " + balance);
}
else
{
System.out.println(Thread.currentThread().getName()
+ "取錢失敗!余額不足!");
}
}
// 下面兩個方法根據accountNo來重寫hashCode()和equals()方法
public int hashCode()
{
return accountNo.hashCode();
}
public boolean equals(Object obj)
{
if(this == obj)
return true;
if (obj !=null
&& obj.getClass() == Account.class)
{
Account target = (Account)obj;
return target.getAccountNo().equals(accountNo);
}
return false;
}
}
??
上面程序中增加了一個代表取錢的draw()
方法,并使用了synchronized
關鍵字修飾該方法,把該方法變成同步方法。
該同步方法的同步監視器是this
,因此對于同一個Account
賬戶而言,任意時刻只能有一個線程獲得對Account
對象的鎖定,然后進入draw()
方法執行取錢操作,這樣也可以保證多個線程并發取錢的線程安全。
3、釋放同步監視器的鎖定
程序無法顯式釋放對同步監視器的鎖定,線程會在如下情況下釋放對同步監視器的鎖定。
當前線程的同步方法、同步代碼塊執行結束,當前線程即釋放同步監視器。
當前線程在同步代碼塊、同步方法中遇到
break
、return
終止了該代碼塊、該方法的繼續執行,當前線程將會釋放同步監視器。當前線程在同步代碼塊、同步方法中出現了未處理的Error 或Exception,導致了該代碼塊、該方法異常結束時,當前線程將會釋放同步監視器。
當前線程執行同步代碼塊或同步方法時,程序執行了同步監視器對象的wait0方法,則當前線程暫停,并釋放同步監視器。
??
在如下所示的情況下,線程不會釋放同步監視器:
線程執行同步代碼塊或同步方法時,程序調用
Thread.sleep()
、Thread.yield()
方法來暫停當前線程的執行,當前線程不會釋放同步監視器。線程執行同步代碼塊時,其他線程調用了該線程的
suspend()
方法將該線程掛起,該線程不會釋放同步監視器。當然,程序應該盡量避免使用suspend()
和resume()
方法來控制線程。
4、同步鎖(Lock)
Lock、ReadWriteLock是Java5提供的兩個根接口,并為Lock提供ReentrantLock(可重入鎖)實現類,為ReadWriteLock提供了ReentrantReadWriteLock 實現類。
Java8新增了新型的StampedLock類,在大多數場景中它可以替代傳統的ReentrantReadWriteLock。
ReentrantReadWriteLock為讀寫操作提供了三種鎖模式:Writing、ReadingOptimistic、Reading。
在實現線程安全的控制中,比較常用的是ReentrantLock(可重入鎖)。使用該Lock對象可以顯式地加鎖、釋放鎖,通常使用ReentrantLock的代碼格式如下:
public class Account
{
// 定義鎖對象
private final ReentrantLock lock = new ReentrantLock();
// 封裝賬戶編號、賬戶余額的兩個成員變量
private String accountNo;
private double balance;
public Account(){}
// 構造器
public Account(String accountNo , double balance)
{
this.accountNo = accountNo;
this.balance = balance;
}
// accountNo的setter和getter方法
public void setAccountNo(String accountNo)
{
this.accountNo = accountNo;
}
public String getAccountNo()
{
return this.accountNo;
}
// 因此賬戶余額不允許隨便修改,所以只為balance提供getter方法,
public double getBalance()
{
return this.balance;
}
// 提供一個線程安全draw()方法來完成取錢操作
public void draw(double drawAmount)
{
// 加鎖
lock.lock();
try
{
// 賬戶余額大于取錢數目
if (balance >= drawAmount)
{
// 吐出鈔票
System.out.println(Thread.currentThread().getName()
+ "取錢成功!吐出鈔票:" + drawAmount);
try
{
Thread.sleep(1);
}
catch (InterruptedException ex)
{
ex.printStackTrace();
}
// 修改余額
balance -= drawAmount;
System.out.println("\t余額為: " + balance);
}
else
{
System.out.println(Thread.currentThread().getName()
+ "取錢失敗!余額不足!");
}
}
finally
{
// 修改完成,釋放鎖
lock.unlock();
}
}
// 下面兩個方法根據accountNo來重寫hashCode()和equals()方法
public int hashCode()
{
return accountNo.hashCode();
}
public boolean equals(Object obj)
{
if(this == obj)
return true;
if (obj !=null
&& obj.getClass() == Account.class)
{
Account target = (Account)obj;
return target.getAccountNo().equals(accountNo);
}
return false;
}
}
??
同步方法或同步代碼塊使用與競爭資源相關的、隱式的同步監視器,并且強制要求加鎖和釋放鎖要出現在一個塊結構中,而且當獲取了多個鎖時,它們必須以相反的順序釋放,且必須在與所有鎖被獲取時相同的范圍內釋放所有鎖。
Lock提供了同步方法和同步代碼塊所沒有的其他功能,包括用于非塊結構的tryLock()
方法,以及試圖獲取可中斷鎖的lockInterruptibly()
方法,還有獲取超時失效鎖的tryLock(long,TimeUnit)
方法。
ReentrantLock
鎖具有可重入性,也就是說,一個線程可以對已被加鎖的ReentrantLock
鎖再次加鎖,ReentrantLock
對象會維持一個計數器來追蹤lock()
方法的嵌套調用,線程在每次調用lock()
加鎖后,必須顯式調用unlock()
來釋放鎖,所以一段被鎖保護的代碼可以調用另一個被相同鎖保護的方法。
死鎖
當兩個線程相互等待對方釋放同步監視器時就會發生死鎖一旦出現死鎖,整個程序既不會發生任何異常,也不會給出任何提示,只是所有線程處于阻塞狀態,無法繼續。
死鎖示例:
有兩個類 A 和 B ,這兩個類每個類都各含有兩個同步方法,利用兩個線程來進行操作。
首先線程1調用 A 類的同步方法 A1,然后休眠,此時線程2會開始工作,它會調用 B 類的同步方法 B1,然后也休眠。
此時線程1休眠結束,它繼續執行方法 A1 ,A1的下一步操作是調用 B 中的同步方法 B2,因為此時 B 的對象示例正被線程2所占據,因此線程1只能等待對 B 的鎖的釋放。
此時線程2又蘇醒了,它繼續執行方法 B1,B1的下一步操作是調用 A 中的同步方法 A2,因此是 A 類的對象也被線程1給鎖住了,因此線程2也只能等待,這樣就造成了線程1和線程2相互等待,從而導致了死鎖的發生。
代碼示例:
//A類
class A
{
public synchronized void foo( B b )
{
System.out.println("當前線程名: " + Thread.currentThread().getName()
+ " 進入了A實例的foo()方法" ); // ①
try
{
Thread.sleep(200);
}
catch (InterruptedException ex)
{
ex.printStackTrace();
}
System.out.println("當前線程名: " + Thread.currentThread().getName()
+ " 企圖調用B實例的last()方法"); // ③
b.last();
}
public synchronized void last()
{
System.out.println("進入了A類的last()方法內部");
}
}
//B類
class B
{
public synchronized void bar( A a )
{
System.out.println("當前線程名: " + Thread.currentThread().getName()
+ " 進入了B實例的bar()方法" ); // ②
try
{
Thread.sleep(200);
}
catch (InterruptedException ex)
{
ex.printStackTrace();
}
System.out.println("當前線程名: " + Thread.currentThread().getName()
+ " 企圖調用A實例的last()方法"); // ④
a.last();
}
public synchronized void last()
{
System.out.println("進入了B類的last()方法內部");
}
}
//線程類
public class DeadLock implements Runnable
{
A a = new A();
B b = new B();
public void init()
{
Thread.currentThread().setName("主線程");
// 調用a對象的foo方法
a.foo(b);
System.out.println("進入了主線程之后");
}
public void run()
{
Thread.currentThread().setName("副線程");
// 調用b對象的bar方法
b.bar(a);
System.out.println("進入了副線程之后");
}
//主函數
public static void main(String[] args)
{
DeadLock dl = new DeadLock();
// 以dl為target啟動新線程
new Thread(dl).start();
// 調用init()方法
dl.init();
}
}
??
線程通信
1、傳統的線程通信——通過Object
類提供的方法實現
借助于Object
類提供的wait()
、notify()
和notifyAll()
三個方法。
這三個方法并不屬于Thread
類,而是屬于Object
類。但這三個方法必須由同步監視器對象來調用,這可分成以下兩種情況。
對于使用synchronized修飾的同步方法,因為該類的默認實例(this)就是同步監視器,所以可以在同步方法中直接調用這三個方法。
對于使用synchronized修飾的同步代碼塊,同步監視器是synchronized后括號里的對象,所以必須使用該對象調用這三個方法。
關于這三個方法的解釋如下:
wait()
:導致當前線程等待,直到其他線程調用該同步監視器的notify()
方法或notifyAll()
方法來喚醒該線程。notify()
:喚醒在此同步監視器上等待的單個線程。如果所有線程都在此同步監視器上等待,則會選擇喚醒其中一個線程。選擇是任意性的。只有當前線程放棄對該同步監視器的鎖定后(使用wait()
方法),才可以執行被喚醒的線程。notifyAll
:喚醒在此同步監視器上等待的所有線程。只有當前線程放棄對該同步監視器的鎖定后,才可以執行被喚醒的線程。
使用Condition
控制線程通信
如果程序不使用synchronized 關鍵字來保證同步,而是直接便用Lock對象采保證同步,則系統中下存在隱式的同步監視器,也就不能使用wait()
、notify()
、notifyAll()
方法進行線程通信了。
當使用Lock 對象來保證同步時,Java提供了一個Condition
類來保持協調,使用Condition
可以讓那些已經得到Lock對象卻無法繼續執行的線程釋放Lock
對象,Condition
對象也可以喚醒其他處于等待的線程。
Condition
實例被綁定在一個Lock
對象上。要獲得特定Lock
實例的Condition
實例,調用Lock
對象的newCondition()
方法即可。Condition
類提供了如下三個方法:
await()
:類似于隱式同步監視器上的wait()
方法,導致當前線程等待,直到其他線程調用該Condition
的signal()
方法或signalAll()
方法來喚醒該線程。signal()
:喚醒在此Lock
對象上等待的單個線程。如果所有線程都在該Lock
對象上等待,則會選擇喚醒其中一個線程。選擇是任意性的。只有當前線程放棄對該Lock
對象的鎖定后(使用await()
方法),才可以執行被喚醒的線程。signalAIl()
:喚醒在此Lock
對象上等待的所有線程。只有當前線程放棄對該Lock
對象的鎖定后,才可以執行被喚醒的線程。
public class Account
{
// 顯式定義Lock對象
private final Lock lock = new ReentrantLock();
// 獲得指定Lock對象對應的Condition
private final Condition cond = lock.newCondition();
// 封裝賬戶編號、賬戶余額的兩個成員變量
private String accountNo;
private double balance;
// 標識賬戶中是否已有存款的旗標
private boolean flag = false;
public Account(){}
// 構造器
public Account(String accountNo , double balance)
{
this.accountNo = accountNo;
this.balance = balance;
}
// accountNo的setter和getter方法
public void setAccountNo(String accountNo)
{
this.accountNo = accountNo;
}
public String getAccountNo()
{
return this.accountNo;
}
// 因此賬戶余額不允許隨便修改,所以只為balance提供getter方法,
public double getBalance()
{
return this.balance;
}
public void draw(double drawAmount)
{
// 加鎖
lock.lock();
try
{
// 如果flag為假,表明賬戶中還沒有人存錢進去,取錢方法阻塞
if (!flag)
{
cond.await();
}
else
{
// 執行取錢
System.out.println(Thread.currentThread().getName()
+ " 取錢:" + drawAmount);
balance -= drawAmount;
System.out.println("賬戶余額為:" + balance);
// 將標識賬戶是否已有存款的旗標設為false。
flag = false;
// 喚醒其他線程
cond.signalAll();
}
}
catch (InterruptedException ex)
{
ex.printStackTrace();
}
// 使用finally塊來釋放鎖
finally
{
lock.unlock();
}
}
public void deposit(double depositAmount)
{
lock.lock();
try
{
// 如果flag為真,表明賬戶中已有人存錢進去,則存錢方法阻塞
if (flag) // ①
{
cond.await();
}
else
{
// 執行存款
System.out.println(Thread.currentThread().getName()
+ " 存款:" + depositAmount);
balance += depositAmount;
System.out.println("賬戶余額為:" + balance);
// 將表示賬戶是否已有存款的旗標設為true
flag = true;
// 喚醒其他線程
cond.signalAll();
}
}
catch (InterruptedException ex)
{
ex.printStackTrace();
}
// 使用finally塊來釋放鎖
finally
{
lock.unlock();
}
}
// 下面兩個方法根據accountNo來重寫hashCode()和equals()方法
public int hashCode()
{
return accountNo.hashCode();
}
public boolean equals(Object obj)
{
if(this == obj)
return true;
if (obj !=null
&& obj.getClass() == Account.class)
{
Account target = (Account)obj;
return target.getAccountNo().equals(accountNo);
}
return false;
}
}
??
使用阻塞隊列(BlockingQueue)控制線程通信
Java5提供了一個BlockingQueue
接口,雖然BlockingQueue
也是Queue
的子接口,但它的主要用途并不是作為容器,而是作為線程同步的工具。
BlockingQueue
具有一個特征:
當生產者線程試圖向BlockingOueue
中放入元素時,如果該隊列已滿,則該線程被阻塞;
當消費者線程試圖從BlockingQueue
中取出元素時,如果該隊列已空,則該線程被阻塞。
BlockingQueue
提供如下兩個支持阻塞的方法。
put(E e):嘗試把E元素放入
BlockingQueue
中,如果該隊列的元素已滿,則阻塞該線程。take()
:嘗試從BlockingQueue
的頭部取出元素,如果該隊列的元素已空,則阻塞該線程。
BlockingQueue
繼承了Queue
接口,當然也可使用Queue
接口中的方法。這些方法歸納起來可分為如下三組。
在隊列尾部插入元素。包括
add(E e)
、offer(E e)
和put(Ee)
方法,當該隊列已滿時,這三個方法分別會拋出異常、返回false、阻塞隊列。在隊列頭部刪除并返回刪除的元素。包括
remove()
、poll()
和take()
方法。當該隊列已空時,這三個方法分別會拋出異常、返回false、阻塞隊列。在隊列頭部取出但不刪除元素。包括
element()
和peek()
方法,當隊列已空時,這兩個方法分別拋出異常、返回false。
使用阻塞隊列(BlockingQueue)來實現線程通信,以消費者生產者為例:
//生產者類
class Producer extends Thread
{
private BlockingQueue<String> bq;
public Producer(BlockingQueue<String> bq)
{
this.bq = bq;
}
public void run()
{
String[] strArr = new String[]
{
"Java",
"Struts",
"Spring"
};
for (int i = 0 ; i < 999999999 ; i++ )
{
System.out.println(getName() + "生產者準備生產集合元素!");
try
{
Thread.sleep(200);
// 嘗試放入元素,如果隊列已滿,線程被阻塞
bq.put(strArr[i % 3]);
}
catch (Exception ex){ex.printStackTrace();}
System.out.println(getName() + "生產完成:" + bq);
}
}
}
//消費者類
class Consumer extends Thread
{
private BlockingQueue<String> bq;
public Consumer(BlockingQueue<String> bq)
{
this.bq = bq;
}
public void run()
{
while(true)
{
System.out.println(getName() + "消費者準備消費集合元素!");
try
{
Thread.sleep(200);
// 嘗試取出元素,如果隊列已空,線程被阻塞
bq.take();
}
catch (Exception ex){ex.printStackTrace();}
System.out.println(getName() + "消費完成:" + bq);
}
}
}
//主程序
public class BlockingQueueTest2
{
public static void main(String[] args)
{
// 創建一個容量為1的BlockingQueue
BlockingQueue<String> bq = new ArrayBlockingQueue<>(1);
// 啟動3條生產者線程
new Producer(bq).start();
new Producer(bq).start();
new Producer(bq).start();
// 啟動一條消費者線程
new Consumer(bq).start();
}
}
??
線程池
系統啟動一個新線程的成本是比較高的,因為它涉及與操作系統交互。在這種情形下,使用線程池可以很好地提高性能,尤其是當程序中需要創建大量生存期很短暫的線程時,更應該考慮使用線程池。
與數據庫連接池類似的是,線程池在系統啟動時即創建大量空閑的線程,程序將一個Runnable
對象或Callable
對象傳給線程池,線程池就會啟動一個線程來執行它們的run()
或call()
方法。
當run()
或call()
方法執行結束后,該線程并不會死亡,而是再次返回線程池中成為空閑狀態,等待執行下一個Runnable
對象的run()
或call()
方法。
除此之外,使用線程池可以有效地控制系統中并發線程的數量,當系統中包含大量并發線程時,會導致系統性能劇烈下降,甚至導致JVM崩潰,而線程池的最大線程數參數可以控制系統中并發線程數不超過此數。
創建線程池
在Java5以前,開發者必須手動實現自己的線程池;從Java5開始,Java內建支持線程池。
Java5新增了一個Executors
工廠類來產生線程池,該工廠類包含如下幾個靜態工廠方法來創建線程池。
newCachedThreadPool()
:創建一個具有緩存功能的線程池,系統根據需要創建線程,這些線程將會被緩存在線程池中。newFixedThreadPool(int nThreads)
:創建一個可重用的、具有固定線程數的線程池。newSingle ThreadExecutor()
:創建一個只有單線程的線程池,它相當于調用newFixedThread Pool()
方法時傳入參數為1。newScheduledThreadPool(int corePoolSize)
:創建具有指定線程數的線程池,它可以在指定延遲后執行線程任務。corePoolSize
指池中所保存的線程數,即使線程是空閑的也被保存在線程池內。newSingle ThreadScheduledExecutor)
:創建只有一個線程的線程池,它可以在指定延遲后執行線程任務。ExecutorService new WorkStealingPool(int parallelism)
:創建持有足夠的線程的線程池來支持給定的并行級別,該方法還會使用多個隊列來減少競爭。ExecutorService new WorkStealingPool)
:該方法是前一個方法的簡化版本。如果當前機器有4個CPU,則目標并行級別被設置為4,也就是相當于為前一個方法傳入4作為參數。
上面7個方法中的前三個方法返回一個ExecutorService
對象,該對象代表一個線程池,它可以執行Runnable
對象或Callable
對象所代表的線程;
而中間兩個方法返回一個ScheduledExecutorService
線程池,它是ExecutorService
的子類,它可以在指定延遲后執行線程任務;
最后兩個方法則是Java8新增的,這兩個方法可充分利用多CPU并行的能力。這兩個方法生成的work stealing
池,都相當于后臺線程池,如果所有的前臺線程都死亡了,work stealing
池中的線程會自動死亡。
ExecutorService
代表盡快執行線程的線程池(只要線程池中有空閑線程,就立即執行線程任務)
程序只要將一個Runnable
對象或Callable
對象(代表線程任務)提交給該線程池,該線程池就會盡快執行該任務。
ExecutorService里提供了如下三個方法。
-
Future<?>submit(Runnable task)
:將一個Runnable
對象提交給指定的線程池,線程池將在有空閑線程時執行Runnable
對象代表的任務。其中Future
對象代表Runnable
任務的返回值,但run()
方法沒有返回值,所以Future
對象將在run()
方法執行結束后返回null
。
但可以調用Future
的isDone()
、isCancelled()
方法來獲得Runnable
對象的執行狀態。
<T>Future-T>submit(Runnable task,T result)
:將一個Runnable
對象提交給指定的線程池,線程池將在有空閑線程時執行Runnable對象代表的任務。其中result
顯式指定線程執行結束后的返回值,所以Future
對象將在run()
方法執行結束后返回result
。<T>Future-T>submit(Callable<T>task)
:將一個Callable
對象提交給指定的線程池,線程池將在有空閑線程時執行Callable
對象代表的任務。其中Future
代表Callable
對象里call()
方法的返回值。
ScheduledExecutorService
代表可在指定延遲后或周期性地執行線程任務的線程池,它提供了如下4個方法。
ScheduledFuture<V> schedule(Callable-V> callable,long delay,TimeUnit unit)
:指定callable
任務將在delay
延遲后執行。ScheduledFuture<?>schedule(Runnable command,long delay,TimeUnit unit)
:指定command
任務將在delay
延遲后執行。ScheduledFuture<?> scheduleAtFixedRate(Runnable command,long initialDelay,long period,TimeUnit unit)
:指定command
任務將在delay延遲后執行,而且以設定頻率重復執行。也就是說,在initialDelay
后開始執行,依次在initialDelay+period、initialDelay+2*period…
處重復執行,依此類推。ScheduledFuture<?>scheduleWithFixedDelay(Runnable command,long initialDelay,long delay,TimeUnit unit)
:創建并執行一個在給定初始延遲后首次啟用的定期操作,隨后在每一次執行終止和下一次執行開始之間都存在給定的延遲。如果任務在任一次執行時遇到異常,就會取消后續執行;否則,只能通過程序來顯式取消或終止該任務。
用完一個線程池后,應該調用該線程池的shutdown0方法,該方法將啟動線程池的關閉序列,調用shutdown()
方法后的線程池不再接收新任務,但會將以前所有已提交任務執行完成。當線程池中的所有任務都執行完成后,池中的所有線程都會死亡;
另外也可以調用線程池的shutdownNow()
方法來關閉線程池,該方法試圖停止所有正在執行的活動任務,暫停處理正在等待的任務,并返回等待執行的任務列
表。
使用線程池來執行線程任務的步驟如下。
①調用Executors
類的靜態工廠方法創建一個ExecutorService
對象,該對象代表一個線程池。
②創建Runnable
實現類或Callable
實現類的實例,作為線程執行任務。
③調用ExecutorService
對象的submit()
方法來提交Runnable
實例或Callable
實例。
④當不想提交任何任務時,調用ExecutorService
對象的shutdown()
方法來關閉線程池。
代碼示例:
public class ThreadPoolTest
{
public static void main(String[] args)
throws Exception
{
// 創建足夠的線程來支持4個CPU并行的線程池
// 創建一個具有固定線程數(6)的線程池
ExecutorService pool = Executors.newFixedThreadPool(6);
// 使用Lambda表達式創建Runnable對象
Runnable target = () -> {
for (int i = 0; i < 100 ; i++ )
{
System.out.println(Thread.currentThread().getName() + "的i值為:" + i);
}
};
// 向線程池中提交兩個線程
pool.submit(target);
pool.submit(target);
// 關閉線程池
pool.shutdown();
}
}
??
Java8增強的ForkJoinPool
Java7提供了ForkJoinPool
來支持將一個任務拆分成多個“小任務”并行計算,再把多個“小任務”的結果合并成總的計算結果。ForkJoinPool
是ExecutorService
的實現類,因此是一種特殊的線程池。
ForkJoinPool
提供了如下兩個常用的構造器。
ForkJoinPool(int parallelism)
:創建一個包含parallelism
個并行線程的ForkJoinPool
。ForkJoinPool()
:以Runtime.availableProcessors()
方法的返回值作為parallelism
參數來創建ForkJoinPool
。
Java8進一步擴展了ForkJoinPool
的功能,Java8為ForkJoinPool
增加了通用池功能。
ForkJoinPool
通過如下兩個靜態方法提供通用池功能。
-
ForkJoinPool commonPool()
:該方法返回一個通用池。
通用池的運行狀態不會受shutdown()
或shutdownNow()
方法的影響。當然,如果程序直接執行System.exit(0)
;來終止虛擬機,通用池以及通用池中正在執行的任務都會被自動終止。
-
int getCommonPoolParallelism()
:該方法返回通用池的并行級別。
創建了ForkJoinPool
實例之后,就可調用ForkJoinPool
的submit(ForkJoin Task task)
或invoke(ForkJoinTask task)
方法來執行指定任務了。
其中ForkJoinTask
代表一個可以并行、合并的任務。
ForkJoinTask
是一個抽象類,它還有兩個抽象子類:RecursiveAction
和Recursive Task
。
其中Recursive Task
代表有返回值的任務,而RecursiveAction
代表沒有返回值的任務。
下面以執行沒有返回值的“大任務”(簡單地打印0-300的數值)為例,程序將一個“大任務”拆分成多個“小任務”,并將任務交給ForkJoinPool
來執行。
// 繼承RecursiveAction來實現"可分解"的任務
class PrintTask extends RecursiveAction
{
// 每個“小任務”只最多只打印50個數
private static final int THRESHOLD = 50;
private int start;
private int end;
// 打印從start到end的任務
public PrintTask(int start, int end)
{
this.start = start;
this.end = end;
}
@Override
protected void compute()
{
// 當end與start之間的差小于THRESHOLD時,開始打印
if(end - start < THRESHOLD)
{
for (int i = start ; i < end ; i++ )
{
System.out.println(Thread.currentThread().getName() + "的i值:" + i);
}
}
else
{
// 如果當end與start之間的差大于THRESHOLD時,即要打印的數超過50個
// 將大任務分解成兩個小任務。
int middle = (start + end) / 2;
PrintTask left = new PrintTask(start, middle);
PrintTask right = new PrintTask(middle, end);
// 并行執行兩個“小任務”
left.fork();
right.fork();
}
}
}
/**
* description: 主函數
**/
public class ForkJoinPoolTest
{
public static void main(String[] args)
throws Exception
{
ForkJoinPool pool = new ForkJoinPool();
// 提交可分解的PrintTask任務
pool.submit(new PrintTask(0 , 300));
pool.awaitTermination(2, TimeUnit.SECONDS);
// 關閉線程池
pool.shutdown();
}
}
上面定義的任務是一個沒有返回值的打印任務,如果大任務是有返回值的任務,則可以讓任務繼承Recursive Task<T>
,其中泛型參數T就代表了該任務的返回值類型。下面程序示范了使用Recursive Task
對一個長度為100的數組的元素值進行累加。
// 繼承RecursiveTask來實現"可分解"的任務
class CalTask extends RecursiveTask<Integer>
{
// 每個“小任務”只最多只累加20個數
private static final int THRESHOLD = 20;
private int arr[];
private int start;
private int end;
// 累加從start到end的數組元素
public CalTask(int[] arr , int start, int end)
{
this.arr = arr;
this.start = start;
this.end = end;
}
@Override
protected Integer compute()
{
int sum = 0;
// 當end與start之間的差小于THRESHOLD時,開始進行實際累加
if(end - start < THRESHOLD)
{
for (int i = start ; i < end ; i++ )
{
sum += arr[i];
}
return sum;
}
else
{
// 如果當end與start之間的差大于THRESHOLD時,即要累加的數超過20個時
// 將大任務分解成兩個小任務。
int middle = (start + end) / 2;
CalTask left = new CalTask(arr , start, middle);
CalTask right = new CalTask(arr , middle, end);
// 并行執行兩個“小任務”
left.fork();
right.fork();
// 把兩個“小任務”累加的結果合并起來
return left.join() + right.join(); // ①
}
}
}
/**
* description: 主函數
**/
public class Sum
{
public static void main(String[] args)
throws Exception
{
int[] arr = new int[100];
Random rand = new Random();
int total = 0;
// 初始化100個數字元素
for (int i = 0 , len = arr.length; i < len ; i++ )
{
int tmp = rand.nextInt(20);
// 對數組元素賦值,并將數組元素的值添加到sum總和中。
total += (arr[i] = tmp);
}
System.out.println(total);
// 創建一個通用池
ForkJoinPool pool = ForkJoinPool.commonPool();
// 提交可分解的CalTask任務
Future<Integer> future = pool.submit(new CalTask(arr , 0 , arr.length));
System.out.println(future.get());
// 關閉線程池
pool.shutdown();
}
}
??
線程相關的類
ThreadLocal
類
ThreadLocal,是Thread Local Variable(線程局部變量)的意思,它就是為每一個使用該變量的線程都提供一個變量值的副本,使每一個線程都可以獨立地改變自己的副本,而不會和其他線程的副本沖突。從線程的角度看,就好像每一個線程都完全擁有該變量一樣。
它只提供了如下三個public方法。
T get()
:返回此線程局部變量中當前線程副本中的值。void remove()
:刪除此線程局部變量中當前線程的值。void set(T value)
:設置此線程局部變量中當前線程副本中的值。
代碼示例:
/**
* description: 賬戶類
**/
class Account
{
/* 定義一個ThreadLocal類型的變量,該變量將是一個線程局部變量
每個線程都會保留該變量的一個副本 */
private ThreadLocal<String> name = new ThreadLocal<>();
// 定義一個初始化name成員變量的構造器
public Account(String str)
{
this.name.set(str);
// 下面代碼用于訪問當前線程的name副本的值
System.out.println("---" + this.name.get());
}
// name的setter和getter方法
public String getName()
{
return name.get();
}
public void setName(String str)
{
this.name.set(str);
}
}
/**
* description: 線程類
**/
class MyTest extends Thread
{
// 定義一個Account類型的成員變量
private Account account;
public MyTest(Account account, String name)
{
super(name);
this.account = account;
}
public void run()
{
// 循環10次
for (int i = 0 ; i < 10 ; i++)
{
// 當i == 6時輸出將賬戶名替換成當前線程名
if (i == 6)
{
account.setName(getName());
}
// 輸出同一個賬戶的賬戶名和循環變量
System.out.println(account.getName() + " 賬戶的i值:" + i);
}
}
}
/**
* description: 主程序
**/
public class ThreadLocalTest
{
public static void main(String[] args)
{
// 啟動兩條線程,兩條線程共享同一個Account
Account at = new Account("初始名");
/*
雖然兩條線程共享同一個賬戶,即只有一個賬戶名
但由于賬戶名是ThreadLocal類型的,所以每條線程
都完全擁有各自的賬戶名副本,所以從i == 6之后,將看到兩條
線程訪問同一個賬戶時看到不同的賬戶名。
*/
new MyTest(at , "線程甲").start();
new MyTest(at , "線程乙").start ();
}
}
??
程序結果如圖:
分析:
上面Account
類中的三行粗體字代碼分別完成了創建ThreadLocal
對象、從ThreadLocal
中取出線程局部變量、修改線程局部變量的操作。
由于程序中的賬戶名是一個ThreadLocal
變量,所以雖然程序中只有一個Account
對象,但兩個子線程將會產生兩個賬戶名(主線程也持有一個賬戶名的副本)。
兩個線程進行循環時都會在i=6
時將賬戶名改為與線程名相同,這樣就可以看到兩個線程擁有兩個賬戶名的情形,如圖所示。
從上面程序可以看出,實際上賬戶名有三個副本,主線程一個,另外啟動的兩個線程各一個,它們的值互不干擾,每個線程完全擁有自己的ThreadLocal
變量,這就是ThreadLocal
的用途。
ThreadLocal
和其他所有的同步機制一樣,都是為了解決多線程中對同一變量的訪問沖突。
在普通的同步機制中,是通過對象加鎖來實現多個線程對同一變量的安全訪問的。該變量是多個線程共享的,所以要使用這種同步機制,需要很細致地分析在什么時候對變量進行讀寫,什么時候需要鎖定某個對象,什么時候釋放該對象的鎖等。在這種情況下,系統并沒有將這份資源復制多份,只是采用了安全機制來控制對這份資源的訪問而已。
ThreadLocal
從另一個角度來解決多線程的并發訪問,ThreadLocal
將需要并發訪問的資源復制多份,每個線程擁有一份資源,每個線程都擁有自己的資源副本,從而也就沒有必要對該變量進行同步了。
ThreadLocal
提供了線程安全的共享對象,在編寫多線程代碼時,可以把不安全的整個變量封裝進ThreadLocal
,或者把該對象與線程相關的狀態使用ThreadLocal
保存。
ThreadLocal
并不能替代同步機制,兩者面向的問題領域不同。同步機制是為了同步多個線程對相同資源的并發訪問,是多個線程之間進行通信的有效方式;
而ThreadLocal
是為了隔離多個線程的數據共享,從根本上避免多個線程之間對共享資源(變量)的競爭,也就不需要對多個線程進行同步了。
通常建議:
如果多個線程之間需要共享資源,以達到線程之間的通信功能,就使用同步機制;如果僅僅需要隔離多個線程之間的共享沖突,則可以使用ThreadLocal
。
??
包裝線程不安全的集合
像ArrayList
、LinkedList
、HashSet
、TreeSet
、HashMap
、TreeMap
等都是線程不安全的,也就是說,當多個并發線程向這些集合中存、取元素時,就可能會破壞這些集合的數據完整性。
如果程序中有多個線程可能訪問以上這些集合,就可以使用Collections
提供的類方法把這些集合包裝成線程安全的集合。Collections
提供了如下幾個靜態方法。
<T>Collection<T>synchronizedCollection(Collection<T>c)
:返回指定collection對應的線程安全的collection。static<T>List<T>synchronizedList(List<T>list)
:返回指定List對象對應的線程安全的List對象。static<K,V>Map<K,V> synchronizedMap(Map<K,V>m)
:返回指定Map對象對應的線程安全的Map對象。static<T>Set<T>synchronizedSet(Set<T>s)
:返回指定Set對象對應的線程安全的Set對象。static<K,V>SortedMap<K,V>synchronizedSortedMap(SortedMap<K,V>m)
:返回指定SortedMap對象對應的線程安全的SortedMap對象。static<T>SortedSet-T>synchronizedSortedSet(SortedSet<T>s)
:返回指定SortedSet對象對應的線程安全的SortedSet對象。
例如需要在多線程中使用線程安全的HashMap對象,則可以采用如下代碼:
//使用Collections的synchronizedMap方法將一個普通的HashMap包裝成線程安全的類
HashMap m=Collections.synchronizedMap(new HashMap());
??
tips:
如果需要把某個集合包裝成線程安全的集合,則應該在創建之后立即包裝,如上程序所示,當HashMap
對象創建后立即被包裝成線程安全的HashMap
對象。
線程安全的集合類
線程安全的集合類可分為如下兩類:
以
Concurrent
開頭的集合類,如ConcurrentHashMap
、ConcurrentSkipListMap
、ConcurrentSkip ListSet
、
ConcurrentLinkedQueue
和ConcurrentLinkedDeque
以
CopyOnWrite
開頭的集合類,如CopyOnWriteArrayList
、CopyOnWriteArraySet
其中以Concurrent
開頭的集合類代表了支持并發訪問的集合,它們可以支持多個線程并發寫入訪問,這些寫入線程的所有操作都是線程安全的,但讀取操作不必鎖定。
以Concurrent
開頭的集合類采用了更復雜的算法來保證永遠不會鎖住整個集合,因此在并發寫入時有較好的性能。
在默認情況下,ConcurrentHashMap
支持16個線程并發寫入,當有超過16個線程并發向該Map
中寫入數據時,可能有一些線程需要等待。實際上,程序通過設置concurrencyLevel
構造參數(默認值為16)來支持更多的并發寫入線程。
與前面介紹的HashMap
和普通集合不同的是,因為ConcurrentLinkedQueue
和ConcurrentHashMap
支持多線程并發訪問,所以當使用迭代器來遍歷集合元素時,該迭代器可能不能反映出創建迭代器之后所做的修改,但程序不會拋出任何異常。
Java8擴展了ConcurrentHashMap
的功能,Java8為該類新增了30多個新方法,這些方法可借助于Stream
和Lambda
表達式支持執行聚集操作。ConcurrentHashMap
新增的方法大致可分為如下三類:
forEach
系列(forEach,forEachKey,forEach Value,forEachEntry)
search
系列(search,searchKeys,search Values,searchEntries)
reduce
系列(reduce,reduce ToDouble,reduce ToLong,reduceKeys,reduceValues)
除此之外,ConcurrentHashMap
還新增了mappingCount()
、newKeySet()
等方法,增強后的ConcurrentHashMap
更適合作為緩存實現類使用。
??
CopyOnWriteAtraySet
由于CopyOnWriteAtraySet
的底層封裝了CopyOnWriteArmayList
,因此它的實現機制完全類似于CopyOnWriteArrayList
集合。
對于CopyOnWriteArrayList
集合,,它采用復制底層數組的方式來實現寫操作。
當線程對CopyOnWriteArrayList
集合執行讀取操作時,線程將會直接讀取集合本身,無須加鎖與阻塞。
當線程對CopyOnWriteArrayList
集合執行寫入操作時(包括調用add()、
remove()、
set()`等方法)該集合會在底層復制一份新的數組,接下來對新的數組執行寫入操作。
由于對 CopyOnWriteArmayList
集合的寫入操作都是對數組的副本執行操作,因此它是線程安全的。
需要指出的是,由于CopyOnWriteArrayList
執行寫入操作時需要頻繁地復制數組,性能比較差。
但由于讀操作與寫操作不是操作同一個數組,而且讀操作也不需要加鎖,因此讀操作就很快、很安全。由此可見,CopyOnWriteArayList
適合用在讀取操作遠遠大于寫入操作的場景中,例如緩存等。