從底層數據結構以及訪問、添加、刪除的效率分別說明
注意: ArrayList當數組大小不知足時須要增長存儲能力,就要將已經有數組的數據複製到新的存儲空間java
Vector也是經過數組實現的,但它支持線程同步,避免同時寫引發的不一致性,實現同步須要很高的開銷,所以它的訪問比ArrayList慢算法
jdk1.7中的HashMap
HashMap是一個數組,數組的每個元素是一個Entry的鏈表
每個Entry對象包括了Key,value,hash值和指向下一個元素的next數組
HashMap包括兩個構造參數:
1.capacity
:當前數組的容量,可擴容,擴容後的大小爲當前數組大小的兩倍
2.loadFactor
:負載因子,默認0.75緩存
HashMap(int initialCapacity, float loadFactor)
安全
threshold
:擴容的閾值,等於capacity * loadFactor
數據結構
JDK1.8中,HashMap採用位桶+鏈表+紅黑樹實現,當鏈表長度超過閾值(8)時,將鏈表轉換爲紅黑樹,在這些位置進行查找的時候能夠下降時間複雜度爲 O(logN)。多線程
(1). HashMap 容許有一個null的key,容許多個value爲null
(2). HashMap 是線程不安全的,可經過Collections的synchronizedMap 方法使HashMap 具備線程安全的能力,或者使用ConcurrentHashMap
構造參數:concurrencyLevel
:並行數,也即segment的個數,默認16,初始化後不可更改或擴容;一個segment同時只容許一個線程操做, 16個segment容許16個線程在各類不一樣的segment上併發寫;併發
HashTable是上古版本的遺留類,如今不多使用,app
HashSet 底層是由HashMap實現, 因此也是非線程安全,
其實HashSet也是<K,V> 只不過對每一個元素都是同一個object對象做爲Vjvm
它繼承HashMap、底層使用哈希表與雙向鏈表來保存全部元素
Entry元素比HashMap多了:Entry<K, V> before
Entry<K, V> after
before、After是用於維護Entry插入的前後順序的。
大概的圖:
1.減小了建立和銷燬線程的次數,每一個工做線程均可以被重複利用,可執行多個任務。
2.能夠根據系統的承受能力,調整線程池中工做線線程的數目,防止消耗過多的內存
建立一個線程池,它可安排在給定延遲後運行命令或者按期地執行。
ScheduledExecutorService scheduledThreadPool= Executors.newScheduledThreadPool(3); scheduledThreadPool.schedule(newRunnable(){ @Override public void run() { System.out.println("延遲三秒"); }}, 3, TimeUnit.SECONDS); scheduledThreadPool.scheduleAtFixedRate(newRunnable(){ @Override public void run() { System.out.println("延遲1 秒後每三秒執行一次"); }},1,3,TimeUnit.SECONDS);
在線程的生命週期中,它要通過新建(New)、就緒(Runnable)、運行(Running)、阻塞(Blocked)和死亡(Dead)5 種狀態
當程序使用new 關鍵字建立了一個線程以後,該線程就處於新建狀態,此時僅由JVM 爲其分配內存,並初始化其成員變量的值
當線程對象調用了start()方法以後,該線程處於就緒狀態。Java 虛擬機會爲其建立方法調用棧和程序計數器,等待調度運行。
若是處於就緒狀態的線程得到了CPU,開始執行run()方法的線程執行體,則該線程處於運行狀態。
阻塞狀態是指線程由於某種緣由放棄了cpu 使用權,暫時中止運行
阻塞的狀況分三種:
運行(running)的線程執行o.wait()方法,JVM 會把該線程放入等待隊列(waitting queue)
中。
運行(running)的線程在獲取對象的同步鎖時,若該同步鎖被別的線程佔用,則JVM 會把該線程放入鎖池(lock pool)中。
運行(running)的線程執行Thread.sleep(long ms)或t.join()方法,或者發出了I/O 請求時,JVM 會把該線程置爲阻塞狀態。當sleep()狀態超時、join()等待線程終止或者超時、或者I/O
處理完畢時,線程從新轉入可運行(runnable)狀態。
正常結束
異常結束
調用 stop
在訪問volatile 變量時不會執行加鎖操做,所以也就不會使執行線程阻塞,所以volatile 變量是一種比sychronized 關鍵字更輕量級的同步機制。volatile 適合這種場景:一個變量被多個線程共享,線程直接給這個變量賦值
簡單原理:
當對非 volatile 變量進行讀寫的時候,每一個線程先從內存拷貝變量到CPU 緩存中。若是計算機有
多個CPU,每一個線程可能在不一樣的CPU 上被處理,這意味着每一個線程能夠拷貝到不一樣的 CPU
cache 中。而聲明變量是 volatile 的,JVM 保證了每次讀變量都從內存中讀,跳過 CPU cache這一步。
指令重排序是編譯器和處理器爲了高效對程序進行優化的手段,它只能保證程序執行的結果是正確的,可是沒法保證程序的操做順序與代碼順序一致。這在單線程中不會構成問題,可是在多線程中就會出現問題。很是經典的例子是在單例方法中同時對字段加入voliate,就是爲了防止指令重排序。
public class Singleton { private volatile static Singleton singleton; private Singleton() {} public static Singleton getInstance() { if (singleton == null) { // 1 synchronized(Singleton.class) { if (singleton == null) { singleton = new Singleton(); // 2 } } } return singleton; } }
實際上當程序執行到2處的時候,若是咱們沒有使用volatile關鍵字修飾變量singleton,就可能會形成錯誤。這是由於使用new關鍵字初始化一個對象的過程並非一個原子的操做,它分紅下面三個步驟進行:
a. 給 singleton 分配內存
b. 調用 Singleton 的構造函數來初始化成員變量
c. 將 singleton 對象指向分配的內存空間(執行完這步 singleton 就爲非 null 了)
若是虛擬機存在指令重排序優化,則步驟b和c的順序是沒法肯定的。若是A線程率先進入同步代碼塊並先執行了c而沒有執行b,此時由於singleton已經非null。這時候線程B到了1處,判斷singleton非null並將其返回使用,由於此時Singleton實際上還未初始化,天然就會出錯。synchronized能夠解決內存可見性,可是不能解決重排序問題。
舉例說明:
以i++爲例,其包括讀取、操做、賦值三個操做,下面是兩個線程的操做順序
假如說線程A在作了i+1,但未賦值的時候,線程B就開始讀取i,那麼當線程A賦值i=1,並回寫到主內存,而此時線程B已經再也不須要i的值了,而是直接交給處理器去作+1的操做,因而當線程B執行完並回寫到主內存,i的值仍然是1,而不是預期的2。也就是說,volatile縮短了普通變量在不一樣線程之間執行的時間差,但仍然存有漏洞,依然不能保證原子性。
參考文章 : java的可見性、有序性和原子性
參考資料: Java:CAS(樂觀鎖)
在JDK 5以前Java語言是靠synchronized關鍵字保證同步的,該機制存在如下問題:
(1)在多線程競爭下,加鎖、釋放鎖會致使比較多的上下文切換和調度延時,引發性能問題。
(2)一個線程持有鎖會致使其它全部須要此鎖的線程掛起。
(3)若是一個優先級高的線程等待一個優先級低的線程釋放鎖會致使優先級倒置,引發性能風險。
獨佔鎖是一種悲觀鎖,synchronized就是一種獨佔鎖,會致使其它全部須要鎖的線程掛起,等待持有鎖的線程釋放鎖。
而另外一個更加有效的鎖就是樂觀鎖。
所謂樂觀鎖就是,每次不加鎖而是假設沒有衝突而去完成某項操做,若是由於衝突失敗就重試,直到成功爲止。
樂觀鎖用到的機制就是CAS,Compare and Swap。
CAS機制當中使用了3個基本操做數:內存地址V,舊的預期值A,要修改的新值B。
更新一個變量的時候,只有當變量的預期值A和內存地址V當中的實際值相同時,纔會將內存地址V對應的值修改成B。
從Java1.5開始JDK的atomic包裏提供了一個類AtomicStampedReference來解決ABA問題。這個類的compareAndSet方法做用是首先檢查當前引用是否等於預期引用,而且當前標誌是否等於預期標誌,若是所有相等,則以原子方式將該引用和該標誌的值設置爲給定的更新值。
參加我的文章 java內存模型
同上
JVM在Server模式下的逃逸分析能夠分析出某個對象是否永遠只在某個方法、線程的範圍內,並無「逃逸」出這個範圍,逃逸分析的一個結果就是對於某些未逃逸對象能夠直接在棧上分配,因爲該對象必定是局部的,因此棧上分配不會有問題。
假設有一個Person類,以及兩個子類Boy和Girl
當這三個類加載到虛擬機後,方法區就包含了這三個類的信息,包括各自的方法表
Girl 和 Boy 在方法區中的方法表可表示以下:
方法表中的條目指向的具體的方法地址
如 Girl 繼承自 Object 的方法中,只有 toString() 指向本身的實現(Girl 的方法代碼,被重寫),其他皆指向 Object 的方法代碼;其繼承自於 Person 的方法 eat() [未重寫]和 speak() [被重寫]分別指向 Person 的方法實現和自己的實現。
若是子類改寫了父類的方法,那麼子類和父類的那些同名的方法仍然共享一個方法表項。(能夠理解爲這個方法在方法表裏的叫法/直接引用仍是不變的)
所以,方法在方法表中的偏移量老是固定的,全部繼承父類的子類的方法表中,其父類所定義的方法的偏移量也老是一個定值。(能夠理解爲即便被重寫,子類還能夠經過和父類的一樣直接引用找到該方法)
這樣 JVM 在調用實例方法其實只須要指定調用方法表中的第幾個方法便可
假設代碼以下:
class Party { void happyHour() { Person girl = new Girl(); girl.speak(); } }
(1)在常量池中找到方法調用的符號引用。
(2)查看Person的方法表,獲得speak方法在該方法表的偏移量(假設爲15),這樣就獲得該方法的直接引用。
(3)根據this指針獲得具體的對象(即 girl 所指向的位於堆中的對象)。
(4)根據對象獲得該對象對應的方法表,根據偏移量15查看有無重寫(override)該方法,若是重寫,則能夠直接調用(Girl的方法表的speak項指向自身的方法而非父類);若是沒有重寫,則須要拿到按照繼承關系從下往上的基類(這裏是Person類)的方法表,一樣按照這個偏移量15查看有無該方法。
下面代碼的執行結果是?
class Person{ { System.out.println("P1"); } static { System.out.println("P2"); } public Person(){ System.out.println("P3"); } } class Student extends Person{ static { System.out.println("S1"); } { System.out.println("S2"); } public Student(){ System.out.println("S3"); } } public class Main { public static void main(String[] args) { Student s = new Student(); } }
答案:P2,S1,P1,P3,S2,S3
參考 java中類加載與靜態變量、靜態方法與靜態代碼塊詳解與初始化順序