JAVA面試題 請談談你對Sychronized關鍵字的理解?

Java螞蟻發表於2019-07-24

面試官:sychronized關鍵字有哪些特性?

應聘者:

  • 可以用來修飾方法;

  • 可以用來修飾程式碼塊;

  • 可以用來修飾靜態方法;

  • 可以保證執行緒安全;

  • 支援鎖的重入;

  • sychronized使用不當導致死鎖;

 

瞭解sychronized之前,我們先來看一下幾個常見的概念:內建鎖、互斥鎖、物件鎖和類鎖。

 

內建鎖

在Java中每一個物件都可以作為同步的鎖,那麼這些鎖就被稱為內建鎖。執行緒進入同步程式碼塊或方法的時候會自動獲得該鎖,在退出同步程式碼塊或方法時會釋放該鎖。獲得內建鎖的唯一途徑就是進入這個鎖的保護的同步程式碼塊或方法。

 

互斥鎖

內建鎖同時也是一個互斥鎖,這就是意味著最多隻有一個執行緒能夠獲得該鎖,當執行緒A嘗試去獲得執行緒B持有的內建鎖時,執行緒A必須等待或者阻塞,直到執行緒B丟擲異常或者正常執行完畢釋放這個鎖;如果B執行緒不釋放這個鎖,那麼A執行緒將永遠等待下去。

 

物件鎖和類鎖

物件鎖和類鎖在鎖的概念上基本上和內建鎖是一致的,但是,兩個鎖實際是有很大的區別的。

  • 物件鎖是用於物件例項方法;

  • 類鎖是用於類的靜態方法或者一個類的class物件上的

一個物件無論有多少個同步方法區,它們共用一把鎖,某一時刻某個執行緒已經進入到某個synchronzed方法,那麼在該方法沒有執行完畢前,其他執行緒無法訪問該物件的任何synchronzied 方法的,但可以訪問非synchronzied方法。

如果synchronized方法是static的,那麼當執行緒訪問該方法時,它鎖的並不是synchronized方法所在的物件,而是synchronized方法所在物件的對應的Class物件,

因為java中無論一個類有多少個物件,這些物件會對應唯一一個Class物件,因此當執行緒分別訪問同一個類的兩個物件的static,synchronized方法時,他們的執行也是按順序來的,也就是說一個執行緒先執行,一個執行緒後執行。

 synchronized的用法:修飾方法和修飾程式碼塊,下面分別分析這兩種用法在物件鎖和類鎖上的效果。

 

物件鎖的synchronized修飾方法和程式碼塊

public class TestSynchronized {
    public void test1() {
        synchronized (this) {
            int i = 5;
            while (i-- > 0) {
                System.out.println(Thread.currentThread().getName() + " : " + i);
                try {
                    Thread.sleep(500);
                } catch (InterruptedException ie) {
                }
            }
        }
    }

    public synchronized void test2() {
        int i = 5;
        while (i-- > 0) {
            System.out.println(Thread.currentThread().getName() + " : " + i);
            try {
                Thread.sleep(500);
            } catch (InterruptedException ie) {
            }
        }
    }

    public static void main(String[] args) {
        final TestSynchronized myt2 = new TestSynchronized();
        Thread test1 = new Thread(new Runnable() {
            public void run() {
                myt2.test1();
            }
        }, "test1");
        Thread test2 = new Thread(new Runnable() {
            public void run() {
                myt2.test2();
            }
        }, "test2");
        test1.start();
        test2.start();
    }
}

列印結果如下:

test2 : 4
test2 : 3
test2 : 2
test2 : 1
test2 : 0
test1 : 4
test1 : 3
test1 : 2
test1 : 1
test1 : 0

上述的程式碼,第一個方法用了同步程式碼塊的方式進行同步,傳入的物件例項是this,表明是當前物件;第二個方法是修飾方法的方式進行同步。因為第一個同步程式碼塊傳入的this,所以兩個同步程式碼所需要獲得的物件鎖都是同一個物件鎖,下面main方法時分別開啟兩個執行緒,分別呼叫test1和test2方法,那麼兩個執行緒都需要獲得該物件鎖,另一個執行緒必須等待。上面也給出了執行的結果可以看到:直到test2執行緒執行完畢,釋放掉鎖,test1執行緒才開始執行。這裡test2方法先搶到CPU資源,故它先執行,它獲得了鎖,它執行完畢後,test1才開始執行。

