為什么有線程安全問題?
當(dāng)多個線程同時共享,同一個全局變量或靜態(tài)變量,做寫的操作時,可能會發(fā)生數(shù)據(jù)沖突問題,也就是線程安全問題。但是做讀操作是不會發(fā)生數(shù)據(jù)沖突問題。
案例:需求現(xiàn)在有100張火車票,有兩個窗口同時搶火車票,請使用多線程模擬搶票效果。
- 代碼:
class ThreadTrain1 implements Runnable {
private int count = 100;
private static Object oj = new Object();
@Override
public void run() {
while (count > 0) {
try {
Thread.sleep(50);
} catch (Exception e) {
// TODO: handle exception
}
sale();
}
}
public void sale() {
// 前提 多線程進(jìn)行使用、多個線程只能拿到一把鎖。
// 保證只能讓一個線程 在執(zhí)行 缺點(diǎn)效率降低
// synchronized (oj) {
if (count > 0) {
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();
}
}
結(jié)論發(fā)現(xiàn),多個線程共享同一個全局成員變量時,做寫的操作可能會發(fā)生數(shù)據(jù)沖突問題。
線程安全解決辦法
問:如何解決多線程之間線程安全問題?
答:使用多線程之間同步synchronized或使用鎖(lock)。
問:為什么使用線程同步或使用鎖能解決線程安全問題呢?
答:將可能會發(fā)生數(shù)據(jù)沖突問題(線程不安全問題),只能讓當(dāng)前一個線程進(jìn)行執(zhí)行。代碼執(zhí)行完成后釋放鎖,然后才能讓其他線程進(jìn)行執(zhí)行。這樣的話就可以解決線程不安全問題。
問:什么是多線程之間同步?
答:當(dāng)多個線程共享同一個資源,不會受到其他線程的干擾。
同步代碼塊
- 什么是同步代碼塊?
就是將可能會發(fā)生線程安全問題的代碼,給包括起來。
synchronized(同一個數(shù)據(jù)){
可能會發(fā)生線程沖突問題
}
這就是同步代碼塊
定義:
synchronized(對象) //這個對象可以為任意對象
{
需要被同步的代碼
}
對象如同鎖,持有鎖的線程可以在同步中執(zhí)行 ,沒持有鎖的線程即使獲取CPU的執(zhí)行權(quán),也進(jìn)不去 。
-
同步的前提:
1,必須要有兩個或者兩個以上的線程
2,必須是多個線程使用同一個鎖
3,必須保證同步中只能有一個線程在運(yùn)行
好處:解決了多線程的安全問題
弊端:多個線程需要判斷鎖,較為消耗資源、搶鎖的資源。代碼樣例: private static Object oj = new Object(); public void sale() { // 前提 多線程進(jìn)行使用、多個線程只能拿到一把鎖。 // 保證只能讓一個線程 在執(zhí)行 缺點(diǎn)效率降低 synchronized (oj) { if (count > 0) { System.out.println(Thread.currentThread().getName() + ",出售第" + (100 - count + 1) + "票"); count--; } } }
同步函數(shù)
- 同步函數(shù):在方法上修飾synchronized 稱為同步函數(shù)。
- 同步函數(shù)用的是什么鎖?為什么?
答:同步函數(shù)使用this鎖。
證明方式: 一個線程使用同步代碼塊(this明鎖),另一個線程使用同步函數(shù)。如果兩個線程搶票不能實(shí)現(xiàn)同步,那么會出現(xiàn)數(shù)據(jù)錯誤。 - 代碼:
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() {
// 前提 多線程進(jìn)行使用、多個線程只能拿到一把鎖。
// 保證只能讓一個線程 在執(zhí)行 缺點(diǎn)效率降低
// 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();
}
}
靜態(tài)同步函數(shù)
靜態(tài)同步函數(shù):方法上加上static關(guān)鍵字,使用synchronized 關(guān)鍵字修飾,使用類.class文件作為鎖對象。
靜態(tài)的同步函數(shù)使用的鎖為該函數(shù)所屬字節(jié)碼文件對象,可以用 getClass方法獲取,也可以用當(dāng)前 類名.class 表示。
代碼樣例:
synchronized (ThreadTrain.class) {
System.out.println(Thread.currentThread().getName() + ",出售 第" + (100 - trainCount + 1) + "張票.");
trainCount--;
try {
Thread.sleep(100);
} catch (Exception e) {
}
}
面試常問:
一個線程使用同步函數(shù),另一個線程使用同步代碼塊(this),能夠?qū)崿F(xiàn)同步。
一個線程使用同步函數(shù),另一個線程使用同步代碼塊(非this),不能實(shí)現(xiàn)同步。
一個線程使用同步函數(shù),另一個線程使用靜態(tài)同步函數(shù),不能實(shí)現(xiàn)同步。
總結(jié):
同步函數(shù)使用this鎖;
同步代碼塊可使用任意對象鎖或者this鎖;
靜態(tài)同步函數(shù)使用類的字節(jié)碼.class文件鎖。
多線程死鎖
多線程死鎖:同步中嵌套同步,導(dǎo)致鎖無法釋放
-
代碼:
class ThreadTrain6 implements Runnable { // 這是貨票總票數(shù),多個線程會同時共享資源 private int trainCount = 100; public boolean flag = true; private Object obj= new Object(); @Override public void run() { if (flag) { while (true) { synchronized (obj) { // 鎖(同步代碼塊)在什么時候釋放? 代碼執(zhí)行完, 自動釋放鎖. // 如果flag為true 先拿到 obj鎖,在拿到this 鎖、 才能執(zhí)行。 // 如果flag為false先拿到this,在拿到obj鎖,才能執(zhí)行。 // 死鎖解決辦法:不要在同步中嵌套同步。 sale(); } } } else { while (true) { sale(); } } } public synchronized void sale() { synchronized (obj) { if (trainCount > 0) { try { Thread.sleep(40); } catch (Exception e) { } System.out.println(Thread.currentThread().getName() + ",出售 第" + (100 - trainCount + 1) + "張票."); trainCount--; } } } } public class DeadlockThread { public static void main(String[] args) throws InterruptedException { ThreadTrain6 threadTrain = new ThreadTrain6(); // 定義 一個實(shí)例 Thread thread1 = new Thread(threadTrain, "一號窗口"); Thread thread2 = new Thread(threadTrain, "二號窗口"); thread1.start(); Thread.sleep(40); threadTrain.flag = false; thread2.start(); } }
原因分析:
線程一先拿到同步代碼塊的obj鎖,再拿到同步函數(shù)的this鎖;
線程一先拿到同步函數(shù)的this鎖,再拿到同步代碼塊的obj鎖,
每個線程都需要對方的鎖,但又互不讓鎖,就會導(dǎo)致死鎖。
多線程有三大特性
原子性、可見性、有序性
什么是原子性
即一個操作或者多個操作 要么全部執(zhí)行并且執(zhí)行的過程不會被任何因素打斷,要么就都不執(zhí)行。
一個很經(jīng)典的例子就是銀行賬戶轉(zhuǎn)賬問題: 比如從賬戶A向賬戶B轉(zhuǎn)1000元,那么必然包括2個操作:從賬戶A減去1000元,往賬戶B加上1000元。這2個操作必須要具備原子性才能保證不出現(xiàn)一些意外的問題。
我們操作數(shù)據(jù)也是如此,比如i = i+1;其中就包括,讀取i的值,計算i,寫入i。這行代碼在Java中是不具備原子性的,則多線程運(yùn)行肯定會出問題,所以也需要我們使用同步和lock這些東西來確保這個特性了。
原子性其實(shí)就是保證數(shù)據(jù)一致、線程安全一部分,
什么是可見性
當(dāng)多個線程訪問同一個變量時,一個線程修改了這個變量的值,其他線程能夠立即看得到修改的值。
若兩個線程在不同的cpu,那么線程1改變了i的值還沒刷新到主存,線程2又使用了i,那么這個i值肯定還是之前的,線程1對變量的修改線程沒看到這就是可見性問題。
什么是有序性
程序執(zhí)行的順序按照代碼的先后順序執(zhí)行。
一般來說處理器為了提高程序運(yùn)行效率,可能會對輸入代碼進(jìn)行優(yōu)化,它不保證程序中各個語句的執(zhí)行先后順序同代碼中的順序一致,但是它會保證程序最終執(zhí)行結(jié)果和代碼順序執(zhí)行的結(jié)果是一致的。如下:
int a = 10; //語句1
int r = 2; //語句2
a = a + 3; //語句3
r = a*a; //語句4
則因?yàn)橹嘏判?,他還可能執(zhí)行順序?yàn)?2-1-3-4,1-3-2-4 但絕不可能 2-1-4-3,因?yàn)檫@打破了依賴關(guān)系。 顯然重排序?qū)尉€程運(yùn)行是不會有任何問題,而多線程就不一定了,所以我們在多線程編程時就得考慮這個問題了。
Java內(nèi)存模型
共享內(nèi)存模型指的就是Java內(nèi)存模型(簡稱JMM),JMM決定一個線程對共享變量的寫入時,能對另一個線程可見。從抽象的角度來看,JMM定義了線程和主內(nèi)存之間的抽象關(guān)系:線程之間的共享變量存儲在主內(nèi)存(main memory)中,每個線程都有一個私有的本地內(nèi)存(local memory),本地內(nèi)存中存儲了該線程以讀/寫共享變量的副本。本地內(nèi)存是JMM的一個抽象概念,并不真實(shí)存在。它涵蓋了緩存,寫緩沖區(qū),寄存器以及其他的硬件和編譯器優(yōu)化。

