進程與線程
概念
進程和線程作爲必知必會的知識,想來讀者們也都是耳熟能詳了,但真的是這樣嘛?今天我們就來重新捋一捋,看看有沒有什麼知識點欠缺的。
先來一張我隨手截的活動監視器的圖,分清一下什麼叫做進程,什麼叫做線程。
想來很多面試官會問,你對進程和線程的理解是什麼,他們有什麼樣的區別呢?其實不用死記硬背,記住上面的圖就OK了。
正好裏面有個奇形怪狀的App
,我們就拿愛優騰中的愛舉例。
先來插個題外話,今天突然看到愛奇藝給我的推送,推出了新的會員機制 —— 星鑽VIP會員
,超前點播、支持 五臺 設備在線、。。我預計之後可能還會推出新的VIP等級會員
,那我先給他安排一下名字,你看星鑽是不是星耀+鑽石,那下一個等級我們就叫做耀王VIP會員
(榮耀王者)。哇!!太讚了把,愛奇藝運營商過來打錢。🙄🙄🙄🙄,作爲愛奇藝的老黃金VIP用戶
了,女朋友用一下,分享給室友用一下,我自己要麼沒得看到了,要麼只能夜深人靜的時候,🤔🤔🤔🤔,點到爲止好吧,輪到你發揮無限的想象力了。。
收!!回到我們的正題,我們不是講到了進程和線程嘛,那進程是什麼,顯而易見嘛這不是,上面已經寫了一個 進程名稱 了,那顯然就是愛奇藝這整一隻龐然大物嘛。 那線程呢?
你是否看到愛奇藝中的數據加載上並不是一次性的,這些任務的進行就是依靠我們的線程來進行執行的,你可以把這樣的一個個數據加載過程認爲是一條條線程。
生命週期
不管是進程還是線程,生和死是他們必然要去經歷的過程。
進程 | 線程 |
---|---|
你能看到進程中少了兩個狀態,也就是他的出生和他的死亡,不過這是同樣是爲了方便我們去進行記憶。 進程因創建而產生,因調度而執行,因得不到資源而阻塞,因得不到資源而阻塞,因撤銷而消亡。 圖中代表的4個值:
- 得到CPU的時間片 / 調度。
- 時間片用完,等待下一個時間片。
- 等待 I/O 操作 / 等待事件發生。
- I/O操作結束 / 事件完成。
而對於線程,他在Java
的Thread
類中對應了6種狀態,可以自行進行查看。
多線程編程入門
多線程編程就好像我們這樣生活,週末我呆在家裏邊燒開水,邊讓洗衣機洗衣服,邊炒菜,一秒鐘幹三件事,你是不是也有點心動呢?
廢話不多說,我們趕緊入門一下。
// 1
public class MyRunnable implements Runnable {
@Override
public void run() {
System.out.println("this is a Runnable");
}
}
// 2
public class MyThread extends Thread {
@Override
public void run() {
super.run();
System.out.println("this is thread");
}
}
// 具體使用
public class Main {
public static void main(String[] args) {
// 第一種
Thread thread1 = new Thread(new MyRunnable());
thread1.start();
// 第二種
MyThread thread2 = new MyThread();
thread2.start();
}
}
一般來說推薦第一種寫法,也就是重寫Runnable
了。不過這樣的玩意兒存在他全是好事嘛???顯然作爲高手的你們肯定知道他有問題存在了。我們以一段代碼爲例。
public class Main {
public int i = 0;
public void increase(){
I++;
}
public static void main(String[] args) {
final Main main = new Main();
for(int i=0; i< 10; i++){
new Thread(new Runnable() {
@Override
public void run() {
for(int j=0; j<1000; j++){
main.increase();
}
}
}).start();
}
while(Thread.activeCount() > 2){
Thread.yield();
}
System.out.println(main.i);
}
}
這樣的一段程序,你覺得最後跑出來的數據是什麼?他會是10000
嘛?
以答案作爲標準,顯然不是,他甚至說可能下次跑出來也不是我給你的這個數值,但是這是爲什麼呢?這就牽扯到我們的線程同步問題了。
線程同步
一般情況下,我們可以通過三種方式來實現。
- Synchronized
- Lock
- Volatile
在操作系統中,有這麼一個概念,叫做臨界區。其實就是同一時間只能允許存在一個任務訪問的代碼區間。代碼模版如下:
Lock lock = new ReentrantLock();
public void lockModel(){
lock.lock();
// 用於書寫共同代碼,比如說賣同一輛動車的車票等等。
lock.unlock();
}
// 上述模版近似等價於下面的函數
public synchronized void lockModel(){}
其實這就是大家常說的鎖機制,通過加解鎖的方法,來保證數據的正確性。
但是鎖的開銷還是我們需要考慮的範疇,在不太必要時,我們更頻繁的會使用是volatile
關鍵詞來修飾變量,來保證數據的準確性。
對上述的共享變量內存而言,如果線程A和B之間要通信,則必須先更新主內存中的共享變量,然後由另外一個線程去主內存中去讀取。但是普通變量一般是不可見的。而volatile關鍵詞就將這件事情變成了可能。
打個比方,共享變量如果使用了volatile關鍵詞,這個時候線程B改變了共享變量副本,線程A就能夠感知到,然後經歷上述的通信步驟。
這個時候就保障了可見性。
但是另外兩種特性,也就是有序性和原子性中,原子性是無法保障的。拿我們最開始的Main
的類做例子,就只改變一個變量。
public volatile int i = 0;
他最後的數值終究不是10000,這是爲什麼呢?其實對代碼進行反編譯,你能夠注意到這樣的一個問題。
iconst_0 //把數值0 push到操作數棧
istore_1 // 把操作數棧寫回到本地變量第2個位置
iinc 1,1 // 把本地變量表第2個位置加1
iload_1 // 把本地變量第2個位置的值push到操作數棧
istore_1 // 把操作數據棧寫回本地變量第2個位置
一個++i
的操作被反編譯後出現的結果如上,給人的感覺是啥,你還會覺得它是原子操作嗎?
Synchronized
這個章節的最後來簡單介紹一下synchronized
這個老大哥,他從過去的版本被優化後性能高幅度提高。
在他的內部結構依舊和我們Lock
類似,但是存在了這樣的三種鎖。
偏向鎖 ---------> 輕量鎖(棧幀) ---------> 重量鎖(Monitor)
(存在線程爭奪) (自旋一定次數還是拿不到鎖)
三種加鎖對象:
- 實例方法
- 靜態方法
- 代碼塊
public class SyncDemo {
// 對同一個實例加鎖
private synchronized void fun(){}
// 對同一個類加鎖
private synchronized static void fun_static(){}
// 視情況而定
// 1\. this:實例加鎖
// 2\. SyncDemo.class:類加鎖
private void fun_inner(){
synchronized(this){
}
synchronized(SyncDemo.class){
}
}
}
線程池
讓我們先來正題感受一下線程池的工作流程
五大參數
- 任務隊列(workQueue)
- 核心線程數(coolPoolSize): 即使處於空閒狀態,也會被保留下來的線程
- 最大線程數(maximumPoolSize): 核心線程數 + 非核心線程數。控制可以創建的線程的數量。
- 飽和策略(RejectedExecutionHandler)
- 存活時間(keepAliveTime): 設定非核心線程空閒下來後將被銷燬的時間
任務隊列
- 基於數組的有界阻塞隊列(ArrayBlockingQueue): 放入的任務有限,到達上限時會觸發拒絕策略。
- 基於鏈表的無界阻塞隊列(LinkedBlockingQuene): 可以放入無限多的任務。
- 不緩存的隊列(SynchronousQuene): 一次只能進行一個任務的生產和消費。
- 帶優先級的阻塞隊列(PriorityBlockingQueue): 可以設置任務的優先級。
- 帶時延的任務隊列(DelayedWorkQueue)
飽和策略
- CallerRunsPolicy
public static class CallerRunsPolicy implements RejectedExecutionHandler {
// 如果線程池還沒關閉,就在調用者線程中直接執行Runnable
public void rejectedExecution(Runnable r, ThreadPoolExecutor e) {
if (!e.isShutdown()) {
r.run();
}
}
}
- AbortPolicy
public static class AbortPolicy implements RejectedExecutionHandler {
// 拒絕任務,並且拋出RejectedExecutionException異常
public void rejectedExecution(Runnable r, ThreadPoolExecutor e) {
throw new RejectedExecutionException("Task " + r.toString() +
" rejected from " +
e.toString());
}
}
- DiscardPolicy
public static class DiscardPolicy implements RejectedExecutionHandler {
// 拒絕任務,但是啥也不幹
public void rejectedExecution(Runnable r, ThreadPoolExecutor e) {
}
}
- DiscardOldestPolicy
public static class DiscardOldestPolicy implements RejectedExecutionHandler {
// 如果線程池還沒有關閉,就把隊列中最早的任務拋棄,把當前的線程插入
public void rejectedExecution(Runnable r, ThreadPoolExecutor e) {
if (!e.isShutdown()) {
e.getQueue().poll();
e.execute(r);
}
}
}
五種線程池
FixedThreadPool
public static ExecutorService newFixedThreadPool(int nThreads) {
return new ThreadPoolExecutor(nThreads, nThreads,
0L, TimeUnit.MILLISECONDS,
new LinkedBlockingQueue<Runnable>());
}
固定線程池 , 最大線程數和核心線程數的數量相同,也就意味着只有核心線程了,多出的任務,將會被放置到LinkedBlockingQueue中。
CachedThreadPool
public static ExecutorService newCachedThreadPool() {
return new ThreadPoolExecutor(0, Integer.MAX_VALUE,
60L, TimeUnit.SECONDS,
new SynchronousQueue<Runnable>());
}
沒有核心線程,最大線程數爲無窮,適用於頻繁IO的操作,因爲他們的任務量小,但是任務基數非常龐大,使用核心線程處理的話,數量創建方面就很成問題。
ScheduledThreadPool
public ScheduledThreadPoolExecutor(int corePoolSize,
ThreadFactory threadFactory) {
// 最後對應的還是 ThreadPoolExecutor
super(corePoolSize, Integer.MAX_VALUE, 0, NANOSECONDS,
new DelayedWorkQueue(), threadFactory);
}
SingleThreadExecutor
public static ExecutorService newSingleThreadExecutor() {
return new FinalizableDelegatedExecutorService
(new ThreadPoolExecutor(1, 1,
0L, TimeUnit.MILLISECONDS,
new LinkedBlockingQueue<Runnable>()));
}
核心線程數和最大線程數相同,且都爲1,也就意味着任務是按序工作的。
WorkStealingPool
public static ExecutorService newWorkStealingPool() {
return new ForkJoinPool
(Runtime.getRuntime().availableProcessors(), // 可用的處理器數
ForkJoinPool.defaultForkJoinWorkerThreadFactory,
null, true);
}
這是JDK1.8
以後才加入的線程池,引入了搶佔式,雖然這個概念挺早就有了。本質上就是如果當前有兩個核在工作,一個核的任務已經處理完成,而另一個還有大量工作積壓,那我們的這個空閒核就會趕緊衝過去幫忙。
優勢
- 線程的複用
每次使用線程我們是不是需要去創建一個
Thread
,然後start()
,然後就等結果,最後的銷燬就等着垃圾回收機制來了。 但是問題是如果有1000個任務呢,你要創建1000個Thread嗎?如果創建了,那回收又要花多久的時間?
- 控制線程的併發數
存在覈心線程和非核心線程,還有任務隊列,那麼就可以保證資源的使用和爭奪是處於一個可控的狀態的。
- 線程的管理
協程
Q1:什麼是協程? 一種比線程更加輕量級的存在,和進程還有線程不同的地方時他的掌權者不再是操作系統,而是程序了。但是你要注意,協程不像線程,線程最後會被CPU進行操作,但是協程是一種粒度更小的函數,我們可以對其進行控制,他的開始和暫停操作我們可以認爲是C
中的goto
。
我們通過引入Kotlin
的第三方庫來完成一些使用上的講解。
implementation "org.jetbrains.kotlinx:kotlinx-coroutines-core:1.2.1"
引入完成後我們以launch()
爲例來講解。
public fun CoroutineScope.launch(
context: CoroutineContext = EmptyCoroutineContext,
start: CoroutineStart = CoroutineStart.DEFAULT,
block: suspend CoroutineScope.() -> Unit
)
你可以看到3個參數CoroutineContext
、CoroutineStart
、block
。
- CoroutineContext:
- Dispatchers.Default - 默認
- Dispatchers.IO - 適用於IO操作的線程
- Dispatchers.Main - 主線程
- Dispatchers.Unconfined - 沒指定,就是在當前線程
- CoroutineStart:
- DEAFAULT - 默認模式
- ATOMIC - 這種模式下協程執行之前不能被取消
- UNDISPATCHED - 立即在當前線程執行協程體,遇到第一個suspend函數調用
- LAZY - 懶加載模式,需要的時候開啓
- block: 寫一些你要用的方法。
// 當然還有async、runBlocking等用法
GlobalScope.launch(Dispatchers.Default,
CoroutineStart.ATOMIC,
{ Log.e("Main", "run") }
)
Q2:他的優勢是什麼? 其實我們從Q1
中已經進行過了回答,協程的掌權者是程序,那我們就不會再有經過用戶態到內核態的切換,節省了很多的系統開銷。同時我們說過他用的是類似於goto
跳轉方式,就類似於將我們的堆棧空間拆分,這就是我所說的更小粒度的函數,假如我們有3個協程A
、B
、C
在運行,放在主函數中時假如是這樣的壓棧順序,A
、B
、C
。那從C
想要返回A
時勢必要經過B
,而協程我們可以直接去運行A
,這就是協程所帶來的好處。
最後這裏是關於我自己的Android 學習,面試文檔,視頻收集大整理,有興趣的夥伴們可以看看~