java面試題彙總

Nginx負載均衡

  • 輪詢、輪詢是默認的,每一個請求按順序逐一分配到不同的後端服務器,如果後端服務器down掉了,則能自動剔除

  • ip_hash、個請求按訪問IP的hash結果分配,這樣來自同一個IP的訪客固定訪問一個後端服務器,有效解決了動態網頁存在的session共享問題。

  • weight、weight是設置權重,用於後端服務器性能不均的情況,訪問比率約等於權重之比

  • fair(第三方)、這是比上面兩個更加智能的負載均衡算法。此種算法可以依據頁面大小和加載時間長短智能地進行負載均衡,也就是根據後端服務器的響應時間來分配請求,響應時間短的優先分配。Nginx本身是不支持fair的,如果需要使用這種調度算法,必須下載Nginx的upstream_fair模塊。

  • url_hash(第三方)此方法按訪問url的hash結果來分配請求,使每個url定向到同一個後端服務器,可以進一步提高後端緩存服務器的效率。Nginx本身是不支持url_hash的,如果需要使用這種調度算法,必須安裝Nginx 的hash軟件包。

代理的概念

正向代理,也就是傳說中的代理, 簡單的說,我是一個用戶,我訪問不了某網站,但是我能訪問一個代理服務器,這個代理服務器呢,他能訪問那個我不能訪問的網站,於是我先連上代理服務器,告訴他我需要那個無法訪問網站的內容,代理服務器去取回來,然後返回給我。從網站的角度,只在代理服務器來取內容的時候有一次記錄,有時候並不知道是用戶的請求,也隱藏了用戶的資料,這取決於代理告不告訴網站。

反向代理: 結論就是,反向代理正好相反,對於客戶端而言它就像是原始服務器,並且客戶端不需要進行任何特別的設置。客戶端向反向代理的命名空間(name-space)中的內容發送普通請求,接着反向代理將判斷向何處(原始服務器)轉交請求,並將獲得的內容返回給客戶端,就像這些內容原本就是它自己的一樣。

Volatile的特徵:

A、原子性 :對任意單個volatile變量的讀/寫具有原子性,但類似於volatile++這種複合操作不具有原子性。
B、可見性:對一個volatile變量的讀,總是能看到(任意線程)對這個volatile變量最後的寫入。

Volatile的內存語義:

當寫一個volatile變量時,JMM會把線程對應的本地內存中的共享變量值刷新到主內存。

這裏寫圖片描述

當讀一個volatile變量時,JMM會把線程對應的本地內存置爲無效,線程接下來將從主內存中讀取共享變量。

這裏寫圖片描述

Volatile的重排序

1、當第二個操作爲volatile寫操做時,不管第一個操作是什麼(普通讀寫或者volatile讀寫),都不能進行重排序。這個規則確保volatile寫之前的所有操作都不會被重排序到volatile之後;

2、當第一個操作爲volatile讀操作時,不管第二個操作是什麼,都不能進行重排序。這個規則確保volatile讀之後的所有操作都不會被重排序到volatile之前;

3、當第一個操作是volatile寫操作時,第二個操作是volatile讀操作,不能進行重排序。

這個規則和前面兩個規則一起構成了:兩個volatile變量操作不能夠進行重排序;

除以上三種情況以外可以進行重排序。

比如:

1、第一個操作是普通變量讀/寫,第二個是volatile變量的讀;
2、第一個操作是volatile變量的寫,第二個是普通變量的讀/寫;


內存屏障/內存柵欄

內存屏障(Memory Barrier,或有時叫做內存柵欄,Memory Fence)是一種CPU指令,用於控制特定條件下的重排序和內存可見性問題。Java編譯器也會根據內存屏障的規則禁止重排序。(也就是讓一個CPU處理單元中的內存狀態對其它處理單元可見的一項技術。)

內存屏障可以被分爲以下幾種類型:

LoadLoad屏障:對於這樣的語句Load1; LoadLoad; Load2,在Load2及後續讀取操作要讀取的數據被訪問前,保證Load1要讀取的數據被讀取完畢。

StoreStore屏障:對於這樣的語句Store1; StoreStore; Store2,在Store2及後續寫入操作執行前,保證Store1的寫入操作對其它處理器可見。

LoadStore屏障:對於這樣的語句Load1; LoadStore; Store2,在Store2及後續寫入操作被刷出前,保證Load1要讀取的數據被讀取完畢。

StoreLoad屏障:對於這樣的語句Store1; StoreLoad; Load2,在Load2及後續所有讀取操作執行前,保證Store1的寫入對所有處理器可見。它的開銷是四種屏障中最大的。

在大多數處理器的實現中,這個屏障是個萬能屏障,兼具其它三種內存屏障的功能。

內存屏障阻礙了CPU採用優化技術來降低內存操作延遲,必須考慮因此帶來的性能損失。爲了達到最佳性能,最好是把要解決的問題模塊化,這樣處理器可以按單元執行任務,然後在任務單元的邊界放上所有需要的內存屏障。採用這個方法可以讓處理器不受限的執行一個任務單元。合理的內存屏障組合還有一個好處是:緩衝區在第一次被刷後開銷會減少,因爲再填充改緩衝區不需要額外工作了。


happens-before原則

如果一個操作執行的結果需要對另一個操作可見,那麼這兩個操作之間必須要存在happens-before關係。

這裏寫圖片描述


Java是如何實現跨平臺的?

跨平臺是怎樣實現的呢?這就要談及Java虛擬機(Java Virtual Machine,簡稱 JVM)。

JVM也是一個軟件,不同的平臺有不同的版本。我們編寫的Java源碼,編譯後會生成一種 .class 文件,稱爲字節碼文件。Java虛擬機就是負責將字節碼文件翻譯成特定平臺下的機器碼然後運行。也就是說,只要在不同平臺上安裝對應的JVM,就可以運行字節碼文件,運行我們編寫的Java程序。

而這個過程中,我們編寫的Java程序沒有做任何改變,僅僅是通過JVM這一”中間層“,就能在不同平臺上運行,真正實現了”一次編譯,到處運行“的目的。

JVM是一個”橋樑“,是一個”中間件“,是實現跨平臺的關鍵,Java代碼首先被編譯成字節碼文件,再由JVM將字節碼文件翻譯成機器語言,從而達到運行Java程序的目的。

注意:編譯的結果不是生成機器碼,而是生成字節碼,字節碼不能直接運行,必須通過JVM翻譯成機器碼才能運行。不同平臺下編譯生成的字節碼是一樣的,但是由JVM翻譯成的機器碼卻不一樣。

所以,運行Java程序必須有JVM的支持,因爲編譯的結果不是機器碼,必須要經過JVM的再次翻譯才能執行。即使你將Java程序打包成可執行文件(例如 .exe),仍然需要JVM的支持。

注意:跨平臺的是Java程序,不是JVM。JVM是用C/C++開發的,是編譯後的機器碼,不能跨平臺,不同平臺下需要安裝不同版本的JVM。

垃圾蒐集器

  1. 按照線程數量來分:
    1. 串行 串行垃圾回收器一次只使用一個線程進行垃圾回收
    2. 並行 並行垃圾回收器一次將開啓多個線程同時進行垃圾回收。
  2. 按照工作模式來分:
    1. 併發 併發式垃圾回收器與應用程序線程交替工作,以儘可能減少應用程序的停頓時間
    2. 獨佔 一旦運行,就停止應用程序中的其他所有線程,直到垃圾回收過程完全結束
  3. 按照碎片處理方式:
    1. 壓縮式 壓縮式垃圾回收器會在回收完成後,對存活對象進行壓縮整消除回收後的碎片;
    2. 非壓縮式 非壓縮式的垃圾回收器不進行這步操作。
  4. 按工作的內存區間 可分爲新生代垃圾回收器和老年代垃圾回收器
  • 新生代串行收集器 serial 它僅僅使用單線程進行垃圾回收;第二,它獨佔式的垃圾回收。使用複製算法。

  • 老年代串行收集器 serial old 年代串行收集器使用的是標記-壓縮算法。和新生代串行收集器一樣,它也是一個串行的、獨佔式的垃圾回收器

  • 並行收集器 parnew 並行收集器是工作在新生代的垃圾收集器,它只簡單地將串行回收器多線程化。它的回收策略、算法以及參數和串行回收器一樣 並行回收器也是獨佔式的回收器,在收集過程中,應用程序會全部暫停。但由於並行回收器使用多線程進行垃圾回收,因此,在併發能力比較強的 CPU 上,它產生的停頓時間要短於串行回收器,而在單 CPU 或者併發能力較弱的系統中,並行回收器的效果不會比串行回收器好,由於多線程的壓力,它的實際表現很可能比串行回收器差。

  • 新生代並行回收 (Parallel Scavenge) 收集器 新生代並行回收收集器也是使用複製算法的收集器。從表面上看,它和並行收集器一樣都是多線程、獨佔式的收集器。但是,並行回收收集器有一個重要的特點:它非常關注系統的吞吐量。

  • 老年代並行回收收集器 parallel old 老年代的並行回收收集器也是一種多線程併發的收集器。和新生代並行回收收集器一樣,它也是一種關注吞吐量的收集器。老年代並行回收收集器使用標記-壓縮算法,JDK1.6 之後開始啓用。

  • CMS 收集器 CMS 收集器主要關注於系統停頓時間。CMS 是 Concurrent Mark Sweep 的縮寫,意爲併發標記清除,從名稱上可以得知,它使用的是標記-清除算法,同時它又是一個使用多線程併發回收的垃圾收集器。

    • CMS 工作時,主要步驟有:初始標記、併發標記、重新標記、併發清除和併發重置。其中初始標記和重新標記是獨佔系統資源的,而併發標記、併發清除和併發重置是可以和用戶線程一起執行的。因此,從整體上來說,CMS 收集不是獨佔式的,它可以在應用程序運行過程中進行垃圾回收。

      根據標記-清除算法,初始標記、併發標記和重新標記都是爲了標記出需要回收的對象。併發清理則是在標記完成後,正式回收垃圾對象;併發重置是指在垃圾回收完成後,重新初始化 CMS 數據結構和數據,爲下一次垃圾回收做好準備。併發標記、併發清理和併發重置都是可以和應用程序線程一起執行的。

  • G1 收集器 G1 收集器是基於標記-壓縮算法的。因此,它不會產生空間碎片,也沒有必要在收集完成後,進行一次獨佔式的碎片整理工作。G1 收集器還可以進行非常精確的停頓控制。

網絡基本概念

OSI模型

OSI 模型(Open System Interconnection model)是一個由國際標準化組織􏰁提出的概念模型,試圖􏰁供一個使各種不同的計算機和網絡在世界範圍內實現互聯的標準框架。
它將計算機網絡體系結構劃分爲七層,每層都可以􏰁供抽象良好的接口。瞭解 OSI 模型有助於理解實際上互聯網絡的工業標準——TCP/IP 協議。
OSI 模型各層間關係和通訊時的數據流向如圖所示:

OSI 模型.png

顯然、如果一個東西想包羅萬象、一般時不可能的;在實際的開發應用中一般時在此模型的基礎上進行裁剪、整合!

七層模型介紹

  • 物理層:
    物理層負責最後將信息編碼成電流脈衝或其它信號用於網上傳輸;
    eg:RJ45等將數據轉化成0和1;
  • 數據鏈路層:
    數據鏈路層通過物理網絡鏈路􏰁供數據傳輸。不同的數據鏈路層定義了不同的網絡和協 議特徵,其中包括物理編址、網絡拓撲結構、錯誤校驗、數據幀序列以及流控;
    可以簡單的理解爲:規定了0和1的分包形式,確定了網絡數據包的形式;
  • 網絡層
    網絡層負責在源和終點之間建立連接;
    可以理解爲,此處需要確定計算機的位置,怎麼確定?IPv4,IPv6!
  • 傳輸層
    傳輸層向高層􏰁提供可靠的端到端的網絡數據流服務。
    可以理解爲:每一個應用程序都會在網卡註冊一個端口號,該層就是端口與端口的通信!常用的(TCP/IP)協議;
  • 會話層
    會話層建立、管理和終止表示層與實體之間的通信會話;
    建立一個連接(自動的手機信息、自動的網絡尋址);
  • 表示層:
    表示層􏰁供多種功能用於應用層數據編碼和轉化,以確保以一個系統應用層發送的信息 可以被另一個系統應用層識別;
    可以理解爲:解決不同系統之間的通信,eg:Linux下的QQ和Windows下的QQ可以通信;
  • 應用層:
    OSI 的應用層協議包括文件的傳輸、訪問及管理協議(FTAM) ,以及文件虛擬終端協議(VIP)和公用管理系統信息(CMIP)等;
    規定數據的傳輸協議;

常見的應用層協議:

常見的應用層協議.png

互聯網分層結構的好處: 上層的變動完全不影響下層的結構。

TCP/IP 協議基本概念

OSI 模型所分的七層,在實際應用中,往往有一些層被整合,或者功能分散到其他層去。TCP/IP 沒有照搬 OSI 模型,也沒有 一個公認的 TCP/IP 層級模型,一般劃分爲三層到五層模型來􏰂述 TCP/IP 協議。

  • 在此描述用一個通用的四層模型來描述,每一層都和 OSI 模型有較強的相關性但是又可能會有交叉。
  • TCP/IP 的設計,是吸取了分層模型的精華思想——封裝。每層對上一層􏰁供服務的時 候,上一層的數據結構是黑盒,直接作爲本層的數據,而不需要關心上一層協議的任何細節。

TCP/IP 分層模型的分層以以太網上傳輸 UDP 數據包如圖所示;

UDP 數據包.png

數據包

寬泛意義的數據包:每一個數據包都包含"標頭"和"數據"兩個部分."標頭"包含本數據包的一些說明."數據"則是本數據包的內容.

細分數據包:

  • 應用程序數據包: 標頭部分規定應用程序的數據格式.數據部分傳輸具體的數據內容.** ——對應上圖中的數據!**
  • TCP/UDP數據包:標頭部分包含雙方的發出端口和接收端口. UDP數據包:'標頭'長度:8個字節,"數據包"總長度最大爲65535字節,正好放進一個IP數據包. TCP數據包:理論上沒有長度限制,但是,爲了保證網絡傳輸效率,通常不會超過IP數據長度,確保單個包不會被分割. ——對應上圖中的UDP數據!
  • IP數據包: 標頭部分包含通信雙方的IP地址,協議版本,長度等信息. '標頭'長度:20~60字節,"數據包"總長度最大爲65535字節. ——對應上圖中的IP數據
  • 以太網數據包: 最基礎的數據包.標頭部分包含了通信雙方的MAC地址,數據類型等. '標頭'長度:18字節,'數據'部分長度:46~1500字節. ——對應上圖中的以太網數據

四層模型

  1. 網絡接口層
    網絡接口層包括用於協作IP數據在已有網絡介質上傳輸的協議。
    它定義像地址解析協議(Address Resolution Protocol,ARP)這樣的協議,􏰁供 TCP/IP 協議的數據結構和實際物理硬件之間的接口。
    可以理解爲:確定了網絡數據包的形式
  2. 網間層
    網間層對應於 OSI 七層參考模型的網絡層,本層包含 IP 協議、RIP 協議(Routing Information Protocol,路由信息協議),負責數據的包裝、尋址和路由。同時還包含網間控制報文協議(Internet Control Message Protocol,ICMP)用來􏰁供網絡診斷信息;
    可以理解爲:該層時確定計算機的位置
  3. 傳輸層
    傳輸層對應於 OSI 七層參考模型的傳輸層,它􏰁供兩種端到端的通信服務。其中 TCP 協議(Transmission Control Protocol)􏰁供可靠的數據流運輸服務,UDP 協議(Use Datagram Protocol)􏰁供不可靠的用戶數據報服務。
    TCP:三次握手、四次揮手;UDP:只發不管別人收不收得到--任性哈
  4. 應用層
    應用層對應於 OSI 七層參考模型的應用層和表達層;
    不明白的再看看7層參考模型的描述

TCP/IP 協議族常用協議

  • 應用層:TFTP,HTTP,SNMP,FTP,SMTP,DNS,Telnet 等等
  • 傳輸層:TCP,UDP
  • 網絡層:IP,ICMP,OSPF,EIGRP,IGMP
  • 數據鏈路層:SLIP,CSLIP,PPP,MTU

