一、線程安全問題
1. 一個典型的線程不安全的例子
- 多個線程同時操作同一份資源的(主要是進行讀寫操作)時候,就有可能會發生線程安全問題;比如兩個人同時對同一個賬戶進行取款操作的時候,就有可能會出現餘額爲負數的結果。
- 示例:兩個人同時操作一個賬戶
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
- 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;
}
}
- 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;
}
}
- 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的作用主要有三個:
- 確保線程互斥的訪問同步代碼;
- 保證共享變量的修改能夠及時可見;
- 有效解決重排序問題;
4. 用法
從語法上講,Synchronized總共有三種用法:
- 修飾普通方法
- 修飾靜態方法
- 修飾代碼塊
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. 不能使用緩存鎖的情況
- 共享數據不在緩存中
- 不支持緩存的處理器
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會存在兩個問題
- ABA問題:一個變量初始值是A,變成了B,又變成了A;在CAS操作時,認爲變量沒有發生變化;解決方式是加版本號:1A->2B->3C;Java中提供了AtomicStampedReference類來解決ABA問題
- 循環時間長開銷大:當設置值不成功時,會循環進行CAS操作,佔用CPU,造成開銷過大
2. 利用鎖
Java中第二種原子操作的方式是利用鎖:偏向鎖,輕量級鎖,互斥鎖(重量級鎖);其實除了偏向鎖,輕量級鎖和互斥鎖的實現原理也是利用CAS操作,來獲取鎖和釋放鎖
五、死鎖
- 死鎖:當兩個線程互相等待對方釋放同步監視器時就會發生死鎖
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()方法");
}
}