知識沉澱
多線程
多線程的使用場景
在不阻塞主線程的基礎上啟動其它線程去完成某些比較耗時的任務,例如JavaWeb就是主線程監聽用戶Http請求,然後啟動子線程去處理用戶http請求。Jvm垃圾回收。
如何使用多線程
兩種方式,繼承Thread類,實現Runnable接口。Java是單繼承,更多使用實現接口。new 一個Thread後,執行start啟動子線程。
Runnable可以實現資源共享。
進程/線程間如何通訊
進程間通訊:socket
線程間通訊:
- synchronized關鍵字,多線程訪問同一個共享變量,獲取到對象的鎖的可以執行
- wait/notify機製,Object類的方法
線程的狀態
- 新建:線程對象已經創建,但還沒有調用start()方法
- 可運行狀態:當前線程有資格運行,但還沒被選定為運行線程,當start()方法調用時,線程進入可運行狀態,從阻塞、等待、睡眠狀態回來後也返回到此狀態
- 運行running ,獲取到CPU的時間片
- 睡眠/阻塞/等待:線程仍然存活,但是沒有條件運行,通過某些喚醒操作,可以返回到可運行狀態
- 死亡dead
Synchronized和ReenTrantLock的區別
- 實現依賴:前者依賴JVM實現,後者是JDK實現
- 性能區別:前者優化前性能比後者差,優化後差不多
- ReenTrantLock獨有的功能:
- 可以指定鎖的公平性,前者隻能為非公平鎖
- 後者提供一個Condition類,實現線程的分組喚醒,前者隻能隨機喚醒或者全部喚醒
- 後者提供中斷等待鎖的線程機製,lock.lockInterruptibly()
ReenTrantLock
重入鎖主要集中在Java層麵,所有沒有請求到鎖的線程會進入到等待隊列,有線程釋放鎖後,係統從等待隊列中喚醒線程。
實例化時可指定是否公平。
- 獲取鎖:
- lock(),如果鎖已經被占用則等待
- tryLock(),獲取成功true,失敗false;tryLock(long, TimeUint)定時獲取鎖,不等待立即返回
- 中斷鎖:lockInterruptibly()獲得鎖的時候響應中斷
- 條件變量Condition:
- 一個lock可以對應多個condition,一個condition對象對應一個等待隊列
- 功能和Object.wait()和Object.notify()大致相同
- await()使當前線程等待同時釋放鎖,其它線程使用signal()或者signalAll()時,線程會重新獲得鎖並執行,或者線程被中斷時也可以跳出等待
- signal()方法喚醒一個線程,signalAll()喚醒所有在等待中的線程
- 信號量Semaphore:為多線程協作提供更強大的控製方法,對鎖的擴展
- 無論是內部鎖synchronized還是重入鎖ReentrantLock,一次隻允許一個線程訪問資源,但是信號量可以指定**多個線程,同時訪問某一資源**
- 實例化時可指定是否公平
ReentrantReadWriteLock
讀寫分離鎖,減少鎖的競爭
wait/sleep/yield/join/suspend/resume區別
- wait會釋放對象的鎖,sleep不會
- wait針對同步代碼塊加鎖的對象,sleep是針對一個線程
- yield暫停當前正在執行的線程,隻會讓優先級相同的線程有機會執行
- sleep後的線程在喚醒之後不保證能獲取到CPU,它會先進入就緒態,與其他線程競爭CPU
- join等待調用join方法的線程結束,再繼續執行
- suspend使線程進入阻塞狀態,不會自動恢複,必須其對應的resume被調用才可以進入可執行狀態,suspend和resume會釋放鎖
原子操作,如何同步
不可中斷的一個或一係列操作,利用鎖或Atomic等原子類,實現原理:內部聲明了volatile變量,保證存儲和讀取的一致性。
volatile關鍵字的作用,和synchronized的區別
volatile保證變量在線程工作內存和主存之間一致,它強製線程每次從主內存中講到變量,而不是從線程的私有內存中讀取變量,從而保證了數據的可見性。
- 前者隻能修飾變量,後者還可以修飾方法。
- 前者隻保證數據的可見性,不能用來同步,多個線程訪問volatile修飾的變量不會阻塞。
死鎖/活鎖/饑餓
- 死鎖:兩個或以上的線程在執行過程中爭奪同一資源造成的**互相等待**的現象
- 活鎖:兩個或者多個線程禮讓資源造成的互相等待,最後都無法使用資源
- 饑餓:線程等待訪問一個資源,因為優先級低始終輪不到自己
ThredLocal
提供**線程內部的局部變量**,在線程生命周期內起作用,**隔離其它線程**,減少同一個線程內多個函數或者組件之間一些公共變量的傳遞的複雜度。
如果設置成全局變量,在多線程中獲取到的是同一個值,沒有區分單個線程。
實現原理:
- 早期:每個ThreadLocal類創建一個Map,用線程Id作為Map的Key,實例對象作為Map的Value
- 現在:每個Thread維護一個ThreadLocalMap映射表,這個映射表的Key是ThreadLocal實例本身,Value是真正需要存儲的Object
優勢:
- 每個Map的Entry數量變小了,之前是Thread的數量,現在是ThreadLocal的數量
- 當Thread銷毀後對應的ThreadLocalMap也銷毀,能減少內存使用量
- ThreadLocalMap是使用ThreadLocal的弱引用作為Key的,因為是弱引用,所以gc會有可能回收,導致內存泄漏
使用:建議將ThreadLocal變量定義成private static,這樣ThreadLocal的生命周期更長,由於一直存在ThreadLocal的強引用,所以ThreadLocal不會被回收,也就可以保證任何時候都可以根據ThreadLocal的弱引用訪問到Entry的Value值,然後remove,防止內存泄漏。
線程池
線程池的使用場景
避免多線程頻繁的開啟銷毀線程造成jvm內存的消耗。
常見的線程池有哪幾種
- newSingleThreadExecutor ,單個線程的線程池,線程池中每次隻有一個線程工作
- newFixedThreadPool(n),固定數量的線程池,每提交一個任務就是一個線程,達到最大值進入等待隊列
- newCachedThreadPool,**推薦使用**,可緩存線程池,JVM會自動回收及添加線程
- newScheduledThreadPool ,大小無限製的線程池,支持定時和周期性執行線程
線程池構造
一組線程和一個存放任務的隊列,線程的創建使用銷毀由線程池來管理
corePoolSize:線程池大小,核心池大小
maximumPoolSize:線程池最大容積,超過拒絕,大於corePoolSize開始執行補救措施
線程池的狀態
- running
- shutdown ,不能接受新任務,等待已有任務執行完
- stop ,不能接受新任務,終止當前已有任務
- terminal
任務處理策略
- 如果當前線程池中的線程數目小於corePoolSize,則每來一個任務,就會創建一個線程去執行這個任務;
- 如果當前線程池中的線程數目>=corePoolSize,則每來一個任務,會嚐試將其添加到任務緩存隊列當中,若添加成功,則該任務會等待空閑線程將其取出去執行;若添加失敗(一般來說是任務緩存隊列已滿),則會嚐試創建新的線程去執行這個任務;
- 如果當前線程池中的線程數目達到maximumPoolSize,則會采取任務拒絕策略進行處理;
- 如果線程池中的線程數量大於 corePoolSize時,如果某線程空閑時間超過keepAliveTime,線程將被終止,直至線程池中的線程數目不大於corePoolSize;如果允許為核心池中的線程設置存活時間,那麼核心池中的線程空閑時間超過keepAliveTime,線程也會被終止。
任務緩存策略
- ArrayBlockingQueue:基於數組的先進先出隊列,創建時必須指定大小
- LinkedBlockingQueue:基於鏈表的先進先出,不指定大小則默認最大值
- synchronousQueue:不會保存提交的任務,直接新建線程來執行新任務
ArrayBlockingQueue和LinkedBlockingQueue的區別
- 鎖的實現不同:前者讀寫是同一個鎖,後者分離
- 生產或消費時操作不同:前者在生產和消費時直接將對象插入或移除,後者需要把對象轉換成Node進行插入和移除,影響性能
- 隊列大小初始化方式不同:全部這個必須指定隊列大小,後者可以不指定大小,默認Integer.Max_VALUE
集合類
HashMap實現原理
- 存儲鍵值對,允許null值和null鍵,使用containsKey()判斷一個key是否存在,不能使用get(key)來判斷
- 數據結構:數組+鏈表,鏈表散列,每個 Map.Entry 其實就是一個key-value對,初始值16個bucket
- 工作原理:put()方法存儲數據時,先對key調用hashCode()方法,返回的hashCode用於定位bucket的位置,如果有相同的hashCode(hash碰撞)使用equals()方法比較是否相同,如果相同拋出異常,不相同存入數據。get()方法同理,先定位bucket,再使用Keys.equals()定位到key在鏈表中的節點位置。
- 數據超過負載因子如何處理:默認負載因子0.75,一個map填滿了75%的bucket時候,調用rehashing,實現擴容,為原來2倍。
- Fast-Fail機製:HashMap不是線程安全的,如果迭代過程中有其他線程修改map結構,拋出異常。
- 通過Collections.synchronizeMap(hashMap)可使hashMap線程安全,不過效率低,隻有一個鎖
- 插入數據後校驗是否需要擴容
HashMap和HashTable區別
- 前者允許null值和null鍵,後者不允許
- 前者非線程安全,後者線程安全
- 前者初始值16,大於0.75擴容原來2倍,後者初始值11,大於0.75擴容原來2倍+1
ConcurrentHashMap結構(並發包)
- 由Segment數組結構和HashEntry數組結構組成,Segment時一種可重入鎖ReentrantLock,Segment的結構和HashMap類似,是一種數組和鏈表結構,一個Segment裏包含一個HashEntry數組,每個HashEntry是一個鏈表結構的元素
- 瑣分段,每一個segment一個鎖,所有數據除了value使用final關鍵字修飾,value使用volatile修飾,final修飾表示不能從hash鏈的中間或尾部添加或刪除節點,volatile修飾為了避免加鎖
- 基本操作:put()操作,一律添加到Hash鏈的頭部,remove()操作中間刪除一個節點,會將要刪除節點前麵所有節點複製一遍,最後一個節點指向要刪除節點的下一個節點,刪除後複製回來。
- get()操作不需要鎖,因為值的定義為volatile,
- 首先訪問count變量,由於每次修改操作在修改完後要修改count變量,通過這種機製,保證get操作可以獲取到最新的數據
- 然後根據hash和key對hash鏈進行遍曆找到要獲取的節點,沒找到直接返回null
- 如果值為null,否則在有鎖的狀態下重新讀取一遍
- put()操作在鎖定的正哥segment中執行,超過負載因子時,進行rehash,如果key重複直接覆蓋,不重複則新建一個節點放在hash鏈表頭部,並修改modCount和count的值
- 如何擴容:**插入數據前校驗是否需要擴容**,擴容隻針對某個segment,創建一個兩倍容量的數組,然後再hash後插入到新的數組裏
CopyOnWriteArrayList結構(並發包)
- 寫時複製的容器,這樣做的好處:並發讀取的時候不需要加鎖
- 應用場景:讀多寫少的並發場景,例如白名單、黑名單、商品類目
ConcurrentLinkedQueue結構(並發包)
線程安全的linkedList,高性能的讀寫隊列,不使用鎖而使用非阻塞算法,通過循環判斷尾指針是否改變
CAS:CAS有三個操作數,內存值V、舊的預期值A、要修改的值B,當且僅當預期值A和內存值V相同時,將內存值修改為B並返回true,否則什麼都不做並返回false。
WeakHashMap結構
對hashMap的一種改進,key實行弱引用,一個key不再被外部引用則key可以被gc回收
String/StringBuffer/StringBuilder
- StringBuilder > StringBuffer>String
- String使用final修飾,不可變。
- StringBuilder/StringBuffer是可變字符序列,字符串緩衝區,StringBuilder非線程安全,StringBuffer線程安全
- StringBuilder/StringBuffer擴容:初始值都為16,當前數組容量擴充為原數組容量的2倍+2,如果新容量小於預定的最小值,將容量定位最小值,最後判斷是否溢出,若溢出則將容量設定為整形最大值
JVM
JVM的內存劃分
- 程序計數器,當前線程執行字節碼的行號指示器。
- 棧區,描述的是Java方法執行的內存模型,每個方法被執行時需要創建一個棧幀,用於存儲局部變量表、操作數棧、動態鏈接、方法出口等。每個方法被調用到完成,即出棧。**此區域為線程私有的內存**。
- 本地方法棧,虛擬機棧為虛擬機執行Java方法,本地方法棧則是為虛擬機使用到的Native方法服務。
- 堆區,所有**對象實例**和**數組**都在堆區分配,gc主要在這個區域出現。此區域為**所有線程共享區域**
- 新生代,分為一個Eden和兩個Survivor區
- 老年代
- 方法區,**所有線程共享區域**,存儲被虛擬機加載的類信息、常量、靜態變量、即時編譯後的代碼等數據。gc很少在這個區域出現,回收目標是針對常量池的回收和類型的卸載,也稱**永久代**。
- 運行時常量池,方法區的一部分,存放編譯器生成的各種自變量和符號引用
GC在什麼時候對什麼做了什麼操作
- 什麼時候回收
- Minor GC:對象優先在Eden中分配,當Eden中內存不夠,虛擬機會發生一次Minor GC,Minor GC非常頻繁,速度也很快
- Full GC:發生在老年代GC,當老年代沒有足夠空間時發生Full GC,發生Full GC時一般會伴隨這一次Minor GC。大對象直接進入老年代,例如字符串數組。
- 發生Minor GC時,虛擬機會檢測之前每次晉升到老年代的平均大小是否大於老年代的剩餘空間大小,如果大於則進行一次Full GC,如果小於,則會查看HandlePromotionFailure設置是否允許擔保失敗,如果允許,那隻會進行一次Minor GC,如果不允許,則改為進行一次Full GC。
- 哪些內存需要回收:JVM對不可用的對象進行回收
- 如何判斷一個對象是否可以被回收:采用根搜索算法(GC Root Tracing),當一個對戲那個到GC Roots沒有任何引用相連接,GC Roots到這個對象不可達,則此對象可以被回收。
- 什麼時候被回收:要被回收的對象需要經曆至少兩次標記過程,需要判斷對象在finalize()方法中可能自救,如果重新與引用鏈上的對象建立關聯則不會被回收,如果finalize()方法已經被虛擬機調用執行一次了或沒有要執行的finalize()方法,則將會被GC。
- 如何回收:選擇不同的垃圾收集器,收集算法也不同
- 新生代:大批對象死去,少量存活,使用複製算法,每次使用Eden去和一個Survivor區,當回收時將Eden區和Survivor區還存活的對象一次性拷貝到另一個Survivor區,最後清理掉Eden區和Survivor區。Eden和Survivor默認比例時8:1。保證內存的連續,不會留下內存碎片。
- 老年代中對象存活率高,使用標記-清理或標記-壓縮算法
- 標記-清理:從根節點開始標記所有可達對象,回收後空間不連續
- 標記-壓縮:標記後不複製,存活對象壓縮到內存另一邊,清理邊界外的所有對象。
類加載過程
- 類的加載:類加載機製中的第一步加載,用戶可以通過自定義的類加載器,JVM主要完成三件事
- 通過一個類的名稱(包名與類名)來獲取定義此類的class文件
- 將class文件鎖代表的靜態存儲結構轉化為方法區的運行時數據結構,**方法區**是用來存放已被加載的**類信息,常量,靜態變量,編譯後的代碼**運行時內存區域
- 在內存中生成一個代表這個類的java.lang.Class對象,作為方法區這個類的數據的訪問入口。此對象並沒有放在堆內存中,而是放在方法區中
- 類的連接:負責將類的二進製數據合並到Java運行時環境中,分為三個階段
- 驗證:驗證被加載後的類的數據結構是否符合虛擬機的要求
- 準備:為類的靜態變量在方法區分配內存,並賦默認初始值(0或者null)
- 解析:類的二進製數據中的符號引用轉換為直接引用
- 類的初始化:為靜態變量賦程序設定的初值
類加載器和雙親委派
- 類相等的判定條件:
- 兩個類來自同一個class文件
- 兩個類是由同一個虛擬機加載
- 兩個類是由同一個類加載器加載
- 類加載器分類
- 啟動類加載器:負責Java核心類庫
- 擴展類加載器:負責加載擴展目錄下的jar包
- 應用程序加載器:加載classpath環境變量所指定的jar包與類路徑,用戶自定義的類是由該加載器加載
- 雙親委派加載機製:當一個類收到了類加載請求,它首先不會嚐試自己去加載這個類,而是把這個請求委派給父類去完成,隻有在父類加載器反饋自己無法完成這個請求的時候(在它的加載路徑下沒找到所需加載的class),子類加載器才會嚐試自己去加載。
Spring全家桶
IOC和DI的區別
- 前者是控製反轉,將原本在程序中手動創建對象的控製權交給Spring框架去管理
- 後者是依賴注入,在Spring框架負責創建Bean對象時,動態的將對象依賴屬性通過配置進行注入
AOP
麵向切麵編程,彌補了麵向對象編程的不足,提供了切麵,對關注點進行模塊化,例如橫切多個類型和對象的事務管理
事務管理
- 編程式事務:通過TransactionTemplate手動管理事務
- 聲明式事務:使用XML配置聲明式事務,是通過AOP來實現的,常用的為基於注解方式的事務,在業務層類上添加注解@Transactional
Mysql和NoSql
分布式相關
設計模式
排序算法
最後更新:2017-10-24 10:33:48