Java中多線程同步

Java使用多線程編程帶來的問題就是,多線程同時讀寫共享變量,會出現數據不一致的問題。

對於語句:

n = n + 1;

對變量的賦值操作,實際上對應三條指令:

ILOAD  // 從內存中取出變量值
IADD   // 對其加1操作
ISTORE //放入變量對應的內存地址

由於多線程的併發執行,線程2從內存中取出的值,很可能並不是線程1放入後的值。因此需要一種機制,保證線程執行這三條指令的時候,不會有其他線程干擾。待操作完成後,再將“特權”交給其他線程。

1、使用 synchronized 關鍵字

synchronized(Counter.lock) { // 獲取鎖
    ...
} // 釋放鎖

 synchronized 使用一個對象作爲鎖,多個線程在執行 synchronized 下的代碼時,只有獲得鎖之後,才能繼續運行。以此來保證對共享變量的有序訪問。示例如下:

public class ThreadLock {
    public static void main(String[] args) throws InterruptedException {
        var ts = new Thread[] { new AddStudentThread(), new DesStudentThread(), new AddTeacherThread(), new DesTeacherThread() };
        for (Thread t : ts) {
            t.start();
        }
        for (Thread t : ts) {
            t.join();
        }
        System.out.println(Counter.teacherCount);
        System.out.println(Counter.studentCount);
    }
}

class Counter {
    public static final Object lockStudent = new Object();
    public static final Object lockTeacher = new Object();
    public static int studentCount = 0;
    public static int teacherCount = 0;
}

class AddStudentThread extends Thread {
    @Override
    public void run() {
        for (int i = 0; i <= 1000; i++) {
            synchronized (Counter.lockStudent) {
                Counter.studentCount++;
            }
        }
    }
}

class DesStudentThread extends Thread {
    @Override
    public void run() {
        for (int i = 0; i <= 1000; i++) {
            synchronized (Counter.lockStudent) {
                Counter.studentCount--;
            }
        }
    }
}

class AddTeacherThread extends Thread {
    @Override
    public void run() {
        for (int i = 0; i <= 1000; i++) {
            synchronized (Counter.lockTeacher) {
                Counter.teacherCount++;
            }
        }
    }
}

class DesTeacherThread extends Thread {
    @Override
    public void run() {
        for (int i = 0; i <= 1000; i++) {
            synchronized (Counter.lockTeacher) {
                Counter.teacherCount--;
            }
        }
    }
}

對共享變量 studentCount、teacherCount 進行操作時候,使用 synchronized 進行加鎖操作,保證當前線程執行完成前,不會對共享變量訪問操作。同時對於兩個變量,使用了兩個對象作爲鎖,保證執行效率。

2、原子操作

原子操作指的是不會被操作系統線程調度機制打斷的操作。原子操作是不需要synchronized,JVM規範定義了幾種原子操作:

  • 基本類型變量賦值;(long、double 未定義,x64平臺是作爲原子操作的實現)
  • 引用類型賦值;

注意,多行賦值語句非原子操作,多線程同步需要加鎖。

3、同步方法

使用synchronized,需要指定一個鎖對象,同時加鎖的邏輯與業務邏輯代碼會混在一起,造成邏輯混亂。更好的做法是封裝加鎖的邏輯,外層代碼只負責調用,而無需考慮線程安全。

使用synchronized修飾方法,表示該方法是同步方法,使用this實例進行加鎖。設計一個線程安全的類:

class MyCounter {
    private int count = 0;
    
    public synchronized void add(int n) {
        this.count += n;
    }
    
    public synchronized void dec(int n) {
        this.count -= n;
    }
}

由於方法使用 synchronized 修飾,表示方法執行需要先獲取鎖(this)。

4、線程安全

一個類被設計爲允許多線程正確訪問,這個類就是“線程安全”的(thread-safe)。線程安全的類有:

  • StringBuffer
  • 不變類,StringIntegerLocalDate
  • 沒有成員變量的類 Math

一個類默認是非線程安全的。

5、可重入鎖

可重入鎖值得是一個線程重複獲取同一個鎖。java支持可重入鎖。

class MyCounter {
    private int count = 0;
    
    public synchronized void add(int n) {
        if(n < 0) {
            this.dec(n);
        }
        this.count += n;
    }
    
    public synchronized void dec(int n) {
        this.count -= n;
    }
}

在add方法中,調用dec方法,由於兩個方法均使用synchronized修飾,因此在add方法內部執行dec方法,需要再次獲得鎖。

6、死鎖

死鎖指的是兩個線程各持有對方鎖,且雙方均等待對方手中的鎖,造成無限期等待下去。死鎖發生後只能強制結束JVM進程

一個很好理解的例子:假設有兩扇門,門上兩把鎖,鑰匙A和B。甲使用鑰匙A、B可進門,乙也可以使用鑰匙A和B進門。但兩人同時開門,會因缺少對方手裏的鑰匙而相互僵持。這就是死鎖。

class Door {
    private Object lockA = new Object();
    private Object lockB = new Object();
    public void enter() {
        synchronized(lockA) {
            synchronized(lockB) {
                //
            }
        }
    }
    
    public void goin() {
        synchronized(lockB) {
            synchronized(lockA) {
                //
            }
        }
    }
}

當 enter方法和goin方法同時執行,在各自獲得鎖之後,又會各自等待對方手中的鎖,造成無限等待。解決的方法與上述開門解鎖的道理相同,要麼讓甲先進門,要麼讓乙先進。因此設置鎖的順序很重要。

當獲取A、B鎖的順序一致時,任何一方使用A鎖時,另一方必須等待。不存在各自持有A、B鎖的情況。

 

 

參考鏈接:

https://www.liaoxuefeng.com/wiki/1252599548343744/1306580888846370

 

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