當前位置:才華齋>計算機>java語言>

java程式設計的總結與思考

java語言 閱讀(1.13W)

編寫優質的併發程式碼是一件難度極高的事情。Java語言從第一版本開始內建了對多執行緒的支援,這一點在當年是非常了不起的,但是當我們對併發程式設計有了更深刻的認識和更多的實踐後,實現併發程式設計就有了更多的方案和更好的選擇。本文是對併發程式設計的一點總結和思考,

java程式設計的總結與思考

  為什麼需要併發

併發其實是一種解耦合的策略,它幫助我們把做什麼(目標)和什麼時候做(時機)分開。這樣做可以明顯改進應用程式的吞吐量(獲得更多的CPU排程時間)和結構(程式有多個部分在協同工作)。做過Java Web開發的人都知道,Java Web中的Servlet程式在Servlet容器的支援下采用單例項多執行緒的工作模式,Servlet容器幫助你處理了併發請求的問題。

  誤解和正解

最常見的對併發程式設計的誤解有以下這些:

A. 併發總能改進效能。(真相:併發在CPU有很多空閒時間時能明顯改程序序的效能,但當執行緒數量較多的時候,執行緒間頻繁的排程切換反而會讓系統的效能下降)

B. 編寫併發程式無需修改原有的設計。(真相:目的與時機的解耦往往會對系統結構產生巨大的影響)

C. 在使用Web或EJB容器時不用關注併發問題。(真相:只有瞭解了容器在做什麼,才能更好的使用容器)

下面的這些說法才是對併發程式設計比較客觀的認識:

A. 編寫併發程式會在程式碼上增加額外的開銷。

B. 正確的併發是非常複雜的,即使對於很簡單的問題。

C. 併發中的缺陷因為不易重現也不容易被發現。

D. 併發往往需要對設計策略從根本上進行修改。

  併發程式設計的原則和技巧

1. 單一職責原則:分離併發相關程式碼和其他程式碼(併發相關程式碼有自己的開發、修改和調優生命週期)。

2. 限制資料作用域:兩個執行緒修改共享物件的同一欄位時可能會相互干擾,導致不可預期的行為,解決方案之一是構造臨界區,但是必須限制臨界區的數量。

3. 使用資料副本:資料副本是避免共享資料的好方法,複製出來的物件只是以只讀的方式對待。Java 5的urrent包中增加一個名為CopyOnWriteArrayList的類,它是List介面的子型別,所以你可以認為它是ArrayList的執行緒安全的版本,它使用了寫時複製的方式建立資料副本進行操作來避免對共享資料併發訪問而引發的問題。

4. 執行緒應儘可能獨立:讓執行緒存在於自己的世界中,不與其他執行緒共享資料。有過Java Web開發經驗的人都知道,Servlet就是以單例項多執行緒的方式工作,和每個請求相關的資料都是通過Servlet子類的service方法(或者是doGet或doPost方法)的引數傳入的。只要Servlet中的程式碼只使用區域性變數,Servlet就不會導致同步問題。Spring MVC的控制器也是這麼做的,從請求中獲得的物件都是以方法的引數傳入而不是作為類的成員,很明顯Struts 2的做法就正好相反,因此Struts 2中作為控制器的Action類都是每個請求對應一個例項。

  下面的這些說法才是對併發客觀的認識:

-編寫併發程式會在程式碼上增加額外的開銷 -正確的併發是非常複雜的,即使對於很簡單的問題 -併發中的缺陷因為不易重現也不容易被發現

-併發往往需要對設計策略從根本上進行修改併發程式設計的原則和技巧 單一職責原則 分離併發相關程式碼和其他程式碼(併發相關程式碼有自己的開發、修改和調優生命週期)。限制資料作用域 兩個執行緒修改共享物件的同一欄位時可能會相互干擾,導致不可預期的行為,解決方案之一是構造臨界區,但是必須限制臨界區的數量。使用資料副本 資料副本是避免共享資料的好方法,複製出來的物件只是以只讀的方式對待。