如果我們把test2方法的synchronized關鍵字去掉,執行結果會如何呢? 

test1 : 4
test2 : 4
test2 : 3
test2 : 2
test2 : 1
test2 : 0
test1 : 3
test1 : 2
test1 : 1
test1 : 0

我們可以看到,結果輸出是交替著進行輸出的,這是因為,某個執行緒得到了物件鎖,但是另一個執行緒還是可以訪問沒有進行同步的方法或者程式碼。進行了同步的方法(加鎖方法)和沒有進行同步的方法(普通方法)是互不影響的,一個執行緒進入了同步方法,得到了物件鎖,其他執行緒還是可以訪問那些沒有同步的方法(普通方法)。

 

類鎖的修飾(靜態)方法和程式碼塊  

public class TestSynchronized {
    public void test1() {
        synchronized (TestSynchronized.class) {
            int i = 5;
            while (i-- > 0) {
                System.out.println(Thread.currentThread().getName() + " : " + i);
                try {
                    Thread.sleep(500);
                } catch (InterruptedException ie) {
                }
            }
        }
    }

    public static synchronized void test2() {
        int i = 5;
        while (i-- > 0) {
            System.out.println(Thread.currentThread().getName() + " : " + i);
            try {
                Thread.sleep(500);
            } catch (InterruptedException ie) {
            }
        }
    }

    public static void main(String[] args) {
        final TestSynchronized myt2 = new TestSynchronized();
        Thread test1 = new Thread(new Runnable() {
            public void run() {
                myt2.test1();
            }
        }, "test1");
        Thread test2 = new Thread(new Runnable() {
            public void run() {
                TestSynchronized.test2();
            }
        }, "test2");
        test1.start();
        test2.start();
    }
}

輸出結果如下:

test1 : 4
test1 : 3
test1 : 2
test1 : 1
test1 : 0
test2 : 4
test2 : 3
test2 : 2
test2 : 1
test2 : 0

類鎖修飾方法和程式碼塊的效果和物件鎖是一樣的,因為類鎖只是一個抽象出來的概念,只是為了區別靜態方法的特點,因為靜態方法是所有物件例項共用的,所以對應著synchronized修飾的靜態方法的鎖也是唯一的,所以抽象出來個類鎖。其實這裡的重點在下面這塊程式碼,synchronized同時修飾靜態和非靜態方法

public class TestSynchronized {
    public synchronized void test1() {
        int i = 5;
        while (i-- > 0) {
            System.out.println(Thread.currentThread().getName() + " : " + i);
            try {
                Thread.sleep(500);
            } catch (InterruptedException ie) {
            }
        }
    }

    public static synchronized void test2() {
        int i = 5;
        while (i-- > 0) {
            System.out.println(Thread.currentThread().getName() + " : " + i);
            try {
                Thread.sleep(500);
            } catch (InterruptedException ie) {
            }
        }
    }

    public static void main(String[] args) {
        final TestSynchronized myt2 = new TestSynchronized();
        Thread test1 = new Thread(new Runnable() {
            public void run() {
                myt2.test1();
            }
        }, "test1");
        Thread test2 = new Thread(new Runnable() {
            public void run() {
                TestSynchronized.test2();
            }
        }, "test2");
        test1.start();
        test2.start();
    }
}

輸出結果如下:

test1 : 4
test2 : 4
test1 : 3
test2 : 3
test2 : 2
test1 : 2
test2 : 1
test1 : 1
test1 : 0
test2 : 0

上面程式碼synchronized同時修飾靜態方法和例項方法,但是執行結果是交替進行的,這證明了類鎖和物件鎖是兩個不一樣的鎖,控制著不同的區域,它們是互不干擾的。同樣,執行緒獲得物件鎖的同時,也可以獲得該類鎖,即同時獲得兩個鎖,這是允許的。

 

synchronized是如何保證執行緒安全的  

如果有多個執行緒在同時執行,而這些執行緒可能會同時執行這段程式碼。程式每次執行結果和單執行緒執行的結果是一樣的,而且其他的變數的值也和預期的是一樣的,就是執行緒安全的。

我們通過一個案例,演示執行緒的安全問題:

我們來模擬一下火車站賣票過程,總共有100張票,總共有三個視窗賣票。

