大話Android多執行緒(二) synchronized使用解析

Anlia發表於2018-02-01

版權宣告:本文為博主原創文章,未經博主允許不得轉載
原始碼:github.com/AnliaLee
大家要是看到有錯誤的地方或者有啥好的建議,歡迎留言評論

前言

這是Android多執行緒篇的第二章,在上章我們比較了ThreadRunnable建立執行緒的異同,也簡單地模擬了多執行緒執行任務的場景。但實際上,這樣執行多執行緒任務是不安全的,這章我們將分析為何會出現執行緒不安全的情況以及如何使用synchronized解決這樣的問題

往期回顧
大話Android多執行緒(一) Thread和Runnable的聯絡和區別


synchronized使用解析

同步方法(非靜態)

上回說到小R(Runnable)因為誠信經營,生意越來越好了,於是小R便多招了一個業務員。某日,在售出10張門票後不久,小R就收到了顧客的投訴,說他們買到了假票。小R懷疑是自己的手下動了手腳,便展開了調查:

當時的業務流程如下

private class SellTask {
	private int ticket = 10;
	public void sellTicket(){
		if (ticket > 0) {
			try{
				Thread.sleep(500);
				Log.e("R公司",Thread.currentThread().getName() + "賣了一張票,編號為r" + (ticket--));
			}catch (Exception e){
				e.printStackTrace();
			}
		}
	}
}

public class TicketRunnable implements Runnable {
	SellTask sellTask;
	public TicketRunnable(SellTask sellTask){
		this.sellTask = sellTask;
	}

	public void run() {
		for (int i = 0; i < 10; i++) {
			sellTask.sellTicket();
		}
	}
}
複製程式碼

票交由3個業務員去賣

SellTask sellTask = new SellTask();
TicketRunnable runnable = new TicketRunnable(sellTask);
Thread r1 = new Thread(runnable, "1號業務員");
Thread r2 = new Thread(runnable, "2號業務員");
Thread r3 = new Thread(runnable, "3號業務員");

r1.start();
r2.start();
r3.start();
複製程式碼

調查發現,出現了多個業務員售出編號相同的票的情況

大話Android多執行緒(二) synchronized使用解析

進一步調查後得知,出現這樣的情況是因為業務員答應了賣給顧客某編號的票,並收取了訂金,回頭拿票時才發現哥幾個賣得是同一張票(多個執行緒先後操作共享資料造成資料錯誤),沒辦法只能自己複製一張給顧客企圖矇混過關。小R不知道該怎麼約束自己的手下,遂公開招聘能解決問題的人

這天,一位自稱synchronized的男人前來應聘。小R問道:“s先生有何高見啊?”s先生淡定地喝了口茶,答道:

“你現在的業務流程不太可靠(執行緒不安全),讓我來統一管理整個售票業務,每一張票的出售都需經過我的審批,一張票賣完後業務員才能來我這再次申請拿票出售,這樣每張票都只能由一個業務員進行出售,問題也自然解決了(在Java中每一個物件都有一個內部鎖,當使用synchronized關鍵字宣告某個方法時,該方法將受到物件鎖的保護,這樣一次就只能有一個執行緒可以進入該方法獲得該物件的鎖,其他執行緒要想呼叫該方法,只能排隊等待。當獲得鎖的執行緒執行完該方法並釋放物件鎖後,別的執行緒才可拿到鎖進入該方法)。”

小R聽後覺得這方法不錯,便讓s先生來試試。這次依然是要出售10張票,業務流程經過s先生改進後如下

private class SellTask {
	private int ticket = 10;
	public synchronized void sellTicket(){//使用synchronized宣告sellTicket方法
		if (ticket > 0) {
			try{
				Thread.sleep(500);
				Log.e("R公司",Thread.currentThread().getName() + "賣了一張票,編號為r" + (ticket--));
			}catch (Exception e){
				e.printStackTrace();
			}
		}
	}
}
複製程式碼

