当前位置: 首页 > 科技观察

Java多线程的内置锁和显示锁

时间:2023-03-12 17:10:40 科技观察

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));线程w=newThread(newWriteList(list));r.开始();w.start();}类ReadList实现Runnable{privateListlist;publicReadList(Listlist){this.list=list;}@Overridepublicvoidrun(){System.out.println("ReadListbeginat"+System.currentTimeMillis());同步(列表){尝试{Thread.sleep(1000);System.out.println("list.wait()开始于"+System.currentTimeMillis());列表.等待();System.out.println("list.wait()结束于"+System.currentTimeMillis());}catch(InterruptedExceptione){e.printStackTrace();}}System.out.println("ReadList结束于"+System.currentTimeMillis());}}类WriteList实现Runnable{私有列表列表;publicWriteList(Listlist){this.list=list;}@Overridepublicvoidrun(){System.out.println("WriteList开始于"+System.currentTimeMillis());synchronized(list){System.out.println("在+System.currentTimeMillis()获取锁);列表.通知();System.out.println("list.notify()在"+System.currentTimeMillis());尝试{Thread.sleep(2000);}catch(InterruptedExceptione){e.printStackTrace();}System.out.println("在"+System.currentTimeMillis())处退出阻塞;}System.out.println("WriteList结束于"+System.currentTimeMillis());}}运行结果ReadListbeginat1493650526582WriteListbeginat1493650526582list.wait()beginat1493650527584getlockat1493650527584list.notify()at1493650527584getoutofblockat149356854WriteListendat1493650529584list.wait()endat1493650529584ReadListendat1493650529584可以看出读线程开始运行,写线程等待开始后获取锁;read线程在write线程退出同步块后结束wait而不是notify,即获得锁所以notify不会释放锁,wait会释放锁。值得一提的是notifyall()会通知等待队列中的所有线程。编码编码方式比较简单单一,不需要显示锁和释放锁,可以减少因为粗心忘记释放锁的错误。使用方式如下:synchronized(object){}进入同步块时,内置锁采用一直等待的策略。一旦开始等待,既不能被中断也不能被取消,容易出现饥饿和死锁问题。当一个线程调用notify方法时,会在对应对象的等待队列中随机选择一个线程将其唤醒,而不是遵循FIFO方法。如果有很强的公平性要求,比如先进先出,则性能无法满足。Synchronized在JDK1.5及之前的性能(主要指吞吐率)比较差,扩展性不如ReentrantLock。但是在JDK1.6之后,对管理内置锁的算法进行了修改,使得Synchronized和标准ReentrantLock的性能相差不大。ReentrantLockReentrantLock是显示锁,需要显示才能进行加锁和解锁操作。ReentrantLock常用的通信方式是Condition,如下:privateLocklock=newReentrantLock();私有条件条件=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){如果(容量<=0)抛出新的IllegalArgumentException();this.items=newObject[容量];锁=新的ReentrantLock(公平);notEmpty=lock.newCondition();未满=锁定。newCondition();}publicvoidput(Ee)throwsInterruptedException{checkNotNull(e);finalReentrantLocklock=this.lock;锁.lockInterruptibly();尝试{while(count==items.length)notFull.await();入队(e);}最后{lock.unlock();}}publicEtake()throwsInterruptedException{finalReentrantLocklock=this.lock;锁.lockInterruptibly();尝试{while(count==0)notEmpty。等待();返回出队();}最后{lock.unlock();}}privatevoidenqueue(Ex){//断言lock.getHoldCou新台币()==1;//断言items[putIndex]==null;最终对象[]items=this.items;项目[putIndex]=x;如果(++putIndex==items.length)putIndex=0;计数++;notEmpty.signal();}privateEdequeue(){//assertlock.getHoldCount()==1;//断言items[takeIndex]!=null;最终对象[]items=this.items;@SuppressWarnings("unchecked")Ex=(E)items[takeIndex];项目[takeIndex]=null;如果(++takeIndex==items.length)takeIndex=0;数数-;if(itrs!=null)itrs.elementDequeued();notFull.signal();returnx;}CodingLocklock=newReentrantLock();lock.lock();try{}finally{lock.unlock();}比Synchronized更复杂,而且一定要记得在finally中释放锁,而不是其他地方,从而保证即使发生异常也能释放锁。灵活性lock.lockInterruptibly()可以让等待锁的线程支持响应中断;线程等待一段时间后,如果还没有获取到锁,就停止等待,而不是一直等待。有了这两种机制,可以更好的制定获取锁的重试机制,而不是盲目等待,可以更好的避免饥饿和死锁问题。ReentrantLock可以成为公平锁(非默认)。所谓公平锁就是锁的等待队列的FIFO,但是公平锁会带来性能消耗,非必要不建议使用。这类似于CPU重新排序指令的原因。如果强行按照代码编写的顺序执行指令,会浪费很多时钟周期,达不到最大的利用性能。Synchronized和standardReentrantLock虽然性能差别不大,但是ReentrantLock也提供了一种非互斥的读写锁,即不强制一次最多一个线程持有锁,它会避免“read/write”冲突,“write/write”冲突,但是不排除“read/read”冲突,因为“read/read”不影响数据的完整性,所以多个读线程可以持有锁同时,这样在读写比较高的情况下,性能会有很大的提升。很大的推动力。下面用两种锁分别实现的线路安全的链表:classRWLockList{//读写锁privateListlist;privatefinalReadWriteLocklock=newReentrantReadWriteLock();privatefinalLockreadLock=lock.readLock();privatefinalLockwriteLock=lock.writeLock();publicRWLockList(Listlist){this.list=list;}publicintget(intk){readLock.lock();尝试{返回(int)list.get(k);}最后{readLock.unlock();}}publicvoidput(intvalue){writeLock.lock();试试{list.add(value);}最后{writeLock.unlock();}}}classSyncList{私有列表列表;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(){@Overridepublicvoidrun(){for(inti=0;i<10000;i++){rwLockList.put(i);}}});Threadreader1=newThread(newRunnable(){@Overridepublicvoidrun(){for(inti=0;i<10000;i++){rwLockList.get(i);}}});Threadreader2=newThread(newRunnable(){@Overridepublicvoidrun(){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(){@Overridepublicvoidrun(){for(inti=0;i<10000;i++){syncList.put(i);}}});Threadreader1S=newThread(newRunnable(){@Overridepublicvoidrun(){for(inti=0;i<10000;i++){syncList.get(i);}}});Threadreader2S=newThread(newRunnable(){@Overridepublicvoidrun(){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");结果:RWLockListtake248msRWLockList占用255msRWLockList占用249msRWLockList占用224msSyncList占用351msSyncList占用367msSyncList占用315msSyncList需要323ms可以看出,读写锁确实比纯断互斥锁要好。总结内置锁最大的优点就是简单易用。当内置锁功能不尽如人意时,我会考虑显示锁。关于这两种锁,目前接触的就这么多了。如果总结不到位,欢迎拍砖。