第一模塊--並發與多線程
Java多線程方法:
實現Runnable接口, 繼承thread類, 使用線程池
操作系統層面的進程與線程(對JAVA多線程和高並發有了解嗎?)
1.進程
定義:進程是具有一定獨立功能的程序關於某個數據集合上的一次運行活動, 進程是系統進行資源分配和調度的一個獨立單位。
進程的三種基本狀態:
1.就緒狀態:除CPU外已分配所有資源,等待獲得處理機執行
2.執行狀態:獲得處理機,程序正在執行
3.阻塞狀態:因等待而無法執行,放棄處理機,處於等待狀態。(等待I/O口完成,申請緩沖區不滿足,等待信號等)
2.線程:
線程是進程的一個實體, 是CPU調度和分派的基本單位,它是比進程更小的能獨立運行的基本單位。
3.進程和線程的區別:
- 進程的內存空間是獨立的,有獨立的地址空間,不允許突破進程邊界的存取其他進程的內存空間; 線程自己基本上不擁有系統資源,只擁有一點在運行中必不可少的資源(如程序計數器,一組寄存器和棧), 但是它可與同屬一個進程的其他的線程共享進程所擁有的全部資源。Linux的COW技術可以使得子線程只有在對數據段有改變行為的時候才作一個自己的備份。(線程共享同一進程中的內存空間(共享的是進程代碼段、進程的公有數據(利用這些共享的數據,線程很容易的實現相互之間的通訊)、進程打開的文件描述符、信號的處理器、進程的當前目錄和進程用戶ID與進程組ID)/(並且每個線程擁有自己的棧內存))
- 線程的執行速度大於進程。
4. 隸屬關系:
線程屬於進程,進程退出時結束所有線程,線程占用資源少於進程。
多個執行:
兩個進程不能同時執行 (准確的說是在單個CPU上不能同時跑兩個進程,但多個CPU每個核心可以跑多個進程)
多個線程可以同時執行 (所謂的同時指的是concurrent, 采用時間片輪轉的方式來給各個線程分配執行時間。)
多線程程序只要有一個線程死掉,整個進程也死掉
一般來說因為同一進程下的線程間是共享內存資源,因此共享內存也成為理所應當的線程通信的方式。共享內存常用的數據結構有LRU和FIFO。
具體對JAVA來說,有如下的工具可以實現線程通信:
- Object類的wait/notify 方法
- Volatile 關鍵字將線程變量同步到主內存
- Sychronized機制
- CountDownLatch 、CyclicBarrier 、Semaphore等JUC下的並發工具
A.Wait/notify機制(JAVA特有);
方法wait()的作用是使當前執行代碼的線程進行等待,wait()方法只能在同步方法中或同步塊中調用,wait()方法執行后,當前線程釋放鎖,線程與其他線程競爭重新獲取鎖。方法notify()也要在同步方法或同步塊中調用,該方法是用來通知那些可能等待該對象的對象鎖的其他線程,對其發出通知notify,並使進入就緒態,等待獲取鎖。如果有多個線程等待,則有線程規划器隨機挑選出一個呈wait狀態的線程。在notify()方法后,當前線程不會馬上釋放該對象鎖,要等到執行notify()方法的線程將程序執行完,也就是退出同步代碼塊中。
B. 共享內存(下面是JAVA實現):
如果每個線程執行的代碼相同,可以使用同一個Runnable對象,這個Runnable對象中有那個共享數據,例如,賣票系統就可以這么做。
如果每個線程執行的代碼不同,這時候需要用不同的Runnable對象,例如,設計4個線程。其中兩個線程每次對j增加1,另外兩個線程對j每次減1,銀行存取款
6.進程的通訊方式(本質上是不同進程的線程間通信的方式):5種
- 無名管道:
數據只能在一個方向上流動
用於具有親緣關系的進程之間的通信
特殊的文件,存在於內存
- 2.命名管道:
可以在無關的進程之間交換數據
文件形式存在於文件系統
write_fifo的作用類似於客戶端,可以打開多個客戶端向一個服務器發送請求信息,read_fifo類似於服務器,它適時監控着FIFO的讀端,當有數據時,讀出並進行處理,但是有一個關鍵的問題是,每一個客戶端必須預先知道服務器提供的FIFO接口
- 3.消息隊列:
消息的鏈表,存放在內核中。一個消息隊列由一個標識符(即隊列ID)來標識。
消息隊列克服了信號傳遞信息少、管道只能承載無格式字節流以及緩沖區大小受限等缺點。
(消息隊列是面向記錄的,其中的消息具有特定的格式以及特定的優先級。
消息隊列獨立於發送與接收進程。進程終止時,消息隊列及其內容並不會被刪除。
消息隊列可以實現消息的隨機查詢,消息不一定要以先進先出的次序讀取,也可以按消息的類型讀取。)
- 4.信號量:
一個計數器,實現進程間的互斥與同步,而不是用於存儲進程間通信數據。
常作為一種鎖機制,防止某進程正在訪問共享資源時,其他進程也訪問該資源。主要作為進程間以及同一進程內不同線程之間的同步手段
- 5.共享內存:
兩個或多個進程共享一個給定的存儲區
共享內存是最快的一種 IPC,因為進程是直接對內存進行存取。
因為多個進程可以同時操作,所以需要進行同步。
信號量+共享內存通常結合在一起使用,信號量用來同步對共享內存的訪問
五種進程間通訊方式總結
1.管道:速度慢,容量有限,只有父子進程能通訊 。
2.FIFO文件(即命名管道):任何進程間都能通訊,但速度慢 。
3.消息隊列:容量受到系統限制,且要注意第一次讀的時候,要考慮上一次沒有讀完數據的問題 ;信號傳遞信息較管道多。
4.信號量:不能傳遞復雜消息,只能用來同步 。
5.共享內存區:能夠很容易控制容量,速度快,但要保持同步,比如一個進程在寫的時候,另一個進程要注意讀寫的問題,相當於線程中的線程安全,當然,共享內存區同樣可以用作線程間通訊,不過沒這個必要,線程間本來就已經共享了同一進程內的一塊內存。
sleep()方法和wait()方法的區別
線程的資源有不少,但應該包含CPU資源和鎖資源這兩類。
sleep(long mills):讓出CPU資源,但是不會釋放鎖資源。Sleep方法退出CPU時間片的競爭,但鎖住通往某些數據的操作。
wait():讓出CPU資源和鎖資源。
鎖是用來線程同步的,sleep(long mills)雖然讓出了CPU,但是不會讓出鎖,其他線程可以利用CPU時間片了,但如果其他線程要獲取sleep(long mills)擁有的鎖才能執行,則會因為無法獲取鎖而不能執行,繼續等待。
但是那些沒有和sleep(long mills)競爭鎖的線程,一旦得到CPU時間片即可運行了。
1. 這兩個方法來自不同的類, wait是Object類中的方法, sleep是Thread類中的方法。
2. sleep方法沒有釋放鎖(但釋放了CPU),而wait方法釋放了鎖(也釋放了CPU)。
3. wait,notify和notifyAll只能在同步控制方法或者同步控制塊里面使用,而sleep可以在任何地方使用。
4. sleep必須捕獲異常,而wait,notify和notifyAll不需要捕獲異常。
線程的狀態有幾種
Java中的線程的生命周期大體可分為5種狀態。
1. 新建(NEW):新創建了一個線程對象。
2. 就緒態(RUNNABLE):線程對象創建后,其他線程(比如main線程)調用了該對象的start()方法。該狀態的線程位於可運行線程池中,等待被線程調度選中,獲取cpu 的使用權 。
3. 運行態(RUNNING):可運行狀態(runnable)的線程獲得了cpu 時間片(timeslice) ,執行程序代碼。
4. 阻塞態(BLOCKED):阻塞狀態是指線程因為某種原因放棄了cpu 使用權,也即讓出了cpu timeslice,暫時停止運行。直到線程進入可運行(runnable)狀態,才有機會再次獲得cpu timeslice 轉到運行(running)狀態。阻塞的情況分三種:
(一). 等待阻塞:運行(running)的線程執行o.wait()方法,JVM會把該線程放入等待隊列(waitting queue)中。
(二). 同步阻塞:運行(running)的線程在獲取對象的同步鎖時,若該同步鎖被別的線程占用,則JVM會把該線程放入鎖池(lock pool)中。
(三). 其他阻塞:運行(running)的線程執行Thread.sleep(long ms)或t.join()方法,或者發出了I/O請求時,JVM會把該線程置為阻塞狀態。當sleep()狀態超時、join()等待線程終止或者超時、或者I/O處理完畢時,線程重新轉入可運行(runnable)狀態。
5. 死亡態(DEAD):線程run()、main() 方法執行結束,或者因異常退出了run()方法,則該線程結束生命周期。死亡的線程不可再次復生
死鎖產生的原因及解決方法
死鎖:兩個或兩個以上的進程在執行過程中,由於競爭資源或者由於彼此通信而造成的一種阻塞的現象,若無外力作用,它們都將無法推進下去。
注意這里產生死鎖的可以是資源競爭,也有可能是彼此的通信等待。
資源競爭: 進程A, B必須同時持有資源1和2才能執行。 某一時刻進程A持有資源1並嘗試搶占資源2, 進程B持有資源2並嘗試搶占資源1。這時就造成了死鎖。
通信等待: 進程A需要收到B的信號才能繼續往下執行(包括向其他進程發信號),進程B需要收到進程A的信號才能繼續往下執行。這個時候進程A和B就會因為相互等待對方的信號而死鎖。
死鎖產生的四個必要條件
避免死鎖的方法
首先互斥條件一般是無法避免,否則也不會出線多線程的問題了。因此可以從其他三個條件出發,打破其中之一就能避免死鎖。
1. 破壞“請求和保持”條件
當某個線程申請兩個及以上的資源時,讓它要么能一次性申請成功所有資源,要么就不申請任何資源(釋放申請的部分資源)。
2. 破壞“不可搶占”條件
允許進程進行資源搶占。如果線程在持有部分資源的時候這部分資源發生了搶占,則允許這部分資源被搶占。更好的方法是允許操作系統搶占資源,讓優先級大的線程可以搶占其他線程占有但未使用的資源。
3. 破壞“循環等待”條件
a. 將系統中的所有資源統一編號,進程可在任何時刻提出資源申請,但所有申請必須按照資源的編號順序提出。
當多個線程需要相同的一些鎖,但是按照不同的順序加鎖,死鎖就很容易發生。
如果能確保所有的線程都是按照相同的順序獲得鎖,那么死鎖就不會發生。
如果一個線程(比如線程3)需要一些鎖,那么它必須按照確定的順序獲取鎖。它只有獲得了從順序上排在前面的鎖之后,才能獲取后面的鎖。
例如,線程2和線程3只有在獲取了鎖A之后才能嘗試獲取鎖C (獲取鎖A是獲取鎖C的必要條件)。因為線程1已經擁有了鎖A,所以線程2和3需要一直等到鎖A被釋放。然后在它們嘗試對B或C加鎖之前,必須成功地對A加了鎖。
b. 為線程設置加鎖時限
在嘗試獲取鎖的時候加一個超時時間,在嘗試獲取鎖的過程中若超過了這個時限該線程則放棄對該鎖請求。若一個線程沒有在給定的時限內成功獲得所有需要的鎖,則會進行回退並釋放所有已經獲得的鎖,然后等待一段隨機的時間再重試。這段隨機的等待時間讓其它線程有機會嘗試獲取相同的這些鎖。
4. 死鎖檢測及處理
死鎖檢測是一個更好的死鎖預防機制,它主要是針對那些不可能實現按序加鎖、不可設置鎖超時、資源不可搶占、破壞請求保持條件的時間效率不高(如果一次性申請到所有的資源的可能性不大,則會出現頻繁的資源申請、資源釋放操作)。
死鎖檢測的方法
主要需要為所有的線程及資源建立一個資源分配圖,根據資源分配圖,如過資源分配圖沒有環,則系統沒有發生死鎖。(判斷圖是否成環使用拓撲排序算法)
檢測到死鎖了怎么辦:
1. 一個可行的做法是釋放所有鎖,回退,並且等待一段隨機的時間后重試雖然有回退和等待,但是如果有大量的線程競爭同一批鎖,它們還是會重復地死鎖(原因同超時類似,不能從根本上減輕競爭)。
2. 一個更好的方案是隨機選取若干個線程而不是全部線程,讓這些線程回退,剩下的線程就像沒發生死鎖一樣繼續保持着它們需要的鎖。
競態條件 & 臨界區
當兩個線程競爭同一資源時,如果對資源的訪問順序敏感,就稱存在競態條件。導致競態條件發生的代碼區稱作臨界區。在臨界區中使用適當的同步就可以避免競態條件。
如何開啟一個新線程
繼承Thread, 實現Runnable, 實現Callable(使用線程池, ExecutorService)
說一下線程池
A. 使用線程池的原因
-
- 利用線程池管理並復用線程、控制最大並發數等,節約資源,頻繁的創建銷毀線程對系統產生很大的壓力。
- 實現任務線程隊列緩存策略和拒絕機制。
- 實現某些與時間相關的功能,如定時執行、周期執行等。
- 隔離線程環境。比如,交易服務和搜索服務在同一台服務器上,分別開啟兩個線程池,交易線程的資源消耗明顯要大;因此,通過配置獨立的線程池,將較慢的交易服務與搜索服務隔離開,避免各服務線程相互影響。
B. 創建線程池需要使用 ThreadPoolExecutor 類
該類的幾個核心的參數有:
corePoolSize: 表示常駐核心線程數。如果等於0,則任務執行完之后,沒有任何請求進入時銷毀線程池的線程;如果大於0,即使本地任務執行完畢,核心線程也不會被銷毀。這個值的設置非常關鍵,設置過大會浪費資源,設置過小會導致線程頻繁地創建或銷毀。(初始化的時候線程池中並不是直接創建corePoolSize個線程,而是根據需要來創建。只不過在事務完成后向線程池交還線程的時候,如果線程池中線程的數量<=coolPoolSize, 則空閑的線程不會被消除,而是常駐在線程池中等待被取用。)
maximumPoolSize: 表示線程池能夠容納同時執行的最大線程數。必須大於或等於1。如果待執行的線程數大於此值,需要借助workQueue參數的幫助,緩存在隊列中。如果maximumPoolSize與corePoolSize相等,即是固定大小線程池。
keepAliveTime: 表示線程池中的線程空閑時間,當空閑時間達到keepAliveTime值時,線程會被銷毀,直到只剩下corePoolSize個線程為止,避免浪費內存和句柄資源。在默認情況下,當線程池的線程數大於corePoolSize時,keepAliveTime 才會起作用。但是當ThreadPoolExecutor的allowCore Thread TimeOut變量設置為true時,核心線程超時后也會被回收。
TimeUnit: 表示時間單位。keepAliveTime的時間單位通常是TimeUnit.SECONDS。
workQueue: 表示緩存隊列。當請求的線程數大於corePoolSize時,線程進入BlockingQee阻塞隊列。后續示例代碼中使用的LinkedBlockingQueue是單向鏈表,使用鎖來控制入隊和出隊的原子性,兩個鎖分別控制元素的添加和獲取,是一個生產消費模型隊列。
threadFactory表示線程工廠。它用來生產一組相同任務的線程。線程池的命名是通過給這個ctoy增加組名前級來實現的。在虛擬機棧分析時,就可以知道線程任務是由哪個線程工廠產生的。
handler:表示執行拒絕策路的對象。當workQueue參數的任務緩有區到達上限后,並且活動線程數大於maximumPoolSize的時候,線程池通過該策略處理請求,這是一種簡單的限流保護。
handler:表示當拒絕處理任務時的策略,有以下四種取值:
ThreadPoolExecutor.AbortPolicy:丟棄任務並拋出RejectedExecutionException異常。 ThreadPoolExecutor.DiscardPolicy:也是丟棄任務,但是不拋出異常。 ThreadPoolExecutor.DiscardOldestPolicy:丟棄隊列最前面的任務,然后重新嘗試執行任務(重復此過程) ThreadPoolExecutor.CallerRunsPolicy:由調用線程處理該任務
C. JDK 為我們內置了五種常見線程池的實現:
-
- 一個單線程的線程池。這個線程池只有一個線程在工作,也就是相當於單線程串行執行所有任務。此線程池保證所有任務的執行順序按照任務的提交順序執行。
- FixedThreadPool 的核心線程數和最大線程數都是指定值,也就是說當線程池中的線程數超過核心線程數后,任務都會被放到阻塞隊列中。
- CachedThreadPool 沒有核心線程,非核心線程數無上限,也就是全部使用外包,但是每個外包空閑的時間只有 60 秒,超過后就會被回收。
- ScheduledThreadPool 此線程池支持定時以及周期性執行任務的需求。
D. 講一下自定義線程池的工作流程
根據ThreadPoolExecutor源碼, 當試圖通過excute方法將一個Runnable任務添加到線程池中時,按照如下順序來處理:
1、如果線程池中的線程數量少於corePoolSize,即使線程池中有空閑線程,也會創建一個新的線程來執行新添加的任務;
2、如果線程池中的線程數量大於等於corePoolSize,但緩沖隊列workQueue未滿,則將新添加的任務放到workQueue中,按照FIFO的原則依次等待執行(線程池中有線程空閑出來后依次將緩沖隊列中的任務交付給空閑的線程執行);
3、如果線程池中的線程數量大於等於corePoolSize,且緩沖隊列workQueue已滿,但線程池中的線程數量小於maximumPoolSize,則會創建新的線程來處理被添加的任務;
4、如果線程池中的線程數量等於了maximumPoolSize,觸發拒絕策略。
總結起來,也即是說,當有新的任務要處理時,先看線程池中的線程數量是否大於corePoolSize,再看緩沖隊列workQueue是否滿,最后看線程池中的線程數量是否大於maximumPoolSize。
另外,當線程池中的線程數量大於corePoolSize時,如果里面有線程的空閑時間超過了keepAliveTime,就將其移除線程池,這樣,可以動態地調整線程池中線程的數量。
E. Executor 、ExecutorService 與 Executors 的區別與聯系
-
- ExecutorService 接口繼承了 Executor 接口,是 Executor 的子接口
- Executor 接口定義了 execute()方法用來接收一個Runnable接口的對象;而 ExecutorService 接口中的 submit()方法可以接受Runnable和Callable接口的對象。
- Executor 中的 execute() 方法不返回任何結果,而 ExecutorService 中的 submit()方法可以通過一個 Future 對象返回運算結果。
- 除了允許客戶端提交一個任務,ExecutorService 還提供用來控制線程池的方法。比如:調用 shutDown() 方法終止線程池。
- Executors 類提供五個靜態工廠方法用來創建不同類型的線程池。
F. 任務排隊策略(WorkQueue)(存疑)
1、直接提交。緩沖隊列采用 SynchronousQueue,它將任務直接交給線程處理而不保持它們。如果不存在可用於立即運行任務的線程(即線程池中的線程都在工作),則試圖把任務加入緩沖隊列將會失敗,因此會構造一個新的線程來處理新添加的任務,並將其加入到線程池中。直接提交通常要求無界 maximumPoolSizes(Integer.MAX_VALUE) 以避免拒絕新提交的任務。newCachedThreadPool采用的便是這種策略。
2、無界隊列。使用無界隊列(典型的便是采用預定義容量的 LinkedBlockingQueue,理論上是該緩沖隊列可以對無限多的任務排隊)將導致在所有 corePoolSize 線程都工作的情況下將新任務加入到緩沖隊列中。這樣,創建的線程就不會超過 corePoolSize,也因此,maximumPoolSize 的值也就無效了。當每個任務完全獨立於其他任務,即任務執行互不影響時,適合於使用無界隊列。newFixedThreadPool采用的便是這種策略。
3、有界隊列。當使用有限的 maximumPoolSizes 時,有界隊列(一般緩沖隊列使用ArrayBlockingQueue,並制定隊列的最大長度)有助於防止資源耗盡,但是可能較難調整和控制,隊列大小和最大池大小需要相互折衷,需要設定合理的參數。
E. Callable 與 Runnable
1. Runnable是一個接口,在它里面只聲明了一個run()方法:
public interface Runnable { public abstract void run(); }
run()方法返回值為void類型,所以在執行完任務之后無法返回任何結果
2. Callable位於java.util.concurrent包下,它也是一個接口,在它里面也只聲明了一個方法,只不過這個方法叫做call()
public interface Callable<V> { V call() throws Exception; }
這是一個泛型接口,該接口聲明了一個名稱為call()的方法,同時這個方法可以有返回值V,也可以拋出異常。call()方法返回的類型就是傳遞進來的V類型
3. 如何使用Callable
一般情況下配合ExecutorService來使用callable,在ExecutorService接口中聲明了若干個submit方法的重載版本:
<T> Future<T> submit(Callable<T> task); <T> Future<T> submit(Runnable task, T result); Future<?> submit(Runnable task);
第一個方法:submit提交一個實現Callable接口的任務,並且返回封裝了異步計算結果的Future。
第二個方法:submit提交一個實現Runnable接口的任務,並且指定了在調用Future的get方法時返回的result對象。
第三個方法:submit提交一個實現Runnable接口的任務,並且返回封裝了異步計算結果的Future。
因此我們只要創建好我們的線程對象(實現Callable接口或者Runnable接口),然后通過上面3個方法提交給線程池去執行即可。
Future就是對於具體的Runnable或者Callable任務的執行結果進行取消、查詢是否完成、獲取結果。必要時可以通過get方法獲取執行結果,該方法會阻塞直到任務返回結果。
F. Future
Future就是對於具體的Runnable或者Callable任務的執行結果進行取消、查詢是否完成、獲取結果。必要時可以通過get方法獲取執行結果,get方法會阻塞直到任務返回結果。
Future接口定義了下面5種方法:
- cancel方法用來取消任務
- isCancelled方法表示任務是否被取消成功
- isDone方法表示任務是否已經完成
- get()方法用來獲取執行結果,這個方法會產生阻塞,會一直等到任務執行完畢才返回
- get(long timeout, TimeUnit unit)用來獲取執行結果,如果在指定時間內,還沒獲取到結果,就直接返回null。
也就是說Future提供了三種功能:
1)判斷任務是否完成;
2)能夠中斷任務;
3)能夠獲取任務執行結果。
因為Future只是一個接口,所以是無法直接用來創建對象使用的,FutureTask是Future接口的一個唯一實現類。
Java並發包(JUC)相關
並發包主要分成以下幾個類族:
- 線程同步類 逐步淘汰了使用Object的wait()和notify()的同步方式,主要代表為CountDownLatch, Semaphore,CyclicBarrier等
- 並發集合類 最著名的是ConcurrentHashMap, 由剛開始的分段鎖到后來的CAS,不斷提升並發性能,還有ConcurrentSkipListMap, CopyOnWriteArrayList, BlockingQueue等
- 線程管理類 線程池,如使用Executors靜態工廠或者使用ThreadPoolExecutor等,另外,使用ScheduledExecutorService來執行定時任務
- 鎖相關類 以Lock接口為核心,派生出一些類,最有名的是ReentrantLock
並發包中的鎖類
Lock是JUC包的頂層接口,它的實現邏輯並未用到synchronized, 而是利用了volatile的可見性和CAS
ReentrantLock對於Lock接口的實現主要依賴了Sync,而Sync繼承了AbstractQueuedSynchronizer(AQS),它是JUC包實現同步的基礎工具。在AQS中,定義了一個volatile int state變量作為共享資源,如果線程獲取資源失敗,則進入同步FIFO隊列中等待;如果成功獲取資源就執行臨界區代碼。執行完釋放資源時,會通知同步隊列中的等待線程來獲取資源后出隊並執行。
AQS是抽象類,內置自旋鎖實現的同步隊列,封裝入隊和出隊的操作,提供獨占、共享、中斷等特性的方法。AQS的子類可以定義不同的資源實現不同性質的方法。
- 比如可重入鎖ReentrantLock,定義state為0時可以獲取資源並置為1。若已獲得資源,state 不斷加1,在釋放資源時state減1,直至為0;
- CountDownLatch初始時定義了資源總量state=count,countDown()不斷將state減1,當state=0時才能獲得鎖,釋放后state就一直為0。所有線程調用await()都不會等待,所以CountDownLatch是一次性的,用完后如果再想用就只能重新創建一個;如果希望循環使用,推薦使用基於RentrantLock 實現的CyclicBarrier。
- Semaphore 與CountDownLatch略有不同,同樣也是定義了資源總量state=permits,當state>0時就能獲得鎖,並將state減1,當state=0時只能等待其他線程釋放鎖,當釋放鎖時state加1,其他等待線程又能獲得這個鎖。
當Semphore的permits定義為1時,就是互斥鎖,當permits>1就是共享鎖。總之,ReentrantLock, CountDownLatch, CyclicBarrier,Semaphore等工具都是基於AQS的不同配置來實現的。
自旋鎖是一種互斥鎖的實現方式而已,相比一般的互斥鎖會在等待期間放棄cpu,自旋鎖(spinlock)則是不斷循環並測試鎖的狀態,這樣就一直占着cpu。與互斥量類似,它不是通過休眠使進程阻塞,而是在獲取鎖之前一直處於忙等(自旋)阻塞狀態。用在以下情況:鎖持有的時間短,而且線程並不希望在重新調度上花太多的成本。"原地打轉"。
互斥鎖:用於保護臨界區,確保同一時間只有一個線程訪問數據。對共享資源的訪問,先對互斥量進行加鎖,如果互斥量已經上鎖,調用線程會阻塞,直到互斥量被解鎖。在完成了對共享資源的訪問后,要對互斥量進行解鎖。
Volatile關鍵字
每個線程都有獨占的內存區域,如操作棧、本地變量表等。線程本地內存保存了引用變量在堆內存中的副本,線程對變量的所有操作都在本地內存區域中進行,執行結束后再同步到堆內存中去。這里必然有一個時間差,在這個時間差內,該線程對副本的操作,對於其他線程都是不可見的。
volatile的英文本義是“揮發、不穩定的”,延伸意義為敏感的。當使用volatile修飾變量時,意味着任何對此變量的操作都會在內存中進行,不會產生副本,以保證共享變量的可見性,局部阻止了指令重排的發生。(從JDK5開始java增強了volatile的內存語義,除了線程讀取volatile變量要從主內存獲取和線程寫volatile變量要及時刷回到主內存以外,JDK5開始還嚴格限制編譯器和處理器對volatile變量和普通變量的重排序,從而確保volatiled的寫-讀和鎖的釋放-獲取具有相同的語義。)
鎖也可以確保變量的可見性,但是實現方式和volatile略有不同。線程在得到鎖時讀入副本,釋放時寫回內存。
volatile解決的是多線程共享變量的可見性問題,類似於synchronized,但不具備synchronized的互斥性。
因為所有的操作同需要同步給內存,因此volatile一定使線程的執行速度變慢
volatile變量與鎖的區別:
由於volatile僅僅保證對對單個volatile變量的讀、寫具有原子性(復合操作不保證原子性如volatie i, i++),而鎖的互斥執行的特性可以確保對整個臨界區代碼的執行具有原子性。在功能上,鎖比volatile更強大,在可伸縮性上和執行性能上,volatile更有優勢。
信號量同步
信號量同步是指在不同的線程之間,通過傳遞同步信號量來協調線程執行的先后次序。
CountDownLatch是基於執行時間的同步類。在實際編碼中,可能需要處理基於空閑信號的同步情況。比如海關安檢的場景,任何國家公民在出國時,都要走海關的查驗通道。假設某機場的海關通道共有3個窗口,一批需要出關的人排成長隊,每個人都是一個線程。當3個窗口中的任意一個出現空閑時,工作人員指示隊列中第一個人出隊到該空閑窗口接受查驗。對於上述場景,JDK中提供了一個Semaphore的信號同步類,只有在調用Semaphore對象的acquire()成功后,才可以往下執行,完成后執行release()釋放持有的信號量,下一個線程就可以馬上獲取這個空閑信號量進入執行。
1、CountDownLatch end = new CountDownLatch(N); //構造對象時候 需要傳入參數N
2、end.await() 能夠阻塞線程 直到調用N次end.countDown() 方法才釋放線程
3、end.countDown() 可以在多個線程中調用 計算調用次數是所有線程調用次數的總和
還有其他同步方式,如CyclicBarrier是基於同步到達某個點的信號量觸發機制。CyclicBarrier從命名上即可知道它是一個可以循環使用(Cyclic)的屏障式(Barrier)多線程協作方式。采用這種方式進行剛才的安檢服務,就是3個人同時進去,只有3個人都完成安檢,才會放下一批進來。這是一種非常低效的安檢方式。但在某種場景下就是非常正確的方式,假設在機場排隊打車時,現場工作人員統一指揮,每次放3輛車進來,坐滿后開走,再放下一批車和人進來。通過CyclicBarrier的reset)來釋放線程資源。
ThreadLocal 變量
ThreadLocal是用來維護線程中的變量不被其他線程干擾而出現的一個結構,內部包含一個ThreadLocalMap類,該類為Thread類的一個局部變量,該Map存儲的key為ThreadLocal對象所在線程對象,value為我們要存儲的對象,這樣一來,在不同線程中,持有的其實都是當前線程的變量副本,與其他線程完全隔離,以此來保證線程執行過程中不受其他線程的影響。利用ThreadLocal可以實現讓所有的線程持有初始值相同的一個變量副本,但在初始化以后每個線程都只能操作自己的那個變量副本,不同線程之間的變量副本互不干擾。
ThreadLocal可能會造成內存泄漏的問題:
ThreadLocal中的鍵值對中的鍵是一個弱引用,那么在內存回收的時候,這個鍵很可能會被回收掉,然后鍵沒了,就無法找到value的值,造成了內存泄漏;
我們看下set方法的實現:
public void set(T value) { Thread t = Thread.currentThread(); ThreadLocalMap map = getMap(t); if (map != null) map.set(this, value); else createMap(t, value); }可以看到,先通過Thread.currentThread()方法獲取到了當前線程,然后如果取不到map對象,就會創建,下面看下create方法
void createMap(Thread t, T firstValue) { t.threadLocals = new ThreadLocalMap(this, firstValue); }很簡單的方法,就是新建一個ThreadLocal的Map,這個map以ThreadLocal自身為key,以我們要設值的對象為value,創建出來map之后,將對象賦值到線程的局部變量去。
看到這里,就知道ThreadLocal主要目的就是將變量設值到當前的線程上,以此來保證線程安全。
那么下面看下get方法:public T get() { Thread t = Thread.currentThread(); ThreadLocalMap map = getMap(t); if (map != null) { ThreadLocalMap.Entry e = map.getEntry(this); if (e != null) { @SuppressWarnings("unchecked") T result = (T)e.value; return result; } } return setInitialValue(); }可以看出來,get方法就是拿的當前線程的局部變量threadLocals,然后從中取出map中存儲的對象,這樣每個線程中獲取的一定是自己線程中存儲的對象了。
由上述分析可知,使用ThreadLocal是可以保證線程安全的。
ThreadLocal實際上是為解決多線程程序的並發問題提供了一種新的思路(區別與sychronized關鍵字)。
ThreadLocal這個類提供線程本地的變量。這些變量與一般正常的變量不同,它們在每個線程中都是獨立的。ThreadLocal實例最典型的運用就是在類的私有靜態變量中定義,並與線程關聯。並且ThreadLocal不會產生內存泄漏的問題
原文:https://blog.csdn.net/caoyishuai100/article/details/68946037
Java引用類型和GC時機
對象在堆上創建之后所持有的用基實是一種變量類型,引用之間可以通過賦值構成一條引用鏈。從GC Roots開始遍歷,判斷引用是否可達。引用的可達性是判斷能否被垃圾回收的基本條件。JMM會根據此自動管理內存的分配與回收,不需要開發工程師干預。但在某些場景下,即使引用可達,也希望能夠根據語義的強弱進行有選擇的回收,以保證系統的正常運行。根據引用類型語義的強弱來決定垃圾回收的階段,我們可以把引用分為強引用、軟引用、弱引用和虛引用四類。后三類引用,本質上是可以讓開發工程師通過代碼方式來決定對象的垃圾回收時機。
- 強引用,即Strong Reference,最為常見。如Object object=new Objecto);這樣的變量聲明和定義就會產生對該對象的強引用。只要對象有強引用指向,並且GC Roots可達,那么Java內存回收時,即使瀕臨內存耗盡,也不會回收該對象。
- 軟引用,即Soft Reference,引用力度弱於“強引用”,是用在非必需對象的場景。在即將OMM之前,垃圾回收器會把這些軟引用指向的對象加入回收范圍,以獲得更多的內存空間,讓程序能夠繼續健康運行。主要用來緩存服務器中間計算結果及不需要實時保存的用戶行為等。
- 弱引用,即Weak Reference,引用強度較前兩者更弱,也是用來描述非必需對象的。如果弱引用指向的對象只存在弱引用這一條線路,則在下一次YGC時會被回收。由於YGC時間的不確定性,弱引用何時被回收也具有不確定性。弱引用主要用於指向某個易消失的對象,在強引用斷開后,此引用不會劫持對象。
-
虛引用,即Phantom Reference,是極弱的一種引用關系,定義完成后,就無法通過該引用獲取指向的對象。為一個對象設置虛引用的唯一目的就是希望能在這個對象被回收時收到一個系統通知。虛引用必須與引用隊列聯合使用,當垃圾回收時,如果發現存在虛引用,就會在回收對象內存前,把這個虛引用加入與之關聯的引用隊列中。
Static變量是線程安全的嗎?
JAVA對象鎖和方法鎖的區別
首先的明白Java中鎖的機制 synchronized
在修飾代碼塊的時候需要一個reference對象作為鎖的對象.
在修飾方法的時候默認是當前對象作為鎖的對象.
在修飾類時候默認是當前類的Class對象作為鎖的對象.
方法鎖(synchronized修飾方法時)
通過在方法聲明中加入 synchronized關鍵字來聲明 synchronized 方法。
synchronized 方法控制對類成員變量的訪問:
每個類實例對應一把鎖,每個 synchronized 方法都必須獲得調用該方法的類實例的鎖方能執行,否則所屬線程阻塞,方法一旦執行,就獨占該鎖,直到從該方法返回時才將鎖釋放,此后被阻塞的線程方能獲得該鎖,重新進入可執行狀態。這種機制確保了同一時刻對於每一個類實例,其所有聲明為 synchronized 的成員函數中至多只有一個處於可執行狀態,從而有效避免了類成員變量的訪問沖突。
對象鎖(synchronized修飾方法或代碼塊)
當一個對象中有synchronized method或synchronized block的時候調用此對象的同步方法或進入其同步區域時,就必須先獲得對象鎖。如果此對象的對象鎖已被其他調用者占用,則需要等待此鎖被釋放。(方法鎖也是對象鎖)
java的所有對象都含有1個互斥鎖,這個鎖由JVM自動獲取和釋放。線程進入synchronized方法的時候獲取該對象的鎖,當然如果已經有線程獲取了這個對象的鎖,那么當前線程會等待;synchronized方法正常返回或者拋異常而終止,JVM會自動釋放對象鎖。這里也體現了用synchronized來加鎖的1個好處,方法拋異常的時候,鎖仍然可以由JVM來自動釋放。
類鎖(synchronized 修飾靜態的方法或代碼塊)
由於一個class不論被實例化多少次,其中的靜態方法和靜態變量在內存中都只有一份。所以,一旦一個靜態的方法被申明為synchronized。此類所有的實例化對象在調用此方法,共用同一把鎖,我們稱之為類鎖。
對象鎖是用來控制實例方法之間的同步,類鎖是用來控制靜態方法(或靜態變量互斥體)之間的同步。
總結:
1.類鎖是對靜態方法使用synchronized關鍵字后,無論是多線程訪問單個對象還是多個對象的sychronized塊,都是同步的。
2.對象鎖是實例方法使用synchronized關鍵字后,如果是多個線程訪問同個對象的sychronized塊,才是同步的,但是訪問不同對象的話就是不同步的。
3.類鎖和對象鎖是兩種不同的鎖,可以同時使用,但是注意類鎖不要嵌套使用,這樣子容易發生死鎖。
類鎖和對象鎖區別
- 類鎖所有對象一把鎖
- 對象鎖一個對象一把鎖,多個對象多把鎖
同步是對同一把鎖而言的,同步這個概念是在多個線程爭奪同一把鎖的時候才能實現的,如果多個線程爭奪不同的鎖,那多個線程是不能同步的
- 兩個線程一個取對象鎖,一個取類鎖,則不能同步
- 兩個線程一個取a對象鎖,一個取b對象鎖,則不能同步
Sychronized方法加在方法上和加在代碼塊上有什么區別?
synchronize修飾方法的鎖對象只能是this當前對象
synchronize修飾代碼塊可以修改鎖對象(可以是this對象,也可以自行指定)
使用this對象鎖的好處:
這個鎖由JVM自動獲取和釋放。線程進入synchronized方法的時候獲取該對象的鎖,當然如果已經有線程獲取了這個對象的鎖,那么當前線程會等待;synchronized方法正常返回或者拋異常而終止,JVM會自動釋放對象鎖。這里也體現了用synchronized來加鎖的1個好處,方法拋異常的時候,鎖仍然可以由JVM來自動釋放。
壞處:
鎖的粒度大,代碼效率降低;
synchronized的缺陷:當某個線程進入同步方法獲得對象鎖,那么其他線程訪問這個對象的其余的同步方法時,也必須等待或者阻塞,這對高並發的系統是致命的,這很容易導致系統的崩潰。如果某個線程在同步方法里面發生了死循環,那么它就永遠不會釋放這個對象鎖,那么其他線程就要永遠的等待。這是一個致命的問題。
當然同步方法和同步代碼塊都會有這樣的缺陷,只要用了synchronized關鍵字就會有這樣的風險和缺陷。既然避免不了這種缺陷,那么就應該將風險降到最低。這也是同步代碼塊在某種情況下要優於同步方法的方面。例如在某個類的方法里面:這個類里面聲明了一個對象實例,SynObject so=new SynObject();在某個方法里面調用了這個實例的方法so.testsy();但是調用這個方法需要進行同步,不能同時有多個線程同時執行調用這個方法。
這時如果直接用synchronized修飾調用了so.testsy();代碼的方法,那么當某個線程進入了這個方法之后,這個對象其他同步方法都不能給其他線程訪問了。假如這個方法需要執行的時間很長,那么其他線程會一直阻塞,影響到系統的性能。
如果這時用synchronized來修飾代碼塊:synchronized(so){so.testsy();},那么這個方法加鎖的對象是so這個對象,跟執行這行代碼的對象沒有關系,當一個線程執行這個方法時,這對其他同步方法時沒有影響的,因為他們持有的鎖都完全不一樣。
不過這里還有一種特例,就是上面演示的第一個例子,對象鎖synchronized同時修飾方法和代碼塊,這時也可以體現到同步代碼塊的優越性,如果test1方法同步代碼塊后面有非常多沒有同步的代碼,而且有一個100000的循環,這導致test1方法會執行時間非常長,那么如果直接用synchronized修飾方法,那么在方法沒執行完之前,其他線程是不可以訪問test2方法的,但是如果用了同步代碼塊,那么當退出代碼塊時就已經釋放了對象鎖,當線程還在執行test1的那個100000的循環時,其他線程就已經可以訪問test2方法了。這就讓阻塞的機會或者線程更少。讓系統的性能更優越。
Synchronized 鎖的升級(膨脹)
Java1.6 中為了減少獲得鎖和釋放鎖帶來的性能消耗而引入的偏向鎖和輕量級鎖。鎖是按順序膨脹的,且鎖的膨脹是不可逆的。
偏向鎖:大多數情況下,鎖不僅不存在多線程競爭,而且總是由同一線程多次獲得,為了讓線程獲得鎖的代價更低而引入了偏向鎖。當一個線程A訪問加了同步鎖的代碼塊時,會在對象頭中存儲當前線程的id,后續這個線程進入和退出這段加了同步鎖的代碼塊時,不需要再次加鎖和釋放鎖。
輕量級鎖:在偏向鎖情況下,如果線程B也訪問了同步代碼塊,比較對象頭的線程id不一樣,會升級為輕量級鎖,並且通過自旋的方式來獲取輕量級鎖。
重量級鎖:如果線程A和線程B同時訪問同步代碼塊,則輕量級鎖會升級為重量級鎖,線程A獲取到重量級鎖的情況下,線程B只能入隊等待,進入BLOCK狀態。
多線程並發問題的技術選擇
- 當只有一個線程寫,其它線程都是讀的時候,可以用 volatile 修飾變量
- 當多個線程寫,那么一般情況下並發不嚴重的話可以用 Synchronized ,Synchronized並不是一開始就是重量級鎖,在並發不嚴重的時候,比如只有一個線程訪問的時候,是偏向鎖;當多個線程訪問,但不是同時訪問,這時候鎖升級為輕量級鎖;當多個線程同時訪問,這時候升級為重量級鎖。所以在並發不是很嚴重的情況下,使用Synchronized是可以的。不過Synchronized有局限性,比如不能設置鎖超時,不能通過代碼釋放鎖。
- ReentranLock 可以通過代碼釋放鎖,可以設置鎖超時。
- 高並發下,Synchronized、ReentranLock 效率低,因為同一時刻只有一個線程能進入同步代碼塊,如果同時有很多線程訪問,那么其它線程就都在等待鎖。這個時候可以使用並發包下的數據結構,例如 ConcurrentHashMap , LinkBlockingQueue ,以及原子性的數據結構如: AtomicInteger 。
volatile和synchronized特點
樂觀鎖和悲觀鎖
悲觀鎖
總是假設最壞的情況,每次去拿數據的時候都認為別人會修改,所以每次在拿數據的時候都會上鎖,這樣別人想拿這個數據就會阻塞直到它拿到鎖(共享資源每次只給一個線程使用,其它線程阻塞,用完后再把資源轉讓給其它線程)。傳統的關系型數據庫里邊就用到了很多這種鎖機制,比如行鎖,表鎖等,讀鎖,寫鎖等,都是在做操作之前先上鎖。Java中synchronized和ReentrantLock等獨占鎖就是悲觀鎖思想的實現。
樂觀鎖
總是假設最好的情況,每次去拿數據的時候都認為別人不會修改,所以不會上鎖,但是在更新的時候會判斷一下在此期間別人有沒有去更新這個數據,可以使用版本號機制和CAS算法實現。樂觀鎖適用於多讀的應用類型,這樣可以提高吞吐量,像數據庫提供的類似於write_condition機制,其實都是提供的樂觀鎖。在Java中java.util.concurrent.atomic包下面的原子變量類就是使用了樂觀鎖的一種實現方式CAS實現的。
CAS:
Compare and Swap(CAS)
CAS是項樂觀鎖技術,當多個線程嘗試使用CAS同時更新同一個變量時,只有其中一個線程能更新變量的值,而其它線程都失敗,失敗的線程並不會被掛起,而是被告知這次競爭中失敗,並可以再次嘗試。CAS是一種非阻塞式的同步方式。
CAS 操作包含三個操作數 —— 內存位置(V)、預期原值(A)和新值(B)。如果內存位置的值與預期原值相匹配,那么處理器會自動將該位置值更新為新值。否則,處理器不做任何操作。無論哪種情況,它都會在 CAS 指令之前返回該位置的值。(在 CAS 的一些特殊情況下將僅返回 CAS 是否成功,而不提取當前值。)CAS 有效地說明了“我認為位置 V 應該包含值 A;如果包含該值,則將 B 放到這個位置;否則,不要更改該位置,只告訴我這個位置現在的值即可。”這其實和樂觀鎖的沖突檢查+數據更新的原理是一樣的。
兩種鎖的使用場景
從上面對兩種鎖的介紹,我們知道兩種鎖各有優缺點,不可認為一種好於另一種,像樂觀鎖適用於寫比較少的情況下(多讀場景),即沖突真的很少發生的時候,這樣可以省去了鎖的開銷,加大了系統的整個吞吐量。但如果是多寫的情況,一般會經常產生沖突,這就會導致上層應用會不斷的進行retry,這樣反倒是降低了性能,所以一般多寫的場景下用悲觀鎖就比較合適。
Java常見鎖
- 公平鎖/非公平鎖
- 可重入鎖
- 獨享鎖/共享鎖
- 互斥鎖/讀寫鎖
- 樂觀鎖/悲觀鎖
- 分段鎖
- 偏向鎖/輕量級鎖/重量級鎖
- 自旋鎖
重入鎖與不可重入鎖的區別
重入鎖是可重復獲得資源的鎖,已經獲得鎖的線程可以對當前的資源重入加鎖而不會引起阻塞;不可重入鎖是不可重復獲得資源的鎖,當已經獲得鎖的線程對當前資源再次加鎖時,會把自己阻塞。這樣做的好處是可以防止死鎖。
可重入性:
從名字上理解,ReenTrantLock的字面意思就是再進入的鎖,其實synchronized關鍵字所使用的鎖也是可重入的,兩者關於這個的區別不大。同一個線程每進入一次,鎖的計數器都自增1,所以要等到鎖的計數器下降為0時才能釋放鎖。
可重入鎖的設計思路
輕松學習java可重入鎖(ReentrantLock)的實現原理
第二模塊--計算機網路
TCP/IP結構有幾層
TCP/IP參考模型
分為四個層次:應用層、傳輸層、網絡互連層和主機到網絡層。常見的協議如下:
基於UDP的常見的還有DNS、、RIP(路由選擇協議)、DHCP 動態主機設置協議
在TCP/IP參考模型中,去掉了OSI參考模型中的會話層和表示層(這兩層的功能被合並到應用層實現)。同時將OSI參考模型中的數據鏈路層和物理層合並為主機到網絡層。
TCP/IP七層參考模型
物理層(Physical Layer)
數據鏈路層(Datalink Layer)
網絡層(Network Layer)
傳輸層(Transport Layer)
會話層(Session Layer)
表示層(Presentation Layer)
應用層(Application Layer)
常用網絡硬件設備分別工作在哪一層
1.網絡層:路由器、防火牆
2.數據鏈路層:網卡、網橋、交換機
3.物理層:中繼器、集線器
路由器(Router),是連接因特網中各局域網、廣域網的設備,它會根據信道的情況自動選擇和設定路由,以最佳路徑,按前后順序發送信號。路由和交換機之間的主要區別就是交換機發生在OSI參考模型第二層(數據鏈路層),而路由發生在第三層,即網絡層。這一區別決定了路由和交換機在移動信息的過程中需使用不同的控制信息,所以說兩者實現各自功能的方式是不同的。
解釋TCP/IP 的三次握手
TCP會話通過三次握手來初始化。三次握手的目標是使數據段的發送和接收同步。同時也向其他主機表明其一次可接收的數據量(窗口大小),並建立邏輯連接。這三次握手的過程可以簡述如下:
●源主機發送一個同步標志位(SYN)置1的TCP數據段, 此段中同時包含一個初始值隨機的客戶端序列號,ClientISN;
●目標主機發回確認數據段,此段中的同步標志位(SYN)同樣被置1,且確認標志位(ACK)也置1,同時將ClientISN + 1,此外,此段中還包含一個初始值隨機的服務端序列號ServerISN;
●源主機收到目標主機確認數據段后,再回送一個數據段,包含ACK = 1, serverISN+1;
Source ---> Destination: SYN = 1, Seq = ClientISN
Destination ---> Source: SYN = 1, ACK =1, ClientISN + 1, Seq = ServerISN
Source ---> Destination: ACK = 1, ServerISN+1
至此為止,TCP會話的三次握手完成。接下來,源主機和目標主機可以互相收發數據。整個過程可用下圖表示。
解釋TCP/IP四次揮手的過程
起初A和B處於ESTABLISHED狀態
A發出連接釋放報文段(Fin = 1, Seq = u;)並處於FIN-WAIT-1狀態,A不再有數據發送,但仍然可以接受數據;
B發出確認報文段(ACK = 1, Seq = v, ack = u+1)且進入CLOSE-WAIT狀態, 並繼續發送剩下的數據, A收到確認后,進入FIN-WAIT-2狀態,等待B的連接釋放報文段;
B沒有要向A發出的數據,B發出連接釋放報文段(Fin = 1, ACK = 1, Seq = w, ack = u+1)且進入LAST-ACK狀態;
A發出確認報文段(ACK = 1, Seq = w, ack = u+1)且進入TIME-WAIT狀態, B收到確認報文段后進入CLOSED狀態;
A經過等待計時器時間2MSL后,進入CLOSED狀態。
A(FIN-WAIT-1) ---> B (ESTABLISHED): Fin = 1, Seq = u;
B(CLOSE-WAIT) ---> A: ACK = 1, Seq = v, ack = u+1 (繼續發送剩余數據);
---> A (FIN-WAIT-2): A 收到上一步數據,A等待B的Fin;
B(LAST-ACK) ---> A : Fin = 1, ACK = 1, Seq = w, ack = u+1;
A(TIME-WAIT) ---> B: 收到上一步數據, 發送ACK = 1, Seq = w, ack = u+1;
---> B(CLOSED): 收到上一步數據,ClOSED.
A(ClOSED): 等待2MSL, 進入CLOSED.
TCP的連接的拆除需要發送四個包,因此稱為四次揮手(four-way handshake)。客戶端或服務器均可主動發起揮手動作,在socket編程中,任何一方執行close()操作即可產生揮手操作。
為什么TCP/IP需要握手三次,揮手四次
- 為什么需要三次握手呢?
兩個原因:信息對等和防止超時
信息對等: 假設A對B 發起連接請求, 在第二次握手時,A機器能夠確認自己的發報和收報能力正常並且B的發報收報能力正常,而B機器只能確定自己的收報能力和對方的發報能力正常,不能確定對方的收報能力和自己的發報能力是否正常,這些只能通過第三次握手確認;
防止超時 為了防止已失效的連接請求報文段突然又傳送到了服務端,浪費服務端資源。
比如:client發出的第一個連接請求報文段並沒有丟失,而是在某個網絡結點長時間的滯留了,以致延誤到連接釋放以后的某個時間才到達server。本來這是一個早已失效的報文段,但是server收到此失效的連接請求報文段后,就誤認為是client再次發出的一個新的連接請求,於是就向client發出確認報文段,同意建立連接。假設不采用“三次握手”,那么只要server發出確認,新的連接就建立了,由於client並沒有發出建立連接的請求,因此不會理睬server的確認,也不會向server發送數據,但server卻以為新的運輸連接已經建立,並一直等待client發來數據。所以沒有采用“三次握手”,這種情況下server的很多資源就白白浪費掉了。
- 為什么需要四次揮手呢?因為TCP是全雙工模式,一方發送FIN報文時只是單方面沒有信息發送了,但是另一方可以繼續發送報文,只有雙方都發送了FIN報文整個通信過程才能結束。
TCP是全雙工模式,當client發出FIN報文段時,只是表示client已經沒有數據要發送了,client告訴server,它的數據已經全部發送完畢了;但是,這個時候client還是可以接受來server的數據;當server返回ACK報文段時,表示它已經知道client沒有數據發送了,但是server還是可以發送數據到client的;當server也發送了FIN報文段時,這個時候就表示server也沒有數據要發送了,就會告訴client,我也沒有數據要發送了,如果收到client確認報文段,之后彼此就會愉快的中斷這次TCP連接。
為什么A在TIME-WAIT狀態必須等待2MSL的時間?
MSL最長報文段壽命Maximum Segment Lifetime,MSL=2
兩個理由:
1)保證A發送的最后一個ACK報文段能夠到達B。
2)防止“已失效的連接請求報文段”出現在本連接中。
1)這個ACK報文段有可能丟失,使得處於LAST-ACK狀態的B收不到對已發送的FIN+ACK報文段的確認,B超時重傳FIN+ACK報文段,而A能在2MSL時間內收到這個重傳的FIN+ACK報文段,接着A重傳一次確認,重新啟動2MSL計時器,最后A和B都進入到CLOSED狀態,若A在TIME-WAIT狀態不等待一段時間,而是發送完ACK報文段后立即釋放連接,則無法收到B重傳的FIN+ACK報文段,所以不會再發送一次確認報文段,則B無法正常進入到CLOSED狀態。
2)A在發送完最后一個ACK報文段后,再經過2MSL,就可以使本連接持續的時間內所產生的所有報文段都從網絡中消失,使下一個新的連接中不會出現這種舊的連接請求報文段。
通過調整2MSL時間(可以手動更改),可以有效降低TIME_WAIT狀態的連接數目,達到系統調優的目的。建議將高並發服務器的TIME_WAIT超時調小。
TCP協議和UDP協議的區別是什么
- TCP協議是有連接的,有連接的意思是開始傳輸實際數據之前TCP的客戶端和服務器端必須通過三次握手建立連接,會話結束之后也要結束連接,而UDP是無連接的。
- TCP協議保證數據按序發送,按序到達,提供超時重傳來保證可靠性,但是UDP不保證按序到達,甚至不保證到達,只是努力交付,即便是按序發送的序列,也不保證按序送到。
- TCP有流量控制和擁塞控制,UDP沒有,網絡擁堵不會影響發送端的發送速率
- TCP是一對一的連接,而UDP則可以支持一對一,多對多,一對多的通信。
- TCP面向的是字節流的服務,UDP面向的是報文的服務。
- TCP協議所需資源多,TCP首部需20個字節(不算可選項),UDP首部字段只需8個字節
TCP的特點是 連序控點流: 有連接、有序、流量控制、點對點、字節流
面向字節流 和面向報文:
用UDP傳輸100個字節的數據:
面向數據報
如果發送端調用一次sendto, 發送100個字節, 那么接收端也必須調用對應的一次recvfrom, 接收100個字節;
而不能循環調用10次recvfrom, 每次接收10個字節;面向字節流
由於緩沖區的存在, TCP程序的讀和寫不需要一一匹配, 例如:
寫100個字節數據時, 可以調用一次write寫100個字節, 也可以調用100次write, 每次寫⼀一個字節;
讀100個字節數據時, 也完全不需要考慮寫的時候是怎么寫的, 既可以一次read 100個字節, 也可以一次read一個字節, 重復100次;應用層交給UDP多長的報文, UDP原樣發送, 既不會拆分, 也不會合並;
TCP有一個緩沖,當應用程序傳送的數據塊太長,TCP就可以把它划分短一些再傳送。如果應用程序一次只發送一個字節,TCP也可以等待積累有足夠多的字節后再構成報文段發送出去。
常見的應用中有哪些是應用TCP協議的,哪些又是應用UDP協議的,為什么它們被如此設計?
以下應用一般或必須用udp實現
- 多播的信息一定要用udp實現,因為tcp只支持一對一通信。
- 如果一個應用場景中大多是簡短的信息,適合用udp實現,因為udp是基於報文段的,它直接對上層應用的數據封裝成報文段,然后丟在網絡中,如果信息量太大,會在鏈路層中被分片,影響傳輸效率。
- 如果一個應用場景重性能甚於重完整性和安全性,那么適合於udp,比如多媒體應用,缺一兩幀不影響用戶體驗,但是需要流媒體到達的速度快,因此比較適合用udp
- 如果要求快速響應,那么udp比較合適
- 如果又要利用udp的快速響應優點,又想可靠傳輸,那么只能考上層應用自己制定規則了。
- 常見的使用udp的例子:ICQ,QQ的聊天模塊。
UDP能不能實現可靠連接,怎么實現
UDP它不屬於連接型協議,因而具有資源消耗小,處理速度快的優點,所以通常音頻、視頻和普通數據在傳送時使用UDP較多,因為它們即使偶爾丟失一兩個數據包,也不會對接收結果產生太大影響。傳輸層無法保證數據的可靠傳輸,只能通過應用層來實現了。實現的方式可以參照tcp可靠性傳輸的方式,只是實現不在傳輸層,實現轉移到了應用層。
實現確認機制、重傳機制、窗口確認機制。
必須實現如下功能:
發送:包的分片、包確認、包的重發
接收:包的調序、包的序號確認
目前有如下開源程序利用udp實現了可靠的數據傳輸。分別為RUDP、RTP、UDT。
基於UDP的數據傳輸協議(UDP-basedData Transfer Protocol,簡稱UDT)是一種互聯網數據傳輸協議
解釋Server端受到SYN攻擊
服務器端的資源分配是在二次握手時分配的,而客戶端的資源是在完成三次握手時分配的,所以服務器容易受到SYN洪泛攻擊,SYN攻擊就是Client在短時間內偽造大量不存在的IP地址,並向Server不斷地發送SYN包,Server則回復確認包,並等待Client確認,由於源地址不存在,因此Server需要不斷重發直至超時,這些偽造的SYN包將長時間占用未連接隊列,導致正常的SYN請求因為隊列滿而被丟棄,從而引起網絡擁塞甚至系統癱瘓。
防范SYN攻擊措施:降低主機的等待時間使主機盡快的釋放半連接的占用,短時間受到某IP的重復SYN則丟棄后續請求
HTTP的八種 請求方法
根據HTTP標准,HTTP請求可以使用多種請求方法。
HTTP1.0定義了三種請求方法: GET, POST 和 HEAD方法。
HTTP1.1新增了五種請求方法:OPTIONS, PUT, DELETE, TRACE 和 CONNECT 方法。
序號 | 方法 | 描述 |
---|---|---|
1 | GET | 請求指定的頁面信息,並返回實體主體。 |
2 | HEAD | 類似於get請求,只不過返回的響應中沒有具體的內容,用於獲取報頭 |
3 | POST | 向指定資源提交數據進行處理請求(例如提交表單或者上傳文件)。數據被包含在請求體中。POST請求可能會導致新的資源的建立和/或已有資源的修改。 |
4 | PUT | 從客戶端向服務器傳送的數據取代指定的文檔的內容。 |
5 | DELETE | 請求服務器刪除指定的頁面。 |
6 | CONNECT | HTTP/1.1協議中預留給能夠將連接改為管道方式的代理服務器。 |
7 | OPTIONS | 允許客戶端查看服務器的性能。 |
8 | TRACE | 回顯服務器收到的請求,主要用於測試或診斷。 |
GET和POST兩種基本請求方法的區別
“標准答案”:
瀏覽器行為:
- GET在瀏覽器回退時是無害的,而POST會再次提交請求。
- GET產生的URL地址可以被Bookmark,而POST不可以。
- GET請求會被瀏覽器主動cache,而POST不會,除非手動設置。
- GET請求只能進行url編碼,而POST支持多種編碼方式。
- GET請求參數會被完整保留在瀏覽器歷史記錄里,而POST中的參數不會被保留。
- GET參數通過URL傳遞,且參數是有長度限制的,POST放在Request body中。
- 對參數的數據類型,GET只接受ASCII字符,而POST沒有限制。
- GET比POST更不安全,因為參數直接暴露在URL上,所以不能用來傳遞敏感信息。GET提交的數據會放在URL之后,以?分割URL和傳輸數據,參數之間以&相連,如EditPosts.aspx?name=test1&id=123456. POST方法是把提交的數據放在HTTP包的Body中.
- GET產生一個TCP數據包;POST產生兩個TCP數據包。
對於GET方式的請求,瀏覽器會把http header和data一並發送出去,服務器響應200(返回數據);
而對於POST,瀏覽器先發送header,服務器響應100 continue,瀏覽器再發送data,服務器響應200 ok(返回數據)
GET和POST本質上沒有區別!
GET和POST是HTTP協議中的兩種發送請求的方法。
HTTP是是基於TCP/IP的關於數據如何在萬維網中如何通信的協議。
HTTP的底層是TCP/IP。所以GET和POST的底層也是TCP/IP,也就是說,GET/POST都是TCP鏈接。GET和POST能做的事情是一樣一樣的。你要給GET加上request body,給POST帶上url參數,技術上是完全行的通的。
但不同的瀏覽器(發起http請求)和服務器(接受http請求)在技術層面上不允許 url中無限加參數。他們會限制單次運輸量來控制風險,數據量太大對瀏覽器和服務器都是很大負擔。業界不成文的規定是,(大多數)瀏覽器通常都會限制url長度在2K個字節,而(大多數)服務器最多處理64K大小的url。超過的部分,恕不處理。如果你用GET服務,在request body偷偷藏了數據,不同服務器的處理方式也是不同的,有些服務器會幫你卸貨,讀出數據,有些服務器直接忽略,所以,雖然GET可以帶request body,也不能保證一定能被接收到。
GET和POST還有一個重大區別,簡單的說:
GET產生一個TCP數據包;POST產生兩個TCP數據包。
對於GET方式的請求,瀏覽器會把http header和data一並發送出去,服務器響應200(返回數據);
而對於POST,瀏覽器先發送header,服務器響應100 continue,瀏覽器再發送data,服務器響應200 ok(返回數據)。
但並不是所有瀏覽器都會在POST中發送兩次包,Firefox就只發送一次。
HTTP和HTTPS
http協議與https協議的區別?
- http 是超文本傳輸協議,信息是明文傳輸,https 則是具有安全性的 ssl 加密傳輸協議。
- http 的連接很簡單,是無狀態的;HTTPS 協議是由 SSL+HTTP 協議構建的可進行加密傳輸、身份認證的網絡協議,HTTPS為了數據傳輸的安全,在HTTP的基礎上加入了SSL協議,SSL依靠證書來驗證服務器的身份,並為瀏覽器和服務器之間的通信加密,比 http 協議安全。
- http 和 https 使用的是完全不同的連接方式,用的端口也不一樣,前者是 80,后者是 443。
- https 協議需要到 ca 申請證書,一般免費證書較少,因而需要一定費用。。
HTTPS的優點(確保數據發送到正確的客戶機和服務器,確保數據的完整性,增加了中間人攻擊的成本)
盡管HTTPS並非絕對安全,掌握根證書的機構、掌握加密算法的組織同樣可以進行中間人形式的攻擊,但HTTPS仍是現行架構下最安全的解決方案,主要有以下幾個好處:
(1)使用HTTPS協議可認證用戶和服務器,確保數據發送到正確的客戶機和服務器;
(2)HTTPS協議是由SSL+HTTP協議構建的可進行加密傳輸、身份認證的網絡協議,要比http協議安全,可防止數據在傳輸過程中不被竊取、改變,確保數據的完整性。
(3)HTTPS是現行架構下最安全的解決方案,雖然不是絕對安全,但它大幅增加了中間人攻擊的成本。
HTTPS的缺點(效率低)
雖然說HTTPS有很大的優勢,但其相對來說,還是存在不足之處的:
(1)HTTPS協議握手階段比較費時,會使頁面的加載時間延長近50%,增加10%到20%的耗電;
(2)HTTPS連接緩存不如HTTP高效,會增加數據開銷和功耗,甚至已有的安全措施也會因此而受到影響;
Cookies和Session的區別
cookie和session的區別:
①存在的位置:
cookie 存在於客戶端,臨時文件夾中; session存在於服務器的內存中,一個session域對象為一個用戶瀏覽器服務
②安全性
cookie是以明文的方式存放在客戶端的,安全性低,可以通過一個加密算法進行加密后存放; session存放於服務器的內存中,所以安全性好
③網絡傳輸量
cookie會傳遞消息給服務器; session本身存放於服務器,不會有傳送流量
④生命周期(以20分鍾為例)
cookie的生命周期是累計的,從創建時,就開始計時,20分鍾后,cookie生命周期結束;
session的生命周期是間隔的,從創建時,開始計時如在20分鍾,沒有訪問session,那么session生命周期被銷毀。但是,如果在20分鍾內(如在第19分鍾時)訪問過session,那么,將重新計算session的生命周期。關機會造成session生命周期的結束,但是對cookie沒有影響
⑤訪問范圍
cookie為多個用戶瀏覽器共享; session為一個用戶瀏覽器獨享
可以看自己的筆記: https://www.cnblogs.com/greatLong/articles/11146045.html
Session:
服務器為 每個會話創建一個session對象,所以session中的數據可供當前會話中所有servlet共享。作用域: 會話從用戶打開瀏覽器開始,直到關閉瀏覽器才結束,一次會話期間只會創建一個session對象。session是服務器端對象,保存在服務器端,並且服務器 可以將創建session后產生的 sessionid 通過一個 cookie 返回給客戶端,以便下次驗證。(session底層的一種實現依賴於cookie,session一般配合cookie來保存會話信息,但在不允許使用cookie的情況下,也可以使用url重寫的形式傳遞信息)
Cookie
cookie是http協議提供的,不是java獨有的;
cookie保存在客戶端;
可以設置生命時長;
cookie有個屬性是路徑,但該路徑指的是服務端路徑;
cookie的形式是一個鍵值對,如uid:lxj;
http約定,單條cookie不超過4KB,單個會話最多保存20條cookie; 瀏覽器最多儲存300條cookie;但一般瀏覽器對這個規定會有超出,如允許單個會話超過20個cookie;
與之對應的,session是Java獨有的,全稱是httpsession, 每個session 都有JSEESIONID;
session一般配合cookie來保存會話信息,但在不允許cookie的情況下,也可以使用url重寫的形式傳遞信息
此部分知識不完全,需要進一步理解
中間人攻擊和重放攻擊
- 重放攻擊是攻擊者獲取客戶端發送給服務器端的包,不做修改,原封不動的發送給服務器用來實現某些功能。比如說客戶端發送給服務器端一個包的功能是查詢某個信息,攻擊者攔截到這個包,然后想要查詢這個信息的時候,把這個包發送給服務器,服務器就會做相應的操作,返回查詢的信息。
防御方案: 加時間戳(需要時鍾同步),使用與當前事件有關的一次性隨機數N
- 中間人攻擊就不一樣了,中間人攻擊是攻擊者把自己當作客戶端與服務器端的中間人,客戶端發送的信息會被攻擊者截取然后做一些操作再發送給服務器端,服務器端響應返回的包也會被攻擊者截取然后再發送給客戶端。你可以看作是相對於客戶端來說攻擊者是服務器端,(攻擊者假冒客戶端需要證書,但是攻擊者自己制作的證書不是信任的證書,會彈警告,但往往人們對於這些警告大意或不懂就忽略了);相對於服務器段來說攻擊者是客戶端,兩邊都欺騙,從而獲取所有的信息。常見的有DNS劫持。
防御方案: 采用認證方式連接
轉發與重定向的區別
forward(轉發): 是服務器內部重定向,程序收到請求后重新定向到另一個程序,客戶機並不知道。服務器直接訪問目標地址的URL,並把那個URL的相應內容讀取過來,然后把這些內容再發給瀏覽器,瀏覽器根本不知道服務器發送的內容是從哪里來的,所以它的 地址欄中還是原來的地址,轉發時並不通知客戶機, 對象可以存儲在請求中,並發給下一個資源使用,並且完全在服務器上面進行;HTTP狀態碼
- 1**:請求收到,繼續處理
100——客戶必須繼續發出請求
101——客戶要求服務器根據請求轉換HTTP協議版本
- 2**:操作成功收到,分析、接受
200("OK") 一般來說,這是客戶端希望看到的響應代碼。它表示服務器成功執行了客戶端所請求的動作
- 3XX 重定向
3XX系列響應代碼表明:客戶端需要做些額外工作才能得到所需要的資源
301 redirect: 301 代表永久性轉移(Permanently Moved)
302 redirect: 302 代表暫時性轉移(Temporarily Moved )
- 4**:客戶端錯誤, 請求包含一個錯誤語法或不能完成
404——沒有發現文件、查詢或URl
401——未授權
- 5XX 服務端錯誤
這些響應代碼表明服務器端出現錯誤。一般來說,這些代碼意味着服務器處於不能執行客戶端請求的狀態,此時客戶端應稍后重試。
500("Internal Server Error")
- 這是一個通用的服務器錯誤響應。對於大多數web框架,如果在執行請求處理代碼時遇到了異常,它們就發送此響應代碼。
說一說HTTP請求頭的內容
HTTP請求報文由3部分組成(請求行+請求頭+請求體):
①是請求方法,GET和POST是最常見的HTTP方法,除此以外還包括DELETE、HEAD、OPTIONS、PUT、TRACE。
②為請求對應的URL地址,它和報文頭的Host屬性組成完整的請求URL,③是協議名稱及版本號。
④是HTTP的報文頭,報文頭包含若干個屬性,格式為“屬性名:屬性值”,服務端據此獲取客戶端的信息。
⑤是報文體,它將一個頁面表單中的組件值通過param1=value1¶m2=value2的鍵值對形式編碼成一個格式化串,它承載多個請求參數的數據。不但報文體可以傳遞請求參數,請求URL也可以通過類似於“/chapter15/user.html? param1=value1¶m2=value2”的方式傳遞請求參數。
常見的HTTP請求報文頭屬性
Accept:客戶端接受什么類型的響應,常見的如Accept:text/plain,Accept屬性的值可以為一個或多個MIME類型的值。
Cookie: 客戶端的Cookie就是通過這個報文頭屬性傳給服務端
Referer 表示這個請求是從哪個URL過來的
Cache-Control 對緩存進行控制,控制是否緩存及緩存時間為多久。
Accept-Language:接受的語言類型
User-Agent: 通過何種代理(瀏覽器)訪問
Content-type, Content-Length等
Connection :
Connection: keep-alive 當一個網頁打開完成后,客戶端和服務器之間用於傳輸HTTP數據的TCP連接不會關閉,如果客戶端再次訪問這個服務器上的網頁,會繼續使用這一條已經建立的連接
Connection: close 代表一個Request完成后,客戶端和服務器之間用於傳輸HTTP數據的TCP連接會關閉, 當客戶端再次發送Request,需要重新建立TCP連接。
談談HTTP響應報文
HTTP的響應報文也由三部分組成(響應行+響應頭+響應體):
以下是一個實際的HTTP響應報文:
①報文協議及版本;
②狀態碼及狀態描述;
③響應報文頭,也是由多個屬性組成;
④響應報文體,即我們真正要的“干貨”。
響應狀態碼
和請求報文相比,響應報文多了一個“響應狀態碼”,它以“清晰明確”的語言告訴客戶端本次請求的處理結果。
HTTP的響應狀態碼由5段組成:
- 1xx 消息,一般是告訴客戶端,請求已經收到了,正在處理,別急...
- 2xx 處理成功,一般表示:請求收悉、我明白你要的、請求已受理、已經處理完成等信息.
- 3xx 重定向到其它地方。它讓客戶端再發起一個請求以完成整個處理。
- 4xx 處理發生錯誤,責任在客戶端,如客戶端的請求一個不存在的資源,客戶端未被授權,禁止訪問等。
- 5xx 處理發生錯誤,責任在服務端,如服務端拋出異常,路由出錯,HTTP版本不支持等。
從瀏覽器發出請求開始,到服務端應用接受到請求返回結果顯示的過程
第一步,解析域名,找到ip
查本地緩存--查hosts文件--查ISP服務商的DNS緩存--從根域名開始遞歸搜索返回
瀏覽器會緩存DNS一段時間,一般2-30分鍾不等,如果有緩存,直接返回ip,否則下一步。
緩存中無法找到ip,瀏覽器會進行一個系統調用,查詢hosts文件。如果找到,直接返回ip,否則下一步。
進行1 和2 本地查詢無果,只能借助於網絡,路由器一般都會有自己的DNS緩存,ISP服務商DNS緩存,這時一般都能夠得到相應的ip,如果還是無果,只能借助於DNS遞歸解析了。
這時ISP的DNS服務器就會開始從根域名服務器開始遞歸搜索,從.com 頂級域名服務器,到baidu的域名服務器。
到這里,瀏覽器就獲得網絡ip,在DNS解析過程中,常常解析出不同的IP。
第二步,瀏覽器於網站建立TCP連接
瀏覽器利用ip直接網站主機通信,瀏覽器發出TCP連接請求,主機返回TCP應答報文,瀏覽器收到應答報文發現ACK標志位為1,表示連接請求確認,瀏覽器返回TCP()確認報文,主機收到確認報文,三次握手,TCP連接建立完成。
第三步, 瀏覽器發起默認的GET請求
瀏覽器向主機發起一個HTTP-GET方法報文請求,請求中包含訪問的URL,也就是http://www.baidu.com/還有User-Agent用戶瀏覽器操作系統信息,編碼等,值得一提的是Accep-Encoding和Cookies項。Accept-Encoding一般采用gzip,壓縮之后傳輸html文件,Cookies如果是首次訪問,會提示服務器簡歷用戶緩存信息,如果不是,可以利用Cookies對應鍵值,找到相應緩存,緩存里面存放着用戶名,密碼和一些用戶設置項
第四步,顯示頁面或返回其他
返回狀態碼200 OK,表示服務器可以響應請求,返回報文,由於在報頭中Content-type為“text/html”,瀏覽器以HTML形式呈現,而不是下載文件。
但是對於大型網站存在多個主機站點,往往不會直接返回請求頁面,而是重定向。返回的狀態碼就不是 200 OK, 而是301,302以3開頭的重定向嗎。瀏覽器在獲取了重定向響應后,在響應報文中Location項找到重定向地址,瀏覽器重新第一步訪問即可。
第三模塊--操作系統、Git
Linux常用指令 (查看內存、進程之類的)
- 文件查找:find
- 文本搜索:grep
- 排序:sort
- 按列切分文本:cut
- 統計行和字符:wc
- 文本替換:sed
- 數據流處理:awk
- 性能分析
- 進程查詢:ps
- 進程監控:top
- 打開文件查詢:lsof
- 內存使用量:free
- 監控性能指標:sar
網絡工具
- 網卡配置:ifconfig
- 查看當前網絡連接:netstat
- 查看路由表:route
- 檢查網絡連通性:ping
- 轉發路徑:traceroute
- 命令行抓包:tcpdump
- 域名解析工具:dig
- 網絡請求:curl
其他
- 終止進程:kill
- 修改文件權限:chmod
- 創建鏈接:ln
- 顯示文件尾:tail
- 版本控制:git
- 設置別名:alias
以殺掉tomcat為例子
ps -ef|grep tomcat|grep -v|awk -F '{print $2}'|xargs kill -9
等同於
kill -9 `ps -ef | grep 'tomcat' | awk '{print $2}'`
xargs接收管道前面傳過來的字符;
awk '{print $2}'的意思是選取並輸出第二列的數據
kill 與kill -9的區別
- kill 的默認參數是 -15, kill pid的效果等同於kill -15 pid。 執行kill命令,系統會發送一個關閉信號給對應的程序。當程序接收到該信號后,將有機會釋放資源、處理善后工作后再停止;
- kill -9命令,系統給對應程序發送的信號是SIGKILL,即exit。exit信號不會被系統阻塞,所以kill -9能順利殺掉進程。相當於強制關閉程序。
在使用 kill -9 前,應該先使用 kill -15,給目標進程一個清理善后工作的機會。如果沒有,可能會留下一些不完整的文件或狀態,從而影響服務的再次啟動。
查詢日志
1. 按行號查看---過濾出關鍵字附近的日志
首先: cat -n test.log |grep "keyword" 得到關鍵日志的行號
然后,得到"keyword"關鍵字所在的行號是102行. 此時如果想查看這個關鍵字前10行和后10行的日志:
cat -n test.log |tail -n +92|head -n 20
2. 刷新顯示日志變動
tail -f test.log 查看日志的尾部,並刷新顯示日志變動。此方法適合在調試程序的時候查看日志,日志變動會實時刷新顯示到終端。
top指令能查看進程的哪些信息
包括進程的相關信息,包括進程ID,內存占用率,CPU占用率
待補充
守護進程了解嗎
線程是調度的基本單位,進程是資源分配的基本單位。。
守護進程是生存期長的一種進程。它們獨立於控制終端並且周期性的執行某種任務或等待處理某些發生的事件。他們常常在系統引導裝入時啟動,在系統關閉時終止。linux系統有很多守護進程,大多數服務器都是用守護進程實現的。linux上的守護進程類似於windows上的服務。
父進程在調用fork接口之后和子進程已經可以獨立開,之后父進程和子進程就以未知的順序向下執行(異步過程)。所以父進程和子進程都有可能先執行完。當父進程先結束,子進程此時就會變成孤兒進程,不過這種情況問題不大,孤兒進程會自動向上被init進程收養,init進程完成對狀態收集工作。而且這種過繼的方式也是守護進程能夠實現的因素。如果子進程先結束,父進程並未調用wait或者waitpid獲取進程狀態信息,那么子進程描述符就會一直保存在系統中,這種進程稱為僵屍進程。
Git常見指令
fetch和merge和pull的區別
pull相當於git fetch 和 git merge,即更新遠程倉庫的代碼到本地倉庫,然后將內容合並到當前分支。
git fetch:相當於是從遠程獲取最新版本到本地,不會自動merge
git merge : 將內容合並到當前分支
git pull:相當於是從遠程獲取最新版本並merge到本地
常用命令
git show # 顯示某次提交的內容 git show $id
git add <file> # 將工作文件修改提交到本地暫存區
git rm <file> # 從版本庫中刪除文件
git reset <file> # 從暫存區恢復到工作文件
git reset HEAD^ # 恢復最近一次提交過的狀態,即放棄上次提交后的所有本次修改
git diff <file> # 比較當前文件和暫存區文件差異 git diff
git log -p <file> # 查看每次詳細修改內容的diff
git branch -r # 查看遠程分支
git merge <branch> # 將branch分支合並到當前分支
git stash # 暫存
git stash pop #恢復最近一次的暫存
git pull # 抓取遠程倉庫所有分支更新並合並到本地
git push origin master # 將本地主分支推到遠程主分支
第四模塊--JAVA語言特性
Integer緩存數據的范圍
java定義:在自動裝箱時對於值從–128到127之間的值,它們被裝箱為Integer對象后,會存在內存中被重用,始終只存在一個對象。這歸結於java對於Integer與int的自動裝箱與拆箱的設計,是一種模式:享元模式(flyweight)
而如果超過了從–128到127之間的值,被裝箱后的Integer對象並不會被重用,即相當於每次裝箱時都新建一個 Integer對象;以上的現象是由於使用了自動裝箱所引起的,如果你沒有使用自動裝箱,而是跟一般類一樣,用new來進行實例化,就會每次new就都一個新的對象。
https://www.cnblogs.com/greatLong/p/10776561.html
面向對象編程和面向過程編程的優缺點
面向過程
優點:性能比面向對象高,因為類調用時需要實例化,開銷比較大,比較消耗資源;比如單片機、嵌入式開發、 Linux/Unix等一般采用面向過程開發,性能是最重要的因素。
缺點:沒有面向對象易維護、易復用、易擴展
面向對象
優點:易維護、易復用、易擴展,由於面向對象有封裝、繼承、多態性的特性,可以設計出低耦合的系統,使系統更加靈活、更加易於維護
缺點:性能比面向過程低
JAVA提供了幾個類加載器?分別是?怎么對類進行加載的?
根加載器、擴展類加載器、系統類加載器
Bootstrap 是JVM在啟動時創建的,通常由與操作系統相關的本地代碼實現,是最根基的類加載器,負責裝載最核心的JAVA類,如Object, System, String;
然后第二層在JDK9中稱為Platform 加載器, JDK9之前叫Extension加載器,加載一些擴展的系統類,如XML, 加密, 壓縮相關的功能類;
第三層是Application 加載器, 主要加載用戶定義的CLASSPATH路徑下的類。
類加載的雙親委任模型
低層次的當前類加載器,不能覆蓋更高層次類加載器已經加載的類。如果低層次的類加載器想加載一個未知類,要非常禮貌地向上逐級詢問:“請問,這個類已經加載了嗎?”被詢問的高層次類加載器會自問兩個問題:第一,我是否已加載過此類?第二,如果沒有,是否可以加載此類?只有當所有高層次類加載器在兩個問題上的答案均為“否”時,才可以讓當前類加載器加載這個未知類。如圖4-6所示,左側綠色箭頭向上逐級詢問是否已加載此類,直至Bootstrap ClassLoader,然后向下逐級嘗試是否能夠加載此類,如果都加載不了,則通知發起加載請求的當前類加載器,准予加載。
JAVA類的加載過程
在JVM中,類從被加載到虛擬機內存中開始,到卸載出內存為止,它的整個生命周期包括:加載、驗證、准備、解析、初始化5個階段。而解析階段即是虛擬機將常量池內的符號引用替換為直接引用的過程。
- 加載:程序運行之前jvm會把編譯完成的.class二進制文件加載到內存,供程序使用,用到的就是類加載器classLoader 2、
- 連接:分為三步 驗證 》准備 》解析
驗證:確保類加載的正確性。
准備:為類的靜態變量分配內存,將其初始化為默認值 。
解析:把類中的符號引用轉化為直接引用。
3. 初始化:為類的靜態變量賦予正確的初始值,上述的准備階段為靜態變量賦予的是虛擬機默認的初始值,此處賦予的才是程序編寫者為變量分配的真正的初始值
現在java程序的執行就可以分為
類成員初始化順序總結:先靜態,后父類普通構造,再子類普通構造,同級看書寫順序
1.先執行父類靜態變量和靜態代碼塊,再執行子類靜態變量和靜態代碼塊 2.先執行父類普通變量和代碼塊,再執行父類構造器(static方法) 3.先執行子類普通變量和代碼塊,再執行子類構造器(static方法) 4.static方法初始化先於普通方法,靜態初始化只有在必要時刻才進行且只初始化一次。
反射機制
- 定義: 在運行狀態中,對於任意一個類,都能夠獲取到這個類的所有屬性和方法,對於任意一個對象,都能夠調用它的任意一個方法和屬性(包括私有的方法和屬性),這種動態獲取的信息以及動態調用對象的方法的功能就稱為java語言的反射機制。
- 使用: 想要使用反射機制,就必須要先獲取到該類的字節碼文件對象(.class),通過字節碼文件對象,就能夠通過該類中的方法獲取到我們想要的所有信息(方法,屬性,類名,父類名,實現的所有接口等等),每一個類對應着一個字節碼文件也就對應着一個Class類型的對象,也就是字節碼文件對象。
- Java反射的三種實現方式
Foo foo = new Foo();
第一種:通過Object類的getClass方法
Class cla = foo.getClass();
第二種:通過對象實例方法獲取對象
Class cla = foo.class;
第三種:通過Class.forName方式
Class cla = Class.forName("xx.xx.Foo");
HashCode 是干什么用的?如果重寫HashCode 有哪些注意點?
hashCode方法的主要作用是為了配合基於散列的集合一起正常運行,這樣的散列集合包括HashSet、HashMap以及HashTable。當向集合中插入對象時,如何判別在集合中是否已經存在該對象了?(注意:集合中不允許重復的元素存在)也許大多數人都會想到調用equals方法來逐個進行比較,這個方法確實可行。但是如果集合中已經存在一萬條數據或者更多的數據,如果采用equals方法去逐一比較,效率必然是一個問題。此時hashCode方法的作用就體現出來了,當集合要添加新的對象時,先調用這個對象的hashCode方法,得到對應的hashcode值,實際上在HashMap的具體實現中會用一個table保存已經存進去的對象的hashcode值,如果table中沒有該hashcode值,它就可以直接存進去,不用再進行任何比較了;如果存在該hashcode值, 就調用它的equals方法與新元素進行比較,相同的話就不存了,不相同就散列其它的地址,所以這里存在一個沖突解決的問題,這樣一來實際調用equals方法的次數就大大降低了。
在每個類中,在重寫 equals 方法的時侯,一定要重寫 hashcode 方法。如果不這樣做,就違反了hashCode的通用約定,這會阻止它在HashMap和HashSet這樣的集合中正常工作。
- 如果兩個對象根據equals(Object)方法比較是相等的,那么在兩個對象上調用hashCode就必須產生的結果是相同的整數。
- 如果兩個對象根據equals(Object)方法比較並不相等,則不要求在每個對象上調用hashCode都必須產生不同的結果。 但是,程序員應該意識到,為不相等的對象生成不同的結果可能會提高散列表(hash tables)的性能。
JAVA集合類的常見問題
HashMap底層是怎么實現的
數組加鏈表的形式
在JAVA 8 中,改進為,當鏈表長度達到8及以上,轉為紅黑樹來實現; 紅黑樹的數據數量將為6的時候又會自動轉為鏈表。
(因為長度為6的鏈表平均查詢次數是3, 查找樹的話,3次查詢能查到8個數據)
HashMap 是一個用於存儲Key-Value 鍵值對的集合,每一個鍵值對也叫做Entry。這些個Entry 分散存儲在一個數組當中,這個數組就是HashMap 的主干。 每個Entry都是鏈表的結構,含有可以指向一個Entry的next指針;
HashMap 數組每一個元素的初始值都是Null。
1. Put 方法的原理
調用Put方法的時候發生了什么呢? 根據鍵計算哈希值,確定放在哪個Entry里,如果計算出index位置的entry已經有值,就將新加入的鍵值對掛在這個entry的鏈表尾部。
比如調用 hashMap.put(“apple”, 0) ,插入一個Key為“apple”的元素。這時候我們需要利用一個哈希函數來確定Entry的插入位置(index): index = Hash("apple")
假定最后計算出的index是2,那么結果如下:
但是,因為HashMap的長度是有限的,當插入的Entry越來越多時,再完美的Hash函數也難免會出現index沖突的情況。比如下面這樣:
這時候該怎么辦呢?我們可以利用鏈表來解決。
HashMap數組的每一個元素不止是一個Entry對象,也是一個鏈表的頭節點。每一個Entry對象通過Next指針指向它的下一個Entry節點。當新來的Entry映射到沖突的數組位置時,只需要插入到對應的鏈表即可:
新來的Entry節點插入鏈表時,使用的是頭插法。
2. Get方法的原理
使用Get方法根據Key來查找Value的時候,發生了什么呢?
首先會把輸入的Key做一次Hash映射,得到對應的index:
index = Hash(“apple”)
由於剛才所說的Hash沖突,同一個位置有可能匹配到多個Entry,這時候就需要順着對應鏈表的頭節點,一個一個向下來查找。假設我們要查找的Key是“apple”:
第一步,我們查看的是頭節點Entry6,Entry6的Key是banana,顯然不是我們要找的結果。
第二步,我們查看的是Next節點Entry1,Entry1的Key是apple,正是我們要找的結果。
之所以把Entry放在頭節點,是因為HashMap的發明者認為,后插入的Entry被查找的可能性更大。
3. HashMap的初始長度
初始長度為16,且每次自動擴容或者手動初始化的時候必須是2的冪。
對於新插入的數據或者待讀取的數據,HashMap將Key的哈希值對數組長度取模,結果作為該Entry在數組中的index。在計算機中,取模的代價遠高於位操作的代價,因此HashMap要求數組的長度必須為2的N次方。此時將Key的哈希值對2^N-1進行與運算,其效果即與取模等效。HashMap並不要求用戶在指定HashMap容量時必須傳入一個2的N次方的整數,而是會通過Integer.highestOneBit算出比指定整數大的最小的2^N值。
如何進行位運算呢?有如下的公式(Length是HashMap的長度):
之前說過,從Key映射到HashMap數組的對應位置,會用到一個Hash函數: index = Hash(“apple”)
如何實現一個盡量均勻分布的Hash函數呢?我們通過利用Key的HashCode值來做某種運算。 index = HashCode(Key) & (Length - 1)
下面我們以值為“book”的Key來演示整個過程:
- 計算book的hashcode,結果為十進制的3029737,二進制的
101110001110101110 1001
。 - 假定HashMap長度是默認的16,計算Length-1的結果為十進制的15,二進制的1111。
- 把以上兩個結果做與運算,
101110001110101110 1001 & 1111 = 1001
,十進制是9,所以 index=9。
可以說,Hash算法最終得到的index結果,完全取決於Key的Hashcode值的最后幾位。這里的位運算其實是一種快速取模算法。
HashMap 的size為什么必須是2的冪?。這是因為2的冪用二進制表示時所有位都為1,例如16-1=15 的二進制就是1111B。我們說了Hash算法是為了讓hash 的分布變得均勻。其實我們可以把1111看成四個通道,表示跟1111 做&運算后分布是均勻的。假如默認長度取10,二進制表示為1010,這樣就相當於有兩個通道是關閉的,所以計算出來的索引重復的幾率比較大。
HashMap的擴容條件:
(size>=threshold) && (null != table[bucketIndex]) 即達到閾值,並且當前需要存放對象的slot上已經有值。
解決hash沖突的辦法
- 開放定址法(線性探測再散列,二次探測再散列,偽隨機探測再散列)
- 再哈希法
- 鏈地址法
- 建立一個公共溢出區
Java中hashmap的解決辦法就是采用的鏈地址法
解決Hash沖突的開放地址檢測是怎么實現的
這個方法的基本思想是:當發生地址沖突時,按照某種方法繼續探測哈希表中的其他存儲單元,直到找到空位置為止。這個過程可用下式描述:
H i ( key ) = ( H ( key )+ d i ) mod m ( i = 1,2,…… , k ( k ≤ m – 1))
其中: H ( key ) 為關鍵字 key 的直接哈希地址, m 為哈希表的長度, di 為每次再探測時的地址增量。
增量 d 可以有不同的取法,並根據其取法有不同的稱呼:
( 1 ) d i = 1 , 2 , 3 , …… 線性探測再散列;
( 2 ) d i = 1^2 ,- 1^2 , 2^2 ,- 2^2 , k^2, -k^2…… 二次探測再散列;
( 3 ) d i = 偽隨機序列 偽隨機再散列;
從哈希表中刪除一個元素,再加入元素時恰好與原來的那個哈希沖突,這個元素會放在哪
放在沖突Entry最開始的地方,因為哈希表在沖突時采用的是頭插法,之所以把Entry放在頭節點,是因為HashMap的發明者認為,后插入的Entry被查找的可能性更大。
並發容器, hashtable和concurrenthashmap的區別
HashMap為什么線程不安全?
主要有死鏈和高並發下的數據用丟失兩個問題
-
死鏈
https://www.jianshu.com/p/e2f75c8cce01
主要發生在多線程情況下rezise時遷移數據的transfer()方法,put(), get()方法。多個線程同時擴容時, hashmap會可能產生環,造成死循環。
-
高並發下的數據丟失
新增對象丟失的原因一般有:
-
- 並發賦值時被覆蓋
- resize線程已遍歷區間新增元素會丟失
- 多個線程同時resize“新表“被覆蓋
- 遷移丟失
1. 數據丟失場景: 在表擴容時,當前線程遷移數據的過程中,其他線程新增的元素有可能落在遷移線程已經遍歷過的哈希槽上;在遍歷完成之后,table數組引用指向了newTable, 這時剛才新增的元素就會失去引用,被GC回收。
2. 數據覆蓋場景:
put的時候導致的多線程數據不一致:有兩個線程A和B,首先A希望插入一個key-value對到HashMap中,首先計算記錄所要落到的桶的索引坐標,然后獲取到該桶里面的鏈表頭結點,此時線程A的時間片用完了,而此時線程B被調度得以執行,和線程A一樣執行,只不過線程B成功將記錄插到了桶里面,假設線程A插入的記錄計算出來的桶索引和線程B要插入的記錄計算出來的桶索引是一樣的,那么當線程B成功插入之后,線程A再次被調度運行時,它依然持有過期的鏈表頭但是它對此一無所知,以至於它認為它應該這樣做,如此一來就覆蓋了線程B插入的記錄,這樣線程B插入的記錄就憑空消失了,造成了數據不一致的行為。
或者多個線程同時執行resize,也會造成table的覆蓋問題。
ConcurrentHashMap
Hashtable 在JDK1.0引入,以全互斥方式處理並發問題,性能極差;
HashMap在JDK1.2引入,非線程安全,在並發寫的情形下,容易出現死鏈的問題;
ConcurrentHashMap在JDK5中引入,是線程安全的哈希式集合,JDK1.8之前采用分段鎖的設計理念,相當於Hashtable和HashMap的折衷版。分段鎖由內部類Segment實現,繼承於ReentrantLock, 用它來管轄轄區每個HashEntry。JDK11對JDK7做了如下優化:
- 取消分段鎖機制,進一步降低沖突的概率;
- 引入紅黑樹結構,同一個哈希槽上的元素個數超過一定閾值后(8),將單向鏈表轉為紅黑樹結構(在轉化中,使用同步塊鎖住當前槽的首元素,防止其他線程對當前槽進行修改,轉化完成后利用CAS替換原有鏈表);當某個槽的元素減少至6個時,由紅黑樹重新轉回鏈表;
- 使用更加優化的方式統計集合內的元素數量
待補充:
快排
索引
LinkedHashMap
LinkedHashMap是有序的HashMap
map = new LinkedHashMap<K, V>(capacity, DEFAULT_LOAD_FACTORY, false)
- 第三個參數設置為true,代表linkedlist按訪問順序排序,可作為LRU緩存 ;
- 第三個參數設置為false,代表按插入順序排序,可作為FIFO緩存 ;
JAVA創建對象的方法有哪些
Java中有5種創建對象的方式,下面給出它們的例子還有它們的字節碼
使用new關鍵字 | } → 調用了構造函數 |
使用Class類的newInstance方法 | } → 調用了構造函數 |
使用Constructor類的newInstance方法 | } → 調用了構造函數 |
使用clone方法 | } → 沒有調用構造函數 |
使用反序列化 | } → 沒有調用構造函數 |
- 使用Class類的newInstance方法:我們也可以使用Class類的newInstance方法創建對象,這個newInstance方法調用無參的構造器創建對象
- 使用Constructor類的newInstance方法我們可以通過這個newInstance方法調用有參數的和私有的構造函數。這兩種newInstance的方法就是大家所說的反射,事實上Class的newInstance方法內部調用Constructor的newInstance方法。
- 調用一個對象的clone方法,JVM就會創建一個新的對象,將前面的對象的內容全部拷貝進去,用clone方法創建對象並不會調用任何構造函數。要使用clone方法,我們必須先實現Cloneable接口並實現其定義的clone方法。
- 使用反序列化:當我們序列化和反序列化一個對象,JVM會給我們創建一個單獨的對象,在反序列化時,JVM創建對象並不會調用任何構造函數。為了反序列化一個對象,我們需要讓我們的類實現Serializable接口。
Object 類有哪些方法?
九個方法:clone, getclass, toString, finalize, equals, hashcode, wait, notify, notifyAll
什么是JAVA序列化
把對象轉換為字節序列的過程稱為對象的序列化。
把字節序列恢復為對象的過程稱為對象的反序列化。
對象的序列化主要有兩種用途:
1) 把對象的字節序列永久地保存到硬盤上,通常存放在一個文件中;
2) 在網絡上傳送對象的字節序列。
String, stringbuilder, stringbuffer區別
- 首先說運行速度(特指修改的操作),在這方面運行速度快慢為:StringBuilder > StringBuffer > String
String最慢的原因:
String為字符串常量,String對象一旦創建之后該對象是不可更改的,Java中對String對象進行的更改操作實際上是一個不斷創建新的對象並且將舊的對象回收的一個過程,所以執行速度很慢。而StringBuilder和StringBuffer均為字符串變量,對變量進行操作就是直接對該對象進行更改,而不進行創建和回收的操作,所以速度要比String快很多。
- 在線程安全上,StringBuilder是線程不安全的,而StringBuffer是線程安全的。
如果一個StringBuffer對象在字符串緩沖區被多個線程使用時,StringBuffer中很多方法可以帶有synchronized關鍵字,所以可以保證線程是安全的,但StringBuilder的方法則沒有該關鍵字,所以不能保證線程安全。
String:適用於少量的字符串操作的情況
StringBuilder:適用於單線程下在字符緩沖區進行大量操作的情況
StringBuffer:適用多線程下在字符緩沖區進行大量操作的情況
string的不可變如何實現
通常情況下,在java中通過以下步驟實現不可變
- 對於屬性不提供設值方法
- 所有的屬性定義為private final
- 類聲明為final不允許繼承
如果讀了String 的源碼的話,在Java中String類其實就是對字符數組的封裝, JDK6中, value是String封裝的數組,offset是String在這個value數組中的起始位置,count是String所占的字符的個數。會發現String 類的所有變量(value,offset和count等)都是 private final 聲明的,且沒有對外提供任何set方法。所以在String類的外部無法修改String。也就是說一旦初始化就不能修改, 並且在String類的外部不能訪問這三個成員。一旦String初始化了, 也不能被改變。所以可以認為String對象是不可變的了。
string為何設計為不可變
1. 字符串常量池的需要
字符串常量池(String pool, String intern pool, String保留池) 是Java堆內存中一個特殊的存儲區域, 當創建一個String對象時,假如此字符串值已經存在於常量池中,則不會創建一個新的對象,而是引用已經存在的對象。假若字符串對象允許改變,那么將會導致各種邏輯錯誤,比如改變一個對象會影響到另一個獨立對象. 嚴格來說,這種常量池的思想,是一種優化手段。
2. 允許String對象緩存HashCode
Java中String對象的哈希碼被頻繁地使用, 比如在hashMap 等容器中。字符串不變性保證了hash碼的唯一性,因此可以放心地進行緩存.這也是一種性能優化手段,意味着不必每次都去計算新的哈希碼.
3. 安全性
String被許多的Java類(庫)用來當做參數,例如 網絡連接地址URL,文件路徑path,還有反射機制所需要的String參數等, 假若String不是固定不變的,將會引起各種安全隱患。
總體來說, String不可變的原因包括 設計考慮,效率優化問題,以及安全性這三大方面。
為什么String被設計為不可變?
- 安全:首要原因是安全,不僅僅體現在你的應用中,而且在JDK中,Java的類裝載機制通過傳遞的參數(通常是類名)加載類,這些類名在類路徑下,想象一下,假設String是可變的,一些人通過自定義類裝載機制分分鍾黑掉應用。如果沒有了安全,Java不會走到今天。
- 性能: string不可變的設計出於性能考慮,當然背后的原理是string pool,當然string pool不可能使string類不可變,不可變的string更好的提高性能。
- 線程安全: 當多線程訪問時,不可變對象是線程安全的,不需要什么高深的邏輯解釋,如果對象不可變,線程也不能改變它。
異常
Java把異常當作對象來處理,並定義一個基類java.lang.Throwable
作為所有異常的超類。Throwable 下又分為 Error 和 Exception。
- Error 用來表示 JVM 無法處理的錯誤
Error
:Error
類對象由 Java 虛擬機生成並拋出,大多數錯誤與代碼編寫者所執行的操作無關。最常見的erro是當JVM不再有繼續執行操作所需的內存資源時,將出現
OutOfMemoryError
。這些異常發生時,Java虛擬機(JVM)一般會選擇線程終止;
- Exception 分為兩種:
-
- 受檢異常 :需要用 try...catch... 語句捕獲並進行處理,並且可以從異常中恢復;
- 非受檢異常 :是程序運行時錯誤,例如除 0 會引發 Arithmetic Exception,此時程序崩潰並且無法恢復。
設計模式有哪些
單例模式: (懶漢模式、飢漢模式)
1、構造方法私有化,除了自己類中能創建外其他地方都不能創建
2、在自己的類中創建一個單實例(飽漢模式是一出來就創建創建單實例,而飢漢模式需要的時候才創建)
3、提供一個方法獲取該實例對象(創建時需要進行方法同步)
餓漢模式
餓漢模式就是立即加載,在方法調用前,實例就已經被創建了,所以是線程安全的。
public class MyObject1 { private static MyObject1 myObject1 = new MyObject1(); private MyObject1() {} public static MyObject1 getInstance() { return myObject1; } }懶漢模式
懶漢就是延遲化加載,當需要使用的時候才進行實例化。
線程不安全方式:
public class MyObject2 { private static MyObject2 myObject2; private MyObject2() {} public static MyObject2 getInstance() { if (myObject2 == null) { myObject2 = new MyObject2(); } return myObject2; } }線程安全但是效率低下的方式:
public class MyObject3 { private static MyObject3 myObject3; private MyObject3() {} synchronized public static MyObject3 getInstance() { if (myObject3 == null) { myObject3 = new MyObject3(); } return myObject3; } }使用DCL雙檢查鎖(雙重檢查鎖定),線程安全而且效率得到提高,只將進行實例化的代碼進行加鎖:
public class MyObject4 { private volatile static MyObject4 myObject4; private MyObject4() {} public static MyObject4 getInstance() { if (myObject4 == null) { synchronized (MyObject4.class) { if (myObject4 == null) { myObject4 = new MyObject4(); } } } return myObject4; } }但要注意實例化的對象一定要聲明位volatile型,以禁止指令重排序,否則仍然出現線程安全問題,分析見此。
還有其他初始化的手段,見本博客
工廠模式: Spring IOC就是使用了工廠模式.
對象的創建交給一個工廠去創建。在工廠方法模式中,核心的工廠類不再負責所有的產品的創建,而是將具體創建的工作交給子類去做。
抽象工廠模式:抽象工廠模式可以向客戶端提供一個接口,使客戶端在不必指定產品的具體的情況下,創建多個產品族中的產品對象。
代理模式: Spring AOP就是使用的動態代理。
觀察者模式:在對象之間定義了一對多的依賴,這樣一來,當一個對象改變狀態,依賴它的對象會收到通知並自動更新。
裝飾器模式:在不影響其他對象的情況下,以動態、透明的方式給單個對象添加職責,在Java源碼中典型的裝飾器模式就是java I/O, 其實裝飾者模式也有其缺點,就是產生了太多的裝飾者小類,有類爆炸的趨勢。
第五模塊--數據結構:
BST 二叉搜索樹
1 首先它也是一個二叉樹,故滿足遞歸定義;
2 需滿足 左子樹值<=根值<=右子樹 ,BST的中序遍歷必定是嚴格遞增的。
3. 任意節點的左右子樹也分別是二叉查找樹.
4. 沒有鍵值相等的節點.
5. 對於某些情況,二叉查找樹會退化成一個有n個節點的線性鏈
AVL樹
1. 是帶有平衡條件的二叉查找樹,一般是用平衡因子差值判斷是否平衡並通過旋轉來實現平衡,
2. 左右子樹樹高不超過1,和紅黑樹相比,它是嚴格的平衡二叉樹,平衡條件必須滿足(所有節點的左右子樹高度差不超過1).
3. 不管我們是執行插入還是刪除操作,只要不滿足上面的條件,就要通過旋轉來保持平衡,而旋轉是非常耗時的,由此我們可以知道AVL樹適合用於插入刪除次數比較少,但查找多的情況。
紅黑樹的六大特征
本質上是一個平衡搜索二叉樹,和Btree不一樣,可以說是AVL樹的變種,犧牲了一定查詢效率,減少了維護平衡的時間。log(n) 中序排序都能得到遞增序列。
1)每個結點要么是紅的,要么是黑的。(沒有其他顏色)
2)根結點是黑的。
3)每個葉結點(葉結點即指樹尾端NIL(Nothing In the Leaf)指針或NULL結點)是黑的。(葉子節點即為樹尾端的NIL指針,或者說NULL節點。)
4)一條路徑上不能出現相鄰的兩個紅色節點
5)對於任一結點而言,其到葉結點(樹尾端NIL指針)的每一條路徑都包含相同數目的黑結點。(最長路徑是最小路徑的2倍)
6)最多三次旋轉,可以達到平衡
上述條件保證紅黑樹的新增、查找、刪除的最壞時間復雜度均為O(logN)。
通過對任何一條從根到葉子的路徑上各個節點着色的方式的限制,紅黑樹確保沒有一條路徑會比其它路徑長出兩倍.它是一種弱平衡二叉樹(由於是弱平衡,可以推出,相同的節點情況下,AVL樹的高度低於紅黑樹),相對於要求嚴格的AVL樹來說,它的旋轉次數變少,所以對於搜索,插入,刪除操作多的情況下,我們就用紅黑樹.
紅黑樹相比於BST和AVL樹有什么優點?
紅黑樹是犧牲了嚴格的高度平衡的優越條件為代價,它只要求部分地達到平衡要求,降低了對旋轉的要求,從而提高了性能。紅黑樹能夠以O(logN)的時間復雜度進行搜索、插入、刪除操作。此外,由於它的設計,任何不平衡都會在三次旋轉之內解決。當然,還有一些更好的,但實現起來更復雜的數據結構能夠做到一步旋轉之內達到平衡,但紅黑樹能夠給我們一個比較“便宜”的解決方案。
相比於BST,因為紅黑樹可以能確保樹的最長路徑不大於兩倍的最短路徑的長度,所以可以看出它的查找效果是有最低保證的。在最壞的情況下也可以保證O(logN)的,這是要好於二叉查找樹的。因為二叉查找樹最壞情況可以讓查找達到O(N)。紅黑樹的算法時間復雜度和AVL相同,但統計性能比AVL樹更高,所以在插入和刪除中所做的后期維護操作肯定會比紅黑樹要耗時好多
在插入時,紅黑樹和AVL樹都能在至多2次旋轉內恢復平衡;在刪除時,紅黑樹由於只追求大致上的平衡,能在至多3次旋轉內恢復平衡,而追求絕對平衡的AVL樹至多需要O(logN)次旋轉。
B-樹(B樹、平衡多路樹)
B-tree是一種平衡多路搜索樹(並不一定是二叉的)B-tree樹即B樹
B樹,一般用於外存儲索引結構。系統從磁盤讀取數據到內存時是以磁盤塊(block)為基本單位的。二叉樹、紅黑樹 [復雜度O(h)]導致樹高度非常高(平衡二叉樹一個節點只能有左子樹和右子樹),邏輯上很近的節點(父子)物理上可能很遠,無法利用局部性,IO次數多查找慢,效率低。
特點:
(1)根節點的左子樹和右子樹的深度最多相差1.(確保了不會出現上圖右邊的極端現象)
(2)是一個平衡多路搜索樹,單個節點能放多個子節點
(3)所有結點都有存儲關鍵字(數據);
(4)不太穩定,可能走不到葉子節點
B+TREE
B+樹是B-樹的變體,也是一種平衡多路搜索樹,非葉子節點只做索引,所有數據都保存在葉子節點中。
和B樹區別
1:根節點和分支節點中不保存數據,只用於索引 。數據和索引分離
2:所有數據都保存在葉子節點中
B+的搜索與B-樹也基本相同,區別是B+樹只有達到葉子結點才命中(B-樹可以在非葉子結點命中),其性能也等價於在關鍵字全集做一次二分查找;
B+Tree內節點去掉了data域,只做索引,減少了io次數,因此可以擁有更大的出度,擁有更好的性能。只利用索引快速定位數據索引范圍,先定位索引再通過索引高效快速定位數據。
B+樹和B樹的區別
為什么用B+樹作為數據庫索引,而不是平衡樹或者B樹?
- 為什么平衡二叉樹或者紅黑樹不適合作為索引
索引是存在於索引文件中,是存在於磁盤中的。因為索引通常是很大的,因此無法一次將全部索引加載到內存當中,因此每次只能從磁盤中讀取一個磁盤頁的數據到內存中。而這個磁盤的讀取的速度較內存中的讀取速度而言是差了好幾個級別。注意,我們說的平衡二叉樹結構,指的是邏輯結構上的平衡二叉樹,其物理實現是數組。由於在邏輯結構上相近的節點在物理結構上可能會差很遠。因此,每次讀取的磁盤頁的數據中有許多是用不上的。因此,查找過程中要進行許多次的磁盤讀取操作。而適合作為索引的結構應該是盡可能少的執行磁盤IO操作,因為執行磁盤IO操作非常的耗時。因此,平衡二叉樹並不適合作為索引結構。另外一點,平衡二叉樹和紅黑樹都是二叉樹,數據量較大的時候,樹的高度也比較高,因此會產生較多的磁盤IO。B樹的查詢,由於每層可以儲存多個節點,因此可以作磁盤預讀,將相關的數據一次性讀進內存里,因此他的查找主要發生在內存中,而平衡二叉樹的查詢,則是發生在磁盤讀取中。因此,雖然B樹查詢查詢的次數不比平衡二叉樹的次數少,但是相比起磁盤IO速度,內存中比較的耗時就可以忽略不計了。因此,B樹更適合作為索引。
- 為什么B+樹比B樹更適合作索引
B樹:有序數組+平衡多叉樹;
B+樹:有序數組鏈表+平衡多叉樹;
B+樹的關鍵字全部存放在葉子節點中,非葉子節點用來做索引,而葉子節點中有一個指針指向一下個葉子節點。做這個優化的目的是為了提高區間訪問的性能。而正是這個特性決定了B+樹更適合用來存儲外部數據。
B樹必須用中序遍歷的方法按序掃庫,而B+樹直接從葉子結點挨個掃一遍就完了,B+樹支持范圍查找非常方便,而B樹不支持。這是數據庫選用B+樹的最主要原因。 比如要查 5-10之間的,B+樹一次找到到5這個標記,再一次找到10這個標記,然后串起來就行了,B樹就非常麻煩。
舉個例子來對比。
B樹:比如說,我們要查找關鍵字范圍在3到7的關鍵字,在找到第一個符合條件的數字3后,訪問完第一個關鍵字所在的塊后,得遍歷這個B樹,獲取下一個塊,直到遇到一個不符合條件的關鍵字。遍歷的過程是比較復雜的。
B+樹:
相比之下,B+樹的基於范圍的查詢簡潔很多。由於葉子節點有指向下一個葉子節點的指針,因此從塊1到塊2的訪問,通過塊1指向塊2的指針即可。從塊2到塊3也是通過一個指針即可。
數據庫索引采用B+樹的主要原因是B樹在提高了磁盤IO性能的同時並沒有解決元素遍歷的效率低下的問題。正是為了解決這個問題,B+樹應運而生。B+樹只要遍歷葉子節點就可以實現整棵樹的遍歷。而且在數據庫中基於范圍的查詢是非常頻繁的,而B樹不支持這樣的操作(或者說效率太低)。
B+樹由於非葉子節點也有數據,這樣在做范圍查找的時候有可能某個范圍的數據散落在葉子節點和非葉子節點(各層),這樣就需要多次掃描B樹才能找全數據,會產生多次的磁盤IO;而B+樹所有的數據都在葉子節點,且葉子節點的數據前后用指針相連,只要通過索引找到范圍數據在葉子節點中的起始或結束位置,就可以順着葉子節點鏈表把這個范圍內的數據全都讀出來,避免了再次掃描,從而提高范圍查找的效率。
八大排序的特點
選擇排序、快速排序、希爾排序、堆排序不是穩定的排序算法(快選希堆),
冒泡排序、插入排序、歸並排序和基數排序是穩定的排序算法。
希爾排序,也稱遞減增量排序算法,是插入排序的一種更高效的改進版本。但希爾排序是非穩定排序算法。希爾排序的基本思想是:先將整個待排序的記錄序列分割成為若干子序列分別進行直接插入排序,待整個序列中的記錄“基本有序”時,再對全體記錄進行依次直接插入排序。
鏈表的倒數第K個節點(快排)
樹的深度優先遍歷和廣度優先遍歷
搜索方法有哪些,穩定性怎么樣
第六模塊--數據庫
Reids 支持的數據類型
Redis支持五種數據類型:
- string(字符串)
- hash(哈希)是一個鍵值(key=>value)對集合。
- list(列表)是簡單的字符串列表,按照插入順序排序
- set(集合),Set是string類型的無序集合。集合是通過哈希表實現的
- zset(sorted set:有序集合),是string類型元素的集合,且不允許重復的成員,不同的是每個元素都會關聯一個double類型的分數。redis正是通過分數來為集合中的成員進行從小到大的排序。
redis的替換策略
替換對象分為 allkeys 和 volatile,
替換方式分為LRU、random 和ttl
總的策略 = 2*3 = 6
LRU(Least recently used,最近最少使用)算法根據數據的歷史訪問記錄來進行淘汰數據,其核心思想是“如果數據最近被訪問過,那么將來被訪問的幾率也更高”。
騰訊一面后端問了LRU的算法原理,自己怎么實現LRU
- volatile-lru : 對具有生存周期的key(設置失效(expire set)的key)進行LRU算法置換.
- volatile-random : 對具有生存周期的key進行隨機置換.
- volatile-ttl : 對具有生存周期的key隨機進行抽樣, 置換出抽樣中生存周期最短的.
- allkeys-lru : 對整個db進行LRU算法置換.
- allkeys-random : 對整個db進行隨機置換.
- noeviction : 不進行置換.
Redis 持久化方法
redis提供兩種方式進行持久化:
- 一種是RDB(快照)持久化(原理是將Reids在內存中的數據庫記錄定時dump到磁盤上的RDB持久化)
- 另外一種是AOF(append only file)持久化(原理是將Reids的操作日志以追加的方式寫入文件)
RDB持久化是指在指定的時間間隔內將內存中的數據集快照寫入磁盤,實際操作過程是fork一個子進程,先將數據集寫入臨時文件,寫入成功后,再替換之前的文件,用二進制壓縮存儲。
AOF持久化以日志的形式記錄服務器所處理的每一個寫、刪除操作,查詢操作不會記錄,以文本的方式記錄,可以打開文件看到詳細的操作記錄。
什么時候用RDB什么時候用AOF:
備份、全量復制時使用RDB;需要實時備份的時候采用AOF
RDB的優點:
- RDB是一個緊湊壓縮的二進制文件, 代表Redis在某個時間點上的數據快照。 非常適用於備份, 全量復制等場景。 比如每6小時執行bgsave備份,並把RDB文件拷貝到遠程機器或者文件系統中(如hdfs) , 用於災難恢復。
- Redis加載RDB恢復數據遠遠快於AOF的方式。
RDB的缺點:·
- RDB方式數據沒辦法做到實時持久化/秒級持久化。 因為bgsave每次運行都要執行fork操作創建子進程, 屬於重量級操作, 頻繁執行成本過高。
- RDB文件使用特定二進制格式保存, Redis版本演進過程中有多個格式的RDB版本, 存在老版本Redis服務無法兼容新版RDB格式的問題。
AOF: 通過追加寫命令到文件實現持久化, 通過appendfsync參數可以控制實時/秒級持久化。 因為需要不斷追加寫命令, 所以AOF文件體積逐漸變大,
需要定期執行重寫操作來降低文件體積。 Redis執行AOF恢復數據遠遠慢於加載RDB的方式。
Redis其他知識:
1)Redis提供5種數據結構,每種數據結構都有多種內部編碼實現。
2)純內存存儲、IO多路復用技術、單線程架構是造就Redis高性能的三個因素。
3)由於Redis的單線程架構,所以需要每個命令能被快速執行完,否則會存在阻塞Redis的可能,理解Redis單線程命令處理機制是開發和運維Redis的核心之一。
4)批量操作(例如mget、mset、hmset等)能夠有效提高命令執行的效率,但要注意每次批量操作的個數和字節數。
5)了解每個命令的時間復雜度在開發中至關重要,例如在使用keys、hgetall、smembers、zrange等時間復雜度較高的命令時,需要考慮數據規模對於Redis的影響。
6)persist命令可以刪除任意類型鍵的過期時間,但是set命令也會刪除字符串類型鍵的過期時間,這在開發時容易被忽視。
7)move、dump+restore、migrate是Redis發展過程中三種遷移鍵的方式,其中move命令基本廢棄,migrate命令用原子性的方式實現了dump+restore,並且支持批量操作,是Redis Cluster實現水平擴容的重要工具。
8)scan命令可以解決keys命令可能帶來的阻塞問題,同時Redis還提供了hscan、sscan、zscan漸進式地遍歷hash、set、zset。
Redis應用場景:
1. 緩存功能
Redis作為緩存層,MySQL作為存儲層,絕大部分請求的熱點數據都從Redis中獲取。由於Redis具有支撐高並發的特性,所以緩存通常能起到加速讀寫和降低后端壓力的作用。
2. 限速
為了短信接口不被頻繁訪問,會限制用戶每分鍾獲取驗證碼的頻率,例如一分鍾不能超過5次。此功能可以使用Redis來實現,利用Redis可以設置鍵過期的功能,在用戶首次訪問時為其新建一個帶有過期時間的key(如60秒),在這個key過期之前,都不允許用戶再次申請短信驗證碼。
3. 消息隊列
Redis的lpush+brpop命令組合即可實現阻塞隊列,生產者客戶端使用lrpush從列表左側插入元素,多個消費者客戶端使用brpop命令阻塞式的“搶”列表尾部的元素,多個客戶端保證了消費的負載均衡和高可用性。
4. 用戶標簽系統
集合類型比較典型的使用場景是標簽(tag)。例如一個用戶可能對娛樂、體育比較感興趣,另一個用戶可能對歷史、新聞比較感興趣,這些興趣點就是標簽。可以通過redis set找出集合的交集並集等
5. 排行榜系統
有序集合比較典型的使用場景就是排行榜系統。例如視頻網站需要對用戶上傳的視頻做排行榜,榜單的維度可能是多個方面的:按照時間、按照播放數量、按照獲得的贊數。
https://www.cnblogs.com/davygeek/p/7995072.html
數據庫事物特性(acid):
數據庫事務必須具備ACID特性,ACID是Atomic原子性,Consistency一致性,Isolation隔離性,Durability持久性
MySQL的架構
- MySQL 主要分為 Server 層和引擎層,Server 層主要包括連接器、查詢緩存、分析器、優化器、執行器,同時還有一個日志模塊(binlog),這個日志模塊所有執行引擎都可以共用,redolog 只有 InnoDB 有。
連接器: 身份認證和權限相關(登錄 MySQL 的時候)。
查詢緩存: 執行查詢語句的時候,會先查詢緩存(MySQL 8.0 版本后移除,因為這個功能不太實用)。
分析器: 沒有命中緩存的話,SQL 語句就會經過分析器,分析器說白了就是要先看你的 SQL 語句要干嘛,再檢查你的 SQL 語句語法是否正確。
優化器: 按照 MySQL 認為最優的方案去執行。
執行器: 執行語句,然后從存儲引擎返回數據。
- 引擎層是插件式的,目前主要包括,MyISAM,InnoDB,Memory 等。
一條SQL語句的執行過程
SQL 等執行過程分為兩類
- 對於查詢等過程如下:權限校驗—》查詢緩存—》分析器—》優化器—》權限校驗—》執行器—》引擎
- 對於更新等語句執行流程如下:分析器----》權限校驗----》執行器—》引擎—redo log prepare—》binlog—》redo log commit
內連接、外連接 與 左連接、右連接
驅動表與被驅動表:
在兩表連接查詢中,驅動表只需要訪問一次,被驅動表可能被訪問多次。
內連接與外連接:
- 對於
內連接
的兩個表,驅動表中的記錄在被驅動表中找不到匹配的記錄,該記錄不會加入到最后的結果集,我們上邊提到的連接都是所謂的內連接
。- 對於
外連接
的兩個表,驅動表中的記錄即使在被驅動表中沒有匹配的記錄,也仍然需要加入到結果集。在MySQL
中,根據選取驅動表的不同,外連接仍然可以細分為2種:
- 左外連接 選取左側的表為驅動表
- 右外連接 選取右側的表為驅動表
過濾條件where 和 on:
WHERE
子句中的過濾條件
WHERE
子句中的過濾條件就是我們平時見的那種,不論是內連接還是外連接,凡是不符合WHERE
子句中的過濾條件的記錄都不會被加入最后的結果集。
ON
子句中的過濾條件對於外連接的驅動表的記錄來說,如果無法在被驅動表中找到匹配
ON
子句中的過濾條件的記錄,那么該記錄仍然會被加入到結果集中,對應的被驅動表記錄的各個字段使用NULL
值填充。需要注意的是,這個
ON
子句是專門為外連接驅動表中的記錄在被驅動表找不到匹配記錄時應不應該把該記錄加入結果集這個場景下提出的,所以如果把ON
子句放到內連接中,MySQL
會把它和WHERE
子句一樣對待,也就是說:內連接中的WHERE子句和ON子句是等價的。一般情況下,我們都把只涉及單表的過濾條件放到
WHERE
子句中,把涉及兩表的過濾條件都放到ON
子句中,我們也一般把放到ON
子句中的過濾條件也稱之為連接條件
左連接與右連接:
左連接和右連接是左外連接和右外連接簡稱, 只有外連接才分左右,且外連接一定會分左右。
可以據此來分辨內連接和外連接:
如果join語句前面有left或者right, 則一定是外連接,此時on的語義與where的語義不同;
如果join語句前面沒有left或者right, 則一定是內連接,此時on的語義與where相同。
標識內連接和外連接的關鍵字inner|cross 和 outer都是可以省略的。
什么是事務?
事務指的是邏輯上的一組操作,這組操作要么全部發生,要么全部失敗。
舉例 : 張三和李四 進行 轉賬的操作
張三向轉賬李四 1000元 張三余額-1000元 李四余額+1000元
不應該出現的是 在轉賬過程中由於一些意外,使張三的余額減去了1000元, 而李四並沒有收到這筆錢。 使用事務來進行管理。 必須一起成功或者一起失敗
事務四大特性(ACID)
1、原子性:事務包含的所有數據庫操作要么全部成功,要不全部失敗回滾
2、一致性:一個事務執行之前和執行之后都必須處於一致性狀態。拿轉賬來說,假設用戶A和用戶B兩者的錢加起來一共是5000,那么不管A和B之間如何轉賬,轉幾次賬,事務結束后兩個用戶的錢相加起來應該還得是5000,這就是事務的一致性。
3、隔離性:一個事務未提交的業務結果是否對於其它事務可見。級別一般有:read_uncommit,read_commit,read_repeatable,串行化訪問。
4、持久性:一個事務一旦被提交了,那么對數據庫中數據的改變就是永久性的,即便是在數據庫系統遇到故障的情況下也不會丟失提交事務的操作。
什么是臟數據,臟讀,不可重復讀,幻覺讀
1、臟數據所指的就是未提交的數據。。也就是說,一個事務正在對一條記錄做修改,在這個事務完成並提交之前,這條數據是處於待定狀態的(可能提交也可能回滾),這時,第二個事務來讀取這條沒有提交的數據,並據此做進一步的處理,就會產生未提交的數據依賴關系。一個事務中訪問到了另外一個事務未提交的數據,這種現象被稱為臟讀。
2、不可重復讀(Non-Repeatable Reads):一個事務先后讀取同一條記錄,而事務在兩次讀取之間該數據被其它事務所修改,則兩次讀取的數據不同,我們稱之為不可重復讀。所謂不可重復讀是指在一個事務內根據同一個條件對行記錄進行多次查詢,但是搜出來的結果卻不一致。發生不可重復讀的原因是在多次搜索期間查詢條件覆蓋的數據被其他事務修改了。
3、幻讀(Phantom Reads):一個事務按相同的查詢條件重新讀取以前檢索過的數據,卻發現其他事務插入了滿足其查詢條件的新數據,這種現象就稱為幻讀。
4、所謂幻讀是指同一個事務內多次查詢返回的結果集不一樣(比如增加了或者減少了行記錄)。比如同一個事務A內第一次查詢時候有n條記錄,但是第二次同等條件下查詢卻又n+1條記錄,這就好像產生了幻覺,為啥兩次結果不一樣那。其實和不可重復讀一樣,發生幻讀的原因也是另外一個事務新增或者刪除或者修改了第一個事務結果集里面的數據。不同在於不可重復讀是同一個記錄的數據內容被修改了,幻讀是數據行記錄變多了或者少了。
數據庫隔離級別:
數據庫事務的隔離級別有4個,由低到高依次為Read uncommitted 、Read committed 、Repeatable read 、Serializable ,這四個級別可以逐個解決臟讀 、不可重復讀 、幻讀 這幾類問題。
√: 可能出現 ×: 不會出現
|
臟讀 |
不可重復讀 |
幻讀 |
Read uncommitted |
√ |
√ |
√ |
Read committed |
× |
√ |
√ |
Repeatable read |
× |
× |
√ |
Serializable |
× |
× |
× |
當隔離級別設置為Read uncommitted 時,就可能出現臟讀;
當隔離級別設置為Read committed 時,避免了臟讀,但是可能會造成不可重復讀。大多數數據庫的默認級別就是Read committed,比如Sql Server , Oracle。
當隔離級別設置為Repeatable read 時,可以避免不可重復讀。
Mysql的默認隔離級別就是Repeatable read。
Serializable 是最高的事務隔離級別,同時代價也花費最高,性能很低,一般很少使用,在該級別下,事務順序執行,不僅可以避免臟讀、不可重復讀,還避免了幻讀。
MVCC的原理 | MySQL 的事務是怎樣實現的?
https://www.cnblogs.com/AnXinliang/p/9955331.html
1. 未提交讀隔離級別總是讀取最新的數據行,無需使用 MVCC;
2. 提交讀和可重復讀這兩種隔離級別, MySQL 的 InnoDB 存儲引擎用 多版本並發控制(Multi-Version Concurrency Control, MVCC) 實現;
3. 可串行化隔離級別需要對所有讀取的行都加鎖。
MVCC的原理
MVCC通過版本號、記錄隱藏的兩個列(創建版本號、刪除版本號)和Undo日志(通過回滾指針將所有的快照連接成版本鏈)
版本號
- 系統版本號:是一個遞增的數字,每開始一個新的事務,系統版本號就會自動遞增。
- 事務版本號:事務開始時的系統版本號。
隱藏的列
MVCC 在每行記錄后面都保存着兩個隱藏的列,用來存儲兩個版本號:
- 創建版本號:指示創建一個數據行的快照時的系統版本號;
- 刪除版本號:如果該快照的刪除版本號大於當前事務版本號表示該快照有效,否則表示該快照已經被刪除了。
Undo 日志
每次對記錄進行改動,都會記錄一條undo日志。MVCC 使用到的快照存儲在 Undo 日志中,該日志通過回滾指針把一個數據行(Record)的所有快照連接起來。
對於使用
READ COMMITTED
和REPEATABLE READ
隔離級別的事務來說,都必須保證讀到已經提交了的事務修改過的記錄,也就是說假如另一個事務已經修改了記錄但是尚未提交,是不能直接讀取最新版本的記錄的,核心問題就是:需要判斷一下版本鏈中的哪個版本是當前事務可見的。為此,InnoDB
提出了一個ReadView
的概念。還需要讀掘金小冊的相關部分
MySQL 主從復制的原理以及流程
復制基本原理流程
1. 主:binlog線程——記錄下所有改變了數據庫數據的語句,放進master上的 binlog中;2. 從:io線程——在使用start slave 之后,負責從master上拉取 binlog 內容,放進 自己的 relay log中;3. 從:sql執行線程——執行relay log中的語句;
解釋數據庫設計三大范式
為了建立冗余較小、結構合理的數據庫,設計數據庫時必須遵循一定的規則。在關系型數據庫中這種規則就稱為范式。
1.第一范式(確保每列保持原子性)
第一范式是最基本的范式。如果數據庫表中的所有字段值都是不可分解的原子值,就說明該數據庫表滿足了第一范式。
第一范式的合理遵循需要根據系統的實際需求來定。比如某些數據庫系統中需要用到“地址”這個屬性,本來直接將“地址”屬性設計成一個數據庫表的字段就行。但是如果系統經常會訪問“地址”屬性中的“城市”部分,那么就非要將“地址”這個屬性重新拆分為省份、城市、詳細地址等多個部分進行存儲,這樣在對地址中某一部分操作的時候將非常方便。
2.第二范式(確保表中的每列都和主鍵相關)
第二范式在第一范式的基礎之上更進一層。第二范式需要確保數據庫表中的每一列都和主鍵相關,而不能只與主鍵的某一部分相關(主要針對聯合主鍵而言)。也就是說在一個數據庫表中,一個表中只能保存一種數據,不可以把多種數據保存在同一張數據庫表中。
3.第三范式(確保每列都和主鍵列直接相關,而不是間接相關)
第三范式需要確保數據表中的每一列數據都和主鍵直接相關,而不能間接相關。
比如在設計一個訂單數據表的時候,可以將客戶編號作為一個外鍵和訂單表建立相應的關系。而不可以在訂單表中添加關於客戶其它信息(比如姓名、所屬公司等)的字段。
數據庫索引
索引是關系型數據庫中給數據庫表中一列或多列的值排序后的存儲結構,聚集索引以及非聚集索引用的是B+樹索引。
MySQL 索引類型有:唯一索引,主鍵(聚集)索引,非聚集索引,全文索引
聚集(clustered)索引,也叫聚簇索引,MySQL里主鍵就是聚集索引。
定義:數據行的物理順序與列值(一般是主鍵的那一列)的邏輯順序相同,一個表中只能擁有一個聚集索引。查詢方面,聚集索引的速度往往會更占優勢。
數據行的物理順序與列值的順序相同,如果我們查詢id比較靠后的數據,那么這行數據的地址在磁盤中的物理地址也會比較靠后。而且由於物理排列方式與聚集索引的順序相同,所以也就只能建立一個聚集索引了。
非聚集(unclustered)索引。
定義:該索引中索引的邏輯順序與磁盤上行的物理存儲順序不同,一個表中可以擁有多個非聚集索引。
非聚集索引的二次查詢問題
非聚集索引葉節點仍然是索引節點,只是有一個指針指向對應的數據塊,此如果使用非聚集索引查詢,而查詢列中包含了其他該索引沒有覆蓋的列,那么他還要進行第二次的查詢,查詢節點上對應的數據行的數據。
SQL語句查詢太慢怎么找原因、優化?
https://www.cnblogs.com/kubidemanong/p/10734045.html
分類:
1、大多數情況是正常的,只是偶爾會出現很慢的情況。
2、在數據量不變的情況下,這條SQL語句一直以來都執行的很慢。
偶發性:
a. 數據庫在刷新臟頁。例如 redo log 寫滿了需要同步到磁盤。
b. 拿不到鎖。要執行的這條語句涉及到的表,別人在用,並且加鎖了,我們拿不到鎖,只能慢慢等待別人釋放鎖。要判斷是否真的在等待鎖,我們可以用 show processlist這個命令來查看當前的狀態
經常性:
a. 沒用到索引
b. 索引失效 (可以用explain 看一下這條語句,查看possible-key字段看一下可能用到的索引,key字段查看實際用到的索引)
c. 數據庫索引選擇錯誤。MySQL在執行一條語句的時候會通過分析器估計各種查詢計划的查詢代價,然后在其中選擇代價最小的執行。如果分析器估計走索引的代價比較大,可能就放棄索引而走全表掃描。但分析器的代價估計有可能是不准確的(涉及到一個索引區分度的概念,這個區分度是用采樣來統計的,采樣會有偏差)。 解決方法, 可以強制走索引force index(a); 可以強制數據庫重新統計索引區分度: analyze table t.
可以解釋一下explain 一個查詢語句后的一些字段嗎?
一條查詢語句在經過MySQL
查詢優化器的各種基於成本和規則的優化會后生成一個所謂的執行計划。
explain 就是用來查看一條查詢語句的執行計划的。
查詢計划由如下幾個關鍵的字段:
id : 在一個大的查詢語句中每個SELECT
關鍵字都對應一個唯一的id
table: 該查詢語句涉及的表名
type: 針對單表訪問的方法 (有 const[主鍵或者唯一二級索引列與常數進行等值匹配]、ref[普通的二級索引列與常量進行等值匹配]、all[全表掃描]、index[可以使用索引覆蓋,但需要掃描全部的索引記錄]等)。
possible_key:該查詢可能用到的索引。
key : 該查詢實際用到的索引。
rows: 執行該查詢計划需要掃描的行數。(如果查詢優化器決定使用全表掃描的方式對某個表執行查詢時,執行計划的rows
列就代表預計需要掃描的行數,如果使用索引來執行查詢時,執行計划的rows
列就代表預計掃描的索引記錄行數)
如何查看執行計划的執行成本?
在EXPLAIN
單詞和真正的查詢語句中間加上FORMAT=JSON
。
這樣我們就可以得到一個json
格式的執行計划,里邊兒包含該計划花費的成本
聯合索引和單列索引的區別是什么?
https://www.cnblogs.com/greatLong/articles/11573588.html
聯合索引本質:
是對多個列建立一個索引,稱為聯合索引。當創建(a,b,c)聯合索引時,相當於創建了(a)單列索引,(a,b)聯合索引以及(a,b,c)聯合索引。所以只有查詢條件滿足a 或者 (a,b) 或者 (a,b,c)的時候,才會用上聯合索引。這就是所謂的最左匹配原則。意思是 以最左邊的為起點任何連續的索引都能匹配上。
單列索引只是對一個字段建立索引。
MySQL索引失效的情況有哪些?
1.如果條件中有or,假如or連接的兩個查詢條件字段中有一個沒有索引的話,引擎會放棄索引而產生全表掃描。
2.對於多列索引,不是使用的第一部分(第一個),則不會使用索引
3.like查詢是以%開頭(以%結尾的情況可以使用)
4. 查詢時,采用is null條件時,不能利用到索引,只能全表掃描
5. 如果列類型是字符串,那一定要在條件中將數據使用引號引用起來,否則不使用索引
6.如果mysql估計使用全表掃描要比使用索引快,則不使用索引
7. 查詢條件使用函數在索引列上,或者對索引列進行運算,運算包括(+,-,*,/,! 等) 錯誤的例子:select * from test where id-1=9; 正確的例子:select * from test where id=10;
8. 使用IN 關鍵字進行范圍查找,有可能不走索引(IN 的 范圍里面有超過200個單點區間的時候會放棄索引,因為這個時候優化器對這200個單點區間作成本估計的成本很高。MySQL 5.7.3之前這個單點區間的上限是10, 5.7.3之后上限是200)
如何提高數據庫查詢效率
a. 對查詢進行優化,應盡量避免全表掃描,首先應考慮在 where 及 order by 涉及的列上建立索引;
b. 應盡量避免在 where 子句中對字段進行 null 值判斷,否則將導致引擎放棄使用索引而進行全表掃描;
c. 索引並不是越多越好,索引固然可以提高相應的 select 的效率,但同時也降低了 insert 及 update 的效率,因為 insert 或 update 時有可能會重建索引,所以怎樣建索引需要慎重考慮,視具體情況而定。
d. 應盡量避免在 where 子句中使用!=或<>操作符,否則將引擎放棄使用索引而進行全表掃描
數據庫可以有幾個聚集索引
一個,主鍵是聚集索引,數據行的物理順序與列值(一般是主鍵的那一列)的邏輯順序相同
哪些引擎支持聚集索引
一是主索引的區別,InnoDB的數據文件本身就是索引文件。而MyISAM的索引和數據是分開的。
二是輔助索引的區別:InnoDB的輔助索引data域存儲相應記錄主鍵的值而不是地址。而MyISAM的輔助索引和主索引沒有多大區別。
簡單的SQL語句(更新)
如何創建索引
MySQL中innodb表主鍵設計原則
InnoDB主鍵設計的原則:
1. 一定要顯式定義主鍵
2. 采用與業務無關的單獨列
3. 采用自增列
4. 數據類型采用int,並盡可能小,能用tinyint就不用int,能用int就不用bigint
5. 將主鍵放在表的第一列
這樣設計的原因:
1. 在innodb引擎中只能有一個聚集索引,我們知道,聚集索引的葉子節點上直接存有行數據,所以聚集索引列盡量不要更改,而innodb表在有主鍵時會自動將主鍵設為聚集索引,如果不顯式定義主鍵,會選第一個沒有null值的唯一索引作為聚集索引,唯一索引涉及到的列內容難免被修改引發存儲碎片且可能不是遞增關系,存取效率低,所以最好顯式定義主鍵且采用與業務無關的列以避免修改;如果這個條件也不符合,就會自動添加一個不可見不可引用的6byte大小的rowid作為聚集索引
2. 需采用自增列來使數據順序插入,新增數據順序插入到當前索引的后面,符合葉子節點的分裂順序,性能較高;若不用自增列,數據的插入近似於隨機,插入時需要插入到現在索引頁的某個中間位置,需要移動數據,造成大量的數據碎片,索引結構松散,性能很差
3. 在主鍵插入時,會判斷是否有重復值,所以盡量采用較小的數據類型,以減小比對長度提高性能,且可以減小存儲需求,磁盤占用小,進而減少磁盤IO和內存占用;而且主鍵存儲占用小,普通索引的占用也相應較小,減少占用,減少IO,且存儲索引的頁中能包含較多的數據,減少頁的分裂,提高效率
自己的項目中:取三維模型的名稱+描述+標簽組成的字符串,用MD5或者SHA1計算哈希碼作為主鍵。哈希碰撞的概率極小。或者直接使用自增的序號作為主鍵,索引的效率比較高,但是也存在兩個問題: 1. 如果要從庫中刪除一條記錄,會引起整個索引的重新計算;2. 表中的其他列不能和主鍵直接相關,不符合數據庫設計的第三范式;
數據庫主鍵和外鍵
數據庫
InnoDB, MyISAM和Memory, 默認InnoDB, 支持事務;memory完全在內存中,除了大小限制還有斷電丟失;
- redis的hash算法用的是啥
一致性哈希算法
- nosql為啥比sql快
nosql不需要滿足sql關系數據庫數據一致性等復雜特性,非關系型一般是緩存數據庫,數據加載到內存中自然更快。redis是單線程執行的,任務都放到隊列中。
- 常見關系型數據庫:
Oracle、Microsoft Access、MySQL
- 常見非關系型數據庫:
MongoDb、redis、HBase
- 關系型數據庫和非關系型數據庫的區別與聯系:
非關系型數據庫中,我們查詢一條數據,結果出來一個數組;關系型數據庫中,查詢一條數據結果是一個對象。
數據庫 類型 |
特性 |
優點 |
缺點 |
關系型數據庫 SQLite、Oracle、mysql |
1、關系型數據庫,是指采用了關系模型來組織 數據的數據庫; 2、關系型數據庫的最大特點就是事務的一致性; 3、簡單來說,關系模型指的就是二維表格模型, 而一個關系型數據庫就是由二維表及其之間的聯系所組成的一個數據組織。 |
1、容易理解:二維表結構是非常貼近邏輯世界一個概念,關系模型相對網狀、層次等其他模型來說更容易理解; 2、使用方便:通用的SQL語言使得操作關系型數據庫非常方便; 3、易於維護:豐富的完整性(實體完整性、參照完整性和用戶定義的完整性)大大減低了數據冗余和數據不一致的概率; 4、支持SQL,可用於復雜的查詢。 |
1、為了維護一致性所付出的巨大代價就是其讀寫性能比較差; 2、固定的表結構; 3、高並發讀寫需求; 4、海量數據的高效率讀寫; |
非關系型數據庫 MongoDb、redis、HBase |
1、使用鍵值對存儲數據; 2、分布式; 3、一般不支持ACID特性; 4、非關系型數據庫嚴格上不是一種數據庫,應該是一種數據結構化存儲方法的集合。 |
1、無需經過sql層的解析,讀寫性能很高; 2、基於鍵值對,數據沒有耦合性,容易擴展; 3、存儲數據的格式:nosql的存儲格式是key,value形式、文檔形式、圖片形式等等,文檔形式、圖片形式等等,而關系型數據庫則只支持基礎類型。 |
1、不提供sql支持,學習和使用成本較高; 2、無事務處理,附加功能bi和報表等支持也不好; |
注1:數據庫事務必須具備ACID特性,ACID是Atomic原子性,Consistency一致性,Isolation隔離性,Durability持久性。
注2:數據的持久存儲,尤其是海量數據的持久存儲,還是需要一種關系數據庫。
Myisam 和innodb的區別
- MyISAM:它是基於傳統的ISAM類型,ISAM是Indexed Sequential Access Method (有索引的順序訪問方法) 的縮寫,它是存儲記錄和文件的標准方法。不是事務安全的,而且不支持外鍵,如果執行大量的select,insert MyISAM比較適合。
- InnoDB:支持事務安全的引擎,支持外鍵、行鎖、事務是他的最大特點。如果有大量的update和insert,建議使用InnoDB,特別是針對多個並發和QPS較高的情況。
-
MyISAM索引實現:MyISAM索引文件和數據文件是分離的,索引文件僅保存數據記錄的地址。
- 在InnoDB中,表數據文件本身就是按B+Tree組織的一個索引結構,這棵樹的葉節點data域保存了完整的數據記錄。這個索引的key是數據表的主鍵,因此InnoDB表數據文件本身就是主索引。
第七模塊 高並發與分布式
什么是負載均衡?有哪些常用的負載均衡策略?
負載均衡
由一個獨立的統一入口來收斂流量(接收請求),再由做二次分發的過程就是「負載均衡」。
負載均衡實現方法的分類
根據實現技術不同,可分為DNS負載均衡,HTTP負載均衡,IP負載均衡,反向代理負載均衡、鏈路層負載均衡等。
HTTP重定向負載均衡
當用戶向服務器發起請求時,請求首先被集群調度者截獲;調度者根據某種分配策略,選擇一台服務器,並將選中的服務器的IP地址封裝在HTTP響應消息頭部的Location字段中,並將響應消息的狀態碼設為302,最后將這個響應消息返回給瀏覽器。
當瀏覽器收到響應消息后,解析Location字段,並向該URL發起請求,然后指定的服務器處理該用戶的請求,最后將結果返回給用戶。
缺點:
調度服務器只在客戶端第一次向網站發起請求的時候起作用。當調度服務器向瀏覽器返回響應信息后,客戶端此后的操作都基於新的URL進行的(也就是后端服務器),此后瀏覽器就不會與調度服務器產生關系。
- 由於不同用戶的訪問時間、訪問頁面深度有所不同,從而每個用戶對各自的后端服務器所造成的壓力也不同。而調度服務器在調度時,無法知道當前用戶將會對服務器造成多大的壓力,因此這種方式無法實現真正意義上的負載均衡,只不過是把請求次數平均分配給每台服務器罷了。
- 若分配給該用戶的后端服務器出現故障,並且如果頁面被瀏覽器緩存,那么當用戶再次訪問網站時,請求都會發給出現故障的服務器,從而導致訪問失敗。做不到高可用。
DNS負載均衡
當用戶向我們的域名發起請求時,DNS服務器會自動地根據我們事先設定好的調度策略選一個合適的IP返回給用戶,用戶再向該IP發起請求。它的作用與HTTP重定向類似。問題是DNS會緩存IP,如果某一IP不可用(如機器故障)會導致部分用戶無法正常訪問,此時可以用動態DNS解決。
反向代理負載均衡
用戶發來的請求都首先要經過反向代理服務器,服務器根據用戶的請求要么直接將結果返回給用戶,要么將請求交給后端服務器處理,再返回給用戶。
優點:
- 隱藏后端服務器。與HTTP重定向相比,反向代理能夠隱藏后端服務器,所有瀏覽器都不會與后端服務器直接交互,從而能夠確保調度者的控制權,提升集群的整體性能。
- 故障轉移。與DNS負載均衡相比,反向代理能夠更快速地移除故障結點。當監控程序發現某一后端服務器出現故障時,能夠及時通知反向代理服務器,並立即將其刪除。
- 合理分配任務 。HTTP重定向和DNS負載均衡都無法實現真正意義上的負載均衡,也就是調度服務器無法根據后端服務器的實際負載情況分配任務。但反向代理服務器支持手動設定每台后端服務器的權重。我們可以根據服務器的配置設置不同的權重,權重的不同會導致被調度者選中的概率的不同。
- 像安全防護、故障轉移等都是反向代理才有的好處。
缺點:
- 調度者壓力過大 。由於所有的請求都先由反向代理服務器處理,那么當請求量超過調度服務器的最大負載時,調度服務器的吞吐率降低會直接降低集群的整體性能。
- 制約擴展。當后端服務器也無法滿足巨大的吞吐量時,就需要增加后端服務器的數量,可沒辦法無限量地增加,因為會受到調度服務器的最大吞吐量的制約。
負載均衡的作用
1.解決並發壓力,提高應用處理性能(增加吞吐量,加強網絡處理能力);
2.提供故障轉移,實現高可用;
3.通過添加或減少服務器數量,提供網站伸縮性(擴展性);
4.安全防護;(負載均衡設備上做一些過濾,黑白名單等處理)
常見的負載均衡策略
1. 輪詢
2. 加權輪詢
3. 最少連接次數
4. 最快響應
5. 源地址Hash法
第七模塊--算法題
LRU原理及其實現
LRU(Least recently used,最近最少使用)算法根據數據的歷史訪問記錄來進行淘汰數據,其核心思想是“如果數據最近被訪問過,那么將來被訪問的幾率也更高”。
實現
最常見的實現是使用一個鏈表保存緩存數據,詳細算法實現如下:
1. 新數據插入到鏈表頭部;
2. 每當緩存命中(即緩存數據被訪問),則將數據移到鏈表頭部;
3. 當鏈表滿的時候,將鏈表尾部的數據丟棄。
分析
【命中率】
當存在熱點數據時,LRU的效率很好,但偶發性的、周期性的批量操作會導致LRU命中率急劇下降,緩存污染情況比較嚴重。
【復雜度】
實現簡單。
【代價】
命中時需要遍歷鏈表,找到命中的數據塊索引,然后需要將數據移到頭部。
證明一個數是2的N次方
(value & (value -1)) == 0;
Top K 問題
找出一個大數組里面前K個最大數,如1億個數字中找出最大或最小的前100個數字(考慮判重和內存空間限制)?
如果不判重,可以將前100個數字做成最大堆或最小堆的數據結構(找最大的Top100用最小堆, 找最小的Top100用最大堆), 然后依次遍歷所有數字, 符合條件時替換根節點后並重新構建堆。堆的缺點是允許有重復數字!!!
如果要判重,則創建一個100個空間的空數組, 遍歷1億個數字依次插入值(按照升序), 使用二分查找算法判斷新值在數組中的位置並插入, 該數組最多容納100個值。 當有101個值時先判重, 如果重復繼續向后遍歷, 如果值大於數組最小值則插入到指定位置,數組第一個元素移出數組, 因為數組是連續的,所有可以用內存拷貝方式賦值,只有新插入的值要單獨賦值到對應的下標(原理類似於android.util.SparseArray)。 因內存拷貝可認為不占用時間, 該思路的總會時間復雜度是O(1億*log100), log100是二分查找的復雜度。
熱詞統計問題
搜索引擎會通過日志文件把用戶每次檢索使用的所有檢索串都記錄下來,每個查詢串的長度為1-255字節。假設目前有一千萬個記錄(這些查詢串的重復度比較高,雖然總數是1千萬,但如果除去重復后,不超過3百萬個。一個查詢串的重復度越高,說明查詢它的用戶越多,也就是越熱門。),請你統計最熱門的10個查詢串,要求使用的內存不能超過1G。
如果只有一台設備:
第一步:Query統計 (統計出每個Query出現的次數) Query統計有以下兩個個方法
1、直接排序法
首先我們最先想到的的算法就是排序了,首先對這個日志里面的所有Query都進行排序,然后再遍歷排好序的Query,統計每個Query出現的次數了。
但是題目中有明確要求,那就是內存不能超過1G,一千萬條記錄,每條記錄是255Byte,很顯然要占據2.375G內存,這個條件就不滿足要求了。
讓我們回憶一下數據結構課程上的內容,當數據量比較大而且內存無法裝下的時候,我們可以采用外排序的方法來進行排序,這里我們可以采用歸並排序,因為歸並排序有一個比較好的時間復雜度O(NlgN)。排完序之后我們再對已經有序的Query文件進行遍歷,統計每個Query出現的次數,再次寫入文件中。綜合分析一下,排序的時間復雜度是O(NlgN),而遍歷的時間復雜度是O(N),因此該算法的總體時間復雜度就是O(N+NlgN)=O(NlgN)。
2、Hash Table法 (這種方法統計字符串出現的次數非常好) 在第1個方法中,我們采用了排序的辦法來統計每個Query出現的次數,時間復雜度是NlgN,那么能不能有更好的方法來存儲,而時間復雜度更低呢?
題目中說明了,雖然有一千萬個Query,但是由於重復度比較高,因此事實上只有300萬的Query,每個Query 255Byte,因此我們可以考慮把他們都放進內存中去,而現在只是需要一個合適的數據結構,在這里,Hash Table絕對是我們優先的選擇,因為Hash Table的查詢速度非常的快,幾乎是O(1)的時間復雜度。
那么,我們的算法就有了:
維護一個Key為Query字串,Value為該Query出現次數的HashTable,每次讀取一個Query,如果該字串不在Table中,那么加入該字串,並且將Value值設為1;如果該字串在Table中,那么將該字串的計數加一即可。最終我們在O(N)的時間復雜度內完成了對該海量數據的處理。
本方法相比算法1:在時間復雜度上提高了一個數量級,為O(N),但不僅僅是時間復雜度上的優化,該方法只需要IO數據文件一次,而算法1的IO次數較多的,因此該算法2比算法1在工程上有更好的可操作性。
第二步:找出Top 10 (找出出現次數最多的10個)
算法一:普通排序 (我們只用找出top10,所以全部排序有冗余) 我想對於排序算法大家都已經不陌生了,這里不在贅述,我們要注意的是排序算法的時間復雜度是NlgN,在本題目中,三百萬條記錄,用1G內存是可以存下的。
算法二:部分排序 題目要求是求出Top 10,因此我們沒有必要對所有的Query都進行排序,我們只需要維護一個10個大小的數組,初始化放入10個Query,按照每個Query的統計次數由大到小排序,然后遍歷這300萬條記錄,每讀一條記錄就和數組最后一個Query對比,如果小於這個Query,那么繼續遍歷,否則,將數組中最后一條數據淘汰(還是要放在合適的位置,保持有序),加入當前的Query。最后當所有的數據都遍歷完畢之后,那么這個數組中的10個Query便是我們要找的Top10了。
不難分析出,這樣,算法的最壞時間復雜度是N*K, 其中K是指top多少。
算法三:堆在算法二中,我們已經將時間復雜度由NlogN優化到N*K,不得不說這是一個比較大的改進了,可是有沒有更好的辦法呢?
分析一下,在算法二中,每次比較完成之后,需要的操作復雜度都是K,因為要把元素插入到一個線性表之中,而且采用的是順序比較。這里我們注意一下,該數組是有序的,一次我們每次查找的時候可以采用二分的方法查找,這樣操作的復雜度就降到了logK,可是,隨之而來的問題就是數據移動,因為移動數據次數增多了。不過,這個算法還是比算法二有了改進。
基於以上的分析,我們想想,有沒有一種既能快速查找,又能快速移動元素的數據結構呢?
回答是肯定的,那就是堆。借助堆結構,我們可以在log量級的時間內查找和調整/移動。因此到這里,我們的算法可以改進為這樣,維護一個K(該題目中是10)大小的小根堆,然后遍歷300萬的Query,分別和根元素進行對比。思想與上述算法二一致,只是在算法三,我們采用了最小堆這種數據結構代替數組,把查找目標元素的時間復雜度有O(K)降到了O(logK)。那么這樣,采用堆數據結構,算法三,最終的時間復雜度就降到了N*logK,和算法二相比,又有了比較大的改進。
10億個數中找出最大的10000個數(top K問題)
先拿10000個數建堆,然后一次添加剩余元素,如果大於堆頂的數(10000中最小的),將這個數替換堆頂,並調整結構使之仍然是一個最小堆,這樣,遍歷完后,堆中的10000個數就是所需的最大的10000個。建堆時間復雜度是O(mlogm),算法的時間復雜度為O(nmlogm)(n為10億,m為10000)。
針對top K類問題,通常比較好的方案是分治+Trie樹/hash+小頂堆(就是上面提到的最小堆),即先將數據集按照Hash方法分解成多個小數據集,然后使用Trie樹活着Hash統計每個小數據集中的query詞頻,之后用小頂堆求出每個數據集中出現頻率最高的前K個數,最后在所有top K中求出最終的top K。
---------------------
總結:
至此,算法就完全結束了,經過上述第一步、先用Hash表統計每個Query出現的次數,O(N);然后第二步、采用堆數據結構找出Top 10,N*O(logK)。所以,我們最終的時間復雜度是:O(N) + N'*O(logK)。(N為1000萬,N’為300萬)。
優化的方法:可以把所有10億個數據分組存放,比如分別放在1000個文件中。這樣處理就可以分別在每個文件的10^6個數據中找出最大的10000個數,合並到一起在再找出最終的結果。
如果有多台設備:
可參考Map-Reduce 的思想
核心是“分治”、“歸並”和哈希, 第一次遍歷將關鍵詞散列到不同的文件中(散列算法是性能關鍵,哈希函數的性能直接影響散列的結果, 盡量避免“數據傾斜”), 同一個關鍵詞一定會散列到同一個文件, 理想情況是所有關鍵詞均勻散列到不同的文件中(即分治思想,將大文件分解為小問題)。
讀取每個文件並記錄各關鍵詞的次數, 做個排序, 從每個文件中排序出前100的關鍵詞;
取第一個文件的記錄結果, 和第二個文件做“合並”, 即200個結果中排序出前100個關鍵詞, 然后依次合並第三個、第四個。。。。第N個文件(將子結果合並為總結果)
第八模塊--JVM
JVM執行模式
主流的JVM是Oracle的HotSpot JVM, 采用解釋和編譯混合執行的模式,JIT技術采用分層編譯,極大的提高了Java的執行速度。
三種執行模式:
1. 解釋執行
2.JIT編譯執行
3.JIT編譯與解釋混合執行
混合執行的優勢在於解釋器在啟動時先解釋執行,省去編譯時間。隨着時間推進,JVM通過熱點代碼統計分析,識別高頻的代碼,基於強大的JIT動態編譯技術,將熱點代碼轉換成機器碼,直接交給CPU執行。JIT的作用是將Java字節碼動態地編譯成可以直接發送給處理器指令執行地機器碼。
內存布局:
程序計數器(Program Counter Register):線程私有的,記錄當前線程的行號指示器,為線程的切換提供保障
虛擬機棧(JVM Stacks)//本地方法棧: 線程私有的,主要存放局部變量表,操作數棧,動態鏈接和方法出口等;
堆區(Heap): 堆是所有線程共享的,主要用來存儲對象。其中,堆可分為:年輕代和老年代兩塊區域。使用NewRatio參數來設定比例。對於年輕代,一個Eden區和兩個Suvivor區,使用參數SuvivorRatio來設定大小。
元數據區 (Metaspace): JDK8才有,保存JDK8之前永久代中的類的元信息
本地方法棧(Native Method Stacks)
永久代和方法區的區別
HotSpot使用永久代來實現方法區。永久代是HotSpot對方法區的實現,方法區是Java虛擬機規范中的定義,是一種規范。而永久代是一種實現,一個是標准一個是實現。在Java虛擬機(JVM)內部,class文件中包括類的版本、字段、方法、接口等描述信息,還有運行時常量池,用於存放編譯器生成的各種字面量和符號引用。在過去(自定義類加載器還不是很常見的時候),類大多是”static”的,很少被卸載或收集,因此被稱為“永久的(Permanent)”
對於Java8, HotSpots取消了永久代,那么是不是也就沒有方法區了呢?當然不是,方法區是一個規范,規范沒變,它就一直在。那么取代永久代的就是元空間。
永久代和元空間的區別
1. 存儲位置不同,永久代物理上是堆的一部分,和新生代,老年代地址是連續的,而元空間屬於本地內存;
2. 存儲內容不同,元空間存儲類的元信息,靜態變量和常量池等並入堆中。相當於永久代的數據被分到了堆和元空間中。
https://blog.csdn.net/u011635492/article/details/81046174
為什么使用 元空間+堆 取代 永久代
1. 字符串存在永久代中,容易出現性能問題和內存溢出。
2. 類及方法的信息等比較難確定其大小,因此對於永久代的大小指定比較困難,太小容易出現永久代溢出,太大則容易導致老年代溢出。永久代的垃圾收集是和老年代(old generation)捆綁在一起的,因此無論誰滿了,都會觸發永久代和老年代的垃圾收集。
3. 永久代會為 GC 帶來不必要的復雜度,並且回收效率偏低
什么時候會發生堆溢出什么時候會發生棧溢出?
1. 棧溢出
棧是線程私有的,他的生命周期與線程相同,每個方法在執行的時候都會創建一個棧幀,用來存儲局部變量表,操作數棧,動態鏈接,方法出口燈信息。局部變量表又包含基本數據類型,對象引用類型(局部變量表編譯器完成,運行期間不會變化)
棧溢出就是方法執行是創建的棧幀超過了棧的深度。那么最有可能的就是方法遞歸調用產生棧溢出。
2. 堆溢出
heap space表示堆空間,堆中主要存儲的是對象。如果不斷的new對象則會導致堆中的空間溢出。
3. 永久代溢出(OutOfMemoryError: PermGen space)
永久代物理上是堆的一部分,和新生代,老年代地址是連續的,永久代溢出的表現就是堆溢出。(永久代的GC是和老年代(old generation)捆綁在一起的,因此無論誰滿了,都會觸發永久代和老年代的垃圾收集。)
由於JDK7、8移除永久帶,所以只有JDK1.6及以下會出現永久帶溢出的現象
堆區(Heap):
堆區儲存着幾乎所有的實例對象,堆由垃圾收集器自動回收,堆區由各子線程共享使用,可以通過-Xms設置最小堆容量, 和-Xmx來設置最大堆容量。
堆分成兩大塊:新生代和老年代。
對象產生之初在新生代,步入暮年時進入老年代,但是老年代也接納在新生代無法容納的超大對象。
新生代=1個Eden區+2個Survior區。絕大部分對象在Eden區生成,當Eden區裝填滿的時候,會觸發Young Garbage Collection,即YGC。垃圾回收的時候,在Eden區實現清除策略,沒有被引用的對象則直接回收。依然存活的對象會被移送到Suvivor區。Suvivor區分為S0和S1兩塊內存空間,送到哪塊空間呢?每次YGC的時候,它們將存活的對象復制到未使用的那塊空間,然后將當前正在使用的空間完全清除,交換兩塊空間的使用狀態。如果YGC要移送的對象大於Survivor區容量的上限,則直接移交給老年代。如果老年代也無法放下,則會觸發Full Garbage Collection, FGC。 如果依然無法放下,則拋出OOM。
假如一些沒有進取心的對象以為可以一直在新生代的Survivor區交換來交換去,那就錯了。每個對象都有一個計數器,每次YGC都會加1。 -XXiMax Tenuring Threshold參數能配置計數器的值到達某個閾值的時候,對象從新生代晉升至老年代。如果該參數配置為1,那么從新生代的Eden區直接移至老年代。默認值是15,可以在Survivor區交換14次之后,晉升至老年代。
JVM Stack(虛擬機棧):
棧(Stack)是一個先進后出的數據結構。
JVM中的虛擬機棧是描述Java方法執行的內在區域,它是線程私有的。棧中的元素用於支持虛擬機進行方法調用,每個方法從開始調用到執行完成的過程,就是棧幀從入棧到出棧的過程。在活動線程中,只有位於棧頂的幀才是有效的,稱為當前棧幀。正在執行的方法稱為當前方法,棧幀是方法運行的基本結構。
在執行引擎運行時,所有指令都只能針對當前棧施進行操作。而StackOverflowError表示請求的棧溢出,導致內存耗盡,通常出現在遞歸方法中。
虛擬機棧通過壓棧和出棧的方式,對每個方法對應的活動棧幀進行運算處理,方法正常執行結束,肯定會跳轉到另一個棧幀上。在執行的過程中,如果出現異常,會進行常回溯,返回地址通過異常處理表確定。棧幀在整個JVM體系中的地位頗高,包括局部變量表、操作棧、動態連接、方法返回地址等。
(1)局部變量表
局部變量表是存放方法參數和局部變量的區域。
(2)操作棧
操作棧是一個初始狀態為空的桶式結構棧。在方法執行過程中,會有各種指令往棧中寫入和提取信息。
(3) 動態連接
每個棧幀中包含一個在常量池中對當前方法的引用,目的是支持方法調用過程的動態連接。
(4) 方法返回地址
方法執行遇到退出情況,將返回至方法當前被調用的位置
Native Method Stacks(本地方法棧)
本地方法棧在JVM內存布局中,也是線程對象私有的,但虛擬機“主內”,本地方法棧“主外”。這個“內外”是針對JVM來說的,線程調用本地方法時,會進入一個不再受JVM約束的世界。本地方法棧可以通過JNI(Java Native Interface)來訪問虛擬機運行時的數據區,甚至可以調用寄存器,具有和JVM相同的能力和權限。最著名的本地方法應該是System.currentTimeMillis(), JNI使Java深度使用操作系統的特性功能,復用非Java代碼。
Program Counter Register(程序計數寄存器)
在程序計數寄存器(Program Counter Register,PC)中,Register的命名源於CPU的寄存器,CPU只有把數據裝載到寄存器才能夠運行。寄存器存儲指令相關的現場信息,由於CPU時間片輪限制,眾多線程在並發執行過程中,任何一個確定的時刻,一個處理器或者多核處理器中的一個內核,只會執行某個線程中的一條指令。這樣必然導致經常中斷或恢復,如何保證分毫無差呢?每個線程在創建后,都會產生自己的程序計數器和棧幀,程序計數器用來存放執行指令的偏移量和行號指示器等,線程執行或恢復都要依賴程序計數器。程序計數器在各個線程之間互不影響,此區域也不會發生內存溢出異常。
最后,從線程共享的角度來看,堆和元空間是所有線程共享的,而虛擬機棧、本地方法棧、程序計數器都是線程內部私有的。從這個角度看一下Java的內存結構:
GC(垃圾回收)
-
GC如何判斷對象是否可以回收
為了判斷對象是否存活,JVM 引入了GC Roots。如果一個對象與GC Roots之間沒有直接或間接的引用關系,比如某個失去任何引用的對象,或者兩個互相環島狀循環引用的對象等,判決這些對象“死緩”,是可以被回收的。可以作為GC Roots對象可以是:類靜態屬性中引用的對象、常量引用的對象、虛擬機棧中引用的對象、本地方法棧中引用的對象等。
-
垃圾回收算法
有了判斷對象是否存活的標准后,再了解一下垃圾回收的相關算法。
- “標記--清除算法”,該算法會從每個GC Roots出發,依次標記有引用關系的對象,最后將沒有被標記的對象清除。但是這種算法會帶來大量的空間碎片,導致需要分配一個較大連續空間時容易觸發FGC。典型的例子是CMS回收器。
- “標記--整理算法”,該算法類似計算機的磁盤整理,首先會從GC Roots出發標記存活的對象,然后將存活對象整理到內存空間的一端,形成連續的已使用空間,最后把已使用空間之外的部分全部清理掉,這樣就不會產生空間碎片的問題。典型的例子是老年代的FullGC, G1回收器。
- 復制算法,為了能夠並行地標記和整理將空間分為兩塊,每次只激活其中一塊,垃圾回收時只需把存活的對象復制到另一塊未激活空間上,將未激活空間標記為已激活,將已激活空間標記為未激活,然后清除原空間中的原對象。堆內存空間分為較大的Eden和兩塊較小的Survivor,每次只使用Eden和Survivor區的一塊。這種情形下的“Mark-Copy”減少了內存空間的浪費。典型的例子是年輕代的minGC。
垃圾回收器(Garbage Collector)
是實現垃圾回收算法並應用在JVM環境中的內存管理模塊。當前實現的垃圾回收器有數十種,常見的有Serial、CMS、G1三種。
- Serial回收器是一個主要應用於YGC的垃圾回收器,采用串行單線程的方式完成GC任務,其中“Stop The World”簡稱STW,即垃圾回收的某個階段會暫停整個應用程序的執行。FGC的時間相對較長,頻繁FGC會嚴重影響應用程序的性能
- CMS回收器(Concurrent Mark Sweep Collector)是回收停頓時間比較短、目前比較常用的垃圾回收器。它通過初始標記(Initial Mark)、並發標記(ConcurrentMark)、重新標記(Remark)、並發清除(Concurrent Sweep)四個步驟完成垃圾回收工作。第1、3步的初始標記和重新標記階段依然會引發STW,而第2、4步的並發標記和並發清除兩個階段可以和應用程序並發執行,也是比較耗時的操作,但並不影響應用程序的正常執行。由於CMS采用的是“標記一清除算法”,因此產生大量的空間碎片。為了解決這個問題,CMS可以通過配置-XX:+UseCMSCompactAtFullCo lection參數,強制JVM在FGC完成后對老年代進行壓縮,執行一次空間碎片整理,但是空間碎片整理階段也會引發STW。為了減少STW次數,CMS還可以通過配置一XX:+CMSFullGCsBeforeCompaction=n參數,在執行了n次FGC后,JVM再在老年代執行空間碎片整理。
- G1回收器 是HotSpot在JDK7中推出的新一代G1垃圾回收器,在JDK11中,G1是默認的回收器。G1采用的是”Mark-Copy“,有較好的空間整理能力,不會產生大量空間碎片。G1的優勢是具有可預測的停頓時間,能夠盡快在指定時間完成垃圾回收任務。
- 另外還有在JDK11中引入的實驗性的ZGC,是一個可伸縮的低延遲垃圾收集器。
怎樣分析查找OOM的原因
- 使用jmap將當前的內存 Dump成一個 hprof格式的文件,MAT 讀取這個文件后會給出方便閱讀的信息,配合它的查找,對比功能,就可以定位內存泄漏的原因。
- 用的最多的功能是 Histogram,它按類名將所有的實例對象列出來,可以點擊表頭進行排序,在表的第一行可以輸入正則表達式來匹配結果
舉例一個典型的分析內存泄漏的過程:
1. 使用 Heap查看當前堆大小為 23.00M
2. 添加一個頁后堆大小變為 23.40M
3. 將添加的一個頁刪除,堆大小為 23.40M
4. 多次操作,結果仍相似,說明添加/刪除頁存在內存泄漏 (也應注意排除其它因素的影響)
5. Dump 出操作前后的 hprof 文件 (1.hprof,2.hprof),用 mat打開,並得到 histgram結果
6. 使用 HomePage字段過濾 histgram結果,並列出該類的對象實例列表,看到兩個表中的對象集合大小不同,操作后比操作前多出一個 HomePage,說明確實存在泄漏
7. 將兩個列表進行對比,找出多出的一個對象,用查找 GC Root的方法找出是誰串起了這條引用線路,定位結束
PS :
· 很多時候堆增大是 Bitmap引起的,Bitmap在 Histogram中的類型是 byte [],對比兩個 Histogram中的 byte[] 對象就可以找出哪些 Bitmap有差異
· 多使用排序功能,對找出差異很有用
2 內存泄漏的原因分析
總結出來只有一條: 存在無效的引用!
良好的模塊設計以及合理使用設計模式有助於解決此問題。
參考:
https://blog.csdn.net/liao0801_123/article/details/82900874
https://www.cnblogs.com/lovecindywang/p/10800593.html
http://blog.sina.com.cn/s/blog_73b4b91f0102wze4.html
內存溢出的原因及解決方法
1. 內存溢出原因:
1.內存中加載的數據量過於龐大,如一次從數據庫取出過多數據;
2.集合類中有對對象的引用,使用完后未清空,使得JVM不能回收;
3.代碼中存在死循環或循環產生過多重復的對象實體;
4.使用的第三方軟件中的BUG;
5.啟動參數內存值設定的過小
2. 內存溢出的原因及解決方法:
- 修改JVM啟動參數,直接增加內存。(-Xms,-Xmx參數一定不要忘記加。)
- 檢查錯誤日志,查看“OutOfMemory”錯誤前是否有其 它異常或錯誤。
- 對代碼進行走查和分析,找出可能發生內存溢出的位置。
- 使用內存查看工具動態查看內存使用情況
對代碼分析找出可能發生內存溢出的位置, 可能出現的幾種情況:
1、檢查對數據庫查詢中,是否有一次獲得全部數據的查詢。一般來說,如果一次取十萬條記錄到內存,就可能引起內存溢出。這個問題比較隱蔽,在上線前,數據 庫中數據較少,不容易出問題,上線后,數據庫中數據多了,一次查詢就有可能引起內存溢出。因此對於數據庫查詢盡量采用分頁的方式查詢。
2、檢查代碼中是否有死循環或遞歸調用。
3、檢查是否有大循環重復產生新對象實體。
4、檢查List、MAP等集合對象是否有使用完后,未清除的問題。List、MAP等集合對象會始終存有對對象的引用,使得這些對象不能被GC回收。
第九模塊--個人
項目總結
關於三維模型搜索引擎項目相關度排序算法是怎么做的:
以文字搜模型:
基於Lucene文本搜索引擎,查找最匹配的;
以圖片搜模型:
計算圖片特征,對圖片特征計算HashCode, 搜索的時候匹配HashCode;
以模型搜模型:
計算模型的特征得到n維特征矩陣, 對特征矩陣計算HashCode, 搜索的時候匹配HashCode;
去重和檢測url有效性是怎么做的:
對外網數據去重:
一開始直接使用條件逐個字段比較判斷是否重復;
后來對關鍵字段連接建立聯合哈希值保存,用這個哈希值去重;
后來想到其實外網的url是唯一的,直接對url建立哈希值來去重;后來設想直接用url哈希之后作為主鍵保存,建立聚集索引;
有效性檢測比較簡單:
使用java.net 下的類來實現,主要用到了 URL和HttpURLConnection :
剛開始使用openStream()方法,這樣使用倒是可以,但是速度慢;
最后使用了getResponseCode()方法,可以得到請求的響應狀態,該方法返回一個 int 分別是 200 and 404 如無法從響應中識別任何代碼則返回 -1, 如果對該url發起的5次請求都沒有應答則認為鏈接失效;
你的項目用了哪些技術?
Lucene, Solr
MySQL,
Redis,
Java多線程
遇到過什么問題?你是怎么解決的?
去重的過程經歷了多次迭代:
剛開始直接對記錄逐個字段比較判斷是否重復 ——>然后對關鍵字段建立HashCode作為標識,對比該Hash字段——>再是對外網URL建立HashCode對比;
有什么可以改進的地方?
可以對URL使用布隆過濾器做去重;(位圖+多個哈希函數)
使用緩存數據庫來提高並發訪問;(緩存穿透(查詢一個數據庫一定不存在的數據),緩存擊穿(一個key非常熱點,在不停的扛着大並發,大並發集中對這一個點進行訪問,當這個key在失效的瞬間,持續的大並發就穿破緩存),緩存雪崩(緩存集中過期失效))
使用Elesticsearch來替代Solr(Elasticsearch是分布式的, 不需要其他組件,分發是實時的;solr需要結合依賴其他分布式組件來實現分布式);
第九模塊--待整理問題
阿里:
一面
全局唯一有序ID:
Snowflake, timestamp, 機器id等
馮諾伊曼體系:
shell命令的執行體系:
信息熵:
程序運行中的棧式結構
TCP/IP, TCP傳輸層加端口號,IP網絡層加ip地址;路由器主要工作在IP網絡層
各層常見的協議有哪些
同步與阻塞
並行與並發
java線程的本質、內核線程與用戶進程,線程調度,並行級別
內核態與用戶態,中斷
CPU與內存與硬盤
緩存行與偽共享
內存分配管理,段業式 jemalloc
二面:
java程序的運行原理:
緩存行與偽共享
一個線程忙碌,多個線程閑置怎么解決
Java多線程引發的性能問題以及調優策略
多重繼承會帶來哪些問題
單點登陸
正向代理與反向代理
反爬機制,爬蟲模擬瀏覽器行為
cglib方法攔截
動態代理
依賴注入
servlet的本質
TCP長連接 心跳包 websocket
Netty 百萬級長連接優化
DSL解析到AST
JVM 相關(gc的源碼)
代碼規范,包命名規范
現在流行的線程調度算法是什么(時間片輪轉法)
項目用到了數據庫,談談對事物的理解
假設你要做一個銀行app,有可能碰到多個人同時向一個賬戶打錢的情況,有可能碰到什么問題,如何解決(還有可能出現重復提交的問題,保證服務的冪等性)
排序算法
給定一個文件名,如何在d盤找出這個文件
java對象頭
知道哪些排序算法
快排怎么實現
堆排怎么實現
找出兩個有序數組中的相同元素
常用集合框架
介紹下hashtable
快排如何實現
一個集合里有1000萬個隨機元素,快速求和(多線程)
排他鎖的改進策略
map怎么實現
紅黑樹有什么特性
快排的思路講一下
給大量的qq號,怎么排序(數據庫外排),問算法時間復雜度
代碼:
數組里搜索第K大的數,非遞歸二分查找,鏈表相加