天天看點

幹貨——詳解Java中的關鍵字

在平時編碼中,我們可能隻注意了這些static,final,volatile等關鍵字的使用,忽略了他們的細節,更深層次的意義。

本文總結了Java中所有常見的關鍵字以及一些例子。

static 關鍵字

概述:

當static修飾類的屬性或者方法時,那麼就可以在沒有建立對象的情況下使用該屬性或方法。

靜态塊也是static的一個應用,用于初始化類時的一些操作。

靜态方法和靜态變量

劃重點

被static修飾後的屬性或者方法,使用時不需要new 一個類,用類.屬性名或方法名通路.

比如java.lang.Math就存放了很多靜态資源,可以直接使用Math.random()來擷取随機數.

一些需要注意的地方

非靜态方法是可以通路靜态資源的,

靜态方法是不能引用非靜态資源的。

來看一個代碼執行個體:

1 public class TestStatic {
2 
3     protected int i = 100;
4 
5     public static void main(String args[]){
6         System.out.println(i);
7     }
8 }      

在以上代碼,編譯的時候會出錯,main方法是靜态方法,變量i是非靜态的。

解決辦法是,将變量i加上static修飾。

不經就要提出一個問題,

為什麼非靜态方法可以通路靜态資源,而靜态方法不能通路非靜态資源呢?

 從類加載機制上講,靜态資源是類初始化的時候加載的,然後非靜态資源是new一個該類的對象的時候加載的。

這就帶來一個問題:

加載類時預設先加載靜态資源的,當new一個對象之後,才會加載其他資源,是以在new對象之前,靜态資源是不知道類有哪些非靜态資源的,

但是當對象new出來之後,該類的所有屬性和方法都知道。

還有需要注意的是:

1.靜态屬性和方法可以通過類.屬性名或方法名,而且,該類的對象也是通路靜态屬性和變量的。

2.Java的文法規定,static不能修飾局部變量。沒有為什麼,這就是規定。

靜态塊

靜态塊和靜态變量、靜态方法是沒什麼差別的,也是在類加載的時候執行,而且隻執行一次。

關于靜态塊有兩點需要注意:

1.靜态資源的加載順序嚴格按照靜态資源的定義順序加載的

2.靜态塊,對于定義在它之後的靜态變量,可以指派但不能通路。

static的題目

下面main()方法的輸出結果是什麼:

public class InstanceClass extends ParentClass{

    public static String subStaticField = "子類靜态變量";
    public String subField = "子類非靜态變量";
    public static StaticClass staticClass = new StaticClass("子類");

    static {
        System.out.println("子類 靜态塊初始化");
    }

    {
        System.out.println("子類 [非]靜态塊初始化");
    }

    public InstanceClass(){
        System.out.println("子類構造器初始化");
    }

    public static void main(String args[]) throws InterruptedException {
        new InstanceClass();
    }
}

class ParentClass{
    public static String parentStaticField = "父類靜态變量";
    public String parentField = "父類[非]靜态變量";
    public static StaticClass staticClass = new StaticClass("父類");

    static {
        System.out.println("父類 靜态塊初始化");
    }

    {
        System.out.println("父類 [非]靜态塊初始化");
    }

    public ParentClass(){
        System.out.println("父類  構造器初始化");
    }
}

class StaticClass{
    public StaticClass(String name){
        System.out.println(name+" 靜态變量加載");
    }
}      

輸出結果:

View Code

下面是我總結類加載流程,可以對照着這個流程,可以再重新看一下上面的例子,會有新的了解。

1. 加載父類靜态
    1.1 為靜态屬性配置設定存儲空間并賦初始值
    1.2 執行靜态初始化塊和靜态初始化語句(從上至下)

2. 加載子類靜态
    2.1 為靜态屬性配置設定存儲空間
    2.2 執行靜态初始化塊和靜态初始化語句(從上至下)

3. 加載父類非靜态
    3.1 為非靜态塊配置設定空間  
    3.2 執行非靜态塊

4. 加載子類非靜态
    4.1 為非靜态塊配置設定空間  
    4.2 執行非靜态塊

5. 加載父類構造器
    5.1 為執行個體屬性配置設定存數空間并賦初始值
    5.2 執行執行個體初始化塊和執行個體初始化語句
    5.3 執行構造器内容

6. 加載子類構造器
    6.1 為執行個體屬性配置設定存數空間并賦初始值
    6.2 執行執行個體初始化塊和執行個體初始化語句
    6.3 執行構造器内容      

