當多個線程同時共享,同一個全局變量或靜態變量,作寫的操做時,可能會發生數據衝突問題,也就是線程安全問題。可是作讀操做是不會發生數據衝突問題。java
案例:需求如今有100張火車票,有兩個窗口同時搶火車票,請使用多線程模擬搶票效果。編程
代碼:緩存
public class ThreadTrain implements Runnable {
private int trainCount = 100;
@Override
public void run() {
while (trainCount > 0) {
try {
Thread.sleep(50);
} catch (Exception e) {
}
sale();
}
}
public void sale() {
if (trainCount > 0) {
System.out.println(Thread.currentThread().getName() + ",出售第" + (100 - trainCount + 1) + "張票");
trainCount--;
}
}
public static void main(String[] args) {
ThreadTrain threadTrain = new ThreadTrain();
Thread t1 = new Thread(threadTrain, "①號");
Thread t2 = new Thread(threadTrain, "②號");
t1.start();
t2.start();
}
}複製代碼
運行結果:安全
一號窗口和二號窗口同時出售火車第九九張,部分火車票會重複出售。多線程
結論發現,多個線程共享同一個全局成員變量時,作寫的操做可能會發生數據衝突問題。ide
將可能會發生數據衝突問題(線程不安全問題),只能讓當前一個線程進行執行。代碼執行完成後釋放鎖,而後才能讓其餘線程進行執行。這樣的話就能夠解決線程不安全問題。函數
這樣多個線程共享同一個資源,不會受到其餘線程的干擾。性能
Java提供了一種內置的鎖機制來支持原子性優化
每個Java對象均可以用做一個實現同步的鎖,稱爲內置鎖,線程進入同步代碼塊以前自動獲取到鎖,代碼塊執行完成正常退出或代碼塊中拋出異常退出時會釋放掉鎖this
內置鎖爲互斥鎖,即線程A獲取到鎖後,線程B阻塞直到線程A釋放鎖,線程B才能獲取到同一個鎖
內置鎖使用synchronized關鍵字實現,synchronized關鍵字有兩種用法:
1.修飾須要進行同步的方法(全部訪問狀態變量的方法都必須進行同步),此時充當鎖的對象爲調用同步方法的對象
2.同步代碼塊和直接使用synchronized修飾須要同步的方法是同樣的,可是鎖的粒度能夠更細,而且充當鎖的對象不必定是this,也能夠是其它對象,因此使用起來更加靈活
public void sale() {
// 同步代碼塊(this明鎖),
synchronized (this) {
if (trainCount > 0) {
System.out.println(Thread.currentThread().getName() + ",出售第" + (100 - trainCount + 1) + "張票");
trainCount--;
}
}
}複製代碼
public synchronized void sale() {
if (trainCount > 0) {
System.out.println(Thread.currentThread().getName() + ",出售第" + (100 - trainCount + 1) + "張票");
trainCount--;
}
}複製代碼
方法上加上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 修飾靜態方法使用鎖是當前類的字節碼文件
同步中嵌套同步,致使鎖沒法釋放
class Thread009 implements Runnable {
private int trainCount = 100;
private Object oj = new Object();
public boolean flag = true;
public void run() {
if (flag) {
while (trainCount > 0) {
synchronized (oj) {
try {
Thread.sleep(10);
} catch (Exception e) {
// TODO: handle exception
}
sale();
}
}
} else {
while (trainCount > 0) {
sale();
}
}
}
public synchronized void sale() {
synchronized (oj) {
try {
Thread.sleep(10);
} catch (Exception e) {
}
if (trainCount > 0) {
System.out.println(Thread.currentThread().getName() + "," + "出售第" + (100 - trainCount + 1) + "票");
trainCount--;
}
}
}
}
public class Test009 {
public static void main(String[] args) throws InterruptedException {
Thread009 threadTrain = new Thread009();
Thread t1 = new Thread(threadTrain, "窗口1");
Thread t2 = new Thread(threadTrain, "窗口2");
t1.start();
Thread.sleep(40);
threadTrain.flag = false;
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 ThreadLocaDemo2 extends Thread {
private Res res;
public ThreadLocaDemo2(Res res) {
this.res = res;
}
@Override
public void run() {
for (int i = 0; i < 3; i++) {
System.out.println(Thread.currentThread().getName() + "---" + "i---" + i + "--num:" + res.getNum());
}
}
public static void main(String[] args) {
Res res = new Res();
ThreadLocaDemo2 threadLocaDemo1 = new ThreadLocaDemo2(res);
ThreadLocaDemo2 threadLocaDemo2 = new ThreadLocaDemo2(res);
ThreadLocaDemo2 threadLocaDemo3 = new ThreadLocaDemo2(res);
threadLocaDemo1.start();
threadLocaDemo2.start();
threadLocaDemo3.start();
}
}
複製代碼
ThreadLoca實現原理
ThreadLoca經過map集合
Map.put(「當前線程」,值);
Java內存結構(JVM),
原子性(保證線程安全、保持數據一致性);
可見性(某個線程修改全局變量的時候,其餘的線程也能獲取修改後的變量)
有序性
即一個操做或者多個操做 要麼所有執行而且執行的過程不會被任何因素打斷,要麼就都不執行。
一個很經典的例子就是銀行帳戶轉帳問題:
好比從帳戶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定義了線程和主內存之間的抽象關係:線程之間的共享變量存儲在主內存(main memory)中,每一個線程都有一個私有的本地內存(local memory),本地內存中存儲了該線程以讀/寫共享變量的副本。本地內存是JMM的一個抽象概念,並不真實存在。它涵蓋了緩存,寫緩衝區,寄存器以及其餘的硬件和編譯器優化。
Java內存模型(JMM java memory model),jmm決定一個線程對共享變量的寫入時,能對另外一個線程是否可見;
主內存:共享變量;
本地內存:共有變量的副本;
從上圖來看,線程A與線程B之間如要通訊的話,必需要經歷下面2個步驟:
可見性也就是說一旦某個線程修改了該被volatile修飾的變量,它會保證修改的值會當即被更新到主存,當有其餘線程須要讀取時,能夠當即獲取修改以後的值。
在Java中爲了加快程序的運行效率,對一些變量的操做一般是在該線程的寄存器或是CPU緩存上進行的,以後纔會同步到主存中,而加了volatile修飾符的變量則是直接讀寫主存。
Volatile 保證了線程間共享變量的及時可見性,但不能保證原子性:
class ThreadVolatileDemo extends Thread {
public volatile 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 ThreadVolatile {
public static void main(String[] args) throws InterruptedException {
ThreadVolatileDemo threadVolatileDemo = new ThreadVolatileDemo();
threadVolatileDemo.start();
Thread.sleep(3000);
threadVolatileDemo.setRuning(false);
System.out.println("flag 已經設置成false");
Thread.sleep(1000);
System.out.println(threadVolatileDemo.flag);
}
}複製代碼
Volatile與Synchronized區別
可是要注意volatile關鍵字是沒法替代synchronized關鍵字的,由於volatile關鍵字沒法保證操做的原子性。