從上圖來看,線程A與線程B之間如要通信的話,必須要經(jīng)歷下面2個步驟:
1. 首先,線程A把本地內(nèi)存A中更新過的共享變量刷新到主內(nèi)存中去。
2. 然后,線程B到主內(nèi)存中去讀取線程A之前已更新過的共享變量。
下面通過示意圖來說明這兩個步驟:

如上圖所示,本地內(nèi)存A和B有主內(nèi)存中共享變量x的副本。假設(shè)初始時,這三個內(nèi)存中的x值都為0。線程A在執(zhí)行時,把更新后的x值(假設(shè)值為1)臨時存放在自己的本地內(nèi)存A中。當(dāng)線程A和線程B需要通信時,線程A首先會把自己本地內(nèi)存中修改后的x值刷新到主內(nèi)存中,此時主內(nèi)存中的x值變?yōu)榱?。隨后,線程B到主內(nèi)存中去讀取線程A更新后的x值,此時線程B的本地內(nèi)存的x值也變?yōu)榱?。
從整體來看,這兩個步驟實(shí)質(zhì)上是線程A在向線程B發(fā)送消息,而且這個通信過程必須要經(jīng)過主內(nèi)存。JMM通過控制主內(nèi)存與每個線程的本地內(nèi)存之間的交互,來為java程序員提供內(nèi)存可見性保證。
總結(jié):
Java內(nèi)存模型:java內(nèi)存模型簡稱jmm,定義了一個線程對另一個線程可見。共享變量存放在主內(nèi)存中,每個線程都有自己的本地內(nèi)存,當(dāng)多個線程同時訪問一個數(shù)據(jù)的時候,可能本地內(nèi)存沒有及時刷新到主內(nèi)存,所以就會發(fā)生線程安全問題。
Java內(nèi)存結(jié)構(gòu):是屬于jvm內(nèi)存分配,不要和Java內(nèi)存模型搞混。
volatile
volatile 關(guān)鍵字的作用是變量在多個線程之間可見。
- 代碼:
classThreadVolatileDemo extends Thread {
public boolean flag= true;
@Override
public void run() {
System.out.println("開始執(zhí)行子線程....");
while (flag) {
}
System.out.println("線程停止");
}
public void setRuning(boolean flag) {
this.flag= flag;
}
}
public class ThreadVolatile {
public static voidmain(String[] args) throws InterruptedException {
ThreadVolatileDemo threadVolatileDemo = new ThreadVolatileDemo();
threadVolatileDemo.start();
Thread.sleep(3000);
threadVolatileDemo.setRuning(false);
System.out.println("flag 已經(jīng)設(shè)置成false");
Thread.sleep(1000);
System.out.println(threadVolatileDemo.flag);
}
}
已經(jīng)將結(jié)果設(shè)置為fasle為什么?還一直在運(yùn)行呢。
原因:線程之間是不可見的,讀取的是副本,沒有及時讀取到主內(nèi)存結(jié)果。
解決辦法:使用volatile關(guān)鍵字將解決線程之間可見性, 強(qiáng)制線程每次讀取該值的時候都去“主內(nèi)存”中取值。
Volatile非原子性
注意: Volatile非原子性
public class VolatileNoAtomic extends Thread {
private static volatile int count;
// private static AtomicInteger count = new AtomicInteger(0);
private static void addCount() {
for (int i = 0;i< 1000;i++) {
count++;
// count.incrementAndGet();
}
System.out.println(count);
}
public void run() {
addCount();
}
public static void main(String[] args) {
VolatileNoAtomic[] arr = new VolatileNoAtomic[100];
for (int i = 0; i < 10; i++) {
arr[i] = new VolatileNoAtomic();
}
for (int i = 0; i < 10; i++) {
arr[i].start();
}
}
}
運(yùn)行結(jié)果:

