多線程之間如何實現同步?

1、爲何會有線程安全問題?

線程安全問題通常是發生再多線程環境,當多個線程同時共享一個全局變量或靜態變量作寫的操做時候,可能會發生數據衝突問題,也就是線程安全問題,在讀的操做不會發生數據衝突問題 下面看個簡單的買票例子 案例:需求如今有100張火車票,有兩個窗口同時搶火車票,請使用多線程模擬搶票效果。 代碼: public class ThreadTrain1 implements Runnable { private int count = 100;java

@Override
public void run() {
	while (count > 0) {
		try {
			Thread.sleep(50);
		} catch (Exception e) {
			// TODO: handle exception
		}
		sale();
	}
}

public void sale() {

		System.out.println(Thread.currentThread().getName() + ",出售第" + (100 - count + 1) + "票");
		count--;
	 }
}
複製代碼

}程序員

public class ThreadDemo { public static void main(String[] args) { ThreadTrain1 threadTrain1 = new ThreadTrain1(); Thread t1 = new Thread(threadTrain1, "①號窗口"); Thread t2 = new Thread(threadTrain1, "②號窗口"); t1.start(); t2.start(); } } 運行結果編程

咱們能夠發現一號窗口和二號窗口會賣出重複或者超賣現象,這就是在多線程環境下共享資源形成的線程安全問題

2、如何解決線程安全問題

