淺談 Synchronized 的幾種用法,超多干貨!
01、背景介紹
說到并發(fā)編程,總繞不開線程安全的問題。
實際上,在多線程環(huán)境中,難免會出現(xiàn)多個線程對一個對象的實例變量進行同時訪問和操作,如果編程處理不當,會產(chǎn)生臟讀現(xiàn)象。
02、線程安全問題回顧
我們先來看一個簡單的線程安全問題的例子!
public class DataEntity {
private int count = 0;
public void addCount(){
count++;
}
public int getCount(){
return count;
}
}
public class MyThread extends Thread {
private DataEntity entity;
public MyThread(DataEntity entity) {
this.entity = entity;
}
@Override
public void run() {
for (int j = 0; j < 1000000; j++) {
entity.addCount();
}
}
}
public class MyThreadTest {
public static void main(String[] args) {
// 初始化數(shù)據(jù)實體
DataEntity entity = new DataEntity();
//使用多線程編程對數(shù)據(jù)進行計算
for (int i = 0; i < 10; i++) {
MyThread thread = new MyThread(entity);
thread.start();
}
try {
Thread.sleep(500);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("result: " + entity.getCount());
}
}
多次運行結(jié)果如下:
第一次運行:result: 9788554
第二次運行:result: 9861461
第三次運行:result: 6412249
...
上面的代碼中,總共開啟了 10 個線程,每個線程都累加了 1000000 次,如果結(jié)果正確的話,自然而然總數(shù)就應(yīng)該是 10 * 1000000 = 10000000。
但是多次運行結(jié)果都不是這個數(shù),而且每次運行結(jié)果都不一樣,為什么會出現(xiàn)這個結(jié)果呢?
簡單的說,這是主內(nèi)存和線程的工作內(nèi)存數(shù)據(jù)不一致,以及多線程執(zhí)行時無序,共同造成的結(jié)果!
我們先簡單的了解一下 Java 的內(nèi)存模型,后期我們在介紹里面的原理!
圖片
如上圖所示,線程 A 和線程 B 之間,如果要完成數(shù)據(jù)通信的話,需要經(jīng)歷以下幾個步驟:
- 1.線程 A 從主內(nèi)存中將共享變量讀入線程 A 的工作內(nèi)存后并進行操作,之后將數(shù)據(jù)重新寫回到主內(nèi)存中;
- 2.線程 B 從主存中讀取最新的共享變量,然后存入自己的工作內(nèi)存中,再進行操作,數(shù)據(jù)操作完之后再重新寫入到主內(nèi)存中;
如果線程 A 更新后數(shù)據(jù)并沒有及時寫回到主存,而此時線程 B 從主內(nèi)存中讀到的數(shù)據(jù),可能就是過期的數(shù)據(jù),于是就會出現(xiàn)“臟讀”現(xiàn)象。
因此在多線程環(huán)境下,如果不進行一定干預(yù)處理,可能就會出現(xiàn)像上文介紹的那樣,采用多線程編程時,程序的實際運行結(jié)果與預(yù)期會不一致,就會產(chǎn)生非常嚴重的問題。
針對多線程編程中,程序運行不安全的問題,Java 提供了synchronized關(guān)鍵字來解決這個問題,當多個線程同時訪問共享資源時,會保證線程依次排隊操作共享變量,從而保證程序的實際運行結(jié)果與預(yù)期一致。
我們對上面示例中的DataEntity.addCount()方法進行改造,再看看效果如下。
public class DataEntity {
private int count = 0;
/**
* 在方法上加上 synchronized 關(guān)鍵字
*/
public synchronized void addCount(){
count++;
}
public int getCount(){
return count;
}
}
多次運行結(jié)果如下:
第一次運行:result: 10000000
第二次運行:result: 10000000
第三次運行:result: 10000000
...
運行結(jié)果與預(yù)期一致!
03、synchronized 使用詳解
synchronized作為 Java 中的關(guān)鍵字,在多線程編程中,有著非常重要的地位,也是新手了解并發(fā)編程的基礎(chǔ),從功能角度看,它有以下幾個比較重要的特性:
- 原子性:即一個或多個操作要么全部執(zhí)行成功,要么全部執(zhí)行失敗。synchronized關(guān)鍵字可以保證只有一個線程拿到鎖,訪問共享資源
- 可見性:即一個線程對共享變量進行修改后,其他線程可以立刻看到。執(zhí)行synchronized時,線程獲取鎖之后,一定從主內(nèi)存中讀取數(shù)據(jù),釋放鎖之前,一定會將數(shù)據(jù)寫回主內(nèi)存,從而保證內(nèi)存數(shù)據(jù)可見性
- 有序性:即保證程序的執(zhí)行順序會按照代碼的先后順序執(zhí)行。synchronized關(guān)鍵字,可以保證每個線程依次排隊操作共享變量
synchronized也被稱為同步鎖,它可以把任意一個非 NULL 的對象當成鎖,只有拿到鎖的線程能進入方法體,并且只有一個線程能進入,其他的線程必須等待鎖釋放了才能進入,它屬于獨占式的悲觀鎖,同時也屬于可重入鎖。
關(guān)于鎖的知識,我們后面在介紹,大家先了解一下就行。
從實際的使用角度來看,synchronized修飾的對象有以下幾種:
- 修飾一個方法:被修飾的方法稱為同步方法,其作用的范圍是整個方法,作用的對象是調(diào)用這個方法的對象
- 修飾一個靜態(tài)的方法:其作用的范圍是整個靜態(tài)方法,作用的對象是這個類的所有對象
- 修飾一個代碼塊:被修飾的代碼塊稱為同步語句塊,其作用的范圍是大括號{}括起來的代碼,作用的對象是調(diào)用這個代碼塊的對象,使用上比較靈活
下面我們一起來看看它們的具體用法。
3.1、修飾一個方法
當synchronized修飾一個方法時,多個線程訪問同一個對象,哪個線程持有該方法所屬對象的鎖,就擁有執(zhí)行權(quán)限,否則就只能等待。
如果多線程訪問的不是同一個對象,不會起到保證線程同步的作用。
示例如下:
public class DataEntity {
private int count;
/**
* 在方法上加上 synchronized 關(guān)鍵字
*/
public synchronized void addCount(){
for (int i = 0; i < 3; i++) {
try {
System.out.println(Thread.currentThread().getName() + ":" + (count++));
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
public int getCount() {
return count;
}
}
public class MyThreadA extends Thread {
private DataEntity entity;
public MyThreadA(DataEntity entity) {
this.entity = entity;
}
@Override
public void run() {
entity.addCount();
}
}
public class MyThreadB extends Thread {
private DataEntity entity;
public MyThreadB(DataEntity entity) {
this.entity = entity;
}
@Override
public void run() {
entity.addCount();
}
}
public class MyThreadTest {
public static void main(String[] args) {
// 初始化數(shù)據(jù)實體
DataEntity entity = new DataEntity();
MyThreadA threadA = new MyThreadA(entity);
threadA.start();
MyThreadB threadB = new MyThreadB(entity);
threadB.start();
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("result: " + entity.getCount());
}
}
運行結(jié)果如下:
Thread-0:0
Thread-0:1
Thread-0:2
Thread-1:3
Thread-1:4
Thread-1:5
result: 6
當兩個線程共同操作一個對象時,此時每個線程都會依次排隊執(zhí)行。
假如兩個線程操作的不是一個對象,此時沒有任何效果,示例如下:
public class MyThreadTest {
public static void main(String[] args) {
DataEntity entity1 = new DataEntity();
MyThreadA threadA = new MyThreadA(entity1);
threadA.start();
DataEntity entity2 = new DataEntity();
MyThreadA threadB = new MyThreadA(entity2);
threadB.start();
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("result: " + entity1.getCount());
System.out.println("result: " + entity2.getCount());
}
}
運行結(jié)果如下:
Thread-0:0
Thread-1:0
Thread-0:1
Thread-1:1
Thread-0:2
Thread-1:2
result: 3
result: 3
從結(jié)果上可以看出,當synchronized修飾一個方法,當多個線程訪問同一個對象的方法,每個線程會依次排隊;如果訪問的不是一個對象,線程不會進行排隊,像正常執(zhí)行一樣。
3.2、修飾一個靜態(tài)的方法
synchronized修改一個靜態(tài)的方法時,代表的是對當前.java文件對應(yīng)的 Class 類加鎖,不區(qū)分對象實例。
示例如下:
public class DataEntity {
private static int count;
/**
* 在靜態(tài)方法上加上 synchronized 關(guān)鍵字
*/
public synchronized static void addCount(){
for (int i = 0; i < 3; i++) {
try {
System.out.println(Thread.currentThread().getName() + ":" + (count++));
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
public static int getCount() {
return count;
}
}
public class MyThreadA extends Thread {
@Override
public void run() {
DataEntity.addCount();
}
}
public class MyThreadB extends Thread {
@Override
public void run() {
DataEntity.addCount();
}
}
public class MyThreadTest {
public static void main(String[] args) {
MyThreadA threadA = new MyThreadA();
threadA.start();
MyThreadB threadB = new MyThreadB();
threadB.start();
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("result: " + DataEntity.getCount());
}
}
運行結(jié)果如下:
Thread-0:0
Thread-0:1
Thread-0:2
Thread-1:3
Thread-1:4
Thread-1:5
result: 6
靜態(tài)同步方法和非靜態(tài)同步方法持有的是不同的鎖,前者是類鎖,后者是對象鎖,類鎖可以理解為這個類的所有對象。
3.3、修飾一個代碼塊
synchronized用于修飾一個代碼塊時,只會控制代碼塊內(nèi)的執(zhí)行順序,其他試圖訪問該對象的線程將被阻塞,編程比較靈活,在實際開發(fā)中用的應(yīng)用比較廣泛。
示例如下
public class DataEntity {
private int count;
/**
* 在方法上加上 synchronized 關(guān)鍵字
*/
public void addCount(){
synchronized (this){
for (int i = 0; i < 3; i++) {
try {
System.out.println(Thread.currentThread().getName() + ":" + (count++));
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
public int getCount() {
return count;
}
}
public class MyThreadTest {
public static void main(String[] args) {
// 初始化數(shù)據(jù)實體
DataEntity entity = new DataEntity();
MyThreadA threadA = new MyThreadA(entity);
threadA.start();
MyThreadB threadB = new MyThreadB(entity);
threadB.start();
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("result: " + entity.getCount());
}
}
運行結(jié)果如下:
Thread-0:0
Thread-0:1
Thread-0:2
Thread-1:3
Thread-1:4
Thread-1:5
result: 6
其中synchronized (this)中的this,表示的是當前類實例的對象,效果等同于public synchronized void addCount()。
除此之外,synchronized()還可以修飾任意實例對象,作用的范圍就是具體的實例對象。
比如,修飾個自定義的類實例對象,作用的范圍是擁有l(wèi)ock對象,其實也等價于synchronized (this)。
public class DataEntity {
private Object lock = new Object();
/**
* synchronized 可以修飾任意實例對象
*/
public void addCount(){
synchronized (lock){
// todo...
}
}
}
當然也可以用于修飾類,表示類鎖,效果等同于public synchronized static void addCount()。
public class DataEntity {
/**
* synchronized 可以修飾類,表示類鎖
*/
public void addCount(){
synchronized (DataEntity.class){
// todo...
}
}
}
synchronized修飾代碼塊,比較經(jīng)典的應(yīng)用案例,就是單例設(shè)計模式中的雙重校驗鎖實現(xiàn)。
public class Singleton {
private volatile static Singleton singleton;
private Singleton (){}
public static Singleton getSingleton() {
if (singleton == null) {
synchronized (Singleton.class) {
if (singleton == null) {
singleton = new Singleton();
}
}
}
return singleton;
}
}
采用代碼塊的實現(xiàn)方式,編程會更加靈活,可以顯著的提升并發(fā)查詢的效率。
04、synchronized 鎖重入介紹
synchronized關(guān)鍵字擁有鎖重入的功能,所謂鎖重入的意思就是:當一個線程得到一個對象鎖后,再次請求此對象鎖時可以再次得到該對象的鎖,而無需等待。
我們看個例子就能明白。
public class DataEntity {
private int count = 0;
public synchronized void addCount1(){
System.out.println(Thread.currentThread().getName() + ":" + (count++));
addCount2();
}
public synchronized void addCount2(){
System.out.println(Thread.currentThread().getName() + ":" + (count++));
addCount3();
}
public synchronized void addCount3(){
System.out.println(Thread.currentThread().getName() + ":" + (count++));
}
public int getCount() {
return count;
}
}
public class MyThreadA extends Thread {
private DataEntity entity;
public MyThreadA(DataEntity entity) {
this.entity = entity;
}
@Override
public void run() {
entity.addCount1();
}
}
public class MyThreadB extends Thread {
private DataEntity entity;
public MyThreadB(DataEntity entity) {
this.entity = entity;
}
@Override
public void run() {
entity.addCount1();
}
}
public class MyThreadTest {
public static void main(String[] args) {
// 初始化數(shù)據(jù)實體
DataEntity entity = new DataEntity();
MyThreadA threadA = new MyThreadA(entity);
threadA.start();
MyThreadB threadB = new MyThreadB(entity);
threadB.start();
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("result: " + entity.getCount());
}
}
運行結(jié)果如下:
Thread-0:0
Thread-0:1
Thread-0:2
Thread-1:3
Thread-1:4
Thread-1:5
result: 6
從結(jié)果上看線程沒有交替執(zhí)行,線程Thread-0獲取到鎖之后,再次調(diào)用其它帶有synchronized關(guān)鍵字的方法時,可以快速進入,而Thread-1線程需等待對象鎖完全釋放之后再獲取,這就是鎖重入。
04、小結(jié)
從上文中我們可以得知,在多線程環(huán)境下,恰當?shù)氖褂胹ynchronized關(guān)鍵字可以保證線程同步,使程序的運行結(jié)果與預(yù)期一致。
- 1.當synchronized修飾一個方法時,作用的范圍是整個方法,作用的對象是調(diào)用這個方法的對象;
- 2..當synchronized修飾一個靜態(tài)方法時,作用的范圍是整個靜態(tài)方法,作用的對象是這個類的所有對象;
- 3.當synchronized修飾一個代碼塊時,作用的范圍是代碼塊,作用的對象是修飾的內(nèi)容,如果是類,則這個類的所有對象都會受到控制;如果是任意對象實例子,則控制的是具體的對象實例,誰擁有這個對象鎖,就能進入方法體
synchronized是一種同步鎖,屬于獨占式,使用它進行線程同步,JVM 性能開銷很大,大量的使用未必會帶來好處。