Java基礎知識點面試手冊(線程+JDK8)

  • 2019 年 10 月 11 日
  • 筆記

作者:蠻三刀把刀

前言

本文快速回顧了Java中最基礎的知識點,用作面試複習,事半功倍。

此為下篇,內容包括:高並發編程,Java8新特性。

高並發編程

多線程和單線程的區別和聯繫:

答:

在單核 CPU 中,將 CPU 分為很小的時間片,在每一時刻只能有一個線程在執行,是一種微觀上輪流佔用 CPU 的機制。

多線程會存在線程上下文切換,會導致程序執行速度變慢,即採用一個擁有兩個線程的進程執行所需要的時間比一個線程的進程執行兩次所需要的時間要多一些。

結論:即採用多線程不會提高程序的執行速度,反而會降低速度,但是對於用戶來說,可以減少用戶的響應時間。

如何指定多個線程的執行順序?

三種方式:https://blog.csdn.net/difffate/article/details/63684290

方式一

解析:面試官會給你舉個例子,如何讓 10 個線程按照順序打印 0123456789?(寫代碼實現)

答: 設定一個 orderNum,每個線程執行結束之後,更新 orderNum,指明下一個要執行的線程。並且喚醒所有的等待線程。 在每一個線程的開始,要 while 判斷 orderNum 是否等於自己的要求值!!不是,則 wait,是則執行本線程。

方式二

通過主線程Join()

方式三

通過線程執行時Join()

線程和進程的區別(必考)

答:

進程是一個 「執行中的程序」,是系統進行資源分配和調度的一個獨立單位;

線程是進程的一個實體,一個進程中擁有多個線程,線程之間共享地址空間和其它資源(所以通信和同步等操作線程比進程更加容易);

線程上下文的切換比進程上下文切換要快很多。

(1)進程切換時,涉及到當前進程的 CPU 環境的保存和新被調度運行進程的 CPU 環境的設置

(2)線程切換僅需要保存和設置少量的寄存器內容,不涉及存儲管理方面的操作

多線程產生死鎖的 4 個必要條件

答:

  • 資源互斥條件:一個資源每次只能被一個線程使用;
  • 請求與保持條件:一個線程因請求資源而阻塞時,對已獲得的資源保持不放;
  • 不剝奪條件:進程已經獲得的資源,在未使用完之前,不能強行剝奪;
  • 循環等待條件:若干線程之間形成一種頭尾相接的循環等待資源關係。

sleep( ) 和 wait(n)、wait( ) 的區別

答:

sleep 方法:是 Thread 類的靜態方法,當前線程將睡眠 n 毫秒,線程進入阻塞狀態。當睡眠時間到了,會解除阻塞,進行運行狀態,等待 CPU 的到來。睡眠不釋放鎖(如果有的話)

wait 方法:是 Object 的方法,必須與 synchronized 關鍵字一起使用,線程進入阻塞狀態,當 notify 或者 notifyall 被調用後,會解除阻塞。但是,只有重新佔用互斥鎖之後才會進入可運行狀態。睡眠時,釋放互斥鎖。

synchronized 關鍵字

該關鍵字是一個幾種鎖的封裝。詳細請看Java虛擬機知識點面試手冊。

volatile 關鍵字

經典:https://www.jianshu.com/p/195ae7c77afe

解析:關於指令重排序的問題,可以查閱 DCL 雙檢鎖失效相關資料。

答:

通過關鍵字sychronize可以防止多個線程進入同一段代碼,在某些特定場景中,volatile相當於一個輕量級的sychronize,因為不會引起線程的上下文切換。該關鍵字可以保證可見性不保證原子性。

功能:

主內存和工作內存,直接與主內存產生交互,進行讀寫操作,保證可見性;

禁止 JVM 進行的指令重排序。

ThreadLocal(線程局部變量)關鍵字

詳解:https://www.cnblogs.com/dolphin0520/p/3920407.html

答:

當使用 ThreadLocal 維護變量時,其為每個使用該變量的線程提供獨立的變量副本,所以每一個線程都可以獨立的改變自己的副本,而不會影響其他線程對應的副本。

ThreadLocal 內部實現機制:

每個線程內部都會維護一個類似 HashMap 的對象,稱為 ThreadLocalMap,裡邊會包含若干了 Entry(K-V 鍵值對),相應的線程被稱為這些 Entry 的屬主線程; Entry 的 Key 是一個 ThreadLocal 實例,Value 是一個線程特有對象。

Entry 的作用即是:為其屬主線程建立起一個 ThreadLocal 實例與一個線程特有對象之間的對應關係;

Entry 對 Key 的引用是弱引用;

Entry 對 Value 的引用是強引用。

其中最重要的一個應用實例就是經典 Web 交互模型中的「一個請求對應一個服務器線程」(Thread-per-Request)的處理方式

Atomic 關鍵字

答:可以使基本數據類型以原子的方式實現自增自減等操作。

內存泄漏和內存溢出

答:

內存泄露的幾種方式:

  1. 靜態集合類像HashMap、Vector等的使用最容易出現內存泄露,這些靜態變量的生命周期和應用程序一致,所有的對象Object也不能被釋放,因為他們也將一直被Vector等應用着。
Static Vector v = new Vector();  for (int i = 1; i<100; i++)  {      Object o = new Object();      v.add(o);      o = null;  }  
  1. 各種連接,數據庫連接,網絡連接,IO連接等沒有顯示調用close關閉,不被GC回收導致內存泄露。
  2. 監聽器的使用,在釋放對象的同時沒有相應刪除監聽器的時候也可能導致內存泄露。

概念:

  • 內存溢出指的是內存不夠用了;
  • 內存泄漏是指對象可達,但是沒用了,或者你把指向內存的引用給弄丟了。即本該被 GC 回收的對象並沒有被回收;
  • 內存泄露是導致內存溢出的原因之一;內存泄露積累起來將導致內存溢出。

Java 8

Java語言的新特性

https://blog.csdn.net/u014470581/article/details/54944384

Lambda 表達式(也稱為閉包)

1.8以前,開發者只能使用匿名內部類代替Lambda表達式,現在允許我們將函數當成參數傳遞給某個方法,或者把代碼本身當做數據處理。

Arrays.asList( "a", "b", "d" ).forEach( e -> System.out.println( e ) );  

如果Lambda表達式需要更複雜的語句塊,則可以使用花括號將該語句塊括起來,類似於Java中的函數體,例如:

Arrays.asList( "a", "b", "d" ).forEach( e -> {      System.out.print( e );      System.out.print( e );  } );  

Lambda表達式有返回值,返回值的類型也由編譯器推理得出。如果Lambda表達式中的語句塊只有一行,則可以不用使用return語句,下列兩個代碼片段效果相同:

Arrays.asList( "a", "b", "d" ).sort( ( e1, e2 ) -> e1.compareTo( e2 ) );  

Arrays.asList( "a", "b", "d" ).sort( ( e1, e2 ) -> {      int result = e1.compareTo( e2 );      return result;  } );  

函數式接口

指的是只有一個函數的接口,java.lang.Runnable 和 java.util.concurrent.Callable 就是函數式接口的例子;

只要某個開發者在該接口中添加一個函數,則該接口就不再是函數式接口進而導致編譯失敗。為了克服這種代碼層面的脆弱性,並顯式說明某個接口是函數式接口,java8 提供了一個特殊的註解 @Functionallnterface 來標明該接口是一個函數式接口。

不過有一點需要注意,默認方法和靜態方法不會破壞函數式接口的定義,因此如下的代碼是合法的。

@FunctionalInterface  public interface FunctionalDefaultMethods {      void method();        default void defaultMethod() {      }  }  

接口的默認方法和靜態方法