問題果然解決了,小R懸著的心也終於放了下來

大話Android多執行緒(二) synchronized使用解析


同一個物件內多個同步方法

某日,小R又開始向s先生抱怨起來:“我那幫二愣子手下啊,每次進我辦公室彙報工作都是亂糟糟的,讓他們按順序一個個來就是不聽,s先生覺得該如何管管他們啊?”s先生依然淡定地抿了口茶,說道:“不急,容我先看看他們是怎麼彙報的。”小R便依著s先生的意思安排了兩個手下過來彙報工作

private class ReportTask {
	public void report1(){
		Log.e("R公司","1號業務員" + "進辦公室");
		try{
			Log.e("R公司","1號業務員" + "開始彙報");
			Thread.sleep(1000);

		}catch (Exception e){
			e.printStackTrace();
		}
		Log.e("R公司","1號業務員" + "彙報完畢");
		Log.e("R公司","1號業務員" + "出辦公室");
	}

	public void report2(){
		Log.e("R公司","2號業務員" + "進辦公室");
		try{
			Log.e("R公司","2號業務員" + "開始彙報");
			Thread.sleep(1000);

		}catch (Exception e){
			e.printStackTrace();
		}
		Log.e("R公司","2號業務員" + "彙報完畢");
		Log.e("R公司","2號業務員" + "出辦公室");
	}
}

public class ReportRunnable1 implements Runnable {
	ReportTask task;
	public ReportRunnable1(ReportTask task){
		this.task = task;
	}

	public void run() {
		task.report1();
	}
}

public class ReportRunnable2 implements Runnable {
	ReportTask task;
	public ReportRunnable2(ReportTask task){
		this.task = task;
	}

	public void run() {
		task.report2();
	}
}
複製程式碼

不一會兒,兩個手下前後腳進了辦公室

ReportTask reportTask = new ReportTask();
ReportRunnable1 runnable1 = new ReportRunnable1(reportTask);
ReportRunnable2 runnable2 = new ReportRunnable2(reportTask);

Thread r1 = new Thread(runnable1);
Thread r2 = new Thread(runnable2);
r1.start();
r2.start();
複製程式碼

大話Android多執行緒(二) synchronized使用解析

小R揉了揉腦袋,嘆氣道:“唉,他們就是這樣彙報的,每次他們一起講的時候我都不知該聽誰的。”s先生哈哈一笑,道:

“這個不難解決,下次他們再來彙報,進來第一個人我就把門鎖了,讓下一個在門外等,等第一個講完了我再放第二個進來就行了(當一個執行緒訪問物件的某個synchronized同步方法時,其他執行緒對物件中所有其它synchronized同步方法的訪問將被阻塞)”

小R聽後深以為然,便又安排剛剛那兩個業務員過來重新彙報一次,這次由s先生親自守門(別問我為啥他們傻傻的,計算機就是這麼工作的 ╮(╯▽╰)╭)

private class ReportTask {
	public synchronized void report1(){
		Log.e("R公司","1號業務員" + "進辦公室");
		try{
			Log.e("R公司","1號業務員" + "開始彙報");
			Thread.sleep(1000);

		}catch (Exception e){
			e.printStackTrace();
		}
		Log.e("R公司","1號業務員" + "彙報完畢");
		Log.e("R公司","1號業務員" + "出辦公室");
	}

	public synchronized void report2(){
		Log.e("R公司","2號業務員" + "進辦公室");
		try{
			Log.e("R公司","2號業務員" + "開始彙報");
			Thread.sleep(1000);

		}catch (Exception e){
			e.printStackTrace();
		}
		Log.e("R公司","2號業務員" + "彙報完畢");
		Log.e("R公司","2號業務員" + "出辦公室");
	}
}
複製程式碼

不一會兒,兩個業務員又來了,這次的結果令小R非常滿意

大話Android多執行緒(二) synchronized使用解析

