[Java多線程]-《實戰Java高併發程序設計》多線程併發處理筆記

實戰Java高併發程序設計(葛一鳴,郭超)讀書筆記

獲取方式:
http://www.java1234.com/a/javabook/javabase/2018/1224/12627.html

一. Java線程概念

1. 同步與異步的區別

在這裏插入圖片描述
同步就是發出一個功能調用時,在沒有得到結果之前,該調用就不返回或繼續執行後續操作。
在這裏插入圖片描述
異步與同步相對,當一個異步過程調用發出後,調用者在沒有得到結果之前,就可以繼續執行後續操作。當這個調用完成後,一般通過狀態、通知和回調來通知調用者。對於異步調用,調用的返回並不受調用者控制

2. 並行和併發的區別

在這裏插入圖片描述
併發就是串行分割任務執行,並行是完全同時執行
如果只有一個cpu頻率很高,把兩件事情分割n,m份,穿插去執行,一下A,一下B,人看來就是感覺是A,B在同時執行,實際是併發
但如果有兩個cpu,一個cpu跑A,一個CPU跑B,人看來也是A,B在同時進行,但這個是並行

3. 臨界區:

指的是一個訪問共用資源(例如:共用設備或是共用存儲器)的程序片段,而這些共用資源又無法同時被多個線程訪問的特性。當有線程進入臨界區段時,其他線程或是進程必須等待。

4. 阻塞,非阻塞 :

阻塞調用是指調用結果返回之前,當前線程會被掛起。調用線程只有在得到結果之後纔會返回。
非阻塞調用指在不能立刻得到結果之前,該調用不會阻塞當前線程。

5. 死鎖 :

多線程中最差的一種情況,多個線程相互佔用對方的資源的鎖,而又相互等對方釋放鎖,此時若無外力干預,這些線程則一直處理阻塞的假死狀態,形成死鎖。

死鎖產生的4個必要條件:
1、互斥:某種資源一次只允許一個進程訪問,即該資源一旦分配給某個進程,其他進程就不能再訪問,直到該進程訪問結束。
2、佔有且等待:一個進程本身佔有資源(一種或多種),同時還有資源未得到滿足,正在等待其他進程釋放該資源。
3、不可搶佔:別人已經佔有了某項資源,你不能因爲自己也需要該資源,就去把別人的資源搶過來。
4、循環等待:存在一個進程鏈,使得每個進程都佔有下一個進程所需的至少一種資源。
https://blog.csdn.net/wljliujuan/article/details/79614019

6 .飢餓 :

低優先級的線程長時間獲取不到臨界區運行而掛起的現象

7. 活鎖 :

多個線程主動都互相謙讓臨界區給對方,導致都拿不到臨界區運行的現象

8. 無鎖 :

即沒有對資源進行鎖定,即所有的線程都能訪問並修改同一個資源,但同時只有一個線程能修改成功。

無鎖典型的特點就是一個修改操作在一個循環內進行,線程會不斷的嘗試修改共享資源,如果沒有衝突就修改成功並退出否則就會繼續下一次循環嘗試。所以,如果有多個線程修改同一個值必定會有一個線程能修改成功,而其他修改失敗的線程會不斷重試直到修改成功。

二. Java線程狀態

1.線程的狀態:

java/lang/Thread.java中有枚舉出線程的6種狀態:

  public enum State {
       NEW,
       RUNNABLE,
       BLOCKED,
       WAITING,
       TIMED_WAITING,
       TERMINATED;
   }
狀態名稱 說明
NEW 初始狀態,線程被構建,但是還沒有調用start()方法
RUNNABLE 運行狀態,Java線程將操作系統中的就緒和運行兩種狀態籠統地稱作“運行中”
BLOCKED 阻塞狀態,表示線程阻塞於鎖
WAITING 等待狀態,表示線程進入等待狀態,進入該狀態表示當前線程需要等待其他線程做出一些特定動作(通知或中斷)
TIME_WAITING 超時等待狀態,該狀態不同於WAITING,它是可以在指定的時間自行返回的
TERMINATED 終止狀態,表示當前線程已經執行完畢

2.線程狀態切換:

在這裏插入圖片描述

三. Java線程的操作

1. 新建線程 :

new 一個thread並通過start起來
調用Thread的start方法和run方法的區別:
調用run方法是在當前調用線程中串行執行,相當於簡單的調用了Thread的一個方法,未新開一個線程跑;
看start方法調用過程,的確是重新創建了一個新的線程在跑:
java/lang/Thread.java

