摘要:關鍵字總結有個關鍵字,它們是接下來對其中常用的幾個關鍵字進行概括。而通過關鍵字,并不能解決非原子操作的線程安全性。為了在一個特定對象的一個域上關閉,可以在這個域前加上關鍵字。是語言的關鍵字,用來表示一個域不是該對象串行化的一部分。
java 關鍵字總結
Java有50個關鍵字,它們是:
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
接下來對其中常用的幾個關鍵字進行概括。
public private protected
public,protected,private是Java里用來定義成員的訪問權限的,另外還有一種是“default”,也就是在成員前不加任何權限修飾符。
這四個修飾詞de訪問權限見下:
-- | 類內部 | package內 | 子類 | 其他 |
---|---|---|---|---|
public | 允許 | 允許 | 允許 | 允許 |
protected | 允許 | 允許 | 允許 | 不允許 |
default | 允許 | 允許 | 不允許 | 不允許 |
private | 允許 | 不允許 | 不允許 | 不允許 |
比如:用protected修飾的成員(變量或方法),在類內部可以調用,同一個package下的其他類也可以調用,子類里也可以調用,其他地方則不可以調用,也就是說在其他。
在java中,除了這四種修飾詞外,還有其他如abstract、static、final等11個修飾詞。
public
使用對象:類、接口、成員
介紹:無論它所處在的包定義在哪,該類(接口、成員)都是可訪問的
private
使用對象:成員
介紹:成員只可以在定義它的類中被訪問
static
使用對象:類、方法、字段、初始化函數
介紹:成名為static的內部類是一個頂級類,它和包含類的成員是不相關的。靜態方法是類方法,是被指向到所屬的類而不是類的實例。靜態字段是類字段,無論該字段所在的類創建了多少實例,該字段只存在一個實例被指向到所屬的類而不是類的實例。初始化函數是在裝載類時執行的,而不是在創建實例時執行的。
final
使用對象:類、方法、字段、變量
介紹:被定義成final的類不允許出現子類,不能被覆蓋(不應用于動態查詢),字段值不允許被修改。
abstract
使用對象:類、接口、方法
介紹:類中包括沒有實現的方法,不能被實例化。如果是一個abstract方法,則方法體為空,該方法的實現在子類中被定義,并且包含一個abstract方法的類必須是一個abstract類
protected
使用對象:成員
介紹:成員只能在定義它的包中被訪問,如果在其他包中被訪問,則實現這個方法的類必須是該成員所屬類的子類。
native
使用對象:成員
介紹:與操作平臺相關,定義時并不定義其方法,方法的實現被一個外部的庫實現。native可以與所有其它的java標識符連用,但是abstract除外。
public native int hashCode();
strictfp
使用對象:類、方法
介紹:strictfp修飾的類中所有的方法都隱藏了strictfp修飾詞,方法執行的所有浮點計算遵守IEEE 754標準,所有取值包括中間的結果都必須表示為float或double類型,而不能利用由本地平臺浮點格式或硬件提供的額外精度或表示范圍。
synchronized
使用對象:方法
介紹:對于一個靜態的方法,在執行之前jvm把它所在的類鎖定;對于一個非靜態類的方法,執行前把某個特定對象實例鎖定。
volatile
使用對象:字段
介紹:因為異步線程可以訪問字段,所以有些優化操作是一定不能作用在字段上的。volatile有時可以代替synchronized。
transient
使用對象:字段
介紹:字段不是對象持久狀態的一部分,不應該把字段和對象一起串起。
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關鍵字修改之后,還是會存在并發的情況。
測試volatile、AtomicInteger這是美團一面面試官的一個問題: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: " + "username: " + uid + " login date : " + now.toString() + " 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都是可重入鎖,一定情況下避免了死鎖。詳情請參考可重入鎖
文章版權歸作者所有,未經允許請勿轉載,若此文章存在違規行為,您可以聯系管理員刪除。
轉載請注明本文地址:http://specialneedsforspecialkids.com/yun/70189.html
摘要:導入包注意使用了關鍵字上面使用了關鍵字之后,可以直接使用中的方法。通過關鍵字調用有參構造方法控制臺輸出總結屬性和方法可以再沒有實例化對象的時候直接由類名稱進行調用。屬性保存在全局數據區。 前面兩篇分別介紹了static的屬性以及方法。本篇就做一個收尾,介紹下剩下的。 在之前的總結: 不管多少個對象,都使用同一個 static 屬性 使用 static 方法可以避免掉用實例化方法之后才...
摘要:強制類型轉換下標運算符變量與常量常量是在程序中的不會變化的數據變量其實就是內存中的一個存儲空間,用于存儲數據。表示結束本次循環,繼續下次循環。 Java知識點總結 (基本語法) @(Java知識點總結)[Java, Java基本語法] @(Java開發)[Java基本語法] [toc] Java特點 簡單自然平臺可移植性支持函數式編程JIT 編譯更好的并發編程健壯安全 執行方式 編譯...
摘要:知識點總結面向對象知識點總結面向對象面向對象概念是相對于面向過程而言,過程其實就是函數,對象是將函數和屬性進行了封裝。指向了該對象關鍵字代表對象。靜態變量所屬于類,所以也稱為類變量成員變量存在于堆內存中。 Java知識點總結(面向對象) @(Java知識點總結)[Java, Java面向對象] [toc] 面向對象概念 是相對于面向過程而言,過程其實就是函數,對象是將函數和屬性進行了封...
摘要:無論是互斥鎖,還是自旋鎖,在任何時刻,最多只能有一個保持者,也就說,在任何時刻最多只能有一個執行單元獲得鎖。另外在中引入了自適應的自旋鎖。和關鍵字的總結推薦一 該文已加入開源文檔:JavaGuide(一份涵蓋大部分Java程序員所需要掌握的核心知識)。地址:https://github.com/Snailclimb... 本文是對 synchronized 關鍵字使用、底層原理、JD...
摘要:再附一部分架構面試視頻講解本文已被開源項目學習筆記總結移動架構視頻大廠面試真題項目實戰源碼收錄 Java反射(一)Java反射(二)Java反射(三)Java注解Java IO(一)Java IO(二)RandomAccessFileJava NIOJava異常詳解Java抽象類和接口的區別Java深拷貝和淺拷...
摘要:基礎問題的的性能及原理之區別詳解備忘筆記深入理解流水線抽象關鍵字修飾符知識點總結必看篇中的關鍵字解析回調機制解讀抽象類與三大特征時間和時間戳的相互轉換為什么要使用內部類對象鎖和類鎖的區別,,優缺點及比較提高篇八詳解內部類單例模式和 Java基礎問題 String的+的性能及原理 java之yield(),sleep(),wait()區別詳解-備忘筆記 深入理解Java Stream流水...
閱讀 3189·2021-11-24 10:30
閱讀 1313·2021-09-30 09:56
閱讀 2385·2021-09-07 10:20
閱讀 2596·2021-08-27 13:10
閱讀 698·2019-08-30 11:11
閱讀 2050·2019-08-29 12:13
閱讀 758·2019-08-26 12:24
閱讀 2897·2019-08-26 12:20