京東後端實習一面(附詳解),秒掛!

今天分享的是一位華中科技大學同學分享的京東一面面經,主要是一些非常基礎的問題,也就是比較簡單且容易準備的常規八股。

這也是這位同學人生的第一次面試,直接秒掛了。其實也挺正常,畢竟缺乏經驗。對於 Java 後端實習面試來說,這位同學面試遇到的問題已經非常簡單了。

很多同學覺得這種基礎問題的考查意義不大,實際上還是很有意義的,這種基礎性的知識在日常開發中也會需要經常用到。例如,線程池這塊的拒絕策略、核心參數配置什麼的,如果你不瞭解,實際項目中使用線程池可能就用的不是很明白,容易出現問題。而且,其實這種基礎性的問題是最容易準備的,像各種底層原理、系統設計、場景題以及深挖你的項目這類纔是最難的!

1、Redis 瞭解嗎,作用?

RedisREmote DIctionary Server)是一個基於 C 語言開發的開源 NoSQL 數據庫(BSD 許可)。與傳統數據庫不同的是,Redis 的數據是保存在內存中的(內存數據庫,支持持久化),因此讀寫速度非常快,被廣泛應用於分佈式緩存方向。並且,Redis 存儲的是 KV 鍵值對數據。

爲了滿足不同的業務場景,Redis 內置了多種數據類型實現(比如 String、Hash、Sorted Set、Bitmap、HyperLogLog、GEO)。並且,Redis 還支持事務、持久化、Lua 腳本、多種開箱即用的集羣方案(Redis Sentinel、Redis Cluster)。

Redis 數據類型概覽

Redis 內部做了非常多的性能優化,比較重要的有下面 3 點:

  1. Redis 基於內存,內存的訪問速度是磁盤的上千倍;
  2. Redis 基於 Reactor 模式設計開發了一套高效的事件處理模型,主要是單線程事件循環和 IO 多路複用(Redis 線程模式後面會詳細介紹到);
  3. 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 數據類型的介紹 和我寫的這兩篇文章:

3、同步和異步的區別

  • 同步:發出一個調用之後,在沒有得到結果之前, 該調用就不可以返回,一直等待。
  • 異步:調用在發出之後,不用等待返回結果,該調用直接返回。

4、創建線程的方法哪些?

一般來說,創建線程有很多種方式,例如繼承Thread類、實現Runnable接口、實現Callable接口、使用線程池、使用CompletableFuture類等等。

不過,這些方式其實並沒有真正創建出線程。準確點來說,這些都屬於是在 Java 代碼中使用多線程的方法。

嚴格來說,Java 就只有一種方式可以創建線程,那就是通過new Thread().start()創建。不管是哪種方式,最終還是依賴於new Thread().start()

關於這個問題的詳細分析可以查看這篇文章:大家都說 Java 有三種創建線程的方式!併發編程中的驚天騙局!

5、線程池作用是什麼?

線程池提供了一種限制和管理資源(包括執行一個任務)的方式。 每個線程池還維護一些基本統計信息,例如已完成任務的數量。

這裏借用《Java 併發編程的藝術》提到的來說一下使用線程池的好處

  • 降低資源消耗。通過重複利用已創建的線程降低線程創建和銷燬造成的消耗。
  • 提高響應速度。當任務到達時,任務可以不需要等到線程創建就能立即執行。
  • 提高線程的可管理性。線程是稀缺資源,如果無限制的創建,不僅會消耗系統資源,還會降低系統的穩定性,使用線程池可以進行統一的分配,調優和監控。

《阿里巴巴 Java 開發手冊》中強制線程池不允許使用 Executors 去創建,而是通過 ThreadPoolExecutor 構造函數的方式,這樣的處理方式讓寫的同學更加明確線程池的運行規則,規避資源耗盡的風險

Executors 返回線程池對象的弊端如下(後文會詳細介紹到):

  • FixedThreadPoolSingleThreadExecutor:使用的是無界的 LinkedBlockingQueue,任務隊列最大長度爲 Integer.MAX_VALUE,可能堆積大量的請求,從而導致 OOM。
  • CachedThreadPool:使用的是同步隊列 SynchronousQueue, 允許創建的線程數量爲 Integer.MAX_VALUE ,如果任務數量過多且執行速度較慢,可能會創建大量的線程,從而導致 OOM。
  • ScheduledThreadPoolSingleThreadScheduledExecutor : 使用的無界的延遲阻塞隊列DelayedWorkQueue,任務隊列最大長度爲 Integer.MAX_VALUE,可能堆積大量的請求,從而導致 OOM。

