溫馨提示×

溫馨提示×

您好,登錄后才能下訂單哦!

密碼登錄×
登錄注冊×
其他方式登錄
點擊 登錄注冊 即表示同意《億速云用戶服務條款》

Java并發(fā)編程(03):多線程并發(fā)訪問,同步控制

發(fā)布時間:2020-08-04 21:33:28 來源:ITPUB博客 閱讀:214 作者:知了一笑 欄目:編程語言

本文源碼: GitHub·點這里 || GitEE·點這里

一、并發(fā)問題

多線程學習的時候,要面對的第一個復雜問題就是,并發(fā)模式下變量的訪問,如果不理清楚內(nèi)在流程和原因,經(jīng)常會出現(xiàn)這樣一個問題:線程處理后的變量值不是自己想要的,可能還會一臉懵的說:這不合邏輯吧?

1、成員變量訪問

多個線程訪問類的成員變量,可能會帶來各種問題。

public class AccessVar01 {
    public static void main(String[] args) {
        Var01Test var01Test = new Var01Test() ;
        VarThread01A varThread01A = new VarThread01A(var01Test) ;
        varThread01A.start();
        VarThread01B varThread01B = new VarThread01B(var01Test) ;
        varThread01B.start();
    }
}
class VarThread01A extends Thread {
    Var01Test var01Test = new Var01Test() ;
    public VarThread01A (Var01Test var01Test){
        this.var01Test = var01Test ;
    }
    @Override
    public void run() {
        var01Test.addNum(50);
    }
}
class VarThread01B extends Thread {
    Var01Test var01Test = new Var01Test() ;
    public VarThread01B (Var01Test var01Test){
        this.var01Test = var01Test ;
    }
    @Override
    public void run() {
        var01Test.addNum(10);
    }
}
class Var01Test {
    private Integer num = 0 ;
    public void addNum (Integer var){
        try {
            if (var == 50){
                num = num + 50 ;
                Thread.sleep(3000);
            } else {
                num = num + var ;
            }
            System.out.println("var="+var+";num="+num);
        } catch (Exception e){
            e.printStackTrace();
        }
    }
}

這里案例的流程就是并發(fā)下運算一個成員變量,程序的本意是:var=50,得到num=50,可輸出的實際結(jié)果是:

var=10;num=60
var=50;num=60

VarThread01A線程處理中進入休眠,休眠時num已經(jīng)被線程VarThread01B進行一次加10的運算,這就是多線程并發(fā)訪問導致的結(jié)果。

2、方法私有變量

修改上述的代碼邏輯,把num變量置于方法內(nèi),作為私有的方法變量。

class Var01Test {
    // private Integer num = 0 ;
    public void addNum (Integer var){
        Integer num = 0 ;
        try {
            if (var == 50){
                num = num + 50 ;
                Thread.sleep(3000);
            } else {
                num = num + var ;
            }
            System.out.println("var="+var+";num="+num);
        } catch (Exception e){
            e.printStackTrace();
        }
    }
}

方法內(nèi)部的變量是私有的,且和當前執(zhí)行方法的線程綁定,不會存在線程間干擾問題。

二、同步控制

1、Synchronized關鍵字

使用方式:修飾方法,或者以控制同步塊的形式,保證多個線程并發(fā)下,同一時刻只有一個線程進入方法中,或者同步代碼塊中,從而使線程安全的訪問和處理變量。如果修飾的是靜態(tài)方法,作用的是這個類的所有對象。

獨占鎖屬于悲觀鎖一類,synchronized就是一種獨占鎖,假設處于最壞的情況,只有一個線程執(zhí)行,阻塞其他線程,如果并發(fā)高,處理耗時長,會導致多個線程掛起,等待持有鎖的線程釋放鎖。

2、修飾方法

這個案例和第一個案例原理上是一樣的,不過這里雖然在修改值的地方加入的同步控制,但是又挖了一個坑,在讀取的時候沒有限制,這個現(xiàn)象俗稱臟讀。

