Java併發線程之Lock應用

1. ReentrantLock的基本使用

lock使用以及注意事項

// task.java
public class Task {
    private int count;

    public void read(){
        System.out.println(Thread.currentThread().getName()+"讀取count數據:" +  count);
    }

    public void write(){
        System.out.println(Thread.currentThread().getName()+"開始寫count數據。。。");
        count++;
        System.out.println(Thread.currentThread().getName() +"結束寫count數據。。。");
    }
}
	// main.java
	final ReentrantLock lock = new ReentrantLock();
	Task task = new Task();
	 for (int index = 0; index < 4; index++){
	     new Thread(){
	         @Override
	         public void run() {
	             lock.lock();
	             try{
	//                        dirtyCode();
	                 task.write();
	             }finally {
	                 lock.unlock();
	             }
	         }
	     }.start();
	 }
  • 執行結果如下
Thread-0開始寫count數據。。。
Thread-0結束寫count數據。。。
Thread-3開始寫count數據。。。
Thread-3結束寫count數據。。。
Thread-2開始寫count數據。。。
Thread-2結束寫count數據。。。
Thread-1開始寫count數據。。。
Thread-1結束寫count數據。。。
  • 現在將代碼變更如下
// Task.java
private volatile boolean flag = false;
public void write(){
    System.out.println(Thread.currentThread().getName()+"開始寫count數據。。。");
    count++;
    if (count == 3){
        flag = true;
    }
    System.out.println(Thread.currentThread().getName() +"結束寫count數據。。。");
}

public boolean isFlag() {
    return flag;
}
//main.java
private static void dirtyCode(boolean flag){
    if (flag){
        int sum = 2 / 0;
    }
}
// 修改線程中的run方法
public void run() {
   try{
   		// lock.lock();  // 這裏與上述代碼加鎖是一致的道理,這裏爲了演示效果
        dirtyCode(task.isFlag());
        lock.lock();		// 	可以看到我們是要縮小粒度加鎖的優化
        task.write();
    } finally {
        lock.unlock();
    }
}
  • 此時的執行結果
Thread-0開始寫count數據。。。
Thread-0結束寫count數據。。。
Thread-1開始寫count數據。。。
Thread-1結束寫count數據。。。
Thread-2開始寫count數據。。。
Thread-2結束寫count數據。。。
Exception in thread "Thread-3" Exception in thread "Thread-4" java.lang.IllegalMonitorStateException
  • 分析
    • 上述代碼會拋出異常但是不是我們預期的算法異常,而是非法監視器狀態異常
    • 從代碼分析看,執行dirtyCode之後不會再執行加鎖操作,但是一定會執行unlock操作,於是我們可以推測是調用unlock的時候出錯
    • 查看源碼
    if (Thread.currentThread() != getExclusiveOwnerThread())
                    throw new IllegalMonitorStateException();
    
    • 可以看到原因就是我們當前並沒有加鎖,當前線程並沒有持有獨佔鎖,因此調用unlock會報錯
    • 於是代碼可以變更爲
    run(){
    	dirtyCode(task.isFlag());
    	lock.lock();
    	try{
    		task.write();
    	}finally{
    		lock.unlock();
    	}
    }
    
    • lock.lock()比較好的做法是放在try塊的外面而不是在裏面
2. 讀寫鎖
  • 讀鎖: 意味着允許併發多線程進行讀取操作,但是不能執行寫操作
  • 寫鎖: 意味着僅能一個線程執行寫操作,其他線程必須等待
  • 代碼示例
// task.java
// 讀寫方法分別增加時間點輸出並添加1s的等待操作
// main.java
 	ReentrantReadWriteLock readWriteLock = new ReentrantReadWriteLock();
    final ReentrantReadWriteLock.ReadLock readLock =  readWriteLock.readLock();
    final ReentrantReadWriteLock.WriteLock writeLock =  readWriteLock.writeLock();
    Task task = new Task();

    for (int index = 0; index < 10; index++){
        new Thread(){
            @Override
            public void run() {
                readLock.lock();
                try{
                    task.read();
                }finally {
                    readLock.unlock();
                }
            }
        }.start();

        new Thread(){
            @Override
            public void run() {
                writeLock.lock();
                try {
                    task.write();
                }finally {
                    writeLock.unlock();
                }
            }
        }.start();
    }
  • 註釋掉寫操作的線程,執行讀操作的線程,執行結果如下
Thread-0開始讀取count數據:0, 時間點:1582106214921
Thread-9開始讀取count數據:0, 時間點:1582106214923
Thread-8開始讀取count數據:0, 時間點:1582106214922
Thread-7開始讀取count數據:0, 時間點:1582106214922
Thread-6開始讀取count數據:0, 時間點:1582106214922
Thread-5開始讀取count數據:0, 時間點:1582106214922
Thread-4開始讀取count數據:0, 時間點:1582106214922
Thread-3開始讀取count數據:0, 時間點:1582106214921
Thread-2開始讀取count數據:0, 時間點:1582106214921
Thread-1開始讀取count數據:0, 時間點:1582106214921
Thread-8完成讀取count數據:0, 時間點:1582106215942
Thread-7完成讀取count數據:0, 時間點:1582106215942
Thread-6完成讀取count數據:0, 時間點:1582106215942
Thread-9完成讀取count數據:0, 時間點:1582106215942
Thread-5完成讀取count數據:0, 時間點:1582106215942
Thread-0完成讀取count數據:0, 時間點:1582106215942
Thread-4完成讀取count數據:0, 時間點:1582106215942
Thread-1完成讀取count數據:0, 時間點:1582106215945
Thread-2完成讀取count數據:0, 時間點:1582106215945
Thread-3完成讀取count數據:0, 時間點:1582106215945
  • 可以看出,上述讀鎖可以保證同一個時間點多個線程都持有讀鎖進行讀操作
  • 註釋掉寫操作的線程,查看輸出結果
Thread-0開始寫count數, 時間點:1582106322255
Thread-0完成寫count數, 時間點:1582106323279
Thread-1開始寫count數, 時間點:1582106323279
Thread-1完成寫count數, 時間點:1582106324281
Thread-2開始寫count數, 時間點:1582106324282
Thread-2完成寫count數, 時間點:1582106325287
Thread-3開始寫count數, 時間點:1582106325288
Thread-3完成寫count數, 時間點:1582106326290
  • 上面可以看出寫鎖必須是等待當前一個線程執行完成才能釋放鎖給下一個線程,寫鎖只能一個線程持有
3. 鎖中斷響應操作

lock()與lockInterruptibly()方法比較

private static void testInterrupt(Lock lock) throws InterruptedException {
    lock.lockInterruptibly();
//        lock.lock();
     try{
         System.out.println(Thread.currentThread().getName()+"獲取到鎖執行了。。。。");
         TimeUnit.SECONDS.sleep(5);
     }finally {
         lock.unlock();
         System.out.println(Thread.currentThread().getName()+"釋放鎖。。。。");
     }
 }
// main.java
	final ReentrantLock lock = new ReentrantLock();
    Thread t1 = new Thread("線程1"){
        @Override
        public void run() {
            try {
                testInterrupt(lock);
                System.out.println(Thread.currentThread().getName()+"執行其他操作。。。。");
            }catch (InterruptedException e){
                System.out.println(Thread.currentThread().getName() + "被中斷,獲取鎖失敗。。。");
            }
        }
    };

    Thread t2 = new Thread("線程2"){
        @Override
        public void run() {
            try {
                testInterrupt(lock);
                System.out.println("執行其他操作。。。。");
            }catch (InterruptedException e){
                System.out.println(Thread.currentThread().getName() + "被中斷,獲取鎖失敗。。。");
            }
        }
    };

    t1.start();
    TimeUnit.MICROSECONDS.sleep(1000);
    t2.start();

    TimeUnit.MICROSECONDS.sleep(3000);
    t2.interrupt();
  • 使用lock()執行結果
線程1獲取到鎖執行了。。。。
線程1釋放鎖。。。。
線程2獲取到鎖執行了。。。。
線程1執行其他操作。。。。
線程2釋放鎖。。。。
線程2被中斷,獲取鎖失敗。。。
  • 使用lockInterruptibly()執行結果
線程1獲取到鎖執行了。。。。
線程2被中斷,獲取鎖失敗。。。
線程1釋放鎖。。。。
線程1執行其他操作。。。。
  • 分析,使用lock()的時候lock代碼塊仍然會執行,而主線程已經調用中斷,說明沒有響應到鎖中斷
  • 對於使用lockInterruptibly()一旦收到主線程的中斷操作,立馬響應中斷不會再繼續往下執行
4. 條件鎖

使用典型的生產者-消費者模型

	// 基於Condition的條件鎖
   final ReentrantLock lock = new ReentrantLock();
   final Condition full = lock.newCondition();
   final Condition empty = lock.newCondition();
   final int size = 5;
   final List<String> list = new ArrayList<>();

   // 生產者
   new Thread(){
       @Override
       public void run() {
           while (true){
               lock.lock();
               try{
                   while (list.size() == size){
                       // 說明已經滿了
                       System.out.println("數據已經滿了。。。");
                       full.await();
                   }
                   list.add("aaa");
                   System.out.println("生產數據。。。。");
                   TimeUnit.SECONDS.sleep(1);
                   empty.signalAll();// 通知消費可以消費數據
               }catch (Exception e){

               } finally {
                   lock.unlock();
               }
           }
       }
   }.start();

   new Thread(){
       @Override
       public void run() {
           while (true){
               lock.lock();
               try{
                   while (list.size() == 0){
                       System.out.println("數據已經空了");
                       empty.await();//當前已經是空數據
                   }
                   list.remove("aaa");
                   System.out.println("消費數據。。。");
                   TimeUnit.SECONDS.sleep(1);
                   full.signalAll();   // 通知生產者生產數據
               }catch (Exception e){

               } finally {
                   lock.unlock();
               }
           }
       }
   }.start();
  • 執行結果
....
數據已經空了
生產數據。。。。
生產數據。。。。
生產數據。。。。
生產數據。。。。
生產數據。。。。
數據已經滿了。。。
消費數據。。。
消費數據。。。
消費數據。。。
消費數據。。。
消費數據。。。
....
發佈了72 篇原創文章 · 獲贊 25 · 訪問量 3萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章