title: 一文教會你什么執行緒安全以及如何實作執行緒安全
tags: 執行緒安全 Java并發 Java記憶體模型 synchronized volatile Lock
文章目錄
- 一、執行緒安全的概念
- 二、導致執行緒不安全的原因
- 三、執行緒安全問題
- 3.1 原子性
- 3.2 可見性
- 3.3 有序性
- 案例:搶票
- 四、如何確保執行緒安全?
- 4.1 synchronized關鍵字
- 4.1.1 同步代碼塊
- 4.1.2 同步函式
- 4.1.3 多執行緒死鎖執行緒
- 4.2 Lock
- 4.3 volatile關鍵字
- 解決辦法:
- 4.4 synchronized、volatile和Lock之間的區別
一、執行緒安全的概念
執行緒安全是多執行緒編程是的計算機程式代碼中的一個概念,在擁有共享資料的多條執行緒并行執行的程式中,執行緒安全的代碼會通過同步機制保證各個執行緒都可以正常且準確的執行,不會出現資料污染等意外情況,上述是百度百科給出的一個概念解釋,換言之,執行緒安全就是某個函式在并發環境中呼叫時,能夠處理好多個執行緒之間的共享變數,是程式能夠正確執行完畢,也就是說我們想要確保在多執行緒訪問的時候,我們的程式還能夠按照我們的預期的行為去執行,那么就是執行緒安全了,
二、導致執行緒不安全的原因
首先,可以來看一段代碼,來看看是不是執行緒安全的,代碼如下:
package com.company;
public class TestThread {
private static class XRunnable implements Runnable{
private int count;
public void run(){
for(int i= 0; i<5; i++){
getCount();
}
}
public void getCount(){
count++;
System.out.println(" "+count);
}
}
public static void main(String[] args) {
XRunnable runnable = new XRunnable();
Thread t1 = new Thread(runnable);
Thread t2 = new Thread(runnable);
Thread t3 = new Thread(runnable);
t1.start();
t2.start();
t3.start();
}
}
輸出的結果為:
2
3
2
5
4
7
6
10
11
12
9
8
13
14
15
從代碼上進行分析,當啟動了三個執行緒,每個執行緒應該都是回圈5次得出1到15的結果,但是從輸出的結果,就可以看到有兩個2輸出,出現像這種情況表明這個方法根本就不是執行緒安全的,我們可以這樣理解:在每個行程的記憶體空間中都會有一塊特殊的公共區域,通常稱為堆(記憶體),之所以會輸出兩個2,是因為行程內的所有執行緒都可以訪問到該區域,當第一個執行緒已經獲得2這個數了,還沒來得及輸出,下一個執行緒在這段時間的空隙獲得了2這個值,故輸出時會輸出2的值,
三、執行緒安全問題
要考慮執行緒安全問題,就需要先考慮Java并發的三大基本特性:原子性、可見性以及有序性,
3.1 原子性
原子性是指在一個操作中就是cpu不可以在中途暫停然后再調度,即不被中斷操作,要不全部執行完成,要不都不執行,就好比轉賬,從賬戶A向賬戶B轉1000元,那么必然包括2個操作:從賬戶A減去1000元,往賬戶B加上1000元,2個操作必須全部完成,
那程式中原子性指的是最小的操作單元,比如自增操作,它本身其實并不是原子性操作,分了3步的,包括讀取變數的原始值、進行加1操作、寫入作業記憶體,所以在多執行緒中,有可能一個執行緒還沒自增完,可能才執行到第二部,另一個執行緒就已經讀取了值,導致結果錯誤,那如果我們能保證自增操作是一個原子性的操作,那么就能保證其他執行緒讀取到的一定是自增后的資料,
3.2 可見性
當多個執行緒訪問同一個變數時,一個執行緒修改了這個變數的值,其他執行緒能夠立即看得到修改的值,
若兩個執行緒在不同的cpu,那么執行緒1改變了i的值還沒重繪到主存,執行緒2又使用了i,那么這個i值肯定還是之前的,執行緒1對變數的修改執行緒沒看到這就是可見性問題,
3.3 有序性
程式執行的順序按照代碼的先后順序執行,在多執行緒編程時就得考慮這個問題,
案例:搶票
當多個執行緒同時共享,同一個全域變數或靜態變數(即區域變數不會),做寫的操作時,可能會發生資料沖突問題,也就是執行緒安全問題,但是做讀操作是不會發生資料沖突問題,
Consumer類:
package com.company;
public class Consumer implements Runnable{
private int ticket = 100;
public void run(){
while(ticket>0){
System.out.println(Thread.currentThread().getName() + "售賣第" + (100-ticket+1) + "張票");
ticket--;
}
}
}
主類:
package com.company;
public class ThreadSafeProblem {
public static void main(String[] args){
Consumer abc = new Consumer();
new Thread(abc, "視窗1").start();
new Thread(abc, "視窗2").start();
}
}
結果:
從輸出結果來看,售票視窗買票出現了計票的問題,這就是執行緒安全出現問題了,
四、如何確保執行緒安全?
解決辦法:使用多執行緒之間使用關鍵字synchronized、或者使用鎖(lock),或者volatile關鍵字,
①synchronized(自動鎖,鎖的創建和釋放都是自動的);
②lock 手動鎖(手動指定鎖的創建和釋放),
③volatile關鍵字
為什么能解決?如果可能會發生資料沖突問題(執行緒不安全問題),只能讓當前一個執行緒進行執行,代碼執行完成后釋放鎖,然后才能讓其他執行緒進行執行,這樣的話就可以解決執行緒不安全問題,
4.1 synchronized關鍵字
4.1.1 同步代碼塊
synchronized(同一個鎖){
//可能會發生執行緒沖突問題
}
將可能會發生執行緒安全問題地代碼,給包括起來,也稱為同步代碼塊,synchronized使用的鎖可以是物件鎖也可以是靜態資源,如×××.class,只有持有鎖的執行緒才能執行同步代碼塊中的代碼,沒持有鎖的執行緒即使獲取cpu的執行權,也進不去,
鎖的釋放是在synchronized同步代碼執行完畢后自動釋放,
同步的前提:
1,必須要有兩個或者兩個以上的執行緒 ,如果小于2個執行緒,則沒有用,且還會消耗性能(獲取鎖,釋放鎖)
2,必須是多個執行緒使用同一個鎖
弊端:多個執行緒需要判斷鎖,較為消耗資源、搶鎖的資源,
例子:
public class ThreadSafeProblem {
public static void main(String[] args) {
Consumer abc = new Consumer();
// 注意要使用同一個abc變數作為thread的引數,
// 如果你使用了兩個Consumer物件,那么就不會共享ticket了,就自然不會出現執行緒安全問題
new Thread(abc,"視窗1").start();
new Thread(abc,"視窗2").start();
}
}
class Consumer implements Runnable{
private int ticket = 100;
@Override
public void run() {
while (ticket > 0) {
synchronized (Consumer.class) {
if (ticket > 0) {
System.out.println(Thread.currentThread().getName() + "售賣第" + (100-ticket+1) + "張票");
ticket--;
}
}
}
}
}
4.1.2 同步函式
就是將synchronized加在方法上,
分為兩種:
第一種是非靜態同步函式,即方法是非靜態的,使用的this物件鎖,如下代碼所示
第二種是靜態同步函式,即方法是用static修飾的,使用的鎖是當前類的class檔案(xxx.class),
public synchronized void sale () {
if (ticket > 0) {
System.out.println(Thread.currentThread().getName() + "售賣第" + (100-ticket+1) + "張票");
ticket--;
}
}
4.1.3 多執行緒死鎖執行緒
如下代碼所示,
執行緒t1,運行后在同步代碼塊中需要oj物件鎖,,運行到sale方法時需要this物件鎖
執行緒t2,運行后需要呼叫sale方法,需要先獲取this鎖,再獲取oj物件鎖
那這樣就會造成,兩個執行緒相互等待對方釋放鎖,就造成了死鎖情況,簡單來說就是:
同步中嵌套同步,導致鎖無法釋放,
class ThreadTrain3 implements Runnable {
private static int count = 100;
public boolean flag = true;
private static Object oj = new Object();
@Override
public void run() {
if (flag) {
while (true) {
synchronized (oj) {
sale();
}
}
} else {
while (true) {
sale();
}
}
}
public static 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 ThreadDemo3 {
public static void main(String[] args) throws InterruptedException {
ThreadTrain3 threadTrain1 = new ThreadTrain3();
Thread t1 = new Thread(threadTrain1, "①號視窗");
Thread t2 = new Thread(threadTrain1, "②號視窗");
t1.start();
Thread.sleep(40);
threadTrain1.flag = false;
t2.start();
}
}
4.2 Lock
可以視為synchronized的增強版,提供了更靈活的功能,該介面提供了限時鎖等待、鎖中斷、鎖嘗試等功能,synchronized實作的同步代碼塊,它的鎖是自動加的,且當執行完同步代碼塊或者拋出例外后,鎖的釋放也是自動的,
Lock l = ...;
l.lock();
try {
// access the resource protected by this lock
} finally {
l.unlock();
}
但是Lock鎖是需要手動去加鎖和釋放鎖,所以Lock相比于synchronized更加的靈活,且還提供了更多的功能比如說
tryLock()方法會嘗試獲取鎖,如果鎖不可用則回傳false,如果鎖是可以使用的,那么就直接獲取鎖且回傳true,官方代碼如下:
Lock lock = ...;
if (lock.tryLock()) {
try {
// manipulate protected state
} finally {
lock.unlock();
}
} else {
// perform alternative actions
}
例子:
/*
* 使用ReentrantLock類實作同步
* */
class MyReenrantLock implements Runnable{
//向上轉型
private Lock lock = new ReentrantLock();
public void run() {
//上鎖
lock.lock();
for(int i = 0; i < 5; i++) {
System.out.println("當前執行緒名: "+ Thread.currentThread().getName()+" ,i = "+i);
}
//釋放鎖
lock.unlock();
}
}
public class MyLock {
public static void main(String[] args) {
MyReenrantLock myReenrantLock = new MyReenrantLock();
Thread thread1 = new Thread(myReenrantLock);
Thread thread2 = new Thread(myReenrantLock);
Thread thread3 = new Thread(myReenrantLock);
thread1.start();
thread2.start();
thread3.start();
}
}
輸出結果:
由此我們可以看出,只有當當前執行緒列印完畢后,其他的執行緒才可繼續列印,執行緒列印的資料是分組列印,因為當前執行緒持有鎖,但執行緒之間的列印順序是隨機的,
即呼叫lock.lock() 代碼的執行緒就持有了“物件監視器”,其他執行緒只有等待鎖被釋放再次爭搶,
4.3 volatile關鍵字
先來看一段錯誤的代碼示例:
class ThreadVolatileDemo extends Thread {
public boolean flag = true;
@Override
public void run() {
System.out.println("子執行緒開始執行");
while (flag) {
}
System.out.println("子執行緒執行結束...");
}
public void setFlag(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.setFlag(false);
System.out.println("flag已被修改為false!");
}
}
輸出結果:

雖然flag已被修改,但是子執行緒依然在執行,這里產生的原因就是Java記憶體模型(JMM) 導致的,
由于主執行緒休眠了3秒,所以子執行緒沒有意外的話是一定會被執行run方法的,而當子執行緒由于呼叫start方法而執行run方法時,會將flag這個共享變數拷貝一份副本存到執行緒的本地記憶體中,此時執行緒中的flag為true,即使主執行緒在休眠后修改了flag值為false,子執行緒也不會知道,即不會修改自己副本的flag值,所以這就導致了該問題的出現,
? 注意:在測驗時,一定要讓主執行緒進行sleep或其他耗時操作,如果沒有這步操作,很有可能在子執行緒執行run方法而拷貝共享變數到執行緒本地記憶體之前,主執行緒就已經修改了flag值,
這里再來介紹一下Java記憶體模型吧!!!
在Java記憶體模型規定了所有的變數(這里的變數是指成員變數,靜態欄位等但是不包括區域變數和方法引數,因為這是執行緒私有的)都存盤在主記憶體中,每條執行緒還有自己的作業記憶體,執行緒的作業記憶體中拷貝了該執行緒使用到的主記憶體中的變數(只是副本,從主記憶體中拷貝了一份,放到了執行緒的本地記憶體中),執行緒對變數的所有操作都必須在作業記憶體中進行,而不能直接讀寫主記憶體, 不同的執行緒之間也無法直接訪問對方作業記憶體中的變數,執行緒間變數的傳遞均需要自己的作業記憶體和主存之間進行資料同步進行,
而JMM就作用于作業記憶體和主存之間資料同步程序,他規定了如何做資料同步以及什么時候做資料同步,

1. 首先要將共享變數從主記憶體拷貝到執行緒自己的作業記憶體空間,作業記憶體中存盤著主記憶體中的變數副本拷貝;
2. 執行緒對副本變數進行操作,(不能直接操作主記憶體);
3. 操作完成后通過JMM 將執行緒的共享變數副本與主記憶體進行資料的同步,將資料寫入主記憶體中;
4. 不同的執行緒間無法訪問對方的作業記憶體,執行緒間的通信(傳值)必須通過主記憶體來完成,
當多個執行緒同時訪問一個資料的時候,可能本地記憶體沒有及時重繪到主記憶體,所以就會發生執行緒安全問題
JMM是在執行緒調run方法的時候才將共享變數寫到自己的執行緒本地記憶體中去的,而不是在呼叫start方法的時候,
解決辦法:
當出現這種問題時,就可以使用Volatile關鍵字進行解決,
Volatile 關鍵字的作用是變數在多個執行緒之間可見,使用Volatile關鍵字將解決執行緒之間可見性,強制執行緒每次讀取該值的時候都去“主記憶體”中取值,
只需要在flag屬性上加上該關鍵字即可,
public volatile boolean flag = true;
子執行緒每次都不是讀取的執行緒本地記憶體中的副本變數了,而是直接讀取主記憶體中的屬性值,
volatile雖然具備可見性,但是不具備原子性,
4.4 synchronized、volatile和Lock之間的區別
synochronizd和volatile關鍵字區別:
1)volatile關鍵字解決的是變數在多個執行緒之間的可見性;而sychronized關鍵字解決的是多個執行緒之間訪問共享資源的同步性,
tip: final關鍵字也能實作可見性:被final修飾的欄位在構造器中一旦初始化完成,并且構造器沒有把 “this” 的參考傳遞出去(this參考逃逸是一件很危險的事情,其它執行緒有可能通過這個參考訪問到了"初始化一半"的物件),那在其他執行緒中就能看見final;
2)volatile只能用于修飾變數,而synchronized可以修飾方法,以及代碼塊,(volatile是執行緒同步的輕量級實作,所以volatile性能比synchronized要好,并且隨著JDK新版本的發布,sychronized關鍵字在執行上得到很大的提升,在開發中使用synchronized關鍵字的比率還是比較大);
3)多執行緒訪問volatile不會發生阻塞,而sychronized會出現阻塞;
4)volatile能保證變數在多個執行緒之間的可見性,但不能保證原子性;而sychronized可以保證原子性,也可以間接保證可見性,因為它會將私有記憶體和公有記憶體中的資料做同步,
執行緒安全包含原子性和可見性兩個方面,
對于用volatile修飾的變數,JVM虛擬機只是保證從主記憶體加載到執行緒作業記憶體的值是最新的,
一句話說明volatile的作用:實作變數在多個執行緒之間的可見性,
synchronized和lock區別:
1)Lock是一個介面,而synchronized是Java中的關鍵字,synchronized是內置的語言實作;
2)synchronized在發生例外時,會自動釋放執行緒占有的鎖,因此不會導致死鎖現象發生;而Lock在發生例外時,如果沒有主動通過unLock()去釋放鎖,則很可能造成死鎖現象,因此使用Lock時需要在finally塊中釋放鎖;
3)Lock可以讓等待鎖的執行緒回應中斷,而synchronized卻不行,使用synchronized時,等待的執行緒會一直等待下去,不能夠回應中斷;
4)通過Lock可以知道有沒有成功獲取鎖,而synchronized卻無法辦到,
5)Lock可以提高多個執行緒進行讀操作的效率(讀寫鎖),
在性能上來說,如果競爭資源不激烈,兩者的性能是差不多的,而當競爭資源非常激烈時(即有大量執行緒同時競爭),此時Lock的性能要遠遠優于synchronized,所以說,在具體使用時要根據適當情況選擇,
轉載請註明出處,本文鏈接:https://www.uj5u.com/qita/236029.html
標籤:其他