對照着剛才的規則,再看一下這個例子:

1 public class TestStaticLoad {
 2     Person person = new Person("TestStaticLoad");
 3     static{
 4         System.out.println("TestStaticLoad static");
 5     }
 6 
 7     public TestStaticLoad() {
 8         System.out.println("TestStaticLoad constructor");
 9     }
10 
11     public static void main(String[] args) {
12         new God();
13     }
14 
15 }
16 
17 class Person{
18     static{
19         System.out.println("person static");
20     }
21     public Person(String str) {
22         System.out.println("person "+str);
23     }
24 }
25 
26 
27 class God extends TestStaticLoad {
28     Person person = new Person("God");
29     static{
30         System.out.println("God static");
31     }
32 
33     public God() {
34         System.out.println("God constructor");
35     }
36 }      

一步一步地解析:

  • 在TestStaticLoad 的main方法中,執行了new God(),那就就會去加載God類,在這之前會先加載它的父類:TestStaticLoad
  • 第一步:加載父類靜态,執行System.out.println("TestStaticLoad static");  輸出:TestStaticLoad static,
  • 第二步:加載子類靜态,執行System.out.println("God static");,輸出God static
  • 第三步:加載父類非靜态,Person person = new Person("TestStaticLoad");,這裡執行個體化了Person 對象,那就會去加載Person類。
  • 第四步:加載Person類,首先看有沒有父類,沒有。好,加載靜态塊,執行System.out.println("person static");輸出person static
  • 第五步:Pernson類靜态塊加載完畢,加載構造器,new一個Person對象,輸出person TestStaticLoad。這時TestStaticLoad 類非靜态塊加載完畢
  • 第六步:加載God 父類(TestStaticLoad )構造器,輸出TestStaticLoad constructor
  • 第七步:God父類全部加載完畢,加載God的非靜态塊,Person person = new Person("God");這時又會去加載Person類,需要注意的是,static塊隻加載一次,因為之前在父類已經加載過了,這時隻加載構造器,輸出person God
  • 最後一步:加載本類God 的構造器,輸出God constructor。

 static關鍵字的總結:

  1. static關鍵字 可以再沒有建立對象的時候進行調用類的元素
  2. static 可以修飾類的方法 以及類的變量, 以及靜态代碼塊
  3. 被static修飾的成為靜态方法,靜态方法是沒有this的,靜态方法不能通路同一個類中的非靜态方法和靜态變量,但是非靜态方法 可以可以通路靜态變量
  4. 類的構造器 也是靜态的
  5. 靜态變量被所有的記憶體所有的對象共享,在記憶體中隻有一個副本。非靜态變量是是在建立對象的時候初始化的,存在多個副本,每個副本不受影響。
  6. static 靜态代碼塊,static 代碼塊可以放在類中的任何地方,類加載的時候會按照static代碼塊的順序來加載代碼塊,并且隻會執行一次。
  7. 枚舉類和靜态代碼塊 指派靜态代碼塊的變量
  8. 非靜态方法能夠通過this通路靜态變量
  9. 靜态成員變量雖然獨立于對象,但是不代表不可以通過對象去通路,所有的靜态方法和靜态變量都可以通過對象通路。
  10. static不可以修飾局部變量(java文法規定)

沒想到static能有這麼多需要注意的,可以說Java中的文法還是有很多可以深究的.

final 關鍵字

final關鍵字,在平時的過程中也是很常見的,在這裡進行一下深入的學習,加深對final關鍵字的了解。

 使用注意點:

1.在java中final可以用來修飾類、方法、和變量(包括成員變量和局部變量)

2.final修飾類的時候,這個類将永遠不會被繼承,類中的成員方法也會被隐式的修飾為final(盡量不要用final修飾類)

3.如果不想方法被繼承,可以用final修飾,private也會隐式的将方法指定為final

4.final修飾變量的時候,如果是基本類型的變量,那麼他的值在初始化之後就不能更改

5.final在修飾對象的時候,在其初始化之後就不能指向其他對象

6.被static和final修飾的變量,将會占據一段不能改變的存儲空間,将會被看做編譯期常量

7.不可變的是變量的引用而非引用指向對象的内容。

幾個例子:

1.final變量和普通變量的差別

public class TestFinal {
    public static void main(String args[]){
        String a = "test1";
        final String b = "test";
        String d = "test";
        String c = b + 1; 
        String e = d + 1;
        System.out.println((a == c));
        System.out.println((a.equals(e)));
    }
}      

