Java基礎(chǔ)知識(shí)鞏固

最近發(fā)現(xiàn)自己的Java基礎(chǔ)知識(shí)還是有點(diǎn)薄弱,剛好有點(diǎn)空閑時(shí)間進(jìn)行再補(bǔ)一補(bǔ),然后進(jìn)行整理一下,方便自己以后復(fù)習(xí)。其實(shí)個(gè)人認(rèn)為Java基礎(chǔ)還是很重要的,不管從事Java后端開(kāi)發(fā)還是Android開(kāi)發(fā),Java這塊的基礎(chǔ)還是重中之重,可以多去學(xué)習(xí)一下Java各種類(lèi)和數(shù)據(jù)結(jié)構(gòu)的寫(xiě)法,進(jìn)行學(xué)習(xí)!

基礎(chǔ)

正確使用 equals 方法

盡量使用 "字符串".equals(變量)方法,推薦使用java.util.Objects#equals(JDK7 引入的工具類(lèi))

Objects.equals(null,"SnailClimb");// false

java.util.Objects#equals源碼:

public static boolean equals(Object a, Object b) {
        // 可以避免空指針異常。如果a==null的話(huà)此時(shí)a.equals(b)就不會(huì)得到執(zhí)行,避免出現(xiàn)空指針異常。
        return (a == b) || (a != null && a.equals(b));
}

BigDecimal

浮點(diǎn)數(shù)之間的等值判斷,基本數(shù)據(jù)類(lèi)型不能用==來(lái)比較,包裝數(shù)據(jù)類(lèi)型不能用 equals 來(lái)判斷,會(huì)造成精度丟失問(wèn)題,不要使用構(gòu)造方法BigDecimal(double)方式把double值轉(zhuǎn)化為BigDecimal對(duì)象,推薦使用BigDecimal(String)方法來(lái)定義浮點(diǎn)數(shù)的值,再進(jìn)行浮點(diǎn)數(shù)的運(yùn)算操作。

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ù)類(lèi)型與包裝數(shù)據(jù)類(lèi)型的使用標(biāo)準(zhǔn)

  • 【強(qiáng)制】所有的 POJO 類(lèi)屬性必須使用包裝數(shù)據(jù)類(lèi)型。
  • 【強(qiáng)制】RPC 方法的返回值和參數(shù)必須使用包裝數(shù)據(jù)類(lèi)型。
  • 【推薦】所有的局部變量使用基本數(shù)據(jù)類(lèi)型。

Arrays.asList()使用指南

Arrays.asList()將數(shù)組轉(zhuǎn)換為集合后,底層其實(shí)還是數(shù)組,并沒(méi)有實(shí)現(xiàn)修改集合的方法,所以不能使用其修改集合的相關(guān)方法,add/remove/clear方法會(huì)拋出UnsupportedOperationException異常
傳遞的數(shù)組必須是對(duì)象數(shù)組,而不是基本類(lèi)型(需要使用包裝數(shù)據(jù)類(lèi)型)。

如何將數(shù)組轉(zhuǎn)換成ArrayList:

1.最簡(jiǎn)便的方法(推薦)
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());
//基本類(lèi)型也可以實(shí)現(xiàn)轉(zhuǎn)換(依賴(lài)boxed的裝箱操作)
int [] myArray2 = { 1, 2, 3 };
List myList = Arrays.stream(myArray2).boxed().collect(Collectors.toList());

Collection.toArray()方法使用的坑

該方法是一個(gè)泛型方法:<T> T[] toArray(T[] a); 如果toArray方法中沒(méi)有傳遞任何參數(shù)的話(huà)返回的是Object類(lèi)型數(shù)組。

String [] s= new String[]{
    "dog", "lazy", "a", "over", "jumps", "fox", "brown", "quick", "A"
};
List<String> list = Arrays.asList(s);
Collections.reverse(list);
s=list.toArray(new String[0]);//沒(méi)有指定類(lèi)型的話(huà)會(huì)報(bào)錯(cuò)  
// new String[0]起一個(gè)模板的作用,指定了返回?cái)?shù)組的類(lèi)型,0是為了節(jié)省空間,因?yàn)樗皇菫榱苏f(shuō)明返回的類(lèi)型

不要在 foreach 循環(huán)里進(jìn)行元素的 remove/add 操作

remove元素使用Iterator方式,如果是并發(fā)操作,需要對(duì)Itreator對(duì)象加鎖。foreach循環(huán)會(huì)拋出ConcurrentModificationException異常

String StringBuffer 和 StringBuilder 的區(qū)別是什么? String 為什么是不可變的?

  • 可變性
    String 類(lèi)中使用 final 關(guān)鍵字修飾字符數(shù)組來(lái)保存字符串 private final char value[]所以 String 對(duì)象是不可變的.而StringBuilder 與 StringBuffer 都繼承自 AbstractStringBuilder 類(lèi),在 AbstractStringBuilder 中也是使用字符數(shù)組保存字符串char[]value 但是沒(méi)有用 final 關(guān)鍵字修飾
  • 線(xiàn)程安全性
    String 中的對(duì)象是不可變的,也就可以理解為常量,線(xiàn)程安全。StringBuffer 對(duì)方法加了同步鎖或者對(duì)調(diào)用的方法加了同步鎖,所以是線(xiàn)程安全的。StringBuilder 并沒(méi)有對(duì)方法進(jìn)行加同步鎖,所以是非線(xiàn)程安全的。
  • 性能
    每次對(duì) String 類(lèi)型進(jìn)行改變的時(shí)候,都會(huì)生成一個(gè)新的 String 對(duì)象,然后將指針指向新的 String 對(duì)象。StringBuffer 每次都會(huì)對(duì) StringBuffer 對(duì)象本身進(jìn)行操作,而不是生成新的對(duì)象并改變對(duì)象引用。

對(duì)于三者使用的總結(jié):
操作少量的數(shù)據(jù): 適用String
單線(xiàn)程操作字符串緩沖區(qū)下操作大量數(shù)據(jù): 適用StringBuilder
多線(xiàn)程操作字符串緩沖區(qū)下操作大量數(shù)據(jù): 適用StringBuffer

在 Java 中定義一個(gè)不做事且沒(méi)有參數(shù)的構(gòu)造方法的作用

Java 程序在執(zhí)行子類(lèi)的構(gòu)造方法之前,如果沒(méi)有用 super() 來(lái)調(diào)用父類(lèi)特定的構(gòu)造方法,則會(huì)調(diào)用父類(lèi)中“沒(méi)有參數(shù)的構(gòu)造方法”。因此,如果父類(lèi)中只定義了有參數(shù)的構(gòu)造方法,而在子類(lèi)的構(gòu)造方法中又沒(méi)有用 super() 來(lái)調(diào)用父類(lèi)中特定的構(gòu)造方法,則編譯時(shí)將發(fā)生錯(cuò)誤,因?yàn)?Java 程序在父類(lèi)中找不到?jīng)]有參數(shù)的構(gòu)造方法可供執(zhí)行。解決辦法是在父類(lèi)里加上一個(gè)不做事且沒(méi)有參數(shù)的構(gòu)造方法。

