Java多線程之內(nèi)置鎖與顯示鎖
Java中具有通過Synchronized實(shí)現(xiàn)的內(nèi)置鎖,和ReentrantLock實(shí)現(xiàn)的顯示鎖,這兩種鎖各有各的好處,算是互有補(bǔ)充,今天就來做一個總結(jié)。
Synchronized
內(nèi)置鎖獲得鎖和釋放鎖是隱式的,進(jìn)入synchronized修飾的代碼就獲得鎖,走出相應(yīng)的代碼就釋放鎖。
- synchronized(list){ //獲得鎖
- list.append(); list.count();
- }//釋放鎖
通信
與Synchronized配套使用的通信方法通常有wait(),notify()。
wait()方法會立即釋放當(dāng)前鎖,并進(jìn)入等待狀態(tài),等待到相應(yīng)的notify并重新獲得鎖過后才能繼續(xù)執(zhí)行;notify()不會立刻立刻釋放鎖,必須要等notify()所在線程執(zhí)行完synchronized塊中的所有代碼才會釋放。用如下代碼來進(jìn)行驗(yàn)證:
- 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());
- }
- }
運(yùn)行結(jié)果
- ReadList begin at 1493650526582WriteList begin at 1493650526582list.wait() begin at 1493650527584get lock at 1493650527584list.notify() at 1493650527584get out of block at 1493650529584WriteList end at 1493650529584list.wait() end at 1493650529584ReadList end at 1493650529584
可見讀線程開始運(yùn)行,開始wait過后,寫線程才獲得鎖;寫線程走出同步塊而不是notify過后,讀線程才wait結(jié)束,亦即獲得鎖。所以notify不會釋放鎖,wait會釋放鎖。值得一提的是,notifyall()會通知等待隊(duì)列中的所有線程。
編碼
編碼模式比較簡單,單一,不必顯示的獲得鎖,釋放鎖,能降低因粗心忘記釋放鎖的錯誤。使用模式如下:
- synchronized(object){
- }
靈活性
內(nèi)置鎖在進(jìn)入同步塊時,采取的是***等待的策略,一旦開始等待,就既不能中斷也不能取消,容易產(chǎn)生饑餓與死鎖的問題
在線程調(diào)用notify方法時,會隨機(jī)選擇相應(yīng)對象的等待隊(duì)列的一個線程將其喚醒,而不是按照FIFO的方式,如果有強(qiáng)烈的公平性要求,比如FIFO就無法滿足
性能
Synchronized在JDK1.5及之前性能(主要指吞吐率)比較差,擴(kuò)展性也不如ReentrantLock。但是JDK1.6以后,修改了管理內(nèi)置鎖的算法,使得Synchronized和標(biāo)準(zhǔn)的ReentrantLock性能差別不大。
ReentrantLock
ReentrantLock是顯示鎖,需要顯示進(jìn)行 lock 以及 unlock 操作。
通信
與ReentrantLock搭配的通行方式是Condition,如下:
- 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()才能創(chuàng)建一個Condition。從上面的代碼可以看出,Synchronized能實(shí)現(xiàn)的通信方式,Condition都可以實(shí)現(xiàn),功能類似的代碼寫在同一行中。而Condition的優(yōu)秀之處在于它可以為多個線程間建立不同的Condition,比如對象的讀/寫Condition,隊(duì)列的空/滿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要復(fù)雜一些,而且一定要記得在finally中釋放鎖而不是其他地方,這樣才能保證即使出了異常也能釋放鎖。
靈活性
lock.lockInterruptibly() 可以使得線程在等待鎖是支持響應(yīng)中斷;lock.tryLock() 可以使得線程在等待一段時間過后如果還未獲得鎖就停止等待而非一直等待。有了這兩種機(jī)制就可以更好的制定獲得鎖的重試機(jī)制,而非盲目一直等待,可以更好的避免饑餓和死鎖問題
ReentrantLock可以成為公平鎖(非默認(rèn)的),所謂公平鎖就是鎖的等待隊(duì)列的FIFO,不過公平鎖會帶來性能消耗,如果不是必須的不建議使用。這和CPU對指令進(jìn)行重排序的理由是相似的,如果強(qiáng)行的按照代碼的書寫順序來執(zhí)行指令,就會浪費(fèi)許多時鐘周期,達(dá)不到***利用率
性能
雖然Synchronized和標(biāo)準(zhǔn)的ReentrantLock性能差別不大,但是ReentrantLock還提供了一種非互斥的讀寫鎖,
也就是不強(qiáng)制每次最多只有一個線程能持有鎖,它會避免“讀/寫”沖突,“寫/寫”沖突,但是不會排除“讀/讀”沖突,
因?yàn)?ldquo;讀/讀”并不影響數(shù)據(jù)的完整性,所以可以多個讀線程同時持有鎖,這樣在讀寫比較高的情況下,性能會有很大的提升。
下面用兩種鎖分別實(shí)現(xiàn)的線程安全的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);//初始化數(shù)據(jù)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);//初始化數(shù)據(jù)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");
結(jié)果:
- RWLockList take 248msRWLockList take 255msRWLockList take 249msRWLockList take 224msSyncList take 351msSyncList take 367msSyncList take 315msSyncList take 323ms
可見讀寫鎖的確是優(yōu)于純碎的互斥鎖
總結(jié)
內(nèi)置鎖***優(yōu)點(diǎn)是簡潔易用,顯示鎖***優(yōu)點(diǎn)是功能豐富,所以能用內(nèi)置鎖就用內(nèi)置鎖,在內(nèi)置鎖功能不能滿足之時在考慮顯示鎖。
關(guān)于兩種鎖,目前接觸到的就是這么多,總結(jié)不到位之處,歡迎拍磚。