Java多线程之显示锁和内置锁总结详解
总结多线程之显示锁和内置锁
Java中具有通过Synchronized实现的内置锁,和ReentrantLock实现的显示锁,这两种锁各有各的好处,算是互有补充,这篇文章就是做一个总结。
*Synchronized*
内置锁获得锁和释放锁是隐式的,进入synchronized修饰的代码就获得锁,走出相应的代码就释放锁。
synchronized(list){//获得锁 list.append(); list.count(); }//释放锁
通信
与Synchronized配套使用的通信方法通常有wait(),notify()。
wait()方法会立即释放当前锁,并进入等待状态,等待到相应的notify并重新获得锁过后才能继续执行;notify()不会立刻立刻释放锁,必须要等notify()所在线程执行完synchronized块中的所有代码才会释放。用如下代码来进行验证:
publicstaticvoidmain(String[]args){ Listlist=newLinkedList(); Threadr=newThread(newReadList(list)); Threadw=newThread(newWriteList(list)); r.start(); w.start(); } classReadListimplementsRunnable{ privateListlist; publicReadList(Listlist){ this.list=list; } @Override publicvoidrun(){ System.out.println("ReadListbeginat"+System.currentTimeMillis()); synchronized(list){ try{ Thread.sleep(1000); System.out.println("list.wait()beginat"+System.currentTimeMillis()); list.wait(); System.out.println("list.wait()endat"+System.currentTimeMillis()); } catch(InterruptedExceptione){ e.printStackTrace(); } } System.out.println("ReadListendat"+System.currentTimeMillis()); } } classWriteListimplementsRunnable{ privateListlist; publicWriteList(Listlist){ this.list=list; } @Override publicvoidrun(){ System.out.println("WriteListbeginat"+System.currentTimeMillis()); synchronized(list){ System.out.println("getlockat"+System.currentTimeMillis()); list.notify(); System.out.println("list.notify()at"+System.currentTimeMillis()); try{ Thread.sleep(2000); } catch(InterruptedExceptione){ e.printStackTrace(); } System.out.println("getoutofblockat"+System.currentTimeMillis()); } System.out.println("WriteListendat"+System.currentTimeMillis()); } }
运行结果:
ReadListbeginat1493650526582 WriteListbeginat1493650526582 list.wait()beginat1493650527584 getlockat1493650527584 list.notify()at1493650527584 getoutofblockat1493650529584 WriteListendat1493650529584 list.wait()endat1493650529584 ReadListendat1493650529584
可见读线程开始运行,开始wait过后,写线程才获得锁;写线程走出同步块而不是notify过后,读线程才wait结束,亦即获得锁。所以notify不会释放锁,wait会释放锁。值得一提的是,notifyall()会通知等待队列中的所有线程。
编码
编码模式比较简单,单一,不必显示的获得锁,释放锁,能降低因粗心忘记释放锁的错误。使用模式如下:
synchronized(object){ }
灵活性
1.内置锁在进入同步块时,采取的是无限等待的策略,一旦开始等待,就既不能中断也不能取消,容易产生饥饿与死锁的问题
2.在线程调用notify方法时,会随机选择相应对象的等待队列的一个线程将其唤醒,而不是按照FIFO的方式,如果有强烈的公平性要求,比如FIFO就无法满足
性能
Synchronized在JDK1.5及之前性能(主要指吞吐率)比较差,扩展性也不如ReentrantLock。但是JDK1.6以后,修改了管理内置锁的算法,使得Synchronized和标准的ReentrantLock性能差别不大。
*ReentrantLock*
ReentrantLock是显示锁,需要显示进行lock以及unlock操作。
通信
与ReentrantLock搭配的通行方式是Condition,如下:
privateLocklock=newReentrantLock(); privateConditioncondition=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中就使用了这个特性:
publicArrayBlockingQueue(intcapacity,booleanfair){ if(capacity<=0) thrownewIllegalArgumentException(); this.items=newObject[capacity]; lock=newReentrantLock(fair); notEmpty=lock.newCondition(); notFull=lock.newCondition(); } publicvoidput(Ee)throwsInterruptedException{ checkNotNull(e); finalReentrantLocklock=this.lock; lock.lockInterruptibly(); try{ while(count==items.length) notFull.await(); enqueue(e); }finally{ lock.unlock(); } } publicEtake()throwsInterruptedException{ finalReentrantLocklock=this.lock; lock.lockInterruptibly(); try{ while(count==0) notEmpty.await(); returndequeue(); }finally{ lock.unlock(); } } privatevoidenqueue(Ex){ //assertlock.getHoldCount()==1; //assertitems[putIndex]==null; finalObject[]items=this.items; items[putIndex]=x; if(++putIndex==items.length) putIndex=0; count++; notEmpty.signal(); } privateEdequeue(){ //assertlock.getHoldCount()==1; //assertitems[takeIndex]!=null; finalObject[]items=this.items; @SuppressWarnings("unchecked") Ex=(E)items[takeIndex]; items[takeIndex]=null; if(++takeIndex==items.length) takeIndex=0; count--; if(itrs!=null) itrs.elementDequeued(); notFull.signal(); returnx; }
编码
Locklock=newReentrantLock(); lock.lock(); try{ }finally{ lock.unlock(); }
相比于Synchronized要复杂一些,而且一定要记得在finally中释放锁而不是其他地方,这样才能保证即使出了异常也能释放锁。
灵活性
1.lock.lockInterruptibly()可以使得线程在等待锁是支持响应中断;lock.tryLock()可以使得线程在等待一段时间过后如果还未获得锁就停止等待而非一直等待。有了这两种机制就可以更好的制定获得锁的重试机制,而非盲目一直等待,可以更好的避免饥饿和死锁问题
2.ReentrantLock可以成为公平锁(非默认的),所谓公平锁就是锁的等待队列的FIFO,不过公平锁会带来性能消耗,如果不是必须的不建议使用。这和CPU对指令进行重排序的理由是相似的,如果强行的按照代码的书写顺序来执行指令,就会浪费许多时钟周期,达不到最大利用率
性能
虽然Synchronized和标准的ReentrantLock性能差别不大,但是ReentrantLock还提供了一种非互斥的读写锁,
也就是不强制每次最多只有一个线程能持有锁,它会避免“读/写”冲突,“写/写”冲突,但是不会排除“读/读”冲突,
因为“读/读”并不影响数据的完整性,所以可以多个读线程同时持有锁,这样在读写比较高的情况下,性能会有很大的提升。
下面用两种锁分别实现的线程安全的linkedlist:
classRWLockList{ //读写锁 privateListlist; privatefinalReadWriteLocklock=newReentrantReadWriteLock(); privatefinalLockreadLock=lock.readLock(); privatefinalLockwriteLock=lock.writeLock(); publicRWLockList(Listlist){ this.list=list; } publicintget(intk){ readLock.lock(); try{ return(int)list.get(k); } finally{ readLock.unlock(); } } publicvoidput(intvalue){ writeLock.lock(); try{ list.add(value); } finally{ writeLock.unlock(); } } } classSyncList{ privateListlist; publicSyncList(Listlist){ this.list=list; } publicsynchronizedintget(intk){ return(int)list.get(k); } publicsynchronizedvoidput(intvalue){ list.add(value); } }
读写锁测试代码:
Listlist=newLinkedList(); for(inti=0;i<10000;i++){ list.add(i); } RWLockListrwLockList=newRWLockList(list); //初始化数据 Threadwriter=newThread(newRunnable(){ @Override publicvoidrun(){ for(inti=0;i<10000;i++){ rwLockList.put(i); } } } ); Threadreader1=newThread(newRunnable(){ @Override publicvoidrun(){ for(inti=0;i<10000;i++){ rwLockList.get(i); } } } ); Threadreader2=newThread(newRunnable(){ @Override publicvoidrun(){ for(inti=0;i<10000;i++){ rwLockList.get(i); } } } ); longbegin=System.currentTimeMillis(); writer.start(); reader1.start(); reader2.start(); try{ writer.join(); reader1.join(); reader2.join(); } catch(InterruptedExceptione){ e.printStackTrace(); } System.out.println("RWLockListtake"+(System.currentTimeMillis()-begin)+"ms");
同步锁测试代码:
Listlist=newLinkedList(); for(inti=0;i<10000;i++){ list.add(i); } SyncListsyncList=newSyncList(list);//初始化数据 ThreadwriterS=newThread(newRunnable(){ @Override publicvoidrun(){ for(inti=0;i<10000;i++){ syncList.put(i); } } }); Threadreader1S=newThread(newRunnable(){ @Override publicvoidrun(){ for(inti=0;i<10000;i++){ syncList.get(i); } } }); Threadreader2S=newThread(newRunnable(){ @Override publicvoidrun(){ for(inti=0;i<10000;i++){ syncList.get(i); } } }); longbegin1=System.currentTimeMillis(); writerS.start();reader1S.start();reader2S.start(); try{ writerS.join(); reader1S.join(); reader2S.join(); }catch(InterruptedExceptione){ e.printStackTrace(); } System.out.println("SyncListtake"+(System.currentTimeMillis()-begin1)+"ms");
结果:
RWLockListtake248ms RWLockListtake255ms RWLockListtake249ms RWLockListtake224ms SyncListtake351ms SyncListtake367ms SyncListtake315ms SyncListtake323ms
可见读写锁的确是优于纯碎的互斥锁
总结
内置锁最大优点是简洁易用,显示锁最大优点是功能丰富,所以能用内置锁就用内置锁,在内置锁功能不能满足之时在考虑显示锁。
以上就是本文关于Java多线程之显示锁和内置锁总结详解的全部内容,希望对大家有所帮助。感兴趣的朋友可以继续参阅本站:
Java多线程中断机制三种方法及示例
浅谈Java多线程的优点及代码示例
Java利用future及时获取多线程运行结果
如有不足之处,欢迎留言指出。