深入理解static、volatile關鍵字


static

意思是靜態的,全局的。被修飾的東西在一定范圍內是共享的,被類的所有實例共享,這時候需要注意並發讀寫的問題。

只要這個類被加載,Java虛擬機就能根據類名在運行時數據區的方法區內找到他們。所以,static對象可以在他的任何對象創建之前訪問,無需引用任何對象。

static可以修飾變量、方法和代碼塊。

static修飾類變量的時候,被修飾的變量叫做靜態變量或者類變量;如果該變量的訪問權限是public的話,表示該變量,可以被任何類調用,無需初始化。直接使用 類名.static變量名這種形式訪問即可。

static和final一起修飾的變量可以理解為 “全局常量”,一旦賦值就不可更改,並且可以通過類名訪問。對於static final修飾的方法不可以覆蓋,並且可以通過類名直接訪問。

這時候我們非常需要注意的就是線程安全問題了,當多個線程同時對共享變量進行讀寫時,很可能會出現並發問題

一般有兩種解決方法:

  1. 把線程不安全的變量,例如修飾的是ArrayList替換成線程安全的CopyOnWriteArrayList;
  2. 每次訪問這個變量,手動加鎖。

靜態變量和實例變量的區別
對於靜態變量,在內存中只有一個拷貝,JVM只為靜態分配一次內存,在加載類的過程中完成靜態變量的內存分配,可以用類名直接訪問,可以用對象來訪問(不推薦)。

對於實例變量,每次創建一個實例,JVM就會為其分配一次內存,它在內存中有多個拷貝,互不影響。

static修飾方法時,該方法內部只能調用static方法,不能調用普通方法,但是普通方法可以調用static方法,也可以在普通方法中訪問靜態變量。我們常用的util類里面的方法,大多數是被static修飾的方法,在調用的時候很方便。
因為任何實例對象都可以調用靜態方法,所以靜態方法中不能出現this或者super關鍵字。

static修飾代碼塊時,我們叫做靜態代碼塊或者靜態方法塊,一個類中可以有多個靜態代碼塊,他不在任何方法的內部,JVM加載類的時候會執行這些靜態代碼塊,如果靜態代碼塊有多個,JVM將按照他們在類中的順序依次執行他們,每個代碼塊只會執行一次。他常常在類啟動之前初始化一些值。

private static Integer age;

static{
    age = 18;
}

初始化時機

對於被static修飾的變量、方法塊和方法的時機。


/*子類*/
@Slf4j
public class ChildStaticClass  extends ParentStaticClass{
    public ChildStaticClass() {
      log.info("子類構造方法初始化...");
    }

    public static List<String> list = new ArrayList(){{log.info("子類靜態變量初始化...");}};

    static{
        log.info("子類靜態代碼塊初始化。。。");
    }

    public static void testStatic(){
        log.info("子類靜態方法執行。。。");
    }

    public static void main(String[] args) {
        log.info("main 方法執行");
        new ChildStaticClass();
    }
}
/*父類代碼*/
@Slf4j
public class ParentStaticClass {

    public static List<String> list = new ArrayList() {
        {
            log.info("父類靜態變量初始化...");
        }
    };

    static {
        log.info("父類靜態代碼塊初始化...");
    }

    public ParrentStaticClass() {
        log.info("父類構造方法初始化...");
    }

    public static void testStatic(){
        log.info("父類靜態方法執行...");
    }

}

執行子類的main方法,打印結果是:

在這里插入圖片描述
這里需要注意的是,如果將靜態代碼塊放在靜態變量前面,那么先加載靜態代碼塊。這與靜態順序有關。

在這里插入圖片描述
從結果上看,父類的靜態代碼塊和靜態變量比子類優先初始化;
靜態變量和靜態代碼塊比類構造器先初始化。

如果父類和子類都還有非靜態代碼塊的話,執行順序是:

父類靜態代碼塊->子類靜態代碼塊->父類非靜態代碼塊->父類構造方法->子類非靜態代碼塊->子類構造方法

static 總結

靜態代碼塊內容先執行,接着執行父類非靜態代碼塊和構造方法,然后在執行子類非靜態代碼塊和構造方法。

注意

如果父類沒有不帶參數的構造方法,那么子類必須用super關鍵字調用父類帶參數的構造方法,否則編譯不通過。

final

final一般用於以下三種場景

  1. 被final修飾的類,表名該類是無法繼承的。
  2. 被final修飾的方法,表名該方法是無法復寫的;
  3. 被final修飾的變量是,說明該變量在聲明的時候就必須初始化,而且以后不能修改其內存地址

