版權(quán)聲明:本文為博主原創(chuàng)文章,未經(jīng)博主允許不得轉(zhuǎn)載
Github:github.com/AnliaLee
首發(fā)地址:Anlia_掘金
大家要是看到有錯誤的地方或者有啥好的建議,歡迎留言評論
前言
這是Android多線程篇的第二章,在上章我們比較了Thread和Runnable創(chuàng)建線程的異同,也簡單地模擬了多線程執(zhí)行任務(wù)的場景。但實際上,這樣執(zhí)行多線程任務(wù)是不安全的,這章我們將分析為何會出現(xiàn)線程不安全的情況以及如何使用synchronized解決這樣的問題
synchronized使用解析
同步方法(非靜態(tài))
上回說到小R(Runnable)因為誠信經(jīng)營,生意越來越好了,于是小R便多招了一個業(yè)務(wù)員。某日,在售出10張門票后不久,小R就收到了顧客的投訴,說他們買到了假票。小R懷疑是自己的手下動了手腳,便展開了調(diào)查:
當時的業(yè)務(wù)流程如下
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個業(yè)務(wù)員去賣
SellTask sellTask = new SellTask();
TicketRunnable runnable = new TicketRunnable(sellTask);
Thread r1 = new Thread(runnable, "1號業(yè)務(wù)員");
Thread r2 = new Thread(runnable, "2號業(yè)務(wù)員");
Thread r3 = new Thread(runnable, "3號業(yè)務(wù)員");
r1.start();
r2.start();
r3.start();
調(diào)查發(fā)現(xiàn),出現(xiàn)了多個業(yè)務(wù)員售出編號相同的票的情況

進一步調(diào)查后得知,出現(xiàn)這樣的情況是因為業(yè)務(wù)員答應(yīng)了賣給顧客某編號的票,并收取了訂金,回頭拿票時才發(fā)現(xiàn)哥幾個賣得是同一張票(多個線程先后操作共享數(shù)據(jù)造成數(shù)據(jù)錯誤),沒辦法只能自己復制一張給顧客企圖蒙混過關(guān)。小R不知道該怎么約束自己的手下,遂公開招聘能解決問題的人
這天,一位自稱synchronized的男人前來應(yīng)聘。小R問道:“s先生有何高見啊?”s先生淡定地喝了口茶,答道:
“你現(xiàn)在的業(yè)務(wù)流程不太可靠(線程不安全),讓我來統(tǒng)一管理整個售票業(yè)務(wù),每一張票的出售都需經(jīng)過我的審批,一張票賣完后業(yè)務(wù)員才能來我這再次申請拿票出售,這樣每張票都只能由一個業(yè)務(wù)員進行出售,問題也自然解決了(在Java中每一個對象都有一個內(nèi)部鎖,當使用synchronized關(guān)鍵字聲明某個方法時,該方法將受到對象鎖的保護,這樣一次就只能有一個線程可以進入該方法并獲得該對象的鎖,其他線程要想調(diào)用該方法,只能排隊等待。當獲得鎖的線程執(zhí)行完該方法并釋放對象鎖后,別的線程才可拿到鎖進入該方法)?!?/p>
小R聽后覺得這方法不錯,便讓s先生來試試。這次依然是要出售10張票,業(yè)務(wù)流程經(jīng)過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懸著的心也終于放了下來

同一個對象內(nèi)多個同步方法
某日,小R又開始向s先生抱怨起來:“我那幫二愣子手下啊,每次進我辦公室匯報工作都是亂糟糟的,讓他們按順序一個個來就是不聽,s先生覺得該如何管管他們???”s先生依然淡定地抿了口茶,說道:“不急,容我先看看他們是怎么匯報的?!毙便依著s先生的意思安排了兩個手下過來匯報工作
private class ReportTask {
public void report1(){
Log.e("R公司","1號業(yè)務(wù)員" + "進辦公室");
try{
Log.e("R公司","1號業(yè)務(wù)員" + "開始匯報");
Thread.sleep(1000);
}catch (Exception e){
e.printStackTrace();
}
Log.e("R公司","1號業(yè)務(wù)員" + "匯報完畢");
Log.e("R公司","1號業(yè)務(wù)員" + "出辦公室");
}
public void report2(){
Log.e("R公司","2號業(yè)務(wù)員" + "進辦公室");
try{
Log.e("R公司","2號業(yè)務(wù)員" + "開始匯報");
Thread.sleep(1000);
}catch (Exception e){
e.printStackTrace();
}
Log.e("R公司","2號業(yè)務(wù)員" + "匯報完畢");
Log.e("R公司","2號業(yè)務(wù)員" + "出辦公室");
}
}
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();

