(阿里offer)春招知識點總結1:java基礎+集合+併發+jvm+ssm

寫在前面:大量總結不是系統的總結,而是作者本人根據自己情況總結的,許多我很清楚的地方就不在提及,大量源碼也均未提及,尤其是框架的總結提及很淺。僅作爲自己短期技術棧的一個總結,只作爲參考,不是一定正確的理論知識點。

java基礎

面向對象及三大特性

封裝:把客觀事物抽象成具體的類,並把對類的數據的操作抽象爲方法,通過不同的修飾符對內部數據提供不同級別的保護,封裝以後也使得信息和操作對用戶透明化。

繼承:通過繼承,子類可以使用父類的非私有變量和方法,實現代碼複用,等級劃分等,包括接口和類繼承,也是支持多態的一種基礎。

多態:是指一個類實例的相同方法在不同情形有不同表現形式。通過同一個上級引用指向不同的子類,使得對同一消息做出不同迴應。

多態的實現

一個類中的方法重載是編譯時多態(傳遞參數決定執行那個方法,不同參數的同名方法的簽名是不一樣的,根據簽名可以唯一確認執行什麼方法)。使用父類(或接口)引用指向子類(或實現類);調用靜態方法看左邊;調用非靜態方法(只能訪問重寫的方法或左邊引用對象有而右邊引用對象沒有的方法,右邊有左邊沒有的非靜態方法,直接用左邊的引用訪問會報錯,找不到方法),從右邊的實例開始找,沒有找其父類,逐個往上,直到Object;訪問屬性都看左邊。所以不同的子類或實現類同一個方法對同一個請求有不同的動作,因爲找的是右邊子類的方法,這是運行時多態。

多態允許具體訪問時實現方法的動態綁定。Java對於動態綁定的實現主要依賴於方法表,通過繼承和接口的多態實現有所不同。

  • 繼承:在執行某個方法時,在方法區中找到該類的方法表,再確認該方法在方法表中的偏移量,找到該方法後如果被重寫則直接調用(所以繼承關係的類的同一個方法,在其自己的方法表上偏移值是一樣的,所以可以這樣找,繼承關係使用Invokevirtual 偏移調用方法),否則認爲沒有重寫父類該方法,這時會按照繼承關係搜索父類的方法表中該偏移量對應的方法。

  • 接口:Java 允許一個類實現多個接口,從某種意義上來說相當於多繼承,這樣同一個接口的的方法在不同類方法表中的位置就可能不一樣了。所以不能通過偏移量的方法,而是通過搜索完整的方法表。

註解

使用@interface作爲修飾符進行申請,只有成員變量,註解就像是標籤,用作於描述代碼,反編譯代碼中期繼承了Annotation接口。創建時使用關鍵字表明其信息

/**
*@Documented:指明該註解是否可以將註解信息添加在java文檔中(有就可以)
 *@Inherited:該註解可以被自動繼承(子類繼承父類時,繼承父類方法上的註解等)
 *@Retention:指明在什麼級別顯示該註解,定義了註解的生命週期
    RetentionPolicy.SOURCE 註解存在於源代碼中,編譯時會被拋棄
    RetentionPolicy.CLASS 註解會被編譯到class文件中,但是JVM會忽略,類加載時忽略
    RetentionPolicy.RUNTIME JVM會讀取註解,同時會保存到class文件中
  *@Target:指明該註解可以註解的程序範圍
  	ElementType.TYPE 用於類,接口,枚舉但不能是註解
    ElementType.FIELD 作用於字段,包含枚舉值
    ElementType.METHOD 作用於方法,不包含構造方法
    ElementType.PARAMETER 作用於方法的參數
    ElementType.CONSTRUCTOR 作用於構造方法
    ElementType.LOCAL_VERIABLE 作用於本地變量或者catch語句
    ElementType.ANNOTATION_TYPE 作用於註解
    ElementType.PACKAGE 作用於包
    
    註解只有屬性,若只有一個屬性時可命名爲value,傳參就不需要寫屬性名
    若有其他屬性,且無默認值,就只能創建註解時加上,且用逗號分隔
 */

//使用實例
@Documented//註解可以將註解信息添加在java文檔(doc)中
@Inherited//註解可以被繼承
@Retention(RetentionPolicy.RUNTIME)//生命週期是整個階段
@Target({ElementType.TYPE, ElementType.FIELD})//次註解作用於類和字段上
public @interface FieldTypeAnnotation {
/**
 *leip 2016年12月3日
 *TODO
**/
String type() default "ignore";
int age() default 27;
String[] hobby(); //沒有指定defalut的,需要在註解的時候顯式指明
}

public,protect,private修飾符及默認default。

public可以被任何類調用,protect同包下及其子類(可以是不同包的子類)訪問,default允許同一個包下的類訪問,private只能自己訪問。

static,final

static可以用於修飾屬性,使的屬性屬於類,只實例化一次,類及其實例化對象都使用這一個,可以用類名直接調用;還可以修飾方法,方法只能使用類的靜態變量和調用其他靜態方法,方法屬於類,可以用類名直接調用,其次還能修飾內部類,做靜態塊,靜態導包,直接使用靜態導入包的方法,還不用加類名。static不會影響屬性的訪問區域,static不能修飾局部變量。

final修飾的類不能繼承,所有方法默認使用final修飾,比如Java中的;用final修飾的方法不能被重寫;final修飾的變量需要直接賦值或者在靜態塊裏面賦值,final修飾的變量後面不能改變,final修飾的引用後面地址不能改,但地址對應的內部內容可以改。

String,StringBuilder,StringBuffer,intern方法

String直接用引號創建時會在常量池查看是否已經存在,有的話直接返回,沒有的話創建後返回;new String創建時會在堆上創建對象並返回,然後會在常量池中看有沒有一樣的字符串,沒有的話在字符串中創建一個;用方法返回值賦值的,不會再常量池生成字符串。String是用final修飾的類,所以是一旦申請後不可再變,這樣做可以節約堆空間,因爲多個字符串變量都會指向常量池中同一個字符串;因爲字符串會被多個線程共享,那麼不能變的話就保證了線程安全;在類加載時描述符都是字符串形式的,不可變就提高了安全性;最後就是字符串的不變性讓他很適合做hash映射的鍵,由於本身不變化,所以hashcode也不會變化,那麼可以緩存字符串的hashcode提高效率。String在做+運算連接字符串時,效率是很低的,因爲它會把所有的字符都轉換爲StringBuilder後,依次append,最後在toString。

StringBuffer和StringBuilder都是長度可變的,和string一樣,他們都用final修飾了,是不可繼承的。其擴容機制是變爲原來的兩倍+2。StringBuffer是線程安全的,因爲底層上所有的方法相對於StringBuilder都使用synchronized修飾了。當然沒有進行同步的StringBuilder也就具有更高的效率。

使用+運算拼接直接連接使用引號的字符串或者使用final修飾的字符串引用(賦值不是通過方法返回)時,效率是比較高的(編譯時會直接拼接,不會轉化爲StringBuilder拼接),尤其拼接以後產生的字符串在常量池中已經存在的話,效率是最高的。

調用字符串的intern方法時,1.6及之前,會查看常量池中是否存在,若不存在就把字符串複製到常量池中,並返回常量池中字符串的引用;1.7及以後,會先在常量池中是否存在,存在就返回引用,不存在就去堆裏面查看是否存在,存在的話就把堆中的引用返回,並把這個引用存到常量池裏面。如果都不存在就把當前字符串複製到常量池並返回常量池裏面的引用。

equels與==

對於基本變量比較的是值是否相等,而引用比較的是地址是否相等,equels不能用於比較基本變量類型變量,引用比較默認和一樣,但一般都會重寫來比較引用對象的內容是否相等(比如Integer,String,Double)。

錯誤和異常

異常是能被管理人員處理的,而錯誤是系統自帶的,比如內存溢出等。異常和錯誤都有共同的父類Throwable,異常又分運行時異常RuntimeException(像類型轉換,數組越界,空指針異常,算數異常等),運行時異常又稱非受檢異常,這種異常可以不處理和拋出。而像Exception,找不到類、文件,IO異常,SQL異常等有稱爲受檢異常,受檢異常需要try、catch塊進行處理的,否則編譯無法通過。子類重寫或者實現方法時拋出的異常不能是父類或者接口中對應方法拋出的異常的等級高,只能拋出其一樣的或子類異常。

接口和抽象類的區別
  • 接口的方法都是抽象的,抽象類的方法可以是抽象的,也可以是非抽象的;
  • 抽象類可以有構造方法,但接口沒有(構造方法用於子類繼承時使用,但不能實例化);
  • 實現接口必須實現實現所有類,但繼承接口時可以不用實現所有抽象方法,只要自身依然是抽象類就行;
  • 接口的變量都是使用final修飾的,而抽象類中不是的;
  • 接口成員函數都是默認public,抽象類有多種修飾符;
  • 接口不能被實例化,抽象類也不可以,但抽象類含有main方法時可以被調用;
  • 類是多實現,單繼承的,所以多態調用中繼承使用了偏移直接在方法表找方法,而接口實現類要遍歷方法表找方法,所以繼承抽象類的方法調用比實現類實現接口的方法調用快;

希望某些方法具有默認實現,或者是基本功能經常改變,就用抽象類,抽象類是用來捕捉子類的通用特性。協調不同的模塊間的方法簽名的統一或者涉及多繼承的情況都用接口,大部分情況下也都使用的是接口。

序列化

往往我們需要將對象進行序列化,這時對象要實現Serializable接口,並指定一個序列號,序列號是通過輸入輸出流進行的,將數據在對象與字節數組之間轉換(要使用同樣的編碼格式),對象屬性中若有屬性使用了transient修飾,則這個屬性不可序列化,反序列化後結果爲null(但如果實現的是Externalizable接口就能序列化),靜態變量也不能序列化,反序列化後可以得到結果是因爲其從jvm中獲得的,而不是反序列化的結果

自己寫的序列化工具:

  • 序列化:對象傳入對象輸出流結果給字節輸出流,輸出字節數組並返回。
  • 反序列化:字節數組傳入字節輸入流,把輸入流結果對對象輸入流,得到Object對象並返回。

java集合自定義了序列化工具,其內部真正儲存對象的數據結構(比如數組)使用了transient修飾,避免被直接序列化,因爲這個數組一般存不滿(size一般小於capacity),浪費了空間。集合的序列化是根據size,逐個元素序列化後返回,避免序列化整個數組。

