java继承原理内存角度_Java基础知识巩固
最近發(fā)現(xiàn)自己的Java基礎知識還是有點薄弱,剛好有點空閑時間進行再補一補,然后進行整理一下,方便自己以后復習。其實個人認為Java基礎還是很重要的,不管從事Java后端開發(fā)還是Android開發(fā),Java這塊的基礎還是重中之重,可以多去學習一下Java各種類和數(shù)據(jù)結(jié)構(gòu)的寫法,進行學習!
基礎
正確使用 equals 方法
盡量使用 "字符串".equals(變量)方法,推薦使用java.util.Objects#equals(JDK7 引入的工具類)
Objects.equals(null,"SnailClimb");// false
java.util.Objects#equals源碼:
public static boolean equals(Object a, Object b) {
// 可以避免空指針異常。如果a==null的話此時a.equals(b)就不會得到執(zhí)行,避免出現(xiàn)空指針異常。
return (a == b) || (a != null && a.equals(b));
}
BigDecimal
浮點數(shù)之間的等值判斷,基本數(shù)據(jù)類型不能用==來比較,包裝數(shù)據(jù)類型不能用 equals 來判斷,會造成精度丟失問題,不要使用構(gòu)造方法BigDecimal(double)方式吧double值轉(zhuǎn)化為BigDecimal對象,推薦使用BigDecimal(String)方法
使用使用 BigDecimal 來定義浮點數(shù)的值,再進行浮點數(shù)的運算操作。
BigDecimal a = new BigDecimal("1.0");
BigDecimal b = new BigDecimal("0.9");
BigDecimal c = new BigDecimal("0.8");
BigDecimal x = a.subtract(b);// 0.1
BigDecimal y = b.subtract(c);// 0.1
System.out.println(x.equals(y));// true
BigDecimal 的大小比較:
a.compareTo(b) : 返回 -1 表示小于,0 表示 等于, 1表示 大于。
BigDecimal a = new BigDecimal("1.0");
BigDecimal b = new BigDecimal("0.9");
System.out.println(a.compareTo(b));// 1
基本數(shù)據(jù)類型與包裝數(shù)據(jù)類型的使用標準
【強制】所有的 POJO 類屬性必須使用包裝數(shù)據(jù)類型。
【強制】RPC 方法的返回值和參數(shù)必須使用包裝數(shù)據(jù)類型。
【推薦】所有的局部變量使用基本數(shù)據(jù)類型。
Arrays.asList()使用指南
Arrays.asList()將數(shù)組轉(zhuǎn)換為集合后,底層其實還是數(shù)組,并沒有實現(xiàn)修改集合的方法,所以不能使用其修改集合的相關方法,add/remove/clear方法會拋出UnsupportedOperationException異常
傳遞的數(shù)組必須是對象數(shù)組,而不是基本類型(需要使用包裝數(shù)據(jù)類型)。
如何將數(shù)組轉(zhuǎn)換成ArrayList:
1.最簡便的方法(推薦)
List list = new ArrayList<>(Arrays.asList("a", "b", "c"))
2.使用 Java8 的Stream
Integer [] myArray = { 1, 2, 3 };
List myList = Arrays.stream(myArray).collect(Collectors.toList());
//基本類型也可以實現(xiàn)轉(zhuǎn)換(依賴boxed的裝箱操作)
int [] myArray2 = { 1, 2, 3 };
List myList = Arrays.stream(myArray2).boxed().collect(Collectors.toList());
Collection.toArray()方法使用的坑
該方法是一個泛型方法: T[] toArray(T[] a); 如果toArray方法中沒有傳遞任何參數(shù)的話返回的是Object類型數(shù)組。
String [] s= new String[]{
"dog", "lazy", "a", "over", "jumps", "fox", "brown", "quick", "A"
};
List list = Arrays.asList(s);
Collections.reverse(list);
s=list.toArray(new String[0]);//沒有指定類型的話會報錯
// new String[0]起一個模板的作用,指定了返回數(shù)組的類型,0是為了節(jié)省空間,因為它只是為了說明返回的類型
不要在 foreach 循環(huán)里進行元素的 remove/add 操作
remove元素使用Iterator方式,如果是并發(fā)操作,需要對Itreator對象加鎖。foreach循環(huán)會拋出ConcurrentModificationException異常
String StringBuffer 和 StringBuilder 的區(qū)別是什么? String 為什么是不可變的?
可變性
String 類中使用 final 關鍵字修飾字符數(shù)組來保存字符串 private final char value[]所以 String 對象是不可變的.而StringBuilder 與 StringBuffer 都繼承自 AbstractStringBuilder 類,在 AbstractStringBuilder 中也是使用字符數(shù)組保存字符串char[]value 但是沒有用 final 關鍵字修飾
線程安全性
String 中的對象是不可變的,也就可以理解為常量,線程安全。StringBuffer 對方法加了同步鎖或者對調(diào)用的方法加了同步鎖,所以是線程安全的。StringBuilder 并沒有對方法進行加同步鎖,所以是非線程安全的。
性能
每次對 String 類型進行改變的時候,都會生成一個新的 String 對象,然后將指針指向新的 String 對象。StringBuffer 每次都會對 StringBuffer 對象本身進行操作,而不是生成新的對象并改變對象引用。
對于三者使用的總結(jié):
操作少量的數(shù)據(jù): 適用String
單線程操作字符串緩沖區(qū)下操作大量數(shù)據(jù): 適用StringBuilder
多線程操作字符串緩沖區(qū)下操作大量數(shù)據(jù): 適用StringBuffer
在 Java 中定義一個不做事且沒有參數(shù)的構(gòu)造方法的作用
Java 程序在執(zhí)行子類的構(gòu)造方法之前,如果沒有用 super() 來調(diào)用父類特定的構(gòu)造方法,則會調(diào)用父類中“沒有參數(shù)的構(gòu)造方法”。因此,如果父類中只定義了有參數(shù)的構(gòu)造方法,而在子類的構(gòu)造方法中又沒有用 super() 來調(diào)用父類中特定的構(gòu)造方法,則編譯時將發(fā)生錯誤,因為 Java 程序在父類中找不到?jīng)]有參數(shù)的構(gòu)造方法可供執(zhí)行。解決辦法是在父類里加上一個不做事且沒有參數(shù)的構(gòu)造方法。
接口和抽象類的區(qū)別
接口的方法默認是 public,所有方法在接口中不能有實現(xiàn)(Java 8 開始接口方法可以有默認實現(xiàn)),而抽象類可以有非抽象的方法。
接口中除了static、final變量,不能有其他變量,而抽象類中則不一定。
一個類可以實現(xiàn)多個接口,但只能繼承一個抽象類。接口自己本身可以通過extends關鍵字擴展多個接口。
接口方法默認修飾符是public,抽象方法可以有public、protected和default這些修飾符(抽象方法就是為了被重寫所以不能使用private關鍵字修飾!)。
從設計層面來說,抽象是對類的抽象,是一種模板設計,而接口是對行為的抽象,是一種行為的規(guī)范。
構(gòu)造方法的特性
名字與類名相同。
沒有返回值,但不能用void聲明構(gòu)造函數(shù)。
生成類的對象時自動執(zhí)行,無需調(diào)用。
== 與 equals(重要)
== : 它的作用是判斷兩個對象的地址是不是相等。即,判斷兩個對象是不是同一個對象(基本數(shù)據(jù)類型==比較的是值,引用數(shù)據(jù)類型==比較的是內(nèi)存地址)。
equals() : 它的作用也是判斷兩個對象是否相等。但它一般有兩種使用情況:
情況1:類沒有覆蓋 equals() 方法。則通過 equals() 比較該類的兩個對象時,等價于通過“==”比較這兩個對象。
情況2:類覆蓋了 equals() 方法。一般,我們都覆蓋 equals() 方法來比較兩個對象的內(nèi)容是否相等;若它們的內(nèi)容相等,則返回 true (即,認為這兩個對象相等)。
hashCode 與 equals (重要)
hashCode() 的作用就是獲取哈希碼,也稱為散列碼;它實際上是返回一個int整數(shù)。這個哈希碼的作用是確定該對象在哈希表中的索引位置。hashCode() 在散列表中才有用,在其它情況下沒用。在散列表中hashCode() 的作用是獲取對象的散列碼,進而確定該對象在散列表中的位置。
hashCode()與equals()的相關規(guī)定
如果兩個對象相等,則hashcode一定也是相同的
兩個對象相等,對兩個對象分別調(diào)用equals方法都返回true
兩個對象有相同的hashcode值,它們也不一定是相等的
因此,equals 方法被覆蓋過,則 hashCode 方法也必須被覆蓋
hashCode() 的默認行為是對堆上的對象產(chǎn)生獨特值。如果沒有重寫 hashCode(),則該 class 的兩個對象無論如何都不會相等(即使這兩個對象指向相同的數(shù)據(jù))
Java異常處理
try 塊:用于捕獲異常。其后可接零個或多個catch塊,如果沒有catch塊,則必須跟一個finally塊。
catch 塊:用于處理try捕獲到的異常。
finally 塊:無論是否捕獲或處理異常,finally塊里的語句都會被執(zhí)行。當在try塊或catch塊中遇到return語句時,finally語句塊將在方法返回之前被執(zhí)行。
在以下4種特殊情況下,finally塊不會被執(zhí)行:
在finally語句塊第一行發(fā)生了異常。 因為在其他行,finally塊還是會得到執(zhí)行
在前面的代碼中用了System.exit(int)已退出程序。 exit是帶參函數(shù) ;若該語句在異常語句之后,finally會執(zhí)行
程序所在的線程死亡。
關閉CPU。
當try語句和finally語句中都有return語句時,在方法返回之前,finally語句的內(nèi)容將被執(zhí)行,并且finally語句的返回值將會覆蓋原始的返回值。
Java序列化中如果有些字段不想進行序列化,如何做
對于不想進行序列化的變量,使用transient關鍵字修飾。
transient關鍵字的作用是:阻止實例中那些用此關鍵字修飾的的變量序列化;當對象被反序列化時,被transient修飾的變量值不會被持久化和恢復。transient只能修飾變量,不能修飾類和方法。
Java 中只有值傳遞
Java程序設計語言總是采用按值調(diào)用。方法得到的是所有參數(shù)值的一個拷貝,即方法不能修改傳遞給它的任何參數(shù)變量的內(nèi)容。
一個方法不能修改一個基本數(shù)據(jù)類型的參數(shù)(即數(shù)值型或布爾型)。
一個方法可以改變一個對象參數(shù)的狀態(tài)。
一個方法不能讓對象參數(shù)引用一個新的對象。
容器
List,Set,Map三者的區(qū)別
List(對付順序的好幫手): List接口存儲一組不唯一(可以有多個元素引用相同的對象),有序的對象
Set(注重獨一無二的性質(zhì)): 不允許重復的集合。不會有多個元素引用相同的對象。
Map(用Key來搜索的專家): 使用鍵值對存儲。Map會維護與Key有關聯(lián)的值。兩個Key可以引用相同的對象,但Key不能重復,典型的Key是String類型,但也可以是任何對象。
Arraylist 與 LinkedList 區(qū)別
是否保證線程安全:ArrayList 和LinkedList 都是不同步的,也就是不保證線程安全;
底層數(shù)據(jù)結(jié)構(gòu): Arraylist 底層使用的是** Object 數(shù)組;LinkedList 底層使用的是雙向鏈表**數(shù)據(jù)結(jié)構(gòu)(JDK1.6之前為循環(huán)鏈表,JDK1.7取消了循環(huán)。注意雙向鏈表和雙向循環(huán)鏈表的區(qū)別)
插入和刪除是否受元素位置的影響:① ArrayList 采用數(shù)組存儲,所以插入和刪除元素的時間復雜度受元素位置的影響。 比如:執(zhí)行add(E e) 方法的時候, ArrayList 會默認在將指定的元素追加到此列表的末尾,這種情況時間復雜度就是O(1)。但是如果要在指定位置 i 插入和刪除元素的話(add(int index, E element) )時間復雜度就為 O(n-i)。因為在進行上述操作的時候集合中第 i 和第 i 個元素之后的(n-i)個元素都要執(zhí)行向后位/向前移一位的操作。 ② LinkedList 采用鏈表存儲,所以插入,刪除元素時間復雜度不受元素位置的影響,都是近似 O(1)而數(shù)組為近似 O(n)。
4.是否支持快速隨機訪問:LinkedList 不支持高效的隨機元素訪問,而 ArrayList 支持。快速隨機訪問就是通過元素的序號快速獲取元素對象(對應于get(int index) 方法)。
內(nèi)存空間占用:ArrayList的空間浪費主要體現(xiàn)在在list列表的結(jié)尾會預留一定的容量空間,而LinkedList的空間花費則體現(xiàn)在它的每一個元素都需要消耗比ArrayList更多的空間(因為要存放直接后繼和直接前驅(qū)以及數(shù)據(jù))。
RandomAccess接口
RandomAccess 接口中什么都沒有定義,標識實現(xiàn)這個接口的類具有隨機訪問功能(知識標識,沒有具體作用)。
在 binarySearch()方法中,它要判斷傳入的list 是否 RamdomAccess 的實例,如果是,調(diào)用indexedBinarySearch()方法,如果不是,那么調(diào)用iteratorBinarySearch()方法
實現(xiàn)了 RandomAccess 接口的list,優(yōu)先選擇普通 for 循環(huán) ,其次 foreach,
未實現(xiàn) RandomAccess接口的list,優(yōu)先選擇iterator遍歷(foreach遍歷底層也是通過iterator實現(xiàn)的,),大size的數(shù)據(jù),千萬不要使用普通for循環(huán)
ArrayList 與 Vector 的區(qū)別,為什么要用Arraylist取代Vector
Vector類的所有方法都是同步的。可以由兩個線程安全地訪問一個Vector對象、但是一個線程訪問Vector的話代碼要在同步操作上耗費大量的時間。
Arraylist不是同步的,所以在不需要保證線程安全時建議使用Arraylist
通過ArrayList 源碼探索其擴容機制
ArrayList有三種方式來初始化,構(gòu)造方法源碼如下:
/**
* 默認初始容量大小
*/
private static final int DEFAULT_CAPACITY = 10;
private static final Object[] DEFAULTCAPACITY_EMPTY_ELEMENTDATA = {};
/**
*默認構(gòu)造函數(shù),使用初始容量10構(gòu)造一個空列表(無參數(shù)構(gòu)造)
*/
public ArrayList() {
this.elementData = DEFAULTCAPACITY_EMPTY_ELEMENTDATA;
}
/**
* 帶初始容量參數(shù)的構(gòu)造函數(shù)。(用戶自己指定容量)
*/
public ArrayList(int initialCapacity) {
if (initialCapacity > 0) {//初始容量大于0
//創(chuàng)建initialCapacity大小的數(shù)組
this.elementData = new Object[initialCapacity];
} else if (initialCapacity == 0) {//初始容量等于0
//創(chuàng)建空數(shù)組
this.elementData = EMPTY_ELEMENTDATA;
} else {//初始容量小于0,拋出異常
throw new IllegalArgumentException("Illegal Capacity: "+
initialCapacity);
}
}
/**
*構(gòu)造包含指定collection元素的列表,這些元素利用該集合的迭代器按順序返回
*如果指定的集合為null,throws NullPointerException。
*/
public ArrayList(Collection extends E> c) {
elementData = c.toArray();
if ((size = elementData.length) != 0) {
// c.toArray might (incorrectly) not return Object[] (see 6260652)
if (elementData.getClass() != Object[].class)
elementData = Arrays.copyOf(elementData, size, Object[].class);
} else {
// replace with empty array.
this.elementData = EMPTY_ELEMENTDATA;
}
}
以無參數(shù)構(gòu)造方法創(chuàng)建 ArrayList 時,實際上初始化賦值的是一個空數(shù)組。當真正對數(shù)組進行添加元素操作時,才真正分配容量。即向數(shù)組中添加第一個元素時,數(shù)組容量擴為10。
add 方法
/**
* 將指定的元素追加到此列表的末尾。
*/
public boolean add(E e) {
//添加元素之前,先調(diào)用ensureCapacityInternal方法
ensureCapacityInternal(size + 1); // Increments modCount!!
//這里看到ArrayList添加元素的實質(zhì)就相當于為數(shù)組賦值
elementData[size++] = e;
return true;
}
ensureCapacityInternal() 方法
//得到最小擴容量
private void ensureCapacityInternal(int minCapacity) {
if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA) {
// 獲取默認的容量和傳入?yún)?shù)的較大值
minCapacity = Math.max(DEFAULT_CAPACITY, minCapacity);
}
ensureExplicitCapacity(minCapacity);
}
當 要 add 進第1個元素時,minCapacity為1,在Math.max()方法比較后,minCapacity 為10。
ensureExplicitCapacity() 方法
//判斷是否需要擴容
private void ensureExplicitCapacity(int minCapacity) {
modCount++;
// overflow-conscious code
if (minCapacity - elementData.length > 0)
//調(diào)用grow方法進行擴容,調(diào)用此方法代表已經(jīng)開始擴容了
grow(minCapacity);
}
grow() 方法
/**
* 要分配的最大數(shù)組大小
*/
private static final int MAX_ARRAY_SIZE = Integer.MAX_VALUE - 8;
/**
* ArrayList擴容的核心方法。
*/
private void grow(int minCapacity) {
// oldCapacity為舊容量,newCapacity為新容量
int oldCapacity = elementData.length;
//將oldCapacity 右移一位,其效果相當于oldCapacity /2,
//我們知道位運算的速度遠遠快于整除運算,整句運算式的結(jié)果就是將新容量更新為舊容量的1.5倍,
int newCapacity = oldCapacity + (oldCapacity >> 1);
//然后檢查新容量是否大于最小需要容量,若還是小于最小需要容量,那么就把最小需要容量當作數(shù)組的新容量,
if (newCapacity - minCapacity < 0)
newCapacity = minCapacity;
// 如果新容量大于 MAX_ARRAY_SIZE,進入(執(zhí)行) `hugeCapacity()` 方法來比較 minCapacity 和 MAX_ARRAY_SIZE,
//如果minCapacity大于最大容量,則新容量則為`Integer.MAX_VALUE`,否則,新容量大小則為 MAX_ARRAY_SIZE 即為 `Integer.MAX_VALUE - 8`。
if (newCapacity - MAX_ARRAY_SIZE > 0)
newCapacity = hugeCapacity(minCapacity);
// minCapacity is usually close to size, so this is a win:
elementData = Arrays.copyOf(elementData, newCapacity);
}
hugeCapacity() 方法。
private static int hugeCapacity(int minCapacity) {
if (minCapacity < 0) // overflow
throw new OutOfMemoryError();
//對minCapacity和MAX_ARRAY_SIZE進行比較
//若minCapacity大,將Integer.MAX_VALUE作為新數(shù)組的大小
//若MAX_ARRAY_SIZE大,將MAX_ARRAY_SIZE作為新數(shù)組的大小
//MAX_ARRAY_SIZE = Integer.MAX_VALUE - 8;
return (minCapacity > MAX_ARRAY_SIZE) ?
Integer.MAX_VALUE :
MAX_ARRAY_SIZE;
}
java 中的 length 屬性針對數(shù)組,比如說你聲明了一個數(shù)組,想知道這個數(shù)組的長度則用到了 length 這個屬性.
java 中的 length() 方法針對字符串,如果想看這個字符串的長度則用到 length() 這個方法.
java 中的 size()方法針對泛型集合,如果想看這個泛型有多少個元素,就調(diào)用此方法來查看.
ArrayList源碼中的ensureCapacity方法
最好在 add 大量元素之前用 ensureCapacity 方法,以減少增量重新分配的次數(shù)
/**
如有必要,增加此 ArrayList 實例的容量,以確保它至少可以容納由minimum capacity參數(shù)指定的元素數(shù)。
*
* @param minCapacity 所需的最小容量
*/
public void ensureCapacity(int minCapacity) {
int minExpand = (elementData != DEFAULTCAPACITY_EMPTY_ELEMENTDATA)
// any size if not default element table
? 0
// larger than default for default empty table. It's already
// supposed to be at default size.
: DEFAULT_CAPACITY;
if (minCapacity > minExpand) {
ensureExplicitCapacity(minCapacity);
}
}
HashMap 和 Hashtable 的區(qū)別
線程是否安全: HashMap 是非線程安全的,HashTable 是線程安全的;HashTable 內(nèi)部的方法基本都經(jīng)過synchronized 修飾(要保證線程安全的話就使用 ConcurrentHashMap);
效率:因為線程安全的問題,HashMap 要比 HashTable 效率高一點。另外,HashTable 基本被淘汰,不要在代碼中使用它;
對Null key 和Null value的支持:HashMap 中,null 可以作為鍵,這樣的鍵只有一個,可以有一個或多個鍵所對應的值為 null。但是在 HashTable 中 put 進的鍵值只要有一個 null,直接拋出NullPointerException。
初始容量大小和每次擴充容量大小的不同 : ①創(chuàng)建時如果不指定容量初始值,Hashtable 默認的初始大小為11,之后每次擴充,容量變?yōu)樵瓉淼?n+1。HashMap 默認的初始化大小為16。之后每次擴充,容量變?yōu)樵瓉淼?倍。②創(chuàng)建時如果給定了容量初始值,那么 Hashtable 會直接使用你給定的大小,而 HashMap 會將其擴充為2的冪次方大小(HashMap 中的tableSizeFor()方法保證)。也就是說 HashMap 總是使用2的冪作為哈希表的大小。
底層數(shù)據(jù)結(jié)構(gòu): JDK1.8 以后的 HashMap 在解決哈希沖突時有了較大的變化,當鏈表長度大于閾值(默認為8)時,將鏈表轉(zhuǎn)化為紅黑樹,以減少搜索時間。Hashtable 沒有這樣的機制。
HasMap 中帶有初始容量的構(gòu)造函數(shù):
public HashMap(int initialCapacity, float loadFactor) {
if (initialCapacity < 0)
throw new IllegalArgumentException("Illegal initial capacity: " +
initialCapacity);
if (initialCapacity > MAXIMUM_CAPACITY)
initialCapacity = MAXIMUM_CAPACITY;
if (loadFactor <= 0 || Float.isNaN(loadFactor))
throw new IllegalArgumentException("Illegal load factor: " +
loadFactor);
this.loadFactor = loadFactor;
this.threshold = tableSizeFor(initialCapacity);
}
tableSizeFor方法保證了 HashMap 總是使用2的冪作為哈希表的大小。
/**
* Returns a power of two size for the given target capacity.
*/
static final int tableSizeFor(int cap) {
int n = cap - 1;
n |= n >>> 1;
n |= n >>> 2;
n |= n >>> 4;
n |= n >>> 8;
n |= n >>> 16;
return (n < 0) ? 1 : (n >= MAXIMUM_CAPACITY) ? MAXIMUM_CAPACITY : n + 1;
}
集合框架底層數(shù)據(jù)結(jié)構(gòu)總結(jié)
List
Arraylist: Object數(shù)組
Vector: Object數(shù)組
LinkedList: 雙向鏈表(JDK1.6之前為循環(huán)鏈表,JDK1.7取消了循環(huán))
Set
HashSet(無序,唯一): 基于 HashMap 實現(xiàn)的,底層采用 HashMap 來保存元素
LinkedHashSet: LinkedHashSet 繼承與 HashSet,并且其內(nèi)部是通過 LinkedHashMap 來實現(xiàn)的。
TreeSet(有序,唯一): 紅黑樹(自平衡的排序二叉樹。)
Map
HashMap: JDK1.8之前HashMap由數(shù)組+鏈表組成的,數(shù)組是HashMap的主體,鏈表則是主要為了解決哈希沖突而存在的(“拉鏈法”解決沖突)。JDK1.8以后在解決哈希沖突時有了較大的變化,當鏈表長度大于閾值(默認為8)時,將鏈表轉(zhuǎn)化為紅黑樹,以減少搜索時間
LinkedHashMap: LinkedHashMap 繼承自 HashMap,所以它的底層仍然是基于拉鏈式散列結(jié)構(gòu)即由數(shù)組和鏈表或紅黑樹組成。另外,LinkedHashMap 在上面結(jié)構(gòu)的基礎上,增加了一條雙向鏈表,使得上面的結(jié)構(gòu)可以保持鍵值對的插入順序。同時通過對鏈表進行相應的操作,實現(xiàn)了訪問順序相關邏輯。詳細可以查看:《LinkedHashMap 源碼詳細分析(JDK1.8)》
Hashtable: 數(shù)組+鏈表組成的,數(shù)組是 HashMap 的主體,鏈表則是主要為了解決哈希沖突而存在的
TreeMap: 紅黑樹(自平衡的排序二叉樹)
并發(fā)
synchronized 關鍵字
synchronized關鍵字解決的是多個線程之間訪問資源的同步性,synchronized關鍵字可以保證被它修飾的方法或者代碼塊在任意時刻只能有一個線程執(zhí)行。
synchronized關鍵字最主要的三種使用方式:
修飾實例方法: 作用于當前對象實例加鎖,進入同步代碼前要獲得當前對象實例的鎖
修飾靜態(tài)方法: :也就是給當前類加鎖,會作用于類的所有對象實例。訪問靜態(tài) synchronized 方法占用的鎖是當前類的鎖,而訪問非靜態(tài) synchronized 方法占用的鎖是當前實例對象鎖。
修飾代碼塊: 指定加鎖對象,對給定對象加鎖,進入同步代碼庫前要獲得給定對象的鎖。
總結(jié): synchronized 關鍵字加到 static 靜態(tài)方法和 synchronized(class)代碼塊上都是是給 Class 類上鎖。synchronized 關鍵字加到實例方法上是給對象實例上鎖。盡量不要使用 synchronized(String a) 因為JVM中,字符串常量池具有緩存功能!
雙重校驗鎖實現(xiàn)對象單例(線程安全)
public class Singleton {
private volatile static Singleton instance;
private Singleton() {
}
public static Singleton getInstance() {
//先判斷對象是否已經(jīng)實例過,沒有實例化過才進入加鎖代碼
if (instance== null) {
//類對象加鎖
synchronized (Singleton.class) {
if (instance== null) {
instance= new Singleton();
}
}
}
return instance;
}
}
instance采用 volatile 關鍵字修飾也是很有必要的, instance= new Singleton(); 這段代碼其實是分為三步執(zhí)行:
為 instance分配內(nèi)存空間
初始化 instance
將 instance指向分配的內(nèi)存地址
但是由于 JVM 具有指令重排的特性,執(zhí)行順序有可能變成 1->3->2。指令重排在單線程環(huán)境下不會出先問題,但是在多線程環(huán)境下會導致一個線程獲得還沒有初始化的實例。例如,線程 T1 執(zhí)行了 1 和 3,此時 T2 調(diào)用 getInstance() 后發(fā)現(xiàn) instance不為空,因此返回 instance,但此時 instance還未被初始化。
使用 volatile 可以禁止 JVM 的指令重排,保證在多線程環(huán)境下也能正常運行。
synchronized 關鍵字和 volatile 關鍵字的區(qū)別
volatile關鍵字是線程同步的輕量級實現(xiàn),所以volatile性能肯定比synchronized關鍵字要好。但是volatile關鍵字只能用于變量而synchronized關鍵字可以修飾方法以及代碼塊。synchronized關鍵字在JavaSE1.6之后進行了主要包括為了減少獲得鎖和釋放鎖帶來的性能消耗而引入的偏向鎖和輕量級鎖以及其它各種優(yōu)化之后執(zhí)行效率有了顯著提升,實際開發(fā)中使用 synchronized 關鍵字的場景還是更多一些。
多線程訪問volatile關鍵字不會發(fā)生阻塞,而synchronized關鍵字可能會發(fā)生阻塞
volatile關鍵字能保證數(shù)據(jù)的可見性,但不能保證數(shù)據(jù)的原子性。synchronized關鍵字兩者都能保證。
volatile關鍵字主要用于解決變量在多個線程之間的可見性,而 synchronized關鍵字解決的是多個線程之間訪問資源的同步性。
ThreadLocal
通常情況下,我們創(chuàng)建的變量是可以被任何一個線程訪問并修改的。如果想實現(xiàn)每一個線程都有自己的專屬本地變量該如何解決呢? JDK中提供的ThreadLocal類正是為了解決這樣的問題。 ThreadLocal類主要解決的就是讓每個線程綁定自己的值,可以將ThreadLocal類形象的比喻成存放數(shù)據(jù)的盒子,盒子中可以存儲每個線程的私有數(shù)據(jù)。
如果你創(chuàng)建了一個ThreadLocal變量,那么訪問這個變量的每個線程都會有這個變量的本地副本,這也是ThreadLocal變量名的由來。他們可以使用 get() 和 set() 方法來獲取默認值或?qū)⑵渲蹈臑楫斍熬€程所存的副本的值,從而避免了線程安全問題。
ThreadLocal原理
Thread 類中有一個 threadLocals 和 一個 inheritableThreadLocals 變量,它們都是 ThreadLocalMap 類型的變量,我們可以把 ThreadLocalMap 理解為ThreadLocal 類實現(xiàn)的定制化的 HashMap。默認情況下這兩個變量都是null,只有當前線程調(diào)用 ThreadLocal 類的 set或get方法時才創(chuàng)建它們,實際上調(diào)用這兩個方法的時候,我們調(diào)用的是ThreadLocalMap類對應的 get()、set()方法。
ThreadLocal類的set()方法
public void set(T value) {
Thread t = Thread.currentThread();
ThreadLocalMap map = getMap(t);
if (map != null)
map.set(this, value);
else
createMap(t, value);
}
ThreadLocalMap getMap(Thread t) {
return t.threadLocals;
}
最終的變量是放在了當前線程的 ThreadLocalMap 中,并不是存在 ThreadLocal 上,ThreadLocal 可以理解為只是ThreadLocalMap的封裝,傳遞了變量值。 ThrealLocal 類中可以通過Thread.currentThread()獲取到當前線程對象后,直接通過getMap(Thread t)可以訪問到該線程的ThreadLocalMap對象。
每個Thread中都具備一個ThreadLocalMap,而ThreadLocalMap可以存儲以ThreadLocal為key的鍵值對。 比如我們在同一個線程中聲明了兩個 ThreadLocal 對象的話,會使用 Thread內(nèi)部都是使用僅有那個ThreadLocalMap 存放數(shù)據(jù)的,ThreadLocalMap的 key 就是 ThreadLocal對象,value 就是 ThreadLocal 對象調(diào)用set方法設置的值。ThreadLocal 是 map結(jié)構(gòu)是為了讓每個線程可以關聯(lián)多個 ThreadLocal變量。這也就解釋了 ThreadLocal 聲明的變量為什么在每一個線程都有自己的專屬本地變量。
ThreadLocalMap是ThreadLocal的靜態(tài)內(nèi)部類。
ThreadLocal 內(nèi)存泄露問題
ThreadLocalMap 中使用的 key 為 ThreadLocal 的弱引用,而 value 是強引用。所以,如果 ThreadLocal 沒有被外部強引用的情況下,在垃圾回收的時候會 key 會被清理掉,而 value 不會被清理掉。這樣一來,ThreadLocalMap 中就會出現(xiàn)key為null的Entry。假如我們不做任何措施的話,value 永遠無法被GC 回收,這個時候就可能會產(chǎn)生內(nèi)存泄露。ThreadLocalMap實現(xiàn)中已經(jīng)考慮了這種情況,在調(diào)用 set()、get()、remove() 方法的時候,會清理掉 key 為 null 的記錄。使用完 ThreadLocal方法后 最好手動調(diào)用remove()方法
線程池
線程池提供了一種限制和管理資源(包括執(zhí)行一個任務)。 每個線程池還維護一些基本統(tǒng)計信息,例如已完成任務的數(shù)量。
使用線程池的好處:
降低資源消耗。 通過重復利用已創(chuàng)建的線程降低線程創(chuàng)建和銷毀造成的消耗。
提高響應速度。 當任務到達時,任務可以不需要的等到線程創(chuàng)建就能立即執(zhí)行。
提高線程的可管理性。 線程是稀缺資源,如果無限制的創(chuàng)建,不僅會消耗系統(tǒng)資源,還會降低系統(tǒng)的穩(wěn)定性,使用線程池可以進行統(tǒng)一的分配,調(diào)優(yōu)和監(jiān)控。
實現(xiàn)Runnable接口和Callable接口的區(qū)別
如果想讓線程池執(zhí)行任務的話需要實現(xiàn)的Runnable接口或Callable接口。 Runnable接口或Callable接口實現(xiàn)類都可以被ThreadPoolExecutor或ScheduledThreadPoolExecutor執(zhí)行。兩者的區(qū)別在于 Runnable 接口不會返回結(jié)果但是 Callable 接口可以返回結(jié)果。
備注: 工具類Executors可以實現(xiàn)Runnable對象和Callable對象之間的相互轉(zhuǎn)換。(Executors.callable(Runnable task)或Executors.callable(Runnable task,Object resule))。
執(zhí)行execute()方法和submit()方法的區(qū)別
1)execute() 方法用于提交不需要返回值的任務,所以無法判斷任務是否被線程池執(zhí)行成功與否;
2)submit() 方法用于提交需要返回值的任務。線程池會返回一個Future類型的對象,通過這個Future對象可以判斷任務是否執(zhí)行成功,并且可以通過future的get()方法來獲取返回值,get()方法會阻塞當前線程直到任務完成,而使用 get(long timeout,TimeUnit unit)方法則會阻塞當前線程一段時間后立即返回,這時候有可能任務沒有執(zhí)行完。
如何創(chuàng)建線程池
《阿里巴巴Java開發(fā)手冊》中強制線程池不允許使用 Executors 去創(chuàng)建,而是通過 ThreadPoolExecutor 的方式,這樣的處理方式能更加明確線程池的運行規(guī)則,規(guī)避資源耗盡的風險
Executors 返回線程池對象的弊端如下:
FixedThreadPool 和 SingleThreadExecutor : 允許請求的隊列長度為 Integer.MAX_VALUE ,可能堆積大量的請求,從而導致OOM。
CachedThreadPool 和 ScheduledThreadPool : 允許創(chuàng)建的線程數(shù)量為 Integer.MAX_VALUE ,可能會創(chuàng)建大量線程,從而導致OOM。
方式一:通過構(gòu)造方法實現(xiàn)
通過構(gòu)造方法實現(xiàn).png
方式二:通過Executor 框架的工具類Executors來實現(xiàn) 我們可以創(chuàng)建三種類型的ThreadPoolExecutor:
FixedThreadPool : 該方法返回一個固定線程數(shù)量的線程池。該線程池中的線程數(shù)量始終不變。當有一個新的任務提交時,線程池中若有空閑線程,則立即執(zhí)行。若沒有,則新的任務會被暫存在一個任務隊列中,待有線程空閑時,便處理在任務隊列中的任務。
SingleThreadExecutor: 方法返回一個只有一個線程的線程池。若多余一個任務被提交到該線程池,任務會被保存在一個任務隊列中,待線程空閑,按先入先出的順序執(zhí)行隊列中的任務。
CachedThreadPool: 該方法返回一個可根據(jù)實際情況調(diào)整線程數(shù)量的線程池。線程池的線程數(shù)量不確定,但若有空閑線程可以復用,則會優(yōu)先使用可復用的線程。若所有線程均在工作,又有新的任務提交,則會創(chuàng)建新的線程處理任務。所有線程在當前任務執(zhí)行完畢后,將返回線程池進行復用。
對應Executors工具類中的方法如圖所示:
進程和線程
進程
進程是程序的一次執(zhí)行過程,是系統(tǒng)運行程序的基本單位,因此進程是動態(tài)的。系統(tǒng)運行一個程序即是一個進程從創(chuàng)建,運行到消亡的過程。
在 Java 中,當我們啟動 main 函數(shù)時其實就是啟動了一個 JVM 的進程,而 main 函數(shù)所在的線程就是這個進程中的一個線程,也稱主線程。
線程
線程與進程相似,但線程是一個比進程更小的執(zhí)行單位。一個進程在其執(zhí)行的過程中可以產(chǎn)生多個線程。與進程不同的是同類的多個線程共享進程的堆和方法區(qū)資源,但每個線程有自己的程序計數(shù)器、虛擬機棧和本地方法棧,所以系統(tǒng)在產(chǎn)生一個線程,或是在各個線程之間作切換工作時,負擔要比進程小得多,也正因為如此,線程也被稱為輕量級進程。
線程與進程的關系,區(qū)別及優(yōu)缺點
從 JVM 角度說進程和線程之間的關系
一個進程中可以有多個線程,多個線程共享進程的堆和方法區(qū) (JDK1.8 之后的元空間)資源,但是每個線程有自己的程序計數(shù)器、虛擬機棧 和 本地方法棧。
總結(jié): 線程是進程劃分成的更小的運行單位。線程和進程最大的不同在于基本上各進程是獨立的,而各線程則不一定,因為同一進程中的線程極有可能會相互影響。線程執(zhí)行開銷小,但不利于資源的管理和保護;而進程正相反
程序計數(shù)器、虛擬機棧和本地方法棧是線程私有的,堆和方法區(qū)是線程共享的
程序計數(shù)器為什么是私有的?
程序計數(shù)器主要有下面兩個作用:
字節(jié)碼解釋器通過改變程序計數(shù)器來依次讀取指令,從而實現(xiàn)代碼的流程控制,如:順序執(zhí)行、選擇、循環(huán)、異常處理。
在多線程的情況下,程序計數(shù)器用于記錄當前線程執(zhí)行的位置,從而當線程被切換回來的時候能夠知道該線程上次運行到哪兒了。
需要注意的是,如果執(zhí)行的是 native 方法,那么程序計數(shù)器記錄的是 undefined 地址,只有執(zhí)行的是 Java 代碼時程序計數(shù)器記錄的才是下一條指令的地址。
所以,程序計數(shù)器私有主要是為了線程切換后能恢復到正確的執(zhí)行位置。
虛擬機棧和本地方法棧為什么是私有的?
虛擬機棧: 每個 Java 方法在執(zhí)行的同時會創(chuàng)建一個棧幀用于存儲局部變量表、操作數(shù)棧、常量池引用等信息。從方法調(diào)用直至執(zhí)行完成的過程,就對應著一個棧幀在 Java 虛擬機棧中入棧和出棧的過程。
本地方法棧: 和虛擬機棧所發(fā)揮的作用非常相似,區(qū)別是: 虛擬機棧為虛擬機執(zhí)行 Java 方法 (也就是字節(jié)碼)服務,而本地方法棧則為虛擬機使用到的 Native 方法服務。 在 HotSpot 虛擬機中和 Java 虛擬機棧合二為一。
所以,為了保證線程中的局部變量不被別的線程訪問到,虛擬機棧和本地方法棧是線程私有的。
堆和方法區(qū)
堆和方法區(qū)是所有線程共享的資源,其中堆是進程中最大的一塊內(nèi)存,主要用于存放新創(chuàng)建的對象 (所有對象都在這里分配內(nèi)存),方法區(qū)主要用于存放已被加載的類信息、常量、靜態(tài)變量、即時編譯器編譯后的代碼等數(shù)據(jù)。
并發(fā)與并行的區(qū)別
并發(fā): 同一時間段,多個任務都在執(zhí)行 (單位時間內(nèi)不一定同時執(zhí)行);
并行: 單位時間內(nèi),多個任務同時執(zhí)行。
為什么要使用多線程
先從總體上來說:
從計算機底層來說: 線程可以比作是輕量級的進程,是程序執(zhí)行的最小單位,線程間的切換和調(diào)度的成本遠遠小于進程。另外,多核 CPU 時代意味著多個線程可以同時運行,這減少了線程上下文切換的開銷。
從當代互聯(lián)網(wǎng)發(fā)展趨勢來說: 現(xiàn)在的系統(tǒng)動不動就要求百萬級甚至千萬級的并發(fā)量,而多線程并發(fā)編程正是開發(fā)高并發(fā)系統(tǒng)的基礎,利用好多線程機制可以大大提高系統(tǒng)整體的并發(fā)能力以及性能。
使用多線程可能帶來什么問題?
并發(fā)編程的目的就是為了能提高程序的執(zhí)行效率提高程序運行速度,但是并發(fā)編程并不總是能提高程序運行速度的,而且并發(fā)編程可能會遇到很多問題,比如:內(nèi)存泄漏、上下文切換、死鎖還有受限于硬件和軟件的資源閑置問題。
線程的生命周期和狀態(tài)
Java 線程在運行的生命周期中的指定時刻只可能處于下面 6 種不同狀態(tài)的其中一個狀態(tài)
Java 線程的狀態(tài)
線程在生命周期中并不是固定處于某一個狀態(tài)而是隨著代碼的執(zhí)行在不同狀態(tài)之間切換。
Java 線程狀態(tài)變遷
由上圖可以看出:線程創(chuàng)建之后它將處于 NEW(新建) 狀態(tài),調(diào)用 start() 方法后開始運行,線程這時候處于 READY(可運行) 狀態(tài)。可運行狀態(tài)的線程獲得了 CPU 時間片(timeslice)后就處于 RUNNING(運行) 狀態(tài)。
操作系統(tǒng)隱藏 Java 虛擬機(JVM)中的 RUNNABLE 和 RUNNING 狀態(tài),它只能看到 RUNNABLE 狀態(tài)所以 Java 系統(tǒng)一般將這兩個狀態(tài)統(tǒng)稱為 RUNNABLE(運行中) 狀態(tài) 。
當線程執(zhí)行 wait()方法之后,線程進入 WAITING(等待) 狀態(tài)。進入等待狀態(tài)的線程需要依靠其他線程的通知才能夠返回到運行狀態(tài),而 TIME_WAITING(超時等待) 狀態(tài)相當于在等待狀態(tài)的基礎上增加了超時限制,比如通過 sleep(long millis)方法或 wait(long millis)方法可以將 Java 線程置于 TIMED WAITING 狀態(tài)。當超時時間到達后 Java 線程將會返回到 RUNNABLE 狀態(tài)。當線程調(diào)用同步方法時,在沒有獲取到鎖的情況下,線程將會進入到 BLOCKED(阻塞) 狀態(tài)。線程在執(zhí)行 Runnable 的run()方法之后將會進入到 TERMINATED(終止) 狀態(tài)。
什么是上下文切換
多線程編程中一般線程的個數(shù)都大于 CPU 核心的個數(shù),而一個 CPU 核心在任意時刻只能被一個線程使用,為了讓這些線程都能得到有效執(zhí)行,CPU 采取的策略是為每個線程分配時間片并輪轉(zhuǎn)的形式。當一個線程的時間片用完的時候就會重新處于就緒狀態(tài)讓給其他線程使用,這個過程就屬于一次上下文切換。
概括來說就是:當前任務在執(zhí)行完 CPU 時間片切換到另一個任務之前會先保存自己的狀態(tài),以便下次再切換會這個任務時,可以再加載這個任務的狀態(tài)。任務從保存到再加載的過程就是一次上下文切換。
什么是線程死鎖?如何避免死鎖?
認識線程死鎖
多個線程同時被阻塞,它們中的一個或者全部都在等待某個資源被釋放。由于線程被無限期地阻塞,因此程序不可能正常終止。
如下圖所示,線程 A 持有資源 2,線程 B 持有資源 1,他們同時都想申請對方的資源,所以這兩個線程就會互相等待而進入死鎖狀態(tài)。
線程死鎖示意圖
產(chǎn)生死鎖必須具備以下四個條件:
互斥條件:該資源任意一個時刻只由一個線程占用。
請求與保持條件:一個進程因請求資源而阻塞時,對已獲得的資源保持不放。
不剝奪條件:線程已獲得的資源在末使用完之前不能被其他線程強行剝奪,只有自己使用完畢后才釋放資源。
循環(huán)等待條件:若干進程之間形成一種頭尾相接的循環(huán)等待資源關系。
如何避免線程死鎖
只要破壞產(chǎn)生死鎖的四個條件中的其中一個就可以了。
破壞互斥條件
這個條件我們沒有辦法破壞,因為我們用鎖本來就是想讓他們互斥的(臨界資源需要互斥訪問)。
破壞請求與保持條件
一次性申請所有的資源。
破壞不剝奪條件
占用部分資源的線程進一步申請其他資源時,如果申請不到,可以主動釋放它占有的資源。
破壞循環(huán)等待條件
靠按序申請資源來預防。按某一順序申請資源,釋放資源則反序釋放。破壞循環(huán)等待條件。
sleep() 方法和 wait() 方法區(qū)別和共同點
兩者最主要的區(qū)別在于:sleep 方法沒有釋放鎖,而 wait 方法釋放了鎖 。
兩者都可以暫停線程的執(zhí)行。
Wait 通常被用于線程間交互/通信,sleep 通常被用于暫停執(zhí)行。
wait() 方法被調(diào)用后,線程不會自動蘇醒,需要別的線程調(diào)用同一個對象上的 notify() 或者 notifyAll() 方法。sleep() 方法執(zhí)行完成后,線程會自動蘇醒。或者可以使用wait(long timeout)超時后線程會自動蘇醒。
我們調(diào)用 start() 方法時會執(zhí)行 run() 方法,為什么我們不能直接調(diào)用 run() 方法?
new 一個 Thread,線程進入了新建狀態(tài);調(diào)用 start() 方法,會啟動一個線程并使線程進入了就緒狀態(tài),當分配到時間片后就可以開始運行了。 start() 會執(zhí)行線程的相應準備工作,然后自動執(zhí)行 run() 方法的內(nèi)容,這是真正的多線程工作。 而直接執(zhí)行 run() 方法,會把 run 方法當成一個 main 線程下的普通方法去執(zhí)行,并不會在某個線程中執(zhí)行它,所以這并不是多線程工作。
總結(jié): 調(diào)用 start 方法方可啟動線程并使線程進入就緒狀態(tài),而 run 方法只是 thread 的一個普通方法調(diào)用,還是在主線程里執(zhí)行。
final,static,this,super 關鍵字總結(jié)
final 關鍵字
final關鍵字主要用在三個地方:變量、方法、類。
對于一個final變量,如果是基本數(shù)據(jù)類型的變量,則其數(shù)值一旦在初始化之后便不能更改;如果是引用類型的變量,則在對其初始化之后便不能再讓其指向另一個對象。
當用final修飾一個類時,表明這個類不能被繼承。final類中的所有成員方法都會被隱式地指定為final方法。
使用final方法的原因:把方法鎖定,以防任何繼承類修改它的含義
static 關鍵字
static 關鍵字主要有以下四種使用場景:
修飾成員變量和成員方法: 被 static 修飾的成員屬于類,不屬于單個這個類的某個對象,被類中所有對象共享,靜態(tài)變量 存放在 Java 內(nèi)存區(qū)域的方法區(qū)。靜態(tài)方法不能調(diào)用非靜態(tài)方法和非靜態(tài)成員變量。靜態(tài)變量 存放在 Java 內(nèi)存區(qū)域的方法區(qū)。
靜態(tài)代碼塊:代碼執(zhí)行順序(靜態(tài)代碼塊 —>非靜態(tài)代碼 —>構(gòu)造方法) 該類不管創(chuàng)建多少對象,靜態(tài)代碼塊只執(zhí)行一次.
靜態(tài)內(nèi)部類(只能修飾內(nèi)部類):它的創(chuàng)建是不需要依賴外圍類的創(chuàng)建。不能使用任何外圍類的非static成員變量和方法。
靜態(tài)導包
this 關鍵字
this關鍵字用于引用類的當前實例,代表對本類對象的引用,指向本類對象
super 關鍵字
super關鍵字用于從子類訪問父類的變量和方法, 代表對父類對象的引用,指向父類對象
使用 this 和 super 要注意的問題:
在構(gòu)造器中使用 super() 調(diào)用父類中的其他構(gòu)造方法時,該語句必須處于構(gòu)造器的首行,否則編譯器會報錯。另外,this 調(diào)用本類中的其他構(gòu)造方法時,也要放在首行。
this、super不能用在static方法中。
總結(jié)
以上是生活随笔為你收集整理的java继承原理内存角度_Java基础知识巩固的全部內(nèi)容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 解决VS2015无法打开WinSock2
- 下一篇: java美元兑换,(Java实现) 美元