Android小知識-Java多執行緒相關(同步方法與同步程式碼塊)
本平臺的文章更新會有延遲,大家可以關注微信公眾號-顧林海,如果大家想獲取最新教程,請關注微信公眾號,謝謝!
“非執行緒安全”是指在多個執行緒對同一個物件中的例項變數進行併發訪問,導致讀取到的資料與預期不符,也就是“髒讀”,而“執行緒安全”就是指獲得的例項變數的值是經過同步處理的,不會出現“髒讀”現象。
如果是方法內的私有變數就不會存在“非執行緒安全”問題,也就說“非執行緒安全”的問題存在於“例項變數”中,我們看下面這段程式碼:
public class Task { private String name = "bill"; public void setName(int index) { try { Thread.sleep(2000); switch (index) { case 1: name = "jack"; break; case 2: name = "rose"; break; default: name = "default"; break; } } catch (InterruptedException e) { e.printStackTrace(); } System.out.println("name="+name); } }
Task這個類很簡單,內部只有一個setName方法,傳入一個整型引數,如果是1,name就被賦值為jack,如果是2,name就被賦值為rose,最後列印name。接下來就把這個Task例項交給兩個執行緒去處理。
public class ThreadFirst extends Thread { private Task mTask; public ThreadFirst(Task task) { this.mTask = task; } @Override public void run() { super.run(); mTask.setName(1); } }
public class ThreadSecond extends Thread { private Task mTask; public ThreadSecond(Task task) { this.mTask = task; } @Override public void run() { super.run(); mTask.setName(2); } }
兩個執行緒類都差不多,唯一不同的地方就是呼叫mTask的setName方法分別傳入1和2,如果執行這兩個執行緒,是不是輸出兩個不同的name值,我們看Client程式碼:
public class Client { public static void main(String[] args) { Task task=new Task(); Thread threadFirst=new ThreadFirst(task); Thread threadSecond=new ThreadSecond(task); threadFirst.start(); threadSecond.start(); } }
看看列印結果:
name=jack name=jack
發現輸出的結果和我們預期不一樣,這就是“非執行緒安全”問題,如何解決呢,可以按照上一節 ofollow,noindex">Android小知識-關於多執行緒的基礎知識瞭解下 中提到的給setName方法前加上關鍵字synchronized,程式碼如下:
public class Task { private String name = "bill"; synchronized public void setName(int index) { try { Thread.sleep(2000); switch (index) { case 1: name = "jack"; break; case 2: name = "rose"; break; default: name = "default"; break; } } catch (InterruptedException e) { e.printStackTrace(); } System.out.println("name=" + name); } }
列印結果:
name=jack name=rose
新增關鍵字synchronized後,這個setName方法就是一個同步方法,多個執行緒執行該方法時是排隊執行的。
現在Task類中只有一個 同步方法,再新增一個非同步方法:
public class Task { private String name = "bill"; synchronized public void setName(int index) { try { Thread.sleep(2000); switch (index) { case 1: name = "jack"; break; case 2: name = "rose"; break; default: name = "default"; break; } } catch (InterruptedException e) { e.printStackTrace(); } System.out.println("[" + Thread.currentThread().getName() + "]" + "name=" + name); } public void getName() { System.out.println("[" + Thread.currentThread().getName() + "]" + "name=" + name); } }
新添加了一個getName方法,列印的時候連執行緒名一起列印,方便我們檢視,第一個執行緒程式碼不變,兩個執行緒類如下:
public class ThreadFirst extends Thread { private Task mTask; public ThreadFirst(Task task) { this.mTask = task; } @Override public void run() { super.run(); mTask.setName(1); } }
public class ThreadSecond extends Thread { private Task mTask; public ThreadSecond(Task task) { this.mTask = task; } @Override public void run() { super.run(); mTask.getName(); } }
Client程式碼如下:
public class Client { public static void main(String[] args) { Task task=new Task(); Thread threadFirst=new ThreadFirst(task); threadFirst.setName("ThreadFirst"); Thread threadSecond=new ThreadSecond(task); threadSecond.setName("ThreadSecond"); threadFirst.start(); threadSecond.start(); } }
程式碼沒什麼變化,建立一個Task例項,交由兩個執行緒處理。
列印結果:
[ThreadSecond]name=bill [ThreadFirst]name=jack
按照預期應該是先執行setName方法列印jack,再執行getName方法列印jack,現在是先執行了getName方法,再執行setName方法,也就是說,ThreadFirst執行緒先持有了object物件的Lock鎖,ThreadSecond執行緒可以以非同步的方式呼叫objec物件中的非synchronized型別的方法。
現在我們給getName方法前也加上關鍵字synchronized:
synchronized public void getName() { System.out.println("[" + Thread.currentThread().getName() + "]" + "name=" + name); }
執行程式,列印:
[ThreadFirst]name=jack [ThreadSecond]name=jack
這樣的話ThreadFirst先持有object物件的Lock鎖,ThreadSecond執行緒如果在這時呼叫object物件中的synchronized型別的方法則需等待,也就是同步。
通過多個執行緒呼叫同一個方法時,為了避免資料出現交叉的情況,使用synchronized關鍵字來進行同步,雖然在賦值時進行了同步,但在取值時有可能出現“髒讀”,發生“髒讀”的情況是在讀取例項變數時,此值已經被其它執行緒更改過了,看下面程式碼:
public class Task { private String name = "bill"; private String password="12345"; synchronized public void setName(String name,String password) { try { this.name=name; Thread.sleep(5000); this.password=password; } catch (InterruptedException e) { e.printStackTrace(); } System.out.println("[" + Thread.currentThread().getName() + "]" + "name=" + name+"password="+password); } public void getInfo(){ System.out.println("[" + Thread.currentThread().getName() + "]" + "name=" + name+"password="+password); } }
setName是一個同步方法,傳入姓名和密碼,在賦值密碼前先休眠5秒,最後列印使用者名稱和密碼,而getInfo是非同步方法,用來列印使用者名稱和密碼。
public class ThreadFirst extends Thread { private Task mTask; public ThreadFirst(Task task) { this.mTask = task; } @Override public void run() { super.run(); mTask.setName("jack","poiuytr"); } }
ThreadFirst執行緒類很簡單,就是呼叫mTask例項的setName方法,設定使用者名稱為jack,密碼為poiuytr。
public class Client { public static void main(String[] args) { Task task=new Task(); Thread threadFirst=new ThreadFirst(task); threadFirst.setName("ThreadFirst"); threadFirst.start(); try { Thread.sleep(2000); } catch (InterruptedException e) { e.printStackTrace(); } task.getInfo(); } }
建立Task例項和執行緒例項threadFirst,執行threadFirst執行緒,休眠2秒後獲取相關資訊。
列印:
[main]name=jackpassword=12345 [ThreadFirst]name=jackpassword=poiuytr
通過列印結果就可以看出資料出現了髒讀,出現髒讀的原因是因為getInfo方法並不是同步的,所以可以在任意時候呼叫,解決辦法就是加上同步synchronized關鍵字。
關鍵字synchronized擁有鎖重入的功能,也就是在使用synchronized時,當一個執行緒得到一個物件鎖後,再次請求此物件鎖時是可以再次得到該物件的鎖,在一個synchronized方法/塊的內部呼叫本類的其他synchronized方法/塊時,是永遠可以得到鎖的。當一個執行緒執行的程式碼出現異常時,其所持有的鎖會自動釋放。
用關鍵字synchronized宣告方法在某些情況下是有弊端的,當某個執行緒呼叫同步方法執行一個長時間的任務,那麼其他執行緒就必須等待比較長的時間,在這樣的情況下可以使用synchronized同步語句塊來解決,synchronized方法是對當前物件進行加鎖,而synchronized程式碼塊是對某一個物件進行加鎖。在使用同步synchronized程式碼塊時需要注意的是,當一個執行緒訪問object的一個synchronized(this)同步程式碼塊時,其他執行緒對同一個object中的所有其他synchronized(this)同步程式碼塊的訪問將被阻塞,這說明synchronized使用的“物件監視器”是一個。
多個執行緒呼叫同一個物件中的不同名稱的synchronized同步方法或synchronized(this)同步程式碼塊時,呼叫的效果就是按順序執行的,也就是同步,阻塞的。為此Java提供“任意物件”作為“物件監視器”來實現同步的功能。這個任意物件大多數是例項變數及方法的引數,使用格式為synchronized(非this物件)。

838794-506ddad529df4cd4.webp.jpg