重要的 TCP/IP 協議族協議進行簡單介紹:

  • IP(Internet Protocol,網際協議)是網間層的主要協議,任務是在源地址和和目的地址之間傳輸數據。IP 協議只是盡最大努力來傳輸數據包,並不保證所有的包都可以傳輸 到目的地,也不保證數據包的順序和唯一。
    • IP 定義了 TCP/IP 的地址,尋址方法,以及路由規則。現在廣泛使用的 IP 協議有 IPv4 和 IPv6 兩種:IPv4 使用 32 位二進制整數做地址,一般使用點分十進制方式表示,比如 192.168.0.1。
    • IP 地址由兩部分組成,即網絡號和主機號。故一個完整的 IPv4 地址往往表示 爲 192.168.0.1/24 或192.168.0.1/255.255.255.0 這種形式。
    • IPv6 是爲了解決 IPv4 地址耗盡和其它一些問題而研發的最新版本的 IP。使用 128 位 整數表示地址,通常使用冒號分隔的十六進制來表示,並且可以省略其中一串連續的 0,如:fe80::200:1ff:fe00:1。
      目前使用並不多!
  • ICMP(Internet Control Message Protocol,網絡控制消息協議)是 TCP/IP 的 核心協議之一,用於在 IP 網絡中發送控制消息,􏰁供通信過程中的各種問題反饋。 ICMP 直接使用 IP 數據包傳輸,但 ICMP 並不被視爲 IP 協議的子協議。常見的聯網狀態診斷工具比如依賴於 ICMP 協議;
  • TCP(TransmissionControlProtocol,傳輸控制協議)是一種面向連接的,可靠的, 基於字節流傳輸的通信協議。TCP 具有端口號的概念,用來標識同一個地址上的不 同應用。􏰂述 TCP 的標準文檔是 RFC793。
  • UDP(UserDatagramProtocol,用戶數據報協議)是一個面向數據報的傳輸層協 議。UDP 的傳輸是不可靠的,簡單的說就是發了不管,發送者不會知道目標地址 的數據通路是否發生擁塞,也不知道數據是否到達,是否完整以及是否還是原來的 次序。它同 TCP 一樣有用來標識本地應用的端口號。所以應用 UDP 的應用,都能 夠容忍一定數量的錯誤和丟包,但是對傳輸性能敏感的,比如流媒體、DNS 等。
  • ECHO(EchoProtocol,回聲協議)是一個簡單的調試和檢測工具。服務器器會 原樣回發它收到的任何數據,既可以使用 TCP 傳輸,也可以使用 UDP 傳輸。使用 端口號 7 。
  • DHCP(DynamicHostConfigrationProtocol,動態主機配置協議)是用於局域 網自動分配 IP 地址和主機配置的協議。可以使局域網的部署更加簡單。
  • DNS(DomainNameSystem,域名系統)是互聯網的一項服務,可以簡單的將用“.” 分隔的一般會有意義的域名轉換成不易記憶的 IP 地址。一般使用 UDP 協議傳輸, 也可以使用 TCP,默認服務端口號 53。􏰂
  • FTP(FileTransferProtocol,文件傳輸協議)是用來進行文件傳輸的標準協議。 FTP 基於 TCP 使用端口號 20 來傳輸數據,21 來傳輸控制信息。
  • TFTP(Trivial File Transfer Protocol,簡單文件傳輸協議)是一個簡化的文 件傳輸協議,其設計非常簡單,通過少量存儲器就能輕鬆實現,所以一般被用來通 過網絡引導計算機過程中傳輸引導文件等小文件;
  • SSH(SecureShell,安全Shell),因爲傳統的網絡服務程序比如TELNET本質上都極不安全,明文傳說數據和用戶信息包括密碼,SSH 被開發出來避免這些問題, 它其實是一個協議框架,有大量的擴展冗餘能力,並且􏰁供了加密壓縮的通道可以 爲其他協議使用。
  • POP(PostOfficeProtocol,郵局協議)是支持通過客戶端訪問電子郵件的服務, 現在版本是 POP3,也有加密的版本 POP3S。協議使用 TCP,端口 110。
  • SMTP(Simple Mail Transfer Protocol,簡單郵件傳輸協議)是現在在互聯網 上發送電子郵件的事實標準。使用 TCP 協議傳輸,端口號 25。
  • HTTP(HyperTextTransferProtocol,超文本傳輸協議)是現在廣爲流行的WEB 網絡的基礎,HTTPS 是 HTTP 的加密安全版本。協議通過 TCP 傳輸,HTTP 默認 使用端口 80,HTTPS 使用 443。

以上就是今天回顧的內容。
下篇回顧一下socket、TCP、UDP!

線程池

Executor 框架便是 Java 5 中引入的,其內部使用了線程池機制

好處

第一:降低資源消耗 通過重複利用已創建的線程降低線程創建和銷燬造成的消耗。

第二:提高響應速度。當任務到達時,任務可以不需要等到線程創建就能立即執行。

第三:提高線程的可管理性。線程是稀缺資源,如果無限制的創建,不僅會消耗系統資源,還會降低系統的穩定性,使用線程池可以進行統一的分配,調優和監控。但是要做到合理的利用線程池,必須對其原理瞭如指掌。

Java線程間的通信方式

wait()方法

wait()方法使得當前線程必須要等待,等到另外一個線程調用notify()或者notifyAll()方法。

當前的線程必須擁有當前對象的monitor,也即lock,就是鎖。

線程調用wait()方法,釋放它對鎖的擁有權,然後等待另外的線程來通知它(通知的方式是notify()或者notifyAll()方法),這樣它才能重新獲得鎖的擁有權和恢復執行。

要確保調用wait()方法的時候擁有鎖,即,wait()方法的調用必須放在synchronized方法或synchronized塊中。

一個小比較:

當線程調用了wait()方法時,它會釋放掉對象的鎖。

另一個會導致線程暫停的方法:Thread.sleep(),它會導致線程睡眠指定的毫秒數,但線程在睡眠的過程中是不會釋放掉對象的鎖的。

notify()方法

notify()方法會喚醒一個等待當前對象的鎖的線程。

如果多個線程在等待,它們中的一個將會選擇被喚醒。這種選擇是隨意的,和具體實現有關。(線程等待一個對象的鎖是由於調用了wait方法中的一個)。

被喚醒的線程是不能被執行的,需要等到當前線程放棄這個對象的鎖。

被喚醒的線程將和其他線程以通常的方式進行競爭,來獲得對象的鎖。也就是說,被喚醒的線程並沒有什麼優先權,也沒有什麼劣勢,對象的下一個線程還是需要通過一般性的競爭。

notify()方法應該是被擁有對象的鎖的線程所調用。

(This method should only be called by a thread that is the owner of this object's monitor.)

換句話說,和wait()方法一樣,notify方法調用必須放在synchronized方法或synchronized塊中。

wait()和notify()方法要求在調用時線程已經獲得了對象的鎖,因此對這兩個方法的調用需要放在synchronized方法或synchronized塊中。

  一個線程變爲一個對象的鎖的擁有者是通過下列三種方法:

1.執行這個對象的synchronized實例方法。

2.執行這個對象的synchronized語句塊。這個語句塊鎖的是這個對象。

3.對於Class類的對象,執行那個類的synchronized、static方法。


Java 線程有哪些狀態,這些狀態之間是如何轉化的?

這裏寫圖片描述

  1. 新建(new):新創建了一個線程對象。
  2. 可運行(runnable):線程對象創建後,其他線程(比如main線程)調用了該對象的start()方法。該狀態的線程位於可運行線程池中,等待被線程調度選中,獲取cpu 的使用權 。
  3. 運行(running):可運行狀態(runnable)的線程獲得了cpu 時間片(timeslice) ,執行程序代碼。
  4. 阻塞(block):阻塞狀態是指線程因爲某種原因放棄了cpu 使用權,也即讓出了cpu timeslice,暫時停止運行。直到線程進入可運行(runnable)狀態,纔有機會再次獲得cpu timeslice 轉到運行(running)狀態。阻塞的情況分三種:

(一). 等待阻塞:運行(running)的線程執行o.wait()方法,JVM會把該線程放入等待隊列(waitting queue)中。同時釋放對象鎖

(二). 同步阻塞:運行(running)的線程在獲取對象的同步鎖時,若該同步鎖被別的線程佔用,則JVM會把該線程放入鎖池(lock pool)中。

(三). 其他阻塞:運行(running)的線程執行Thread.sleep(long ms)或t.join()方法,或者發出了I/O請求時,JVM會把該線程置爲阻塞狀態。當sleep()狀態超時、join()等待線程終止或者超時、或者I/O處理完畢時,線程重新轉入可運行(runnable)狀態。

  1. 死亡(dead):線程run()、main() 方法執行結束,或者因異常退出了run()方法,則該線程結束生命週期。死亡的線程不可再次復生。

List接口、Set接口和Map接口的區別

1、List和Set接口自Collection接口,而Map不是繼承的Collection接口

Collection表示一組對象,這些對象也稱爲collection的元素;一些 collection允許有重複的元素,而另一些則不允許;一些collection是有序的,而另一些則是無序的;JDK中不提供此接口的任何直接實 現,它提供更具體的子接口(如 Set 和 List)實現;Map沒有繼承Collection接口,Map提供key到value的映射;一個Map中不能包含相同key,每個key只能映射一個value;Map接口提供3種集合的視圖,Map的內容可以被當做一組key集合,一組value集合,或者一組key-value映射;

2、List接口

元素有放入順序,元素可重複 
List接口有三個實現類:LinkedList,ArrayList,Vector       
LinkedList:底層基於鏈表實現,鏈表內存是散亂的,每一個元素存儲本身內存地址的同時還存儲下一個元素的地址。鏈表增刪快,查找慢 
ArrayList和Vector的區別:ArrayList是非線程安全的,效率高;Vector是基於線程安全的,效率低 
List是一種有序的Collection,可以通過索引訪問集合中的數據,List比Collection多了10個方法,主要是有關索引的方法。
      1).所有的索引返回的方法都有可能拋出一個IndexOutOfBoundsException異常
      2).subList(int fromIndex, int toIndex)返回的是包括fromIndex,不包括toIndex的視圖,該列表的size()=toIndex-fromIndex。
      所有的List中只能容納單個不同類型的對象組成的表,而不是Key-Value鍵值對。例如:[ tom,1,c ];
      所有的List中可以有相同的元素,例如Vector中可以有 [ tom,koo,too,koo ];
      所有的List中可以有null元素,例如[ tom,null,1 ];
      基於Array的List(Vector,ArrayList)適合查詢,而LinkedList(鏈表)適合添加,刪除操作;

3、Set接口

元素無放入順序,元素不可重複(注意:元素雖然無放入順序,但是元素在set中的位置是有該元素的HashCode決定的,其位置其實是固定的)
Set接口有兩個實現類:HashSet(底層由HashMap實現),LinkedHashSet 
  SortedSet接口有一個實現類:TreeSet(底層由平衡二叉樹實現)
  Query接口有一個實現類:LinkList 
  Set具有與Collection完全一樣的接口,因此沒有任何額外的功能,不像前面有兩個不同的List。實際上Set就是Collection,只是行爲不同。(這是繼承與多態思想的典型應用:表現不同的行爲。)Set不保存重複的元素(至於如何判斷元素相同則較爲負責)
  Set : 存入Set的每個元素都必須是唯一的,因爲Set不保存重複元素。加入Set的元素必須定義equals()方法以確保對象的唯一性。Set與Collection有完全一樣的接口。Set接口不保證維護元素的次序。
  HashSet : 爲快速查找設計的Set。存入HashSet的對象必須定義hashCode()。
  TreeSet : 保存次序的Set, 底層爲樹結構。使用它可以從Set中提取有序的序列。
  LinkedHashSet : 具有HashSet的查詢速度,且內部使用鏈表維護元素的順序(插入的次序)。於是在使用迭代器遍歷Set時,結果會按元素插入的次序顯示。

4、map接口

以鍵值對的方式出現的 
Map接口有三個實現類:HashMap,HashTable,LinkeHashMap 

      HashMap非線程安全,高效,支持null;

      HashTable線程安全,低效,不支持null 

      SortedMap有一個實現類:TreeMap 
  

Session機制

一、術語session

session,中文經常翻譯爲會話,其本來的含義是指有始有終的一系列動作/消息,比如打電話時從拿起電話撥號到掛斷電話這中間的一系列過程可以稱之爲一個session。有時候我們可以看到這樣的話“在一個瀏覽器會話期間,...”,這裏的會話一詞用的就是其本義,是指從一個瀏覽器窗口打開到關閉這個期間①。最混亂的是“用戶(客戶端)在一次會話期間”這樣一句話,它可能指用戶的一系列動作(一般情況下是同某個具體目的相關的一系列動作,比如從登錄到選購商品到結賬登出這樣一個網上購物的過程,有時候也被稱爲一個transaction),然而有時候也可能僅僅是指一次連接,也有可能是指含義①,其中的差別只能靠上下文來推斷②。
然而當session一詞與網絡協議相關聯時,它又往往隱含了“面向連接”和/或“保持狀態”這樣兩個含義,“面向連接”指的是在通信雙方在通信之前要先建立一個通信的渠道,比如打電話,直到對方接了電話通信才能開始,與此相對的是寫信,在你把信發出去的時候你並不能確認對方的地址是否正確,通信渠道不一定能建立,但對發信人來說,通信已經開始了。“保持狀態”則是指通信的一方能夠把一系列的消息關聯起來,使得消息之間可以互相依賴,比如一個服務員能夠認出再次光臨的老顧客並且記得上次這個顧客還欠店裏一塊錢。這一類的例子有“一個TCP session”或者“一個POP3 session”③。
而到了web服務器蓬勃發展的時代,session在web開發語境下的語義又有了新的擴展,它的含義是指一類用來在客戶端與服務器之間保持狀態的解決方案④。有時候session也用來指這種解決方案的存儲結構,如“把xxx保存在session裏”⑤。由於各種用於web開發的語言在一定程度上都提供了對這種解決方案的支持,所以在某種特定語言的語境下,session也被用來指代該語言的解決方案,比如經常把Java裏提供的javax.servlet.http.HttpSession簡稱爲session⑥。
鑑於這種混亂已不可改變,本文中session一詞的運用也會根據上下文有不同的含義,請大家注意分辨。
在本文中,使用中文“瀏覽器會話期間”來表達含義①,使用“session機制”來表達含義④,使用“session”表達含義⑤,使用具體的“HttpSession”來表達含義⑥
** 二、HTTP協議與狀態保持**
HTTP協議本身是無狀態的,這與HTTP協議本來的目的是相符的,客戶端只需要簡單的向服務器請求下載某些文件,無論是客戶端還是服務器都沒有必要紀錄彼此過去的行爲,每一次請求之間都是獨立的,好比一個顧客和一個自動售貨機或者一個普通的(非會員制)大賣場之間的關係一樣。
然而聰明(或者貪心?)的人們很快發現如果能夠提供一些按需生成的動態信息會使web變得更加有用,就像給有線電視加上點播功能一樣。這種需求一方面迫使HTML逐步添加了表單、腳本、DOM等客戶端行爲,另一方面在服務器端則出現了CGI規範以響應客戶端的動態請求,作爲傳輸載體的HTTP協議也添加了文件上載、cookie這些特性。其中cookie的作用就是爲了解決HTTP協議無狀態的缺陷所作出的努力。至於後來出現的session機制則是又一種在客戶端與服務器之間保持狀態的解決方案。
讓我們用幾個例子來描述一下cookie和session機制之間的區別與聯繫。筆者曾經常去的一家咖啡店有喝5杯咖啡免費贈一杯咖啡的優惠,然而一次性消費5杯咖啡的機會微乎其微,這時就需要某種方式來紀錄某位顧客的消費數量。想象一下其實也無外乎下面的幾種方案:
1、該店的店員很厲害,能記住每位顧客的消費數量,只要顧客一走進咖啡店,店員就知道該怎麼對待了。這種做法就是協議本身支持狀態。
2、發給顧客一張卡片,上面記錄着消費的數量,一般還有個有效期限。每次消費時,如果顧客出示這張卡片,則此次消費就會與以前或以後的消費相聯繫起來。這種做法就是在客戶端保持狀態。
3、發給顧客一張會員卡,除了卡號之外什麼信息也不紀錄,每次消費時,如果顧客出示該卡片,則店員在店裏的紀錄本上找到這個卡號對應的紀錄添加一些消費信息。這種做法就是在服務器端保持狀態。
由於HTTP協議是無狀態的,而出於種種考慮也不希望使之成爲有狀態的,因此,後面兩種方案就成爲現實的選擇。具體來說cookie機制採用的是在客戶端保持狀態的方案,而session機制採用的是在服務器端保持狀態的方案。同時我們也看到,由於採用服務器端保持狀態的方案在客戶端也需要保存一個標識,所以session機制可能需要藉助於cookie機制來達到保存標識的目的,但實際上它還有其他選擇。
**三、理解cookie機制 **
cookie機制的基本原理就如上面的例子一樣簡單,但是還有幾個問題需要解決:“會員卡”如何分發;“會員卡”的內容;以及客戶如何使用“會員卡”。
正統的cookie分發是通過擴展HTTP協議來實現的,服務器通過在HTTP的響應頭中加上一行特殊的指示以提示瀏覽器按照指示生成相應的cookie。然而純粹的客戶端腳本如JavaScript或者VBScript也可以生成cookie。
而cookie的使用是由瀏覽器按照一定的原則在後臺自動發送給服務器的。瀏覽器檢查所有存儲的cookie,如果某個cookie所聲明的作用範圍大於等於將要請求的資源所在的位置,則把該cookie附在請求資源的HTTP請求頭上發送給服務器。意思是麥當勞的會員卡只能在麥當勞的店裏出示,如果某家分店還發行了自己的會員卡,那麼進這家店的時候除了要出示麥當勞的會員卡,還要出示這家店的會員卡。
cookie的內容主要包括:名字,值,過期時間,路徑和域。
其中域可以指定某一個域比如.google.com,相當於總店招牌,比如寶潔公司,也可以指定一個域下的具體某臺機器比如www.google.com或者froogle.google.com,可以用飄柔來做比。
路徑就是跟在域名後面的URL路徑,比如/或者/foo等等,可以用某飄柔專櫃做比。路徑與域合在一起就構成了cookie的作用範圍。如果不設置過期時間,則表示這個cookie的生命期爲瀏覽器會話期間,只要關閉瀏覽器窗口,cookie就消失了。這種生命期爲瀏覽器會話期的cookie被稱爲會話cookie。會話cookie一般不存儲在硬盤上而是保存在內存裏,當然這種行爲並不是規範規定的。如果設置了過期時間,瀏覽器就會把cookie保存到硬盤上,關閉後再次打開瀏覽器,這些cookie仍然有效直到超過設定的過期時間。
存儲在硬盤上的cookie可以在不同的瀏覽器進程間共享,比如兩個IE窗口。而對於保存在內存裏的cookie,不同的瀏覽器有不同的處理方式。對於IE,在一個打開的窗口上按Ctrl-N(或者從文件菜單)打開的窗口可以與原窗口共享,而使用其他方式新開的IE進程則不能共享已經打開的窗口的內存cookie;對於Mozilla Firefox0.8,所有的進程和標籤頁都可以共享同樣的cookie。一般來說是用javascript的window.open打開的窗口會與原窗口共享內存cookie。瀏覽器對於會話cookie的這種只認cookie不認人的處理方式經常給採用session機制的web應用程序開發者造成很大的困擾。

Cookie和Session的區別

HTTP請求是無狀態的。

共同之處

cookie和session都是用來跟蹤瀏覽器用戶身份的會話方式。

區別

  • cookie數據保存在客戶端,session數據保存在服務器端。簡單的說,當你登錄一個網站的時候, 如果web服務器端使用的是session,那麼所有的數據都保存在服務器上,客戶端每次請求服務器的時候會發送當前會話的sessionid,服務器根據當前sessionid判斷相應的用戶數據標誌,以確定用戶是否登錄或具有某種權限。由於數據是存儲在服務器上面,所以你不能僞造。
  • sessionid是服務器和客戶端鏈接時候隨機分配的. 如果瀏覽器使用的是cookie,那麼所有的數據都保存在瀏覽器端,比如你登錄以後,服務器設置了cookie用戶名,那麼當你再次請求服務器的時候,瀏覽器會將用戶名一塊發送給服務器,這些變量有一定的特殊標記。服務器會解釋爲cookie變量,所以只要不關閉瀏覽器,那麼cookie變量一直是有效的,所以能夠保證長時間不掉線。如果你能夠截獲某個用戶的 cookie變量,然後僞造一個數據包發送過去,那麼服務器還是認爲你是合法的。所以,使用 cookie被攻擊的可能性比較大。

如果設置了的有效時間,那麼它會將 cookie保存在客戶端的硬盤上,下次再訪問該網站的時候,瀏覽器先檢查有沒有 cookie,如果有的話,就讀取該 cookie,然後發送給服務器。如果你在機器上面保存了某個論壇 cookie,有效期是一年,如果有人入侵你的機器,將你的 cookie拷走,然後放在他的瀏覽器的目錄下面,那麼他登錄該網站的時候就是用你的的身份登錄的。所以 cookie是可以僞造的。當然,僞造的時候需要主意,直接copy cookie文件到 cookie目錄,瀏覽器是不認的,他有一個index.dat文件,存儲了 cookie文件的建立時間,以及是否有修改,所以你必須先要有該網站的 cookie文件,並且要從保證時間上騙過瀏覽器

兩個都可以用來存私密的東西,同樣也都有有效期的說法,區別在於session是放在服務器上的,過期與否取決於服務期的設定,cookie是存在客戶端的,過去與否可以在cookie生成的時候設置進去。

(1)cookie數據存放在客戶的瀏覽器上,session數據放在服務器上
(2)cookie不是很安全,別人可以分析存放在本地的COOKIE並進行COOKIE欺騙,如果主要考慮到安全應當使用session
(3)session會在一定時間內保存在服務器上。當訪問增多,會比較佔用你服務器的性能,如果主要考慮到減輕服務器性能方面,應當使用COOKIE
(4)單個cookie在客戶端的限制是3K,就是說一個站點在客戶端存放的COOKIE不能3K。
(5)所以:將登陸信息等重要信息存放爲SESSION;其他信息如果需要保留,可以放在COOKIE中

Java中的equals和hashCode方法詳解

equals()方法是用來判斷其他的對象是否和該對象相等.

equals()方法在object類中定義如下:

public boolean equals(Object obj) {  
   return (this == obj);  
}  

很明顯是對兩個對象的地址值進行的比較(即比較引用是否相同)。但是我們知道,String 、Math、Integer、Double等這些封裝類在使用equals()方法時,已經覆蓋了object類的equals()方法。

比如在String類中如下:

 

 

[

複製代碼

](javascript:void(0);)

public boolean equals(Object anObject) {  
   if (this == anObject) {  
       return true;  
   }  
   if (anObject instanceof String) {  
       String anotherString = (String)anObject;  
       int n = count;  
       if (n == anotherString.count) {  
           char v1[] = value;  
           char v2[] = anotherString.value;  
           int i = offset;  
           int j = anotherString.offset;  
           while (n– != 0) {  
               if (v1[i++] != v2[j++])  
                   return false;  
           }  
           return true;  
       }  
   }  
   return false;  
}  

很明顯,這是進行的內容比較,而已經不再是地址的比較。依次類推Math、Integer、Double等這些類都是重寫了equals()方法的,從而進行的是內容的比較。當然,基本類型是進行值的比較。

它的性質有:

  • 自反性(reflexive)。對於任意不爲null的引用值x,x.equals(x)一定是true
  • 對稱性(symmetric)。對於任意不爲null的引用值xy,當且僅當x.equals(y)true時,y.equals(x)也是true
  • 傳遞性(transitive)。對於任意不爲null的引用值xyz,如果x.equals(y)true,同時y.equals(z)true,那麼x.equals(z)一定是true
  • 一致性(consistent)。對於任意不爲null的引用值xy,如果用於equals比較的對象信息沒有被修改的話,多次調用時x.equals(y)要麼一致地返回true要麼一致地返回false
  • 對於任意不爲null的引用值xx.equals(null)返回false

對於Object類來說,equals()方法在對象上實現的是差別可能性最大的等價關係,即,對於任意非null的引用值xy,當且僅當xy引用的是同一個對象,該方法纔會返回true

需要注意的是當equals()方法被override時,hashCode()也要被override。按照一般hashCode()方法的實現來說,相等的對象,它們的hash code一定相等。

hashcode() 方法詳解

hashCode()方法給對象返回一個hash code值。這個方法被用於hash tables,例如HashMap。

它的性質是:

  • 在一個Java應用的執行期間,如果一個對象提供給equals做比較的信息沒有被修改的話,該對象多次調用hashCode()方法,該方法必須始終如一返回同一個integer。
  • 如果兩個對象根據equals(Object)方法是相等的,那麼調用二者各自的hashCode()方法必須產生同一個integer結果。
  • 並不要求根據equals(java.lang.Object)方法不相等的兩個對象,調用二者各自的hashCode()方法必須產生不同的integer結果。然而,程序員應該意識到對於不同的對象產生不同的integer結果,有可能會提高hash table的性能。

Java中CAS算法--樂觀鎖的一種實現方式

悲觀者與樂觀者的做事方式完全不一樣,悲觀者的人生觀是一件事情我必須要百分之百完全控制纔會去做,否則就認爲這件事情一定會出問題;而樂觀者的人生觀則相反,凡事不管最終結果如何,他都會先嚐試去做,大不了最後不成功。這就是悲觀鎖與樂觀鎖的區別,悲觀鎖會把整個對象加鎖佔爲自有後纔去做操作,樂觀鎖不獲取鎖直接做操作,然後通過一定檢測手段決定是否更新數據。這一節將對樂觀鎖進行深入探討。

上節討論的Synchronized互斥鎖屬於悲觀鎖,它有一個明顯的缺點,它不管數據存不存在競爭都加鎖,隨着併發量增加,且如果鎖的時間比較長,其性能開銷將會變得很大。有沒有辦法解決這個問題?答案是基於衝突檢測的樂觀鎖。這種模式下,已經沒有所謂的鎖概念了,每條線程都直接先去執行操作,計算完成後檢測是否與其他線程存在共享數據競爭,如果沒有則讓此操作成功,如果存在共享數據競爭則可能不斷地重新執行操作和檢測,直到成功爲止,可叫CAS自旋。

樂觀鎖的核心算法是CAS(Compareand Swap,比較並交換),它涉及到三個操作數:內存值、預期值、新值。當且僅當預期值和內存值相等時纔將內存值修改爲新值。這樣處理的邏輯是,首先檢查某塊內存的值是否跟之前我讀取時的一樣,如不一樣則表示期間此內存值已經被別的線程更改過,捨棄本次操作,否則說明期間沒有其他線程對此內存值操作,可以把新值設置給此塊內存。如圖2-5-4-1,有兩個線程可能會差不多同時對某內存操作,線程二先讀取某內存值作爲預期值,執行到某處時線程二決定將新值設置到內存塊中,如果線程一在此期間修改了內存塊,則通過CAS即可以檢測出來,假如檢測沒問題則線程二將新值賦予內存塊。

img

圖2-5-4-1

假如你足夠細心你可能會發現一個疑問,比較和交換,從字面上就有兩個操作了,更別說實際CAS可能會有更多的執行指令,他們是原子性的嗎?如果非原子性又怎麼保證CAS操作期間出現併發帶來的問題?我是不是需要用上節提到的互斥鎖來保證他的原子性操作?CAS肯定是具有原子性的,不然就談不上在併發中使用了,但這個原子性是由CPU硬件指令實現保證的,即使用JNI調用native方法調用由C++編寫的硬件級別指令,jdk中提供了Unsafe類執行這些操作。另外,你可能想着CAS是通過互斥鎖來實現原子性的,這樣確實能實現,但用這種方式來保證原子性顯示毫無意義。下面一個僞代碼加深對CAS的理解:

public class AtomicInt {
 private volatile int value;
 public final int get() {
     return value;
  }
public final int getAndIncrement() {
     for (;;) {
         int current = get();
         int next = current + 1;
         if (compareAndSet(current, next))
              return current;
     }
  }
 public final boolean compareAndSet(int expect, int update) {
   Unsafe類提供的硬件級別的compareAndSwapInt方法;
  }
}

其中最重要的方法是getAndIncrement方法,它裏面實現了基於CAS的自旋。

現在已經瞭解樂觀鎖及CAS相關機制,樂觀鎖避免了悲觀鎖獨佔對象的現象,同時也提高了併發性能,但它也有缺點:

① 觀鎖只能保證一個共享變量的原子操作。如上例子,自旋過程中只能保證value變量的原子性,這時如果多一個或幾個變量,樂觀鎖將變得力不從心,但互斥鎖能輕易解決,不管對象數量多少及對象顆粒度大小。

② 長時間自旋可能導致開銷大。假如CAS長時間不成功而一直自旋,會給CPU帶來很大的開銷。

③ ABA問題。CAS的核心思想是通過比對內存值與預期值是否一樣而判斷內存值是否被改過,但這個判斷邏輯不嚴謹,假如內存值原來是A,後來被一條線程改爲B,最後又被改成了A,則CAS認爲此內存值並沒有發生改變,但實際上是有被其他線程改過的,這種情況對依賴過程值的情景的運算結果影響很大。解決的思路是引入版本號,每次變量更新都把版本號加一。

樂觀鎖是對悲觀鎖的改進,雖然它也有缺點,但它確實已經成爲提高併發性能的主要手段,而且jdk中的併發包也大量使用基於CAS的樂觀鎖。

TimSort原理

 

comparable與comparator的區別

Comparable和Comparator的區別

初次碰到這個問題是之前有一次電話面試,問了一個小時的問題,其中有一個問題就問到Comparable和Comparator的區別,當時沒答出 來。之後是公司入職時候做的一套Java編程題,裏面用JUnit跑用例的時候也用到了Comparator接口,再加上JDK的大量的類包括常見的 String、Byte、Char、Date等都實現了Comparable接口,因此要學習一下這兩個類的區別以及用法。

Comparable

Comparable可以認爲是一個內比較器,實現了Comparable接口的類有一個特點,就是這些類是可以和自己比較的,至於具體和另一個實現了Comparable接口的類如何比較,則依賴compareTo方法的實現,compareTo方法也被稱爲自然比較方法。如果開發者add進入一個Collection的對象想要Collections的sort方法幫你自動進行排序的話,那麼這個對象必須實現Comparable接口。compareTo方法的返回值是int,有三種情況:

1、比較者大於被比較者(也就是compareTo方法裏面的對象),那麼返回正整數

2、比較者等於被比較者,那麼返回0

3、比較者小於被比較者,那麼返回負整數

寫個很簡單的例子:

public class Domain implements Comparable<Domain>
{
   private String str;

   public Domain(String str)
   {
       this.str = str;
   }

   public int compareTo(Domain domain)
   {
       if (this.str.compareTo(domain.str) > 0)
           return 1;
       else if (this.str.compareTo(domain.str) == 0)
           return 0;
       else 
           return -1;
   }
   
   public String getStr()
   {
       return str;
   }
}
public static void main(String[] args)
   {
       Domain d1 = new Domain("c");
       Domain d2 = new Domain("c");
       Domain d3 = new Domain("b");
       Domain d4 = new Domain("d");
       System.out.println(d1.compareTo(d2));
       System.out.println(d1.compareTo(d3));
       System.out.println(d1.compareTo(d4));
   }

運行結果爲:

0
1
-1

注意一下,前面說實現Comparable接口的類是可以支持和自己比較的,但是其實代碼裏面Comparable的泛型未必就一定要是Domain,將泛型指定爲String或者指定爲其他任何任何類型都可以----只要開發者指定了具體的比較算法就行。

Comparator

Comparator可以認爲是是一個外比較器,個人認爲有兩種情況可以使用實現Comparator接口的方式:

1、一個對象不支持自己和自己比較(沒有實現Comparable接口),但是又想對兩個對象進行比較

2、一個對象實現了Comparable接口,但是開發者認爲compareTo方法中的比較方式並不是自己想要的那種比較方式

Comparator接口裏面有一個compare方法,方法有兩個參數T o1和T o2,是泛型的表示方式,分別表示待比較的兩個對象,方法返回值和Comparable接口一樣是int,有三種情況:

1、o1大於o2,返回正整數

2、o1等於o2,返回0

3、o1小於o3,返回負整數

寫個很簡單的例子,上面代碼的Domain不變(假設這就是第2種場景,我對這個compareTo算法實現不滿意,要自己寫實現):

public class DomainComparator implements Comparator<Domain>
{
   public int compare(Domain domain1, Domain domain2)
   {
       if (domain1.getStr().compareTo(domain2.getStr()) > 0)
           return 1;
       else if (domain1.getStr().compareTo(domain2.getStr()) == 0)
           return 0;
       else 
           return -1;
   }
}
public static void main(String[] args)
{
   Domain d1 = new Domain("c");
   Domain d2 = new Domain("c");
   Domain d3 = new Domain("b");
   Domain d4 = new Domain("d");
   DomainComparator dc = new DomainComparator();
   System.out.println(dc.compare(d1, d2));
   System.out.println(dc.compare(d1, d3));
   System.out.println(dc.compare(d1, d4));
}

看一下運行結果:

0
1
-1

當然因爲泛型指定死了,所以實現Comparator接口的實現類只能是兩個相同的對象(不能一個Domain、一個String)進行比較了,因此實現Comparator接口的實現類一般都會以"待比較的實體類+Comparator"來命名

總結

總結一下,兩種比較器Comparable和Comparator,後者相比前者有如下優點:

1、如果實現類沒有實現Comparable接口,又想對兩個類進行比較(或者實現類實現了Comparable接口,但是對compareTo方法內的比較算法不滿意),那麼可以實現Comparator接口,自定義一個比較器,寫比較算法

2、實現Comparable接口的方式比實現Comparator接口的耦合性 要強一些,如果要修改比較算法,要修改Comparable接口的實現類,而實現Comparator的類是在外部進行比較的,不需要對實現類有任何修 改。從這個角度說,其實有些不太好,尤其在我們將實現類的.class文件打成一個.jar文件提供給開發者使用的時候。實際上實現Comparator 接口的方式後面會寫到就是一種典型的策略模式

手寫單例模式(線程安全)

解法一:只適合單線程環境(不好)

package test;
/**
* @author xiaoping
*
*/
public class Singleton {
   private static Singleton instance=null;
   private Singleton(){
       
   }
   public static Singleton getInstance(){
       if(instance==null){
           instance=new Singleton();
       }
       return instance;
   }
}

註解:Singleton的靜態屬性instance中,只有instance爲null的時候才創建一個實例,構造函數私有,確保每次都只創建一個,避免重複創建。
缺點:只在單線程的情況下正常運行,在多線程的情況下,就會出問題。例如:當兩個線程同時運行到判斷instance是否爲空的if語句,並且instance確實沒有創建好時,那麼兩個線程都會創建一個實例。

解法二:多線程的情況可以用。(懶漢式,不好)

public class Singleton {
   private static Singleton instance=null;
   private Singleton(){
       
   }
   public static synchronized Singleton getInstance(){
       if(instance==null){
           instance=new Singleton();
       }
       return instance;
   }
}

註解:在解法一的基礎上加上了同步鎖,使得在多線程的情況下可以用。例如:當兩個線程同時想創建實例,由於在一個時刻只有一個線程能得到同步鎖,當第一個線程加上鎖以後,第二個線程只能等待。第一個線程發現實例沒有創建,創建之。第一個線程釋放同步鎖,第二個線程纔可以加上同步鎖,執行下面的代碼。由於第一個線程已經創建了實例,所以第二個線程不需要創建實例。保證在多線程的環境下也只有一個實例。
缺點:每次通過getInstance方法得到singleton實例的時候都有一個試圖去獲取同步鎖的過程。而衆所周知,加鎖是很耗時的。能避免則避免。

解法三:加同步鎖時,前後兩次判斷實例是否存在(可行)

public class Singleton {
   private static Singleton instance=null;
   private Singleton(){ }
   public static Singleton getInstance(){
       if(instance==null){
           synchronized(Singleton.class){
               if(instance==null){
                   instance=new Singleton();
               }
           }
       }
       return instance;
   }
}

註解:只有當instance爲null時,需要獲取同步鎖,創建一次實例。當實例被創建,則無需試圖加鎖。
缺點:用雙重if判斷,複雜,容易出錯。

解法四:餓漢式(建議使用)

public class Singleton {
   private static Singleton instance=new Singleton();
   private Singleton(){
   }
   public static Singleton getInstance(){
       return instance;
   }
}

註解:初試化靜態的instance創建一次。如果我們在Singleton類裏面寫一個靜態的方法不需要創建實例,它仍然會早早的創建一次實例。而降低內存的使用率。

缺點:沒有lazy loading的效果,從而降低內存的使用率。

解法五:靜態內部內。(建議使用)

public class Singleton {
   private Singleton(){
       
   }
   private static class SingletonHolder{
       private final static Singleton instance=new Singleton();
   }
   public static Singleton getInstance(){
       return SingletonHolder.instance;
   }
}

註解:定義一個私有的內部類,在第一次用這個嵌套類時,會創建一個實例。而類型爲SingletonHolder的類,只有在Singleton.getInstance()中調用,由於私有的屬性,他人無法使用SingleHolder,不調用Singleton.getInstance()就不會創建實例。
優點:達到了lazy loading的效果,即按需創建實例。

 

JVM參數初始值

初始堆大小:1/64內存-Xms 最大堆大小:1/4內存-Xmx

初始永久代大小:1/64內存-XX:PermSize 最大堆大小:1/4內存-XX:MaxPermSize

Java8的內存分代改進

JAVA 8持久代已經被徹底刪除了

取代它的是另一個內存區域也被稱爲元空間。

元空間 —— 快速入門

  • 它是本地內存中的一部分
  • 最直接的表現就是OOM(內存溢出)問題將不復存在,因爲直接利用的是本地內存。
  • 它可以通過-XX:MetaspaceSize和-XX:MaxMetaspaceSize來進行調整
  • 當到達XX:MetaspaceSize所指定的閾值後會開始進行清理該區域
  • 如果本地空間的內存用盡了會收到java.lang.OutOfMemoryError: Metadata space的錯誤信息。
  • 和持久代相關的JVM參數-XX:PermSize及-XX:MaxPermSize將會被忽略掉,並且在啓動的時候給出警告信息。
  • 充分利用了Java語言規範中的好處:類及相關的元數據的生命週期與類加載器的一致