需要注意的是,無法更改的是內存地址,被final修飾的集合,例如Map,List等,可以修改里面元素的值,但是無法修改初始化時的內存地址。

volatile

Java中的volatile用於將變量標記為“存儲在主內存中”,對於volatile變量的每次讀操作都會直接從計算機的主內存中讀取,同樣對於volatile變量的寫操作只寫入主存,而不是僅僅寫入CPU緩存。

可見性的保證

volatile是輕量級的synchronized,他在多處理器中保證共享變量的可見性。可見性的意思是:當一個線程修改一個共享變量時,另一個線程能讀到這個修改的值。volatile之所以比synchronized執行成本更低是因為他不需要切換上下文和調度。
當寫一個volatile變量的時候,Java內存模型(JMM)會把線程對應的本地內存中的共享變量刷新到主內存中;
當讀一個volatile變量時,JMM會把線程對應的本地內存無效化,接下來線程會從主存中讀取這個volatile變量。

實現原理

Java代碼如下

private volatile Object instance = new Singleton();

通過工具轉變成匯編代碼(window下下載這個壓縮包,解壓到你jdk/jre/bin/server下)
https://sourceforge.net/projects/fcml/files/fcml-1.1.1/hsdis-1.1.1-win32-amd64.zip/download
Linux下https://sourceforge.net/projects/fcml/files/fcml-1.1.3/fcml-1.1.3.tar.gz/download
解壓,切換目錄,

  • ./configure && make && sudo make install
  • cd example/hsdis && make && sudo make install
  • sudo ln -s /usr/local/lib/libhsdis.so /lib/amd64/hsdis-amd64.so
  • sudo ln -s /usr/local/lib/libhsdis.so /jre/lib/amd64/hsdis-amd64.so
    接下來便可以使用

執行main函數之前需要加上虛擬機參數
-server -Xcomp -XX:+UnlockDiagnosticVMOptions -XX:+PrintAssembly XX:CompileCommand=compileonly,*類名.方法名
之后有一行指令會有 lock前綴,Lock前綴的指令在多喝處理器下會引發兩件事。

  1. 當前處理器緩存行的數據寫回到系統內存

  2. 寫回內存的操作會使其他CPU里面緩存了該內存地址的變量無效。

  3. Lock前綴指令導致在執行指令期間,聲言處理器的Lock#信號。他在聲言信號期間會獨占共享內存(直接鎖總線,導致其他CPU不能訪問總線,不能訪問總線就意味着不能訪問內存)。但是在最新的處理器,一般不鎖總線,鎖總線開銷會很大,直接鎖的是緩存。這個操作被稱為“緩存鎖定”,緩存一致性機制會阻止同時修改由兩個以上的處理器緩存的內存區域數據。

  4. 為什么寫回內存的數據就會使其他CPU里面的相同內存地址數據的緩存失效呢?
    IA-32和Intel64位的處理器會使用MESI(修改,獨占,共享,無效)控制協議去維護內部緩存和其他處理器緩存的一致性。這兩種處理器會嗅探其他處理器訪問系統內存和他們的內部緩存。處理器使用嗅探技術保證內部緩存,其它處理器緩存,系統緩存在總線上是一致的。如果嗅探到一個處理器在寫內存地址,而這個地址當前處於共享狀態(也就是被volatile修飾),那么正在嗅探的處理器將使他自身的緩存失效,在下次訪問相同的內存地址時,強制執行緩存填充。

什么時候使用volatile

如果兩個線程都對共享變量進行讀寫,那么只是用關鍵字volatile就不能滿足要求了。這種情況你需要使用synchronized保證讀寫變量的原子性。對volatile變量的讀寫不會阻塞其他線程,如果需要阻塞可以換成synchronized。

如果只有一個線程讀寫volatile變量,其他線程只讀取,那么只讀線程一定能看到最新寫入到volatile變量的值。

transient

transient關鍵字是我們常用來修飾類變量的,意思是當前變量是無需進行序列化的。在序列化時,就忽略該變量,這些序列化工具在底層對transient進行了支持。

default

以前的接口中是不能有方法實現的,但是從java8引入default開始,這件事就改變了。default一般用於接口里的方法上,意思是對於該接口,子類無需強制實現該方法,因為有默認的實現。SpringBoot2.x中的一些接口采用了這種實現方式。子類無需實現也可正常使用。

default

public interface DefaultDemo {

    default void test(){
        //todo something 
        System.out.println("Hello");
    }
}

public class DefautDemoImpl implements DefaultDemo {


}

參考:
Java並發編程的藝術
強烈推薦:https://www.cnblogs.com/xrq730/p/7048693.html


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM