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前綴指令會引發兩件事情:
- 將當前處理器緩存行的數據協會到系統內存。
- 這個協會內存的操作會使在其他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類。(代碼略)