最近有很多粉絲問我,有什麼方法能夠快速提升自己,通過阿里、騰訊、位元組跳動、京東等網際網路大廠的面試,我覺得短時間提升自己最快的手段就是背面試題,最近總結了Java常用的面試題,分享給大家,希望大家都能圓夢大廠,加油,我命由我不由天。
目錄
4、java 中的 Math.round(-1.5) 等於多少?
6、String str="i"與 String str=new String(「i」)一樣嗎?
9、new String("a") + new String("b") 會建立幾個物件?
19、為什麼要使用克隆?如何實現物件克隆?深拷貝和淺拷貝區別是什麼?
21、final、finally、finalize 有什麼區別?
22、try-catch-finally 中,如果 catch 中 return 了,finally 還會執行嗎?
27、在 Java 中,為什麼不允許從靜態方法中存取非靜態變數?
36、Collection 和 Collections 有什麼區別?
42、ArrayList 和 LinkedList 的區別是什麼?
44、在 Queue 中 poll()和 remove()有什麼區別?
48、Iterator 和 ListIterator 有什麼區別?
51、Java8開始ConcurrentHashMap,為什麼捨棄分段鎖?
52、ConcurrentHashMap(JDK1.8)為什麼要使用synchronized而不是如ReentranLock這樣的可重入鎖?
53、concurrentHashMap和HashTable有什麼區別
55、請談談 ReadWriteLock 和 StampedLock
57、為什麼我們呼叫 start() 方法時會執行 run() 方法,為什麼我們不能直接呼叫 run() 方法?
60、為什麼 wait(), notify()和 notifyAll()必須在同步方法或者同步塊中被呼叫?
64、請談談 volatile 有什麼特點,為什麼它能保證變數對所有執行緒的可見性?
65、為什麼說 Synchronized 是一個悲觀鎖?樂觀鎖的實現原理又是什麼?什麼是 CAS,它有什麼特性?
67、請儘可能詳盡地對比下 Synchronized 和 ReentrantLock 的異同。
70、跟 Synchronized 相比,可重入鎖 ReentrantLock 其實現原理有什麼不同?
77、執行緒池中的執行緒是怎麼建立的?是一開始就隨著執行緒池的啟動建立好的嗎?
79、既然 volatile 能夠保證執行緒間的變數可見性,是不是就意味著基於 volatile 變數的運算就是並行安全的?
81、請談談 ThreadLocal 是怎麼解決並行安全的?
82、很多人都說要慎用 ThreadLocal,談談你的理解,使用 ThreadLocal 需要注意些什麼?
85、多執行緒中 synchronized 鎖升級的原理是什麼?
86、synchronized 和 ReentrantLock 區別是什麼?
87、Java Concurrency API 中的 Lock 介面(Lock interface)是什麼?對比同步它有什麼優勢?
97、http 響應碼 301 和 302 代表的是什麼?有什麼區別?
108、什麼是設計模式?你是否在你的程式碼裡面使用過任何設計模式?
109、Java 中什麼叫單例設計模式?請用 Java 寫出執行緒安全的單例模式
110、在 Java 中,什麼叫觀察者設計模式(observer design pattern)?
113、舉一個用 Java 實現的裝飾模式(decorator design pattern)?它是作用於物件層次還是類層次?
114、什麼是 Spring 框架?Spring 框架有哪些主要模組?
118、BeanFactory 和 ApplicationContext 有什麼區別?
123、Spring Bean 的作用域之間有什麼區別?Spring容器中的bean可以分為5個範圍:
124、如何在 Spring Boot 中禁用 Actuator 端點安全性?
126、Spring 框架中的單例 Beans 是執行緒安全的麼?
130、spring mvc 和 struts 的區別是什麼?
137、請舉例說明如何在 Spring 中注入一個 Java Collection?
139、mybatis 是否支援延遲載入?延遲載入的原理是什麼?
142、mybatis 和 hibernate 的區別有哪些?
146、hibernate 中如何在控制檯檢視列印的 sql 語句?
148、hibernate 實體類可以被定義為 final 嗎?
149、在 hibernate 中使用 Integer 和 int 做對映有什麼區別?
150、什麼是 Spring Boot?Spring Boot 有哪些優點?
161、什麼是 Swagger?你用 Spring Boot 實現了它嗎?
163、一張自增表裡面總共有 7 條資料,刪除了最後 2 條資料,重新啟動 mysql 資料庫,又插入了一條資料,此時 id 是幾?
177、RabbitMQ有哪些重要的角色?有哪些重要的元件?
189、新生代垃圾回收器和老生代垃圾回收器都有哪些?有什麼區別?
JDK(Java Development Kit),Java開發工具包
JRE(Java Runtime Environment),Java執行環境
JDK中包含JRE,JDK中有一個名為jre的目錄,裡面包含兩個資料夾bin和lib,bin就是JVM,lib就是JVM工作所需要的類庫。
Math提供了三個與取整有關的方法:ceil、floor、round
(1)ceil:向上取整;
Math.ceil(11.3) = 12;
Math.ceil(-11.3) = 11;
(2)floor:向下取整;
Math.floor(11.3) = 11;
Math.floor(-11.3) = -12;
(3)round:四捨五入;
加0.5然後向下取整。
Math.round(11.3) = 11;
Math.round(11.8) = 12;
Math.round(-11.3) = -11;
Math.round(-11.8) = -12;
不屬於。
八種基本資料型別:byte、short、char、int、long、double、float、boolean。
String str="i"會將起分配到常數池中,常數池中沒有重複的元素,如果常數池中存中i,就將i的地址賦給變數,如果沒有就建立一個再賦給變數。
String str=new String(「i」)會將物件分配到堆中,即使記憶體一樣,還是會重新建立一個新的物件。
將物件封裝到stringBuilder中,呼叫reverse方法反轉。
(1)常見String類的獲取功能
length:獲取字串長度;
charAt(int index):獲取指定索引位置的字元;
indexOf(int ch):返回指定字元在此字串中第一次出現處的索引;
substring(int start):從指定位置開始擷取字串,預設到末尾;
substring(int start,int end):從指定位置開始到指定位置結束擷取字串;
(2)常見String類的判斷功能
equals(Object obj): 比較字串的內容是否相同,區分大小寫;
contains(String str): 判斷字串中是否包含傳遞進來的字串;
startsWith(String str): 判斷字串是否以傳遞進來的字串開頭;
endsWith(String str): 判斷字串是否以傳遞進來的字串結尾;
isEmpty(): 判斷字串的內容是否為空串"";
(3)常見String類的轉換功能
byte[] getBytes(): 把字串轉換為位元組陣列;
char[] toCharArray(): 把字串轉換為字元陣列;
String valueOf(char[] chs): 把字元陣列轉成字串。valueOf可以將任意型別轉為字串;
toLowerCase(): 把字串轉成小寫;
toUpperCase(): 把字串轉成大寫;
concat(String str): 把字串拼接;
(4)常見String類的其他常用功能
replace(char old,char new) 將指定字元進行互換
replace(String old,String new) 將指定字串進行互換
trim() 去除兩端空格
int compareTo(String str) 會對照ASCII 碼錶 從第一個字母進行減法運算 返回的就是這個減法的結果,如果前面幾個字母一樣會根據兩個字串的長度進行減法運算返回的就是這個減法的結果,如果連個字串一摸一樣 返回的就是0。
物件1:new StringBuilder()
物件2:new String("a")
物件3:常數池中的"a"
物件4:new String("b")
物件5:常數池中的"b"
深入剖析:StringBuilder中的toString():
物件6:new String("ab")
強調一下,toString()的呼叫,在字串常數池中,沒有生成"ab"
附加題
String s1 = new String("1") + new String("1");//s1變數記錄的地址為:new String
s1.intern();//在字串常數池中生成"11"。如何理解:jdk6:建立了一個新的物件"11",也就有新的地址;jdk7:此時常數池中並沒有建立"11",而是建立了一個指向堆空間中new String("11")的地址;
String s2 = "11";
System.out.println(s1 == s2);//jdk6:false;jdk7:true
新增到StringBuilder中,然後呼叫reverse()。
equals、length、contains、replace、split、hashcode、indexof、substring、trim、toUpperCase、toLowerCase、isEmpty等等。
抽象類不能被範例化;
抽象類可以有抽象方法,只需申明,無須實現;
有抽象方法的類一定是抽象類;
抽象類的子類必須實現抽象類中的所有抽象方法,否則子類仍然是抽象類;
抽象方法不能宣告為靜態、不能被static、final修飾。
(1)介面
介面使用interface修飾;
介面不能範例化;
類可以實現多個介面;
①java8之前,介面中的方法都是抽象方法,省略了public abstract。②java8之後;介面中可以定義靜態方法,靜態方法必須有方法體,普通方法沒有方法體,需要被實現;
(2)抽象類
抽象類使用abstract修飾;
抽象類不能被範例化;
抽象類只能單繼承;
抽象類中可以包含抽象方法和非抽象方法,非抽象方法需要有方法體;
如果一個類繼承了抽象類,①如果實現了所有的抽象方法,子類可以不是抽象類;②如果沒有實現所有的抽象方法,子類仍然是抽象類。
(1)按流劃分,可以分為輸入流和輸出流;
(2)按單位劃分,可以分為位元組流和字元流;
位元組流:inputStream、outputStream;
字元流:reader、writer;
(1)同步阻塞BIO
一個連線一個執行緒。
JDK1.4之前,建立網路連線的時候採用BIO模式,先在啟動伺服器端socket,然後啟動使用者端socket,對伺服器端通訊,使用者端傳送請求後,先判斷伺服器端是否有執行緒響應,如果沒有則會一直等待或者遭到拒絕請求,如果有的話會等待請求結束後才繼續執行。
(2)同步非阻塞NIO
NIO主要是想解決BIO的大並行問題,BIO是每一個請求分配一個執行緒,當請求過多時,每個執行緒佔用一定的記憶體空間,伺服器癱瘓了。
JDK1.4開始支援NIO,適用於連線數目多且連線比較短的架構,比如聊天伺服器,並行侷限於應用中。
一個請求一個執行緒。
(3)非同步非阻塞AIO
一個有效請求一個執行緒。
JDK1.7開始支援AIO,適用於連線數目多且連線比較長的結構,比如相簿伺服器,充分呼叫OS參與並行操作。
exist
createFile
createDirectory
write
read
copy
size
delete
move
所謂反射,是java在執行時進行自我觀察的能力,通過class、constructor、field、method四個方法獲取一個類的各個組成部分。
在Java執行時環境中,對任意一個類,可以知道類有哪些屬性和方法。這種動態獲取類的資訊以及動態呼叫物件的方法的功能來自於反射機制。
序列化就是一種用來處理物件流的機制。將物件的內容流化,將流化後的物件傳輸於網路之間。
序列化是通過實現serializable介面,該介面沒有需要實現的方法,implement Serializable只是為了標註該物件是可被序列化的,使用一個輸出流(FileOutputStream)來構造一個ObjectOutputStream物件,接著使用ObjectOutputStream物件的writeObejct(Object object)方法就可以將引數的obj物件到磁碟,需要恢復的時候使用輸入流。
序列化是將物件轉換為容易傳輸的格式的過程。
例如,可以序列化一個物件,然後通過HTTP通過Internet在使用者端和伺服器之間傳輸該物件。在另一端,反序列化將從流中心構造成物件。
一般程式在執行時,產生物件,這些物件隨著程式的停止而消失,但我們想將某些物件儲存下來,這時,我們就可以通過序列化將物件儲存在磁碟,需要使用的時候通過反序列化獲取到。
物件序列化的最主要目的就是傳遞和儲存物件,儲存物件的完整性和可傳遞性。
譬如通過網路傳輸或者把一個物件儲存成本地一個檔案的時候,需要使用序列化。
(1)什麼要使用克隆?
想對一個物件進行復制,又想保留原有的物件進行接下來的操作,這個時候就需要克隆了。
(2)如何實現物件克隆?
實現Cloneable介面,重寫clone方法;
實現Serializable介面,通過物件的序列化和反序列化實現克隆,可以實現真正的深克隆。
BeanUtils,apache和Spring都提供了bean工具,只是這都是淺克隆。
(3)深拷貝和淺拷貝區別是什麼?
淺拷貝:僅僅克隆基本型別變數,不克隆參照型別變數;
深克隆:既克隆基本型別變數,又克隆參照型別變數;
(4)程式碼範例
(1)throw
作用在方法內,表示丟擲具體異常,由方法體內的語句處理;
一定丟擲了異常;
(2)throws
作用在方法的宣告上,表示丟擲異常,由呼叫者來進行例外處理;
可能出現異常,不一定會發生異常;
final可以修飾類,變數,方法,修飾的類不能被繼承,修飾的變數不能重新賦值,修飾的方法不能被重寫
finally用於拋異常,finally程式碼塊內語句無論是否發生異常,都會在執行finally,常用於一些流的關閉。
finalize方法用於垃圾回收。
一般情況下不需要我們實現finalize,當物件被回收的時候需要釋放一些資源,比如socket連結,在物件初始化時建立,整個生命週期內有效,那麼需要實現finalize方法,關閉這個連結。
但是當呼叫finalize方法後,並不意味著gc會立即回收該物件,所以有可能真正呼叫的時候,物件又不需要回收了,然後到了真正要回收的時候,因為之前呼叫過一次,這次又不會呼叫了,產生問題。所以,不推薦使用finalize方法。
Java中Object有一個方法:
public native int hashcode();
(1)hashcode()方法的作用
hashcode()方法主要配合基於雜湊的集合一起使用,比如HashSet、HashMap、HashTable。
當集合需要新增新的物件時,先呼叫這個物件的hashcode()方法,得到對應的hashcode值,實際上hashmap中會有一個table儲存已經存進去的物件的hashcode值,如果table中沒有改hashcode值,則直接存入,如果有,就呼叫equals方法與新元素進行比較,相同就不存了,不同就存入。
(2)equals和hashcode的關係
如果equals為true,hashcode一定相等;
如果equals為false,hashcode不一定不相等;
如果hashcode值相等,equals不一定相等;
如果hashcode值不等,equals一定不等;
(3)重寫equals方法時,一定要重寫hashcode方法
(4)百度百科
hashcode方法返回該物件的雜湊碼值。支援該方法是為雜湊表提供一些優點,例如,java.util.Hashtable 提供的雜湊表。
hashCode 的常規協定是:
在 Java 應用程式執行期間,在同一物件上多次呼叫 hashCode 方法時,必須一致地返回相同的整數,前提是物件上 equals 比較中所用的資訊沒有被修改。從某一應用程式的一次執行到同一應用程式的另一次執行,該整數無需保持一致。
如果根據 equals(Object) 方法,兩個物件是相等的,那麼在兩個物件中的每個物件上呼叫 hashCode 方法都必須生成相同的整數結果。
以下情況不 是必需的:如果根據 equals(java.lang.Object) 方法,兩個物件不相等,那麼在兩個物件中的任一物件上呼叫 hashCode 方法必定會生成不同的整數結果。但是,程式設計師應該知道,為不相等的物件生成不同整數結果可以提高雜湊表的效能。
實際上,由 Object 類定義的 hashCode 方法確實會針對不同的物件返回不同的整數。(這一般是通過將該物件的內部地址轉換成一個整數來實現的,但是 JavaTM 程式語言不需要這種實現技巧。)當equals方法被重寫時,通常有必要重寫 hashCode 方法,以維護 hashCode 方法的常規協定,該協定宣告相等物件必須具有相等的雜湊碼。
(5)小白解釋
1.hashcode是用來查詢的,如果你學過資料結構就應該知道,在查詢和排序這一章有
例如記憶體中有這樣的位置
0 1 2 3 4 5 6 7
而我有個類,這個類有個欄位叫ID,我要把這個類存放在以上8個位置之一,如果不用hashcode而任意存放,那麼當查詢時就需要到這八個位置裡挨個去找,或者用二分法一類的演演算法。
但如果用hashcode那就會使效率提高很多。
我們這個類中有個欄位叫ID,那麼我們就定義我們的hashcode為ID%8,然後把我們的類存放在取得得餘數那個位置。比如我們的ID為9,9除8的餘數為1,那麼我們就把該類存在1這個位置,如果ID是13,求得的餘數是5,那麼我們就把該類放在5這個位置。這樣,以後在查詢該類時就可以通過ID除 8求餘數直接找到存放的位置了。2.但是如果兩個類有相同的hashcode怎麼辦那(我們假設上面的類的ID不是唯一的),例如9除以8和17除以8的餘數都是1,那麼這是不是合法的,回答是:可以這樣。那麼如何判斷呢?在這個時候就需要定義 equals了。
也就是說,我們先通過 hashcode來判斷兩個類是否存放某個桶裡,但這個桶裡可能有很多類,那麼我們就需要再通過 equals 來在這個桶裡找到我們要的類。
那麼。重寫了equals(),為什麼還要重寫hashCode()呢?
想想,你要在一個桶裡找東西,你必須先要找到這個桶啊,你不通過重寫hashcode()來找到桶,光重寫equals()有什麼用啊。
(1)String
String是不可變物件,每次對String型別的改變時都會生成一個新的物件。
(2)StringBuilder
執行緒不安全,效率高,多用於單執行緒。
(3)StringBuffer
執行緒安全,由於加鎖的原因,效率不如StringBuilder,多用於多執行緒。
不頻繁的字串操作使用String,操作頻繁的情況不建議使用String。
StringBuilder > StringBuffer > String。
(1)強參照
Java中預設宣告的就是強參照,比如:
Object obj = new Object();
obj = null;
只要強參照存在,垃圾回收器將永遠不會回收被參照的物件。如果想被回收,可以將物件置為null;
(2)軟參照(SoftReference)
在記憶體足夠的時候,軟參照不會被回收,只有在記憶體不足時,系統才會回收軟參照物件,如果回收了軟參照物件之後仍然沒有足夠的記憶體,才會跑出記憶體溢位異常。
byte[] buff = new byte[1024 * 1024];
SoftReference<byte[]> sr = new SoftReference<>(buff);
(3)弱參照(WeakReference)
進行垃圾回收時,弱參照就會被回收。
(4)虛參照(PhantomReference)
(5)參照佇列(ReferenceQueue)
參照佇列可以與軟參照、弱參照、虛參照一起配合使用。
當垃圾回收器準備回收一個物件時,如果發現它還有參照,就會在回收物件之前,把這個參照加入到參照佇列中。
程式可以通過判斷參照佇列中是否加入了參照,來判斷被參照的物件是否將要被垃圾回收,這樣可以在物件被回收之前採取一些必要的措施。
屬性為布林型別,可以使用 isXxx() 方法代替 getXxx() 方法。
通常屬性名是要和 包名、類名、方法名、欄位名、常數名作出區別的:
首先:必須用英文,不要用漢語拼音
(1)包(package)
用於將完成不同功能的類分門別類,放在不同的目錄(包)下,包的命名規則:將公司域名反轉作為包名。比如www.sohu.com 對於包名:每個字母都需要小寫。比如:com.sohu.test;該包下的Test類的全名是:com.sohu.Test.Java 。
如果定義類的時候沒有使用package,那麼java就認為我們所定義的類位於預設包裡面(default package)。
(2)類
首字母大寫,如果一個類由多個單詞構成,那麼每個單詞的首字母都大寫,而且中間不使用任何的連線符。儘量使用英文。如ConnectionFactory
(3)方法
首單詞全部小寫,如果一個方法由多個單詞構成,那麼從第二個單詞開始首字母大寫,不使用連線符。addPerson
(4)欄位
與方法相同。如ageOfPerson
(5)常數
所有單詞的字母都是大寫,如果有多個單詞,那麼使用下劃線連結即可。
如:public static final int AGE_OF_PERSON = 20; //通常加上static
public class User {
private String busName;
private String pCount;
private Boolean isRunning;
//正確的命名方式,駝峰式的
public String getBusName() {
return busName;
}
public void setBusName(String busName) {
this.busName = busName;
}
//這是什麼?
public String getpCount() {
return pCount;
}
public void setpCount(String pCount) {
this.pCount = pCount;
}
//這個也是不允許的
public Boolean getIsRunning() {
return isRunning;
}
public void setIsRunning(Boolean isRunning) {
this.isRunning = isRunning;
}
}
1. javabean屬性命名儘量使用常規的駝峰式命名規則
2. 屬性名第一個單詞儘量避免使用一個字母:如eBook, eMail。
3. boolean屬性名避免使用 「is」 開頭的名稱
4. 隨著jdk, eclipse, spring 等軟體版本的不斷提高, 底版本的出現的問題可能在高版本中解決了, 低版本原來正常的程式碼可能在高版本環境下不再支援。
在瞭解什麼是 Java 記憶體模型之前,先了解一下為什麼要提出 Java 記憶體模型。
之前提到過並行程式設計有三大問題
CPU 快取,在多核 CPU 的情況下,帶來了可見性問題
作業系統對當前執行執行緒的切換,帶來了原子性問題
譯器指令重排優化,帶來了有序性問題
為了解決並行程式設計的三大問題,提出了 JSR-133,新的 Java 記憶體模型,JDK 5 開始使用。
簡單總結下
Java 記憶體模型是 JVM 的一種規範
定義了共用記憶體在多執行緒程式中讀寫操作行為的規範
遮蔽了各種硬體和作業系統的存取差異,保證了 Java 程式在各種平臺下對記憶體的存取效果一致
解決並行問題採用的方式:限制處理器優化和使用記憶體屏障
增強了三個同步原語(synchronized、volatile、final)的記憶體語意
定義了 happens-before 規則
(1)過載是多型的集中體現,在類中,要以統一的方式處理不同型別資料的時候,可以用過載。
(2)重寫的使用是建立在繼承關係上的,子類在繼承父類別的基礎上,增加新的功能,可以用重寫。
(3)簡單總結:
過載是多樣性,重寫是增強劑;
目的是提高程式的多樣性和健壯性,以適配不同場景使用時,使用過載進行擴充套件;
目的是在不修改原方法及原始碼的基礎上對方法進行擴充套件或增強時,使用重寫;
生活例子:
你想吃一碗麵,我給你提供了拉麵,炒麵,刀削麵,擔擔麵供你選擇,這是過載;
你想吃一碗麵,我不但給你端來了面,還給你加了青菜,加了雞蛋,這個是重寫;
設計模式:
cglib實現動態代理,核心原理用的就是方法的重寫;
詳細解答:
Java的過載(overload) 最重要的應用場景就是構造器的過載,構造器過載後,提供多種形參形式的構造器,可以應對不同的業務需求,加強程式的健壯性和可延伸性,比如我們最近學習的Spring原始碼中的ClassPathXmlApplicationContext,它的建構函式使用過載一共提供了10個建構函式,這樣就為業務的選擇提供了多選擇性。在應用到方法中時,主要是為了增強方法的健壯性和可延伸性,比如我們在開發中常用的各種工具類,比如我目前工作中的簡訊工具類SMSUtil, 傳簡訊的方法就會使用過載,針對不同業務場景下的不同形參,提供簡訊傳送方法,這樣提高了工具類的擴充套件性和健壯性。
總結:過載必須要修改方法(構造器)的形參列表,可以修改方法的返回值型別,也可以修改方法的異常資訊即存取許可權;使用範圍是在同一個類中,目的是對方法(構造器)進行功能擴充套件,以應對多業務場景的不同使用需求。提高程式的健壯性和擴充套件性。
java的重寫(override) 只要用於子類對父類別方法的擴充套件或修改,但是在我們開發中,為了避免程式混亂,重寫一般都是為了方法的擴充套件,比如在cglib方式實現的動態代理中,代理類就是繼承了目標類,對目標類的方法進行重寫,同時在方法前後進行切面織入。
總結:方法重寫時,參數列,返回值得型別是一定不能修改的,異常可以減少或者刪除,但是不能丟擲新的異常或者更廣的異常,方法的存取許可權可以降低限制,但是不能做更嚴格的限制。
(4)在里氏替換原則中,子類對父類別的方法儘量不要重寫和過載。(我們可以採用final的手段強制來遵循)
介面和抽象類都遵循」面向介面而不是實現編碼」設計原則,它可以增加程式碼的靈活性,可以適應不斷變化的需求。下面有幾個點可以幫助你回答這個問題:在 Java 中,你只能繼承一個類,但可以實現多個介面。所以一旦你繼承了一個類,你就失去了繼承其他類的機會了。
介面通常被用來表示附屬描述或行為如: Runnable 、 Clonable 、 Serializable 等等,因此當你使用抽象類來表示行為時,你的類就不能同時是 Runnable 和 Clonable( 注:這裡的意思是指如果把 Runnable 等實現為抽象類的情況 ) ,因為在 Java 中你不能繼承兩個類,但當你使用介面時,你的類就可以同時擁有多個不同的行為。
在一些對時間要求比較高的應用中,傾向於使用抽象類,它會比介面稍快一點。如果希望把一系列行為都規範在類繼承層次內,並且可以更好地在同一個地方進行編碼,那麼抽象類是一個更好的選擇。有時,介面和抽象類可以一起使用,介面中定義函數,而在抽象類中定義預設的實現。
//用 Class.forName方法獲取類,在呼叫類的newinstance()方法
Class<?> cls = Class.forName("com.dao.User");
User u = (User)cls.newInstance();
//將一個物件範例化後,進行序列化,再反序列化,也可以獲得一個物件(遠端通訊的場景下使用)
ObjectOutputStream out = new ObjectOutputStream (new FileOutputStream("D:/data.txt"));
//序列化物件
out.writeObject(user1);
out.close();
//反序列化物件
ObjectInputStream in = new ObjectInputStream(new FileInputStream("D:/data.txt"));
User user2 = (User) in.readObject();
System.out.println("反序列化user:" + user2);
in.close();
byte的範圍是-128~127。
位元組長度為8位元,最左邊的是符號位,而127的二進位制為01111111,所以執行+1操作時,01111111變為10000000。
大家知道,計算機中儲存負數,存的是二補數的興衰。左邊第一位為符號位。
那麼負數的二補數轉換成十進位制如下:
一個數如果為正,則它的原碼、反碼、二補數相同;一個正數的二補數,將其轉化為十進位制,可以直接轉換。
已知一個負數的二補數,將其轉換為十進位制數,步驟如下:
例如10000000,最高位是1,是負數,①對各位取反得01111111,轉換為十進位制就是127,加上負號得-127,再減去1得-128;
(1)Collection
① set
HashSet、TreeSet
② list
ArrayList、LinkedList、Vector
(2)Map
HashMap、HashTable、TreeMap
(1)Collection是最基本的集合介面,Collection派生了兩個子介面list和set,分別定義了兩種不同的儲存方式。
(2)Collections是一個包裝類,它包含各種有關集合操作的靜態方法(對集合的搜尋、排序、執行緒安全化等)。
此類不能範例化,就像一個工具類,服務於Collection框架。
(1)List簡介
實際上有兩種List:一種是基本的ArrayList,其優點在於隨機存取元素,另一種是LinkedList,它並不是為快速隨機存取設計的,而是快速的插入或刪除。
ArrayList:由陣列實現的List。允許對元素進行快速隨機存取,但是向List中間插入與移除元素的速度很慢。
LinkedList :對順序存取進行了優化,向List中間插入與刪除的開銷並不大。隨機存取則相對較慢。
還具有下列方 法:addFirst(), addLast(), getFirst(), getLast(), removeFirst() 和 removeLast(), 這些方法 (沒有在任何介面或基礎類別中定義過)使得LinkedList可以當作堆疊、佇列和雙向佇列使用。
(2)Set簡介
Set具有與Collection完全一樣的介面,因此沒有任何額外的功能。實際上Set就是Collection,只是行為不同。這是繼承與多型思想的典型應用:表現不同的行為。Set不儲存重複的元素(至於如何判斷元素相同則較為負責)
Set : 存入Set的每個元素都必須是唯一的,因為Set不儲存重複元素。加入Set的元素必須定義equals()方法以確保物件的唯一性。Set與Collection有完全一樣的介面。Set介面不保證維護元素的次序。
HashSet:為快速查詢設計的Set。存入HashSet的物件必須定義hashCode()。
TreeSet: 儲存次序的Set, 底層為樹結構。使用它可以從Set中提取有序的序列。
(3)list與Set區別
① List,Set都是繼承自Collection介面
② List特點:元素有放入順序,元素可重複 ,Set特點:元素無放入順序,元素不可重複,重複元素會覆蓋掉,(元素雖然無放入順序,但是元素在set中的位置是有該元素的HashCode決定的,其位置其實是固定的,加入Set 的Object必須定義equals()方法 ,另外list支援for迴圈,也就是通過下標來遍歷,也可以用迭代器,但是set只能用迭代,因為他無序,無法用下標來取得想要的值。)
③ Set和List對比:
Set:檢索元素效率低下,刪除和插入效率高,插入和刪除不會引起元素位置改變。
List:和陣列類似,List可以動態增長,查詢元素效率高,插入刪除元素效率低,因為會引起其他元素位置改變。
(1)簡介
HashMap基於map介面,元素以鍵值對方式儲存,允許有null值,HashMap是執行緒不安全的。
(2)基本屬性
初始化大小,預設16,2倍擴容;
負載因子0.75;
初始化的預設陣列;
size
threshold。判斷是否需要調整hashmap容量
(3)HashMap的儲存結構
JDK1.7中採用陣列+連結串列的儲存形式。
HashMap採取Entry陣列來儲存key-value,每一個鍵值對組成了一個Entry實體,Entry類時機上是一個單向的連結串列結構,它具有next指標,指向下一個Entry實體,以此來解決Hash衝突的問題。
HashMap實現一個內部類Entry,重要的屬性有hash、key、value、next。
JDK1.8中採用資料+連結串列+紅黑樹的儲存形式。當連結串列長度超過閾值(8)時,將連結串列轉換為紅黑樹。在效能上進一步得到提升。
(1)HashSet
HashSet是set介面的實現類,set下面最主要的實現類就是HashSet(也就是用的最多的),此外還有LinkedHashSet和TreeSet。
HashSet是無序的、不可重複的。通過物件的hashCode和equals方法保證物件的唯一性。
HashSet內部的儲存結構是雜湊表,是執行緒不安全的。
(2)TreeSet
TreeSet對元素進行排序的方式:
元素自身具備比較功能,需要實現Comparable介面,並覆蓋compareTo方法。
元素自身不具備比較功能,需要實現Comparator介面,並覆蓋compare方法。
(3)LinkedHashSet
LinkedHashSet是一種有序的Set集合,即其元素的存入和輸出的順序是相同的。
HashSet實際上是一個HashMap範例,資料儲存結構都是陣列+連結串列。
HashSet是基於HashMap實現的,HashSet中的元素都存放在HashMap的key上面,而value都是一個統一的物件PRESENT。
private static final Object PRESENT = new Object();
HashSet中add方法呼叫的是底層HashMap中的put方法,put方法要判斷插入值是否存在,而HashSet的add方法,首先判斷元素是否存在,如果存在則插入,如果不存在則不插入,這樣就保證了HashSet中不存在重複值。
通過物件的hashCode和equals方法保證物件的唯一性。
ArrayList是動態陣列的資料結構實現,查詢和遍歷的效率較高;
LinkedList 是雙向連結串列的資料結構,增加和刪除的效率較高;
String[] arr = {"zs","ls","ww"};
List<String> list = Arrays.asList(arr);
System.out.println(list);
ArrayList<String> list1 = new ArrayList<String>();
list1.add("張三");
list1.add("李四");
list1.add("王五");
String[] arr1 = list1.toArray(new String[list1.size()]);
System.out.println(arr1);
for(int i = 0; i < arr1.length; i++){
System.out.println(arr1[i]);
}
(1)offer()和add()區別:
增加新項時,如果佇列滿了,add會丟擲異常,offer返回false。
(2)poll()和remove()區別:
poll()和remove()都是從佇列中刪除第一個元素,remove丟擲異常,poll返回null。
(3)peek()和element()區別:
peek()和element()用於查詢佇列頭部元素,為空時element丟擲異常,peek返回null。
Vector:就比Arraylist多了個同步化機制(執行緒安全)。
Stack:棧,也是執行緒安全的,繼承於Vector。
Hashtable:就比Hashmap多了個執行緒安全。
ConcurrentHashMap:是一種高效但是執行緒安全的集合。
為了方便的處理集合中的元素,Java中出現了一個物件,該物件提供了一些方法專門處理集合中的元素.例如刪除和獲取集合中的元素.該物件就叫做迭代器(Iterator)。
Iterator 介面原始碼中的方法:
(1)ListIterator 繼承 Iterator
(2)ListIterator 比 Iterator多方法
(3)使用範圍不同,Iterator可以迭代所有集合;ListIterator 只能用於List及其子類
我們很容易想到用final關鍵字進行修飾,我們都知道
final關鍵字可以修飾類,方法,成員變數,final修飾的類不能被繼承,final修飾的方法不能被重寫,final修飾的成員變數必須初始化值,如果這個成員變數是基本資料型別,表示這個變數的值是不可改變的,如果說這個成員變數是參照型別,則表示這個參照的地址值是不能改變的,但是這個參照所指向的物件裡面的內容還是可以改變的。
那麼,我們怎麼確保一個集合不能被修改?首先我們要清楚,集合(map,set,list…)都是參照型別,所以我們如果用final修飾的話,集合裡面的內容還是可以修改的。
我們可以做一個實驗:
可以看到:我們用final關鍵字定義了一個map集合,這時候我們往集合裡面傳值,第一個鍵值對1,1;我們再修改後,可以把鍵為1的值改為100,說明我們是可以修改map集合的值的。
那我們應該怎麼做才能確保集合不被修改呢?
我們可以採用Collections包下的unmodifiableMap方法,通過這個方法返回的map,是不可以修改的。他會報 java.lang.UnsupportedOperationException錯。
同理:Collections包也提供了對list和set集合的方法。
Collections.unmodifiableList(List)
Collections.unmodifiableSet(Set)
(1)佇列先進先出,棧先進後出。
(2)遍歷資料速度不同。
棧只能從頭部取資料 也就最先放入的需要遍歷整個棧最後才能取出來,而且在遍歷資料的時候還得為資料開闢臨時空間,保持資料在遍歷前的一致性;
佇列則不同,他基於地址指標進行遍歷,而且可以從頭或尾部開始遍歷,但不能同時遍歷,無需開闢臨時空間,因為在遍歷的過程中不影像資料結構,速度要快的多。
ConcurrentHashMap的原理是參照了內部的 Segment ( ReentrantLock ) 分段鎖,保證在操作不同段 map 的時候, 可以並行執行, 操作同段 map 的時候,進行鎖的競爭和等待。從而達到執行緒安全, 且效率大於 synchronized。
但是在 Java 8 之後, JDK 卻棄用了這個策略,重新使用了 synchronized+CAS。
棄用原因
通過 JDK 的原始碼和官方檔案看來, 他們認為的棄用分段鎖的原因由以下幾點:
加入多個分段鎖浪費記憶體空間。
生產環境中, map 在放入時競爭同一個鎖的概率非常小,分段鎖反而會造成更新等操作的長時間等待。
為了提高 GC 的效率
新的同步方案
既然棄用了分段鎖, 那麼一定由新的執行緒安全方案, 我們來看看原始碼是怎麼解決執行緒安全的呢?(原始碼保留了segment 程式碼, 但並沒有使用)。
我想從下面幾個角度討論這個問題:
(1)鎖的粒度
首先鎖的粒度並沒有變粗,甚至變得更細了。每當擴容一次,ConcurrentHashMap的並行度就擴大一倍。
(2)Hash衝突
JDK1.7中,ConcurrentHashMap從過二次hash的方式(Segment -> HashEntry)能夠快速的找到查詢的元素。在1.8中通過連結串列加紅黑樹的形式彌補了put、get時的效能差距。
JDK1.8中,在ConcurrentHashmap進行擴容時,其他執行緒可以通過檢測陣列中的節點決定是否對這條連結串列(紅黑樹)進行擴容,減小了擴容的粒度,提高了擴容的效率。
下面是我對面試中的那個問題的一下看法。
為什麼是synchronized,而不是ReentranLock
(1)減少記憶體開銷
假設使用可重入鎖來獲得同步支援,那麼每個節點都需要通過繼承AQS來獲得同步支援。但並不是每個節點都需要獲得同步支援的,只有連結串列的頭節點(紅黑樹的根節點)需要同步,這無疑帶來了巨大記憶體浪費。
(2)獲得JVM的支援
可重入鎖畢竟是API這個級別的,後續的效能優化空間很小。
synchronized則是JVM直接支援的,JVM能夠在執行時作出相應的優化措施:鎖粗化、鎖消除、鎖自旋等等。這就使得synchronized能夠隨著JDK版本的升級而不改動程式碼的前提下獲得效能上的提升。
concurrentHashMap融合了hashmap和hashtable的優勢,hashmap是不同步的,但是單執行緒情況下效率高,hashtable是同步的同步情況下保證程式執行的正確性。
但hashtable每次同步執行的時候都要鎖住整個結構,如下圖:
concurrentHashMap鎖的方式是細粒度的。concurrentHashMap將hash分為16個桶(預設值),諸如get、put、remove等常用操作只鎖住當前需要用到的桶。
concurrentHashMap的讀取並行,因為讀取的大多數時候都沒有鎖定,所以讀取操作幾乎是完全的並行操作,只是在求size時才需要鎖定整個hash。
而且在迭代時,concurrentHashMap使用了不同於傳統集合的快速失敗迭代器的另一種迭代方式,弱一致迭代器。在這種方式中,當iterator被建立後集合再發生改變就不會丟擲ConcurrentModificationException,取而代之的是在改變時new新的資料而不是影響原來的資料,iterator完成後再講頭指標替代為新的資料,這樣iterator時使用的是原來的資料。
(1)先了解一下HashCode
Java中的集合有兩類,一類是List,一類是Set。
List:元素有序,可以重複;
Set:元素無序,不可重複;
要想保證元素的不重複,拿什麼來判斷呢?這就是Object.equals方法了。如果元素有很多,增加一個元素,就要判斷n次嗎?
顯然不現實,於是,Java採用了雜湊表的原理。雜湊演演算法也稱為雜湊演演算法,是將資料依特定演演算法直接指定到一根地址上,初學者可以簡單的理解為,HashCode方法返回的就是物件儲存的物理位置(實際上並不是)。
這樣一來,當集合新增新的元素時,先呼叫這個元素的hashcode()方法,就一下子能定位到他應該放置的物理位置上。如果這個位置上沒有元素,他就可以直接儲存在這個位置上,不用再進行任何比較了。如果這個位置上有元素,就呼叫它的equals方法與新元素進行比較,想同的話就不存了,不相同就雜湊其它的地址。所以這裡存在一個衝突解決的問題。這樣一來實際上呼叫equals方法的次數就大大降低了,幾乎只需要一兩次。
簡而言之,在集合查詢時,hashcode能大大降低物件比較次數,提高查詢效率。
Java物件的equals方法和hashCode方法時這樣規定的:
相等的物件就必須具有相等的hashcode。
如果兩個Java物件A和B,A和B不相等,但是A和B的雜湊碼相等,將A和B都存入HashMap時會發生雜湊衝突,也就是A和B存放在HashMap內部陣列的位置索引相同,這時HashMap會在該位置建立一個連結表,將A和B串起來放在該位置,顯然,該情況不違反HashMap的使用規則,是允許的。當然,雜湊衝突越少越好,儘量採用好的雜湊演演算法避免雜湊衝突。
equals()相等的兩個物件,hashcode()一定相等;equals()不相等的兩個物件,卻並不能證明他們的hashcode()不相等。
(2)HashMap和HashSet的區別
ReadWriteLock包括兩種子鎖
(1)ReadWriteLock
ReadWriteLock 可以實現多個讀鎖同時進行,但是讀與寫和寫於寫互斥,只能有一個寫鎖執行緒在進行。
(2)StampedLock
StampedLock是Jdk在1.8提供的一種讀寫鎖,相比較ReentrantReadWriteLock效能更好,因為ReentrantReadWriteLock在讀寫之間是互斥的,使用的是一種悲觀策略,在讀執行緒特別多的情況下,會造成寫執行緒處於飢餓狀態,雖然可以在初始化的時候設定為true指定為公平,但是吞吐量又下去了,而StampedLock是提供了一種樂觀策略,更好的實現讀寫分離,並且吞吐量不會下降。
StampedLock包括三種鎖:
(1)寫鎖writeLock:
writeLock是一個獨佔鎖寫鎖,當一個執行緒獲得該鎖後,其他請求讀鎖或者寫鎖的執行緒阻塞, 獲取成功後,會返回一個stamp(憑據)變數來表示該鎖的版本,在釋放鎖時呼叫unlockWrite方法傳遞stamp引數。提供了非阻塞式獲取鎖tryWriteLock。
(2)悲觀讀鎖readLock:
readLock是一個共用讀鎖,在沒有執行緒獲取寫鎖情況下,多個執行緒可以獲取該鎖。如果有寫鎖獲取,那麼其他執行緒請求讀鎖會被阻塞。悲觀讀鎖會認為其他執行緒可能要對自己操作的資料進行修改,所以需要先對資料進行加鎖,這是在讀少寫多的情況下考慮的。請求該鎖成功後會返回一個stamp值,在釋放鎖時呼叫unlockRead方法傳遞stamp引數。提供了非阻塞式獲取鎖方法tryWriteLock。
(3)樂觀讀鎖tryOptimisticRead:
tryOptimisticRead相對比悲觀讀鎖,在運算元據前並沒有通過CAS設定鎖的狀態,如果沒有執行緒獲取寫鎖,則返回一個非0的stamp變數,獲取該stamp後在運算元據前還需要呼叫validate方法來判斷期間是否有執行緒獲取了寫鎖,如果是返回值為0則有執行緒獲取寫鎖,如果不是0則可以使用stamp變數的鎖來運算元據。由於tryOptimisticRead並沒有修改鎖狀態,所以不需要釋放鎖。這是讀多寫少的情況下考慮的,不涉及CAS操作,所以效率較高,在保證資料一致性上需要複製一份要操作的變數到方法棧中,並且在運算元據時可能其他寫執行緒已經修改了資料,而我們操作的是方法棧裡面的資料,也就是一個快照,所以最多返回的不是最新的資料,但是一致性得到了保證。
每個執行緒都是通過某個特定Thread物件所對應的方法run()來完成其操作的,run()方法稱為執行緒體。通過呼叫Thread類的start()方法來啟動一個執行緒。
start() 方法用於啟動執行緒,run() 方法用於執行執行緒的執行時程式碼。run() 可以重複呼叫,而 start() 只能呼叫一次。
start()方法來啟動一個執行緒,真正實現了多執行緒執行。呼叫start()方法無需等待run方法體程式碼執行完畢,可以直接繼續執行其他的程式碼; 此時執行緒是處於就緒狀態,並沒有執行。 然後通過此Thread類呼叫方法run()來完成其執行狀態, run()方法執行結束, 此執行緒終止。然後CPU再排程其它執行緒。
run()方法是在本執行緒裡的,只是執行緒裡的一個函數,而不是多執行緒的。 如果直接呼叫run(),其實就相當於是呼叫了一個普通函數而已,直接待用run()方法必須等待run()方法執行完畢才能執行下面的程式碼,所以執行路徑還是隻有一條,根本就沒有執行緒的特徵,所以在多執行緒執行時要使用start()方法而不是run()方法。
這是另一個非常經典的 java 多執行緒面試問題,而且在面試中會經常被問到。很簡單,但是很多人都會答不上來!
new 一個 Thread,執行緒進入了新建狀態。呼叫 start() 方法,會啟動一個執行緒並使執行緒進入了就緒狀態,當分配到時間片後就可以開始執行了。 start() 會執行執行緒的相應準備工作,然後自動執行 run() 方法的內容,這是真正的多執行緒工作。
而直接執行 run() 方法,會把 run 方法當成一個 main 執行緒下的普通方法去執行,並不會在某個執行緒中執行它,所以這並不是多執行緒工作。
總結: 呼叫 start 方法方可啟動執行緒並使執行緒進入就緒狀態,而 run 方法只是 thread 的一個普通方法呼叫,還是在主執行緒裡執行。
(1)可重入性
synchronized的鎖物件中有一個計數器(recursions變數)會記錄執行緒獲得幾次鎖;
synchronized是可重入鎖,每部鎖物件會有一個計數器記錄執行緒獲取幾次鎖,在執行完同步程式碼塊時,計數器的數量會-1,直到計數器的數量為0,就釋放這個鎖。
(2)不可中斷性
(1)自旋鎖
線上程進行阻塞的時候,先讓執行緒自旋等待一段時間,可能這段時間其它執行緒已經解鎖,這時就無需讓執行緒再進行阻塞操作了。
自旋預設次數是10次。
(2)自適應自旋鎖
自旋鎖的升級,自旋的次數不再固定,由前一次自旋次數和鎖的擁有者的狀態決定。
(3)鎖消除
在動態編譯同步程式碼塊的時候,JIT編譯器藉助逃逸分析技術來判斷鎖物件是否只被一個執行緒存取,而沒有其他執行緒,這時就可以取消鎖了。
4、鎖粗化
當JIT編譯器發現一系列的操作都對同一個物件反覆加鎖解鎖,甚至加鎖操作出現在迴圈中,此時會將加鎖同步的範圍粗化到整個操作系列的外部。
鎖粒度:不要鎖住一些無關的程式碼。
鎖粗化:可以一次性執行完的不要多次加鎖執行。
Java中,任何物件都可以作為鎖,並且 wait(),notify()等方法用於等待物件的鎖或者喚醒執行緒,在 Java 的執行緒中並沒有可供任何物件使用的鎖,所以任意物件呼叫方法一定定義在Object類中。
wait(), notify()和 notifyAll()這些方法在同步程式碼塊中呼叫
有的人會說,既然是執行緒放棄物件鎖,那也可以把wait()定義在Thread類裡面啊,新定義的執行緒繼承於Thread類,也不需要重新定義wait()方法的實現。然而,這樣做有一個非常大的問題,一個執行緒完全可以持有很多鎖,你一個執行緒放棄鎖的時候,到底要放棄哪個鎖?當然了,這種設計並不是不能實現,只是管理起來更加複雜。
綜上所述,wait()、notify()和notifyAll()方法要定義在Object類中。
可以通過中斷 和 共用變數的方式實現執行緒間的通訊和共同作業
比如說最經典的生產者-消費者模型:當佇列滿時,生產者需要等待佇列有空間才能繼續往裡面放入商品,而在等待的期間內,生產者必須釋放對臨界資源(即佇列)的佔用權。因為生產者如果不釋放對臨界資源的佔用權,那麼消費者就無法消費佇列中的商品,就不會讓佇列有空間,那麼生產者就會一直無限等待下去。因此,一般情況下,當佇列滿時,會讓生產者交出對臨界資源的佔用權,並進入掛起狀態。然後等待消費者消費了商品,然後消費者通知生產者佇列有空間了。同樣地,當佇列空時,消費者也必須等待,等待生產者通知它佇列中有商品了。這種互相通訊的過程就是執行緒間的共同作業。
Java中執行緒通訊共同作業的最常見的兩種方式:
1、syncrhoized加鎖的執行緒的Object類的wait()/notify()/notifyAll()
2、ReentrantLock類加鎖的執行緒的Condition類的await()/signal()/signalAll()
執行緒間直接的資料交換:
通過管道進行執行緒間通訊:1)位元組流;2)字元流
yield()應該做的是讓當前執行執行緒回到可執行狀態,以允許具有相同優先順序的其他執行緒獲得執行機會。因此,使用yield()的目的是讓相同優先順序的執行緒之間能適當的輪轉執行。但是,實際中無法保證yield()達到讓步目的,因為讓步的執行緒還有可能被執行緒排程程式再次選中。
結論:yield()從未導致執行緒轉到等待/睡眠/阻塞狀態。在大多數情況下,yield()將導致執行緒從執行狀態轉到可執行狀態,但有可能沒有效果。
當鎖被釋放後,任何一個執行緒都有機會競爭得到鎖,這樣做的目的是提高效率,但缺點是可能產生執行緒飢餓現象。
volatile只能作用於變數,保證了操作可見性和有序性,不保證原子性。
在Java的記憶體模型中分為主記憶體和工作記憶體,Java記憶體模型規定所有的變數儲存在主記憶體中,每條執行緒都有自己的工作記憶體。
主記憶體和工作記憶體之間的互動分為8個原子操作:
volatile修飾的變數,只有對volatile進行assign操作,才可以load,只有load才可以use,,這樣就保證了在工作記憶體操作volatile變數,都會同步到主記憶體中。
Synchronized的並行策略是悲觀的,不管是否產生競爭,任何資料的操作都必須加鎖。
樂觀鎖的核心是CAS,CAS包括記憶體值、預期值、新值,只有當記憶體值等於預期值時,才會將記憶體值修改為新值。
樂觀鎖認為對一個物件的操作不會引發衝突,所以每次操作都不進行加鎖,只是在最後提交更改時驗證是否發生衝突,如果衝突則再試一遍,直至成功為止,這個嘗試的過程稱為自旋。
樂觀鎖沒有加鎖,但樂觀鎖引入了ABA問題,此時一般採用版本號進行控制;
也可能產生自旋次數過多問題,此時並不能提高效率,反而不如直接加鎖的效率高;
只能保證一個物件的原子性,可以封裝成物件,再進行CAS操作;
(1)相似點
它們都是阻塞式的同步,也就是說一個執行緒獲得了物件鎖,進入程式碼塊,其它存取該同步塊的執行緒都必須阻塞在同步程式碼塊外面等待,而進行執行緒阻塞和喚醒的程式碼是比較高的。
(2)功能區別
Synchronized是java語言的關鍵字,是原生語法層面的互斥,需要JVM實現;ReentrantLock 是JDK1.5之後提供的API層面的互斥鎖,需要lock和unlock()方法配合try/finally程式碼塊來完成。
Synchronized使用較ReentrantLock 便利一些;
鎖的細粒度和靈活性:ReentrantLock強於Synchronized;
(3)效能區別
Synchronized引入偏向鎖,自旋鎖之後,兩者的效能差不多,在這種情況下,官方建議使用Synchronized。
① Synchronized
Synchronized會在同步塊的前後分別形成monitorenter和monitorexit兩個位元組碼指令。
在執行monitorenter指令時,首先要嘗試獲取物件鎖。如果這個物件沒被鎖定,或者當前執行緒已經擁有了那個物件鎖,把鎖的計數器+1,相應的執行monitorexit時,計數器-1,當計數器為0時,鎖就會被釋放。如果獲取鎖失敗,當前執行緒就要阻塞,知道物件鎖被另一個執行緒釋放為止。
② ReentrantLock
ReentrantLock是java.util.concurrent包下提供的一套互斥鎖,相比Synchronized,ReentrantLock類提供了一些高階功能,主要有如下三項:
等待可中斷,持有鎖的執行緒長期不釋放的時候,正在等待的執行緒可以選擇放棄等待,這相當於Synchronized避免出現死鎖的情況。通過lock.lockInterruptibly()來實現這一機制;
公平鎖,多個執行緒等待同一個鎖時,必須按照申請鎖的時間順序獲得鎖,Synchronized鎖是非公平鎖;ReentrantLock預設也是非公平鎖,可以通過引數true設為公平鎖,但公平鎖表現的效能不是很好;
鎖繫結多個條件,一個ReentrantLock物件可以同時繫結多個物件。ReentrantLock提供了一個Condition(條件)類,用來實現分組喚醒需要喚醒的執行緒們,而不是像Synchronized要麼隨機喚醒一個執行緒,要麼喚醒全部執行緒。
(1)什麼是可重入性
一個執行緒持有鎖時,當其他執行緒嘗試獲取該鎖時,會被阻塞;而這個執行緒嘗試獲取自己持有鎖時,如果成功說明該鎖是可重入的,反之則不可重入。
(2)synchronized是如何實現可重入性
synchronized關鍵字經過編譯後,會在同步塊的前後分別形成monitorenter和monitorexit兩個位元組碼指令。每個鎖物件內部維護一個計數器,該計數器初始值為0,表示任何執行緒都可以獲取該鎖並執行相應的方法。根據虛擬機器器規範要求,在執行monitorenter指令時,首先要嘗試獲取物件的鎖,如果這個物件沒有被鎖定,或者當前執行緒已經擁有了物件的鎖,把鎖的計數器+1,相應的在執行monitorexit指令後鎖計數器-1,當計數器為0時,鎖就被釋放。如果獲取物件鎖失敗,那當前執行緒就要阻塞等待,直到物件鎖被另一個執行緒釋放為止。
(3)ReentrantLock如何實現可重入性
ReentrantLock使用內部類Sync來管理鎖,所以真正的獲取鎖是由Sync的實現類控制的。Sync有兩個實現,分別為NonfairSync(非公公平鎖)和FairSync(公平鎖)。Sync通過繼承AQS實現,在AQS中維護了一個private volatile int state來計算重入次數,避免頻繁的持有釋放操作帶來的執行緒問題。
(4)ReentrantLock程式碼範例
// Sync繼承於AQS
abstract static class Sync extends AbstractQueuedSynchronizer {
...
}
// ReentrantLock預設是非公平鎖
public ReentrantLock() {
sync = new NonfairSync();
}
// 可以通過向構造方法中傳true來實現公平鎖
public ReentrantLock(boolean fair) {
sync = fair ? new FairSync() : new NonfairSync();
}
protected final boolean tryAcquire(int acquires) {
// 當前想要獲取鎖的執行緒
final Thread current = Thread.currentThread();
// 當前鎖的狀態
int c = getState();
// state == 0 此時此刻沒有執行緒持有鎖
if (c == 0) {
// 雖然此時此刻鎖是可以用的,但是這是公平鎖,既然是公平,就得講究先來後到,
// 看看有沒有別人在佇列中等了半天了
if (!hasQueuedPredecessors() &&
// 如果沒有執行緒在等待,那就用CAS嘗試一下,成功了就獲取到鎖了,
// 不成功的話,只能說明一個問題,就在剛剛幾乎同一時刻有個執行緒搶先了 =_=
// 因為剛剛還沒人的,我判斷過了
compareAndSetState(0, acquires)) {
// 到這裡就是獲取到鎖了,標記一下,告訴大家,現在是我佔用了鎖
setExclusiveOwnerThread(current);
return true;
}
}
// 會進入這個else if分支,說明是重入了,需要操作:state=state+1
// 這裡不存在並行問題
else if (current == getExclusiveOwnerThread()) {
int nextc = c + acquires;
if (nextc < 0)
throw new Error("Maximum lock count exceeded");
setState(nextc);
return true;
}
// 如果到這裡,說明前面的if和else if都沒有返回true,說明沒有獲取到鎖
return false;
}
(5)程式碼分析
當一個執行緒在獲取鎖過程中,先判斷state的值是否為0,如果是表示沒有執行緒持有鎖,就可以嘗試獲取鎖。
當state的值不為0時,表示鎖已經被一個執行緒佔用了,這時會做一個判斷current==getExclusiveOwnerThread(),這個方法返回的是當前持有鎖的執行緒,這個判斷是看當前持有鎖的執行緒是不是自己,如果是自己,那麼將state的值+1,表示重入返回即可。
(1)鎖消除
所消除就是虛擬機器器根據一個物件是否真正存在同步情況,若不存在同步情況,則對該物件的存取無需經過加鎖解鎖的操作。
比如StringBuffer的append方法,因為append方法需要判斷物件是否被佔用,而如果程式碼不存在鎖競爭,那麼這部分的效能消耗是無意義的。於是虛擬機器器在即時編譯的時候就會將上面的程式碼進行優化,也就是鎖消除。
@Override
public synchronized StringBuffer append(String str) {
toStringCache = null;
super.append(str);
return this;
}
從原始碼可以看出,append方法用了 synchronized關鍵字,它是執行緒安全的。但我們可能僅線上程內部把StringBuffer當做區域性變數使用;StringBuffer僅在方法內作用域有效,不存線上程安全的問題,這時我們可以通過編譯器將其優化,將鎖消除,前提是Java必須執行在server模式,同時必須開啟逃逸分析;
-server -XX:+DoEscapeAnalysis -XX:+EliminateLocks
其中+DoEscapeAnalysis表示開啟逃逸分析,+EliminateLocks表示鎖消除。
public static String createStringBuffer(String str1, String str2) {
StringBuffer sBuf = new StringBuffer();
sBuf.append(str1);// append方法是同步操作
sBuf.append(str2);
return sBuf.toString();
}
逃逸分析:比如上面的程式碼,它要看sBuf是否可能逃出它的作用域?如果將sBuf作為方法的返回值進行返回,那麼它在方法外部可能被當作一個全域性物件使用,就有可能發生執行緒安全問題,這時就可以說sBuf這個物件發生逃逸了,因而不應將append操作的鎖消除,但我們上面的程式碼沒有發生鎖逃逸,鎖消除就可以帶來一定的效能提升。
(2)鎖粗化
鎖的請求、同步、釋放都會消耗一定的系統資源,如果高頻的鎖請求反而不利於系統效能的優化,鎖粗化就是把多次的鎖請求合併成一個請求,擴大鎖的範圍,降低鎖請求、同步、釋放帶來的效能損耗。
(1)都是可重入鎖;
(2)ReentrantLock內部是實現了Sync,Sync繼承於AQS抽象類。Sync有兩個實現,一個是公平鎖,一個是非公平鎖,通過建構函式定義。AQS中維護了一個state來計算重入次數,避免頻繁的持有釋放操作帶來的執行緒問題。
(3)ReentrantLock只能定義程式碼塊,而Synchronized可以定義方法和程式碼塊;
4、Synchronized是JVM的一個內部關鍵字,ReentrantLock是JDK1.5之後引入的一個API層面的互斥鎖;
5、Synchronized實現自動的加鎖、釋放鎖,ReentrantLock需要手動加鎖和釋放鎖,中間可以暫停;
6、Synchronized由於引進了偏向鎖和自旋鎖,所以效能上和ReentrantLock差不多,但操作上方便很多,所以優先使用Synchronized。
(1)AQS是AbstractQueuedSynchronizer的縮寫,它提供了一個FIFO佇列,可以看成是一個實現同步鎖的核心元件。
AQS是一個抽象類,主要通過繼承的方式來使用,它本身沒有實現任何的同步介面,僅僅是定義了同步狀態的獲取和釋放的方法來提供自定義的同步元件。
(2)AQS的兩種功能:獨佔鎖和共用鎖
(3)AQS的內部實現
AQS的實現依賴內部的同步佇列,也就是FIFO的雙向佇列,如果當前執行緒競爭失敗,那麼AQS會把當前執行緒以及等待狀態資訊構造成一個Node加入到同步佇列中,同時再阻塞該執行緒。當獲取鎖的執行緒釋放鎖以後,會從佇列中喚醒一個阻塞的節點(執行緒)。
AQS佇列內部維護的是一個FIFO的雙向連結串列,這種結構的特點是每個資料結構都有兩個指標,分別指向直接的後繼節點和直接前驅節點。所以雙向連結串列可以從任意一個節點開始很方便的範文前驅和後繼節點。每個Node其實是由執行緒封裝,當執行緒爭搶鎖失敗後會封裝成Node加入到AQS佇列中。
AQS定義兩種資源共用方式
(1)Exclusive(獨佔)
只有一個執行緒能執行,如ReentrantLock。又可分為公平鎖和非公平鎖:
(2)Share(共用)
多個執行緒可同時執行,如Semaphore/CountDownLatch。Semaphore、CountDownLatch、 CyclicBarrier、ReadWriteLock 我們都會在後面講到。
ReentrantReadWriteLock 可以看成是組合式,因為ReentrantReadWriteLock也就是讀寫鎖允許多個執行緒同時對某一資源進行讀。
不同的自定義同步器爭用共用資源的方式也不同。自定義同步器在實現時只需要實現共用資源 state 的獲取與釋放方式即可,至於具體執行緒等待佇列的維護(如獲取資源失敗入隊/喚醒出隊等),AQS已經在頂層實現好了。
使用區域性變數實現執行緒同步
(1)Semaphore同步器
特徵:
經典的號誌,通過計數器控制對共用資源的存取
Semaphore(int count):建立擁有count個許可證的號誌
acquire()/acquire(int num) : 獲取1/num個許可證
release/release(int num) : 釋放1/num個許可證
(2)CountDownLatch同步器
特徵:
必須發生指定數量的事件後才可以繼續執行(比如賽跑比賽,裁判喊出3,2,1之後大家才同時跑)
CountDownLatch(int count):必須發生count個數量才可以開啟鎖存器
await:等待鎖存器
countDown:觸發事件
(3)CyclicBarrier同步器
特徵:
適用於只有多個執行緒都到達預定點時才可以繼續執行(比如鬥地主,需要等齊三個人才開始)
CyclicBarrier(int num) :等待執行緒的數量
CyclicBarrier(int num, Runnable action) :等待執行緒的數量以及所有執行緒到達後的操作
await() : 到達臨界點後暫停執行緒
(4)交換器(Exchanger)同步器
(5)Phaser同步器
建立一個阻塞佇列來容納任務,在第一次執行任務時建立足夠多的執行緒,並處理任務,之後每個工作執行緒自動從任務佇列中獲取執行緒,直到任務佇列中任務為0為止,此時執行緒處於等待狀態,一旦有工作任務加入任務佇列中,即刻喚醒工作執行緒進行處理,實現執行緒的可複用性。
執行緒池一般包括四個基本組成部分:
(1)執行緒池管理器
用於建立執行緒池,銷燬執行緒池,新增新任務。
(2)工作執行緒
執行緒池中執行緒,可迴圈執行任務,在沒有任務時處於等待狀態。
(3)任務佇列
用於存放沒有處理的任務,一種快取機制。
(4)任務介面
每個任務必須實現的介面,供工作執行緒排程任務的執行,主要規定了任務的開始和收尾工作,和任務的狀態。
// Java執行緒池的完整建構函式
public ThreadPoolExecutor(
int corePoolSize, // 執行緒池長期維持的最小執行緒數,即使執行緒處於Idle狀態,也不會回收。
int maximumPoolSize, // 執行緒數的上限
long keepAliveTime, // 執行緒最大生命週期。
TimeUnit unit, //時間單位
BlockingQueue<Runnable> workQueue, //任務佇列。當執行緒池中的執行緒都處於執行狀態,而此時任務數量繼續增加,則需要一個容器來容納這些任務,這就是任務佇列。
ThreadFactory threadFactory, // 執行緒工廠。定義如何啟動一個執行緒,可以設定執行緒名稱,並且可以確認是否是後臺執行緒等。
RejectedExecutionHandler handler // 拒絕任務處理器。由於超出執行緒數量和佇列容量而對繼續增加的任務進行處理的程式。
)
執行緒池中的執行緒是在第一次提交任務submit時建立的
建立執行緒的方式有繼承Thread和實現Runnable,重寫run方法,start開始執行,wait等待,sleep休眠,shutdown停止。
(1)newSingleThreadExecutor:單執行緒池。
顧名思義就是一個池中只有一個執行緒在執行,該執行緒永不超時,而且由於是一個執行緒,當有多個任務需要處理時,會將它們放置到一個無界阻塞佇列中逐個處理,它的實現程式碼如下:
public static ExecutorService newSingleThreadExecutor() {
return new FinalizableDelegatedExecutorService
(new ThreadPoolExecutor(1, 1,0L, TimeUnit.MILLISECONDS,
new LinkedBlockingQueue<Runnable()));
}
它的使用方法也很簡單,下面是簡單的範例:
public static void main(String[] args) throws ExecutionException,InterruptedException {
// 建立單執行緒執行器
ExecutorService es = Executors.newSingleThreadExecutor();
// 執行一個任務
Future<String> future = es.submit(new Callable<String>() {
@Override
public String call() throws Exception {
return "";
}
});
// 獲得任務執行後的返回值
System.out.println("返回值:" + future.get());
// 關閉執行器
es.shutdown();
}
(2)newCachedThreadPool:緩衝功能的執行緒。
建立了一個執行緒池,而且執行緒數量是沒有限制的(當然,不能超過Integer的最大值),新增一個任務即有一個執行緒處理,或者複用之前空閒的執行緒,或者重親啟動一個執行緒,但是一旦一個執行緒在60秒內一直處於等待狀態時(也就是一分鐘無事可做),則會被終止,其原始碼如下:
public static ExecutorService newCachedThreadPool() {
return new ThreadPoolExecutor(0, Integer.MAX_VALUE,60L, TimeUnit.SECONDS,
new SynchronousQueue<Runnable>());
}
這裡需要說明的是,任務佇列使用了同步阻塞佇列,這意味著向佇列中加入一個元素,即可喚醒一個執行緒(新建立的執行緒或複用空閒執行緒來處理),這種佇列已經沒有佇列深度的概念了。
(3)newFixedThreadPool:固定執行緒數量的執行緒池。
在初始化時已經決定了執行緒的最大數量,若任務新增的能力超出了執行緒的處理能力,則建立阻塞佇列容納多餘的任務,其原始碼如下:
public static ExecutorService newFixedThreadPool(int nThreads) {
return new ThreadPoolExecutor(nThreads, nThreads,
0L, TimeUnit.MILLISECONDS,
new LinkedBlockingQueue<Runnable>());
}
上面返回的是一個ThreadPoolExecutor,它的corePoolSize和maximumPoolSize是相等的,也就是說,最大執行緒數量為nThreads。如果任務增長的速度非常快,超過了LinkedBlockingQuene的最大容量(Integer的最大值),那此時會如何處理呢?會按照ThreadPoolExecutor預設的拒絕策略(預設是DiscardPolicy,直接丟棄)來處理。
以上三種執行緒池執行器都是ThreadPoolExecutor的簡化版,目的是幫助開發人員遮蔽過得執行緒細節,簡化多執行緒開發。當需要執行非同步任務時,可以直接通過Executors獲得一個執行緒池,然後執行任務,不需要關注ThreadPoolExecutor的一系列引數時什麼含義。當然,有時候這三個執行緒不能滿足要求,此時則可以直接操作ThreadPoolExecutor來實現複雜的多執行緒計算。
newSingleThreadExecutor、newCachedThreadPool、newFixedThreadPool是執行緒池的簡化版,而ThreadPoolExecutor則是旗艦版___簡化版容易操作,需要了解的知識相對少些,方便使用,而旗艦版功能齊全,適用面廣,難以駕馭。
對於可見性,Java 提供了 volatile 關鍵字來保證可見性和禁止指令重排。 volatile 提供 happens-before 的保證,確保一個執行緒的修改能對其他執行緒是可見的。當一個共用變數被 volatile 修飾時,它會保證修改的值會立即被更新到主記憶體,當有其他執行緒需要讀取時,它會去記憶體中讀取新值。
從實踐角度而言,volatile 的一個重要作用就是和 CAS 結合,保證了原子性,詳細的可以參見 java.util.concurrent.atomic 包下的類,比如 AtomicInteger。
volatile 常用於多執行緒環境下的單次操作(單次讀或者單次寫)。
volatile修飾的變數在各個執行緒的工作記憶體中不存在一致性的問題(在各個執行緒工作的記憶體中,volatile修飾的變數也會存在不一致的情況,但是由於每次使用之前都會先重新整理主記憶體中的資料到工作記憶體,執行引擎看不到不一致的情況,因此可以認為不存在不一致的問題),但是java的運算並非原子性的操作,導致volatile在並行下並非是執行緒安全的。
ThreadLocal 是一個本地執行緒副本變數工具類,在每個執行緒中都建立了一個 ThreadLocalMap 物件,簡單說 ThreadLocal 就是一種以空間換時間的做法,每個執行緒可以存取自己內部 ThreadLocalMap 物件內的 value。通過這種方式,避免資源在多執行緒間共用。
原理:執行緒區域性變數是侷限於執行緒內部的變數,屬於執行緒自身所有,不在多個執行緒間共用。Java提供ThreadLocal類來支援執行緒區域性變數,是一種實現執行緒安全的方式。但是在管理環境下(如 web 伺服器)使用執行緒區域性變數的時候要特別小心,在這種情況下,工作執行緒的生命週期比任何應用變數的生命週期都要長。任何執行緒區域性變數一旦在工作完成後沒有釋放,Java 應用就存在記憶體洩露的風險。
經典的使用場景是為每個執行緒分配一個 JDBC 連線 Connection。這樣就可以保證每個執行緒的都在各自的 Connection 上進行資料庫的操作,不會出現 A 執行緒關了 B執行緒正在使用的 Connection; 還有 Session 管理 等問題。
在java程式中,常用的有兩種機制來解決多執行緒並行問題,一種是sychronized方式,通過鎖機制,一個執行緒執行時,讓另一個執行緒等待,是以時間換空間的方式來讓多執行緒序列執行。而另外一種方式就是ThreadLocal方式,通過建立執行緒區域性變數,以空間換時間的方式來讓多執行緒並行執行。兩種方式各有優劣,適用於不同的場景,要根據不同的業務場景來進行選擇。
在spring的原始碼中,就使用了ThreadLocal來管理連線,在很多開源專案中,都經常使用ThreadLocal來控制多執行緒並行問題,因為它足夠的簡單,我們不需要關心是否有執行緒安全問題,因為變數是每個執行緒所特有的。
ThreadLocal 變數解決了多執行緒環境下單個執行緒中變數的共用問題,使用名為ThreadLocalMap的雜湊表進行維護(key為ThreadLocal變數名,value為ThreadLocal變數的值);
使用時需要注意以下幾點:
在執行程式時,為了提供效能,處理器和編譯器常常會對指令進行重排序,但是不能隨意重排序,不是你想怎麼排序就怎麼排序,它需要滿足以下兩個條件:
需要注意的是:重排序不會影響單執行緒環境的執行結果,但是會破壞多執行緒的執行語意。
很多 synchronized 裡面的程式碼只是一些很簡單的程式碼,執行時間非常快,此時等待的執行緒都加鎖可能是一種不太值得的操作,因為執行緒阻塞涉及到使用者態和核心態切換的問題。既然 synchronized 裡面的程式碼執行得非常快,不妨讓等待鎖的執行緒不要被阻塞,而是在 synchronized 的邊界做忙迴圈,這就是自旋。如果做了多次迴圈發現還沒有獲得鎖,再阻塞,這樣可能是一種更好的策略。
synchronized 鎖升級原理:在鎖物件的物件頭裡面有一個 threadid 欄位,在第一次存取的時候 threadid 為空,jvm 讓其持有偏向鎖,並將 threadid 設定為其執行緒 id,再次進入的時候會先判斷 threadid 是否與其執行緒 id 一致,如果一致則可以直接使用此物件,如果不一致,則升級偏向鎖為輕量級鎖,通過自旋迴圈一定次數來獲取鎖,執行一定次數之後,如果還沒有正常獲取到要使用的物件,此時就會把鎖從輕量級升級為重量級鎖,此過程就構成了 synchronized 鎖的升級。
鎖的升級的目的:鎖升級是為了減低了鎖帶來的效能消耗。在 Java 6 之後優化 synchronized 的實現方式,使用了偏向鎖升級為輕量級鎖再升級到重量級鎖的方式,從而減低了鎖帶來的效能消耗。
synchronized 是和 if、else、for、while 一樣的關鍵字,ReentrantLock 是類,這是二者的本質區別。既然 ReentrantLock 是類,那麼它就提供了比synchronized 更多更靈活的特性,可以被繼承、可以有方法、可以有各種各樣的類變數
synchronized 早期的實現比較低效,對比 ReentrantLock,大多數場景效能都相差較大,但是在 Java 6 中對 synchronized 進行了非常多的改進。
相同點:兩者都是可重入鎖
兩者都是可重入鎖。「可重入鎖」概念是:自己可以再次獲取自己的內部鎖。比如一個執行緒獲得了某個物件的鎖,此時這個物件鎖還沒有釋放,當其再次想要獲取這個物件的鎖的時候還是可以獲取的,如果不可鎖重入的話,就會造成死鎖。同一個執行緒每次獲取鎖,鎖的計數器都自增1,所以要等到鎖的計數器下降為0時才能釋放鎖。
主要區別如下:
Java中每一個物件都可以作為鎖,這是synchronized實現同步的基礎:
Lock 介面比同步方法和同步塊提供了更具擴充套件性的鎖操作。他們允許更靈活的結構,可以具有完全不同的性質,並且可以支援多個相關類的條件物件。
它的優勢有:
(1)可以使鎖更公平
(2)可以使執行緒在等待鎖的時候響應中斷
(3)可以讓執行緒嘗試獲取鎖,並在無法獲取鎖的時候立即返回或者等待一段時間
(4)可以在不同的範圍,以不同的順序獲取和釋放鎖
整體上來說 Lock 是 synchronized 的擴充套件版,Lock 提供了無條件的、可輪詢的(tryLock 方法)、定時的(tryLock 帶參方法)、可中斷的(lockInterruptibly)、可多條件佇列的(newCondition 方法)鎖操作。另外 Lock 的實現類基本都支援非公平鎖(預設)和公平鎖,synchronized 只支援非公平鎖,當然,在大部分情況下,非公平鎖是高效的選擇。
(1)servlet是伺服器端的Java程式,它擔當使用者端和伺服器端的中間層。
(2)jsp全名為Java server pages,中文名叫Java伺服器頁面,其本質是一個簡化的servlet設計。JSP是一種動態頁面設計,它的主要目的是將表示邏輯從servlet中分離出來。
(3)JVM只能識別Java程式碼,不能識別JSP,JSP編譯後變成了servlet,web容器將JSP的程式碼編譯成JVM能夠識別的Java類(servlet)。
(4)JSP有內建物件、servlet沒有內建物件。
JSP九大內建物件:
application、session、request、page
(1)儲存位置不同
(2)儲存容量不同
(3)儲存方式不同
(4)隱私策略不同
(5)有效期不同
(6)跨域支援上不同
一般預設情況下,在對談中,伺服器儲存 session 的 sessionid 是通過 cookie 存到瀏覽器裡。
如果瀏覽器禁用了 cookie,瀏覽器請求伺服器無法攜帶 sessionid,伺服器無法識別請求中的使用者身份,session失效。
但是可以通過其他方法在禁用 cookie 的情況下,可以繼續使用session。
多執行緒程式設計中一般執行緒的個數都大於 CPU 核心的個數,而一個 CPU 核心在任意時刻只能被一個執行緒使用,為了讓這些執行緒都能得到有效執行,CPU 採取的策略是為每個執行緒分配時間片並輪轉的形式。當一個執行緒的時間片用完的時候就會重新處於就緒狀態讓給其他執行緒使用,這個過程就屬於一次上下文切換。
概括來說就是:當前任務在執行完 CPU 時間片切換到另一個任務之前會先儲存自己的狀態,以便下次再切換回這個任務時,可以再載入這個任務的狀態。任務從儲存到再載入的過程就是一次上下文切換。
上下文切換通常是計算密集型的。也就是說,它需要相當可觀的處理器時間,在每秒幾十上百次的切換中,每次切換都需要納秒量級的時間。所以,上下文切換對系統來說意味著消耗大量的 CPU 時間,事實上,可能是作業系統中時間消耗最大的操作。
Linux 相比與其他作業系統(包括其他類 Unix 系統)有很多的優點,其中有一項就是,其上下文切換和模式切換的時間消耗非常少。
1、session機制
session是伺服器端儲存的一個物件,主要用來儲存所有存取過該伺服器端的使用者端的使用者資訊(也可以儲存其他資訊),從而實現保持使用者對談狀態。但是伺服器重新啟動時,記憶體會被銷燬,儲存的使用者資訊也就消失了。
不同的使用者存取伺服器端的時候會在session物件中儲存鍵值對,「鍵」用來儲存開啟這個使用者資訊的「鑰匙」,在登入成功後,「鑰匙」通過cookie返回給使用者端,使用者端儲存為sessionId記錄在cookie中。當用戶端再次存取時,會預設攜帶cookie中的sessionId來實現對談機制。
(1)session是基於cookie的。
(2)session持久化
用於解決重新啟動伺服器後session消失的問題。在資料庫中儲存session,而不是儲存在記憶體中。通過包:express-mysql-session。
當用戶端儲存的cookie失效後,伺服器端的session不會立即銷燬,會有一個延時,伺服器端會定期清理無效session,不會造成無效資料佔用儲存空間的問題。
2、token機制
適用於前後端分離的專案(前後端程式碼執行在不同的伺服器下)
請求登入時,token和sessionid原理相同,是對key和key對應的使用者資訊進行加密後的加密字元,登入成功後,會在響應主體中將{token:「字串」}返回給使用者端。
使用者端通過cookie都可以進行儲存。再次請求時不會預設攜帶,需要在請求攔截器位置給請求頭中新增認證欄位Authorization攜帶token資訊,伺服器就可以通過token資訊查詢使用者登入狀態。
當用戶端登入完成後,會在伺服器端產生一個session,此時伺服器端會將sessionid返回給使用者端瀏覽器。使用者端將sessionid儲存在瀏覽器的cookie中,當使用者再次登入時,會獲得對應的sessionid,然後將sessionid傳送到伺服器端請求登入,伺服器端在記憶體中找到對應的sessionid,完成登入,如果找不到,返回登入頁面。
因為使用者端和伺服器端都要確認連線,①使用者端請求連線伺服器端;②針對使用者端的請求確認應答,並請求建立連線;③針對伺服器端的請求確認應答,建立連線;
兩次無法確保A能收到B的資料;
xss(Cross Site Scripting),即跨站指令碼攻擊,是一種常見於web應用程式中的電腦保安漏洞。指的是在使用者瀏覽器上,在渲染DOM樹的時候,執行了不可預期的JS指令碼,從而發生了安全問題。
XSS就是通過在使用者端注入惡意的可執行指令碼,若伺服器端對使用者的輸入不進行處理,直接將使用者的輸入輸出到瀏覽器,然後瀏覽器將會執行使用者注入的指令碼。 所以XSS攻擊的核心就是瀏覽器渲染DOM的時候將文字資訊解析成JS指令碼從而引發JS指令碼注入,那麼XSS攻擊的防禦手段就是基於瀏覽器渲染這一步去做防禦。只要我們使用HTML編碼將瀏覽器需要渲染的資訊編碼後,瀏覽器在渲染DOM元素的時候,會自動解碼需要渲染的資訊,將上述資訊解析成字串而不是JS指令碼,這就是我們防禦XSS攻擊的核心想法。
預防:
1、獲取使用者的輸入,不用innerHtml,用innerText.
2、對使用者的輸入進行過濾,如對& < > " ' /等進行跳脫;
跨站請求偽造(英語:Cross-site request forgery),也被稱為 one-click attack 或者 session riding,通常縮寫為 CSRF 或者 XSRF, 是一種挾制使用者在當前已登入的Web應用程式上執行非本意的操作的攻擊方法。跟跨網站指令碼(XSS)相比,XSS 利用的是使用者對指定網站的信任,CSRF 利用的是網站對使用者網頁瀏覽器的信任。
1、攻擊細節
跨站請求攻擊,簡單地說,是攻擊者通過一些技術手段欺騙使用者的瀏覽器去存取一個自己曾經認證過的網站並執行一些操作(如發郵件,發訊息,甚至財產操作如轉賬和購買商品)。由於瀏覽器曾經認證過,所以被存取的網站會認為是真正的使用者操作而去執行。這利用了web中使用者身份驗證的一個漏洞:簡單的身份驗證只能保證請求發自某個使用者的瀏覽器,卻不能保證請求本身是使用者自願發出的。
例子
假如一家銀行用以執行轉賬操作的URL地址如下:http://www.examplebank.com/withdraw?account=AccoutName&amount=1000&for=PayeeName
那麼,一個惡意攻擊者可以在另一個網站上放置如下程式碼: <img src="http://www.examplebank.com/withdraw?account=Alice&amount=1000&for=Badman">
如果有賬戶名為Alice的使用者存取了惡意站點,而她之前剛存取過銀行不久,登入資訊尚未過期,那麼她就會損失1000資金。
這種惡意的網址可以有很多種形式,藏身於網頁中的許多地方。此外,攻擊者也不需要控制放置惡意網址的網站。例如他可以將這種地址藏在論壇,部落格等任何使用者生成資訊的網站中。這意味著如果伺服器端沒有合適的防禦措施的話,使用者即使存取熟悉的可信網站也有受攻擊的危險。
透過例子能夠看出,攻擊者並不能通過CSRF攻擊來直接獲取使用者的賬戶控制權,也不能直接竊取使用者的任何資訊。他們能做到的,是欺騙使用者瀏覽器,讓其以使用者的名義執行操作。
2、防禦措施
檢查Referer欄位
HTTP頭中有一個Referer欄位,這個欄位用以標明請求來源於哪個地址。在處理敏感資料請求時,通常來說,Referer欄位應和請求的地址位於同一域名下。以上文銀行操作為例,Referer欄位地址通常應該是轉賬按鈕所在的網頁地址,應該也位於www.examplebank.com之下。而如果是CSRF攻擊傳來的請求,Referer欄位會是包含惡意網址的地址,不會位於www.examplebank.com之下,這時候伺服器就能識別出惡意的存取。
這種辦法簡單易行,工作量低,僅需要在關鍵存取處增加一步校驗。但這種辦法也有其侷限性,因其完全依賴瀏覽器傳送正確的Referer欄位。雖然http協定對此欄位的內容有明確的規定,但並無法保證來訪的瀏覽器的具體實現,亦無法保證瀏覽器沒有安全漏洞影響到此欄位。並且也存在攻擊者攻擊某些瀏覽器,篡改其Referer欄位的可能。
3、新增校驗token
由於CSRF的本質在於攻擊者欺騙使用者去存取自己設定的地址,所以如果要求在存取敏感資料請求時,要求使用者瀏覽器提供不儲存在cookie中,並且攻擊者無法偽造的資料作為校驗,那麼攻擊者就無法再執行CSRF攻擊。這種資料通常是表單中的一個資料項。伺服器將其生成並附加在表單中,其內容是一個偽亂數。當用戶端通過表單提交請求時,這個偽亂數也一併提交上去以供校驗。正常的存取時,使用者端瀏覽器能夠正確得到並傳回這個偽亂數,而通過CSRF傳來的欺騙性攻擊中,攻擊者無從事先得知這個偽亂數的值,伺服器端就會因為校驗token的值為空或者錯誤,拒絕這個可疑請求。
2、最流行的跨域方案cors
cors是目前主流的跨域解決方案,跨域資源共用(CORS) 是一種機制,它使用額外的 HTTP 頭來告訴瀏覽器 讓執行在一個 origin (domain) 上的Web應用被准許存取來自不同源伺服器上的指定的資源。當一個資源從與該資源本身所在的伺服器不同的域、協定或埠請求一個資源時,資源會發起一個跨域 HTTP 請求。
3、最方便的跨域方案Nginx
nginx是一款極其強大的web伺服器,其優點就是輕量級、啟動快、高並行。
現在的新專案中nginx幾乎是首選,我們用node或者java開發的服務通常都需要經過nginx的反向代理。
反向代理的原理很簡單,即所有使用者端的請求都必須先經過nginx的處理,nginx作為代理伺服器再講請求轉發給node或者java服務,這樣就規避了同源策略。
WebSocket是一個允許Web應用程式(通常指瀏覽器)與伺服器進行雙向通訊的協定。HTML5的WebSocket API主要是為瀏覽器端提供了一個基於TCP協定實現全雙工通訊的方法。
WebSocket優勢: 瀏覽器和伺服器只需要要做一個握手的動作,在建立連線之後,雙方可以在任意時刻,相互推播資訊。同時,伺服器與使用者端之間交換的頭資訊很小。
傳送方需要等緩衝區滿才能傳送出去,造成粘包;
接收方不及時接收緩衝區的包,造成粘包;
1、單例模式
作用:保證類只有一個範例。
JDK中體現:Runtime類。
2、靜態工廠模式
作用:代替建構函式建立物件,方法名比建構函式清晰。
JDK中體現:Integer.valueOf、Class.forName
3、抽象工廠
作用:建立某一種類的物件。
JDK中體現:Java.sql包。
4、原型模式
clone();
原型模式的本質是拷貝原型來建立新的物件,拷貝是比new更快的建立物件的方法,當需要大批次建立新物件而且都是同一個類的物件的時候考慮使用原型模式。
一般的克隆只是淺拷貝(物件的hash值不一樣,但是物件裡面的成員變數的hash值是一樣的)。
有些場景需要深拷貝,這時我們就要重寫clone方法,以ArrayList為例:
5、介面卡模式
作用:使不相容的介面相容。
JDK中體現:InputStream、OutputStream。
6、裝飾器模式
作用:為類新增新的功能,防止類繼承帶來的類爆炸。
JDK中體現:io類、Collections、List。
7、外觀模式
作用:封裝一組互動類,一直對外提供介面。
JDK中體現:logging包。
8、享元模式
作用:共用物件、節省記憶體。
JDK中體現:Integer.valueOf、String常數池。
9、代理模式
作用:
(1)透明呼叫被代理物件,無須知道複雜實現細節;
(2)增加被代理類的功能;
JDK中體現:動態代理。
10、迭代器模式
作用:將集合的迭代和集合本身分離。
JDK中體現:Iterator
11、命令模式
作用:封裝操作,使介面一致。
JDK中體現:Runable、Callable、ThreadPoolExecutor。
1、什麼是設計模式?
設計模式是解決軟體開發某些特定問題而提出的一些解決方案,也可以理解為解決問題的一些固定思路。
通過設計模式可以幫助我們增強程式碼的可複用性、可延伸性、靈活性。
我們使用設計模式的最終目的是實現程式碼的高內聚、低耦合。
2、設計模式的七大原則
3、你是否在你的程式碼裡面使用過任何設計模式?
(1)單例模式
JDK種的runtime,Spring種的singeton。
(2)簡單工廠模式
Spring的BeanFactory,根據傳入一個唯一標識來獲得bean物件。
(3)原型模式
clone()
(4)代理模式
Spring的AOP中,Spring實現AOP功能的原理就是代理模式,①JDK動態代理。②CGLIB動態代理,使用Advice(通知)對類進行方法級別的切面增強。
(5)裝飾器模式
為類新增新的功能,防止類爆炸;
IO流、資料來源包裝,Spring中用到的裝飾器模式表現在Wrapper。
1、觀察者模式是一種一對多的依賴關係,讓多個觀察者同時監聽某一主題物件。當這個主題物件發生變化時,會通知所有觀察者物件,使它們能夠自動更新自己。
2、JAVA提供的對觀察者模式的支援
在JAVA語言的java.util庫裡面,提供了一個Observable類以及一個Observer介面,構成JAVA語言對觀察者模式的支援。
(1)Observer介面
這個介面只定義了一個方法,即update()方法,當被觀察者物件的狀態發生變化時,被觀察者物件的notifyObservers()方法就會呼叫這一方法。
public interface Observer {
void update(Observable o, Object arg);
}
(2)Observable類
被觀察者類都是java.util.Observable類的子類。java.util.Observable提供公開的方法支援觀察者物件,這些方法中有兩個對Observable的子類非常重要:一個是setChanged(),另一個是notifyObservers()。第一方法setChanged()被呼叫之後會設定一個內部標記變數,代表被觀察者物件的狀態發生了變化。第二個是notifyObservers(),這個方法被呼叫時,會呼叫所有登記過的觀察者物件的update()方法,使這些觀察者物件可以更新自己。
1、工廠模式好處
2、在哪裡使用?
有五種自動裝配的方式,可以用來指導 Spring 容器用自動裝配方式來進行依賴注入。
1、no
預設的方式是不進行自動裝配,通過顯式設定 ref 屬性來進行裝配。第 402 頁 共 485 頁
2、byName
通過引數名 自動裝配,Spring 容器在組態檔中發現 bean
的 autowire 屬性被設定成 byname,之後容器試圖匹配、裝配和該 bean 的屬
性具有相同名字的 bean。
3、byType:
通過引數型別自動裝配,Spring 容器在組態檔中發現 bean
的 autowire 屬性被設定成 byType,之後容器試圖匹配、裝配和該 bean 的屬
性具有相同型別的 bean。如果有多個 bean 符合條件,則丟擲錯誤。
4、constructor
這個方式類似於 byType, 但是要提供給構造器引數,如
果沒有確定的帶引數的構造器引數型別,將會丟擲異常。
5、autodetect
首先嚐試使用 constructor 來自動裝配,如果無法運作,
則使用 byType 方式。
在Java IO中運用了裝飾器模式,inputStream作為抽象類,其下有幾個實現類,表示從不同的資料來源輸入:
FilterInputStream作為裝飾器在JDK中是一個普通類,其下面有多個具體裝飾器比如BufferedInputStream、DataInputStream等。
FilterInputStream內部封裝了基礎構件:
protected volatile InputStream in;
而BufferedInputStream在呼叫其read()讀取資料時會委託基礎構件來進行更底層的操作,而它自己所起的裝飾作用就是緩衝,在原始碼中可以很清楚的看到這一切。
Spring是一個控制反轉和麵向切面的容器框架。
Spring有七大功能模組:
1、Core
Core模組是Spring的核心類庫,Core實現了IOC功能。
2、AOP
Apring AOP模組是Spring的AOP庫,提供了AOP(攔截器)機制,並提供常見的攔截器,供使用者自定義和設定。
3、orm
提供對常用ORM框架的管理和支援,hibernate、mybatis等。
4、Dao
Spring提供對JDBC的支援,對JDBC進行封裝。
5、Web
對Struts2的支援。
6、Context
Context模組提供框架式的Bean的存取方式,其它程式可以通過Context存取Spring的Bean資源,相當於資源注入。
7、MVC
MVC模組為spring提供了一套輕量級的MVC實現,即Spring MVC。
1、輕量級框架、容器
Spring是一個容器,管理物件的生命週期和設定。基於一個可設定原型prototype,你的bean可以使單利的,也可以每次需要時都生成一個新的範例。
2、控制反轉IOC
Spring通過控制反轉實現鬆耦合。
3、支援AOP
Spring提供對AOP的支援,它允許將一些通用任務,如安全、事務、紀錄檔等進行集中式處理,從而提高了程式的複用性。
4、輕量級框架
5、方便測試
Spring提供Junit4的支援,可以通過註解方便測試spring程式。
6、對Java中很多API進行了封裝
7、方便整合各種優秀框架
如Struts、hibernate、mybstis。
8、支援宣告式事務處理
只需通過設定就可以完成對事務的管理,而無須手動程式設計。
1、IOC理論的背景
我們都知道,在採用物件導向方法設計的軟體系統中,它的底層實現都是由N個物件組成的,所有的物件通過彼此的合作,最終實現系統的業務邏輯。
如果我們開啟機械式手錶的後蓋,就會看到與上面類似的情形,各個齒輪分別帶動時針、分針和秒針順時針旋轉,從而在錶盤上產生正確的時間。圖1中描述的就是這樣的一個齒輪組,它擁有多個獨立的齒輪,這些齒輪相互齧合在一起,協同工作,共同完成某項任務。我們可以看到,在這樣的齒輪組中,如果有一個齒輪出了問題,就可能會影響到整個齒輪組的正常運轉。
齒輪組中齒輪之間的齧合關係,與軟體系統中物件之間的耦合關係非常相似。物件之間的耦合關係是無法避免的,也是必要的,這是協同工作的基礎。現在,伴隨著工業級應用的規模越來越龐大,物件之間的依賴關係也越來越複雜,經常會出現物件之間的多重依賴性關係,因此,架構師和設計師對於系統的分析和設計,將面臨更大的挑戰。物件之間耦合度過高的系統,必然會出現牽一髮而動全身的情形。
耦合關係不僅會出現在物件與物件之間,也會出現在軟體系統的各模組之間,以及軟體系統和硬體系統之間。如何降低系統之間、模組之間和物件之間的耦合度,是軟體工程永遠追求的目標之一。為了解決物件之間的耦合度過高的問題,軟體專家Michael Mattson提出了IOC理論,用來實現物件之間的「解耦」,目前這個理論已經被成功地應用到實踐當中,很多的J2EE專案均採用了IOC框架產品Spring。
2、什麼是控制反轉
IOC是Inversion of Control的縮寫,多數書籍翻譯成「控制反轉」,還有些書籍翻譯成為「控制反向」或者「控制倒置」。
1996年,Michael Mattson在一篇有關探討物件導向框架的文章中,首先提出了IOC 這個概念。對於物件導向設計及程式設計的基本思想,前面我們已經講了很多了,不再贅述,簡單來說就是把複雜系統分解成相互合作的物件,這些物件類通過封裝以後,內部實現對外部是透明的,從而降低了解決問題的複雜度,而且可以靈活地被重用和擴充套件。IOC理論提出的觀點大體是這樣的:藉助於「第三方」實現具有依賴關係的物件之間的解耦,如下圖:
大家看到了吧,由於引進了中間位置的「第三方」,也就是IOC容器,使得A、B、C、D這4個物件沒有了耦合關係,齒輪之間的傳動全部依靠「第三方」了,全部物件的控制權全部上繳給「第三方」IOC容器,所以,IOC容器成了整個系統的關鍵核心,它起到了一種類似「粘合劑」的作用,把系統中的所有物件粘合在一起發揮作用,如果沒有這個「粘合劑」,物件與物件之間會彼此失去聯絡,這就是有人把IOC容器比喻成「粘合劑」的由來。
我們再來做個試驗:把上圖中間的IOC容器拿掉,然後再來看看這套系統(拿掉IoC容器後的系統):
我們現在看到的畫面,就是我們要實現整個系統所需要完成的全部內容。這時候,A、B、C、D這4個物件之間已經沒有了耦合關係,彼此毫無聯絡,這樣的話,當你在實現A的時候,根本無須再去考慮B、C和D了,物件之間的依賴關係已經降低到了最低程度。所以,如果真能實現IOC容器,對於系統開發而言,這將是一件多麼美好的事情,參與開發的每一成員只要實現自己的類就可以了,跟別人沒有任何關係!
我們再來看看,控制反轉(IOC)到底為什麼要起這麼個名字?我們來對比一下:
軟體系統在沒有引入IOC容器之前,如圖1所示,物件A依賴於物件B,那麼物件A在初始化或者執行到某一點的時候,自己必須主動去建立物件B或者使用已經建立的物件B。無論是建立還是使用物件B,控制權都在自己手上。
軟體系統在引入IOC容器之後,這種情形就完全改變了,如圖3所示,由於IOC容器的加入,物件A與物件B之間失去了直接聯絡,所以,當物件A執行到需要物件B的時候,IOC容器會主動建立一個物件B注入到物件A需要的地方。
通過前後的對比,我們不難看出來:物件A獲得依賴物件B的過程,由主動行為變為了被動行為,控制權顛倒過來了,這就是「控制反轉」這個名稱的由來。
3、IOC的別名:依賴注入(DI)
2004年,Martin Fowler探討了同一個問題,既然IOC是控制反轉,那麼到底是「哪些方面的控制被反轉了呢?」,經過詳細地分析和論證後,他得出了答案:「獲得依賴物件的過程被反轉了」。控制被反轉之後,獲得依賴物件的過程由自身管理變為了由IOC容器主動注入。於是,他給「控制反轉」取了一個更合適的名字叫做「依賴注入(Dependency Injection)」。他的這個答案,實際上給出了實現IOC的方法:注入。所謂依賴注入,就是由IOC容器在執行期間,動態地將某種依賴關係注入到物件之中。
所以,依賴注入(DI)和控制反轉(IOC)是從不同的角度的描述的同一件事情,就是指通過引入IOC容器,利用依賴關係注入的方式,實現物件之間的解耦。
我們舉一個生活中的例子,來幫助理解依賴注入的過程。大家對USB介面和USB裝置應該都很熟悉吧,USB為我們使用電腦提供了很大的方便,現在有很多的外部裝置都支援USB介面。
現在,我們利用電腦主機和USB介面來實現一個任務:從外部USB裝置讀取一個檔案。
電腦主機讀取檔案的時候,它一點也不會關心USB介面上連線的是什麼外部裝置,而且它確實也無須知道。它的任務就是讀取USB介面,掛接的外部裝置只要符合USB介面標準即可。所以,如果我給電腦主機連線上一個U盤,那麼主機就從U盤上讀取檔案;如果我給電腦主機連線上一個外接硬碟,那麼電腦主機就從外接硬碟上讀取檔案。掛接外部裝置的權力由我作主,即控制權歸我,至於USB介面掛接的是什麼裝置,電腦主機是決定不了,它只能被動的接受。電腦主機需要外部裝置的時候,根本不用它告訴我,我就會主動幫它掛上它想要的外部裝置,你看我的服務是多麼的到位。這就是我們生活中常見的一個依賴注入的例子。在這個過程中,我就起到了IOC容器的作用。
通過這個例子,依賴注入的思路已經非常清楚:當電腦主機讀取檔案的時候,我就把它所要依賴的外部裝置,幫他掛接上。整個外部裝置注入的過程和一個被依賴的物件在系統執行時被注入另外一個物件內部的過程完全一樣。
我們把依賴注入應用到軟體系統中,再來描述一下這個過程:
物件A依賴於物件B,當物件 A需要用到物件B的時候,IOC容器就會立即建立一個物件B送給物件A。IOC容器就是一個物件製造工廠,你需要什麼,它會給你送去,你直接使用就行了,而再也不用去關心你所用的東西是如何製成的,也不用關心最後是怎麼被銷燬的,這一切全部由IOC容器包辦。
在傳統的實現中,由程式內部程式碼來控制元件之間的關係。我們經常使用new關鍵字來實現兩個元件之間關係的組合,這種實現方式會造成元件之間耦合。IOC很好地解決了該問題,它將實現元件間關係從程式內部提到外部容器,也就是說由容器在執行期將元件間的某種依賴關係動態注入元件中。
4、IOC為我們帶來了什麼好處
我們還是從USB的例子說起,使用USB外部裝置比使用內建硬碟,到底帶來什麼好處?
第一、USB裝置作為電腦主機的外部裝置,在插入主機之前,與電腦主機沒有任何的關係,只有被我們連線在一起之後,兩者才發生聯絡,具有相關性。所以,無論兩者中的任何一方出現什麼的問題,都不會影響另一方的執行。這種特性體現在軟體工程中,就是可維護性比較好,非常便於進行單元測試,便於偵錯程式和診斷故障。程式碼中的每一個Class都可以單獨測試,彼此之間互不影響,只要保證自身的功能無誤即可,這就是元件之間低耦合或者無耦合帶來的好處。
第二、USB裝置和電腦主機的之間無關性,還帶來了另外一個好處,生產USB裝置的廠商和生產電腦主機的廠商完全可以是互不相干的人,各幹各事,他們之間唯一需要遵守的就是USB介面標準。這種特性體現在軟體開發過程中,好處可是太大了。每個開發團隊的成員都只需要關心實現自身的業務邏輯,完全不用去關心其它的人工作進展,因為你的任務跟別人沒有任何關係,你的任務可以單獨測試,你的任務也不用依賴於別人的元件,再也不用扯不清責任了。所以,在一個大中型專案中,團隊成員分工明確、責任明晰,很容易將一個大的任務劃分為細小的任務,開發效率和產品品質必將得到大幅度的提高。
第三、同一個USB外部裝置可以插接到任何支援USB的裝置,可以插接到電腦主機,也可以插接到DV機,USB外部裝置可以被反覆利用。在軟體工程中,這種特性就是可複用性好,我們可以把具有普遍性的常用元件獨立出來,反覆利用到專案中的其它部分,或者是其它專案,當然這也是物件導向的基本特徵。顯然,IOC不僅更好地貫徹了這個原則,提高了模組的可複用性。符合介面標準的實現,都可以插接到支援此標準的模組中。
第四、同USB外部裝置一樣,模組具有熱插拔特性。IOC生成物件的方式轉為外接方式,也就是把物件生成放在組態檔裡進行定義,這樣,當我們更換一個實現子類將會變得很簡單,只要修改組態檔就可以了,完全具有熱插撥的特性。
以上幾點好處,難道還不足以打動我們,讓我們在專案開發過程中使用IOC框架嗎?
5、IOC容器的技術剖析
IOC中最基本的技術就是「反射(Reflection)」程式設計,目前.Net C#、Java和PHP5等語言均支援,其中PHP5的技術書籍中,有時候也被翻譯成「對映」。有關反射的概念和用法,大家應該都很清楚,通俗來講就是根據給出的類名(字串方式)來動態地生成物件。這種程式設計方式可以讓物件在生成時才決定到底是哪一種物件。反射的應用是很廣泛的,很多的成熟的框架,比如象Java中的Hibernate、Spring框架,.Net中 NHibernate、Spring.Net框架都是把「反射」做為最基本的技術手段。
反射技術其實很早就出現了,但一直被忽略,沒有被進一步的利用。當時的反射程式設計方式相對於正常的物件生成方式要慢至少得10倍。現在的反射技術經過改良優化,已經非常成熟,反射方式生成物件和通常物件生成方式,速度已經相差不大了,大約為1-2倍的差距。
我們可以把IOC容器的工作模式看做是工廠模式的昇華,可以把IOC容器看作是一個工廠,這個工廠裡要生產的物件都在組態檔中給出定義,然後利用程式語言的的反射程式設計,根據組態檔中給出的類名生成相應的物件。從實現來看,IOC是把以前在工廠方法裡寫死的物件生成程式碼,改變為由組態檔來定義,也就是把工廠和物件生成這兩者獨立分隔開來,目的就是提高靈活性和可維護性。
6、IOC容器的一些產品
Sun ONE技術體系下的IOC容器有:輕量級的有Spring、Guice、Pico Container、Avalon、HiveMind;重量級的有EJB;不輕不重的有JBoss,Jdon等等。Spring框架作為Java開發中SSH(Struts、Spring、Hibernate)三劍客之一,大中小專案中都有使用,非常成熟,應用廣泛,EJB在關鍵性的工業級專案中也被使用,比如某些電信業務。
.Net技術體系下的IOC容器有:Spring.Net、Castle等等。Spring.Net是從Java的Spring移植過來的IOC容器,Castle的IOC容器就是Windsor部分。它們均是輕量級的框架,比較成熟,其中Spring.Net已經被逐漸應用於各種專案中。
7、使用IOC框架應該注意什麼
使用IOC框架產品能夠給我們的開發過程帶來很大的好處,但是也要充分認識引入IOC框架的缺點,做到心中有數,杜絕濫用框架。
(1)軟體系統中由於引入了第三方IOC容器,生成物件的步驟變得有些複雜,本來是兩者之間的事情,又憑空多出一道手續,所以,我們在剛開始使用IOC框架的時候,會感覺系統變得不太直觀。所以,引入了一個全新的框架,就會增加團隊成員學習和認識的培訓成本,並且在以後的執行維護中,還得讓新加入者具備同樣的知識體系。
(2)由於IOC容器生成物件是通過反射方式,在執行效率上有一定的損耗。如果你要追求執行效率的話,就必須對此進行權衡。
(3)、具體到IOC框架產品(比如:Spring)來講,需要進行大量的配製工作,比較繁瑣,對於一些小的專案而言,客觀上也可能加大一些工作成本。
(4)IOC框架產品本身的成熟度需要進行評估,如果引入一個不成熟的IOC框架產品,那麼會影響到整個專案,所以這也是一個隱性的風險。
我們大體可以得出這樣的結論:一些工作量不大的專案或者產品,不太適合使用IOC框架產品。另外,如果團隊成員的知識能力欠缺,對於IOC框架產品缺乏深入的理解,也不要貿然引入。最後,特別強調執行效率的專案或者產品,也不太適合引入IOC框架產品,象WEB2.0網站就是這種情況。
藉助Spring實現具有依賴關係的物件之間的解耦。
物件A執行需要物件B,由主動建立變為IOC容器注入,這便是控制反轉。
獲得依賴物件的過程被反轉了,獲取依賴物件的過程由自身建立變為由IOC容器注入,這便是依賴注入。
1、BeanFactory是Spring的最底層介面,包含bean的定義,管理bean的載入,範例化,控制bean的生命週期,特點是每次獲取物件時才會建立物件。
ApplicationContext是BeanFactory的子介面,擁有BeanFactory的全部功能,並且擴充套件了很多高階特性,每次容器啟動時就會建立所有的物件。
2、BeanFactory通常以程式設計的方式被建立,ApplicationContext可以以宣告的方式建立,如使用ContextLoader。
3、BeanFactory 和 ApplicationContext都支援BeanPostProcessor,BeanFactoryPostProcessor,但BeanFactory需要手動註冊,ApplicationContext則是自動註冊。
JavaConfig是Spring3.0新增的概念,就是以註解的形式取代Spring中繁瑣的xml檔案。
JavaConfig結合了xml的解耦和java編譯時檢查的優點。
ORM(Object-relational mapping),物件關係對映。
是為了解決物件導向與關係型資料庫存在的不匹配問題。
ORM框架的優點:
1、xml組態檔
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans-3.0.xsd">
<bean id="jackma" class="com.tyq.dto.User">
<property name="name" value="jackma" />
<property name="age" value="55" />
<property name="dog" ref="jm" />
</bean>
<bean id="jm" class="com.tyq.dto.Dog">
<property name="name" value="jack" />
<property name="breed" value="金毛" />
<property name="age" value="2" />
</bean>
</beans>
2、基於註解的方式
專案越來越大,基於xml設定太麻煩,Spring 2.x時代提供了宣告bean的註解。
(1)Bean的定義
@Component、@Controller、@Service、@Repository。
(2)Bean的注入
@Autowire
3、基於Java的方式
Spring 3.x以後,可以通過Java程式碼裝配Bean。
@Configuration
public class DemoConfig {
@Bean
public User zs(){
return new User();
}
@Bean
public Dog dog(){
return new Dog();
}
@Bean //兩個狗
public Dog haqi(){
return new Dog();
}
}
@Component("zs")
public class User {
private String name;
private int age;
private Dog dog;
//get,set方法略
}
原來就是設定類啊,通過@Bean、@Component、getBean方式進行Bean的註冊和發現。
預設情況下,所有敏感的HTTP端點都是安全的,只有具有Actuator角色的使用者才能存取它們。安全性是使用標準的HTTPServletRequest.isUserInRole方法實施的。我們可以使用management.security.enable = false來禁用安全性。只有在執行機構端點在防火牆後存取時,才建議禁用安全性。
在Spring框架中,無論何時bean被使用時,當僅被呼叫一個屬性。可以將這個bean宣告為內部bean。內部bean可以用setter注入「屬性」和構造方法注入「構造引數」的方式來實現。比如,在我們的應用程式中,一個Customer類參照了一個Person類,我們要做的是建立一個Person範例,然後再Customer內部使用。
package com;
public class Customer {
private Person person;
}
class Person{
private int id;
private String name;
private int age;
}
<bean id="CustomerBean" class="com.Customer">
<property name="person">
<bean class="com.person">
<property name="id" value=1 />
<property name="name" value="素小暖" />
<property name="age" value=18 />
</bean>
</property>
</bean>
Spring框架並沒有對單例bean進行任何多執行緒的封裝處理。關於單例bean的執行緒安全和並行問題需要開發者自行去搞定。但實際上,大部分的Spring bean並沒有可變的狀態,所以在某種程度上說Spring的單例bean時執行緒安全的。如果你的bean有多種狀態的話,比如view model,就需要自行保證執行緒安全啦。
最淺顯的解決辦法就是將多型bean的作用域由singleton變更為prototype。
Spring支援IOC,自動裝配不用類範例化,直接從bean容器中取。
1、設定在xml中
<bean id="employeeDAO" class="com.guor.EmployeeDAOImpl" autowire="byName" />
2、@Autowired自動裝配
要使用 @Autowired
,需要註冊 AutowiredAnnotationBeanPostProcessor
,可以有以下兩種方式來實現:
引入組態檔中的<bean>
下引入 <context:annotation-config>
<beans>
<context:annotation-config />
</beans>
在bean組態檔中直接引入AutowiredAnnotationBeanPostProcessor
<beans>
<bean class="org.springframework.beans.factory.annotation.AutowiredAnnotationBeanPostProcessor"/>
</beans>
1、什麼是spring batch?
spring batch是一個輕量級的、完善的批次處理框架,它主要的目的在於幫助企業建立健壯、高效的批次處理應用。
spring batch是Spring的一個子專案,它使用java語言並基於spring框架作為基礎開發,使得已經使用Spring框架的開發者或者是企業可以更加容易存取和利用企業服務。
spring batch提供了大量可重用的元件,包括了紀錄檔、追蹤、事務、任務作業統計、任務重新啟動、跳過、重複、資源管理。
對巨量資料量和高效能的批次處理任務,spring batch同樣提供了高階功能和特性來支援。
例如:分割區功能、遠端功能。
總的來說,spring batch可以支援簡單的、複雜的和巨量資料量的批次處理作業。
2、spring batch業務場景
週期性的提交批次處理
把一個任務並行處理
訊息驅動應用分級處理
大規模並行批次處理
手工或排程使任務失敗之後重新啟動
有依賴步驟的順序執行(使用工作流驅動擴充套件)
處理時跳過部分記錄
成批事務:為小批次的或有的儲存過程/指令碼的場景使用
1、攔截機制的不同
Struts2是類級別的攔截,每次請求就會建立一個Action,和Spring整合時Struts2的ActionBean注入作用域是原型模式prototype,然後通過setter,getter吧request資料注入到屬性。Struts2中,一個Action對應一個request,response上下文,在接收引數時,可以通過屬性接收,這說明屬性引數是讓多個方法共用的。Struts2中Action的一個方法可以對應一個url,而其類屬性卻被所有方法共用,這也就無法用註解或其他方式標識其所屬方法了,只能設計為多例。
SpringMVC是方法級別的攔截,一個方法對應一個Request上下文,所以方法直接基本上是獨立的,獨享request,response資料。而每個方法同時又何一個url對應,引數的傳遞是直接注入到方法中的,是方法所獨有的。處理結果通過ModeMap返回給框架。在Spring整合時,SpringMVC的Controller Bean預設單例模式Singleton,所以預設對所有的請求,只會建立一個Controller,有應為沒有共用的屬性,所以是執行緒安全的,如果要改變預設的作用域,需要新增@Scope註解修改。
Struts2有自己的攔截Interceptor機制,SpringMVC這是用的是獨立的Aop方式,這樣導致Struts2的組態檔量還是比SpringMVC大。
2、底層框架的不同
Struts2採用Filter(StrutsPrepareAndExecuteFilter)實現,SpringMVC(DispatcherServlet)則採用Servlet實現。Filter在容器啟動之後即初始化;服務停止以後墜毀,晚於Servlet。Servlet在是在呼叫時初始化,先於Filter呼叫,服務停止後銷燬。
3、效能方面
Struts2是類級別的攔截,每次請求對應範例一個新的Action,需要載入所有的屬性值注入,SpringMVC實現了零設定,由於SpringMVC基於方法的攔截,有載入一次單例模式bean注入。所以,SpringMVC開發效率和效能高於Struts2。
4、設定方面
spring MVC和Spring是無縫的。從這個專案的管理和安全上也比Struts2高。
@Required註解應用於bean屬性的setter方法,它表明影響的bean屬性在設定時必須放在XML組態檔中。
十九、請舉例說明@Qualifier 註解?
如果在xml中定義了一種型別的多個bean,同時在java註解中又想把其中一個bean物件作為屬性,那麼此時可以使用@Qualifier加@Autowired來達到這一目的,若不加@Qualifier這個註解,在執行時會出現「 No qualifying bean of type [com.tutorialspoint.Student] is defined: expected single matching bean but found 2: student1,student2」這個異常。
通過了解,現在最普遍的許可權管理模型就是RBAC(Role-Based Access Control)。
1、許可權控制分類
選單功能
url控制(控制存取不同的控制器)
2、RBAC的優缺點
(1)優點
簡化了使用者和許可權的關係
易擴充套件、易維護
(2)缺點
RBAC模型沒有提供操作順序的控制機制,這一缺陷使得RBAC模型很難適應哪些對操作次序有嚴格要求的系統。
3、RBAC支援的安全原則
(1)最小許可權原則
RBAC可以將角色設定成其完成任務所需的最小許可權集合。
(2)責任分離原則
可以通過呼叫相互獨立互斥的角色來共同完成敏感的任務,例如要求一個記賬員和財務管理員共同參與統一過賬操作。
(3)資料抽象原則
可以通過許可權的抽象來體現,例如財務操作用借款、存款等抽象許可權,而不是使用典型的讀寫許可權。
4、遠古時代的許可權控制
當時還沒有RBAC,也沒有這個概念,就是一堆程式設計師在那鼓搗,覺得登入這塊該做點什麼。
1、新建一個使用者,對這個使用者進行賦予許可權。
2、但是一旦使用者多了,許可權複雜了,這工作量也是蠻大的。
5、RBAC
直接上圖,一目瞭然,當程式不是很複雜的時候,RBAC就是這樣設計的,我們公司的許可權驗證模組就是這樣設計的。
簡簡單單,五張表,解決。
RBAC2中的一個基本限制是互斥角色的限制,互斥角色是指各自許可權可以互相制約的兩個角色。對於這類角色一個使用者在某一次活動中只能被分配其中的一個角色,不能同時獲得兩個角色的使用權。
該模型有以下幾種約束
6、rbac的實現理論分析
進入登入頁面;
拿到通過post傳過來的使用者名稱和密碼;
使用orm進行過濾查詢;
如果能找到值,則說明登入成功:登入成功後呼叫rbac初始化函數,初始化函數的主要功能是獲取使用者的許可權和選單儲存到session中,並跳轉客戶列表頁面;如果失敗,頁面進行友好提示;
7、url許可權控制關鍵程式碼
1、Spring MVC如何匹配請求路徑
@RequestMapping是用來對映請求的,比如get請求、post請求、或者REST風格與非REST風格的。該註解可以用在類上或方法上,如果用在類上,表示是該類中所有方法的父路徑。
@RequestMapping("/springmvc")
@Controller
public class SpringMVCTest {
@RequestMapping("/testRequestMapping")
public String testRequestMapping(){
System.out.println("testRequestMapping");
return SUCCESS;
}
}
在類上還新增了一個@Controller註解,該註解在SpringMVC中負責處理由DispatcherServlet分發的請求,它把使用者請求的資料經過業務處理層處理之後封裝成一個model,然後再把該model返回給對應的view進行展示。
我們可以通過「springmvc/testRequestMapping」這個路徑來定位到testRequestMapping這個方法,然後執行方法內的方法體。
RequestMapping可以實現模糊匹配路徑,比如:
/springmvc/**/testRequestMapping 就可以匹配/springmvc/stu/getStudentInfo/testRequestMapping 這樣的路徑了。
2、SpringMVC如何獲取請求的引數
該註解用來對映請求URL中繫結的預留位置。通過@PathVariable可以將URL中預留位置的引數繫結到controller處理方法的入參中。
@RequestMapping("/testPathVariable/{id}")
public String testPathVariable(@PathVariable(value="id") Integer id){
System.out.println("testPathVariable:" + id);
return SUCCESS;
}
在index.jsp中我們新增一條連線,用來觸發一個請求:
<a href="springmvc/testPathVariable/1">testPathVariable</a>
(2) @RequestParam
該註解也是用來獲取請求引數的,那麼該註解和@PathVariable有什麼不同呢?
@RequestMapping(value="/testRequestParam")
public String testRequestParam(@RequestParam(value="username") String username, @RequestParam(value="age", required=false, defaultValue="0") int age){
System.out.println("testRequestParam" + " username:" + username + " age:" +age);
return SUCCESS;
}
在index.jsp新增超連結標籤
<a href="springmvc/testRequestParam?username=jackie&age=12">testRequestParam</a>
3、REST風格的請求
@RequestMapping(value="/testRest/{id}", method=RequestMethod.PUT)
public String testRestPut(@PathVariable(value="id") Integer id){
System.out.println("test put:" + id);
return SUCCESS;
}
@RequestMapping(value="/testRest/{id}", method=RequestMethod.DELETE)
public String testRestDelete(@PathVariable(value="id") Integer id){
System.out.println("test delete:" + id);
return SUCCESS;
}
@RequestMapping(value="/testRest", method=RequestMethod.POST)
public String testRest(){
System.out.println("test post");
return SUCCESS;
}
@RequestMapping(value="/testRest/{id}", method=RequestMethod.GET)
public String testRest(@PathVariable(value="id") Integer id){
System.out.println("test get:" + id);
return SUCCESS;
}
1、通過JavaScript遮蔽提交按鈕(不推薦)
2、給資料庫增加唯一鍵約束(簡單粗暴)
3、利用Session防止表單重複提交(推薦)
4、使用AOP自定義切入實現
1、簡單工廠模式
簡單工廠模式的本質就是一個工廠類根據傳入的引數,動態的決定範例化哪個類。
Spring中的BeanFactory就是簡單工廠模式的體現,根據傳入一個唯一的標識來獲得bean物件。
2、工廠方法模式
應用程式將物件的建立及初始化職責交給工廠物件,工廠Bean。
定義工廠方法,然後通過config.xml組態檔,將其納入Spring容器來管理,需要通過factory-method指定靜態方法名稱。
3、單例模式
Spring用的是雙重判斷加鎖的單例模式,通過getSingleton方法從singletonObjects中獲取bean。
/**
* Return the (raw) singleton object registered under the given name.
* <p>Checks already instantiated singletons and also allows for an early
* reference to a currently created singleton (resolving a circular reference).
* @param beanName the name of the bean to look for
* @param allowEarlyReference whether early references should be created or not
* @return the registered singleton object, or {@code null} if none found
*/
protected Object getSingleton(String beanName, boolean allowEarlyReference) {
Object singletonObject = this.singletonObjects.get(beanName);
if (singletonObject == null && isSingletonCurrentlyInCreation(beanName)) {
synchronized (this.singletonObjects) {
singletonObject = this.earlySingletonObjects.get(beanName);
if (singletonObject == null && allowEarlyReference) {
ObjectFactory<?> singletonFactory = this.singletonFactories.get(beanName);
if (singletonFactory != null) {
singletonObject = singletonFactory.getObject();
this.earlySingletonObjects.put(beanName, singletonObject);
this.singletonFactories.remove(beanName);
}
}
}
}
return (singletonObject != NULL_OBJECT ? singletonObject : null);
}
4、代理模式
Spring的AOP中,使用的Advice(通知)來增強被代理類的功能。Spring實現AOP功能的原理就是代理模式(① JDK動態代理,② CGLIB位元組碼生成技術代理。)對類進行方法級別的切面增強。
5、裝飾器模式
裝飾器模式:動態的給一個物件新增一些額外的功能。
Spring的ApplicationContext中設定所有的DataSource。這些DataSource可能是不同的資料庫,然後SessionFactory根據使用者的每次請求,將DataSource設定成不同的資料來源,以達到切換資料來源的目的。
在Spring中有兩種表現:
一種是類名中含有Wrapper,另一種是類名中含有Decorator。
6、觀察者模式
定義物件間的一對多的關係,當一個物件的狀態發生改變時,所有依賴於它的物件都得到通知並自動更新。
Spring中觀察者模式一般用在listener的實現。
7、策略模式
策略模式是行為性模式,呼叫不同的方法,適應行為的變化 ,強調父類別的呼叫子類的特性 。
getHandler是HandlerMapping介面中的唯一方法,用於根據請求找到匹配的處理器。
8、模板方法模式
Spring JdbcTemplate的query方法總體結構是一個模板方法+回撥函數,query方法中呼叫的execute()是一個模板方法,而預期的回撥doInStatement(Statement state)方法也是一個模板方法。
Spring注入有四種方式,
想要注入java collection,就是注入集合類:
list和set都使用value標籤;map使用entry標籤;props使用prop標籤;
1、mybatis 是否支援延遲載入?
延遲載入其實就是講資料載入時機推遲,比如推遲巢狀查詢的時機。
延遲載入可以實現先查詢主表,按需實時做關聯查詢,返回關聯表結果集,一定程度上提高了效率。
mybatis僅支援關聯物件association和關聯集合物件collection的延遲載入,association是一對一,collection是一對多查詢,在mybatis組態檔中可以設定lazyloadingEnable=true/false。
2、延遲載入的原理是什麼?
使用CGLIB為目標物件建立代理物件,當呼叫目標物件的方法時進入攔截器方法。
比如呼叫a.getB().getName(),攔截器方法invoke()發現a.getB()為null,會單獨傳送事先準備好的查詢關聯B物件的sql語句,把B查詢出來然後呼叫a.setB(b),也是a的物件的屬性b就有值了,然後呼叫getName(),這就是延遲載入的原理。
一級快取是session級別的快取,預設開啟,當查詢一次資料庫時,對查詢結果進行快取,如果之後的查詢在一級快取中存在,則無需再存取資料庫;
二級快取是sessionFactory級別的快取,需要設定才會開啟。當進行sql語句查詢時,先檢視一級快取,如果不存在,存取二級快取,降低資料庫存取壓力。
1、mybatis有三種基本的Executor執行器:
(1)、SimpleExecutor
每執行一次update或select,就開啟一個Statement物件,用完立刻關閉Statement物件。
(2)、PauseExecutor
執行update或select,以sql做為key查詢Statement物件,存在就使用,不存在就建立,用完後,不關閉Statement物件,而且放置於Map內,供下一次使用。簡言之,就是重複使用Statement物件。
(3)、BatchExecutor
執行update,將所有sql通過addBatch()都新增到批次處理中,等待統一執行executeBatch(),它快取了多個Statement物件,每個Statement物件都是addBatch()完畢後,等待逐一執行executeBatch()批次處理。與JDBC批次處理相同。
2、作用範圍:
Executor的這些特點,都嚴格限制在SqlSession生命週期範圍內。
3、Mybatis中如何指定使用哪一種Executor執行器?
在mybatis的組態檔中,可以指定預設的ExecutorType執行器型別,也可以手動給DefaultSqlSessionFactory的建立SqlSession的方法傳遞ExecutorType型別引數。
1、兩者最大的區別
針對簡單邏輯,都有對應的程式碼生成工具,可以生成簡單基本的dao層方法;
針對高階查詢,mybatis要手動編寫sql語句和resultMap,而hibernate有良好的對映機制;
2、開發難度對比
hibernate > mybatis
3、紀錄檔統計
hibernate有自己的紀錄檔統計功能,而mybatis需要藉助log4j來記錄紀錄檔。
4、資料庫擴充套件比較
hibernate > mybatis
5、快取機制比較
因為hibernate對查詢物件有良好的管理機制,使用者無需關心sql,所以使用二級快取如果出現髒資料,系統會報錯。
而mybatis,如果不能獲取最新資料,應該避免快取的使用,髒資料的出現會給系統的正常執行帶來很大的隱患。
6、如何選擇
myBatis查詢多個id(我居然回答用物件來傳遞...)
Page<UserPoJo> getUserListByIds(@Param("ids") List<Integer> ids);
<!--根據id列表批次查詢user-->
<select id="getUserListByIds" resultType="com.guor.UserPoJo">
select * from student
where id in
<foreach collection="ids" item="userid" open="(" close=")" separator=",">
#{userid}
</foreach>
</select>
1、一級快取:指的是mybatis中sqlSession物件的快取,當我們執行查詢以後,查詢的結果會同時存入sqlSession中,再次查詢的時候,先去sqlSession中查詢,有的話直接拿出,當sqlSession消失時,mybatis的一級快取也就消失了,當呼叫sqlSession的修改、新增、刪除、commit()、close()等方法時,會清空一級快取。
2、二級快取:指的是mybatis中的sqlSessionFactory物件的快取,由同一個sqlSessionFactory物件建立的sqlSession共用其快取,但是其中快取的是資料而不是物件。當命中二級快取時,通過儲存的資料構造成物件返回。查詢資料的時候,查詢的流程是二級快取 > 一級快取 > 資料庫。
3、如果開啟了二級快取,sqlSession進行close()後,才會把sqlSession一級快取中的資料新增到二級快取中,為了將快取資料取出執行反序列化,還需要將要快取的pojo實現Serializable介面,因為二級快取資料儲存媒介多種多樣,不一定只存在記憶體中,也可能存在硬碟中。
4、mybatis框架主要是圍繞sqlSessionFactory進行的,具體的步驟:
5、程式碼範例
mybatis-config.xml
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE configuration PUBLIC "-//mybatis.org//DTD Config 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-config.dtd">
<configuration>
<!-- 載入類路徑下的屬性檔案 -->
<properties resource="db.properties"/>
<!-- 設定型別別名 -->
<typeAliases>
<typeAlias type="cn.itcast.javaee.mybatis.app04.Student" alias="student"/>
</typeAliases>
<!-- 設定一個預設的連線環境資訊 -->
<environments default="mysql_developer">
<!-- 連線環境資訊,取一個任意唯一的名字 -->
<environment id="mysql_developer">
<!-- mybatis使用jdbc事務管理方式 -->
<transactionManager type="jdbc"/>
<!-- mybatis使用連線池方式來獲取連線 -->
<dataSource type="pooled">
<!-- 設定與資料庫互動的4個必要屬性 -->
<property name="driver" value="${mysql.driver}"/>
<property name="url" value="${mysql.url}"/>
<property name="username" value="${mysql.username}"/>
<property name="password" value="${mysql.password}"/>
</dataSource>
</environment>
<!-- 連線環境資訊,取一個任意唯一的名字 -->
<environment id="oracle_developer">
<!-- mybatis使用jdbc事務管理方式 -->
<transactionManager type="jdbc"/>
<!-- mybatis使用連線池方式來獲取連線 -->
<dataSource type="pooled">
<!-- 設定與資料庫互動的4個必要屬性 -->
<property name="driver" value="${oracle.driver}"/>
<property name="url" value="${oracle.url}"/>
<property name="username" value="${oracle.username}"/>
<property name="password" value="${oracle.password}"/>
</dataSource>
</environment>
</environments>
<!-- 載入對映檔案-->
<mappers>
<mapper resource="cn/itcast/javaee/mybatis/app14/StudentMapper.xml"/>
</mappers>
</configuration>
public class MyBatisTest {
public static void main(String[] args) {
try {
//讀取mybatis-config.xml檔案
InputStream resourceAsStream = Resources.getResourceAsStream("mybatis-config.xml");
//初始化mybatis,建立SqlSessionFactory類的範例
SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(resourceAsStream);
//建立session範例
SqlSession session = sqlSessionFactory.openSession();
/*
* 接下來在這裡做很多事情,到目前為止,目的已經達到得到了SqlSession物件.通過呼叫SqlSession裡面的方法,
* 可以測試MyBatis和Dao層介面方法之間的正確性,當然也可以做別的很多事情,在這裡就不列舉了
*/
//插入資料
User user = new User();
user.setC_password("123");
user.setC_username("123");
user.setC_salt("123");
//第一個引數為方法的完全限定名:位置資訊+對映檔案當中的id
session.insert("com.cn.dao.UserMapping.insertUserInformation", user);
//提交事務
session.commit();
//關閉session
session.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
注意:但凡是sql注入漏洞的程式,都是因為程式要接受來自使用者端使用者輸入的變數或URL傳遞的引數,並且這個變數或引數是組成sql語句的一部分,對於使用者輸入的內容或傳遞的引數,我們應該要時刻保持警惕,這是安全領域裡的【外部資料不可信任】的原則,縱觀web安全領域的各種攻擊方式,大多數都是因為開發者違反了這個原則而導致的,所以自然能想到,就是變數的檢測、過濾、驗證下手,確保變數是開發者所預想的。
1、檢查變數資料型別和格式
資料型別檢查,sql執行前,要進行資料型別檢查,如果是郵箱,引數就必須是郵箱的格式,如果是日期,就必須是日期格式;
只要是有固定格式的變數,在SQL語句執行前,應該嚴格按照固定格式去檢查,確保變數是我們預想的格式,這樣很大程度上可以避免SQL隱碼攻擊。
如果上述例子中id是int型的,效果會怎樣呢?無法注入,因為輸入注入引數會失敗。比如上述中的name欄位,我們應該在使用者註冊的時候,就確定一個使用者名稱規則,比如5-20個字元,只能由大小寫字母、數位以及漢字組成,不包含特殊字元。此時我們應該有一個函數來完成統一的使用者名稱檢查。不過,仍然有很多場景並不能用到這個方法,比如寫部落格,評論系統,彈幕系統,必須允許使用者可以提交任意形式的字元才行,否則使用者體驗感太差了。
2、過濾特殊符號
3、繫結變數,使用預編譯語句
146、為什麼要使用 hibernate?
spring.jpa.properties.hibernate.show_sql=true
spring.jpa.properties.hibernate.format_sql=true
spring.jpa.properties.hibernate.use_sql_comments=true
1、導航物件圖查詢:根據已載入的物件,導航到其他物件。
例如,對於已經載入的Customer物件,呼叫它的getOrders().iterator()方法就可以導航到所有關聯的Order物件,假如在關聯級別使用了延遲載入檢索策略,那麼首次執行此方法時,hibernate會從資料庫中載入關聯的Order物件,否則就從快取中獲得Order物件。
2、OID方式:按照物件的OID來檢索物件
Session的get()和load()方法提供了這種功能,如果在應用程式中先知道了OID,就可以使用這種方式檢索物件。
get()和load()的用法完全一樣,都需要兩個引數,一個是持久化物件類名class,一個是行號OID,返回固定的某一行的資料,但是需要注意的是,當輸入的OID不存在時,get()會返回一個空物件,load()則直接報錯。
3、HQL檢索方式:(hibernate query language)
使用物件導向的HQL查詢語言,session的find()方法用於執行HQL查詢語句。此外,hibernate還提供了query介面,它是hibernate提供的專門的HQL查詢介面,能夠執行各種複雜的HQL查詢語句。
它具備以下功能:
例如:
Query query = session.createQuery(「from UserPo」);
獲得一個query物件,注意引數字串中不是一個SQL語句,from後面的是持久化物件名稱;
List list = query.list();
就可以獲取資料庫中對應表的資料集合。
4、QBC檢索方式:Query By Criteria的API來檢索物件
這種API封裝了基於字串形式的查詢語句,提供了更加物件導向的介面。
例:Criteria criteria = session.createCriteria(UserPo.class);
建立一個Criteria物件,引數是所關聯的持久化物件,criteria.add(Restrictions.ge("id",2));將查詢條件加入物件中,後面的操作就和Query物件一樣了。
5、本地SQL
使用本地資料庫的SQL查詢語句,hibernate會負責把檢索到的JDBC ResultSet結果對映為持久化物件圖。
可以將hibernate的實體類定義為final,但這種做法不好。
因為hibernate會使用代理模式在延遲關聯的情況下提高效能,如果你把實體類定義成final類之後,因為Java不允許對final類進行擴充套件,所以hibernate就無法再使用代理了,如此一來就限制了使用可以提升效能的手段。
不過,如果你的持久化類實現了一個介面,而且在該介面中宣告了所有定義於實體類中的所有public的方法的話,就能避免出現前面所說的不利後果。
hibernate是物件導向的ORM,所以一般定義成封裝型別,要看資料庫中的定義,如果資料庫中有對應欄位存在null值,就要定義Integer。也可以定義基本型別,在組態檔中寫清楚即可。
1、Spring Boot簡介
基於Spring4.0設計,不僅繼承了Spring框架原有的優秀特性,而且還通過簡化設定來進一步簡化spring應用的整個搭建和開發過程。另外SpringBoot通過整合大量的框架使得依賴包的版本衝突、參照的不穩定性得到了解決。
2、Spring Boot 有哪些優點?
3、SpringBoot有哪些缺點?
監聽器也叫listener,是servlet的監聽器,可以用於監聽web應用程式中某些物件的建立、銷燬、增加、修改、刪除等動作的發生,然後做出相應的響應處理。當範圍物件的狀態發生變化時,伺服器自動呼叫監聽器物件中的方法,常用於系統載入時進行資訊初始化,統計線上人數和線上使用者,統計網站的存取量。
設定監聽器的方法:
通過@Component把監聽器加入Spring容器中管理;
在application.properties中新增context.listener.classes設定;
在方法上加@EventListener註解;
YAML是JSON的一個超集,可以非常方便地將外部設定以層次結構形式儲存起來。YAML可以作為properties組態檔的替代。
YAML使用的注意事項:
使用Spring Data Jpa可以實現將可分頁的傳遞給儲存庫方法。
1、使用 @ExceptionHandler 註解處理區域性異常(只能處理當前controller中的ArithmeticException和NullPointerException異常,缺點就是隻能處理單個controller的異常)
@Controller
public class ExceptionHandlerController {
@RequestMapping("/excep")
public String exceptionMethod(Model model) throws Exception {
String a=null;
System.out.println(a.charAt(1));
int num = 1/0;
model.addAttribute("message", "沒有丟擲異常");
return "index";
}
@ExceptionHandler(value = {ArithmeticException.class,NullPointerException.class})
public String arithmeticExceptionHandle(Model model, Exception e) {
model.addAttribute("message", "@ExceptionHandler" + e.getMessage());
return "index";
}
}
2、使用 @ControllerAdvice + @ExceptionHandler 註解處理全域性異常(value後面可以填寫陣列)
@ControllerAdvice
public class ControllerAdviceException {
@ExceptionHandler(value = {NullPointerException.class})
public String NullPointerExceptionHandler(Model model, Exception e) {
model.addAttribute("message", "@ControllerAdvice + @ExceptionHandler :" + e.getMessage());
return "index";
}
}
3、設定 SimpleMappingExceptionResolver 類處理異常(設定類)
@Configuration
public class SimpleMappingException {
@Bean
public SimpleMappingExceptionResolver getSimpleMappingExceptionResolver(){
SimpleMappingExceptionResolver resolver = new SimpleMappingExceptionResolver();
Properties mappings = new Properties();
//第一個引數為異常全限定名,第二個為跳轉檢視名稱
mappings.put("java.lang.NullPointerException", "index");
mappings.put("java.lang.ArithmeticException", "index");
//設定異常與檢視對映資訊的
resolver.setExceptionMappings(mappings);
return resolver;
}
}
4、實現 HandlerExceptionResolver 介面處理異常
@Configuration
public class HandlerException implements HandlerExceptionResolver {
@Override
public ModelAndView resolveException(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) {
ModelAndView modelAndView = new ModelAndView();
modelAndView.addObject("message", "實現HandlerExceptionResolver介面");
//判斷不同異常型別,做不同檢視跳轉
if(ex instanceof NullPointerException){
modelAndView.setViewName("index");
}
if(ex instanceof ArithmeticException){
modelAndView.setViewName("index");
}
return modelAndView;
}
}
1、概念
單點登入SSO,說的是在一個多系統共存的環境下,使用者在一處登入後,就不用在其他系統中登入,也就是使用者的一次登入能得到其他所有系統的信任。
2、單點登入的要點
儲存信任;
驗證信任;
3、實現單點登入的三種方式
(1)以cookie作為憑證
最簡單的單點登入實現方式,是使用cookie作為媒介,存放使用者憑證。
使用者登入父應用之後,應用返回一個加密的cookie,當使用者存取子應用的時候,攜帶上這個cookie,授權應用解密cookie進行校驗,校驗通過則登入當前使用者。
缺點:
cookie不安全
通過加密可以保證安全性,但如果對方掌握瞭解密演演算法就完蛋了。
不能跨域實現免登
(2)通過JSONP實現
對於跨域問題,可以使用JSONP實現。使用者在父應用中登入後,跟session匹配的cookie會存到使用者端中,當使用者需要登入子應用的時候,授權應用存取父應用提供的JSONP介面,並在請求中帶上父應用域名下的cookie,父應用接收到請求,驗證使用者的登入狀態,返回加密的資訊,子應用通過解析返回來的加密資訊來驗證使用者,如果通過驗證則登入使用者。
缺點:
這種方法雖然能解決跨域問題,但是治標不治本,沒有解決cookie安全性的問題。
(3)通過頁面重定向的方式
最後一種介紹的方式,是通過父應用和子應用來回重定向進行通訊,實現資訊的安全傳遞。
父應用提供一個GET方式的登入介面A(此時的父應用介面固定,攻擊者無法去偽造),使用者通過子應用重定向連線的方式存取這個介面,如果使用者還沒有登入,則返回一個登入頁面,使用者輸入賬號密碼進行登入,如果使用者已經登入了,則生成加密的token,並且重定向到子應用提供的驗證token的介面B(此時的子應用介面固定,攻擊者無法去偽造),通過解密和校驗之後,子應用登入當前使用者。
缺點:
這種方式較前面的兩種方式,是解決了安全性和跨域的問題,但是並沒有前面兩種方式簡單,安全與方便,本來就是矛盾的。
4、使用獨立登入系統
一般來說,大型應用會把授權的邏輯和使用者資訊的相關邏輯獨立成一個應用,稱為使用者中心。使用者中心不處理業務邏輯,只是處理使用者資訊的管理以及授權給第三方應用。第三方應用需要登入的時候,則把使用者的登入請求轉發給使用者中心進行處理,使用者處理完畢後返回憑證,第三方應用驗證憑證,通過後就登入使用者。
5、sso(單點登入)與OAuth2.0(授權)的區別?
(1)sso(單點登入)
通常處理的是一個公司的不同應用間的存取登入問題,如企業應用有很多子系統,只需登入一個系統,就可以實現不同子系統間的跳轉,而避免了登入操作;
通過cookie、jsonp、重定向來實現;
(2)OAuth2.0(授權)
解決的是服務提供方(如微信)給第三方應用授權的問題,簡稱微信登入;
是一種具體的協定,只是為使用者資源的授權提供了一個安全的、開放的而又簡易的標準,OAuth2.0(授權)為客戶開發者開發web應用,桌面應用程式,移動應用及客廳裝置提供特定的授權流程。
Spring和Spring Boot都支援maven和Gradle通用打包管理技術。
Spring Boot相對Spring的一些優點:
最近專案是分散式的專案,都是通過分專案打包部署,然後部署在docker中執行。
可以使用druidDataSource建立DataSource,然後通過jdbcTemplate執行sql。
通過監聽session物件的方式來實現線上人數的統計和線上人資訊展示,並且讓超時的自動銷燬。
對session物件實現監聽,首先必須繼承HttpSessionListener類,該程式的基本原理就是當瀏覽器存取頁面的時候必定會產生一個session物件,當關閉該頁面的時候必然會刪除session物件。所以每當產生一個新的session物件就讓線上人數+1,當刪除一個session物件就讓線上人數-1。
還要繼承一個HttpSessionAttributeListener,來實現對其屬性的監聽。分別實現attributeAdded方法,attributeReplace方法以及attributeRemove方法。
sessionCreated//新建一個對談的時候觸發,也可以說是使用者端第一次喝伺服器互動時觸發。
sessionDestroyed//銷燬對談的時候,一般來說只有某個按鈕觸發進行銷燬,或者設定定時銷燬。
HttpSessionAttributeListener有三個方法需要實現
attributeAdded//在session中新增物件時觸發此操作 籠統的說就是呼叫setAttribute這個方法時候會觸發的
attributeRemoved//修改、刪除session中新增物件時觸發此操作 籠統的說就是呼叫 removeAttribute這個方法時候會觸發的
attributeReplaced//在Session屬性被重新設定時。
非同步讀取
新建一個 ExcelModelListener 監聽類出來,並且 繼承 AnalysisEventListener 類
package com.zh.oukele.listener;
import com.alibaba.excel.context.AnalysisContext;
import com.alibaba.excel.event.AnalysisEventListener;
import com.zh.oukele.model.ExcelMode;
import java.util.ArrayList;
import java.util.List;
/***
* 監聽器
*/
public class ExcelModelListener extends AnalysisEventListener<ExcelMode> {
/**
* 每隔5條儲存資料庫,實際使用中可以3000條,然後清理list ,方便記憶體回收
*/
private static final int BATCH_COUNT = 5;
List<ExcelMode> list = new ArrayList<ExcelMode>();
private static int count = 1;
@Override
public void invoke(ExcelMode data, AnalysisContext context) {
System.out.println("解析到一條資料:{ "+ data.toString() +" }");
list.add(data);
count ++;
if (list.size() >= BATCH_COUNT) {
saveData( count );
list.clear();
}
}
@Override
public void doAfterAllAnalysed(AnalysisContext context) {
saveData( count );
System.out.println("所有資料解析完成!");
System.out.println(" count :" + count);
}
/**
* 加上儲存資料庫
*/
private void saveData(int count) {
System.out.println("{ "+ count +" }條資料,開始儲存資料庫!" + list.size());
System.out.println("儲存資料庫成功!");
}
}
Swagger是用於生成RestFul Web服務的視覺化表示工具,它使檔案和伺服器視覺化更新;
當定義好Swagger後,可以呼叫伺服器端介面,來檢視介面的返回值,驗證返回資料的正確性;
1、列不可再分;
2、每一行資料只做一件事,只與一列相關,主鍵;
3、每個屬性都與主鍵有直接關係,而不是間接關係;
三大正規化只是設計資料庫的基本理念,可以建立冗餘較小、結構合理的資料庫。如果有特殊情結,當然要特殊對待,資料庫設計最重要的是看需求和效能,需求>效能>表結構。
所以不能一味的追求三正規化建立資料庫。
一般情況下,我們建立的表型別是InnoDB。
不重新啟動MySQL,如果新增一條記錄,id是8;
重新啟動,ID是6;因為InnoDB表只把自增主鍵的最大ID記錄在記憶體中,如果重新啟動,已刪除的最大ID會丟失。
如果表型別是MyISAM,重新啟動之後,最大ID也不會丟失,ID是8;
InnoDB必須有主鍵(建議使用自增主鍵,不用UUID,自增主鍵索引查詢效率高)、支援外來鍵、支援事務、支援行級鎖。
系統崩潰後,MyISAM很難恢復;
綜合考慮,優先選擇InnoDB,MySQL預設也是InnoDB。
//MySQL,,mysql -v
select version();
//Oracle
select * from v$version;
ACID是資料庫事務執行的四大基本要素,包括原子性(Atomicity)、一致性(Consistency)、隔離性(Isolation)、永續性(Durability)。
1、原子性
整個事務中的所有操作,要麼全部完成,要不全部不完成,不可能停滯在中間某個環節。事務在執行過程中發生錯誤,會被roolback回滾到事務開始前的狀態,就像這個事務從未執行過一樣。
2、一致性
事務必須始終保持系統處於一致的狀態,不管在任何給定的時間並行事務有多少。
3、隔離性
隔離狀態執行事務,使他們好像是系統在給定時間內執行的唯一操作。
如果有兩個事務,執行在相同的時間內,執行相同的功能,事務的隔離性確保每一個事務在系統中認為只有自己在使用系統。這種屬性稱為序列化,為了防止事務操作間的混淆,必須序列化或序列化請求,使得在同一時間僅有一個請求用於同一資料。
4、永續性
一個成功的事務將永久的改變系統的狀態。
1、float 和 double 的區別是什麼?
(1)記憶體中佔有的位元組數不同
單精度浮點數在記憶體中佔有4個位元組;
雙精度浮點數在記憶體中佔有8個位元組;
(2)有效數位位數不同
單精度浮點數有效數位8位元;
雙精度浮點數有效數位16位元;
(3)數值取值範圍不同
單精度浮點數的表示範圍:-3.40E+38~3.40E+38
雙精度浮點數的表示範圍:-1.79E+308~-1.79E+308
(4)在程式中處理速度不同
一般來說,CPU處理單精度浮點數的速度比雙精度浮點數的速度快
如果不宣告,預設小數是double型別,如果想用float,要進行強轉;
2、例如
float f = 1.3;會編譯報錯,正確的寫法是float f = (float)1.3;或者float a = 1.3f;(f或F都可以不區分大小寫)
3、注意
float是八位有效數位,第七位會四捨五入;
4、面試題
(1)java中3*0.1==0.3將會返回什麼?true還是false?
答:返回false,因為浮點數不能完全精確的表示出來,一般會損失精度;
(2)java中float f = 3.4;是否正確?
答:不正確。因為3.4是雙精度浮點數,將雙精度賦給單精度屬於向下轉型,會造成精度損失,因此需要強制型別轉換float=(float)3.4;或者寫成float f = 3.4f;
#不帶排序的
SELECT * FROM (
SELECT ROWNUM AS rowno, t.* FROM worker t where ROWNUM <=20) table_alias
WHERE table_alias.rowno > 10;
#帶排序的
SELECT * FROM (
SELECT tt.*, ROWNUM AS rowno FROM (
SELECT t.* FROM worker t ORDER BY wkid aSC) tt WHERE ROWNUM <= 20) table_alias
WHERE table_alias.rowno >= 10;
1、主鍵約束
主鍵列上沒有任何兩行具有相同值(即重複值),不允許空(NULL);
2、唯一性約束
保證一個欄位或者一組欄位裡的資料都與表中其它行的對應資料不同。和主鍵約束不同,唯一性約束允許為null,但是隻能有一行;
3、唯一性索引
不允許具有索引值相同的行,從而禁止重複的索引和鍵值;
4、三者的區別
1、資料庫設計最起碼要佔用這個專案開發的40%以上的時間
2、資料庫設計不僅僅停留在頁面demo的表面
頁面內容所需欄位,在資料庫設計中只是一部分,還有系統運轉、模組互動、中轉資料、表之間的聯絡等等所需要的欄位,因此資料庫設計絕對不是簡單的基本資料儲存,還有邏輯資料儲存。
3、資料庫設計完成後,專案80%的設計開發都要存在你的腦海中
每個欄位的設計都要有他存在的意義,要清楚的知道程式中如何去運用這些欄位,多張表的聯絡在程式中是如何體現的。
4、資料庫設計時就要考慮效率和優化問題
資料量大的表示粗粒度的,會冗餘一些必要欄位,達到用最少的表,最弱的表關係去儲存海量的資料。巨量資料的表要建立索引,方便查詢。對於含有計算、資料互動、統計這類需求時,還有考慮是否有必要採用儲存過程。
5、新增必要的冗餘欄位
像建立時間、修改時間、操作使用者IP、備註這些欄位,在每張表中最好都有,一些冗餘的欄位便於日後維護、分析、拓展而新增。
6、設計合理的表關聯
若兩張表之間的關係複雜,建議採用第三張對映表來關聯維護兩張表之間的關係,以降低表之間的直接耦合度。
7、設計表時不加主外來鍵等約束關聯,系統編碼階段完成後再新增約束性關聯
8、選擇合適的主鍵生成策略
資料庫的設計難度其實比單純的技術實現難很多,他充分體現了一個人的全域性設計能力和掌控能力,最後說一句,資料庫設計,很重要,很複雜。
區分度不高的欄位不適合做索引,因為索引頁是需要有開銷的,需要儲存的,不過這類欄位可以做聯合索引的一部分。
1、查詢重複的單個欄位(group by)
select 重複欄位A, count(*) from 表 group by 重複欄位A having count(*) > 1
2、查詢重複的多個欄位(group by)
select 重複欄位A, 重複欄位B, count(*) from 表 group by 重複欄位A, 重複欄位B having count(*) > 1
1、選取適合的欄位屬性
2、使用join連線代替子查詢
3、使用聯合union來代替手動建立的臨時表
注意:union用法中,兩個select語句的欄位型別要匹配,而且欄位個數要相同。
4、事務
要麼都成功,要麼都失敗。
可以保證資料庫中資料的一致性和完整性。事務以begin開始,commit關鍵字結束。
如果出錯,rollback命令可以將資料庫恢復到begin開始之前的狀態。
事務的另一個重要作用是當多個使用者同時使用相同的資料來源時,它可以利用鎖定資料庫的方式為使用者提供一種安全的存取方式,這樣就可以保證使用者的操作不被其他的使用者干擾。
5、鎖定表
儘管事務是維護資料庫完整性的一個非常好的方法,但卻因為它的獨佔性,有時會影響資料庫的效能,尤其是在大應用中。
由於在事務執行的過程中,資料庫會被鎖定,因此其它使用者只能暫時等待直到事務結束。
有的時候可以用鎖定表的方法來獲得更好的效能,
共用鎖:其它使用者只能看,不能修改
lock table person in share mode;
對於通過lock table 命令主動新增的鎖來說,如果要釋放它們,只需發出rollback命令即可。
6、使用外來鍵
鎖定表的方法可以維護資料的完整性,但是它卻不能保證資料的關聯性,這個時候可以使用外來鍵。
7、使用索引
索引是提高資料庫查詢速度的常用方法,尤其是查詢語句中包含max()、min()、order by這些命令的時候,效能提高更為顯著。
一般來說索引應該建在常用於join、where、order by的欄位上。儘量不要對資料庫中含有大量重複的值得欄位建立索引。
8、優化的查詢語句
在索引的欄位上儘量不要使用函數進行操作。
儘量不要使用like關鍵字和萬用字元,這樣做法很簡單,但卻是以犧牲效能為代價的。
避免在查詢中進行自動型別轉換,因為型別轉換也會使索引失效。
1、解決非同步問題
例如使用者註冊,傳送郵件和簡訊反饋註冊成功,可以使用RabbitMQ訊息佇列,使用者無需等待反饋。
2、服務間解耦
訂單系統和庫存系統,中間加入RabbitMQ訊息佇列,當庫存系統出現問題時,訂單系統依舊能正常使用,降低服務間耦合度。
3、秒殺系統
利用RabbitMQ的最大值,實現秒殺系統。
1、RabbitMQ有哪些重要的角色?
使用者端、RabbitMQ、伺服器端。
2、有哪些重要的元件?
(1)connectionFactory(連線管理器)
應用程式與RabbitMQ之間建立連線的管理器。
(2)Channel(通道)
訊息推播使用的通道。
(3)RoutingKey(路由鍵)
用於把生產者的資料分配到交換機上。
(4)Exchange(交換機)
用於接受和分配訊息。
(5)BindKey(繫結鍵)
用於把交換機的訊息繫結到佇列上
(6)Queue(佇列)
用於儲存生產者訊息。
vhost可以理解為mini版的RabbitMQ,其內部均含有獨立的交換機、繫結、佇列,最重要的是擁有獨立的許可權系統,可以做到vhost範圍內的使用者控制。從RabbitMQ全域性考慮,不同的應用可以跑在不同的vhost上,作為不同許可權隔離的手段。
JVM包括類載入子系統、堆、方法區、棧、本地方法棧、程式計數器、直接記憶體、垃圾回收器、執行引擎。
1、類載入子系統
類載入子系統負責載入class資訊,載入的類資訊存放於方法區中。
2、直接記憶體
直接記憶體是在Java堆外的、直接向系統申請的記憶體空間。存取直接記憶體的速度會由於Java堆。出於效能的考慮,讀寫頻繁的場合可能會考慮使用直接記憶體。
3、垃圾回收器
垃圾回收器可以對堆、方法區、直接記憶體進行回收。
4、執行引擎
執行引擎負責執行虛擬機器器的位元組碼,虛擬機器器會使用即時編譯技術將方法編譯成機器碼後再執行。
執行時資料區包括堆、方法區、棧、本地方法棧、程式計數器。
1、堆
堆解決的是物件範例儲存的問題,垃圾回收器管理的主要區域。
2、方法區
方法區可以認為是堆的一部分,用於儲存已被虛擬機器器載入的資訊,常數、靜態變數、即時編譯器編譯後的程式碼。
3、棧
棧解決的是程式執行的問題,棧裡面存的是棧幀,棧幀裡面存的是區域性變數表、運算元棧、動態連結、方法出口等資訊。
(1)棧幀
每個方法從呼叫到執行的過程就是一個棧幀在虛擬機器器棧中入棧到出棧的過程。
(2)區域性變數表
用於儲存函數的引數和區域性變數。
(3)運算元棧
運算元棧又稱操作棧,大多數指令都是從這裡彈出資料,執行運算,然後把結果壓回運算元棧。
4、本地方法棧
與棧功能相同,本地方法棧執行的是本地方法,一個Java呼叫非Java程式碼的介面。
5、程式計數器(PC暫存器)
程式計數器中存放的是當前執行緒所執行的位元組碼的行數。JVM工作時就是通過改變這個計數器的值來選取下一個需要執行的位元組碼指令。
1、什麼是類載入器?
類載入器負責載入所有的類,其為所有被載入記憶體的類生成一個java.lang.Class範例物件。
2、類載入器有哪些?
JVM有三種類載入器:
(1)啟動類載入器
該類沒有父載入器,用來載入Java的核心類,啟動類載入器的實現依賴於底層作業系統,屬於虛擬機器器實現的一部分,它並不繼承自java.lang.classLoader。
(2)擴充套件類載入器
它的父類別為啟動類載入器,擴充套件類載入器是純java類,是ClassLoader類的子類,負責載入JRE的擴充套件目錄。
(3)應用程式類載入器
它的父類別為擴充套件類載入器,它從環境變數classpath或者系統屬性java.lang.path所指定的目錄中載入類,它是自定義的類載入器的父載入器。
當程式主動使用某個類時,如果該類還未被載入到記憶體中,JVM會通過載入、連線、初始化3個步驟對該類進行類載入。
1、載入
載入指的是將類的class檔案讀入到記憶體中,併為之建立一個java.lang.Class物件。
類的載入由類載入器完成,類載入器由JVM提供,開發者也可以通過繼承ClassLoader基礎類別來建立自己的類載入器。
通過使用不同的類載入器可以從不同來源載入類的二進位制資料,通常有如下幾種來源:
2、連線
當類被載入之後,系統為之生成一個對應的Class物件,接著進入連線階段,連線階段負責將類的二進位制資料合併到JRE中。
類連線又可分為三個階段:
(1)驗證
檔案格式驗證
後設資料驗證
位元組碼驗證
符號參照驗證
(2)準備
為類的靜態變數分配記憶體,並設定預設初始值。
(3)解析
將類的二進位制資料中的符號參照替換成直接參照。
3、初始化
為類的靜態變數賦予初始值。
JVM類載入機制主要有三種:
1、全盤負責
類載入器載入某個class時,該class所依賴的和參照其它的class也由該類載入器載入。
2、雙親委派
先讓父載入器載入該class,父載入器無法載入時才考慮自己載入。
3、快取機制
快取機制保證所有載入過的class都會被快取,當程式中需要某個class時,先從快取區中搜尋,如果不存在,才會讀取該類對應的二進位制資料,並將其轉換成class物件,存入快取區中。
這就是為什麼修改了class後,必須重新啟動JVM,程式所做的修改才會生效的原因。
如果一個類收到了類載入請求,它並不會自己先去載入,而是把這個請求委託給父類別的載入器執行,如果父載入器還存在其父載入器,則進一步向上委託,依次遞迴,請求將最終到達頂層的啟動類載入器,如果父類別載入器可以完成父載入任務,就成功返回,如果父載入器無法完成載入任務,子載入器才會嘗試自己去載入,這就是雙親委派模型。
雙親委派模式的優勢:
1、參照計數演演算法
(1)判斷物件的參照數量
通過判斷物件的參照數量來決定物件是否可以被回收;
每個物件範例都有一個參照計數器,被參照+1,完成參照-1;
任何參照計數為0的物件範例可以被當做垃圾回收;
(2)優缺點
優點:執行效率高,程式受影響較小;
缺點:無法檢測出迴圈參照的情況,導致記憶體漏失;
2、可達性分析演演算法
通過判斷物件的參照鏈是否可達來決定物件是否可以被回收。
如果程式無法再參照該物件,那麼這個物件肯定可以被回收,這個狀態稱為不可達。
那麼不可達狀態如何判斷呢?
答案是GC roots,也就是根物件,如果一個物件無法到達根物件的路徑,或者說從根物件無法參照到該物件,該物件就是不可達的。
以下三種物件在JVM中被稱為GC roots,來判斷一個物件是否可以被回收。
(1)虛擬機器器棧的棧幀
每個方法在執行的時候,JVM都會建立一個相應的棧幀(運算元棧、區域性變數表、執行時常數池的參照),當方法執行完,該棧幀就從棧中彈出,這樣一來,方法中臨時建立的獨享就不存在了,或者說沒有任何GC roots指向這些臨時物件,這些物件在下一次GC的時候便會被回收。
(2)方法區中的靜態屬性
靜態屬性資料類屬性,不屬於任何範例,因此該屬性自然會作為GC roots。這要這個class在,該參照指向的物件就一直存在,class也由被回收的時候。
class何時會被回收?
(3)本地方法棧參照的物件
1、物件是否已死演演算法
2、GC演演算法
(1)標記清除演演算法
如果物件被標記後進行清除,會帶來一個新的問題--記憶體碎片化。如果下次有比較大的物件範例需要在堆上分配較大的記憶體空間時,可能會出現無法找到足夠的連續記憶體而不得不再次觸發垃圾回收。
(2)複製演演算法(Java堆中新生代的垃圾回收演演算法)
缺點是損失部分系統記憶體,因為騰出部分記憶體進行復制。
(3)標記壓縮演演算法(Java堆中老年代的垃圾回收演演算法)
對於新生代,大部分物件都不會存活,所以複製演演算法較高效,但對於老年代,大部分物件可能要繼續存活,如果此時使用複製演演算法,效率會降低。
標記壓縮演演算法首先還是標記,將不用回收的記憶體物件壓縮到記憶體一端,此時即可清除邊界處的記憶體,這樣就能避免複製演演算法帶來的效率問題,同時也能避免記憶體碎片化的問題。
老年代的垃圾回收演演算法稱為「Major GC」。
新生代回收器:Serial、ParNew、Parallel Scavenge
老年代回收器:Serial Old、Parallel Old、CMS
新生代回收器一般採用的是複製演演算法,複製演演算法效率較高,但是浪費記憶體;
老生代回收器一般採用標記清楚演演算法,比如最常用的CMS;
CMS 垃圾回收器是Concurrent Mark Sweep,是一種同步的標記-清除,CMS分為四個階段:
初始標記,標記一下GC Root能直接關聯到的物件,會觸發「Stop The World」;
並行標記,通過GC Roots Tracing判斷物件是否在使用中;
重新標記,標記期間產生物件的再次判斷,執行時間較短,會觸發「Stop The World」;
並行清除,清除物件,可以和使用者執行緒並行進行;
分代回收器分為新生代和老年代,新生代大概佔1/3,老年代大概佔2/3;
新生代包括Eden、From Survivor、To Survivor;
Eden區和兩個survivor區的 的空間比例 為8:1:1 ;
垃圾回收器的執行流程:
Redis是一個key-value儲存系統,它支援儲存的value型別相對更多,包括string、list、set、zset(sorted set --有序集合)和hash。這些資料結構都支援push/pop、add/remove及取交集並集和差集及更豐富的操作,而且這些操作都是原子性的。在此基礎上,Redis支援各種不同方式的排序。為了保證效率,資料都是快取在記憶體中,Redis會週期性的把更新的資料寫入磁碟或者把修改操作寫入追加的記錄檔案,並且在此基礎上實現了master-slave(主從)同步。
1、基於本機記憶體的快取
當呼叫api存取資料庫時,假如此過程需要2秒,如果每次請求都要存取資料庫,那將對伺服器造成巨大的壓力,如果將此sql的查詢結果存到Redis中,再次請求時,直接從Redis中取得,而不是存取資料庫,效率將得到巨大的提升,Redis可以定時去更新資料(比如1分鐘)。
2、如果電腦重新啟動,寫入記憶體的資料是不是就失效了呢,這時Redis還提供了持久化的功能。
3、哨兵(Sentinel)和複製
Sentinel可以管理多個Redis伺服器,它提供了監控、提醒以及自動的故障轉移功能;
複製則是讓Redis伺服器可以配備備份的伺服器;
Redis也是通過這兩個功能保證Redis的高可用;
4、叢集(Cluster)
單臺伺服器資源總是有上限的,CPU和IO資源可以通過主從複製,進行讀寫分離,把一部分CPU和IO的壓力轉移到從伺服器上,但是記憶體資源怎麼辦,主從模式只是資料的備份,並不能擴充記憶體;
現在我們可以橫向擴充套件,讓每臺伺服器只負責一部分任務,然後將這些伺服器構成一個整體,對外界來說,這一組伺服器就像是叢集一樣。
1、先把Redis的連線池拿出來
JedisPool pool = new JedisPool(new JedisPoolConfig(),"127.0.0.1");
Jedis jedis = pool.getResource();
2、存取值
jedis.set("key","value");
jedis.get("key");
jedis.del("key");
//給一個key疊加value
jedis.append("key","value2");//此時key的值就是value + value2;
//同時給多個key進行賦值:
jedis.mset("key1","value1","key2","value2");
3、對map進行操作
Map<String,String> user = new HashMap();
user.put("key1","value1");
user.put("key2","value2");
user.put("key3","value3");
//存入
jedis.hmset("user",user);
//取出user中key1
List<String> nameMap = jedis.hmget("user","key1");
//刪除其中一個鍵值
jedis.hdel("user","key2");
//是否存在一個鍵
jedis.exists("user");
//取出所有的Map中的值:
Iterator<String> iter = jedis.hkeys("user").iterator();
while(iter.next()){
jedis.hmget("user",iter.next());
}
Redis6.0之前是單執行緒的,Redis6.0之後開始支援多執行緒;
redis內部使用了基於epoll的多路服用,也可以多部署幾個redis伺服器解決單執行緒的問題;
redis主要的效能瓶頸是記憶體和網路;
記憶體好說,加記憶體條就行了,而網路才是大麻煩,所以redis6記憶體好說,加記憶體條就行了;
而網路才是大麻煩,所以redis6.0引入了多執行緒的概念,
redis6.0在網路IO處理方面引入了多執行緒,如網路資料的讀寫和協定解析等,需要注意的是,執行命令的核心模組還是單執行緒的。
redis提供了兩種持久化的方式,分別是RDB(Redis DataBase)和AOF(Append Only File)。
RDB,簡而言之,就是在不同的時間點,將redis儲存的資料生成快照並儲存到磁碟等媒介上;
AOF,則是換了一個角度來實現持久化,那就是將redis執行過的所有寫指令記錄下來,在下次redis重新啟動時,只要把這些寫指令從前到後再重複執行一遍,就可以實現資料恢復了。
其實RDB和AOF兩種方式也可以同時使用,在這種情況下,如果redis重新啟動的話,則會優先採用AOF方式來進行資料恢復,這是因為AOF方式的資料恢復完整度更高。
如果你沒有資料持久化的需求,也完全可以關閉RDB和AOF方式,這樣的話,redis將變成一個純記憶體資料庫,就像memcache一樣。
1、Redis相比memecache,擁有更多的資料結構和支援更豐富的資料操作。
(1)Redis支援key-value,常用的資料型別主要有String、Hash、List、Set、Sorted Set。
(2)memecache只支援key-value。
2、記憶體使用率對比,Redis採用hash結構來做key-value儲存,由於其組合式的壓縮,其記憶體利用率會高於memecache。
3、效能對比:Redis只使用單核,memecache使用多核。
4、Redis支援磁碟持久化,memecache不支援。
Redis可以將一些很久沒用到的value通過swap方法交換到磁碟。
5、Redis支援分散式叢集,memecache不支援。
Redisson、Jedis、lettuce 等等,官方推薦使用 Redisson。
Jedis 和 Redisson 都是Java中對Redis操作的封裝。Jedis 只是簡單的封裝了 Redis 的API庫,可以看作是Redis使用者端,它的方法和Redis 的命令很類似。Redisson 不僅封裝了 redis ,還封裝了對更多資料結構的支援,以及鎖等功能,相比於Jedis 更加大。但Jedis相比於Redisson 更原生一些,更靈活。
1、快取穿透
一般的快取系統,都是按照key去快取查詢,如果不存在對用的value,就應該去後端系統查詢(比如DB資料庫)。一些惡意的請求會故意查詢不存在的key,請求量很大,就會對後端系統造成很大的壓力。這就叫做快取穿透。
2、怎麼解決?
對查詢結果為空的情況也進行快取,快取時間設定短一點,或者該key對應的資料insert之後清除快取。
對一定不存在的key進行過濾。可以把所有的可能存在的key放到一個大的Bitmap中,查詢時通過該Bitmap過濾。
3、快取雪崩
當快取伺服器重新啟動或者大量快取集中在某一時間段失效,這樣在失效的時候,會給後端系統帶來很大的壓力,導致系統崩潰。
4、如何解決?
1、淘汰快取
資料如果為較為複雜的資料時,進行快取的更新操作就會變得異常複雜,因此一般推薦選擇淘汰快取,而不是更新快取。
2、選擇先淘汰快取,再更新資料庫
假如先更新資料庫,再淘汰快取,如果淘汰快取失敗,那麼後面的請求都會得到髒資料,直至快取過期。
假如先淘汰快取再更新資料庫,如果更新資料庫失敗,只會產生一次快取穿透,相比較而言,後者對業務則沒有本質上的影響。
3、延時雙刪策略
如下場景:同時有一個請求A進行更新操作,另一個請求B進行查詢操作。
次數便出現了資料不一致問題。採用延時雙刪策略得以解決。
public void write(String key,Object data){
redisUtils.del(key);
db.update(data);
Thread.Sleep(100);
redisUtils.del(key);
}
這麼做,可以將1秒內所造成的快取髒資料,再次刪除。這個時間設定可根據俄業務場景進行一個調節。
4、資料庫讀寫分離的場景
兩個請求,一個請求A進行更新操作,另一個請求B進行查詢操作。
依舊採用延時雙刪策略解決此問題。
1、快取穿透
一般的快取系統,都是按照key去快取查詢,如果不存在對用的value,就應該去後端系統查詢(比如DB資料庫)。一些惡意的請求會故意查詢不存在的key,請求量很大,就會對後端系統造成很大的壓力。這就叫做快取穿透。
2、怎麼解決?
對查詢結果為空的情況也進行快取,快取時間設定短一點,或者該key對應的資料insert之後清除快取。
對一定不存在的key進行過濾。可以把所有的可能存在的key放到一個大的Bitmap中,查詢時通過該Bitmap過濾。
3、快取雪崩
當快取伺服器重新啟動或者大量快取集中在某一時間段失效,這樣在失效的時候,會給後端系統帶來很大的壓力,導致系統崩潰。
4、如何解決?
使用Redis實現分散式鎖
redis命令:set users 10 nx ex 12 原子性命令
//使用uuid,解決鎖釋放的問題
@GetMapping
public void testLock() throws InterruptedException {
String uuid = UUID.randomUUID().toString();
Boolean b_lock = redisTemplate.opsForValue().setIfAbsent("lock", uuid, 10, TimeUnit.SECONDS);
if(b_lock){
Object value = redisTemplate.opsForValue().get("num");
if(StringUtils.isEmpty(value)){
return;
}
int num = Integer.parseInt(value + "");
redisTemplate.opsForValue().set("num",++num);
Object lockUUID = redisTemplate.opsForValue().get("lock");
if(uuid.equals(lockUUID.toString())){
redisTemplate.delete("lock");
}
}else{
Thread.sleep(100);
testLock();
}
}
備註:可以通過lua指令碼,保證分散式鎖的原子性。
Redis 分散式鎖不能解決超時的問題,分散式鎖有一個超時時間,程式的執行如果超出了鎖的超時時間就會出現問題。
Redis容易產生的幾個問題:
1、縮短鍵值的長度
2、共用物件池
物件共用池指Redis內部維護[0-9999]的整數物件池。建立大量的整數型別redisObject存在記憶體開銷,每個redisObject內部結構至少佔16位元組,甚至超過了整數自身空間消耗。所以Redis記憶體維護一個[0-9999]的整數物件池,用於節約記憶體。 除了整數值物件,其他型別如list,hash,set,zset內部元素也可以使用整數物件池。因此開發中在滿足需求的前提下,儘量使用整數物件以節省記憶體。
3、字串優化
4、編碼優化
5、控制key的數量
往期精彩內容: