當多個線程同時共享,同一個全局變量或靜態變量,作寫的操做時,可能會發生數據衝突問題,也就是線程安全問題。可是作讀操做是不會發生數據衝突問題。java
案例: 需求如今有100張火車票,有兩個窗口同時搶火車票,請使用多線程模擬搶票效果。git
public class ThreadTrain implements Runnable {
private int trainCount = 10;
@Override
public void run() {
while (trainCount > 0) {
try {
Thread.sleep(500);
sale();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
private void sale() {
if (trainCount > 0) {
--trainCount;
System.out.println(Thread.currentThread().getName() + ",出售第" + (10 - trainCount) + "張票");
}
}
public static void main(String[] args) {
ThreadTrain threadTrain = new ThreadTrain();
Thread t1 = new Thread(threadTrain, "1臺");
Thread t2 = new Thread(threadTrain, "2臺");
t1.start();
t2.start();
}
}
複製代碼
運行結果:程序員
一號窗口和二號窗口同時出售火車第九九張,部分火車票會重複出售。 結論發現,多個線程共享同一個全局成員變量時,作寫的操做可能會發生數據衝突問題。github
問: 如何解決多線程之間線程安全問題
答: 使用多線程之間同步synchronized或使用鎖(lock)。編程
問: 爲何使用線程同步或使用鎖能解決線程安全問題呢?
答: 將可能會發生數據衝突問題(線程不安全問題),只能讓當前一個線程進行執行。代碼執行完成後釋放鎖,讓後才能讓其餘線程進 行執行。這樣的話就能夠解決線程不安全問題。緩存
問: 什麼是多線程之間同步
答: 當多個線程共享同一個資源,不會受到其餘線程的干擾。安全
問: 什麼是多線程同步
答: 當多個線程共享同一個資源,不會受到其餘線程的干擾。bash
Java提供了一種內置的鎖機制來支持原子性,每個Java對象均可以用做一個實現同步的鎖,稱爲內置鎖,線程進入同步代碼塊以前自動獲取到鎖,代碼塊執行完成正常退出或代碼塊中拋出異常退出時會釋放掉鎖多線程
內置鎖爲互斥鎖,即線程A獲取到鎖後,線程B阻塞直到線程A釋放鎖,線程B才能獲取到同一個鎖 內置鎖使用synchronized關鍵字實現,synchronized關鍵字有兩種用法:app
就是將可能會發生線程安全問題的代碼,給包括起來。
synchronized(同一個數據){
可能會發生線程衝突問題
}
就是同步代碼塊
synchronized(對象)//這個對象能夠爲任意對象
{
須要被同步的代碼
}
複製代碼
對象如同鎖,持有鎖的線程能夠在同步中執行,沒持有鎖的線程即便獲取CPU的執行權,也進不去,同步的前提:
必須保證同步中只能有一個線程在運行
好處: 解決了多線程的安全問題
弊端: 多個線程須要判斷鎖,較爲消耗資源、搶鎖的資源。 代碼樣例:
private void sale() {
synchronized (this) {
if (trainCount > 0) {
--trainCount;
System.out.println(Thread.currentThread().getName() + ",出售第" + (10 - trainCount) + "張票");
}
}
}
複製代碼
在方法上修飾synchronized 稱爲同步方法
public synchronized void sale() {
if (trainCount > 0) {
System.out.println(Thread.currentThread().getName() + ",出售第" + (100 - trainCount + 1) + "張票");
trainCount--;
}
}
複製代碼
答:同步函數使用this鎖。
證實方式: 一個線程使用同步代碼塊(this明鎖),另外一個線程使用同步函數。若是兩個線程搶票不能實現同步,那麼會出現數據錯誤。 參考:方法鎖,對象鎖以及類鎖的用法與區別
package com.itmayiedu;
class Thread0009 implements Runnable {
private int trainCount = 10;
private Object oj = new Object();
public boolean flag = true;
public void run() {
if (flag) {
while (trainCount > 0) {
synchronized (this) {
try {
Thread.sleep(10);
} catch (Exception e) {
// TODO: handle exception
}
if (trainCount > 0) {
System.out.println(Thread.currentThread().getName() + "," + "出售第" + (10 - trainCount + 1) + "票");
trainCount--;
}
}
}
} else {
while (trainCount > 0) {
sale();
}
}
}
public synchronized void sale() {
try {
Thread.sleep(10);
} catch (Exception e) {
// TODO: handle exception
}
if (trainCount > 0) {
System.out.println(Thread.currentThread().getName() + "," + "出售第" + (10 - trainCount + 1) + "票");
trainCount--;
}
}
}
public class Test009 {
public static void main(String[] args) throws InterruptedException {
Thread0009 threadTrain1 = new Thread0009();
Thread0009 threadTrain2 = new Thread0009();
threadTrain2.flag = false;
Thread t1 = new Thread(threadTrain1, "窗口1");
Thread t2 = new Thread(threadTrain2, "窗口2");
t1.start();
Thread.sleep(40);
t2.start();
}
}
複製代碼
方法上加上static關鍵字,使用synchronized 關鍵字修飾 或者使用類.class文件。 靜態的同步函數使用的鎖是 該函數所屬字節碼文件對象 能夠用 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 修飾靜態方法使用鎖是當前類的字節碼文件
同步中嵌套同步,致使鎖沒法釋放
public class ThreadTrain3 implements Runnable {
private static int trainCount = 100;
@Override
public void run() {
while (trainCount > 0) {
try {
Thread.sleep(50);
} catch (Exception e) {
}
sale();
}
}
public static void sale() {
synchronized (ThreadTrain3.class) {
if (trainCount > 0) {
System.out.println(Thread.currentThread().getName() + ",出售第" + (100 - trainCount + 1) + "張票");
trainCount--;
}
}
}
public static void main(String[] args) {
ThreadTrain3 threadTrain = new ThreadTrain3();
Thread t1 = new Thread(threadTrain, "①號");
Thread t2 = new Thread(threadTrain, "②號");
t1.start();
t2.start();
}
}
複製代碼
ThreadLocal提升一個線程的局部變量,訪問某個線程擁有本身局部變量。 當使用ThreadLocal維護變量時,ThreadLocal爲每一個使用該變量的線程提供獨立的變量副本,因此每個線程均可以獨立地改變本身的副本,而不會影響其它線程所對應的副本。 ThreadLocal的接口方法 ThreadLocal類接口很簡單,只有4個方法,咱們先來了解一下:
案例:建立三個線程,每一個線程生成本身獨立序列號。
class Res {
public static Integer count = 0;
public static ThreadLocal<Integer> threadLocal = new ThreadLocal<Integer>() {
protected Integer initialValue() {
return 0;
};
};
public Integer getNum() {
int count = threadLocal.get() + 1;
threadLocal.set(count);
return count;
}
}
public class Test006 extends Thread {
private Res res;
public Test006(Res res) {
this.res = res;
}
@Override
public void run() {
for (int i = 0; i < 3; i++) {
System.out.println(Thread.currentThread().getName() + "," + res.getNum());
}
}
public static void main(String[] args) {
Res res = new Res();
Test006 t1 = new Test006(res);
Test006 t2 = new Test006(res);
t1.start();
t2.start();
}
}
複製代碼
ThreadLoca實現原理, ThreadLoca經過map集合,Map.put(「當前線程」,值);
即一個操做或者多個操做 要麼所有執行而且執行的過程不會被任何因素打斷,要麼就都不執行。 一個很經典的例子就是銀行帳戶轉帳問題:
好比從帳戶A向帳戶B轉1000元,那麼必然包括2個操做:從帳戶A減去1000元,往帳戶B加上1000元。這2個操做必需要具有原子性才能保證不出現一些意外的問題。
咱們操做數據也是如此,好比i = i+1;其中就包括,讀取i的值,計算i,寫入i。這行代碼在Java中是不具有原子性的,則多線程運行確定會出問題,因此也須要咱們使用同步和lock這些東西來確保這個特性了。
原子性其實就是保證數據一致、線程安全一部分,
當多個線程訪問同一個變量時,一個線程修改了這個變量的值,其餘線程可以當即看獲得修改的值。
若兩個線程在不一樣的cpu,那麼線程1改變了i的值還沒刷新到主存,線程2又使用了i,那麼這個i值確定仍是以前的,線程1對變量的修改線程沒看到這就是可見性問題。
程序執行的順序按照代碼的前後順序執行。 通常來講處理器爲了提升程序運行效率,可能會對輸入代碼進行優化,它不保證程序中各個語句的執行前後順序同代碼中的順序一致,可是它會保證程序最終執行結果和代碼順序執行的結果是一致的。以下:
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,由於這打破了依賴關係。
顯然重排序對單線程運行是不會有任何問題,而多線程就不必定了,因此咱們在多線程編程時就得考慮這個問題了。
共享內存模型指的就是Java內存模型(簡稱JMM),JMM決定一個線程對共享變量的寫入時,能對另外一個線程可見。 從抽象的角度來看,JMM定義了線程和主內存之間的抽象關係:線程之間的共享變量存儲在主內存(mainmemory)中,每一個線程都有一個私有的本地內存(local memory),本地內存中存儲了該線程以讀/寫共享變量的副本。 本地內存是JMM的一個抽象概念,並不真實存在。它涵蓋了緩存,寫緩衝區,寄存器以及其餘的硬件和編譯器優化。
下面經過示意圖來講明這兩個步驟:
從總體來看,這兩個步驟實質上是線程A在向線程B發送消息,並且這個通訊過程必需要通過主內存。JMM經過控制主內存與每一個線程的本地內存之間的交互,來爲java程序員提供內存可見性保證。
總結: 什麼是Java內存模型:java內存模型簡稱jmm,定義了一個線程對另外一個線程可見。共享變量存放在主內存中,每一個線程都有本身的本地內存,當多個線程同時訪問一個數據的時候,可能本地內存沒有及時刷新到主內存,因此就會發生線程安全問題。
可見性也就是說一旦某個線程修改了該被volatile修飾的變量,它會保證修改的值會當即被更新到主存,當有其餘線程須要讀取時,能夠當即獲取修改以後的值。 在Java中爲了加快程序的運行效率,對一些變量的操做一般是在該線程的寄存器或是CPU緩存上進行的,以後纔會同步到主存中,而加了volatile修飾符的變量則是直接讀寫主存。
Volatile 保證了線程間共享變量的及時可見性,但不能保證原子性
class ThreadDemo004 extends Thread {
public boolean flag = true;
@Override
public void run() {
System.out.println("線程開始...");
while (flag) {
}
System.out.println("線程結束...");
}
public void setRuning(boolean flag) {
this.flag = flag;
}
}
public class Test0004 {
public static void main(String[] args) throws InterruptedException {
ThreadDemo004 threadDemo004 = new ThreadDemo004();
threadDemo004.start();
Thread.sleep(3000);
threadDemo004.setRuning(false);
System.out.println("flag已經改為false");
Thread.sleep(1000);
System.out.println("flag:" + threadDemo004.flag);
}
}
複製代碼
已經將結果設置爲fasle爲何?還一直在運行呢。
緣由:線程之間是不可見的,讀取的是副本,沒有及時讀取到主內存結果。 解決辦法使用Volatile關鍵字將解決線程之間可見性, 強制線程每次讀取該值的時候都去「主內存」中取值
volatile 性能:
volatile 的讀性能消耗與普通變量幾乎相同,可是寫操做稍慢,由於它須要在本地代碼中插入許多內存屏障指令來保證處理器不發生亂序執行。
可是要注意volatile關鍵字是沒法替代synchronized關鍵字的,由於volatile關鍵字沒法保證操做的原子性。
若是兩個操做訪問同一個變量,且這兩個操做中有一個爲寫操做,此時這兩個操做之間就存在數據依賴性。數據依賴分下列三種類型:
名稱 | 代碼示例 | 說明 |
---|---|---|
寫後讀 | a = 1;b = a; | 寫一個變量以後,再讀這個位置。 |
寫後寫 | a = 1;a = 2; | 寫一個變量以後,再寫這個變量。 |
讀後寫 | a = b;b = 1; | 讀一個變量以後,再寫這個變量。 |
上面三種狀況,只要重排序兩個操做的執行順序,程序的執行結果將會被改變。 前面提到過,編譯器和處理器可能會對操做作重排序。編譯器和處理器在重排序時,會遵照數據依賴性,編譯器和處理器不會改變存在數據依賴關係的兩個操做的執行順序。 注意,這裏所說的數據依賴性僅針對單個處理器中執行的指令序列和單個線程中執行的操做,不一樣處理器之間和不一樣線程之間的數據依賴性不被編譯器和處理器考慮。
s-if-serial語義的意思指:無論怎麼重排序(編譯器和處理器爲了提升並行度),(單線程)程序的執行結果不能被改變。編譯器,runtime 和處理器都必須遵照as-if-serial語義。
爲了遵照as-if-serial語義,編譯器和處理器不會對存在數據依賴關係的操做作重排序,由於這種重排序會改變執行結果。可是,若是操做之間不存在數據依賴關係,這些操做可能被編譯器和處理器重排序。爲了具體說明,請看下面計算圓面積的代碼示例:
double pi = 3.14; //A
double r = 1.0; //B
double area = pi * r * r; //C
複製代碼
上面三個操做的數據依賴關係以下圖所示:
根據happens- before的程序順序規則,上面計算圓的面積的示例代碼存在三個happens- before關係:
這裏的第3個happens- before關係,是根據happens- before的傳遞性推導出來的。 這裏A happens- before B,但實際執行時B卻能夠排在A以前執行(看上面的重排序後的執行順序)。在第一章提到過,若是A happens- before B,JMM並不要求A必定要在B以前執行。JMM僅僅要求前一個操做(執行的結果)對後一個操做可見,且前一個操做按順序排在第二個操做以前。這裏操做A的執行結果不須要對操做B可見;並且重排序操做A和操做B後的執行結果,與操做A和操做B按happens- before順序執行的結果一致。在這種狀況下,JMM會認爲這種重排序並不非法(not illegal),JMM容許這種重排序。 在計算機中,軟件技術和硬件技術有一個共同的目標:在不改變程序執行結果的前提下,儘量的開發並行度。編譯器和處理器聽從這一目標,從happens- before的定義咱們能夠看出,JMM一樣聽從這一目標。
/**
* 重排序
*/
class ReorderExample {
int a = 0;
boolean flag = false;
public void writer() {
a = 1; // 1
flag = true; // 2
System.out.println("writer");
}
public void reader() {
if (flag) { // 3
int i = a * a; // 4
System.out.println("i:" + i);
}
System.out.println("reader");
}
public static void main(String[] args) {
ReorderExample reorderExample = new ReorderExample();
Thread t1 = new Thread(new Runnable() {
@Override
public void run() {
reorderExample.writer();
}
});
Thread t2 = new Thread(new Runnable() {
@Override
public void run() {
reorderExample.reader();
}
});
t1.start();
t2.start();
}
}
複製代碼
flag變量是個標記,用來標識變量a是否已被寫入。這裏假設有兩個線程A和B,A首先執行writer()方法,隨後B線程接着執行reader()方法。線程B在執行操做4時,可否看到線程A在操做1對共享變量a的寫入? 答案是:不必定能看到。 因爲操做1和操做2沒有數據依賴關係,編譯器和處理器能夠對這兩個操做重排序;一樣,操做3和操做4沒有數據依賴關係,編譯器和處理器也能夠對這兩個操做重排序。讓咱們先來看看,當操做1和操做2重排序時,可能會產生什麼效果?請看下面的程序執行時序圖:
※注:本文統一用紅色的虛箭線表示錯誤的讀操做,用綠色的虛箭線表示正確的讀操做。
下面再讓咱們看看,當操做3和操做4重排序時會產生什麼效果(藉助這個重排序,能夠順便說明控制依賴性)。下面是操做3和操做4重排序後,程序的執行時序圖:
從圖中咱們能夠看出,猜想執行實質上對操做3和4作了重排序。重排序在這裏破壞了多線程程序的語義!
在單線程程序中,對存在控制依賴的操做重排序,不會改變執行結果(這也是as-if-serial語義容許對存在控制依賴的操做作重排序的緣由);但在多線程程序中,對存在控制依賴的操做重排序,可能會改變程序的執行結果。