Java 多線程 - Synchronized 和變量併發訪問

在非線程安全得狀況下,多個線程對同一個對象中得實例變量進行併發訪問時,產生得後果就是髒讀,也就是取到得數據實際上是被更改過得。java

非線程安全問題存在於"實例變量"中,若是是方法內部得私有變量,則不存在"非線程安全"的問題。安全

1 Synchronized

1.1 synchronized方法

使用synchronized修飾方法時應注意使用同一個鎖對象,不然會致使synchronized失效。bash

public class ThreadTest {
    public static void main(String[] args) {
        Add add = new Add();
        Add add1 = new Add();
        ThreadAA threadAA = new ThreadAA(add);
        threadAA.start();
        ThreadBB threadBB = new ThreadBB(add1);
        threadBB.start();
    }
}

class ThreadAA extends Thread{
    private Add a;
    public ThreadAA(Add add){
        this.a = add;
    }
    @Override
    public void run(){
        a.add("a");
    }
}

class ThreadBB extends Thread{
    private Add b;
    public ThreadBB(Add add){
        this.b = add;
    }
    @Override
    public void run(){
        b.add("b");
    }
}

class Add{
    private int num = 0;
    //同步方法
    synchronized public void add(String username){
        try{
            if (username.equals("a")){
                num = 100;
                System.out.println("add a end");
                Thread.sleep(2000);
            }else {
                num = 200;
                System.out.println("add b end");
            }
            System.out.println(username + " name " + num);
        }catch (Exception e){
            e.printStackTrace();
        }
    }
}
複製代碼

打印結果多線程

add a end
add b end
b name 200
a name 100
複製代碼

從結果看出打印的順序不是同步的,而是交叉的,這是由於關鍵字synchronized取得的鎖都是對象鎖。因此上面的示例中,那個線程先執行帶synchronized關鍵字的方法,那個線程就持有該方法所屬對象的鎖,那麼其餘線程只能呈等待狀態,前提是多個線程訪問的是同一個對象。併發

驗證synchronized方法持有的鎖爲對象鎖異步

//將上面的ThreadTest類中的main方法進行修改
public class ThreadTest {
    public static void main(String[] args) {
        Add add = new Add();
//        Add add1 = new Add();
        ThreadAA threadAA = new ThreadAA(add);
        threadAA.start();
        ThreadBB threadBB = new ThreadBB(add);
        threadBB.start();
    }
}
複製代碼

運行結果jvm

add a end
a name 100
add b end
b name 200
複製代碼

此時看多的運行結果就是順序打印的。ide

1.2 synchronized同步代碼塊

上面講了同步方法,可是用synchronized聲明方法在某些狀況下是有弊端的,好比A線程調用同步方法執行一個長時間的任務,那麼其餘線程必須等待較長的時間。在這樣的狀況下,咱們可使用synchronized同步代碼塊來解決,使用synchronized同步代碼塊來包裹必需要同步執行的代碼部分。性能

public class ThreadFunction {
    public static void main(String[] args) {
        ObjFunction objFunction = new ObjFunction();
        FunA funA = new FunA(objFunction);
        funA.setName("a");
        funA.start();
        FunB funB = new FunB(objFunction);
        funB.setName("b");
        funB.start();

    }
}

class FunB extends Thread{
    private ObjFunction objFunction;
    public FunB(ObjFunction objFunction){
        this.objFunction = objFunction;
    }
    @Override
    public void run(){
        objFunction.objMethod();
    }
}

class FunA extends Thread{
    private ObjFunction objFunction;
    public FunA(ObjFunction objFunction){
        this.objFunction = objFunction;
    }
    @Override
    public void run(){
        objFunction.objMethod();
    }
}

class ObjFunction{
    public void objMethod(){
        try{
            System.out.println(Thread.currentThread().getName() + " start");
            synchronized (this) {
                System.out.println("start time = " + System.currentTimeMillis());
                Thread.sleep(2000);
                System.out.println("end time = "+ System.currentTimeMillis());
            }
            System.out.println(Thread.currentThread().getName() + " end");
        }catch (Exception e){
            e.printStackTrace();
        }
    }
}
複製代碼

