首先我們來看一段代碼:
//這個抽象類從它的構造方法中分別先後調用first和second方法
public abstract class TestParent{
public TestParent(){
first();
second();
}
abstract void first();//首先調用
abstract void second();//其次調用
}
//TestThread繼承上面的抽象類,並聲明瞭一個boolean類型常量,初始化賦值爲false
public class TestThread extends TestParent{
private boolean b = false;//關鍵量
TestThread(){
super();//這裏調用父類的構造方法,而後父類的構造方法先後調用first、second
}
@Override
public void first(){
b = true;//在first方法中將b的值修改爲true
System.out.println("first方法中:此時b的值爲->"+b);
}
@Override
public void second(){
new Thread(new Runnable(){
@Override
public void run() {
System.out.println("子線程中:此時b的值爲->"+b);
}
}).start();
}
}
現在根據上面的代碼,大家能否推測出如果我new TestThread()會輸出什麼嗎?
//剛開始我認爲這段代碼會輸出:
first方法中:此時b的值爲->true
子線程中:此時b的值爲->true
//而實際結果是:
first方法中:此時b的值爲->true
子線程中:此時b的值爲->false
wtf!!?爲什麼會這樣呢?明明在first方法中已經將b的值修改爲true啊!而且根據日誌輸出,肯定是先運行了first方法,然後在運行second方法。爲什麼在second中依然是false呢?
爲了一探究竟,我又在second方法中加了一行輸出:
public void second(){
//在線程啓動前再加一行輸出
System.out.println("second方法中:此時b的值爲->"+b);
new Thread(new Runnable(){
@Override
public void run() {
System.out.println("子線程中:此時b的值爲->"+b);
}
}).start();
}
//然而其輸出結果爲:
first方法中:此時b的值爲->true
second方法中:此時b的值爲->true
子線程中:此時b的值爲->false
從輸出結果來看,問題應該是出在了new Thread()上面了。。。second方法中的b值還爲true,而線程中的b值已經是false了。
而b的賦值只有兩個地方,一個是first中將其賦值爲true,另一個是初始化時的默認值false。顯然在second中的這個Thread拿到的是b的初始化默認值。那麼爲什麼會這樣呢?明明在這個Thread被new出來的時候,b的值已經改變了。Thread是從哪裏拿到的這個值呢?
帶着這個問題,我又修改了代碼,假設我們不通過父類的構造方法來調用first和second這兩個方法,而是直接調用會發生什麼呢?
public class TestThread extends TestParent{
private boolean b = false;
TestThread(){
// super(); 不再調用父類的構造,直接調用first和second
first();
second();
}
@Override
public void first(){
b = true;
System.out.println("first方法中:此時b的值爲->"+b);
}
@Override
public void second(){
System.out.println("second方法中:此時b的值爲->"+b);
new Thread(new Runnable(){
@Override
public void run() {
System.out.println("子線程中:此時b的值爲->"+b);
}
}).start();
}
}
修改之後的運行結果:
first方法中:此時b的值爲->true
second方法中:此時b的值爲->true
first方法中:此時b的值爲->true
second方法中:此時b的值爲->true
子線程中:此時b的值爲->true
子線程中:此時b的值爲->true
哇哦。。。結果刷新了我對java的認識。。。原來即時我們不通過super()關鍵字,父類的構造方法還是會被調用。進而導致了first和second方法都被調用了兩次。
但是這次b的結果是沒有問題的,統一爲true了。。。爲什麼呢?按照我之前的猜測,如果是Thread的問題,那麼這次運行結果應該也是要讓b值爲false呀。。。由此可見,Thread只是暴露了這個問題,但並不是問題的本質。。。
那麼本質是什麼呢。。。爲什麼不通過super方法也會調用父類的構造方法呢?而且不通過super方法之後,Thread中的值也正常了。這是爲什麼呢???
爲了再探究竟。。。我又雙修改了代碼:
public class TestThread extends TestParent{
//。。。省略部分代碼
TestThread(){
//這裏增加輸出
System.out.println("子類的構造方法");
// first();
// second();
}
//。。。省略部分代碼
}
public abstract class TestParent{
public TestParent(){
System.out.println("父類的構造方法");
first();
second();
}
//。。。省略部分代碼
}
修改之後的運行情況:
父類的構造方法
first方法中:此時b的值爲->true
second方法中:此時b的值爲->true
子類的構造方法
子線程中:此時b的值爲->false
根據輸出的日誌。。。我們來進行一下java類生成原理的分(xia)析(cai):
首先必須先生成父類,現有父類纔有子類
隨後會先運行父類構造方法中調用的方法(不管是抽象方法,還是普通方法)
以上都做完之後,子類纔會開始被生成
OK這套理論非常完美(沾沾自喜)。。。但是!!!這仍然不能解釋爲什麼Thread中輸出的b值爲false。
沒錯這確實沒法解釋,因爲這還涉及Thread對象的特殊性。
Thread是java中的線程類,用於異步處理數據。因爲它是異步的,所以它並不能直接在java虛擬機中對非異步變量進行修改。
在java中每一個線程都會被分配一個獨立內存空間,這個空間中的變量值是java虛擬機中其它變量值的副本。當一個線程執行完畢之後,會將自己獨立空間中的副本覆蓋到java虛擬機中對應的變量中去。
這就是java中線程的工作原理,也正是因爲這樣。當多個線程同時對一個值進行操作時,經常會出現線程不安全的情況。其主要原因就是這樣,多個線程對應了多個變量副本,每個線程各自爲政將自己處理好的副本覆蓋到對應的變量中。這樣就引出了volatile關鍵字的作用,下次再開一篇博客來講。。。
回到我們剛剛的代碼,second方法中new 了一個Thread對象,注意此時的Thread已經初始化完成,並且調用了start方法。這意味着此時它已經拿到b值的副本。(但它沒有馬上運行,因爲線程的運行需要由虛擬機調配)
注意哦,代碼運行到這裏TestThread對象(子類)還沒被加載出來呢。。。所以Thread拿到的副本只能是b的默認值false。
當然有小夥伴會問了,那first方法中修改的b值是哪來的呢?子類還沒被加載,爲什麼就可以直接調用子類的常量了?
這是因爲java中類的加載過程:
1、加載class文件
2、堆中開闢空間
3、變量的默認初始化
4、變量的顯示初始化
5、構造代碼塊初始化
6、構造方法初始化
7、如果存在繼承的情況,會先加載父類的數據(包括變量和方法),再加載子類的數據(包括變量和方法)
於是乎。。。由於這其中的種種原因。。。導致了我們看到輸出的值非常懵逼!!!(java的運行原理是基本功呀)