《OD面試》Java面試題整理
1.1.1 List
以陣列實現。節約空間,但陣列有容量限制。超出限制時會增加50%容量,用System.arraycopy()複製到新的陣列。因此最好能給出陣列大小的預估值。預設第一次插入元素時建立大小為10的陣列。
按陣列下標訪問元素-get(i)、set(i,e) 的效能很高,這是陣列的基本優勢。
如果按下標插入元素、刪除元素-add(i,e)、 remove(i)、remove(e),則要用System.arraycopy()來複制移動部分受影響的元素,效能就變差了。
越是前面的元素,修改時要移動的元素越多。直接在陣列末尾加入元素-常用的add(e),刪除最後一個元素則無影響。
1.1.1.2 LinkedList
1)概述
以雙向連結串列實現。連結串列無容量限制,但雙向連結串列本身使用了更多空間,每插入一個元素都要構造一個額外的Node物件,也需要額外的連結串列指標操作。
按下標訪問元素-get(i)、set(i,e) 要悲劇的部分遍歷連結串列將指標移動到位 (如果i>陣列大小的一半,會從末尾移起)。
插入、刪除元素時修改前後節點的指標即可,不再需要複製移動。但還是要部分遍歷連結串列的指標才能移動到下標所指的位置。
只有在連結串列兩頭的操作-add()、addFirst()、removeLast()或用iterator()上的remove()倒能省掉指標的移動。
Apache Commons 有個TreeNodeList,裡面是棵二叉樹,可以快速移動指標到位。
1.1.1.3 CopyOnWriteArrayList
1)CopyOnWriteArrayList概述
併發優化的ArrayList。基於不可變物件策略,在修改時先複製出一個數組快照來修改,改好了,再讓內部指標指向新陣列。
因為對快照的修改對讀操作來說不可見,所以讀讀之間不互斥,讀寫之間也不互斥,只有寫寫之間要加鎖互斥。但複製快照的成本昂貴,典型的適合讀多寫少的場景。
雖然增加了addIfAbsent(e)方法,會遍歷陣列來檢查元素是否已存在,效能可想像的不會太好。
2)CopyOnWriteArrayList可以用於什麼應用場景?
答:CopyOnWriteArrayList(免鎖容器)的好處之一是當多個迭代器同時遍歷和修改這個列表時,不會丟擲ConcurrentModificationException。在CopyOnWriteArrayList中,寫入將導致建立整個底層陣列的副本,而源陣列將保留在原地,使得複製的陣列在被修改時,讀取操作可以安全地執行。
1.1.1.4 List的總結
無論哪種實現,按值返回下標contains(e), indexOf(e), remove(e) 都需遍歷所有元素進行比較,效能可想像的不會太好。
沒有按元素值排序的SortedList。除了CopyOnWriteArrayList,再沒有其他執行緒安全又併發優化的實現如ConcurrentLinkedList。
湊合著用Set與Queue中的等價類時,會缺少一些List特有的方法如get(i)。如果更新頻率較高,或陣列較大時,還是得用Collections.synchronizedList(list),對所有操作用同一把鎖來保證執行緒安全。
1.1.2 Map
Java的HashMap工作原理
雜湊表是由陣列+連結串列組成的,我接下來解釋的是最常用的一種方法—— 拉鍊法,我們可以理解為“連結串列的陣列”。
HashMap有一個叫做Entry的內部類,它用來儲存key-value對。
上面的Entry物件是儲存在一個叫做table的Entry陣列中。table的索引在邏輯上叫做“桶”(bucket),它儲存了連結串列的第一個元素。
key的hashcode()方法用來找到Entry物件所在的桶。如果兩個key有相同的hash值,他們會被放在table陣列的同一個桶裡面。
key的equals()方法用來確保key的唯一性。
value物件的equals()和hashcode()方法根本一點用也沒有。
3)兩個重要的引數
resize的實現)
當put時,如果發現目前的bucket佔用程度已經超過了Load Factor所希望的比例,那麼就會發生resize。在resize的過程,簡單的說就是把bucket擴充為2倍,之後重新計算index,把節點再放到新的bucket中。
當超過限制的時候會resize,然而又因為我們使用的是2次冪的擴充套件(指長度擴為原來2倍),所以,元素的位置要麼是在原位置,要麼是在原位置再移動2次冪的位置。
6)你知道hash的實現嗎?為什麼要這樣實現?
在Java 1.8的實現中,是通過hashCode()的高16位異或低16位實現的:(h = k.hashCode()) ^ (h >>> 16)
,主要是從速度、功效、質量來考慮的,這麼做可以在bucket的n比較小的時候,也能保證考慮到高低bit都參與到hash的計算中,同時不會有太大的開銷。
7)如果HashMap的大小超過了負載因子(load factor)定義的容量,怎麼辦?
如果超過了負載因子(預設0.75),則會重新resize一個原來長度兩倍的HashMap,並且重新呼叫hash方法。
8)關於HashMap的總結
以Entry[]陣列實現的雜湊桶陣列,用Key的雜湊值取模桶陣列的大小可得到陣列下標。
插入元素時,如果兩條Key落在同一個桶(比如雜湊值1和17取模16後都屬於第一個雜湊桶),Entry用一個next屬性實現多個Entry以單向連結串列存放,後入桶的Entry將next指向桶當前的Entry。
查詢雜湊值為17的key時,先定位到第一個雜湊桶,然後以連結串列遍歷桶裡所有元素,逐個比較其key值。
當Entry數量達到桶數量的75%時(很多文章說使用的桶數量達到了75%,但看程式碼不是),會成倍擴容桶陣列,並重新分配所有原來的Entry,所以這裡也最好有個預估值。
取模用位運算(hash & (arrayLength-1))會比較快,所以陣列的大小永遠是2的N次方, 你隨便給一個初始值比如17會轉為32。預設第一次放入元素時的初始值是16。
iterator()時順著雜湊桶陣列來遍歷,看起來是個亂序。
在JDK8裡,新增預設為8的閥值,當一個桶裡的Entry超過閥值,就不以單向連結串列而以紅黑樹來存放以加快Key的查詢速度。
9)怎樣讓HashMap同步
Map m = Collections.synchronizeMap(hashMap);
10)HashMap怎麼實現,自己實現HashMap會注意哪些問題,怎麼實現一個hashMap一分鐘以後key過期
1.1.2.2 Hashtable
1)HashMap和HashTable 區別,HashTable執行緒安全嗎?
HashMap是Hashtable的輕量級實現(非執行緒安全的實現),他們都完成了Map介面,主要區別在於HashMap允許空(null)鍵值(key),由於非執行緒安全,效率上可能高於Hashtable。
HashMap允許將null作為一個entry的key或者value,而Hashtable不允許。
HashMap把Hashtable的contains方法去掉了,改成containsvalue和containsKey。因為contains方法容易讓人引起誤解。
Hashtable繼承自Dictionary類,而HashMap是Java1.2引進的Map interface的一個實現。
最大的不同是,Hashtable的方法是Synchronize的,而HashMap不是,在多個執行緒訪問Hashtable時,不需要自己為它的方法實現同步,而HashMap 就必須為之提供外同步。
Hashtable和HashMap採用的hash/rehash演算法都大概一樣,所以效能不會有很大的差別。
1.1.2.3 LinkedHashMap
擴充套件HashMap,每個Entry增加雙向連結串列,號稱是最佔記憶體的資料結構。
支援iterator()時按Entry的插入順序來排序(如果設定accessOrder屬性為true,則所有讀寫訪問都排序)。
插入時,Entry把自己加到Header Entry的前面去。如果所有讀寫訪問都要排序,還要把前後Entry的before/after拼接起來以在連結串列中刪除掉自己,所以此時讀操作也是執行緒不安全的了。
1.1.2.4 TreeMap
以紅黑樹實現,紅黑樹又叫自平衡二叉樹:
對於任一節點而言,其到葉節點的每一條路徑都包含相同數目的黑結點。
上面的規定,使得樹的層數不會差的太遠,使得所有操作的複雜度不超過 O(lgn),但也使得插入,修改時要複雜的左旋右旋來保持樹的平衡。
支援iterator()時按Key值排序,可按實現了Comparable介面的Key的升序排序,或由傳入的Comparator控制。可想象的,在樹上插入/刪除元素的代價一定比HashMap的大。
支援SortedMap介面,如firstKey(),lastKey()取得最大最小的key,或sub(fromKey, toKey), tailMap(fromKey)剪取Map的某一段。
TreeMap 是一個有序的key-value集合,它是通過紅黑樹實現的。
TreeMap 繼承於AbstractMap,所以它是一個Map,即一個key-value集合。
TreeMap 實現了NavigableMap介面,意味著它支援一系列的導航方法。比如返回有序的key集合。
TreeMap 實現了Cloneable介面,意味著它能被克隆。
TreeMap 實現了java.io.Serializable介面,意味著它支援序列化。
TreeMap基於紅黑樹(Red-Black tree)實現。該對映根據其鍵的自然順序進行排序,或者根據建立對映時提供的 Comparator 進行排序,具體取決於使用的構造方法。
TreeMap的基本操作 containsKey、get、put 和 remove 的時間複雜度是 log(n) 。
另外,TreeMap是非同步的。 它的iterator 方法返回的迭代器是fail-fast的。
1.1.2.5 EnumMap
EnumMap的原理是,在建構函式裡要傳入列舉類,那它就構建一個與列舉的所有值等大的陣列,按Enum. ordinal()下標來訪問陣列。效能與記憶體佔用俱佳。
美中不足的是,因為要實現Map介面,而 V get(Object key)中key是Object而不是泛型K,所以安全起見,EnumMap每次訪問都要先對Key進行型別判斷,在JMC裡錄得不低的取樣命中頻率。
1.1.2.6 ConcurrentHashMap
參考:
4)併發優化的HashMap。
在JDK5裡的經典設計,預設16把寫鎖(可以設定更多),有效分散了阻塞的概率。資料結構為Segment[],每個Segment一把鎖。Segment裡面才是雜湊桶陣列。Key先算出它在哪個Segment裡,再去算它在哪個雜湊桶裡。
也沒有讀鎖,因為put/remove動作是個原子動作(比如put的整個過程是一個對陣列元素/Entry 指標的賦值操作),讀操作不會看到一個更新動作的中間狀態。
但在JDK8裡,Segment[]的設計被拋棄了,改為精心設計的,只在需要鎖的時候加鎖。
支援ConcurrentMap介面,如putIfAbsent(key,value)與相反的replace(key,value)與以及實現CAS的replace(key, oldValue, newValue)。
5)分段加鎖
從ConcurrentHashMap程式碼中可以看出,它引入了一個“分段鎖”的概念,具體可以理解為把一個大的Map拆分成N個小的HashTable,根據key.hashCode()來決定把key放到哪個HashTable中。
在ConcurrentHashMap中,就是把Map分成了N個Segment,put和get的時候,都是現根據key.hashCode()算出放到哪個Segment中。
6)synchronizedMap和ConcurrentHashMap有什麼區別?
答:java5中新增了ConcurrentMap介面和它的一個實現類ConcurrentHashMap。ConcurrentHashMap提供了和Hashtable以及SynchronizedMap中所不同的鎖機制。比起synchronizedMap來,它提供了好得多的併發性。多個讀操作幾乎總可以併發地執行,同時進行的讀和寫操作通常也能併發地執行,而同時進行的寫操作仍然可以不時地併發進行(相關的類也提供了類似的多個讀執行緒的併發性,但是,只允許有一個活動的寫執行緒)。Hashtable中採用的鎖機制是一次鎖住整個hash表,從而同一時刻只能由一個執行緒對其進行操作;而ConcurrentHashMap中則是一次鎖住一個桶。ConcurrentHashMap預設將hash表分為16個桶,諸如get,put,remove等常用操作只鎖當前需要用到的桶。這樣,原來只能一個執行緒進入,現在卻能同時有16個寫執行緒執行,併發效能的提升是顯而易見的。前面說到的16個執行緒指的是寫執行緒,而讀操作大部分時候都不需要用到鎖。只有在size等操作時才需要鎖住整個hash表。
在迭代方面,ConcurrentHashMap使用了一種不同的迭代方式。在這種迭代方式中,當iterator被建立後集合再發生改變就不再是丟擲ConcurrentModificationException,取而代之的是在改變時new新的資料從而不影響原有的資料 ,iterator完成後再將頭指標替換為新的資料 ,這樣iterator執行緒可以使用原來老的資料,而寫執行緒也可以併發的完成改變。
1.1.2.7 ConcurrentSkipListMap
JDK6新增的併發優化的SortedMap,以SkipList結構實現。Concurrent包選用它是因為它支援基於CAS的無鎖演算法,而紅黑樹則沒有好的無鎖演算法。
原理上,可以想象為多個連結串列組成的N層樓,其中的元素從稀疏到密集,每個元素有往右與往下的指標。從第一層樓開始遍歷,如果右端的值比期望的大,那就往下走一層,繼續往前走。
典型的空間換時間。每次插入,都要決定在哪幾層插入,同時,要決定要不要多蓋一層樓。
它的size()同樣不能隨便調,會遍歷來統計。
1.1.3 Set
所有Set幾乎都是內部用一個Map來實現, 因為Map裡的KeySet就是一個Set,而value是假值,全部使用同一個Object即可。
Set的特徵也繼承了那些內部的Map實現的特徵。
HashSet:內部是HashMap。
LinkedHashSet:內部是LinkedHashMap。
TreeSet:內部是TreeMap的SortedSet。
ConcurrentSkipListSet:內部是ConcurrentSkipListMap的併發優化的SortedSet。
CopyOnWriteArraySet:內部是CopyOnWriteArrayList的併發優化的Set,利用其addIfAbsent()方法實現元素去重,如前所述該方法的效能很一般。
好像少了個ConcurrentHashSet,本來也該有一個內部用ConcurrentHashMap的簡單實現,但JDK偏偏沒提供。Jetty就自己簡單封了一個,Guava則直接用java.util.Collections.newSetFromMap(new ConcurrentHashMap()) 實現。
1.1.4 Queue
Queue是在兩端出入的List,所以也可以用陣列或連結串列來實現。
普通佇列
1.1.4.1 LinkedList
是的,以雙向連結串列實現的LinkedList既是List,也是Queue。
1.1.4.2 ArrayDeque
以迴圈陣列實現的雙向Queue。大小是2的倍數,預設是16。
為了支援FIFO,即從陣列尾壓入元素(快),從陣列頭取出元素(超慢),就不能再使用普通ArrayList的實現了,改為使用迴圈陣列。
有隊頭隊尾兩個下標:彈出元素時,隊頭下標遞增;加入元素時,隊尾下標遞增。如果加入元素時已到陣列空間的末尾,則將元素賦值到陣列[0],同時隊尾下標指向0,再插入下一個元素則賦值到陣列[1],隊尾下標指向1。如果隊尾的下標追上隊頭,說明陣列所有空間已用完,進行雙倍的陣列擴容。
1.1.4.3 PriorityQueue
用平衡二叉最小堆實現的優先順序佇列,不再是FIFO,而是按元素實現的Comparable介面或傳入Comparator的比較結果來出隊,數值越小,優先順序越高,越先出隊。但是注意其iterator()的返回不會排序。
平衡最小二叉堆,用一個簡單的陣列即可表達,可以快速定址,沒有指標什麼的。最小的在queue[0] ,比如queue[4]的兩個孩子,會在queue[2*4+1] 和 queue[2*(4+1)],即queue[9]和queue[10]。
入隊時,插入queue[size],然後二叉地往上比較調整堆。
出隊時,彈出queue[0],然後把queque[size]拿出來二叉地往下比較調整堆。
初始大小為11,空間不夠時自動50%擴容。
執行緒安全的佇列
1.1.4.4 ConcurrentLinkedQueue/Deque
無界的併發優化的Queue,基於連結串列,實現了依賴於CAS的無鎖演算法。
ConcurrentLinkedQueue的結構是單向連結串列和head/tail兩個指標,因為入隊時需要修改隊尾元素的next指標,以及修改tail指向新入隊的元素兩個CAS動作無法原子,所以需要的特殊的演算法。
執行緒安全的阻塞佇列
BlockingQueue,一來如果佇列已空不用重複的檢視是否有新資料而會阻塞在那裡,二來佇列的長度受限,用以保證生產者與消費者的速度不會相差太遠。當入隊時佇列已滿,或出隊時佇列已空,不同函式的效果見下表:
立刻報異常 | 立刻返回布林 | 阻塞等待 | 可設定等待時間 | |
入隊 | add(e) | offer(e) | put(e) | offer(e, timeout, unit) |
出隊 | remove() | poll() | take() | poll(timeout, unit) |
檢視 | element() | peek() | 無 | 無 |
1.1.4.5 ArrayBlockingQueue
定長的併發優化的BlockingQueue,也是基於迴圈陣列實現。有一把公共的鎖與notFull、notEmpty兩個Condition管理佇列滿或空時的阻塞狀態。
1.1.4.6 LinkedBlockingQueue/Deque
可選定長的併發優化的BlockingQueue,基於連結串列實現,所以可以把長度設為Integer.MAX_VALUE成為無界無等待的。
利用連結串列的特徵,分離了takeLock與putLock兩把鎖,繼續用notEmpty、notFull管理佇列滿或空時的阻塞狀態。
1.1.4.7 PriorityBlockingQueue
無界的PriorityQueue,也是基於陣列儲存的二叉堆(見前)。一把公共的鎖實現執行緒安全。因為無界,空間不夠時會自動擴容,所以入列時不會鎖,出列為空時才會鎖。
1.1.4.8 DelayQueue
內部包含一個PriorityQueue,同樣是無界的,同樣是出列時才會鎖。一把公共的鎖實現執行緒安全。元素需實現Delayed介面,每次呼叫時需返回當前離觸發時間還有多久,小於0表示該觸發了。
pull()時會用peek()檢視隊頭的元素,檢查是否到達觸發時間。ScheduledThreadPoolExecutor用了類似的結構。
同步佇列
SynchronousQueue同步佇列本身無容量,放入元素時,比如等待元素被另一條執行緒的消費者取走再返回。JDK執行緒池裡用它。
JDK7還有個LinkedTransferQueue,在普通執行緒安全的BlockingQueue的基礎上,增加一個transfer(e) 函式,效果與SynchronousQueue一樣。
參考:
1.2.1 執行緒安全、併發、執行緒安全的容器
1.2.1.1 執行緒安全
當多個執行緒訪問一個物件時,如果不用考慮這些執行緒在執行時的環境下的排程和交替執行,
也不需要進行額外的同步,或者呼叫其他協作,這個情況下,執行緒就是安全的。
1.2.1.2 併發
併發和並行從巨集觀上來講都是同時處理多路請求的概念。但併發和並行又有區別,並行是指兩個或者多個事件在同一時刻發生;而併發是指兩個或多個事件在同一時間間隔內發生。
在作業系統中,併發是指一個時間段中有幾個程式都處於已啟動執行到執行完畢之間,且這幾個程式都是在同一個處理機上執行,但任一個時刻點上只有一個程式在處理機上執行。
①程式與計算不再一一對應,一個程式副本可以有多個計算
②併發程式之間有相互制約關係,直接制約體現為一個程式需要另一個程式的計算結果,間接制約體現為多個程式競爭某一資源,如處理機、緩衝區等。
③併發程式在執行中是走走停停,斷續推進的。
1.2.1.3 執行緒安全的容器
多執行緒環境正確釋出共享資料的方法之一就是執行緒安全容器。
執行緒安全的容器是由鎖保護的域,將資料放入執行緒安全的容器中,可以保障其被安全地釋出給所有從這個容器訪問它的執行緒。
1)同步容器類
JDK1.0開始有兩個很老的同步容器類:Vector和HashTable。
JDK1.2之後Collections工具類中添加了一些工廠方法返回類似的同步封裝器類:
public static <T> Collection<T>synchronizedCollection(Collection<T> c)
static<T> List<T> synchronizedList(List<T> list) //包裝ArrayList、LinkedList
static<T> Set<T> synchronizedSet(Set<T> s) //包裝HashSet
static<K,V> Map<K,V> synchronizedMap(Map<K,V> m) //包裝HashMap
static <T> SortedSet<T> synchronizedSortedSet(SortedSet<T>s) //包裝TreeSet
static <K,V> SortedMap<K,V>synchronizedSortedMap(SortedMap<K,V> m) //包裝TreeMap
實現方式:
將它們的狀態封裝起來,並對每一個公有方法進行同步。
其中Vector就是Object[]+synchronized方法,Hashtable是HashtableEntry[]+synchronized方法。而synchronizedXXX()方法返回的同步封裝器類更是簡單地將傳進來的Collection的所有方法封裝為synchronized方法而已。
缺點:
1、 通過同步方法將訪問操作序列化,導致併發環境下效率低下
2、 複合操作(迭代、條件運算如沒有則新增等)非執行緒安全,需要客戶端程式碼來實現加鎖。
2)併發容器類
併發容器出現的最大的需求就是提升同步容器類的效能!
可以對比(非併發容器類)看看,將單執行緒版本和併發版本做一個比較。
1、HashMap和HashSet的併發版本
1.1 ConcurrentHashMap<K, V>(HashMap的併發版本)
版本:JDK5
目標:代替Hashtable、synchronizedMap,支援複合操作
原理:採用一種更加細粒度的加鎖機制“分段鎖”,任意數量讀取執行緒可以併發讀取,任意數量的讀取執行緒和一個寫執行緒可以併發訪問,一定數量的寫入執行緒可以併發訪問。併發環境下ConcurrentHashMap帶來了更高的吞吐量,而在單執行緒環境下只損失了很小的效能。
1.2 CopyOnWriteArraySet<E>(HashSet的併發版本)
版本:JDK5
目標:代替synchronizedSet
原理:CopyOnWriteArraySet基於CopyOnWriteArrayList實現,其唯一的不同是在add時呼叫的是CopyOnWriteArrayList的addIfAbsent方法,其遍歷當前Object陣列,如Object陣列中已有了當前元素,則直接返回,如果沒有則放入Object陣列的尾部,並返回。
2、TreeMap和TreeSet的併發版本
ConcurrentSkipListMap<K, V>(TreeMap的併發版本)
版本:JDK6
目標:代替synchronizedSortedMap(TreeMap)
原理:Skip list(跳錶)是一種可以代替平衡樹的資料結構,預設是按照Key值升序的。Skip list讓已排序的資料分佈在多層連結串列中,以0-1隨機數決定一個數據的向上攀升與否,通過"空間來換取時間"的一個演算法。ConcurrentSkipListMap提供了一種執行緒安全的併發訪問的排序對映表。內部是SkipList(跳錶)結構實現,在理論上能夠在O(log(n))時間內完成查詢、插入、刪除操作。
ConcurrentSkipListSet<E>(TreeSet的併發版本)
版本:JDK6
目標:代替synchronizedSortedSet
原理:內部基於ConcurrentSkipListMap實現!
3、ArrayList和LinkedList的併發版本
CopyOnWriteArrayList<E>(ArrayList的併發版本)
目標:代替Vector、synchronizedList
原理:CopyOnWriteArrayList的核心思想是利用高併發往往是讀多寫少的特性,對讀操作不加鎖,對寫操作,先複製一份新的集合,在新的集合上面修改,然後將新集合賦值給舊的引用,並通過volatile 保證其可見性,當然寫操作的鎖是必不可少的了。
ConcurrentLinkedQueue<E>(LinkedList的併發版本)
目標:代替Vector、synchronizedList
特點:基於連結串列實現的FIFO佇列,特別注意單執行緒環境中LinkedList除了可以用作連結串列,也可用作佇列,併發版本也一樣
阻塞佇列:BlockingQueue
版本:JDK1.5
特點:拓展了Queue,增加了可阻塞的插入和獲取等操作
實現類
LinkedBlockingQueue:基於連結串列實現的可阻塞的FIFO佇列
ArrayBlockingQueue:基於陣列實現的可阻塞的FIFO佇列
PriorityBlockingQueue:按優先順序排序的佇列
原理:通過ReentrantLock實現執行緒安全,通過Condition實現阻塞和喚醒。
1.2.2 核心理論
1)共享性
資料共享性是執行緒安全的主要原因之一。如果所有的資料只是在執行緒內有效,那就不存線上程安全性問題,這也是我們在程式設計的時候經常不需要考慮執行緒安全的主要原因之一。但是,在多執行緒程式設計中,資料共享是不可避免的。最典型的場景是資料庫中的資料,為了保證資料的一致性,我們通常需要共享同一個資料庫中資料,即使是在主從的情況下,訪問的也同一份資料,主從只是為了訪問的效率和資料安全,而對同一份資料做的副本。
2)互斥性
資源互斥是指同時只允許一個訪問者對其進行訪問,具有唯一性和排它性。我們通常允許多個執行緒同時對資料進行讀操作,但同一時間內只允許一個執行緒對資料進行寫操作。所以我們通常將鎖分為共享鎖和排它鎖,也叫做讀鎖和寫鎖。如果資源不具有互斥性,即使是共享資源,我們也不需要擔心執行緒安全。例如,對於不可變的資料共享,所有執行緒都只能對其進行讀操作,所以不用考慮執行緒安全問題。但是對共享資料的寫操作,一般就需要保證互斥性,上述例子中就是因為沒有保證互斥性才導致資料的修改產生問題。Java 中提供多種機制來保證互斥性,最簡單的方式是使用Synchronized。
3)原子性
原子性就是指對資料的操作是一個獨立的、不可分割的整體。換句話說,就是一次操作,是一個連續不可中斷的過程,資料不會執行的一半的時候被其他執行緒所修改。保證原子性的最簡單方式是作業系統指令,就是說如果一次操作對應一條作業系統指令,這樣肯定可以能保證原子性。但是很多操作不能通過一條指令就完成。例如,對long型別的運算,很多系統就需要分成多條指令分別對高位和低位進行操作才能完成。還比如,我們經常使用的整數 i++ 的操作,其實需要分成三個步驟:(1)讀取整數 i 的值;(2)對 i 進行加一操作;(3)將結果寫回記憶體。
這個過程在多執行緒下就可能出現如下現象:
這也是程式碼段一執行的結果為什麼不正確的原因。對於這種組合操作,要保證原子性,最常見的方式是加鎖,如Java中的Synchronized或Lock都可以實現,程式碼段二就是通過Synchronized實現的。除了鎖以外,還有一種方式就是CAS(Compare And Swap),即修改資料之前先比較與之前讀取到的值是否一致,如果一致,則進行修改,如果不一致則重新執行,這也是樂觀鎖的實現原理。不過CAS在某些場景下不一定有效,比如另一執行緒先修改了某個值,然後再改回原來值,這種情況下,CAS是無法判斷的(ABA問題)。
4)可見性
要理解可見性,需要先對JVM的記憶體模型有一定的瞭解,JVM的記憶體模型與作業系統類似,如圖所示:
從這個圖中我們可以看出,每個執行緒都有一個自己的工作記憶體(相當於CPU高階緩衝區,這麼做的目的還是在於進一步縮小儲存系統與CPU之間速度的差異,提高效能),對於共享變數,執行緒每次讀取的是工作記憶體中共享變數的副本,寫入的時候也直接修改工作記憶體中副本的值,然後在某個時間點上再將工作記憶體與主記憶體中的值進行同步。這樣導致的問題是,如果執行緒1對某個變數進行了修改,執行緒2卻有可能看不到執行緒1對共享變數所做的修改。
5)有序性
為了提高效能,編譯器和處理器可能會對指令做重排序。重排序可以分為三種:
(1)編譯器優化的重排序。編譯器在不改變單執行緒程式語義的前提下,可以重新安排語句的執行順序。
(2)指令級並行的重排序。現代處理器採用了指令級並行技術(Instruction-Level Parallelism, ILP)來將多條指令重疊執行。如果不存在資料依賴性,處理器可以改變語句對應機器指令的執行順序。
(3)記憶體系統的重排序。由於處理器使用快取和讀/寫緩衝區,這使得載入和儲存操作看上去可能是在亂序執行。
1.2.3 java中執行緒安全的5個級別
(1)不可改變 final
從內部類中訪問本地變數,需要被宣告為最終型別。
(2)絕對安全
絕對安全其實很難描述,比如Vector是安全的。但是在多執行緒的情況下,它也是不安全的。
(3)相對安全
相對安全其實就是我們一般意義上的執行緒安全。
它需要保證對這個物件的單獨操作是安全的。但是對於特定的順序,需要一些方法保證執行緒安全。
(4)執行緒相容
這就是我們常見的情況,需要使用synchronized等手段來保證執行緒安全。
(5)執行緒對立
比較極端的情況,就是無論怎麼加鎖,程式碼無法併發執行。一種情況就是死鎖。
1.2.4 如何實現執行緒安全
(1)互斥同步
保持共享資料在同一時刻只被一個執行緒使用。
互斥是手段,同步是目的。
在java中最常見的就是synchronized方法。
synchronized標記的程式碼,會生成monitorenter & monitorexit 2段程式碼。
這是java編譯器自動生成的,不會有遺漏。使用其他鎖,lock & unlock成對出現,但是
開發者有時候會容易疏忽這個操作,尤其在catch程式碼裡面忘記呼叫unlock,將是一個隱患。
java.util.concurrent 下面有不少同步的方法。ReentrantLock也是一個可以的方法,在1.5以前,效能
遠由於synchronized。但是在1.6, java還是把synchronized做了很大的提升。原因就是synchronized使用的
程式碼已經遠遠大於ReentrantLock,並且引入ReentrantLock,可能會令需要開發者混淆。所以ReentrantLock可以認為是
一道開胃小菜而已。
(2)非阻塞同步
互斥同步是一種阻塞同步,但是有些情況下,我們不需要互斥,只要能夠同步就可以。
java.util.concurrent.atomic.AtomicInteger
1.2.5 synchronized、lock、volatile
1.2.5.1 synchronized
1)synchronized的主要作用
(1)確保執行緒互斥的訪問同步程式碼
(2)保證共享變數的修改能夠及時可見
(3)有效解決重排序問題。
2)語法上的主要用法
(1)修飾普通方法
(2)修飾靜態方法
(3)修飾程式碼塊
3)實現原理
(1)synchronized的語義底層是通過一個monitor的物件來完成,其實wait/notify等方法也依賴於monitor物件,這就是為什麼只有在同步的塊或者方法中才能呼叫wait/notify等方法,否則會丟擲java.lang.IllegalMonitorStateException的異常的原因。
monitorenter :
每個物件有一個監視器鎖(monitor)。當monitor被佔用時就會處於鎖定狀態,執行緒執行monitorenter指令時嘗試獲取monitor的所有權,過程如下:
- 如果monitor的進入數為0,則該執行緒進入monitor,然後將進入數設定為1,該執行緒即為monitor的所有者。
- 如果執行緒已經佔有該monitor,只是重新進入,則進入monitor的進入數加1.
- 如果其他執行緒已經佔用了monitor,則該執行緒進入阻塞狀態,直到monitor的進入數為0,再重新嘗試獲取monitor的所有權。
monitorexit:
執行monitorexit的執行緒必須是objectref所對應的monitor的所有者。
指令執行時,monitor的進入數減1,如果減1後進入數為0,那執行緒退出monitor,不再是這個monitor的所有者。其他被這個monitor阻塞的執行緒可以嘗試去獲取這個 monitor 的所有權。
(2)方法的同步並沒有通過指令monitorenter和monitorexit來完成(理論上其實也可以通過這兩條指令來實現),不過相對於普通方法,其常量池中多了ACC_SYNCHRONIZED標示符。JVM就是根據該標示符來實現方法的同步的:當方法呼叫時,呼叫指令將會檢查方法的 ACC_SYNCHRONIZED 訪問標誌是否被設定,如果設定了,執行執行緒將先獲取monitor,獲取成功之後才能執行方法體,方法執行完後再釋放monitor。在方法執行期間,其他任何執行緒都無法再獲得同一個monitor物件。 其實本質上沒有區別,只是方法的同步是一種隱式的方式來實現,無需通過位元組碼來完成。
4)當一個執行緒進入某個物件的一個synchronized的例項方法後,其它執行緒是否可進入此物件的其它方法?
答:A、一個執行緒在訪問一個物件的同步方法時,另一個執行緒可以同時訪問這個物件的非同步方法
B、 一個執行緒在訪問一個物件的同步方法時,另一個執行緒不能同時訪問這個同步方法。
1.2.5.2 volatile
volatile關鍵字就是Java中提供的另一種解決可見性和有序性問題的方案。對於原子性,需要強調一點,也是大家容易誤解的一點:對volatile變數的單次讀/寫操作可以保證原子性的,如long和double型別變數,但是並不能保證i++這種操作的原子性,因為本質上i++是讀、寫兩次操作。
volatile的使用
1)防止重排序
先要了解物件的構造過程,例項化一個物件其實可以分為三個步驟:
(1)分配記憶體空間。
(2)初始化物件。
(3)將記憶體空間的地址賦值給對應的引用。
但是由於作業系統可以對指令進行重排序,所以上面的過程也可能會變成如下過程:
(1)分配記憶體空間。
(2)將記憶體空間的地址賦值給對應的引用。
(3)初始化物件
如果是這個流程,多執行緒環境下就可能將一個未初始化的物件引用暴露出來,從而導致不可預料的結果。因此,為了防止這個過程的重排序,我們需要將變數設定為volatile型別的變數。
有序性實現原理:
happen-before規則:
- 同一個執行緒中的,前面的操作 happen-before 後續的操作。(即單執行緒內按程式碼順序執行。但是,在不影響在單執行緒環境執行結果的前提下,編譯器和處理器可以進行重排序,這是合法的。換句話說,這一是規則無法保證編譯重排和指令重排)。
- 監視器上的解鎖操作 happen-before 其後續的加鎖操作。(Synchronized 規則)
- 對volatile變數的寫操作 happen-before 後續的讀操作。(volatile 規則)
- 執行緒的start() 方法 happen-before 該執行緒所有的後續操作。(執行緒啟動規則)
- 執行緒所有的操作 happen-before 其他執行緒在該執行緒上呼叫 join 返回成功後的操作。
- 如果 a happen-before b,b happen-before c,則a happen-before c(傳遞性)。
2)實現可見性
可見性問題主要指一個執行緒修改了共享變數值,而另一個執行緒卻看不到。引起可見性問題的主要原因是每個執行緒擁有自己的一個快取記憶體區——執行緒工作記憶體。volatile關鍵字能有效的解決這個問題。
可見性實現原理:
在前文中已經提及過,執行緒本身並不直接與主記憶體進行資料的互動,而是通過執行緒的工作記憶體來完成相應的操作。這也是導致執行緒間資料不可見的本質原因。因此要實現volatile變數的可見性,直接從這方面入手即可。對volatile變數的寫操作與普通變數的主要區別有兩點:
(1)修改volatile變數時會強制將修改後的值重新整理的主記憶體中。
(2)修改volatile變數後會導致其他執行緒工作記憶體中對應的變數值失效。因此,再讀取該變數值的時候就需要重新從讀取主記憶體中的值。
通過這兩個操作,就可以解決volatile變數的可見性問題。
3)保證原子性
volatile只能保證對單次讀/寫的原子性。
因為long和double兩種資料型別的操作可分為高32位和低32位兩部分,因此普通的long或double型別讀/寫可能不是原子的。因此,鼓勵大家將共享的long和double變數設定為volatile型別,這樣能保證任何情況下對long和double的單次讀/寫操作都具有原子性。
i++其實是一個複合操作,包括三步驟:
(1)讀取i的值。
(2)對i加1。
(3)將i的值寫回記憶體。
volatile是無法保證這三個操作是具有原子性的,我們可以通過AtomicInteger或者Synchronized來保證+1操作的原子性。
1.2.5.3 lock
1)什麼是可重入鎖(ReentrantLock)?
答: java.util.concurrent.lock 中的 Lock 框架是鎖定的一個抽象,它允許把鎖定的實現作為 Java 類,而不是作為語言的特性來實現。這就為 Lock 的多種實現留下了空間,各種實現可能有不同的排程演算法、效能特性或者鎖定語義。 ReentrantLock 類實現了 Lock ,它擁有與 synchronized 相同的併發性和記憶體語義,但是添加了類似鎖投票、定時鎖等候和可中斷鎖等候的一些特性。此外,它還提供了在激烈爭用情況下更佳的效能。(換句話說,當許多執行緒都想訪問共享資源時,JVM 可以花更少的時候來排程執行緒,把更多時間用在執行執行緒上。)
reentrant 鎖意味著什麼呢?簡單來說,它有一個與鎖相關的獲取計數器,如果擁有鎖的某個執行緒再次得到鎖,那麼獲取計數器就加1,然後鎖需要被釋放兩次才能獲得真正釋放。這模仿了 synchronized 的語義;如果執行緒進入由執行緒已經擁有的監控器保護的 synchronized 塊,就允許執行緒繼續進行,當執行緒退出第二個(或者後續)synchronized 塊的時候,不釋放鎖,只有執行緒退出它進入的監控器保護的第一個 synchronized 塊時,才釋放鎖。
2)synchronized和java.util.concurrent.locks.Lock的異同?
答:Lock 和 synchronized 有一點明顯的區別 —— lock 必須在 finally 塊中釋放。否則,如果受保護的程式碼將丟擲異常,鎖就有可能永遠得不到釋放!這一點區別看起來可能沒什麼,但是實際上,它極為重要。忘記在 finally 塊中釋放鎖,可能會在程式中留下一個定時炸彈,當有一天炸彈爆炸時,您要花費很大力氣才有找到源頭在哪。而使用同步,JVM 將確保鎖會獲得自動釋放。
1.2.5.4 記憶體屏障
為了實現volatile可見性和happen-befor的語義。JVM底層是通過一個叫做“記憶體屏障”的東西來完成。記憶體屏障,也叫做記憶體柵欄,是一組處理器指令,用於實現對記憶體操作的順序限制。下面是完成上述規則所要求的記憶體屏障:
Required barriers | 2nd operation | |||
1st operation | Normal Load | Normal Store | Volatile Load | Volatile Store |
Normal Load | LoadStore | |||
Normal Store | StoreStore | |||
Volatile Load | LoadLoad | LoadStore | LoadLoad | LoadStore |
Volatile Store | StoreLoad | StoreStore |
(1)LoadLoad 屏障
執行順序:Load1—>Loadload—>Load2
確保Load2及後續Load指令載入資料之前能訪問到Load1載入的資料。
(2)StoreStore 屏障
執行順序:Store1—>StoreStore—>Store2
確保Store2以及後續Store指令執行前,Store1操作的資料對其它處理器可見。
(3)LoadStore 屏障
執行順序: Load1—>LoadStore—>Store2
確保Store2和後續Store指令執行前,可以訪問到Load1載入的資料。
(4)StoreLoad 屏障
執行順序: Store1—> StoreLoad—>Load2
確保Load2和後續的Load指令讀取之前,Store1的資料對其他處理器是可見的。
1.2.6 java鎖型別,各自的特性
鎖的狀態總共有四種:無鎖狀態、偏向鎖、輕量級鎖和重量級鎖。隨著鎖的競爭,鎖可以從偏向鎖升級到輕量級鎖,再升級的重量級鎖(但是鎖的升級是單向的,也就是說只能從低到高升級,不會出現鎖的降級)。JDK 1.6中預設是開啟偏向鎖和輕量級鎖的,我們也可以通過-XX:-UseBiasedLocking來禁用偏向鎖。鎖的狀態儲存在物件的標頭檔案中,以32位的JDK為例:
鎖狀態 | 25 bit | 4bit | 1bit | 2bit | ||
23bit | 2bit | 是否是偏向鎖 | 鎖標誌位 | |||
輕量級鎖 | 指向棧中鎖記錄的指標 | 00 | ||||
重量級鎖 | 指向互斥量(重量級鎖)的指標 | 10 | ||||
GC標記 | 空 | 11 | ||||
偏向鎖 | 執行緒ID | Epoch | 物件分代年齡 | 1 | 01 | |
無鎖 | 物件的hashCode | 物件分代年齡 | 0 | 01 |
“輕量級”是相對於使用作業系統互斥量來實現的傳統鎖而言的。但是,首先需要強調一點的是,輕量級鎖並不是用來代替重量級鎖的,它的本意是在沒有多執行緒競爭的前提下,減少傳統的重量級鎖使用產生的效能消耗。在解釋輕量級鎖的執行過程之前,先明白一點,輕量級鎖所適應的場景是執行緒交替執行同步塊的情況,如果存在同一時間訪問同一鎖的情況,就會導致輕量級鎖膨脹為重量級鎖。
1)自旋鎖
自旋鎖有時候會白白的耗用處理器的資源,但是沒有任何實際效果。
適應性自旋(Adaptive Spinning):從輕量級鎖獲取的流程中我們知道,當執行緒在獲取輕量級鎖的過程中執行CAS操作失敗時,是要通過自旋來獲取重量級鎖的。問題在於,自旋是需要消耗CPU的,如果一直獲取不到鎖的話,那該執行緒就一直處在自旋狀態,白白浪費CPU資源。解決這個問題最簡單的辦法就是指定自旋的次數,例如讓其迴圈10次,如果還沒獲取到鎖就進入阻塞狀態。但是JDK採用了更聰明的方式——適應性自旋,簡單來說就是執行緒如果自旋成功了,則下次自旋的次數會更多,如果自旋失敗了,則自旋的次數就會減少。
2)鎖消除(Lock Elimination)
鎖消除即刪除不必要的加鎖操作。根據程式碼逃逸技術,如果判斷到一段程式碼中,堆上的資料不會逃逸出當前執行緒,那麼可以認為這段程式碼是執行緒安全的,不必要加鎖。
如果程式碼不可能存在共享資料需要同步,編譯器就會把鎖拿掉。
3)鎖粗化(Lock Coarsening)
鎖粗化的概念應該比較好理解,就是將多次連線在一起的加鎖、解鎖操作合併為一次,將多個連續的鎖擴充套件成一個範圍更大的鎖。
原則上鎖的互斥模組儘可能的小,但是如果對於同一物件,反覆的lock & unlock 尤其是迴圈體中。會帶來很大的效能損失。
鎖 | 優點 | 缺點 | 適用場景 |
偏向鎖 | 加鎖和解鎖不需要額外的消耗,和執行非同步方法比僅存在納秒級的差距。 | 如果執行緒間存在鎖競爭,會帶來額外的鎖撤銷的消耗。 | 適用於只有一個執行緒訪問同步塊場景。 |
輕量級鎖 | 競爭的執行緒不會阻塞,提高了程式的響應速度。 | 如果始終得不到鎖競爭的執行緒使用自旋會消耗CPU。 | 追求響應時間。 同步塊執行速度非常快。 |
重量級鎖 | 執行緒競爭不使用自旋,不會消耗CPU。 | 執行緒阻塞,響應時間緩慢。 | 追求吞吐量。 同步塊執行速度較長。 |
1.2.8 執行緒的狀態
Java中執行緒中狀態可分為五種:New(新建狀態),Runnable(就緒狀態),Running(執行狀態),Blocked(阻塞狀態),Dead(死亡狀態)。
New:新建狀態,當執行緒建立完成時為新建狀態,即new Thread(...),還沒有呼叫start方法時,執行緒處於新建狀態。
Runnable:就緒狀態,當呼叫執行緒的的start方法後,執行緒進入就緒狀態,等待CPU資源。處於就緒狀態的執行緒由Java執行時系統的執行緒排程程式(thread scheduler)來排程。
Running:執行狀態,就緒狀態的執行緒獲取到CPU執行權以後進入執行狀態,開始執行run方法。
Blocked:阻塞狀態,執行緒沒有執行完,由於某種原因(如,I/O操作等)讓出CPU執行權,自身進入阻塞狀態。
Dead:死亡狀態,執行緒執行完成或者執行過程中出現異常,執行緒就會進入死亡狀態。
這五種狀態之間的轉換關係如下圖所示:
1.2.9 執行緒間的協作
參考:
1)wait方法
(1)wait()方法的作用是將當前執行的執行緒掛起(即讓其進入阻塞狀態),直到notify或notifyAll方法來喚醒執行緒.
(2)wait(long timeout),該方法與wait()方法類似,唯一的區別就是在指定時間內,如果沒有notify或notifAll方法的喚醒,也會自動喚醒。
(3)至於wait(long timeout,long nanos),本意在於更精確的控制排程時間,不過從目前版本來看,該方法貌似沒有完整的實現該功能
wait方法的使用必須在同步的範圍內,否則就會丟擲IllegalMonitorStateException異常,wait方法的作用就是阻塞當前執行緒等待notify/notifyAll方法的喚醒,或等待超時後自動喚醒。
void notify() | Wakes up a single thread that is waiting on this object's monitor. |
void notifyAll() | Wakes up all threads that are waiting on this object's monitor. |
有了對wait方法原理的理解,notify方法和notifyAll方法就很容易理解了。既然wait方式是通過物件的monitor物件來實現的,所以只要在同一物件上去呼叫notify/notifyAll方法,就可以喚醒對應物件monitor上等待的執行緒了。notify和notifyAll的區別在於前者只能喚醒monitor上的一個執行緒,對其他執行緒沒有影響,而notifyAll則喚醒所有的執行緒。
最後,有兩點點需要注意:
(1)呼叫wait方法後,執行緒是會釋放對monitor物件的所有權的。
(2)一個通過wait方法阻塞的執行緒,必須同時滿足以下兩個條件才能被真正執行:
- 執行緒需要被喚醒(超時喚醒或呼叫notify/notifyll)。
- 執行緒喚醒後需要競爭到鎖(monitor)。
notify()是喚醒一個執行緒。只喚醒一個。
notifyAll()是喚醒全部執行緒,但是注意是一個個喚醒,喚醒一個再喚下一個。
隨機喚醒和喚醒全部。
3)sleep方法
這組方法跟上面方法的最明顯區別是:這幾個方法都位於Thread類中,而上面三個方法都位於Object類中。
sleep方法的作用是讓當前執行緒暫停指定的時間(毫秒),sleep方法是最簡單的方法,在上述的例子中也用到過,比較容易理解。唯一需要注意的是其與wait方法的區別。最簡單的區別是,wait方法依賴於同步,而sleep方法可以直接呼叫。而更深層次的區別在於sleep方法只是暫時讓出CPU的執行權,並不釋放鎖。而wait方法則需要釋放鎖。
通過sleep方法實現的暫停,程式是順序進入同步塊的,只有當上一個執行緒執行完成的時候,下一個執行緒才能進入同步方法,sleep暫停期間一直持有monitor物件鎖,其他執行緒是不能進入的。而wait方法則不同,當呼叫wait方法後,當前執行緒會釋放持有的monitor物件鎖,因此,其他執行緒還可以進入到同步方法,執行緒被喚醒後,需要競爭鎖,獲取到鎖之後再繼續執行。
這個結果的區別很明顯,通過sleep方法實現的暫停,程式是順序進入同步塊的,只有當上一個執行緒執行完成的時候,下一個執行緒才能進入同步方法,sleep暫停期間一直持有monitor物件鎖,其他執行緒是不能進入的。而wait方法則不同,當呼叫wait方法後,當前執行緒會釋放持有的monitor物件鎖,因此,其他執行緒還可以進入到同步方法,執行緒被喚醒後,需要競爭鎖,獲取到鎖之後再繼續執行。
sleep()和 wait()有什麼區別?
定時等待和在監視器上等待,不同範疇。
4)yield方法
yield方法的作用是暫停當前執行緒,以便其他執行緒有機會執行,不過不能指定暫停的時間,並且也不能保證當前執行緒馬上停止。yield方法只是將Running狀態轉變為Runnable狀態。
通過yield方法來實現兩個執行緒的交替執行。不過請注意:這種交替並不一定能得到保證。
/** * A hint to the scheduler that the current thread is willing to yield * its current use of a processor. The scheduler is free to ignore this * hint. * * <p> Yield is a heuristic attempt to improve relative progression * between threads that would otherwise over-utilise a CPU. Its use * should be combined with detailed profiling and benchmarking to * ensure that it actually has the desired effect. * * <p> It is rarely appropriate to use this method. It may be useful * for debugging or testing purposes, where it may help to reproduce * bugs due to race conditions. It may also be useful when designing * concurrency control constructs such as the ones in the * {@link java.util.concurrent.locks} package. */
這段話主要說明了三個問題:
- 排程器可能會忽略該方法。
- 使用的時候要仔細分析和測試,確保能達到預期的效果。
- 很少有場景要用到該方法,主要使用的地方是除錯和測試。
5)join方法
void join() | Waits for this thread to die. |
void join(long millis) | Waits at most millis milliseconds for this thread to die. |
void join(long millis, int nanos) | Waits at most millis milliseconds plus nanos nanoseconds for this thread to die. |
join方法的作用是父執行緒等待子執行緒執行完成後再執行,換句話說就是將非同步執行的執行緒合併為同步的執行緒。JDK中提供三個版本的join方法,其實現與wait方法類似,join()方法實際上執行的join(0),而join(long millis, int nanos)也與wait(long millis, int nanos)的實現方式一致,暫時對納秒的支援也是不完整的。
6)問題:wait/notify/notifyAll方法的作用是實現執行緒間的協作,那為什麼這三個方法不是位於Thread類中,而是位於Object類中?
位於Object中,也就相當於所有類都包含這三個方法(因為Java中所有的類都繼承自Object類)。
要回答這個問題,還是得回過來看wait方法的實現原理,大家需要明白的是,wait等待的到底是什麼東西?如果對上面內容理解的比較好的話,我相信大家應該很容易知道wait等待其實是物件monitor,由於Java中的每一個物件都有一個內建的monitor物件,自然所有的類都理應有wait/notify方法。
1)什麼是ThreadLocal
早在JDK 1.2的版本中就提供java.lang.ThreadLocal,ThreadLocal為解決多執行緒程式的併發問題提供了一種新的思路。使用這個工具類可以很簡潔地編寫出優美的多執行緒程式。
當使用ThreadLocal維護變數時,ThreadLocal為每個使用該變數的執行緒提供獨立的變數副本,所以每一個執行緒都可以獨立地改變自己的副本,而不會影響其它執行緒所對應的副本。
從執行緒的角度看,目標變數就象是執行緒的本地變數,這也是類名中“Local”所要表達的意思。
2)Thread同步機制的比較
對於多執行緒資源共享的問題,同步機制採用了“以時間換空間”的方式,而ThreadLocal採用了“以空間換時間”的方式。前者僅提供一份變數,讓不同的執行緒排隊訪問,而後者為每一個執行緒都提供了一份變數,因此可以同時訪問而互不影響。
3)java.lang.ThreadLocal<T>的具體實現
(1)set
getMap和createMap
(2)get方法
(3)setInitialValue方法
4)jvm怎麼讓多執行緒new 物件時候 記憶體不用加鎖
預先分配一定量的記憶體給執行緒。
1.2.10 java多執行緒api及jdk1.5以後new api
1)concurrent包下面,都用過什麼?
Executor :具體Runnable任務的執行者。執行緒池
ExecutorService :一個執行緒池管理者,其實現類有多種,我會介紹一部分。我們能把Runnable,Callable提交到池中讓其排程。
Semaphore :一個計數訊號量
ReentrantLock :一個可重入的互斥鎖定 Lock,功能類似synchronized,但要強大的多。
Future :是與Runnable,Callable進行互動的介面,比如一個執行緒執行結束後取返回的結果等等,還提供了cancel終止執行緒。
BlockingQueue :阻塞佇列。
CompletionService : ExecutorService的擴充套件,可以獲得執行緒執行結果的
CountDownLatch :一個同步輔助類,在完成一組正在其他執行緒中執行的操作之前,它允許一個或多個執行緒一直等待。
CyclicBarrier :一個同步輔助類,它允許一組執行緒互相等待,直到到達某個公共屏障點
Future :Future 表示非同步計算的結果。
ScheduledExecutorService :一個 ExecutorService,可安排在給定的延遲後執行或定期執行的命令
2)執行緒池用過嗎?
執行緒池是一種多執行緒處理形式,處理過程中將任務新增到佇列,然後在建立執行緒後自動啟動這些任務。
執行緒池執行緒都是後臺執行緒。每個執行緒都使用預設的堆疊大小,以預設的優先順序執行,並處於多執行緒單元中。
如果某個執行緒在託管程式碼中空閒(如正在等待某個事件),則執行緒池將插入另一個輔助執行緒來使所有處理器保持繁忙。
如果所有執行緒池執行緒都始終保持繁忙,但佇列中包含掛起的工作,則執行緒池將在一段時間後建立另一個輔助執行緒但執行緒的數目永遠不會超過最大值。
超過最大值的執行緒可以排隊,但他們要等到其他執行緒完成後才啟動。
3)java中有幾種方法可以建立一個執行緒?
4)如何停止一個正在執行的執行緒?
(1)public void Thread.interrupt() // 無返回值- 如果該執行緒正阻塞於Object類的wait()、wait(long)、wait(long, int)方法,或者Thread類的join()、join(long)、join(long, int)、sleep(long)、sleep(long, int)方法,則該執行緒的中斷狀態將被清除,並收到一個java.lang.InterruptedException。
- 如果該執行緒正阻塞於interruptible channel上的I/O操作,則該通道將被關閉,同時該執行緒的中斷狀態被設定,並收到一個java.nio.channels.ClosedByInterruptException。
- 如果該執行緒正阻塞於一個java.nio.channels.Selector操作,則該執行緒的中斷狀態被設定,它將立即從選擇操作返回,並可能帶有一個非零值,就好像呼叫java.nio.channels.Selector.wakeup()方法一樣。
- 如果上述條件都不成立,則該執行緒的中斷狀態將被設定。