1.锁的进化2.ReentrantReadWriteLock锁降级3.比读写锁更快的锁-邮戳锁4.总结1.锁的一路进化当我们学习java锁,我们经历了以下四个锁进化的阶段:无锁→独占锁→读写锁→邮戳锁。无锁:我们刚开始学习写代码的时候,一定要写无锁代码。优点:执行效率高缺点:多线程乱序抓取导致数据错误。然后发现问题,学习了synchronized,reentrantlock。优点:序列化保证数据一致性缺点:所有操作都是互斥的,执行效率低。然后我们发现效率太低了。如果读线程占多数,写线程占少数,我们再学习ReentrantReadWriteLock:优点:读读共享,读写互斥,提高大面积的共享性能。缺点:读线程并没有永远结束,写线程将永远得不到锁(造成锁饥饿)。其实还有比读写锁更快的锁。StampedLock(下面我们会解释)优点:还允许在读过程中获取写锁,效率更高。缺点:不支持重入,不支持Condition,不支持中断。于是就有了下表:NoneLocksynchronized,reentrantlockReentrantReadWriteLockStampedLock优点执行效率高序列化保证数据一致性读写共享,读写互斥,提高大区共享性能也允许在读过程中获取写锁,这效率更高缺点多线程顺序抢夺导致数据错误所有操作都是互斥的,执行效率低。读线程并没有永远结束,写线程永远得不到锁(造成锁饥饿)。不支持重入、条件和中断。2.ReentrantReadWriteLock锁降级刚刚学习了JAVA并发编程——同步和锁升级。今天我们就来学习一下锁降级。我们先看看ReentrantReadWriteLock锁的定义:一个资源可以被多个读线程访问,也可以被一个写线程访问。也就是说:read-read不互斥,write-write互斥,read-write互斥,只有在读多写少的情况下,读写锁才能有更高的性能。什么是锁降级?锁降级:将写锁降级为读锁(就像linux文件的读写权限一样,写权限必须高于读权限)换句话说,我们可以lock.readLockwhilelock.writeLock();(),此时读锁会降级为写锁,反之,程序就会死锁。这样我们可能还是看不懂,直接用代码解释一下。importjava.util.concurrent.locks.ReentrantReadWriteLock;/***锁降级:按照获取写锁→获取读锁→释放写锁的顺序,写锁可以降级为读锁。**如果一个线程持有写锁,它仍然可以持有读锁而不释放写锁,即写锁降级为读锁。*/publicclassLockDownGradingDemo{publicstaticvoidmain(String[]args){ReentrantReadWriteLockreadWriteLock=newReentrantReadWriteLock();ReentrantReadWriteLock.ReadLockreadLock=readWriteLock.readLock();ReentrantReadWriteLock.WriteLockwriteLock.writeLock=readWrite();System.out.println("--------写作");readLock.lock();System.out.println("--------阅读");写锁.unlock();}}降级读写锁的目的:在高并发的情况下,为了让程序感知到我们修改了内容,我们先用读锁锁定结果,防止其他写线程进入,因为Reading是可以共享的,保证了这个变化的数据可见性。以下是ReentrantReadWriteLock的源代码节选:1代码中声明了一个volatilecacheValid变量,保证其可见性。2先获取读锁,如果缓存不可用,释放读锁,获取写锁,更改数据前再次检查cacheValid的值,修改数据,设置cacheValid为true,再获取读锁在释放写锁Lock之前;此时缓存中的数据可用,对缓存中的数据进行处理,最后释放读锁。这个过程是一个完整的锁降级过程,保证数据的可见性。如果违反了锁降级的步骤,如果当前线程C没有获取到读锁而是在修改了缓存中的数据后直接释放了写锁,那么假设此时另一个线程D获取了写锁并修改了数据时间,则C线程无法感知数据被修改,数据发生错误。如果按照锁降级的步骤,线程C在释放写锁之前先获取读锁,那么线程D在获取写锁的过程中会被阻塞,直到线程C完成数据处理过程释放读锁。这样就保证了返回的数据是本次更新后的数据,这种机制是专门为缓存设计的。3、比读写锁更快的锁——邮戳锁因为读写锁存在锁饥饿的问题。Lockhunger:如果有1000个线程,读999次,写1次,说明读线程长期占用锁,写线程长期获取不到锁。那么如何缓解锁饥饿问题呢?我们有以下解决方案:1)使用公平锁策略可以在一定程度上缓解这个问题,但是吞吐量不高2)使用邮戳锁解决这个问题,我们使用邮戳锁:SteamedLock有三种访问模式1)Reading(读模式):功能类似于ReentrantReadWriteLock读锁2)Writing(写模式):功能类似于ReentrantReadWriteLock写锁3)Optimisticreading(乐观读模式):无锁机制,类似到数据库中的乐观锁,支持读写Concurrency,乐观的是读的时候不会有人修改,如果修改了,就会实现为悲观读模式。//乐观读//我们用一开始获取的版本号来判断是否有人动过这条数据//然后如果有人修改了,则进行锁升级publicvoidtryOptimisticRead(){//得到一个乐观读标志第一个长戳=stampedLock.tryOptimisticRead();int结果=数字;//间隔为4秒。我们看好没有其他线程修改过number的值,就看判断了。System.out.println("stampedLock.validate4秒前的值(true没有修改,false有修改)"+"\t"+stampedLock.validate(stamp));对于(inti=1;i<4;i++){try{TimeUnit.SECONDS.sleep(1);}catch(InterruptedExceptione){e.printStackTrace();}System.out.println(Thread.currentThread().getName()+"\t正在读......"+i+"秒后stampedLock.validate值(true没有修改,false有修改)"+"\t"+stampedLock.validate(stamp));}if(!stampedLock.validate(stamp)){System.out.println("有人动了--------有写操作!");stamp=stampedLock.readLock();try{System.out.println("从乐观读升级到悲观读");结果=数字;System.out.println("通过获取成员变量值result获得的再悲观读锁:"+result);}catch(Exceptione){e.printStackTrace();}最后{stampedLock.unlockRead(戳);}}System.out.println(Thread.currentThread().getName()+"\tfinallyvalue:"+result);}4.总结一下我们这次学习的锁的演变过程,每种锁都有自己的优缺点。让我们看看上表。Lock-freesynchronized,reentrantlockReentrantReadWriteLockStampedLock优点执行效率高序列化保证数据一致性读写共享,读写互斥,提高大规模读进程的共享性能还允许写锁的获取介入,更高效的。缺点:多线程乱序抢夺导致数据错误。所有操作互斥,执行效率低。读线程并没有永远结束,写线程永远得不到锁(造成锁饥饿)。不支持Reentrant,不支持Condition,不支持中断