相關閱讀:

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主要模塊

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 容器完成對象的注入。這樣可以很大程度上簡化應用的開發,把應用從複雜的依賴關係中解放出來。 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 生成一個被代理對象的子類來作爲代理,如下圖所示:

SpringAOPProcess

當然你也可以使用 AspectJ !Spring AOP 已經集成了 AspectJ ,AspectJ 應該算的上是 Java 生態系統中最完整的 AOP 框架了。

AOP 切面編程涉及到的一些專業術語:

術語 含義
目標(Target) 被通知的對象
代理(Proxy) 向目標對象應用通知之後創建的代理對象
連接點(JoinPoint) 目標對象的所屬類中,定義的所有方法均爲連接點
切入點(Pointcut) 被切面攔截 / 增強的連接點(切入點一定是連接點,連接點不一定是切入點)
通知(Advice) 增強的邏輯 / 代碼,也即攔截到目標對象的連接點之後要做的事情
切面(Aspect) 切入點(Pointcut)+通知(Advice)
Weaving(織入) 將通知應用到目標對象,進而生成代理對象的過程動作

8、淺拷貝和深拷貝

關於深拷貝和淺拷貝區別,我這裏先給結論:

  • 淺拷貝:淺拷貝會在堆上創建一個新的對象(區別於引用拷貝的一點),不過,如果原對象內部的屬性是引用類型的話,淺拷貝會直接複製內部對象的引用地址,也就是說拷貝對象和原對象共用同一個內部對象。
  • 深拷貝:深拷貝會完全複製整個對象,包括這個對象所包含的內部對象。

上面的結論沒有完全理解的話也沒關係,我們來看一個具體的案例!

淺拷貝

淺拷貝的示例代碼如下,我們這裏實現了 Cloneable 接口,並重寫了 clone() 方法。

clone() 方法的實現很簡單,直接調用的是父類 Objectclone() 方法。

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 是不可變的。

StringBuilderStringBuffer 都繼承自 AbstractStringBuilder 類,在 AbstractStringBuilder 中也是使用字符數組保存字符串,不過沒有使用 finalprivate 關鍵字修飾,最關鍵的是這個 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 中的對象是不可變的,也就可以理解爲常量,線程安全。AbstractStringBuilderStringBuilderStringBuffer 的公共父類,定義了一些字符串的基本操作,如 expandCapacityappendinsertindexOf 等公共方法。StringBuffer 對方法加了同步鎖或者對調用的方法加了同步鎖,所以是線程安全的。StringBuilder 並沒有對方法進行加同步鎖,所以是非線程安全的。

性能

每次對 String 類型進行改變的時候,都會生成一個新的 String 對象,然後將指針指向新的 String 對象。StringBuffer 每次都會對 StringBuffer 對象本身進行操作,而不是生成新的對象並改變對象引用。相同情況下使用 StringBuilder 相比使用 StringBuffer 僅能獲得 10%~15% 左右的性能提升,但卻要冒多線程不安全的風險。

對於三者使用的總結:

  1. 操作少量的數據: 適用 String
  2. 單線程操作字符串緩衝區下操作大量數據: 適用 StringBuilder
  3. 多線程操作字符串緩衝區下操作大量數據: 適用 StringBuffer

12、你項目中怎麼向前端傳數據的

後端向前端傳數據的幾種常用途徑:

  1. RESTful API:使用 HTTP 請求進行數據交換,前端可以通過 GET、POST、PUT 等方法請求服務端數據或者發送數據到服務端。
  2. Websocket:提供全雙工通信渠道,允許服務端和客戶端之間進行實時數據傳輸。
  3. Server-Sent Events (SSE):允許服務端向客戶端推送實時數據更新,通常用於單向通信,如推送通知。

這些方法各有優劣,選擇哪種方式取決於應用的需求和特定場景。例如,需要實時雙向通信可以選擇 Websocket,只需要服務端向客戶端推送數據可以選擇 SSE,標準的客戶端和服務端數據交換可以選擇 RESTful API(這也是平時用的最頻繁的)。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章