Synchronized(/'sɪŋkrənaɪzd/) ------至關於自動擋 Lock(/lɒk/ )---jdk1.5併發包才又 ------至關於手動擋緩存

  1. 如何解決多線程之間線程安全問題? 答:使用多線程之間同步synchronized或使用鎖(lock)。
  2. 爲何使用線程同步或使用鎖能解決線程安全問題呢? 答:將可能會發生數據衝突問題(線程不安全問題),只能讓當前一個線程進行執行。代碼執行完成後釋放鎖,讓後才能讓其餘線程進行執行。這樣的話就能夠解決線程不安全問題。
  3. 什麼是多線程之間同步? 答:當多個線程共享同一個資源,不會受到其餘線程的干擾。
  4. 什麼地方須要考慮枷鎖? 答:真正產生共享同一個全局變量的時候。
  5. 鎖是在何時釋放的? 答:代碼執行完畢或者是程序拋出異常,都會把鎖釋放掉

3、同步

3.一、什麼是同步代碼塊? 答:就是將可能會發生線程安全問題的代碼,給包括起來。 synchronized(同一個數據){ 可能會發生線程衝突問題 } 就是同步代碼塊 synchronized(對象) { //這個對象能夠爲任意對象 須要被同步的代碼 } 對象如同鎖,持有鎖的線程能夠在同步中執行 沒持有鎖的線程即便獲取CPU的執行權,也進不去 同步的前提:安全

  1. 必需要有兩個或者兩個以上的線程
  2. 必須是多個線程使用同一個鎖 必須保證同步中只能有一個線程在運行 好處:解決了多線程的安全問題 弊端:多個線程須要判斷鎖,較爲消耗資源、搶鎖的資源。 原理:有一個線程已經拿到了鎖,其餘線程已經有了cpu執行權,一直排隊等待其餘線程釋放鎖。 代碼樣例: private static Object oj = new Object(); public void sale() { // 前提 多線程進行使用、多個線程只能拿到一把鎖。 // 保證只能讓一個線程 在執行 缺點效率下降 synchronized (oj) { if (count > 0) { System.out.println(Thread.currentThread().getName() + ",出售第" + (100 - count + 1) + "票"); count--; } } } 四、同步函數 4.一、什麼是同步函數? 答:在方法上修飾synchronized 稱爲同步函數 代碼樣例: public synchronized void sale() { if (trainCount > 0) { try { Thread.sleep(40); } catch (Exception e) { } System.out.println(Thread.currentThread().getName() + ",出售 第" + (100 - trainCount + 1) + "張票."); trainCount--; } } 同窗們思考問題?同步函數用的是什麼鎖? 答:同步函數使用this鎖。 證實方式: 一個線程使用同步代碼塊(this明鎖),另外一個線程使用同步函數。若是兩個線程搶票不能實現同步,那麼會出現數據錯誤。 代碼: package com.itmayiedu;

class ThreadTrain2 implements Runnable { private int count = 100; public boolean flag = true; private static Object oj = new Object();多線程

@Override
public void run() {
	if (flag) {

		while (count > 0) {

			synchronized (this) {
				if (count > 0) {
					try {
						Thread.sleep(50);
					} catch (Exception e) {
						// TODO: handle exception
					}
					System.out.println(Thread.currentThread().getName() + ",出售第" + (100 - count + 1) + "票");
					count--;
				}
			}

		}

	} else {
		while (count > 0) {
			sale();
		}
	}

}

public synchronized void sale() {
	// 前提 多線程進行使用、多個線程只能拿到一把鎖。
	// 保證只能讓一個線程 在執行 缺點效率下降
	// synchronized (oj) {
	if (count > 0) {
		try {
			Thread.sleep(50);
		} catch (Exception e) {
			// TODO: handle exception
		}
		System.out.println(Thread.currentThread().getName() + ",出售第" + (100 - count + 1) + "票");
		count--;
	}
	// }
}
複製代碼

}併發

public class ThreadDemo2 { public static void main(String[] args) throws InterruptedException { ThreadTrain2 threadTrain1 = new ThreadTrain2(); Thread t1 = new Thread(threadTrain1, "①號窗口"); Thread t2 = new Thread(threadTrain1, "②號窗口"); t1.start(); Thread.sleep(40); threadTrain1.flag = false; t2.start(); } } 五、靜態同步函數 5.一、什麼是靜態同步函數?ide

  1. 方法上加上static(/'stætɪk/)關鍵字,使用synchronized 關鍵字修飾 或者使用類.class文件。
  2. 靜態的同步函數使用的鎖是 該函數所屬字節碼文件對象
  3. 能夠用 getClass方法獲取,也能夠用當前 類名.class 表示。 代碼: synchronized (ThreadTrain.class) { System.out.println(Thread.currentThread().getName() + ",出售 第" + (100 - trainCount + 1) + "張票."); trainCount--; try { Thread.sleep(100); } catch (Exception e) { } } 總結: synchronized 修飾方法使用鎖是當前this鎖。 synchronized 修飾靜態方法使用鎖是當前類的字節碼文件 。 多線程死鎖 3.一、什麼是多線程死鎖 答:同步中嵌套同步,致使鎖沒法釋放 代碼: package com.itmayiedu;

class ThreadTrain6 implements Runnable { // 這是貨票總票數,多個線程會同時共享資源 private int trainCount = 100; public boolean flag = true; private Object mutex = new Object();函數

@Override
public void run() {
	if (flag) {
		while (true) {
			synchronized (mutex) {
				// 鎖(同步代碼塊)在何時釋放? 代碼執行完, 自動釋放鎖.
				// 若是flag爲true 先拿到 obj鎖,在拿到this 鎖、 才能執行。
				// 若是flag爲false先拿到this,在拿到obj鎖,才能執行。
				// 死鎖解決辦法:不要在同步中嵌套同步。
				sale();
			}
		}
	} else {
		while (true) {
			sale();
		}
	}
}

/**
 * 
 * @methodDesc: 功能描述:(出售火車票)
 */
public synchronized void sale() {
	synchronized (mutex) {
		if (trainCount > 0) {
			try {
				Thread.sleep(40);
			} catch (Exception e) {

			}
			System.out.println(Thread.currentThread().getName() + ",出售 第" + (100 - trainCount + 1) + "張票.");
			trainCount--;
		}
	}
}
複製代碼

}post

public class DeadlockThread {

public static void main(String[] args) throws InterruptedException {

	ThreadTrain6 threadTrain = new ThreadTrain6(); // 定義 一個實例
	Thread thread1 = new Thread(threadTrain, "一號窗口");
	Thread thread2 = new Thread(threadTrain, "二號窗口");
	thread1.start();
	Thread.sleep(40);
	threadTrain.flag = false;
	thread2.start();
}
複製代碼

}

四、多線程有三大特性

  1. 原子性 一個操做或者多個操做要麼所有執行要麼就都不執行。
  2. 可見性 私有內存修改能及時同步到主內存,保證私有和主的可見一致性
  3. 有序性 程序執行的順序按照代碼的前後順序執行。

4.一、什麼是原子性 即一個操做或者多個操做 要麼所有執行而且執行的過程不會被任何因素打斷,要麼就都不執行。 一個很經典的例子就是銀行帳戶轉帳問題: 好比從帳戶A向帳戶B轉1000元,那麼必然包括2個操做:從帳戶A減去1000元,往帳戶B加上1000元。這2個操做必需要具有原子性才能保證不出現一些意外的問題。 咱們操做數據也是如此,好比i = i+1;其中就包括,讀取i的值,計算i,寫入i。這行代碼在Java中是不具有原子性的,則多線程運行確定會出問題,因此也須要咱們使用同步和lock這些東西來確保這個特性了。 原子性其實就是保證數據一致、線程安全一部分, 4.二、什麼是可見性 當多個線程訪問同一個變量時,一個線程修改了這個變量的值,其餘線程可以當即看獲得修改的值。 若兩個線程在不一樣的cpu,那麼線程1改變了i的值還沒刷新到主存,線程2又使用了i,那麼這個i值確定仍是以前的,線程1對變量的修改線程沒看到這就是可見性問題。 4.三、什麼是有序性 程序執行的順序按照代碼的前後順序執行。 通常來講處理器爲了提升程序運行效率,可能會對輸入代碼進行優化,它不保證程序中各個語句的執行前後順序同代碼中的順序一致,可是它會保證程序最終執行結果和代碼順序執行的結果是一致的。以下: 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,由於這打破了依賴關係。 顯然重排序對單線程運行是不會有任何問題,而多線程就不必定了,因此咱們在多線程編程時就得考慮這個問題了。

5、java內存模型

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

從上圖來看,線程A與線程B之間如要通訊的話,必需要經歷下面2個步驟:

  1. 首先,線程A把本地內存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,定義了一個線程對另外一個線程可見。共享變量存放在主內存中,每一個線程都有本身的本地內存,當多個線程同時訪問一個數據的時候,可能本地內存沒有及時刷新到主內存,因此就會發生線程安全問題。

6、volatile實現線程可見性

Volatile 關鍵字的做用是變量在多個線程之間可見,但不保證原子性,下面的文章連接講的很是好,這裏不詳細說了 juejin.im/post/5afd22…

6、AtomicInteger原子類

AtomicInteger是一個提供原子操做的Integer類,經過線程安全的方式操做加減。下面是個例子能夠運行對比下count和atomicInteger的結果會發現不管運行多少次,atomicInteger的結果都是正確的 package com;

import java.util.concurrent.atomic.AtomicInteger;

public class VolatileNoAtomic extends Thread { static int count = 0; private static AtomicInteger atomicInteger = new AtomicInteger(0);

@Override
public  void  run() {
	for (int i = 0; i < 1000; i++) {
		//等同於i++
		atomicInteger.incrementAndGet();
		count++;
	}
	System.out.println(atomicInteger+","+count);
}

public static void main(String[] args) {
	// 初始化10個線程
	VolatileNoAtomic[] volatileNoAtomic = new VolatileNoAtomic[10];
	for (int i = 0; i < 10; i++) {
		// 建立
		volatileNoAtomic[i] = new VolatileNoAtomic();
	}
	for (int i = 0; i < volatileNoAtomic.length; i++) {
		volatileNoAtomic[i].start();
	}
}
複製代碼

} 上面只是簡單的一個原子類的介紹,詳細的能夠看如下連接說的很好 blog.csdn.net/xiaoliuliu2…

7、volatile(/'vɒlətaɪl/)與synchronized(/'sɪŋkrənaɪzd/)區別

僅靠volatile不能保證線程的安全性。(原子性) ①volatile輕量級,只能修飾變量。synchronized重量級,還可修飾方法 ②volatile只能保證數據的可見性,不能用來同步,由於多個線程併發訪問volatile修飾的變量不會阻塞。 synchronized不只保證可見性,並且還保證原子性,由於,只有得到了鎖的線程才能進入臨界區,從而保證臨界區中的全部語句都所有執行。多個線程爭搶synchronized鎖對象時,會出現阻塞。 線程安全性 線程安全性包括兩個方面,①可見性。②原子性。 從上面自增的例子中能夠看出:僅僅使用volatile並不能保證線程安全性。而synchronized則可實現線程的安全性。 8、ThreadLocal深度解析 www.imooc.com/article/255…

相關文章
相關標籤/搜索