二、Java併發編程:Java併發機制的底層原理

一、線程安全問題

1. 一個典型的線程不安全的例子

  1. 多個線程同時操作同一份資源的(主要是進行讀寫操作)時候,就有可能會發生線程安全問題;比如兩個人同時對同一個賬戶進行取款操作的時候,就有可能會出現餘額爲負數的結果。
  2. 示例:兩個人同時操作一個賬戶
package concurrency.account;

/**
 * 賬戶類,主要記錄賬戶餘額,以及提供取款方法
 * @author lt
 * @date 2018年7月2日
 * @version v1.0
 */
public class Account {
	private String accountNo;
	private double balance;
	public Account(String accountNo, double balance){
		this.accountNo = accountNo;
		this.balance = balance;
	}
	public String getAccountNo() {
		return accountNo;
	}
	public void setAccountNo(String accountNo) {
		this.accountNo = accountNo;
	}
	//賬戶餘額不允許隨便修改,故只提供get方法
	public double getBalance() {
		return balance;
	}
	public void draw(double drawAmount){
		//取錢數不能超過餘額數
		if(balance>=drawAmount){
			System.out.println(Thread.currentThread().getName()+"取錢成功!吐出鈔票:"+drawAmount);
			try {
				Thread.sleep(1);
			} catch (InterruptedException e) {
				e.printStackTrace();
			}
			//修改餘額
			balance -= drawAmount;
			System.out.println("\t餘額爲:"+balance);
		} else {
			System.out.println("餘額不足!取錢失敗!");
		}
	}
	@Override
	public int hashCode() {
		final int prime = 31;
		int result = 1;
		result = prime * result
				+ ((accountNo == null) ? 0 : accountNo.hashCode());
		long temp;
		temp = Double.doubleToLongBits(balance);
		result = prime * result + (int) (temp ^ (temp >>> 32));
		return result;
	}
	@Override
	public boolean equals(Object obj) {
		if (this == obj)
			return true;
		if (obj == null)
			return false;
		if (getClass() != obj.getClass())
			return false;
		Account other = (Account) obj;
		if (accountNo == null) {
			if (other.accountNo != null)
				return false;
		} else if (!accountNo.equals(other.accountNo))
			return false;
		if (Double.doubleToLongBits(balance) != Double
				.doubleToLongBits(other.balance))
			return false;
		return true;
	}
}

package concurrency.account;
/**
 * 取款操作的線程,繼承Thread類
 * @author lt
 * @date 2018年7月2日
 * @version v1.0
 */
public class DrawThread extends Thread{

	private Account account;
	private double drawAmount;
	public DrawThread(String name, Account account, double drawAmount){
		super(name);
		this.account = account;
		this.drawAmount = drawAmount;
	}
	public void run(){
		account.draw(drawAmount);
	}
}

package concurrency.account;
/**
 * 測試類測試兩個人同時操作同一個賬戶(取同一個賬戶的錢)
 * @author lt
 * @date 2018年7月2日
 * @version v1.0
 */
public class DrawTest {

	public static void main(String[] args) {
		for(int i=0; i<10; i++){
			Account account = new Account("0001", 1000);
			new DrawThread("甲", account, 800).start();
			new DrawThread("乙", account, 800).start();
		}
	}
}

/**
 * 輸出結果
 */
乙取錢成功!吐出鈔票:800.0
甲取錢成功!吐出鈔票:800.0
	餘額爲:200.0
	餘額爲:-600.0

2. 解決方案:synchronized,lock

  1. synchronized修飾代碼塊
package concurrency.account;

/**
 * 線程同步:修飾代碼塊
 * @author lt
 * @date 2018年7月2日
 * @version v1.0
 */
