Java有50個關鍵字,它們是:html
abstract do implements private throw Boolean double import protected throws break else instanceof public transient byte extends int return true case false interface short try catch final long static void char finally native super volatile class float new switch while continue for null synchronized const default if package this goto
接下來對其中經常使用的幾個關鍵字進行歸納。java
public private protectedweb
public,protected,private是Java裏用來定義成員的訪問權限的,另外還有一種是「default」,也就是在成員前不加任何權限修飾符。
這四個修飾詞de訪問權限見下:面試
-- | 類內部 | package內 | 子類 | 其餘 |
---|---|---|---|---|
public | 容許 | 容許 | 容許 | 容許 |
protected | 容許 | 容許 | 容許 | 不容許 |
default | 容許 | 容許 | 不容許 | 不容許 |
private | 容許 | 不容許 | 不容許 | 不容許 |
好比:用protected修飾的成員(變量或方法),在類內部能夠調用,同一個package下的其餘類也能夠調用,子類裏也能夠調用,其餘地方則不能夠調用,也就是說在其餘。 spring
在java中,除了這四種修飾詞外,還有其餘如abstract、static、final等11個修飾詞。數組
使用對象:類、接口、成員
介紹:不管它所處在的包定義在哪,該類(接口、成員)都是可訪問的緩存
使用對象:成員
介紹:成員只能夠在定義它的類中被訪問安全
使用對象:類、方法、字段、初始化函數
介紹:成名爲static的內部類是一個頂級類,它和包含類的成員是不相關的。靜態方法是類方法,是被指向到所屬的類而不是類的實例。靜態字段是類字段,不管該字段所在的類建立了多少實例,該字段只存在一個實例被指向到所屬的類而不是類的實例。初始化函數是在裝載類時執行的,而不是在建立實例時執行的。多線程
使用對象:類、方法、字段、變量
介紹:被定義成final的類不容許出現子類,不能被覆蓋(不該用於動態查詢),字段值不容許被修改。併發
使用對象:類、接口、方法
介紹:類中包括沒有實現的方法,不能被實例化。若是是一個abstract方法,則方法體爲空,該方法的實如今子類中被定義,而且包含一個abstract方法的類必須是一個abstract類
使用對象:成員
介紹:成員只能在定義它的包中被訪問,若是在其餘包中被訪問,則實現這個方法的類必須是該成員所屬類的子類。
使用對象:成員
介紹:與操做平臺相關,定義時並不定義其方法,方法的實現被一個外部的庫實現。native能夠與全部其它的java標識符連用,可是abstract除外。
public native int hashCode();
使用對象:類、方法
介紹:strictfp修飾的類中全部的方法都隱藏了strictfp修飾詞,方法執行的全部浮點計算遵照IEEE 754標準,全部取值包括中間的結果都必須表示爲float或double類型,而不能利用由本地平臺浮點格式或硬件提供的額外精度或表示範圍。
使用對象:方法
介紹:對於一個靜態的方法,在執行以前jvm把它所在的類鎖定;對於一個非靜態類的方法,執行前把某個特定對象實例鎖定。
介紹:由於異步線程能夠訪問字段,因此有些優化操做是必定不能做用在字段上的。volatile有時能夠代替synchronized。
介紹:字段不是對象持久狀態的一部分,不該該把字段和對象一塊兒串起。
volatile
先補充一下概念:Java 內存模型中的可見性、原子性和有序性。 可見性: 可見性是一種複雜的屬性,由於可見性中的錯誤老是會違背咱們的直覺。一般,咱們沒法確保執行讀操做的線程能適時地看到其餘線程寫入的值,有時甚至是根本不可能的事情。爲了確保多個線程之間對內存寫入操做的可見性,必須使用同步機制。 可見性,是指線程之間的可見性,一個線程修改的狀態對另外一個線程是可見的。也就是一個線程修改的結果。另外一個線程立刻就能看到。好比:用volatile修飾的變量,就會具備可見性。volatile修飾的變量不容許線程內部緩存和重排序,即直接修改內存。因此對其餘線程是可見的。可是這裏須要注意一個問題,volatile只能讓被他修飾內容具備可見性,但不能保證它具備原子性。好比 volatile int a = 0;以後有一個操做 a++;這個變量a具備可見性,可是a++ 依然是一個非原子操做,也就是這個操做一樣存在線程安全問題。 在 Java 中 volatile、synchronized 和 final 實現可見性。 原子性: 原子是世界上的最小單位,具備不可分割性。好比 a=0;(a非long和double類型) 這個操做是不可分割的,那麼咱們說這個操做時原子操做。再好比:a++; 這個操做實際是a = a + 1;是可分割的,因此他不是一個原子操做。非原子操做都會存在線程安全問題,須要咱們使用同步技術(sychronized)來讓它變成一個原子操做。一個操做是原子操做,那麼咱們稱它具備原子性。java的concurrent包下提供了一些原子類,咱們能夠經過閱讀API來了解這些原子類的用法。好比:AtomicInteger、AtomicLong、AtomicReference等。 在 Java 中 synchronized 和在 lock、unlock 中操做保證原子性。 有序性: Java 語言提供了 volatile 和 synchronized 兩個關鍵字來保證線程之間操做的有序性,volatile 是由於其自己包含「禁止指令重排序」的語義,synchronized 是由「一個變量在同一個時刻只容許一條線程對其進行 lock 操做」這條規則得到的,此規則決定了持有同一個對象鎖的兩個同步塊只能串行執行。
用volatile修飾的變量,線程在每次使用變量的時候,都會讀取變量修改後的最的值。volatile很容易被誤用,用來進行原子性操做。
volatile強制要求了全部線程在使用變量的時候要去公共內存堆中獲取值, 不能夠偷懶使用本身的.
volatile絕對不保證原子性, 原子性只能用Synchronized同步修飾符實現.
下面看一個例子:
public class Counter { public static int count = 0; public static void inc() { //這裏延遲1毫秒,使得結果明顯 try { Thread.sleep(1); } catch (InterruptedException e) { } count++; } public static void main(String[] args) { //同時啓動1000個線程,去進行i++計算,看看實際結果 for (int i = 0; i < 1000; i++) { new Thread(new Runnable() { @Override public void run() { Counter.inc(); } }).start(); } //這裏每次運行的值都有可能不一樣,可能爲1000 System.out.println("運行結果:Counter.count=" + Counter.count); } }
運行結果:Counter.count=995
實際運算結果每次可能都不同,本機的結果爲:運行結果:Counter.count=995,能夠看出,在多線程的環境下,Counter.count並無指望結果是1000。
不少人覺得,這個是多線程併發問題,只須要在變量count以前加上volatile就能夠避免這個問題,那咱們在修改代碼看看,看看結果是否是符合咱們的指望
public class Counter { public volatile static int count = 0; public static void inc() { //這裏延遲1毫秒,使得結果明顯 try { Thread.sleep(1); } catch (InterruptedException e) { } count++; } public static void main(String[] args) { //同時啓動1000個線程,去進行i++計算,看看實際結果 for (int i = 0; i < 1000; i++) { new Thread(new Runnable() { @Override public void run() { Counter.inc(); } }).start(); } //這裏每次運行的值都有可能不一樣,可能爲1000 System.out.println("運行結果:Counter.count=" + Counter.count); } }
運行結果:Counter.count=992
運行結果仍是沒有咱們指望的1000,下面咱們分析一下緣由:
在 java 垃圾回收整理一文中,描述了jvm運行時刻內存的分配。其中有一個內存區域是jvm虛擬機棧,每個線程運行時都有一個線程棧,線程棧保存了線程運行時候變量值信息。當線程訪問某一個對象時候值的時候,首先經過對象的引用找到對應在堆內存的變量的值,而後把堆內存變量的具體值load到線程本地內存中,創建一個變量副本,以後線程就再也不和對象在堆內存變量值有任何關係,而是直接修改副本變量的值,在修改完以後的某一個時刻(線程退出以前),自動把線程變量副本的值回寫到對象在堆中變量。這樣在堆中的對象的值就產生變化了。
read and load 從主存複製變量到當前工做內存
use and assign 執行代碼,改變共享變量值
store and write 用工做內存數據刷新主存相關內容
其中use and assign 能夠屢次出現
可是這一些操做並非原子性,也就是在read load以後,若是主內存count變量發生修改以後,線程工做內存中的值因爲已經加載,不會產生對應的變化,因此計算出來的結果會和預期不同。
對於volatile修飾的變量,jvm虛擬機只是保證從主內存加載到線程工做內存的值是最新的
例如假如線程1,線程2 在進行read,load操做中,發現主內存中count的值都是5,那麼都會加載這個最新的值。在線程1堆count進行修改以後,會write到主內存中,主內存中的count變量就會變爲6,線程2因爲已經進行read,load操做,在進行運算以後,也會更新主內存count的變量值爲6,致使兩個線程及時用volatile關鍵字修改以後,仍是會存在併發的狀況。
這是美團一面面試官的一個問題:i++;在多線程環境下是否存在問題?若是存在,那怎麼解決?。。。大部分人會說加鎖或者synchronized同步方法。那有沒有更好的方法?
示例代碼:
public class IncrementTestDemo { public static int count = 0; public static Counter counter = new Counter(); public static AtomicInteger atomicInteger = new AtomicInteger(0); volatile public static int countVolatile = 0; public static void main(String[] args) { for (int i = 0; i < 10; i++) { new Thread() { public void run() { for (int j = 0; j < 1000; j++) { count++; counter.increment(); atomicInteger.getAndIncrement(); countVolatile++; } } }.start(); } try { Thread.sleep(3000); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println("static count: " + count); System.out.println("Counter: " + counter.getValue()); System.out.println("AtomicInteger: " + atomicInteger.intValue()); System.out.println("countVolatile: " + countVolatile); } } class Counter { private int value; public synchronized int getValue() { return value; } public synchronized int increment() { return ++value; } public synchronized int decrement() { return --value; } } 輸出結果爲: static count: 9952 Counter: 10000 AtomicInteger: 10000 countVolatile: 9979
經過上面的例子說明,要解決自增操做在多線程環境下線程不安全的問題,能夠選擇使用Java提供的原子類,或者使用synchronized同步方法。
而經過Volatile關鍵字,並不能解決非原子操做的線程安全性。
結論分析:雖然遞增操做++i是一種緊湊的語法,使其看上去只是一個操做,但這個操做並不是原子的,於是它並不會做爲一個不可分割的操做來執行。實際上,它包含了三個獨立的操做:讀取count的值,將值加1,而後將計算結果寫入count。這是一個「讀取 - 修改 - 寫入」的操做序列,而且其結果狀態依賴於以前的狀態。
使用建議:在兩個或者更多的線程訪問的成員變量上使用volatile。當要訪問的變量已在synchronized代碼塊中,或者爲常量時,沒必要使用。
因爲使用volatile屏蔽掉了VM中必要的代碼優化,因此在效率上比較低,所以必定在必要時才使用此關鍵字。
參考文獻:Java中Volatile關鍵字詳解
參考文獻:Java自增原子性問題(測試Volatile、AtomicInteger)
transient
Java的serialization提供了一種持久化對象實例的機制。當持久化對象時,可能有一個特殊的對象數據成員,咱們不想 用serialization機制來保存它。爲了在一個特定對象的一個域上關閉serialization,能夠在這個域前加上關鍵字transient。 transient是Java語言的關鍵字,用來表示一個域不是該對象串行化的一部分。當一個對象被串行化的時候,transient型變量的值不包括在串行化的表示中,然而非transient型的變量是被包括進去的。 注意static變量也是能夠串行化的
下面使用實例能夠看出效果:
public class Login implements java.io.Serializable { private Date now = new Date(); private String uid; private transient String pwd; LoggingInfo(String username, String password) { uid = username; pwd = password; } public String toString() { String password=null; if(pwd == null) { password = "NOT SET"; } else { password = pwd; } return "logon info: \n " + "username: " + uid + "\n login date : " + now.toString() + "\n password: " + password; } }
如今咱們建立一個這個類的實例,而且串行化(serialize)它 ,而後將這個串行化對象寫如磁盤。
Login login = new Login("yy", "123456"); System.out.println(login.toString()); try { ObjectOutputStream o = new ObjectOutputStream( new FileOutputStream("login.out")); o.writeObject(login); o.close(); } catch(Exception e) {//deal with exception} To read the object back, we can write try { ObjectInputStream in =new ObjectInputStream( new FileInputStream("logInfo.out")); LoggingInfo logInfo = (LoggingInfo)in.readObject(); System.out.println(logInfo.toString()); } catch(Exception e) {//deal with exception}
若是咱們運行這段代碼,咱們會注意到從磁盤中讀回(read——back (de-serializing))的對象打印password爲"NOT SET"。這是當咱們定義pwd域爲transient時,所指望的正確結果。
如今,讓咱們來看一下粗心對待transient域可能引發的潛在問題。假設咱們修改了類定義,提供給transient域一個默認值,
代碼以下:
public class Login implements java.io.Serializable { private Date now = new Date(); private String uid; private transient String pwd; Login() { uid = "guest"; pwd = "guest"; } public String toString() { //same as above } }
如今,若是咱們串行化Login的一個實例,將它寫入磁盤,而且再將它從磁盤中讀出,咱們仍然看到讀回的對象打印password 爲 "NOT SET"。當從磁盤中讀出某個類的實例時,實際上並不會執行這個類的構造函數, 而是載入了一個該類對象的持久化狀態,並將這個狀態賦值給該類的另外一個對象。
參考文獻:Java transient關鍵字
static
Java語言的關鍵字,用來定義一個變量爲類變量。類只維護一個類變量的拷貝,無論該類當前有多少個實例。"static" 一樣可以用來定義一個方法爲類方法。類方法經過類名調用而不是特定的實例,而且只能操做類變量。
static這塊面試常常問的則是:Java的初始化塊、靜態初始化塊、構造函數的執行順序及用途
執行順序:
寫一個簡單的demo來實驗:
class A { static { System.out.println("Static init A."); } { System.out.println("Instance init A."); } A() { System.out.println("Constructor A."); } } class B extends A { static { System.out.println("Static init B."); } { System.out.println("Instance init B."); } B() { System.out.println("Constructor B."); } } class C extends B { static { System.out.println("Static init C."); } { System.out.println("Instance init C."); } C() { System.out.println("Constructor C."); } } public class Main { static { System.out.println("Static init Main."); } { System.out.println("Instance init Main."); } public Main() { System.out.println("Constructor Main."); } public static void main(String[] args) { C c = new C(); //B b = new B(); }
固然這裏不使用內部類,由於==內部類不能使用靜態的定義==;而用靜態內部類就失去了通常性。
執行main方法,結果爲:
Static init Main. Static init A. Static init B. Static init C. Instance init A. Constructor A. Instance init B. Constructor B. Instance init C. Constructor C.
由以上結果咱們能夠發現:
先執行了Main類的靜態塊,接下來分別是A、B、C類的靜態塊,而後是A、B、C的初始化塊和構造函數。其中,Main類的構造函數沒有執行。
全部的靜態初始化塊都優先執行,其次纔是非靜態的初始化塊和構造函數,它們的執行順序是:
總結:
拓展:
在spring中,若是在某一個類中使用初始化塊或者靜態塊的話,要注意一點:不能再靜態塊或者初始化塊中使用其餘注入容器的bean或者帶有@Value註解的變量值,由於該靜態塊或者初始化塊會在spring容器初始化bean以前就執行,這樣的話,在塊中拿到的值則爲null。可是若是隻是要執行一下其餘的操做(沒有引用其餘注入容器的bean或者帶有@Value註解的變量值)時,則能夠代替@PostConstruct或者implement InitializingBean 類。
synchronized
synchronized,Java同步關鍵字。用來標記方法或者代碼塊是同步的。Java同步塊用來避免線程競爭。同步塊在Java中是同步在某個對象上。全部同步在一個對象上的同步塊在同時只能被一個線程進入並執行操做。全部其餘等待進入該同步塊的線程將被阻塞,直到執行該同步塊中的線程退出。
有四種不一樣的同步塊:
上述同步塊都同步在不一樣對象上。實際須要那種同步塊視具體狀況而定。
1.實例方法同步
下面是一個同步的實例方法:
public synchronized void add(int value){ this.count += value; }
在方法聲明synchronized關鍵字,告訴Java該方法是同步的。
Java實例方法同步是同步在擁有該方法的對象上。只有一個線程可以在實例方法同步塊中運行。若是有多個實例存在,那麼一個線程一次能夠在一個實例同步塊中執行操做。一個實例一個線程。
2.靜態方法同步
Java靜態方法同步以下示例:
public static synchronized void add(int value){ this.count += value; }
一樣,這裏synchronized 關鍵字告訴Java這個方法是同步的。
靜態方法的同步是指同步在該方法所在的類對象上。由於在Java虛擬機中一個類只能對應一個類對象,而靜態方法是類對象所持有的,因此同時只容許一個線程執行同一個類中的靜態同步方法。
3.實例方法中的同步塊
在非同步的Java方法中的同步塊的例子以下所示:
public void add(int value){ synchronized(this){ this.count += value; } }
注意Java同步塊構造器用括號將對象括起來。在上例中,使用了「this」,即爲調用add方法的實例自己。在同步構造器中用括號括起來的對象叫作監視器對象。上述代碼使用監視器對象同步,同步實例方法使用調用方法自己的實例做爲監視器對象。
一次只有一個線程可以在同步於同一個監視器對象的Java方法內執行。
下面兩個例子都同步他們所調用的實例對象上,所以他們在同步的執行效果上是等效的。
public class MyClass { public synchronized void log1(String msg1, String msg2){ log.writeln(msg1); log.writeln(msg2); } public void log2(String msg1, String msg2){ synchronized(this){ log.writeln(msg1); log.writeln(msg2); } } }
在上例中,每次只有一個線程可以在兩個同步塊中任意一個方法內執行。
若是第二個同步塊不是同步在this實例對象上,那麼兩個方法能夠被線程同時執行。
4.靜態方法中的同步塊
public class MyClass { public static synchronized void log1(String msg1, String msg2){ log.writeln(msg1); log.writeln(msg2); } public static void log2(String msg1, String msg2){ synchronized(MyClass.class){ log.writeln(msg1); log.writeln(msg2); } } }
這兩個方法不容許同時被線程訪問。
若是第二個同步塊不是同步在MyClass.class這個對象上。那麼這兩個方法能夠同時被線程訪問。
關於synchronized的用法,不少人用它的時候都會理解誤差。咱們來看一個例子,下面這個例子也是很經典的。
public class Demo { public void synchronize A() { //do something... } public void synchronized B() { //do something... } public void C() { synchronized(this){ //do something } } }
不少人認爲在多線程狀況下,線程執行A方法和B方法時能夠同時進行的,實際上是錯的。若是是不一樣的實例,固然不會有影響,可是那樣synchronized就會失去意義。還有一種狀況下,在使用spring進行web開發時,ApplicationContext容器默認全部的bean都是單例的,因此在這種狀況下,同一時間,只能有一個線程進入A方法或者B方法。這樣的話,在A方法和B方法上分別加synchronized就失去了高併發的意義。C方法意義和A、B方法是同樣的,都是使用當前實例做爲對象鎖。因此咱們儘可能避免這樣使用。能夠參考下面的作法。
public class Demo { private byte[] lock1 = new byte[0];//java中生成一個0長度的byte數組比生成一個普通的Object容易; private byte[] lock2 = new byte[0]; public void synchronize A() { //do something... } public void B() { synchronized(lock) { //do something... } } public void C() { synchronized(lock2){ //do something } } }
除此以外,咱們還要注意死鎖的狀況。在JAVA環境下 ReentrantLock和synchronized都是可重入鎖,必定狀況下避免了死鎖。詳情請參考可重入鎖