Java多線程6:Synchronized鎖代碼塊(this和任意對象)


一、Synchronized(this)鎖代碼塊

   用關鍵字synchronized修飾方法在有些情況下是有弊端的,若是執行該方法所需的時間比較長,線程1執行該方法的時候,線程2就必須等待。這種情況下就可以使用synchronized同步該方法中會引起線程安全的那部分代碼,其余不會引起線程安全的就不需要同步,這部分代碼就可以多線程並發執行,減少時間提高效率。

  舉例:多線程執行同一個方法時,同步方法和同步代碼塊花費時間的比較

  1、synchronized修飾方法(同步方法)

  synchronized修飾longTimeTask方法,其中花費時間比較長的且與線程安全無關的是37-39行代碼,會引起線程安全問題的是42-46。

 1 public class ThreadSynch {
 2 
 3     private int num;
 4 
 5     public synchronized void longTimeTask(String userName){
 6         //定義各線程的進入時間
 7         long thread0StartTime = 0L;
 8         long thread1StartTime = 0L;
 9         long thread2StartTime = 0L;
10         long thread3StartTime = 0L;
11         long thread4StartTime = 0L;
12         //定義各線程執行該方法所需的時間
13         long thread0LastTime;
14         long thread1LastTime;
15         long thread2LastTime;
16         long thread3LastTime;
17         long thread4LastTime;
18         //顯示各線程進入的時間
19         if(Thread.currentThread().getName().contains("-0")){
20             thread0StartTime = System.currentTimeMillis();
21             System.out.println(Thread.currentThread().getName() + "進入時間為====" + thread0StartTime);
22         }else if(Thread.currentThread().getName().contains("-1")){
23             thread1StartTime = System.currentTimeMillis();
24             System.out.println(Thread.currentThread().getName() + "進入時間為====" + thread1StartTime);
25         }else if(Thread.currentThread().getName().contains("-2")){
26             thread2StartTime = System.currentTimeMillis();
27             System.out.println(Thread.currentThread().getName() + "進入時間為====" + thread2StartTime);
28         }else if(Thread.currentThread().getName().contains("-3")){
29             thread3StartTime = System.currentTimeMillis();
30             System.out.println(Thread.currentThread().getName() + "進入時間為====" + thread3StartTime);
31         }else if(Thread.currentThread().getName().contains("-4")){
32             thread4StartTime = System.currentTimeMillis();
33             System.out.println(Thread.currentThread().getName() + "進入時間為====" + thread4StartTime);
34         }
35 
36         //花費時間較長,與線程安全無關的代碼
37         for(int i = 200000000; i > 0; i--) {
38             String nameID = Thread.currentThread().getName() + Thread.currentThread().getId();
39         }
40 
41         //與線程安全相關的代碼塊
42         if("zs".equals(userName)){
43             num = 100;
44         }else if("ls".equals(userName)){
45             num = 200;
46         }
47 
48         //顯示各線程執行該方法的時間
49         if(Thread.currentThread().getName().contains("0")){
50             thread0LastTime = System.currentTimeMillis() - thread0StartTime;
51             System.out.println(Thread.currentThread().getName() + "執行時間為===" + thread0LastTime + "ms");
52         }else if(Thread.currentThread().getName().contains("1")){
53             thread1LastTime = System.currentTimeMillis() - thread1StartTime;
54             System.out.println(Thread.currentThread().getName() + "執行時間為===" + thread1LastTime + "ms");
55         }else if(Thread.currentThread().getName().contains("2")){
56             thread2LastTime = System.currentTimeMillis() - thread2StartTime;
57             System.out.println(Thread.currentThread().getName() + "執行時間為===" + thread2LastTime + "ms");
58         }else if(Thread.currentThread().getName().contains("3")){
59             thread3LastTime = System.currentTimeMillis() - thread3StartTime;
60             System.out.println(Thread.currentThread().getName() + "執行時間為===" + thread3LastTime + "ms");
61         }else if(Thread.currentThread().getName().contains("4")){
62             thread4LastTime = System.currentTimeMillis() - thread4StartTime;
63             System.out.println(Thread.currentThread().getName() + "執行時間為===" + thread4LastTime + "ms");
64         }
65 
66     }
67 }

  繼承Thread的Thread01類,其run方法調用上述對象的longTimeTask方法

