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