Java多線程(二)線程同步

本文目錄
線程同步簡介
  需求
  原子操作
  synchronized簡介
  JVM原子操作
  總結
synchronized使用詳解
  加鎖對象
  讀取方法
  Thread-safe
  總結
死鎖
  死鎖現象
  死鎖形成條件
  避免死鎖
wait/notify
  生產者-消費者

線程同步簡介

需求

多個線程同時運行,線程調度由操作系統決定,程序本身無法決定.

當多個線程同時讀寫同一個共享變量時,就會出現變量的值不準確的現象。

class Counter {
  	public static int count = 0;
}
class Addthread extends Thread {
  	@Override
  	public void run {
      	for(int i=0;i<10000;i++) {
          	Counter.count += 1;
        }
    }
}
class DecThread extends Thread {
  	@Override
  	public void run {
      	for(int i=0;i<10000;i++) {
          	Counter.count -= 1;
        }
    }
}
public class Main {
  	public static void main(String[] args) throws Exception {
      	Thread t1 = new AddThread();
      	Thread t2 = new DecThread();
      	t1.start();
      	t2.start();
      	t1.join();
      	t2.join();
      	// 多運行幾次會發現,count的值不一定是0
      	System.out.println(Counter.count);
    }
}
原子操作
  • 對共享變量進行寫入時,必須保證是原子操作
  • 原子操作是指不能被中斷的一個或一系列操作

爲了保證一系列操作爲原子操作:

  • 必須保證一系列操作執行過程中不被其他線程執行。因此可以對操作進行加鎖和解鎖。
  • Java使用synchronized對一個對象進行加鎖
synchronized(lock) {
  	n = n + 1;
}
synchronized簡介
特點
  • 性能低:同步代碼塊會消耗資源
  • 不用擔心異常:即使有異常,同步代碼塊也能釋放鎖
基本使用
  • 找出修改共享變量的線程代碼塊
  • 選擇一個實例作爲鎖
  • 使用synchronized(lock){}

對上例進行修改

class Counter {
  	public static int count = 0;
}
class Addthread extends Thread {
  	@Override
  	public void run {
      	for(int i=0;i<10000;i++) {
          	// 加鎖
          	synchronized(Main.LOCK) {
              	Counter.count += 1;
            }
        }
    }
}
class DecThread extends Thread {
  	@Override
  	public void run {
      	for(int i=0;i<10000;i++) {
          	// 加鎖
          	synchronized(Main.LOCK) {
              	Counter.count -= 1;
            }
        }
    }
}
public class Main {
  	// 定義一個鎖
  	public static final Object LOCK = new Object();
  	public static void main(String[] args) throws Exception {
      	Thread t1 = new AddThread();
      	Thread t2 = new DecThread();
      	t1.start();
      	t2.start();
      	t1.join();
      	t2.join();
      	// 多運行幾次會發現,count的值始終是0
      	System.out.println(Counter.count);
    }
}
JVM原子操作
類型
  • 基本類型賦值(long和double除外)
int n = 1;
  • 引用類型賦值
List<String> list = aList;

對原子操作不需要進行同步,如果多個操作需要同步,也可以轉換成原子操作,如下:

class Pair {
  	int first;
    int last;
    public void set(int first, int last) {
				// 使用同步
      	synchronized(lock) {
            this.first = first;
         		this.last = last;
        }
    }
}

// 不使用同步
class Pair {
  	intp[] pair;
  	public void set(int first, int last) {
      	int[] ps = new int[]{first, last};
      	this.pair = ps;
    }
}
總結

多線程同時修改同一個變量,會造成邏輯錯誤:

  • 需要通過synchronized同步
  • 同步的本質就是給指定對象加鎖
  • 注意加鎖對象必須是同一個實例
  • 對JVM定義的單個原子操作不需要同步

synchronized使用詳解

加鎖對象

加鎖對象的選擇:把同步邏輯封裝到到持有數據的實例中,使用this加鎖

synchronized可以用在代碼塊上,也可以用在方法上,用在方法上表示對整個方法內的代碼進行加鎖

private int count = 0;
// 對方法加鎖
public synchronized void add() {
  	count += 1;
  	count -= 1;
}
// 等同於下面
public void add() {
  	// 對代碼塊使用當前對象加鎖
  	synchronized(this) {
      	count += 1;
      	count -= 1;
    }
}

如果對靜態方法進行加鎖,那麼要使用當前對象的Class實例

public class A {
  	static int count;
  	static void add(int n) {
      	// 鎖住的是當前類的Class實例
      	synchronized(A.class) {
          	count += n;
        }
    }
}
讀取方法

如果只是單純的一個原子操作進行讀取數據,那麼可以不加鎖。

public int get() {
  	// 可以進行同步
  	return this.value;
}

但是如果讀取過程較複雜存在線程安全問題,則需要進行加鎖。

public synchronized int[] get() {
  	int[] result = new int[2];
  	result[0] = this.value[0];
  	// 如果不使用同步,在讀取this.value[0]的時候
  	// this.value[1]可能會被其他線程修改
  	result[1] = this.value[1];
  	return result;
}
Thread-safe

如果一個類被設計爲允許多線程正確訪問的,那這個類就是線程安全的(thread-safe),比如java.lang.StringBuffer

// StringBuffer的方法都使用了synchronized來標識
// ... ...
		@Override
    public synchronized int length() {
        return count;
    }

    @Override
    public synchronized int capacity() {
        return value.length;
    }


    @Override
    public synchronized void ensureCapacity(int minimumCapacity) {
        super.ensureCapacity(minimumCapacity);
    }

    /**
     * @since      1.5
     */
    @Override
    public synchronized void trimToSize() {
        super.trimToSize();
    }

    /**
     * @throws IndexOutOfBoundsException {@inheritDoc}
     * @see        #length()
     */
    @Override
    public synchronized void setLength(int newLength) {
        toStringCache = null;
        super.setLength(newLength);
    }
// ... ...

線程安全的類:

  • 不變的類:String,Integer,LocalDate
  • 沒有成員變量的類(多是工具類):Math
  • 正確使用synchronized的類:StringBuffer

非線程安全的類:

  • 不能在多線程中共享實例並修改:ArrayList
  • 可以在多線程中以只讀方式共享
總結
  • 用synchronized修飾方法可以把整個方法變爲同步代碼塊
  • synchronized方法加鎖對象是this
  • 通過合理的設計和數據封裝可以讓一個類變爲“線程安全”
  • 一個類沒有特殊說明,默認不是thread-safe
  • 多線程能否訪問某個非線程安全的實例,需要具體情況具體分析

死鎖

死鎖現象

要執行synchronized代碼塊,必須要先獲得指定對象的鎖才能運行。

Java的線程鎖是可重入的鎖,即獲取到一個對象的鎖的synchronized代碼塊中再次獲取這個對象的鎖

public void add(int n) {
  	synchronized(lock) {
      	this.value += n;
      	// 調用另一個加了相同鎖的方法
      	addAnother(n);
    }
}
public void addAnother(int m) {
  	// 獲取同一個鎖
  	synchronized(lock) {
      	this.another += m;
    }
}

// 上面代碼等同於
public void add(int m) {
  	synchronized(lock) {
      	this.value += m;
      	synchronized(lock) {
          	this.another += m;
        }
    }
}

也可以是兩個不同的鎖

public void add(int m) {
  	// 獲取lockA的鎖
  	synchronized(lockA) {
      	this.value += m;
      	// 獲取lockB的鎖
      	synchronized(lockB) {
          	this.another += m;
        }// 釋放lockB的鎖
    }// 釋放lockA的鎖
}

當不同線程獲取多個不同對象的鎖可能導致死鎖

public void add(int m) {
  	synchronized(lockA) {
      	this.value += m;
      	// 等待獲取lockB的鎖
      	synchronized(lockB) {
          	this.another += m;
        }
    }
}

public void add(int m) {
  	synchronized(lockB) {
      	this.value += m;
      	// 等待獲取lockA的鎖
      	synchronized(lockA) {
          	this.another += m;
        }
    }
}

當兩個線程分別執行以上代碼時,線程A獲取到lockA的鎖,線程B獲取到lockB的鎖,然後線程A開始等待獲取lockB的鎖,而線程B開始等待獲取lockA的鎖,兩個線程陷入互相等待的僵局,舊形成了死鎖

死鎖形成條件
  • 兩個線程各自持有不同的鎖
  • 兩個線程各自試圖獲取對方已持有的鎖
  • 雙方無限等待下去,導致死鎖

死鎖處理

  • 沒有任何機制能解除死鎖
  • 只能強制結束JVM進程
避免死鎖
  • 多線程獲取鎖的順序要一致

wait/notify

synchronized解決了多線程競爭的問題,但沒有解決多線程協調的問題,比較典型的生產者消費者問題,消費者必須在有產品時才能消費,如果沒有就必須等待

生產者-消費者
// 一個簡單的生產者-消費者案例
// 倉庫
class Repository {
    private LinkedList<Object> list = new LinkedList<>();
    public synchronized void produce() {
        list.add("1");
        System.out.println("生產一個,當前有" + list.size() + "個");
        // 喚醒所有等待的消費者
        this.notifyAll();
    }
    public synchronized void consume() {
        while (list.size() == 0) {
            try {
                // 消費者開始等待
                this.wait();
            } catch (InterruptedException e) {
                e.printStackTrace();
                return;
            }
        }
        list.remove();
        System.out.println("消費一個,當前有" + list.size() + "個");
    }
}
// 生產者
class Producer extends Thread {
    private Repository repository;
    public Producer(Repository repository) {
        this.repository = repository;
    }
    @Override
    public void run() {
        try {
            Thread.sleep(1000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        repository.produce();
    }
}
// 消費者
class Consumer extends Thread {
    private Repository repository;
    public Consumer(Repository repository) {
        this.repository = repository;
    }
    @Override
    public void run() {
        try {
            sleep(1000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        repository.consume();
    }
}
// 主類
public class Main {

    public static void main(String[] args) {
				// 創建公共倉庫
        Repository repository = new Repository();
      	// 創建生產者
        Producer producer1 = new Producer(repository);
        Producer producer2 = new Producer(repository);
        Producer producer3 = new Producer(repository);
				// 創建消費者
        Consumer consumer1 = new Consumer(repository);
        Consumer consumer2 = new Consumer(repository);
        Consumer consumer3 = new Consumer(repository);
				// 開始工作
        producer1.start();
        producer2.start();
        producer3.start();
        consumer1.start();
        consumer2.start();
        consumer3.start();
    }
}

// 運行結果(順序不固定)
生產一個,當前有1個
消費一個,當前有0個
生產一個,當前有1個
生產一個,當前有2個
消費一個,當前有1個
消費一個,當前有0

可以看出,如果消費者不進行等待,那麼會出現消費的產品爲空,並且線程會結束,使用wait()進行等待,當倉庫中有產品時,生產者再調用notify/notifyAll來喚醒等待的線程,這樣消費者就可以繼續消費了。

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