首頁  >  文章  >  Java  >  深入理解java中的synchronized關鍵字

深入理解java中的synchronized關鍵字

高洛峰
高洛峰原創
2016-12-13 11:15:321079瀏覽

synchronized 關鍵字,代表這個方法加鎖,相當於不管哪一個線程A每次運行到這個方法時,都要檢查有沒有其它正在用這個方法的線程B(或者C D等),有的話要等正在使用這個方法的線程B(或C D)運行完這個方法後再運行此線程A,沒有的話,直接運行它包括兩種用法:synchronized 方法和synchronized 塊。

1. synchronized 方法:
透過在方法宣告中加入 synchronized關鍵字來宣告 synchronized 方法。如:

public synchronized void accessVal(int newVal); 

synchronized 方法控制對類別成員變數的存取:每個類別實例對應一把鎖,每個synchronized 方法都必須獲得呼叫該方法的類別實例的鎖定方能執行,否則所屬執行緒阻塞,方法一旦執行,就獨佔該鎖,直到從該方法返回時才將鎖釋放,此後被阻塞的線程方能獲得該鎖,重新進入可執行狀態。這種機制確保了同一時刻對於每一個類別實例,其所有宣告為synchronized 的成員函數中至多只有一個處於可執行狀態(因為至多只有一個能夠獲得該類別實例對應的鎖),從而有效避免了類別成員變數的存取衝突(只要所有可能存取類別成員變數的方法均被宣告為synchronized)。  在 Java 中,不光是類別實例,每一個類別也對應一把鎖,這樣我們也可將類別的靜態成員函數宣告為 synchronized ,以控制其對類別的靜態成員變數的存取。 synchronized 方法的缺陷:若將一個大的方法宣告為synchronized 將會大大影響效率,典型地,若將執行緒類別的方法run()宣告為synchronized ,由於在執行緒的整個生命期內它一直在運行,因此將導致它對本類任何synchronized 方法的呼叫都永遠不會成功。當然我們可以透過將存取類別成員變數的程式碼放到專門的方法中,將其宣告為synchronized ,並在主方法中呼叫來解決這一問題,但是Java 為我們提供了更好的解決辦法,那就是synchronized 塊。

2. synchronized 區塊:
透過 synchronized關鍵字來聲明synchronized 區塊。語法如下: 

synchronized(syncObject)
{  
//允许访问控制的代码  
} 

synchronized 區塊是這樣一個程式碼區塊,其中的程式碼必須獲得物件 syncObject (如前所述,可以是類別實例或類別)的鎖方能執行,具體機制同前所述。由於可以針對任意程式碼區塊,且可任意指定上鎖的對象,故靈活性較高。

對synchronized (this)的一些理解
一、當兩個並發線程訪問同一個物件object中的這個synchronized(this)同步程式碼區塊時,一個時間內只能有一個執行緒來執行。另一個執行緒必須等待目前執行緒執行完這個程式碼區塊以後才能執行該程式碼區塊。  

二、當一個執行緒存取object的一個synchronized(this)同步程式碼區塊時,其他執行緒對object中所有其它synchronized(this)同步程式碼區塊的存取將被阻塞。  

三、然而,當一個執行緒存取object的一個synchronized(this)同步程式碼區塊時,另一個執行緒仍然可以存取該object中的除synchronized(this)同步程式碼區塊以外的部分。 

四、第三個例子同樣適用其它同步程式碼區塊。也就是說,當一個執行緒存取object的一個synchronized(this)同步程式碼區塊時,它就獲得了這個object的物件鎖定。結果,其它執行緒對該object物件所有同步程式碼部分的存取都被暫時阻塞。  

五、以上規則對其它物件鎖同樣適用。

synchronized的一個簡單例子

public class TextThread
{
/**
* @param args
*/
public static void main(String[] args)
{
// TODO 自动生成方法存根
        TxtThread tt = new TxtThread();
        new Thread(tt).start();
        new Thread(tt).start();
        new Thread(tt).start();
        new Thread(tt).start();
}
}
class TxtThread implements Runnable
{
int num = 100;
String str = new String();
public void run()
{
while (true)
{
   synchronized(str)
   {
   if (num>0)
   {
    try
    {
     Thread.sleep(10);
    }
    catch(Exception e)
    {
     e.getMessage();
    }
    System.out.println(Thread.currentThread().getName()+ "this is "+ num--);
   }
   }
}
}
}

上面的例子中為了製造一個時間差,也就是出錯的機會,使用了Thread.sleep(10)Java對多線程的支持與同步機制深受大家的喜愛,似乎看起來使用了synchronized關鍵字就可以輕鬆解決多執行緒共享資料同步問題。到底如何? ――還得對synchronized關鍵字的作用進行深入了解才可定論。