public synchronized void start() {
        boolean started = false;
        try {
            start0();
            started = true;
        } finally {
        }
}

private native void start0();

libcore/ojluni/src/main/native/Thread.c

static JNINativeMethod methods[] = {
    {"start0",           "(JZ)V",        (void *)&JVM_StartThread},
    {"setPriority0",     "(I)V",       (void *)&JVM_SetThreadPriority},
    {"yield",            "()V",        (void *)&JVM_Yield},
    {"sleep",            "(Ljava/lang/Object;J)V",       (void *)&JVM_Sleep},
    {"currentThread",    "()" THD,     (void *)&JVM_CurrentThread},
    {"interrupt0",       "()V",        (void *)&JVM_Interrupt},
    {"isInterrupted",    "(Z)Z",       (void *)&JVM_IsInterrupted},
    {"holdsLock",        "(" OBJ ")Z", (void *)&JVM_HoldsLock},
    {"setNativeName",    "(" STR ")V", (void *)&JVM_SetNativeThreadName},
};

art/runtime/openjdkjvm/OpenjdkJvm.cc

JNIEXPORT void JVM_StartThread(JNIEnv* env, jobject jthread, jlong stack_size, jboolean daemon) {
  art::Thread::CreateNativeThread(env, jthread, stack_size, daemon == JNI_TRUE);
}

runtime/thread.cc

void Thread::CreateNativeThread(JNIEnv* env, jobject java_peer, size_t stack_size, bool is_daemon) {
  CHECK(java_peer != nullptr);
  Thread* self = static_cast<JNIEnvExt*>(env)->self;
  Thread* child_thread = new Thread(is_daemon);
  ...
   // 創建JNIEnvExt Try to allocate a JNIEnvExt for the thread.
   std::unique_ptr<JNIEnvExt> child_jni_env_ext(
       JNIEnvExt::Create(child_thread, Runtime::Current()->GetJavaVM(), &error_msg));
   ...
   int pthread_create_result = 0;
   if (child_jni_env_ext.get() != nullptr) {
     ...
     // 創建線程
     pthread_create_result = pthread_create(&new_pthread,
                                            &attr,
                                            Thread::CreateCallback,
                                            child_thread);
     CHECK_PTHREAD_CALL(pthread_attr_destroy, (&attr), "new thread");
     ...
   } 
}

2. 終止線程

Thread.stop()方法在結束線程時,會直接終止線程,並且會立即釋放這個線程鎖持有的鎖。如果線程寫數據寫了一半被終止,其他線程就有可能獲取到鎖而可以讀操作,結果讀出來一個寫了一半的錯誤值。
該方法已經Deprecated,如果不是很清楚確定stop操作不會帶來負面影響,就不要使用了。

3. 線程中斷

Thread.interrupt()方法調用後線程並不會立即退出,而是會給線程發一個通知,而目標線程收到通知後如何操作取決與目標線程自己的操作

如果收到中斷後需要退出需要目標線程自己實現退出操作,如果中斷收到通知後線程無條件stop退出的話就會遇到上面的風險。

在Thread.sleep()方法使用時需要catch一個InterruptedException,即中斷異常處理,Thread.sleep() 拋出中斷異常之後會清除中斷標記,如果不加處理,在下一次循環時則無法處理這個中斷,故在異常處理位置再次設置中斷標記位。

另外,在捕獲中斷異常之後做一些數據正確性的回退或設置標誌等操作來避免操作一半退出其他線程獲得鎖來讀出錯誤數據的情況。

4. 等待wait()和通知notify()

wait()和notify()這兩個方法不是Thread中的,而是object類中的,意味着任何對象都可以調用這兩個方法。

如果一個線程調用來object.wait()方法,那麼它就會進入該object的等待隊列中,這個等待隊列中可能有多個等待該object的線程;

當object.notify()調用之後,會從這個等待隊列中隨機選擇一個線程並將其喚醒,這個選擇是不公平的,不是誰先來誰先被調度,而是完全是隨機的。object.notifyAll()則是喚醒這個隊列中的所有線程。

object.wait()方法不能隨意調用,需要在鎖內,無論wait()還是notify()都需要先獲得對應的鎖才能操作。

object.wait() 和Thread.sleep()都可以讓線程等待若干時間,但是不同是objetc.wait()會釋放目標對象鎖,而Thread.sleep()不會釋放任何資源。

5. 掛起suspend()和繼續執行resume()

