1、HashMap
- 數組的存儲方式, 需要指定下標,那麼下標的來源就是將key進行hashcode,數值過大,然後進行取模,如"周瑜"的hashcode爲699636,699636%8,8爲數組的長度, 就是要獲取的下標,而真正使用的是&與操作完成,即hash&(table.length-1)。其中的hash算法進行了大量的異或和右移操作,是由於採用的是table.length-1的與操作,那麼基本上算出的都是低位與的結果,而高位沒有影響。
- 但是hashcode是隨機的,所以如果出現了hashcode值相同,就出現了哈希衝突。解決該問題的方式,一種是再次進行hashcode,即再散列法;另一種方式就是使用鏈表,而一個新的節點到來時,直接將新節點的next指針指向head節點,然後head指針就重新指向新節點。
- 構造器中的容量參數,會進行2的冪次方比較,如傳入的爲15,那麼容量就是16;這麼做的目的是在獲取hash值時使用的是與操作,性能要比%好一些。
- 在插入新節點的時候,會進行是否數組擴容的操作,當前存儲的元素的個數大於閾值(容量*加載因子),同時當前要插入的節點找到的索引位置不爲null的時候,纔會進行擴容。擴容的時候,會創建新的2倍數組。循環數組每一個元素,然後針對元素所在的鏈表進行循環,找到新的hash位置,放置到新的數組上去。
- 重寫equals方法的時候一定要重寫hashcode方法,因爲hashmap在獲取元素的時候,判斷hash的同時也判斷了equals方法。
- 1.8版本的加入了紅黑樹,是爲了解決鏈表過長,查詢效率低的問題。
2、Redis
- 單線程原因:使用單線程 -- 多路複用IO模型來實現高性能的內存服務。
- 緩存穿透,查詢一個一定不存在的數據,由於緩存是不命中時需要從數據庫中查詢,查不到數據則不寫入緩存,就導致這個不存在的數據每次都要到數據庫中去查詢。解決辦法,是對空值進行緩存,只是將緩存時間設置的比較短。
- 緩存擊穿,在緩存正好失效的情況下,高併發的請求過來,都去查詢了數據庫,需要使用鎖機制來解決。在查詢時,緩存中有數據直接返回,沒有數據,先上鎖,此處加入再次查詢緩存的代碼,然後去數據庫查詢數據,放入到緩存中,然後解鎖。
1、查詢緩存 redis.get
2、緩存有數據,直接返回 cache != null
3、緩存沒有數據 cache == null
4、上鎖 redis.lock
5、查詢緩存 redis.get
6、第一個線程:緩存沒有數據
7、第二個線程在第一個線程解鎖之後,緩存有數據,直接返回
7、第一個線程去查詢數據庫,放入緩存,解鎖
- 緩存和數據庫一致性,第一種情況,先寫入數據庫,然後刪除緩存,如果刪除緩存失敗,造成數據庫數據是新的,而緩存數據是舊的。第二中情況,先刪除緩存,然後寫入數據庫,如果寫入數據庫失敗,頂多用戶讀取的是舊的數據,數據還是一致的。多線程情況下出現問題:
- 持久化方式,有RDB和AOF兩種方式,RDB:當達到一定條件的時候,將內存中的整個數據全部寫到磁盤存儲,整個過程redis服務器內部需要將緩存的數據進行格式化處理,壓縮最後緩存,這是比較耗時的,同時也會佔用服務器內部資源,最重要的是快照不是實時操作,中間有時間間隔,這就意味着如果服務器宕機,需要恢復數據是不完整的。爲了解決這個問題,可以將用戶的操作指令記錄並保存,如果需要進行數據恢復,則會通過操作指令一步步進行數據還原,就是AOF。
- 分佈式鎖,併發編程中,我們一般使用鎖來避免由於競爭而造成的數據不一致問題,但是隻能保證在同一個JVM進程中執行。
3、Java 內存模型
- 和cpu緩存模型類似,是基於CPU緩存模型建立的。每個線程操作的都是自己的工作內存,也就是操作的是共享變量副本。其他線程是感知不到當前線程共享變量副本的變化的。操作的過程:從主內存中read出來,然後load到工作內存,然後從工作內存中讀取變量來進行計算,修改之後,assign賦值寫到工作內存中,此時該共享變量在主內存中沒有發生變化,加入volatile關鍵字之後,將工作內存的修改後的值store到主內存,然後write到主內存的共享變量中,將主內存中該共享變量lock加鎖,標示爲線程獨佔狀態,採用的是總線加鎖的方式,但是性能太低,後面採用的是MESI緩存一致性協議來解決。
- MESI緩存一致性協議:多個CPU從主內存讀取同一個數據到各自的高速緩存,當某一個cpu修改了緩存的數據,該數據會立馬同步到主內存,其他CPU通過總線嗅探機制可以感知到數據的變化,從而將自己緩存裏的數據失效。
- volatile是輕量級的同步機制,保證可見性,不保證原子性(num++,可以使用Atomic開頭的類),禁止指令重排。
4、AQS原理
- park+自旋,沒有競爭到鎖時掛起,其實就是將該線程放入到一個等待隊列中,一旦鎖釋放的時候,就去該隊列中取出等待的線程,此時那個等待的線程在while循環中,可以去競爭鎖,如果拿到了就執行其他邏輯,拿不到繼續掛起;
- 使用AQS需要子類去重寫tryAcquire和tryRelease方法。
- AQS:同步器,獲取鎖調用的是acquire方法,該方法中調用的tryAcquire方法需要子類去重寫,如果tryAcquire成功了,說明搶到鎖了,失敗了,通過for循環進行CAS操作,將新結點加入到tail結點(此處有初始化頭結點操作);然後獲取到前驅結點,如果爲head,那麼就去執行tryAcquire方法,失敗的話去判斷前一個結點的狀態(此處有初始化頭結點狀態的操作),爲SINGAL就掛起(使用的是LockSupport類),並返回中斷狀態,如果爲CANCEL就一直往前找到不是該狀態的前驅節點。
- 狀態值:CANCELD 1、初始狀態 0、SIGNAL -1、CONDITION -2、PROPAGATE -3,也就是說獨佔模式下只有一個結點會處於SINGAL狀態。
final boolean acquireQueued(final Node node, int arg) {
boolean failed = true;
try {
boolean interrupted = false;
for (;;) {
final Node p = node.predecessor();
if (p == head && tryAcquire(arg)) {
setHead(node);
p.next = null; // help GC
failed = false;
return interrupted;
}
if (shouldParkAfterFailedAcquire(p, node) &&
parkAndCheckInterrupt())
interrupted = true;
}
} finally {
if (failed)
cancelAcquire(node);
}
}
- ReentrantLock:非公平鎖的情況下,會先去執行CAS搶鎖,失敗的話去判斷state狀態,state=0,嘗試CAS搶鎖,state!=0,判斷持有線程是不是自己,是自己再次加鎖;公平鎖的情況下,直接去判斷state狀態,state=0,先去判斷隊列有沒有其他等待的線程,有的話,去排隊,沒有的話,嘗試CAS搶鎖,state!=0,判斷持有線程是不是自己,是自己再次加鎖。
1、非公平鎖
final void lock() {
if (compareAndSetState(0, 1))
setExclusiveOwnerThread(Thread.currentThread());
else
acquire(1);
}
final boolean nonfairTryAcquire(int acquires) {
final Thread current = Thread.currentThread();
int c = getState();
if (c == 0) {
if (compareAndSetState(0, acquires)) {
setExclusiveOwnerThread(current);
return true;
}
}
else if (current == getExclusiveOwnerThread()) {
int nextc = c + acquires;
if (nextc < 0) // overflow
throw new Error("Maximum lock count exceeded");
setState(nextc);
return true;
}
return false;
}
2、公平鎖
final void lock() {
acquire(1);
}
protected final boolean tryAcquire(int acquires) {
final Thread current = Thread.currentThread();
int c = getState();
if (c == 0) {
if (!hasQueuedPredecessors() &&
compareAndSetState(0, acquires)) {
setExclusiveOwnerThread(current);
return true;
}
}
else if (current == getExclusiveOwnerThread()) {
int nextc = c + acquires;
if (nextc < 0)
throw new Error("Maximum lock count exceeded");
setState(nextc);
return true;
}
return false;
}
5、Redis命令
- 常用的存儲方式:string、hash、set、list、zset
- string
1、單值緩存
SET key value
GET key
2、對象緩存
SET user:1 value(json格式數據)
//優點:可以獲取單個屬性值,比較靈活,修改起來也很容易。
MSET user:1:name admin user:1:balance 100
MGET user:1:name user:1:balance
3、分佈式鎖
服務器1線程:SETNX product:10001 true //返回1代表獲取鎖成功
服務器2線程:SETNX product:10001 true //返回0代表獲取鎖失敗
DEL product:10001 //執行完業務邏輯釋放鎖
SET product:10001 true ex 10 nx //放置程序意外終止導致死鎖
4、計數器,可以解決併發問題
INCR article:1000:readcount
GET article:1000:readcount
5、session共享,同一個war包部署在不同的服務器上
spring session + redis實現session共享
- hash
1、對象緩存,key field value
HMSET user 1:name admin 1:balance 100
HMGET user 1:name 1:balance
2、購物車
添加商品:hset cart:1001 10080 1
增加數量:hincrby cart:1001 10080 1
商品總數:hlen cart:1001
刪除商品:hdel cart:1001 10080
獲取購物車所有商品:hgetall cart:1001
- list
1、命令
LPUSH key value
RPUSH key value
LPOP key
RPOP key
LRANGE key start end
BLPOP key timeout
BRPOP key timeout
2、數據結構
棧:LPUSH + LPOP
隊列:LPUSH + RPOP
阻塞隊列:LPUSH + BRPOP
3、微博和公衆號消息流
A發微博,消息ID爲10018
LPUSH msg:1001 10018
B發微博,消息ID爲20018
LPUSH msg:1001 20018
查看最新微博消息
LRANGE msg:1001 0 5
- set
1、微信抽獎小程序
點擊參與抽獎用戶加入集合
SADD activity:1001 111
查看參與抽獎的所有用戶
SMEMBERS activity:1001
開始抽獎
SRANDMEMBER activity:1001 2 //選出來的數據不刪除
SPOP activity:1001 2 //選出來的數據刪除
6、消息中間件
- 重試機制:如果消費者程序業務邏輯部分出現了異常時,會自動實現補償機制,也就是重試機制,默認一直重試到不出現異常爲止。自動簽收的功能其實就是rabbitmq在底層使用aop進行攔截,沒有異常自動提交事務,有異常的話實現補償機制。重試機制不會出現併發情況,都是在前一次重試的結果上進行時間間隔的。
- 重試場景:消費者獲取到消息後,調用第三方接口的時候,但是該接口暫時無法訪問,需要重試機制,可以通過http請求的返回碼是不是200來判斷,不是直接拋出異常,將由rabbitmq開始重試機制;但是如果拋出異常,不需要進行重試,應該採用日誌記錄+人工進行補償。
- 重複消費:使用rabbitmq的全局性ID方式,爲每一個消息加一個唯一性ID,然後在消費方根據返回的消息是否有ID來判斷是否是重複消費。
- 好處:解耦系統之間調用;將消息寫入消息隊列,非必要的業務邏輯以異步的方式運行,加快響應速度;併發量大的時候,可以通過消息隊列進行削峯。
- 應用場景:日誌記錄,將不同級別的日誌通過topic機制發送到exchange中。
- 項目中是怎麼用消息隊列的:
- 1、爲什麼使用消息隊列?使用消息隊列有哪些優點和缺點?kafka、activemq、rabbitmq、rocketmq都有什麼優點和缺點:
- 結合項目來說明,
- 如何保證消息隊列的高可用
- 如何保證消息不被重複消費和冪等性
- 如何保證消息的可靠性傳輸,丟失了消息怎麼辦
- 如何保證消息的順序性
- 如何解決消息隊列的延時和過期失效問題,消息隊列滿了之後怎麼辦
- 如何讓你寫一個消息隊列,該如何進行架構設計
7、RPC冪等性
- 如果客戶 端調用服務端接口超時的話,會採用重試機制,可能會造成服務端出現重複消費。
- 人爲的form表單提交也會出現重複消費。
- 解決辦法:調用接口前,傳遞一個全局性的ID,服務器消費前先根據ID判斷是否有處理過該請求。將該ID存儲在redis中,處理完邏輯之後,將該ID從redis中刪除。
public class TokenUtils{
public Boolean getToken(String token){
String redisToken = redisUtils.getString(token);
if(StringUtils.isEmpty(redisToken)){
return Boolean.FALSE;
}
//redis是單線程的
boolean delKey = redisUtils.delKey(redisToken);
if(!delKey){
System.out.println("已經被其他請求刪除");
}
return delKey;
}
}
- 但是業務邏輯處理失敗的時候,會造成後續的再次提交也被攔截。可以使用aop方式進行異常捕捉,如果業務邏輯出現異常,可以將token重新放置到redis中。
7、推送
- 短輪詢:不斷地間隔去請求服務器,缺陷是佔用了服務器的資源,數據響應不及時。好處是簡單,服務端不需要改造。
- 長輪詢:基於Http長連接,無須在瀏覽器安裝插件的服務器推送技術,如Servlet3中的異步任務和Spring的DeferedResult。相比短輪詢,只是改造了實時性問題。還有一種:Server-Sent-Event(SSE)。
- websocket協議:Html5中的協議,實現客戶端與服務端的雙向,基於消息的文本或二進制數據通信。適用於對數據實時性要求較高的場景,後端需要單獨實現,並不是所有的瀏覽器都支持。
- websocket建立的時候,是發送的http協議。
8、BIO、NIO
- 阻塞IO:讀寫過程中會發生阻塞現象,用戶線程在發出IO請求之後,會去查看數據是否準備就緒,沒有的話就會阻塞,然後讓出CPU。典型的是socket的read方法。
- 非阻塞IO:當用戶線程發出一個IO請求之後,馬上會得到一個結果(可能是準備好,也可能是沒有準備好),需要用戶線程不斷的詢問內核數據是否準備就緒,不會讓出CPU,缺陷是CPU佔用率非常高。
- 多路複用IO:會有一個線程不斷地去輪詢多個socket的狀態,只有當socket真正的有讀寫事件時,才真正的調用實際的IO操作,如socket的read操作。在Java NIO中是使用selector.select()方法去查詢每個通道是否有到達的事件。而輪詢每個socket的狀態是在內核進行的,效率較高,這樣在單線程的情況下可以同時處理多個客戶端請求。
- 異步IO:當用戶線程發起IO請求之後,立刻可以去做其他的事情,然後當數據準備好時,內核會給用戶線程一個信號,告訴它IO操作完成了。
- 服務端建立ServerSocket,監聽某一個端口,然後阻塞接受客戶端的連接,客戶端建立Socket,連接到服務端的端口,發送數據。阻塞的情況會出現在accept和read兩處,所以不支持併發操作。
- 爲了解決上述問題,爲每一個socket開啓一個獨立的線程,也就是需要藉助多線程來支持高併發,缺陷是浪費服務器資源。
- NIO的設計初衷是使用單線程來處理併發,類似redis的單線程處理併發。方式就是將accept和read兩處的阻塞都變成非阻塞。
package com.vim;
import java.net.InetSocketAddress;
import java.net.SocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.ServerSocketChannel;
import java.nio.channels.SocketChannel;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.TimeUnit;
public class App {
public static List<SocketChannel> socketChannelList = new ArrayList<>();
private static ByteBuffer byteBuffer = ByteBuffer.allocate(1024);
public static void main( String[] args ) throws Exception {
try{
//解決了accept阻塞的問題
ServerSocketChannel serverSocket = ServerSocketChannel.open();
SocketAddress socketAddress = new InetSocketAddress("127.0.0.1", 8888);
serverSocket.bind(socketAddress);
serverSocket.configureBlocking(false);
while (true){
//輪詢判斷是否有數據
for(SocketChannel channel:socketChannelList){
int read = channel.read(byteBuffer);
if(read > 0){
}else if(read == -1){
socketChannelList.remove(channel);
}
}
SocketChannel accept = serverSocket.accept();
if(accept != null){
//解決了read阻塞的問題
accept.configureBlocking(false);
socketChannelList.add(accept);
}
}
}catch (Exception e){
e.printStackTrace();
}
}
}
- 以上新加入的api解決了阻塞的問題,但是瓶頸主要在兩個問題:for循環可以交給內核去執行,所以出現了selector選擇器。
- tomcat使用的是線程池的方式,每來一個請求都會分配一個單獨的線程去處理。所以BIO也可以使用,只是不適合適用長連接的場景,如果大部分都是短連接的話,可以使用BIO+線程池的方式去處理。
- NIO:channel+buffer+selector
package com.vim;
import java.net.InetSocketAddress;
import java.net.SocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.SelectionKey;
import java.nio.channels.Selector;
import java.nio.channels.ServerSocketChannel;
import java.nio.channels.SocketChannel;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;
import java.util.concurrent.TimeUnit;
public class App {
public static List<SocketChannel> socketChannelList = new ArrayList<>();
private static ByteBuffer byteBuffer = ByteBuffer.allocate(1024);
public static void main( String[] args ) throws Exception {
try{
//解決了accept阻塞的問題
ServerSocketChannel serverSocket = ServerSocketChannel.open();
SocketAddress socketAddress = new InetSocketAddress("127.0.0.1", 8888);
serverSocket.bind(socketAddress);
serverSocket.configureBlocking(false);
//獲取選擇器
Selector selector = Selector.open();
serverSocket.register(selector, SelectionKey.OP_ACCEPT);
while (true){
selector.select(1000);
Iterator<SelectionKey> iterator = selector.selectedKeys().iterator();
while (iterator.hasNext()){
SelectionKey result = iterator.next();
iterator.remove();
if(result.isAcceptable()){
SocketChannel socketChannel = serverSocket.accept();
socketChannel.configureBlocking(false);
socketChannel.register(selector, SelectionKey.OP_READ);
}else if(result.isReadable()){
SocketChannel socketChannel = (SocketChannel) result.channel();
socketChannel.configureBlocking(false);
//取消監聽,此處交給線程池去處理
//...線程池代碼,在其中代碼的finally中要重新將該socketChannel註冊到selector上的OP_READ
result.cancel();
}
}
}
}catch (Exception e){
e.printStackTrace();
}
}
}
- 上述代碼中while中循環,相當於netty中的bossGroup,線程池,相當於netty中的workGroup。即Reactor中的多線程模型,一個線程接收連接,一個線程處理IO讀寫事件。
- Netty是一個高性能、異步事件驅動的NIO框架。提供了對TCP、UDP、文件傳輸的支持。其所有的IO操作都是異步非阻塞的。
- TCP粘包:只有TCP會產生粘包的現象,而UDP不會產生,是因爲TCP是基於流的協議,而UDP是基於數據包的協議。
9、數據結構 -- 樹
- 每個結點有0個或多個子結點;沒有父結點的結點爲根結點;每個非根結點有且只有一個父結點
- 結點的度:結點擁有的子樹的個數,二叉樹的度不大於3
- 葉子結點:度爲0的結點
- 兄弟結點:擁有共同父結點的結點
- 樹的深度:樹中結點的最大層次
- 二叉樹:每個結點最多有兩個子樹的樹結構。
package com.vim;
public class DoubleTree {
//根結點
private Node root;
//添加結點
public void add(int value){
Node newNode = new Node(value);
if(root == null){
root = newNode;
}else{
Node temp = root;
while (true){
if(value < temp.getValue()){
//當前結點有沒有左孩子
if(temp.getLeft() == null){
temp.setLeft(newNode);
break;
}else{
//向左邊移動
temp = temp.getLeft();
}
}else{
//當前結點有沒有右孩子
if(temp.getRight() == null){
temp.setRight(newNode);
break;
}else{
//向右邊移動
temp = temp.getRight();
}
}
}
}
}
public void showNode(Node node){
//前序遍歷
// System.out.println(node.getValue());
if(null != node.getLeft()){
showNode(node.getLeft());
}
//中序遍歷
System.out.println(node.getValue());
if(null != node.getRight()){
showNode(node.getRight());
}
//後序遍歷
// System.out.println(node.getValue());
}
public static void main(String[] args) {
DoubleTree tree = new DoubleTree();
tree.add(4);
tree.add(1);
tree.add(9);
tree.add(6);
tree.add(0);
tree.add(3);
tree.add(8);
tree.showNode(tree.root);
}
}
10、排序
- 冒泡排序
package com.vim;
public class BubbleSort {
public static void main(String[] args) {
int[] arr = {3, 7, 4, 2, 6, 1};
//排序一趟,會把最大值放到數組的最後面
for(int j=arr.length; j>1; j--){
//比較相鄰的兩個數字,只要左邊的比右邊的大,就進行交換
for(int i=0; i<j-1; i++){
if(arr[i] > arr[i+1]){
int temp = arr[i];
arr[i] = arr[i+1];
arr[i+1] = temp;
}
}
}
for(int i=0; i<arr.length; i++){
System.out.println(arr[i]);
}
}
}
- 選擇排序
package com.vim;
public class SelectSort {
public static void main(String[] args) {
//在每le一次數組中找到最大的數據,然後和最後的數進行交換
int[] arr = {3, 7, 4, 2, 6, 1};
for(int j=arr.length; j>1; j--){
//找到最大值所在的索引
int max = 0;
for(int i=1; i<j; i++){
if(arr[i] > arr[max]){
max = i;
}
}
//交換
int temp = arr[max];
arr[max] = arr[j-1];
arr[j-1] = temp;
}
for(int i=0; i<arr.length; i++){
System.out.println(arr[i]);
}
}
}
- 插入排序
package com.vim;
public class InsertSort {
public static void main(String[] args) {
//將數字不斷地插入到已經排好序的數組中,從最後一個元素開始和數字比較,如果大於數字,就往前差
int[] arr = {3, 7, 4, 2, 6, 1};
for(int j=1; j<arr.length; j++){
//要插入的元素
int insertVal = arr[j];
//要插入的位置
int index = j-1;
while (index >= 0 && insertVal < arr[index]){
//將元素後移
arr[index+1] = arr[index];
//繼續往前判斷
index--;
}
arr[index+1] = insertVal;
}
for(int i=0; i<arr.length; i++){
System.out.println(arr[i]);
}
}
}
- 堆排序
package com.vim;
public class Tree {
public static void main(String[] args) {
int[] arr = {0,9,4,7,2,1,8,6,3,5};
//1、從下往上,兒子中比出最大的值,然後將這個值與父親比較,這個值比父親大,就與父親交換,稱爲建立最大堆。
//2、有多少個父結點,每個父結點的索引以及子結點的索引
//父結點的個數 =(數組長度-1)/2
//每個父結點索引 = 0 到 父結點個數-1
//左兒子索引 = 父結點索引*2+1,右兒子索引 = 父結點索引*2+2
//建立最大堆的時候,可以確定從下往上循環的次數
int end = arr.length;
while (end > 1){
//建立最大堆,遍歷所有的父結點(從最後一個父結點索引開始遍歷)
int parentLength = (end-1)/2;
for(int i=parentLength-1; i>=0 ;i--){
//默認左兒子最大,因爲可能出現某個結點沒有右兒子的情況
int maxIndex = i*2+1;
if((maxIndex+1 < end) && arr[maxIndex+1] > arr[maxIndex]){
//最大的是右兒子
maxIndex++;
}
//最大的兒子和父結點進行比較交換
if(arr[maxIndex] > arr[i]){
int temp = arr[maxIndex];
arr[maxIndex] = arr[i];
arr[i] = temp;
}
}
//根結點數據與最後一個結點進行交換
int temp = arr[0];
arr[0] = arr[end-1];
arr[end-1] = temp;
//每循環一次,最後一個數的位置向前移動一位
end--;
}
for(int j=0; j<arr.length; j++){
System.out.println(arr[j]);
}
}
}
11、線程池
- 在線程數目到達corePoolSize之前,來的請求會馬上創建新的線程去處理; 當線程數目達到corePoolSize後,就會把任務加入到緩存隊列中,當緩存隊列Queue滿了之後,就會創建新的線程去處理任務,一直增加到maxmiumPoolSize,當Queue滿了並且線程數目達到了maxmiumPoolSize之後,就會執行拒絕策略。
- keepAliveTime:當線程數目超過corePoolSize之後,線程的空閒時間達到keepAliveTime時,多餘的線程會被銷燬直到剩下corePoolSize個線程爲止。
- 拒絕策略:AbortPolicy默認策略是拋出RejectedExecutionException異常;DiscardPolicy是指直接丟棄任務,不做任何處理也不拋出異常;DiscardOldestPolicy拋棄隊列中等待最久的那個任務,然後把新任務放入到隊列中;CallerRunsPolicy將請求交給調用者去執行。
- Executors提供的幾個方法,存在的問題:FixedThreadPool和SingleThreadPool允許的請求隊列長度最大爲Integet.MAX_VALUE,可能會堆積大量請求。CachedThreadPool和ScheduledThreadPool允許創建的線程數量爲Integer.MAX_VALUE,可能會創建大量的線程。
package com.vim.modules.web.controller;
import java.util.concurrent.Executors;
import java.util.concurrent.LinkedBlockingQueue;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;
public class Test {
public static void main(String[] args) {
ThreadPoolExecutor executor = new ThreadPoolExecutor(
2,
5,
1L,
TimeUnit.SECONDS,
new LinkedBlockingQueue<>(3),
Executors.defaultThreadFactory(),
new ThreadPoolExecutor.AbortPolicy());
for(int i=0; i<9; i++){
executor.submit(()->{
System.out.println(11);
try {
TimeUnit.SECONDS.sleep(2);
} catch (InterruptedException e) {
e.printStackTrace();
}
});
}
}
}
- 線程池的關閉方式有幾種,各自的區別
12、CAS
- 比較並交換,判斷內存中的某個位置的值是否爲預期值,如果是,說明沒有其他線程改過,則更改爲新的值,這個過程是原子性的,是一條CPU的原子指令。
- Atomic開頭的類,內部使用unsafe類+volatile修飾的數據來解決volatile的非原子性問題,來解決同步問題。
- unsafe裏面的所有方法都是native的,基於該類可以像C指針一樣的直接操作內存的數據,內部使用的就是compareAndSwap開頭的方法來進行while循環判斷預期值是否一致。
- 缺點:如果CAS失敗,會一直進行嘗試,可能會給CPU帶來很大的開銷。
13、集合 fail-fast 機制(線程不安全)
- ArrayList集合是線程不安全的集合,在高併發下可能會出現ConcurrentModificationException。
- Vector類相比ArrayList,出現要早,而且修改的方法都加了synchronized關鍵字,底層實現都是使用數組。
- 還可以使用Collections.synchronizedList方法來包裝ArrayList。
- 類似的不安全類集合還有:HashSet、HashMap。
14、Java 鎖
- 公平鎖:多個線程按照申請鎖的順序來獲取鎖,非公平鎖:多個線程獲取鎖的順序並不是按照申請鎖的順序,可能造成優先級翻轉或飢餓(每次都沒有搶到鎖)現象。
- 可重入鎖:又名遞歸鎖,指的是同一個線程外層函數獲得鎖之後,進入內層方法會自動獲取該鎖。synchronized和ReentrantLock都是可衝入鎖。
package com.vim;
public class Test {
public synchronized void method1() throws Exception{
System.out.println("111");
method2();
System.out.println("333");
}
public synchronized void method2() throws Exception{
System.out.println("222");
}
public static void main(String[] args) throws Exception{
Test test = new Test();
new Thread(()->{
try {
test.method1();
} catch (Exception e) {
e.printStackTrace();
}
}).start();
}
}
- 自旋鎖:嘗試獲取鎖的線程不會阻塞,會採用循環的方式去嘗試獲取鎖,這樣的好處是減少了線程上下文切換的消耗,缺點是循環會消耗CPU。
- 讀寫鎖:寫鎖是獨佔鎖,讀鎖是共享鎖,讀寫,寫寫都是互斥鎖。
- CountDownLatch:一個線程或多個線程一直等待,直到其他線程執行的操作完成。調用該類await方法的線程會一直處於阻塞狀態,直到其他線程調用countDown方法使當前計數器的值變爲零。
15、阻塞隊列
- 生產消費者模型使用的是synchronized、wait、notify的方式來完成。現在可以使用阻塞隊列來完成,好處是不需要關心什麼時候需要阻塞,什麼時候需要喚醒線程。
- ArrayBlockingQueue:數組結構組成,有界隊列。
- LinkedBlockingQueue:鏈表結構組成,有界(默認值爲Integet.MAX_VALUE)隊列,需要注意大小。
- PriorityBlockingQueue:優先級排序的無界隊列。
- DelayQueue:使用優先級隊列實現的延遲無界隊列
- SynchronousQueue:不存儲元素的阻塞隊列,也即單個元素的隊列,生產一個,消費一個,不消費,不生產。
- LinkedBlockingDeque:鏈表結構組成,雙向隊列。
- 拋出異常:add、remove
- 阻塞方法:put、take
- 返回值:offer、poll
- 檢查:element、peek
16、異常機制
- Throwable是所有錯誤和異常的超類,子類有Error和Exception。Exception分爲運行時異常RuntimeException和檢查異常CheckException(也稱編譯時異常)。
- 運行時異常:NullPointerException、ClassCastException、ArrayIndexOutBoundException,此類異常不需要用戶強制處理異常。
- 檢查異常:IOException,需要用戶必須去處理的一類異常, 不處理,程序無法通過編譯。
- throw拋出一個具體的異常對象;throws申明異常,將異常的處理交給上一級調用者。在程序中如果手動throw異常對象,需要在方法的後面使用throws申明可能拋出的異常。
17、反射和註解
- 獲取想要操作類的Class對象,通過該對象可以獲取內部的field、method、constructor。
- 獲取Class對象的方式
package com.vim.modules.web.controller;
import java.lang.reflect.Constructor;
import java.lang.reflect.Field;
import java.lang.reflect.Method;
public class Test {
public static void main(String[] args) throws Exception{
Test test = new Test();
//1.通過實例對象獲取
Class testCls1 = test.getClass();
//2.通過類獲取
Class testCls2 = Test.class;
//3.通過Class.forName
Class testCls3 = Class.forName(Test.class.getName());
//4.通過Class獲取類的屬性、方法、構造器等信息
Constructor[] constructors = testCls3.getDeclaredConstructors();
Field[] fields = testCls3.getDeclaredFields();
Method[] methods = testCls3.getDeclaredMethods();
}
}
- 創建對象的方式
package com.vim.modules.web.controller;
import java.lang.reflect.Constructor;
public class Test {
public Test(String i, String j){}
public static void main(String[] args) throws Exception{
//1.new關鍵字
Test test1 = new Test();
//2.該方式要求Class對象有默認的空構造器
Class testCls = Class.forName(Test.class.getName());
Test test2 = (Test) testCls.newInstance();
//3.利用構造方法區創建對象
Constructor constructor = testCls.getDeclaredConstructor(String.class, String.class);
Test test3 = (Test) constructor.newInstance("1", "2");
}
}
- Annotation註解,是一個接口,程序可以通過反射來獲取Annotation對象,通過該對象獲取元數據信息。
package com.vim.modules.web.controller;
import java.lang.annotation.*;
@Documented
//修飾的對象範圍
@Target(ElementType.FIELD)
//保留的時間,用於描述註解的生命週期:SOURCE、CLASS、RUNTIME
@Retention(RetentionPolicy.RUNTIME)
public @interface TestAnnotation {
String name() default "";
}
- 反射中,Class.forName 和 ClassLoader 區別
18、數據庫
- tinyInt 1字節,smallint 2字節,mediumint 3字節,int 4字節,decimal和varchar屬於變長型。
19、ThreadLocal
- 爲每一個線程提供一個獨立的變量副本,從而隔離了線程對數據的訪問衝突。相比之下,同步機制採用了“時間換空間”,ThreadLocal採用了“空間換時間”的方式。
- 在Spring中,我們使用模板類(JdbcTemplate、RedisTemplate等)來訪問底層數據,雖然模板類通過資源池獲取數據連接或會話,但是資源池本身解決的是數據連接和會話的緩存問題,並非數據連接或會話的線程安全問題。比如Spring中的事務控制,爲了保證事務內的每一個SQL操作拿到的連接都是一個,就需要使用ThreadLocal。
- 每個Thread內部有一個ThreadLocalMap成員變量,該變量使用Entry數組的方式存儲數據,其中Entry的key爲threadLoca變量,value就是需要存儲的值,這樣就可以在一個線程中定義多個ThreadLocal變量。
20、類的實例化順序
- 比如父類靜態數據,構造函數,字段,子類靜態數據,構造函數,字段,當 new 的時候, 他們的執行順序
package com.vim.modules.web.controller;
public class A {
//成員變量
int age = f1();
int f1(){
System.out.println("parent == 成員變量");
return 4;
}
//靜態成員變量
static int id=f2();
static int f2(){
System.out.println("parent == 靜態成員變量");
return 6;
}
//構造方法
public A() {
System.out.println("parent == 構造方法");
}
//普通塊
{
System.out.println("parent == 普通塊");
}
//靜態塊
static {
System.out.println("parent == 靜態塊");
}
//普通方法
void run1(){
System.out.println("parent成員函數加載");
}
//靜態方法
static void walk1(){
System.out.println("parent靜態成員函數加載");
}
}
package com.vim.modules.web.controller;
/**
* @作者 Administrator
* @時間 2020-01-15 11:07
* @版本 1.0
* @說明
*/
public class B extends A{
//成員變量
int age = f3();
int f3(){
System.out.println("children == 成員變量");
return 4;
}
//靜態成員變量
static int id=f4();
static int f4(){
System.out.println("children == 靜態成員變量");
return 6;
}
//構造方法
public B() {
System.out.println("children == 構造方法");
}
//普通塊
{
System.out.println("children == 普通塊");
}
//靜態塊
static {
System.out.println("children == 靜態塊");
}
//普通方法
void run(){
System.out.println("成員函數加載");
}
//靜態方法
static void walk(){
System.out.println("靜態成員函數加載");
}
}
//執行結果
parent == 靜態成員變量
parent == 靜態塊
children == 靜態成員變量
children == 靜態塊
parent == 成員變量
parent == 普通塊
parent == 構造方法
children == 成員變量
children == 普通塊
children == 構造方法
21、Map 實現類, 是怎麼保證有序的
22、動態代理的幾種實現方式
- 使用 JDK 動態代理
package com.vim.modules.web.aop;
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;
public class ProxyTest {
//接口
interface Person {
void print();
}
//實現類
static class Children implements Person {
@Override
public void print() {
System.out.println("success");
}
}
//代理執行類
static class ProxyPerson implements InvocationHandler{
private Person person;
public ProxyPerson(Person person){
this.person = person;
}
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
System.out.println("before");
method.invoke(person, args);
System.out.println("after");
return null;
}
}
public static void main(String[] args) {
Person person = new Children();
//生成代理類
Person proxyPerson = (Person) Proxy.newProxyInstance(person.getClass().getClassLoader(), person.getClass().getInterfaces(), new ProxyPerson(person));
//執行代理類
proxyPerson.print();
}
}
- 使用CGLIB
package com.vim.modules.web.cglib;
import net.sf.cglib.proxy.Enhancer;
import net.sf.cglib.proxy.MethodInterceptor;
import net.sf.cglib.proxy.MethodProxy;
import java.lang.reflect.Method;
public class CglibTest {
//被代理類
static class Children{
public void print(){
System.out.println("success");
}
}
//代理執行類
static class CglibHandler implements MethodInterceptor{
private Object object;
public CglibHandler(Object o){
this.object = o;
}
@Override
public Object intercept(Object o, Method method, Object[] objects, MethodProxy methodProxy) throws Throwable {
System.out.println("before");
method.invoke(object, objects);
System.out.println("after");
return null;
}
}
public static void main(String[] args) {
Children children = new Children();
//生成代理類
Children childrenProxy = (Children) Enhancer.create(children.getClass(), new CglibHandler(children));
//執行代理類
childrenProxy.print();
}
}
23、單例模式
- 餓漢模式
package com.vim.modules.web.single;
public class Singleton {
private static final Singleton instance = new Singleton();
private Singleton(){}
public Singleton getInstance(){
return instance;
}
}
- holder模式
package com.vim.modules.web.single;
public class Singleton {
private static class SingletonHolder{
private static Singleton instance = new Singleton();
}
private Singleton(){}
public Singleton getInstance(){
return SingletonHolder.instance;
}
}
24、深拷貝和淺拷貝
- 淺拷貝:如果屬性是基本類型,拷貝的就是基本類型的值;如果屬性是內存地址(引用類型),拷貝的就是內存地址 ,因此如果其中一個對象改變了這個地址,就會影響到另一個對象。比如clone方法,類實現Cloneable接口,並且覆寫Object類的clone方法,調用super.clone即可。
- 深拷貝實現,使用序列化的方式,需要實現 Serializable 接口
package com.vim.modules.web.clone;
import java.io.*;
public class DeepClone {
//淺拷貝實現Cloneable,深拷貝實現Serializable
static class Person implements Cloneable,Serializable{
private String name;
private Book book;
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public Book getBook() {
return book;
}
public void setBook(Book book) {
this.book = book;
}
//淺拷貝
@Override
public Object clone() throws CloneNotSupportedException {
return super.clone();
}
//深拷貝
public Object deepClone() throws IOException, ClassNotFoundException{
ByteArrayOutputStream bos = new ByteArrayOutputStream();
ObjectOutputStream oos = new ObjectOutputStream(bos);
oos.writeObject(this);
ByteArrayInputStream bis = new ByteArrayInputStream(bos.toByteArray());
ObjectInputStream ois = new ObjectInputStream(bis);
return ois.readObject();
}
}
static class Book implements Serializable{
private String name;
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
}
public static void main(String[] args) throws Exception {
Book book = new Book();
book.setName("java");
Person person1 = new Person();
person1.setBook(book);
//淺拷貝
// Person person2 = (Person) person1.clone();
//深拷貝
Person person2 = (Person) person1.deepClone();
System.out.println(person1.getBook().getName());
person1.getBook().setName("php");
System.out.println(person2.getBook().getName());
}
}
25、Spring 相關
- aop
- ioc:控制反轉:不需要去new對象,只需要將該對象的控制權交給Spring;依賴注入:告訴Spring要使用某個對象。
- 事務
- springmvc運行流程
26、Mybatis 相關
- 一級緩存和二級緩存
- 分頁插件原理