Java併發核心:解決共享資源競爭

併發編程使我們將程序劃分爲多個分離的、獨立運行的任務。 通過使用多線程機制,這些獨立運行的任務(也被稱爲子任務)中的每一個都由一個執行線程來驅動。 一個線程就是在進程中的一個單一的順序控制流,因此單個進程可以擁有多個併發執行的任務。

實現併發最直接的方式是在操作系統級別使用進程; 進程是運行在它自己的地址空間內的自包容的程序; 而實現併發變成最大的困難是如何協調不同線程驅動的任務之間對這些資源的訪問,以使得這些資源不會同時被多個任務訪問

併發的多面性

使用併發編程時需要解決的問題有多個,而實現併發的方式也有多種,並且在這兩者之間沒有明顯的映射關係。

用併發解決的問題基本上可分爲“速度”和“設計可管理性”兩種。

阻塞的定義:

程序中的某個任務因爲該程序控制範圍之外的某些條件(通常是I/O)而導致不能繼續運行,那麼我們就說這個任務阻塞了

兩種線程的調度模式:

搶佔式調度:

搶佔式調度指的是每條線程執行的時間、線程的切換都由系統控制,系統控制指的是在系統某種運行機制下,可能每條線程都分同樣的執行時間片,也可能是某些線程執行的時間片較長,甚至某些線程得不到執行的時間片。在這種機制下,一個線程的堵塞不會導致整個進程堵塞。

協同式調度:

協同式調度指某一線程執行完後主動通知系統切換到另一線程上執行,這種模式就像接力賽一樣,一個人跑完自己的路程就把接力棒交接給下一個人,下個人繼續往下跑。線程的執行時間由線程本身控制,線程切換可以預知,不存在多線程同步問題,但它有一個致命弱點:如果一個線程編寫有問題,運行到一半就一直堵塞,那麼可能導致整個系統崩潰。

線程讓步:

給線程調度器一個暗示: 你的工作已經差不多了,可以讓出CPU給別的線程了。 這個暗示通過yield()方法來作出(不過這是暗示,沒有任何機制會保證它將會被採納)。

核心問題:解決共享資源競爭

對於併發工作,你需要某種方式來防止兩個任務訪問相同的資源,至少在關鍵時候不會出現這種情況 也就是Brain的同步規則: 如果你正在寫一個變量,他可能接下來將被另一個線程讀取,或者正在讀取一個上一次已經被另一個線程改寫過的變量,那麼你必須使用同步,並且,讀寫線程都必須使用相同的監視器鎖同步。一般採用下面兩種解決辦法:

1.同步控制

java 一般用synchronized關鍵字的形式來防止資源衝突 ; 當任務執行到被synchronized關鍵字保護的代碼片段的時候,它將檢查鎖是否可用,然後獲取鎖,執行代碼,釋放鎖

臨界區
synchronized可以用於域,也可以用於對象,被用於對象的鎖是對花括號內的代碼進行同步控制,synchronized(syncobject){

}
,這也被稱爲同步代碼塊;在進入此段代碼前,必須先獲得synObject對象的鎖。通過這種方式分離出來的代碼被稱爲臨界區。通過使用同步代碼塊,而不是對整個方法進行同步控制,可以使多個任務同時訪問對象的時間性能得到顯著的提高。
基本上所有的併發模式在解決線程衝突問題的時候,都是採用序列化訪問共享資源的方案。也就意味着在給定時刻只允許一個任務訪問資源。 通常這是通過在代碼前面加上一條鎖語句來實現的。 因爲鎖語句產生了一種互相排斥的效果,所以這稱爲互斥量。

/**
 * 
 */
package threads;

import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;

class Car{
	private boolean waxOn = false;
	public synchronized void Wax(){
		waxOn = true;
		notifyAll();
	}
	
	public synchronized void Polishing() {
		waxOn = false;
		notifyAll();
	}
	
	public synchronized void waitWax() throws InterruptedException {
		while(waxOn == false)               //可能有多個任務處於同一個原因在等待一個鎖,而第一個喚醒這個鎖的可能會改變這個
			wait();                         //在這個任務從其wait狀態喚醒時,有可能別的任務做出來改變,從而使得這個任務不能執行
	}
	
	public synchronized void waitPolishing() throws InterruptedException {
		while(waxOn == true)                 //最關鍵的任務就是檢查其所感興趣的特定條件,並在條件不滿足的情況下返回到wait中
			wait();
	}
}

class WaxOn implements Runnable{
	private Car car;
	public WaxOn(Car c) {
		car = c;
	}
	public void run() {
		try {
			while(!Thread.interrupted()) {
			    System.out.println("Waxing!");
				TimeUnit.MILLISECONDS.sleep(200);
			    car.Wax();
			    car.waitPolishing();
			}
		}catch(InterruptedException e) {
			System.out.println("Exiting via interrupt");
		}
		System.out.println("Wax Over!");
	}
}

class WaxOff implements Runnable{
	private Car car;
	public WaxOff(Car c) {
		car = c;
	}
	
	public void run() {
		try {
			while(!Thread.interrupted()) {
				car.waitWax();
				System.out.println("Polishing!");
				TimeUnit.MILLISECONDS.sleep(200);
				car.Polishing();
			}
		}catch(InterruptedException e) {
		     System.out.println("Exiting via InterruptedException!");
		}
		System.out.println("Polishing over!");
	}
}
public class WatiAndNotify {
      public static void main(String[] args) throws Exception{
		Car car = new Car();
		ExecutorService exec = Executors.newCachedThreadPool();
		exec.execute(new WaxOn(car));
		exec.execute(new WaxOff(car));
		TimeUnit.SECONDS.sleep(2);
		exec.shutdownNow();
	}            
}

運行結果:

Polishing!
Waxing!
Polishing!
Waxing!
Polishing!
Waxing!
Polishing!
Waxing!
Polishing!
Exiting via interrupt
Wax Over!
Exiting via InterruptedException!
Polishing over!

2.線程本地存儲

線程本地存儲是一種自動化機制,可以爲使用相同變量的不同線程都創建不同的存儲。 創建和管理本地線程都由ThreadLocal類來實現。

/**
 * 
 */
package threads;

import java.util.Random;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;

class Accessor implements Runnable{
	
	private final int id;
	public Accessor(int idn) {
		id = idn;
	}
	public void run() {
		while(!Thread.currentThread().isInterrupted()) {
			ThreadLocalVariableHodler.increment();
			System.out.println(this);
			Thread.yield();
		}
	}
	public String toString() {
		return "#" + id +": " + ThreadLocalVariableHodler.get(); 
	}
}
public class ThreadLocalVariableHodler {
    private static ThreadLocal<Integer> value = new ThreadLocal<Integer>() {
    	private Random rand = new Random(47);
    	protected synchronized Integer initialValue() {
    		return rand.nextInt(10000);
    	}
    };
	public static void increment() {
    	value.set(value.get()+1);
    }
    public static int get() {
    	return value.get();
    }
    
    public static void main(String[] args) throws Exception {
		ExecutorService exec = Executors.newCachedThreadPool();
		for(int i=0;i<5;i++) {
			exec.execute(new Accessor(i));
		}
		Thread.sleep(3);
		exec.shutdownNow();
	}
}


輸出結果:
#0: 6694
#3: 962
#2: 1862
#4: 556
#1: 9259
#3: 963
#2: 1863
#2: 1864
#2: 1865
#2: 1866
#2: 1867
#2: 1868
#2: 1869
...

ThreadLocal對象通常當作靜態域存儲。 在創建ThreadLocal時,你只能通過get()和set()方法來訪問該對象的內容,其中get()方法返回與線程相關聯的副本,而set()方法會將數據插入到其線程存儲的對象中,並返回存儲中原有的對象。

volatile關鍵字

也許還有人會說可以用volatile關鍵字來解決,因爲有這麼一句話:如果一個域可能會被多個任務同時訪問,或者這些任務中至少有一個是寫入任務,那麼你就應該把這個域設置爲volatile的。但是volatile關鍵字是不能保證線程安全的,該關鍵字只能使修飾的變量獲得原子性(簡單的賦值與返回操作),看個例子就知道了

package test;

public class volatileTest {
	public static volatile int race = 0;
	
	public static void increase() {
		race++;
	}
	private static final int THREADS_COUNT = 20;
	public static void main(String[] args) {
	   Thread[] threads = new Thread[THREADS_COUNT];
	   for(int i=0;i<THREADS_COUNT;i++) {
		   threads[i] = new Thread(new Runnable() {
			   public void run() {
				   for(int i =0; i<1000; i++) {
					   increase();
				   }
			   }
		   });
		   threads[i].start();
	   }
	   while(Thread.activeCount() >1) {
		   Thread.yield();
	   }
	   System.out.println(race);
 	}
}

