併發面試題(01) :volatile和synchronized 相關面試題

1、synchronized 鎖的是對象還是代碼塊?

class TestSynchronizedMethod {
    public synchronized void method01() {
        // do sth
    }
    public void method02() {
        synchronized(this) {
        	// do sth
        }
    }
}

解 : synchronized 鎖住的是對象,事實上,鎖存在於每個對象之中(詳見JVM內存),synchronized 獲得到了對象的鎖,其他鎖想要獲取對象的鎖時會失敗,Java通過對象鎖的獲取保證線程安全

解析 : synchronized 用的鎖是存在Java對象頭裏的。

Java 對象頭裏的 Mark Word 裏默認存儲對象的HashCode,分代年齡和鎖標誌位。

鎖標誌位通常是兩字節,數組對象是三字節。

32 位Mark Word的狀態變化

[外鏈圖片轉存失敗,源站可能有防盜鏈機制,建議將圖片保存下來直接上傳(img-G9fnby9P-1572232278637)(C:\Users\Administrator\Pictures\Mark Word的狀態變化.png)]

可以看到:鎖的升級就是對象頭中的Mark Word的鎖標誌位的改變。–《13p》

2、volatile和synchronized的區別.

解 :

特性 volatile synchronized
可見性
原子性 ×
修飾字段
修飾方法 ×
執行成本 極小 較大(1.6之後,鎖逐步升級)
很大(1.6之前)

解析 : volatile和synchronized在多線程併發編程中都扮演着重要的角色,volatile 是輕量級的synchronized,它在多處理器開發中保證了共享變量的"可見性",它比synchronized 的使用和執行成本更低,因爲他不會因爲線程上下文的切換和調度。

volatile是通過Lock指令,使CPU的緩存數據無效,從而使每個線程獲取到最新值。—《8p》

3、同步方法和非同步方法是否可以同時調用?

可以,線程在執行同步方法前,需要獲取對應的對象鎖,然後才能執行。

非同步方法不需要獲取到對象鎖,所以與同步方法不矛盾。

例:

class Test {
    synchronized void m1 () {
        Test t = new Test();
        // 調用非同步方法
        t.m2();
    }
    void m2() {
        System.out.println("m2");
    }
}

4、什麼是髒讀,髒寫?

5、一個同步方法是否可以調用另外一個同步方法?

解: 可以。因爲當線程去執行另一個同步方法時仍需要獲取鎖,然後獲取時發現:自己已經獲得了對象鎖的所有權,然後執行第二個同步方法。

例:

class T {
    // 鎖的深度++
    synchronized void m1 () {
        Test t = new Test();
        // 調用同步方法
        t.m2();
    }// 深度--
    
    // 調用時會發現,自己已經獲得了當前 T 的實例鎖,所以直接執行,這就是可重入鎖(鎖的深度++)。
    synchronized void m2() {
        System.out.println("m2");
    }// 執行完畢後,鎖的深度--
}// if (鎖的深度==0) 釋放鎖;

同步方法可以調用另一個同步方法時,說明鎖是可重入的。

可重入鎖往往是通過一個深度計數器,來實現的。

6、程序執行中出現異常,鎖會釋放嗎?這會造成什麼影響?怎樣去避免?

當程序執行中出現異常,鎖會釋放,這可能造成代碼執行到一半。

例:

class T {
    int a = 0;
  	int aCopy = 0;
    synchronized void m() {
        a++;
        // do sth
        aCopy++;
    } 
    public static void main(String[] args) {
        T t = new T();
        // 這個線程執行後拋出異常。
        new Thread(t::m, "thread-1").start();
        // 這個線程正常執行。
        new Thread(t::m, "thread-2").start();
    }
}// a = 2; aCopy = 1;

7、以下代碼會出現問題嗎?

public class T {
    static boolean flag = true;
    void m() {
        System.out.println("m start");
        while (flag) {
            
        }
        System.out.println("m end");
    }
    public static void main (String[] args) {
        T t = new T();
        new Thread(t::m, "t1").start();
        try {
            Thread.sleep(1000);
        }catch (Exception e) {
            e.printStackTrace();
        }
        flag = false;
    }
}// 結果: m start

雖然看似一切正常,但是控制檯不會打印 m end。

因爲t1中的flag是CPU的緩存,只有等到緩存過期才能拿到false。

修改方案 :

volatile boolean flag = true;

也可以通過在死循環中做一些費時操作:如print(),sleep()等,現代JVM會自動的刷新最新值到CPU緩存中,但是不建議這樣做,因爲不確定性較大。

