一、Java中的同步容器類和缺陷java
在Java中,同步容器主要包括2類:編程
1)Vector、HashTable。數組
2)Collections類中提供的靜態工廠方法建立的類。Collections.synchronizedXXX()。緩存
缺陷:安全
1)性能問題。多線程
在有多個線程進行訪問時,若是多個線程都只是進行讀取操做,那麼每一個時刻就只能有一個縣城進行讀取,其餘線程便只能等待,這些線程必須競爭同一把鎖。併發
2)ConcurrentModificationException異常。工具
在對Vector等容器進行迭代修改時,可是在併發容器中(如ConcurrentHashMap,CopyOnWriteArrayList等)不會出現這個問題。性能
二、爲何說ConcurrentHashMap是弱一致性的?以及爲什麼多個線程併發修改ConcurrentHashMap時不會報ConcurrentModificationException?優化
1)ConcurrentHashMap #get()
正是由於GET操做幾乎全部時候都是一個無鎖操做(GET中有一個readValueUnderLock調用,不過這句執行到的概率極小),使得同一個Segment實例上的PUT和GET能夠同時進行,這就是GET操做是弱一致的根本緣由。
2)ConcurrentHashMap #clear()
public void clear(){
for(int i=0;i<segments.length;++i)
segments[i].clear;
}
由於沒有全局的鎖,在消除完一個segment以後,正在清理下一個segment的時候,已經清理的segment可能又被加入了數據,所以clear返回的時候,ConcurrentHashMap中是可能存在數據的。所以,clear方法是弱一致的。
ConcurrentHashMap中的迭代器
在遍歷過程當中,若是已經遍歷的數組上的內容變化了,迭代器不會拋出ConcurrentModificationException異常。若是未遍歷的數組上的內容發生了變化,則有可能反映到迭代過程當中。這就是ConcurrentHashMap迭代器弱一致的表現。
在這種迭代方式中,當iterator被建立後,集合再發生改變就再也不是拋出ConcurrentModificationException,取而代之的是在改變時new新的數據從而不影響原有的數據,iterator完成後再將頭指針替換爲新的數據,這樣iterator線程可使用原來老的數據,而寫線程也能夠併發的完成改變,更重要的,這保證了多個線程併發執行的連續性和擴展性,是性能提高的關鍵。
總結,ConcurrentHashMap的弱一致性主要是爲了提高效率,是一致性與效率之間的一種權衡。要成爲強一致性,就獲得處使用鎖,甚至是全局鎖,這就與HashTable和同步的HashMap同樣了。
三、CopyOnWriteArrayList的實現原理
CopyOnWrite容器即寫時複製的容器,也就是當咱們往一個容器添加元素的時候,不直接往當前容器添加,而是先將當前容器進行Copy,複製出一個新的容器,而後新的容器裏添加元素,添加完元素以後,再將原容器的引用指向新的容器(改變引用的指向)。這樣作的好處是咱們能夠對CopyOnWrite容器進行併發的讀,而不須要加鎖,由於當前容器不會添加任何元素。因此CopyOnWrite容器也是一種讀寫分離的思想,讀和寫在不一樣的容器上進行,注意,寫的時候須要加鎖。
1)一下代碼是向CopyOnWriteArrayList中add方法的實現,能夠發如今添加的時候是須要加鎖的,不然多線程寫的時候會Copy出N個副本。
public boolean add(E e){ final ReentrantLock lock = this.lock;//加的是lock鎖
lock.lock(); try{ Object[] elements = getArray(); int len = elements.length; Object[] newElements = Arrays.copyOf(elements,len+1); newElements[len]=e; setArray(newElements);//將原容器的引用指向新的容器;
return true; }finally{ lock.unlock(); } }
在CopyOnWriteArrayList裏處理寫操做(包括add,remove,set等)是先將原始的數據經過Arrays.copyof()來生成一份新的數據,而後再新的數據對象上進行寫,寫完後再將原來的引用指向到當前這個數據對象,這樣保證了每次寫都是在新的對象上。而後讀的時候就是在引用的當前對象上進行讀(包括get,iterator等),不存在加鎖和阻塞。
CopyOnWriteArrayList中寫操做須要大面積複製數組,因此性能確定不好,可是讀操做由於操做的對象和寫操做不是同一個對象,讀之間也不須要加鎖,讀和寫之間的同步處理只是在寫完後經過一個簡單的「=」將引用指向新的數組對象上來,這個幾乎不須要時間,這樣讀操做就很快很安全,適合在多線程裏使用。
2)讀的時候不須要加鎖,若是讀的時候有線程正在向CopyOnWriteArrayList添加數據,讀仍是會讀到舊的數據(在原容器中進行讀)。
public E get(int index){ return get(getArray(),index); }
CopyOnWriteArrayList在讀上效率很高,因爲,寫的時候每次都要將源數組複製到一個新的數組中,因此寫的效率不高。
CopyOnWriteArrayList容器有不少優勢,可是同時也存在兩個問題,即內存佔用問題和數據一致性的問題。
1)內存佔用問題。由於CopyOnWrite的寫時複製機制,因此在進行寫操做的時候,內存裏會同時駐紮兩個對象的內存,舊的對象和新寫入的對象。針對內存佔用問題,能夠
a. 經過壓縮容器中的元素的方法來減小大對象的內存消耗,好比,若是元素全是10進制的數字,能夠考慮把它壓縮成36進制或64進制。
b. 不使用CopyOnWrite容器,而使用其餘的併發容器,如ConcurrentHashMap。
2)數據一致性問題。CopyOnWrite容器只能保證數據的最終一致性,不能保證數據的實時一致性。因此若是你但願寫入的數據,立刻能讀到,請不要使用CopyOnWrite容器!!
四、Java中堆和棧有什麼不一樣?
棧是一塊和線程緊密相關的內存區域。每一個線程都有本身的棧內存,用於存儲本地變量,方法參數和棧調用,一個線程中存儲的變量對其餘線程是不可見的。而堆是全部線程共享的一片公用內存區域。對象都在堆裏建立,爲了提高效率,縣城會從堆中弄一個緩存到本身的棧,若是多個線程使用該變量就可能引起問題,這時volatile變量就能夠發揮做用了。它要求線程從主存中讀取變量的值。
五、Java中的活鎖、死鎖、飢餓有什麼區別?
死鎖:是指兩個或兩個以上的進程在執行過程當中,因爭奪資源而形成的一種互相等待的現象,若無外力做用,它們都將沒法推動下去,此時稱系統處於死鎖狀態或系統產生了死鎖。
飢餓:考慮一臺打印機分配的例子,當有多個進程須要打印文件時,系統按照短文件優先的策略排序,該策略具備平均等待時間短的優勢,彷佛很是合理,但當短文件打印任務源源不斷時,長文件的打印任務將被無限期地推遲,致使飢餓以致餓死。
活鎖:與飢餓相關的另一個概念稱爲活鎖,在忙式等待條件下發生的飢餓,稱爲活鎖。
不進入等待狀態的等待稱爲忙式等待。另外一種等待方式是阻塞式等待,進程得不到共享資源時將進入阻塞狀態,讓出CPU給其餘進程使用。忙等待和阻塞式等待的相通之處在於進程都不具有繼續向前推動的條件,不一樣之處在於忙等待的進程不主動放棄CPU,儘管CPU可能被剝奪,於是是低效的;而處於阻塞狀態的進程主動放棄CPU,於是是高效的。
活鎖的例子:若是事務T1封鎖了數據R,事務T2又請求封鎖R,因而T2等待。T3也請求封鎖R,當T1釋放了R上的封鎖後,系統首先批准了T3的請求,T2仍然等待。而後T4又請求封鎖R,當T3釋放了R上的封鎖以後,系統有批准了T4的請求......T2可能永遠等待(在整個過程當中,事務T2在不斷的重複嘗試獲取鎖R)。
活鎖的時候,進程是不會阻塞的,這會致使耗盡CPU資源,這是與死鎖最明顯的區別。
活鎖指的是任務或執行者沒有被阻塞,因爲某些條件沒有知足,致使一直重複嘗試,失敗,嘗試,失敗。活鎖和死鎖的區別在於,處於活鎖的實體是在不斷地改變狀態,所謂的「活」,而處於死鎖的實體表現爲等待;活鎖有必定概率解開,而死鎖是沒法解開的。
避免活鎖的簡單方法是採用先來先服務的策略。當多個事務請求封鎖同一數據對象時,封鎖子系統按請求封鎖的前後次序對事務排隊,數據對象上的鎖一旦釋放就批准申請隊列中第一個事務得到鎖。
六、實現線程之間的通訊?
當線程間是能夠共享資源時,線程間通訊是協調它們的重要的手段。
1)Object 類中wait()、notify()、notifyAll()方法。
2)用Condition接口。
Condition是被綁定到Lock上的,要建立一個Lock的Condition對象必須用newCondition()方法。在一個Lock對象裏面能夠建立多個Condition對象,線程能夠註冊在指定的Condition對象中,從而能夠有選擇性地進行線程通知,在線程調度上更加靈活。
在Condition中,用await()替換wait(),用signal替換notify(),用signalAll()替換notifyAll(),傳統線程的通訊方式,Condition均可以實現。調用Condition對象中的方法中,須要被包含在lock()和unlock()之間。
3)管道實現線程間的通訊
實現方式:一個縣城發送數據到輸出管道流,另外一個線程從輸入管道流中讀取數據。
基本流程:
1> 建立管道輸出流PipedOutputStream pos 和管道輸入流 PipedInputStream pis。
2> 將pos和pis匹配,pos.connect(pis)。
3> 將pos賦給輸入信息的線程,pis賦給獲取信息的線程,就能夠實現線程間的通信了。
缺點:
1> 管道流只能在兩個線程之間傳遞數據。
線程consumer1 和 consumer2同時從pis中read數據,當線程producer往管道流中寫入一段數據(1,2,3,4,5,6)後,每個時刻只有一個線程能獲取到數據,並非兩個線程都能獲取到producer發送來的數據,所以一個管道流只能用於兩個線程間的通信。
2> 管道流只能實現單向發送,若是要兩個線程之間互通信,則須要兩個管道流。
線程producer經過管道流向線程consumer發送數據,若是線程consumer想給線程producer發送數據,則須要新建另外一個管道流pos1和pis1,將pos1賦給consumer1,將pis1賦給producer。
4)使用volatile 關鍵字。
見之前內容。
七、如何確保線程安全?
若是多個線程同時運行某段代碼,若是每次運行結果和單線程運行的結果是同樣的,並且其餘變量的值也和預期的是同樣的,就是線程安全的。
synchronized,Lock,原子類(如atomicInteger等),同步容器,併發容器,阻塞隊列,同步輔助類(好比CountDownLatch,Semaphore,CyclicBarrier)。
八、多線程的優勢和缺點?
優勢:
1)充分利用CPU,避免CPU空轉。
2)程序響應更快。
缺點:
1)上下文切換的開銷
當CPU從執行一個線程切換到執行另一個線程時,它須要先存儲當前線程的本地數據,程序指針等,而後載入另一個線程的本地數據,程序指針等,最後纔開始執行。這種切換稱爲「上下文切換」。CPU會在一個上下文中執行一個線程,而後切換到另一個上下文中執行另一個線程。上下文切換並不廉價。若是沒有必要,應該減小上下文切換的發生。
2)增長資源消耗
線程在運行時須要從計算機裏面獲得一些資源。除了CPU,線程還須要一些內存來維持它本地的堆棧。它也須要佔用操做系統中一些資源來管理線程。
3)編程更加複雜
在多線程訪問共享數據時,要考慮線程安全問題。
九、寫出3條你遵循的多線程最佳實踐
1)給線程起個有意義的名字。
2)避免鎖定和縮小同步的範圍
相對於同步方法我更喜歡同步塊,它給我擁有對鎖的絕對控制權。
3)多用同步輔助類,少用wait和notify。
首先,CountDownLatch,Semaphore,CyclicBarrier這些同步輔助類簡化了編碼操做,而用wait和notify很難實現對複雜控制流的控制。其次,這些類是由最好的企業編寫和維護在後續的JDK中它們還會不斷優化和完善,使用這些更高等級的同步工具你的程序能夠不費吹灰之力得到優化。
4)多用併發容器,少用同步容器。
若是下一次你須要用到map,你應該首先想到用ConcurrentHashMap。
十、多線程的性能必定就優於單線程嗎?
不必定,要看具體的任務以及計算機的配置。好比說:
對於單核CPU,若是是CPU密集型任務,如解壓文件,多線程的性能反而不如單線程性能,由於解壓文件須要一直佔用CPU資源,若是採用多線程,線程切換致使的開銷反而會讓性能降低。若是是交互類型的任務,確定是須要使用多線程的。
對於多核CPU,對於解壓文件來講,多線程確定優於單線程,由於多個線程可以更加充分利用每一個核的資源。
十一、怎麼檢測一個線程是否擁有鎖?
在java.lang.Thread中有一個方法叫 holdsLock(Object obj),它返回true,若是當且僅當當前線程擁有某個具體對象的鎖。
十二、什麼是線程調度器?
線程調度器是一個操做系統服務,它負責爲Runnable狀態的線程分配CPU時間。一旦咱們建立一個線程並啓動它,它的執行便依賴於線程調度器的實現。
1三、Java程序如何中止一個線程?
建議使用「異常法」來終止線程的繼續運行。在想要被中斷執行的線程中,調用interrupted()方法,該方法用來檢驗當前線程是否已經被中斷,即該線程是否被打上了中斷的標記,並不使得線程當即中止運行,若是返回true,則拋出異常,中止線程的運行。在線程外,調用interrupt()方法,使得該線程打上中斷的標記。