多執行緒筆記一
多執行緒筆記二
多執行緒筆記三
多執行緒相關問題
1.Exclusive write / Concurrent read access 互斥讀寫
有時候我們會對一份資料同時進行讀和寫的操作
ReadWriteLock 介面還有他的實現類ReentrantReadWriteLock 可以讓我們實現如下場景的功能:
- 可能有任意數量的同步讀取操作。如果有至少一個讀取操作獲得允許,那麼就不會產生寫入操作。
- 最多隻能有一個寫操作,如果已經有一個寫操作已經被允許那麼就不能進行讀操作。
import java.util.concurrent.locks.ReadWriteLock;
import java.util.concurrent.locks.ReentrantReadWriteLock;
public class Sample {
// Our lock. The constructor allows a "fairness" setting, which guarantees the chronology of lock attributions. protected static final ReadWriteLock RW_LOCK = new ReentrantReadWriteLock();
// This is a typical data that needs to be protected for concurrent access protected static int data = 0;
/**
* This will write to the data, in an exclusive access
*/
public static void writeToData() {
RW_LOCK.writeLock().lock();
try {
data++;
} finally {
RW_LOCK.writeLock().unlock();
}
}
public static int readData() {
RW_LOCK.readLock().lock();
try {
return data;
} finally {
RW_LOCK.readLock().unlock();
}
}
}
複製程式碼
備註:如上場景我們應該使用AtomicInteger,但是我們在這邊只是用來舉例,這個鎖操作並不關心這個資料是否是一個原子型別的變數。
在讀操作這一邊的鎖是非常有必要的,雖然這個操作看起來像是針對普通讀操作的。事實上如果你不在讀檔案時候進加鎖,那麼任何操作都有可能會出錯:
- 基本型別的寫入操作在任何行虛擬機器上都不保證是原子型別的操作。在寫入一個64bits 的 long型資料最後只會有32bits。
為了更高的效能要求,還有一種更快型別的鎖,叫做StampedLock ,除此之外還有一些一起繼承樂觀鎖的型別。這個所以與ReadWriteLock工作情況區別很大。
Producer-Consumer 生產者-消費者模型
- 一個簡單的 Producer-Consumer 問題解決方法。注意 JDK的 類 (AtomicBoolean and BlockingQueue) 是用來同步的,他麼不能減少了建立無效方法。有興趣的話可以看一下 BlockingQueue。通過幾種不同的實現,可能會產生不同的行為。例如e DelayQueue 延遲佇列 or Priority Queue 優先佇列。
public class Producer implements Runnable {
private final BlockingQueue<ProducedData> queue;
public Producer(BlockingQueue<ProducedData> queue) {
this.queue = queue;
}
public void run() {
int producedCount = 0;
try {
while (true) {
producedCount++; //put throws an InterruptedException when the thread is interrupted queue.put(new ProducedData()); } } catch (InterruptedException e) { // the thread has been interrupted: cleanup and exit producedCount--; //re-interrupt the thread in case the interrupt flag is needeed higher up Thread.currentThread().interrupt(); } System.out.println("Produced " + producedCount + " objects"); } }
}
public class Consumer implements Runnable {
private final BlockingQueue<ProducedData> queue;
public Consumer(BlockingQueue<ProducedData> queue) {
this.queue = queue;
}
public void run() {
int consumedCount = 0;
try {
while (true) { //put throws an InterruptedException when the thread is interrupted ProducedData data = queue.poll(10, TimeUnit.MILLISECONDS); // process data consumedCount++; } } catch (InterruptedException e) { // the thread has been interrupted: cleanup and exit consumedCount--; //re-interrupt the thread in case the interrupt flag is needeed higher up Thread.currentThread().interrupt(); } System.out.println("Consumed " + consumedCount + " objects"); } }
}
public class ProducerConsumerExample {
static class ProducedData { // empty data object }
public static void main(String[] args) throws InterruptedException {
BlockingQueue<ProducedData> queue = new ArrayBlockingQueue<ProducedData>(1000); // choice of queue determines the actual behavior: see various BlockingQueue implementations
Thread producer = new Thread(new Producer(queue));
Thread consumer = new Thread(new Consumer(queue));
producer.start();
consumer.start();
Thread.sleep(1000);
producer.interrupt();
Thread.sleep(10);
consumer.interrupt();
}
}
}
複製程式碼
使用synchronized / volatile 對讀寫操作可見性的影響
- 正如我們瞭解的那樣,我們應該使用synchronized 關鍵字來進行同步方法,或者同步程式碼塊。但是我們有些人可能會注意到 synchronized 與 volatile 關鍵字。提供了read / write barrier。那麼問題來了什麼是 read / write barrier。我們來看一下下面你的例子:
class Counter {
private Integer count = 10;
public synchronized void incrementCount() {
count++;
}
public Integer getCount() {
return count;
}
}
複製程式碼
- 我們假設執行緒A 呼叫了incrementCount() 執行緒B呼叫了getCount。在這個場景中我不能保證資料更新對執行緒B可見,甚至很有可能執行緒B永遠看不到數值更新。
- 為了理解這個行為,我們需要理解java 記憶體模型與硬體之間的關係。在Java中每個執行緒都有自己的執行緒stack。這個stack 裡面包含:呼叫執行緒的方法還有執行緒中建立的本地變數在多核作業系統中,這個場景中很有可能這個執行緒存在於某個cpu核心或者快取中。如果存在於某個執行緒中,一個物件使用 synchronized (or volatile) 關鍵字在synchronized程式碼塊之後執行緒與主記憶體同步自己的本地變數。 synchronized (or volatile)關鍵字建立了一個讀寫屏障並且確保執行緒的最新資料可見。
- 在我們的這個案例中,既然執行緒B還沒有使用synchronized 來同步計算,或許執行緒B永遠看不到執行緒A的資料更新了。為了確保最新的資料我們需要用synchronized 來修飾getCount方法。
public synchronized Integer getCount() { return count; }
複製程式碼
- 現在當執行緒A更新完資料 ,然後釋放Couter 物件的鎖。與此同時建立一個寫屏障並且將執行緒A的變化更新到主記憶體。類似的當執行緒B請求一個鎖,在主記憶體讀取數進入讀屏障,然後久可以看到所有的更新改動。
- 使用volatile 關鍵字也可以代理同樣的效果。volatile關鍵字修飾的變數的寫入操作, 會將所有更新同步到主記憶體。volatile關鍵字修飾的變數的讀取操作將會讀取主記憶體中的值。
獲取你的程式中的所有執行緒狀態
程式碼片段 Code snippet
import java.util.Set;
public class ThreadStatus {
public static void main(String args[]) throws Exception {
for (int i = 0; i < 5; i++) {
Thread t = new Thread(new MyThread());
t.setName("MyThread:" + i);
t.start();
}
int threadCount = 0;
Set<Thread> threadSet = Thread.getAllStackTraces().keySet();
for (Thread t : threadSet) {
if (t.getThreadGroup() == Thread.currentThread().getThreadGroup()) {
System.out.println("Thread :" + t + ":" + "state:" + t.getState());
++threadCount;
}
}
System.out.println("Thread count started by Main thread:" + threadCount);
}
}
class MyThread implements Runnable {
public void run() {
try {
Thread.sleep(2000);
} catch (Exception err) {
err.printStackTrace();
}
}
}
複製程式碼
解釋:
Thread.getAllStackTraces().keySet()返回包含application 和 系統的所有執行緒。如果你只對你建立的執行緒的狀態感興趣,那麼遍歷Thread set 然後通過檢查 Thread Group 來判斷執行緒是否屬於app的執行緒。
使用ThreadLocal
- ThreadLocal 是在Java併發程式設計中經常用到的工具。他允許一個變數在不同執行緒有不同的值。這樣拿來說,即使是相同的程式碼在不同的執行緒中執行,這些操作將不貢獻value,而且每個執行緒都有自己的本地變數。
- 例如這個在servlet中經常被髮布的context 。你可能會這麼做:
private static final ThreadLocal<MyUserContext> contexts = new ThreadLocal<>();
public static MyUserContext getContext() {
return contexts.get(); // get returns the variable unique to this thread
}
public void doGet(...) {
MyUserContext context = magicGetContextFromRequest(request);
contexts.put(context); // save that context to our thread-local - other threads
// making this call don't overwrite ours
try {
// business logic
} finally {
contexts.remove(); // 'ensure' removal of thread-local variable
}
}
複製程式碼
使用共享全域性佇列的多producer/consumer 案例
- 如下程式碼展示了 多producer/consumer 程式設計。生產者和消費者模型共享一個全域性佇列。
import java.util.concurrent.*;
import java.util.Random;
public class ProducerConsumerWithES {
public static void main(String args[]) {
BlockingQueue<Integer> sharedQueue = new LinkedBlockingQueue<Integer>();
ExecutorService pes = Executors.newFixedThreadPool(2);
ExecutorService ces = Executors.newFixedThreadPool(2);
pes.submit(new Producer(sharedQueue, 1));
pes.submit(new Producer(sharedQueue, 2));
ces.submit(new Consumer(sharedQueue, 1));
ces.submit(new Consumer(sharedQueue, 2));
pes.shutdown();
ces.shutdown();
}
}
/* Different producers produces a stream of integers continuously to a shared queue,
which is shared between all Producers and consumers */
class Producer implements Runnable {
private final BlockingQueue<Integer> sharedQueue;
private int threadNo;
private Random random = new Random();
public Producer(BlockingQueue<Integer> sharedQueue,int threadNo) {
this.threadNo = threadNo;
this.sharedQueue = sharedQueue;
}
@Override
public void run() {
// Producer produces a continuous stream of numbers for every 200 milli seconds
while (true) {
try {
int number = random.nextInt(1000);
System.out.println("Produced:" + number + ":by thread:"+ threadNo);
sharedQueue.put(number);
Thread.sleep(200);
} catch (Exception err) {
err.printStackTrace();
}
}
}
}
class Consumer implements Runnable {
private final BlockingQueue<Integer> sharedQueue;
private int threadNo;
public Consumer (BlockingQueue<Integer> sharedQueue,int threadNo) {
this.sharedQueue = sharedQueue;
this.threadNo = threadNo;
}
@Override
public void run() {
// Consumer consumes numbers generated from Producer threads continuously
while(true){
try {
int num = sharedQueue.take();
System.out.println("Consumed: "+ num + ":by thread:"+threadNo);
} catch (Exception err) {
err.printStackTrace();
}
}
}
}
複製程式碼
- 輸出
Produced:497:by thread:1
Produced:300:by thread:2
Consumed: 497:by thread:1
Consumed: 300:by thread:2
Produced:64:by thread:2
Produced:984:by thread:1
Consumed: 64:by thread:1
Consumed: 984:by thread:2
Produced:102:by thread:2
Produced:498:by thread:1
Consumed: 102:by thread:1
Consumed: 498:by thread:2
Produced:168:by thread:2
Produced:69:by thread:1
Consumed: 69:by thread:2
Consumed: 168:by thread:1
複製程式碼
- 說明
- sharedQueue,是一個LinkedBlockingQueue,在生產者和消費者執行緒之間共享
- 生產者執行緒每隔200ms 生產一個數字 然後持續的新增入佇列
- 消費者從sharedQueue 持續消耗數字
- 這個程式實現無需 synchronized或者鎖結構。 BlockingQueue 是實現這個模型的關鍵。
- BlockingQueue 就是為了生產/消費 模型來設計的
- BlockingQueue是執行緒安全的。所有對列的方法都是原子型別的操作,其使用了 內部鎖或者其他型別的併發控制。
使用Threadpool 相加兩個 int 型別的陣列
- 一個執行緒池就是一個佇列的任務,其中每個任務都會被其中的執行緒執行。
- 如下的案例展示瞭如何使用執行緒池新增兩個int 型別的陣列。
public static void testThreadpool() {
int[] firstArray = { 2, 4, 6, 8 };
int[] secondArray = { 1, 3, 5, 7 };
int[] result = { 0, 0, 0, 0 };
ExecutorService pool = Executors.newCachedThreadPool();
// Setup the ThreadPool:
// for each element in the array, submit a worker to the pool that adds elements
for (int i = 0; i < result.length; i++) {
final int worker = i;
pool.submit(() -> result[worker] = firstArray[worker] + secondArray[worker] );
}
// Wait for all Workers to finish:
try {
// execute all submitted tasks
pool.shutdown();
// waits until all workers finish, or the timeout ends
pool.awaitTermination(12, TimeUnit.SECONDS);
}
catch (InterruptedException e) {
pool.shutdownNow(); //kill thread
}
System.out.println(Arrays.toString(result));
}
複製程式碼
說明:
- 這個案例只是單純展示用。在實際使用中,我們不會僅僅為了這點任務就使用執行緒池。
- Java7 中你將看到使用匿名內部類而不是lamda 來實現這個任務。
Pausing Execution 暫停執行處理器時間對其他執行緒可用。sleep 方法有兩個複寫方法在Thread 類製作。
- 指定sleep 時間
public static void sleep(long millis) throws InterruptedException
複製程式碼
- 指定sleep時間
public static void sleep(long millis, int nanos)
複製程式碼
Thread 原始碼介紹
這對於系統核心的排程是非常重要的。這個可能產生不可預測的結果,而且有些實現甚至不考慮nano s引數。我們建議在 用try catch 包住 Thread.sleep 操作並且catch InterruptedException. 異常。
執行緒中斷/終止執行緒
- 每個Java執行緒都有一個interrupt flag,預設的是false。打斷一個執行緒,最基礎的就是將這個flag 設定成true。在這個執行緒中執行的程式碼會暗中觀察這個標記,然後做出反應。當然程式碼也可以忽略這個flag。但是為啥每個執行緒都要樹flag?畢竟線上程中有一個Boolean 變數我們可以更好的管理執行緒。當然了線上程裡面還有一些特別的方法,他們會線上程被中斷的時候執行。這些方法叫做阻塞方法。這些方法會將執行緒設定成WAITING或是WAITING 狀態。當執行緒是這個狀態的話,那麼打斷執行緒會丟擲一個InterruptedException。而不是interrupt flag 被設定成true。然後這個執行緒狀態有一次變成RUNNABLE。呼叫阻塞方法的時候會強制要求處理InterruptedException。之後在這個執行緒中打斷的時候就會產生一個WAIT 狀態。注意,不是所有方法都要響應中斷行為。最終執行緒被設定成中斷狀態,然後進入一個阻塞方法,然後立刻丟擲一個InterruptedException, interrupt flag 將被清除。
- 與這些原理不同,java 並沒有特別的特別語義描述中斷。程式碼非常容易描述打斷。但是大多數情況下中斷是用來通知一個執行緒應該儘快停下來。從上面的描述可以清楚的看出,這取決於執行緒上的程式碼,對中斷作出適當的反應以停止執行。停止執行緒是一種寫作。當一個執行緒被打斷了,它在執行的程式碼可能會在棧空間下沉好幾個level。大多數方法不呼叫阻塞方法,並且結束時間充足,無須延遲 關閉 執行緒。程式碼在一個loop 中執行,處理任務應該首先關注中斷。Loop 應該儘可能的初始化任務,檢測打斷狀態來推出loop。對於一個有限的loop,所有任務必須在loop終止之前被執行完畢,以防有任務沒有被執行。如果在語義上是可能的,那麼它可以 簡單地傳遞InterruptedException斷,並宣告丟擲它。那麼它對於它的呼叫者來說的話它就是一個阻塞方法。如果不能傳遞異常,那麼它至少應該設定打斷狀態,那麼呼叫者就會知道執行緒被打斷了。在一些案例中,需要持續等待而無視等待異常。在這種情況下,必須延遲設定打斷狀態,知道它不再等待。這可能呼叫本地變數,這個本地變數用來檢查推出方法和打斷方法的優先順序。
案例
用中斷執行緒來打斷任務執行
class TaskHandler implements Runnable {
private final BlockingQueue<Task> queue;
TaskHandler(BlockingQueue<Task> queue) {
this.queue = queue;
}
@Override
public void run() {
while (!Thread.currentThread().isInterrupted()) { //
try {
Task task = queue.take(); // blocking call, responsive to interruption
handle(task);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
}
}
private void handle(Task task) {
// actual handling
}
}
}
複製程式碼
等待程式執行完畢,延遲設定打斷flag
class MustFinishHandler implements Runnable {
private final BlockingQueue<Task> queue;
MustFinishHandler(BlockingQueue<Task> queue) {
this.queue = queue;
}
@Override
public void run() {
boolean shouldInterrupt = false;
while (true) {
try {
Task task = queue.take();
if (task.isEndOfTasks()) {
if (shouldInterrupt) {
Thread.currentThread().interrupt();
}
return;
}
handle(task);
} catch (InterruptedException e) {
shouldInterrupt = true; // must finish, remember to set interrupt flag when we're
done
}
}
}
private void handle(Task task) {
// actual handling
}
}
複製程式碼
固定的任務列表不過在中斷時候可能會提前退出。
class GetAsFarAsPossible implements Runnable {
private final List<Task> tasks = new ArrayList<>();
@Override
public void run() {
for (Task task : tasks) {
if (Thread.currentThread().isInterrupted()) {
return;
}
handle(task);
}
}
private void handle(Task task) {
// actual handling
}
}
複製程式碼