java知識點總結(二)

  1. Java提供了哪些IO方式? NIO如何實現多路複用?

Java IO方式有很多種,基於不同的IO抽象模型和交互方式,可以進行簡單區分。
第一,傳統的java.io包,它基於流模型實現,提供了我們最熟知的一些IO功能,比如File抽象、輸入輸出流等。交互方式是同步、阻塞的方式,也就是說,在讀取輸入流或者寫入輸出流時,在讀、寫動作完成之前,線程會一直阻塞在那裏,它們之間的調用是可靠的線性順序。java.io包的好處是代碼比較簡單、直觀,缺點則是IO效率和擴展性存在侷限性,容易成爲應用性能的瓶頸。很多時候,人們也把java.net下面提供的部分網絡API,比如Socket、ServerSocket、HttpURLConnection也歸類到同步阻塞IO類庫,因爲網絡通信同樣是IO行爲。

第二,在Java 1.4中引入了NIO框架(java.nio包),提供了Channel、Selector、Buffer等新的抽象,可以構建多路複用的、同步非阻塞IO程序,同時提供了更接近操作系統底層的高性能數據操作方式。

第三,在Java 7中,NIO有了進一步的改進,也就是NIO 2,引入了異步非阻塞IO方式,也有很多人叫它AIO(Asynchronous IO)。異步IO操作基於事件和回調機制,可以簡單理解爲,應用操作直接返回,而不會阻塞在那裏,當後臺處理完成,操作系統會通知相應線程進行後續工作。

  1. Java有幾種文件拷貝方式?哪一種最高效?

    Java有多種比較典型的文件拷貝實現方式,比如:
    利用java.io類庫,直接爲源文件構建一個FileInputStream讀取,然後再爲目標文件構建一個FileOutputStream,完成寫入工作。

      public static void copyFileByStream(File source, File des) throws IOException {
            try (InputStream is = new FileInputStream(source);
                 OutputStream os = new FileOutputStream(des);) {
                byte[] bufer = new byte[1024];
                int length;
                while ((length = is.read(bufer)) > 0) {
                    os.write(bufer, 0, length);
                }
            }
        }
    

    或者,利用java.nio類庫提供的transferTo或transferFrom方法實現。

       public static void copyFileByChannel(File source, File des) throws IOException {
    
            try (FileChannel sourceChannel = new FileInputStream(source).getChannel();
                 FileChannel targetChannel = new FileOutputStream(des).getChannel()
            ) {
    
                for (long count = sourceChannel.size(); count > 0; ) {
                    long transferred = sourceChannel.transferTo(
                            sourceChannel.position(), count, targetChannel);
                    sourceChannel.position(sourceChannel.position() + transferred);
                    count -= transferred;
                }
            }
        }
    

    當然,Java標準類庫本身已經提供了幾種Files.copy的實現。
    對於Copy的效率,這個其實與操作系統和配置等情況相關,總體上來說,NIO transferTo/From的方式可能更快,因爲它更能利用現代操作系統底層機制,避免不必要拷貝和上下文切換。

  2. synchronized和ReentrantLock有什麼區別呢?

    synchronized是Java內建的同步機制,所以也有人稱其爲Intrinsic Locking,它提供了互斥的語義和可見性,當一個線程已經獲取當前鎖時,其他試圖獲取的線程只能等待或者阻塞在那裏。

    在Java 5以前,synchronized是僅有的同步手段,在代碼中, synchronized可以用來修飾方法,也可以使用在特定的代碼塊兒上,本質上synchronized方法等同於把方法全部語句用synchronized塊包起來。

    ReentrantLock,通常翻譯爲再入鎖,是Java 5提供的鎖實現,它的語義和synchronized基本相同。再入鎖通過代碼直接調用lock()方法獲取,代碼書寫也更加靈活。與此同時,ReentrantLock提供了很多實用的方法,能夠實現很多synchronized無法做到的細節控制,比如可以控制fairness,也就是公平性,或者利用定義條件等。但是,編碼中也需
    要注意,必須要明確調用unlock()方法釋放,不然就會一直持有該鎖。

    synchronized和ReentrantLock的性能不能一概而論,早期版本synchronized在很多場景下性能相差較大,在後續版本進行了較多改進,在低競爭場景中表現可能優於ReentrantLock。

  3. 談談接口和抽象類有什麼區別?

    接口和抽象類是Java面向對象設計的兩個基礎機制。

    接口是對行爲的抽象,它是抽象方法的集合,利用接口可以達到API定義和實現分離的目的。接口,不能實例化;不能包含任何非常量成員,任何field都是隱含着public static final的意義;同時,沒有非靜態方法實現,也就是說要麼是抽象方法,要麼是靜態方法。Java標準類庫中,定義了非常多的接口,比如java.util.List。

    抽象類是不能實例化的類,用abstract關鍵字修飾class,其目的主要是代碼重用。除了不能實例化,形式上和一般的Java類並沒有太大區別,可以有一個或者多個抽象方法,也可以沒有抽象方法。抽象類大多用於抽取相關Java類的共用方法實現或者是共同成員變量,然後通過繼承的方式達到代碼複用的目的。Java標準庫中,比如collection框架,很多通用部分就被抽取成爲抽象類,例如java.util.AbstractList。

    Java類實現interface使用implements關鍵詞,繼承abstract class則是使用extends關鍵詞。

  4. 談談你知道的設計模式?請手動實現單例模式,Spring等框架中使用了哪些模式?

    大致按照模式的應用目標分類,設計模式可以分爲創建型模式、結構型模式和行爲型模式。

  • 創建型模式,是對對象創建過程的各種問題和解決方案的總結,包括各種工廠模式(Factory、Abstract Factory)、單例模式(Singleton)、構建器模式(Builder)、原型模式(ProtoType)。

  • 結構型模式,是針對軟件設計結構的總結,關注於類、對象繼承、組合方式的實踐經驗。常見的結構型模式,包括橋接模式(Bridge)、適配器模式(Adapter)、裝飾者模式(Decorator)、代理模式(Proxy)、組合模式(Composite)、外觀模式(Facade)、享元模式(Flyweight)等。

  • 行爲型模式,是從類或對象之間交互、職責劃分等角度總結的模式。比較常見的行爲型模式有策略模式(Strategy)、解釋器模式(Interpreter)、命令模式(Command)、 觀察者模式(Observer)、迭代器模式(Iterator)、模板方法模式(Template Method)、訪問者模式(Visitor)。

    • 雙重檢查鎖
       public class Singleton {
           	//這裏的volatile能夠提供可見性,以及保證getInstance返回的是初始化完全的對象。
              private static volatile Singleton singleton = null;
           	//私有構造器,禁止實例化
              private Singleton() {
              }
           
              public static Singleton getSingleton() {
                  if (singleton == null) { // 儘量避免重複進入同步塊
                      synchronized (Singleton.class) { // 同步.class,意味着對同步類方法調用
                          if (singleton == null) {
                              singleton = new Singleton();
                          }
                      }
                  }
                  return singleton;
              }
          }
          
          
    
    • 內部持有靜態對象
          public class Singleton {
              private Singleton() {
              }
      
              public static Singleton getSingleton() {
                  return Holder.singleton;
              }
      
              private static class Holder {
                  private satic Singleton
                  singleton =new Singleton();
              }
          }
      
    
  1. synchronized底層如何實現?什麼是鎖的升級、降級?

    synchronized代碼塊是由一對兒monitorenter/monitorexit指令實現的,Monitor對象是同步的基本實現單元。
    在Java 6之前,Monitor的實現完全是依靠操作系統內部的互斥鎖,因爲需要進行用戶態到內核態的切換,所以同步操作是一個無差別的重量級操作。

    現代的(Oracle)JDK中,JVM對此進行了大刀闊斧地改進,提供了三種不同的Monitor實現,也就是常說的三種不同的鎖:偏斜鎖(Biased Locking)、輕量級鎖和重量級鎖,大大改進了其性能。

    所謂鎖的升級、降級,就是JVM優化synchronized運行的機制,當JVM檢測到不同的競爭狀況時,會自動切換到適合的鎖實現,這種切換就是鎖的升級、降級。

    當沒有競爭出現時,默認會使用偏斜鎖。JVM會利用CAS操作(compare and swap),在對象頭上的Mark Word部分設置線程ID,以表示這個對象偏向於當前線程,所以並不涉及真正的互斥鎖。這樣做的假設是基於在很多應用場景中,大部分對象生命週期中最多會被一個線程鎖定,使用偏斜鎖可以降低無競爭開銷。

    如果有另外的線程試圖鎖定某個已經被偏斜過的對象,JVM就需要撤銷(revoke)偏斜鎖,並切換到輕量級鎖實現。輕量級鎖依賴CAS操作Mark Word來試圖獲取鎖,如果重試成功,就使用普通的輕量級鎖;否則,進一步升級爲重量級鎖。

    我注意到有的觀點認爲Java不會進行鎖降級。實際上據我所知,鎖降級確實是會發生的,當JVM進入安全點(SafePoint)的時候,會檢查是否有閒置的Monitor,然後試圖進行降級。

  2. 一個線程兩次調用start()方法會出現什麼情況?談談線程的生命週期和狀態轉移。

    Java的線程是不允許啓動兩次的,第二次調用必然會拋出IllegalThreadStateException,這是一種運行時異常,多次調用start被認爲是編程錯誤。

    關於線程生命週期的不同狀態,在Java 5以後,線程狀態被明確定義在其公共內部枚舉類型java.lang.Thread.State中,分別是:

    • 新建(NEW),表示線程被創建出來還沒真正啓動的狀態,可以認爲它是個Java內部狀態。
    • 就緒(RUNNABLE),表示該線程已經在JVM中執行,當然由於執行需要計算資源,它可能是正在運行,也可能還在等待系統分配給它CPU片段,在就緒隊列裏面排隊。
      在其他一些分析中,會額外區分一種狀態RUNNING,但是從Java API的角度,並不能表示出來。
    • 阻塞(BLOCKED),這個狀態和我們前面兩講介紹的同步非常相關,阻塞表示線程在等待Monitor lock。比如,線程試圖通過synchronized去獲取某個鎖,但是其他線程已經獨佔了,那麼當前線程就會處於阻塞狀態。
    • 等待(WAITING),表示正在等待其他線程採取某些操作。一個常見的場景是類似生產者消費者模式,發現任務條件尚未滿足,就讓當前消費者線程等待(wait),另外的生產者線程去準備任務數據,然後通過類似notify等動作,通知消費線程可以繼續工作了。Thread.join()也會令線程進入等待狀態。
    • 計時等待(TIMED_WAIT),其進入條件和等待狀態類似,但是調用的是存在超時條件的方法,比如wait或join等方法的指定超時版本,如下面示例:
     public final native void wait(long timeout) throws InterruptedException;
    
    • 終止(TERMINATED),不管是意外退出還是正常執行結束,線程已經完成使命,終止運行,也有人把這個狀態叫作死亡。

      在第二次調用start()方法的時候,線程可能處於終止或者其他(非NEW)狀態,但是不論如何,都是不可以再次啓動的。

      在這裏插入圖片描述

  3. 什麼情況下Java程序會產生死鎖?如何定位、修復?

    死鎖是一種特定的程序狀態,在實體之間,由於循環依賴導致彼此一直處於等待之中,沒有任何個體可以繼續前進。死鎖不僅僅是在線程之間會發生,存在資源獨佔的進程之間同樣也可能出現死鎖。通常來說,我們大多是聚焦在多線程場景中的死鎖,指兩個或多個線程之間,由於互相持有對方需要的鎖,而永久處於阻塞的狀態。

    你可以利用下面的示例圖理解基本的死鎖問題:

    在這裏插入圖片描述

    定位死鎖最常見的方式就是利用jstack等工具獲取線程棧,然後定位互相之間的依賴關係,進而找到死鎖。如果是比較明顯的死鎖,往往jstack等就能直接定位,類似JConsole甚至可以在圖形界面進行有限的死鎖檢測。

    如果程序運行時發生了死鎖,絕大多數情況下都是無法在線解決的,只能重啓、修正程序本身問題。所以,代碼開發階段互相審查,或者利用工具進行預防性排查,往往也是很重要的。

  4. Java併發包提供了哪些併發工具類?

    我們通常所說的併發包也就是java.util.concurrent及其子包,集中了Java併發的各種基礎工具類,具體主要包括幾個方面:

    • 提供了比synchronized更加高級的各種同步結構,包括CountDownLatch、CyclicBarrier、Semaphore等,可以實現更加豐富的多線程操作,比如利用Semaphore作爲資源控制器,限制同時進行工作的線程數量。

    • 各種線程安全的容器,比如最常見的ConcurrentHashMap、有序的ConcunrrentSkipListMap,或者通過類似快照機制,實現線程安全的動態數組CopyOnWriteArrayList等。

    • 各種併發隊列實現,如各種BlockedQueue實現,比較典型的ArrayBlockingQueue、 SynchorousQueue或針對特定場景的PriorityBlockingQueue等。

    • 強大的Executor框架,可以創建各種不同類型的線程池,調度任務運行等,絕大部分情況下,不再需要自己從頭實現線程池和任務調度器。

  5. 併發包中的ConcurrentLinkedQueue和LinkedBlockingQueue有什麼區別?

    有時候我們把併發包下面的所有容器都習慣叫作併發容器,但是嚴格來講,類似ConcurrentLinkedQueue這種“Concurrent*”容器,纔是真正代表併發。
    關於問題中它們的區別:

    • Concurrent類型基於lock-free,在常見的多線程訪問場景,一般可以提供較高吞吐量。
    • 而LinkedBlockingQueue內部則是基於鎖,並提供了BlockingQueue的等待性方法。

    不知道你有沒有注意到,java.util.concurrent包提供的容器(Queue、List、Set)、Map,從命名上可以大概區分爲Concurrent、CopyOnWrite和Blocking 等三類,同樣是線程安全容器,可以簡單認爲:

    • Concurrent類型沒有類似CopyOnWrite之類容器相對較重的修改開銷。
    • 但是,凡事都是有代價的,Concurrent往往提供了較低的遍歷一致性。你可以這樣理解所謂的弱一致性,例如,當利用迭代器遍歷時,如果容器發生修改,迭代器仍然可以繼續 進行遍歷。
    • 與弱一致性對應的,就是我介紹過的同步容器常見的行爲“fast-fail”,也就是檢測到容器在遍歷過程中發生了修改,則拋出ConcurrentModifcationException, 不再繼續遍歷。
    • 弱一致性的另外一個體現是,size等操作準確性是有限的,未必是100%準確。
    • 與此同時,讀取的性能具有一定的不確定性。
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章