不可變類

類使用final聲明;成員全部私有並使用final修飾;不提供setter方法;get方法,返回對象的克隆拷貝

引用傳遞與深淺拷貝

調用函數傳入對象引用時是值傳遞,此時的值是傳入對象引用的拷貝,我們可以在函數中通過引用改變原對象引用指向區域的內容,但在函數中改變引用不影響原引用。

淺拷貝會爲拷貝對象新開空間,然後利用被拷貝對象來初始化拷貝對象,對應被拷貝對象的基礎變量會在堆中新建一份一樣的,但對於對象引用是把對象複製一份後直接給,也就是說被拷貝對象和拷貝對象的屬性中的同一個對象引用指向的是同一個區域,clone方法默認爲淺拷貝。而深拷貝會把對象引用對應的實際內容在堆中再建一份,並指向,所以深拷貝時,基本數據類型,和對象引用全部不是一樣的,實現深拷貝可以使用序列化和反序列化,或者讓類內部屬性對應的類都實現Cloneable接口。注意存儲循環時不要使用深拷貝。

反射

正常使用的類全部是被jvm加載以後的,我們運行的程序在編譯器的時候就已經知道需要的類已經被加載了。而反射是在編譯時不確定那個類被加載了,而是在運行時才加載,探知自審(可以獲得類的方法與屬性,並獲得屬性的類型,對應Type對象;還可以獲得生命週期爲Runtime和Class的註解),使用編譯期並不知道的類就是反射。

通過反射獲得類的class對象(Class.forName(全限定名);類名.class;實例對象.getClass()。)

  • 從而可以獲得該類的信息,比如訪問修飾符,實現了什麼接口,父類信息。還可以獲得構造方法,打包在哪,導了那些包。

    方法(Method[] 返回本身和父類public方法:getMethods(),返回本身所有方法:getDeclaredMethods()

    ,用getMethods()拿私有方法返回NoSuchMethodException)。

    成員(Field[] 返回本身和父類public變量:getFields(),返回本身所有變量:getDeclaredFields())。

  • 使用class對象的newInstance創建實例。

  • 對變量設值或調用對應方法

    • 操作私有變量:獲取變量(field)後,field.setAccessible(true)獲取私有變量的訪問權,使用field.set(testClass, 修改成的值)修改變量的值。
    • 操作私有方法:拿到方法後用privateMethod.setAccessible(true)獲得私有方法訪問權,然後用privateMethod.invoke(class對象, 參數1, 參數2…)執行方法。
  • 修改私有常量(final修飾)

    jvm優化:對於基本類型和final修飾的類的引用,如果申請時用final修飾,在使用時,jvm會把常量名直接替換爲具體對應的值,而不會使用常量名。所以對於這部分常量,無法通過反射去修改,除非改源碼。

    但如果申請時沒有直接賦值的話,比如常量的值在構造函數中賦值的(用三目運算符賦值等,不直接賦值,編譯時無法確定常量的值,必須在運行時才確定就可以,這樣就無法優化了),那就能修改常量的值了。

BIO/NIO

BIO:傳統IO在讀寫時會產生阻塞,因爲需要等待傳輸方將數據在緩衝區中準備好,首先創建socket綁定端口以後死循環等待連接請求到達,而對於socket接收連接和讀寫數據都是要阻塞的(阻塞等待可連接,網卡可寫,緩衝區可讀,等待過程不佔用cpu資源,真正操作數據時(比如從網卡把數據讀到內存)除了AIO都需要阻塞),所以使用線程池將每個到達的連接從線程池中分配線程進行執行,這樣阻塞時可以有其他線程執行,每個線程對於請求,在當前線程被中斷或者socket關閉前死循環讀取數據並處理。這樣做在線程少時是不錯的選擇,但線程多時,要考慮線程池的設計,不然線程的切換就需要花費大量的功夫。

NIO概念:而NIO是多路複用的,在內核設立了專門的線程去輪詢訪問所有的socket,而不需要具體操作的線程阻塞等待,沒準備好的時候,線程仍然可以進行其他的工作,其在Selector上註冊標誌位到註冊集合表示其對什麼事件感興趣(註冊事件處理器),當輪詢線程發現準備好時後會在Selector上修改對應的標誌位並放到就緒集合,使得工作線程知道數據已經準備好,可以開始操作(Reactor:有事件分發器交給事先約定的事件處理函數或回調函數;Proactor:異步的發起讀寫請求。Selector.select)。

NIO使用:java使用NIO讀寫時一般建立文件輸入出流,從流對象中getChannel獲得通過(FileChannel)實例對象,再建立緩衝區實例對象(如BufferByte.allocate),然後調用channel的讀寫函數,傳入緩衝區實例,進行文件的讀寫,buffer的flip方法把寫模式變成讀模式(limit變爲position,position歸零),讀到寫用clear,一般用於文件複製,put、get函數寫讀緩衝區。包含三個重要參數:capacity(包含元素的數量),limit(上限,第一個不能操作的元素的索引),position(下一個要操作的元素的索引),mark用於備份position位置(mark()方法讓mark=position,reset()方法反過來)

三個核心組件:NIO的工具包提出了基於Selector(選擇器)、Buffer(緩衝區)、Channel(通道用作讀寫Buffer)的新模式;Selector(選擇器)、可選擇的Channel(通道)和SelectionKey(選擇鍵)配合起來使用,可以實現併發的非阻塞型I/O能力。

java8新特性
  • Lambda表達式和Functional接口:Lambda表達式可以允許把函數作爲參數傳參可以用(類型 參數)->{方法體}替代以前的匿名內部類,方法體中可以調用參數與成員變量和局部變量(隱式轉換爲final),一般使用參數數組結合forEach或sort等方法時使用,傳參也可以不指定類型,參數類型和返回值類型是編譯器推測出來的。Functional接口使用@FunctionalInterface註解修飾接口,使得這樣的接口可以被隱式轉換爲lambda表達式。

  • default修飾符修飾接口中的方法,可以爲接口中的方法寫默認實現,實現類可以自己決定是否覆蓋這個方法

  • 方法引用:直接引用已有Java類或對象(實例)的方法(Class名::method)或構造器(Class名::new)。與lambda聯合使用

  • 重複註解

  • Stream,函數式編程風格,集合的批處理和流式操作,搭配lambda表達式進行去重(distinct)、分類(Collectors.groupingBy)、過濾(filter),加和,最大最小等函數操作。

  • 新的Date-Time API :Clock,LocaleDate與LocalTime

  • jdk1.8中JVM的新特性:拋棄了永久代,數據轉移到堆中,引入元空間,元空間作用與永久代類似,都是方法區的實現,元空間不在虛擬機中,而是在本地內存下,字符串不存在元空間,存在字符串常量池裏面。

負載均衡算法

輪詢分發,每個服務器接收一個請求的來,一臺一個依次;按權重,權重越大接收到請求的概率就越高,權重可以按照服務器的性能來劃分;哈希分發,哈希分發又分ip哈希和url哈希,使得客戶端和服務器對應的服務器總是一致,這樣還能在服務器保存客戶端的session;按響應時間劃分,優先分配給響應時間短的;隨機發;發給任務最少的機器。

集合

collection

是set,list的父接口,包含了基本的添加,刪除,迭代,判空,判斷存在,清空等基本方法。集合中的數據在迭代時,如果被修改可能導致fast-fail,比如併發情況下,一個線程迭代訪問,一個線程刪除元素會導致這個問題,這個時候建議使用併發的集合;又或者迭代的使用集合的remove在單線程下刪除元素也會導致,建議使用迭代器的刪除元素的方法,可以避免。

list和set都是實現了collection接口,list可以存null,set可以存一個null;TreeMap和TreeSet在第二次及以上加入元素時會進行比較,所以不能存null鍵(第一次放null可以放進去,但比較的時候一旦有null就會出錯)。

java7/8中的HashMap(轉set機制),HashTable,TreeMap,LinkedHashMap

都是實現了Map接口。

HashMap在1.7和1.8中有些許改動,1.7中HashMap是一個長度爲2的n次方的數組,這樣的好處是可以計算hash時使用位運算代替模運算,並且rehash時增加兩倍的特性使得同一個節點下的數據只會分散到兩個節點下,每個數組節點都是一個鏈表,所以採用的是鏈地址法解決哈希衝突,put操作時先計算hash對應的下標,找到具體的數組項,接着會逐個遍歷鏈表項,有相等的就直接覆蓋,沒有相等的鍵或者數組項爲空會新插入,值得注意的是插入時會先判斷當前負擔因子是否超過限額,超過會進行rehash,1.7中採取先擴容後插入的策略,並且爲了效率1.7中採用的是頭插法,這導致多線程併發訪問下,同時觸發rehash時可能出現鏈表結構死循環現象,導致線程死在無休止的get中。get操作也是根據hashcode找到對應下標後遍歷。在1.8中發生了一些變化,使用紅黑樹的結構在一個節點存儲數據項大於8時代替鏈表,提升遍歷效率,用尾插代替了頭插,但仍然不建議在併發情況下使用hashmap,還有就是數據要先插入再觸發rehash。

hashmap有擾動函數,將除null以外的鍵與鍵右移16位的結果異或,再將得到的結果與低位掩碼進行與操作得到對應的數組下標。

HashTable就是HashMap中所有方法使用synchronized修飾後的類,但在多線程下仍然可能出現問題,比如邊刪邊查導致越界現象等。但HashMap可以存一個null鍵和很多個null值,放在table[0]裏面,put進來處理,HashTable不能存null鍵和值,因爲這是併發集合,如果get時,返回了null,不能確定是鍵無映射,還是儲值爲null,而hashmap是單線程的集合,有contains方法判斷鍵是否存在。

TreeMap使用紅黑樹作爲數據結構,以鍵爲排序的依據,按鍵的自然順序,或者使用比較器。鍵與值組成一個節點。

LinkedHashMap中每個節點除了有鍵和值以爲,還存在前後指針,使得每個節點插入時,不僅會根據hash值散列存放起來,還會有前後指針維護成一個雙向鏈表,保證訪問順序

可以返回成set集合,鍵使用keySet,值使用values,鍵值對一起Set<Map.Entry<X,X>> set=map.entrySet();

只有HashMap中允許鍵爲空

ArrayList和LinkedList,Vector,Stack

