1.思考題:同步方法和非同步方法是否可以同時調用?
答案:可以
public class T {
public synchronized void m1() {
System.out.println(Thread.currentThread().getName() + " m1 start...");
try {
Thread.sleep(10000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + " m1 end");
}
public void m2() {
try {
Thread.sleep(5000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + " m2 ");
}
public static void main(String[] args) {
T t = new T();
/*new Thread(()->t.m1(), "t1").start();
new Thread(()->t.m2(), "t2").start();*/
new Thread(t::m1, "t1").start();
new Thread(t::m2, "t2").start();
/*
//1.8之前的寫法
new Thread(new Runnable() {
@Override
public void run() {
t.m1();
}
});
*/
}
}
執行結果:
思考:如果兩個方法同時加了synchronized鎖,當m1()睡眠的時候,m2()可以同步調用嗎?
答案:不可以
運行結果:
同步方法和非同步方法可以同時調用的原因是因爲syc對於m1()方法加了鎖,但是m2()是一個無鎖的方法,所以訪問是不需要拿到對象所持有的鎖的,所以可以同時訪問。但是當兩個都是同步方法的時候,對象只有一個,多個線程訪問時只有順序執行。
2.模擬銀行賬戶:對業務寫方法加鎖,對業務讀方法不加鎖這樣可以不?
答案:容易產生髒讀問題(dirtyRead)
public class Account {
String name;
double balance;
public synchronized void set(String name, double balance) {
this.name = name;
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
this.balance = balance;
}
public /*synchronized*/ double getBalance(String name) {
return this.balance;
}
public static void main(String[] args) {
Account a = new Account();
new Thread(()->a.set("zhangsan", 100.0)).start();
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(a.getBalance("zhangsan"));
try {
TimeUnit.SECONDS.sleep(2);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(a.getBalance("zhangsan"));
}
}
3.synchronized爲什麼可重入?
什麼是可重入鎖?
若一個程序或子程序可以“在任意時刻被中斷然後操作系統調度執行另外一段代碼,這段代碼又調用了該子程序不會出錯”,則稱其爲可重入(reentrant或re-entrant)的。即當該子程序正在運行時,執行線程可以再次進入並執行它,仍然獲得符合設計時預期的結果。與多線程併發執行的線程安全不同,可重入強調對單個線程執行時重新進入同一個子程序仍然是安全的。
通俗來說:當線程A請求一個由線程B持有的對象鎖時,該線程會阻塞,而當線程B請求由自己持有的對象鎖時,如果該鎖是重入鎖,請求就會成功,否則阻塞。
我先給大家一個結論:synchronized 是可重入鎖!
假設我們現在不知道它是不是一個可重入鎖,那我們就應該想方設法來驗證它是不是可重入鎖?怎麼驗證呢?看下面的代碼!
public class Xttblog extends SuperXttblog {
public static void main(String[] args) {
Xttblog child = new Xttblog();
child.doSomething();
}
public synchronized void doSomething() {
System.out.println("child.doSomething()" + Thread.currentThread().getName());
doAnotherThing(); // 調用自己類中其他的synchronized方法
}
private synchronized void doAnotherThing() {
super.doSomething(); // 調用父類的synchronized方法
System.out.println("child.doAnotherThing()" + Thread.currentThread().getName());
}
}
class SuperXttblog {
public synchronized void doSomething() {
System.out.println("father.doSomething()" + Thread.currentThread().getName());
}
}
上面的代碼也不是隨便寫的,我是根據維基百科的定義寫出這段代碼來驗證它。現在運行一下上面的代碼,我們看一下結果:
child.doSomething()Thread-5492
father.doSomething()Thread-5492
child.doAnotherThing()Thread-5492
現在可以驗證出 synchronized 是可重入鎖了吧!因爲這些方法輸出了相同的線程名稱,表明即使遞歸使用synchronized也沒有發生死鎖,證明其是可重入的。
還看不懂?那我就再解釋下!
這裏的對象鎖只有一個,就是 child 對象的鎖,當執行 child.doSomething 時,該線程獲得 child 對象的鎖,在 doSomething 方法內執行 doAnotherThing 時再次請求child對象的鎖,因爲synchronized 是重入鎖,所以可以得到該鎖,繼續在 doAnotherThing 裏執行父類的 doSomething 方法時第三次請求 child 對象的鎖,同樣可得到。如果不是重入鎖的話,那這後面這兩次請求鎖將會被一直阻塞,從而導致死鎖。
所以在 java 內部,同一線程在調用自己類中其他 synchronized 方法/塊或調用父類的 synchronized 方法/塊都不會阻礙該線程的執行。就是說同一線程對同一個對象鎖是可重入的,而且同一個線程可以獲取同一把鎖多次,也就是可以多次重入。因爲java線程是基於“每線程(per-thread)”,而不是基於“每調用(per-invocation)”的(java中線程獲得對象鎖的操作是以線程爲粒度的,per-invocation 互斥體獲得對象鎖的操作是以每調用作爲粒度的)。
可重入鎖的實現原理?
看到這裏,你終於明白了 synchronized 是一個可重入鎖。但是面試官要再問你,可重入鎖的原理是什麼?
對不起,你又卡殼了。
那麼我現在先給你說一下,可重入鎖的原理。具體我們後面再寫 ReentrantLock 的時候來驗證或看它源碼。
重入鎖實現可重入性原理或機制是:每一個鎖關聯一個線程持有者和計數器,當計數器爲 0 時表示該鎖沒有被任何線程持有,那麼任何線程都可能獲得該鎖而調用相應的方法;當某一線程請求成功後,JVM會記下鎖的持有線程,並且將計數器置爲 1;此時其它線程請求該鎖,則必須等待;而該持有鎖的線程如果再次請求這個鎖,就可以再次拿到這個鎖,同時計數器會遞增;當線程退出同步代碼塊時,計數器會遞減,如果計數器爲 0,則釋放該鎖。