java多執行緒3:synchronized

讓我發會呆發表於2021-12-13

執行緒安全

多個執行緒共同訪問一個物件的例項變數,那麼就可能出現執行緒不安全的問題。

 先看一段程式碼示例,定義一個物件 MyDomain1

public class MyDomain1 {

    private int num = 0;

    public void addI(String username) {
        try {
            if (username.equals("a")) {
                num = 100;
                System.out.println("a set over!");
                Thread.sleep(2000);
            } else {
                num = 200;
                System.out.println("b set over!");
            }
            System.out.println(username + " num=" + num);
        } catch (InterruptedException e) {
            // TODO Auto-generated catch block
            e.printStackTrace();
        }
    }
}

  寫兩個執行緒分別去add字串"a"和字串"b"

public class Mythread1_1 extends Thread {

    private MyDomain1 numRef;

    public Mythread1_1(MyDomain1 numRef) {
        super();
        this.numRef = numRef;
    }

    @Override
    public void run() {
        super.run();
        numRef.addI("a");
    }

}
@Test
    public void test() throws InterruptedException {
        MyDomain1 mythread1 = new MyDomain1();
        Mythread1_1 athread = new Mythread1_1(mythread1);
        athread.start();

        Mythread1_2 bthread = new Mythread1_2(mythread1);
        bthread.start();

        athread.join();
        bthread.join();
        System.out.println(Thread.currentThread().getName());
    }

  執行結果:

a set over!
b set over!
b num=200
a num=200
main

  按照正常來看應該列印"a num = 100"和"b num = 200"才對,現在卻列印了"b num = 200"和"a num = 200",這就是執行緒安全問題。

當我們給 MyDomain1的 addI方法加上同步 synchronized 後,

執行結果為:

a set over!
a num=100
b set over!
b num=200
main

  多個執行緒例項,訪問同一個共享例項變數,非執行緒安全問題:多個執行緒對同一個物件中的同一個例項變數操作時 ,Mythread1 方法用synchronized修飾 可以解決執行緒非安全問題。

 

多個物件多個鎖

@Test
    public void test1() throws InterruptedException {
        MyDomain1 mythread1 = new MyDomain1();
        Mythread1_1 athread = new Mythread1_1(mythread1);
        athread.start();
        MyDomain1 mythread2 = new MyDomain1();
        Mythread1_2 bthread = new Mythread1_2(mythread2);
        bthread.start();

        athread.join();
        bthread.join();
        System.out.println(Thread.currentThread().getName());
    }

  執行結果:

a set over!
b set over!
b num=200
a num=100
main

  第6行,我們再定義一個新物件,當同時執行 athread 和 bthread的時候,列印的順序是交叉的。

關鍵字synchronized取得的鎖都是物件鎖,而不是把一段程式碼或方法(函式)當作鎖,

所以在上面的示例中,哪個執行緒先執行帶synchronized關鍵字的方法,哪個執行緒就持有該方法所屬物件的鎖Lock,

那麼其他執行緒只能呈等待狀態,前提是多個執行緒訪問的是同一個物件。但如果多個執行緒訪問多個物件,則JVM會建立多個鎖。

上面的示例就是建立了2個MyDomain1.java類的物件,所以就會產生出2個鎖,因此兩個執行緒之間不會受到對方加鎖的約束。

 

synchronized方法與鎖物件

在一個實體類, 定義一個同步方法和一個非同步方法:

public class MyDomain2 {

synchronized public void methodA() {
try {
System.out.println("begin methodA threadName=" + Thread.currentThread().getName()+ " begin time="
+ System.currentTimeMillis());
Thread.sleep(5000);
System.out.println("end endTime=" + System.currentTimeMillis());
} catch (InterruptedException e) {
e.printStackTrace();
}
}

// 也加上synchronized修飾
public void methodB() {
try {
System.out.println("begin methodB threadName=" + Thread.currentThread().getName() + " begin time="
+ System.currentTimeMillis());
Thread.sleep(5000);
System.out.println("end endTime=" + System.currentTimeMillis());
} catch (InterruptedException e) {
e.printStackTrace();
}
}

}

  然後定義兩個執行緒類,一個呼叫同步方法,一個呼叫非同步方法

public class Mythread2_1 extends Thread {

    private MyDomain2 object;

    public Mythread2_1(MyDomain2 object) {
        this.object = object;
    }

    @Override
    public void run() {
        object.methodA();
    }

}
public class Mythread2_2 extends Thread {

    private MyDomain2 object;

    public Mythread2_2(MyDomain2 object) {
        this.object = object;
    }

