① java在編程上有什麼安全缺陷
在Java平台上進行多線程編程的缺陷
就其自身來說,並發編程是一種技術,提供了操作的同時執行,不論是在單一系統上還是分布在大量系統上。這類操作實際是一些指令順序,例如單獨某個頂級任務的子任務,這類操作能夠並行執行,或者是作為線程,或者是作為進程。線程和進程之間的本質區別在於:進程通常是獨立的(例如獨立的地址空間),所以只能通過系統提供的進程間通信機制進行交互,而線程通常共享單一進程的狀態信息,能夠直接共享系統資源和內存中的對象。
可以使用下面兩種方法之一,通過多個進程來實現並發。第一種方法是在同一個處理器上運行進程,由操作系統處理進程之間的上下文環境切換。(可以理解,這種切換要比同一進程內多線程之間的上下文環境切換更慢。)第二種方法是構建大規模的並行和復雜的分布式系統,在不同的物理處理器上運行多個進程。
從內建支持的角度來說,Java 語言通過線程提供並發編程;每個 JVM 都能支持許多線程同時執行。可以用以下兩種方法之一在 Java 語言中創建線程:
繼承 java.lang.Thread 類。在這種情況下,已經重寫的子類的 run() 方法必須包含實現線程運行時行為的代碼。要執行這個代碼,需要實例化子類對象,然後調用對象的 start() 方法,這樣就可以在內部執行 run() 方法了。
創建 Runnable 介面的定製實現。這個介面只包含一個 run() 方法,在這個方法中,要放置應用程序代碼。要執行這個代碼,需要實例化實現類的對象,然後在創建新 Thread 時,把對象作為構造函數的參數傳入。然後調用新創建的線程對象的 start() 方法,開始執行控制的新線程。
線程安全性和同步
如果 Java 對象中的某個方法能夠安全地運行在多線程環境中,那麼就稱該方法是 線程安全的。要獲得這種安全性,必須有一種機制,通過該機制,運行同一方法的多個線程就能夠同步其操作,這樣,在訪問相同的對象或代碼行時,就會只允許一個線程被處理。這種同步要求線程使用叫作 信號 的對象彼此進行溝通。
有一種類型的信號叫作 互斥信號 或 互斥體。顧名思義,這個信號對象的擁有權是互斥的,也就是說,在任意指定時間,只有一個線程能夠擁有互斥體。其他想獲得所有權的線程會被阻塞,它們必須等待,直到擁有互斥體的線程釋放互斥體。如果多個線程按順序排隊等候同一互斥體,那麼在當前擁有者釋放它的時候,只有一個等候線程能夠得到它;其他線程將繼續阻塞。
在 1970 年代初,C.A.R. Hoare 和其他人共同開發了一個叫作 監視器 的概念。一個 監視器 就是一個代碼主體,它的訪問受到互斥體的保護。任何想執行這個代碼的線程,都必須在代碼塊頂部得到關聯的互斥體,然後在底部再釋放它。因為在指定時間只有一個線程能夠擁有互斥體,所以這就有效地保證了只有擁有它的線程才能執行監視器的代碼塊。(受保護的代碼不需要相鄰 —— 例如,Java 語言中的每個對象都有一個與之關聯的監視器。)
任何想在 Java 語言中進行線程編程的開發人員,都會立即把上面的內容當成 synchronized 關鍵字所帶來的效果。可以確保包含在 synchronized 塊中的 Java 代碼在指定時間只被一個線程執行。在內部,可以由運行時將 synchronized 關鍵字轉換成某一種情況:所有的競爭線程都試圖獲得與它們(指線程)正在操作的對象實例關聯的那個(惟一的一個)互斥體。成功得到互斥體的線程將運行代碼,然後在退出 synchronized 塊時釋放互斥體。
等候和通知
wait/notify 構造在 Java 語言的線程間通信機制中也扮演了重要的角色。基本的想法是:一個線程需要的某個條件可以由另外一個線程促成。這樣,條件的 wait 就可以得到滿足。一旦條件為真,那麼引發條件的線程就會 notify 等候線程蘇醒,並從中止的地方繼續進行。
wait/notify 機制要比 synchronized 機制更難理解和判斷。要想判斷出使用 wait/notify 的方法的行為邏輯,就要求判斷出使用它的所有方法的邏輯。一次判斷一個方法,把該方法和其他方法隔離開,是對整體系統行為得出錯誤結論的可靠方式。顯然,這樣做的復雜性會隨著要判斷的方法的數量增長而迅速提高。
線程狀態
我前面提到過,必須調用新創建的線程的 start() 方法來啟動它的執行。但是,僅僅是調用 start() 方法並不意味著線程會立即開始運行。這個方法只是把線程的狀態從 new 變成 runnable。只有在操作系統真正安排線程執行的時候,線程狀態才會變成 running (從 runnable)。
典型的操作系統支持兩種線程模型 —— 協作式和搶占式。在協作式 模型中,每個線程對於自己對 CPU 的控制權要保留多久、什麼時候放棄有最終意見。在這個模型中,因為可能存在某個無賴線程佔住控制權不放,所以其他線程可能永遠無法得到運行。在 搶占式 模型中,操作系統本身採用基於時鍾「滴答」的計時器,基於這個計時器,操作系統可以強制把控制權從一個線程轉移到另外一個線程。在這種情況下,決定哪個線程會得到下一次控制權的調度策略就有可能基於各種指標,例如相對優先順序、某個線程已經等待執行的時間長短,等等。
如果出於某些原因,處在 running 狀態的線程需要等候某個資源(例如,等候設備的輸入數據到達,或者等候某些條件已經設定的通知),或者在試圖獲得互斥體的時候被阻塞,因此線程決定睡眠,那麼這時它可以進入 blocked 狀態。當睡眠周期到期、預期輸入到達,或者互斥體當前的擁有者將其釋放並通知等候線程可以再次奪取互斥體時,阻塞的線程重新進入 runnable 狀態。
當線程的 run() 方法完成時(或者正常返回,或者拋出 RuntimeException 這樣的未檢測到異常),線程將終止。這時,線程的狀態是 dead。當線程死亡時,就不能通過再次調用它的 start() 方法來重新啟動它,如果那麼做,則會拋出 InvalidThreadStateException 異常。
四個常見缺陷
正如我已經展示過的,Java 語言中的多線程編程是通過語言支持的大量精心設計的構造實現的。另外,還設計了大量設計模式和指導原則,來幫助人們了解這種復雜性帶來的許多缺陷。除此之外,多線程編程會很容易地在不經意間把細微的 bug 帶進多線程代碼,而且更重要的是,這類問題分析和調試起來非常困難。接下來要介紹的是用 Java 語言進行多線程編程時將會遇到(或者可能已經遇到過)的最常見問題的一個列表。
爭用條件
據說 爭用條件 存在於這樣的系統中:多個線程之間存在對共享資源的競爭,而勝出者決定系統的行為。Allen Holub 在他撰寫的文章 「programming Java threads in the real world」 提供了一個帶有這樣 bug 的簡單的多線程程序示例。在沖突的訪問請求之間進行不正確同步的另一個更可怕的後果是 數據崩潰,此時,共享的數據結構有一部分由一個線程更新,而另一部分由另一個線程更新。在這種情況下,系統的行為不是按照勝出線程的意圖進行,系統根本不按照任何一個線程的意圖行動,所以兩個線程最後都將以失敗告終。
死鎖
死鎖 的情況是指:線程由於等候某種條件變成真(例如資源可以使用),但是它等候的條件無法變成真,因為能夠讓條件變成真的線程在等候第一個線程「做某件事」。這樣,兩個線程都在等候對方先採取第一步,所以都無法做事。
活動鎖
活動鎖 與 死鎖 不同,它是在線程實際工作的時候發生的,但這時還沒有完成工作。這通常是在兩個線程交叉工作的時候發生,所以第一個線程做的工作被另一個線程取消。一個簡單的示例就是:每個線程已經擁有了一個對象,同時需要另外一個線程擁有的另外一個對象。可以想像這樣的情況:每個線程放下自己擁有的對象,撿起另外一個線程放下的對象。顯然,這兩個線程會永遠都運行在上鎖這一步操作上,結果是什麼都做不成。(常見的真實示例就是,兩個人在狹窄的走廊相遇。每個人都禮貌地讓到另一邊讓對方先行,但卻在相同的時間都讓到同一邊了,所以兩個人還都沒法通過。這種情況會持續一些時間,然後兩個人都從這邊閃到那邊,結果還是一點進展也沒有。)
資源耗盡
資源耗盡,又稱為 線程耗盡,是 Java 語言的 wait/notify 原語無法保證 live-ness 的後果。Java 強制這些方法要擁有它們等候或通知的對象的鎖。在某個線程上調用的 wait() 方法在開始等候之前必須釋放監視器鎖,然後在從方法返回並獲得通知之後,必須再次重新獲得鎖。因此,Java 語言規范在鎖本身之外,還描述了一套與每個對象相關的 等候集(wait set)。一旦線程釋放了對象上的鎖(在 wait 的調用之後),線程就會放在這個等候集上。
多數 JVM 實現把等候線程放在隊列中。所以,如果在通知發生的時候,還有其他線程在等候監視器,那麼就會把一個新線程放在隊列尾部,而它並不是下一個獲得鎖的線程。所以,等到被通知線程實際得到監視器的時候,通知該線程的條件可能已經不再為真,所以它不得不再次 wait。這種情況可能無限持續下去,從而造成運算工作上浪費(因為要反復把該線程放入等候集和從中取出)和線程耗盡。
貪心哲學家的寓言
演示這種行為的原型示例是 Peter Welch 教授描述的「聰明人沒有雞肉」。在這個場景中考慮的系統是一所由五位哲學家、一位廚師和一個食堂組成的學院。所有的哲學家(除了一位)都要想想(在代碼示例中,考慮的時間是 3 秒)之後才去食堂取飯。而「貪心的」哲學家則不想把時間浪費在思考上 —— 相反,他一次又一次地回到食堂,企圖拿到雞肉來吃。
廚師按照一批四份的定量准備雞肉,每准備好一批,就送到食堂。貪心的哲學家不斷地去廚房,但他總是錯過食物!事情是這樣的:他第一次到的時候,時間太早,廚師還沒開火。因此貪心的哲學家只好乾等著(通過 wait() 方法調用)。在開飯的時候(通過 notify() 方法調用),貪心的哲學家再一次回到食堂排隊等候。但是這次,在他前來等候的時候,他的四位同事已經到了,所以他在食堂隊列中的位置在他們後面。他的同事們把廚房送來的一批四份雞肉全部拿走了,所以貪心的哲學家又要在一邊等著了。 可憐(也可能是公平的) ,他永遠處在這個循環之外。
驗證的問題
一般來說,很難按照普通的規范對 Java 編程的多線程程序進行驗證。同樣,開發自動化工具對於常見的並發問題(例如死鎖、活動鎖和資源耗盡)進行完整而簡單的分析也不太容易——特別是在任意 Java 程序中或者在缺乏並發的正式模型的時候。
更糟的是,並發性問題出了名的變化多端、難於跟蹤。每個 Java 開發人員都曾經聽說過(或者親自編寫過)這樣的 Java 程序:經過嚴格分析,而且正常運行了相當一段時間,沒有表現出潛在的死鎖。然後突然有一天,問題發生了,結果弄得開發團隊經歷許多的不眠之夜來試圖發現並修補根本原因。
一方面,多線程 Java 程序容易發生的錯誤非常不明顯,有可能在任意什麼時候發生。另一方面,完全有可能這些 bug 在程序中從不出現。問題取決於一些不可知的因素。多線程程序的復雜本質,使得人們很難有效地對其進行驗證。沒有一套現成的規則可以找出多線程代碼中的這類問題,也無法確切地證明這些問題不存在,這些導致許多 Java 開發人員完全避開多線程應用程序的設計和開發,即使用並發和並行的方式對系統進行建模會非常棒,他們也不使用多線程。
確實想進行多線程編程的開發人員通常准備好了以下一個或兩個解決方案(至少是一部分):
長時間艱苦地測試代碼,找出所有出現的並發性問題,誠心地希望到應用程序真正運行地時候已經發現並修復了所有這類問題。
大量運行設計模式和為多線程編程建立的指導原則。但是,這類指導原則只在整個系統都按照它們的規范設計的時候才有效,沒有設計規則能夠覆蓋所有類型的系統。
雖然知道的人不多,但是對於編寫(然後驗證)正確的多線程應用程序這一問題,還有第三個選項。使用稱為通信順序進程( Communicating Sequential Processes,CSP)的精確的線程同步的數學理論,可以在設計時最好地處理死鎖和活動鎖之類的問題。CSP 由 C.A.R. Hoare 與 20 世紀 70 年代後期設計,CSP 提供了有效的方法,證明用它的構造和工具構建的系統可以免除並發的常見問題。
結束語
在這份面向 Java 程序員的 CSP 全面介紹中,我把重點放在克服多線程應用程序開發常見問題的第一步上,即了解這些問題。我介紹了 Java 平台上目前支持的多線程編程構造,解釋了它們的起源,討論了這類程序可能會有的問題。我還解釋了用正式理論在任意的、大型的和復雜的應用程序中清除這些問題(即競爭冒險、死鎖、活動鎖和資源耗盡)或者證明這些問題不存在的困難。
② Java中HashMap和TreeMap的區別
首先介紹一下什麼是Map。在數組中我們是通過數組下標來對其內容索引的,而在Map中我們通過對象來對對象進行索引,用來索引的對象叫做key,其對應的對象叫做value。這就是我們平時說的鍵值對。
HashMap通過hashcode對其內容進行快速查找,而
TreeMap中所有的元素都保持著某種固定的順序,如果你需要得到一個有序的結果你就應該使用TreeMap(HashMap中元素的排列順序是不固定的)。
HashMap 非線程安全 TreeMap 非線程安全
線程安全
在Java里,線程安全一般體現在兩個方面:
1、多個thread對同一個java實例的訪問(read和modify)不會相互干擾,它主要體現在關鍵字synchronized。如ArrayList和Vector,HashMap和Hashtable
(後者每個方法前都有synchronized關鍵字)。如果你在interator一個List對象時,其它線程remove一個element,問題就出現了。
2、每個線程都有自己的欄位,而不會在多個線程之間共享。它主要體現在java.lang.ThreadLocal類,而沒有Java關鍵字支持,如像static、transient那樣。
1.AbstractMap抽象類和SortedMap介面
AbstractMap抽象類:(HashMap繼承AbstractMap)覆蓋了equals()和hashCode()方法以確保兩個相等映射返回相同的哈希碼。如果兩個映射大小相等、包含同樣的鍵且每個鍵在這兩個映射中對應的值都相同,則這兩個映射相等。映射的哈希碼是映射元素哈希碼的總和,其中每個元素是Map.Entry介面的一個實現。因此,不論映射內部順序如何,兩個相等映射會報告相同的哈希碼。
SortedMap介面:(TreeMap繼承自SortedMap)它用來保持鍵的有序順序。SortedMap介面為映像的視圖(子集),包括兩個端點提供了訪問方法。除了排序是作用於映射的鍵以外,處理SortedMap和處理SortedSet一樣。添加到SortedMap實現類的元素必須實現Comparable介面,否則您必須給它的構造函數提供一個Comparator介面的實現。TreeMap類是它的唯一一份實現。
2.兩種常規Map實現
HashMap:基於哈希表實現。使用HashMap要求添加的鍵類明確定義了hashCode()和equals()[可以重寫hashCode()和equals()],為了優化HashMap空間的使用,您可以調優初始容量和負載因子。
(1)HashMap(): 構建一個空的哈希映像
(2)HashMap(Map m): 構建一個哈希映像,並且添加映像m的所有映射
(3)HashMap(int initialCapacity): 構建一個擁有特定容量的空的哈希映像
(4)HashMap(int
initialCapacity, float loadFactor): 構建一個擁有特定容量和載入因子的空的哈希映像
TreeMap:基於紅黑樹實現。TreeMap沒有調優選項,因為該樹總處於平衡狀態。
(1)TreeMap():構建一個空的映像樹
(2)TreeMap(Map m): 構建一個映像樹,並且添加映像m中所有元素
(3)TreeMap(Comparator c):
構建一個映像樹,並且使用特定的比較器對關鍵字進行排序
(4)TreeMap(SortedMap s):
構建一個映像樹,添加映像樹s中所有映射,並且使用與有序映像s相同的比較器排序
3.兩種常規Map性能
HashMap:適用於在Map中插入、刪除和定位元素。
Treemap:適用於按自然順序或自定義順序遍歷鍵(key)。
4.總結
HashMap通常比TreeMap快一點(樹和哈希表的數據結構使然),建議多使用HashMap,在需要排序的Map時候才用TreeMap。