8、volatile是如何實現可見性的?

volatile在編譯後變爲Lock前綴指令,Lock前綴指令會引發兩件事情:

  1. 將當前處理器緩存行的數據協會到系統內存。
  2. 這個協會內存的操作會使在其他CPU裏緩存了該內存地址的數據無效。

也就是緩存一致性協議

緩存一致性協議: 每個CPU通過嗅探在總線上傳播的數據來檢查自己緩存的值是不是過期了,當CPU發現自己緩存行對應的內存地址被修改,就會將當前CPU的緩存行設置爲無效狀態,當CPU對這個數據進行修改操作的時候,會重新從系統內存中把數據讀到CPU緩存中

9、以下代碼的輸出是什麼?

public class T {
    volatile int count = 0;
    void m() {
        for(int i = 0; i < 10000; i++) {
            count++;
        }
    }
    public static void main(String[] args) {
        T t = new T();
        List<Thread> threads = new ArrayList<>();
        for(int i = 0; i< 10; i++) {
            // t::m 的意思是 t 的 m 將被作爲 thread.run();
            threads.add(new Thread(t::m, "thread-" + i));
        }
        thread.forEach((o)->o.start());
        
        threads.forEach((o)->{
            try {
                o.join();
            }catch(Exception e) {
                e.printStackTrace();
            }
        });
        System.out.println(t.count);
    }
}// 三次運行結果:99028,82472,72664

小於100000的原因:volatile只保證了count 的可見性,但是不保證原子性,cnt++是兩步操作,出現了髒寫。

volatile只避免了髒讀。

修改:

常規答案:count++代碼塊加鎖。太沉重了。

優秀答案:將count值變爲AtomicInteger,“++”換爲count.incrementAndGet();

10、 以下代碼輸出的結果是什麼?

public class T {
	Object o = new Object();
    void m(){
        while (true) {
            synchronized (o) {
                Thread.yield();
                System.out.println(Thread.currentThread.getName());
            }
        }
    }
    public static void main(String[] args) {
    	T t = new T();
        new Thread(t::m, "t1").start();
        try {
            	Thread.sleep(1000);
        }catch (Exception e) {
            	e.printStackTrace();
        }
        Thread t2 = new Thread(t::m, "t2");
        t.o = new Object();
        t2.start();
    }
}/**
t1
t2
t1
t2
t1
t1
*/

// 在於o 的指向內存改變了,所以t1和t2獲取的鎖不矛盾了,兩者交互yield(),所以是兩者不嚴格的交互執行。

11、觀察代碼,回答問題

class T {
    String s1 = "str";
    String s2 = "str";
    
    void m1() {
        synchronized(s1) {
            while (true) {
                // sleep一秒
                System.out.println("m1");
            }
        }
    }
    void m2() {
        synchronized(s2) {  
            while (true) {
                // sleep一秒
                System.out.println("m2");
            }
        }
    }
    public static void main(String[] args) {
        T t = new T();
        new Thread(t::m1, "t1").start();
        // sleep一秒.
        new Thread(t::m2, "t2").start();
    }
}
// 會打印出m2嗎?爲什麼?

不會,因爲str1 == str2,指向的是同一塊內存區域,其對象頭中的鎖相同,所以t1和t2處於競爭關係。

12、寫兩個線程,線程1添加10個元素到容器中,線程2實現監控元素的個數,當個數達到五個時,線程2給出提示並結束。(淘寶面試題)

// 可以通過wait-notify模仿生產者–消費者模式。(較複雜,面試原題就是填空wait—notify)

import java.util.ArrayList;
import java.util.Collection;
import java.util.concurrent.locks.Lock;

public class MyCollection {
    private Collection<Integer> c = new ArrayList<>();

    void add(int v) {
        c.add(v);
    }

    int size() {
        return c.size();
    }

    public static void main(String[] args) {
        MyCollection myCollection = new MyCollection();
        Object lock = new Object();
        Thread t2 = new Thread(() -> {
            if (myCollection.size() != 5) {
                synchronized (lock) {
                    try {
                        lock.wait();
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
            }
            System.out.println("t2 end");
        });
        t2.start();
        
        Thread.yield();
        Thread t1 = new Thread(() -> {
            for (int i = 1; i <= 10; i++) {
                myCollection.add(i);
                System.out.println(i);
                if (i == 5) {
                    synchronized (lock) {
                        lock.notify();
                    }
                    Thread.yield();
                }
            }
            System.out.println("t1 end");
        });
        t1.start();
    }
}

// 也可以用CountDownLatch類。(代碼略)

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章