java中類的加載和線程使用所導致的變量值異常情況

首先我們來看一段代碼:

//這個抽象類從它的構造方法中分別先後調用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的運行原理是基本功呀

 

 

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