public class Thread01 extends Thread{
    private ThreadSynch threadSynch;

    public Thread01(ThreadSynch threadSynch) {
        this.threadSynch = threadSynch;
    }

    @Override
    public void run() {
        threadSynch.longTimeTask("ls");
    }
}

  測試,構建同一對象的多個線程

public class Test {
    public static void main(String[] args) {
        ThreadSynch threadSynch = new ThreadSynch();
        //五個線程使用同一個對象構建
        Thread thread01 = new Thread01(threadSynch);
        Thread thread02 = new Thread01(threadSynch);
        Thread thread03 = new Thread01(threadSynch);
        Thread thread04 = new Thread01(threadSynch);
        Thread thread05 = new Thread01(threadSynch);
        //五個線程同時調用該對象中的方法
        thread01.start();
        thread02.start();
        thread03.start();
        thread04.start();
        thread05.start();
    }
}

  結果:

Thread-0進入時間為====1553150692703
Thread-0執行時間為===8437ms
Thread-3進入時間為====1553150701140
Thread-3執行時間為===7014ms
Thread-1進入時間為====1553150708154
Thread-1執行時間為===7002ms
Thread-4進入時間為====1553150715157
Thread-4執行時間為===7121ms
Thread-2進入時間為====1553150722278
Thread-2執行時間為===7147ms

  說明:因為synchronized修飾的是整個方法,所以線程Thread-0訪問longTimeTask方法的時候,其余四個線程都處於阻塞狀態,待其執行結束釋放鎖的時候,線程Thread-3開始執行,其余三個線程還是處於阻塞狀態,所以,這五個線程執行完畢所需的時間是各自執行時間的相加,8.4 + 7.0 + 7.0 + 7.1 + 7.1 = 36.6s。

  2、synchronized修飾代碼塊(同步代碼塊)

  synchronized由同步方法改為同步方法中引起線程安全問題的代碼塊,其余都不變

public class ThreadSynch {

    private int num;

    public void longTimeTask(String userName){
        long thread0StartTime = 0L;
        long thread1StartTime = 0L;
        long thread2StartTime = 0L;
        long thread3StartTime = 0L;
        long thread4StartTime = 0L;
        long thread0LastTime;
        long thread1LastTime;
        long thread2LastTime;
        long thread3LastTime;
        long thread4LastTime;
        if(Thread.currentThread().getName().contains("-0")){
            thread0StartTime = System.currentTimeMillis();
            System.out.println(Thread.currentThread().getName() + "進入時間為====" + thread0StartTime);
        }else if(Thread.currentThread().getName().contains("-1")){
            thread1StartTime = System.currentTimeMillis();
            System.out.println(Thread.currentThread().getName() + "進入時間為====" + thread1StartTime);
        }else if(Thread.currentThread().getName().contains("-2")){
            thread2StartTime = System.currentTimeMillis();
            System.out.println(Thread.currentThread().getName() + "進入時間為====" + thread2StartTime);
        }else if(Thread.currentThread().getName().contains("-3")){
            thread3StartTime = System.currentTimeMillis();
            System.out.println(Thread.currentThread().getName() + "進入時間為====" + thread3StartTime);
        }else if(Thread.currentThread().getName().contains("-4")){
            thread4StartTime = System.currentTimeMillis();
            System.out.println(Thread.currentThread().getName() + "進入時間為====" + thread4StartTime);
        }

        //花費時間較長,與線程安全無關的代碼
        for(int i = 200000000; i > 0; i--) {
            String nameID = Thread.currentThread().getName() + Thread.currentThread().getId();
        }

        //與線程安全相關的代碼塊用synchronized修飾
        synchronized(this){
            if("zs".equals(userName)){
                num = 100;
            }else if("ls".equals(userName)){
                num = 200;
            }
        }

        if(Thread.currentThread().getName().contains("0")){
            thread0LastTime = System.currentTimeMillis() - thread0StartTime;
            System.out.println(Thread.currentThread().getName() + "執行時間為===" + thread0LastTime + "ms");
        }else if(Thread.currentThread().getName().contains("1")){
            thread1LastTime = System.currentTimeMillis() - thread1StartTime;
            System.out.println(Thread.currentThread().getName() + "執行時間為===" + thread1LastTime + "ms");
        }else if(Thread.currentThread().getName().contains("2")){
            thread2LastTime = System.currentTimeMillis() - thread2StartTime;
            System.out.println(Thread.currentThread().getName() + "執行時間為===" + thread2LastTime + "ms");
        }else if(Thread.currentThread().getName().contains("3")){
            thread3LastTime = System.currentTimeMillis() - thread3StartTime;
            System.out.println(Thread.currentThread().getName() + "執行時間為===" + thread3LastTime + "ms");
        }else if(Thread.currentThread().getName().contains("4")){
            thread4LastTime = System.currentTimeMillis() - thread4StartTime;
            System.out.println(Thread.currentThread().getName() + "執行時間為===" + thread4LastTime + "ms");
        }

    }
}

  同樣的五個線程訪問,看一下結果:

Thread-0進入時間為====1553151204348
Thread-3進入時間為====1553151204348
Thread-1進入時間為====1553151204348
Thread-2進入時間為====1553151204348
Thread-4進入時間為====1553151204380
Thread-3執行時間為===19330ms
Thread-2執行時間為===19383ms
Thread-1執行時間為===19854ms
Thread-4執行時間為===20498ms
Thread-0執行時間為===20782ms

  說明:因為synchronized修飾的是方法中會引起線程安全問題的代碼塊,所以僅僅是這一部分代碼無法並發執行。可以看到Thread-0,Thread-1,Thread-2,Thread-3,Thread-4幾乎同時進入longTimeTask方法,並發執行for循環中花費時間較長的代碼,由結果看,Thread-3最先執行完這部分代碼,開始執行synchronized修飾的代碼塊,其余四個線程隨后進入阻塞狀態。因為同步代碼塊中執行時間較短,Thread-3執行完后,Thread-2開始執行,最后是Thread-0執行,至此,五個線程執行完畢,所花費的時間就是Thread-0花費的時間,即20.8s。

  可以看到,在longTimeTask方法中,synchronized由修飾方法改為修飾代碼塊,多線程執行所花費的時間由36.6s變成20.8s,執行時間明顯減少,效率提升。

 二、任意對象作為對象監視器

  2.1 上述同步代碼塊使用的是synchronized(this)格式,其實Java還支持對“任意對象”作為對象監視器來實現同步的功能。這種任意對象大多是該方法所屬類中的實例變量或該方法的參數,不然拋開這個類去使用別的對象作為對象監視器,意義不大。使用的格式是synchronized(非this的任意對象)。

  舉例:以ThreadSynch類中的變量student作為對象監視器去同步代碼塊

public class ThreadSynch {

    private Student student = new Student();
    private String schoolName;

    public void setNameAndPassWord(String name,String age){
        synchronized(student){
            System.out.println(Thread.currentThread().getName() + "===" + "進入同步代碼塊");
            try {
                Thread.sleep(3000);
                this.student.setName(name);
                this.student.setAge(age);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println(Thread.currentThread().getName() + "===" + "離開同步代碼塊");
        }
    }
}

  Thread01的run方法調用setNameAndPassWord方法

public class Thread01 extends Thread{
    private ThreadSynch threadSynch;

    public Thread01(ThreadSynch threadSynch) {
        this.threadSynch = threadSynch;
    }

    @Override
    public void run() {
        threadSynch.setNameAndPassWord("ls","11");
    }
}

  測試:

public class Test {
    public static void main(String[] args) {
        ThreadSynch threadSynch = new ThreadSynch();
        //三個線程使用同一個對象構建
        Thread thread01 = new Thread01(threadSynch);
        Thread thread02 = new Thread01(threadSynch);
        Thread thread03 = new Thread01(threadSynch);
        //三個線程同時調用該對象中的方法
        thread01.start();
        thread02.start();
        thread03.start();
    }
}

  結果:

Thread-1===進入同步代碼塊
Thread-1===離開同步代碼塊
Thread-2===進入同步代碼塊
Thread-2===離開同步代碼塊
Thread-0===進入同步代碼塊
Thread-0===離開同步代碼塊

  說明:Thread-0,Thread-1,Thread-2執行到同步代碼塊synchronized(student)時,都會去獲取與student對象關聯的monitor,判斷該monitor是否被別的線程所有,因為三個線程中的student都是同一個對象,所以一個線程執行的時候,與student關聯的那個monitor會被當前線程所有,別的線程都會處於阻塞狀態。

  稍微改一下ThreadSynch類中setNameAndPassWord的方法,添加7-9行的代碼

 1 public class ThreadSynch {
 2 
 3     private Student student = new Student();
 4     private String schoolName;
 5 
 6     public void setNameAndPassWord(String name,String age){
 7         if(Thread.currentThread().getName().contains("1")){
 8             student = new Student();
 9         }
10         synchronized(student){
11             System.out.println(Thread.currentThread().getName() + "===" + "進入同步代碼塊");
12             try {
13                 Thread.sleep(3000);
14                 this.student.setName(name);
15                 this.student.setAge(age);
16             } catch (InterruptedException e) {
17                 e.printStackTrace();
18             }
19             System.out.println(Thread.currentThread().getName() + "===" + "離開同步代碼塊");
20         }
21     }
22 }

  其余都不變,看一下結果:

Thread-0===進入同步代碼塊
Thread-1===進入同步代碼塊
Thread-0===離開同步代碼塊
Thread-1===離開同步代碼塊
Thread-2===進入同步代碼塊
Thread-2===離開同步代碼塊

  說明:可以看到,Thread-0和Thread-1同時進入同步代碼塊。分析一下原因,Thread-0執行到synchronized(student)時,會去獲取與該student對象關聯的monitor的所有權,該monitor沒有被別的線程占有,Thread-0進入同步代碼塊中。Thread-1執行setNameAndPassWord方法的時候,新添加的7-9行的代碼將student變量指向了一個新的student對象,此時的student對象和Thread-0時的student對象已經不是同一個了,對應的monitor也不是Thread-0時的那個monitor,所以Thread-1在Thread-0還未離開同步代碼塊的時候,也可以進入到同步代碼塊中執行。但Thread-2執行同步代碼塊時的student還是Thread-1時的那個student,所以Thread-2只能等到Thread-1執行結束,才能進入同步代碼塊中。

  所以,多個線程訪問同步代碼塊時,只要synchronized(this對象/非this對象)中的對象是同一個對象,那么同一時間只能有一個線程可以執行同步代碼塊中的內容。這里注意一下當任意對象是string類型時,使用不當可能會有一些麻煩。具體就是以下兩個例子:

public class Test {
    public static void main(String[] args) {
        String str1 = "111";
        String str2 = "111";
        System.out.println(str1 == str2);

        String str3 = new String("222");
        String str4 = new String("222");
        System.out.println(str3 == str4);

    }
}

  結果:

true
false

  多線程並發執行時,當synchronized(str1)由str1變成str2時,其余線程是否還會處於阻塞狀態(會)。

  多線程並發執行時,當synchronized(str3)由str3變成str4時,其余線程是否還會處於阻塞狀態(不會)。

  具體的string常量與new String對象的區別,參見這篇文章從為什么String=String談到StringBuilder和StringBuffer

 

參考資料:

Java多線程5:synchronized鎖方法塊


免責聲明!

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



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