public class AccessVar02 {
    public static void main(String[] args) {
        Var02Test var02Test = new Var02Test ();
        VarThread02A varThread02A = new VarThread02A(var02Test) ;
        varThread02A.start();
        VarThread02B varThread02B = new VarThread02B(var02Test) ;
        varThread02B.start();
        var02Test.readValue();
    }
}
class VarThread02A extends Thread {
    Var02Test var02Test = new Var02Test ();
    public VarThread02A (Var02Test var02Test){
        this.var02Test = var02Test ;
    }
    @Override
    public void run() {
        var02Test.change("my","name");
    }
}
class VarThread02B extends Thread {
    Var02Test var02Test = new Var02Test ();
    public VarThread02B (Var02Test var02Test){
        this.var02Test = var02Test ;
    }
    @Override
    public void run() {
        var02Test.change("you","age");
    }
}
class Var02Test {
    public String key = "cicada" ;
    public String value = "smile" ;
    public synchronized void change (String key,String value){
        try {
            this.key = key ;
            Thread.sleep(2000);
            this.value = value ;
            System.out.println("key="+key+";value="+value);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
    public void readValue (){
        System.out.println("讀?。簁ey="+key+";value="+value);
    }
}

在線程中,邏輯上已經(jīng)修改了,只是沒執(zhí)行到,但是在main線程中讀取的value毫無意義,需要在讀取方法上也加入同步的線程控制。

3、同步控制邏輯

同步控制實現(xiàn)是基于Object的監(jiān)視器。

  • 線程對Object的訪問,首先要先獲得Object的監(jiān)視器 ;
  • 如果獲取成功,則會獨占該對象 ;
  • 其他線程會掉進同步隊列,線程狀態(tài)變?yōu)樽枞?;
  • 等Object的持有線程釋放鎖,會喚醒隊列中等待的線程,嘗試重啟獲取對象監(jiān)視器;

4、修飾代碼塊

說明一點,代碼塊包含方法中的全部邏輯,鎖定的粒度和修飾方法是一樣的,就寫在方法上吧。同步代碼塊一個很核心的目的,減小鎖定資源的粒度,就如同表鎖和行級鎖。

public class AccessVar03 {
    public static void main(String[] args) {
        Var03Test var03Test1 = new Var03Test() ;
        Thread thread1 = new Thread(var03Test1) ;
        thread1.start();
        Thread thread2 = new Thread(var03Test1) ;
        thread2.start();
        Thread thread3 = new Thread(var03Test1) ;
        thread3.start();
    }
}
class Var03Test implements Runnable {
    private Integer count = 0 ;
    public void countAdd() {
        try {
            Thread.sleep(1000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        synchronized(this) {
            count++ ;
            System.out.println("count="+count);
        }
    }
    @Override
    public void run() {
        countAdd() ;
    }
}

這里就是鎖定count處理這個動作的核心代碼邏輯,不允許并發(fā)處理。

5、修飾靜態(tài)方法

靜態(tài)方法屬于類層級的方法,對象是不可以直接調(diào)用的。但是synchronized修飾的靜態(tài)方法鎖定的是這個類的所有對象。

public class AccessVar04 {
    public static void main(String[] args) {
        Var04Test var04Test1 = new Var04Test() ;
        Thread thread1 = new Thread(var04Test1) ;
        thread1.start();
        Var04Test var04Test2 = new Var04Test() ;
        Thread thread2 = new Thread(var04Test2) ;
        thread2.start();
    }
}
class Var04Test implements Runnable {
    private static Integer count ;
    public Var04Test (){
        count = 0 ;
    }
    public synchronized static void countAdd() {
        System.out.println(Thread.currentThread().getName()+";count="+(count++));
    }
    @Override
    public void run() {
        countAdd() ;
    }
}

如果不是使用同步控制,從邏輯和感覺上,輸出的結(jié)果應該如下:

Thread-0;count=0
Thread-1;count=0

加入同步控制之后,實際測試輸出結(jié)果:

Thread-0;count=0
Thread-1;count=1

6、注意事項

  • 繼承中子類覆蓋父類方法,synchronized關鍵字特性不能繼承傳遞,必須顯式聲明;
  • 構(gòu)造方法上不能使用synchronized關鍵字,構(gòu)造方法中支持同步代碼塊;
  • 接口中方法,抽象方法也不支持synchronized關鍵字 ;

三、Volatile關鍵字

1、基本描述

Java內(nèi)存模型中,為了提升性能,線程會在自己的工作內(nèi)存中拷貝要訪問的變量的副本。這樣就會出現(xiàn)同一個變量在某個時刻,在一個線程的環(huán)境中的值可能與另外一個線程環(huán)境中的值,出現(xiàn)不一致的情況。

使用volatile修飾成員變量,不能修飾方法,即標識該線程在訪問這個變量時需要從共享內(nèi)存中獲取,對該變量的修改,也需要同步刷新到共享內(nèi)存中,保證了變量對所有線程的可見性。

2、使用案例

class Var05Test {
    private volatile boolean myFlag = true ;
    public void setFlag (boolean myFlag){
        this.myFlag = myFlag ;
    }
    public void method() {
        while (myFlag){
            try {
                System.out.println(Thread.currentThread().getName()+myFlag);
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
}

3、注意事項

  • 可見性只能確保每次讀取的是最新的值,但不支持變量操作的原子性;
  • volatile并不會阻塞線程方法,但是同步控制會阻塞;
  • Java同步控制的根本:保證并發(fā)下資源的原子性和可見性;

四、源代碼地址

GitHub·地址
https://github.com/cicadasmile/java-base-parent
GitEE·地址
https://gitee.com/cicadasmile/java-base-parent
向AI問一下細節(jié)

免責聲明:本站發(fā)布的內(nèi)容(圖片、視頻和文字)以原創(chuàng)、轉(zhuǎn)載和分享為主,文章觀點不代表本網(wǎng)站立場,如果涉及侵權(quán)請聯(lián)系站長郵箱:is@yisu.com進行舉報,并提供相關證據(jù),一經(jīng)查實,將立刻刪除涉嫌侵權(quán)內(nèi)容。

AI