看著小R這麼開心,s先生不禁潑起了冷水:

“你別高興得太早,你窗戶可沒鎖呢,說不定你那幫二愣子手下進不了門就從窗戶爬進來了(當一個執行緒訪問物件的某個synchronized同步方法時,另一個執行緒仍然可以訪問該物件中的非synchronized同步方法)。”

果然,之後的某次工作彙報中,這樣的事就發生了

private class ReportTask {
	public synchronized void report1(){
		Log.e("R公司","1號業務員" + "進辦公室");
		try{
			Log.e("R公司","1號業務員" + "開始彙報");
			Thread.sleep(1000);

		}catch (Exception e){
			e.printStackTrace();
		}
		Log.e("R公司","1號業務員" + "彙報完畢");
		Log.e("R公司","1號業務員" + "出辦公室");
	}

	public synchronized void report2(){
		Log.e("R公司","2號業務員" + "進辦公室");
		try{
			Log.e("R公司","2號業務員" + "開始彙報");
			Thread.sleep(1000);

		}catch (Exception e){
			e.printStackTrace();
		}
		Log.e("R公司","2號業務員" + "彙報完畢");
		Log.e("R公司","2號業務員" + "出辦公室");
	}

	public void report3(){
		Log.e("R公司","3號業務員" + "進辦公室");
		try{
			Log.e("R公司","3號業務員" + "開始彙報");
			Thread.sleep(1000);

		}catch (Exception e){
			e.printStackTrace();
		}
		Log.e("R公司","3號業務員" + "彙報完畢");
		Log.e("R公司","3號業務員" + "出辦公室");
	}
}

//執行緒啟動程式碼略...
複製程式碼

大話Android多執行緒(二) synchronized使用解析

小R:

大話Android多執行緒(二) synchronized使用解析


同步程式碼塊

是日,好友小T前來拜訪小R,卻看見小R的辦公室門窗緊閉,幾個業務員在門外排著隊。小T十分疑惑,遂敲門招呼小R讓他開門,然而卻沒有得到任何迴應。沒辦法,小T只能跟著業務員在辦公室外面等了

private class ReportTask {
	public void report1(){
		synchronized(this){
			Log.e("R公司","1號業務員" + "進辦公室");
			try{
				Log.e("R公司","1號業務員" + "開始彙報");
				Thread.sleep(1000);

			}catch (Exception e){
				e.printStackTrace();
			}
			Log.e("R公司","1號業務員" + "彙報完畢");
			Log.e("R公司","1號業務員" + "出辦公室");
		}
	}

	public void report2(){
		synchronized(this){
			Log.e("R公司","2號業務員" + "進辦公室");
			try{
				Log.e("R公司","2號業務員" + "開始彙報");
				Thread.sleep(1000);

			}catch (Exception e){
				e.printStackTrace();
			}
			Log.e("R公司","2號業務員" + "彙報完畢");
			Log.e("R公司","2號業務員" + "出辦公室");
		}
	}

	public void report3(){
		synchronized(this){
			Log.e("R公司","3號業務員" + "進辦公室");
			try{
				Log.e("R公司","3號業務員" + "開始彙報");
				Thread.sleep(1000);

			}catch (Exception e){
				e.printStackTrace();
			}
			Log.e("R公司","3號業務員" + "彙報完畢");
			Log.e("R公司","3號業務員" + "出辦公室");
		}
	}

	public void report4(){
		synchronized (this){
			Log.e("R公司","小T" + "進辦公室");
		}

	}
}
複製程式碼
ReportTask reportTask = new ReportTask();
ReportRunnable1 runnable1 = new ReportRunnable1(reportTask);
ReportRunnable2 runnable2 = new ReportRunnable2(reportTask);
ReportRunnable3 runnable3 = new ReportRunnable3(reportTask);
ReportRunnable4 runnable4 = new ReportRunnable4(reportTask);

Thread s1 = new Thread(runnable1);
Thread s2 = new Thread(runnable2);
Thread s3 = new Thread(runnable3);
Thread s4 = new Thread(runnable4);
s1.start();
s2.start();
s3.start();
s4.start();
複製程式碼

好不容易等到門開了,一個業務員走了出來,小T便一閃身溜了進去,門立刻被小R鎖上了。“你幹啥呢,差點就夾到我了!”小T抱怨道。小R不好意思笑笑,說道:“原來是小T啊,你坐你坐,待會再向你解釋,我先放下個業務員進來...”

大話Android多執行緒(二) synchronized使用解析

好不容易應付完所有業務員,小R向小T解釋了來龍去脈,並拿出了一把鑰匙交給小T

private class ReportTask {
	public void report1(){
		synchronized(this){
		//省略部分程式碼...
		}
	}

	public void report2(){
		synchronized(this){
		//省略部分程式碼...
		}
	}

	public void report3(){
		synchronized(this){
		//省略部分程式碼...
		}
	}

	private String window = "window";
	public void report4(){
		synchronized (window){
			Log.e("R公司","小T" + "進辦公室");
		}
	}
}
複製程式碼

"s先生自然有應對這種情況的妙計,這把鑰匙可以開啟窗戶(持有window物件的內建鎖),你以後可以從窗戶直接爬進來,不用在門外排隊(synchronized (obj){}同步程式碼塊用synchronized宣告方法的作用基本一致,都是對synchronized作用範圍內的程式碼進行加鎖保護,其區別在於synchronized同步程式碼塊使用更加靈活、輕巧synchronized (obj){}括號內的物件引數即為該程式碼塊持有鎖的物件。例如上述例子中,前面三個report方法中的同步程式碼塊持有鎖的物件為ReportTask的例項物件,而report4方法中的同步程式碼塊持有鎖的物件則為window。因為物件都有自己的物件鎖,只能保護屬於自己的同步程式碼塊或同步方法,所以即使其他執行緒進入前三個方法的同步程式碼塊中並獲得相應物件的鎖,也不會阻塞進入report4方法的執行緒執行其中的同步程式碼)。"

拿到鑰匙後,小T再也不用和業務員一起在門外排隊了

大話Android多執行緒(二) synchronized使用解析


靜態同步方法

瞭解了上述知識後,我們回過頭再來理解同步方法和靜態同步方法的區別。從持有鎖的物件的不同我們可以將synchronized同步程式碼的方式分為兩大派系:

  • synchronized宣告非靜態方法同步程式碼塊的synchronized (this){}和synchronized (非this物件){}這三者持有鎖的物件例項物件(類的例項物件可以有很多個),執行緒想要執行該synchronized作用範圍內的同步程式碼,需獲得物件鎖
public class SynchronizedTest {
    public synchronized void test1(){
        //持有鎖的物件為SynchronizedTest的例項物件
    }

    public void test2(){
        synchronized (this){
            //持有鎖的物件為SynchronizedTest的例項物件
        }
    }

    private String obj = "obj";
    public void test3(){
        synchronized (obj){
            //持有鎖的物件為obj
        }
    }
}
複製程式碼
  • synchronized宣告靜態方法以及同步程式碼塊的synchronized (類.class){}這兩者持有鎖的物件Class物件(每個類只有一個Class物件,而Class物件是Java類編譯後生成的.class檔案,它包含了與類有關的資訊),執行緒想要執行該synchronized作用範圍內的同步程式碼,需獲得類鎖
public class SynchronizedTest {
    public static synchronized void test4(){
        //持有鎖的物件為SynchronizedTest的Class物件(SynchronizedTest.class)
    }

    public void test5(){
        synchronized (SynchronizedTest.class){
            //持有鎖的物件為SynchronizedTest的Class物件(SynchronizedTest.class)
        }
    }
}
複製程式碼

有關例項物件Class物件的詳細知識大家可以繼續在網上查詢資料進行深挖,這裡就不贅述了,總之我們要記住的一點是