元空間 —— 內存分配模型絕大多數的類元數據的空間都從本地內存中分配。用來描述類元數據的類也被刪除了,分元數據分配了多個虛擬內存空間給每個類加載器分配一個內存塊的列表,只進行線性分配。塊的大小取決於類加載器的類型, sun/反射/代理對應的類加載器的塊會小一些。不會單獨回收某個類,如果GC發現某個類加載器不再存活了,會把相關的空間整個回收掉。這樣減少了碎片,並節省GC掃描和壓縮的時間。

元空間 —— 調優使用-XX:MaxMetaspaceSize參數可以設置元空間的最大值,默認是沒有上限的,也就是說你的系統內存上限是多少它就是多少。使用-XX:MetaspaceSize選項指定的是元空間的初始大小,如果沒有指定的話,元空間會根據應用程序運行時的需要動態地調整大小。 一旦類元數據的使用量達到了“MaxMetaspaceSize”指定的值,對於無用的類和類加載器,垃圾收集此時會觸發。爲了控制這種垃圾收集的頻率和延遲,合適的監控和調整Metaspace非常有必要。過於頻繁的Metaspace垃圾收集是類和類加載器發生內存泄露的徵兆,同時也說明你的應用程序內存大小不合適,需要調整。

** 快速過一遍JVM的內存結構,JVM中的內存分爲5個虛擬的區域:(程序計數器、

虛擬機棧、本地方法棧、堆區、方法區)

Java8的JVM持久代 - 何去何從?

  • 你的Java程序中所分配的每一個對象都需要存儲在內存裏。堆是這些實例化的對象所存儲的地方。是的——都怪new操作符,是它把你的Java堆都佔滿了的!
  • 它由所有線程共享
  • 當堆耗盡的時候,JVM會拋出java.lang.OutOfMemoryError 異常
  • 堆的大小可以通過JVM選項-Xms和-Xmx來進行調整

堆被分爲:

  • Eden區 —— 新對象或者生命週期很短的對象會存儲在這個區域中,這個區的大小可以通過-XX:NewSize和-XX:MaxNewSize參數來調整。新生代GC(垃圾回收器)會清理這一區域。
  • Survivor區 —— 那些歷經了Eden區的垃圾回收仍能存活下來的依舊存在引用的對象會待在這個區域。這個區的大小可以由JVM參數-XX:SurvivorRatio來進行調節。
  • 老年代 —— 那些在歷經了Eden區和Survivor區的多次GC後仍然存活下來的對象(當然了,是拜那些揮之不去的引用所賜)會存儲在這個區裏。這個區會由一個特殊的垃圾回收器來負責。年老代中的對象的回收是由老年代的GC(major GC)來進行的。

方法區

  • 也被稱爲非堆區域(在HotSpot JVM的實現當中)
  • 它被分爲兩個主要的子區域

持久代 —— 這個區域會 存儲包括類定義,結構,字段,方法(數據及代碼)以及常量在內的類相關數據。它可以通過-XX:PermSize及 -XX:MaxPermSize來進行調節。如果它的空間用完了,會導致java.lang.OutOfMemoryError: PermGen space的異常。

代碼緩存——這個緩存區域是用來存儲編譯後的代碼。編譯後的代碼就是本地代碼(硬件相關的),它是由JIT(Just In Time)編譯器生成的,這個編譯器是Oracle HotSpot JVM所特有的。

JVM棧

  • 和Java類中的方法密切相關
  • 它會存儲局部變量以及方法調用的中間結果及返回值
  • Java中的每個線程都有自己專屬的棧,這個棧是別的線程無法訪問的。
  • 可以通過JVM選項-Xss來進行調整

本地棧

  • 用於本地方法(非Java代碼)
  • 按線程分配

PC寄存器

  • 特定線程的程序計數器
  • 包含JVM正在執行的指令的地址(如果是本地方法的話它的值則未定義)

好吧,這就是JVM內存分區的基礎知識了。現在再說說持久代這個話題吧。

對Java內存模型的理解以及其在併發當中的作用

概述

Java平臺自動集成了線程以及多處理器技術,這種集成程度比Java以前誕生的計算機語言要厲害很多,該語言針對多種異構平臺的平臺獨立性而使用的多線程技術支持也是具有開拓性的一面,有時候在開發Java同步和線程安全要求很嚴格的程序時,往往容易混淆的一個概念就是內存模型。究竟什麼是內存模型?內存模型描述了程序中各個變量(實例域、靜態域和數組元素)之間的關係,以及在實際計算機系統中將變量存儲到內存和從內存中取出變量這樣的底層細節,對象最終是存儲在內存裏面的,這點沒有錯,但是編譯器、運行庫、處理器或者系統緩存可以有特權在變量指定內存位置存儲或者取出變量的值。【JMM】(Java Memory Model的縮寫)允許編譯器和緩存以數據在處理器特定的緩存(或寄存器)和主存之間移動的次序擁有重要的特權,除非程序員使用了final或synchronized明確請求了某些可見性的保證。在Java中應爲不同的目的可以將java劃分爲兩種內存模型:gc內存模型。併發內存模型。

gc內存模型

java與c++之間有一堵由內存動態分配與垃圾收集技術所圍成的“高牆”。牆外面的人想進去,牆裏面的人想出來。java在執行java程序的過程中會把它管理的內存劃分若干個不同功能的數據管理區域。如圖:

img

img

img

hotspot中的gc內存模型

整體上。分爲三部分:棧,堆,程序計數器,他們每一部分有其各自的用途;虛擬機棧保存着每一條線程的執行程序調用堆棧;堆保存着類對象、數組的具體信息;程序計數器保存着每一條線程下一次執行指令位置。這三塊區域中棧和程序計數器是線程私有的。也就是說每一個線程擁有其獨立的棧和程序計數器。我們可以看看具體結構:

虛擬機/本地方法棧

在棧中,會爲每一個線程創建一個棧。線程越多,棧的內存使用越大。對於每一個線程棧。當一個方法在線程中執行的時候,會在線程棧中創建一個棧幀(stack frame),用於存放該方法的上下文(局部變量表、操作數棧、方法返回地址等等)。每一個方法從調用到執行完畢的過程,就是對應着一個棧幀入棧出棧的過程。

本地方法棧與虛擬機棧發揮的作用是類似的,他們之間的區別不過是虛擬機棧爲虛擬機執行java(字節碼)服務的,而本地方法棧是爲虛擬機執行native方法服務的。

方法區/堆

在hotspot的實現中,方法區就是在堆中稱爲永久代的堆區域。幾乎所有的對象/數組的內存空間都在堆上(有少部分在棧上)。在gc管理中,將虛擬機堆分爲永久代、老年代、新生代。通過名字我們可以知道一個對象新建一般在新生代。經過幾輪的gc。還存活的對象會被移到老年代。永久代用來保存類信息、代碼段等幾乎不會變的數據。堆中的所有數據是線程共享的。

  • 新生代:應爲gc具體實現的優化的原因。hotspot又將新生代劃分爲一個eden區和兩個survivor區。每一次新生代gc時候。只用到一個eden區,一個survivor區。新生代一般的gc策略爲mark-copy。
  • 老年代:當新生代中的對象經過若干輪gc後還存活/或survisor在gc內存不夠的時候。會把當前對象移動到老年代。老年代一般gc策略爲mark-compact。
  • 永久代:永久代一般可以不參與gc。應爲其中保存的是一些代碼/常量數據/類信息。在永久代gc。清楚的是類信息以及常量池。

JVM內存模型中分兩大塊,一塊是 NEW Generation, 另一塊是Old Generation. 在New Generation中,有一個叫Eden的空間,主要是用來存放新生的對象,還有兩個Survivor Spaces(from,to), 它們用來存放每次垃圾回收後存活下來的對象。在Old Generation中,主要存放應用程序中生命週期長的內存對象,還有個Permanent Generation,主要用來放JVM自己的反射對象,比如類對象和方法對象等。

程序計數器

如同其名稱一樣。程序計數器用於記錄某個線程下次執行指令位置。程序計數器也是線程私有的。

併發內存模型

java試圖定義一個Java內存模型(Java memory model jmm)來屏蔽掉各種硬件/操作系統的內存訪問差異,以實現讓java程序在各個平臺下都能達到一致的內存訪問效果。java內存模型主要目標是定義程序中各個變量的訪問規則,即在虛擬機中將變量存儲到內存和從內存中取出變量這樣的底層細節。模型圖如下:

img

java併發內存模型以及內存操作規則

java內存模型中規定了所有變量都存貯到主內存(如虛擬機物理內存中的一部分)中。每一個線程都有一個自己的工作內存(如cpu中的高速緩存)。線程中的工作內存保存了該線程使用到的變量的主內存的副本拷貝。線程對變量的所有操作(讀取、賦值等)必須在該線程的工作內存中進行。不同線程之間無法直接訪問對方工作內存中變量。線程間變量的值傳遞均需要通過主內存來完成。

關於主內存與工作內存之間的交互協議,即一個變量如何從主內存拷貝到工作內存。如何從工作內存同步到主內存中的實現細節。java內存模型定義了8種操作來完成。這8種操作每一種都是原子操作。8種操作如下:

  • lock(鎖定):作用於主內存,它把一個變量標記爲一條線程獨佔狀態;
  • unlock(解鎖):作用於主內存,它將一個處於鎖定狀態的變量釋放出來,釋放後的變量才能夠被其他線程鎖定;
  • read(讀取):作用於主內存,它把變量值從主內存傳送到線程的工作內存中,以便隨後的load動作使用;
  • load(載入):作用於工作內存,它把read操作的值放入工作內存中的變量副本中;
  • use(使用):作用於工作內存,它把工作內存中的值傳遞給執行引擎,每當虛擬機遇到一個需要使用這個變量的指令時候,將會執行這個動作;
  • assign(賦值):作用於工作內存,它把從執行引擎獲取的值賦值給工作內存中的變量,每當虛擬機遇到一個給變量賦值的指令時候,執行該操作;
  • store(存儲):作用於工作內存,它把工作內存中的一個變量傳送給主內存中,以備隨後的write操作使用;
  • write(寫入):作用於主內存,它把store傳送值放到主內存中的變量中。

Java內存模型還規定了執行上述8種基本操作時必須滿足如下規則:

  • 不允許read和load、store和write操作之一單獨出現,以上兩個操作必須按順序執行,但沒有保證必須連續執行,也就是說,read與load之間、store與write之間是可插入其他指令的。
  • 不允許一個線程丟棄它的最近的assign操作,即變量在工作內存中改變了之後必須把該變化同步回主內存。
  • 不允許一個線程無原因地(沒有發生過任何assign操作)把數據從線程的工作內存同步回主內存中。
  • 一個新的變量只能從主內存中“誕生”,不允許在工作內存中直接使用一個未被初始化(load或assign)的變量,換句話說就是對一個變量實施use和store操作之前,必須先執行過了assign和load操作。
  • 一個變量在同一個時刻只允許一條線程對其執行lock操作,但lock操作可以被同一個條線程重複執行多次,多次執行lock後,只有執行相同次數的unlock操作,變量纔會被解鎖。
  • 如果對一個變量執行lock操作,將會清空工作內存中此變量的值,在執行引擎使用這個變量前,需要重新執行load或assign操作初始化變量的值。
  • 如果一個變量實現沒有被lock操作鎖定,則不允許對它執行unlock操作,也不允許去unlock一個被其他線程鎖定的變量。
  • 對一個變量執行unlock操作之前,必須先把此變量同步回主內存(執行store和write操作)。

volatile型變量的特殊規則

關鍵字volatile可以說是Java虛擬機提供的最輕量級的同步機制,但是它並不容易完全被正確、完整的理解,以至於許多程序員都不習慣去使用它,遇到需要處理多線程的問題的時候一律使用synchronized來進行同步。瞭解volatile變量的語義對後面瞭解多線程操作的其他特性很有意義。Java內存模型對volatile專門定義了一些特殊的訪問規則,當一個變量被定義成volatile之後,他將具備兩種特性:

  • 保證此變量對所有線程的可見性。第一保證此變量對所有線程的可見性,這裏的“可見性”是指當一條線程修改了這個變量的值,新值對於其他線程來說是可以立即得知的。而普通變量是做不到這點,普通變量的值在線程在線程間傳遞均需要通過住內存來完成,例如,線程A修改一個普通變量的值,然後向主內存進行會寫,另外一個線程B在線程A回寫完成了之後再從主內存進行讀取操作,新變量值纔會對線程B可見。另外,java裏面的運算並非原子操作,會導致volatile變量的運算在併發下一樣是不安全的。
  • 禁止指令重排序優化。普通的變量僅僅會保證在該方法的執行過程中所有依賴賦值結果的地方都能獲得正確的結果,而不能保證變量賦值操作的順序與程序中的執行順序一致,在單線程中,我們是無法感知這一點的。

由於volatile變量只能保證可見性,在不符合以下兩條規則的運算場景中,我們仍然要通過加鎖來保證原子性。

  • 1.運算結果並不依賴變量的當前值,或者能夠確保只有單一的線程修改變量的值。
  • 2.變量不需要與其他的狀態比阿尼浪共同參與不變約束。

原子性、可見性與有序性

Java內存模型是圍繞着在併發過程中如何處理原子性、可見性和有序性這三個特徵來建立的,我們逐個看下哪些操作實現了這三個特性。

  • 原子性(Atomicity):由Java內存模型來直接保證的原子性變量包括read、load、assign、use、store和write,我們大致可以認爲基本數據類型的訪問讀寫是具備原子性的。如果應用場景需要一個更大方位的原子性保證,Java內存模型還提供了lock和unlock操作來滿足這種需求,儘管虛擬機未把lock和unlock操作直接開放給用戶使用,但是卻提供了更高層次的字節碼指令monitorenter和monitorexit來隱式的使用這兩個操作,這兩個字節碼指令反應到Java代碼中就是同步塊--synchronized關鍵字,因此在synchronized塊之間的操作也具備原子性。
  • 可見性(Visibility):可見性是指當一個線程修改了共享變量的值,其他線程能夠立即得知這個修改。上文在講解volatile變量的時候我們已詳細討論過這一點。Java內存模型是通過在變量修改後將新值同步回主內存,在變量讀取前從主內存刷新變量值這種依賴主內存作爲傳遞媒介的方式來實現可見性的,無論是普通變量還是volatile變量都是如此,普通變量與volatile變量的區別是,volatile的特殊規則保證了新值能立即同步到主內存,以及每次使用前立即從主內存刷新。因此,可以說volatile保證了多線程操作時變量的可見性,而普通變量則不能保證這一點。除了volatile之外,Java還有兩個關鍵字能實現可見性,即synchronized和final.同步快的可見性是由“對一個變量執行unlock操作前,必須先把此變量同步回主內存”這條規則獲得的,而final關鍵字的可見性是指:被final修飾的字段在構造器中一旦初始化完成,並且構造器沒有把"this"的引用傳遞出去,那麼在其他線程中就能看見final字段的值。
  • 有序性(Ordering):Java內存模型的有序性在前面講解volatile時也詳細的討論過了,Java程序中天然的有序性可以總結爲一句話:如果在本線程內觀察,所有的操作都是有序的:如果在一個線程中觀察另外一個線程,所有的線程操作都是無序的。前半句是指“線程內表現爲串行的語義”,後半句是指“指令重排序”現象和“工作內存與主內存同步延遲”現象。Java語言提供了volatile和synchronized兩個關鍵字來保證線程之間操作的有序性,volatile關鍵字本身就包含了禁止指令重排序的語義,而synchronized則是由“一個變量在同一個時刻只允許一條線程對其進行lock操作”這條規則獲得的,這條規則決定了持有同一個鎖的兩個同步塊只能串行的進入。

Arrays和Collections 對於sort的不同實現原理

1、Arrays.sort()
該算法是一個經過調優的快速排序,此算法在很多數據集上提供N*log(N)的性能,這導致其他快速排序會降低二次型性能。

2、Collections.sort()
該算法是一個經過修改的合併排序算法(其中,如果低子列表中的最高元素效益高子列表中的最低元素,則忽略合併)。此算法可提供保證的N*log(N)的性能,此實現將指定列表轉儲到一個數組中,然後再對數組進行排序,在重置數組中相應位置處每個元素的列表上進行迭代。這避免了由於試圖原地對鏈接列表進行排序而產生的n2log(n)性能。

Java中object常用方法

1、clone()
2、equals()
3、finalize()
4、getclass()
5、hashcode()
6、notify()
7、notifyAll()
8、toString()

對於Java中多態的理解

所謂多態就是指程序中定義的引用變量所指向的具體類型和通過該引用變量發出的方法調用在編程時並不確定,而是在程序運行期間才確定,即一個引用變量到底會指向哪個類的實例對象,該引用變量發出的方法調用到底是哪個類中實現的方法,必須在由程序運行期間才能決定。因爲在程序運行時才確定具體的類,這樣,不用修改源程序代碼,就可以讓引用變量綁定到各種不同的類實現上,從而導致該引用調用的具體方法隨之改變,即不修改程序代碼就可以改變程序運行時所綁定的具體代碼,讓程序可以選擇多個運行狀態,這就是多態性。

多態的定義:指允許不同類的對象對同一消息做出響應。即同一消息可以根據發送對象的不同而採用多種不同的行爲方式。(發送消息就是函數調用)

Java實現多態有三個必要條件:繼承、重寫、父類引用指向子類對象。

繼承:在多態中必須存在有繼承關係的子類和父類。

重寫:子類對父類中某些方法進行重新定義,在調用這些方法時就會調用子類的方法。

父類引用指向子類對象:在多態中需要將子類的引用賦給父類對象,只有這樣該引用才能夠具備技能調用父類的方法和子類的方法。

實現多態的技術稱爲:動態綁定(dynamic binding),是指在執行期間判斷所引用對象的實際類型,根據其實際的類型調用其相應的方法。

多態的作用:消除類型之間的耦合關係。

Java序列化與反序列化是什麼?爲什麼需要序列化與反序列化?如何實現Java序列化與反序列化

spring AOP 實現原理

什麼是AOP

AOP(Aspect-OrientedProgramming,面向方面編程),可以說是OOP(Object-Oriented Programing,面向對象編程)的補充和完善。OOP引入封裝、繼承和多態性等概念來建立一種對象層次結構,用以模擬公共行爲的一個集合。當我們需要爲分散的對象引入公共行爲的時候,OOP則顯得無能爲力。也就是說,OOP允許你定義從上到下的關係,但並不適合定義從左到右的關係。例如日誌功能。日誌代碼往往水平地散佈在所有對象層次中,而與它所散佈到的對象的核心功能毫無關係。對於其他類型的代碼,如安全性、異常處理和透明的持續性也是如此。這種散佈在各處的無關的代碼被稱爲橫切(cross-cutting)代碼,在OOP設計中,它導致了大量代碼的重複,而不利於各個模塊的重用。

而AOP技術則恰恰相反,它利用一種稱爲“橫切”的技術,剖解開封裝的對象內部,並將那些影響了多個類的公共行爲封裝到一個可重用模塊,並將其名爲“Aspect”,即方面。所謂“方面”,簡單地說,就是將那些與業務無關,卻爲業務模塊所共同調用的邏輯或責任封裝起來,便於減少系統的重複代碼,降低模塊間的耦合度,並有利於未來的可操作性和可維護性。AOP代表的是一個橫向的關係,如果說“對象”是一個空心的圓柱體,其中封裝的是對象的屬性和行爲;那麼面向方面編程的方法,就彷彿一把利刃,將這些空心圓柱體剖開,以獲得其內部的消息。而剖開的切面,也就是所謂的“方面”了。然後它又以巧奪天功的妙手將這些剖開的切面復原,不留痕跡。

使用“橫切”技術,AOP把軟件系統分爲兩個部分:核心關注點和橫切關注點。業務處理的主要流程是核心關注點,與之關係不大的部分是橫切關注點。橫切關注點的一個特點是,他們經常發生在覈心關注點的多處,而各處都基本相似。比如權限認證、日誌、事務處理。Aop 的作用在於分離系統中的各種關注點,將核心關注點和橫切關注點分離開來。正如Avanade公司的高級方案構架師Adam Magee所說,AOP的核心思想就是“將應用程序中的商業邏輯同對其提供支持的通用服務進行分離。”

實現AOP的技術,主要分爲兩大類:一是採用動態代理技術,利用截取消息的方式,對該消息進行裝飾,以取代原有對象行爲的執行;二是採用靜態織入的方式,引入特定的語法創建“方面”,從而使得編譯器可以在編譯期間織入有關“方面”的代碼。

AOP使用場景

AOP用來封裝橫切關注點,具體可以在下面的場景中使用:

Authentication 權限

Caching 緩存

Context passing 內容傳遞

Error handling 錯誤處理

Lazy loading 懶加載

Debugging  調試

logging, tracing, profiling and monitoring 記錄跟蹤 優化 校準

Performance optimization 性能優化

Persistence  持久化

Resource pooling 資源池

Synchronization 同步

Transactions 事務

AOP相關概念

方面(Aspect):一個關注點的模塊化,這個關注點實現可能另外橫切多個對象。事務管理是J2EE應用中一個很好的橫切關注點例子。方面用spring的 Advisor或攔截器實現。

連接點(Joinpoint): 程序執行過程中明確的點,如方法的調用或特定的異常被拋出。

通知(Advice): 在特定的連接點,AOP框架執行的動作。各種類型的通知包括“around”、“before”和“throws”通知。通知類型將在下面討論。許多AOP框架包括Spring都是以攔截器做通知模型,維護一個“圍繞”連接點的攔截器鏈。Spring中定義了四個advice: BeforeAdvice, AfterAdvice, ThrowAdvice和DynamicIntroductionAdvice

切入點(Pointcut): 指定一個通知將被引發的一系列連接點的集合。AOP框架必須允許開發者指定切入點:例如,使用正則表達式。 Spring定義了Pointcut接口,用來組合MethodMatcher和ClassFilter,可以通過名字很清楚的理解, MethodMatcher是用來檢查目標類的方法是否可以被應用此通知,而ClassFilter是用來檢查Pointcut是否應該應用到目標類上

引入(Introduction): 添加方法或字段到被通知的類。 Spring允許引入新的接口到任何被通知的對象。例如,你可以使用一個引入使任何對象實現 IsModified接口,來簡化緩存。Spring中要使用Introduction, 可有通過DelegatingIntroductionInterceptor來實現通知,通過DefaultIntroductionAdvisor來配置Advice和代理類要實現的接口

目標對象(Target Object): 包含連接點的對象。也被稱作被通知或被代理對象。POJO

AOP代理(AOP Proxy): AOP框架創建的對象,包含通知。 在Spring中,AOP代理可以是JDK動態代理或者CGLIB代理。

織入(Weaving): 組裝方面來創建一個被通知對象。這可以在編譯時完成(例如使用AspectJ編譯器),也可以在運行時完成。Spring和其他純Java AOP框架一樣,在運行時完成織入。

Spring AOP組件

下面這種類圖列出了Spring中主要的AOP組件

img

如何使用Spring AOP

可以通過配置文件或者編程的方式來使用Spring AOP。

配置可以通過xml文件來進行,大概有四種方式:

\1. 配置ProxyFactoryBean,顯式地設置advisors, advice, target等

  1.    配置AutoProxyCreator,這種方式下,還是如以前一樣使用定義的bean,但是從容器中獲得的其實已經是代理對象
    
  2.    通過<aop:config>來配置
    
  3.    通過<aop: aspectj-autoproxy>來配置,使用AspectJ的註解來標識通知及切入點
    

也可以直接使用ProxyFactory來以編程的方式使用Spring AOP,通過ProxyFactory提供的方法可以設置target對象, advisor等相關配置,最終通過 getProxy()方法來獲取代理對象

具體使用的示例可以google. 這裏略去

Spring AOP代理對象的生成

Spring提供了兩種方式來生成代理對象: JDKProxy和Cglib,具體使用哪種方式生成由AopProxyFactory根據AdvisedSupport對象的配置來決定。默認的策略是如果目標類是接口,則使用JDK動態代理技術,否則使用Cglib來生成代理。下面我們來研究一下Spring如何使用JDK來生成代理對象,具體的生成代碼放在JdkDynamicAopProxy這個類中,直接上相關代碼:

友情鏈接 :Spring AOP 實現原理

/**
   * <ol>
   * <li>獲取代理類要實現的接口,除了Advised對象中配置的,還會加上SpringProxy, Advised(opaque=false)
   * <li>檢查上面得到的接口中有沒有定義 equals或者hashcode的接口
   * <li>調用Proxy.newProxyInstance創建代理對象
   * </ol>
   */
  public Object getProxy(ClassLoader classLoader) {
      if (logger.isDebugEnabled()) {
          logger.debug("Creating JDK dynamic proxy: target source is " +this.advised.getTargetSource());
      }
      Class[] proxiedInterfaces =AopProxyUtils.completeProxiedInterfaces(this.advised);
      findDefinedEqualsAndHashCodeMethods(proxiedInterfaces);
      return Proxy.newProxyInstance(classLoader, proxiedInterfaces, this);
}

那這個其實很明瞭,註釋上我也已經寫清楚了,不再贅述。

下面的問題是,代理對象生成了,那切面是如何織入的?

我們知道InvocationHandler是JDK動態代理的核心,生成的代理對象的方法調用都會委託到InvocationHandler.invoke()方法。而通過JdkDynamicAopProxy的簽名我們可以看到這個類其實也實現了InvocationHandler,下面我們就通過分析這個類中實現的invoke()方法來具體看下Spring AOP是如何織入切面的。


Servlet 工作原理

Servlet 工作原理解析

從 Servlet 容器說起

前面說了 Servlet 容器作爲一個獨立發展的標準化產品,目前它的種類很多,但是它們都有自己的市場定位,很難說誰優誰劣,各有特點。例如現在比較流行的 Jetty,在定製化和移動領域有不錯的發展,我們這裏還是以大家最爲熟悉 Tomcat 爲例來介紹 Servlet 容器如何管理 Servlet。Tomcat 本身也很複雜,我們只從 Servlet 與 Servlet 容器的接口部分開始介紹,關於 Tomcat 的詳細介紹可以參考我的另外一篇文章《 Tomcat 系統架構與模式設計分析》。

Tomcat 的容器等級中,Context 容器是直接管理 Servlet 在容器中的包裝類 Wrapper,所以 Context 容器如何運行將直接影響 Servlet 的工作方式。

圖 1 . Tomcat 容器模型

從上圖可以看出 Tomcat 的容器分爲四個等級,真正管理 Servlet 的容器是 Context 容器,一個 Context 對應一個 Web 工程,在 Tomcat 的配置文件中可以很容易發現這一點,如下:

清單 1 Context 配置參數

<Context path="/projectOne " docBase="D:\projects\projectOne" 
reloadable="true" />

下面詳細介紹一下 Tomcat 解析 Context 容器的過程,包括如何構建 Servlet 的過程。

Servlet 容器的啓動過程

Tomcat7 也開始支持嵌入式功能,增加了一個啓動類 org.apache.catalina.startup.Tomcat。創建一個實例對象並調用 start 方法就可以很容易啓動 Tomcat,我們還可以通過這個對象來增加和修改 Tomcat 的配置參數,如可以動態增加 Context、Servlet 等。下面我們就利用這個 Tomcat 類來管理新增的一個 Context 容器,我們就選擇 Tomcat7 自帶的 examples Web 工程,並看看它是如何加到這個 Context 容器中的。

清單 2 . 給 Tomcat 增加一個 Web 工程

Tomcat tomcat = getTomcatInstance(); 
File appDir = new File(getBuildDirectory(), "webapps/examples"); 
tomcat.addWebapp(null, "/examples", appDir.getAbsolutePath()); 
tomcat.start(); 
ByteChunk res = getUrl("http://localhost:" + getPort() + 
              "/examples/servlets/servlet/HelloWorldExample"); 
assertTrue(res.toString().indexOf("<h1>Hello World!</h1>") > 0);

清單 1 的代碼是創建一個 Tomcat 實例並新增一個 Web 應用,然後啓動 Tomcat 並調用其中的一個 HelloWorldExample Servlet,看有沒有正確返回預期的數據。

Tomcat 的 addWebapp 方法的代碼如下:

清單 3 .Tomcat.addWebapp

public Context addWebapp(Host host, String url, String path) { 
       silence(url); 
       Context ctx = new StandardContext(); 
       ctx.setPath( url ); 
       ctx.setDocBase(path); 
       if (defaultRealm == null) { 
           initSimpleAuth(); 
       } 
       ctx.setRealm(defaultRealm); 
       ctx.addLifecycleListener(new DefaultWebXmlListener()); 
       ContextConfig ctxCfg = new ContextConfig(); 
       ctx.addLifecycleListener(ctxCfg); 
       ctxCfg.setDefaultWebXml("org/apache/catalin/startup/NO_DEFAULT_XML"); 
       if (host == null) { 
           getHost().addChild(ctx); 
       } else { 
           host.addChild(ctx); 
       } 
       return ctx; 
}

前面已經介紹了一個 Web 應用對應一個 Context 容器,也就是 Servlet 運行時的 Servlet 容器,添加一個 Web 應用時將會創建一個 StandardContext 容器,並且給這個 Context 容器設置必要的參數,url 和 path 分別代表這個應用在 Tomcat 中的訪問路徑和這個應用實際的物理路徑,這個兩個參數與清單 1 中的兩個參數是一致的。其中最重要的一個配置是 ContextConfig,這個類將會負責整個 Web 應用配置的解析工作,後面將會詳細介紹。最後將這個 Context 容器加到父容器 Host 中。

接下去將會調用 Tomcat 的 start 方法啓動 Tomcat,如果你清楚 Tomcat 的系統架構,你會容易理解 Tomcat 的啓動邏輯,Tomcat 的啓動邏輯是基於觀察者模式設計的,所有的容器都會繼承 Lifecycle 接口,它管理者容器的整個生命週期,所有容器的的修改和狀態的改變都會由它去通知已經註冊的觀察者(Listener),關於這個設計模式可以參考《 Tomcat 的系統架構與設計模式,第二部分:設計模式》。Tomcat 啓動的時序圖可以用圖 2 表示。

圖 2. Tomcat 主要類的啓動時序圖(查看大圖

上圖描述了 Tomcat 啓動過程中,主要類之間的時序關係,下面我們將會重點關注添加 examples 應用所對應的 StandardContext 容器的啓動過程。

當 Context 容器初始化狀態設爲 init 時,添加在 Contex 容器的 Listener 將會被調用。ContextConfig 繼承了 LifecycleListener 接口,它是在調用清單 3 時被加入到 StandardContext 容器中。ContextConfig 類會負責整個 Web 應用的配置文件的解析工作。

ContextConfig 的 init 方法將會主要完成以下工作:

  1. 創建用於解析 xml 配置文件的 contextDigester 對象
  2. 讀取默認 context.xml 配置文件,如果存在解析它
  3. 讀取默認 Host 配置文件,如果存在解析它
  4. 讀取默認 Context 自身的配置文件,如果存在解析它
  5. 設置 Context 的 DocBase

ContextConfig 的 init 方法完成後,Context 容器的會執行 startInternal 方法,這個方法啓動邏輯比較複雜,主要包括如下幾個部分:

  1. 創建讀取資源文件的對象
  2. 創建 ClassLoader 對象
  3. 設置應用的工作目錄
  4. 啓動相關的輔助類如:logger、realm、resources 等
  5. 修改啓動狀態,通知感興趣的觀察者(Web 應用的配置)
  6. 子容器的初始化
  7. 獲取 ServletContext 並設置必要的參數
  8. 初始化“load on startup”的 Servlet

Web 應用的初始化工作

Web 應用的初始化工作是在 ContextConfig 的 configureStart 方法中實現的,應用的初始化主要是要解析 web.xml 文件,這個文件描述了一個 Web 應用的關鍵信息,也是一個 Web 應用的入口。

Tomcat 首先會找 globalWebXml 這個文件的搜索路徑是在 engine 的工作目錄下尋找以下兩個文件中的任一個 org/apache/catalin/startup/NO_DEFAULT_XML 或 conf/web.xml。接着會找 hostWebXml 這個文件可能會在 System.getProperty("catalina.base")/conf/${EngineName}/${HostName}/web.xml.default,接着尋找應用的配置文件 examples/WEB-INF/web.xml。web.xml 文件中的各個配置項將會被解析成相應的屬性保存在 WebXml 對象中。如果當前應用支持 Servlet3.0,解析還將完成額外 9 項工作,這個額外的 9 項工作主要是爲 Servlet3.0 新增的特性,包括 jar 包中的 META-INF/web-fragment.xml 的解析以及對 annotations 的支持。

接下去將會將 WebXml 對象中的屬性設置到 Context 容器中,這裏包括創建 Servlet 對象、filter、listener 等等。這段代碼在 WebXml 的 configureContext 方法中。下面是解析 Servlet 的代碼片段:

清單 4. 創建 Wrapper 實例

for (ServletDef servlet : servlets.values()) { 
           Wrapper wrapper = context.createWrapper(); 
           String jspFile = servlet.getJspFile(); 
           if (jspFile != null) { 
               wrapper.setJspFile(jspFile); 
           } 
           if (servlet.getLoadOnStartup() != null) { 
               wrapper.setLoadOnStartup(servlet.getLoadOnStartup().intValue()); 
           } 
           if (servlet.getEnabled() != null) { 
               wrapper.setEnabled(servlet.getEnabled().booleanValue()); 
           } 
           wrapper.setName(servlet.getServletName()); 
           Map<String,String> params = servlet.getParameterMap(); 
           for (Entry<String, String> entry : params.entrySet()) { 
               wrapper.addInitParameter(entry.getKey(), entry.getValue()); 
           } 
           wrapper.setRunAs(servlet.getRunAs()); 
           Set<SecurityRoleRef> roleRefs = servlet.getSecurityRoleRefs(); 
           for (SecurityRoleRef roleRef : roleRefs) { 
               wrapper.addSecurityReference( 
                       roleRef.getName(), roleRef.getLink()); 
           } 
           wrapper.setServletClass(servlet.getServletClass()); 
           MultipartDef multipartdef = servlet.getMultipartDef(); 
           if (multipartdef != null) { 
               if (multipartdef.getMaxFileSize() != null && 
                       multipartdef.getMaxRequestSize()!= null && 
                       multipartdef.getFileSizeThreshold() != null) { 
                   wrapper.setMultipartConfigElement(new 
MultipartConfigElement( 
                           multipartdef.getLocation(), 
                           Long.parseLong(multipartdef.getMaxFileSize()), 
                           Long.parseLong(multipartdef.getMaxRequestSize()), 
                           Integer.parseInt( 
                                   multipartdef.getFileSizeThreshold()))); 
               } else { 
                   wrapper.setMultipartConfigElement(new 
MultipartConfigElement( 
                           multipartdef.getLocation())); 
               } 
           } 
           if (servlet.getAsyncSupported() != null) { 
               wrapper.setAsyncSupported( 
                       servlet.getAsyncSupported().booleanValue()); 
           } 
           context.addChild(wrapper); 
}

這段代碼清楚的描述瞭如何將 Servlet 包裝成 Context 容器中的 StandardWrapper,這裏有個疑問,爲什麼要將 Servlet 包裝成 StandardWrapper 而不直接是 Servlet 對象。這裏 StandardWrapper 是 Tomcat 容器中的一部分,它具有容器的特徵,而 Servlet 爲了一個獨立的 web 開發標準,不應該強耦合在 Tomcat 中。

除了將 Servlet 包裝成 StandardWrapper 並作爲子容器添加到 Context 中,其它的所有 web.xml 屬性都被解析到 Context 中,所以說 Context 容器纔是真正運行 Servlet 的 Servlet 容器。一個 Web 應用對應一個 Context 容器,容器的配置屬性由應用的 web.xml 指定,這樣我們就能理解 web.xml 到底起到什麼作用了。

回頁首

創建 Servlet 實例

前面已經完成了 Servlet 的解析工作,並且被包裝成 StandardWrapper 添加在 Context 容器中,但是它仍然不能爲我們工作,它還沒有被實例化。下面我們將介紹 Servlet 對象是如何創建的,以及如何被初始化的。

創建 Servlet 對象

如果 Servlet 的 load-on-startup 配置項大於 0,那麼在 Context 容器啓動的時候就會被實例化,前面提到在解析配置文件時會讀取默認的 globalWebXml,在 conf 下的 web.xml 文件中定義了一些默認的配置項,其定義了兩個 Servlet,分別是:org.apache.catalina.servlets.DefaultServlet 和 org.apache.jasper.servlet.JspServlet 它們的 load-on-startup 分別是 1 和 3,也就是當 Tomcat 啓動時這兩個 Servlet 就會被啓動。

創建 Servlet 實例的方法是從 Wrapper. loadServlet 開始的。loadServlet 方法要完成的就是獲取 servletClass 然後把它交給 InstanceManager 去創建一個基於 servletClass.class 的對象。如果這個 Servlet 配置了 jsp-file,那麼這個 servletClass 就是 conf/web.xml 中定義的 org.apache.jasper.servlet.JspServlet 了。

創建 Servlet 對象的相關類結構圖如下:

圖 3. 創建 Servlet 對象的相關類結構

初始化 Servlet

初始化 Servlet 在 StandardWrapper 的 initServlet 方法中,這個方法很簡單就是調用 Servlet 的 init 的方法,同時把包裝了 StandardWrapper 對象的 StandardWrapperFacade 作爲 ServletConfig 傳給 Servlet。Tomcat 容器爲何要傳 StandardWrapperFacade 給 Servlet 對象將在後面做詳細解析。

如果該 Servlet 關聯的是一個 jsp 文件,那麼前面初始化的就是 JspServlet,接下去會模擬一次簡單請求,請求調用這個 jsp 文件,以便編譯這個 jsp 文件爲 class,並初始化這個 class。

這樣 Servlet 對象就初始化完成了,事實上 Servlet 從被 web.xml 中解析到完成初始化,這個過程非常複雜,中間有很多過程,包括各種容器狀態的轉化引起的監聽事件的觸發、各種訪問權限的控制和一些不可預料的錯誤發生的判斷行爲等等。我們這裏只抓了一些關鍵環節進行闡述,試圖讓大家有個總體脈絡。

下面是這個過程的一個完整的時序圖,其中也省略了一些細節。

圖 4. 初始化 Servlet 的時序圖(查看大圖

回頁首

Servlet 體系結構

我們知道 Java Web 應用是基於 Servlet 規範運轉的,那麼 Servlet 本身又是如何運轉的呢?爲何要設計這樣的體系結構。

圖 5.Servlet 頂層類關聯圖

從上圖可以看出 Servlet 規範就是基於這幾個類運轉的,與 Servlet 主動關聯的是三個類,分別是 ServletConfig、ServletRequest 和 ServletResponse。這三個類都是通過容器傳遞給 Servlet 的,其中 ServletConfig 是在 Servlet 初始化時就傳給 Servlet 了,而後兩個是在請求達到時調用 Servlet 時傳遞過來的。我們很清楚 ServletRequest 和 ServletResponse 在 Servlet 運行的意義,但是 ServletConfig 和 ServletContext 對 Servlet 有何價值?仔細查看 ServletConfig 接口中聲明的方法發現,這些方法都是爲了獲取這個 Servlet 的一些配置屬性,而這些配置屬性可能在 Servlet 運行時被用到。而 ServletContext 又是幹什麼的呢? Servlet 的運行模式是一個典型的“握手型的交互式”運行模式。所謂“握手型的交互式”就是兩個模塊爲了交換數據通常都會準備一個交易場景,這個場景一直跟隨個這個交易過程直到這個交易完成爲止。這個交易場景的初始化是根據這次交易對象指定的參數來定製的,這些指定參數通常就會是一個配置類。所以對號入座,交易場景就由 ServletContext 來描述,而定製的參數集合就由 ServletConfig 來描述。而 ServletRequest 和 ServletResponse 就是要交互的具體對象了,它們通常都是作爲運輸工具來傳遞交互結果。

ServletConfig 是在 Servlet init 時由容器傳過來的,那麼 ServletConfig 到底是個什麼對象呢?

下圖是 ServletConfig 和 ServletContext 在 Tomcat 容器中的類關係圖。

圖 6. ServletConfig 在容器中的類關聯圖

上圖可以看出 StandardWrapper 和 StandardWrapperFacade 都實現了 ServletConfig 接口,而 StandardWrapperFacade 是 StandardWrapper 門面類。所以傳給 Servlet 的是 StandardWrapperFacade 對象,這個類能夠保證從 StandardWrapper 中拿到 ServletConfig 所規定的數據,而又不把 ServletConfig 不關心的數據暴露給 Servlet。

同樣 ServletContext 也與 ServletConfig 有類似的結構,Servlet 中能拿到的 ServletContext 的實際對象也是 ApplicationContextFacade 對象。ApplicationContextFacade 同樣保證 ServletContex 只能從容器中拿到它該拿的數據,它們都起到對數據的封裝作用,它們使用的都是門面設計模式。

通過 ServletContext 可以拿到 Context 容器中一些必要信息,比如應用的工作路徑,容器支持的 Servlet 最小版本等。

Servlet 中定義的兩個 ServletRequest 和 ServletResponse 它們實際的對象又是什麼呢?,我們在創建自己的 Servlet 類時通常使用的都是 HttpServletRequest 和 HttpServletResponse,它們繼承了 ServletRequest 和 ServletResponse。爲何 Context 容器傳過來的 ServletRequest、ServletResponse 可以被轉化爲 HttpServletRequest 和 HttpServletResponse 呢?

圖 7.Request 相關類結構圖

上圖是 Tomcat 創建的 Request 和 Response 的類結構圖。Tomcat 一接受到請求首先將會創建 org.apache.coyote.Request 和 org.apache.coyote.Response,這兩個類是 Tomcat 內部使用的描述一次請求和相應的信息類它們是一個輕量級的類,它們作用就是在服務器接收到請求後,經過簡單解析將這個請求快速的分配給後續線程去處理,所以它們的對象很小,很容易被 JVM 回收。接下去當交給一個用戶線程去處理這個請求時又創建 org.apache.catalina.connector. Request 和 org.apache.catalina.connector. Response 對象。這兩個對象一直穿越整個 Servlet 容器直到要傳給 Servlet,傳給 Servlet 的是 Request 和 Response 的門面類 RequestFacade 和 RequestFacade,這裏使用門面模式與前面一樣都是基於同樣的目的——封裝容器中的數據。一次請求對應的 Request 和 Response 的類轉化如下圖所示:

圖 8.Request 和 Response 的轉變過程

回頁首

Servlet 如何工作

我們已經清楚了 Servlet 是如何被加載的、Servlet 是如何被初始化的,以及 Servlet 的體系結構,現在的問題就是它是如何被調用的。

當用戶從瀏覽器向服務器發起一個請求,通常會包含如下信息:http://hostname: port /contextpath/servletpath,hostname 和 port 是用來與服務器建立 TCP 連接,而後面的 URL 纔是用來選擇服務器中那個子容器服務用戶的請求。那服務器是如何根據這個 URL 來達到正確的 Servlet 容器中的呢?

Tomcat7.0 中這件事很容易解決,因爲這種映射工作有專門一個類來完成的,這個就是 org.apache.tomcat.util.http.mapper,這個類保存了 Tomcat 的 Container 容器中的所有子容器的信息,當 org.apache.catalina.connector. Request 類在進入 Container 容器之前,mapper 將會根據這次請求的 hostnane 和 contextpath 將 host 和 context 容器設置到 Request 的 mappingData 屬性中。所以當 Request 進入 Container 容器之前,它要訪問那個子容器這時就已經確定了。

圖 9.Request 的 Mapper 類關係圖

可能你有疑問,mapper 中怎麼會有容器的完整關係,這要回到圖 2 中 19 步 MapperListener 類的初始化過程,下面是 MapperListener 的 init 方法代碼 :

清單 5. MapperListener.init

public void init() { 
       findDefaultHost(); 
       Engine engine = (Engine) connector.getService().getContainer(); 
       engine.addContainerListener(this); 
       Container[] conHosts = engine.findChildren(); 
       for (Container conHost : conHosts) { 
           Host host = (Host) conHost; 
           if (!LifecycleState.NEW.equals(host.getState())) { 
               host.addLifecycleListener(this); 
               registerHost(host); 
           } 
       } 
}

這段代碼的作用就是將 MapperListener 類作爲一個監聽者加到整個 Container 容器中的每個子容器中,這樣只要任何一個容器發生變化,MapperListener 都將會被通知,相應的保存容器關係的 MapperListener 的 mapper 屬性也會修改。for 循環中就是將 host 及下面的子容器註冊到 mapper 中。

圖 10.Request 在容器中的路由圖

上圖描述了一次 Request 請求是如何達到最終的 Wrapper 容器的,我們現正知道了請求是如何達到正確的 Wrapper 容器,但是請求到達最終的 Servlet 還要完成一些步驟,必須要執行 Filter 鏈,以及要通知你在 web.xml 中定義的 listener。

接下去就要執行 Servlet 的 service 方法了,通常情況下,我們自己定義的 servlet 並不是直接去實現 javax.servlet.servlet 接口,而是去繼承更簡單的 HttpServlet 類或者 GenericServlet 類,我們可以有選擇的覆蓋相應方法去實現我們要完成的工作。

Servlet 的確已經能夠幫我們完成所有的工作了,但是現在的 web 應用很少有直接將交互全部頁面都用 servlet 來實現,而是採用更加高效的 MVC 框架來實現。這些 MVC 框架基本的原理都是將所有的請求都映射到一個 Servlet,然後去實現 service 方法,這個方法也就是 MVC 框架的入口。

當 Servlet 從 Servlet 容器中移除時,也就表明該 Servlet 的生命週期結束了,這時 Servlet 的 destroy 方法將被調用,做一些掃尾工作。

回頁首

Session 與 Cookie

前面我們已經說明了 Servlet 如何被調用,我們基於 Servlet 來構建應用程序,那麼我們能從 Servlet 獲得哪些數據信息呢?

Servlet 能夠給我們提供兩部分數據,一個是在 Servlet 初始化時調用 init 方法時設置的 ServletConfig,這個類基本上含有了 Servlet 本身和 Servlet 所運行的 Servlet 容器中的基本信息。根據前面的介紹 ServletConfig 的實際對象是 StandardWrapperFacade,到底能獲得哪些容器信息可以看看這類提供了哪些接口。還有一部分數據是由 ServletRequest 類提供,它的實際對象是 RequestFacade,從提供的方法中發現主要是描述這次請求的 HTTP 協議的信息。所以要掌握 Servlet 的工作方式必須要很清楚 HTTP 協議,如果你還不清楚趕緊去找一些參考資料。關於這一塊還有一個讓很多人迷惑的 Session 與 Cookie。

Session 與 Cookie 不管是對 Java Web 的熟練使用者還是初學者來說都是一個令人頭疼的東西。Session 與 Cookie 的作用都是爲了保持訪問用戶與後端服務器的交互狀態。它們有各自的優點也有各自的缺陷。然而具有諷刺意味的是它們優點和它們的使用場景又是矛盾的,例如使用 Cookie 來傳遞信息時,隨着 Cookie 個數的增多和訪問量的增加,它佔用的網絡帶寬也很大,試想假如 Cookie 佔用 200 個字節,如果一天的 PV 有幾億的時候,它要佔用多少帶寬。所以大訪問量的時候希望用 Session,但是 Session 的致命弱點是不容易在多臺服務器之間共享,所以這也限制了 Session 的使用。

不管 Session 和 Cookie 有什麼不足,我們還是要用它們。下面詳細講一下,Session 如何基於 Cookie 來工作。實際上有三種方式能可以讓 Session 正常工作:

  1. 基於 URL Path Parameter,默認就支持
  2. 基於 Cookie,如果你沒有修改 Context 容器個 cookies 標識的話,默認也是支持的
  3. 基於 SSL,默認不支持,只有 connector.getAttribute("SSLEnabled") 爲 TRUE 時才支持

第一種情況下,當瀏覽器不支持 Cookie 功能時,瀏覽器會將用戶的 SessionCookieName 重寫到用戶請求的 URL 參數中,它的傳遞格式如 /path/Servlet;name=value;name2=value2? Name3=value3,其中“Servlet;”後面的 K-V 對就是要傳遞的 Path Parameters,服務器會從這個 Path Parameters 中拿到用戶配置的 SessionCookieName。關於這個 SessionCookieName,如果你在 web.xml 中配置 session-config 配置項的話,其 cookie-config 下的 name 屬性就是這個 SessionCookieName 值,如果你沒有配置 session-config 配置項,默認的 SessionCookieName 就是大家熟悉的“JSESSIONID”。接着 Request 根據這個 SessionCookieName 到 Parameters 拿到 Session ID 並設置到 request.setRequestedSessionId 中。

請注意如果客戶端也支持 Cookie 的話,Tomcat 仍然會解析 Cookie 中的 Session ID,並會覆蓋 URL 中的 Session ID。

如果是第三種情況的話將會根據 javax.servlet.request.ssl_session 屬性值設置 Session ID。

有了 Session ID 服務器端就可以創建 HttpSession 對象了,第一次觸發是通過 request. getSession() 方法,如果當前的 Session ID 還沒有對應的 HttpSession 對象那麼就創建一個新的,並將這個對象加到 org.apache.catalina. Manager 的 sessions 容器中保存,Manager 類將管理所有 Session 的生命週期,Session 過期將被回收,服務器關閉,Session 將被序列化到磁盤等。只要這個 HttpSession 對象存在,用戶就可以根據 Session ID 來獲取到這個對象,也就達到了狀態的保持。

圖 11.Session 相關類圖

上從圖中可以看出從 request.getSession 中獲取的 HttpSession 對象實際上是 StandardSession 對象的門面對象,這與前面的 Request 和 Servlet 是一樣的原理。下圖是 Session 工作的時序圖:

圖 12.Session 工作的時序圖(查看大圖

還有一點與 Session 關聯的 Cookie 與其它 Cookie 沒有什麼不同,這個配置的配置可以通過 web.xml 中的 session-config 配置項來指定。

回頁首

Servlet 中的 Listener

整個 Tomcat 服務器中 Listener 使用的非常廣泛,它是基於觀察者模式設計的,Listener 的設計對開發 Servlet 應用程序提供了一種快捷的手段,能夠方便的從另一個縱向維度控制程序和數據。目前 Servlet 中提供了 5 種兩類事件的觀察者接口,它們分別是:4 個 EventListeners 類型的,ServletContextAttributeListener、ServletRequestAttributeListener、ServletRequestListener、HttpSessionAttributeListener 和 2 個 LifecycleListeners 類型的,ServletContextListener、HttpSessionListener。如下圖所示:

圖 13.Servlet 中的 Listener(查看大圖

它們基本上涵蓋了整個 Servlet 生命週期中,你感興趣的每種事件。這些 Listener 的實現類可以配置在 web.xml 中的 <listener> 標籤中。當然也可以在應用程序中動態添加 Listener,需要注意的是 ServletContextListener 在容器啓動之後就不能再添加新的,因爲它所監聽的事件已經不會再出現。掌握這些 Listener 的使用,能夠讓我們的程序設計的更加靈活

Java NIO和IO的區別

下表總結了Java NIO和IO之間的主要差別,我會更詳細地描述表中每部分的差異。

複製代碼代碼如下:

IO NIO
面向流 面向緩衝
阻塞IO 非阻塞IO
無 選擇器

面向流與面向緩衝

Java NIO和IO之間第一個最大的區別是,IO是面向流的,NIO是面向緩衝區的。 Java IO面向流意味着每次從流中讀一個或多個字節,直至讀取所有字節,它們沒有被緩存在任何地方。此外,它不能前後移動流中的數據。如果需要前後移動從流中讀取的數據,需要先將它緩存到一個緩衝區。 Java NIO的緩衝導向方法略有不同。數據讀取到一個它稍後處理的緩衝區,需要時可在緩衝區中前後移動。這就增加了處理過程中的靈活性。但是,還需要檢查是否該緩衝區中包含所有您需要處理的數據。而且,需確保當更多的數據讀入緩衝區時,不要覆蓋緩衝區裏尚未處理的數據。

阻塞與非阻塞IO

Java IO的各種流是阻塞的。這意味着,當一個線程調用read() 或 write()時,該線程被阻塞,直到有一些數據被讀取,或數據完全寫入。該線程在此期間不能再幹任何事情了。 Java NIO的非阻塞模式,使一個線程從某通道發送請求讀取數據,但是它僅能得到目前可用的數據,如果目前沒有數據可用時,就什麼都不會獲取。而不是保持線程阻塞,所以直至數據變的可以讀取之前,該線程可以繼續做其他的事情。 非阻塞寫也是如此。一個線程請求寫入一些數據到某通道,但不需要等待它完全寫入,這個線程同時可以去做別的事情。 線程通常將非阻塞IO的空閒時間用於在其它通道上執行IO操作,所以一個單獨的線程現在可以管理多個輸入和輸出通道(channel)。

選擇器(Selectors)

Java NIO的選擇器允許一個單獨的線程來監視多個輸入通道,你可以註冊多個通道使用一個選擇器,然後使用一個單獨的線程來“選擇”通道:這些通道里已經有可以處理的輸入,或者選擇已準備寫入的通道。這種選擇機制,使得一個單獨的線程很容易來管理多個通道。

NIO和IO如何影響應用程序的設計

無論您選擇IO或NIO工具箱,可能會影響您應用程序設計的以下幾個方面:

1.對NIO或IO類的API調用。
2.數據處理。
3.用來處理數據的線程數。

API調用

當然,使用NIO的API調用時看起來與使用IO時有所不同,但這並不意外,因爲並不是僅從一個InputStream逐字節讀取,而是數據必須先讀入緩衝區再處理。

數據處理

使用純粹的NIO設計相較IO設計,數據處理也受到影響。

在IO設計中,我們從InputStream或 Reader逐字節讀取數據。假設你正在處理一基於行的文本數據流,例如:

複製代碼代碼如下:

Name: Anna
Age: 25
Email: [email protected]
Phone: 1234567890

該文本行的流可以這樣處理:

複製代碼代碼如下:

BufferedReader reader = new BufferedReader(new InputStreamReader(input));
String nameLine = reader.readLine();
String ageLine = reader.readLine();
String emailLine = reader.readLine();
String phoneLine = reader.readLine();

請注意處理狀態由程序執行多久決定。換句話說,一旦reader.readLine()方法返回,你就知道肯定文本行就已讀完, readline()阻塞直到整行讀完,這就是原因。你也知道此行包含名稱;同樣,第二個readline()調用返回的時候,你知道這行包含年齡等。 正如你可以看到,該處理程序僅在有新數據讀入時運行,並知道每步的數據是什麼。一旦正在運行的線程已處理過讀入的某些數據,該線程不會再回退數據(大多如此)。下圖也說明了這條原則:

img

 

(Java IO: 從一個阻塞的流中讀數據) 而一個NIO的實現會有所不同,下面是一個簡單的例子:

複製代碼代碼如下:

ByteBuffer buffer = ByteBuffer.allocate(48);
int bytesRead = inChannel.read(buffer);

注意第二行,從通道讀取字節到ByteBuffer。當這個方法調用返回時,你不知道你所需的所有數據是否在緩衝區內。你所知道的是,該緩衝區包含一些字節,這使得處理有點困難。
假設第一次 read(buffer)調用後,讀入緩衝區的數據只有半行,例如,“Name:An”,你能處理數據嗎?顯然不能,需要等待,直到整行數據讀入緩存,在此之前,對數據的任何處理毫無意義。

所以,你怎麼知道是否該緩衝區包含足夠的數據可以處理呢?好了,你不知道。發現的方法只能查看緩衝區中的數據。其結果是,在你知道所有數據都在緩衝區裏之前,你必須檢查幾次緩衝區的數據。這不僅效率低下,而且可以使程序設計方案雜亂不堪。例如:

複製代碼代碼如下:

ByteBuffer buffer = ByteBuffer.allocate(48);
int bytesRead = inChannel.read(buffer);
while(! bufferFull(bytesRead) ) {
bytesRead = inChannel.read(buffer);
}

bufferFull()方法必須跟蹤有多少數據讀入緩衝區,並返回真或假,這取決於緩衝區是否已滿。換句話說,如果緩衝區準備好被處理,那麼表示緩衝區滿了。

bufferFull()方法掃描緩衝區,但必須保持在bufferFull()方法被調用之前狀態相同。如果沒有,下一個讀入緩衝區的數據可能無法讀到正確的位置。這是不可能的,但卻是需要注意的又一問題。

如果緩衝區已滿,它可以被處理。如果它不滿,並且在你的實際案例中有意義,你或許能處理其中的部分數據。但是許多情況下並非如此。下圖展示了“緩衝區數據循環就緒”:

img


3) 用來處理數據的線程數

 

NIO可讓您只使用一個(或幾個)單線程管理多個通道(網絡連接或文件),但付出的代價是解析數據可能會比從一個阻塞流中讀取數據更復雜。

如果需要管理同時打開的成千上萬個連接,這些連接每次只是發送少量的數據,例如聊天服務器,實現NIO的服務器可能是一個優勢。同樣,如果你需要維持許多打開的連接到其他計算機上,如P2P網絡中,使用一個單獨的線程來管理你所有出站連接,可能是一個優勢。一個線程多個連接的設計方案如

img

 

Java NIO: 單線程管理多個連接

如果你有少量的連接使用非常高的帶寬,一次發送大量的數據,也許典型的IO服務器實現可能非常契合。下圖說明了一個典型的IO服務器設計:

img

 

Java IO: 一個典型的IO服務器設計- 一個連接通過一個線程處理

Java中堆內存和棧內存區別

Java把內存分成兩種,一種叫做棧內存,一種叫做堆內存

在函數中定義的一些基本類型的變量和對象的引用變量都是在函數的棧內存中分配。當在一段代碼塊中定義一個變量時,java就在棧中爲這個變量分配內存空間,當超過變量的作用域後,java會自動釋放掉爲該變量分配的內存空間,該內存空間可以立刻被另作他用。

堆內存用於存放由new創建的對象和數組。在堆中分配的內存,由java虛擬機自動垃圾回收器來管理。在堆中產生了一個數組或者對象後,還可以在棧中定義一個特殊的變量,這個變量的取值等於數組或者對象在堆內存中的首地址,在棧中的這個特殊的變量就變成了數組或者對象的引用變量,以後就可以在程序中使用棧內存中的引用變量來訪問堆中的數組或者對象,引用變量相當於爲數組或者對象起的一個別名,或者代號。

引用變量是普通變量,定義時在棧中分配內存,引用變量在程序運行到作用域外釋放。而數組&對象本身在堆中分配,即使程序運行到使用new產生數組和對象的語句所在地代碼塊之外,數組和對象本身佔用的堆內存也不會被釋放,數組和對象在沒有引用變量指向它的時候,才變成垃圾,不能再被使用,但是仍然佔着內存,在隨後的一個不確定的時間被垃圾回收器釋放掉。這個也是java比較佔內存的主要原因,********實際上,棧中的變量指向堆內存中的變量,這就是 Java 中的指針!


java中內存分配策略及堆和棧的比較
  1 內存分配策略
  按照編譯原理的觀點,程序運行時的內存分配有三種策略,分別是靜態的,棧式的,和堆式的.
  靜態存儲分配是指在編譯時就能確定每個數據目標在運行時刻的存儲空間需求,因而在編譯時就可以給他們分配固定的內存空間.這種分配策略要求程序代碼中不允許有可變數據結構(比如可變數組)的存在,也不允許有嵌套或者遞歸的結構出現,因爲它們都會導致編譯程序無法計算準確的存儲空間需求.
  棧式存儲分配也可稱爲動態存儲分配,是由一個類似於堆棧的運行棧來實現的.和靜態存儲分配相反,在棧式存儲方案中,程序對數據區的需求在編譯時是完全未知的,只有到運行的時候才能夠知道,但是規定在運行中進入一個程序模塊時,必須知道該程序模塊所需的數據區大小才能夠爲其分配內存.和我們在數據結構所熟知的棧一樣,棧式存儲分配按照先進後出的原則進行分配。
  靜態存儲分配要求在編譯時能知道所有變量的存儲要求,棧式存儲分配要求在過程的入口處必須知道所有的存儲要求,而堆式存儲分配則專門負責在編譯時或運行時模塊入口處都無法確定存儲要求的數據結構的內存分配,比如可變長度串和對象實例.堆由大片的可利用塊或空閒塊組成,堆中的內存可以按照任意順序分配和釋放.
  2 堆和棧的比較
  上面的定義從編譯原理的教材中總結而來,除靜態存儲分配之外,都顯得很呆板和難以理解,下面撇開靜態存儲分配,集中比較堆和棧:
  從堆和棧的功能和作用來通俗的比較,堆主要用來存放對象的,棧主要是用來執行程序的.而這種不同又主要是由於堆和棧的特點決定的:
  在編程中,例如C/C++中,所有的方法調用都是通過棧來進行的,所有的局部變量,形式參數都是從棧中分配內存空間的。實際上也不是什麼分配,只是從棧頂向上用就行,就好像工廠中的傳送帶(conveyor belt)一樣,Stack Pointer會自動指引你到放東西的位置,你所要做的只是把東西放下來就行.退出函數的時候,修改棧指針就可以把棧中的內容銷燬.這樣的模式速度最快, 當然要用來運行程序了.需要注意的是,在分配的時候,比如爲一個即將要調用的程序模塊分配數據區時,應事先知道這個數據區的大小,也就說是雖然分配是在程序運行時進行的,但是分配的大小多少是確定的,不變的,而這個"大小多少"是在編譯時確定的,不是在運行時.
  堆是應用程序在運行的時候請求操作系統分配給自己內存,由於從操作系統管理的內存分配,所以在分配和銷燬時都要佔用時間,因此用堆的效率非常低.但是堆的優點在於,編譯器不必知道要從堆裏分配多少存儲空間,也不必知道存儲的數據要在堆裏停留多長的時間,因此,用堆保存數據時會得到更大的靈活性。事實上,面向對象的多態性,堆內存分配是必不可少的,因爲多態變量所需的存儲空間只有在運行時創建了對象之後才能確定.在C++中,要求創建一個對象時,只需用 new命令編制相關的代碼即可。執行這些代碼時,會在堆裏自動進行數據的保存.當然,爲達到這種靈活性,必然會付出一定的代價:在堆裏分配存儲空間時會花掉更長的時間!這也正是導致我們剛纔所說的效率低的原因,看來列寧同志說的好,人的優點往往也是人的缺點,人的缺點往往也是人的優點(暈~).
  3 JVM中的堆和棧
  JVM是基於堆棧的虛擬機.JVM爲每個新創建的線程都分配一個堆棧.也就是說,對於一個Java程序來說,它的運行就是通過對堆棧的操作來完成的。堆棧以幀爲單位保存線程的狀態。JVM對堆棧只進行兩種操作:以幀爲單位的壓棧和出棧操作。
  我們知道,某個線程正在執行的方法稱爲此線程的當前方法.我們可能不知道,當前方法使用的幀稱爲當前幀。當線程激活一個Java方法,JVM就會在線程的 Java堆棧裏新壓入一個幀。這個幀自然成爲了當前幀.在此方法執行期間,這個幀將用來保存參數,局部變量,中間計算過程和其他數據.這個幀在這裏和編譯原理中的活動紀錄的概念是差不多的.
  從Java的這種分配機制來看,堆棧又可以這樣理解:堆棧(Stack)是操作系統在建立某個進程時或者線程(在支持多線程的操作系統中是線程)爲這個線程建立的存儲區域,該區域具有先進後出的特性。
  每一個Java應用都唯一對應一個JVM實例,每一個實例唯一對應一個堆。應用程序在運行中所創建的所有類實例或數組都放在這個堆中,並由應用所有的線程共享.跟C/C++不同,Java中分配堆內存是自動初始化的。Java中所有對象的存儲空間都是在堆中分配的,但是這個對象的引用卻是在堆棧中分配,也就是說在建立一個對象時從兩個地方都分配內存,在堆中分配的內存實際建立這個對象,而在堆棧中分配的內存只是一個指向這個堆對象的指針(引用)而已。
  Java 中的堆和棧
  Java把內存劃分成兩種:一種是棧內存,一種是堆內存。
  在函數中定義的一些基本類型的變量和對象的引用變量都在函數的棧內存中分配。
  當在一段代碼塊定義一個變量時,Java就在棧中爲這個變量分配內存空間,當超過變量的作用域後,Java會自動釋放掉爲該變量所分配的內存空間,該內存空間可以立即被另作他用。
  堆內存用來存放由new創建的對象和數組。
  在堆中分配的內存,由Java虛擬機的自動垃圾回收器來管理。
  在堆中產生了一個數組或對象後,還可以在棧中定義一個特殊的變量,讓棧中這個變量的取值等於數組或對象在堆內存中的首地址,棧中的這個變量就成了數組或對象的引用變量。
  引用變量就相當於是爲數組或對象起的一個名稱,以後就可以在程序中使用棧中的引用變量來訪問堆中的數組或對象。
  具體的說:
  棧與堆都是Java用來在Ram中存放數據的地方。與C++不同,Java自動管理棧和堆,程序員不能直接地設置棧或堆。
  Java的堆是一個運行時數據區,類的(對象從中分配空間。這些對象通過new、newarray、anewarray和multianewarray等指令建立,它們不需要程序代碼來顯式的釋放。堆是由垃圾回收來負責的,堆的優勢是可以動態地分配內存大小,生存期也不必事先告訴編譯器,因爲它是在運行時動態分配內存的,Java的垃圾收集器會自動收走這些不再使用的數據。但缺點是,由於要在運行時動態分配內存,存取速度較慢。
  棧的優勢是,存取速度比堆要快,僅次於寄存器,棧數據可以共享。但缺點是,存在棧中的數據大小與生存期必須是確定的,缺乏靈活性。棧中主要存放一些基本類型的變量(,int, short, long, byte, float, double, boolean, char)和對象句柄。
  棧有一個很重要的特殊性,就是存在棧中的數據可以共享。假設我們同時定義:
  int a = 3;
  int b = 3;
  編譯器先處理int a = 3;首先它會在棧中創建一個變量爲a的引用,然後查找棧中是否有3這個值,如果沒找到,就將3存放進來,然後將a指向3。接着處理int b = 3;在創建完b的引用變量後,因爲在棧中已經有3這個值,便將b直接指向3。這樣,就出現了a與b同時均指向3的情況。這時,如果再令a=4;那麼編譯器會重新搜索棧中是否有4值,如果沒有,則將4存放進來,並令a指向4;如果已經有了,則直接將a指向這個地址。因此a值的改變不會影響到b的值。要注意這種數據的共享與兩個對象的引用同時指向一個對象的這種共享是不同的,因爲這種情況a的修改並不會影響到b, 它是由編譯器完成的,它有利於節省空間。而一個對象引用變量修改了這個對象的內部狀態,會影響到另一個對象引用變量

反射講一講,主要是概念,都在哪需要反射機制,反射的性能,如何優化

反射機制的定義:

是在運行狀態中,對於任意的一個類,都能夠知道這個類的所有屬性和方法,對任意一個對象都能夠通過反射機制調用一個類的任意方法,這種動態獲取類信息及動態調用類對象方法的功能稱爲java的反射機制。

反射的作用:

1、動態地創建類的實例,將類綁定到現有的對象中,或從現有的對象中獲取類型。

2、應用程序需要在運行時從某個特定的程序集中載入一個特定的類

如何保證RESTful API安全性

友情鏈接: 如何設計好的RESTful API之安全性

如何預防MySQL注入

所謂SQL注入,就是通過把SQL命令插入到Web表單遞交或輸入域名或頁面請求的查詢字符串,最終達到欺騙服務器執行惡意的SQL命令。

我們永遠不要信任用戶的輸入,我們必須認定用戶輸入的數據都是不安全的,我們都需要對用戶輸入的數據進行過濾處理。

1.以下實例中,輸入的用戶名必須爲字母、數字及下劃線的組合,且用戶名長度爲 8 到 20 個字符之間:

if (preg_match("/^\w{8,20}$/", $_GET['username'], $matches))
{
$result = mysql_query("SELECT * FROM users 
      WHERE username=$matches[0]");
}
else 
{
echo "username 輸入異常";
}

讓我們看下在沒有過濾特殊字符時,出現的SQL情況:

// 設定$name 中插入了我們不需要的SQL語句
$name = "Qadir'; DELETE FROM users;";
mysql_query("SELECT * FROM users WHERE name='{$name}'");

以上的注入語句中,我們沒有對 $name 的變量進行過濾,$name 中插入了我們不需要的SQL語句,將刪除 users 表中的所有數據。

2.在PHP中的 mysql_query() 是不允許執行多個SQL語句的,但是在 SQLite 和 PostgreSQL 是可以同時執行多條SQL語句的,所以我們對這些用戶的數據需要進行嚴格的驗證。

防止SQL注入,我們需要注意以下幾個要點:

1.永遠不要信任用戶的輸入。對用戶的輸入進行校驗,可以通過正則表達式,或限制長度;對單引號和 雙"-"進行轉換等。
2.永遠不要使用動態拼裝sql,可以使用參數化的sql或者直接使用存儲過程進行數據查詢存取。
3.永遠不要使用管理員權限的數據庫連接,爲每個應用使用單獨的權限有限的數據庫連接。
4.不要把機密信息直接存放,加密或者hash掉密碼和敏感的信息。
5.應用的異常信息應該給出儘可能少的提示,最好使用自定義的錯誤信息對原始錯誤信息進行包裝
6.sql注入的檢測方法一般採取輔助軟件或網站平臺來檢測,軟件一般採用sql注入檢測工具jsky,網站平臺就有億思網站安全平臺檢測工具。MDCSOFT SCAN等。採用MDCSOFT-IPS可以有效的防禦SQL注入,XSS攻擊等。

3.防止SQL注入

在腳本語言,如Perl和PHP你可以對用戶輸入的數據進行轉義從而來防止SQL注入。

PHP的MySQL擴展提供了mysql_real_escape_string()函數來轉義特殊的輸入字符。

if (get_magic_quotes_gpc()) 
{
$name = stripslashes($name);
}
$name = mysql_real_escape_string($name);
mysql_query("SELECT * FROM users WHERE name='{$name}'");

4.Like語句中的注入

like查詢時,如果用戶輸入的值有""和"%",則會出現這種情況:用戶本來只是想查詢"abcd",查詢結果中卻有"abcd_"、"abcde"、"abcdf"等等;用戶要查詢"30%"(注:百分之三十)時也會出現問題。

在PHP腳本中我們可以使用addcslashes()函數來處理以上情況,如下實例:

$sub = addcslashes(mysql_real_escape_string("%something_"), "%_");
// $sub == \%something\_
mysql_query("SELECT * FROM messages WHERE subject LIKE '{$sub}%'");

addcslashes()函數在指定的字符前添加反斜槓。

語法格式:

addcslashes(string,characters)

參數 描述
string 必需。規定要檢查的字符串。
characters 可選。規定受 addcslashes() 影響的字符或字符範圍。

ThreadLocal(線程變量副本)

Synchronized實現內存共享,ThreadLocal爲每個線程維護一個本地變量。

採用空間換時間,它用於線程間的數據隔離,爲每一個使用該變量的線程提供一個副本,每個線程都可以獨立地改變自己的副本,而不會和其他線程的副本衝突。

ThreadLocal類中維護一個Map,用於存儲每一個線程的變量副本,Map中元素的鍵爲線程對象,而值爲對應線程的變量副本。

ThreadLocal在spring中發揮着巨大的作用,在管理Request作用域中的Bean、事務管理、任務調度、AOP等模塊都出現了它的身影。

Spring中絕大部分Bean都可以聲明成Singleton作用域,採用ThreadLocal進行封裝,因此有狀態的Bean就能夠以singleton的方式在多線程中正常工作了。

你能不能談談,Java GC是在什麼時候,對什麼東西,做了什麼事情?

在什麼時候:

1.新生代有一個Eden區和兩個survivor區,首先將對象放入Eden區,如果空間不足就向其中的一個survivor區上放,如果仍然放不下就會引發一次發生在新生代的minor GC,將存活的對象放入另一個survivor區中,然後清空Eden和之前的那個survivor區的內存。在某次GC過程中,如果發現仍然又放不下的對象,就將這些對象放入老年代內存裏去。

2.大對象以及長期存活的對象直接進入老年區。

3.當每次執行minor GC的時候應該對要晉升到老年代的對象進行分析,如果這些馬上要到老年區的老年對象的大小超過了老年區的剩餘大小,那麼執行一次Full GC以儘可能地獲得老年區的空間。

對什麼東西:從GC Roots搜索不到,而且經過一次標記清理之後仍沒有復活的對象。

做什麼:
新生代:複製清理;
老年代:標記-清除和標記-壓縮算法
永久代:存放Java中的類和加載類的類加載器本身。

GC Roots都有哪些:
\1. 虛擬機棧中的引用的對象
\2. 方法區中靜態屬性引用的對象,常量引用的對象
\3. 本地方法棧中JNI(即一般說的Native方法)引用的對象。

Volatile和Synchronized四個不同點:

1 粒度不同,前者鎖對象和類,後者針對變量
2 syn阻塞,volatile線程不阻塞
3 syn保證三大特性,volatile不保證原子性
4 syn編譯器優化,volatile不優化
volatile具備兩種特性:
\1. 保證此變量對所有線程的可見性,指一條線程修改了這個變量的值,新值對於其他線程來說是可見的,但並不是多線程安全的。
\2. 禁止指令重排序優化。
Volatile如何保證內存可見性:
1.當寫一個volatile變量時,JMM會把該線程對應的本地內存中的共享變量刷新到主內存。
2.當讀一個volatile變量時,JMM會把該線程對應的本地內存置爲無效。線程接下來將從主內存中讀取共享變量。

同步:就是一個任務的完成需要依賴另外一個任務,只有等待被依賴的任務完成後,依賴任務才能完成。
異步:不需要等待被依賴的任務完成,只是通知被依賴的任務要完成什麼工作,只要自己任務完成了就算完成了,被依賴的任務是否完成會通知回來。(異步的特點就是通知)。
打電話和發短信來比喻同步和異步操作。
阻塞:CPU停下來等一個慢的操作完成以後,纔會接着完成其他的工作。
非阻塞:非阻塞就是在這個慢的執行時,CPU去做其他工作,等這個慢的完成後,CPU纔會接着完成後續的操作。
非阻塞會造成線程切換增加,增加CPU的使用時間能不能補償系統的切換成本需要考慮。

線程池的作用:

在程序啓動的時候就創建若干線程來響應處理,它們被稱爲線程池,裏面的線程叫工作線程
第一:降低資源消耗。通過重複利用已創建的線程降低線程創建和銷燬造成的消耗。
第二:提高響應速度。當任務到達時,任務可以不需要等到線程創建就能立即執行。
第三:提高線程的可管理性。
常用線程池:ExecutorService 是主要的實現類,其中常用的有
Executors.newSingleThreadPool(),newFixedThreadPool(),newcachedTheadPool(),newScheduledThreadPool()。

一致性哈希:

Redis數據結構: String—字符串(key-value 類型)

索引:B+,B-,全文索引

MySQL的索引是一個數據結構,旨在使數據庫高效的查找數據。
常用的數據結構是B+Tree,每個葉子節點不但存放了索引鍵的相關信息還增加了指向相鄰葉子節點的指針,這樣就形成了帶有順序訪問指針的B+Tree,做這個優化的目的是提高不同區間訪問的性能。
什麼時候使用索引:

  1. 經常出現在group by,order by和distinc關鍵字後面的字段
  2. 經常與其他表進行連接的表,在連接字段上應該建立索引
  3. 經常出現在Where子句中的字段
  4. 經常出現用作查詢選擇的字段

Spring IOC AOP(控制反轉,依賴注入)

IOC容器:就是具有依賴注入功能的容器,是可以創建對象的容器,IOC容器負責實例化、定位、配置應用程序中的對象及建立這些對象間的依賴。通常new一個實例,控制權由程序員控制,而"控制反轉"是指new實例工作不由程序員來做而是交給Spring容器來做。。在Spring中BeanFactory是IOC容器的實際代表者

DI(依賴注入Dependency injection) :在容器創建對象後,處理對象的依賴關係。

Spring支持三種依賴注入方式,分別是屬性(Setter方法)注入,構造注入和接口注入。

在Spring中,那些組成應用的主體及由Spring IOC容器所管理的對象被稱之爲Bean。

Spring的IOC容器通過反射的機制實例化Bean並建立Bean之間的依賴關係。
簡單地講,Bean就是由Spring IOC容器初始化、裝配及被管理的對象。
獲取Bean對象的過程,首先通過Resource加載配置文件並啓動IOC容器,然後通過getBean方法獲取bean對象,就可以調用他的方法。
Spring Bean的作用域
Singleton:Spring IOC容器中只有一個共享的Bean實例,一般都是Singleton作用域。
Prototype:每一個請求,會產生一個新的Bean實例。
Request:每一次http請求會產生一個新的Bean實例。

AOP就是縱向的編程,如業務1和業務2都需要一個共同的操作,與其往每個業務中都添加同樣的代碼,不如寫一遍代碼,讓兩個業務共同使用這段代碼。在日常有訂單管理、商品管理、資金管理、庫存管理等業務,都會需要到類似日誌記錄事務控制、****權限控制、性能統計、異常處理及事務處理等。AOP把所有共有代碼全部抽取出來,放置到某個地方集中管理,然後在具體運行時,再由容器動態織入這些共有代碼。

Spring AOP應用場景
性能檢測,訪問控制,日誌管理,事務等。
默認的策略是如果目標類實現接口,則使用JDK動態代理技術,如果目標對象沒有實現接口,則默認會採用CGLIB代理

友情鏈接: Spring框架IOC容器和AOP解析

友情鏈接:淺談Spring框架註解的用法分析

友情鏈接:關於Spring的69個面試問答——終極列表

代理的共有優點:業務類只需要關注業務邏輯本身,保證了業務類的重用性。

Java靜態代理
代理對象和目標對象實現了相同的接口,目標對象作爲代理對象的一個屬性,具體接口實現中,代理對象可以在調用目標對象相應方法前後加上其他業務處理邏輯。
缺點:一個代理類只能代理一個業務類。如果業務類增加方法時,相應的代理類也要增加方法。
Java動態代理
Java動態代理是寫一個類實現InvocationHandler接口,重寫Invoke方法,在Invoke方法可以進行增強處理的邏輯的編寫,這個公共代理類在運行的時候才能明確自己要代理的對象,同時可以實現該被代理類的方法的實現,然後在實現類方法的時候可以進行增強處理。
實際上:代理對象的方法 = 增強處理 + 被代理對象的方法

JDK和CGLIB生成動態代理類的區別:
JDK動態代理只能針對實現了接口的類生成代理(實例化一個類)。此時代理對象和目標對象實現了相同的接口,目標對象作爲代理對象的一個屬性,具體接口實現中,可以在調用目標對象相應方法前後加上其他業務處理邏輯
CGLIB是針對類實現代理,主要是對指定的類生成一個子類(沒有實例化一個類),覆蓋其中的方法 。

SpringMVC運行原理

\1. 客戶端請求提交到DispatcherServlet
\2. 由DispatcherServlet控制器查詢HandlerMapping,找到並分發到指定的Controller中。
\4. Controller調用業務邏輯處理後,返回ModelAndView
\5. DispatcherServlet查詢一個或多個ViewResoler視圖解析器,找到ModelAndView指定的視圖
\6. 視圖負責將結果顯示到客戶端

友情鏈接:Spring:基於註解的Spring MVC(上)

友情鏈接: Spring:基於註解的Spring MVC(下)

友情鏈接:SpringMVC與Struts2區別與比較總結

友情鏈接:SpringMVC與Struts2的對比

TCP三次握手,四次揮手

TCP作爲一種可靠傳輸控制協議,其核心思想:既要保證數據可靠傳輸,又要提高傳輸的效率,而用三次恰恰可以滿足以上兩方面的需求!****雙方都需要確認自己的發信和收信功能正常,收信功能通過接收對方信息得到確認,發信功能需要發出信息—>對方回覆信息得到確認。

三次握手過程:

  1. 第一次握手:建立連接。客戶端發送連接請求報文段,將SYN位置爲1,Sequence Number爲x;然後,客戶端進入SYN_SEND狀態,等待服務器的確認;
  2. 第二次握手:服務器收到客戶端的SYN報文段,需要對這個SYN報文段進行確認,設置ACK爲x+1(Sequence Number+1);同時,自己還要發送SYN請求信息,將SYN位置爲1,Sequence Number爲y;服務器端將上述所有信息放到一個報文段(即SYN+ACK報文段)中,一併發送給客戶端,此時服務器進入SYN_RECV狀態;
  3. 第三次握手:客戶端收到服務器的SYN+ACK報文段。然後將Acknowledgment Number設置爲y+1,向服務器發送ACK報文段,這個報文段發送完畢以後,客戶端和服務器端都進入ESTABLISHED狀態,完成TCP三次握手。

TCP工作在網絡OSI的七層模型中的第四層——Transport層,IP在第三層——Network層
�ARP在第二層——Data Link層;在第二層上的數據,我們把它叫Frame,在第三層上的數據叫Packet,第四層的數據叫Segment。

四次揮手過程:

  1. 第一次分手:主機1(可以使客戶端,也可以是服務器端),設置Sequence NumberAcknowledgment Number,向主機2發送一個FIN報文段;此時,主機1進入FIN_WAIT_1狀態;這表示主機1沒有數據要發送給主機2了;
  2. 第二次分手:主機2收到了主機1發送的FIN報文段,向主機1回一個ACK報文段,Acknowledgment NumberSequence Number加1;主機1進入FIN_WAIT_2狀態;主機2告訴主機1,我“同意”你的關閉請求;
  3. 第三次分手:主機2向主機1發送FIN報文段,請求關閉連接,同時主機2進入LAST_ACK狀態;
  4. 第四次分手:主機1收到主機2發送的FIN報文段,向主機2發送ACK報文段,然後主機1進入TIME_WAIT狀態;主機2收到主機1的ACK報文段以後,就關閉連接;此時,主機1等待2MSL後依然沒有收到回覆,則證明Server端已正常關閉,那好,主機1也可以關閉連接了。

 

    (2)而關閉連接卻是四次揮手呢?
    這是因爲服務端在LISTEN狀態下,收到建立連接請求的SYN報文後,把ACK和SYN放在一個報文裏發送給客戶端。

爲什麼建立連接是三次握手

這是因爲服務端在LISTEN狀態下,收到建立連接請求的SYN報文後,把ACK和SYN放在一個報文裏發送給客戶端。

關閉連接卻是四次揮手呢

而關閉連接時,當收到對方的FIN報文時,僅僅表示對方不再發送數據了但是還能接收數據,己方也未必全部數據都發送給對方了,所以己方可以立即close,也可以發送一些數據給對方後,再發送FIN報文給對方來表示同意現在關閉連接,因此,己方ACK和FIN一般都會分開發送。

HTTPS和HTTP 爲什麼更安全,先看這些

http默認端口是80 https是443

http是HTTP協議運行在TCP之上。所有傳輸的內容都是明文,客戶端和服務器端都無法驗證對方的身份。

https是HTTP運行在SSL/TLS之上,SSL/TLS運行在TCP之上。所有傳輸的內容都經過加密,加密採用對稱加密,但對稱加密的密鑰用服務器方的證書進行了非對稱加密。此外客戶端可以驗證服務器端的身份,如果配置了客戶端驗證,服務器方也可以驗證客戶端的身份。HTTP(應用層) 和TCP(傳輸層)之間插入一個SSL協議,

一個Http請求

DNS域名解析 –> 發起TCP的三次握手 –> 建立TCP連接後發起http請求 –> 服務器響應http請求,瀏覽器得到html代碼 –> 瀏覽器解析html代碼,並請求html代碼中的資源(如js、css、圖片等) –> 瀏覽器對頁面進行渲染呈現給用戶

友情鏈接: HTTP與HTTPS的區別

友情鏈接: HTTPS 爲什麼更安全,先看這些

友情鏈接: HTTP請求報文和HTTP響應報文

友情鏈接: HTTP 請求方式: GET和POST的比較

Mybatis

每一個Mybatis的應用程序都以一個SqlSessionFactory對象的實例爲核心。首先用字節流通過Resource將配置文件讀入,然後通過SqlSessionFactoryBuilder().build方法創建SqlSessionFactory,然後再通過sqlSessionFactory.openSession()方法創建一個sqlSession爲每一個數據庫事務服務。
經歷了Mybatis初始化 –>創建SqlSession –>運行SQL語句 返回結果三個過程

Servlet和Filter的區別:

整的流程是:Filter對用戶請求進行預處理,接着將請求交給Servlet進行處理並生成響應,最後Filter再對服務器響應進行後處理。

Filter有如下幾個用處:
Filter可以進行對特定的url請求和相應做預處理和後處理。
在HttpServletRequest到達Servlet之前,攔截客戶的HttpServletRequest。
根據需要檢查HttpServletRequest,也可以修改HttpServletRequest頭和數據。
在HttpServletResponse到達客戶端之前,攔截HttpServletResponse。
根據需要檢查HttpServletResponse,也可以修改HttpServletResponse頭和數據

實際上Filter和Servlet極其相似,區別只是Filter不能直接對用戶生成響應。實際上Filter裏doFilter()方法裏的代碼就是從多個Servlet的service()方法裏抽取的通用代碼,通過使用Filter可以實現更好的複用。

Filter和Servlet的生命週期:
1.Filter在web服務器啓動時初始化
2.如果某個Servlet配置了 1 ,該Servlet也是在Tomcat(Servlet容器)啓動時初始化。
3.如果Servlet沒有配置1 ,該Servlet不會在Tomcat啓動時初始化,而是在請求到來時初始化。
4.每次請求, Request都會被初始化,響應請求後,請求被銷燬
5.Servlet初始化後,將不會隨着請求的結束而註銷。
6.關閉Tomcat時,Servlet、Filter依次被註銷。

HashMap和TreeMap區別

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沒有調優選項,因爲該樹總處於平衡狀態。 適合按照自然順序或者自定義的順序排序遍歷key
(1)TreeMap():構建一個空的映像樹
(2)TreeMap(Map m): 構建一個映像樹,並且添加映像m中所有元素
(3)TreeMap(Comparator c): 構建一個映像樹,並且使用特定的比較器對關鍵字進行排序
(4)TreeMap(SortedMap s): 構建一個映像樹,添加映像樹s中所有映射,並且使用與有序映像s相同的比較器排序

友情鏈接: Java中HashMap和TreeMap的區別深入理解

HashMap衝突

友情鏈接: HashMap衝突的解決方法以及原理分析

友情鏈接: HashMap的工作原理

友情鏈接: HashMap和Hashtable的區別

友情鏈接: 2種辦法讓HashMap線程安全

HashMap,ConcurrentHashMap與LinkedHashMap的區別

  1. ConcurrentHashMap是使用了鎖分段技術技術來保證線程安全的,鎖分段技術:首先將數據分成一段一段的存儲,然後給每一段數據配一把鎖,當一個線程佔用鎖訪問其中一個段數據的時候,其他段的數據也能被其他線程訪問

  2. ConcurrentHashMap 是在每個段(segment)中線程安全的

  3. LinkedHashMap維護一個雙鏈表,可以將裏面的數據按寫入的順序讀出

  4. ConcurrentHashMap應用場景

1:ConcurrentHashMap的應用場景是高併發,但是並不能保證線程安全,而同步的HashMap和HashMap的是鎖住整個容器,而加鎖之後ConcurrentHashMap不需要鎖住整個容器,只需要鎖住對應的Segment就好了,所以可以保證高併發同步訪問,提升了效率。

2:可以多線程寫。
ConcurrentHashMap把HashMap分成若干個Segmenet
1.get時,不加鎖,先定位到segment然後在找到頭結點進行讀取操作。而value是volatile變量,所以可以保證在競爭條件時保證讀取最新的值,如果讀到的value是null,則可能正在修改,那麼久調用ReadValueUnderLock函數,加鎖保證讀到的數據是正確的。
2.Put時會加鎖,一律添加到hash鏈的頭部。
3.Remove時也會加鎖,由於next是final類型不可改變,所以必須把刪除的節點之前的節點都複製一遍。
4.ConcurrentHashMap允許多個修改操作併發進行,其關鍵在於使用了鎖分離技術。它使用了多個鎖來控制對Hash表的不同Segment進行的修改。

ConcurrentHashMap的應用場景是高併發,但是並不能保證線程安全,而同步的HashMap和HashTable的是鎖住整個容器,而加鎖之後ConcurrentHashMap不需要鎖住整個容器,只需要鎖住對應的segment就好了,所以可以保證高併發同步訪問,提升了效率。

友情鏈接:Java集合—ConcurrentHashMap原理分析

ThreadPoolExecutor 的內部工作原理

進程間的通信方式

  1. 管道( pipe ):管道是一種半雙工的通信方式,數據只能單向流動,而且只能在具有親緣關係的進程間使用。進程的親緣關係通常是指父子進程關係。
  2. 有名管道 (named pipe) : 有名管道也是半雙工的通信方式,但是它允許無親緣關係進程間的通信。
    3.信號量( semophore ) : 信號量是一個計數器,可以用來控制多個進程對共享資源的訪問。它常作爲一種鎖機制,防止某進程正在訪問共享資源時,其他進程也訪問該資源。因此,主要作爲進程間以及同一進程內不同線程之間的同步手段。
  3. 消息隊列( message queue ) : 消息隊列是由消息的鏈表,存放在內核中並由消息隊列標識符標識。消息隊列克服了信號傳遞信息少、管道只能承載無格式字節流以及緩衝區大小受限等缺點。
    5.信號 ( sinal ) : 信號是一種比較複雜的通信方式,用於通知接收進程某個事件已經發生。
    6.共享內存( shared memory ) :共享內存就是映射一段能被其他進程所訪問的內存,這段共享內存由一個進程創建,但多個進程都可以訪問。共享內存是最快的 IPC 方式,它是針對其他進程間通信方式運行效率低而專門設計的。它往往與其他通信機制,如信號量,配合使用,來實現進程間的同步和通信。
    7.套接字( socket ) : 套解口也是一種進程間通信機制,與其他通信機制不同的是,它可用於不同機器間的進程通信。

死鎖的必要條件

  1. 互斥 至少有一個資源處於非共享狀態
  2. 佔有並等待
  3. 非搶佔
  4. 循環等待
    解決死鎖,第一個是死鎖預防,就是不讓上面的四個條件同時成立。二是,合理分配資源。
    三是使用銀行家算法,如果該進程請求的資源操作系統剩餘量可以滿足,那麼就分配。



作者:時芥藍
鏈接:https://www.jianshu.com/p/1acdfac2b4e4
來源:簡書
著作權歸作者所有。商業轉載請聯繫作者獲得授權,非商業轉載請註明出處。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章