Java多線程以內置鎖與顯式鎖

Java中具備經過Synchronized實現的內置鎖,和ReentrantLock實現的顯示鎖,這兩種鎖各有各的好處,算是互有補充,今天就來作一個總結。算法

Synchronized

內置鎖得到鎖和釋放鎖是隱式的,進入synchronized修飾的代碼就得到鎖,走出相應的代碼就釋放鎖。安全

synchronized(list){ //得到鎖
    list.append();
    list.count();
}//釋放鎖

通訊

與Synchronized配套使用的通訊方法一般有wait(),notify()。app

wait()方法會當即釋放當前鎖,並進入等待狀態,等待到相應的notify並從新得到鎖事後才能繼續執行;notify()不會馬上馬上釋放鎖,必需要等notify()所在線程執行完synchronized塊中的全部代碼纔會釋放。用以下代碼來進行驗證:ide

public static void main(String[] args){
    List list = new LinkedList();
    Thread r = new Thread(new ReadList(list));
    Thread w = new Thread(new WriteList(list));
    r.start();
    w.start();
}
class ReadList implements Runnable{

    private List list;

    public ReadList(List list){ this.list = list; }

    @Override
    public void run(){
        System.out.println("ReadList begin at "+System.currentTimeMillis());
        synchronized (list){
            try {
                Thread.sleep(1000);
                System.out.println("list.wait() begin at "+System.currentTimeMillis());
                list.wait();
                System.out.println("list.wait() end at "+System.currentTimeMillis());
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
        System.out.println("ReadList end at "+System.currentTimeMillis());

    }
}

class WriteList implements Runnable{

    private List list;

    public WriteList(List list){ this.list = list; }

    @Override
    public void run(){
        System.out.println("WriteList begin at "+System.currentTimeMillis());
        synchronized (list){
            System.out.println("get lock at "+System.currentTimeMillis());
            list.notify();
            System.out.println("list.notify() at "+System.currentTimeMillis());
            try {
                Thread.sleep(2000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println("get out of block at "+System.currentTimeMillis());
        }
        System.out.println("WriteList end at "+System.currentTimeMillis());

    }
}

運行結果性能

ReadList begin at 1493650526582
WriteList begin at 1493650526582
list.wait() begin at 1493650527584
get lock at 1493650527584
list.notify() at 1493650527584
get out of block at 1493650529584
WriteList end at 1493650529584
list.wait() end at 1493650529584
ReadList end at 1493650529584

可見讀線程開始運行,開始wait事後,寫線程纔得到鎖;寫線程走出同步塊而不是notify事後,讀線程才wait結束,亦即得到鎖。因此notify不會釋放鎖,wait會釋放鎖。
值得一提的是,notifyall()會通知等待隊列中的全部線程。測試

編碼

編碼模式比較簡單,單一,沒必要顯示的得到鎖,釋放鎖,能下降因粗心忘記釋放鎖的錯誤。使用模式以下:this

synchronized(object){ 
    
}

靈活性

  • 內置鎖在進入同步塊時,採起的是無限等待的策略,一旦開始等待,就既不能中斷也不能取消,容易產生飢餓與死鎖的問題
  • 在線程調用notify方法時,會隨機選擇相應對象的等待隊列的一個線程將其喚醒,而不是按照FIFO的方式,若是有強烈的公平性要求,好比FIFO就沒法知足

性能

Synchronized在JDK1.5及以前性能(主要指吞吐率)比較差,擴展性也不如ReentrantLock。可是JDK1.6之後,修改了管理內置鎖的算法,使得Synchronized和標準的ReentrantLock性能差異不大。編碼

ReentrantLock

ReentrantLock是顯示鎖,須要顯示進行 lock 以及 unlock 操做。線程

通訊

與ReentrantLock搭配的通行方式是Condition,以下:code

private Lock lock = new ReentrantLock();  
private Condition condition = lock.newCondition(); 
condition.await();//this.wait();  
condition.signal();//this.notify();  
condition.signalAll();//this.notifyAll();

Condition是被綁定到Lock上的,必須使用lock.newCondition()才能建立一個Condition。從上面的代碼能夠看出,Synchronized能實現的通訊方式,Condition均可以實現,功能相似的代碼寫在同一行中。
而Condition的優秀之處在於它能夠爲多個線程間創建不一樣的Condition,好比對象的讀/寫Condition,隊列的空/滿Condition,在JDK源碼中的ArrayBlockingQueue中就使用了這個特性:

public ArrayBlockingQueue(int capacity, boolean fair) {
    if (capacity <= 0)
        throw new IllegalArgumentException();
    this.items = new Object[capacity];
    lock = new ReentrantLock(fair);
    notEmpty = lock.newCondition();
    notFull =  lock.newCondition();
}
public void put(E e) throws InterruptedException {
    checkNotNull(e);
    final ReentrantLock lock = this.lock;
    lock.lockInterruptibly();
    try {
        while (count == items.length)
            notFull.await();
        enqueue(e);
    } finally {
        lock.unlock();
    }
}
public E take() throws InterruptedException {
    final ReentrantLock lock = this.lock;
    lock.lockInterruptibly();
    try {
        while (count == 0)
            notEmpty.await();
        return dequeue();
    } finally {
        lock.unlock();
    }
}
private void enqueue(E x) {
    // assert lock.getHoldCount() == 1;
    // assert items[putIndex] == null;
    final Object[] items = this.items;
    items[putIndex] = x;
    if (++putIndex == items.length)
        putIndex = 0;
    count++;
    notEmpty.signal();
}
private E dequeue() {
    // assert lock.getHoldCount() == 1;
    // assert items[takeIndex] != null;
    final Object[] items = this.items;
    @SuppressWarnings("unchecked")
    E x = (E) items[takeIndex];
    items[takeIndex] = null;
    if (++takeIndex == items.length)
        takeIndex = 0;
    count--;
    if (itrs != null)
        itrs.elementDequeued();
    notFull.signal();
    return x;
}

編碼

Lock lock = new ReentrantLock();
lock.lock();
try{
    
}finally{
    lock.unlock();
}

相比於Synchronized要複雜一些,並且必定要記得在finally中釋放鎖而不是其餘地方,這樣才能保證即便出了異常也能釋放鎖。

靈活性

  • lock.lockInterruptibly() 可使得線程在等待鎖是支持響應中斷;lock.tryLock() 可使得線程在等待一段時間事後若是還未得到鎖就中止等待而非一直等待。有了這兩種機制就能夠更好的制定得到鎖的重試機制,而非盲目一直等待,能夠更好的避免飢餓和死鎖問題
  • ReentrantLock能夠成爲公平鎖(非默認的),所謂公平鎖就是鎖的等待隊列的FIFO,不過公平鎖會帶來性能消耗,若是不是必須的不建議使用。這和CPU對指令進行重排序的理由是類似的,若是強行的按照代碼的書寫順序來執行指令,就會浪費許多時鐘週期,達不到最大利用率

性能

雖然Synchronized和標準的ReentrantLock性能差異不大,可是ReentrantLock還提供了一種非互斥的讀寫鎖,
也就是不強制每次最多隻有一個線程能持有鎖,它會避免「讀/寫」衝突,「寫/寫」衝突,可是不會排除「讀/讀」衝突,
由於「讀/讀」並不影響數據的完整性,因此能夠多個讀線程同時持有鎖,這樣在讀寫比較高的狀況下,性能會有很大的提高。

下面用兩種鎖分別實現的線程安全的linkedlist:

class RWLockList {//讀寫鎖

    private List list;
    private final ReadWriteLock lock = new ReentrantReadWriteLock();
    private final Lock readLock = lock.readLock();
    private final Lock writeLock = lock.writeLock();

    public RWLockList(List list){this.list = list;}

    public int get(int k) {
        readLock.lock();
        try {
            return (int)list.get(k);
        } finally {
            readLock.unlock();
        }
    }

    public void put(int value) {
        writeLock.lock();
        try {
            list.add(value);
        } finally {
            writeLock.unlock();
        }
    }
}

class SyncList  {

    private List list;

    public SyncList(List list){this.list = list;}

    public synchronized int  get(int k){
        return (int)list.get(k);
    }

    public synchronized void put(int value){
        list.add(value);
    }

}

讀寫鎖測試代碼:

List list = new LinkedList();
for (int i=0;i<10000;i++){
    list.add(i);
}
RWLockList rwLockList = new RWLockList(list);//初始化數據

Thread writer = new Thread(new Runnable() {
    @Override
    public void run() {
        for (int i=0;i<10000;i++){
            rwLockList.put(i);
        }
    }
});
Thread reader1 = new Thread(new Runnable() {
    @Override
    public void run() {
        for (int i=0;i<10000;i++){
            rwLockList.get(i);
        }
    }
});
Thread reader2 = new Thread(new Runnable() {
    @Override
    public void run() {
        for (int i=0;i<10000;i++){
            rwLockList.get(i);
        }
    }
});
long begin = System.currentTimeMillis();
writer.start();reader1.start();reader2.start();
try {
    writer.join();
    reader1.join();
    reader2.join();
} catch (InterruptedException e) {
    e.printStackTrace();
}
System.out.println("RWLockList take "+(System.currentTimeMillis()-begin) + "ms");

同步鎖測試代碼:

List list = new LinkedList();
for (int i=0;i<10000;i++){
    list.add(i);
}
SyncList syncList = new SyncList(list);//初始化數據
Thread writerS = new Thread(new Runnable() {
    @Override
    public void run() {
        for (int i=0;i<10000;i++){
            syncList.put(i);
        }
    }
});
Thread reader1S = new Thread(new Runnable() {
    @Override
    public void run() {
        for (int i=0;i<10000;i++){
            syncList.get(i);
        }
    }
});
Thread reader2S = new Thread(new Runnable() {
    @Override
    public void run() {
        for (int i=0;i<10000;i++){
            syncList.get(i);
        }
    }
});
long begin1 = System.currentTimeMillis();
writerS.start();reader1S.start();reader2S.start();
try {
    writerS.join();
    reader1S.join();
    reader2S.join();
} catch (InterruptedException e) {
    e.printStackTrace();
}
System.out.println("SyncList take "+(System.currentTimeMillis()-begin1) + "ms");

結果:

RWLockList take 248ms
RWLockList take 255ms
RWLockList take 249ms
RWLockList take 224ms

SyncList take 351ms
SyncList take 367ms
SyncList take 315ms
SyncList take 323ms

可見讀寫鎖的確是優於純碎的互斥鎖

總結

內置鎖最大優勢是簡潔易用,顯示鎖最大優勢是功能豐富,因此能用內置鎖就用內置鎖,在內置鎖功能不能知足之時在考慮顯示鎖。

關於兩種鎖,目前接觸到的就是這麼多,總結不到位之處,歡迎拍磚。個人主頁 mageek.cn

相關文章
相關標籤/搜索