synchronized同步方法(程式碼塊)持有鎖的物件不同,則多執行緒執行相應的同步程式碼時互不干擾;若相同,則獲得該物件鎖的執行緒先執行同步程式碼,其他訪問同步程式碼的執行緒會被阻塞等待鎖的釋放


更新

為了更好地理解靜態和非靜態同步方法的區別,我這裡補充一下示例吧

在某個平行宇宙中,小R穿越去了火影的世界,為了在村子裡謀個一官半職,遂決定請村長鳴人(本體,即Class物件)和他的影分身(例項物件)吃拉麵(影分身也可以理解為克隆體,現實中沒找到啥好的例子可以代入,大家將就吧哈哈)。為了找到鳴人分佈在各個地方的影分身,小R也使出了影分身

public class SynchronizedTest {
    public synchronized void test1(){
        try{
            System.out.println("請鳴人影分身 "+Thread.currentThread().getName()+" 吃拉麵");
            Thread.sleep(1000);

        }catch (Exception e){
            e.printStackTrace();
        }
        System.out.println("鳴人影分身 "+Thread.currentThread().getName()+" 吃完拉麵了");
    }

    public static void main(String args[]) {
        Thread t1 = new Thread(new Runnable() {
            @Override
            public void run() {
                new SynchronizedTest().test1();
            }
        }, "1號");
        Thread t2 = new Thread(new Runnable() {
            @Override
            public void run() {
                new SynchronizedTest().test1();
            }
        }, "2號");
        Thread t3 = new Thread(new Runnable() {
            @Override
            public void run() {
                new SynchronizedTest().test1();
            }
        }, "3號");
        t1.start();
        t2.start();
        t3.start();
    }
}
複製程式碼

鳴人的影分身各吃各的,並沒有出現爭搶的現象(上述程式碼中非靜態同步方法使用new SynchronizedTest().xxx的方式進行呼叫,所以它們持有鎖的物件不同,因此相互之間沒有影響

大話Android多執行緒(二) synchronized使用解析

至於鳴人本體嘛,肯定要重點“賄賂”的,所以小R派了3個影分身去請他吃拉麵

public class SynchronizedTest {
    public static synchronized void test2(){
        try{
            System.out.println("請鳴人 本體 吃拉麵:" + Thread.currentThread().getName());
            Thread.sleep(1000);

        }catch (Exception e){
            e.printStackTrace();
        }
        System.out.println("鳴人 本體 吃完拉麵了:" + Thread.currentThread().getName());
    }

    public static void main(String args[]) {
        Thread t4 = new Thread(new Runnable() {
            @Override
            public void run() {
//                test2();//test2方法是靜態方法,可以直接呼叫
                new SynchronizedTest().test2();//這裡的呼叫方式僅為了作對照
            }
        },"t4");
        Thread t5 = new Thread(new Runnable() {
            @Override
            public void run() {
                new SynchronizedTest().test2();
            }
        },"t5");
        Thread t6 = new Thread(new Runnable() {
            @Override
            public void run() {
                new SynchronizedTest().test2();
            }
        },"t6");

        t4.start();
        t5.start();
        t6.start();
    }
}
複製程式碼

鳴人本體只有一個,即時3個人一起請面也是要一碗一碗吃的(靜態同步方法持有鎖的物件都是Class物件,持有鎖的執行緒正常執行任務,其他未持有鎖的執行緒則阻塞等待

大話Android多執行緒(二) synchronized使用解析

至於同步程式碼塊synchronized (this){}、synchronized (xxx.class){}的區別和上面一致,我就不贅述了,總之理解靜態和非靜態同步方法的區別最重要的是理清Class物件例項物件的關係,這裡推薦一篇部落格給大家:JAVA中類、例項與Class物件。如果大家還有什麼疑問,歡迎留言評論,我有(shang)空(ban)會(tou)來(lan)一一答覆的

相關文章