    @Override
    public void run() {
        object.methodB();
    }

}
@Test
    public void test2() throws InterruptedException {
        MyDomain2 object = new MyDomain2();
        Mythread2_1 a = new Mythread2_1(object);
        a.setName("A");
        Mythread2_2 b = new Mythread2_2(object);
        b.setName("B");

        a.start();
        b.start();
        a.join();
        b.join();
    }

  執行結果:

begin methodA threadName=A begin time=1639384569539
begin methodB threadName=B begin time=1639384569560
end endTime=1639384574541
end endTime=1639384574564

  可以看到methodA和methodB基本同時執行,當我們把methodB也加上 synchronized 修飾後:

begin methodA threadName=A begin time=1639384695430
end endTime=1639384700433
begin methodB threadName=B begin time=1639384700433
end endTime=1639384705437

  可以看出methodA執行完之後,methodB方法才開始執行。

因此我們可以得出結論:

1、A執行緒持有Object物件的Lock鎖,B執行緒可以以非同步方式呼叫Object物件中的非synchronized型別的方法
2、A執行緒持有Object物件的Lock鎖,B執行緒如果在這時呼叫Object物件中的synchronized型別的方法則需要等待,也就是同步

鎖重入

關鍵字synchronized擁有鎖重入的功能,也就是在使用synchronized時,當一個執行緒得到一個物件鎖後,再次請求此物件鎖時是可以再次得到該物件的鎖的。

這也證明在一個synchronized方法/塊的內部呼叫本類的其他synchronized方法/塊時,是永遠可以得到鎖的。

定義三個同步方法,它們之間順序呼叫:

public class MyDomain3 {

    synchronized public void service1() {
        System.out.println("service1");
        service2();
    }

    synchronized public void service2() {
        System.out.println("service2");
        service3();
    }

    synchronized public void service3() {
        System.out.println("service3");
    }
}
public class Mythread3 extends Thread {

    public void run() {
        MyDomain3 service = new MyDomain3();
        service.service1();
    }

}

  

@Test
    public void test3() throws InterruptedException {
	Mythread3 t = new Mythread3();
	t.start();

        t.join();
    }

  執行結果:

service1
service2
service3

  “可重入鎖”的概念是:自己可以再次獲取自己的內部鎖。

比如有1條執行緒獲得了某個物件的鎖,此時這個物件鎖還沒有釋放,當其再次想要獲取這個物件的鎖的時候還是可以獲取的,如果不可鎖重入的話,就會造成死鎖。

 

定義子父類:

public class MyDomain3_1_Father {

    public int i = 10;

    synchronized public void operateIMainMethod() {
        try {
            i--;
            System.out.println("main print i=" + i);
            Thread.sleep(100);
        } catch (InterruptedException e) {
            // TODO Auto-generated catch block
            e.printStackTrace();
        }
    }

} 
public class MyDomain3_1_Son extends MyDomain3_1_Father {

    synchronized public void operateISubMethod() {
        try {
            while (i > 0) {
                i--;
                System.out.println("sub print i=" + i);
                Thread.sleep(100);
                this.operateIMainMethod();
            }
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }

}

  執行緒類執行子類方法

public class Mythread3_1 extends Thread {
    @Override
    public void run() {
        MyDomain3_1_Son sub = new MyDomain3_1_Son();
        sub.operateISubMethod();
    }

}

  

 @Test
    public void test3() throws InterruptedException {
        // 子類完全可以通過可重入鎖呼叫父類的同步方法
        Mythread3_1 t = new Mythread3_1();
        t.start();

        t.join();
    }

  執行結果:

sub print i=9
main print i=8
sub print i=7
main print i=6
sub print i=5
main print i=4
sub print i=3
main print i=2
sub print i=1
main print i=0

  當存在父子類繼承關係時,子類是完全可以通過“可重入鎖”呼叫父類的同步方法的。

 

異常自動釋放鎖

當一個執行緒執行的程式碼出現異常時,其所持有的鎖會自動釋放

模擬的是把一個long型數作為除數,從MAX_VALUE開始遞減,直至減為0,從而產生ArithmeticException。看一下例子:

public class MyDomain4 {