默認方法和抽象方法之間的區別在於抽象方法需要實現,而默認方法不需要。接口提供的默認方法會被接口的實現類繼承或者覆寫。(換句話說,可以直接繼承,也可以覆寫該默認方法

靜態方法:見上方鏈接

由於JVM上的默認方法的實現在位元組碼層面提供了支持因此效率非常高。默認方法允許在不打破現有繼承體系的基礎上改進接口。該特性在官方庫中的應用是:給java.util.Collection接口添加新方法,如stream()、parallelStream()、forEach()和removeIf()等等。

儘管默認方法有這麼多好處,但在實際開發中應該謹慎使用:在複雜的繼承體系中,默認方法可能引起歧義和編譯錯誤。如果你想了解更多細節,可以參考官方文檔。

方法引用

不甚理解,見上方鏈接

引入重複註解

註解有一個很大的限制是:在同一個地方不能多次使用同一個註解。Java 8打破了這個限制,引入了重複註解的概念,允許在同一個地方多次使用同一個註解。

在Java 8中使用@Repeatable註解定義重複註解,實際上,這並不是語言層面的改進,而是編譯器做的一個trick,底層的技術仍然相同。

更好的類型推斷

Java 8編譯器在類型推斷方面有很大的提升,在很多場景下編譯器可以推導出某個參數的數據類型,從而使得代碼更為簡潔。

註解的使用場景拓寬

註解幾乎可以使用在任何元素上:局部變量、接口類型、超類和接口實現類,甚至可以用在函數的異常定義上。

Java編譯器的新特性

參數名稱

為了在運行時獲得Java程序中方法的參數名稱,老一輩的Java程序員必須使用不同方法,例如Paranamer liberary。Java 8終於將這個特性規範化,在語言層面(使用反射API和Parameter.getName()方法)和位元組碼層面(使用新的javac編譯器以及-parameters參數)提供支持。

Java官方庫的新特性

HashMap/CurrentHashMap等變化

HashMap 是數組 + 鏈表 + 紅黑樹(JDK1.8 增加了紅黑樹部分)實現。

Optional

在Java 8之前,Google Guava引入了Optionals類來解決NullPointerException,從而避免源碼被各種null檢查污染,以便開發者寫出更加整潔的代碼。Java 8也將Optional加入了官方庫。

接下來看一點使用Optional的例子:可能為空的值或者某個類型的值:

Optional< String > fullName = Optional.ofNullable( null );  System.out.println( "Full Name is set? " + fullName.isPresent() );  System.out.println( "Full Name: " + fullName.orElseGet( () -> "[none]" ) );  System.out.println( fullName.map( s -> "Hey " + s + "!" ).orElse( "Hey Stranger!" ) );  

如果Optional實例持有一個非空值,則isPresent()方法返回true,否則返回false;orElseGet()方法,Optional實例持有null,則可以接受一個lambda表達式生成的默認值;map()方法可以將現有的Opetional實例的值轉換成新的值;orElse()方法與orElseGet()方法類似,但是在持有null的時候返回傳入的默認值。

Streams

新增的Stream API(java.util.stream)將生成環境的函數式編程引入了Java庫中。

// Calculate total points of all active tasks using sum()  final long totalPointsOfOpenTasks = tasks      .stream()      .filter( task -> task.getStatus() == Status.OPEN )      .mapToInt( Task::getPoints )      .sum();    System.out.println( "Total points: " + totalPointsOfOpenTasks );  

Date/Time API

包含了所有關於日期、時間、時區、持續時間和時鐘操作的類。(Java 8 的日期與時間問題解決方案)

新的java.time包包含了所有關於日期、時間、時區、Instant(跟日期類似但是精確到納秒)、duration(持續時間)和時鐘操作的類。

這些類都是不可變的、線程安全的。

Base64

對Base64編碼的支持已經被加入到Java 8官方庫中,這樣不需要使用第三方庫就可以進行Base64編碼

並行數組

Java8版本新增了很多新的方法,用於支持並行數組處理。

最重要的方法是parallelSort(),可以顯著加快多核機器上的數組排序。

並發性

基於新增的lambda表達式和steam特性,為Java 8中為java.util.concurrent.ConcurrentHashMap類添加了新的方法來支持聚焦操作

另外,也為java.util.concurrentForkJoinPool類添加了新的方法來支持通用線程池操作

JVM的新特性

JVM 內存管理方面,由元空間代替了永久代。

其實,移除永久代的工作從JDK1.7就開始了。JDK1.7中,存儲在永久代的部分數據就已經轉移到了Java Heap或者是 Native Heap。但永久代仍存在於JDK1.7中,並沒完全移除,譬如符號引用(Symbols)轉移到了native heap;字面量(interned strings)轉移到了java heap;類的靜態變量(class statics)轉移到了java heap。

區別:

  1. 元空間並不在虛擬機中,而是使用本地內存
  2. 默認情況下,元空間的大小僅受本地內存限制
  3. 也可以通過 -XX:MetaspaceSize 指定元空間大小。

為什麼要做這個轉換?所以,最後給大家總結以下幾點原因:

  • 字符串存在永久代中,容易出現性能問題和內存溢出。
  • 類及方法的信息等比較難確定其大小,因此對於永久代的大小指定比較困難,太小容易出現永久代溢出,太大則容易導致老年代溢出。
  • 永久代會為 GC 帶來不必要的複雜度,並且回收效率偏低。
  • Oracle 可能會將HotSpot 與 JRockit 合二為一。

Java 7和8的新特性(英文)

New highlights in Java SE 8

  1. Lambda Expressions
  2. Pipelines and Streams
  3. Date and Time API
  4. Default Methods
  5. Type Annotations
  6. Nashhorn JavaScript Engine
  7. Concurrent Accumulators
  8. Parallel operations
  9. PermGen Error Removed

New highlights in Java SE 7

  1. Strings in Switch Statement
  2. Type Inference for Generic Instance Creation
  3. Multiple Exception Handling
  4. Support for Dynamic Languages
  5. Try with Resources
  6. Java nio Package
  7. Binary Literals, Underscore in literals
  8. Diamond Syntax
  • Difference between Java 1.8 and Java 1.7?
  • Java 8 特性

Java 與 C++ 的區別

  • Java 是純粹的面向對象語言,所有的對象都繼承自 java.lang.Object,
  • C++ 為了兼容 C 即支持面向對象也支持面向過程
  • Java 通過虛擬機從而實現跨平台特性,但是 C++ 依賴於特定的平台。
  • Java 沒有指針,它的引用可以理解為安全指針,而 C++ 具有和 C 一樣的指針。
  • Java 支持自動垃圾回收,而 C++ 需要手動回收。
  • Java 不支持多重繼承,只能通過實現多個接口來達到相同目的,而 C++ 支持多重繼承。
  • Java 不支持操作符重載,雖然可以對兩個 String 對象支持加法運算,但是這是語言內置支持的操作,不屬於操作符重載,而 C++ 可以。
  • Java 的 goto 是保留字,但是不可用,C++ 可以使用 goto。
  • Java 不支持條件編譯,C++ 通過 #ifdef #ifndef 等預處理命令從而實現條件編譯。

What are the main differences between Java and C++?