Java多線程開發中,爲了避免多個線程對同一份數據的操作,我們需要對我們的線程做加鎖的操作,只要加鎖,就必然存在鎖競爭的問題,如果鎖競爭的問題處理不當就會出現死鎖問題。死鎖會讓程序一直卡住,程序不再往下執行。我們只能通過中止並重啓的方式來讓程序重新執行。
這是我們非常不願意看到的一種現象,我們要儘可能避免死鎖的情況發生!
造成死鎖的原因可以概括成三句話:
- 當前線程擁有其他線程需要的資源
- 當前線程等待其他線程已擁有的資源
- 都不放棄自己擁有的資源
動態順序鎖死鎖
動態順序鎖導致死鎖是最常見的死鎖,例如我們在2個線程對2個賬戶進行轉賬操作,我們需要先鎖定匯賬賬戶減錢,然後再鎖定入款賬戶加錢,一般來講這個邏輯順序是沒有問題的,但是如果這2個線程併發同時處理,就會產生死鎖。
public class DeadLock {
public static void main(String[] args) {
Account accountA = new Account(1L,"A",10000L);
Account accountB = new Account(1L,"B",10000L);
Thread a = new Thread(()->{
try {
transferMoney(accountA,accountB,100L);
} catch (Exception e) {
e.printStackTrace();
}
});
Thread b = new Thread(()->{
try {
transferMoney(accountB,accountA,100L);
} catch (Exception e) {
e.printStackTrace();
}
});
a.start();
b.start();
while (true){
try {
Thread.sleep(1000);
}catch (Exception e){
e.printStackTrace();
}
}
}
// 轉賬
public static void transferMoney(Account fromAccount,
Account toAccount,
Long amount) throws Exception {
// 鎖定匯賬賬戶
synchronized (fromAccount) {
System.out.println(Thread.currentThread().getName()+"獲取賬戶"+fromAccount.getName()+"鎖");
// 鎖定來賬賬戶
synchronized (toAccount) {
System.out.println(Thread.currentThread().getName()+"獲取賬戶"+toAccount.getName()+"鎖");
// 判餘額是否大於0
if (fromAccount.getAmount().compareTo(amount) < 0) {
throw new Exception("No enough money");
} else {
// 匯賬賬戶減錢
fromAccount.debit(amount);
// 來賬賬戶增錢
toAccount.credit(amount);
System.out.println(Thread.currentThread().getName()+"完成轉賬");
}
}
}
}
}
@Data
class Account{
private Long id;
private String name;
private Long amount;
public Account(){}
public Account(long id,String name, long amount){
this.id = id;
this.name = name;
this.amount = amount;
}
public void debit(Long amount) {
this.amount -= amount;
}
public void credit(Long amount) {
this.amount += amount;
}
}
程序執行結果如下:
Thread-1獲取賬戶B鎖
Thread-0獲取賬戶A鎖
我們會發現我們的轉賬一直無法完成交易,程序一直卡住,程序不再往下執行。即發生了死鎖。
Jconsole 查看死鎖
Jconsole是JDK自帶的圖形化界面工具,使用JDK給我們的的工具JConsole,我們可以直接查看Java進程中出現的死鎖。
控制檯輸入jconsole啓動圖形化界面工具
jconsole
選擇連接我們的本地調試進程
選擇線程欄,我們會看到我們的正在運存的測試代碼的3個線程
選擇我們的線程,檢測死鎖
很明顯看出,Thread-0需要的資源被Thread-1佔用,Thread-0被阻塞。
固定鎖順序避免死鎖
針對動態鎖順序導致的死鎖,我們可以通過固定加鎖的順序來解決
public class FixedOrderDeadLock {
public static void main(String[] args) {
Account accountA = new Account(1L,"A",10000L);
Account accountB = new Account(1L,"B",10000L);
Thread a = new Thread(()->{
try {
transferMoney(accountA,accountB,100L);
} catch (Exception e) {
e.printStackTrace();
}
});
Thread b = new Thread(()->{
try {
transferMoney(accountB,accountA,100L);
} catch (Exception e) {
e.printStackTrace();
}
});
a.start();
b.start();
while (true){
try {
Thread.sleep(1000);
}catch (Exception e){
e.printStackTrace();
}
}
}
public static void transferMoney(final Account fromAcct,
final Account toAcct,
final Long amount)
throws Exception {
class Helper {
public void transfer() throws Exception {
if (fromAcct.getAmount().compareTo(amount) < 0)
throw new Exception("No enough money");
else {
fromAcct.debit(amount);
toAcct.credit(amount);
System.out.println(Thread.currentThread().getName()+"完成轉賬");
}
}
}
// 得到鎖的hash值
int fromHash = System.identityHashCode(fromAcct);
int toHash = System.identityHashCode(toAcct);
// 根據hash值來上鎖 不管是匯款賬戶還是入款賬戶,總是Hash值小的先鎖,則對象鎖的順序是固定的
if (fromHash < toHash) {
synchronized (fromAcct) {
System.out.println(Thread.currentThread().getName()+"獲取賬戶"+fromAcct.getName()+"鎖");
synchronized (toAcct) {
System.out.println(Thread.currentThread().getName()+"獲取賬戶"+toAcct.getName()+"鎖");
new Helper().transfer();
}
}
} else if (fromHash > toHash) {// 根據hash值來上鎖
synchronized (toAcct) {
System.out.println(Thread.currentThread().getName()+"獲取賬戶"+toAcct.getName()+"鎖");
synchronized (fromAcct) {
System.out.println(Thread.currentThread().getName()+"獲取賬戶"+fromAcct.getName()+"鎖");
new Helper().transfer();
}
}
} else {//如果是同對象,由於 synchronized 已經支持可重入鎖,所以併發的同賬戶互相轉賬不會產生死鎖,鎖順序不產生影響
synchronized (fromAcct) {
synchronized (toAcct) {
new Helper().transfer();
}
}
}
}
}
程序執行結果
Thread-0獲取賬戶A鎖
Thread-0獲取賬戶B鎖
Thread-0完成轉賬
Thread-1獲取賬戶A鎖
Thread-1獲取賬戶B鎖
Thread-1完成轉賬
如結果所示,並不會產生死鎖。
協作鎖之間發生死鎖
除了動態順序鎖發生死鎖的情況,還存在一些死鎖的情況,但是不是順序鎖那麼簡單可以被發現。因爲有可能並不是在同一個方法中顯示請求兩個鎖,而是嵌套另一個方法去獲取第二個鎖。這就是隱式獲取兩個鎖(對象之間協作)。
例如,ProductProducer生產者會不斷的生產商品並且向倉庫註冊商品,ProductDepository商品倉庫會不斷的處理註冊到倉庫的商品加工,生成編號。
public class CooperatingDeadlock {
public static void main(String[] args) {
ProductProducer productProducer = new ProductProducer();
ProductDepository productDepository = new ProductDepository();
productProducer.setProductDepository(productDepository);
productProducer.start();
productDepository.start();
while (true){
try {
Thread.sleep(1000);
}catch (Exception e){
e.printStackTrace();
}
}
}
}
class ProductProducer extends Thread{
public void setProductDepository(ProductDepository productDepository) {
this.productDepository = productDepository;
}
private ProductDepository productDepository;
@Override
public void run(){
produce();
}
// produce()需要ProductProducer對象鎖
private synchronized void produce(){
do {
Product p = new Product(this);
System.out.println(Thread.currentThread().getName()+"獲取"+getClass().getName()+"對象鎖,生產商品,並向倉庫註冊");
this.productDepository.addAvailable(p);
try{
Thread.sleep(100);
}catch (Exception e){
e.printStackTrace();
}
}while (true);
}
public synchronized void handle(Product product) {
System.out.println(Thread.currentThread().getName()+"獲取"+getClass().getName()+"對象鎖,加工商品,生成編號");
product.setNo(System.currentTimeMillis());
}
}
class ProductDepository extends Thread{
public ProductDepository(){
availableProductList = new ArrayList<>();
}
private ArrayList<Product> availableProductList;
@Override
public void run(){
handleAvailable();
}
public synchronized void addAvailable(Product product) {
System.out.println(Thread.currentThread().getName()+"獲取"+getClass().getName()+"對象鎖,註冊到倉庫");
availableProductList.add(product);
}
// handleAvailable()需要ProductDepository對象鎖
public synchronized void handleAvailable() {
System.out.println(Thread.currentThread().getName()+"獲取"+getClass().getName()+"對象鎖,將註冊的商品進行加工");
do{
for (Product t : availableProductList)
// 調用handle()需要ProductProducer對象鎖
t.handle();
availableProductList.clear();
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
}while (true);
}
}
class Product{
private ProductProducer productProducer;
public Product(){}
public void setNo(Long no) {
this.no = no;
}
private Long no;
public Product(ProductProducer productProducer){
this.productProducer = productProducer;
}
public void handle(){
this.productProducer.handle(this);
}
}
此時我們執行程序:
Thread-0獲取com.pubutech.multithread.example.deadlock.ProductProducer對象鎖,生產商品,並向倉庫註冊
Thread-1獲取com.pubutech.multithread.example.deadlock.ProductDepository對象鎖,將註冊的商品進行加工
很顯然,我們的程序將陷入死鎖無法繼續執行下去。
開放調用避免死鎖
在協作對象之間發生死鎖的例子中,主要是因爲在調用某個方法時就需要持有鎖,並且在方法內部也調用了其他帶鎖的方法!
如果在調用某個方法時不再持有鎖,而改爲同步代碼塊僅用於保護那些涉及共享狀態的操作!那麼這種調用被稱爲開放調用!
我們可以這樣來改造:
public class OpenMethodCooperationDeadLock {
public static void main(String[] args) {
OpenMethodProductProducer productProducer = new OpenMethodProductProducer();
OpenMethodProductDepository productDepository = new OpenMethodProductDepository();
productProducer.setProductDepository(productDepository);
productProducer.start();
productDepository.start();
while (true){
try {
Thread.sleep(1000);
}catch (Exception e){
e.printStackTrace();
}
}
}
}
class OpenMethodProductProducer extends Thread{
public void setProductDepository(OpenMethodProductDepository productDepository) {
this.productDepository = productDepository;
}
private OpenMethodProductDepository productDepository;
@Override
public void run(){
produce();
}
private void produce(){
do {
OpenMethodProduct p = null;
//需要ProductProducer對象鎖,但是縮小鎖的範圍,不會同時去獲取兩個鎖
synchronized (this){
System.out.println(Thread.currentThread().getName()+"獲取"+getClass().getName()+"對象鎖,生產商品,並向倉庫註冊");
p = new OpenMethodProduct(this);
}
if (null != p){
System.out.println(Thread.currentThread().getName()+"向倉庫註冊");
this.productDepository.addAvailable(p);
}
try{
Thread.sleep(100);
}catch (Exception e){
e.printStackTrace();
}
}while (true);
}
public synchronized void handle(OpenMethodProduct product) {
System.out.println(Thread.currentThread().getName()+"獲取"+getClass().getName()+"對象鎖,加工商品,生成編號");
product.setNo(System.currentTimeMillis());
}
}
class OpenMethodProductDepository extends Thread{
public OpenMethodProductDepository(){
availableProductList = new ArrayList<>();
}
private ArrayList<OpenMethodProduct> availableProductList;
@Override
public void run(){
handleAvailable();
}
public synchronized void addAvailable(OpenMethodProduct product) {
System.out.println(Thread.currentThread().getName()+"獲取"+getClass().getName()+"對象鎖,註冊到倉庫");
availableProductList.add(product);
}
public void handleAvailable() {
do{
ArrayList<OpenMethodProduct> availableProductListCopy;
//需要ProductDepository對象鎖,但是縮小鎖的範圍,不會同時去獲取兩個鎖
synchronized (this){
System.out.println(Thread.currentThread().getName()+"獲取"+getClass().getName()+"對象鎖,準備將註冊的商品進行加工");
availableProductListCopy = new ArrayList<>(availableProductList);
availableProductList.clear();
}
for (OpenMethodProduct t : availableProductListCopy)
// 調用handle()需要ProductProducer對象鎖
t.handle();
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
}while (true);
}
}
class OpenMethodProduct{
private OpenMethodProductProducer productProducer;
public OpenMethodProduct(){}
public void setNo(Long no) {
this.no = no;
}
private Long no;
public OpenMethodProduct(OpenMethodProductProducer productProducer){
this.productProducer = productProducer;
}
public void handle(){
this.productProducer.handle(this);
}
}
程序執行結果如下,將會不斷的協作下去直至我們終端程序而不會產生死鎖
Thread-0獲取com.pubutech.multithread.example.deadlock.OpenMethodProductProducer對象鎖,生產商品,並向倉庫註冊
Thread-1獲取com.pubutech.multithread.example.deadlock.OpenMethodProductDepository對象鎖,準備將註冊的商品進行加工
Thread-0向倉庫註冊
Thread-0獲取com.pubutech.multithread.example.deadlock.OpenMethodProductDepository對象鎖,註冊到倉庫
Thread-1獲取com.pubutech.multithread.example.deadlock.OpenMethodProductDepository對象鎖,準備將註冊的商品進行加工
Thread-1獲取com.pubutech.multithread.example.deadlock.OpenMethodProductProducer對象鎖,加工商品,生成編號
Thread-0獲取com.pubutech.multithread.example.deadlock.OpenMethodProductProducer對象鎖,生產商品,並向倉庫註冊
Thread-0向倉庫註冊
Thread-0獲取com.pubutech.multithread.example.deadlock.OpenMethodProductDepository對象鎖,註冊到倉庫
Thread-1獲取com.pubutech.multithread.example.deadlock.OpenMethodProductDepository對象鎖,準備將註冊的商品進行加工
Thread-0獲取com.pubutech.multithread.example.deadlock.OpenMethodProductProducer對象鎖,生產商品,並向倉庫註冊
Thread-0向倉庫註冊
Thread-0獲取com.pubutech.multithread.example.deadlock.OpenMethodProductDepository對象鎖,註冊到倉庫
Thread-1獲取com.pubutech.multithread.example.deadlock.OpenMethodProductProducer對象鎖,加工商品,生成編號
Thread-0獲取com.pubutech.multithread.example.deadlock.OpenMethodProductProducer對象鎖,生產商品,並向倉庫註冊
Thread-0向倉庫註冊
Thread-0獲取com.pubutech.multithread.example.deadlock.OpenMethodProductDepository對象鎖,註冊到倉庫
Thread-1獲取com.pubutech.multithread.example.deadlock.OpenMethodProductDepository對象鎖,準備將註冊的商品進行加工
Thread-1獲取com.pubutech.multithread.example.deadlock.OpenMethodProductProducer對象鎖,加工商品,生成編號
Thread-1獲取com.pubutech.multithread.example.deadlock.OpenMethodProductProducer對象鎖,加工商品,生成編號
Thread-1獲取com.pubutech.multithread.example.deadlock.OpenMethodProductDepository對象鎖,準備將註冊的商品進行加工
Thread-0獲取com.pubutech.multithread.example.deadlock.OpenMethodProductProducer對象鎖,生產商品,並向倉庫註冊
Thread-0向倉庫註冊
Thread-0獲取com.pubutech.multithread.example.deadlock.OpenMethodProductDepository對象鎖,註冊到倉庫
Thread-1獲取com.pubutech.multithread.example.deadlock.OpenMethodProductDepository對象鎖,準備將註冊的商品進行加工
Thread-0獲取com.pubutech.multithread.example.deadlock.OpenMethodProductProducer對象鎖,生產商品,並向倉庫註冊
Thread-0向倉庫註冊
Thread-0獲取com.pubutech.multithread.example.deadlock.OpenMethodProductDepository對象鎖,註冊到倉庫
Thread-1獲取com.pubutech.multithread.example.deadlock.OpenMethodProductProducer對象鎖,加工商品,生成編號
Thread-0獲取com.pubutech.multithread.example.deadlock.OpenMethodProductProducer對象鎖,生產商品,並向倉庫註冊
Thread-0向倉庫註冊
Thread-0獲取com.pubutech.multithread.example.deadlock.OpenMethodProductDepository對象鎖,註冊到倉庫
RetreenLock鎖超時解決死鎖
synchronized 關鍵字的鎖是由Java虛擬機實現的,它無法顯示獲取鎖超時,但是Java5以後Java RetreenLock提供了tryLock()方法來實現獲取鎖超時,我們在獲取鎖時可以使用tryLock()方法。當等待超過時限的時候,tryLock()不會一直等待,而是返回錯誤信息。