ArrayList和Vector底層都是用的數組,包含了對數組基本的添加,刪除,獲取,清空等操作,數組的擴容每次都擴容爲原來的1.5倍,但多線程往arraylist中添加數據時,在臨界要擴容時是可能出現數組越界情況的(兩個線程交替進入add,先進的判斷不會越界後阻塞,後來的也同樣可以添加,導致連續添加兩個,size+2,在數組邊界處越界),LinkedList和Vector底層都是雙向鏈表,理論上無大小限制,也包含對應的各種操作,鏈表刪除新增比數組快,但數組的讀取比較快,Vector和Stack是用synchronized修飾了的,是同步的集合,Stack繼承了Vector。

HashSet、TreeSet、LinkedHashSet

他們都實現了Set接口,set中不允許出現重複元素的,重複元素指的是對象使用equals比較相等。HashSet是無序的集合,他是基於HashMap實現的,每個元素都是當做鍵存入了HashMap,然後值使用一個默認值;TreeSet是使用紅黑樹實現的,鍵就是存放的值,是有序的集合,在傳入值沒有自然順序時,需要實現比較器。LinkedHashSet繼承於HashSet,底層使用LinkedHashMap的鍵存放對應的值,保證了有序性。

collections

集合工具類,維護了很多操作集合的工具,例如反轉,排序(轉化爲Arrays裏面的排序方法,數據多的時候使用歸併,少的時候使用插排,中間層次使用快排,47,286),二分檢索,查找,同步控制( synchronizedCollection方法可把傳入的集合變成同步集合,原理是裝飾原來的方法,每一個都用synchronized包起來),還可設置不可變集合。

Comparable和Comparator接口

實現Comparable接口的compareTo方法的對象,在TreeSet和TreeMap中保存時或使用工具類排序時會使用這個方法進行比較,實現方法時用this與傳入對象進行比較。也可以自定義類去實現Comparator接口的compare方法,直接傳入兩個對象進行比較,在新建有序容器時把一個比較器實例對象傳入即可。這是使用了策略模式

併發

java內存模型

內存,緩存,寄存器被抽象分爲主存和工作內存,每個線程都有工作內存,會從主存中拷貝共享的變量到工作主存中,進行寫時,先寫工作內存再寫會到主存,如此在多線程下就可能導致不一致的現象,物理上是沒有分區的,工作內存是邏輯的分區,而每個線程對應位一個內核線程,在內核調度器下使用cpu。

java內存模型需要承諾原子性,可見性,有序性

  • 原子性:如對一個32位靜態變量賦值,這樣的具有原子性的操作,過程中是不允許打斷的,synchronized和lock都可保證原子性,CAS操作也可以。
  • 指令重排序(沒有前後數據依賴時)(導致違背可見性與有序性)(使用volatile)
    • 編譯器優化:編譯器在編譯時,不改變單線程程序語義的前提下,是可以重新安排語句執行順序的。所以即使寫的代碼看起來不會有問題,但由於重排導致了問題。
    • 指令並行的重排:現在操作在彙編中對應取址等一系列操作,由於前後數據存在關聯導致流水線某一步等待使得流水線需要等待好幾個週期才能恢復,所以也有重排來提高效率
    • 內存系統重排:由於緩存和緩衝區的存在,導致load和store操作亂序執行。實際的讀取可能出現在修改之前
  • 可見性:一個線程在自己的工作內存修改某個共享變量後,其它線程要馬上得知。
  • 有序性:實際運行與順序的執行代碼結果一致。
happens-before
  • 程序順序原則,保證語句執行串行化,按照代碼順序執行
  • 解鎖發生在另一個加鎖前
  • volatile變量寫執行完後纔會執行後續的讀,寫完強制寫會主存,讀從主存直接讀
  • 線程的start()發生於線程內部代碼之前,執行代碼先於線程終止
  • 傳遞性,A先於B,B先於C,則A必定先於C
  • 線程中斷(interrupt())後可以理解被檢測到(interrupted)
  • 對象的構造方法先於回收。
volatile

java內存模型中每個線程都有一個高速緩衝區保存主存中某些變量的副本,而內存中的共享變量都需要解決緩存一致性問題。最基礎的方法是直接對總線加鎖,阻塞其他cpu對其他部件的訪問,但這樣效率很低。所以後來使用緩存一致性協議來確保效率和一致性,volatile就是這樣的,它具備兩種特性,可見性和禁止重排序。可見性就是確保數據一致性的體系,它規定在線程對數據進行寫時,立即從副本寫回內存,其他線程工作線程中這個變量會被置爲無效,強制讀取內存。禁止重排序是指被volatile修飾的變量不會因爲重排序而亂序,會確保此變量之前的操作不會出現到後面,後面的操作不會出現在前面,對應變量進行操作時前面的修改都已完成並對後續操作可見。

而這些體現到彙編是編譯時多出了內存屏障(lock前綴指令)。屏障分四種。執行遇到內存屏障會確保前面的操作已經完成纔會執行後面的,後面的不會出現到屏障前面,避免屏障前後的執行順序出現亂序;強制將對緩存的修改操作立即寫入內存並導致cpu中其他的緩衝行無效。

CAS

比較替換原則,每次根據預期值去比較內存對應變量的值,相等就更新爲設定數據,不相等就返回false。比較加替換利用了操作系統的原語操作,也會有加鎖存在,但是利用底層硬件實現的鎖機制,比synchronized的性能更好,但cas存在ABA問題(後來就有引入了時間戳引用原子類來解決,它比較不僅會比較值,還會比較時間戳),且大部分更新失敗的情況下會直旋重複嘗試,競爭較大時效率不高,並只能控制單個變量同步

原子類

大部分操作,比如自增等,都是基於CAS來實現的。

AQS

AQS同步器內部維持一個雙向隊列保存等待線程以及一個頭結點一個尾節點,頭結點是當前獲得同步狀態的線程,AQS利用這個隊列確保每次只有一個線程可以獲得同步狀態,幾乎所有juc包下的所有鎖機制都是基於同步器來實現的,AQS中的包含獨佔鎖與共享鎖兩種鎖,獲取鎖的時候都會首先判斷是否有線程持有同步狀態,沒有就直接嘗試獲取,獲取成功就直接進入同步狀態,否則就嘗試把線程放到同步隊列尾部,這一步入隊會使用CAS自旋來確保入隊,入隊後是一個自旋鎖一直嘗試獲得同步狀態,每次先查看自己前面的節點是不是頭節點,是的話就會去嘗試獲得頭部節點,不是或者獲取失敗或嘗試去跳過前面被取消的節點,然後阻塞自己。然後線程接下來能被喚醒或者中斷,又會去看前驅節點是不是頭結點,並嘗試獲得同步狀態。當頭節點釋放同步狀態時會去喚醒後驅節點,後驅節點拿到同步狀態後,排他鎖就正常執行,共享鎖會主動去傳播共享狀態。AQS還分公平和不公平鎖兩種,不公平鎖進來就會直接嘗試CAS去拿同步狀態,在隊列裏面也不會老實排隊,而是隻要同步狀態釋放就直接CAS去搶。

線程創建方式
  • 自定義類繼承Thread,重寫run,創建實例並調用start方法。
  • 自定義類實現Runnable接口,實現run方法,創建Thread實例,並傳入自定義類,調用Thread實例的start
  • 自定義類實現Callable接口,並實現call方法(可以有返回值),新建自定義類的實例對象傳給FutureTask創建實現對象task,在把task傳給Thread創建實例,調用start方法開始執行,執行是異步的,並且這種創建方式可以有返回值,返回值用task.get()獲得。
    • FutureTask繼承了Runnable接口和Future接口
      • 用volatile定義了一個整數,對應0-6代表了七種狀態,對應創建,任務執行,中斷等待,已中斷,已取消,成功,異常。取消時任務沒被執行就直接變已取消,在執行就變爲中斷等待,執行完成後變成已中斷,正常執行任務根據結果變爲成功和異常狀態
      • FutureTask既然實現了Runnable接口就有run方法,而run方法就執行傳入Callable接口實現類的call方法(若狀態爲創建才執行任務,不然表示任務已經執行了,**接着用unsafe類把任務執行線程保存在runner字段,保存失敗會直接返回,**執行結果用set保存結果,或者setException保存產生的異常,返回喚醒等待線程(Unsafe.unpark)並返回結果)
      • 取消任務時,如果任務不是創建狀態,那麼表示已經執行過了,直接返回false;若是執行狀態,以中斷等待作爲中間態,執行完後變成已中斷;若不是執行狀態,直接變爲取消;最後喚醒等待任務結果的線程。
      • get用來獲得執行結果,有結果就返回,沒結果就等待,等待是一個死循環來判斷任務的狀態,也可以傳入時間參數限時等待
        • 等待狀態:
          • 線程若被中斷,則移除這個等待節點,拋出中斷異常
          • 任務完成,直接返回
          • 若未創建等待節點,就新創建節點,讓當前線程加入等待隊列
          • 通過LockSupport.park設置等待時間進行阻塞(用Unsafe類提供的硬件級別原子操作park和unpark)
    • Future接口定於了取消任務,查看任務是否取消,查看任務是否執行完成,阻塞獲得執行結果,限時獲得執行結果方法
java7/8的concurrentHashMap

