詳解JAVA中的關鍵字

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

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

 

static 關鍵字


概述:緩存

當static修飾類的屬性或者方法時,那麼就能夠在沒有建立對象的狀況下使用該屬性或方法。多線程

靜態塊也是static的一個應用,用於初始化類時的一些操做。dom

 

靜態方法和靜態變量

劃重點ide

被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 }
複製代碼

 

輸出結果:

  View Code

 

一步一步地解析:

  • 在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)));
    }
}
複製代碼
  View Code

由於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關鍵字


緩存一致性:

首先來看看線程的內存模型圖:

 

當執行代碼:

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的狀況。輸出結果以下:

 對於這種狀況,可使用鎖,synchronize,Lock,也可使用原子變量。

原子變量的例子:

View Code

 

 volatile的原理

複製代碼
 下面這段話摘自《深刻理解Java虛擬機》:

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

lock前綴指令實際上至關於一個內存屏障(也成內存柵欄),內存屏障會提供3個功能:

1)它確保指令重排序時不會把其後面的指令排到內存屏障以前的位置,也不會把前面的指令排到內存屏障的後面;即在執行到內存屏障這句指令時,在它前面的操做已經所有完成;

2)它會強制將對緩存的修改操做當即寫入主存;

3)若是是寫操做,它會致使其餘CPU中對應的緩存行無效。
複製代碼

 

assert關鍵字

assert斷言

在目前的java編碼中,是不推薦使用的,這裏只是稍微瞭解一下:

使用方式:

 
一、assert <boolean表達式>
若是<boolean表達式>爲true,則程序繼續執行。
若是爲false,則程序拋出AssertionError,並終止執行。
 
二、assert <boolean表達式> : <錯誤信息表達式>
若是<boolean表達式>爲true,則程序繼續執行。
若是爲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修飾,那麼將不會被序列

相關文章
相關標籤/搜索