race++這條代碼編譯成字節碼之後是有四條字節碼指令的,

 getstatic  // Field race:I
 iconst_1    
 iadd
 putstatic  //Field race: I

當getstatic指令把race的值取到操作棧頂時,volatile關鍵字保證了race的值在此時是正確的,但是在執行iconset_1、iadd這些指令時,其他線程肯能已經把race的值加大了,而操作棧頂的數據就變成了過期的數據,所以putstatic指令執行後就可能把較小的race值同步到主內存之中。
volatile關鍵字主要功能就是兩點

  1. 保證此變量對所有線程的可見性,指一條線程修改了這個變量的值,新值對於其他線程是可見的,但並不是多線程安全的。
  2. 禁止指令重排序優化。

Volatile如何保證內存可見性
當寫一個volatile變量時,JMM會把該線程對應的本地內存中的共享變量刷新到內存。
當讀一個valatile時,JMM會把該線程對應的本地內存置爲無效。線程將會從主內存中讀取共享變量。

線程中斷

有時我們必須中斷一個被阻塞的任務,這時我們需要調用Thread類的Interrupt()方法, 這個方法可以設置線程的中斷狀態。
不能中斷正在試圖獲取synchronized鎖或者正在執行I/O操作的線程。

當一個線程已經被阻塞,或者試圖執行一個阻塞操作,那麼設置這個線程的中斷狀態將拋出InterruptException。 當拋出該異常或者該任務調用Thread.interrupted()時,中斷狀態將被複位;Thread.interrupted提供了離開run()循環而不拋出異常的第二種方式。

檢查中斷

當我們在線程上調用interrupt()方法時,中斷髮生的唯一時刻是在任務要進入到阻塞操作中,或者已經在阻塞操作的內部(如我們所見,除了不可中斷的I/O操作或被阻塞的synchronized方法之外,在其餘的例外情況下,似乎並沒有什麼作用);
但是如果我們只能通過在阻塞上拋出異常來退出,那麼我們就無法總是可以離開run()循環;因此我們就需要用到第二種方法來退出,就是上面提到的interrupted()方法,該方法可以檢查中斷狀態,這不僅可以告訴我們intrrupt()是否被調用過。而且還可以清除中斷狀態。 清除中斷狀態可以確保不併髮結構不會就某個任務被中斷這個問題而通知我們兩次,我們可以經由單一的InterruptedException或單一的成功的Thread.interrupted()測試來得到通知。

在任務之間使用管道進行輸入/輸出

提供線程功能的類庫以“管道”的形式對線程間的輸入/輸出進行了支持。 Java類庫中對應的輸入/輸出類就是 pipedWriter類(允許任意管道間的寫)和PipedReader類(允許多個不同線程對同一個管道讀取,並且是可中斷的)。

死鎖的產生:

某個任務在等待另一個任務,而後者又在等待別的任務,這樣一直下去,直到這個任務鏈上的某個任務又在等待第一個任務釋放鎖。
要發生死鎖必須同時滿足四個條件:

  1. 互斥條件。任務使用的資源至少有一個不是共享的
  2. 至少有一個任務它持有一個資源且正在等待一個當前被別的任務持有的資源
  3. 資源不能被任務搶佔,任務必須把資源釋放當作普通事件。
  4. 必須有循環等待,這時,一個任務等待其他任務所持有的資源,後者由在等待另一個任務所持有的資源。

免鎖容器的定義:

容器是所有編程中基礎工具,這其中自然也包括併發編程。 出於這個原因,像Vector和HashTable這種早期容器就包含有許多synchronized方法,當它們用於非多線程的程序時,便會導致不可接受的開銷。所以後面添加了新的容器,通過使用更靈巧的技術來消除加鎖,從而提高線程安全的性能。

這些免鎖容器背後的策略是:對容器的修改可以與讀取操作同時發生,只要用戶只能看到完成修改的結果即可。 修改是在容器數據結構的某個部分的一個單獨的副本(有時是整個數據結構的副本)上執行的,並且這個副本在修改過程中是不可視的;只有當修改完成時,被修改的結構纔會自動地與主數據結構進行交換。

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