結(jié)果發(fā)現(xiàn) 數(shù)據(jù)不同步,因?yàn)閂olatile不用具備原子性。
使用AtomicInteger原子類
AtomicInteger是一個提供原子操作的Integer類,通過線程安全的方式操作加減。(JDK1.5并發(fā)包中的類)
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();
}
System.out.println(count);
}
public static void main(String[] args) {
// 初始化10個線程
VolatileNoAtomic[] volatileNoAtomic = new VolatileNoAtomic[10];
for (int i = 0; i < 10; i++) {
// 創(chuàng)建
volatileNoAtomic[i] = new VolatileNoAtomic();
}
for (int i = 0; i < volatileNoAtomic.length; i++) {
volatileNoAtomic[i].start();
}
}
}
volatile與synchronized區(qū)別
僅靠volatile不能保證線程的安全性。(原子性)
①volatile輕量級,只能修飾變量。
synchronized重量級,還可修飾方法
②volatile只能保證數(shù)據(jù)的可見性,不能用來同步,因?yàn)槎鄠€線程并發(fā)訪問volatile修飾的變量不會阻塞。
synchronized不僅保證可見性,而且還保證原子性(數(shù)據(jù)一致),因?yàn)?,只有獲得了鎖的線程才能進(jìn)入臨界區(qū),從而保證臨界區(qū)中的所有語句都全部執(zhí)行。多個線程爭搶synchronized鎖對象時,會出現(xiàn)阻塞。
③線程安全性
線程安全性包括兩個方面:1)可見性,2)原子性。
從上面自增的例子中可以看出:僅僅使用volatile并不能保證線程安全性。而synchronized則可實(shí)現(xiàn)線程的安全性。