因為final變量是基本類型以及String時,在編譯期的時候就把它當做常量來使用,不需要在運作時候使用。“==”是對比兩個對象基于記憶體引用,如果兩個對象的引用完全相同,則傳回true,是以這裡b是用通路常量的方式去通路,d是連結的方式,是以a的記憶體引用和c的記憶體引用是相等的,是以結果為true,a和e兩個對象的值是相等的,是以結果為true

2.final在修飾對象的時候

1 public class TestFinal {
2     public static void main(String args[]){
3         final TestFinal obj1 = new TestFinal();
4         final TestFinal obj2 = new TestFinal();
5         
6         obj1 = obj2;
7     }
8 }      

在編譯的時候,或報錯, 不能指向一個final對象。

volatile關鍵字

緩存一緻性:

首先來看看線程的記憶體模型圖:

幹貨——詳解Java中的關鍵字

當執行代碼:

i = i + 1;      
  1. 首先從主存中讀取i的值,
  2. 然後複制I到Cache中,
  3. CPU執行指令對i進行加1
  4. 将加1後的值寫入到Cache中
  5. 最後将Cache中i的值重新整理到主存中

這個在單線程的環境中是沒有問題的,但是運作到多線程中就存在問題了。

問題出在主存中的變量,因為有可能其他線程讀的值,線程的Cache還沒有同步到主存中,每個線程中的Cahe中的值副本不一樣,可能會造成"髒讀"。

緩存一緻性協定解決了這樣的問題,它規定每個線程中的Cache使用的共享變量副本是一樣的。

核心内容是當CPU寫資料時,如果發現操作的變量式共享變量,它将通知其他CPU該變量的緩存行為無效,

是以當其他CPU需要讀取這個變量的時候,發現自己的緩存行為無效,那麼就會從主存中重新擷取。

三個概念

Jvm定義了記憶體規範,試圖做到各個平台對記憶體通路的差異,但是依舊會發生緩存一緻性的問題。

首先了解三個概念,原子性,可見性,有序性。

原子性:指某個操作,一個或者多個,要麼全部執行并且執行的過程中不會被任何因素打斷,要麼都不執行。

在JVM中,隻有簡單的讀取、指派(而且必須是将數字指派給某個變量,變量之間的互相指派不是原子操作)才是原子操作。看一個例子:

x = 70;         //語句1
y = x;         //語句2
y++;           //語句3
y = x + 1;     //語句4      

上面四個語句中,隻有語句1是原子性,其他都不是。

可見性:當多個線程通路一個變量時,一個線程修改了這個變量的值,其他線程能夠看得到。

未加volatile變量修飾的變量,在被修改之後,什麼時候寫入到主存是不确定的,是以其他線程讀取該變量的值可能還是未被修改的值。

如果改變了被volatile關鍵字修飾了,那麼JVM将會标記它為共享變量,共享變量一經修改,就會立即同步到主存中,并且通知其他線程(CPU緩存)中值生效,請去主存中讀取該值。

有序性:程式的執行順序按照代碼的先後順序執行。但是JVM在執行語句的過程會對代碼進行重排序(重排序:CPU為了提高程式運作效率,可能會對輸入代碼進行優化,但是不保證程式的執行先後順序和代碼中的順序一緻,但是會保證程式最終執行結果和代碼順序執行的結果是一緻的)。

在多線程的環境下,原有的順序執行會發生錯誤。

在JVM中保證了一定的有序性,比如被volatile修飾後的變量,那麼該變量的寫操作先行發生于後面對這個變量的讀操作。

是以要想程式在多線程環境下正确運作,必須保證原子性,可見性,有序性。

volatile的作用

當一個變量(類的普通變量,靜态變量)被volatile修飾之後,那麼将具備兩個屬性:

1)保證了不同線程對這個變量進行操作時的可見性,即一個線程修改了某個變量的值,這新值對其他線程來說是立即可見的。

2)禁止進行指令重排序

下面來看看線程池中一些變量的定義:

private volatile ThreadFactory threadFactory;

    private volatile RejectedExecutionHandler handler;

    private volatile long keepAliveTime;

    private volatile boolean allowCoreThreadTimeOut;

    private volatile int corePoolSize;

    private volatile int maximumPoolSize;      

 可以看到線程工廠threadFactory,拒絕政策handler,沒有任務時的活躍時間keepAliveTime,keepAliveTime的開關allowCoreThreadTimeOut,核心池大小corePoolSize,最大線程數maximumPoolSize

都是被volatile修飾中,因為線上程池中有若幹個線程,這些變量必需保持對線程可見性,不然會引起線程池運作不正确。

volatile不能保證原子性

i++;

它是非原子性的,當變量i被volatile修飾時,是否能保證原子性呢?

做個試驗:

public class TestAtomVolatile {
    public volatile int i = 0;

    public void increase() {
        i++;
    }

    public static void main(String[] args) throws InterruptedException {
        final TestAtomVolatile test = new TestAtomVolatile();
        for(int i=0;i<10;i++){
            new Thread(){
                public void run() {
                    for(int j=0;j<1000;j++)
                        test.increase();
                    System.out.println(test.i);
                };
            }.start();
        }

    }
}      

以上代碼就是10個線程,分别對變量i進行自增操作,預期結果應該是10000,但是總會存在着小于10000的情況。輸出結果如下:

幹貨——詳解Java中的關鍵字

 對于這種情況,可以使用鎖,synchronize,Lock,也可以使用原子變量。

原子變量的例子:

 volatile的原理

下面這段話摘自《深入了解Java虛拟機》:

“”觀察加入volatile關鍵字和沒有加入volatile關鍵字時所生成的彙編代碼發現,加入volatile關鍵字時,會多出一個lock字首指令”

lock字首指令實際上相當于一個記憶體屏障(也成記憶體栅欄),記憶體屏障會提供3個功能:

1)它確定指令重排序時不會把其後面的指令排到記憶體屏障之前的位置,也不會把前面的指令排到記憶體屏障的後面;即在執行到記憶體屏障這句指令時,在它前面的操作已經全部完成;

2)它會強制将對緩存的修改操作立即寫入主存;

3)如果是寫操作,它會導緻其他CPU中對應的緩存行無效。      

assert關鍵字

assert斷言

在目前的java編碼中,是不推薦使用的,這裡隻是稍微了解一下:

使用方式:

1、assert <boolean表達式>

如果<boolean表達式>為true,則程式繼續執行。

如果為false,則程式抛出AssertionError,并終止執行。

2、assert <boolean表達式> : <錯誤資訊表達式>

如果為false,則程式抛出java.lang.AssertionError,并輸入<錯誤資訊表達式>。

如果要開啟斷言檢查,則需要用開關-enableassertions或-ea來開啟,java中IDE工具預設支援開啟-ea

下面是一個例子:

public class LearnAssert {
    public static void main(String args[]){
        assert true;
        System.out.println("斷言1成功執行");
        System.out.println("-----------");
        assert false:"error";
        System.out.println("斷言2成功執行");
    }
}      

 assert是為了在調試程式時候使用的,預設不推薦使用,測試程式可以使用junit。

synchronized關鍵字

關于鎖關鍵字,有以下幾個總結:

  • 無論synchronized關鍵字加在方法上還是對象上,如果它作用的對象是非靜态的,則它取得的鎖是對象;如果synchronized作用的對象是一個靜态方法或一個類,則它取得的鎖是對類,該類所有的對象同一把鎖。
  • 每個對象隻有一個鎖(lock)與之相關聯,誰拿到這個鎖誰就可以運作它所控制的那段代碼。
  • 實作同步是要很大的系統開銷作為代價的,甚至可能造成死鎖,是以盡量避免無謂的同步控制。

下面介紹一個鎖的執行個體:

public class ManyThread {
    int count = 0;

    public synchronized void autoIncrement() {
        count++;
    }

    public static void main(String args[]) {
        ManyThread manyThread = new ManyThread();
        Runnable runnable = new MyRunnable2(manyThread);
        new Thread(runnable, "a").start();
        new Thread(runnable, "b").start();
        new Thread(runnable, "c").start();
        new Thread(runnable, "d").start();
    }


}

class MyRunnable2 implements Runnable {

    private  ManyThread manyThread;

    public MyRunnable2(ManyThread manyThread) {
        this.manyThread = manyThread;
    }

    @Override
    public void run() {
        for (int i = 0; i < 10000; i++) {
            manyThread.autoIncrement();
            System.out.println(Thread.currentThread().getName() + " 執行中 " + "count:" + manyThread.count);
        }
    }


}      

用synchronized修飾後的autoIncrement()方法,會被加鎖,確定它每次執行的時候都能保證隻有一個線程在運作。

transient關鍵字

 Java中,一個類想要序列化,可以通過實作Serilizable接口的方式來實作,實作該接口之後,該類所有屬性和方法都會自動序列化。

但是如果屬性或方法被transient修飾,那麼将不會被序列化。