總的說來,synchronized關鍵字可以作為函數的修飾符,也可作為函數內的語句,也就是平常說的同步方法和同步語句塊。如果再細的分類,synchronized可作用於instance變數、object reference(物件參考)、static函數和class literals(類別名稱字面常數)身上。在進一步闡述之前,我們需要先明確幾點:

A.無論synchronized關鍵字加在方法上或對像上,它所取得的鎖都是對象,而不是把一段程式碼或函式當作鎖――而且同步方法很可能還會被其他執行緒的對象存取。

B.每個物件只有一個鎖(lock)與之相關聯。

C.實現同步是要很大的系統開銷作為代價的,甚至可能造成死鎖,所以盡量避免無謂的同步控制。

接著來討論synchronized用到不同地方對代碼產生的影響: 假設P1、P2是同一類的不同對象,這個類中定義了以下幾種情況的​​同步塊或同步方法,P1、P2就都可以調用它們。

1.把synchronized當作函數修飾符時,範例程式碼如下:

Public synchronized void methodAAA()
{
//…
}

这也就是同步方法,那这时synchronized锁定的是哪个对象呢?它锁定的是调用这个同步方法对象。也就是说,当一个对象P1在不同的线程中执行这个同步方法时,它们之间会形成互斥,达到同步的效果。但是这个对象所属的Class所产生的另一对象P2却可以任意调用这个被加了synchronized关键字的方法。上边的示例代码等同于如下代码:

public void methodAAA()
{
synchronized (this)      // (1)
{
       //…..
}
}

(1)处的this指的是什么呢?它指的就是调用这个方法的对象,如P1。可见同步方法实质是将synchronized作用于object reference。――那个拿到了P1对象锁的线程,才可以调用P1的同步方法,而对P2而言,P1这个锁与它毫不相干,程序也可能在这种情形下摆脱同步机制的控制,造成数据混乱。


2.同步块,示例代码如下:

public void method3(SomeObject so)
{
    synchronized(so)
    {
       //…..
    }
}

这时,锁就是so这个对象,谁拿到这个锁谁就可以运行它所控制的那段代码。当有一个明确的对象作为锁时,就可以这样写程序,但当没有明确的对象作为锁,只是想让一段代码同步时,可以创建一个特殊的instance变量(它得是一个对象)来充当锁:

class Foo implements Runnable
{
        private byte[] lock = new byte[0]; // 特殊的instance变量
        Public void methodA()
        {
           synchronized(lock) { //… }
        }
        //…..
}

注:零长度的byte数组对象创建起来将比任何对象都经济――查看编译后的字节码:生成零长度的byte[]对象只需3条操作码,而Object lock = new Object()则需要7行操作码。

3.将synchronized作用于static 函数,示例代码如下:

Class Foo
{
    public synchronized static void methodAAA()   // 同步的static 函数
    {
        //….
    }
    public void methodBBB()
    {
       synchronized(Foo.class)   // class literal(类名称字面常量)
    }
}

代码中的methodBBB()方法是把class literal作为锁的情况,它和同步的static函数产生的效果是一样的,取得的锁很特别,是当前调用这个方法的对象所属的类(Class,而不再是由这个Class产生的某个具体对象了)。

记得在《Effective Java》一书中看到过将 Foo.class和 P1.getClass()用于作同步锁还不一样,不能用P1.getClass()来达到锁这个Class的目的。P1指的是由Foo类产生的对象。可以推断:如果一个类中定义了一个synchronized的static函数A,也定义了一个synchronized 的instance函数B,那么这个类的同一对象Obj在多线程中分别访问A和B两个方法时,不会构成同步,因为它们的锁都不一样。A方法的锁是Obj这个对象,而B的锁是Obj所属的那个Class。

小结如下:

搞清楚synchronized锁定的是哪个对象,就能帮助我们设计更安全的多线程程序。 还有一些技巧可以让我们对共享资源的同步访问更加安全:1.定义private 的instance变量+它的 get方法,而不要定义public/protected的instance变量。如果将变量定义为public,对象在外界可以绕过同步方法的控制而直接取得它,并改动它。这也是JavaBean的标准实现方式之一。2.如果instance变量是一个对象,如数组或ArrayList什么的,那上述方法仍然不安全,因为当外界对象通过get方法拿到这个instance对象的引用后,又将其指向另一个对象,那么这个private变量也就变了,岂不是很危险。这个时候就需要将get方法也加上synchronized同步,并且,只返回这个private对象的clone()――这样,调用端得到的就是对象副本的引用了  还有,比较常用的就有:Collections.synchronizedMap(new HashMap()),当然这个MAP就是生命在类中的全局变量,就是一个线程安全的HashMap,web的application是全web容器公用的,所以要使用线程安全来保证数据的正确。


陳述:
本文內容由網友自願投稿,版權歸原作者所有。本站不承擔相應的法律責任。如發現涉嫌抄襲或侵權的內容,請聯絡admin@php.cn