Java中多線程synchronized的用法


  在Java中synchronized可用來給對象和方法或者代碼塊加鎖,當它鎖定一個方法或者一個代碼塊的時候,同一時刻最多只有一個線程執行這段代碼。

  而synchronized底層是通過使用對象的監視器鎖(monitor)來確保同一時刻只有一個線程執行被修飾的方法或者代碼塊。

對於同步控制,我們需要明確幾點:

  A.無論synchronized關鍵字加在方法上還是對象上,它取得的鎖都是對象,而不是把一段代碼或函數當作鎖――而且同步方法很可能還會被其他線程的對象訪問。

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

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

Synchronized修飾的對象:

  1.修飾一個代碼塊,被修飾的代碼塊稱為同步語句塊,其作用的范圍是大括號{}括起來的代碼,作用的對象是調用這個代碼塊的對象(對象鎖)

  2.修飾一個方法,被修飾的方法稱為同步方法,其作用的范圍是整個方法,作用的對象是調用這個方法的對象(對象鎖)

  3.修改一個靜態的方法,其作用的范圍是整個靜態方法,作用的對象是這個類的所有對象(類鎖)

  4.修改一個類,其作用的范圍是synchronized后面括號括起來的部分,作用主的對象是這個類的所有對象(類鎖)

  從上面的鎖對象可以看出,Synchronized修飾的鎖對象其實就2種:對象鎖,類鎖。

解釋:

  (1)對象鎖:Synchronized修飾非靜態方法代碼塊(引入的是對象是:非靜態變量、非類);

非靜態方法:

public synchronized void method(){// todo}

非靜態變量:

private Object o = new Object();
public void method(){
   //o 鎖定的對象
   synchronized(o){
    // todo
   }
}

非類(這個可以與類對比,就可以明白):

public void method(){
 //方法里的this 指的是當前的方法,方法鎖
 synchronized(this){
  // todo
 }
}

測試:

  1)當多個線程使用同一個對象sync1,即:

SyncThread sync1 = new SyncThread();
Thread thread1 = new Thread(sync1, "thread1");
Thread thread2 = new Thread(sync1, "thread2");
thread1.start();
thread2.start();

  因為線程thread1thread2引入同一個對象sync1,屬於同一個鎖對象,所以線程執行是按照先執行thread1后釋放鎖,thread2才能獲取鎖並執行其方法。

 2)當多個線程使用不同對象sync1與sync2,即:

SyncThread sync1 = new SyncThread();
SyncThread sync2 = new SyncThread();
Thread thread1 = new Thread(sync1, "thread1");
Thread thread2 = new Thread(sync2, "thread2");
thread1.start();
thread2.start();

  因為線程thread1thread2引入不同對象sync1與sync2,屬於不同鎖對象,所以線程thread1thread2並行執行方法。

 2)類鎖:Synchronized修飾靜態方法代碼塊(引入的是對象是:靜態變量、類);

靜態方法:

public synchronized static void method(){// todo}

靜態變量:

private static Object o = new Object();
public void method(){
 //o 鎖定的對象
 synchronized(o){
  // todo
 }
}

類(與非類對比,就可以明白):

class SyncThread implements Runnable{
  public void method(){
    synchronized(SyncThread.class){
      // todo 同步代碼塊
    }
  }
  public void run(){
      method();
  }
}

測試:

  1)當多個線程使用同一個對象,還是不同對象,即:

Thread thread1 = new Thread(對象1, "thread1");
Thread thread2 = new Thread(對象2, "thread2");
thread1.start();
thread2.start();

  線程thread1thread2肯定是按照先后順序執行,即:先執行thread1后,才能執行thread2

總結:

  (1)如果synchronized引入的鎖對象是類鎖(該類所有的對象同一把鎖),那么多個線程訪問該鎖鎖定的功能時,肯定是有先后順序的,而不能同時執行。

  (2)如果synchronized引入的鎖對象是方法鎖,那么多個線程訪問該鎖鎖定的功能時,要區分是是不是同一個鎖對象,是同一個鎖對象,功能與類鎖類似;否則,多個線程可以同時執行該鎖鎖定的功能。

具體功能詳解與代碼示例:

1.1修飾代碼塊

1synchronized修飾代碼塊

  說明:一個線程訪問一個對象中的synchronized(this)同步代碼塊時,其他試圖訪問該對象的線程將被阻塞

例子:

public class SyncThread implements Runnable {
  private static int count;
  public SyncThread(){
    count = 0;
  }
  @Override
  public void run() {
    synchronized (this) {  //鎖對象是該方法,即方法鎖
      for (int i = 0; i < 5; i++) {
        try {
           System.out.println("當前線程名稱" + Thread.currentThread().getName() + ":" + (count++));
           Thread.sleep(100);
        } catch (Exception e) {
            e.printStackTrace();
        }
      }
    }
   }
  public static int getCount() {
    return count;
  }
}

測試代碼:

public class SyncThreadTest {
  public static void main(String[] args) {
    SyncThread sync = new SyncThread();
    Thread thread1 = new Thread(sync, "thread1");
    Thread thread2 = new Thread(sync, "thread2");
    thread1.start();
    thread2.start();
   }
}

測試結果:

當前線程名稱thread1:0
當前線程名稱thread1:1
當前線程名稱thread1:2
當前線程名稱thread1:3
當前線程名稱thread1:4
當前線程名稱thread2:5
當前線程名稱thread2:6
當前線程名稱thread2:7
當前線程名稱thread2:8
當前線程名稱thread2:9

結果分析:

  當並發的2個線程(thread1thread2)訪問同一個對象(sync)中的synchronized代碼塊時,同一個時刻只能被一個線程訪問;而另一個線程只能被堵塞,且必須要等待當前線程訪問完,並獲取已釋放對象鎖,才能訪問該同步的代碼塊。線程Thread1thread2互斥的,因為在執行synchronized代碼塊時會鎖定當前的對象(同一個對象sync,只有執行完該代碼塊才能釋放該對象鎖,下一個線程才能執行並鎖定該對象。

那怎么才能引入不同的對象?

修改一下測試代碼:

public class SyncThreadTest {
   public static void main(String[] args) {
     SyncThread sync1 = new SyncThread();
     SyncThread sync2 = new SyncThread();
     Thread thread1 = new Thread(sync1, "thread1");
     Thread thread2 = new Thread(sync2, "thread2");
     thread1.start();
     thread2.start(); 
   }
}

測試結果:

當前線程名稱thread1:0
當前線程名稱thread2:1
當前線程名稱thread2:3
當前線程名稱thread1:2
當前線程名稱thread1:4
當前線程名稱thread2:5
當前線程名稱thread2:6
當前線程名稱thread1:7
當前線程名稱thread2:8
當前線程名稱thread1:9

結果分析:

  不是說一個線程執行synchronized代碼塊時其它的線程受阻塞嗎?為什么上面的例子中thread1thread2同時在執行

  原因:這時創建了2SyncThread對象分別為sync1sync2,線程thread1執行的是對象sync1中的run;而線程thread2執行的是對象sync2中的run。由於2個不同的對象sync1sync2synchronized會為它們分別分配2個對象鎖,而這2把鎖是互不干擾,不形成互斥關系,所以會並行執行。

 2、代碼塊中引入不同的對象,即指定對象上加鎖:(成員變量,類變量-靜態變量)

public class SynchronizedObjectDemo {
    //靜態的全局變量,類變量(可以是自定義對象)
    private static Object staticObject = new Object();
    //非靜態變量,成員變量(可以是自定義對象)
    private Object o = new Object();
    /**
     * 功能描述: <br>
     * 類變量的方法上鎖
     */
    public void printStaticObject(){
        synchronized (staticObject) {  //staticObject是靜態變量,即類變量上鎖
            try {
                //調用線程休眠5秒,鎖競爭效果更加明顯
                Thread.sleep(5000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }finally{
                //輸出表示被調用
                System.out.println(System.currentTimeMillis()+" printByStaticObj is called");
            }
        }
    }
    /**
     * 功能描述: <br>
     * 在成員變量上鎖的方法
     */
    public void printObj(){
        synchronized (o) { //o是成員變量
            try {
                //調用線程休眠5秒,鎖競爭效果更加明顯
                Thread.sleep(5000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }finally{
                //輸出表示被調用
                System.out.println(System.currentTimeMillis()+" printByObj is called");
            }
        }
    }
    
    public static void main(String[] args) {
        //定義一個線程組
        ThreadGroup tgroups = new ThreadGroup(Thread.currentThread().getThreadGroup(), "SynchronizedObjectDemo");
        //循環,每次啟動一個線程
        for (int i = 0; i < 5 ; i++) {
            //每一個線程,重新構造對象
            final SynchronizedObjectDemo syncObj = new SynchronizedObjectDemo();
            //構造新線程的時候把它加入到定義的線程組
            Thread obj = new Thread(tgroups, new Runnable() {
                @Override
                public void run() {
                    syncObj.printObj();
                }
            });
            obj.start();
        }
        while (true) {//直到定義的線程組中的線程都終止才執行后續步驟
            if(tgroups.activeCount() == 0){
                for (int i = 0; i < 5; i++) {
                    final SynchronizedObjectDemo syncObj = new SynchronizedObjectDemo();
                    //構造新線程的時候把它加入到定義的線程組
                    Thread obj = new Thread(tgroups, new Runnable() {
                        @Override
                        public void run() {
                            syncObj.printStaticObject();
                        }
                    });
                    obj.start();
                }
                break;
            }
        }
    }
}

分析:

  從這個輸出結果可以看出,其實對於一個類變量加鎖就類似靜態方法加鎖,需要注意的一點是靜態方法鎖定的是Class對象,而由於類變量全局只有一個,所以行為是類似靜態方法,但是鎖定的不是同一個對象

  同樣,而對於一個成員變量加鎖就類似給非靜態方法加鎖。

  注意如果把第一次for循環中的SynchronizedObjectDemo對象的構造放到第一次for循環之前的話,輸出的結果就會類似第二次for循環了,因為此時鎖的是同一個對象。

 除此之外還可以有其他方式指定鎖對象:

方式一(作為方法參數):

public void method3(SomeObject obj){
 //obj 鎖定的對象
 synchronized(obj){
   // todo
 }
}

方式二當沒有明確的對象作為鎖,只是想讓一段代碼同步時,可以創建一個特殊的對象來充當鎖,如上面的Object對象,但是並不是最經濟的方式):

class Test implements Runnable{
   private byte[] lock = new byte[0]; // 特殊的instance變量 public void method(){
     synchronized(lock) {
       // todo 同步代碼塊
     }
   }
   public void run() {
   }
}

  說明:零長度的byte數組對象創建起來將比任何對象都經濟――查看編譯后的字節碼:生成零長度的byte[]對象只需3條操作碼,而Object lock = new Object()則需要7行操作碼。

3當一個線程訪問對象的一個synchronized(this)同步代碼塊時,另一個線程仍然可以訪問該對象中的非synchronized(this)同步代碼塊。

 代碼示例:

public class Counts implements Runnable {
    private int count;
    public Counts(){
        count = 0;
    }
    //synchronized方法
    public void countAdd(){
        synchronized (this) {
            for (int i = 0; i < 5; i++) {
                try {
                    System.out.println("當前線程名稱" + Thread.currentThread().getName() + ":" + (count++));
                    Thread.sleep(100);
                } catch (Exception e) {
                    e.printStackTrace();
                }
            }
        }
    }
    //非synchronized方法,未對count進行寫操作,所以可以不用synchronized
    public void countRead(){
        for (int i = 0; i < 5; i++) {
            try {
                System.out.println("當前線程名稱" + Thread.currentThread().getName() + ":" + count);
                Thread.sleep(100);
            } catch (Exception e) {
                e.printStackTrace();
            }
        }
    }
    @Override
    public void run() {
        String threadName = Thread.currentThread().getName();
        if (threadName.equals("A")) {
            countAdd();
        } else if (threadName.equals("B")) {
            countRead();
        }
    }
}

測試代碼:

    public class CountsTest {
        public static void main(String[] args) {
            Counts counter = new Counts();
            Thread thread1 = new Thread(counter, "A");
            Thread thread2 = new Thread(counter, "B");
            thread1.start();
            thread2.start();
        }
    }

測試結果:

當前線程名稱A:0
當前線程名稱B:1
當前線程名稱A:1
當前線程名稱B:2
當前線程名稱A:2
當前線程名稱B:3
當前線程名稱A:3
當前線程名稱B:3
當前線程名稱B:4
當前線程名稱A:4

結果分析:

  上面代碼中countAdd是一個synchronized的,countRead是非synchronized的。從上面的結果中可以看出,在線程A訪問synchronizedcountAdd方法過程中,線程B可以同時訪問非synchronizedcountRead方法。也就是說:一個線程訪問一個對象的synchronized代碼塊時,別的線程可以訪問該對象的非synchronized代碼塊而不受阻塞。

 1.2修飾方法

1.修飾非靜態方法

  說明:Synchronized修飾一個方法很簡單,就是在方法的前面加synchronized

public synchronized void method(){//todo};

  synchronized修飾方法和修飾一個代碼塊類似,只是作用范圍不一樣,修飾代碼塊是大括號括起來的范圍,而修飾方法范圍是整個方法。如將【Demo1】中的run方法改成如下的方式,實現的效果一樣。

 Synchronized作用於整個方法的寫法。
寫法一:

public synchronized void method(){
   // todo
}

寫法

public void method(){
  synchronized(this) {
      // todo
  }
}
   

  寫法一修飾的是一個方法,寫法二修飾的是一個代碼塊,但寫法一與寫法二是等價的,都是鎖定了整個方法時的內容。

 注意點

1)synchronized關鍵字不能繼承。

  雖然可以使用synchronized來定義方法,但synchronized並不屬於方法定義的一部分因此,synchronized關鍵字不能被繼承。

  如果在父類中的某個方法使用了synchronized關鍵字,而在子類中覆蓋了這個方法,在子類中的這個方法默認情況下並不是同步的,而必須顯式地在子類的這個方法中加上synchronized關鍵字才可以。

  當然,還可以在子類方法中調用父類中相應的方法,這樣雖然子類中的方法不是同步的,但子類調用了父類的同步方法,因此,子類的方法也就相當於同步了。

示例代碼如下:
1)在子類方法中加上synchronized關鍵字