Java 5的urrent包中增加一個名為CopyOnWriteArrayList的類,它是List介面的子型別,所以你可以認為它是ArrayList的執行緒安全的版本,它使用了寫時複製的方式建立資料副本進行操作來避免對共享資料併發訪問而引發的問題。執行緒應儘可能獨立 讓執行緒存在於自己的世界中,不與其他執行緒共享資料。有過Java Web開發經驗的人都知道,Servlet就是以單例項多執行緒的方式工作,和每個請求相關的資料都是通過Servlet子類的service方法(或者是doGet或doPost方法)的引數傳入的。只要Servlet中的程式碼只使用區域性變數,Servlet就不會導致同步問題。

springMVC的控制器也是這麼做的,從請求中獲得的物件都是以方法的引數傳入而不是作為類的成員,很明顯Struts 2的做法就正好相反,因此Struts 2中作為控制器的Action類都是每個請求對應一個例項。 Java 5以前的併發程式設計 Java的執行緒模型建立在搶佔式執行緒排程的基礎上,也就是說: 所有執行緒可以很容易的共享同一程序中的物件。能夠引用這些物件的任何執行緒都可以修改這些物件。為了保護資料,物件可以被鎖住。 Java基於執行緒和鎖的併發過於底層,而且使用鎖很多時候都是很萬惡的,因為它相當於讓所有的併發都變成了排隊等待。

在Java 5以前,可以用synchronized關鍵字來實現鎖的功能,它可以用在程式碼塊和方法上,表示在執行整個程式碼塊或方法之前執行緒必須取得合適的鎖。對於類的非靜態方法(成員方法)而言,這意味這要取得物件例項的鎖,對於類的靜態方法(類方法)而言,要取得類的Class物件的鎖,對於同步程式碼塊,程式設計師可以指定要取得的是那個物件的鎖。 不管是同步程式碼塊還是同步方法,每次只有一個執行緒可以進入,如果其他執行緒試圖進入(不管是同一同步塊還是不同的同步塊),JVM會將它們掛起(放入到等鎖池中)。這種結構在併發理論中稱為臨界區(critical section)。

這裡我們可以對Java中用synchronized實現同步和鎖的功能做一個總結: 只能鎖定物件,不能鎖定基本資料型別被鎖定的物件陣列中的單個物件不會被鎖定同步方法可以視為包含整個方法的synchronized(this) { … }程式碼塊靜態同步方法會鎖定它的Class物件內部類的同步是獨立於外部類的 synchronized修飾符並不是方法簽名的組成部分,所以不能出現在介面的方法宣告中非同步的方法不關心鎖的狀態,它們在同步方法執行時仍然可以得以執行 synchronized實現的鎖是可重入的鎖。在JVM內部,為了提高效率,同時執行的每個執行緒都會有它正在處理的資料的快取副本,當我們使用synchronzied進行同步的時候,真正被同步的是在不同執行緒中表示被鎖定物件的記憶體塊(副本資料會保持和主記憶體的同步,現在知道為什麼要用同步這個詞彙了吧),簡單的說就是在同步塊或同步方法執行完後,對被鎖定的物件做的任何修改要在釋放鎖之前寫回到主記憶體中;在進入同步塊得到鎖之後,被鎖定物件的資料是從主記憶體中讀出來的,持有鎖的執行緒的資料副本一定和主記憶體中的資料檢視是同步的 。 在Java最初的版本中,就有一個叫Volatile的關鍵字,它是一種簡單的同步的處理機制,因為被volatile修飾的變數遵循以下規則: 變數的值在使用之前總會從主記憶體中再讀取出來。對變數值的修改總會在完成之後寫回到主記憶體中。使用volatile關鍵字可以在多執行緒環境下預防編譯器不正確的優化假設(編譯器可能會將在一個執行緒中值不會發生改變的變數優化成常量),但只有修改時不依賴當前狀態(讀取時的值)的變數才應該宣告為volatile變數。