?????上篇文章我們主要介紹了并發(fā)的基本思想以及線程的基本知識雕拼,通過多線程我們可以實現(xiàn)對計算機資源的充分利用曼尊,但是在最后我們也說明了多線程給程序帶來的兩種典型的問題菲盾,針對它們昏兆,synchronized關鍵字可以很好的解決問題竿屹。對于synchronized的介紹主要包含以下一些內(nèi)容:
- synchronized修飾實例方法
- synchronized修飾靜態(tài)方法
- synchronized修飾代碼塊
- 使用synchronized解決競態(tài)條件問題
- 使用synchronized解決內(nèi)存可見性問題
一报强、使用synchronized關鍵字修飾實例方法
?????在我們的Java中,每個對象都有一把鎖和兩個隊列拱燃,一個用于掛起未獲得鎖的線程秉溉,一個用于掛起條件不滿足而不得不等待的線程。而我們的synchronized實際上也就是一個加鎖和釋放鎖的集成扼雏。先看個例子:
/*定義一個計數(shù)器類*/
public class Counter {
private int count;
public synchronized int getCount(){return this.count;}
public synchronized void addCount(){this.count++;}
}
/*定義一個線程類*/
public class MyThread extends Thread{
public static Counter counter = new Counter();
@Override
public void run(){
try {
Thread.sleep((int)(Math.random()*100));
} catch (InterruptedException e) {
e.printStackTrace();
}
counter.addCount();
}
}
/*main方法啟動100個線程*/
public static void main(String[] args){
Thread[] threads = new Thread[100];
for (int i=0;i<100;i++){
threads[i] = new MyThread();
threads[i].start();
}
for (int j=0;j<100;j++){
threads[j].join();
}
System.out.println(MyThread.counter.getCount());
}
上述程序無論運行多少次坚嗜,結(jié)果都是一樣的。
這是一個典型的使用synchronized關鍵字修飾實例方法來解決競態(tài)條件問題的示例诗充。首先在我們定義的線程類中苍蔬,我們定義了一個Counter實例,然后讓以后的每個線程在運行的時候都先隨機睡眠蝴蜓,然后調(diào)用這個公共變量count的自增方法碟绑,只不過該自增方法是有synchronized關鍵字修飾的俺猿。我們說過每個對象都有鎖和兩個隊列,這里的count實例就是一個對象格仲,這一百個線程每次在睡醒之后都要調(diào)用count的addCount方法押袍,而所有要調(diào)用addCount方法的線程都必須先獲得count這個對象的鎖,也就是說凯肋,如果有一個線程獲取了count對象的鎖并開始調(diào)用addCount方法時谊惭,其他線程都得阻塞在該對象的一個隊列上,等待獲得鎖的線程執(zhí)行結(jié)束釋放鎖侮东。
所以圈盔,在同一時刻,只可能有一個線程獲得count的鎖并對其進行自增操作悄雅,其他的線程都在該對象的阻塞隊列上進行等待驱敲,自然是不會出現(xiàn)多個線程在某個時間段同時操作同一個變量而引起該變量數(shù)據(jù)值不正確的情況。
二宽闲、使用synchronized關鍵字修飾靜態(tài)方法
?????對于靜態(tài)方法众眨,其實和實例方法是類似的。只不過synchronized關鍵字對實例方法而言容诬,它獲得的是實例對象的鎖娩梨,所有共享相同該對象的線程都必須先獲得該對象的鎖。而對于靜態(tài)方法而言放案,synchronized關鍵字獲得的是類的鎖姚建,也就是對于所有需要訪問相同類的線程都是需要先獲得該類的鎖的,否則將需要在某個阻塞隊列上進行等待吱殉。
/*定義一個線程類*/
public class MyThread extends Thread{
public static int count;
public synchronized static void addCount(){
count++;
}
@Override
public void run(){
try {
Thread.sleep((int)(Math.random()*100));
} catch (InterruptedException e) {
e.printStackTrace();
}
addCount();
}
}
/*啟動100個線程*/
public static void main(String[] args) throws InterruptedException {
Thread[] threads = new Thread[100];
for (int i=0;i<100;i++){
threads[i] = new MyThread();
threads[i].start();
}
for (int j=0;j<100;j++){
threads[j].join();
}
System.out.println(MyThread.count);
}
程序基本和我們的第一個例子相差無幾,在線程類中我們定義了一個靜態(tài)變量和一個靜態(tài)方法厘托,該方法被synchronized關鍵字修飾友雳,然后run方法依然是讓當前線程隨機睡眠,然后調(diào)用這個被synchronized關鍵字修飾的靜態(tài)方法铅匹。我們可以看到押赊,無論運行多少次的程序,結(jié)果都是一樣包斑。
每個線程在睡醒之后流礁,都要去調(diào)用addCount方法,而調(diào)用該方法前提是要獲取到類Count的鎖罗丰,如果獲取不到就必須在該對象的阻塞隊列上進行等待神帅。所以一次只會有一個線程調(diào)用addCount方法,自然是無論運行多少次萌抵,結(jié)果都會是100找御。
三元镀、使用synchronized關鍵字修飾代碼塊
?????使用synchronized關鍵字修飾一段代碼塊和上述介紹的兩種情況略微有點不同。對于實例方法霎桅,synchronized關鍵字總是嘗試去獲取某個對象的鎖栖疑,對于靜態(tài)方法,synchronized關鍵字始終嘗試去獲取某個類的鎖滔驶,而對于我們的代碼塊遇革,它就需要顯式指定以誰為鎖了。例如:
/*定義一個線程類*/
public class MyThread extends Thread{
public static Integer count = 0;
@Override
public void run(){
try {
Thread.sleep((int)(Math.random()*100));
} catch (InterruptedException e) {
e.printStackTrace();
}
synchronized (count){
count++;
}
}
}
在我們定義的線程類中揭糕,我們定義了一個靜態(tài)變量count澳淑,而每個線程在醒來之后都會去嘗試著去獲取該對象的鎖,如果得不到就阻塞在該對象的阻塞隊列上等待鎖的釋放插佛。實際上這里的synchronized關鍵字利用的就是對象count的鎖杠巡,我們上述介紹的兩種形式,synchronized關鍵字修飾在實例方法和靜態(tài)方法上雇寇,默認利用的是類對象的鎖和類的鎖氢拥。例如:
public synchronized void show(){....}
調(diào)用show方法等價于:
synchronized(this){
public void show(){...}
}
而對于靜態(tài)方法:
public class A{
public synchronized static void show(){....}
}
等價于:
synchronized(A.class){
public static void show(){....}
}
四、使用synchronized關鍵字解決內(nèi)存可見性問題
?????通過了解了synchronized應用的三種不同場景锨侯,我們對它應該有了大致的一個了解嫩海。下面我們使用它解決上篇提到的多線程的一個問題 ----- 內(nèi)存可見性問題。至于競態(tài)條件問題已經(jīng)在第一小節(jié)間接的進行介紹了囚痴,此處不再贅述叁怪。這里我們再簡單重復下內(nèi)存可見性問題,因為我們的CPU是有緩存的深滚,所以當一個線程在運行的時候奕谭,有些變量值的修改并沒有立馬寫回內(nèi)存,而是緩存在各級緩存中痴荐,這就導致其他線程訪問這個公共變量的時候就拿不到最新的值血柳,因此導致數(shù)據(jù)的值偏差,計算結(jié)果不準確生兆。我們看看一個例子:
/*定義一個線程類难捌,并定義一個共享的變量count*/
public class MyThread extends Thread{
public static int count = 0;
@Override
public void run(){
while (count==0){
//running
}
System.out.println("mythread exit");
}
}
/*main函數(shù)啟動一個線程*/
public static void main(String[] args) throws InterruptedException {
Thread thread = new MyThread();
thread.start();
Thread.sleep(1000);
MyThread.count = 1;
System.out.println(MyThread.count);
System.out.println("exit main");
}
我們在定義的線程類中定義了一個共享變量,run方法主要的工作是循環(huán)等待count不為0鸦难,而我們在main線程中修改了這個count的值根吁,由于循環(huán)這個操作是比較頻繁的判斷條件的,所以該線程并不會每次都從內(nèi)存中取出count的值合蔽,而是在它的緩存中取击敌,所以主線程對count的修改,在thread線程中是始終看不見的辈末。所以我們的程序輸出的結(jié)果如下:
主線程在修改count的值之后愚争,輸出顯示的確count的值為1映皆,然后主線程退出,但是我們發(fā)現(xiàn)程序卻沒有結(jié)束轰枝,thread的退出信息也沒有被打印捅彻。也就是說線程thread還被困在了while循環(huán)中,雖然main線程已經(jīng)修改了count的值鞍陨。這就是內(nèi)存可見性問題步淹,主要是由于多線程之間進行通訊的橋梁是內(nèi)存,而各個線程內(nèi)部又有各自的緩存诚撵,如果對公共變量的的修改沒有及時更新到內(nèi)存的話缭裆,那么就很容易導致其他線程訪問的是數(shù)據(jù)不是最新的。
我們使用synchronized關鍵字解決上述問題:
public class MyThread extends Thread{
public static int count = 0;
public synchronized static int returnCount(){return count;}
@Override
public void run(){
while(returnCount()==0){
}
System.out.println("mythread exit");
}
}
我們使用synchronized關鍵修飾了一個方法寿烟,該方法返回count的值澈驼。jvm對synchronized的兩條規(guī)定,其一是線程在解鎖之前必須把所有共享變量刷新到內(nèi)存中筛武,其二是線程在釋放鎖的時候?qū)⑶蹇账械木彺嫫仁贡揪€程在使用該共享變量的時候從內(nèi)存中去讀取缝其。這樣就可以保證每次對共享變量的讀取都是最新的。
當然如果僅僅是為了解決內(nèi)存可見性問題而使用synchronized關鍵字的話徘六,會有點大材小用内边。畢竟synchronized的成本開銷相對而言是較大的。Java中提供了一個volatile關鍵字用于解決這種內(nèi)存可見性問題待锈。例如:
public static volatile int count = 0;
像這樣漠其,我們只需要在某個變量前面加上修飾符 volatile 即可讓該變量在被讀的時候從內(nèi)存去取,也就是保持最新數(shù)據(jù)值以實現(xiàn)對內(nèi)存可見性問題的解決竿音。
至此和屎,我們簡單的介紹了synchronized關鍵字的一些基本用法,介紹了它可以修飾的場景谍失,以及使用它來解決我們的兩個典型的多線程問題眶俩。下篇文章我們將著重介紹線程間的協(xié)作機制。