接口和抽象類(lèi)的區(qū)別

  1. 接口的方法默認(rèn)是 public,所有方法在接口中不能有實(shí)現(xiàn)(Java 8 開(kāi)始接口方法可以有默認(rèn)實(shí)現(xiàn)),而抽象類(lèi)可以有非抽象的方法。
  2. 接口中除了static、final變量,不能有其他變量,而抽象類(lèi)中則不一定。
  3. 一個(gè)類(lèi)可以實(shí)現(xiàn)多個(gè)接口,但只能繼承一個(gè)抽象類(lèi)。接口自己本身可以通過(guò)extends關(guān)鍵字?jǐn)U展多個(gè)接口。
  4. 接口方法默認(rèn)修飾符是public,抽象方法可以有public、protected和default這些修飾符(抽象方法就是為了被重寫(xiě)所以不能使用private關(guān)鍵字修飾?。?。
  5. 從設(shè)計(jì)層面來(lái)說(shuō),抽象是對(duì)類(lèi)的抽象,是一種模板設(shè)計(jì),而接口是對(duì)行為的抽象,是一種行為的規(guī)范。

構(gòu)造方法的特性

  1. 名字與類(lèi)名相同。
  2. 沒(méi)有返回值,但不能用void聲明構(gòu)造函數(shù)。
  3. 生成類(lèi)的對(duì)象時(shí)自動(dòng)執(zhí)行,無(wú)需調(diào)用。

== 與 equals(重要)

== : 它的作用是判斷兩個(gè)對(duì)象的地址是不是相等。即,判斷兩個(gè)對(duì)象是不是同一個(gè)對(duì)象(基本數(shù)據(jù)類(lèi)型==比較的是值,引用數(shù)據(jù)類(lèi)型==比較的是內(nèi)存地址)。

equals() : 它的作用也是判斷兩個(gè)對(duì)象是否相等。但它一般有兩種使用情況:

  • 情況1:類(lèi)沒(méi)有覆蓋 equals() 方法。則通過(guò) equals() 比較該類(lèi)的兩個(gè)對(duì)象時(shí),等價(jià)于通過(guò)“==”比較這兩個(gè)對(duì)象。
  • 情況2:類(lèi)覆蓋了 equals() 方法。一般,我們都覆蓋 equals() 方法來(lái)比較兩個(gè)對(duì)象的內(nèi)容是否相等;若它們的內(nèi)容相等,則返回 true (即,認(rèn)為這兩個(gè)對(duì)象相等)。

hashCode 與 equals (重要)

hashCode() 的作用就是獲取哈希碼,也稱(chēng)為散列碼;它實(shí)際上是返回一個(gè)int整數(shù)。這個(gè)哈希碼的作用是確定該對(duì)象在哈希表中的索引位置。hashCode() 在散列表中才有用,在其它情況下沒(méi)用。在散列表中hashCode() 的作用是獲取對(duì)象的散列碼,進(jìn)而確定該對(duì)象在散列表中的位置。

hashCode()與equals()的相關(guān)規(guī)定

  • 如果兩個(gè)對(duì)象相等,則hashcode一定也是相同的
  • 兩個(gè)對(duì)象相等,對(duì)兩個(gè)對(duì)象分別調(diào)用equals方法都返回true
  • 兩個(gè)對(duì)象有相同的hashcode值,它們也不一定是相等的
  • 因此,equals 方法被覆蓋過(guò),則 hashCode 方法也必須被覆蓋
  • hashCode() 的默認(rèn)行為是對(duì)堆上的對(duì)象產(chǎn)生獨(dú)特值。如果沒(méi)有重寫(xiě) hashCode(),則該 class 的兩個(gè)對(duì)象無(wú)論如何都不會(huì)相等(即使這兩個(gè)對(duì)象指向相同的數(shù)據(jù))

Java異常處理

  • try 塊:用于捕獲異常。其后可接零個(gè)或多個(gè)catch塊,如果沒(méi)有catch塊,則必須跟一個(gè)finally塊。
  • catch 塊:用于處理try捕獲到的異常。
  • finally 塊:無(wú)論是否捕獲或處理異常,finally塊里的語(yǔ)句都會(huì)被執(zhí)行。當(dāng)在try塊或catch塊中遇到return語(yǔ)句時(shí),finally語(yǔ)句塊將在方法返回之前被執(zhí)行。

在以下4種特殊情況下,finally塊不會(huì)被執(zhí)行:

  1. 在finally語(yǔ)句塊第一行發(fā)生了異常。 因?yàn)樵谄渌?,finally塊還是會(huì)得到執(zhí)行
  2. 在前面的代碼中用了System.exit(int)已退出程序。 exit是帶參函數(shù) ;若該語(yǔ)句在異常語(yǔ)句之后,finally會(huì)執(zhí)行
  3. 程序所在的線(xiàn)程死亡。
  4. 關(guān)閉CPU。

當(dāng)try語(yǔ)句和finally語(yǔ)句中都有return語(yǔ)句時(shí),在方法返回之前,finally語(yǔ)句的內(nèi)容將被執(zhí)行,并且finally語(yǔ)句的返回值將會(huì)覆蓋原始的返回值。

Java序列化中如果有些字段不想進(jìn)行序列化,如何做

對(duì)于不想進(jìn)行序列化的變量,使用transient關(guān)鍵字修飾。

transient關(guān)鍵字的作用是:阻止實(shí)例中那些用此關(guān)鍵字修飾的的變量序列化;當(dāng)對(duì)象被反序列化時(shí),被transient修飾的變量值不會(huì)被持久化和恢復(fù)。transient只能修飾變量,不能修飾類(lèi)和方法。

Java 中只有值傳遞

Java程序設(shè)計(jì)語(yǔ)言總是采用按值調(diào)用。方法得到的是所有參數(shù)值的一個(gè)拷貝,即方法不能修改傳遞給它的任何參數(shù)變量的內(nèi)容。

  • 一個(gè)方法不能修改一個(gè)基本數(shù)據(jù)類(lèi)型的參數(shù)(即數(shù)值型或布爾型)。
  • 一個(gè)方法可以改變一個(gè)對(duì)象參數(shù)的狀態(tài)。
  • 一個(gè)方法不能讓對(duì)象參數(shù)引用一個(gè)新的對(duì)象。

容器

List,Set,Map三者的區(qū)別

  • List(對(duì)付順序的好幫手): List接口存儲(chǔ)一組不唯一(可以有多個(gè)元素引用相同的對(duì)象),有序的對(duì)象
  • Set(注重獨(dú)一無(wú)二的性質(zhì)): 不允許重復(fù)的集合。不會(huì)有多個(gè)元素引用相同的對(duì)象。
  • Map(用Key來(lái)搜索的專(zhuān)家): 使用鍵值對(duì)存儲(chǔ)。Map會(huì)維護(hù)與Key有關(guān)聯(lián)的值。兩個(gè)Key可以引用相同的對(duì)象,但Key不能重復(fù),典型的Key是String類(lèi)型,但也可以是任何對(duì)象。

Arraylist 與 LinkedList 區(qū)別

  1. 是否保證線(xiàn)程安全:ArrayList 和LinkedList 都是不同步的,也就是不保證線(xiàn)程安全;
  2. 底層數(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ū)別)
  3. 插入和刪除是否受元素位置的影響:① ArrayList 采用數(shù)組存儲(chǔ),所以插入和刪除元素的時(shí)間復(fù)雜度受元素位置的影響。 比如:執(zhí)行add(E e) 方法的時(shí)候, ArrayList 會(huì)默認(rèn)在將指定的元素追加到此列表的末尾,這種情況時(shí)間復(fù)雜度就是O(1)。但是如果要在指定位置 i 插入和刪除元素的話(huà)(add(int index, E element) )時(shí)間復(fù)雜度就為 O(n-i)。因?yàn)樵谶M(jìn)行上述操作的時(shí)候集合中第 i 和第 i 個(gè)元素之后的(n-i)個(gè)元素都要執(zhí)行向后位/向前移一位的操作。 ② LinkedList 采用鏈表存儲(chǔ),所以插入,刪除元素時(shí)間復(fù)雜度不受元素位置的影響,都是近似 O(1)而數(shù)組為近似 O(n)。
    4.是否支持快速隨機(jī)訪(fǎng)問(wèn):LinkedList 不支持高效的隨機(jī)元素訪(fǎng)問(wèn),而 ArrayList 支持。快速隨機(jī)訪(fǎng)問(wèn)就是通過(guò)元素的序號(hào)快速獲取元素對(duì)象(對(duì)應(yīng)于get(int index) 方法)。
  4. 內(nèi)存空間占用:ArrayList的空間浪費(fèi)主要體現(xiàn)在在list列表的結(jié)尾會(huì)預(yù)留一定的容量空間,而LinkedList的空間花費(fèi)則體現(xiàn)在它的每一個(gè)元素都需要消耗比ArrayList更多的空間(因?yàn)橐娣胖苯雍罄^和直接前驅(qū)以及數(shù)據(jù))。