運行結果ui

a start
b start
start time = 1559033466082
end time = 1559033468083
a end
start time = 1559033468083
end time = 1559033470084
b end
複製代碼

能夠看出,同步代碼塊外的代碼是異步執行的,而同步代碼塊中的則是同步執行的。而且synchronized(this)的鎖對象也是當前對象。

除了以this來做爲鎖對象,java還支持任意對象做爲鎖來實現同步功能,但須要注意的是做爲同步監視器的必須是同一對象,不然運行結果就是異步調用了。

1.3 synchronized靜態同步方法

關鍵字synchronized還能夠應用到static靜態方法上,這樣的話就是一當前的*.java文件對應的Class類做爲鎖對象。

靜態同步方法持有的鎖對象=synchronized(class)

public class ThreadTest {
    public static void main(String[] args) {
        ThreadAA threadAA = new ThreadAA();
        threadAA.start();
        ThreadBB threadBB = new ThreadBB();
        threadBB.start();
    }
}

class ThreadAA extends Thread{
    @Override
    public void run(){
        Add.add("a");
    }
}

class ThreadBB extends Thread{
    @Override
    public void run(){
        Add.add("b");
    }
}

class Add{
    private static int num = 0;
    //同步方法
    synchronized static public void add(String username){
        try{
            if (username.equals("a")){
                num = 100;
                System.out.println("add a end");
                Thread.sleep(2000);
            }else {
                num = 200;
                System.out.println("add b end");
            }
            System.out.println(username + " name " + num);
        }catch (Exception e){
            e.printStackTrace();
        }
    }
}
複製代碼

運行結果

add a end
a name 100
add b end
b name 200
複製代碼

1.4 synchronized類

使用關鍵字synchronized修飾一個類,那麼這個類中全部的方法都是同步方法,在編譯得時候會把全部方法自動加上synchronized。

class ClassName {
   public void method() {
      synchronized(ClassName.class) {
         // todo
      }
   }
}
複製代碼

1.5 synchronized鎖重入

synchronized擁有鎖重入的功能,也就是在使用synchronized時,當一個線程獲得一個對象鎖後,再次請求此對象鎖時是能夠再次獲得該對象的鎖的。也就是說synchronized方法/代碼塊的內部調用本類的其餘synchronized方法/代碼塊時,永遠能夠獲得所。

public class ThreadAgain {
    public static void main(String[] args) {
        new Thread(new Runnable() {
            @Override
            public void run() {
                new Service().service1();
            }
        }).start();
    }
}

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

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

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

}
複製代碼

運行結果

service1
service2
service3
複製代碼

2 volatile

關鍵字volatile的做用主要是使變量在多個線程間可見。

是強制從公共堆中取得變量的值,而不是從線程的私有數據棧中取得變量的值。在多線程中,棧與程序計數器是私有的,堆與全局變量是公有的。

先看代碼

public class MyVolatile {
    public static void main(String[] args) {
        try {
            RunThread runThread = new RunThread();
            runThread.start();
            Thread.sleep(2000);
            runThread.setRun(false);
            System.out.println("爲runThread複製false");
        }catch (Exception e){
            e.printStackTrace();
        }
    }

}

class RunThread extends Thread{
    private boolean isRun = true;

    public boolean isRun() {
        return isRun;
    }

    public void setRun(boolean run) {
        isRun = run;
    }

    @Override
    public void run(){
        System.out.println("進入了run方法");
        while (isRun == true){
        }
        System.out.println("退出run方法,線程中止");
    }
}
複製代碼

從控制檯能夠看到,線程並無結束。這個問題就是私有堆棧中的值和工有堆棧中的值不一樣步形成的,想解決這樣的問題使用volatile關鍵字就能夠。

