Java學習——併發編程之線程安全

二、線程安全

1.爲什麼有線程安全問題?

當多個線程同時共享同一個全局變量靜態變量,做寫操作時,可能會發生數據衝突問題,也就是線程安全問題。但是做讀操作是不會發生數據衝突問題。

2. 如何解決多線程之間的線程安全問題?

使用同步synchronized或使用鎖(lock)。線程在執行的時候,必須先獲得鎖,一次只能允許一個線程獲得鎖,其他線程必須等待,代碼執行完後釋放鎖,讓其他線程去執行,相當與變成了單線程執行,保證了線程的原子性。

3.什麼是多線程之間同步?

線程之間共享同一個資源,相互之間不會產生干擾。

4.內置鎖

Java提供一種內置的鎖機制來支持原子性,每個java對象都可以看作是一個鎖,稱爲內置鎖。內置鎖爲互斥鎖:線程A獲得鎖後,線程B必須等待A執行完釋放鎖後才能獲得鎖。內置鎖使用synchronize關鍵字實現。實現方法如下:

(1)修飾需要同步的方法,此時充當鎖的對象爲——調用同步方法的對象。

(2)同步代碼塊,同步代碼塊的粒度比同步方法更細,並且充當鎖的對象不一定是this鎖,也可以是其他對象。使用更加靈活。

   //同步方法
   public synchronized void sale(){
		if(count > 0){
			System.out.println("正在出售第" + (100-count+1) + "張火車票");
			count--;
		}
	}
	//同步代碼塊
	public synchronized void sale(){
		synchronized(obj){  //參數爲任意對象
			try {
				Thread.sleep(10);
			} catch (InterruptedException e) {
				// TODO Auto-generated catch block
				e.printStackTrace();
			}
			if(count > 0){
				System.out.println("正在出售第" + (100-count+1) + "張火車票");
				count--;
		    }
		}
		
	}

5.靜態同步函數

靜態同步函數就是被static和synchronize同時修飾的方法。靜態同步函數使用的鎖是該函數所屬的字節碼文件對象,可以用getClass()方法獲取也可以用當前類名.class表示。

public static void sale() {
		synchronized (ThreadTrain3.class) {
			if (trainCount > 0) {
				System.out.println(Thread.currentThread().getName() + ",出售第" + (100 - trainCount + 1) + "張票");
				trainCount--;
			}
		}
}

總結:

synchronized 修飾方法使用鎖是當前this鎖。

synchronized 修飾靜態方法使用鎖是當前類的字節碼文件

6.多線程死鎖

什麼是多線程死鎖:同步中嵌套同步,線程之間相互等待,導致鎖無法釋放。線程A先獲取lock1,讓線程A休眠50ms,同時線程B先獲取lock2,然後去獲取lock1,但是lock1已經被線程A獲取,所以只能等待,線程A休眠結束後去獲取lock2,但是lock2被線程B佔用,所以也只能等待,就造成了死鎖。

以下代碼會造成死鎖,把sale()的synchronized去掉就不會死鎖或者把同步代碼塊的修飾去掉。

class threadTest2 implements Runnable {
	
	private static int count = 100; //定義火車票總數
	private static Object obj = new Object();   //靜態的變量存放在方法區,被共享
	public boolean flag = true;
	public void run() {
		if(flag){
			while(count > 0){
				synchronized (obj) {
					try {
						Thread.sleep(10);
					} catch (InterruptedException e) {
						e.printStackTrace();
					}
					sale();
				}
			}
		}
		else {
			while(count > 0){
				sale();
			}
		}
		
		
	}
	
	//同步代碼塊
	public synchronized void sale(){
		//synchronized(obj){  //參數爲任意對象
			try {
				Thread.sleep(10);
			} catch (InterruptedException e) {
				// TODO Auto-generated catch block
				e.printStackTrace();
			}
			if(count > 0){
				System.out.println("正在出售第" + (100-count+1) + "張火車票");
				count--;
		}
		//}
		
	}
	
}


/**
 * 
 * @author johson
 * 模擬多線程死鎖問題,多線程死鎖是因爲在同步中嵌套了同步,把sale()的synchronized去掉就不會死鎖或者把同步代碼塊的修飾去掉
 *
 */
public class test2 {
	
	public static void main(String[] args) throws InterruptedException {
		threadTest2 threadTest1 = new threadTest2();
		//窗口1
		Thread t1 = new Thread(threadTest1,"窗口1");
		//窗口2
		Thread t2 = new Thread(threadTest1,"窗口2");
		
		t1.start();
		Thread.sleep(40);
		threadTest1.flag = false;
		t2.start();
		
	}

7.ThreadLocal

使用TreadLocal維護變量是,ThreadLocal爲每個使用變量的線程創建變量的副本,所以每一個線程都可以獨立的修改自己的副本,而不會影響到其他線程的副本。ThreadLocal是通過map來實現的。

這裏介紹ThreadLocal的四個方法:

(1)set方法:設置當前線程的線程局部變量的值

    public void set(T value) {
        Thread t = Thread.currentThread();
        ThreadLocalMap map = getMap(t);
        if (map != null)
            map.set(this, value);
        else
            createMap(t, value);
    }

(2)get()方法:該方法返回當前線程所對應的線程局部變量。

    public T get() {
        Thread t = Thread.currentThread();
        ThreadLocalMap map = getMap(t);
        if (map != null) {
            ThreadLocalMap.Entry e = map.getEntry(this);
            if (e != null) {
                @SuppressWarnings("unchecked")
                T result = (T)e.value;
                return result;
            }
        }
        return setInitialValue();
    }

(3)remove()方法:將當前局部變量的值刪除,目的是爲了減少內存的佔用,是jdk5.0新增的方法。

線程執行完成後,對線程的局部變量會自動被垃圾回收,所以remove()方法並不是必須要顯式調用的,但是調用了可以加快內存回收的速度。

