1. 程式人生 > 其它 >Java程式設計師修煉之道 人民郵電出版社 吳海星譯

Java程式設計師修煉之道 人民郵電出版社 吳海星譯

前言

  • 併發,效能,位元組碼和類載入是最讓我們著迷的核心技術.
  • java7跟之前版本相比有一個主要區別:它仕第一個明確著眼於下一次釋出的新版本.根據Oracle有關釋出的"B計劃",Java 7為Java 8的主要變化打下了基礎.
第一部分:用java 7做開發
  • java 7的 變化可以大致分為兩塊:Coin專案和NIO.2
    • Coin專案:設計他們的初中仕提高開發人員的生產率,但又不會對底層平臺造成太大影響.
      • try-with-resources結構(可以自動關閉資源)
      • switch中的字串
      • 對數字常量的改進
      • Multi-catch(在一個catch塊中宣告多個要捕獲的異常)
      • 鑽石語法(在處理泛型時不用那麼繁瑣了)
    • 新I/O API (NIO.2):跟java原有的檔案系統支援相比,它具有壓倒性的優勢,還提供了強大的非同步能力.
      • 用於引用檔案和類檔案實體的新path結構
      • 簡化檔案的建立,賦值,移動和刪除的工具類Files
      • 內建的目錄樹導航
      • 在後臺處理大型I/O的將來式和回撥式非同步I/O
第一章:初始Java 7
  • java既是程式語言,也是平臺
  • 語言與平臺
    • 控制Java系統的規範有多種,其中最重要的是<Java語言規範>(JLS)和<JVM規範>(VMSpec).
    • 連線Java語言和平臺之間的紐帶仕統一的類檔案(即.class檔案)格式定義.認真研究類檔案的定義能讓你獲益匪淺,這是優秀java程式設計師向偉大java程式設計師轉變的一個途徑.
    • 實際上,JVM位元組碼更像是中途的驛站,仕一種從人類可讀的原始碼向及其碼過度的中間狀態.用編譯原理術語講,位元組碼實際上是一種中間語言(IL)形態,不是真正的機器碼.也就是說,將Java原始碼程式設計位元組碼的過程不是C/C++程式設計師所理解的那種變異.Java所謂的編譯器javac也不同於gcc,實際上它致使一個針對java原始碼生成類檔案的工具.java體系中真正的編譯器仕JIT.
    • 有人說java是"動態編譯"的,他們所說的編譯仕指JIT的執行時編譯,不是指構建時建立類檔案的過程.所以如果被問及"Java仕編譯型語言還是解釋型語言",你可以回答"都是"
  • Coin專案:濃縮的都是精華
    • 我們覺得解釋語言"為什麼要變"和"變成了什麼"同樣重要.
    • 語法糖-數字中的下劃線
      • 語法糖是描述一種語言特性的短語.它表示這是冗餘的語法-在語言中已經存在一種表示形式了-但語法糖用起來更便捷.一般來說,程式的語法糖在編譯處理早期會從編譯結果中移除,變為相同特性的基礎表示形式,這稱為"去糖化",因此,語法糖仕比較容易實現的修改,他們通常不需要做太多工作,只需要修改編譯器(對java來說就是javac)
    • java7是以開源方式開發後釋出的第一個版本.開源的java平臺開發主要集中在專案OpenJDK上.
    • Coin專案中的修改:Coin專案主要給java7引入了6個新特性
      • switch語句中的String
      • 更強的數值文字表示法
        • 數字常量(如基本型別種的integer)尅用二進位制文字表示
          • int x = 0b1100110;
        • 在整型常量中可以適用下劃線來提高可讀性.
          • Coin專案中的提案借用了Ruby的創意,用下劃線(_)做分隔符.
      • 改善後的異常處理
        • 異常處理有兩處改進-multicatch和final重拋.
        • catch中的final重拋中的final關鍵字不是必需的,但實際上,在向catch和重拋語義調整的過渡階段,留著它可以給你提個醒.
      • try-with-resources(TWR)
        • 墨菲定律(任何事都可能出錯)
        • try (OutputStream out = new FileOutputStream(file);
          InputStream is = url.openStream()) {
          byte[] buf = new byte[4096];
          int len;
          while ((len = is.read(buf)) > 0) {
          out.write(buf, 0, len);
          }
          }
        • 上面的程式碼例子是資源自動化管理程式碼塊的基本形式-把資源放在try的圓括號內.
        • 但是在適用try-with-resources特性時還是要小心,因為在某些情況下資源可能無法關閉.比如在下面的程式碼中,如果從檔案(someFile.bin)建立ObjectInputStream時出錯,FileInputStream可能就無法正確關閉.
        • try(ObjectInputStream in = new ObjectInputStream(new FileInputStream("someFile.bin"))){...}
        • 要確保try-with-resources生效,正確的用法是為各個資源宣告獨立變數.
        • TWR和AutoCloseable:目前TWR特性依靠一個新定義的介面實現AutoClosseable.TWR的try從句中出現的資源類都必須實現這個介面.java7平臺中的大多數資源類都被修改過,已經實現了AtuoCloseable(java7中還定義了其父介面Closeable),但並不是全部資源相關的類都採用了這項新技術.不過,JDBC4.1已經具備了這個特性.
      • 鑽石語法
        • Map<Integer,Map<String,String>> usersLists = new HashMap<>();
        • 編譯器為這個特性採用了新的型別推斷形式.它能推斷出表示式右側的正確型別,而不是僅僅替換成定義完整型別的文字.
      • 簡化變參方法呼叫
        • java7中的警告去了哪裡?過去仕在編譯適用API的程式碼時觸發警告,而現在仕在編譯這種可能會破壞型別安全的API時觸發.編譯器會警告建立這種API的程式設計師,讓他注意型別系統的安全.
        • 型別系統的修改:Coin專案曾奉勸諸位貢獻者遠離型別系統,因為把這麼一個小變化講清楚要大費周章.