修改RunThread類中的代碼

class RunThread extends Thread{
   volatile private boolean isRun = true;

    public boolean isRun() {
        return isRun;
    }

    public void setRun(boolean run) {
        isRun = run;
    }

    @Override
    public void run(){
        System.out.println("進入了run方法");
        while (isRun == true){
        }
        System.out.println("退出run方法,線程中止");
    }
}
複製代碼

再次運行,線程正常結束了。

雖然volatile關鍵字可使實例變量在多線程之間可見,可是volatile有一個致命的缺點就是不支持原子性。

驗證volatile不支持原子性

public class IsAtomic {
    public static void main(String[] args) {
        MyAtomicRun[] myAtomicRuns = new MyAtomicRun[100];
        for (int i = 0;i<100;i++){
            myAtomicRuns[i] = new MyAtomicRun();
        }
        for (int i = 0;i<100;i++){
            myAtomicRuns[i].start();
        }
    }
}

class MyAtomicRun extends Thread{
    volatile public static int count;
    private static void count(){
        for (int i = 0;i<100;i++){
            count++;
        }
        System.out.println("count: " + count);
    }
    @Override
    public void run(){
        count();
    }
}
複製代碼

打印輸出

//篇幅較長,沒有所有粘貼
count: 5000
count: 4900
count: 4800
count: 4700
count: 4600
count: 4500
count: 4400
count: 4400
複製代碼

從輸出的結果看,並無輸出咱們理想狀態中的10000。

對代碼進行改進

class MyAtomicRun extends Thread{
    volatile public static int count;
    //須要使用同步靜態方法,這樣是以class爲鎖,才能達到同步效果
    synchronized private static void count(){
        for (int i = 0;i<100;i++){
            count++;
        }
        System.out.println("count: " + count);
    }
    @Override
    public void run(){
        count();
    }
}
複製代碼

打印輸出

count: 9300
count: 9400
count: 9500
count: 9600
count: 9700
count: 9800
count: 9900
count: 10000
複製代碼

這一次輸出的纔是正確的結果。

關鍵字volatile主要使用的場合是在多個線程中能夠感知實例變量被更改了,而且能夠獲取最新的值使用,也就是多線程讀取共享變量時能夠獲取最新的值。

2.1 volatile與synchronized的比較

  1. volatile時線程同步的輕量級實現,因此性能確定比synchronized要好,而且volatile只能修飾變量,而synchronized能夠修飾方法,以及代碼塊。
  2. 多線程訪問時volatile不會發生阻塞,而synchronized會出現阻塞。
  3. volatile能保證數據的可見性,但不能保證原子性;而synchronized既能夠保證原子性,也能夠間接保證可見性,由於synchronized是將私有內存和公共內存中的數據同步。
  4. volatile解決的是變量在多個線程之間的可見性;而synchronized解決的是多個線程之間訪問資源的同步性。

2.2 變量在內存中的工做

像上面volatile關鍵字修飾的變量進行++運算這樣的操做其實並非一個原子操做,也就是非線程安全的。

i++操做步驟:

  1. 從內存中取出i的值
  2. 計算i
  3. 將i寫入內存

若是在第二步計算的時候另外一個線程也修改了i的值,那麼這個時候就會出現髒數據。

img

  • read和load階段:從主內存複製變量到當前線程的工做內存;
  • use和assign階段:執行代碼,改變共享變量值;
  • store和write階段:用工做內存數據刷新主內存對應的變量值。

在多線程環境中,use和assign是屢次出現的,但這個操做並非原子性的,也就是讀取階段後,若是主內存中的變量值被修改,工做線程的內存由於已經加載過了,因此不會產生對應的變化,就形成了私有內存和公有內存中變量值不一樣步,計算出來的結果和預期就不同,出現非線程安全問題。

對於volatile關鍵字修飾的變量,jvm只保證從主內存加載到工做內存中的值是最新的。

相關文章
相關標籤/搜索