本文目錄
線程同步簡介
需求
原子操作
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來喚醒等待的線程,這樣消費者就可以繼續消費了。