RandomAccess接口

RandomAccess 接口中什么都沒(méi)有定義,標(biāo)識(shí)實(shí)現(xiàn)這個(gè)接口的類(lèi)具有隨機(jī)訪(fǎng)問(wèn)功能(知識(shí)標(biāo)識(shí),沒(méi)有具體作用)。
在 binarySearch()方法中,它要判斷傳入的list 是否 RamdomAccess 的實(shí)例,如果是,調(diào)用indexedBinarySearch()方法,如果不是,那么調(diào)用iteratorBinarySearch()方法

  • 實(shí)現(xiàn)了 RandomAccess 接口的list,優(yōu)先選擇普通 for 循環(huán) ,其次 foreach,
  • 未實(shí)現(xiàn) RandomAccess接口的list,優(yōu)先選擇iterator遍歷(foreach遍歷底層也是通過(guò)iterator實(shí)現(xiàn)的,),大size的數(shù)據(jù),千萬(wàn)不要使用普通for循環(huán)

ArrayList 與 Vector 的區(qū)別,為什么要用Arraylist取代Vector

Vector類(lèi)的所有方法都是同步的??梢杂蓛蓚€(gè)線(xiàn)程安全地訪(fǎng)問(wèn)一個(gè)Vector對(duì)象、但是一個(gè)線(xiàn)程訪(fǎng)問(wèn)Vector的話(huà)代碼要在同步操作上耗費(fèi)大量的時(shí)間。

Arraylist不是同步的,所以在不需要保證線(xiàn)程安全時(shí)建議使用Arraylist

通過(guò)ArrayList 源碼探索其擴(kuò)容機(jī)制

ArrayList有三種方式來(lái)初始化,構(gòu)造方法源碼如下:

    /**
     * 默認(rèn)初始容量大小
     */
    private static final int DEFAULT_CAPACITY = 10;
    

    private static final Object[] DEFAULTCAPACITY_EMPTY_ELEMENTDATA = {};

    /**
     *默認(rèn)構(gòu)造函數(shù),使用初始容量10構(gòu)造一個(gè)空列表(無(wú)參數(shù)構(gòu)造)
     */
    public ArrayList() {
        this.elementData = DEFAULTCAPACITY_EMPTY_ELEMENTDATA;
    }
    
    /**
     * 帶初始容量參數(shù)的構(gòu)造函數(shù)。(用戶(hù)自己指定容量)
     */
    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;
        }
    }

以無(wú)參數(shù)構(gòu)造方法創(chuàng)建 ArrayList 時(shí),實(shí)際上初始化賦值的是一個(gè)空數(shù)組。當(dāng)真正對(duì)數(shù)組進(jìn)行添加元素操作時(shí),才真正分配容量。即向數(shù)組中添加第一個(gè)元素時(shí),數(shù)組容量擴(kuò)為10。

  1. add 方法
    /**
     * 將指定的元素追加到此列表的末尾。 
     */
    public boolean add(E e) {
   //添加元素之前,先調(diào)用ensureCapacityInternal方法
        ensureCapacityInternal(size + 1);  // Increments modCount!!
        //這里看到ArrayList添加元素的實(shí)質(zhì)就相當(dāng)于為數(shù)組賦值
        elementData[size++] = e;
        return true;
    }
  1. ensureCapacityInternal() 方法
   //得到最小擴(kuò)容量
    private void ensureCapacityInternal(int minCapacity) {
        if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA) {
              // 獲取默認(rèn)的容量和傳入?yún)?shù)的較大值
            minCapacity = Math.max(DEFAULT_CAPACITY, minCapacity);
        }

        ensureExplicitCapacity(minCapacity);
    }

當(dāng) 要 add 進(jìn)第1個(gè)元素時(shí),minCapacity為1,在Math.max()方法比較后,minCapacity 為10。

  1. ensureExplicitCapacity() 方法
  //判斷是否需要擴(kuò)容
    private void ensureExplicitCapacity(int minCapacity) {
        modCount++;

        // overflow-conscious code
        if (minCapacity - elementData.length > 0)
            //調(diào)用grow方法進(jìn)行擴(kuò)容,調(diào)用此方法代表已經(jīng)開(kāi)始擴(kuò)容了
            grow(minCapacity);
    }
  1. grow() 方法
