今天分享的是一位華中科技大學同學分享的京東一面面經,主要是一些非常基礎的問題,也就是比較簡單且容易準備的常規八股。
這也是這位同學人生的第一次面試,直接秒掛了。其實也挺正常,畢竟缺乏經驗。對於 Java 後端實習面試來說,這位同學面試遇到的問題已經非常簡單了。
很多同學覺得這種基礎問題的考查意義不大,實際上還是很有意義的,這種基礎性的知識在日常開發中也會需要經常用到。例如,執行緒池這塊的拒絕策略、核心引數配置什麼的,如果你不瞭解,實際專案中使用執行緒池可能就用的不是很明白,容易出現問題。而且,其實這種基礎性的問題是最容易準備的,像各種底層原理、系統設計、場景題以及深挖你的專案這類才是最難的!
1、Redis 瞭解嗎,作用?
Redis (REmote DIctionary Server)是一個基於 C 語言開發的開源 NoSQL 資料庫(BSD 許可)。與傳統資料庫不同的是,Redis 的資料是儲存在記憶體中的(記憶體資料庫,支援持久化),因此讀寫速度非常快,被廣泛應用於分散式快取方向。並且,Redis 儲存的是 KV 鍵值對資料。
為了滿足不同的業務場景,Redis 內建了多種資料型別實現(比如 String、Hash、Sorted Set、Bitmap、HyperLogLog、GEO)。並且,Redis 還支援事務、持久化、Lua 指令碼、多種開箱即用的叢集方案(Redis Sentinel、Redis Cluster)。
Redis 內部做了非常多的效能最佳化,比較重要的有下面 3 點:
- Redis 基於記憶體,記憶體的訪問速度是磁碟的上千倍;
- Redis 基於 Reactor 模式設計開發了一套高效的事件處理模型,主要是單執行緒事件迴圈和 IO 多路複用(Redis 執行緒模式後面會詳細介紹到);
- Redis 內建了多種最佳化過後的資料型別/結構實現,效能非常高。
Redis 除了做快取,還能做什麼?
- 分散式鎖:透過 Redis 來做分散式鎖是一種比較常見的方式。通常情況下,我們都是基於 Redisson 來實現分散式鎖。關於 Redis 實現分散式鎖的詳細介紹,可以看我寫的這篇文章:如何基於 Redis 實現分散式鎖?。
- 限流:一般是透過 Redis + Lua 指令碼的方式來實現限流。相關閱讀:《我司用了 6 年的 Redis 分散式限流器,可以說是非常厲害了!》。
- 訊息佇列:Redis 自帶的 List 資料結構可以作為一個簡單的佇列使用。Redis 5.0 中增加的 Stream 型別的資料結構更加適合用來做訊息佇列。它比較類似於 Kafka,有主題和消費組的概念,支援訊息持久化以及 ACK 機制。
- 延時佇列:Redisson 內建了延時佇列(基於 Sorted Set 實現的)。
- 分散式 Session :利用 String 或者 Hash 資料型別儲存 Session 資料,所有的伺服器都可以訪問。
- 複雜業務場景:透過 Redis 以及 Redis 擴充套件(比如 Redisson)提供的資料結構,我們可以很方便地完成很多複雜的業務場景比如透過 Bitmap 統計活躍使用者、透過 Sorted Set 維護排行榜。
- ……
詳細介紹可以看這篇文章:Redis 除了快取還能做什麼?可以做訊息佇列嗎? 。
2、Redis 資料結構有哪些?
Redis 中比較常見的資料型別有下面這些:
- 5 種基礎資料型別:String(字串)、List(列表)、Set(集合)、Hash(雜湊)、Zset(有序集合)。
- 3 種特殊資料型別:HyperLogLog(基數統計)、Bitmap (點陣圖)、Geospatial (地理位置)。
除了上面提到的之外,還有一些其他的比如 Bloom filter(布隆過濾器)、Bitfield(位域)。
關於 Redis 5 種基礎資料型別和 3 種特殊資料型別的詳細介紹請看 Redis 官方文件對 Redis 資料型別的介紹 和我寫的這兩篇文章:
- Redis 5 種基本資料型別詳解
- Redis 3 種特殊資料型別詳解
3、同步和非同步的區別
- 同步:發出一個呼叫之後,在沒有得到結果之前, 該呼叫就不可以返回,一直等待。
- 非同步:呼叫在發出之後,不用等待返回結果,該呼叫直接返回。
4、建立執行緒的方法哪些?
一般來說,建立執行緒有很多種方式,例如繼承Thread
類、實現Runnable
介面、實現Callable
介面、使用執行緒池、使用CompletableFuture
類等等。
不過,這些方式其實並沒有真正建立出執行緒。準確點來說,這些都屬於是在 Java 程式碼中使用多執行緒的方法。
嚴格來說,Java 就只有一種方式可以建立執行緒,那就是透過new Thread().start()
建立。不管是哪種方式,最終還是依賴於new Thread().start()
。
關於這個問題的詳細分析可以檢視這篇文章:大家都說 Java 有三種建立執行緒的方式!併發程式設計中的驚天騙局!。
5、執行緒池作用是什麼?
執行緒池提供了一種限制和管理資源(包括執行一個任務)的方式。 每個執行緒池還維護一些基本統計資訊,例如已完成任務的數量。
這裡借用《Java 併發程式設計的藝術》提到的來說一下使用執行緒池的好處:
- 降低資源消耗。透過重複利用已建立的執行緒降低執行緒建立和銷燬造成的消耗。
- 提高響應速度。當任務到達時,任務可以不需要等到執行緒建立就能立即執行。
- 提高執行緒的可管理性。執行緒是稀缺資源,如果無限制的建立,不僅會消耗系統資源,還會降低系統的穩定性,使用執行緒池可以進行統一的分配,調優和監控。
《阿里巴巴 Java 開發手冊》中強制執行緒池不允許使用 Executors
去建立,而是透過 ThreadPoolExecutor
建構函式的方式,這樣的處理方式讓寫的同學更加明確執行緒池的執行規則,規避資源耗盡的風險
Executors
返回執行緒池物件的弊端如下(後文會詳細介紹到):
FixedThreadPool
和SingleThreadExecutor
:使用的是無界的LinkedBlockingQueue
,任務佇列最大長度為Integer.MAX_VALUE
,可能堆積大量的請求,從而導致 OOM。CachedThreadPool
:使用的是同步佇列SynchronousQueue
, 允許建立的執行緒數量為Integer.MAX_VALUE
,如果任務數量過多且執行速度較慢,可能會建立大量的執行緒,從而導致 OOM。ScheduledThreadPool
和SingleThreadScheduledExecutor
: 使用的無界的延遲阻塞佇列DelayedWorkQueue
,任務佇列最大長度為Integer.MAX_VALUE
,可能堆積大量的請求,從而導致 OOM。
相關閱讀:
- 8 個執行緒池最佳實踐和坑!使用不當直接生產事故!!
- 手寫一個輕量級動態執行緒池,很香!!
6、Spring,Spring MVC,Spring Boot 之間什麼關係?
很多人對 Spring,Spring MVC,Spring Boot 這三者傻傻分不清楚!這裡簡單介紹一下這三者,其實很簡單,沒有什麼高深的東西。
Spring 包含了多個功能模組(上面剛剛提到過),其中最重要的是 Spring-Core(主要提供 IoC 依賴注入功能的支援) 模組, Spring 中的其他模組(比如 Spring MVC)的功能實現基本都需要依賴於該模組。
下圖對應的是 Spring4.x 版本。目前最新的 5.x 版本中 Web 模組的 Portlet 元件已經被廢棄掉,同時增加了用於非同步響應式處理的 WebFlux 元件。
Spring MVC 是 Spring 中的一個很重要的模組,主要賦予 Spring 快速構建 MVC 架構的 Web 程式的能力。MVC 是模型(Model)、檢視(View)、控制器(Controller)的簡寫,其核心思想是透過將業務邏輯、資料、顯示分離來組織程式碼。
使用 Spring 進行開發各種配置過於麻煩比如開啟某些 Spring 特性時,需要用 XML 或 Java 進行顯式配置。於是,Spring Boot 誕生了!
Spring 旨在簡化 J2EE 企業應用程式開發。Spring Boot 旨在簡化 Spring 開發(減少配置檔案,開箱即用!)。
Spring Boot 只是簡化了配置,如果你需要構建 MVC 架構的 Web 程式,你還是需要使用 Spring MVC 作為 MVC 框架,只是說 Spring Boot 幫你簡化了 Spring MVC 的很多配置,真正做到開箱即用!
7、IoC 和 AOP
IoC
IoC(Inversion of Control:控制反轉) 是一種設計思想,而不是一個具體的技術實現。IoC 的思想就是將原本在程式中手動建立物件的控制權,交由 Spring 框架來管理。不過, IoC 並非 Spring 特有,在其他語言中也有應用。
為什麼叫控制反轉?
- 控制:指的是物件建立(例項化、管理)的權力
- 反轉:控制權交給外部環境(Spring 框架、IoC 容器)
將物件之間的相互依賴關係交給 IoC 容器來管理,並由 IoC 容器完成物件的注入。這樣可以很大程度上簡化應用的開發,把應用從複雜的依賴關係中解放出來。 IoC 容器就像是一個工廠一樣,當我們需要建立一個物件的時候,只需要配置好配置檔案/註解即可,完全不用考慮物件是如何被建立出來的。
在實際專案中一個 Service 類可能依賴了很多其他的類,假如我們需要例項化這個 Service,你可能要每次都要搞清這個 Service 所有底層類的建構函式,這可能會把人逼瘋。如果利用 IoC 的話,你只需要配置好,然後在需要的地方引用就行了,這大大增加了專案的可維護性且降低了開發難度。
在 Spring 中, IoC 容器是 Spring 用來實現 IoC 的載體, IoC 容器實際上就是個 Map(key,value),Map 中存放的是各種物件。
Spring 時代我們一般透過 XML 檔案來配置 Bean,後來開發人員覺得 XML 檔案來配置不太好,於是 SpringBoot 註解配置就慢慢開始流行起來。
AOP
AOP(Aspect-Oriented Programming:面向切面程式設計)能夠將那些與業務無關,卻為業務模組所共同呼叫的邏輯或責任(例如事務處理、日誌管理、許可權控制等)封裝起來,便於減少系統的重複程式碼,降低模組間的耦合度,並有利於未來的可擴充性和可維護性。
Spring AOP 就是基於動態代理的,如果要代理的物件,實現了某個介面,那麼 Spring AOP 會使用 JDK Proxy,去建立代理物件,而對於沒有實現介面的物件,就無法使用 JDK Proxy 去進行代理了,這時候 Spring AOP 會使用 Cglib 生成一個被代理物件的子類來作為代理,如下圖所示:
當然你也可以使用 AspectJ !Spring AOP 已經整合了 AspectJ ,AspectJ 應該算的上是 Java 生態系統中最完整的 AOP 框架了。
AOP 切面程式設計涉及到的一些專業術語:
術語 | 含義 |
---|---|
目標(Target) | 被通知的物件 |
代理(Proxy) | 向目標物件應用通知之後建立的代理物件 |
連線點(JoinPoint) | 目標物件的所屬類中,定義的所有方法均為連線點 |
切入點(Pointcut) | 被切面攔截 / 增強的連線點(切入點一定是連線點,連線點不一定是切入點) |
通知(Advice) | 增強的邏輯 / 程式碼,也即攔截到目標物件的連線點之後要做的事情 |
切面(Aspect) | 切入點(Pointcut)+通知(Advice) |
Weaving(織入) | 將通知應用到目標物件,進而生成代理物件的過程動作 |
8、淺複製和深複製
關於深複製和淺複製區別,我這裡先給結論:
- 淺複製:淺複製會在堆上建立一個新的物件(區別於引用複製的一點),不過,如果原物件內部的屬性是引用型別的話,淺複製會直接複製內部物件的引用地址,也就是說複製物件和原物件共用同一個內部物件。
- 深複製:深複製會完全複製整個物件,包括這個物件所包含的內部物件。
上面的結論沒有完全理解的話也沒關係,我們來看一個具體的案例!
淺複製
淺複製的示例程式碼如下,我們這裡實現了 Cloneable
介面,並重寫了 clone()
方法。
clone()
方法的實現很簡單,直接呼叫的是父類 Object
的 clone()
方法。
public class Address implements Cloneable{
private String name;
// 省略建構函式、Getter&Setter方法
@Override
public Address clone() {
try {
return (Address) super.clone();
} catch (CloneNotSupportedException e) {
throw new AssertionError();
}
}
}
public class Person implements Cloneable {
private Address address;
// 省略建構函式、Getter&Setter方法
@Override
public Person clone() {
try {
Person person = (Person) super.clone();
return person;
} catch (CloneNotSupportedException e) {
throw new AssertionError();
}
}
}
測試:
Person person1 = new Person(new Address("武漢"));
Person person1Copy = person1.clone();
// true
System.out.println(person1.getAddress() == person1Copy.getAddress());
從輸出結構就可以看出, person1
的克隆物件和 person1
使用的仍然是同一個 Address
物件。
深複製
這裡我們簡單對 Person
類的 clone()
方法進行修改,連帶著要把 Person
物件內部的 Address
物件一起復制。
@Override
public Person clone() {
try {
Person person = (Person) super.clone();
person.setAddress(person.getAddress().clone());
return person;
} catch (CloneNotSupportedException e) {
throw new AssertionError();
}
}
測試:
Person person1 = new Person(new Address("武漢"));
Person person1Copy = person1.clone();
// false
System.out.println(person1.getAddress() == person1Copy.getAddress());
從輸出結構就可以看出,顯然 person1
的克隆物件和 person1
包含的 Address
物件已經是不同的了。
那什麼是引用複製呢? 簡單來說,引用複製就是兩個不同的引用指向同一個物件。
我專門畫了一張圖來描述淺複製、深複製、引用複製:
9、List a, list b; a=b 是淺複製還是深複製,怎麼才能深複製?
List a, list b; a=b
本質上來說應該屬於是引用複製,也就是兩個不同的引用指向同一個物件,即 a 和 b 都會指向同一個 List 物件。當你更改 b 時,a 也會相應地更改,反之亦然。
示例程式碼:
List<String> a = new ArrayList<>();
a.add("Element1");
a.add("Element2");
List<String> b = new ArrayList<>();
b.add("Element3");
a=b;
System.out.println(a.hashCode() == b.hashCode());
b.add("Element4");
System.out.println(a);
輸出:
true
[Element3, Element4]
如果想要深複製的話,需要建立一個新的 List
物件,並將原始列表中的元素複製到新列表中。如果列表中的元素本身物件的話,還需要確保這些物件也被複制。
10、介面和抽象類的區別,抽象類的作用
介面和抽象類的共同點:
- 都不能被例項化。
- 都可以包含抽象方法。
- 都可以有預設實現的方法(Java 8 可以用
default
關鍵字在介面中定義預設方法)。
介面和抽象類的區別:
- 介面主要用於對類的行為進行約束,你實現了某個介面就具有了對應的行為。抽象類主要用於程式碼複用,強調的是所屬關係。
- 一個類只能繼承一個類,但是可以實現多個介面。
- 介面中的成員變數只能是
public static final
型別的,不能被修改且必須有初始值,而抽象類的成員變數預設 default,可在子類中被重新定義,也可被重新賦值。
抽象類的作用:
抽象類的作用主要是為子類提供一個共同的模板,定義了一些通用的方法和屬性。子類可以繼承抽象類擁有這些通用屬性並按需實現或覆蓋其中的方法。抽象類是物件導向程式設計中重要的概念,能夠提高程式碼的複用性和可讀性,同時也能夠對類的繼承進行限制。
11、String、StringBuffer、StringBuilder 的區別?
可變性
String
是不可變的。
StringBuilder
與 StringBuffer
都繼承自 AbstractStringBuilder
類,在 AbstractStringBuilder
中也是使用字元陣列儲存字串,不過沒有使用 final
和 private
關鍵字修飾,最關鍵的是這個 AbstractStringBuilder
類還提供了很多修改字串的方法比如 append
方法。
abstract class AbstractStringBuilder implements Appendable, CharSequence {
char[] value;
public AbstractStringBuilder append(String str) {
if (str == null)
return appendNull();
int len = str.length();
ensureCapacityInternal(count + len);
str.getChars(0, len, value, count);
count += len;
return this;
}
//...
}
執行緒安全性
String
中的物件是不可變的,也就可以理解為常量,執行緒安全。AbstractStringBuilder
是 StringBuilder
與 StringBuffer
的公共父類,定義了一些字串的基本操作,如 expandCapacity
、append
、insert
、indexOf
等公共方法。StringBuffer
對方法加了同步鎖或者對呼叫的方法加了同步鎖,所以是執行緒安全的。StringBuilder
並沒有對方法進行加同步鎖,所以是非執行緒安全的。
效能
每次對 String
型別進行改變的時候,都會生成一個新的 String
物件,然後將指標指向新的 String
物件。StringBuffer
每次都會對 StringBuffer
物件本身進行操作,而不是生成新的物件並改變物件引用。相同情況下使用 StringBuilder
相比使用 StringBuffer
僅能獲得 10%~15% 左右的效能提升,但卻要冒多執行緒不安全的風險。
對於三者使用的總結:
- 操作少量的資料: 適用
String
- 單執行緒操作字串緩衝區下操作大量資料: 適用
StringBuilder
- 多執行緒操作字串緩衝區下操作大量資料: 適用
StringBuffer
12、你專案中怎麼向前端傳資料的
後端向前端傳資料的幾種常用途徑:
- RESTful API:使用 HTTP 請求進行資料交換,前端可以透過 GET、POST、PUT 等方法請求服務端資料或者傳送資料到服務端。
- Websocket:提供全雙工通訊渠道,允許服務端和客戶端之間進行實時資料傳輸。
- Server-Sent Events (SSE):允許服務端向客戶端推送實時資料更新,通常用於單向通訊,如推送通知。
這些方法各有優劣,選擇哪種方式取決於應用的需求和特定場景。例如,需要實時雙向通訊可以選擇 Websocket,只需要服務端向客戶端推送資料可以選擇 SSE,標準的客戶端和服務端資料交換可以選擇 RESTful API(這也是平時用的最頻繁的)。