    synchronized public void testMethod() {
        try {
            System.out.println(Thread.currentThread().getName() + "進入synchronized方法");
            long l = Integer.MAX_VALUE;
            while (true) {
                long lo = 2 / l;
                l--;
            }
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

}
public class Mythread4 extends Thread {

    private MyDomain4 service;

    public Mythread4(MyDomain4 service) {
        this.service = service;
    }

    public void run() {
        service.testMethod();
    }

}

  

@Test
    public void test4() throws InterruptedException {
        MyDomain4 myDomain4 = new MyDomain4();
        Mythread4 a = new Mythread4(myDomain4);
        Mythread4 b = new Mythread4(myDomain4);
        a.start();
        b.start();
        a.join();
        b.join();
    }

  執行結果:

Thread-0進入synchronized方法
java.lang.ArithmeticException: / by zero
	at multithreading.synchronizedDemo.MyDomain4.testMethod(MyDomain4.java:15)
	at multithreading.synchronizedDemo.Mythread4.run(Mythread4.java:17)
Thread-1進入synchronized方法
java.lang.ArithmeticException: / by zero
	at multithreading.synchronizedDemo.MyDomain4.testMethod(MyDomain4.java:15)
	at multithreading.synchronizedDemo.Mythread4.run(Mythread4.java:17)

  

加鎖方式

synchronized 的不同加鎖方式有不同是阻塞效果

1:synchronized (this) 

在實體類中定義兩個同步方法

public class MyDomain8 {

    public void serviceMethodA() {
        synchronized (this) {
            try {
                System.out.println("A begin time = " + System.currentTimeMillis());
                Thread.sleep(2000);
                System.out.println("A end time = " + System.currentTimeMillis());
            } catch (InterruptedException e) {
                e.printStackTrace();
            }

        }
    }

    public void serviceMethodB() {
        synchronized (this) {
            System.out.println("B begin time = " + System.currentTimeMillis());
            System.out.println("B end time = " + System.currentTimeMillis());
        }
    }
}

  呼叫同步方法B

public class MyThread8_1 extends Thread {

    private MyDomain8 td;

    public MyThread8_1(MyDomain8 td) {
        this.td = td;
    }

    public void run() {
        td.serviceMethodB();
    }

}

  呼叫同步方法A

public class MyThread8_2 extends Thread {

    private MyDomain8 td;

    public MyThread8_2(MyDomain8 td) {
        this.td = td;
    }

    public void run() {
        td.serviceMethodA();
    }

}

  

@Test
    public void test8() throws InterruptedException {
        MyDomain8 td = new MyDomain8();
        MyThread8_1 a = new MyThread8_1(td);
        MyThread8_2 b = new MyThread8_2(td);
        a.start();
        b.start();
        a.join();
        b.join();
    }

  執行結果:

B begin time = 1639386305338
B end time = 1639386305338
A begin time = 1639386305339
A end time = 1639386307339

  synchronized(this)塊 獲得的是一個物件鎖,換句話說,synchronized塊鎖定的是整個物件。

 

2:synchronized 非靜態方法

public class MyDomain9 {

    public void serviceMethodA() {
        synchronized (this) {
            try {
                System.out.println("A begin time = " + System.currentTimeMillis());
                Thread.sleep(2000);
                System.out.println("A end time = " + System.currentTimeMillis());
            } catch (InterruptedException e) {
                e.printStackTrace();
            }

        }
    }

    synchronized public void serviceMethodB() {
        System.out.println("B begin time = " + System.currentTimeMillis());
        try {
            Thread.sleep(2000);
        } catch (InterruptedException e) {
            // TODO Auto-generated catch block
            e.printStackTrace();
        }
        System.out.println("B end time = " + System.currentTimeMillis());
    }
}

  

@Test
    public void test9() throws InterruptedException {
        MyDomain9 td = new MyDomain9();
        MyThread9_1 a = new MyThread9_1(td);
        MyThread9_2 b = new MyThread9_2(td);
        a.start();
        b.start();
        a.join();
        b.join();
    }

  執行結果:

B begin time = 1639386457971
B end time = 1639386459972
A begin time = 1639386459974
A end time = 1639386461974

結論:(1)對其他synchronized同步方法或synchronized(this)同步程式碼塊呈阻塞狀態
   (2)同一時間只有一個執行緒可以執行synchronized同步方法中的程式碼

3:synchronized(非this非自身其他物件x)

定義一個非自身物件的鎖

public class MyDomain10 {

    private String anyString = new String();

    public void serviceMethodA() {
        synchronized (anyString) {
            try {
                System.out.println("A begin time = " + System.currentTimeMillis());
                Thread.sleep(2000);
                System.out.println("A end time = " + System.currentTimeMillis());
            } catch (InterruptedException e) {
                e.printStackTrace();
            }

        }
    }

    synchronized public void serviceMethodB() {
        try {
            System.out.println("B begin time = " + System.currentTimeMillis());
            Thread.sleep(2000);
            System.out.println("B end time = " + System.currentTimeMillis());
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}

  

@Test
    public void test10() throws InterruptedException {
        MyDomain10 td = new MyDomain10();
        MyThread10_1 a = new MyThread10_1(td);
        MyThread10_2 b = new MyThread10_2(td);
        a.start();
        b.start();
        a.join();
        b.join();
    }

  執行結果:

B begin time = 1639387076574
A begin time = 1639387076575
B end time = 1639387078577
A end time = 1639387078578

  兩個方法幾乎同時執行,同時結束,說明 synchronized(非this非自身其他物件x)程式碼塊與synchronized方法呈非阻塞狀態。

 

4:synchronized(非this自身物件x)

定義一個分別需要自身物件的同步方法A和 非靜態同步方法B

public class MyDomain11 {

    public void serviceMethodA(MyDomain11 anyString) {
        synchronized (anyString) {
            try {
                System.out.println("A begin time = " + System.currentTimeMillis());
                Thread.sleep(2000);
                System.out.println("A end time = " + System.currentTimeMillis());
            } catch (InterruptedException e) {
                e.printStackTrace();
            }

        }
    }

    synchronized public void serviceMethodB() {
        try {
            System.out.println("B begin time = " + System.currentTimeMillis());
            Thread.sleep(2000);
            System.out.println("B end time = " + System.currentTimeMillis());
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    } 

@Test
    public void test11() throws InterruptedException {
        MyDomain11 td = new MyDomain11();
        MyThread11_1 a = new MyThread11_1(td);
        MyThread11_2 b = new MyThread11_2(td);
        a.start();
        b.start();
        a.join();
        b.join();
    }

  執行結果:

B begin time = 1639387213624
B end time = 1639387215625
A begin time = 1639387215625
A end time = 1639387217628

  可見 synchronized(非this自身物件x)程式碼塊與synchronized方法或synchronized(this)都呈阻塞狀態,

這兩個測試方法,也能證明 synchronized方法與synchronized(this) 持有的都是物件鎖。

 

5:synchronized靜態方法

代表的是對當前.java檔案對應的Class類加鎖

public class MyDomain12 {

    public synchronized static void printA() {
        try {
            System.out.println(
                    "執行緒名稱為:" + Thread.currentThread().getName() + "在" + System.currentTimeMillis() + "進入printA()方法");
            Thread.sleep(3000);
            System.out.println(
                    "執行緒名稱為:" + Thread.currentThread().getName() + "在" + System.currentTimeMillis() + "離開printA()方法");
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }

    public static void printB() {
        // synchronized靜態方法持有的是對當前.java檔案對應的Class類加鎖
        synchronized (MyDomain12.class) {
            System.out.println(
                    "執行緒名稱為:" + Thread.currentThread().getName() + "在" + System.currentTimeMillis() + "進入printB()方法");
            System.out.println(
                    "執行緒名稱為:" + Thread.currentThread().getName() + "在" + System.currentTimeMillis() + "離開printB()方法");
        }

    }
    
  public synchronized void printC() {
        System.out.println(
                "執行緒名稱為:" + Thread.currentThread().getName() + "在" + System.currentTimeMillis() + "進入printC()方法");
        System.out.println(
                "執行緒名稱為:" + Thread.currentThread().getName() + "在" + System.currentTimeMillis() + "離開printC()方法");
    }

}

  

@Test
    public void test12() throws InterruptedException {
        MyDomain12 md = new MyDomain12();
        Mythread12_1 mt1 = new Mythread12_1();
        Mythread12_2 mt2 = new Mythread12_2();
        Mythread12_3 mt3 = new Mythread12_3(md);
        mt1.start();
        mt2.start();
        mt3.start();

        mt1.join();
        mt2.join();
        mt3.join();
    }

  執行結果:

執行緒名稱為:Thread-0在1639387516115進入printA()方法
執行緒名稱為:Thread-2在1639387516116進入printC()方法
執行緒名稱為:Thread-2在1639387516116離開printC()方法
執行緒名稱為:Thread-0在1639387519119離開printA()方法
執行緒名稱為:Thread-1在1639387519120進入printB()方法
執行緒名稱為:Thread-1在1639387519120離開printB()方法

  結論:synchronized靜態方法也是互斥的(printA和printB可以看出)

     synchronized靜態方法與synchronized方法持有的是不同的鎖(printC()方法的呼叫和對printA()方法、printB()方法的呼叫時非同步的)

       synchronized靜態方法持有的是對當前.java檔案對應的Class類加鎖

 

 

參考文獻

1:《Java併發程式設計的藝術》

2:《Java多執行緒程式設計核心技術》

 

相關文章