一、線程安全問題
提起java多線程與併發就不得不提起Synchronized關鍵字,本篇就介紹一下博主對該關鍵字的理解與應用。
Synchronized一般用於解決線程安全問題,那麼我們首先來看一看爲什麼會由線程安全問題。在JVM中,程序運行的實體是一個個的進程,而進程在創建時也會爲自身開闢一段空間存放自身線程內的私有數據。同時,我們新建的基本變量、對象實例、靜態變量等存放在程序共享內存(堆、棧、靜態方法區等)之中,這部分內存對每個線程都是開放的,但線程不會直接在共享內存中對變量進行修改,而是先將變量複製到自身私有線程內,產生一個變量副本,在私有內存中修改完畢後(此時變量不具備可見性)再將此變量寫回至共享內存中,完成一次變量的修改。
在線程的私有內存中,變量副本是不具備可見性的。因此,在多個線程同時對同一個共享變量操作時,每個線程中的修改對於其他線程而言都是不可見的,而各個線程之間的運行也是不具備有序性的,最終使共享內存中的變量值與預期不一致,此時便會出現線程安全問題。
需要注意的是,這裏提到的共享變量並非只有靜態變量,任何變量都可能發生線程安全問題,在線程安全問題中,我們要關注的是變量的內存而非變量本身。也就是說我們可以得到線程安全問題的發生條件:
多個線程 同時 對 同一個內存地址 進行讀寫操作時,會引發線程安全問題。
以下是一個比較經典的例子,線程A和線程B同時對num自加100000次,按正常邏輯,最終num應該爲200000,但實際結果與預期總會有出入:
public class IncreaseTest {
private int num = 0;
public void increase(){
for (int i = 0; i < 1000000; i++) {
num++;
}
}
public int getNum(){
return num;
}
}
public class HashMapActivity extends Activity {
private static final String TAG = "HashMapActivity";
private TextView tv_showDemo;
private IncreaseTest it,it2;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_hash_map);
tv_showDemo = (TextView)findViewById(R.id.tv_showDemo);
it = new IncreaseTest();
it2 = new IncreaseTest();
Thread t1 = new Thread(new myRunnable1());
Thread t2 = new Thread(new myRunnable2());
try {
t1.start();
t2.start();
//join()防止父線程提前結束,會等待子線程執行完後再結束
t1.join();
t2.join();
}catch (Exception e){
e.printStackTrace();
}
Log.d(TAG,"num is : " +it.getNum());
}
private class myRunnable1 implements Runnable{
@Override
public void run() {
it.increase();
}
}
private class myRunnable2 implements Runnable {
@Override
public void run() {
it.increase();
}
}
運行多次每次的值都不符合預期,且變化無規律:
09-23 15:38:44.759 18272 18272 D HashMapActivity: num is : 1391757
09-23 15:39:02.363 18407 18407 D HashMapActivity: num is : 1446119
09-23 15:39:15.233 18523 18523 D HashMapActivity: num is : 1613234
解決該問題的方法其實就是通過爲increase()實例方法添加Synchronized關鍵字即可,接下來我們就討論一下該關鍵字的作用。
二、Synchronized的理解與應用
在各種資料書籍中都有介紹Synchronized關鍵字有以下三種用法:
直接作用於實例方法: 相當於對當前實例加鎖,進入同步代碼前要獲得當前實例的鎖;
直接作用於靜態方法: 相當於對當前類加鎖,進入同步代碼前要獲得當前類的鎖。
指定加鎖對象,作用於代碼塊:對給定對象加鎖,進入同步代碼前要獲得給定對象的鎖;
這三種對代碼塊、對實例、對類加鎖相信都看煩了已經,但往往在程序中一看到Synchronized還是一臉懵逼,理不清楚同步鎖到底什麼時候應該加,應該加給誰?
要回答以上問題,我們還得從源頭入手,我們再來看一下線程安全問題的原因:
多個線程 同時 對 同一個內存地址 進行讀寫操作時,會引發線程安全問題。
這裏有兩個關鍵點,即同時及同一個內存地址,Synchronized要解決線程安全,則必須打破這兩個條件纔可以。首先,同步鎖的作用機制即擁有鎖的線程纔可以運行,其他線程必須等待鎖的持有者執行完畢纔可以再競爭鎖,解決了同時的問題。那麼我們只需要分析,同一個內存地址是否有可能被多個線程同時訪問即可知道什麼時候該加鎖以及把鎖加給誰。
此外,我們還需要弄明白一下爲什麼給實例方法加Synchronized就可以鎖到實例,這是因爲java其實和C++、VB等一樣,都是先編譯、再執行的,方法Increase()編譯後的可執行代碼只有1塊。如果創建了兩個對象it、it2,它們內部的“方法表”中都會記下指向可執行代碼塊的“函數指針”;至於如何讓函數調用的時候能夠訪問對應對象的成員,其實就是調用時,把 it 或 it2 作爲 this。你可以簡單理解爲,方式會自動加上一個參數,按照 Increase(this) 的定義編譯/執行。靜態方法的區別就是不加這個參數,所以和對象實例無關的。
接下來我們通過幾個場景分析一下具體的使用方法。
場景一:針對非靜態成員變量的實例鎖
在剛纔的例子裏,之所以發生線程安全問題,是因爲線程t1和t2同時訪問了對象it的num屬性,此時的內存狀態如下圖,it對象的實例引用存放於棧中,對象內容存放於堆中,線程t1,t2同時訪問非靜態成員變量num,即構成了對同一個內存地址進行訪問。
解決該問題也很簡單,就是對increase()方法添加Synchronized進行同步就可以,如此,先獲得鎖的線程執行完畢後,另一個線程纔可以繼續操作,這樣num最終的運算結果就符合預期了。
public synchronized void increase(){
for (int i = 0; i < 1000000; i++) {
num++;
}
}
09-25 10:20:02.761 9147 9147 D HashMapActivity: num is : 2000000
09-25 10:22:16.977 9563 9563 D HashMapActivity: num is : 2000000
09-25 10:23:24.775 9868 9868 D HashMapActivity: num is : 2000000
當然,如果兩個線程同時訪問的是不同的實例對象,那麼就不會產生線程安全問題,因爲不同的實例其非靜態成員變量在堆中有不同的內存地址,相互之間不會影響,所以我們說這裏對實例方法添加的Synchronized鎖的是當前實例,不同實例之間不會觸發鎖的互斥。
private class myRunnable1 implements Runnable{
@Override
public void run() {
//IncreaseTest.sIncrease();
it.increase();
}
}
private class myRunnable2 implements Runnable {
@Override
public void run() {
//IncreaseTest.sIncrease();
it2.increase();
}
}
運行結果:
09-25 10:41:06.313 12216 12216 D HashMapActivity: it num is : 1000000 it2 num is :1000000
09-25 10:41:50.594 12449 12449 D HashMapActivity: it num is : 1000000 it2 num is :1000000
09-25 10:42:36.345 12678 12678 D HashMapActivity: it num is : 1000000 it2 num is :1000000
總結: 對於非靜態成員變量,一個實例享有獨自的內存地址,而不同實例之間的內存地址不同,多個線程同時訪問相同實例時會觸發鎖的互斥,同時訪問不同實例時就不會產生互斥,即表現爲實例鎖。
場景二:靜態成員變量的類鎖
以上我們討論的是非靜態成員變量,但當我們把num設置爲靜態成員變量後發現,兩個線程同時訪問不同的實例時也會引發線程安全問題,因爲靜態成員變量存儲在方法區中,每個實例共享同一個靜態變量,即訪問的是同一個內存地址。此外,雖然對increase()方法加了鎖,但調用時實際是increase(it) 和 increase(it2),因此不會觸發鎖的互斥。
public class IncreaseTest {
private static int num = 0;
public synchronized void increase(){
for (int i = 0; i < 1000000; i++) {
num++;
}
}
...
}
09-26 16:17:45.988 8539 8539 D HashMapActivity: it num is : 1539043 it2 num is :1539043
09-26 16:21:49.796 8539 8539 D HashMapActivity: it num is : 3395161 it2 num is :3395161
09-26 16:21:55.138 8539 8539 D HashMapActivity: it num is : 5395161 it2 num is :5395161
此時內存狀態如下:
解決該場景的問題,就需要把increase()方法也設置爲靜態方法,這樣該方法就成爲了類方法,與實例再無關係,不同的對象訪問時就會觸發鎖的互斥而不會引發線程安全問題:
public class IncreaseTest {
private static int num = 0;
public static synchronized void increase(){
for (int i = 0; i < 1000000; i++) {
num++;
}
}
...
}
09-26 16:17:45.988 8539 8539 D HashMapActivity: it num is : 2000000 it2 num is :2000000
09-26 16:21:49.796 8539 8539 D HashMapActivity: it num is : 2000000 it2 num is :2000000
09-26 16:21:55.138 8539 8539 D HashMapActivity: it num is : 2000000 it2 num is :2000000
此時內存狀態如下:
場景三:指定對象,爲代碼塊加鎖
以上是兩種單獨的場景,但在實際使用中,確有很大限制,例如靜態方法裏不能調用非靜態對象或者一個長方法中只有一部分需要同步,這時候給整個方法加鎖雖然保證了線程安全,但卻犧牲了不少效率,好在Synchronized還爲我們提供了一種用法,基於一個對象,爲一片代碼塊加鎖,該對象必須爲已實例化的對象。這種場景原理不變,該對象爲非靜態時,相當於給當前實例加鎖; 而該對象爲靜態時,則相當於給當前類加鎖,使用方法如下:
public class IncreaseTest {
private static int num = 0;
//非靜態對象,爲當前實例加鎖; 靜態對象爲當前類加鎖
private Object lock = new Object();
private static Object ob = new Object();
public void increase(){
...
synchronized (lock) {
for (int i = 0; i < 1000000; i++) {
num++;
}
}
...
}
}
以上就是博主對安全和Synchronized關鍵字的理解及應用方法,希望能幫助到各位,有缺陷的地方還請指正~