public class Account {
	private String accountNo;
	private double balance;
	public Account(String accountNo, double balance){
		this.accountNo = accountNo;
		this.balance = balance;
	}
	public String getAccountNo() {
		return accountNo;
	}
	public void setAccountNo(String accountNo) {
		this.accountNo = accountNo;
	}
	//賬戶餘額不允許隨便修改,故只提供get方法
	public double getBalance() {
		return balance;
	}
	public void draw(double drawAmount){
		/**
		 * 一、synchronized加鎖機制
		 * 1.synchronized關鍵字修飾代碼塊或者方法,同步監視器爲this;
		 * 2.任何時刻,只能有一個線程獲得同步監視器的鎖,進而對資源進行操作;
		 * 二、synchronized釋放鎖
		 * 1.代碼塊正常終止或拋出異常;
		 * 2.調用同步監視器的wait()方法;
		 */
		synchronized(this){
			//取錢數不能超過餘額數
			if(balance>=drawAmount){
				System.out.println(Thread.currentThread().getName()+"取錢成功!吐出鈔票:"+drawAmount);
				try {
					Thread.sleep(1);
				} catch (InterruptedException e) {
					e.printStackTrace();
				}
				//修改餘額
				balance -= drawAmount;
				System.out.println("\t餘額爲:"+balance);
			} else {
				System.out.println("餘額不足!取錢失敗!");
			}
		}
	}
	@Override
	public int hashCode() {
		final int prime = 31;
		int result = 1;
		result = prime * result
				+ ((accountNo == null) ? 0 : accountNo.hashCode());
		long temp;
		temp = Double.doubleToLongBits(balance);
		result = prime * result + (int) (temp ^ (temp >>> 32));
		return result;
	}
	@Override
	public boolean equals(Object obj) {
		if (this == obj)
			return true;
		if (obj == null)
			return false;
		if (getClass() != obj.getClass())
			return false;
		Account other = (Account) obj;
		if (accountNo == null) {
			if (other.accountNo != null)
				return false;
		} else if (!accountNo.equals(other.accountNo))
			return false;
		if (Double.doubleToLongBits(balance) != Double
				.doubleToLongBits(other.balance))
			return false;
		return true;
	}
}

  1. synchronized修飾方法(不能修飾static方法)
package concurrency.account;

/**
 * 線程同步:修飾方法
 * @author lt
 * @date 2018年7月2日
 * @version v1.0
 */
public class Account {
	private String accountNo;
	private double balance;
	public Account(String accountNo, double balance){
		this.accountNo = accountNo;
		this.balance = balance;
	}
	public String getAccountNo() {
		return accountNo;
	}
	public void setAccountNo(String accountNo) {
		this.accountNo = accountNo;
	}
	//賬戶餘額不允許隨便修改,故只提供get方法
	public double getBalance() {
		return balance;
	}
	/**
	 * 一、synchronized加鎖機制
	 * 1.synchronized關鍵字修飾代碼塊或者方法,同步監視器爲this;
	 * 2.任何時刻,只能有一個線程獲得同步監視器的鎖,進而對資源進行操作;
	 * 二、synchronized釋放鎖
	 * 1.代碼塊正常終止或拋出異常;
	 * 2.調用同步監視器的wait()方法;
	 */
	public synchronized void draw(double drawAmount){
		//取錢數不能超過餘額數
		if(balance>=drawAmount){
			System.out.println(Thread.currentThread().getName()+"取錢成功!吐出鈔票:"+drawAmount);
			try {
				Thread.sleep(1);
			} catch (InterruptedException e) {
				e.printStackTrace();
			}
			//修改餘額
			balance -= drawAmount;
			System.out.println("\t餘額爲:"+balance);
		} else {
			System.out.println("餘額不足!取錢失敗!");
		}
	}
	@Override
	public int hashCode() {
		final int prime = 31;
		int result = 1;
		result = prime * result
				+ ((accountNo == null) ? 0 : accountNo.hashCode());
		long temp;
		temp = Double.doubleToLongBits(balance);
		result = prime * result + (int) (temp ^ (temp >>> 32));
		return result;
	}
	@Override
	public boolean equals(Object obj) {
		if (this == obj)
			return true;
		if (obj == null)
			return false;
		if (getClass() != obj.getClass())
			return false;
		Account other = (Account) obj;
		if (accountNo == null) {
			if (other.accountNo != null)
				return false;
		} else if (!accountNo.equals(other.accountNo))
			return false;
		if (Double.doubleToLongBits(balance) != Double
				.doubleToLongBits(other.balance))
			return false;
		return true;
	}
}

  1. luck加鎖