小R揉了揉腦袋,嘆氣道:“唉,他們就是這樣匯報的,每次他們一起講的時候我都不知該聽誰的。”s先生哈哈一笑,道:
“這個不難解決,下次他們再來匯報,進來第一個人我就把門鎖了,讓下一個在門外等,等第一個講完了我再放第二個進來就行了(當一個線程訪問對象的某個synchronized同步方法時,其他線程對對象中所有其它synchronized同步方法的訪問將被阻塞)”
小R聽后深以為然,便又安排剛剛那兩個業(yè)務(wù)員過來重新匯報一次,這次由s先生親自守門(別問我為啥他們傻傻的,計算機就是這么工作的 ╮(╯▽╰)╭)
private class ReportTask {
public synchronized void report1(){
Log.e("R公司","1號業(yè)務(wù)員" + "進辦公室");
try{
Log.e("R公司","1號業(yè)務(wù)員" + "開始匯報");
Thread.sleep(1000);
}catch (Exception e){
e.printStackTrace();
}
Log.e("R公司","1號業(yè)務(wù)員" + "匯報完畢");
Log.e("R公司","1號業(yè)務(wù)員" + "出辦公室");
}
public synchronized void report2(){
Log.e("R公司","2號業(yè)務(wù)員" + "進辦公室");
try{
Log.e("R公司","2號業(yè)務(wù)員" + "開始匯報");
Thread.sleep(1000);
}catch (Exception e){
e.printStackTrace();
}
Log.e("R公司","2號業(yè)務(wù)員" + "匯報完畢");
Log.e("R公司","2號業(yè)務(wù)員" + "出辦公室");
}
}
不一會兒,兩個業(yè)務(wù)員又來了,這次的結(jié)果令小R非常滿意

看著小R這么開心,s先生不禁潑起了冷水:
“你別高興得太早,你窗戶可沒鎖呢,說不定你那幫二愣子手下進不了門就從窗戶爬進來了(當一個線程訪問對象的某個synchronized同步方法時,另一個線程仍然可以訪問該對象中的非synchronized同步方法)?!?/p>
果然,之后的某次工作匯報中,這樣的事就發(fā)生了
private class ReportTask {
public synchronized void report1(){
Log.e("R公司","1號業(yè)務(wù)員" + "進辦公室");
try{
Log.e("R公司","1號業(yè)務(wù)員" + "開始匯報");
Thread.sleep(1000);
}catch (Exception e){
e.printStackTrace();
}
Log.e("R公司","1號業(yè)務(wù)員" + "匯報完畢");
Log.e("R公司","1號業(yè)務(wù)員" + "出辦公室");
}
public synchronized void report2(){
Log.e("R公司","2號業(yè)務(wù)員" + "進辦公室");
try{
Log.e("R公司","2號業(yè)務(wù)員" + "開始匯報");
Thread.sleep(1000);
}catch (Exception e){
e.printStackTrace();
}
Log.e("R公司","2號業(yè)務(wù)員" + "匯報完畢");
Log.e("R公司","2號業(yè)務(wù)員" + "出辦公室");
}
public void report3(){
Log.e("R公司","3號業(yè)務(wù)員" + "進辦公室");
try{
Log.e("R公司","3號業(yè)務(wù)員" + "開始匯報");
Thread.sleep(1000);
}catch (Exception e){
e.printStackTrace();
}
Log.e("R公司","3號業(yè)務(wù)員" + "匯報完畢");
Log.e("R公司","3號業(yè)務(wù)員" + "出辦公室");
}
}
//線程啟動代碼略...

小R:

同步代碼塊
是日,好友小T前來拜訪小R,卻看見小R的辦公室門窗緊閉,幾個業(yè)務(wù)員在門外排著隊。小T十分疑惑,遂敲門招呼小R讓他開門,然而卻沒有得到任何回應(yīng)。沒辦法,小T只能跟著業(yè)務(wù)員在辦公室外面等了
private class ReportTask {
public void report1(){
synchronized(this){
Log.e("R公司","1號業(yè)務(wù)員" + "進辦公室");
try{
Log.e("R公司","1號業(yè)務(wù)員" + "開始匯報");
Thread.sleep(1000);
}catch (Exception e){
e.printStackTrace();
}
Log.e("R公司","1號業(yè)務(wù)員" + "匯報完畢");
Log.e("R公司","1號業(yè)務(wù)員" + "出辦公室");
}
}
public void report2(){
synchronized(this){
Log.e("R公司","2號業(yè)務(wù)員" + "進辦公室");
try{
Log.e("R公司","2號業(yè)務(wù)員" + "開始匯報");
Thread.sleep(1000);
}catch (Exception e){
e.printStackTrace();
}
Log.e("R公司","2號業(yè)務(wù)員" + "匯報完畢");
Log.e("R公司","2號業(yè)務(wù)員" + "出辦公室");
}
}
public void report3(){
synchronized(this){
Log.e("R公司","3號業(yè)務(wù)員" + "進辦公室");
try{
Log.e("R公司","3號業(yè)務(wù)員" + "開始匯報");
Thread.sleep(1000);
}catch (Exception e){
e.printStackTrace();
}
Log.e("R公司","3號業(yè)務(wù)員" + "匯報完畢");
Log.e("R公司","3號業(yè)務(wù)員" + "出辦公室");
}
}
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();
好不容易等到門開了,一個業(yè)務(wù)員走了出來,小T便一閃身溜了進去,門立刻被小R鎖上了?!澳愀缮赌?,差點就夾到我了!”小T抱怨道。小R不好意思笑笑,說道:“原來是小T啊,你坐你坐,待會再向你解釋,我先放下個業(yè)務(wù)員進來...”

好不容易應(yīng)付完所有業(yè)務(wù)員,小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先生自然有應(yīng)對這種情況的妙計,這把鑰匙可以打開窗戶(持有window對象的內(nèi)置鎖),你以后可以從窗戶直接爬進來,不用在門外排隊(synchronized (obj){}同步代碼塊和用synchronized聲明方法的作用基本一致,都是對synchronized作用范圍內(nèi)的代碼進行加鎖保護,其區(qū)別在于synchronized同步代碼塊使用更加靈活、輕巧,synchronized (obj){}括號內(nèi)的對象參數(shù)即為該代碼塊持有鎖的對象。例如上述例子中,前面三個report方法中的同步代碼塊持有鎖的對象為ReportTask的實例對象,而report4方法中的同步代碼塊持有鎖的對象則為window。因為對象都有自己的對象鎖,只能保護屬于自己的同步代碼塊或同步方法,所以即使其他線程進入前三個方法的同步代碼塊中并獲得相應(yīng)對象的鎖,也不會阻塞進入report4方法的線程執(zhí)行其中的同步代碼)。"
拿到鑰匙后,小T再也不用和業(yè)務(wù)員一起在門外排隊了

靜態(tài)同步方法
了解了上述知識后,我們回過頭再來理解同步方法和靜態(tài)同步方法的區(qū)別。從持有鎖的對象的不同我們可以將synchronized同步代碼的方式分為兩大派系:
- synchronized聲明非靜態(tài)方法、同步代碼塊的synchronized (this){}和synchronized (非this對象){}這三者持有鎖的對象為實例對象(類的實例對象可以有很多個),線程想要執(zhí)行該synchronized作用范圍內(nèi)的同步代碼,需獲得對象鎖
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聲明靜態(tài)方法以及同步代碼塊的synchronized (類.class){}這兩者持有鎖的對象為Class對象(每個類只有一個Class對象,而Class對象是Java類編譯后生成的.class文件,它包含了與類有關(guān)的信息),線程想要執(zhí)行該synchronized作用范圍內(nèi)的同步代碼,需獲得類鎖
public class SynchronizedTest {
public static synchronized void test4(){
//持有鎖的對象為SynchronizedTest的Class對象(SynchronizedTest.class)
}
public void test5(){
synchronized (SynchronizedTest.class){
//持有鎖的對象為SynchronizedTest的Class對象(SynchronizedTest.class)
}
}
}
有關(guān)實例對象和Class對象的詳細知識大家可以繼續(xù)在網(wǎng)上查找資料進行深挖,這里就不贅述了,總之我們要記住的一點是
若synchronized同步方法(代碼塊)持有鎖的對象不同,則多線程執(zhí)行相應(yīng)的同步代碼時互不干擾;若相同,則獲得該對象鎖的線程先執(zhí)行同步代碼,其他訪問同步代碼的線程會被阻塞并等待鎖的釋放