Java 重入锁和读写锁的具体使用
重入锁
重入锁ReentrantLock,顾名思义,就是支持重进入的锁,它表示该锁能够支持一个线程对资源的重复加锁。除此之外,该锁还支持获取锁时的公平和非公平性选择
所谓不支持重进入,可以考虑如下场景:当一个线程调用lock()方法获取锁之后,如果再次调用lock()方法,则该线程将会被自己阻塞,原因是在调用tryAcquire(intacquires)方法时会返回false,从而导致线程阻塞
synchronize关键字隐式的支持重进入,比如一个synchronize修饰的递归方法,在方法执行时,执行线程在获取锁之后仍能连续多次地获得该锁。ReentrantLock虽然不能像synchronize关键字一样支持隐式的重进入,但在调用lock()方法时,已经获得锁的线程,能够再次调用lock()方法获取锁而不被阻塞
1.实现重进入
重进入特性的实现需要解决以下两个问题:
线程再次获取锁
锁需要去识别获取锁的线程是否为当前占据锁的线程,如果是,则再次成功获取
锁的最终释放
线程重复n次获取锁,随后在第n次释放该锁后,其他线程能获取到锁。实现此功能,理应考虑使用计数
ReentrantLock通过组合自定义同步器来实现锁的获取与释放,以非公平锁实现为例,获取同步状态的代码如下所示,主要是增加了再次获取同步状态的处理逻辑
finalbooleannonfairTryAcquire(intacquires){ finalThreadcurrent=Thread.currentThread(); intc=getState(); if(c==0){ if(compareAndSetState(0,acquires)){ setExclusiveOwnerThread(current); returntrue; } } //判断当前线程是否为获取锁的线程 elseif(current==getExclusiveOwnerThread()){ //将同步值进行增加,并返回true intnextc=c+acquires; if(nextc<0) thrownewError("Maximumlockcountexceeded"); setState(nextc); returntrue; } returnfalse; }
考虑到成功获取锁的线程再次获取锁,只是增加同步状态值,这也就要求ReentrantLock在释放同步状态时减少同步状态值,该方法代码如下:
protectedfinalbooleantryRelease(intreleases){ //减少状态值 intc=getState()-releases; if(Thread.currentThread()!=getExclusiveOwnerThread()) thrownewIllegalMonitorStateException(); booleanfree=false; //当同步状态为0,将占有线程设为null,并返回true,表示释放成功 if(c==0){ free=true; setExclusiveOwnerThread(null); } setState(c); returnfree; }
2.公平与非公平获取锁的区别
如果一个锁是公平的,那么锁的获取顺序就应该符合请求的绝对时间顺序,也即FIFO。回顾上一节,非公平锁只要CAS设置同步状态成功,即表示当前线程获取了锁,而公平锁则不同,代码如下:
protectedfinalbooleantryAcquire(intacquires){ finalThreadcurrent=Thread.currentThread(); intc=getState(); if(c==0){ /* *唯一不同的就是判断条件多了hasQueuedPredecessors() *该方法用来判断当前节点是否有前驱节点 *如果该方法返回true,表示有线程比当前线程更早请求获取锁 *因此需要等待前驱线程释放锁之后才能继续获取锁 */ if(!hasQueuedPredecessors()&&compareAndSetState(0,acquires)){ setExclusiveOwnerThread(current); returntrue; } } elseif(current==getExclusiveOwnerThread()){ intnextc=c+acquires; if(nextc<0) thrownewError("Maximumlockcountexceeded"); setState(nextc); returntrue; } returnfalse; }
读写锁
之前提到的锁基本都是排它锁,同一时刻只允许一个线程访问,而读写锁在同一时刻可以允许多个线程访问,但在写线程访问时,所有的读线程和其他写线程均被阻塞。读写锁维护了一对锁,一个读锁和一个写锁,通过分离读锁和写锁,使得并发性相比一般的排它锁有了很大提升
1.接口示例
下面通过缓存示例说明读写锁的使用方式
publicclassCache{ staticMapmap=newHashMap<>(); staticReentrantReadWriteLockrwl=newReentrantReadWriteLock(); staticLockr=rwl.readLock(); staticLockw=rwl.writeLock(); /** *获取一个key对应的value */ publicstaticObjectget(Stringkey){ r.lock(); try{ returnmap.get(key); }finally{ r.unlock(); } } /** *设置key对应的value,并返回旧的value */ publicstaticObjectput(Stringkey,Objectvalue){ w.lock(); try{ returnmap.put(key,value); }finally{ w.unlock(); } } /** *清空所有的内容 */ publicstaticvoidclear(){ w.lock(); try{ map.clear(); }finally{ w.unlock(); } } }
2.读写状态的设计
读写锁同样依赖自定义同步器来实现功能,而读写状态就是其同步器状态。读写锁的自定义同步器需要在同步状态(一个整型变量)上维护多个读线程和一个写线程的状态,为此需要读写锁将变量切分成两部分,高16位表示读,低16位表示写
上图表示一个线程已经获取了写锁,且重进入了两次,同时也连续两次获取了读锁。通过位运算可以迅速确定读和写各自的状态,假设当前同步状态值为S,则:
- 写状态等于S&0x0000FFFF(将高16位全部抹去)
- 读状态等于S>>>16(无符号右移16位)
- 当写状态增加1时,等于S+1
- 当读状态增加1时,等于S+(1<<6),也就是S+0x00010000
根据状态的划分能得出一个结论:S不等于0时,当写状态(S&0x0000FFFF)等于0时,则读状态(S>>>16)大于0,即读锁已被获取
3.写锁的获取与释放
写锁是一个支持重进入的排它锁。如果当前线程已经获取了写锁,则增加写状态。如果当前线程在获取写锁时,读锁已被获取,或者该线程不是获取写锁的线程,则当前线程进入等待状态,获取写锁的代码如下:
protectedfinalbooleantryAcquire(intacquires){ Threadcurrent=Thread.currentThread(); intc=getState(); //exclusiveCount方法会用c&0x0000FFFF,即得出写状态个数 intw=exclusiveCount(c); if(c!=0){ //根据上面提到的推论,c不等于0,而w等于0,证明存在读锁 //当前线程也不是获取了写锁的线程 if(w==0||current!=getExclusiveOwnerThread()) returnfalse; if(w+exclusiveCount(acquires)>MAX_COUNT) thrownewError("Maximumlockcountexceeded"); setState(c+acquires); returntrue; } if(writerShouldBlock()||!compareAndSetState(c,c+acquires)) returnfalse; setExclusiveOwnerThread(current); returntrue; }
写锁的每次释放均会减少写状态,当写状态为0时表示写锁已被释放,从而等待的读写线程能够继续访问读写锁,同时前次写线程的修改对后续读写线程可见
4.读锁的获取与释放
读锁是一个支持重进入的共享锁,它能被多个线程同时获取,在没有其他写线程访问时,读锁总能被成功获取,这里对获取读锁的代码做了简化:
protectedfinalinttryAcquireShared(intunused){ for(;;){ intc=getState(); intnextc=c+(1<<16); if(nextc读锁的每次释放均减少读状态,减少的值是1<<16
5.锁降级
锁降级指的是写锁降级成为读锁。如果当前线程拥有写锁,然后将其释放,最后再获取读锁,这种分段完成的过程不能称之为锁降级。锁降级是指把持住写锁,再获取读锁,随后释放写锁的过程
publicvoidprocessData(){ readLock.lock(); if(!update){ //必须先释放读锁 readLock.unlock(); //锁降级从写锁获取到开始 writeLock.lock(); try{ if(!update){ //准备数据的流程(略) update=true; } readLock.lock(); }finally{ writeLock.unlock(); } } try{ //使用数据的流程(略) }finally{ readLock.unlock(); } }上例中,当数据发生变更,则update(使用volatile修饰)被设置为false,此时所有访问processData方法的线程都能感知到变化,但只有一个线程能获取到写锁,其余线程会被阻塞在写锁的lock方法上。当前线程获取写锁完成数据准备之后,再次获取读锁,随后释放写锁,完成锁降级
到此这篇关于Java重入锁和读写锁的具体使用的文章就介绍到这了,更多相关Java重入锁和读写锁内容请搜索毛票票以前的文章或继续浏览下面的相关文章希望大家以后多多支持毛票票!
声明:本文内容来源于网络,版权归原作者所有,内容由互联网用户自发贡献自行上传,本网站不拥有所有权,未作人工编辑处理,也不承担相关法律责任。如果您发现有涉嫌版权的内容,欢迎发送邮件至:czq8825#qq.com(发邮件时,请将#更换为@)进行举报,并提供相关证据,一经查实,本站将立刻删除涉嫌侵权内容。