package concurrency.account;

import java.util.concurrent.locks.ReentrantLock;

/**
 * 線程同步
 * @author lt
 * @date 2018年7月2日
 * @version v1.0
 */
public class Account {
	private ReentrantLock lock = new ReentrantLock();
	private String accountNo;
	private double balance;
	public Account(String accountNo, double balance){
		this.accountNo = accountNo;
		this.balance = balance;
	}
	public String getAccountNo() {
		return accountNo;
	}
	public void setAccountNo(String accountNo) {
		this.accountNo = accountNo;
	}
	//賬戶餘額不允許隨便修改,故只提供get方法
	public double getBalance() {
		return balance;
	}
	/**
	 * 一、luck加鎖機制
	 * 1.顯示加鎖,顯示釋放
	 */
	public void draw(double drawAmount){
		/**
		 * 加鎖
		 */
		lock.lock();
		try{
			//取錢數不能超過餘額數
			if(balance>=drawAmount){
				System.out.println(Thread.currentThread().getName()+"取錢成功!吐出鈔票:"+drawAmount);
				try {
					Thread.sleep(1);
				} catch (InterruptedException e) {
					e.printStackTrace();
				}
				//修改餘額
				balance -= drawAmount;
				System.out.println("\t餘額爲:"+balance);
			} else {
				System.out.println("餘額不足!取錢失敗!");
			}
		} finally {
			/**
			 * 釋放
			 */
			lock.unlock();
		}
	}
	@Override
	public int hashCode() {
		final int prime = 31;
		int result = 1;
		result = prime * result
				+ ((accountNo == null) ? 0 : accountNo.hashCode());
		long temp;
		temp = Double.doubleToLongBits(balance);
		result = prime * result + (int) (temp ^ (temp >>> 32));
		return result;
	}
	@Override
	public boolean equals(Object obj) {
		if (this == obj)
			return true;
		if (obj == null)
			return false;
		if (getClass() != obj.getClass())
			return false;
		Account other = (Account) obj;
		if (accountNo == null) {
			if (other.accountNo != null)
				return false;
		} else if (!accountNo.equals(other.accountNo))
			return false;
		if (Double.doubleToLongBits(balance) != Double
				.doubleToLongBits(other.balance))
			return false;
		return true;
	}
}

通過上邊的案例,我們瞭解到,在使用多線程的時候,可能會發生線程安全的問題,加鎖是處理線程安全問題的常見方式,接下來,就來深入瞭解一下Java併發機制的底層原理,這樣做可以更好的使用並多線程來解決問題

二、volatile

用於保證共享變量在多個線程之間的可見性(當一個線程修改變量時,其他線程可以讀取到修改的值),不會引起線程上下文的切換與調度,是輕量級的synchronized

1. volatile的定義與實現原理

定義:當一個變量被volatile修飾,Java線程內存模型保證任一線程對此變量的修改,其他線程均可讀取到修改的值

原理:

三、synchronized

1. 簡介

synchronized用於修飾代碼塊或者方法,被synchronized修飾的代碼塊或者方法,同一時間只能有一個線程在執行,其餘線程只能等待該線程執行結束後才能繼續執行;

2. 原理

由JVM規範可以瞭解,synchronized在JVM底層基於monitor對象的進入和退出來實現方法和代碼塊的同步;對於代碼塊同步使用的是monitorenter和monitorexit指令實現;monitorenter在代碼編譯後插入同步代碼塊的開始位置,monitorexit插入結束和異常位置;每一個對象都有一個monitor對象與之關聯,當monitor對象被線程持有時,對象處於鎖定狀態

3. 作用

