2.變量的併發訪問

前記:師夷長技以自強

1.問題背景

在上一篇文章中我們已經討論了線程具有異步運行的特性,因此當多線程同時訪問同一個實例變量時就會引發髒讀的問題。而這顯然不是我們願意看到的,解決辦法也很簡單,就是給訪問該變量的程序部分加鎖。多線程併發在一些追求效率的系統中常存在變量不可見的問題,由於變量的不可見也會導致程序運行的結果不是我們想要的。一句話,同步性和可見性問題是多線程中的兩大重點內容,他們分別對應於synchronized和volitle關鍵字的使用。本文主要圍繞了在各種情況下如何使用這兩個關鍵字而展開的。

2.synchronized同步方法

2.1方法內變量是線程安全的

ex1:

class HasSelfPrivateNum{
    public void addI(String username){
        try {
            int num = 0;
            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) {
            e.printStackTrace();
        }
    }
}

class ThreadA extends Thread{
    private HasSelfPrivateNum numRef;

    public ThreadA(HasSelfPrivateNum numRef) {
        this.numRef = numRef;
    }

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

class ThreadB extends Thread{
    private HasSelfPrivateNum numRef;

    public ThreadB(HasSelfPrivateNum numRef) {
        this.numRef = numRef;
    }

    @Override
    public void run() {
        numRef.addI("b");
    }
}
public class Test{
    public static void main(String[] args) {
        HasSelfPrivateNum numRef = new HasSelfPrivateNum();
        ThreadA threadA = new ThreadA(numRef);
        threadA.start();
        ThreadB threadB = new ThreadB(numRef);
        threadB.start();
    }
}

output:
a set over!
b set over!
b num=200
a num=100
可以看到b的寫覆蓋對a是完全沒有影響的。

2.2實例變量非線程安全

把上面的num變量改爲實例變量
ex2:

class HasSelfPrivateNum{
    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) {
            e.printStackTrace();
        }
    }
}

class ThreadA extends Thread{
    private HasSelfPrivateNum numRef;

    public ThreadA(HasSelfPrivateNum numRef) {
        this.numRef = numRef;
    }

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

class ThreadB extends Thread{
    private HasSelfPrivateNum numRef;

    public ThreadB(HasSelfPrivateNum numRef) {
        this.numRef = numRef;
    }

    @Override
    public void run() {
        numRef.addI("b");
    }
}
public class Test{
    public static void main(String[] args) {
        HasSelfPrivateNum numRef = new HasSelfPrivateNum();
        ThreadA threadA = new ThreadA(numRef);
        threadA.start();
        ThreadB threadB = new ThreadB(numRef);
        threadB.start();
    }
}

output:
a set over!
b set over!
b num=200
a num=200
當然,解決辦法也就是在addI函數前加synchronized,這裏不再演示。

2.3給非靜態方法加對象鎖

如果同步方法屬於多個對象,則每個方法屬於不同的鎖,因此其運行也是異步的。synchronized關鍵字加到static靜態方法上是給Class類上鎖,而synchronized關鍵字加到非static靜態方法上是給對象上鎖。
ex3:

class HasSelfPrivateNum{
    private int num = 0;
    synchronized 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) {
            e.printStackTrace();
        }
    }
}

class ThreadA extends Thread{
    private HasSelfPrivateNum numRef;

    public ThreadA(HasSelfPrivateNum numRef) {
        this.numRef = numRef;
    }

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

class ThreadB extends Thread{
    private HasSelfPrivateNum numRef;

    public ThreadB(HasSelfPrivateNum numRef) {
        this.numRef = numRef;
    }

    @Override
    public void run() {
        numRef.addI("b");
    }
}
public class Test{
    public static void main(String[] args) {
        HasSelfPrivateNum numRefA = new HasSelfPrivateNum();
        HasSelfPrivateNum numRefB = new HasSelfPrivateNum();
        ThreadA threadA = new ThreadA(numRefA);
        threadA.start();
        ThreadB threadB = new ThreadB(numRefB);
        threadB.start();
    }
}

output:
a set over!
b set over!
b num=200
a num=100
可見,addI被兩個線程調用運行時都是異步交叉運行的。

2.4synchronized鎖可重入

也就是說,當一個線程得到一個對象鎖後,再次請求此對象時是可以再次得到該對象的鎖的。因此,一個synchronized方法/塊的內部調用本類的其他synchronized方法/塊時,是永遠可以得到鎖的。
ex4:

class Service{
    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");
    }
}

class MyThread extends Thread{
    @Override
    public void run() {
        Service service = new Service();
        service.service1();
    }
}

public class Test{
    public static void main(String[] args) {
        MyThread myThread = new MyThread();
        myThread.start();
    }
}

output:
service1
service2
service3
假如不允許鎖重入,那麼程序將會進入死鎖。
除此之外,子類的同步函數也可以重入從父類繼承的函數。如下:
ex5:

class Main {
    public int i = 10;

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


class Sub extends Main {
    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();
        }
    }
}

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

