ConCurrent併發包 - Lock詳解

synchronized的缺陷


我們知道,可以利用synchronized關鍵字來實現共享資源的互斥訪問。Java 5在java.util.concurrent.locks包下提供了另一種來實現線程的同步訪問,那就是Lock。既然有了synchronized來實現線程同步,Java爲什麼還需要提供Lock呢?
synchronized是Java的一個關鍵字,當我們使用synchronized來修飾方法或代碼塊時,線程必須先獲得對應的鎖才能執行該段代碼。而其他線程只能一直等待,直到當前線程釋放鎖並獲得對應的鎖才能進入該段代碼。這裏獲取鎖的線程釋放鎖只會有兩種情況:
  • 獲取鎖的線程執行完該段代碼,線程會釋放佔有的鎖;
  • 線程執行發生異常,此時JVM會讓線程自動釋放鎖。
那麼如果這個佔有鎖的線程由於等待IO或其他原因(比如調用sleep方法)被阻塞,但是還沒有釋放鎖,那麼其他線程只能乾巴巴的等着,試想這多麼影響程序的執行效率。
當多個線程同時讀寫文件是,我們知道讀操作和寫操作會發生衝突,寫操作和寫操作也會發生衝突,但是讀操作和讀操作之間不會衝突。synchronized關鍵字對一段代碼加鎖,所有的線程必須先獲得對應的鎖纔有該代碼段的執行權限。如果多個線程同時進行讀操作時,使用synchronized關鍵字會導致在任何時刻只有一個線程讀,其他線程等待,大大降低執行效率。
Lock可以對以上種種情況作優化,提供更好的執行效率。另外,Lock方便了對鎖的管理,可以自由的加鎖和釋放鎖,還可以判斷有沒有成功獲取鎖。但是在使用Lock時要注意,Lock需要開發者手動去釋放鎖,如果沒有主動釋放鎖,就要可能導致死鎖出現。建議在finally語句塊中釋放Lock鎖。

concurrent.locks包下常用類


1. Lock

首先要說明的是Lock,它是一個接口:
public interface Lock {
    void lock();
    void lockInterruptibly() throws InterruptedException;
    boolean tryLock();
    boolean tryLock(long time, TimeUnit unit) throws InterruptedException;
    void unlock();
    Condition newCondition();
}
  • lock()方法用來獲取鎖。
  • tryLock()嘗試獲取鎖,如果成功則返回true,失敗返回false(其他線程已佔有鎖)。這個方法會立即返回,在拿不到鎖時也不會等待。
  • tryLock(long time, TimeUnit unit)方法和tryLock()方法類似,只不過在拿不到鎖時等待一定的時間,如果超過等待時間還拿不到鎖就返回false。
  • lockInterruptibly()方法比較特殊,當通過這個方法獲取鎖時,如果該線程正在等待獲取鎖,則它能夠響應中斷。也就是說,當兩個線程同時通過lockInterruptibly()獲取某個鎖時,假如線程A獲得了鎖,而線程B仍在等待獲取鎖,那麼對線程B調用interrupt()方法可以中斷B的等待過程。
// lock()的使用
Lock lock = ...;
lock.lock();
try{
    //處理任務
}catch(Exception ex){
     
}finally{
    lock.unlock();   //釋放鎖
}
// tryLock()的使用
Lock lock = ...;
if(lock.tryLock()) {
     try{
         //處理任務
     }catch(Exception ex){
         
     }finally{
         lock.unlock();   //釋放鎖
     } 
}else {
    //如果不能獲取鎖,則直接做其他事情
}
// lockInterruptibly()的使用
public void method() throws InterruptedException {
    lock.lockInterruptibly();
    try {  
     //.....
    }
    finally {
        lock.unlock();
    }  
}
使用synchronized關鍵字,當線程處於等待鎖的狀態時,是無法被中斷的,只能一直等待。

2.ReentrantLock

ReentrantLock是可重入鎖。如果所具備可重入性,則稱爲可重入鎖,synchronized可ReentrantLock都是可重入鎖。可重入鎖也叫遞歸鎖,當一個線程已經獲得該代碼塊的鎖時,再次進入該代碼塊不必重新申請鎖,可以直接執行。

例1, lock()的使用方法:
public class Test {
    private ArrayList<Integer> arrayList = new ArrayList<Integer>();
    private Lock lock = new ReentrantLock();    //注意這個地方
    public static void main(String[] args)  {
        final Test test = new Test();
         
        new Thread(){
            public void run() {
                test.insert(Thread.currentThread());
            };
        }.start();
         
        new Thread(){
            public void run() {
                test.insert(Thread.currentThread());
            };
        }.start();
    }  
     
    public void insert(Thread thread) {
        lock.lock();
        try {
            System.out.println(thread.getName()+"得到了鎖");
            for(int i=0;i<5;i++) {
                arrayList.add(i);
            }
        } catch (Exception e) {
            // TODO: handle exception
        }finally {
            System.out.println(thread.getName()+"釋放了鎖");
            lock.unlock();
        }
    }
}
例2, lockInterruptibly()響應中斷的使用方法:
public class Test {
    private Lock lock = new ReentrantLock();   
    public static void main(String[] args)  {
        Test test = new Test();
        MyThread thread1 = new MyThread(test);
        MyThread thread2 = new MyThread(test);
        thread1.start();
        thread2.start();
         
        try {
            Thread.sleep(2000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        thread2.interrupt();
    }  
     
    public void insert(Thread thread) throws InterruptedException{
        lock.lockInterruptibly();   //注意,如果需要正確中斷等待鎖的線程,必須將獲取鎖放在外面,然後將InterruptedException拋出
        try {  
            System.out.println(thread.getName()+"得到了鎖");
            long startTime = System.currentTimeMillis();
            for(    ;     ;) {
                if(System.currentTimeMillis() - startTime >= Integer.MAX_VALUE)
                    break;
                //插入數據
            }
        }
        finally {
            System.out.println(Thread.currentThread().getName()+"執行finally");
            lock.unlock();
            System.out.println(thread.getName()+"釋放了鎖");
        }  
    }
}
 
class MyThread extends Thread {
    private Test test = null;
    public MyThread(Test test) {
        this.test = test;
    }
    @Override
    public void run() {
         
        try {
            test.insert(Thread.currentThread());
        } catch (InterruptedException e) {
            System.out.println(Thread.currentThread().getName()+"被中斷");
        }
    }
}

3. ReadWriteLock

ReadWriteLock也是一個接口,它只定義了兩個方法:
public interface ReadWriteLock {
    /**
     * Returns the lock used for reading.
     */
    Lock readLock();
 
    /**
     * Returns the lock used for writing.
     */
    Lock writeLock();
}
readLock()用來獲取讀鎖,writeLock()用來獲取寫鎖。也就是將文件的讀寫操作分開,分成兩個鎖來分配給線程,從而使多個線程可以同時進行讀操作。ReentrantReadWriteLock是它的實現類。

public class Test {
    private ReentrantReadWriteLock rwl = new ReentrantReadWriteLock();
     
    public static void main(String[] args)  {
        final Test test = new Test();
         
        new Thread(){
            public void run() {
                test.get(Thread.currentThread());
            };
        }.start();
         
        new Thread(){
            public void run() {
                test.get(Thread.currentThread());
            };
        }.start();
         
    }  
     
    public void get(Thread thread) {
        rwl.readLock().lock();
        try {
            long start = System.currentTimeMillis();
             
            while(System.currentTimeMillis() - start <= 1) {
                System.out.println(thread.getName()+"正在進行讀操作");
            }
            System.out.println(thread.getName()+"讀操作完畢");
        } finally {
            rwl.readLock().unlock();
        }
    }
}









發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章