synchronized的作用主要有三個:

  1. 確保線程互斥的訪問同步代碼;
  2. 保證共享變量的修改能夠及時可見;
  3. 有效解決重排序問題;

4. 用法

從語法上講,Synchronized總共有三種用法:

  1. 修飾普通方法
  2. 修飾靜態方法
  3. 修飾代碼塊

5. synchronized優化

使用監視器monitor來實現,而監視器monitor依賴於底層操作系統的Mutex Lock來實現。基於Mutex Lock進行線程切換時間較長,成本較高,所以稱synchronized爲重量級鎖。爲了提高性能,JDK1.6之後,引入了偏向鎖,輕量級鎖

6. 偏向鎖

Java SE 1.6爲了減少獲得鎖和釋放鎖時的資源消耗,引入了偏向鎖和輕量鎖,至此Java中的鎖有四種狀態,級別由低到高:無鎖狀態,偏向鎖狀態,輕量級鎖狀態,重量級鎖狀態;鎖可以升級但是不能降級;鎖的狀態保存在對象頭中,以32位JDK爲例:

鎖狀態

25 bit

4bit

1bit 2bit
23bit 2bit 是否是偏向鎖 鎖標誌位
輕量級鎖 指向棧中鎖記錄的指針 00
重量級鎖 指向互斥量(重量級鎖)的指針 10
GC標記 11
偏向鎖 線程ID Epoch 對象分代年齡 1 01
無鎖 對象的hashCode 對象分代年齡 0 01

定義:偏向鎖更像一種策略,用於降低多個線程在競爭獲取鎖的代價;它是通過在對象頭和棧幀中記錄偏向鎖的線程ID,之後線程在進入和退出同步塊時不需要CAS操作來加鎖和解鎖;當其他線程競爭鎖的時候,偏向鎖會撤銷;Java 6和Java 7中默認啓用偏向鎖;可以通過-XX:BiasedLocking來禁用偏向鎖;

7. 輕量級鎖

8. 鎖的優缺點對比

在這裏插入圖片描述

四、原子操作的實現原理

原子操作是指不可中斷的一個操作或者一系列操作

1. 處理器如何實現原子操作

32位IA-32處理器通過總線加鎖緩存加鎖的方式實現原子操作

1. 通過總線鎖保證原子性

舉個栗子:兩個處理器執行同一條指令:i++,(i++指令可以拆分成三步:第一步,從內存中讀取i的值;第二步,i+1;第三步,i賦值);兩個處理器在同時執行時,有可能會發生這種情況:cpu1和cpu2並行執行第1,2,3步,執行完成後,內存中的i的值爲2;多個處理器的情況下,這是有可能發生的;爲了保證原子性操作,可以使用處理器提供的總線鎖,在cpu1執行時,使用總線鎖在總線上輸出Lock#信號,其他處理器被阻塞,cpu1獨佔內存

2. 通過緩存鎖保證原子性

通過總線鎖的說明可知:總線鎖鎖住了其他cpu和內存之間的通信,開銷巨大;緩存鎖是指在修改緩存中的數據時,修改完成後,緩存回寫到內存中,其他cpu重新從內存中讀取

3. 不能使用緩存鎖的情況
  1. 共享數據不在緩存中
  2. 不支持緩存的處理器

2. Java如何實現原子操作

1. 利用循環CAS實現原子操作

CAS(Compare and swap),即比較並交換;JVM的CAS利用的是處理器的CMPXCHG指令實現的;自旋CAS的核心操作即:循環進行CAS操作,直至成功爲止;CAS也是實現我們平時所說的自旋鎖或樂觀鎖的核心操作

示例

下面的例子展示了線程安全的計數器和非線程安全的計數器,其中線程安全的計數器是利用JUC中的Atomic包下的相關類來實現

package com.lt.thread04;

import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.atomic.AtomicInteger;

/**
 * 1.驗證Java利用循環CAS驗證操作完成原子操作
 * @author lt
 * @date 2019年5月11日
 * @version v1.0
 */
public class Counter {

