Java -- 題目

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 相關

  • 一級緩存和二級緩存
  • 分頁插件原理

 

 

 

 

 

 

發佈了100 篇原創文章 · 獲贊 20 · 訪問量 1萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章