
一. ThreadLocal 的原理
ThreadLocal 相當于一個容器, 用于存放每個線程的局部變量。 ThreadLocal 實例通常來說都是 private static 類型的。 ThreadLocal 可以 給一個初始值,而每個線程都會獲得這個初始化值的一個副本,這樣才能保證 不同的線程都有一份拷貝。
一般情況下,通過 ThreadLocal.set() 到線程中的對象是該線程自己使用 的對象,其他線程是訪問不到的,各個線程中訪問的是不同的對象。如果 ThreadLocal.set()進去的東西本來就是多個線程共享的同一個對象,那么多個 線程的 ThreadLocal.get()取得的還是這個共享對象本身,還是有并發(fā)訪問問 題。
向 ThreadLocal 中 set 的變量是由 Thread 線程對象自身保存的,當用戶 調(diào) 用 ThreadLocal 對象的 set(Object o) 時 , 該方法則通過 Thread.currentThread() 獲取當前線程, 將變量存入線程中的 ThreadLocalMap 類的對象內(nèi),Map 中元素的鍵為當前的 threadlocal 對象, 而值對應線程的變量副本。
public T get() {
Thread t = Thread.currentThread(); //每個 Thread 對象內(nèi)都保存一個 ThreadLocalMap 對象。
ThreadLocalMap map = getMap(t); //map 中 元 素 的 鍵 為 共 用 的
threadlocal 對象,而值為對應線程的變量副本。
if (map != null)
return (T)map.get(this);
}
T value = initialValue();
createMap(t, value);
return value;
}
public void set(T value) {
Thread t = Thread.currentThread();
ThreadLocalMap map = getMap(t);
if (map != null)
map.set(this, value);
else
createMap(t, value);
}
ThreadLocalMap getMap(Thread t) {
return t.threadLocals;
}
void createMap(Thread t, T firstValue) {
t.threadLocals = new ThreadLocalMap(this, firstValue);
}
二. Collections.synchronizedXX 方法的原理。
public E get(int index) {
synchronized (mutex) {
return list.get(index);
}
}
public E set(int index, E element) {
synchronized (mutex) {
return list.set(index,element);
}
}
public void add(int index, E element) {
synchronized (mutex) {
list.add(index, element);
}
}
public E remove(int index) {
synchronized (mutex) {
return list.remove(index);
}
}
在返回的列表上進行迭代時,用戶必須手工在返回的列表上進行同步:
List list = Collections.synchronizedList(new ArrayList());
...
synchronized(list) {
Iterator i = list.iterator();
// Must be in synchronized block
while (i.hasNext())
foo(i.next());
}
三. 如何在兩個線程間共享數(shù)據(jù)?
1. 每個線程執(zhí)行的代碼相同
若每個線程執(zhí)行的代碼相同,共享數(shù)據(jù)就比較方便??梢允褂猛粋€ Runnable 對象,這個 Runnable 對象中就有那個共享數(shù)據(jù)。
public class MultiThreadShareData1 {
public static void main(String[] args) {
SaleTickets sale = new SaleTickets();
new Thread(sale).start();
new Thread(sale).start();
}
}
class SaleTickets implements Runnable {
public int allTicketCount = 20;
public void run() {
while (allTicketCount > 0) {
sale();
}
}
public synchronized void sale() {
System.out.println("剩下" + allTicketCount);
allTicketCount--;
}
}
2. 每個線程執(zhí)行的代碼不相同
如果每個線程執(zhí)行的代碼不同,這時候需要用不同的 Runnable 對象,將需要共享的 數(shù)據(jù)封裝成一個對象,將該對象傳給執(zhí)行不同代碼的 Runnable 對象。
三. 簡述 JAVA 的內(nèi)存模型。
區(qū)別于“JVM 的內(nèi)存模型”。
Java 內(nèi)存模型規(guī)定所有的變量都是存在主存當中(類似于前面說的物理內(nèi) 存),每個線程都有自己的工作內(nèi)存(類似于前面的高速緩存)。線程對變量 的所有操作都必須在工作內(nèi)存中進行,而不能直接對主存進行操作,并且每個 線程不能訪問其他線程的工作內(nèi)存。
Java 內(nèi) 存 模 型 的 Volatile 關 鍵 字 和 原 子 性 、 可 見 性 、 有 序 性 和 happens-before 關系等。
1. Volatile 關鍵字 解析見上面。
2. 要想并發(fā)程序正確地執(zhí)行,必須要保證原子性、可見性以及有序性。 只要有一個沒有被保證,就有可能會導致程序運行不正確。
①.原子性:即一個操作或者多個操作 要么全部執(zhí)行并且執(zhí)行的過程不會被任 何因素打斷,要么就都不執(zhí)行。 可以通過 Synchronized 和 Lock 實現(xiàn)“原子性”。
例題:請分析以下哪些操作是原子性操作。
x = 10;
//語句 1
y = x;
//語句 2
x++;
//語句 3
x = x + 1;
//語句 4
特別注意,在 java 中,只有對除 long 和 double 外的基本類型進行簡 單的賦值(如 int a=1)或讀取操作,才是原子的。只要給 long 或 double 加上 volatile,操作就是原子的了。
- 語句 1 是原子性操作,其他三個語句都不是原子性操作。
- 語句 2 實際上包含 2 個操作,它先要去讀取 x 的值,再將 x 的值寫入工作 內(nèi)存,雖然讀取 x 的值以及將 x 的值寫入工作內(nèi)存這 2 個操作都是原子性操作, 但是合起來就不是原子性操作了。
- 同樣的,x++和 x = x+1 包括 3 個操作:讀取 x 的值,進行加 1 操作, 寫入新的值。
2.可見性:是指當多個線程訪問同一個變量時,一個線程修改了這個變量的值,其他線 程能夠立即看得到修改的值。
通過 Synchronized 和 Lock 和 volatile 實現(xiàn)“可見性”。
3.有序性:即程序執(zhí)行的順序按照代碼的先后順序執(zhí)行。
我的理解就是一段程序代碼的執(zhí)行在單個線程中看起來是有序的。這個應該是程序看 起來執(zhí)行的順序是按照代碼順序執(zhí)行的,因為虛擬機可能會對程序代碼進行指令重排序。 雖然進行重排序,但是最終執(zhí)行的結果是與程序順序執(zhí)行的結果一致的,它只會對不存在 數(shù)據(jù)依賴性的指令進行重排序。因此,在單個線程中,程序執(zhí)行看起來是有序執(zhí)行的,這 一點要注意理解。事實上,這個規(guī)則是用來保證程序在單線程中執(zhí)行結果的正確性,但無 法保證程序在多線程中執(zhí)行的正確性。
3.happens-before 原則(先行發(fā)生原則):
- 程序次序規(guī)則:一個線程內(nèi),按照代碼順序,書寫在前面的操作先行發(fā)生于書寫在 后面的操作
- 鎖定規(guī)則:一個 unLock 操作先行發(fā)生于后面對同一個鎖的 lock 操作
- volatile 變量規(guī)則:對一個變量的寫操作先行發(fā)生于后面對這個變量的讀操作
- 傳遞規(guī)則:如果操作 A 先行發(fā)生于操作 B,而操作 B 又先行發(fā)生于操作 C,則可以 得出操作 A 先行發(fā)生于操作 C
- 線程啟動規(guī)則:Thread 對象的 start()方法先行發(fā)生于此線程的每個一個動作
- 線程中斷規(guī)則:對線程 interrupt()方法的調(diào)用先行發(fā)生于被中斷線程的代碼檢測 到中斷事件的發(fā)生
- 線程終結規(guī)則:線程中所有的操作都先行發(fā)生于線程的終止檢測,我們可以通過 T hread.join()方法結束、Thread.isAlive()的返回值手段檢測到線程已經(jīng)終止執(zhí)行
- 對象終結規(guī)則:一個對象的初始化完成先行發(fā)生于他的 finalize()方法的開始
四. Java 中的同步容器類和缺陷。
在 Java 中,同步容器主要包括 2 類:
- Vector、HashTable。
- Collections 類中提供的靜態(tài)工廠方法創(chuàng)建的類。 Collections.synchronizedXXX()。
缺陷:
①. 性能問題:在有多個線程進行訪問時,如果多個線程都只是進行讀取操作,那么每個 時刻就只能有一個線程進行讀取,其他線程便只能等待,這些線程必須競爭同 一把鎖。
②. ConcurrentModificationException 異常:在對 Vector 等容器進行迭代修改時,會報 ConcurrentModificationException 異常。但是在并發(fā)容器中(如 ConcurrentHashMap,CopyOnWriteArrayList 等)不會出現(xiàn)這個問 題。
五. Java 中堆和棧有什么不同?
棧是一塊和線程緊密相關的內(nèi)存區(qū)域。每個線程都有自己的棧內(nèi)存,用于 存儲本地變量,方法參數(shù)和棧調(diào)用,一個線程中存儲的變量對其它線程是不可 見的。而堆是所有線程共享的一片公用內(nèi)存區(qū)域。對象都在堆里創(chuàng)建,為了提 升效率線程會從堆中弄一個緩存到自己的棧,如果多個線程使用該變量就可能 引發(fā)問題,這時 volatile 變量就可以發(fā)揮作用了,它要求線程從主存中讀取變 量的值。
六. 網(wǎng)站的高并發(fā),大流量訪問怎么解決?
1.HTML 頁面靜態(tài)化
訪問頻率較高但內(nèi)容變動較小,使用網(wǎng)站 HTML 靜態(tài)化方案來優(yōu)化訪問速度。將社區(qū) 內(nèi)的帖子、文章進行實時的靜態(tài)化,有更新的時候再重新靜態(tài)化也是大量使用的策略。
優(yōu)勢 :
- 減輕服務器負擔。
- 加快頁面打開速度,靜態(tài)頁面無需訪問數(shù)據(jù)庫,打開速度較動態(tài)頁面有明顯提高;
- 很多搜索引擎都會優(yōu)先收錄靜態(tài)頁面,不僅被收錄的快,還收錄的全,容易被搜 索引擎找到;
- HTML 靜態(tài)頁面不會受程序相關漏洞的影響,減少攻擊 ,提高安全性。
2.圖片服務器和應用服務器相分離
現(xiàn)在很多的網(wǎng)站上都會用到大量的圖片,而圖片是網(wǎng)頁傳輸中占主要的數(shù)據(jù)量,也是影 響網(wǎng)站性能的主要因素。因此很多網(wǎng)站都會將圖片存儲從網(wǎng)站中分離出來,另外架構一個 或多個服務器來存儲圖片,將圖片放到一個虛擬目錄中,而網(wǎng)頁上的圖片都用一個 URL 地 址來指向這些服務器上的圖片的地址,這樣的話網(wǎng)站的性能就明顯提高了。
優(yōu)勢:
- 分擔 Web 服務器的 I/O 負載-將耗費資源的圖片服務分離出來,提高服務器的性能 和穩(wěn)定性。
- 能夠?qū)iT對圖片服務器進行優(yōu)化-為圖片服務設置有針對性的緩存方案,減少帶寬 成本,提高訪問速度。
- 提高網(wǎng)站的可擴展性-通過增加圖片服務器,提高圖片吞吐能力。
3.數(shù)據(jù)庫
見“數(shù)據(jù)庫部分的---如果有一個特別大的訪問量到數(shù)據(jù)庫上,怎么做優(yōu)化?”。
4.緩存
盡量使用緩存,包括用戶緩存,信息緩存等,多花點內(nèi)存來做緩存,可以大量減少與 數(shù)據(jù)庫的交互,提高性能。
假如我們能減少數(shù)據(jù)庫頻繁的訪問,那對系統(tǒng)肯定大大有利的。比如一個電子商務系 統(tǒng)的商品搜索,如果某個關鍵字的商品經(jīng)常被搜,那就可以考慮這部分商品列表存放到緩 存(內(nèi)存中去),這樣不用每次訪問數(shù)據(jù)庫,性能大大增加。
5.鏡像
鏡像是冗余的一種類型,一個磁盤上的數(shù)據(jù)在另一個磁盤上存在一個完全相同的副本 即為鏡像。
6.負載均衡
在網(wǎng)站高并發(fā)訪問的場景下,使用負載均衡技術(負載均衡服務器)為一個應用構建 一個由多臺服務器組成的服務器集群,將并發(fā)訪問請求分發(fā)到多臺服務器上處理,避免單 一服務器因負載壓力過大而響應緩慢,使用戶請求具有更好的響應延遲特性。
7.并發(fā)控制
加鎖,如樂觀鎖和悲觀鎖。
8. 消息隊列
通過 mq 一個一個排隊方式,跟 12306 一樣。
七. 可擴展到任何高并發(fā)網(wǎng)站要考慮的并發(fā)讀寫問題
訂票系統(tǒng),某車次只有一張火車票,假定有 1w 個人同 時打開 12306 網(wǎng)站來訂票,如何解決并發(fā)問題?(可擴展 到任何高并發(fā)網(wǎng)站要考慮的并發(fā)讀寫問題)。
不但要保證 1w 個人能同時看到有票(數(shù)據(jù)的可讀性),還要保證最終只能 由一個人買到票(數(shù)據(jù)的排他性)。
使用數(shù)據(jù)庫層面的并發(fā)訪問控制機制。采用樂觀鎖即可解決此問題。樂觀 鎖意思是不鎖定表的情況下,利用業(yè)務的控制來解決并發(fā)問題,這樣既保證數(shù) 據(jù)的并發(fā)可讀性,又保證保存數(shù)據(jù)的排他性,保證性能的同時解決了并發(fā)帶來 的臟數(shù)據(jù)問題。hibernate 中實現(xiàn)樂觀鎖。
銀行兩操作員同時操作同一賬戶就是典型的例子。比如 A、B 操作員同 時讀取一余額為 1000 元的賬戶,A 操作員為該賬戶增加 100 元,B 操作員同時 為該賬戶減去 50 元, A 先提交, B 后提交。 最后實際賬戶余額為 1000-50=950 元,但本該為 1000+100-50=1050。這就是典型的并發(fā)問題。如何解決?可以 用鎖。
八. 編程實現(xiàn)一個最大元素為 100 的阻塞隊列。
Lock lock = new ReentrantLock();
Condition notFull = lock.newCondition();
Condition notEmpty = lock.newCondition();
Object[] items = new Object[100];
int putptr, takeptr, count;
public void put(Object x) throws InterruptedException {
lock.lock();
try {
while (count == items.length)
notFull.await();
items[putptr] = x;
if (++putptr == items.length)
putptr = 0;
++count;
notEmpty.signal();
}
finally {
lock.unlock();
}
}
public Object take() throws InterruptedException {
lock.lock();
try {
while (count == 0)
notEmpty.await();
Object x = items[takeptr];
if (++takeptr == items.length)
takeptr = 0;
--count;
notFull.signal();
return x;
}
finally {
lock.unlock();
}
}
九. 設計一個雙緩沖阻塞隊列,寫代碼。
在服務器開發(fā)中,通常的做法是把邏輯處理線程和 I/O 處理線程分離。
- 邏輯處理線程:對接收的包進行邏輯處理。
- I/0 處理線程:網(wǎng)絡數(shù)據(jù)的發(fā)送和接收,連接的建立和維護。
通常邏輯處理線程和 I/O 處理線程是通過數(shù)據(jù)隊列來交換數(shù)據(jù),就是生產(chǎn) 者--消費者模型。
這個數(shù)據(jù)隊列是多個線程在共享,每次訪問都需要加鎖,因此如何減少互 斥/同步的開銷就顯得尤為重要。解決方案:雙緩沖隊列。
兩個隊列,將讀寫分離,一個給邏輯線程讀,一個給 IO 線程用來寫,當 邏輯線程讀完隊列后會將自己的隊列與 IO 線程的隊列相調(diào)換。這里需要加鎖的 地方有兩個,一個是 IO 線程每次寫隊列時都要加鎖,另一個是邏輯線程在調(diào)換 隊列時也需要加鎖,但邏輯線程在讀隊列時是不需要加鎖的。如果是一塊緩沖 區(qū),讀、寫操作是不分離的,雙緩沖區(qū)起碼節(jié)省了單緩沖區(qū)時讀部分操作互斥/ 同步的開銷。本質(zhì)是采用空間換時間的優(yōu)化思路。
十. Java 中的隊列都有哪些,有什么區(qū)別。
- 隊列都實現(xiàn)了 Queue 接口。
- 阻塞隊列和非阻塞隊列。
- 阻塞隊列:見上面的講解。
- 非阻塞隊列:LinkedList,PriorityQueue。
寫在最后
作為一名Java程序員,想進BAT只學多線程還遠遠不夠!
想進BAT,像Kafka、Mysql、Tomcat、Docker、Spring、MyBatis、Nginx、Netty、Dubbo、Redis、Netty、Spring cloud、分布式、高并發(fā)、性能調(diào)優(yōu)、微服務等架構技術你都要學習!
當然以上技術能夠掌握百分之八九十的話,進阿里P8還是沒什么大問題的!
那么筆者也針對以上技術整理了一份完整的面試資料(詳見下圖)
需要的朋友,點擊下方傳送門即可免費領??!
傳送門
以下是部分面試題截圖