public class SellTicket {
    public static void main(String[] args) {
        // 建立票物件
        Ticket ticket = new Ticket();
        // 建立3個視窗
        Thread t1 = new Thread(ticket, "視窗1");
        Thread t2 = new Thread(ticket, "視窗2");
        Thread t3 = new Thread(ticket, "視窗3");
        t1.start();
        t2.start();
        t3.start();
    }
}

// 模擬票
class Ticket implements Runnable {
    // 共100票
    int ticket = 100;

    @Override
    public void run() {
        // 模擬賣票
        while (true) {
            if (ticket > 0) {
                // 模擬選坐的操作
                try {
                    Thread.sleep(1);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                System.out.println(Thread.currentThread().getName() + "正在賣票:"
                        + ticket--);
            }
        }
    }
}

執行結果發現:上面程式出現了問題

  • 票出現了重複的票

  • 錯誤的票 0、-1

其實,執行緒安全問題都是由全域性變數及靜態變數引起的。若每個執行緒中對全域性變數、靜態變數只有讀操作,而無寫操作,這個全域性變數是執行緒安全的;若有多個執行緒同時執行寫操作,一般都需要考慮執行緒同步,否則的話就可能影響執行緒安全。

 

那麼出現了上述問題,我們應該如何解決呢?

執行緒同步(執行緒安全處理Synchronized)

java中提供了執行緒同步機制,它能夠解決上述的執行緒安全問題。

執行緒同步的方式有兩種:

  • 方式1:同步程式碼塊

  • 方式2:同步方法

同步程式碼塊

同步程式碼塊: 在程式碼塊宣告上 加上synchronized

synchronized (鎖物件) {
    可能會產生執行緒安全問題的程式碼
}

同步程式碼塊中的鎖物件可以是任意的物件;但多個執行緒時,要使用同一個鎖物件才能夠保證執行緒安全。

使用同步程式碼塊,對火車站賣票案例中Ticket類進行如下程式碼修改:

public class SellTicket {
    public static void main(String[] args) {
        // 建立票物件
        Ticket ticket = new Ticket();
        // 建立3個視窗
        Thread t1 = new Thread(ticket, "視窗1");
        Thread t2 = new Thread(ticket, "視窗2");
        Thread t3 = new Thread(ticket, "視窗3");
        t1.start();
        t2.start();
        t3.start();
    }
}

// 模擬票
class Ticket implements Runnable {
    // 共100票
    int ticket = 100;

    Object lock = new Object();

    @Override
    public void run() {
        // 模擬賣票
        while (true) {
            // 同步程式碼塊
            synchronized (lock) {
                if (ticket > 0) {
                    // 模擬選坐的操作
                    try {
                        Thread.sleep(1);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    System.out.println(Thread.currentThread().getName()
                            + "正在賣票:" + ticket--);
                }
            }
        }
    }
}

當使用了同步程式碼塊後,上述的執行緒的安全問題,解決了。

 

同步方法

同步方法:在方法宣告上加上synchronized

public synchronized void method(){
       可能會產生執行緒安全問題的程式碼
}

同步方法中的鎖物件是 this

使用同步方法,對火車站賣票案例中Ticket類進行如下程式碼修改:

public class SellTicket {
    public static void main(String[] args) {
        // 建立票物件
        Ticket ticket = new Ticket();
        // 建立3個視窗
        Thread t1 = new Thread(ticket, "視窗1");
        Thread t2 = new Thread(ticket, "視窗2");
        Thread t3 = new Thread(ticket, "視窗3");
        t1.start();
        t2.start();
        t3.start();
    }
}

// 模擬票
class Ticket implements Runnable {
    // 共100票
    int ticket = 100;

    Object lock = new Object();

    @Override
    public void run() {
        // 模擬賣票
        while (true) {
            // 同步方法
            method();
        }
    }

    // 同步方法,鎖物件this
    public synchronized void method() {
        if (ticket > 0) {
            // 模擬選坐的操作
            try {
                Thread.sleep(10);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println(Thread.currentThread().getName() + "正在賣票:"
                    + ticket--);
        }
    }
}

  

synchronized支援鎖的重入嗎?  

我們先來看下面一段程式碼:

public class ReentrantLockDemo {
    public synchronized void a() {
        System.out.println("a");
        b();
    }

    private synchronized void b() {
        System.out.println("b");
    }