/**
     * 要分配的最大數(shù)組大小
     */
    private static final int MAX_ARRAY_SIZE = Integer.MAX_VALUE - 8;

    /**
     * ArrayList擴(kuò)容的核心方法。
     */
    private void grow(int minCapacity) {
        // oldCapacity為舊容量,newCapacity為新容量
        int oldCapacity = elementData.length;
        //將oldCapacity 右移一位,其效果相當(dāng)于oldCapacity /2,
        //我們知道位運(yùn)算的速度遠(yuǎn)遠(yuǎn)快于整除運(yùn)算,整句運(yùn)算式的結(jié)果就是將新容量更新為舊容量的1.5倍,
        int newCapacity = oldCapacity + (oldCapacity >> 1);
        //然后檢查新容量是否大于最小需要容量,若還是小于最小需要容量,那么就把最小需要容量當(dāng)作數(shù)組的新容量,
        if (newCapacity - minCapacity < 0)
            newCapacity = minCapacity;
       // 如果新容量大于 MAX_ARRAY_SIZE,進(jìn)入(執(zhí)行) `hugeCapacity()` 方法來(lái)比較 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);
    }
  1. hugeCapacity() 方法。
    private static int hugeCapacity(int minCapacity) {
        if (minCapacity < 0) // overflow
            throw new OutOfMemoryError();
        //對(duì)minCapacity和MAX_ARRAY_SIZE進(jìn)行比較
        //若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 屬性針對(duì)數(shù)組,比如說(shuō)你聲明了一個(gè)數(shù)組,想知道這個(gè)數(shù)組的長(zhǎng)度則用到了 length 這個(gè)屬性.
  • java 中的 length() 方法針對(duì)字符串,如果想看這個(gè)字符串的長(zhǎng)度則用到 length() 這個(gè)方法.
  • java 中的 size()方法針對(duì)泛型集合,如果想看這個(gè)泛型有多少個(gè)元素,就調(diào)用此方法來(lái)查看.

ArrayList源碼中的ensureCapacity方法

最好在 add 大量元素之前用 ensureCapacity 方法,以減少增量重新分配的次數(shù)

  /**
    如有必要,增加此 ArrayList 實(shí)例的容量,以確保它至少可以容納由minimum capacity參數(shù)指定的元素?cái)?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ū)別

  1. 線(xiàn)程是否安全: HashMap 是非線(xiàn)程安全的,HashTable 是線(xiàn)程安全的;HashTable 內(nèi)部的方法基本都經(jīng)過(guò)synchronized 修飾(要保證線(xiàn)程安全的話(huà)就使用 ConcurrentHashMap);
  2. 效率:因?yàn)榫€(xiàn)程安全的問(wèn)題,HashMap 要比 HashTable 效率高一點(diǎn)。另外,HashTable 基本被淘汰,不要在代碼中使用它;
  3. 對(duì)Null key 和Null value的支持:HashMap 中,null 可以作為鍵,這樣的鍵只有一個(gè),可以有一個(gè)或多個(gè)鍵所對(duì)應(yīng)的值為 null。但是在 HashTable 中 put 進(jìn)的鍵值只要有一個(gè) null,直接拋出NullPointerException。
  4. 初始容量大小和每次擴(kuò)充容量大小的不同 : ①創(chuàng)建時(shí)如果不指定容量初始值,Hashtable 默認(rèn)的初始大小為11,之后每次擴(kuò)充,容量變?yōu)樵瓉?lái)的2n+1。HashMap 默認(rèn)的初始化大小為16。之后每次擴(kuò)充,容量變?yōu)樵瓉?lái)的2倍。②創(chuàng)建時(shí)如果給定了容量初始值,那么 Hashtable 會(huì)直接使用你給定的大小,而 HashMap 會(huì)將其擴(kuò)充為2的冪次方大?。℉ashMap 中的tableSizeFor()方法保證)。也就是說(shuō) HashMap 總是使用2的冪作為哈希表的大小。
  5. 底層數(shù)據(jù)結(jié)構(gòu): JDK1.8 以后的 HashMap 在解決哈希沖突時(shí)有了較大的變化,當(dāng)鏈表長(zhǎng)度大于閾值(默認(rèn)為8)時(shí),將鏈表轉(zhuǎn)化為紅黑樹(shù),以減少搜索時(shí)間。Hashtable 沒(méi)有這樣的機(jī)制。

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é)

  1. List
  • Arraylist: Object數(shù)組
  • Vector: Object數(shù)組
  • LinkedList: 雙向鏈表(JDK1.6之前為循環(huán)鏈表,JDK1.7取消了循環(huán))
  1. Set
  • HashSet(無(wú)序,唯一): 基于 HashMap 實(shí)現(xiàn)的,底層采用 HashMap 來(lái)保存元素
  • LinkedHashSet: LinkedHashSet 繼承與 HashSet,并且其內(nèi)部是通過(guò) LinkedHashMap 來(lái)實(shí)現(xiàn)的。
  • TreeSet(有序,唯一): 紅黑樹(shù)(自平衡的排序二叉樹(shù)。)
  1. Map
  • HashMap: JDK1.8之前HashMap由數(shù)組+鏈表組成的,數(shù)組是HashMap的主體,鏈表則是主要為了解決哈希沖突而存在的(“拉鏈法”解決沖突)。JDK1.8以后在解決哈希沖突時(shí)有了較大的變化,當(dāng)鏈表長(zhǎng)度大于閾值(默認(rèn)為8)時(shí),將鏈表轉(zhuǎn)化為紅黑樹(shù),以減少搜索時(shí)間
  • LinkedHashMap: LinkedHashMap 繼承自 HashMap,所以它的底層仍然是基于拉鏈?zhǔn)缴⒘薪Y(jié)構(gòu)即由數(shù)組和鏈表或紅黑樹(shù)組成。另外,LinkedHashMap 在上面結(jié)構(gòu)的基礎(chǔ)上,增加了一條雙向鏈表,使得上面的結(jié)構(gòu)可以保持鍵值對(duì)的插入順序。同時(shí)通過(guò)對(duì)鏈表進(jìn)行相應(yīng)的操作,實(shí)現(xiàn)了訪(fǎng)問(wèn)順序相關(guān)邏輯。詳細(xì)可以查看:《LinkedHashMap 源碼詳細(xì)分析(JDK1.8)》
  • Hashtable: 數(shù)組+鏈表組成的,數(shù)組是 HashMap 的主體,鏈表則是主要為了解決哈希沖突而存在的
  • TreeMap: 紅黑樹(shù)(自平衡的排序二叉樹(shù))

并發(fā)

synchronized 關(guān)鍵字

synchronized關(guān)鍵字解決的是多個(gè)線(xiàn)程之間訪(fǎng)問(wèn)資源的同步性,synchronized關(guān)鍵字可以保證被它修飾的方法或者代碼塊在任意時(shí)刻只能有一個(gè)線(xiàn)程執(zhí)行。
synchronized關(guān)鍵字最主要的三種使用方式:

  • 修飾實(shí)例方法: 作用于當(dāng)前對(duì)象實(shí)例加鎖,進(jìn)入同步代碼前要獲得當(dāng)前對(duì)象實(shí)例的鎖
  • 修飾靜態(tài)方法: :也就是給當(dāng)前類(lèi)加鎖,會(huì)作用于類(lèi)的所有對(duì)象實(shí)例。訪(fǎng)問(wèn)靜態(tài) synchronized 方法占用的鎖是當(dāng)前類(lèi)的鎖,而訪(fǎng)問(wèn)非靜態(tài) synchronized 方法占用的鎖是當(dāng)前實(shí)例對(duì)象鎖。
  • 修飾代碼塊: 指定加鎖對(duì)象,對(duì)給定對(duì)象加鎖,進(jìn)入同步代碼庫(kù)前要獲得給定對(duì)象的鎖。

總結(jié): synchronized 關(guān)鍵字加到 static 靜態(tài)方法和 synchronized(class)代碼塊上都是是給 Class 類(lèi)上鎖。synchronized 關(guān)鍵字加到實(shí)例方法上是給對(duì)象實(shí)例上鎖。盡量不要使用 synchronized(String a) 因?yàn)镴VM中,字符串常量池具有緩存功能!

雙重校驗(yàn)鎖實(shí)現(xiàn)對(duì)象單例(線(xiàn)程安全)

public class Singleton {

    private volatile static Singleton instance;

    private Singleton() {
    }

    public static Singleton getInstance() {
       //先判斷對(duì)象是否已經(jīng)實(shí)例過(guò),沒(méi)有實(shí)例化過(guò)才進(jìn)入加鎖代碼
        if (instance== null) {
            //類(lèi)對(duì)象加鎖
            synchronized (Singleton.class) {
                if (instance== null) {
                    instance= new Singleton();
                }
            }
        }
        return instance;
    }
}

instance采用 volatile 關(guān)鍵字修飾也是很有必要的, instance= new Singleton(); 這段代碼其實(shí)是分為三步執(zhí)行:

  1. 為 instance分配內(nèi)存空間
  2. 初始化 instance
  3. 將 instance指向分配的內(nèi)存地址
    但是由于 JVM 具有指令重排的特性,執(zhí)行順序有可能變成 1->3->2。指令重排在單線(xiàn)程環(huán)境下不會(huì)出先問(wèn)題,但是在多線(xiàn)程環(huán)境下會(huì)導(dǎo)致一個(gè)線(xiàn)程獲得還沒(méi)有初始化的實(shí)例。例如,線(xiàn)程 T1 執(zhí)行了 1 和 3,此時(shí) T2 調(diào)用 getInstance() 后發(fā)現(xiàn) instance不為空,因此返回 instance,但此時(shí) instance還未被初始化。

使用 volatile 可以禁止 JVM 的指令重排,保證在多線(xiàn)程環(huán)境下也能正常運(yùn)行。

synchronized 關(guān)鍵字和 volatile 關(guān)鍵字的區(qū)別

  • volatile關(guān)鍵字是線(xiàn)程同步的輕量級(jí)實(shí)現(xiàn),所以volatile性能肯定比synchronized關(guān)鍵字要好。但是volatile關(guān)鍵字只能用于變量而synchronized關(guān)鍵字可以修飾方法以及代碼塊。synchronized關(guān)鍵字在JavaSE1.6之后進(jìn)行了主要包括為了減少獲得鎖和釋放鎖帶來(lái)的性能消耗而引入的偏向鎖和輕量級(jí)鎖以及其它各種優(yōu)化之后執(zhí)行效率有了顯著提升,實(shí)際開(kāi)發(fā)中使用 synchronized 關(guān)鍵字的場(chǎng)景還是更多一些。
  • 多線(xiàn)程訪(fǎng)問(wèn)volatile關(guān)鍵字不會(huì)發(fā)生阻塞,而synchronized關(guān)鍵字可能會(huì)發(fā)生阻塞
  • volatile關(guān)鍵字能保證數(shù)據(jù)的可見(jiàn)性,但不能保證數(shù)據(jù)的原子性。synchronized關(guān)鍵字兩者都能保證。
  • volatile關(guān)鍵字主要用于解決變量在多個(gè)線(xiàn)程之間的可見(jiàn)性,而 synchronized關(guān)鍵字解決的是多個(gè)線(xiàn)程之間訪(fǎng)問(wèn)資源的同步性。

ThreadLocal

通常情況下,我們創(chuàng)建的變量是可以被任何一個(gè)線(xiàn)程訪(fǎng)問(wèn)并修改的。如果想實(shí)現(xiàn)每一個(gè)線(xiàn)程都有自己的專(zhuān)屬本地變量該如何解決呢? JDK中提供的ThreadLocal類(lèi)正是為了解決這樣的問(wèn)題。 ThreadLocal類(lèi)主要解決的就是讓每個(gè)線(xiàn)程綁定自己的值,可以將ThreadLocal類(lèi)形象的比喻成存放數(shù)據(jù)的盒子,盒子中可以存儲(chǔ)每個(gè)線(xiàn)程的私有數(shù)據(jù)。

如果你創(chuàng)建了一個(gè)ThreadLocal變量,那么訪(fǎng)問(wèn)這個(gè)變量的每個(gè)線(xiàn)程都會(huì)有這個(gè)變量的本地副本,這也是ThreadLocal變量名的由來(lái)。他們可以使用 get()set() 方法來(lái)獲取默認(rèn)值或?qū)⑵渲蹈臑楫?dāng)前線(xiàn)程所存的副本的值,從而避免了線(xiàn)程安全問(wèn)題。

ThreadLocal原理

Thread 類(lèi)中有一個(gè) threadLocals 和 一個(gè) inheritableThreadLocals 變量,它們都是 ThreadLocalMap 類(lèi)型的變量,我們可以把 ThreadLocalMap 理解為ThreadLocal 類(lèi)實(shí)現(xiàn)的定制化的 HashMap。默認(rèn)情況下這兩個(gè)變量都是null,只有當(dāng)前線(xiàn)程調(diào)用 ThreadLocal 類(lèi)的 setget方法時(shí)才創(chuàng)建它們,實(shí)際上調(diào)用這兩個(gè)方法的時(shí)候,我們調(diào)用的是ThreadLocalMap類(lèi)對(duì)應(yīng)的 get()、set()方法。

ThreadLocal類(lèi)的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;
    }

最終的變量是放在了當(dāng)前線(xiàn)程的 ThreadLocalMap 中,并不是存在 ThreadLocal 上,ThreadLocal 可以理解為只是ThreadLocalMap的封裝,傳遞了變量值。 ThrealLocal 類(lèi)中可以通過(guò)Thread.currentThread()獲取到當(dāng)前線(xiàn)程對(duì)象后,直接通過(guò)getMap(Thread t)可以訪(fǎng)問(wèn)到該線(xiàn)程的ThreadLocalMap對(duì)象。

每個(gè)Thread中都具備一個(gè)ThreadLocalMap,而ThreadLocalMap可以存儲(chǔ)以ThreadLocal為key的鍵值對(duì)。 比如我們?cè)谕粋€(gè)線(xiàn)程中聲明了兩個(gè) ThreadLocal 對(duì)象的話(huà),會(huì)使用 Thread內(nèi)部都是使用僅有那個(gè)ThreadLocalMap 存放數(shù)據(jù)的,ThreadLocalMap的 key 就是 ThreadLocal對(duì)象,value 就是 ThreadLocal 對(duì)象調(diào)用set方法設(shè)置的值。ThreadLocal 是 map結(jié)構(gòu)是為了讓每個(gè)線(xiàn)程可以關(guān)聯(lián)多個(gè) ThreadLocal變量。這也就解釋了 ThreadLocal 聲明的變量為什么在每一個(gè)線(xiàn)程都有自己的專(zhuān)屬本地變量。

ThreadLocalMapThreadLocal的靜態(tài)內(nèi)部類(lèi)。

ThreadLocal 內(nèi)存泄露問(wèn)題

ThreadLocalMap 中使用的 key 為 ThreadLocal 的弱引用,而 value 是強(qiáng)引用。所以,如果 ThreadLocal 沒(méi)有被外部強(qiáng)引用的情況下,在垃圾回收的時(shí)候會(huì) key 會(huì)被清理掉,而 value 不會(huì)被清理掉。這樣一來(lái),ThreadLocalMap 中就會(huì)出現(xiàn)key為null的Entry。假如我們不做任何措施的話(huà),value 永遠(yuǎn)無(wú)法被GC 回收,這個(gè)時(shí)候就可能會(huì)產(chǎn)生內(nèi)存泄露。ThreadLocalMap實(shí)現(xiàn)中已經(jīng)考慮了這種情況,在調(diào)用 set()get()、remove() 方法的時(shí)候,會(huì)清理掉 key 為 null 的記錄。使用完 ThreadLocal方法后 最好手動(dòng)調(diào)用remove()方法

線(xiàn)程池

線(xiàn)程池提供了一種限制和管理資源(包括執(zhí)行一個(gè)任務(wù))。 每個(gè)線(xiàn)程池還維護(hù)一些基本統(tǒng)計(jì)信息,例如已完成任務(wù)的數(shù)量。

使用線(xiàn)程池的好處:

  • 降低資源消耗。 通過(guò)重復(fù)利用已創(chuàng)建的線(xiàn)程降低線(xiàn)程創(chuàng)建和銷(xiāo)毀造成的消耗。
  • 提高響應(yīng)速度。 當(dāng)任務(wù)到達(dá)時(shí),任務(wù)可以不需要的等到線(xiàn)程創(chuàng)建就能立即執(zhí)行。
  • 提高線(xiàn)程的可管理性。 線(xiàn)程是稀缺資源,如果無(wú)限制的創(chuàng)建,不僅會(huì)消耗系統(tǒng)資源,還會(huì)降低系統(tǒng)的穩(wěn)定性,使用線(xiàn)程池可以進(jìn)行統(tǒng)一的分配,調(diào)優(yōu)和監(jiān)控。

實(shí)現(xiàn)Runnable接口和Callable接口的區(qū)別

如果想讓線(xiàn)程池執(zhí)行任務(wù)的話(huà)需要實(shí)現(xiàn)的Runnable接口或Callable接口。 Runnable接口或Callable接口實(shí)現(xiàn)類(lèi)都可以被ThreadPoolExecutor或ScheduledThreadPoolExecutor執(zhí)行。兩者的區(qū)別在于 Runnable 接口不會(huì)返回結(jié)果但是 Callable 接口可以返回結(jié)果。

備注: 工具類(lèi)Executors可以實(shí)現(xiàn)Runnable對(duì)象和Callable對(duì)象之間的相互轉(zhuǎn)換。(Executors.callable(Runnable task)Executors.callable(Runnable task,Object resule))。

執(zhí)行execute()方法和submit()方法的區(qū)別

1)execute() 方法用于提交不需要返回值的任務(wù),所以無(wú)法判斷任務(wù)是否被線(xiàn)程池執(zhí)行成功與否;

2)submit() 方法用于提交需要返回值的任務(wù)。線(xiàn)程池會(huì)返回一個(gè)Future類(lèi)型的對(duì)象,通過(guò)這個(gè)Future對(duì)象可以判斷任務(wù)是否執(zhí)行成功,并且可以通過(guò)future的get()方法來(lái)獲取返回值,get()方法會(huì)阻塞當(dāng)前線(xiàn)程直到任務(wù)完成,而使用 get(long timeout,TimeUnit unit)方法則會(huì)阻塞當(dāng)前線(xiàn)程一段時(shí)間后立即返回,這時(shí)候有可能任務(wù)沒(méi)有執(zhí)行完。

如何創(chuàng)建線(xiàn)程池

《阿里巴巴Java開(kāi)發(fā)手冊(cè)》中強(qiáng)制線(xiàn)程池不允許使用 Executors 去創(chuàng)建,而是通過(guò) ThreadPoolExecutor 的方式,這樣的處理方式能更加明確線(xiàn)程池的運(yùn)行規(guī)則,規(guī)避資源耗盡的風(fēng)險(xiǎn)

Executors 返回線(xiàn)程池對(duì)象的弊端如下:

  • FixedThreadPool 和 SingleThreadExecutor : 允許請(qǐng)求的隊(duì)列長(zhǎng)度為 Integer.MAX_VALUE ,可能堆積大量的請(qǐng)求,從而導(dǎo)致OOM。
  • CachedThreadPool 和 ScheduledThreadPool : 允許創(chuàng)建的線(xiàn)程數(shù)量為 Integer.MAX_VALUE ,可能會(huì)創(chuàng)建大量線(xiàn)程,從而導(dǎo)致OOM。

方式一:通過(guò)構(gòu)造方法實(shí)現(xiàn)

  • 通過(guò)構(gòu)造方法實(shí)現(xiàn).png

方式二:通過(guò)Executor 框架的工具類(lèi)Executors來(lái)實(shí)現(xiàn) 我們可以創(chuàng)建三種類(lèi)型的ThreadPoolExecutor:

  • FixedThreadPool : 該方法返回一個(gè)固定線(xiàn)程數(shù)量的線(xiàn)程池。該線(xiàn)程池中的線(xiàn)程數(shù)量始終不變。當(dāng)有一個(gè)新的任務(wù)提交時(shí),線(xiàn)程池中若有空閑線(xiàn)程,則立即執(zhí)行。若沒(méi)有,則新的任務(wù)會(huì)被暫存在一個(gè)任務(wù)隊(duì)列中,待有線(xiàn)程空閑時(shí),便處理在任務(wù)隊(duì)列中的任務(wù)。
  • SingleThreadExecutor: 方法返回一個(gè)只有一個(gè)線(xiàn)程的線(xiàn)程池。若多余一個(gè)任務(wù)被提交到該線(xiàn)程池,任務(wù)會(huì)被保存在一個(gè)任務(wù)隊(duì)列中,待線(xiàn)程空閑,按先入先出的順序執(zhí)行隊(duì)列中的任務(wù)。
  • CachedThreadPool: 該方法返回一個(gè)可根據(jù)實(shí)際情況調(diào)整線(xiàn)程數(shù)量的線(xiàn)程池。線(xiàn)程池的線(xiàn)程數(shù)量不確定,但若有空閑線(xiàn)程可以復(fù)用,則會(huì)優(yōu)先使用可復(fù)用的線(xiàn)程。若所有線(xiàn)程均在工作,又有新的任務(wù)提交,則會(huì)創(chuàng)建新的線(xiàn)程處理任務(wù)。所有線(xiàn)程在當(dāng)前任務(wù)執(zhí)行完畢后,將返回線(xiàn)程池進(jìn)行復(fù)用。

對(duì)應(yīng)Executors工具類(lèi)中的方法如圖所示:

進(jìn)程和線(xiàn)程

進(jìn)程
進(jìn)程是程序的一次執(zhí)行過(guò)程,是系統(tǒng)運(yùn)行程序的基本單位,因此進(jìn)程是動(dòng)態(tài)的。系統(tǒng)運(yùn)行一個(gè)程序即是一個(gè)進(jìn)程從創(chuàng)建,運(yùn)行到消亡的過(guò)程。

在 Java 中,當(dāng)我們啟動(dòng) main 函數(shù)時(shí)其實(shí)就是啟動(dòng)了一個(gè) JVM 的進(jìn)程,而 main 函數(shù)所在的線(xiàn)程就是這個(gè)進(jìn)程中的一個(gè)線(xiàn)程,也稱(chēng)主線(xiàn)程。

線(xiàn)程

線(xiàn)程與進(jìn)程相似,但線(xiàn)程是一個(gè)比進(jìn)程更小的執(zhí)行單位。一個(gè)進(jìn)程在其執(zhí)行的過(guò)程中可以產(chǎn)生多個(gè)線(xiàn)程。與進(jìn)程不同的是同類(lèi)的多個(gè)線(xiàn)程共享進(jìn)程的方法區(qū)資源,但每個(gè)線(xiàn)程有自己的程序計(jì)數(shù)器、虛擬機(jī)棧本地方法棧,所以系統(tǒng)在產(chǎn)生一個(gè)線(xiàn)程,或是在各個(gè)線(xiàn)程之間作切換工作時(shí),負(fù)擔(dān)要比進(jìn)程小得多,也正因?yàn)槿绱?,線(xiàn)程也被稱(chēng)為輕量級(jí)進(jìn)程。

線(xiàn)程與進(jìn)程的關(guān)系,區(qū)別及優(yōu)缺點(diǎn)

從 JVM 角度說(shuō)進(jìn)程和線(xiàn)程之間的關(guān)系
一個(gè)進(jìn)程中可以有多個(gè)線(xiàn)程,多個(gè)線(xiàn)程共享進(jìn)程的方法區(qū) (JDK1.8 之后的元空間)資源,但是每個(gè)線(xiàn)程有自己的程序計(jì)數(shù)器、虛擬機(jī)棧本地方法棧

總結(jié): 線(xiàn)程是進(jìn)程劃分成的更小的運(yùn)行單位。線(xiàn)程和進(jìn)程最大的不同在于基本上各進(jìn)程是獨(dú)立的,而各線(xiàn)程則不一定,因?yàn)橥贿M(jìn)程中的線(xiàn)程極有可能會(huì)相互影響。線(xiàn)程執(zhí)行開(kāi)銷(xiāo)小,但不利于資源的管理和保護(hù);而進(jìn)程正相反
程序計(jì)數(shù)器虛擬機(jī)棧本地方法棧是線(xiàn)程私有的,堆和方法區(qū)是線(xiàn)程共享的

程序計(jì)數(shù)器為什么是私有的?

程序計(jì)數(shù)器主要有下面兩個(gè)作用:

  1. 字節(jié)碼解釋器通過(guò)改變程序計(jì)數(shù)器來(lái)依次讀取指令,從而實(shí)現(xiàn)代碼的流程控制,如:順序執(zhí)行、選擇、循環(huán)、異常處理。
  2. 在多線(xiàn)程的情況下,程序計(jì)數(shù)器用于記錄當(dāng)前線(xiàn)程執(zhí)行的位置,從而當(dāng)線(xiàn)程被切換回來(lái)的時(shí)候能夠知道該線(xiàn)程上次運(yùn)行到哪兒了。

需要注意的是,如果執(zhí)行的是 native 方法,那么程序計(jì)數(shù)器記錄的是 undefined 地址,只有執(zhí)行的是 Java 代碼時(shí)程序計(jì)數(shù)器記錄的才是下一條指令的地址。

所以,程序計(jì)數(shù)器私有主要是為了線(xiàn)程切換后能恢復(fù)到正確的執(zhí)行位置。

虛擬機(jī)棧和本地方法棧為什么是私有的?

  • 虛擬機(jī)棧: 每個(gè) Java 方法在執(zhí)行的同時(shí)會(huì)創(chuàng)建一個(gè)棧幀用于存儲(chǔ)局部變量表、操作數(shù)棧、常量池引用等信息。從方法調(diào)用直至執(zhí)行完成的過(guò)程,就對(duì)應(yīng)著一個(gè)棧幀在 Java 虛擬機(jī)棧中入棧和出棧的過(guò)程。
  • 本地方法棧: 和虛擬機(jī)棧所發(fā)揮的作用非常相似,區(qū)別是: 虛擬機(jī)棧為虛擬機(jī)執(zhí)行 Java 方法 (也就是字節(jié)碼)服務(wù),而本地方法棧則為虛擬機(jī)使用到的 Native 方法服務(wù)。 在 HotSpot 虛擬機(jī)中和 Java 虛擬機(jī)棧合二為一。

所以,為了保證線(xiàn)程中的局部變量不被別的線(xiàn)程訪(fǎng)問(wèn)到,虛擬機(jī)棧和本地方法棧是線(xiàn)程私有的。

堆和方法區(qū)

堆和方法區(qū)是所有線(xiàn)程共享的資源,其中堆是進(jìn)程中最大的一塊內(nèi)存,主要用于存放新創(chuàng)建的對(duì)象 (所有對(duì)象都在這里分配內(nèi)存),方法區(qū)主要用于存放已被加載的類(lèi)信息、常量、靜態(tài)變量、即時(shí)編譯器編譯后的代碼等數(shù)據(jù)。

并發(fā)與并行的區(qū)別

  • 并發(fā): 同一時(shí)間段,多個(gè)任務(wù)都在執(zhí)行 (單位時(shí)間內(nèi)不一定同時(shí)執(zhí)行);
  • 并行: 單位時(shí)間內(nèi),多個(gè)任務(wù)同時(shí)執(zhí)行。

為什么要使用多線(xiàn)程

先從總體上來(lái)說(shuō):

  • 從計(jì)算機(jī)底層來(lái)說(shuō): 線(xiàn)程可以比作是輕量級(jí)的進(jìn)程,是程序執(zhí)行的最小單位,線(xiàn)程間的切換和調(diào)度的成本遠(yuǎn)遠(yuǎn)小于進(jìn)程。另外,多核 CPU 時(shí)代意味著多個(gè)線(xiàn)程可以同時(shí)運(yùn)行,這減少了線(xiàn)程上下文切換的開(kāi)銷(xiāo)。
  • 從當(dāng)代互聯(lián)網(wǎng)發(fā)展趨勢(shì)來(lái)說(shuō): 現(xiàn)在的系統(tǒng)動(dòng)不動(dòng)就要求百萬(wàn)級(jí)甚至千萬(wàn)級(jí)的并發(fā)量,而多線(xiàn)程并發(fā)編程正是開(kāi)發(fā)高并發(fā)系統(tǒng)的基礎(chǔ),利用好多線(xiàn)程機(jī)制可以大大提高系統(tǒng)整體的并發(fā)能力以及性能。

使用多線(xiàn)程可能帶來(lái)什么問(wèn)題?

并發(fā)編程的目的就是為了能提高程序的執(zhí)行效率提高程序運(yùn)行速度,但是并發(fā)編程并不總是能提高程序運(yùn)行速度的,而且并發(fā)編程可能會(huì)遇到很多問(wèn)題,比如:內(nèi)存泄漏、上下文切換、死鎖還有受限于硬件和軟件的資源閑置問(wèn)題。

線(xiàn)程的生命周期和狀態(tài)

Java 線(xiàn)程在運(yùn)行的生命周期中的指定時(shí)刻只可能處于下面 6 種不同狀態(tài)的其中一個(gè)狀態(tài)

Java 線(xiàn)程的狀態(tài)

線(xiàn)程在生命周期中并不是固定處于某一個(gè)狀態(tài)而是隨著代碼的執(zhí)行在不同狀態(tài)之間切換。


Java 線(xiàn)程狀態(tài)變遷

由上圖可以看出:線(xiàn)程創(chuàng)建之后它將處于 NEW(新建) 狀態(tài),調(diào)用 start() 方法后開(kāi)始運(yùn)行,線(xiàn)程這時(shí)候處于 READY(可運(yùn)行) 狀態(tài)??蛇\(yùn)行狀態(tài)的線(xiàn)程獲得了 CPU 時(shí)間片(timeslice)后就處于 RUNNING(運(yùn)行) 狀態(tài)。

操作系統(tǒng)隱藏 Java 虛擬機(jī)(JVM)中的 RUNNABLE 和 RUNNING 狀態(tài),它只能看到 RUNNABLE 狀態(tài)所以 Java 系統(tǒng)一般將這兩個(gè)狀態(tài)統(tǒng)稱(chēng)為 RUNNABLE(運(yùn)行中) 狀態(tài) 。

當(dāng)線(xiàn)程執(zhí)行 wait()方法之后,線(xiàn)程進(jìn)入 WAITING(等待) 狀態(tài)。進(jìn)入等待狀態(tài)的線(xiàn)程需要依靠其他線(xiàn)程的通知才能夠返回到運(yùn)行狀態(tài),而 TIME_WAITING(超時(shí)等待) 狀態(tài)相當(dāng)于在等待狀態(tài)的基礎(chǔ)上增加了超時(shí)限制,比如通過(guò) sleep(long millis)方法或 wait(long millis)方法可以將 Java 線(xiàn)程置于 TIMED WAITING 狀態(tài)。當(dāng)超時(shí)時(shí)間到達(dá)后 Java 線(xiàn)程將會(huì)返回到 RUNNABLE 狀態(tài)。當(dāng)線(xiàn)程調(diào)用同步方法時(shí),在沒(méi)有獲取到鎖的情況下,線(xiàn)程將會(huì)進(jìn)入到 BLOCKED(阻塞) 狀態(tài)。線(xiàn)程在執(zhí)行 Runnable 的run()方法之后將會(huì)進(jìn)入到 TERMINATED(終止) 狀態(tài)。

什么是上下文切換

多線(xiàn)程編程中一般線(xiàn)程的個(gè)數(shù)都大于 CPU 核心的個(gè)數(shù),而一個(gè) CPU 核心在任意時(shí)刻只能被一個(gè)線(xiàn)程使用,為了讓這些線(xiàn)程都能得到有效執(zhí)行,CPU 采取的策略是為每個(gè)線(xiàn)程分配時(shí)間片并輪轉(zhuǎn)的形式。當(dāng)一個(gè)線(xiàn)程的時(shí)間片用完的時(shí)候就會(huì)重新處于就緒狀態(tài)讓給其他線(xiàn)程使用,這個(gè)過(guò)程就屬于一次上下文切換。

概括來(lái)說(shuō)就是:當(dāng)前任務(wù)在執(zhí)行完 CPU 時(shí)間片切換到另一個(gè)任務(wù)之前會(huì)先保存自己的狀態(tài),以便下次再切換會(huì)這個(gè)任務(wù)時(shí),可以再加載這個(gè)任務(wù)的狀態(tài)。任務(wù)從保存到再加載的過(guò)程就是一次上下文切換。

什么是線(xiàn)程死鎖?如何避免死鎖?

認(rèn)識(shí)線(xiàn)程死鎖

多個(gè)線(xiàn)程同時(shí)被阻塞,它們中的一個(gè)或者全部都在等待某個(gè)資源被釋放。由于線(xiàn)程被無(wú)限期地阻塞,因此程序不可能正常終止。

如下圖所示,線(xiàn)程 A 持有資源 2,線(xiàn)程 B 持有資源 1,他們同時(shí)都想申請(qǐng)對(duì)方的資源,所以這兩個(gè)線(xiàn)程就會(huì)互相等待而進(jìn)入死鎖狀態(tài)。

線(xiàn)程死鎖示意圖

產(chǎn)生死鎖必須具備以下四個(gè)條件:

  1. 互斥條件:該資源任意一個(gè)時(shí)刻只由一個(gè)線(xiàn)程占用。
  2. 請(qǐng)求與保持條件:一個(gè)進(jìn)程因請(qǐng)求資源而阻塞時(shí),對(duì)已獲得的資源保持不放。
  3. 不剝奪條件:線(xiàn)程已獲得的資源在末使用完之前不能被其他線(xiàn)程強(qiáng)行剝奪,只有自己使用完畢后才釋放資源。
  4. 循環(huán)等待條件:若干進(jìn)程之間形成一種頭尾相接的循環(huán)等待資源關(guān)系。

如何避免線(xiàn)程死鎖

只要破壞產(chǎn)生死鎖的四個(gè)條件中的其中一個(gè)就可以了。

破壞互斥條件

這個(gè)條件我們沒(méi)有辦法破壞,因?yàn)槲覀冇面i本來(lái)就是想讓他們互斥的(臨界資源需要互斥訪(fǎng)問(wèn))。

破壞請(qǐng)求與保持條件

一次性申請(qǐng)所有的資源。

破壞不剝奪條件

占用部分資源的線(xiàn)程進(jìn)一步申請(qǐng)其他資源時(shí),如果申請(qǐng)不到,可以主動(dòng)釋放它占有的資源。

破壞循環(huán)等待條件

靠按序申請(qǐng)資源來(lái)預(yù)防。按某一順序申請(qǐng)資源,釋放資源則反序釋放。破壞循環(huán)等待條件。

sleep() 方法和 wait() 方法區(qū)別和共同點(diǎn)

  • 兩者最主要的區(qū)別在于:sleep 方法沒(méi)有釋放鎖,而 wait 方法釋放了鎖 。
  • 兩者都可以暫停線(xiàn)程的執(zhí)行。
  • Wait 通常被用于線(xiàn)程間交互/通信,sleep 通常被用于暫停執(zhí)行。
  • wait() 方法被調(diào)用后,線(xiàn)程不會(huì)自動(dòng)蘇醒,需要?jiǎng)e的線(xiàn)程調(diào)用同一個(gè)對(duì)象上的 notify() 或者 notifyAll() 方法。sleep() 方法執(zhí)行完成后,線(xiàn)程會(huì)自動(dòng)蘇醒?;蛘呖梢允褂脀ait(long timeout)超時(shí)后線(xiàn)程會(huì)自動(dòng)蘇醒。

我們調(diào)用 start() 方法時(shí)會(huì)執(zhí)行 run() 方法,為什么我們不能直接調(diào)用 run() 方法?

new 一個(gè) Thread,線(xiàn)程進(jìn)入了新建狀態(tài);調(diào)用 start() 方法,會(huì)啟動(dòng)一個(gè)線(xiàn)程并使線(xiàn)程進(jìn)入了就緒狀態(tài),當(dāng)分配到時(shí)間片后就可以開(kāi)始運(yùn)行了。 start() 會(huì)執(zhí)行線(xiàn)程的相應(yīng)準(zhǔn)備工作,然后自動(dòng)執(zhí)行 run() 方法的內(nèi)容,這是真正的多線(xiàn)程工作。 而直接執(zhí)行 run() 方法,會(huì)把 run 方法當(dāng)成一個(gè) main 線(xiàn)程下的普通方法去執(zhí)行,并不會(huì)在某個(gè)線(xiàn)程中執(zhí)行它,所以這并不是多線(xiàn)程工作。

總結(jié): 調(diào)用 start 方法方可啟動(dòng)線(xiàn)程并使線(xiàn)程進(jìn)入就緒狀態(tài),而 run 方法只是 thread 的一個(gè)普通方法調(diào)用,還是在主線(xiàn)程里執(zhí)行。

final,static,this,super 關(guān)鍵字總結(jié)

final 關(guān)鍵字

final關(guān)鍵字主要用在三個(gè)地方:變量、方法、類(lèi)。

  1. 對(duì)于一個(gè)final變量,如果是基本數(shù)據(jù)類(lèi)型的變量,則其數(shù)值一旦在初始化之后便不能更改;如果是引用類(lèi)型的變量,則在對(duì)其初始化之后便不能再讓其指向另一個(gè)對(duì)象。
  2. 當(dāng)用final修飾一個(gè)類(lèi)時(shí),表明這個(gè)類(lèi)不能被繼承。final類(lèi)中的所有成員方法都會(huì)被隱式地指定為final方法。
  3. 使用final方法的原因:把方法鎖定,以防任何繼承類(lèi)修改它的含義

static 關(guān)鍵字

static 關(guān)鍵字主要有以下四種使用場(chǎng)景:

  1. 修飾成員變量和成員方法: 被 static 修飾的成員屬于類(lèi),不屬于單個(gè)這個(gè)類(lèi)的某個(gè)對(duì)象,被類(lèi)中所有對(duì)象共享,靜態(tài)變量 存放在 Java 內(nèi)存區(qū)域的方法區(qū)。靜態(tài)方法不能調(diào)用非靜態(tài)方法和非靜態(tài)成員變量。靜態(tài)變量 存放在 Java 內(nèi)存區(qū)域的方法區(qū)。
  2. 靜態(tài)代碼塊:代碼執(zhí)行順序(靜態(tài)代碼塊 —>非靜態(tài)代碼 —>構(gòu)造方法) 該類(lèi)不管創(chuàng)建多少對(duì)象,靜態(tài)代碼塊只執(zhí)行一次.
  3. 靜態(tài)內(nèi)部類(lèi)(只能修飾內(nèi)部類(lèi)):它的創(chuàng)建是不需要依賴(lài)外圍類(lèi)的創(chuàng)建。不能使用任何外圍類(lèi)的非static成員變量和方法。
  4. 靜態(tài)導(dǎo)包

this 關(guān)鍵字

this關(guān)鍵字用于引用類(lèi)的當(dāng)前實(shí)例,代表對(duì)本類(lèi)對(duì)象的引用,指向本類(lèi)對(duì)象

super 關(guān)鍵字

super關(guān)鍵字用于從子類(lèi)訪(fǎng)問(wèn)父類(lèi)的變量和方法, 代表對(duì)父類(lèi)對(duì)象的引用,指向父類(lèi)對(duì)象

使用 this 和 super 要注意的問(wèn)題:

  • 在構(gòu)造器中使用 super() 調(diào)用父類(lèi)中的其他構(gòu)造方法時(shí),該語(yǔ)句必須處于構(gòu)造器的首行,否則編譯器會(huì)報(bào)錯(cuò)。另外,this 調(diào)用本類(lèi)中的其他構(gòu)造方法時(shí),也要放在首行。
  • this、super不能用在static方法中。
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書(shū)系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

  • 九種基本數(shù)據(jù)類(lèi)型的大小,以及他們的封裝類(lèi)。(1)九種基本數(shù)據(jù)類(lèi)型和封裝類(lèi) (2)自動(dòng)裝箱和自動(dòng)拆箱 什么是自動(dòng)裝箱...
    關(guān)瑋琳l(shuí)inSir閱讀 2,077評(píng)論 0 47
  • 一、基礎(chǔ)知識(shí):1、JVM、JRE和JDK的區(qū)別:JVM(Java Virtual Machine):java虛擬機(jī)...
    殺小賊閱讀 2,576評(píng)論 0 4
  • Java SE 基礎(chǔ): 封裝、繼承、多態(tài) 封裝: 概念:就是把對(duì)象的屬性和操作(或服務(wù))結(jié)合為一個(gè)獨(dú)立的整體,并盡...
    Jayden_Cao閱讀 2,259評(píng)論 0 8
  • Java-Review-Note——4.多線(xiàn)程 標(biāo)簽: JavaStudy PS:本來(lái)是分開(kāi)三篇的,后來(lái)想想還是整...
    coder_pig閱讀 1,780評(píng)論 2 17
  • 整理來(lái)自互聯(lián)網(wǎng) 1,JDK:Java Development Kit,java的開(kāi)發(fā)和運(yùn)行環(huán)境,java的開(kāi)發(fā)工具...
    Ncompass閱讀 1,627評(píng)論 0 6

友情鏈接更多精彩內(nèi)容