廢棄方法,由於suspend()在導致線程暫停的同時不會釋放任何鎖資源,會牽連其他線程獲得鎖。另外suspend()之後的線程狀態依然是Runnable。

6. 等待線程結束(join)和謙讓(yield)

在調用線程中調用T1的join()方法,調用線程會等待T1運行完畢退出後在繼續執行下面的操作。join(long millis)可以指定等到超時。
join()的本質上讓調用線程wait()在目標線程的對象實例上,當目標線程執行完,目標線程在退出志強調用notifyAll()方法通知等待線程繼續執行。

yield()方法調用可以會使目標線程讓出CPU:
yield是一個靜態的原生(native)方法
yield告訴當前正在執行的線程把運行機會交給線程池中擁有相同優先級的線程。
yield不能保證使得當前正在運行的線程迅速轉換到可運行的狀態
它僅能使一個線程從運行狀態轉到可運行狀態,而不是等待或阻塞狀態

四. Java線程的同步手段

4.1 線程安全控制方法

一. volatile:

正確使用 Volatile 變量:https://www.ibm.com/developerworks/cn/java/j-jtp06197.html

  1. volatile可以用來修飾字段(成員變量),就是告知程序任何對該變量的訪問均需要從共享內存中獲取,而對它的改變必須同步刷新回共享內存,它能保證所有線程對變量訪問的可見性。
  2. 對其修飾的變量操作不會進行指令重排

二. synchronized

可以修飾方法或者以同步塊的形式來進行使用,它主要確保多個線程在同一個時刻,只能有一個線程處於方法或者同步塊中,它保證了線程對變量訪問的可見性和排他性。

讓你徹底理解Synchronized:https://www.jianshu.com/p/d53bf830fa09
線程安全和鎖Synchronized概念:https://blog.csdn.net/xlgen157387/article/details/77920497
在這裏插入圖片描述

三. ReentrantLock

重入鎖:ReentrantLock 詳解:https://blog.csdn.net/Somhu/article/details/78874634

  1. 有加鎖就必須有釋放鎖,而且加鎖與釋放鎖的分數要相同
  2. 中斷響應
    lock.lockInterruptibly(); // 以可以響應中斷的方式加鎖
    Thread.interrupt()
  3. 可以使用 tryLock()或者tryLock(long timeout, TimeUtil unit) 方法進行一次限時的鎖等待
  4. 使用重入鎖(默認是非公平鎖)創建公平鎖,維護了一個有序隊列,實現成本高,性能較低。

四. Condition條件

和ReentrantLock配合使用,ArrayBlockingQueue即是兩者配合實現
基本使用,具體可以看
java併發編程之Condition:https://www.jianshu.com/p/be2dc7c878dc
Java併發——使用Condition線程間通信:https://www.cnblogs.com/shijiaqi1066/p/3412346.html

  1. 基本接口
void await() throws InterruptedException //當前線程進入等待狀態,直到被通知(signal)或者被中斷時,當前線程進入運行狀態,從await()返回;
boolean await(long time, TimeUnit unit) throws InterruptedException//
同樣是在接口1的返回條件基礎上增加了超時響應,與接口3不同的是:
可以自定義超時時間單位;
void signal()
喚醒一個等待在Condition上的線程;
void signalAll()
喚醒等待在Condition上所有的線程。
  1. 和object.wait()和notify()方法一樣,當線程使用Contition.wait()和signal()時要先獲得鎖,在Contition.wait()調用之後會釋放這把鎖,在signal()調用之後系統會從當前contition對象的等待隊列中喚醒一個線程,如過該線程獲取到鎖就開始執行了,所以一般signal()之後會主動去釋放相關鎖,讓給喚醒的線程。

五. 信號量 semaphore

Java之 Semaphore信號量的原理和示例:https://blog.csdn.net/Emmanuel__/article/details/78586945
允許多個線程同時訪問,Synchronized和ReentrantLock 一次只允許一個線程訪問一個資源,而信號量可以指定多個線程,同時訪問某一個資源。

  1. 構造信號量時指定準入數量
// 創建具有給定的許可數和非公平的公平設置的 Semaphore。
Semaphore(int permits)
// 創建具有給定的許可數和給定的公平設置的 Semaphore。
Semaphore(int permits, boolean fair)
  1. 基本接口:
// 從此信號量獲取一個許可,在提供一個許可前一直將線程阻塞,否則線程被中斷。
void acquire()
// 從此信號量獲取給定數目的許可,在提供這些許可前一直將線程阻塞,或者線程已被中斷。
void acquire(int permits)
// 從此信號量中獲取許可,在有可用的許可前將其阻塞。
void acquireUninterruptibly()
// 從此信號量獲取給定數目的許可,在提供這些許可前一直將線程阻塞。
void acquireUninterruptibly(int permits)
// 返回此信號量中當前可用的許可數。

  1. 注意申請信號量使用acquire()操作,離開時務必要i使用release()釋放該信號量,如果發生量信號量泄漏,acquire了但沒有release便會導致可入線程越來越少。

六. ReadWriteLock

【Java併發】ReadWriteLock讀寫鎖的使用:https://www.jianshu.com/p/9cd5212c8841

public interface ReadWriteLock {
    Lock readLock();
    Lock writeLock();
}
  1. 讀寫分離鎖可以有效的幫助鎖競爭,以提升系統性能
  2. 在讀多寫少的情況下性能提升很高

七. 倒計時器

兩種常用的線程計數器CountDownLatch和循環屏障CyclicBarrier:https://blog.csdn.net/xlgen157387/article/details/78218736
CountDownLatch是一個非常實用的多線程控制工具類,稱之爲“倒計時器”,它允許一個或多個線程一直等待,直到其他線程的操作執行完後再執行。

八. 循環柵欄

CyclicBarrier是另一種多線程併發控制使用工具,和CountDownLatch非常類似,他也可以實現線程間的計數等待,但他的功能要比CountDownLatch更加強大一些。
循環屏障CyclicBarrier以及和CountDownLatch的區別:
https://www.cnblogs.com/twoheads/p/9555867.html

九 . LockSupport

LockSupport裏面的park和unpark:https://blog.csdn.net/anLA_/article/details/78635300
LockSupport:https://www.cnblogs.com/skywang12345/p/3505784.html
LockSupport原理剖析:https://segmentfault.com/a/1190000008420938

  1. 和object .wait()相比它不需要先獲得某個對象鎖,也不拋InterruptedException異常
  2. LockSupport使用了類似信號量機制,爲每個使用它的線程都與一個許可,如果許可可用就立即返回並消費掉這個許可,如果許可不可用就阻塞;和信號量不同的是許可不可累加,許可永遠只有一個。
  3. 處於park()掛起的線程狀態是明確的WAITING狀態。

十. ThreadLocal

線程私有,線程安全

十一.原子數 無鎖線程安全整數

AtomicInteger,CAS指令進行操作,線程安全
AtomicReference
AtomicIntegerFieldUpdater

4.2 線程安全的類型

Java 中一些常用的線程安全類型:

類型 非線程安全 線程安全 備註
數組 Arraylist CopyOnWriteArrayList
數組 vector/stack stack繼承自vector,vector是線程安全的,stack也是
map類型 Hashmap ConcurrentHashmap
map類型 ConcurrentSkipListMap
隊列 LinkedList ConcurrentLinkedQueue
隊列 BlockingQueue

五. 多線程管理:

5.1 守護線程的使用

當一個應用進程僅存在守護進程時,Java虛擬機就會自然退出。通過Thread.setDaemon()進行設置。
需注意設置需要在線程start()調用之前設置,否則只是一個普通的用戶線程。

5.2 線程優先級

通過Thread.setPriority()方法進行設置,值從1到10,數字越大優先級越高。

5.3 線程組的使用

新建線程組對象,在之後創建線程可作爲參數將一些線程指定爲一組

  1. 線程組也有stop方法,會停止線程組中的所有線程,也會有Thread.stop同樣的問題。

5.4 線程池的使用

JDK提供一套線程池框架 Executor

  1. 可以創建固定線程數量的線程池
  2. 可以實現計劃任務 newScheduleThreadPool()
  3. 自定義線程創建:ThreadFactory
  4. 擴展線程池,ThreadPoolExecutor是一個可擴展的線程池,提供如下接口對線程池進行控制:
    beforeExecutor()
    afterExecutor()
    terminated()
  5. 線程池數量優化,需要考慮CPU數量內存情況等
  6. 線程池可能會喫掉線程中的異常,導致沒有堆棧信息,解決方法:放棄submit方法採用execute方法

六. 鎖優化及注意事項

1.減少持鎖時間

2.減少鎖粒度

3.鎖分離

4.鎖粗化

七. 並行模式

1. 線程安全的單例模式

2.不變模式

3. 消費者生產者模式

4. Future模式

5. 並行搜索&並行排序&並行算法

6. 並行流水線

7. 比較交換CAS

8. 樂觀鎖&悲觀鎖

----未完待續—

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