	private int m = 0;
	private AtomicInteger n = new AtomicInteger();
	//非線程安全的計數方法
	public void count(){
		m++;
	}
	//利用JUC的相關類實現線程安全的計數器(CAS)
	public void safeCount(){
		//循環進行CAS操作,直至成功爲止
		while(true){
			int i = n.get();
			//如果當前值==期望值,則以原子方式將值設置爲給定的更新值。相當於i=++i
			boolean flag = n.compareAndSet(i, ++i);
			//如果設置成功,則跳出循環,否則繼續設置
			if(flag) break;
		}
	}
	public static void main(String[] args) throws Exception {
		Counter c = new Counter();
		List<Thread> ts = new ArrayList<>();
		for(int i=0; i<1000; i++){
			Thread t = new Thread(new Runnable() {
				@Override
				public void run() {
					c.count();
					c.safeCount();
				}
			}, "線程"+i);
			ts.add(t);
		}
		for(Thread t : ts){
			t.start();
		}
		//等待當前線程執行完畢
		for(Thread t : ts){
			t.join();
		}
		System.out.println(c.m);
		System.out.println(c.n);
	}
}
結果
996
1000
注意

使用CAS會存在兩個問題

  1. ABA問題:一個變量初始值是A,變成了B,又變成了A;在CAS操作時,認爲變量沒有發生變化;解決方式是加版本號:1A->2B->3C;Java中提供了AtomicStampedReference類來解決ABA問題
  2. 循環時間長開銷大:當設置值不成功時,會循環進行CAS操作,佔用CPU,造成開銷過大
2. 利用鎖

Java中第二種原子操作的方式是利用鎖:偏向鎖,輕量級鎖,互斥鎖(重量級鎖);其實除了偏向鎖,輕量級鎖和互斥鎖的實現原理也是利用CAS操作,來獲取鎖和釋放鎖

五、死鎖

  1. 死鎖:當兩個線程互相等待對方釋放同步監視器時就會發生死鎖
package concurrency.deadlock;
/**
 * 死鎖驗證
 * @author lt
 * @date 2018年7月3日
 * @version v1.0
 */
public class DeadLock {

	public static void main(String[] args) {
		final A a = new A();
		final B b = new B();
		new Thread(new Runnable() {
			@Override
			public void run() {
				a.invoke(b);
			}
		}, "線程1").start();;
		new Thread(new Runnable() {
			@Override
			public void run() {
				b.invoke(a);
			}
		}, "線程2").start();;
	}
}
class A{
	//① 線程一調用A的invoke()方法,並對a對象進行加鎖
	public synchronized void invoke(B b){
		System.out.println(Thread.currentThread().getName()+"進入A的invlke()方法");
		//② 線程一休眠100毫秒,CPU切換執行線程二
		try {
			Thread.sleep(100);
		} catch (InterruptedException e) {
			e.printStackTrace();
		}
		//⑤ 線程一繼續運行,調用B的print方法,但是b對象在第③步被加鎖,沒有釋放鎖,所以線程阻塞等待鎖釋放
		b.print();
	}
	public synchronized void print(){
		System.out.println("A的print()方法");
	}
}
class B{
	//③ 線程二調用B的invoke()方法,並對b對象進行加鎖
	public synchronized void invoke(A a){
		System.out.println(Thread.currentThread().getName()+"進入B的invlke()方法");
		//④ 線程二休眠100毫秒,CPU切換執行線程一
		try {
			Thread.sleep(100);
		} catch (InterruptedException e) {
			e.printStackTrace();
		}
		//⑥ 線程二繼續運行,調用A的print方法,但是a對象在第①步被加鎖,沒有釋放鎖,所以線程阻塞等待鎖釋放
		a.print();
	}
	public synchronized void print(){
		System.out.println("B的print()方法");
	}
}

參考資料

【1】Java總結篇系列:Java多線程(三)
【2】Java多線程系列目錄(共43篇)
【3】Java併發編程的藝術
【4】Java併發編程:Synchronized底層優化(偏向鎖、輕量級鎖)
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章