     public void remove() {
         ThreadLocalMap m = getMap(Thread.currentThread());
         if (m != null)
             m.remove(this);
     }

(4)initialValue()方法:返回線程局部變量的初始值。需要重寫這個方法。這個方法是一個延遲調用方法,在線程第一次調用get()或set()方法時纔會執行,並且只執行一次。

    protected T initialValue() {
        return null;
    }

下面是一個ThreadLocal的demo

class Res{
	//public Integer count = 0;
	
	public static ThreadLocal<Integer> threadLocal = new ThreadLocal<Integer>(){
		//初始化
		protected Integer initialValue() {
			return 0;
		};
	};
	
	public Integer getNumber(){
		Integer count = threadLocal.get()+1;
		threadLocal.set(count);
		return count;
	}
}

/**
 * ThreadLocal測試
 * @author johson
 *
 */
public class test3 extends Thread{
	
	private Res res;
	
	public test3(Res res) {
		this.res = res;
	}
	
	@Override
	public void run() {
		for(int i = 0;i < 3;i++){
			System.out.println(Thread.currentThread().getName() + "," + res.getNumber());
		}
	}
	
	public static void main(String[] args){
		Res res = new Res();
		
		test3 t = new test3(res);
		
		test3 t1 = new test3(res);
		
		t.start();
		
		t1.start();
		
	}

8.多線程的三大特性

(1)原子性:即一個操作或者多個操作 要麼全部執行並且執行的過程不會被任何因素打斷,要麼就都不執行。

一個很經典的例子就是銀行賬戶轉賬問題:
比如從賬戶A向賬戶B轉1000元,那麼必然包括2個操作:從賬戶A減去1000元,往賬戶B加上1000元。這2個操作必須要具備原子性才能保證不出現一些意外的問題。

我們操作數據也是如此,比如i = i+1;其中就包括,讀取i的值,計算i,寫入i。這行代碼在Java中是不具備原子性的,則多線程運行肯定會出問題,所以也需要我們使用同步和lock這些東西來確保這個特性了。

原子性其實就是保證數據一致、線程安全一部分.

(2)可見性:兩個線程同時訪問一個變量,A線程修改了這個變量,在B線程中能立刻看到。

如果A線程修改了i的值,沒有及時刷新到主內存中,B線程使用的i還是之前的那個i,這就是線程的可見性問題。

(3)有序性:代碼的執行順序按照代碼的先後順序執行。代碼在cpu中運行的時候,代碼的運行順序會被改變,但是不會改變邏輯順序,會保證執行結果的一致。——這就是重排序。例如:

int a = 10;    //語句1

int r = 2;    //語句2

a = a + 3;    //語句3

r = a*a;     //語句4

則因爲重排序,他還可能執行順序爲 2-1-3-4,1-3-2-4
但絕不可能 2-1-4-3,因爲這打破了依賴關係。
顯然重排序對單線程運行是不會有任何問題,而多線程就不一定了,所以我們在多線程編程時就得考慮這個問題了。

9.java內存模型

共享內存模型指的就是Java內存模型(簡稱JMM),JMM決定一個線程對共享變量的寫入時,能對另一個線程可見。從抽象的角度來看,JMM定義了線程和主內存之間的抽象關係:線程之間的共享變量存儲在主內存(main memory)中,每個線程都有一個私有的本地內存(local memory),本地內存中存儲了該線程以讀/寫共享變量的副本。本地內存是JMM的一個抽象概念,並不真實存在。它涵蓋了緩存,寫緩衝區,寄存器以及其他的硬件和編譯器優化。

線程A和線程B如果要通信,必須經過以下兩個步驟:

(1)線程A將更新的變量刷新到主內存中

(2)線程B再去主內存中讀取線程A更新的變量

如上圖所示,本地內存A和B有主內存中共享變量x的副本。假設初始時,這三個內存中的x值都爲0。線程A在執行時,把更新後的x值(假設值爲1)臨時存放在自己的本地內存A中。當線程A和線程B需要通信時,線程A首先會把自己本地內存中修改後的x值刷新到主內存中,此時主內存中的x值變爲了1。隨後,線程B到主內存中去讀取線程A更新後的x值,此時線程B的本地內存的x值也變爲了1。

從整體來看,這兩個步驟實質上是線程A在向線程B發送消息,而且這個通信過程必須要經過主內存。JMM通過控制主內存與每個線程的本地內存之間的交互,來爲java程序員提供內存可見性保證。

總結:什麼是Java內存模型:java內存模型簡稱jmm義了一個線程另一個線程可見。共享變量存放在主內存中,每個線程都有自己的本地內存,多個線程同時訪問一個數據的時候,可能本地內存沒有及時刷新到主內存,所以就會發生線程安全問題。

10.volatile的作用

(1)保證此變量對所有的線程的可見性,當一個線程修改了這個變量的值,volatile 保證了新值能立即同步到主內存,以及每次使用前立即從主內存刷新。但普通變量做不到這點,普通變量的值在線程間傳遞均需要通過主內存(詳見:Java內存模型)來完成。

(2)禁止指令重排序優化。

11.Volatile和Synchronize的區別

(1)volatile雖然具有可見性但是並不能保證原子性。

(2)性能方面,synchronized關鍵字是防止多個線程同時執行一段代碼,就會影響程序執行效率,而volatile關鍵字在某些情況下性能要優於synchronized。

但是要注意volatile關鍵字是無法替代synchronized關鍵字的,因爲volatile關鍵字無法保證操作的原子性。

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