文章目錄
java synchronized 關鍵字 使用方法 學習筆記
1. 綜述
線程安全是併發編程中的重要關注點,應該注意到的是,造成線程安全問題的主要誘因有兩點:
- 存在共享數據(也稱臨界資源)
- 存在多條線程共同操作共享數據
因此爲了解決這個問題,我們可能需要這樣一個方案,當存在多個線程操作共享數據時,需要保證同一時刻有且只有一個線程在操作共享數據,其他線程必須等到該線程處理完數據後再進行,這種方式有個高尚的名稱叫互斥鎖,即能達到互斥訪問目的的鎖,也就是說當一個共享數據被當前正在訪問的線程加上互斥鎖後,在同一個時刻,其他線程只能處於等待的狀態,直到當前線程處理完畢釋放該鎖。
在 Java 中,關鍵字 synchronized 可以保證在同一個時刻,只有一個線程可以執行某個方法或者某個代碼塊(主要是對方法或者代碼塊中存在共享數據的操作),同時我們還應該注意到synchronized另外一個重要的作用,synchronized可保證一個線程的變化(主要是共享數據的變化)被其他線程所看到(保證可見性,完全可以替代Volatile功能),這點確實也是很重要的。
它修飾的對象有以下幾種:
- 修飾一個方法,被修飾的方法稱爲同步方法,其作用的範圍是整個方法,作用的對象是調用這個方法的對象;
- 修飾一個靜態的方法,其作用的範圍是整個靜態方法,作用的對象是這個類的所有對象;
- 修飾一個代碼塊,被修飾的代碼塊稱爲同步語句塊,其作用的範圍是大括號{}括起來的代碼,作用的對象是調用這個代碼塊的對象;
- 修飾一個類,其作用的範圍是synchronized後面括號括起來的部分,作用主的對象是這個類的所有對象。
2. synchronized作用於實例方法
所謂的實例對象鎖就是用synchronized修飾實例對象中的實例方法,注意是實例方法不包括靜態方法
package sychronized;
import static net.mindview.util.Print.*;
import java.util.concurrent.*;
public class AccountingSync2 implements Runnable {
//共享資源(臨界資源)
static int i = 0;
/**
* synchronized 修飾實例方法
*/
synchronized void getI() {
if (i % 1000000 == 0) {
print(i);
}
}
public synchronized void increase() {
i++;
getI();
}
@Override
public void run() {
for (int j = 0; j < 1000000; j++) {
increase();
}
print(i);
}
public static void main(String[] args) throws InterruptedException {
ExecutorService exec = Executors.newCachedThreadPool();
AccountingSync2 accountingSync2 = new AccountingSync2();
exec.execute(accountingSync2);
exec.execute(accountingSync2);
exec.shutdown();
}
}
最後的結果爲:
1000000
1519541
2000000
2000000
- 上述代碼中,我們開啓兩個線程操作同一個共享資源即變量i,由於i++操作並不具備原子性,該操作是先讀取值,然後寫回一個新值,相當於原來的值加上1,分兩步完成,如果第二個線程在第一個線程讀取舊值和寫回新值期間讀取i的域值,那麼第二個線程就會與第一個線程一起看到同一個值,並執行相同值的加1操作,這也就造成了線程安全失敗,因此對於increase方法必須使用synchronized修飾,以便保證線程安全。
- 此時注意到synchronized修飾的是實例方法increase,在這樣的情況下,當前線程的鎖便是實例對象instance,注意Java中的線程同步鎖可以是任意對象。
- 當一個線程正在訪問一個對象的 synchronized 實例方法,那麼其他線程不能訪問該對象的其他 synchronized 方法,畢竟一個對象只有一把鎖,當一個線程獲取了該對象的鎖之後,其他線程無法獲取該對象的鎖,所以無法訪問該對象的其他synchronized實例方法,但是其他線程還是可以訪問該實例對象的其他非synchronized方法,但是一個 synchronized 方法可以調用另一個需要獲得同樣鎖的synchronized方法,因爲已經獲取了鎖。
- 如果是一個線程 A 需要訪問實例對象 obj1 的 synchronized 方法 f1(當前對象鎖是obj1),另一個線程 B 需要訪問實例對象 obj2 的 synchronized 方法f2(當前對象鎖是obj2),這樣是允許的,因爲兩個實例對象鎖並不同相同,此時如果兩個線程操作數據並非共享的,線程安全是有保障的,遺憾的是如果兩個線程操作的是共享數據,那麼線程安全就有可能無法保證了。
package sychronized;
import static net.mindview.util.Print.*;
import java.util.concurrent.*;
public class AccountingSync2 implements Runnable {
//共享資源(臨界資源)
static int i = 0;
/**
* synchronized 修飾實例方法
*/
synchronized void getI() {
if (i % 1000000 == 0) {
print(i);
}
}
public synchronized void increase() {
i++;
getI();
}
@Override
public void run() {
for (int j = 0; j < 1000000; j++) {
increase();
}
print(i);
}
public static void main(String[] args) throws InterruptedException {
ExecutorService exec = Executors.newCachedThreadPool();
AccountingSync2 accountingSync2 = new AccountingSync2();
exec.execute(accountingSync2);
exec.execute(new AccountingSync2());
exec.shutdown();
}
}
#輸出結果:
1000000
1249050
1329218
- 上述代碼與前面不同的是我們同時創建了兩個新實例AccountingSync2,然後啓動兩個不同的線程對共享變量i進行操作,但很遺憾操作結果是1329218而不是期望結果2000000。
- 雖然我們使用synchronized修飾了increase方法,但卻new了兩個不同的實例對象,這也就意味着存在着兩個不同的實例對象鎖,因此兩個進程都會進入各自持有對象的對象鎖,也就是說兩個線程使用的是不同的鎖,因此線程安全是無法保證的。
- 解決這種困境的的方式是將synchronized作用於靜態的increase方法,這樣的話,對象鎖就當前類對象,由於無論創建多少個實例對象,但對於的類對象擁有隻有一個,所有在這樣的情況下對象鎖就是唯一的。下面我們看看如何使用將synchronized作用於靜態的increase方法。
3. synchronized作用於靜態方法
當synchronized作用於靜態方法時,其鎖就是當前類的class對象鎖。由於靜態成員不專屬於任何一個實例對象,是類成員,因此通過class對象鎖可以控制靜態成員的併發操作。需要注意的是如果一個線程A調用一個實例對象的非static synchronized 方法,而線程B需要調用這個實例對象所屬類的靜態 synchronized方法,是允許的,不會發生互斥現象,因爲訪問靜態 synchronized 方法佔用的鎖是當前類的 class 對象,而訪問非靜態 synchronized 方法佔用的鎖是當前實例對象鎖。
package sychronized;
import static net.mindview.util.Print.*;
import java.util.concurrent.*;
class OtherTask implements Runnable{
AccountingSyncClass accounting = new AccountingSyncClass();
@Override
public void run(){
for (int j = 0; j < 1000000; j++) {
accounting.increaseForObject();
}
print(accounting.getI());
}
}
public class AccountingSyncClass implements Runnable {
//共享資源(臨界資源)
private static int i = 0;
/**
* synchronized 修飾實例方法
*/
public synchronized void increaseForObject() {
i++;
}
public synchronized static void increase() {
i++;
}
@Override
public void run() {
for (int j = 0; j < 1000000; j++) {
increase();
}
print(i);
}
public int getI(){
return i;
}
public static void main(String[] args) throws InterruptedException {
ExecutorService exec = Executors.newCachedThreadPool();
exec.execute(new AccountingSyncClass());
exec.execute(new AccountingSyncClass());
exec.execute(new OtherTask()); // 1
exec.shutdown();
}
}
輸出結果爲:
1459696
2692181
2754098
註釋掉代碼中的 1 那一行代碼的輸出結果爲:
1468495
2000000
- 由於synchronized關鍵字修飾的是靜態increase方法,與修飾實例方法不同的是,其鎖對象是當前類的class對象。
- 注意代碼中的increase4Obj方法是實例方法,其對象鎖是當前實例對象,如果別的線程調用該方法,將不會產生互斥現象,畢竟鎖對象不同,但我們應該意識到這種情況下可能會發現線程安全問題(操作了共享靜態變量i)。
- 因此在設計同步代碼的時候一定要仔細思考到底該用 多大的同步粒度 和 該對什麼對象 使用同步操作。
4. synchronized同步代碼塊
除了使用關鍵字修飾實例方法和靜態方法外,還可以使用同步代碼塊,**在某些情況下,我們編寫的方法體可能比較大,同時存在一些比較耗時的操作,而需要同步的代碼又只有一小部分,如果直接對整個方法進行同步操作,可能會得不償失,此時我們可以使用同步代碼塊的方式對需要同步的代碼進行包裹,**這樣就無需對整個方法進行同步操作了。
public class AccountingSync implements Runnable{
static AccountingSync instance=new AccountingSync();
static int i=0;
@Override
public void run() {
//省略其他耗時操作....
//使用同步代碼塊對變量i進行同步操作,鎖對象爲instance
synchronized(instance){
for(int j=0;j<1000000;j++){
i++;
}
}
}
public static void main(String[] args) throws InterruptedException {
Thread t1=new Thread(instance);
Thread t2=new Thread(instance);
t1.start();t2.start();
t1.join();t2.join();
System.out.println(i);
}
}
- 從代碼看出,將synchronized作用於一個給定的實例對象instance,即當前實例對象就是鎖對象,每次當線程進入synchronized包裹的代碼塊時就會要求當前線程持有instance實例對象鎖。
- 如果當前有其他線程正持有該對象鎖,那麼新到的線程就必須等待,這樣也就保證了每次只有一個線程執行i++操作。
5. synchronized作用於類
當然除了instance作爲對象外,我們還可以使用this對象(代表當前實例)或者當前類的class對象作爲鎖。
//this,當前實例對象鎖
synchronized(this){
for(int j=0;j<1000000;j++){
i++;
}
}
//class對象鎖
synchronized(AccountingSync.class){
for(int j=0;j<1000000;j++){
i++;
}
}
同步方法最好運用在 共享資源 內部而不是使用它的外部
package sychronized;
import static net.mindview.util.Print.*;
import java.util.concurrent.*;
class OtherTask implements Runnable{
AccountingSyncClass accounting = new AccountingSyncClass();
@Override
public void run(){
for (int j = 0; j < 1000000; j++) {
accounting.increaseForObject();
}
print(accounting.getAdapterInteger());
}
}
class AdapterInteger {
private int i = 0;
public synchronized void increase(){
++i;
}
public synchronized int getI(){
return i;
}
}
public class AccountingSyncClass implements Runnable {
//共享資源(臨界資源)
private static AdapterInteger adapterInteger = new AdapterInteger();
/**
* synchronized 修飾實例方法
*/
public synchronized void increaseForObject() {
adapterInteger.increase();
}
public synchronized static void increase() {
adapterInteger.increase();
}
@Override
public void run() {
for (int j = 0; j < 1000000; j++) {
increase();
}
print(getAdapterInteger());
}
public static int getAdapterInteger() {
return adapterInteger.getI();
}
public static void main(String[] args) throws InterruptedException {
ExecutorService exec = Executors.newCachedThreadPool();
exec.execute(new AccountingSyncClass());
exec.execute(new AccountingSyncClass());
exec.execute(new OtherTask());
exec.shutdown();
}
}
#輸出結果爲
1183139
2688189
3000000
這樣三個線程中的任務的鎖都是我們的共享變量 adapterInteger 對象的鎖,這樣就可以完成真正的同步,不管哪個線程都是獲得了 adapterInteger 對象的鎖才能運行相應的代碼。
總結
- 無論synchronized關鍵字加在方法上還是對象上,如果它作用的對象是非靜態的,則它取得的鎖是對象;如果synchronized作用的對象是一個靜態方法或一個類,則它取得的鎖是對類,該類所有的對象同一把鎖。
- 每個對象只有一個鎖(lock)與之相關聯,誰拿到這個鎖誰就可以運行它所控制的那段代碼。
- 實現同步是要很大的系統開銷作爲代價的,甚至可能造成死鎖,所以儘量避免無謂的同步控制。
Ref
- https://blog.csdn.net/zhangqilugrubby/article/details/80500505
- https://segmentfault.com/a/1190000012203437#articleHeader2
- https://www.jianshu.com/p/ffb8e441b55b