synchronized關鍵字主要有以下這3種用法:
-
修飾實例方法,作用於當前實例加鎖,進入同步代碼前要獲得當前實例的鎖
-
修飾靜態方法,作用於當前類對象加鎖,進入同步代碼前要獲得當前類對象的鎖
-
修飾代碼塊,指定加鎖對象,對給定對象加鎖,進入同步代碼庫前要獲得給定對象的鎖。
1、synchronized作用於實例方法
所謂的實例對象鎖就是用synchronized修飾實例對象中的實例方法,注意是實例方法不包括靜態方法,如下:
public class AccountingSync implements Runnable{
//共享資源(臨界資源)
static int i=0;
/**
* synchronized 修飾實例方法
*/
public synchronized void increase(){
i++;
}
@Override
public void run() {
for(int j=0;j<1000000;j++){
increase();
}
}
public static void main(String[] args) throws InterruptedException {
AccountingSync instance=new AccountingSync();
Thread t1=new Thread(instance);
Thread t2=new Thread(instance);
t1.start();
t2.start();
t1.join();
t2.join();
System.out.println(i);
}
/**
* 輸出結果:
* 2000000
*/
}
上述代碼中,我們開啟兩個線程操作同一個共享資源即變量i,由於i++;操作並不具備原子性,該操作是先讀取值,然后寫回一個新值,相當於原來的值加上1,分兩步完成,如果第二個線程在第一個線程讀取舊值和寫回新值期間讀取i的域值,那么第二個線程就會與第一個線程一起看到同一個值,並執行相同值的加1操作,這也就造成了線程安全失敗,因此對於increase方法必須使用synchronized修飾,以便保證線程安全。此時我們應該注意到synchronized修飾的是實例方法increase,在這樣的情況下,當前線程的鎖便是實例對象instance,注意Java中的線程同步鎖可以是任意對象。從代碼執行結果來看確實是正確的,倘若我們沒有使用synchronized關鍵字,其最終輸出結果就很可能小於2000000,這便是synchronized關鍵字的作用。這里我們還需要意識到,當一個線程正在訪問一個對象的 synchronized 實例方法,那么其他線程不能訪問該對象的其他 synchronized 方法,畢竟一個對象只有一把鎖,當一個線程獲取了該對象的鎖之后,其他線程無法獲取該對象的鎖,所以無法訪問該對象的其他synchronized實例方法,但是其他線程還是可以訪問該實例對象的其他非synchronized方法,當然如果是一個線程 A 需要訪問實例對象 obj1 的 synchronized 方法 f1(當前對象鎖是obj1),另一個線程 B 需要訪問實例對象 obj2 的 synchronized 方法 f2(當前對象鎖是obj2),這樣是允許的,因為兩個實例對象鎖並不同相同,此時如果兩個線程操作數據並非共享的,線程安全是有保障的,遺憾的是如果兩個線程操作的是共享數據,那么線程安全就有可能無法保證了,如下代碼將演示出該現象:
public class AccountingSyncBad implements Runnable{
static int i=0;
public synchronized void increase(){
i++;
}
@Override
public void run() {
for(int j=0;j<1000000;j++){
increase();
}
}
public static void main(String[] args) throws InterruptedException {
//new新實例
Thread t1=new Thread(new AccountingSyncBad());
//new新實例
Thread t2=new Thread(new AccountingSyncBad());
t1.start();
t2.start();
//join含義:當前線程A等待thread線程終止之后才能從thread.join()返回
t1.join();
t2.join();
System.out.println(i);
}
}
上述代碼與前面不同的是我們同時創建了兩個新實例AccountingSyncBad,然后啟動兩個不同的線程對共享變量i進行操作,但很遺憾操作結果是1452317而不是期望結果2000000,因為上述代碼犯了嚴重的錯誤,雖然我們使用synchronized修飾了increase方法,但卻new了兩個不同的實例對象,這也就意味着存在着兩個不同的實例對象鎖,因此t1和t2都會進入各自的對象鎖,也就是說t1和t2線程使用的是不同的鎖,因此線程安全是無法保證的。解決這種困境的的方式是將synchronized作用於靜態的increase方法,這樣的話,對象鎖就當前類對象,由於無論創建多少個實例對象,但對於的類對象擁有只有一個,所有在這樣的情況下對象鎖就是唯一的。下面我們看看如何使用將synchronized作用於靜態的increase方法。
2、synchronized作用於靜態方法
當synchronized作用於靜態方法時,其鎖就是當前類的class對象鎖。由於靜態成員不專屬於任何一個實例對象,是類成員,因此通過class對象鎖可以控制靜態 成員的並發操作。需要注意的是如果一個線程A調用一個實例對象的非static synchronized方法,而線程B需要調用這個實例對象所屬類的靜態 synchronized方法,是允許的,不會發生互斥現象,因為訪問靜態 synchronized 方法占用的鎖是當前類的class對象,而訪問非靜態 synchronized 方法占用的鎖是當前實例對象鎖,看如下代碼:
public class AccountingSyncClass implements Runnable{
static int i=0;
/**
* 作用於靜態方法,鎖是當前class對象,也就是
* AccountingSyncClass類對應的class對象
*/
public static synchronized void increase(){
i++;
}
/**
* 非靜態,訪問時鎖不一樣不會發生互斥
*/
public synchronized void increase4Obj(){
i++;
}
@Override
public void run() {
for(int j=0;j<1000000;j++){
increase();
}
}
public static void main(String[] args) throws InterruptedException {
//new新實例
Thread t1=new Thread(new AccountingSyncClass());
//new心事了
Thread t2=new Thread(new AccountingSyncClass());
//啟動線程
t1.start();t2.start();
t1.join();t2.join();
System.out.println(i);
}
}
由於synchronized關鍵字修飾的是靜態increase方法,與修飾實例方法不同的是,其鎖對象是當前類的class對象。注意代碼中的increase4Obj方法是實例方法,其對象鎖是當前實例對象,如果別的線程調用該方法,將不會產生互斥現象,畢竟鎖對象不同,但我們應該意識到這種情況下可能會發現線程安全問題(操作了共享靜態變量i)。
3、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++;操作。當然除了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++;
}
}
好了,到這里synchronized的基本含義及其使用方法就講解完了,希望對大家有幫助~
