前言
最近準備更新 Android 面試必備基礎知識系列,有興趣的可以關注我的微信公衆號 stormjun94,有更新時,第一時間會在微信公衆號上面發佈,同時,也會同步在 GitHub 上面更新,如果覺得對你有所幫助的話,請幫忙 star。
Android 面試必備 - http 與 https 協議
Android 面試必備 - 計算機網絡基本知識(TCP,UDP,Http,https)
java 線程有幾種狀態
一種解釋
java thread的運行週期中, 有幾種狀態, 在 java.lang.Thread.State 中有詳細定義和說明:
NEW
狀態是指線程剛創建, 尚未啓動
RUNNABLE
狀態是線程正在正常運行中,當然可能會有某種耗時計算/IO等待的操作/CPU時間片切換等, 這個狀態下發生的等待一般是其他系統資源, 而不是鎖, Sleep等
BLOCKED
這個狀態下, 是在多個線程有同步操作的場景, 比如正在等待另一個線程的synchronized 塊的執行釋放, 或者可重入的 synchronized塊裏別人調用wait() 方法, 也就是這裏是線程在等待進入臨界區
WAITING
這個狀態下是指線程擁有了某個鎖之後, 調用了他的wait方法, 等待其他線程/鎖擁有者調用 notify / notifyAll 一遍該線程可以繼續下一步操作, 這裏要區分 BLOCKED 和 WATING 的區別, 一個是在臨界點外面等待進入, 一個是在理解點裏面wait等待別人notify, 線程調用了join方法 join了另外的線程的時候, 也會進入WAITING狀態, 等待被他join的線程執行結束
TIMED_WAITING
這個狀態就是有限的(時間限制)的WAITING, 一般出現在調用wait(long), join(long)等情況下, 另外一個線程sleep後, 也會進入TIMED_WAITING狀態
TERMINATED
這個狀態下表示 該線程的run方法已經執行完畢了, 基本上就等於死亡了(當時如果線程被持久持有, 可能不會被回收)
另外一種解釋
https://www.cnblogs.com/barrywxx/p/4343069.html
- 新建狀態(New):新創建了一個線程對象。
- 就緒狀態(Runnable):線程對象創建後,其他線程調用了該對象的start()方法。該狀態的線程位於可運行線程池中,變得可運行,等待獲取CPU的使用權。
- 運行狀態(Running):就緒狀態的線程獲取了CPU,執行程序代碼。
- 阻塞狀態(Blocked):阻塞狀態是線程因爲某種原因放棄CPU使用權,暫時停止運行。直到線程進入就緒狀態,纔有機會轉到運行狀態。阻塞的情況分三種:
- (一)、等待阻塞:運行的線程執行wait()方法,JVM會把該線程放入等待池中。
- (二)、同步阻塞:運行的線程在獲取對象的同步鎖時,若該同步鎖被別的線程佔用,則JVM會把該線程放入鎖池中。
- (三)、其他阻塞:運行的線程執行sleep()或join()方法,或者發出了I/O請求時,JVM會把該線程置爲阻塞狀態。當sleep()狀態超時、join()等待線程終止或者超時、或者I/O處理完畢時,線程重新轉入就緒狀態。
- 死亡狀態(Dead):線程執行完了或者因異常退出了run()方法,該線程結束生命週期。
Java中的鎖分類
在讀很多併發文章中,會提及各種各樣鎖如公平鎖,樂觀鎖等等,這篇文章介紹各種鎖的分類。介紹的內容如下:
- 公平鎖/非公平鎖
- 可重入鎖(已經獲得了鎖,再次嘗試獲取該鎖,可以直接獲得)
- 獨享鎖/共享鎖
- 互斥鎖/讀寫鎖
- 樂觀鎖/悲觀鎖
- 分段鎖
- 偏向鎖/輕量級鎖/重量級鎖
- 自旋鎖
上面是很多鎖的名詞,這些分類並不是全是指鎖的狀態,有的指鎖的特性,有的指鎖的設計,下面總結的內容是對每個鎖的名詞進行一定的解釋。
java 中鎖的類型
公平鎖/非公平鎖
公平鎖是指多個線程按照申請鎖的順序來獲取鎖。
非公平鎖是指多個線程獲取鎖的順序並不是按照申請鎖的順序,有可能後申請的線程比先申請的線程優先獲取鎖。有可能,會造成優先級反轉或者飢餓現象。
對於Java ReentrantLock而言,通過構造函數指定該鎖是否是公平鎖,默認是非公平鎖。非公平鎖的優點在於吞吐量比公平鎖大。
對於Synchronized而言,也是一種非公平鎖。由於其並不像ReentrantLock是通過AQS的來實現線程調度,所以並沒有任何辦法使其變成公平鎖。
可重入鎖
可重入鎖又名遞歸鎖,是指在同一個線程在外層方法獲取鎖的時候,在進入內層方法會自動獲取鎖。說的有點抽象,下面會有一個代碼的示例。
對於 Java ReentrantLock而言, 他的名字就可以看出是一個可重入鎖,其名字是 Reentrant Lock重新進入鎖。
對於Synchronized而言,也是一個可重入鎖。可重入鎖的一個好處是可一定程度避免死鎖。
synchronized void setA() throws Exception{
Thread.sleep(1000);
setB();
}
synchronized void setB() throws Exception{
Thread.sleep(1000);
}
上面的代碼就是一個可重入鎖的一個特點,如果不是可重入鎖的話,setB可能不會被當前線程執行,可能造成死鎖。
- 獨享鎖/共享鎖
獨享鎖是指該鎖一次只能被一個線程所持有。
共享鎖是指該鎖可被多個線程所持有。
對於Java ReentrantLock而言,其是獨享鎖。但是對於Lock的另一個實現類ReadWriteLock,其讀鎖是共享鎖,其寫鎖是獨享鎖。
讀鎖的共享鎖可保證併發讀是非常高效的,讀寫,寫讀 ,寫寫的過程是互斥的。
獨享鎖與共享鎖也是通過AQS來實現的,通過實現不同的方法,來實現獨享或者共享。
對於Synchronized而言,當然是獨享鎖。
- 互斥鎖/讀寫鎖
上面講的獨享鎖/共享鎖就是一種廣義的說法,互斥鎖/讀寫鎖就是具體的實現。
互斥鎖在Java中的具體實現就是ReentrantLock
讀寫鎖在Java中的具體實現就是ReadWriteLock
- 樂觀鎖/悲觀鎖
樂觀鎖與悲觀鎖不是指具體的什麼類型的鎖,而是指看待併發同步的角度。
悲觀鎖認爲對於同一個數據的併發操作,一定是會發生修改的,哪怕沒有修改,也會認爲修改。因此對於同一個數據的併發操作,悲觀鎖採取加鎖的形式。悲觀的認爲,不加鎖的併發操作一定會出問題。
樂觀鎖則認爲對於同一個數據的併發操作,是不會發生修改的。在更新數據的時候,會採用嘗試更新,不斷重新的方式更新數據。樂觀的認爲,不加鎖的併發操作是沒有事情的。
從上面的描述我們可以看出,悲觀鎖適合寫操作非常多的場景,樂觀鎖適合讀操作非常多的場景,不加鎖會帶來大量的性能提升。
悲觀鎖在Java中的使用,就是利用各種鎖。
樂觀鎖在Java中的使用,是無鎖編程,常常採用的是CAS算法,典型的例子就是原子類,通過CAS自旋實現原子操作的更新。
- 分段鎖
分段鎖其實是一種鎖的設計,並不是具體的一種鎖,對於ConcurrentHashMap而言,其併發的實現就是通過分段鎖的形式來實現高效的併發操作。
我們以ConcurrentHashMap來說一下分段鎖的含義以及設計思想,ConcurrentHashMap中的分段鎖稱爲Segment,它即類似於HashMap(JDK7與JDK8中HashMap的實現)的結構,即內部擁有一個Entry數組,數組中的每個元素又是一個鏈表;同時又是一個ReentrantLock(Segment繼承了ReentrantLock)。
當需要put元素的時候,並不是對整個hashmap進行加鎖,而是先通過hashcode來知道他要放在那一個分段中,然後對這個分段進行加鎖,所以當多線程put的時候,只要不是放在一個分段中,就實現了真正的並行的插入。
但是,在統計size的時候,可就是獲取hashmap全局信息的時候,就需要獲取所有的分段鎖才能統計。
分段鎖的設計目的是細化鎖的粒度,當操作不需要更新整個數組的時候,就僅僅針對數組中的一項進行加鎖操作。
- 偏向鎖/輕量級鎖/重量級鎖
這三種鎖是指鎖的狀態,並且是針對Synchronized。在Java 5通過引入鎖升級的機制來實現高效Synchronized。這三種鎖的狀態是通過對象監視器在對象頭中的字段來表明的。
偏向鎖是指一段同步代碼一直被一個線程所訪問,那麼該線程會自動獲取鎖。降低獲取鎖的代價。
輕量級鎖是指當鎖是偏向鎖的時候,被另一個線程所訪問,偏向鎖就會升級爲輕量級鎖,其他線程會通過自旋的形式嘗試獲取鎖,不會阻塞,提高性能。
重量級鎖是指當鎖爲輕量級鎖的時候,另一個線程雖然是自旋,但自旋不會一直持續下去,當自旋一定次數的時候,還沒有獲取到鎖,就會進入阻塞,該鎖膨脹爲重量級鎖。重量級鎖會讓其他申請的線程進入阻塞,性能降低。
- 自旋鎖
在Java中,自旋鎖是指嘗試獲取鎖的線程不會立即阻塞,而是採用循環的方式去嘗試獲取鎖,這樣的好處是減少線程上下文切換的消耗,缺點是循環會消耗CPU。
典型的自旋鎖實現的例子,可以參考自旋鎖的實現
public class SpinLock {
private AtomicReference<Thread> sign =new AtomicReference<>();
public void lock(){
Thread current = Thread.currentThread();
while(!sign .compareAndSet(null, current)){
}
}
public void unlock (){
Thread current = Thread.currentThread();
sign .compareAndSet(current, null);
}
}
死鎖
比如說兩個線程,因爲鎖造成互相等待。具體來說, A,B 線程需要鎖住 a,b 變量,但是因爲 A 線程鎖住了 a 變量,而 b 變量被 B 線程鎖住了,導致無法獲得 b 變量的鎖,而 B 線程需要鎖住 a 變量,造成互相等待。
import java.util.Date;
public class LockTest {
public static String obj1 = "obj1";
public static String obj2 = "obj2";
public static void main(String[] args) {
LockA la = new LockA();
new Thread(la).start();
LockB lb = new LockB();
new Thread(lb).start();
}
}
class LockA implements Runnable{
public void run() {
try {
System.out.println(new Date().toString() + " LockA 開始執行");
while(true){
synchronized (LockTest.obj1) {
System.out.println(new Date().toString() + " LockA 鎖住 obj1");
Thread.sleep(3000); // 此處等待是給B能鎖住機會
synchronized (LockTest.obj2) {
System.out.println(new Date().toString() + " LockA 鎖住 obj2");
Thread.sleep(60 * 1000); // 爲測試,佔用了就不放
}
}
}
} catch (Exception e) {
e.printStackTrace();
}
}
}
class LockB implements Runnable{
public void run() {
try {
System.out.println(new Date().toString() + " LockB 開始執行");
while(true){
synchronized (LockTest.obj2) {
System.out.println(new Date().toString() + " LockB 鎖住 obj2");
Thread.sleep(3000); // 此處等待是給A能鎖住機會
synchronized (LockTest.obj1) {
System.out.println(new Date().toString() + " LockB 鎖住 obj1");
Thread.sleep(60 * 1000); // 爲測試,佔用了就不放
}
}
}
} catch (Exception e) {
e.printStackTrace();
}
}
}
java 生產者消費者
wait() / nofity()方法是基類Object的兩個方法,也就意味着所有Java類都會擁有這兩個方法,這樣,我們就可以爲任何對象實現同步機制。
wait()方法:當緩衝區已滿/空時,生產者/消費者線程停止自己的執行,放棄鎖,使自己處於等等狀態,讓其他線程執行。
notify()方法:當生產者/消費者向緩衝區放入/取出一個產品時,向其他等待的線程發出可執行的通知,同時放棄鎖,使自己處於等待狀態。
光看文字可能不太好理解,咱來段代碼就明白了:
import java.util.LinkedList;
/**
* 倉庫類Storage實現緩衝區
*
* Email:[email protected]
*
* @author MONKEY.D.MENG 2011-03-15
*
*/
public class Storage
{
// 倉庫最大存儲量
private final int MAX_SIZE = 100;
// 倉庫存儲的載體
private LinkedList<Object> list = new LinkedList<Object>();
// 生產num個產品
public void produce(int num)
{
// 同步代碼段
synchronized (list)
{
// 如果倉庫剩餘容量不足
while (list.size() + num > MAX_SIZE)
{
System.out.println("【要生產的產品數量】:" + num + "/t【庫存量】:"
+ list.size() + "/t暫時不能執行生產任務!");
try
{
// 由於條件不滿足,生產阻塞
list.wait();
}
catch (InterruptedException e)
{
e.printStackTrace();
}
}
// 生產條件滿足情況下,生產num個產品
for (int i = 1; i <= num; ++i)
{
list.add(new Object());
}
System.out.println("【已經生產產品數】:" + num + "/t【現倉儲量爲】:" + list.size());
list.notifyAll();
}
}
// 消費num個產品
public void consume(int num)
{
// 同步代碼段
synchronized (list)
{
// 如果倉庫存儲量不足
while (list.size() < num)
{
System.out.println("【要消費的產品數量】:" + num + "/t【庫存量】:"
+ list.size() + "/t暫時不能執行生產任務!");
try
{
// 由於條件不滿足,消費阻塞
list.wait();
}
catch (InterruptedException e)
{
e.printStackTrace();
}
}
// 消費條件滿足情況下,消費num個產品
for (int i = 1; i <= num; ++i)
{
list.remove();
}
System.out.println("【已經消費產品數】:" + num + "/t【現倉儲量爲】:" + list.size());
list.notifyAll();
}
}
// get/set方法
public LinkedList<Object> getList()
{
return list;
}
public void setList(LinkedList<Object> list)
{
this.list = list;
}
public int getMAX_SIZE()
{
return MAX_SIZE;
}
}
/**
* 生產者類Producer繼承線程類Thread
*
* Email:[email protected]
*
* @author MONKEY.D.MENG 2011-03-15
*
*/
public class Producer extends Thread
{
// 每次生產的產品數量
private int num;
// 所在放置的倉庫
private Storage storage;
// 構造函數,設置倉庫
public Producer(Storage storage)
{
this.storage = storage;
}
// 線程run函數
public void run()
{
produce(num);
}
// 調用倉庫Storage的生產函數
public void produce(int num)
{
storage.produce(num);
}
// get/set方法
public int getNum()
{
return num;
}
public void setNum(int num)
{
this.num = num;
}
public Storage getStorage()
{
return storage;
}
public void setStorage(Storage storage)
{
this.storage = storage;
}
}
/**
* 消費者類Consumer繼承線程類Thread
*
* Email:[email protected]
*
* @author MONKEY.D.MENG 2011-03-15
*
*/
public class Consumer extends Thread
{
// 每次消費的產品數量
private int num;
// 所在放置的倉庫
private Storage storage;
// 構造函數,設置倉庫
public Consumer(Storage storage)
{
this.storage = storage;
}
// 線程run函數
public void run()
{
consume(num);
}
// 調用倉庫Storage的生產函數
public void consume(int num)
{
storage.consume(num);
}
// get/set方法
public int getNum()
{
return num;
}
public void setNum(int num)
{
this.num = num;
}
public Storage getStorage()
{
return storage;
}
public void setStorage(Storage storage)
{
this.storage = storage;
}
}
/**
* 測試類Test
*
* Email:[email protected]
*
* @author MONKEY.D.MENG 2011-03-15
*
*/
public class Test
{
public static void main(String[] args)
{
// 倉庫對象
Storage storage = new Storage();
// 生產者對象
Producer p1 = new Producer(storage);
Producer p2 = new Producer(storage);
Producer p3 = new Producer(storage);
Producer p4 = new Producer(storage);
Producer p5 = new Producer(storage);
Producer p6 = new Producer(storage);
Producer p7 = new Producer(storage);
// 消費者對象
Consumer c1 = new Consumer(storage);
Consumer c2 = new Consumer(storage);
Consumer c3 = new Consumer(storage);
// 設置生產者產品生產數量
p1.setNum(10);
p2.setNum(10);
p3.setNum(10);
p4.setNum(10);
p5.setNum(10);
p6.setNum(10);
p7.setNum(80);
// 設置消費者產品消費數量
c1.setNum(50);
c2.setNum(20);
c3.setNum(30);
// 線程開始執行
c1.start();
c2.start();
c3.start();
p1.start();
p2.start();
p3.start();
p4.start();
p5.start();
p6.start();
p7.start();
}
}
在JDK5.0之後,Java提供了更加健壯的線程處理機制,包括同步、鎖定、線程池等,它們可以實現更細粒度的線程控制。await()和signal()就是其中用來做同步的兩種方法,它們的功能基本上和wait() / nofity()相同,完全可以取代它們,但是它們和新引入的鎖定機制Lock直接掛鉤,具有更大的靈活性。通過在Lock對象上調用newCondition()方法,將條件變量和一個鎖對象進行綁定,進而控制併發程序訪問競爭資源的安全。下面來看代碼:
import java.util.LinkedList;
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
/**
* 倉庫類Storage實現緩衝區
*
* Email:[email protected]
*
* @author MONKEY.D.MENG 2011-03-15
*
*/
public class Storage
{
// 倉庫最大存儲量
private final int MAX_SIZE = 100;
// 倉庫存儲的載體
private LinkedList<Object> list = new LinkedList<Object>();
// 鎖
private final Lock lock = new ReentrantLock();
// 倉庫滿的條件變量
private final Condition full = lock.newCondition();
// 倉庫空的條件變量
private final Condition empty = lock.newCondition();
// 生產num個產品
public void produce(int num)
{
// 獲得鎖
lock.lock();
// 如果倉庫剩餘容量不足
while (list.size() + num > MAX_SIZE)
{
System.out.println("【要生產的產品數量】:" + num + "/t【庫存量】:" + list.size()
+ "/t暫時不能執行生產任務!");
try
{
// 由於條件不滿足,生產阻塞
full.await();
}
catch (InterruptedException e)
{
e.printStackTrace();
}
}
// 生產條件滿足情況下,生產num個產品
for (int i = 1; i <= num; ++i)
{
list.add(new Object());
}
System.out.println("【已經生產產品數】:" + num + "/t【現倉儲量爲】:" + list.size());
// 喚醒其他所有線程
full.signalAll();
empty.signalAll();
// 釋放鎖
lock.unlock();
}
// 消費num個產品
public void consume(int num)
{
// 獲得鎖
lock.lock();
// 如果倉庫存儲量不足
while (list.size() < num)
{
System.out.println("【要消費的產品數量】:" + num + "/t【庫存量】:" + list.size()
+ "/t暫時不能執行生產任務!");
try
{
// 由於條件不滿足,消費阻塞
empty.await();
}
catch (InterruptedException e)
{
e.printStackTrace();
}
}
// 消費條件滿足情況下,消費num個產品
for (int i = 1; i <= num; ++i)
{
list.remove();
}
System.out.println("【已經消費產品數】:" + num + "/t【現倉儲量爲】:" + list.size());
// 喚醒其他所有線程
full.signalAll();
empty.signalAll();
// 釋放鎖
lock.unlock();
}
// set/get方法
public int getMAX_SIZE()
{
return MAX_SIZE;
}
public LinkedList<Object> getList()
{
return list;
}
public void setList(LinkedList<Object> list)
{
this.list = list;
}
}
生產者與消費者模式在 Handler 中的體現
面試常見問題
1、進程和線程之間有什麼不同?
一個進程是一個獨立(self contained)的運行環境,它可以被看作一個程序或者一個應用。而線程是在進程中執行的一個任務。Java運行環境是一個包含了不同的類和程序的單一進程。線程可以被稱爲輕量級進程。線程需要較少的資源來創建和駐留在進程中,並且可以共享進程中的資源。
2、用戶線程和守護線程有什麼區別?
當我們在Java程序中創建一個線程,它就被稱爲用戶線程。一個守護線程是在後臺執行並且不會阻止JVM終止的線程。當沒有用戶線程在運行的時候,JVM關閉程序並且退出。一個守護線程創建的子線程依然是守護線程。
3、有哪些不同的線程生命期?
當我們在Java程序中新建一個線程時,它的狀態是New。當我們調用線程的start()方法時,狀態被改變爲Runnable。線程調度器會爲Runnable線程池中的線程分配CPU時間並且講它們的狀態改變爲Running。其他的線程狀態還有Waiting,Blocked 和Dead。讀這篇文章可以瞭解更多關於線程生命週期的知識。
4、什麼是線程調度器(Thread Scheduler)和時間分片(Time Slicing)?
線程調度器是一個操作系統服務,它負責爲Runnable狀態的線程分配CPU時間。一旦我們創建一個線程並啓動它,它的執行便依賴於線程調度器的實現。時間分片是指將可用的CPU時間分配給可用的Runnable線程的過程。分配CPU時間可以基於線程優先級或者線程等待的時間。線程調度並不受到Java虛擬機控制,所以由應用程序來控制它是更好的選擇(也就是說不要讓你的程序依賴於線程的優先級)。
5、你如何確保main()方法所在的線程是Java程序最後結束的線程?
我們可以使用Thread類的join()方法來確保所有程序創建的線程在main()方法退出前結束。這裏有一篇文章關於Thread類的joint()方法。
6、爲什麼線程通信的方法wait(), notify()和notifyAll()被定義在Object類裏?
一個很明顯的原因是JAVA提供的鎖是對象級的而不是線程級的,每個對象都有鎖,通過線程獲得。如果線程需要等待某些鎖那麼調用對象中的wait()方法就有意義了。如果wait()方法定義在Thread類中,線程正在等待的是哪個鎖就不明顯了。
7、 爲什麼wait(), notify()和notifyAll()必須在同步方法或者同步塊中被調用?
當一個線程需要調用對象的wait()方法的時候,這個線程必須擁有該對象的鎖,接着它就會釋放這個對象鎖並進入等待狀態直到其他線程調用這個對象上的notify()方法。同樣的,當一個線程需要調用對象的notify()方法時,它會釋放這個對象的鎖,以便其他在等待的線程就可以得到這個對象鎖。由於所有的這些方法都需要線程持有對象的鎖,這樣就只能通過同步來實現,所以他們只能在同步方法或者同步塊中被調用。
8、 爲什麼Thread類的sleep()和yield()方法是靜態的?
Thread類的sleep()和yield()方法將在當前正在執行的線程上運行。所以在其他處於等待狀態的線程上調用這些方法是沒有意義的。這就是爲什麼這些方法是靜態的。它們可以在當前正在執行的線程中工作,並避免程序員錯誤的認爲可以在其他非運行線程調用這些方法。
9、 volatile關鍵字在Java中有什麼作用?
當我們使用volatile關鍵字去修飾變量的時候,所以線程都會直接讀取該變量並且不緩存它。這就確保了線程讀取到的變量是同內存中是一致的。
10、同步方法和同步塊,哪個是更好的選擇?
同步塊是更好的選擇,因爲它不會鎖住整個對象(當然你也可以讓它鎖住整個對象)。同步方法會鎖住整個對象,哪怕這個類中有多個不相關聯的同步塊,這通常會導致他們停止執行並需要等待獲得這個對象上的鎖。
推薦閱讀
Android 面試必備 - http 與 https 協議
Android 面試必備 - 計算機網絡基本知識(TCP,UDP,Http,https)
掃一掃,歡迎關注我的公衆號 stormjun94。如果你有好的文章,也歡迎你的投稿。