    public static void main(String[] args) {
        new Thread(new Runnable() {
            @Override
            public void run() {
                ReentrantLockDemo d = new ReentrantLockDemo();
                d.a();
            }
        }).start();
    }
}

上述的程式碼,我們分析一下,兩個方法,方法a和方法b都被synchronized關鍵字修飾,鎖物件是當前物件例項,按照上文我們對synchronized的瞭解,如果呼叫方法a,在方法a還沒有執行完之前,我們是不能執行方法b的,方法a必須先釋放鎖,方法b才能執行,方法b處於等待狀態,那樣不就形成死鎖了嗎?那麼事實真的如分析一致嗎?

執行結果發現:

a
b

程式碼很快就執行完了,實驗結果與分析不一致,這就引入了另外一個概念:重入鎖。在 java 內部,同一執行緒在呼叫自己類中其他 synchronized 方法/塊或呼叫父類的 synchronized 方法/塊都不會阻礙該執行緒的執行。就是說同一執行緒對同一個物件鎖是可重入的,而且同一個執行緒可以獲取同一把鎖多次,也就是可以多次重入。在JDK1.5後對synchronized關鍵字做了相關優化。

 

synchronized死鎖問題

同步鎖使用的弊端:當執行緒任務中出現了多個同步(多個鎖)時,如果同步中巢狀了其他的同步。這時容易引發一種現象:程式出現無限等待,這種現象我們稱為死鎖。這種情況能避免就避免掉。

synchronzied(A鎖){
    synchronized(B鎖){
    }
}

我們進行下死鎖情況的程式碼演示:

public class DeadLock {
    Object obj1 = new Object();
    Object obj2 = new Object();

    public void a() {
        synchronized (obj1) {
            synchronized (obj2) {
                System.out.println("a");
            }
        }
    }

    public void b() {
        synchronized (obj2) {
            synchronized (obj1) {
                System.out.println("b");
            }
        }
    }

    public static void main(String[] args) {
        DeadLock d = new DeadLock();
        new Thread(new Runnable() {
            @Override
            public void run() {
                d.a();
            }
        }).start();

        new Thread(new Runnable() {
            @Override
            public void run() {
                d.b();
            }
        }).start();
    }
}

上述的程式碼,我們分析一下,兩個方法,我們假設兩個執行緒T1,T2,T1執行到方法a了,拿到了obj1這把鎖,此時T2執行到方法b了,拿到了obj2這把鎖,T1要往下執行,就必須等待T2釋放了obj2這把鎖,執行緒T2要往下面執行,就必須等待T1釋放了持有的obj1這把鎖,他們兩個互相等待,就形成了死鎖。

為了演示的更明白,需要讓兩個方法執行過程中睡眠10ms,要不然很難看到現象,因為計算機執行速度賊快

public class DeadLock {
    Object obj1 = new Object();
    Object obj2 = new Object();

    public void a() {
        synchronized (obj1) {
            try {
                Thread.sleep(10);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            synchronized (obj2) {
                System.out.println("a");
            }
        }
    }

    public void b() {
        synchronized (obj2) {
            try {
                Thread.sleep(10);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            synchronized (obj1) {
                System.out.println("b");
            }
        }
    }

    public static void main(String[] args) {
        DeadLock d = new DeadLock();
        new Thread(new Runnable() {
            @Override
            public void run() {
                d.a();
            }
        }).start();

        new Thread(new Runnable() {
            @Override
            public void run() {
                d.b();
            }
        }).start();
    }

}

感興趣的童鞋,下去可以試一下,程式執行不完,永遠處於等待狀態。

 

總結

  • sychronized是隱式鎖,是JVM底層支援的關鍵字,由JVM來維護;

  • 單體應用下,多執行緒併發操作時,使用sychronized關鍵字可以保證執行緒安全;

  • sychronized可以用來修飾方法和程式碼塊,此時鎖是當前物件例項,修飾靜態方法時,鎖是物件的class位元組碼檔案;

  • 一個執行緒進入了sychronized修飾的同步方法,得到了物件鎖,其他執行緒還是可以訪問那些沒有同步的方法(普通方法);

  • sychronized支援鎖的重入;

  

作者:Java螞蟻

出處:https://www.cnblogs.com/marsitman/p/11235552.html

版權:轉載請在文章明顯位置註明作者及出處。

相關文章