在多線程編程中,線程安全問題是一個最爲關鍵的問題,其核心概念就在於正確性,即當多個線程訪問某一共享、可變數據時,始終都不會導致數據破壞以及其他不該出現的結果。而所有的併發模式在解決這個問題時,採用的方案都是序列化訪問臨界資源 。在 Java 中,提供了兩種方式來實現同步互斥訪問:synchronized 和 Lock。本文針對 synchronized 內置鎖 詳細討論了其在 Java 併發 中的應用,包括它的具體使用場景(同步方法、同步代碼塊、實例對象鎖 和 Class 對象鎖)、可重入性 和 注意事項。synchronized 使得在一段時間內只有一個任務可以運行這段代碼。因爲鎖語句產生了一種互相排斥的效果。這種機制常常稱爲互斥量(mute)
一. 線程安全問題
在單線程中不會出現線程安全問題,而在多線程編程中,有可能會出現同時訪問同一個 共享、可變資源 的情況,這種資源可以是:一個變量、一個對象、一個文件等。特別注意兩點,
- 共享: 意味着該資源可以由多個線程同時訪問;
可變: 意味着該資源可以在其生命週期內被修改。
所以,當多個線程同時訪問這種資源的時候,就會存在一個問題:
由於每個線程執行的過程是不可控的,所以需要採用同步機制來協同對對象可變狀態的訪問。
package com.huanghe.chapter21;
/**
* @Author: River
* @Date:Created in 20:58 2018/5/31
* @Description: 用買票的案例說明線程的安全問題
*/
class Ticket implements Runnable {
private int num=100;
@Override
public void run() {
while (true) {
if (num > 0) {
try {
Thread.sleep(10);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName()+"......sale....."+num--);
}
}
}
}
public class TicketDemo {
public static void main(String[] args) {
Ticket ticket = new Ticket();
Thread t1=new Thread(ticket);
Thread t2=new Thread(ticket);
Thread t3=new Thread(ticket);
Thread t4=new Thread(ticket);
t1.start();
t2.start();
t3.start();
t4.start();
}
}
輸出的結果最後的幾條數據是:
Thread-0--sale---3
Thread-3--sale---2
Thread-2--sale---1
Thread-1--sale---0
Thread-0--sale----1
Thread-3--sale----2
Process finished with exit code 1
2. 線程安全問題的原因
這其實就是一個線程安全問題,即多個線程同時訪問一個資源時,會導致程序運行結果並不是想看到的結果。這裏面,這個資源被稱爲:臨界資源。也就是說,當多個線程同時訪問臨界資源(一個對象,對象中的屬性,一個文件,一個數據庫等)時,就可能會產生線程安全問題。
不過,當多個線程執行一個方法時,該方法內部的局部變量並不是臨界資源,因爲這些局部變量是在每個線程的私有棧中,因此不具有共享性,不會導致線程安全問題。
1:多個線程在操作共享的數據
2:操作共享的線程代碼有多條
3. 線程安全問題的解決方式
實際上,所有的併發模式在解決線程安全問題時,採用的方案都是 序列化訪問臨界資源 。即在同一時刻,只能有一個線程訪問臨界資源,也稱作 同步互斥訪問。換句話說,就是在訪問臨界資源的代碼前面加上一個鎖,當訪問完臨界資源後釋放鎖,讓其他線程繼續訪問。
在 Java 中,提供了兩種方式來實現同步互斥訪問:synchronized 和 Lock。本文主要講述 synchronized 的使用方法
4. synchronized 同步方法或者同步塊
在瞭解 synchronized 關鍵字的使用方法之前,我們先來看一個概念:互斥鎖,即 能到達到互斥訪問目的的鎖。舉個簡單的例子,如果對臨界資源加上互斥鎖,當一個線程在訪問該臨界資源時,其他線程便只能等待。
在 Java 中,可以使用 synchronized 關鍵字來標記一個方法或者代碼塊,當某個線程調用該對象的synchronized方法或者訪問synchronized代碼塊時,這個線程便獲得了該對象的鎖,其他線程暫時無法訪問這個方法,只有等待這個方法執行完畢或者代碼塊執行完畢,這個線程纔會釋放該對象的鎖,其他線程才能執行這個方法或者代碼塊。
同步代碼塊的格式:
synchronized(對象){
需要被同步的代碼;
}
package com.huanghe.chapter21;
/**
* @Author: River
* @Date:Created in 20:58 2018/5/31
* @Description: 用買票的案例說明線程的安全問題
*/
class Ticket implements Runnable {
private int num = 100;
Object obj = new Object();
@Override
public void run() {
while (true) {
synchronized (obj) {
if (num > 0) {
try {
Thread.sleep(10);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + "......sale....." + num--);
}
}
}
}
}
public class TicketDemo {
public static void main(String[] args) {
Ticket ticket = new Ticket();
Thread t1 = new Thread(ticket);
Thread t2 = new Thread(ticket);
Thread t3 = new Thread(ticket);
Thread t4 = new Thread(ticket);
t1.start();
t2.start();
t3.start();
t4.start();
}
}
當在某個線程中執行這段代碼塊,該線程會獲取對象lock的鎖,從而使得其他線程無法同時訪問該代碼塊。其中,lock 可以是 this,代表獲取當前對象的鎖,也可以是類中的一個屬性,代表獲取該屬性的鎖。特別地, 實例同步方法 與 synchronized(this)同步塊 是互斥的,因爲它們鎖的是同一個對象。但與 synchronized(非this)同步塊 是異步的,因爲它們鎖的是不同對象。
synchronized方法
package com.huanghe.chapter21;
import sun.invoke.util.BytecodeName;
/**
* @Author: River
* @Date:Created in 22:09 2018/5/31
* @Description:
*/
public class BankDemo {
public static void main(String[] args) {
Custom c = new Custom();
Thread t1 = new Thread(c);
Thread t2 = new Thread(c);
t1.start();
t2.start();
}
}
class Bank{
private int sum;
//這個方法會引起線程不安全問題,比如線程1進來執行了sum=0+100=100;之後切換到了線程2進行執行sum=sum+100=200,線程2執行之後
//輸出的是200,線程2執行之後切換到線程1輸出200,所以會輸出200,200,這就出現問題了,此時在方法出添加synchronized,就可以避免
public synchronized void add(int num) {
sum = sum + num;
System.out.println("sum="+sum);
}
}
class Custom implements Runnable {
private Bank b = new Bank();
@Override
public void run() {
for (int i = 0; i <3 ; i++) {
b.add(100);
}
}
}
不過需要注意以下三點:
1)當一個線程正在訪問一個對象的 synchronized 方法,那麼其他線程不能訪問該對象的其他 synchronized 方法。這個原因很簡單,因爲一個對象只有一把鎖,當一個線程獲取了該對象的鎖之後,其他線程無法獲取該對象的鎖,所以無法訪問該對象的其他synchronized方法。
2)當一個線程正在訪問一個對象的 synchronized 方法,那麼其他線程能訪問該對象的非 synchronized 方法。這個原因很簡單,訪問非 synchronized 方法不需要獲得該對象的鎖,假如一個方法沒用 synchronized 關鍵字修飾,說明它不會使用到臨界資源,那麼其他線程是可以訪問這個方法的。
3)如果一個線程 A 需要訪問對象 object1 的 synchronized 方法 fun1,另外一個線程 B 需要訪問對象 object2 的 synchronized 方法 fun1,即使 object1 和 object2 是同一類型),也不會產生線程安全問題,因爲他們訪問的是不同的對象,所以不存在互斥問題。
驗證同步代碼塊使用的是哪個鎖?package com.huanghe.chapter21;
/**
* @Author: River
* @Date:Created in 9:32 2018/6/1
* @Description:
*/
public class SynFunctionLockDemo {
public static void main(String[] args) {
Ticket1 t = new Ticket1();
System.out.println(t);
Thread t1 = new Thread(t);
Thread t2 = new Thread(t);
t1.start();
//讓主線程sleep
try {
Thread.sleep(10);
} catch (InterruptedException e) {
e.printStackTrace();
}
t.flag=false;
t2.start();
}
}
class Ticket1 implements Runnable {
private int num = 100;
Object obj = new Object();
boolean flag = true;
@Override
public void run() {
if (flag) {
while (true) {
synchronized (this) {
if (num > 0) {
try {
Thread.sleep(10);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + "......obj....." + num--);
}
}
}
} else {
while (true) {
show();
}
}
}
public synchronized void show() {
if (num > 0) {
try {
Thread.sleep(10);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + "......fun....." + num--);
}
}
}
結果:
Thread-0......obj.....100
Thread-0......obj.....99
Thread-0......obj.....98
Thread-1......fun.....97
Thread-1......fun.....96
Thread-1......fun.....95
Thread-1......fun.....94
Thread-1......fun.....93
Thread-1......fun.....92
Thread-1......fun.....91
Thread-1......fun.....90
Thread-1......fun.....89
Thread-1......fun.....88
Thread-1......fun.....87
Thread-1......fun.....86
Thread-1......fun.....85
Thread-1......fun.....84
Thread-1......fun.....83
Thread-1......fun.....82
Thread-1......fun.....81
Thread-1......fun.....80
Thread-1......fun.....79
Thread-1......fun.....78
Thread-1......fun.....77
Thread-1......fun.....76
Thread-1......fun.....75
Thread-1......fun.....74
Thread-1......fun.....73
Thread-1......fun.....72
Thread-1......fun.....71
Thread-1......fun.....70
Thread-1......fun.....69
可以驗證同步函數使用的鎖是this
同步函數和同步代碼塊的區別:
1:同步方法使用synchronized修飾方法,在調用該方法前,需要獲得內置鎖(java每個對象都有一個內置鎖),否則就處於阻塞狀態
2:同步代碼塊使用synchronized(object){}進行修飾,在調用該代碼塊時,需要獲得內置鎖,否則就處於阻塞狀態
3:同步函數使用的鎖匙this,而同步代碼塊使用的鎖匙任意的對象
靜態同步函數使用的鎖(class 對象鎖,類.class):
特別地,每個類也會有一個鎖,靜態的 synchronized方法 就是以Class對象作爲鎖。另外,它可以用來控制對 static 數據成員 (static 數據成員不專屬於任何一個對象,是類成員) 的併發訪問。並且,如果一個線程執行一個對象的非static synchronized 方法,另外一個線程需要執行這個對象所屬類的 static synchronized 方法,也不會發生互斥現象。因爲訪問 static synchronized 方法佔用的是類鎖,而訪問非 static synchronized 方法佔用的是對象鎖,所以不存在互斥現象。
public class Test {
public static void main(String[] args) {
final InsertData insertData = new InsertData();
new Thread(){
@Override
public void run() {
insertData.insert();
}
}.start();
new Thread(){
@Override
public void run() {
insertData.insert1();
}
}.start();
}
}
class InsertData {
// 非 static synchronized 方法
public synchronized void insert(){
System.out.println("執行insert");
try {
Thread.sleep(5000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("執行insert完畢");
}
// static synchronized 方法
public synchronized static void insert1() {
System.out.println("執行insert1");
System.out.println("執行insert1完畢");
}
}/* Output:
執行insert
執行insert1
執行insert1完畢
執行insert完畢
*///:~
根據執行結果,我們可以看到第一個線程裏面執行的是insert方法,不會導致第二個線程執行insert1方法發生阻塞現象。下面,我們看一下 synchronized 關鍵字到底做了什麼事情,我們來反編譯它的字節碼看一下,下面這段代碼反編譯後的字節碼爲:
有一點要注意:對於 synchronized方法 或者 synchronized代碼塊,當出現異常時,JVM會自動釋放當前線程佔用的鎖,因此不會由於異常導致出現死鎖現象。
四. 可重入性
一旦有一個線程訪問某個對象的synchronized修飾的方法或代碼區域時,該線程則獲取這個對象的鎖,其他線程不能再調用該對象被synchronized影響的任何方法。那麼,如果這個線程自己調用該對象的其他synchronized方法,Java是如何判定的?這就涉及到了Java中鎖的重要特性:可重入性,
重入的一種實現方法是,爲每個鎖關聯一個獲取計數值和一個所有者線程。當計數值爲0時,這個鎖就被認爲是沒有被任何線程所持有,當線程請求一個未被持有的鎖時,JVM將記下鎖的持有者,並且將獲取計數值置爲1,如果同一個線程再次獲取這個鎖,計數值將遞增,而當線程退出同步代碼塊時,計數器會相應地遞減。當計數值爲0時,這個鎖將被釋放。
public class Father
{
public synchronized void doSomething(){
......
}
}
public class Child extends Father
{
public synchronized void doSomething(){
......
super.doSomething();
}
}
子類覆寫了父類的同步方法,然後調用父類中的方法,此時如果沒有可重入的鎖,那麼這段代碼件產生死鎖。
由於Father和Child中的doSomething方法都是synchronized方法,因此每個doSomething方法在執行前都會獲取Child對象實例上的鎖。如果內置鎖不是可重入的,那麼在調用super.doSomething時將無法獲得該Child對象上的互斥鎖,因爲這個鎖已經被持有,從而線程會永遠阻塞下去,一直在等待一個永遠也無法獲取的鎖。重入則避免了這種死鎖情況的發生。
同一個線程在調用本類中其他synchronized方法/塊或父類中的synchronized方法/塊時,都不會阻礙該線程地執行,因爲互斥鎖時可重入的。
五. 死鎖
常見的情景之一是同步的嵌套
package com.huanghe.chapter21;
/**
* @Author: River
* @Date:Created in 10:49 2018/6/1
* @Description:
*/
public class DeadLockTest {
public static void main(String[] args) {
Test a = new Test(true);
Test b = new Test(false);
Thread t1 = new Thread(a);
Thread t2 = new Thread(b);
}
}
class Test implements Runnable{
private boolean flag;
Test(boolean flag) {
this.flag=flag;
}
@Override
public void run() {
if (flag) {
synchronized (MyLock.locka) {
System.out.println(Thread.currentThread().getName()+"if locka....");
synchronized (MyLock.lockb) {
System.out.println(Thread.currentThread().getName()+"if locka....");
}
}
} else {
synchronized (MyLock.lockb) {
System.out.println(Thread.currentThread().getName()+"else lockb....");
synchronized (MyLock.locka) {
System.out.println(Thread.currentThread().getName()+"else locka.....");
}
}
}
}
}
class MyLock{
public static final Object locka=new Object();
public static final Object lockb=new Object();
}
輸出的結果:
Thread-1 else lockb.......
Thread-0 if locka.......
從結果中可以看出來,當線程1拿到了b鎖,所以執行了else lockb.......,而線程0拿到了a鎖執行if locka
線程0接下來需要去執行第二條語句的時候由於b鎖被線程1拿着所以無法執行,線程1接下來需要去執行第二條語句的時候需要locka,但是locka被線程0擁有着,所以出現了死鎖的情況。