最近面試一些公司造壮,被問到的關(guān)于Java并發(fā)編程的問題,以及自己總結(jié)的回答绪商。
Java線程的狀態(tài)及如何轉(zhuǎn)換苛谷。
線程狀態(tài)及其轉(zhuǎn)換圖
多個線程之間如何協(xié)調(diào)?
- wait()格郁、notify()腹殿、notifyAll():這三個方法用于協(xié)調(diào)多個線程對共享數(shù)據(jù)的存取独悴,所以必須在同步語句塊內(nèi)使用。wait方法要等待notify/notifyAll的線程釋放鎖后才能開始繼續(xù)往下執(zhí)行锣尉。
// 等待方
synchronized(lockObj){
while(condition is false){
lockObj.wait();
}
// do business
}
// 通知方
synchronized(lockObj){
// change condition
lockObj.notifyAll();
}
說說Java的線程池是如何實現(xiàn)的刻炒?
- 創(chuàng)建線程要花費昂貴的資源和時間,如果任務(wù)來了才創(chuàng)建線程那么響應(yīng)時間會變長自沧,而且一個進(jìn)程能創(chuàng)建的線程數(shù)有限坟奥。為了避免這些問題,在程序啟動的時候就創(chuàng)建若干線程來響應(yīng)處理暂幼,它們被稱為線程池筏勒,里面的線程叫工作線程。
- maximumPoolSize和corePoolSize的區(qū)別:這個概念很重要旺嬉,maximumPoolSize為線程池最大容量管行,也就是說線程池最多能起多少Worker。corePoolSize是核心線程池的大小邪媳,當(dāng)corePoolSize滿了時捐顷,同時workQueue full(ArrayBolckQueue是可能滿的) 那么此時允許新建Worker去處理workQueue中的Task,但是不能超過maximumPoolSize雨效。超過corePoolSize之外的線程會在空閑超時后終止迅涮。可以通過beforeExecute和afterExecute實現(xiàn)線程池的監(jiān)聽徽龟;
線程池
線程池處理流程
public static ExecutorService newFixedThreadPool(int nThreads) {
return new ThreadPoolExecutor(nThreads, nThreads,
0L, TimeUnit.MILLISECONDS,
new LinkedBlockingQueue<Runnable>());
}
關(guān)于BlockingQueue和TransferQueue的異同叮姑。
- TransferQueue繼承了BlockingQueue并擴(kuò)展了一些新方法。BlockingQueue是Java 5中加入的接口据悔,它是指這樣的一個隊列:當(dāng)生產(chǎn)者向隊列添加元素但隊列已滿時传透,生產(chǎn)者會被阻塞;當(dāng)消費者從隊列移除元素但隊列為空時极颓,消費者會被阻塞朱盐。
- TransferQueue則更進(jìn)一步,生產(chǎn)者會一直阻塞直到所添加到隊列的元素被某一個消費者所消費(不僅僅是添加到隊列里就完事)菠隆,新添加的transfer方法用來實現(xiàn)這種約束兵琳。顧名思義,阻塞就是發(fā)生在元素從一個線程transfer到另一個線程的過程中骇径,它有效地實現(xiàn)了元素在線程之間的傳遞(以建立Java內(nèi)存模型中的happens-before關(guān)系的方式)躯肌。
- TransferQueue還包括了其他的一些方法:兩個tryTransfer方法,一個是非阻塞的既峡,另一個帶有timeout參數(shù)設(shè)置超時時間的羡榴。還有兩個輔助方法hasWaitingConsumer()和getWaitingConsumerCount()。
- TransferQueue相比SynchronousQueue用處更廣运敢、更好用校仑,因為你可以決定是使用BlockingQueue的方法(例如put方法)還是確保一次傳遞完成(即transfer方法)忠售。在隊列中已有元素的情況下,調(diào)用transfer方法迄沫,可以確保隊列中被傳遞元素之前的所有元素都能被處理稻扬。Doug Lea說從功能角度來講,LinkedTransferQueue實際上是ConcurrentLinkedQueue羊瘩、SynchronousQueue(公平模式)和LinkedBlockingQueue的超集泰佳。而且LinkedTransferQueue更好用,因為它不僅僅綜合了這幾個類的功能尘吗,同時也提供了更高效的實現(xiàn)逝她。
談?wù)凥ashMap的實現(xiàn)。
- 從結(jié)構(gòu)實現(xiàn)來講睬捶,HashMap是數(shù)組+鏈表+紅黑樹(JDK1.8增加了紅黑樹部分)實現(xiàn)的黔宛,如下如所示。
HashMap
從源碼可知擒贸,HashMap類中有一個非常重要的字段臀晃,就是 Node[] table,即哈希桶數(shù)組介劫;
Node是HashMap的一個內(nèi)部類徽惋,實現(xiàn)了Map.Entry接口,本質(zhì)是就是一個映射(鍵值對)座韵。
HashMap就是使用哈希表來存儲的险绘。為解決沖突,Java中HashMap采用了鏈地址法誉碴,簡單來說隆圆,就是數(shù)組加鏈表的結(jié)合。在每個數(shù)組元素上都一個鏈表結(jié)構(gòu)翔烁,當(dāng)數(shù)據(jù)被Hash后,得到數(shù)組下標(biāo)旨涝,把數(shù)據(jù)放在對應(yīng)下標(biāo)元素的鏈表上蹬屹。
Node[] table的初始化長度默認(rèn)值是16,Load factor為負(fù)載因子(默認(rèn)值是0.75)白华,threshold是HashMap所能容納的最大數(shù)據(jù)量的Node(鍵值對)個數(shù)慨默。threshold = length * Load factor。也就是說弧腥,在數(shù)組定義好長度之后厦取,負(fù)載因子越大,所能容納的鍵值對個數(shù)越多管搪。
- 確定哈希桶數(shù)組索引位置:取key的hashCode值虾攻、高位運算(通過hashCode()的高16位異或低16位實現(xiàn)的)铡买、取模運算。
static final int hash(Object key) {
int h;
return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}
數(shù)組索引位置
- HashMap的put方法執(zhí)行過程可以通過下圖來理解霎箍。
put方法執(zhí)行過程
public V put(K key, V value) {
return putVal(hash(key), key, value, false, true);
}
final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
boolean evict) {
Node<K,V>[] tab; Node<K,V> p; int n, i;
// 步驟①:tab為空則創(chuàng)建
if ((tab = table) == null || (n = tab.length) == 0)
n = (tab = resize()).length;
// 步驟②:計算index奇钞,并對null做處理
if ((p = tab[i = (n - 1) & hash]) == null)
tab[i] = newNode(hash, key, value, null);
else {
Node<K,V> e; K k;
// 步驟③:節(jié)點key存在,直接覆蓋value
if (p.hash == hash &&
((k = p.key) == key || (key != null && key.equals(k))))
e = p;
// 步驟④:判斷該鏈為紅黑樹
else if (p instanceof TreeNode)
e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
// 步驟⑤:該鏈為鏈表
else {
for (int binCount = 0; ; ++binCount) {
if ((e = p.next) == null) {
p.next = newNode(hash, key, value, null);
//鏈表長度大于8轉(zhuǎn)換為紅黑樹進(jìn)行處理
if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
treeifyBin(tab, hash);
break;
}
// key已經(jīng)存在直接覆蓋value
if (e.hash == hash &&
((k = e.key) == key || (key != null && key.equals(k))))
break;
p = e;
}
}
if (e != null) { // existing mapping for key
V oldValue = e.value;
if (!onlyIfAbsent || oldValue == null)
e.value = value;
afterNodeAccess(e);
return oldValue;
}
}
++modCount;
// 步驟⑥:超過最大容量 就擴(kuò)容
if (++size > threshold)
resize();
afterNodeInsertion(evict);
return null;
}
- 擴(kuò)容機(jī)制:擴(kuò)容(resize)就是重新計算容量漂坏,向HashMap對象里不停的添加元素景埃,而HashMap對象內(nèi)部的數(shù)組無法裝載更多的元素時,對象就需要擴(kuò)大數(shù)組的長度顶别,以便能裝入更多的元素谷徙。當(dāng)然Java里的數(shù)組是無法自動擴(kuò)容的,方法是使用一個新的數(shù)組代替已有的容量小的數(shù)組驯绎。在擴(kuò)充HashMap的時候完慧,不需要像JDK1.7的實現(xiàn)那樣重新計算hash,只需要看看原來的hash值新增的那個bit是1還是0就好了条篷,是0的話索引沒變骗随,是1的話索引變成“原索引+oldCap”。這個設(shè)計確實非常的巧妙赴叹,既省去了重新計算hash值的時間鸿染,而且同時,由于新增的1bit是0還是1可以認(rèn)為是隨機(jī)的乞巧,因此resize的過程涨椒,均勻的把之前的沖突的節(jié)點分散到新的bucket了。
final Node<K,V>[] resize() {
Node<K,V>[] oldTab = table;
int oldCap = (oldTab == null) ? 0 : oldTab.length;
int oldThr = threshold;
int newCap, newThr = 0;
if (oldCap > 0) {
// 超過最大值就不再擴(kuò)充了绽媒,就只好隨你碰撞去吧
if (oldCap >= MAXIMUM_CAPACITY) {
threshold = Integer.MAX_VALUE;
return oldTab;
}
// 沒超過最大值蚕冬,就擴(kuò)充為原來的2倍
else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY &&
oldCap >= DEFAULT_INITIAL_CAPACITY)
newThr = oldThr << 1; // double threshold
}
else if (oldThr > 0) // initial capacity was placed in threshold
newCap = oldThr;
else { // zero initial threshold signifies using defaults
newCap = DEFAULT_INITIAL_CAPACITY;
newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY);
}
// 計算新的resize上限
if (newThr == 0) {
float ft = (float)newCap * loadFactor;
newThr = (newCap < MAXIMUM_CAPACITY && ft < (float)MAXIMUM_CAPACITY ?
(int)ft : Integer.MAX_VALUE);
}
threshold = newThr;
@SuppressWarnings({"rawtypes","unchecked"})
Node<K,V>[] newTab = (Node<K,V>[])new Node[newCap];
table = newTab;
if (oldTab != null) {
// 把每個bucket都移動到新的buckets中
for (int j = 0; j < oldCap; ++j) {
Node<K,V> e;
if ((e = oldTab[j]) != null) {
oldTab[j] = null;
if (e.next == null)
newTab[e.hash & (newCap - 1)] = e;
else if (e instanceof TreeNode)
((TreeNode<K,V>)e).split(this, newTab, j, oldCap);
else { // preserve order
Node<K,V> loHead = null, loTail = null;
Node<K,V> hiHead = null, hiTail = null;
Node<K,V> next;
do {
next = e.next;
// 原索引
if ((e.hash & oldCap) == 0) {
if (loTail == null)
loHead = e;
else
loTail.next = e;
loTail = e;
}
// 原索引+oldCap
else {
if (hiTail == null)
hiHead = e;
else
hiTail.next = e;
hiTail = e;
}
} while ((e = next) != null);
// 原索引放到bucket里
if (loTail != null) {
loTail.next = null;
newTab[j] = loHead;
}
// 原索引+oldCap放到bucket里
if (hiTail != null) {
hiTail.next = null;
newTab[j + oldCap] = hiHead;
}
}
}
}
}
return newTab;
}
談?wù)劸€程安全的ConcurrentHashMap的實現(xiàn)原理。
- ConcurrentHashMap在jdk1.8中主要做了2方面的改進(jìn):改進(jìn)一是取消segments字段是辕,直接采用transient volatile HashEntry<K,V>[] table保存數(shù)據(jù)囤热,采用table數(shù)組元素作為鎖,從而實現(xiàn)了對每一行數(shù)據(jù)進(jìn)行加鎖获三,進(jìn)一步減少并發(fā)沖突的概率旁蔼;改進(jìn)二是將原先table數(shù)組+單向鏈表的數(shù)據(jù)結(jié)構(gòu),變更為table數(shù)組+單向鏈表+紅黑樹的結(jié)構(gòu)疙教,對于hash表來說棺聊,最核心的能力在于將key hash之后能均勻的分布在數(shù)組中,如果hash之后散列的很均勻贞谓,那么table數(shù)組中的每個隊列長度主要為0或者1限佩。但實際情況并非總是如此理想,雖然ConcurrentHashMap類默認(rèn)的加載因子為0.75,但是在數(shù)據(jù)量過大或者運氣不佳的情況下祟同,還是會存在一些隊列長度過長的情況作喘,如果還是采用單向列表方式,那么查詢某個節(jié)點的時間復(fù)雜度為O(n)耐亏;因此徊都,對于個數(shù)超過8(默認(rèn)值)的列表,jdk1.8中采用了紅黑樹的結(jié)構(gòu)广辰,那么查詢的時間復(fù)雜度可以降低到O(logN)暇矫,可以改進(jìn)性能。
- TreeNode類:樹節(jié)點類择吊,另外一個核心的數(shù)據(jù)結(jié)構(gòu)李根。當(dāng)鏈表長度過長的時候,會轉(zhuǎn)換為TreeNode几睛。但是與HashMap不相同的是房轿,它并不是直接轉(zhuǎn)換為紅黑樹,而是把這些結(jié)點包裝成TreeNode放在TreeBin對象中所森,由TreeBin完成對紅黑樹的包裝囱持。而且TreeNode在ConcurrentHashMap繼承自Node類,而并非HashMap中的集成自LinkedHashMap.Entry焕济。
- 二叉查找樹纷妆,也稱有序二叉樹(ordered binary tree),是指一棵空樹或者具有下列性質(zhì)的二叉樹:
1.若任意節(jié)點的左子樹不空晴弃,則左子樹上所有結(jié)點的值均小于它的根結(jié)點的值掩幢;
2.若任意節(jié)點的右子樹不空,則右子樹上所有結(jié)點的值均大于它的根結(jié)點的值上鞠;
3.任意節(jié)點的左际邻、右子樹也分別為二叉查找樹。
4.沒有鍵值相等的節(jié)點(no duplicate nodes)芍阎。
- 紅黑樹雖然本質(zhì)上是一棵二叉查找樹世曾,但它在二叉查找樹的基礎(chǔ)上增加了著色和相關(guān)的性質(zhì)使得紅黑樹相對平衡,從而保證了紅黑樹的查找谴咸、插入度硝、刪除的時間復(fù)雜度最壞為O(log n)。但它是如何保證一棵n個結(jié)點的紅黑樹的高度始終保持在logn的呢寿冕?這就引出了紅黑樹的5個性質(zhì):
1.每個結(jié)點要么是紅的要么是黑的。
2.根結(jié)點是黑的椒袍。
3.每個葉結(jié)點(葉結(jié)點即指樹尾端NIL指針或NULL結(jié)點)都是黑的驼唱。
4.如果一個結(jié)點是紅的,那么它的兩個兒子都是黑的驹暑。
5.對于任意結(jié)點而言玫恳,其到葉結(jié)點樹尾端NIL指針的每條路徑都包含相同數(shù)目的黑結(jié)點辨赐。
什么是一致性哈希?
- 環(huán)形Hash空間:按照常用的hash算法來將對應(yīng)的key哈希到一個具有232次方個桶的空間中京办,即0~(232)-1的數(shù)字空間中∠菩颍現(xiàn)在我們可以將這些數(shù)字頭尾相連,想象成一個閉合的環(huán)形惭婿;
- 把數(shù)據(jù)通過一定的hash算法處理后映射到環(huán)上不恭;
- 將機(jī)器通過hash算法映射到環(huán)上(一般情況下對機(jī)器的hash計算是采用機(jī)器的IP或者機(jī)器唯一的別名作為輸入值),然后以順時針的方向計算财饥,將所有對象存儲到離自己最近的機(jī)器中换吧;
- 機(jī)器的刪除與添加:普通hash求余算法最為不妥的地方就是在有機(jī)器的添加或者刪除之后會照成大量的對象存儲位置失效,這樣就大大的不滿足單調(diào)性了钥星。通過對節(jié)點的添加和刪除的分析沾瓦,一致性哈希算法在保持了單調(diào)性的同時,還是數(shù)據(jù)的遷移達(dá)到了最小谦炒,這樣的算法對分布式集群來說是非常合適的贯莺,避免了大量數(shù)據(jù)遷移,減小了服務(wù)器的的壓力宁改。
- 平衡性:在一致性哈希算法中缕探,為了盡可能的滿足平衡性,其引入了虛擬節(jié)點透且。它實際上是節(jié)點在hash空間的復(fù)制品撕蔼,一實際個節(jié)點對應(yīng)了若干個“虛擬節(jié)點”,這個對應(yīng)個數(shù)也成為“復(fù)制個數(shù)”秽誊,“虛擬節(jié)點”在hash空間中以hash值排列鲸沮。
一致性哈希
Java有哪些實現(xiàn)鎖的方式?
- synchronized同步鎖:它無法中斷一個正在等候獲得鎖的線程锅论,也無法通過投票得到鎖讼溺,如果不想等下去,也就沒法得到鎖最易。但除非對鎖的某個高級特性有明確的需要怒坯,或者有明確的證據(jù)表明在特定情況下,同步已經(jīng)成為瓶頸藻懒,否則還是應(yīng)當(dāng)繼續(xù)使用synchronized剔猿。
- volatile是比synchronized更輕量,因為它沒有上下文切換嬉荆;其實現(xiàn)是通過lock指令將緩存行數(shù)據(jù)寫到系統(tǒng)內(nèi)存且讓其他緩存數(shù)據(jù)無效归敬;
- ReentrantLock可以支持公平鎖,當(dāng)然公平鎖性能會有影響,默認(rèn)為非公平的汪茧;
// 對于非公平鎖椅亚,會執(zhí)行該方法:
final boolean nonfairTryAcquire(int acquires) {
final Thread current = Thread.currentThread();
int c = getState();//獲取狀態(tài)變量
if (c == 0) {//表明沒有線程占有該同步狀態(tài)
if (compareAndSetState(0, acquires)) {//以原子方式設(shè)置該同步狀態(tài)
setExclusiveOwnerThread(current);//該線程擁有該FairSync同步狀態(tài)
return true;
}
}
else if (current == getExclusiveOwnerThread()) {//當(dāng)前線程已經(jīng)擁有該同步狀態(tài)
int nextc = c + acquires;
if (nextc < 0) // overflow
throw new Error("Maximum lock count exceeded");
setState(nextc);//重復(fù)設(shè)置狀態(tài)變量(鎖的可重入特性)
return true;
}
return false;
}
// 而對于公平鎖,該方法則是這樣:
protected final boolean tryAcquire(int acquires) {
final Thread current = Thread.currentThread();
int c = getState();
if (c == 0) {
//先判斷該線程節(jié)點是否是隊列的頭結(jié)點
//是則以原子方式設(shè)置同步狀態(tài)舱污,獲取鎖
//否則失敗返回
if (!hasQueuedPredecessors() && compareAndSetState(0, acquires)) {
setExclusiveOwnerThread(current);
return true;
}
}
else if (current == getExclusiveOwnerThread()) {//重入
int nextc = c + acquires;
if (nextc < 0)
throw new Error("Maximum lock count exceeded");
setState(nextc);
return true;
}
return false;
}
- AQS管理的FIFO等待隊列呀舔,獲取鎖狀態(tài)失敗的線程會被放入該隊列,等待再次嘗試獲取鎖扩灯。而state成員變量媚赖,代表著鎖的同步狀態(tài),一個線程成功獲得鎖驴剔,這個行為的實質(zhì)就是該線程成功的設(shè)置了state變量的狀態(tài)省古。
public final void acquire(int arg) {
if (!tryAcquire(arg) && acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
selfInterrupt();
}
private Node addWaiter(Node mode) {
Node node = new Node(Thread.currentThread(), mode);
// Try the fast path of enq; backup to full enq on failure
Node pred = tail;
// 這個if分支其實是一種優(yōu)化:CAS操作失敗的話才進(jìn)入enq中的循環(huán)。
if (pred != null) {
node.prev = pred;
if (compareAndSetTail(pred, node)) {
pred.next = node;
return node;
}
}
enq(node);
return node;
}
private Node enq(final Node node) {
for (;;) {
Node t = tail;
if (t == null) { // Must initialize
if (compareAndSetHead(new Node()))
tail = head;
} else {
node.prev = t;
if (compareAndSetTail(t, node)) {
t.next = node;
return t;
}
}
}
}
final boolean acquireQueued(final Node node, int arg) {
boolean failed = true;
try {
boolean interrupted = false;
for (;;) {
final Node p = node.predecessor();
if (p == head && tryAcquire(arg)) {
setHead(node);
p.next = null; // help GC
failed = false;
return interrupted;
}
if (shouldParkAfterFailedAcquire(p, node) && parkAndCheckInterrupt())
interrupted = true;
}
} finally {
if (failed)
cancelAcquire(node);
}
}
- ReentrantReadWriteLock對重入鎖再進(jìn)一步分離為讀鎖和寫鎖丧失,在讀多寫少的場景下能顯著提升性能豺妓。
- ReadLock可以被多個線程持有并且在作用時排斥任何的WriteLock,而WriteLock則是完全的互斥布讹。
- 寫線程獲取寫入鎖后可以獲取讀取鎖琳拭,然后釋放寫入鎖,這樣就從寫入鎖變成了讀取鎖描验,從而實現(xiàn)鎖降級的特性白嘁。讀取鎖是不能直接升級為寫入鎖的。因為獲取一個寫入鎖需要釋放所有讀取鎖膘流,所以如果有兩個讀取鎖視圖獲取寫入鎖而都不釋放讀取鎖時就會發(fā)生死鎖絮缅。
- 如果讀取執(zhí)行情況很多,寫入很少的情況下呼股,使用ReentrantReadWriteLock可能會使寫入線程遭遇饑餓問題耕魄,也就是寫入線程吃吃無法競爭到鎖定而一直處于等待狀態(tài)。
- StampedLock控制鎖有三種模式(寫彭谁,讀吸奴,樂觀讀),一個StampedLock狀態(tài)是由版本和模式兩個部分組成缠局,鎖獲取方法返回一個數(shù)字作為票據(jù)stamp则奥,它用相應(yīng)的鎖狀態(tài)表示并控制訪問,數(shù)字0表示沒有寫鎖被授權(quán)訪問狭园。在讀鎖上分為悲觀鎖和樂觀鎖读处。所謂的樂觀讀模式,也就是若讀的操作很多唱矛,寫的操作很少的情況下档泽,你可以樂觀地認(rèn)為俊戳,寫入與讀取同時發(fā)生幾率很少,因此不悲觀地使用完全的讀取鎖定馆匿,程序可以查看讀取資料之后,是否遭到寫入執(zhí)行的變更燥滑,再采取后續(xù)的措施(重新讀取變更信息渐北,或者拋出異常) ,這一個小小改進(jìn)铭拧,可大幅度提高程序的吞吐量赃蛛!
class Point {
private double x, y;
private final StampedLock sl = new StampedLock();
void move(double deltaX, double deltaY) { // an exclusively locked method
long stamp = sl.writeLock();
try {
x += deltaX;
y += deltaY;
} finally {
sl.unlockWrite(stamp);
}
}
//下面看看樂觀讀鎖案例
double distanceFromOrigin() { // A read-only method
long stamp = sl.tryOptimisticRead(); //獲得一個樂觀讀鎖
double currentX = x, currentY = y; //將兩個字段讀入本地局部變量
if (!sl.validate(stamp)) { //檢查發(fā)出樂觀讀鎖后同時是否有其他寫鎖發(fā)生?
stamp = sl.readLock(); //如果沒有搀菩,我們再次獲得一個讀悲觀鎖
try {
currentX = x; // 將兩個字段讀入本地局部變量
currentY = y; // 將兩個字段讀入本地局部變量
} finally {
sl.unlockRead(stamp);
}
}
return Math.sqrt(currentX * currentX + currentY * currentY);
}
//下面是悲觀讀鎖案例
void moveIfAtOrigin(double newX, double newY) { // upgrade
// Could instead start with optimistic, not read mode
long stamp = sl.readLock();
try {
while (x == 0.0 && y == 0.0) { //循環(huán)呕臂,檢查當(dāng)前狀態(tài)是否符合
long ws = sl.tryConvertToWriteLock(stamp); //將讀鎖轉(zhuǎn)為寫鎖
if (ws != 0L) { //這是確認(rèn)轉(zhuǎn)為寫鎖是否成功
stamp = ws; //如果成功 替換票據(jù)
x = newX; //進(jìn)行狀態(tài)改變
y = newY; //進(jìn)行狀態(tài)改變
break;
}
else { //如果不能成功轉(zhuǎn)換為寫鎖
sl.unlockRead(stamp); //我們顯式釋放讀鎖
stamp = sl.writeLock(); //顯式直接進(jìn)行寫鎖 然后再通過循環(huán)再試
}
}
} finally {
sl.unlock(stamp); //釋放讀鎖或?qū)戞i
}
}
}
AtomicLong、LongAdder和LongAccumulator的實現(xiàn)有何不同肪跋?
- AtomicLong是Java 5引入的基于CAS的無鎖的操作長整形值的工具類歧蒋;
- LongAdder是Java 8提供的累加器,基于Striped64實現(xiàn)州既。它常用于狀態(tài)采集谜洽、統(tǒng)計等場景。AtomicLong也可以用于這種場景吴叶,但在線程競爭激烈的情況下阐虚,LongAdder要比AtomicLong擁有更高的吞吐量,但會耗費更多的內(nèi)存空間蚌卤。
- Striped64的設(shè)計核心思路就是通過內(nèi)部的分散計算來避免競爭(比如多線程CAS操作時的競爭)实束。Striped64內(nèi)部包含一個基礎(chǔ)值和一個單元哈希表。沒有競爭的情況下逊彭,要累加的數(shù)會累加到這個基礎(chǔ)值上咸灿;如果有競爭的話,會將要累加的數(shù)累加到單元哈希表中的某個單元里面诫龙。所以整個Striped64的值包括基礎(chǔ)值和單元哈希表中所有單元的值的總和析显。
/**
* 存放Cell的hash表,大小為2的冪签赃。
*/
transient volatile Cell[] cells;
/**
* 基礎(chǔ)值谷异,沒有競爭時會使用(更新)這個值,同時做為初始化競爭失敗的回退方案锦聊。
* 原子更新歹嘹。
*/
transient volatile long base;
/**
* 自旋鎖,通過CAS操作加鎖孔庭,用于保護(hù)創(chuàng)建或者擴(kuò)展Cell表尺上。
*/
transient volatile int cellsBusy;
- LongAccumulator和LongAdder類似材蛛,也基于Striped64實現(xiàn)。但要比LongAdder更加靈活(要傳入一個函數(shù)接口)怎抛,LongAdder相當(dāng)于是LongAccumulator的一種特例卑吭。
CompletableFuture對比Future有哪些改進(jìn),怎么用马绝?
- Future對象代表一個尚未完成異步操作的結(jié)果豆赏。從Java 5以來,JUC包一直提供著最基本的Future富稻,不過它太雞肋了掷邦,除了get、cancel椭赋、isDone和isCancelled方法之外就沒有其他的操作了抚岗,對于結(jié)果的獲取很不方便,只能通過阻塞或者輪詢的方式得到任務(wù)的結(jié)果哪怔。阻塞的方式顯然和我們的異步編程的初衷相違背宣蔚,輪詢的方式又會耗費無謂的CPU資源,而且也不能及時地得到計算結(jié)果蔓涧,這樣很不方便件已。
- 好在Java 8中引入了具有函數(shù)式風(fēng)格的CompletableFuture,支持一系列的函數(shù)式的組合元暴、運算操作篷扩,非常方便,可以寫出函數(shù)式風(fēng)格的代碼而擺脫callback hell茉盏。
- 主動完成計算:CompletableFuture類實現(xiàn)了CompletionStage和Future接口鉴未,所以你還是可以像以前一樣通過阻塞或者輪詢的方式獲得結(jié)果,盡管這種方式不推薦使用鸠姨。
- 主要的API如下所示:
supplyAsync/runAsync -- 創(chuàng)建CompletableFuture對象铜秆;
whenComplete/whenCompleteAsync/exceptionally -- 計算完成或者拋出異常的時可以執(zhí)行特定的Action债沮;
thenApply/thenApplyAsync -- 對數(shù)據(jù)進(jìn)行一些處理或變換畴椰;
thenAccept/thenAcceptAsync -- 純消費上祈,不返回新的計算值靠胜;
thenAcceptBoth/thenAcceptBothAsync/runAfterBoth -- 當(dāng)兩個CompletionStage都正常完成計算的時候,就會執(zhí)行提供的Action呻右;
thenCompose/thenComposeAsync -- 這個Function的輸入是當(dāng)前的CompletableFuture的計算值傲隶,返回結(jié)果將是一個新的CompletableFuture英上。
記住祟峦,thenCompose返回的對象并不一是函數(shù)fn返回的對象罚斗,如果原來的CompletableFuture還沒有計算出來,
它就會生成一個新的組合后的CompletableFuture宅楞≌胱耍可以用來實現(xiàn)異步pipline袱吆;
thenCombine/thenCombineAsync - 并行執(zhí)行的,它們之間并沒有先后依賴順序距淫,和thenAcceptBoth的區(qū)別在于有返回值绞绒;
allOf/anyOf -- 所有的/其中一個CompletableFuture都執(zhí)行完后執(zhí)行計算;
其他面試小結(jié)
掃一掃 關(guān)注我的微信公眾號