1.7中的concurrentHashMap使用了多段鎖的機制,定義了16個segment,這種結構繼承了可重入鎖,所以每個segment是一個單獨的容器,也是一把鎖。初始化時會爲16個segment的第一個進行初始化,並用CAS操作放入,初始化第一個的作用在於後面的segment初始化都直接在第一個裏面拿值進行初始化。在進行put操作時通過hash計算節點應該放到哪個segment裏面,接着查看這個segment是否爲空,爲空要進行初始化,初始化時線程需要用CAS操作修改某個標誌的值,修改成功再進行初始化,不成功就放棄初始化,這保證只有一個線程初始化了segment,初始化時是利用第一個segment的值來初始化的。初始化成功後再進入segment之前要操作獲得這個segment的鎖,獲取失敗會進行自旋嘗試獲得,並且在自旋時還會嘗試去初始化節點,拿到鎖後在內部再次散列獲得數組元素節點,遍歷鏈表,若鍵存在就更新對應的值,若不存在建立節點後使用頭插法插入節點。插入時使用CAS操作保證與get之間的併發性。最後有鎖的釋放。刪除操作同理也要使用CAS保證同步性,插入數據後是可能觸發rehash的,rehash會讓當前segment下數組長度擴容兩倍後依次移動放入,建立的臨時數組要用volatile修飾,保證get時是可見的。1.8中拋棄了segment結構,只使用了一個數組,用鏈地址法解決hash衝突,但單個節點上數據過多會使得鏈表變成紅黑樹,這樣訪問效率就從O(n)變成O(logn),put時首先檢查對應數組節點是不是空的,空的話嘗試用CAS插入,然後判斷是否當前節點的hash值爲MOVED,這表示有線程再進行rehash,那麼當前線程要去幫忙rehash,然後前面兩種都不滿足,說明嘗試訪問非空的數組節點,這是會首先以鏈表頭作爲鎖對象利用synchronized關鍵字建立同步代碼塊,然後對節點進行訪問遍歷,有相等的鍵覆蓋,沒有就尾插到鏈表尾,然後計算節點數據個數看要不要轉換爲紅黑樹,計算負擔因子看要不要擴容。前面說MOVED表示當前有線程再擴容,這是個特殊節點對應的hash值常量,並且此時會發現指向新數組的引用不爲空,線程幫忙時會被分配任務,執行對於數組節點上的鏈表搬運,線程首先檢查當前節點的頭節點是不是特殊節點,是的話表示這個節點已經被其他節點搬運走了,不是的話,嘗試獲取頭結點的鎖,獲得成功就鎖起來,然後開始搬運鏈表中的節點,首先把節點根據hash值分成兩條鏈表,再分別放到新數組的兩個節點下,最後把久的節點的頭節點設置爲特殊節點,表示搬運完畢。接着搬運其他,直到結束rehash。

synchronized與lock同步鎖

synchronized用於修飾方法使方法成爲同步方法,或者建立同步代碼塊,他們使用的鎖,都是對象頭中的鎖,修飾方法時,如果不是靜態方法,那麼使用的是this實例的鎖,靜態使用類的class對象作爲鎖,同步代碼塊需要傳入一個對象用作鎖,synchronized在發生異常時可以jvm實現鎖的釋放,多種線程使用synchronized用作同步時,可使用wait,notify,notifyAll,yield,sleep,interrupt進行通信,(過期stop,suspend),等待狀態的線程無法被中斷,除io阻塞的阻塞線程可以被中斷,且線程無法得知自己是否拿到了鎖。

Lock接口定義方法lock去獲取鎖,拿不到就進入隊列排隊等;tryLock去獲取鎖,拿不到鎖就返回false,不會排隊等候;而lockInterruptibly獲取鎖的話,獲取失敗會進入阻塞隊列等待,但等待期間可以被中斷,synchronized在等待期間不能中斷退出。lock的實現類ReentrantLock實現了鎖的可重入,是lock直接的實現類。ReentrantReadWriteLock鎖是ReadWriteLock接口(繼承Lock接口)的實現類,可取出讀寫兩種鎖來進行同步。

兩種鎖都是可重入的,lock是可中斷的,synchronized不是,都默認是非公平鎖,但lock可通過傳參變成公平鎖,lock同步鎖可使用讀鎖,正常下兩種鎖效率差距不大,但在讀操作頻繁的地方,具備讀鎖的Lock接口明顯更高效。

線程池

ThreadPoolExecutor,線程池的重要參數和屬性包括核心線程池大小,最大限制創建線程數量,任務緩存隊列,空閒時間。如果當前線程數量小於核心線程池的數量限制,那麼不管當前線程池中的線程是否空閒,每來一個任務就創建一個線程去執行這個任務;如果線程數量大於等於核心線程池大小,每來一個新的任務就放入任務緩存隊列中,線程池中有線程空閒時就會去拿任務執行;如果任務緩存隊列滿了,線程數量大於等於核心線程池數量但小於最大線程池創建數量就會新建線程去執行任務;如果任務緩存隊列滿了,線程數量也等於了最大線程創建數量,就會採取任務拒絕策略;拒絕策略有四種:直接丟棄、丟棄後返回異常、丟棄隊列首部,新任務再次入隊、由嘗試放入任務的線程來執行;當線程池數量大於等於核心池數量時,會回收空閒時間大於設定空閒值的線程,直到線程數量小於核心線程池數量;一般線程池設計數量等於cpu數量加1,若進程io,會阻塞線程,可以考慮把線程數量提高。

工具類Executors下有四種方法創建線程池,這是jdk提供的工具類,但不允許使用,四種方法爲

  • 均採用無界隊列來保存任務,可能由於任務堆積導致OOM

    • newSingleThreadExecutor:核心池與線程池最大容量都爲1的線程池(該線程不會被回收),並使用無界的阻塞隊列,所以就一個線程去執行任務隊列的任務。用於任務一個一個執行的情況
    • newFixedThreadPool:核心池大小與線程池最大容量相等,線程不會被回收,任務隊列是無界隊列,固定了線程數量,任務不會丟失,適用於長任務
  • 由於都不限制線程創建上限,均可能由於線程創建過多導致OOM

    • newCachedThreadPool:核心池大小爲0,最大線程容量不限,線程空閒回收時間60秒,每個任務都會新建一個線程,空閒線程短時間內會被回收,適用多且短的異步任務。
    • NewScheduledThreadPool:核心池的大小爲傳入參數,最大線程容量不限,使用一個按超時時間決定順序的隊列,可定時執行
  • newSingleThreadScheduledExecutor:核心池與線程池最大容量都爲1的線程池(該線程不會被回收),使用一個按超時時間決定順序的隊列,可定時執行

線程池一般不會使用工具類Executors去創建,而是直接調用ThreadPoolExecutor的構造函數來自己創建線程池,創建的時候自己定義參數創建。

submit和execute的區別:

  • execute提交的方式只能提交一個Runnable的對象,且該方法的返回值是void,也即是提交後如果線程運行後,和主線程就脫離了關係了,當然可以設置一些變量來獲取到線程的運行結果。並且當線程的執行過程中拋出了異常通常來說主線程也無法獲取到異常的信息的,只有通過ThreadFactory主動設置線程的異常處理類才能感知到提交的線程中的異常信息。
  • submit可以提交實現了Callable接口的對象,用get可以返回結果,同時也可以提交一個Runnable的對象。
ThreadLocal

每個線程本地維護一個map,這個map是ThreadLocal的內部類,每個線程自己都有一個獨立的這種類型的map,而map中以ThreadLocal爲鍵,要對應存儲的值爲值建立聯繫,調用ThreadLocal的get方法,其實是在當前線程中取出這個map,在以this(訪問的ThreadLocal實例對象)爲鍵取出,所以實際使用中,需要爲多個線程設計多少個想要獨享的變量,就應該建立多少個ThreadLocal實例對象。ThreadLocal本身與key之間是弱引用,當ThreadLocal被回收後,就導致key對應的value永遠無法訪問,造成內存泄漏。

ArrayBlockingQueue和LinkedBlockingQueue(兩種阻塞隊列)

底層一個使用數組,一個使用鏈表,所以數組隊列有界,鏈表隊列理論無界,兩者都是一種消費生產者模型,但數組使用一把鎖,生產消費都用這把,而鏈表有兩把鎖,一個鎖頭一個鎖尾,一個用於同步生產,一個用於同步消費

CopyOnWriteArrayList

在遍歷時會直接訪問集合,但寫時會copy一份副本,修改完以後再將集合引用指向這個副本,且copy副本時會加lock鎖,不用擔心會在併發情況下copy多份,修改完後將集合引用指向這個副本後,之前開始遍歷的線程依然遍歷舊的集合。

CountDownLatch和CyclicBarrier和Semaphore(同步輔助類)

CountDownLatch使用一個同步隊列加一個計數器count=n(創建實例時傳參傳入),每來一個線程就讓他們進等待隊列,等着獲取共享鎖(同步狀態已經被持有),並讓count減一,當count歸零時,釋放同步狀態讓這n個線程全部獲得共享鎖一起運行。線程調用其await方法會將自身阻塞,調用countDown讓count減1。

而CyclicBarrier也設count=n(創建實例時傳參傳入)爲計數,每來一個就讓它阻塞(調用await方法,await可傳入參數指定等待時間,時間過了柵欄沒開就拋出異常並繼續執行),計數減一,當出現了要讓計數歸零的線程出現時,不阻塞,讓它喚醒所有線程,使得多個線程一起運行。他們的不同點在於,前者使用了共享鎖,等待的是一個事件的到達,並且不可重置,後者使用阻塞,是等待線程的到達,可重置。且可傳入一個Runnable的實現類,在柵欄打開時執行。

Semaphore是信號量機制,它可以掌握n個資源,讓多個線程因爲這個資源進行PV操作。P操作是acquire() (拿不到就阻塞),V操作是release(),並且都可以傳入參數指定獲得和釋放的資源數量。還可以使用tryAcquire(num,time),指定獲得多少資源,拿不到等待多少時間,不傳time默認直接返回。

ConcurrentModificationException異常

對同步類進行迭代時修改會產生這個錯誤,建議在使用iterator迭代的時候使用synchronized或者Lock進行同步;使用併發容器CopyOnWriteArrayList代替ArrayList和Vector。

JVM

jvm在執行java程序時有若干數據區:方法區(類信息,常量,靜態變量,可以不GC),虛擬機棧(存java方法的數據),本地方法棧(存native方法的數據),堆(存對象的地方,GC的主要位置),程序計數器(類似PC)

對象
  • 對象創建:

    • 檢測對象是否被加載,解析,初始化過了,如果未加載需要進行加載
    • 爲對象分配內存(要考慮併發(原子操作或私有空間)),連續內存指針碰撞,不連續空閒列表(實際)
    • 初始化:分配完後,內存初始值全部爲0;然後進行初始化將對象信息賦值
  • 對象結構

    • 對象頭:有兩部分,運行時的數據,像哈希碼,GC分代年齡,鎖狀態標誌等;對象的類型指針,指向它的類元數據,標識對象是那個類的實例。
    • 實例數據:存放有效信息的實例數據。
  • 訪問方式:1.句柄:句柄池兩個指針(指向對象實例數據和方法區中對象類型數據);

    ​ 2.直接指針:指針指向堆區一個地方(實例數據+指向方法區中對象類型數據的指針)。

內存泄漏

