java后台常见问题
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。
垃圾搜集器
-
新生代串行收集器 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字節.?——對應上圖中的以太網數據
四層模型
網絡接口層包括用于協作IP數據在已有網絡介質上傳輸的協議。
它定義像地址解析協議(Address Resolution Protocol,ARP)這樣的協議,?供 TCP/IP 協議的數據結構和實際物理硬件之間的接口。
可以理解為:確定了網絡數據包的形式。
網間層對應于 OSI 七層參考模型的網絡層,本層包含 IP 協議、RIP 協議(Routing Information Protocol,路由信息協議),負責數據的包裝、尋址和路由。同時還包含網間控制報文協議(Internet Control Message Protocol,ICMP)用來?供網絡診斷信息;
可以理解為:該層時確定計算機的位置。
傳輸層對應于 OSI 七層參考模型的傳輸層,它?供兩種端到端的通信服務。其中 TCP 協議(Transmission Control Protocol)?供可靠的數據流運輸服務,UDP 協議(Use Datagram Protocol)?供不可靠的用戶數據報服務。
TCP:三次握手、四次揮手;UDP:只發不管別人收不收得到--任性哈
應用層對應于 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 線程有哪些狀態,這些狀態之間是如何轉化的?
這里寫圖片描述(一). 等待阻塞:運行(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)狀態。
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有一個實現類:TreeMapSession機制
一、術語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的引用值x和y,當且僅當x.equals(y)是true時,y.equals(x)也是true。
- 傳遞性(transitive)。對于任意不為null的引用值x、y和z,如果x.equals(y)是true,同時y.equals(z)是true,那么x.equals(z)一定是true。
- 一致性(consistent)。對于任意不為null的引用值x和y,如果用于equals比較的對象信息沒有被修改的話,多次調用時x.equals(y)要么一致地返回true要么一致地返回false。
- 對于任意不為null的引用值x,x.equals(null)返回false。
對于Object類來說,equals()方法在對象上實現的是差別可能性最大的等價關系,即,對于任意非null的引用值x和y,當且僅當x和y引用的是同一個對象,該方法才會返回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 imghotspot中的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內存模型主要目標是定義程序中各個變量的訪問規則,即在虛擬機中將變量存儲到內存和從內存中取出變量這樣的底層細節。模型圖如下:
imgjava并發內存模型以及內存操作規則
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等
也可以直接使用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 方法將會主要完成以下工作:
ContextConfig 的 init 方法完成后,Context 容器的會執行 startInternal 方法,這個方法啟動邏輯比較復雜,主要包括如下幾個部分:
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 正常工作:
第一種情況下,當瀏覽器不支持 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:?anna@mailserver.com
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()方法被調用之前狀態相同。如果沒有,下一個讀入緩沖區的數據可能無法讀到正確的位置。這是不可能的,但卻是需要注意的又一問題。
如果緩沖區已滿,它可以被處理。如果它不滿,并且在你的實際案例中有意義,你或許能處理其中的部分數據。但是許多情況下并非如此。下圖展示了“緩沖區數據循環就緒”:
img3) 用來處理數據的線程數
?
NIO可讓您只使用一個(或幾個)單線程管理多個通道(網絡連接或文件),但付出的代價是解析數據可能會比從一個阻塞流中讀取數據更復雜。
如果需要管理同時打開的成千上萬個連接,這些連接每次只是發送少量的數據,例如聊天服務器,實現NIO的服務器可能是一個優勢。同樣,如果你需要維持許多打開的連接到其他計算機上,如P2P網絡中,使用一個單獨的線程來管理你所有出站連接,可能是一個優勢。一個線程多個連接的設計方案如
imgJava NIO: 單線程管理多個連接
如果你有少量的連接使用非常高的帶寬,一次發送大量的數據,也許典型的IO服務器實現可能非常契合。下圖說明了一個典型的IO服務器設計:
imgJava 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,做這個優化的目的是提高不同區間訪問的性能。
什么時候使用索引:
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作為一種可靠傳輸控制協議,其核心思想:既要保證數據可靠傳輸,又要提高傳輸的效率,而用三次恰恰可以滿足以上兩方面的需求!****雙方都需要確認自己的發信和收信功能正常,收信功能通過接收對方信息得到確認,發信功能需要發出信息—>對方回復信息得到確認。
三次握手過程:
TCP工作在網絡OSI的七層模型中的第四層——Transport層,IP在第三層——Network層
�ARP在第二層——Data Link層;在第二層上的數據,我們把它叫Frame,在第三層上的數據叫Packet,第四層的數據叫Segment。
四次揮手過程:
為什么建立連接是三次握手
這是因為服務端在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的區別
ConcurrentHashMap是使用了鎖分段技術技術來保證線程安全的,鎖分段技術:首先將數據分成一段一段的存儲,然后給每一段數據配一把鎖,當一個線程占用鎖訪問其中一個段數據的時候,其他段的數據也能被其他線程訪問
ConcurrentHashMap 是在每個段(segment)中線程安全的
LinkedHashMap維護一個雙鏈表,可以將里面的數據按寫入的順序讀出
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 的內部工作原理
進程間的通信方式
3.信號量( semophore ) : 信號量是一個計數器,可以用來控制多個進程對共享資源的訪問。它常作為一種鎖機制,防止某進程正在訪問共享資源時,其他進程也訪問該資源。因此,主要作為進程間以及同一進程內不同線程之間的同步手段。
5.信號 ( sinal ) : 信號是一種比較復雜的通信方式,用于通知接收進程某個事件已經發生。
6.共享內存( shared memory ) :共享內存就是映射一段能被其他進程所訪問的內存,這段共享內存由一個進程創建,但多個進程都可以訪問。共享內存是最快的 IPC 方式,它是針對其他進程間通信方式運行效率低而專門設計的。它往往與其他通信機制,如信號量,配合使用,來實現進程間的同步和通信。
7.套接字( socket ) : 套解口也是一種進程間通信機制,與其他通信機制不同的是,它可用于不同機器間的進程通信。
死鎖的必要條件
解決死鎖,第一個是死鎖預防,就是不讓上面的四個條件同時成立。二是,合理分配資源。
三是使用銀行家算法,如果該進程請求的資源操作系統剩余量可以滿足,那么就分配。
轉載于:https://www.cnblogs.com/lenglangjin/p/10531240.html
總結
以上是生活随笔為你收集整理的java后台常见问题的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 网络爬虫的分析算法
- 下一篇: 虚拟化(4)_离线克隆与在线克隆