在平時編碼中,咱們可能只注意了這些static,final,volatile等關鍵字的使用,忽略了他們的細節,更深層次的意義。java
本文總結了Java中全部常見的關鍵字以及一些例子。react
概述:緩存
當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.靜態塊,對於定義在它以後的靜態變量,能夠賦值但不能訪問。
下面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+" 靜態變量加載"); } }
輸出結果:
下面是我總結類加載流程,能夠對照着這個流程,能夠再從新看一下上面的例子,會有新的理解。
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 }
輸出結果:
一步一步地解析:
沒想到static能有這麼多須要注意的,能夠說Java中的語法仍是有不少能夠深究的.
概述:
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對象。
緩存一致性:
首先來看看線程的內存模型圖:
當執行代碼:
i = i + 1;
這個在單線程的環境中是沒有問題的,可是運行到多線程中就存在問題了。
問題出在主存中的變量,由於有可能其餘線程讀的值,線程的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修飾以後,那麼將具有兩個屬性:
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修飾中,由於在線程池中有若干個線程,這些變量必需保持對線程可見性,否則會引發線程池運行不正確。
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,也可使用原子變量。
原子變量的例子:
volatile的原理
下面這段話摘自《深刻理解Java虛擬機》: 「」觀察加入volatile關鍵字和沒有加入volatile關鍵字時所生成的彙編代碼發現,加入volatile關鍵字時,會多出一個lock前綴指令」 lock前綴指令實際上至關於一個內存屏障(也成內存柵欄),內存屏障會提供3個功能: 1)它確保指令重排序時不會把其後面的指令排到內存屏障以前的位置,也不會把前面的指令排到內存屏障的後面;即在執行到內存屏障這句指令時,在它前面的操做已經所有完成; 2)它會強制將對緩存的修改操做當即寫入主存; 3)若是是寫操做,它會致使其餘CPU中對應的緩存行無效。
assert斷言
在目前的java編碼中,是不推薦使用的,這裏只是稍微瞭解一下:
使用方式:
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。
關於鎖關鍵字,有如下幾個總結:
下面介紹一個鎖的實例:
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()方法,會被加鎖,確保它每次執行的時候都能保證只有一個線程在運行。
Java中,一個類想要序列化,能夠經過實現Serilizable接口的方式來實現,實現該接口以後,該類全部屬性和方法都會自動序列化。
可是若是屬性或方法被transient修飾,那麼將不會被序列