當棧申請空間,堆滿,方法區滿都可以產生OOM錯誤,這時除了考慮空間不足,也需要考慮是不是由於產生了內存泄漏,如果長生命週期的對象持有短生命週期的引用,就很可能會出現內存泄露,比如只用於某個方法的局部變量聲明在類屬性中,這樣局部變量必須要等到類回收時纔會回收,所以我們應該儘可能的減少對象的作用域;其次就是容器容易導致內存泄漏,在容器使用完畢後置空比較合理;靜態變量的生命週期十分的長,也是可能導致內存泄漏的原因之一,尤其是靜態集合;還有就是單例模式生命週期也是十分的長;內部類整個生命週期都會持有外部類的對象,所以內部類的引用沒有處理好導致外部類無法回收,可能導致內存泄漏;同理外部模塊的引用也是,處理結束後不刪除這個引用就導致模塊一直持有這個引用;逃逸問題;ThreadLocal(弱引用)被回收,對應線程的map中的鍵值對無法訪問。

棧溢出(java.lang.StackOverflowError)。

  • 減少GC次數的方法

用StringBuilder代替String,避免常量池在字符串變化過程中產生大量字符串常量;創建對象時避免在短時間內創建大量對象,用時創建;不用的對象,賦值null,方便回收;儘量少的創建靜態變量,尤其是靜態的集合;監聽器不容易被回收;數據庫等連接要記得關閉並小心監視器這樣不好回收的對象;避免逃逸,比如儘可能的使用基本標量,就不使用封裝的基本類;內部類的引用小心處理,否則導致外部類無法回收,同理外部模塊的引用。

垃圾回收
  • 死亡判斷程序計數器(記錄引用),可達性分析(從GC root開始能到的對象)

  • GC root對象:虛擬機棧和本地方法棧引用的對象,方法區中的靜態屬性引用對象和常量對象

  • 強引用:垃圾收集器永遠都不會回收的對象,一般用於程序代碼中普遍存在的引用。

  • 軟引用:描述一些還有用但非必需的對象,發生內存溢出異常之前會把這些對象列入回收範圍進行回收

  • 弱引用:描述非必需對象,無論是否內存緊張,都會在下一次GC時被回收虛引用

  • 虛引用:沒有強度的等級,完全不會對生存時間構成影響。甚至無法用來取得一個對象實例,設立虛引用的目的是用來在對象被回收時接收一個系統通知

  • finalize方法(每個對象的finalize方法只能執行一次,該方法執行代價大,一般不使用。)

    • 第一次GC:如果覆蓋finalize方法並沒執行過會進行二次回收(F-Queue隊列,低優先級執行)
    • 第二次GC:若放入F-Queue隊列的對象二次GC的時候還是不可達會被回收,可達被拯救
  • GC觸發
    • Minor GC:新生代回收,eden去空間不足時觸發
    • Full GC:當老年代和方法區空間不足時;Minor GC用老年代當做擔保時老年代空間不足;新生代中創建大對象直接扔到老年代,老年代存不下時;CMS回收時,由於是並行回收,預留空間不足,放入到了老年代中,老年代空間也不足;主動調用System.gc時
  • 垃圾收集算法

    • 標記-清除:標記回收對象後逐個回收,標記和清除效率都低,且產生很大碎片
    • 複製算法:1:1。老年代做空間擔保
    • 標記-整理:標記後,將對象往前移動,效率低,但回收到連續空間
    • 分代收集算法:分老年代和新生代,並進行實際物理分塊。結合複製8:1:1,老年代擔保
  • 垃圾收集器

    • GC停頓:在進行可達性分析時會進行**GC停頓(停掉所有線程)**確保一致性。不需要遍歷整個堆來尋找對象引用再判斷(OopMap的數據結構使虛擬機直接得知什麼地方存放着對象引用)。
    • 安全點:實際中,許多指令都會導致引用發送變化,每次變化都設置一個OopMap不合理,於是設置安全點,每個安全點出記錄引用變化(所以在兩個安全點之間記錄的引用情況不準確),安全點一般設置在方法調用,循環跳轉,異常跳轉,進行GC時要保證所有線程到了安全點,確保機制
      • 搶佔式中斷:停掉所有線程,再喚醒沒到安全點的線程跑到安全點停下,用的比較少
      • 主動式中斷:安全點設flag,線程過路的時候會主動詢問是否需要停下(要gc就設置爲真)
    • 安全區域:安全點無法確保阻塞和掛起狀態的這些線程,他們不能響應中斷,安全區域是一段代碼區,整個區域內引用不發生變化。線程進入後標記自己安全,GC時不管他們,出去詢問GC是否完成。
    • 垃圾收集器
      • Serial收集器:複製算法,回收時暫停所有線程。
      • parNew收集器:多線程版本的Serial,cpu數量有影響。
      • Parallel Scavenge收集器:多線程,複製算法,不關注收集效率,設置每次垃圾回收的量來控制吞吐量和GC的時間。若每次回收過少會導致吞吐量降低和空間不足,吞吐量=runtime/(runtime+GCtime)
      • Serial Old收集器:標記-整理算法,單線程,(作用:java5及之前搭配Parallel Scavenge;作爲CMS後備)
      • Parallel Old收集器:標記-整理算法,Parallel Scavenge的老年代版本,因爲搭配Serial Old拖累性能,java6產生,搭配Parallel Scavenge用於吞吐量和cpu資源敏感(防止cpu使用過高)的地方。
      • CMS收集器:標記-清除算法,分四步,兩步執行時間較長的步驟運行工作線程一起執行,但只能搭配Serial收集器或者parNew收集器。
        • 初始標記:只標記GC Roots直接關聯到的對象,這個過程要使得所有工作線程停頓
        • 併發標記:進行GC Roots Tracing,允許工作線程一起運許
        • 重新標記:工作線程的運行會導致標記發生變動,這一步用於修正這些變動的標記。
        • 併發清除:開始垃圾回收,可以與工作線程一起執行
        • CMS的缺點:
          • 併發標記和併發清除的時候佔用了一部分線程從而導致應用程序變慢,cpu數量有影響。
          • 浮動垃圾:併發清除階段與用戶程序一起執行,所以新產生的垃圾在本次無法回收,所以每次需要預留空間給用戶線程產生新對象,預留過多使GC頻率變高,預留過少可能導致剩餘內存不夠。此時導致虛擬機啓動後備預案使用Serial Old收集器來進行垃圾收集,單線程,停止所有進程,極大拖低效率。
          • 使用標記-清除算法導致可用空間零碎分佈,若出現大的對象無法分配內存時,導致提前觸發Full GC,CMS收集器在進行Full GC會進行碎片整理。
      • G1收集器
        • 特點:
          • 併發與並行:G1利用硬件優勢縮短GC停頓的時間,且部分步驟可以與工作進程併發
          • 分代收集:分代概念在G1中仍有保留,獨立管理整個GC堆。
          • 空間整合:使用標記-整理算法,回收時不會產生可用空間零碎分佈的現象。
          • 可預測的停頓:與CMS一樣,都追求降低停頓,但其還能預測停頓的時間模型,預測停頓的時間,
        • G1把java堆分成多個區域,且爲區域設置優先級,優先級高的區域先回收,每個區域大小可以配置具體大小,區域有四種,新生代eden,新生代survivor,老年代,大對象區域。
        • 區域之間的對象可能相互引用,爲了保持區域獨立性又不需要掃描整個堆,每個區域都有自己的Remembered Set,記錄引用的其他區域的對象,可達性分析就檢查本區域和區域的Remembered Set
        • G1的步驟分爲四步
          • 初始標記:類似CMS,標記GC Roots能直接關聯到的對象,需要進行GC停頓,然後標記區域使得下一次併發時,用戶進程找到正確的Region進行新對象創建。
          • 併發標記:類似CMS,進行可達性分析,尋找要回收的對象,耗時較長,可併發執行用戶進程
          • 最終標記:修正併發標記期間,用戶進程併發執行引起的引用關係變化,變化消息記錄在線程Remembered Set Log中,需要把數據合併到Remembered Set中,這階段可停頓,也可並行執行。
          • 篩選回收:根據優先級來逐個進行區域的回收。
      • 若對象分配過快來不及回收會觸發full gc,使用Serial收集器作爲回收策略
內存分配
  • 對象優先在Eden區分配內存,Eden區空間不足進行GC,GC時若survivor存不下了,則放入老年代(空間擔保)
  • 大對象(設定參數值)直接在老年代中分配,避免Eden區內存耗費過快,提前GC
  • 長期存放在新生代的對象會放入老年代,每個對象維護年齡計數器,每經歷一次GC就加1
    • 若年齡大於設置的閾值就放入老年代
    • 若同樣年齡的對象佔用內存達總內存一半,大於等於這個年齡的對象放入老年代
  • GC時空間擔保:保證老年代中最大可用連續空間大於新生代所有對象空間大小,則可擔保進行GC,若不夠,jvm允許時會嘗試進行冒險(最大可用連續空間大於歷屆晉升老年代對象平均大小)
內存調試工具
  • jps: 列出正在虛擬機運行的虛擬機進程,並顯示虛擬機執行主類的名稱,以及這些進程的本地虛擬機的唯一ID。
  • jstat : 監視虛擬機各種運行狀態信息的命令。可以顯示本地或遠程虛擬機進程中類裝載,垃圾收集,JIT編譯,內存等數據。
  • jinof: 實時查看和調整虛擬機的各項參數。
  • jmap(堆): 生成堆轉存儲快照,查詢fianlize執行隊列、java堆和永生代詳細信息,如空間使用率,當前用的是那種收集器。
  • Jhat: 和jmap搭配使用,來分析jmap生成的堆轉存儲快照。內置一個微型的HTTP/HTML服務器,生成dump文件的分析結果後,可以通過瀏覽器查看
  • jstack(線程):用於生成當前時刻線程快照.線程快照是當前虛擬機內每一條線程正在執行的方法堆棧的集合.生成線程快照的主要目的是爲了定位線程長時間停頓的原因.如死鎖,死循環,請求外部資源導致的長時間等待.
  • JConsole:可視化監視和管理工具,幾乎包括以上工具的所有功能
類加載

類的加載被分爲了七個階段:

加載–>驗證–>準備–>解析–>初始化–>使用–>卸載

  • 加載:通過一個類的全限定名(靈活度提高)來獲取定義此類的二進制字節流,並把此二進制字節流表示的靜態存儲結構轉化爲方法區運行時數據結構,並生成代表這個類的class對象放在方法區(HotSpot放在方法區,其他的可能不一樣)中作爲類數據的訪問入口。

  • 驗證:確保class文件的自己留包含信息符合jvm的要求,不會危害jvm安全,比如魔術塊,版本檢測,是否符合java規範,符號引用驗證(解析)

  • 準備:爲類變量(static修飾的變量,不是所有的變量哦)分配內存,並設初值(設初值,全部是零值,真正賦值在初始化那裏),但對於像存在final修飾的類變量會使用屬性表附帶的值直接初始化。

  • 解析:將符號引用變爲直接引用。符號引用包含如類引用,方法和字段的引用,都要找到對應的類或接口中尋找,並轉化爲直接引用(指向目標的指針),用全限定名去找。

  • 初始化:收集類中所有賦值語句和靜態塊內容,組合成類構造器,收集順序按照出現順序,且靜態塊裏面對於在其後定義的變量可寫不可讀。然後執行這個類構造器,且執行順序會先執行最頂級父類,從上往下,所有最先執行的永遠是Object(接口(無靜態塊,但有賦值)與實現類順序是先子類)

  • 類加載的時機是由虛擬機自定義實現的,但初始化過程的觸發一般源於以下情況

    • new關鍵字創建實例;讀取設置一個類的靜態字段,調用類的靜態方法;利用反射進行類的調用;初始化子類時,會先初始化其父類;虛擬機會先初始化帶main方法的類
  • 類與類加載器:類加載器只用於實現類的加載動作,但作用卻遠不止此,判斷兩個類是否相等,相等的前提條件是兩個類由同一個類加載器加載(不然就算兩個類來着與同一個class文件,也會判斷不相等)

  • 雙親委派模型

    同一個class文件若被不同的類加載器加載會導致兩個類不相等,而由於自定義類加載器的存在,這可能導致許多最基本的行爲無法保證,應用程序變得混亂(如自定義加載器加載Object類到不同路徑下,導致不相等,會導致需要混亂後果)

    於是引入了雙親委派模型,首先要說一下三種系統提供的類加載器

    • 啓動類加載器Bootstrap ClassLoader(虛擬機的一部分)
      是嵌在JVM內核中的加載器,該加載器是用C++語言寫的,主要負載加載JAVA_HOME/lib下的類庫和被-Xbootclasspath參數指定路徑下由虛擬機識別的類庫,啓動類加載器無法被應用程序直接使用(涉及虛擬機本地實現細節)。
    • 擴展類加載器Extension ClassLoader:
      用JAVA編寫,且它的父類加載器是啓動類加載器(查看父類爲空,因爲啓動類是c++寫的),主要加載JAVA_HOME/lib/ext目錄中的類庫。開發者可以直接使用擴展類加載器。
    • 應用程序類加載器(Application ClassLoader),也稱爲系統類加載器,默認負責加載應用程序classpath目錄下的所有jar和class文件(用戶寫代碼路徑下)。它的父加載器爲擴展類加載器。

    自定義加載器的父類加載器是應用程序類加載器,但類加載器之間的父子關係不是使用繼承來實現的,而是使用組合關係來複用父加載器的代碼。

    雙親委派模型:
    如果一個類加載器收到了一個類加載請求,它不會自己去嘗試加載這個類,它會調用自己的loadClass方法:首先把這個請求轉交給父類加載器去完成。每一個層次的類加載器都是如此。因此所有的類加載請求都應該傳遞到最頂層的啓動類加載器中,只有到父類加載器反饋自己無法完成這個加載請求(在它的搜索範圍沒有找到這個類)時,子類加載器纔會調用自己的findClass方法嘗試自己去加載。
    委派的好處:

    類加載器帶有優先級的層次關係,避免有些類被重複加載;保證了Java程序的穩定運行;實現簡單。

當我們自己寫java包下的類時,並打包到同樣的包下,由於雙親委派模型的原因,這個類不會被加載,因爲已經有一個一樣的類被加載了,這時會返回異常。

線程上下文加載器:第二次破壞時建立,爲了打破父級循環調用子類加載器,第三次熱部署。

參數調優
  • 常用基礎參數:-Xms:初始化堆大小;-Xmx:最大堆大小;-Xmn:年輕代大小;-Xss:棧大小參數;-XX:SurvivorRatio:Eden區與倖存區的大小比例;晉升年齡設置;直接在老年代中分配內存的大對象的閾值設置。
  • 並行收集器參數:設置年輕代,老年代使用並行收集器;配置並行收集的線程數;設置收集時間與吞吐量
  • CMS參數:設置使用CMS收集器;設置利用full GC來整理空間,整理碎片;因爲浮動垃圾無法清除的原因,設置進行回收時是在空間使用多少時觸發;
  • 調優思路:把-Xms和-Xmx設置的一樣大,避免初始化很多對象時,多次增加內存;內存分配一般要與處理器個數成正比;對應響應時間敏感和吞吐量敏感的吧年輕代設置大一些,避免minor GC頻繁觸發;使用CMS時要特別注意空間碎片,合理使用壓縮;
鎖優化

1.自旋鎖:許多應用,共享數據的鎖定狀態只會持續很短一段時間忙循環(自旋),避免了線程切換的開銷;自適應自旋鎖,自旋的時間不再固定(自旋容易獲得就多次自旋等待,不然就減少或不旋轉)

2.鎖消除:被檢測到不可能存在共享數據競爭的鎖進行消除

3.鎖粗化:有一串零碎的操作都對同一個對象加鎖,將會把加鎖同步的範圍粗化到整個操作序列的外部。

4.輕量級鎖

代碼進入同步塊時,虛擬機使用CAS操作嘗試將對象的Mark word(指向重量級鎖的指針) 更新爲指向線程棧的Lock Record的指針,如果更新成功,則輕量級鎖獲取成功*,記錄鎖狀態爲輕量級鎖;*否則,說明已經有線程獲得了輕量級鎖,目前發生了鎖競爭(不適合繼續使用輕量級鎖),接下來膨脹爲重量級鎖。(使用CAS代替重鎖進行控制,效率更高,但不適用於競爭大的時候)

5.偏向鎖:那偏向鎖就是在無競爭的情況下把整個同步都消除掉,連CAS都不做。偏向鎖的意思是這個鎖會偏向第一個獲得它的線程,如果在接下來的執行過程中,該鎖沒有被其他線程獲取,則持有偏向鎖的線程不需要再進行同步。

消息隊列

流量削峯,異步處理,程序解耦。

秒殺系統

將庫存預存到redis中,前臺首先應該禁止同一個用戶重複提交請求,然後前臺秒殺時來臨的大量請求先過中間件(消息隊列)進行緩衝(比如用一個原子變量做key,用戶id做值),key對應就買到的庫存值,達到額定庫存值後,不再進行隊列的插入操作。後續用戶均不在入隊,全部是未搶到商品狀態。然後後端再從消息隊列中取數據依次處理。(若不使用key對應庫存的想法,可對庫存進行減操作再比較,使用decr命令)

  • 可靠投遞(最終一致性):發送方需要收到接收方的確認消息,確定消息已經到達,纔會刪除消息,不然都當做接收方未收到,但這樣會導致消息重複(如果是單方發送可以利用版本號機制來避免重複)
  • 消息重複消費問題:對於本身冪等的消息沒事,但對應修改數據庫等操作是明顯不能重複消費的。消費者更加消息進度去消費消息,還沒來得及提交進度,就crash了,還沒提交消費進度,重啓就會按照沒更改的消費進度去重複消費同一條消息,這種只能儘可能保證不發生crash,或者耗費時間在本地記錄發生的消費避免重複消費。對於另一種消費者轉換消息隊列消費時,應當先提交消息進度再切換,避免重複的消費,同時使用新消息隊列時也要保證原消費者提交了進度。重要的要保證冪等性(給消息帶上id,最近消費過的幾條id存入redis,每次消費一條消息之前都在一個儲存中查詢一下)。
  • 消息隊列消息積壓,一般消息積壓說明消費者有問題,可能死在了某個地方,無法繼續消費,所以首先解決消費者問題,讓消費者恢復正常;接下來要對消息隊列進行擴容,避免消息隊列爆掉,其次增加消費者,把積壓的消息先處理掉。對於可能過期丟失的消息,晚上加班寫代碼排查重新入隊。

JDBC

註冊驅動,建立連接,創建Statement進行sql的運輸,利用Statement執行sql語句。

事務

通過連接的setAutoCommit(設置事務開始點)commit(提交事務)rollback(回滾事務),事務原子性被違背時會返回SQLException,利用try,catch實現,在try最後提交事務,在catch裏面回滾事務並關閉連接。

servlet

訪問servlet,及生命週期(127.0.0.1:8080/app/hello)

根據ip和端口請求發到tomcat;解析app對應的應用,找到其對應的web.xml文件;在web.xml中根據映射hello找到本地對應那個類的那個方法;在tomcat下找整個類文件;第一次要初始化servlet(調用init);執行(request包含請求內容);返回給服務器;服務器讀取response;發送http給客戶端;servlet會一直存在到服務器被關閉或應用被卸載。

servlet是單例的,web服務器中有線程池,每個請求都會去請求一個線程,由線程去調用servlet,單例的好處在於減少開銷,然後用多線程調用單實例。

ServletContext

所有Servlet共享同一個ServletContext對象。ServletContext對象通常也被稱之爲context域對象

request&response

獲取客戶機提交的數據找request對象。

  • 儲存了客戶端發送信息的頭和參數,數據等;
  • 應用:獲取表單信息,請求轉發(getRequestDispatcher().forward),封裝數據進去讓轉發的服務器取。

向容器輸出數據找response對象。

  • 響應頭裏面指定編碼格式,發送http頭,控制刷新網頁,用輸出字節流給客戶端輸出數據
  • 應用:文件下載,通知客戶端請求重定向(response.sendRedirect(),302/307狀態碼)
cookie&session
  • Cookie

Cookie是客戶端技術,程序把每個用戶的數據以cookie的形式寫給用戶各自的瀏覽器(程序給瀏覽器)。用戶會帶着自己的數據訪問服務器中的web資源。

當瀏覽器第一次訪問服務器時,服務器會爲用戶生成一個cookie(sessionId要通過這個找到),並通過響應發給用戶,瀏覽器收到響應會把自己的cookie存下來,以後發送請求時就會帶上。

  • HttpSession