父類:

class Parent {
   public synchronized void method() { }
}

子類:

class Child extends Parent {
   public synchronized void method() { }
}

2)在子類方法中調用父類的同步方法

父類:

class Parent {
   public synchronized void method() { }
}

子類:

class Child extends Parent {
   public void method() { super.method(); }
}

2)在定義接口方法時不能使用synchronized關鍵字。

3)構造方法不能使用synchronized關鍵字,但可以使用synchronized代碼塊來進行同步。

 2.修飾靜態方法

 代碼示例:

    public class StaticMethod implements Runnable{
        private static int count;
        public StaticMethod(){
            count = 0;
        }
        //靜態方法
        public synchronized static void getCounts(){ //屬於類級別的鎖,對於不同對象不可同時進入同一個類
            for (int i = 0; i < 5; i++) {
                try {
                    System.out.println("當前線程名稱" + Thread.currentThread().getName() + ":" + (count++));
                    Thread.sleep(100);
                } catch (Exception e) {
                    e.printStackTrace();
                }
            }
        }
        @Override
        public synchronized void run() { //屬於方法級別的鎖,對於不同對象可同時進入
            getCounts();
        }
    }

測試代碼:

    public class StaticMethodTest {
        public static void main(String[] args) {
            StaticMethod sync1 = new StaticMethod();
            StaticMethod sync2 = new StaticMethod();
            Thread thread1 = new Thread(sync1, "thread1");
            Thread thread2 = new Thread(sync2, "thread2");
            thread1.start();
            thread2.start();
        }
    }

結果分析:

  sync1sync2StaticMethod的兩個對象,但在thread1thread2並發執行時卻保持了線程同步。這是因為run中調用了靜態方法getCounts,而靜態方法是屬於類的,所以sync1sync2相當於用了同一把鎖。這與非靜態方法是不同的。 

1.3修飾一個類

  說明:對於類鎖,有幾種方式:(1synchronized修飾靜態方法;(2synchronized修飾靜態變量;(3synchronized代碼塊中直接引入類名稱。前2種方式,前面已經做了詳細的解釋,這里主要解釋第三種。

代碼示例:

class ClassName {
  public void method() {
    synchronized(ClassName.class) {
      // todo
    }
  }
}

  synchronized作用於一個類ClassName時,是給這個類ClassName加鎖,ClassName的所有對象用的是同一把鎖。

參考資料:

http://www.importnew.com/21866.html

http://www.importnew.com/21907.html

http://www.cnblogs.com/xujian2014/tag/


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM