1. 程式人生 > 其它 >java synchronized靜態同步方法與非靜態同步方法,同步語句塊

java synchronized靜態同步方法與非靜態同步方法,同步語句塊

技術標籤:java核心程式設計java多執行緒面試

對程式碼進行同步控制我們可以選擇同步方法,也可以選擇同步塊,這兩種方式各有優缺點。同步塊不僅可以更加精確的控制物件鎖,還可以控制鎖的作用域,何謂鎖的作用域?鎖的作用域就是從鎖被獲取到其被釋放的時間。而且可以選擇要獲取哪個物件的物件鎖。但是如果在使用同步塊機制時,如果使用過多的鎖也會容易引起死鎖問題,同時獲取和釋放所也有代價,而同步方法,它們所擁有的鎖就是該方法所屬的類的物件鎖,換句話說,也就是this物件,而且鎖的作用域也是整個方法,這可能導致其鎖的作用域可能太大,也有可能引起死鎖,同時因為可能包含了不需要進行同步的程式碼塊在內,也會降低程式的執行效率。而不管是同步方法還是同步塊,我們都不應該在他們的程式碼塊內包含無限迴圈,如果程式碼內部要是有了無限迴圈,那麼這個同步方法或者同步塊在獲取鎖以後因為程式碼會一直不停的迴圈著執行下去,也就沒有機會釋放它所獲取的鎖,而其它等待這把鎖的執行緒就永遠無法獲取這把鎖,這就造成了一種死鎖現象。

詳細解說一下同步方法的鎖,同步方法分為靜態同步方法與非靜態同步方法,先看程式碼再說理論。以下的案例,如果show方法不是靜態的即使被synchronized修飾也無法解決執行緒問題,大家可以先試一下

class Windows implements Runnable {
    private static int ticketNum = 10;
  
    public static synchronized void show(){
        String name = Thread.currentThread().getName();
        if(ticketNum >
0) { try { //這一步是為了演示錯票,原理是當前執行緒進入了if語句陷入沉睡的時候票被賣光, //然後當該執行緒甦醒時再來一次ticketNum--產生0號這個非法票 Thread.sleep(100); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println(name +"賣出第"
+ ticketNum + "張票"); ticketNum--; } } @Override public void run() { while (true){ show(); } } } public class ThreadTest{ public static void main(String[] args) { Windows windows = new Windows(); Windows windows1 = new Windows(); Thread thread1 = new Thread(windows); thread1.setName("視窗1"); Thread thread2 = new Thread(windows);; thread2.setName("視窗2"); Thread thread3 = new Thread(windows1); thread3.setName("視窗3"); thread1.start(); thread2.start(); thread3.start(); } }

執行結果,沒有執行緒安全問題
在這裡插入圖片描述
為何show必須是靜態同步才能解決執行緒問題?

1 所有的非靜態同步方法用的都是同一把鎖——例項物件本身(this,本例中有windows和windows1)也就是說如果一個例項物件的非靜態同步方法獲取鎖後,該例項物件的其他非靜態同步方法必須等待獲取鎖的方法釋放鎖後才能獲取鎖(本例中沒有體現,大家可以參考我的部落格:生產者消費者案例),可是別的例項物件(windows1)的非靜態同步方法因為跟該例項物件(windows)的非靜態同步方法用的是不同的鎖,所以毋須等待該例項物件已獲取鎖的非靜態同步方法釋放鎖就可以獲取他們自己的鎖,自然無法保證執行緒安全。

2 而所有的靜態同步方法用的也是同一把鎖——類物件本身(Windwos.class),這兩把鎖是兩個不同的物件,所以靜態同步方法與非靜態同步方法之間是不會有競態條件的。但是一旦一個靜態同步方法獲取鎖後,其他的靜態同步方法都必須等待該方法釋放鎖後才能獲取鎖,而不管是同一個例項物件的靜態同步方法之間,還是不同的例項物件的靜態同步方法之間,只要它們同一個類的例項物件!

同理,看以下程式碼,我們選擇使用Windows.class作為同步鎖,可以保證執行緒安全。但是如果我們還是選用this做同步鎖依然無法保證執行緒安全

class Windows implements Runnable {
    private static int ticketNum = 10;
  
    @Override
    public void run() {
        String name = Thread.currentThread().getName();
        while (true){
            //這裡也推薦用this(this代表的是main中的windows物件)或Windows.class
            synchronized (Windows.class){
                if(ticketNum > 0) {
                    try {
                        //這一步是為了演示錯票,原理是當前執行緒進入了if語句陷入沉睡的時候票被賣光,
                        //然後當該執行緒甦醒時再來一次ticketNum--產生0號這個非法票
                        Thread.sleep(100);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    System.out.println(name + "賣出第" + ticketNum + "張票");
                    ticketNum--;
                }
            }
        }
    }
}

public class ThreadTest{
        public static void main(String[] args) {
            Windows windows = new Windows();
            Windows windows1 = new Windows();
            Thread thread1 = new Thread(windows);
            thread1.setName("視窗1");
            Thread thread2 = new Thread(windows);;
            thread2.setName("視窗2");
            Thread thread3 = new Thread(windows1);
            thread3.setName("視窗3");
            thread1.start();
            thread2.start();
            thread3.start();
        }
}

對於同步塊,由於其鎖是可以選擇的,所以只有使用同一把鎖的同步塊之間才有著競態條件,這就得具體情況具體分析了,但這裡有個需要注意的地方,同步塊的鎖是可以選擇的,但是不是可以任意選擇的!!!!這裡必須要注意一個物理物件和一個引用物件的例項變數之間的區別!使用一個引用物件的例項變數作為鎖並不是一個好的選擇,因為同步塊在執行過程中可能會改變它的值,其中就包括將其設定為null,而對一個null物件加鎖會產生異常,並且對不同的物件加鎖也違背了同步的初衷!這看起來是很清楚的,但是一個經常發生的錯誤就是選用了錯誤的鎖物件,因此必須注意:同步是基於實際物件而不是物件引用的!多個變數可以引用同一個物件,變數也可以改變其值從而指向其他的物件,因此,當選擇一個物件鎖時,我們要根據實際物件而不是其引用來考慮!作為一個原則,不要選擇一個可能會在鎖的作用域中改變值的例項變數作為鎖物件!!!!