public class Test {
    public static void main(String[] args) {
        MyThread myThread = new MyThread();
        myThread.start();
    }
}

2.4異常自動釋放鎖

線程即使獲得了鎖,但在運行的過程中如果遇到異常就會自動釋放鎖。如下:
ex6:

class Service{
    synchronized public void testMethod(){
        if(Thread.currentThread().getName().equals("a")){
            System.out.println("ThreadName = "+Thread.currentThread().getName()+" run beginTime="+ System.currentTimeMillis());
            int i = 1;
            while (i==1){
                if((""+Math.random()).substring(0,8).equals("0.123456")){
                    System.out.println("ThreadName="+Thread.currentThread().getName()+" run exceptionTime="+System.currentTimeMillis());
                    Integer.parseInt("a");
                }
            }
        }else {
            System.out.println("Thread B run Time="+System.currentTimeMillis());
        }
    }
}

class ThreadA extends Thread{
    private Service service;

    public ThreadA(Service service) {
        this.service = service;
    }

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

class ThreadB extends Thread{
    private Service service;

    public ThreadB(Service service) {
        this.service = service;
    }

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

public class Test {
    public static void main(String[] args) {
        try {
            Service service = new Service();
            ThreadA a = new ThreadA(service);
            a.setName("a");
            a.start();
            Thread.sleep(500);
            ThreadB b = new ThreadB(service);
            b.setName("b");
            b.start();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}

output:
ThreadName = a run beginTime=1591535415742
ThreadName=a run exceptionTime=1591535416623
Exception in thread “a” Thread B run Time=1591535416624
java.lang.NumberFormatException: For input string: “a”
at java.base/java.lang.NumberFormatException.forInputString(NumberFormatException.java:65)
at java.base/java.lang.Integer.parseInt(Integer.java:652)
at java.base/java.lang.Integer.parseInt(Integer.java:770)
at Service.testMethod(Test.java:9)
at ThreadA.run(Test.java:27)
可以看到,在線程a最先獲得了對象鎖,但因爲異常而釋放鎖,由線程b獲得。

3.synchronized同步代碼塊

當同步方法執行耗時很長時,其他線程都要等待其運行完畢,因此總體的運行時間將會很長。這個時候就要同步代碼塊來解決了。同步代碼塊是作用於一個語句塊而不是整個方法,同步代碼塊提供多線程間的互斥訪問。
**synchronized(this)**以當前對象爲對象監視器。
**synchronized(非this)**當一個類中有很多synchronized方法時,雖然能實現同步,但會受阻塞而影響運行效率。使用同步代碼塊非this對象不與其他鎖this同步方法爭搶this鎖,可大大提高運行效率。
synchronized(非this)的三個結論
1).多個線程同時執行synchronized(x){}同步代碼塊呈同步效果。
2)當其他線程執行x對象中的synchronized同步代碼塊呈同步效果。
3)當其他線程執行x對象方法裏面的synchronized(this)代碼塊時也呈同步效果。
多線程死鎖
當線程相互等待對方釋放鎖時就會產生死鎖。
ex7:

class DealThread implements Runnable{
    public String username;
    public Object lock1 = new Object();
    public Object lock2 = new Object();
    public void setFlag(String username){
        this.username = username;
    }
    @Override
    public void run() {
        if(username.equals("a")){
            synchronized (lock1){
                try{
                    System.out.println("username = "+username);
                    Thread.sleep(3000);
                }catch (InterruptedException e){
                    e.printStackTrace();
                }

                synchronized (lock2){
                    System.out.println("按lock1->lock2代碼順序執行了");
                }
            }
        }
        if(username.equals("b")){
            synchronized (lock2){
                try {
                    System.out.println("username = "+username);
                    Thread.sleep(3000);
                }catch (InterruptedException e){
                    e.printStackTrace();
                }
                synchronized (lock1){
                    System.out.println("按lock2->lock1代碼順序執行了");
                }
            }
        }
    }
}

public class Test {
    public static void main(String[] args) {
        try {
            DealThread t1 = new DealThread();
            t1.setFlag("a");
            Thread thread1 = new Thread(t1);
            thread1.start();
            Thread.sleep(100);
            t1.setFlag("b");
            Thread thread2 = new Thread(t1);
            thread2.start();
        }catch (InterruptedException e){
            e.printStackTrace();
        }
    }
}

output:
username = a
username = b
可以看到此時程序進入了死鎖狀態。

4.volatile

volatile關鍵字是爲了強制編譯器從主內存中訪問數據,其與synchronized的區別主要如下:
1)從性能來看volatitle更好。
2)從修飾的對象來看,volatile只能修飾於變量,而synchronized可以修飾方法以及代碼塊。
3)多線程訪問volatile不會發生阻塞,而synchronized會,也即volatile不支持原子性。

5.總結

本文對synchronized和volatile關鍵字都做了講解,volatile演示部分較少,但因爲其功能簡單所以略去。學過操作系統我們都知道,線程之間不僅有競爭,還有同步,那麼下一篇文章講介紹線程間的通信。
在這裏插入圖片描述

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章