第二章:新I/O
  • JSR-203
  • NIO.2是一組新的類和方法,主要存在於java.nio包內.優點
    • 它完全取代了java.io.File與檔案系統的互動.
    • 它提供了新的非同步處理類,讓你無需手動配置執行緒池和其他底層併發控制,便可在後臺執行緒中執行檔案和網路I/O操作.
    • 它引入了新的Network-Channel構造方法,簡化了套接字socket與通道的編碼工作.
  • 將try-with-resources和NIO.2中的新API結合起來可以寫出非常安全的I/O程式,這在java中還是破天荒的第一次!
  • Perl,正則表示式之王.
  • 2.2檔案I/O的基石:Path
    • NIO.2中的Path仕一個抽象構造. NIO.2把位置(由Path表示)的概念和物理檔案系統的處理(比如賦值一個檔案)分的很清楚,物理檔案系統的處理通常是由Files輔助類實現的.
    • Path不一定代表真實的檔案或目錄.你可以隨心所欲地操作Path,用Files中的功能來檢查檔案是否存在,並對它進行處理.
    • Path並不僅限於傳統的檔案系統,它也能表示zip或jar這樣的檔案系統.
    • 建立Path時可以用相對路徑.比如:"../xxx"
    • 移除冗餘項:在java7中,有兩個輔助方法可以用來弄清Path的真是位置.
      • 用normalize()方法去掉Path中的冗餘資訊.
      • toRealPath()方法也很有效,它融合了toAbsolutePath()和normalize()兩個方法的功能,還能檢測並跟隨符號連線.
    • 轉換Path
      • 合併Path,呼叫resolve方法.
      • 取得兩個Path之間的路徑,用relativize(Path)方法.
      • 用startsWith(Path prefix),equals(Path path)等值比較或endsWith(Path suffix)來對路徑進行比較.
    • NIO.2 Path和Java已有的File類
      • 新API中的類可以完全替代過去基於java.io.File的API.
      • java.io.File類中新增了toPath()方法,它可以馬上把已有的File轉化為新的Path
      • Path類中有toFile()方法,它可以馬上把已有的path轉化為File
  • 2.3 處理目錄和目錄樹
    • 在目錄中查詢檔案
      • Path dir = Paths.get("C:\\workspace\\java7developer"); try (DirectoryStream<Path> stream = Files.newDirectoryStream(dir,
        "*.properties")) {
        for (Path entry : stream) {
        System.out.println(entry.getFileName());
        }
        } catch (IOException e) {
        System.out.println(e.getMessage());
        }
    • 遍歷目錄樹
      • java7支援整個目錄樹的遍歷.
      • 關鍵方法:Files.walkFileTree(Path startingDir,FileVisitor<? super Path> visitor);
      • java7的設計者們已經提供了一個預設實現類,SimpleFileVisitor<T>.
      • public static void main(String[] args) throws IOException {
        Path startingDir = Paths
        .get("/Users/karianna/Documents/workspace/java7developer_code_trunk");
        Files.walkFileTree(startingDir, new FindJavaVisitor());
        }
        private static class FindJavaVisitor extends SimpleFileVisitor<Path> {
        @Override
        public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) {
        if (file.toString().endsWith(".java")) {
        System.out.println(file.getFileName());
        }
        return FileVisitResult.CONTINUE;
        }
        }
      • 為了保證地櫃等操作的安全性,walkFileTree方法不會自動跟隨符號連結.如果你卻是需要跟符號連結,就需要檢查哪個屬性並執行相應的操作.
  • 2.4 NIO.2的檔案系統I/O
    • Files類
    • WatchService
    • NIO.2 API對原子操作的支援有很大改進,但涉及檔案系統處理時,仍然主要依靠程式碼來提供保護.即使仕執行了一般的操作,也很可能會因為突然斷網等情況的原因而出錯.儘管API的某些方法還是會偶爾跑出各RuntimeException,但某些異常組昂礦可以由Files.exists(Path)這樣的輔助方法來緩解.
    • 2.4.1 建立和刪除檔案
      • 如果在建立檔案時要制定訪問許可,不要忽略其父目錄強加給該檔案的umask限制或受限許可.比如說,你會發現即便你為新檔案制定了rw-rw-rw許可,但由於目錄的掩碼,實際上檔案最終的訪問許可卻是rw-r--r--
      • 刪除檔案,可以用Files.delete(Path)方法.
    • 2.4.2檔案的複製和移動
      • Files.copy(Path source,Path target),複製檔案時通常需要設定某些選項
        • REPLACE_EXISTING:覆蓋即替換已有檔案
        • COPY_ATTRIBUTES:複製檔案屬性
        • ATOMIC_MOVE:確保在連gia你的操作都成功,否則回滾
      • 移動和複製很像,都是用原子Files.move(Path source,Path target)方法完成的.
    • 2.4.3檔案的屬性
      • 檔案屬性控制者誰能對檔案做什麼.一般情況下,做什麼許可包括能否讀取,寫入或執行檔案,而由誰許可包括屬主,群組或所有人.
      • 介面BasicFileAttributes定義了這個通用集.但工具類Files就能得到一些屬性了
      • java7也支援跨檔案系統的檔案屬性檢視和處理功能.
      • 為了支援檔案系統特定的檔案屬性,java7允許檔案系統ing提供者實現FileAttributeView和BasicFileAttributes介面.
      • 在編寫特定檔案系統的程式碼時一定要小心.一定要確保你的邏輯和異常處理考慮到了程式碼在不同檔案系統上執行的情況.
      • java7對符號連結的支援
        • 在寫軟體時,比如備份工具或部署指令碼,你需要慎重考慮仕否應該跟隨符號連結,NIO.2允許你做出選擇.
        • java7對符號連結的支援遵循UNIX作業系統中實現的語義.
        • Files.isSymbolicLink(Path path);
        • Files.readSymbolicLink(Path path);
        • NIO.2 API預設會跟隨符號連結.如果不想跟隨,需要用LInkOption.NOFOLLOW_LINKS選項.
        • 如果你要讀取符號連結本身的基本檔案屬性,應該呼叫:Files.readAttributes(target,BasicFileAttributes.class,LinkOption.NOFOLLOW_LINKS);
        • 符號連結仕java7對特定檔案系統支援最常用的例子,API設計者也考慮到了未來對特定檔案系統支援特性的擴充套件,比如量子加密檔案系統.
    • 2.4.4 快速讀寫資料
      • 開啟檔案
        • java7可以直接用帶緩衝區的讀取器和寫入器或輸入輸出流(為了和以前的java I/O程式碼相容)開啟檔案.
        • 在處理String時,不要忘了檢視它的字元編碼.忘記設定字元編碼(通過StandardCharsets類)可能導致不可預料的字元編碼問題.
      • 簡化讀取和寫入
        • 輔助類Files有兩個輔助方法,用於讀取檔案中的全部行和全部位元組.
    • 2.4.5 檔案修改通知
      • 在java7中可以用java.nio.file.WatchService類檢測檔案或目錄的變化.和很多持續輪詢的設計一樣,它也需要一個輕量的退出機制.
    • SeekableByteChannel
      • 非常重要,抽象的新API-用於資料的讀寫,使非同步I/O成為現實的SeekableByteChannel.
      • java7引入SeekableByteChannel介面,是為了讓開發人員能夠改變位元組通道的位置和大小.
      • JDK中有一個java.nio.channels.SeekableByteChannel介面的實現類--java.nio.channels.FileChannel.FileChannel類的定址能力意味著開發人員可以更加靈活地處理檔案內容.
  • 2.5 非同步I/O操作.
    • 如果你還沒接觸過NIO通道,我們建議你去看看Ron Hitchens寫的java NIO(O'Reilly ,2002)一書,你會從中獲益匪淺.
    • java7中有三個新的非同步通道:
      • AsynchronousFileChannel-用於檔案I/O
      • AsynchronousSocketChannel-用於套接字I/O,支援超時.
      • AsynchronousServerSocketChannel-用於套接字接受非同步連線.
    • 適用新的非同步I/O API時,主要有兩種形式,將來式回撥式
    • 2.5.1 將來式
      • NIO.2 API的設計人員用將來式(future)這個術語來表明適用java.util.concurrent.Future介面.通常會用Future get()方法(帶或不帶超時引數)在非同步I/O操作完成時獲取其結果.
      • public static void main(String[] args) {
        try {
        Path file = Paths.get("/usr/karianna/foobar.txt"); AsynchronousFileChannel channel = AsynchronousFileChannel.open(file); ByteBuffer buffer = ByteBuffer.allocate(100_000);
        Future<Integer> result = channel.read(buffer, 0); while (!result.isDone()) {
        ProfitCalculator.calculateTax();
        } Integer bytesRead = result.get();
        System.out.println("Bytes read [" + bytesRead + "]");
        } catch (IOException | ExecutionException | InterruptedException e) {
        System.out.println(e.getMessage());
        }
        }
      • 一定要注意,我們在這裡用isDone()手工判斷result是否結束.通常情況下,result或結束(主執行緒會繼續執行),或等待後臺I/O完成.
      • AsynchronousFileChannel的javadoc有解釋:AsynchronousFileChannel會關聯執行緒池,它的任務仕接收I/O處理事件,並分發給負責處理通道中I/O操作結果的結果處理器.跟通道中發起的I/O操作關聯的結果處理器確保仕由執行緒池中的某個執行緒產生的.
      • 預設的執行緒池是由AsynchronousChannelGroup類定義的系統屬性進行配置的.
    • 回撥式
      • 基本思想是主執行緒會派一個偵查員CompletionHandler到獨立的執行緒中執行I/O操作.這個偵查員將帶著I/O操作的結果返回到主執行緒中,這個結果會觸發它自己的completed或failed方法(你會重寫這兩個方法).
      • 在非同步事件剛一成功或失敗並需要馬上採取行動時,一般會用回撥式.
  • 2.6 Socket 和Channel 的整合
    • java7推出了NetworkChannel,把Socket和Channel結合到一起,讓開發人員可以輕鬆應對.
    • NetworkChannel
      • 新介面java.nio.channels.NetworkChannel代表一個連結到網路套接字通道的對映.NetworkChannel的出現使得多播操作成為可能.
    • MulticastChannel
      • 像BitTorrent這樣的對等網路程式一般都具備多播的功能.在java的早起版本中,雖然拼湊一下也能實現多播,但卻沒有很好的API抽象層.java7中的新介面MulticastChannel解決了這個問題.
      • 術語多播(或組播)表示一對多的網路通訊,通常用來指呆IP多播.其基本前提是將一個包傳送到一個組播地址,然後網路對該包進行復制,分發給所有的接收端(註冊到組播地址匯中).
      • 為了讓新來的NetworkChannel加入多播組,java7提供了一個新介面java.nio.channels.MulticastChannel及其預設實現類DatagramChannel.也就是說可以輕鬆的對多播組傳送和接收資料.
  • 小結
    • 在平臺特性的支援下,java7可以任意穿梭於檔案系統中,並能夠處理大型目錄結構.
第二部分:關鍵技術 第三章:依賴注入 知識注入:理解IoC和DI
  1. 依賴注入(控制反轉的一種形式).簡言之,適用DI技術可以讓物件從別處得到依賴項,而不是由它自己來構造.Java DI的官方標準JSR-330,JSR-330的參考實現(RI)Guice 3---一個眾所周知的輕量,精巧的DI框架.
  2. 物件關係對映(Object Relational Mapping,ORM)框架,比如Hibernate...
  3. 控制反轉
    1. 使用IoC,這個"中心控制"的設計原則會被反轉過來.呼叫者的程式碼處理程式的執行順序,而程式邏輯則被封裝在接收呼叫的子流程中.IoC也被成為好萊塢原則("不要給我們打電話,我們會打給你".好萊塢經紀人總是給人打電話,而不讓別人打給他們!),其思想可以歸結為會有另一段程式碼擁有最初的控制執行緒,並且由它來呼叫你的程式碼,而不是由你的程式碼呼叫它.
    2. 程式的主控被反轉了,將控制權從應用邏輯中轉移到GUI框架.
    3. IoC有幾種不同的實現,包括工廠模式,服務定位器模式,當然,還有依賴注入.這一術語最初由Martin Fowler在"控制反轉容器和依賴注入模式"中提出.
    4. 從字面上來看,IoC是指一種機制,使用這種機制的用例很多,實現方式也很多.DI只是其中一種具體用例的具體實現方式.但因為DI非常流行,所以人們經常誤以為IoC就是DI,並且認為DI這種叫法比IoC更貼切.
  4. 依賴注入
    1. 依賴注入是IoC的一種特定形態,是指尋找依賴項的過程不在當前執行程式碼的直接控制之下.可以把IoC容器看做執行時環境.java中為依賴注入提供的容器有Guice,Spring和PicoContainer.
    2. 把依賴項注入物件的方法有很多種.可以用專門的DI框架,但也可以不這麼做!顯式地建立物件例項(依賴項)並把他們傳入物件中也可以和框架注入做的一樣好.
  5. 轉成DI
    1. 把不用IoC的程式碼變成使用工廠(或服務定位器)模式的程式碼,再變成使用DI的程式碼.在這些轉變之後有一個共同的關鍵技術,即面向介面程式設計.使用面向介面程式設計,甚至可以在執行時更換物件.
    2. 打個比方,DI框架就是把你的程式碼抱起來的執行時環境,在你需要時為你注入依賴項.DI框架的優勢在於它可以隨時隨地為你的程式碼提供依賴項.因為框架中有IoC容器,在執行時,你的程式碼需要的所有依賴項都會在哪裡準備好.
    3. 儘管JSR-330註解可以在方法上注入依賴項,但通常志勇於構造方法或set方法中.
    4. 新的DI標準化方式(JSR-330)就是要解決這個問題.它對大多數java DI框架的核心能力做了很好的彙總.
  6. java中標準化的DI
    1. JSR-330(javax.inject)規範.倡導java se的DI標準化
    2. java ee中的DI標準化情況如何?java企業應用從JEE 6開始構建了自己的依賴注入體系(即CDI),由JSR-299(java ee平臺中的上下文及依賴注入)規範確定,你可在http://jcp.org/中搜索JSR-299瞭解其詳細資訊.簡言之,JSR-299構建在JSR-330基礎之上,旨在為企業應用提供標準化的配置.
    3. 警告:實際上,程式碼遷移並不容易.一旦你的程式碼用到了僅由特定ID框架支援的特性,就不太可能拜託這一框架了.儘管javax.inject包提供了常用DI功能的子集,但是你可能需要適用更高階的DI特性.正如你想象的那樣,對於哪些特性應該作為通用的標準也是眾說紛紜,很難統一.雖然現狀不盡如人意,但java畢竟朝DI框架的標準化方向邁出了一步.
    4. javax.inject包只是提供了一個介面和幾個註解型別,這些都會被遵循JSR330標準的各種DI框架實現.也就是說,除非你在建立與JSR-330相容的IoC容器(如果如此,想你致敬),通常不用自己實現它們.
    5. javax.inject包:這個包志明了獲取物件的一種方式,與傳統的構造方法,工廠模式和伺服器定位器墨仕(比如JNDI)等相比,這種方式的可重用性,可測試性和可維護性都的到了極大提升.這種方式成為依賴注入,對於大多數非小型應用程式都很有幫助.
    6. @Inject註解
      1. 規範中規定向構造器注入的引數數量為0或多個,所i在不含引數的構造器上使用@Inject也是合法的.警告:因為JRE無法決定構造器注入的優先順序,所以規範中規定類中只能有一個構造器帶@Inject註解.
      2. @Inject註解方法,執行時可注入引數的數量也可以是0或多個.但適用引數注入的方法不能宣告為抽象方法,也不能宣告其自身的型別引數.
      3. 提示:向構造器中注入的通常仕類中必需的依賴項,而對於非必需的依賴項,通常仕在set方法上注入.比如已經給出了預設值的屬性就是非必需的依賴項.這一最佳時間已經成了慣例.
      4. 可以直接在屬性上注入,只要他們不是final,雖然這樣做簡單直接,但你最好不要用,因為這樣可能會讓單元測試更加困難.
    7. @Qualifier註解
      1. 支援JSR-330規範的框架要用註解@Qualifier限定(標識)要注入的物件.
      2. 如果你用過由框架實現的限定符,應該指導要建立一個@Qualifier實現必需遵循如下規則:
        1. 必需標記為@Qualifier和@Retention(RUNTIME),以確保該限定註解在執行時一直有效.
        2. 通常還應該加上@Documented註解,這樣該實現就能加到API的公共javadoc中了.
        3. 可以有屬性
        4. @Target註解可以限定其使用範圍;比如將其適用範圍限制為屬性,而不是限定為屬性的預設值和方法中的引數.
      3. JSR-330規範中要求所有IoC容器都要提供一個預設的@Qualifier註解:@Named
    8. @Named註解
      1. @Named仕一個特別的@Qualifier註解,藉助@Named可以用名字標明要注入的物件.將@Named和@Inject一起使用,符合制定名稱並且型別正確的物件會被注入
    9. @Scope註解
      1. @Scope註解用於定義注入器(即IoC容器)對注入物件的重用方式.JSR-330規範中明確瞭如下集中預設行為.
        1. 如果沒有宣告任何@Scope註解介面的實現,注入器應該建立注入物件並且僅使用該物件一次.
        2. 如果聲明瞭@Scope註解介面的實現,那麼注入物件的生命週期由所宣告的@Scope註解實現決定.
        3. 如果注入物件在@Scope實現中要由多個執行緒使用,則需要保證注入物件的執行緒安全性.
        4. 如果某個磊尚聲明瞭多個@Scope註解,或聲明瞭不受支援的@Scope註解,IoC容器應該丟擲異常.
      2. DI框架管理注入物件的生命週期時不會超出這些預設行為劃定的界限.因為大家工人的通用@Scope實現只有@Singleton一個,所以JSR-330規範中僅確定了它這麼一個標準的生命週期註解.
    10. @Singleton註解
      1. 在需要注入一個不會改變的物件時,就要用@Singleton
      2. 請謹慎使用單例模式,因為它有時候會變成反模式.
      3. 大多數DI框架都將@Singleton作為注入物件的預設生命週期,無需顯式宣告.
    11. 介面Prvoider<T>
      1. 如果你想對由DI框架注入程式碼中的物件擁有更多的控制權,可以要求DI框架將Prvoider<T>介面實現注入物件.控制物件的好處在於:
        1. 可以獲取該物件的多個例項.
        2. 可以延遲獲取該物件(延遲載入)
        3. 可以打破迴圈依賴
        4. 可以定義作用域,能在比整個被載入的應用小的作用域中查詢物件.
  7. java中的DI參考實現:Guice 3:這一節看暈了,待以後琢磨
    1. Guice 3是JSR-330規範的完整參考實現. "為了讓注入器建立物件關係圖,需要建立宣告各種繫結關係的模組,其中繫結是用來明確要注入的具體實現類的."
    2. 水手繩結:Guice的各種繫結
  8. 小結:IoC是個複雜的概念.但通過對工廠和服務定位器模式的探討你能瞭解基本IoC實現仕如何工作的.工廠模式有助於你理解DI以及DI給程式碼帶來的好處.JSR-330不僅僅是統一DI通用功能的重要標準,它還提供了你需要了解的幕後規則及限制.通過研究標準DI註解集,你會更加欣賞不同DI框架對規範的實現,因而可以更有效地使用他們.
第四章:現代併發
  • 併發理論簡介:系統設計和實現中"設計原則"的影響以及其中最主要的兩個原則:安全性和活躍度.
    • 解釋java執行緒模型:java執行緒模型建立在兩個基本概念之上
      • 共享的,預設可見的可變狀態
      • 搶佔式執行緒排程
      • 思考這兩個概念:
        • java基於執行緒和鎖的併發非常底層,並且一般都比較難用.為了解決這個問題,java 5引入了一組併發類庫java.util.concurrent.
    • 設計理念
      • Doug Lea在創造他那裡程碑式的作品java.util.concurrent時列出了下面這些最重要的設計原則:
        • 安全性(也叫做併發型別安全性)
          • 安全性是指不管同時發生多少操作都能確保物件保持自相一致.如果一個物件系統具備這以特性,那它就是併發型別安全的.
          • 併發型別安全的概念跟物件型別安全一樣,但它用在更復雜的環境下.在這樣的環境中,其他執行緒在不同CPU核心上同時操作同一物件.
          • 保證安全: 保證安全的策略之一是在處於非一致狀態時決不能從非私有方法中返回,也決不能呼叫任何非私有方法,而且也決不能呼叫其他任何物件中的方法.如果把這個策略跟某種對非一致物件的保護方法(比如同步鎖或臨界區)結合起來,就可以 保證系統是安全的.
        • 活躍度
          • 在一個活躍的系統中,所有做出嘗試的活動最終或取得進展,或者失敗.
        • 效能
          • 暫時可以看成仕測量系統用給定資源能做多少工作的辦法.
        • 重用性
          • 用可重用工具集(比如:java.util.concurrent),並把不可重用的應用程式碼構建在工具集之上是一種可行的辦法.
      • 這些原則如何以及為何會相互衝突
        • 安全性與活躍度相互對立--安全性是為了確保壞事不會發生,而活躍度要求見到進展.
        • 可重用的系統傾向於對外開放其核心,可這會引發安全問題.
        • 一個安全但編寫方式幼稚的系統性能通常都不會太好,因為裡面一般會用大量的鎖來保證安全性.
        • 以上問題實戰技巧,如下:
          • 儘可能限制子系統之間的通訊.隱藏資料對安全性非常有幫助.
          • 儘可能保證子系統內部結構的確定性.比如說,即便子系統會以併發的,非確定性的方式進行互動,子系統內部的設計也應該參照執行緒和物件的靜態知識.
          • 採用客戶端應用必須遵守的策略方針.這個技巧雖然強大,卻依賴於使用者應用程式的合作程度,並且如果某個糟糕的應用不遵守規則,便很難發現問題所在.
          • 在文件中記錄所要求的行為.這是最遜的辦法,但如果程式碼要部署在非常通用的環境中,就必須採用這個辦法.
      • 系統開銷之源
        • 併發系統中的系統開銷是與生俱來的,來自:
          • 鎖與檢測
          • 環境切換的次數
          • 執行緒的個數
          • 排程
          • 記憶體的區域性性
            • 區域性性值得是程式行為的一種規律:在程式執行中的短時間內,程式訪問資料位置的集合限於區域性範圍.區域性性有兩種基本形式:時間區域性性與空間區域性性.時間區域性性是指反覆訪問同一個位置的資料;空間區域性性指的是反覆訪問相鄰的資料.-譯者注
          • 演算法設計 :推薦書籍Thomas H.Corman等人編著的<演算法導論>(MIT,2009) 和Steven Skiena寫的<演算法設計手冊>(Springer-Verlag,2008)
        • 假設有一個基本事務處理系統.構建這種程式有個簡單的標準辦法,就是先將業務流程的 不同環節對應到應用程式的不同階段,然後用不同的執行緒池表示不同的應用階段,每個執行緒池逐一接收公主偶像,在對每個工作項進行一系列的處理後,交給下一個執行緒池.通常來說,好的設計會讓每個執行緒池所做的處理集中在一個特定功能區內.
      • 塊結構併發(java5之前)
        • 同步與鎖
          • 臨界區的概念
          • 同步與鎖的基本事實:
            • 被鎖定的物件陣列中的單個物件不會被鎖定.
            • 同步方法可以視同為包含整個方法的同步(this) {...}程式碼塊(但要注意它們的二進位制碼 表示是不同的).
            • 如果要鎖定一個類物件,請慎重考慮是用顯示鎖定,還是用getClass(),兩種方式對子類的影響不同.
            • 內部類的同步是獨立於外部類的(要明白為什麼會這樣,請記住內部類是如何實現的).
            • synchronized並不是方法簽名的組成部分,所以不能出現在介面的方法宣告中.
            • 非同步的方法不檢視或關心任何鎖的狀態,而且在同步方法執行時它們仍能繼續執行.
            • Java的執行緒鎖是可重入的.也就是說持有鎖的執行緒在遇到同一個鎖的同步點(比如一個同步方法呼叫同一個類內的另一個同步方法)時是可以繼續的.
          • 執行緒的狀態模型
          • 完全同步物件
            • 如果一個類遵從下面的所有規則,就可以認為它是執行緒安全並且活躍的:一個滿足下面所有條件的類就是完全同步類.
              • 所有域在任何構造方法中的初始化都能達到一致的狀態.
              • 沒有公共域
              • 從任何非私有方法返回後,都可以保證物件例項處於一致的狀態(假定呼叫方法時狀態是一致的).
              • 所有方法經證明都可在有限時間內終止.
              • 所有方法都是同步的.
              • 當處於非一致狀態時,不會呼叫其他例項的方法.
              • 當處於非一致狀態時,不會呼叫非私有方法.
          • 死鎖
            • 有一個處理死鎖的技巧,就是在所有縣城中都以相同的順序獲取執行緒鎖.
            • 就完全同步物件方式而言,要防止這種死鎖出現是因為程式碼破壞了狀態一致性規則.
          • 為什麼是synchronized
            • 以前,併發變成過去主要考慮如果分享CPU時間,縣城門在單核上輪流上位,相互調換.現在做的應該把多個執行緒在同一物理時刻執行在不同核心(並且很可能會操作共享的資料)的情況也考慮在內.
            • 在synchronized程式碼塊(或方法)執行完之後,對被鎖定物件所做的任何修改全部都會線上程鎖釋放之前刷回到主記憶體中.
            • 當進入一個同步的程式碼塊,得到執行緒鎖之後,對被鎖定物件的任何修改都是從主記憶體中讀出來的,所i在鎖定區域程式碼開始執行之前,持有鎖的執行緒就和鎖定物件朱記憶體中的檢視同步了.
          • 關鍵子volatile
            • 是一種簡單的物件域同步處理辦法,包括原始型別.
            • 一個volatile域需遵循一下規則:
              • 執行緒所見的值在使用之前總會從朱記憶體中再讀出來.
              • 執行緒所寫的值總會在指令完成之前被刷回到主記憶體中.
            • 付出的代價是每次訪問都要額外刷依次記憶體,還有就是volatile變數不會引入執行緒鎖,所以使用volatile變數不可能發生死鎖.
            • volatile變數是真正執行緒安全的.但只有寫入時不依賴當前狀態(讀取的狀態)的變數才應該宣告為volatile變數.
          • 不可變性
            • 構建器模式:它由兩部分組成,一個是實現了構建器泛型介面的內部靜態類,另一個是構建不可變類例項的私有構造方法.內部靜態類是不可變類的構建器,開發人員只能通過它獲取不可變類的新例項.比較常見的實現方式是讓構建器類擁有於不可變類一模一樣的域,但構建器的域是可修改的.
            • 關鍵字final僅對其直接只想的物件有用.也就是說final引用可以指向帶有非final域的物件.
            • 有時候只用不可變物件開發效率不行,因為每次修改物件狀態就需要構建一個新物件.
        • 現代併發應用程式的構件
          • 原子類:java.util.concurrent.atomic
            • 語義基本上和volatile一樣,致使封裝在一個API裡了,這個API包含為操作提供的適當的原子(要麼不做,要麼就全做)方法.
            • 常見的用法是實現序列號機制,在AtomicInteger或AtomicLong上用原子操作getAndIncrement()方法.要做序列號,該類應該有個nextId()方法,每次呼叫時肯定能返回一個唯一併且完全增長的數值.這和資料庫裡的序列號的概念很像.
            • 原子類不是從有相似名稱的類繼承而來的,所以AtomicBoolean不能當Boolean用,AtomicInteger也不是Integer,雖然它確實擴充套件了Number.
          • 執行緒鎖:java.util.concurrent.locks
            • 塊結構同步方式基於鎖這樣一個簡單的概念.這種方式有幾個缺點:
              • 鎖只有一種型別
              • 對被鎖住物件的所有同步操作都是一樣的作用.
              • 在同步程式碼塊或方法開始時取得執行緒鎖.
              • 在同步程式碼塊或方法結束時釋放執行緒鎖.
              • 執行緒或者得到鎖,或者阻塞--沒有其他可能.
            • 有兩個實現類
              • ReentrantLock---本質上跟用在同步塊上那種鎖是一樣的,但它要稍微靈活點
              • ReentrantReadWriteLock---在需要讀取很多執行緒而寫入很少執行緒時,用它效能會更好.
              • 用鎖時帶上try...finally,把lock()放在try...finally塊中(釋放也在這裡)的模式是另外一個好用的小工具.在跟塊結構併發相似的情景中它同樣很好用.而另一方面,如果需要傳遞Lock物件,比如從一個方法中返回,則不能用這個模式.適用Lock物件可能要比塊結構方式強大得多,但有時用它們很難設計出完善的鎖定策略.
              • 對付死鎖的策略有很多,但你應該特別注意一個不起任何作用的策略.用帶有超時機制的Lock.tryLock()替換了無條件的鎖.通過這種辦法可以為其他執行緒提供得到執行緒鎖的機會,從而去除死鎖.但這種方法死鎖問題並沒有真正解決.
            • CountDownLatch:鎖存器
              • 是一種簡單的同步模式,這種模式允許執行緒在通過同步屏障之前做些少量的準備工作.為了達到這種效果,在構建新的CountDownLatch例項時要給它提供一個int值(計數器).