原博文地址:http://www.cnblogs.com/dolphin0520/p/3923737.html
1.什麼是線程安全問題:
1)多個線程同時訪問一個資源時,會導致程序的運行結果與預測的並不一致。這個資源在這裏成爲臨界資源or共享資源【一個對象,對象中的屬性,一個文件,一個數據庫等】。
eg:用戶註冊的時候,不能往數據庫裏面插入相同的名稱M,如果Thread-1和Thread-2同時讀到Yes【即可以插入】,那麼這個線程就會將數據插入到數據庫。
這裏就產生了一個隱患了,假若Thread-1和Thread-2的數據M不一樣就沒問題。要是剛好一樣,這樣數據庫數據就出現問題了。這時候就產生了線程安全問題。
2)當多個線程執行一個方法時,方法內部的局部變量並不是臨界資源,因爲方法是在棧上執行的,而Java棧是線程私有的,因此不會產生線程安全問題。
2.解決線程安全問題:
1)通常來說,都是在訪問臨界資源的代碼前面加上一個鎖,當訪問完臨界資源後釋放鎖,讓其他線程繼續訪問。即在同一個時刻只有一個線程能夠訪問臨界資源,術語上就是:同步互斥訪問。
2)java中使用synchronized【同步】和Lock【鎖】來實現同步互斥訪問。
3.synchronized關鍵字:
1)在Java中,每一個對象都擁有一個鎖標記(monitor),也稱爲監視器,多線程同時訪問某個對象時,線程只有獲取了該對象的鎖才能訪問。
2)在java中,可以用synchronized來標記一個方法或者代碼塊,
3)當某個線程調用該對象的synchronized方法或者訪問synchronized代碼塊時,這個線程便獲得了該對象的鎖,其他線程暫時無法訪問這個方法,只有等待這個方法執行完畢或者代碼塊執行完畢,這個線程纔會釋放該對象的鎖,其他線程才能執行這個方法或者代碼塊。【synchronized作用原理】
4.synchronized方法塊:
1)加入synchronized前:
public class Test {
public static void main(String[] args) {
final InsertData insertData = new InsertData();
new Thread() {
public void run() {
insertData.insert(Thread.currentThread());
};
}.start();
new Thread() {
public void run() {
insertData.insert(Thread.currentThread());
};
}.start();
}
}
class InsertData {
private ArrayList<Integer> arrayList = new ArrayList<Integer>();
public void insert(Thread thread){
for(int i=0;i<5;i++){
System.out.println(thread.getName()+"在插入數據"+i);
arrayList.add(i);
}
}
}
結果:
結論:兩個線程在同時執行insert方法。
2)加入synchronized關鍵字後:
class InsertData {
private ArrayList<Integer> arrayList = new ArrayList<Integer>();
public synchronized void insert(Thread thread){
for(int i=0;i<5;i++){
System.out.println(thread.getName()+"在插入數據"+i);
arrayList.add(i);
}
}
}
結果:
結論:
1)Thread-1插入數據是等Thread-0插入完數據之後才進行的。說明Thread-0和Thread-1是順序執行insert方法的。
注意點:
1)當一個線程正在訪問一個對象的synchronized方法,那麼其他線程不能訪問該對象的其他synchronized方法。這個原因很簡單,因爲一個對象只有一把鎖,當一個線程獲取了該對象的鎖之後,其他線程無法獲取該對象的鎖,所以無法訪問該對象的其他synchronized方法。
2)當一個線程正在訪問一個對象的synchronized方法,那麼其他線程能訪問該對象的非synchronized方法。這個原因很簡單,訪問非synchronized方法不需要獲得該對象的鎖,假如一個方法沒用synchronized關鍵字修飾,說明它不會使用到臨界資源,那麼其他線程是可以訪問這個方法的
3)如果一個線程A需要訪問對象object1的synchronized方法fun1,另外一個線程B需要訪問對象object2的synchronized方法fun1,即使object1和object2是同一類型),也不會產生線程安全問題,因爲他們訪問的是不同的對象,所以不存在互斥問題。
5.synchronized代碼塊:
1)公式如下:
synchronized(synObject) {
}
2)當在某個線程中執行這段代碼塊,該線程會獲取對象synObject的鎖,從而使得其他線程無法同時訪問該代碼塊。
3)synObject可以是this,代表獲取當前對象的鎖,也可以是類中的一個屬性,代表獲取該屬性的鎖。
4)synchronized代碼塊可以進行如下改造:
class InsertData {
private ArrayList<Integer> arrayList = new ArrayList<Integer>();
public void insert(Thread thread){
synchronized (this) {
for(int i=0;i<100;i++){
System.out.println(thread.getName()+"在插入數據"+i);
arrayList.add(i);
}
}
}
}
class InsertData {
private ArrayList<Integer> arrayList = new ArrayList<Integer>();
private Object object = new Object();
public void insert(Thread thread){
synchronized (object) {
for(int i=0;i<100;i++){
System.out.println(thread.getName()+"在插入數據"+i);
arrayList.add(i);
}
}
}
}
好處:
1)synchronized代碼塊使用起來比synchronized方法要靈活得多。因爲也許一個方法中只有一部分代碼只需要同步,如果此時對整個方法用synchronized進行同步,會影響程序執行效率。而使用synchronized代碼塊就可以避免這個問題,synchronized代碼塊可以實現只對需要同步的地方進行同步。
2)每個類也會有一個鎖,它可以用來控制對static數據成員的併發訪問。
3)如果一個線程執行一個對象的非static synchronized方法,另外一個線程需要執行這個對象所屬類的static synchronized方法,此時不會發生互斥現象,因爲訪問static synchronized方法佔用的是類鎖,而訪問非static synchronized方法佔用的是對象鎖,所以不存在互斥現象。
例子如下:
public class Test {
public static void main(String[] args) {
final InsertData insertData = new InsertData();
new Thread(){
@Override
public void run() {
insertData.insert();
}
}.start();
new Thread(){
@Override
public void run() {
insertData.insert1();
}
}.start();
}
}
class InsertData {
public synchronized void insert(){
System.out.println("執行insert");
try {
Thread.sleep(5000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("執行insert完畢");
}
public synchronized static void insert1() {
System.out.println("執行insert1");
System.out.println("執行insert1完畢");
}
}
結果:
結論:
1)第一個線程裏面執行的是insert方法,不會導致第二個線程執行insert1方法發生阻塞現象。
注意點:
1)對於synchronized方法或者synchronized代碼塊,當出現異常時,JVM會自動釋放當前線程佔用的鎖,因此不會由於異常導致出現死鎖現象。