Session是服務器端技術,服務器在運行時爲每一個用戶的瀏覽器創建一個其獨享的HttpSession對象,由於session爲用戶瀏覽器獨享,所以用戶在訪問服務器的web資源時,可以把各自的數據放在各自的session中,session爲值與key成爲映射,而服務器發送給瀏覽器的cookie就保存了這個key,所以瀏覽器帶cookie訪問服務器其他資源時,就能拿到對應的session。

5.jsp

JSP實際上就是Servlet,可嵌java代碼,JSP既用java代碼產生動態數據,又做美化會導致頁面難以維護

Listener和Filter
  • Listener
    • 監聽web常見對象(HttpServletRequest,HttpSession,ServletContext)
    • 監聽對象創建銷燬,屬性變化,session綁定javaBean(配置只需要配置監聽器類,監聽器就能運行)
    • 步驟:自定義類實現對應監聽器接口,重寫方法,web.xml中配置監聽(綁定javaBean不需要配置)
  • Filter
    • 攔截訪問web資源的請求與響應操作(Jsp, Servlet, 靜態圖片文件或靜態 html 文件等)
    • 作用:實現URL級別的權限訪問控制、過濾敏感詞彙、壓縮響應信息等一些高級功能
    • 步驟:自定義類實現Filter接口,重寫方法,web.xml中配置(配置這個類及要攔截那些請求)
    • 多個Filter時會逐個訪問,按照在web.xml裏配置的順序
    • 在監聽器內部能拿到request,並進行操作後調用doFilter後纔算請求過了這層過濾器,不然不能訪問。

Spring

IOC

控制反轉,把創建對象的權限有java程序交付給spring容器

  • DI:依賴注入,爲要創建的對象提起綁定屬性等(構造函數,set),實現松耦合(面向接口),依賴注入方式有構造函數,setter,註解注入。
  • ApplicationContext和BeanFactory:Spring中提供的兩種IoC容器。A是B的子類,功能更強大。
    • BeanFactory:無特殊指定採用延遲初始化的機制,即訪問容器中的對象時纔會對其管理對象進行初始化和依賴注入。啓動快,適合資源受限的場景
    • ApplicationContext:提供了許多其他高級特性,且容器初始化時就初始化其管理對象,啓動較慢,資源要求較多,更適用於功能要求更高的時候
  • 信息加載:容器中每一個對象都對應一個BeanDefinition實例對象,此對象用於保存注入對象的所有信息(class類型,抽象類,構造方法參數等)。用一個專門解析配置文件的類(XmlBeanDefinitionReader),負責讀取spring指定格式的xml配置文件並解析,將解析後的文件內容映射爲相應的BeanDefinition。
  • Bean的註冊:容器創造一個對象的過程稱爲Bean的註冊,實現Bean的註冊的接口爲BeanDefinitionRegistry BeanFactory其實也只是接口,真正的實現類是同時實現這兩個接口的,註冊利用BeanDefinitionRegistry接口中的registerBeanDefinition(BeanDefinition beandefinition)方法來進行Bean的註冊。
  • ApplicationContext容器初始化過程
    • 調用父類容器的構造方法(super(parent)方法)爲容器設置好Bean資源加載器(傳參爲空,但父類構造方法會加載一個資源加載器)。
    • 調用父類的setConfigLocations(configLocations)方法設置Bean定義資源文件(.xml)的定位路徑。
    • IoC容器對Bean定義資源的載入:refresh()函數,refresh()方法的作用是:在創建IoC容器前,如果已經有容器存在,則需要把已有的容器銷燬和關閉,以保證在refresh之後使用的是新建立起來的IoC容器。refresh的作用類似對IoC容器的重啓,在新建立的容器中對容器進行初始化,對Bean定義資源進行載入
      • 調用容器準備刷新的方法,獲取容器的當時時間,同時給容器設置同步標識 (prepareRefresh())
      • 加載beanFactory(最常用實現XmlBeanFactory)
        • 調用本類的obtainFreshBeanFactory()方法去調用子類的refreshBeanFactory()方法
        • refreshBeanFactory()中首先判斷BeanFactory是否存在,存在則銷燬beans並關閉beanFactory;接着創建DefaultListableBeanFactory,並調用loadBeanDefinitions(beanFactory)裝載bean定義。
        • loadBeanDefinitions(beanFactory)裏面創建了Bean讀取器XmlBeanDefinitionReader(得到要加載的資源,資源加載器就在這裏用到的),其負責讀取spring指定格式的xml配置文件並解析,將解析後的文件內容映射爲相應的BeanDefinition,處理每個Bean元素和元素的值,最後將 beanDefinition 註冊進 BeanFactory。
        • 處理每個Bean元素和元素的值,最後將 beanDefinition 註冊進 BeanFactory。
      • 加載bean
        • refresh方法中有個方法會實例化非懶加載的單例Bean
        • 調用preInstantiateSingletons方法分析BeanDefinition判斷,如果是自己配置的Bean調用getBean方法
        • 調用doGetBean方法,這裏會發現Spring把實例好的Bean存入singletonObjects中,這是一個ConcurrentHashMap,但是這裏bean還未進行實例化,轉入下面語句執行
        • 在這裏拿到RootBeanDefinition檢查,並獲得Bean的依賴,循環迭代實例化依賴Bean,完成之後判斷是否是單例的或者非單例,區別在於單例是被緩存起來的,非單例不用緩存,最終調用createBean(beanName, mbd, args)
        • 調用instantiate(RootBeanDefiniton, beanName, BeanFactory)方法,判斷如果沒有無參構造器則生成CGLIB子類,否則直接反射生成實例,調用instantiateClass調用Constructor的newInstance方法
        • 有了實例對象之後,調用postProcessPropertyValues方法委託InjectionMetadata對象完成屬性注入,依賴關係保存在checkedElements集合裏,然後執行inject方法,反射注入
bean的生命週期
  • 實例化:通過new關鍵字將bean實例化,使用默認構造函數
  • 填入屬性:spring用bean讀取器根據配置的xml文件爲每個bean生成了BeanDefinition,BeanDefinition中保存了每個bean的詳細信息,spring會調用bean的setter方法,將屬性注入(最先)。
    • 首先調用Bean自身的方法,配置文件中⑥bean的init-method(自定義初始化方法)
    • Bean級生命週期接口方法,①BeanNameAware的setBeanName(設置bean的id屬性,讓bean獲取得自身的id屬性)、②BeanFactoryAware的setBeanFactory(設置bean對應的工廠讓Bean擁有訪問beanFactory的能力,但增加了耦合度)、③ApplicationContextAware的setApplicationContext(設置bean對應的容器讓Bean擁有訪問Spring容器的能力,但增加了耦合度)、⑤InitializingBean的afterPropertiesSet方法(屬性初始化後的處理方法)
    • 容器級生命週期接口方法,BeanPostProcessor(初始化前後添加一些自己的邏輯處理) 兩個對應實現類的④postProcessAfterInitialization和⑦postProcessAfterInitialization方法。
  • bean準備就緒,可以正常使用
  • 銷燬,銷燬時調用bean的destroy-method,根據scope配置的不同銷燬時間也不一樣
    • singleton單例:spring的bean默認是單例的,此時bean創建的是唯一實例,容器初始化時被創建,容器銷燬時銷燬
    • prototype原型:每個請求或每次使用getbean方法對應創建一個新的bean,銷燬釋放由客戶端代碼決定。
    • request:每一次Http對應一個新的bean,僅在此次請求request中有效
    • session:每一次Http對應一個新的bean,僅在此次http中的session中有效
    • globalsession:類似上面
AOP

爲業務功能進行增強,且增強對業務透明。

  • 動態代理:竊取消息並裝飾,運行時增強
    • JDK動態代理(目標類是實現了接口的類,可以用,也可以用CHLIB)

      • 自定義事件處理器(invoke方法中就能指定方法前後要幹嘛了)
        • 事件處理器實現InvocationHandler接口,並在屬性中創建Object對象保存要代理的對象實例
        • 重寫invoke方法,invoke方法中參數爲代理對象實例,方法對象,方法參數,method.invoke(target,args);可以執行傳入的方法並返回實際返回值,這段代碼前後就加自己的邏輯
        • 通過Proxy.newProxyInstance(classLoder,interfaces,事件處理器實例)返回傳入目標的代理類
        • 事件處理器也往往使用匿名類來傳入
      • 利用反射機制生成一個實現代理接口的匿名類,在調用具體方法前調用InvokeHandler來處理。
      • 代理類繼承了Proxy類並且實現了要代理的接口,由於java不支持多繼承,所以JDK動態代理不能代理沒有接口的類
    • CGLIB代理(對指定的類生成一個子類,覆蓋其中的方法)

      • 自定義事件處理器(intercept方法中就能指定方法前後要幹嘛了)
      • 事件處理器實現MethodInterceptor接口,實現intercept方法,保存要代理對象的實例
      • 使用Enhancer保存要代理對象的類加載器,設計回調方法的類實例(事件處理器),並創建返回
      Enhancer enhancer=new Enhancer();
      enhancer.setSuperclass(target.getClass());
      enhancer.setCallback(this);
      return enhancer.create();
      
    • 利用asm開源包,對代理對象類的class文件加載進來,通過修改其字節碼生成子類來處理。

    • JDK調用代理方法,是通過反射機制調用,Cglib是通過FastClass機制直接調用方法,Cglib執行效率更高。

  • 靜態織入:特定語法來創建方面,編譯時織入,類加載期織入
    • 自定義類實現各種方法(類前加@Aspect表示這是切面類)
      • 在每個方法前加註解表示把這個方法織入到那個業務方法的那個位置,同時在spring配置這個切面(聲明切面的存在)
      • 另一種方法方法前不加註解指定,而是寫完方法後直接去配置spring.xml,在配置文件中指定切面類中的方法要織入的那些業務的那些位置。

事務(特性:ACID,原子,一致,隔離,持久)

  • 在xml中配置事務管理器(用作管理事務,不由spring負責)和啓動事務註解,Spring事務處理模塊通過AOP功能來實現聲明式事務處理,AOP生成代理對象,然後把事務處理的功能編織到攔截的方法中。
  • 事務註解-@Transactional
  • 傳播行爲
    • REQUIRED:當前方法必須運行在事務中,有事務存在就允許,沒有就創建了運行
    • SUPPORTS:不需要事務上下文,存在事務就在當前事務運行
    • MANDATORY:必須在事務上運行,沒有事務拋異常
    • REQUIRED_NEW:當前方法必須運行在自己的事務中,事務中調用方法會爲這個方法開啓一個新事務,並把當前事務掛起,等待新事務提交或回滾再執行自己的事務。
    • NOT_SUPPORTED:當前方法不能運行在事務中,否則導致事務掛起
    • NEVER:當前方法不能運行在事務中,否則拋異常
    • NESTED:允許事務嵌套,嵌套的事務可單獨回滾和提交
  • 隔離級別
    • DEFAULT:後端數據庫默認隔離級別(mysql:repeatable read;oracle:read commited)
    • read uncommited:最低級別,不加鎖,允許讀取未提交數據,可能出現髒讀,不可重複讀,幻讀
    • read commited:寫的過程行鎖鎖住數據,避免髒讀,可能出現不可重複讀,幻讀
    • repeatable read:行鎖鎖住數據整個事務,避免髒讀和不可重複讀,可能出現幻讀
    • serializable:表鎖鎖住數據整個事務,避免三種情況,效率最低,鎖整個表。

SpringMVC

在這裏插入圖片描述

(1)客戶端通過url發送請求

(2-3)核心控制器Dispatcher Servlet接收到請求,通過系統或自定義的映射器配置找到對應的handler,並將url映射的控制器controller返回給核心控制器。

(4)通過核心控制器找到系統或默認的適配器

(5-7)找到的適配器(springmvc定義了多種接口實現,所以需要適配器),調用實現對應接口的處理器,並將結果返回給適配器,結果中包含數據模型和視圖對象,再由適配器返回給核心控制器

(8-9)核心控制器將獲取的數據和視圖結合的對象傳遞給視圖解析器,獲取解析得到的結果,並由視圖解析器響應給核心控制器

(10)核心控制器將結果返回給客戶端

Mybatis

動態SQL
  • if用作判斷

    <if test="id!=null">
    id=#{id}<!--sql語句-->
    </if>
    
  • where用作拼接去掉多餘前置and和or

    <where>
                <if test="id!=null">
                    id=#{id}
                </if>
                <if test="lastName!=null and lastName!=&quot;&quot;">
                    AND last_name LIKE #{lastName}
                </if>
                <if test="email!=null and email.trim()!=&quot;&quot;">
                    AND email=#{email}
                </if>
                <!--ognl會進行字符串與數字的轉換判斷-->
                <if test="gender==0 or gender==1">
                    AND gender=#{gender}
                </if>
    
     </where>
    
  • trim去掉自定義前後綴,添加自定義前後綴,prefix:前綴,trim標籤體是整個字符串拼串後的結果,
    prefix給拼串後的整個字符串加一個前綴
    prefixOverrides:前綴覆蓋:去掉整個字符串前面多餘的字符
    suffix:後綴,給整個字符串後綴加一個後綴

    suffixOverrides:後綴覆蓋:去掉整個字符串後面多餘的字符

     <trim prefix="where" suffixOverrides="and">
                    <if test="id!=null">
                        id=#{id} AND
                    </if>
                    <if test="lastName!=null and lastName!=&quot;&quot;">
                        last_name LIKE #{lastName} AND
                    </if>
                    <if test="email!=null and email.trim()!=&quot;&quot;">
                        email=#{email} AND
                    </if>
                    <if test="gender==0 or gender==1">
                        gender=#{gender}
                    </if>
         </trim>
    
類型轉換

在使用mybatis時都是直接傳入對象,返回也是直接返回對象,所以在傳入對象到實際儲存之間有一個轉換器,我們需要自定義轉換器實現TypeHandler接口,比如String數組需要轉換爲String,中間用某些符號進行分割,才能用varchar存進數據庫。

緩存
  • 一級緩存:通過配置文件開啓一級緩存後<setting name="localCacheScope" value="SESSION"/>,通過配置value的值爲SESSION或者STATEMENT指定一級緩存當前會話內(sqlSession)對整個緩存有效,還是當前的Statement有效。這時在配置範圍內的多條一樣的sql中(內部使用多個關鍵字段結合做鍵存在hashmap中),除了第一條sql語句會去訪問數據庫後更新緩存,後面的都會直接讀緩存。同一個sqlSession更新數據後會導致一級緩存失效,但要注意一級緩存只在單個會話內有效,如果另一個會話更新數據是不會導致緩存失效的,就可能導致了緩存與實際數據的不一致現象。

  • 二級緩存:<setting name="cacheEnabled" value="true"/>二級緩存的開啓使得緩存對於所有會話有效,二級緩存會被多個sqlSession共享。訪問順序:二級緩存、一級緩存、數據庫。當一個會話執行修改時,二級緩存會全部失效(缺點,所以適用於多讀少寫場景),另一個會話會直接訪問數據庫。

Maven

生命週期
  • clean 清理之前的項目
    • pre-clean:執行清理前的準備工作
    • clean:清理上一次構建的項目
    • post-clean:執行清理的後續
  • default構建項目
    • 比較多,一般先要處理主資源文件,然後編譯項目的主源代碼,處理項目的測試資源文件並編譯,測試運行程序(test)
    • package:將編譯好的主資源文件打包爲可發佈格式,比如jar
    • install:將包安裝到本地倉庫工本地其他maven項目使用
    • deploy:將最終的包複製到遠程倉庫,供其他人員和項目使用
  • site建立項目站點
    • pre-site,site,post-site:生成項目站點文檔
    • site-depoly:將生成項目站點發布到服務器
解決jar衝突

由於jar包存在傳遞依賴問題,在導入一個包時同時也會導入其依賴的其他包,然後再導入新導入包的依賴包,這樣由於兩個jar包依賴同一個jar包的不同版本就產生了包衝突現象。

maven處理方式是兩個不同版本的包只導入一個,選擇思路爲最短路徑優先(選擇依賴層數少的那個),最先聲明優先(誰先要求導入,要那個,後面聲明的衝突包就不要了)

設計模式

jdk用的設計模式:Format工廠模式,單例RuntimeException,適配器Arrays.asList,裝飾器Reader和Writer,代理Proxy,迭代器模式

單例模式

  • 餓漢:線程安全,但單例會被直接創建,如果沒使用,會存在浪費(static)
  • 懶漢:線程不安全,但單例只有在被使用的時候纔會創建(static)
    • 雙重校驗鎖:判空,同步代碼塊(class對象),判空,初始化(變量static,volatile(避免重排序))
    • 同步方法:同步方法,只創建一次,效率低(變量static,方法synchronized(class對象))
  • 靜態內部類:靜態內部類中創建靜態實例,通過另一個方法返回其靜態實例。加載類時不加載靜態內部類,調用方法返回靜態內部類實例纔會初始化,jvm保證只初始化一次。
  • 枚舉:寫法簡單,併發安全,加載時,賦值其實是在靜態塊初始化,類似餓漢模式,jvm保證只會初始化一次,還能防止序列化破壞單例對象(自由序列化,編譯器禁用了一些方法防止單例被破壞)。

簡單工廠

創建一個工廠類,存在一個創建方法,根據傳入的參數來返回對應的類(多態)

Produce produce = new Factory.createProduct1);//對應參數返回各種子類produce

工廠方法

使用一個抽象類定義工廠,其中定義一個抽象方法,抽象方法需要返回一個produce,使用另一個方法來返回這個抽象方法的返回值,而每個produce對應的子類都寫一個工廠類來繼承這個抽象類工廠,並實現創建的抽象方法。

抽象工廠模式

抽象工廠模式一次創建的是多個對象,而不是一個,假設現在有A產品3種,B產品3種,並一一對應是三個組合,那麼需要有三個工廠(都具有方法createA()和createB()),保證每種工廠的方法創建出一種組合。思路是首先創建兩個類AbsA和AbsB,並讓A的3種類繼承AbsA,B的三種類繼承AbsB(此時就相當於用繼承對AB進行了分類);創建抽象類AbsFactory(具有抽象方法createA()和createB()),此時再創建三種工廠Factory1,Factory2,Factory3都繼承AbsFactory,實現其兩個抽象方法,分別返回組合A1和B1,A2和B2,A3和B3。

觀察者模式

一個主題和多個觀察者,主題內維護一個列表,列表中放置觀察主題的對象,主題可以設置自己的參數狀態(同時更新觀察者的狀態),可以加入觀察者,刪除觀察者。觀察者初始化時傳入一個主題,直接讓當前對象進入主題維護的隊列中,且觀察者具備一個update方法,方便主題修改自身狀態時,逐個調用列表中所有觀察者的update從而實現同步觀察者狀態的目的。

策略模式

一組算法在運行時動態的選擇合適的算法,比如定義了排序算法接口Sort,方法sortMethod(),現在有冒泡,插入,希爾,快速,歸併等多種算法都實現這個接口,並實現不同種類的sortMethod()。並自定義類DoSort中用Sort定義一個排序類對象sort,這個對象可以隨時通過set方法修改對應的實體類,這樣隨時動態修改sort,外部創建一個DoSort後,傳入不同的實例類就可通過sortMethod()使用不同的排序算法。

適配器模式

假設現在有A和B兩個接口,然後對應實現類AI和BI,內部具有方法aaa()和bbb(),現在想要BI的實例類調用aaa()方法(雞發出鴨叫)。那麼可以創建一個適配器實現A接口,內部定義一個B的變量,實例化時傳入BI實現類給B創建的變量,然後實現aaa()方法(aaa中調用BI實例的bbb()方法)。

裝飾者模式

爲基礎一直添加裝飾,且有多種裝飾,每種裝飾可以重複添加。例如往煎餅裏面加雞蛋,熱狗,雞柳等,每個材料都有自己的價格,根據不同的配方返回總價。策略是定義一個煎餅接口Pan,接口方法返回價格cost(),接下來定義初始的煎餅類Pancake,並實現cost()返回煎餅初始價格。接下來所有的配料類都實現煎餅類,初始化需要傳入當前的煎餅對象,其實現cost()返回自己的價格+傳入煎餅實例的價格(n+pan.cost())。

代理模式

實現同一個接口,代理類和被代理類都實現同一個接口方法,在代理類中定義被代理類的對象,代理類實現接口方法時其實就是調用被代理

靜態代理與動態代理的區別主要在:

  • 靜態代理在編譯時就已經實現,編譯完成後代理類是一個實際的class文件
  • 動態代理是在運行時動態生成的,即編譯完成後沒有實際的class文件,而是在運行時動態生成類字節碼,並加載到JVM中
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章