Java多线程——Lock独占重入锁与读写锁详解
一、独占式重入锁——ReentrantLock
1、重入的实现原理
①重入概念:
表示能够对共享资源重复加锁,即当前线程再次获取锁时不会被阻塞。
ReentrantLock究竟是怎样实现可重入性的?
总结一下ReentrantLock实现可重入性:同一线程再次获取锁时计数自增,释放锁时计数自减直到等于0才释放成功
。
②重入的获取
如果该同步状态不为0,表示此时同步状态(锁)已被线程获取,再判断持有同步状态的线程是否是当前线程,如果是,同步状态再次+1并返回true,表示持有线程重入同步块。(类似于monitor)
执行流程:lock()->acquire()->tryAcquire()->nonfairTryAcquire()
final boolean nonfairTryAcquire(int acquires) {
final Thread current = Thread.currentThread();
int c = getState();
// 当前锁未被任何线程占用,当前线程可直接获取该锁,同步状态+1
if (c == 0) {
if (compareAndSetState(0, acquires)) {
setExclusiveOwnerThread(current);
return true;
}
}
// 体现可重入性:若当前线程是占有该锁的线程,则计数++
else if (current == getExclusiveOwnerThread()) {
int nextc = c + acquires;
if (nextc < 0) // overflow
throw new Error("Maximum lock count exceeded");
setState(nextc);
return true;
}
return false;
}
a)若当前状态==0(设置当前线程持有同步状态的线程)或者当前线程是持有同步状态的线程(同步状态+1),返回true,acquire()的tryAcquire()就不用继续判断了,lock也会直接将持有同步状态线程(当前线程)返回回去
b)若当前状态!=0或当前线程不是持有同步状态的线程,说明当前线程没有获取到同步状态,acquire()的tryAcquire()再次获取锁失败,继续接下来的流程(加入同步队列…)即可。
④重入的释放
当且仅当同步状态减为0并且持有线程为当前线程时表示锁被正确释放,否则调用setState()将减1后的状态设置为同步状态。若非持有锁线程调用了tryRelease()方法会抛出 IllegalMonitorStateException异常
执行流程:unlock()->release()->tryRelease()
protected final boolean tryRelease(int releases) {
// 同步状态-1
int c = getState() - releases;
if (Thread.currentThread() != getExclusiveOwnerThread())
throw new IllegalMonitorStateException();
boolean free = false;
if (c == 0) {
// 只有同步状态==0了,才说明锁可以正常释放
free = true;
setExclusiveOwnerThread(null);
}
// 否则,将同步状态设置为c(同步状态-1),返回false,锁不能释放
setState(c);
return free;
}
tryRelease()会判断当前同步状态-1后的值
a)若为0,返回true,说明同步状态为初始状态(锁被完全释放,唤醒其他线程竞争该锁)
b)若不为0,返回flase,说明同步状态不是初始状态(锁没有被完全释放)
可重入锁有什么特点?
2、可重入锁的特点
①在线程获取锁的时候,如果已经获取锁的线程是当前线程的话则直接再次获取成功;
②由于锁会被获取n次,那么只有锁在被释放同样的n次之后,该锁才算是完全释放成功。
什么是非公平锁?什么是公平锁?
3、公平锁OR非公平锁
①公平锁与非公平锁的概念
公平锁:锁的获取顺序符合时间上的顺序,即等待时间最长的线程最先获取锁
非公平锁:不是公平锁
②公平锁与非公平锁的特点
if (!hasQueuedPredecessors() &&
compareAndSetState(0, acquires))
a)公平锁:获取同步状态并且要是同步队列中的第⼀个节点
(通过hasQueuedPredecessors()实现)
b)非公平锁:只要获取了同步状态即成功获取锁(同步队列显得无意义了)
非公平锁与公平锁有什么区别?
③非公平锁(常用默认)与公平锁的对比
公平锁执行acquire()的第一步tryAcquire()尝试获取锁时,需要多加一个判断(hasQueuedPredecessors),判断当前节点在同步队列中是否存在前驱节点,若存在,则该节点绝对不会获取同步状态(后面的就不用执行了),若不存在,说明该节点是同步队列的首节点(等待最久了),才会有继续获取同步状态的可能。(从源头上扼杀获取同步状态的可能),而非公平锁的同步队列中的每一个结点都有可能获取到同步状态。
a)公平锁保证了获取到锁的线程一定是等待时间最长的线程,保证了请求资源时间上的绝对顺序,需要频繁的进行上下文切换,性能开销较大,效率较低。
b)非公平锁保证系统有更大的吞吐量(效率较高),但是会造成线程“饥饿现象”(有的线程可能永远无法获取到锁)
二、读写锁——ReentrantReadWriteLock
究竟什么是读写锁?写程序对应独占锁?而读程序对应共享锁?
1、读写锁的定义
读写锁允许同一时刻被多个读线程访问,但是在写线程访问时,所有的读线程和其他的写线程都会被阻塞。
读写锁维护了一个读锁和一个写锁,通过分离读锁和写锁,是并发性大大提高
①写线程-独占锁
能够获取到锁的前提条件
:没有任何读/写线程拿到锁
②读线程-共享锁
能够获取到锁的前提条件
:没有写线程拿到锁
为什么前提条件是这样设定?
读写锁要保证写锁的操作对读锁可见,若允许读锁在已被获取的情况下对写锁的获取,name正在运行的其他读线程就无法感知到当前写线程的操作
③当写锁被获取到,阻塞后续(非当前写操作线程)的读写操作。当读锁被获取到,阻塞后续(非当前写操作线程)的写操作,不阻塞读操作。
读锁与无锁到底是不是一回事
PS:读锁 != 无锁
:当有写线程写的时候,所有读线程都必须全部停止,但如果是无锁的话,其他线程就不会停(无锁的话,不同线程之间互不干扰)。
读写锁是怎么记录读锁和写锁的状态?
④同步状态的低16位表示写锁获取次数,高16位表示读锁获取次数
。
写锁的逻辑是怎么实现?
2、写锁-独占锁-WriteLock
①写锁获取逻辑
a)当读锁已被读线程获取或者写锁已被其他写线程获取,则写线程获取写锁失败;
b)否则,当前同步状态没有被任何读写线程获取,当前线程获取写锁成功并且支持重入。
②写锁注意事项
a)读写锁确保写锁的操作对读锁可见,不允许读锁在已被获取的情况下对写锁获取(正在运行的其他读线程无法感知到当前写线程的操作)
b)只有等待其他读线程都释放了读锁,写锁才能被当前线程获取,而写锁一旦被获取,则其他读写线程的后续访问均被阻塞。
③写锁的释放逻辑同独占式锁的释放(release)
读锁的逻辑是怎么实现?
3、读锁-共享式锁-tryAcquireShared()
当写锁被其他线程获取后,读锁获取失败,其他情况均可成功。
ReentrantReadWriteLock该如何应用?
ReentrantReadWriteLock中有许多操作读锁写锁的方法。
getReadLockCount():返回读锁被获取的次数(不等于线程数,可能一个线程多次获取读锁)
getReadHoldCount():当前线程获取读锁的次数(保存在ThreadLocal中)
isWriteLocked():写锁是否被获取
getWriteHoldCount():写锁被获取的次数
4、读写锁实例
如何线程安全的使用HashMap?
public class ReentrantReadWriteLockTest {
// HashMap是线程不安全的,使用读写锁,使得线程安全
static Map<String,Object> map = new HashMap<>();
static ReentrantReadWriteLock reentrantReadWriteLock
= new ReentrantReadWriteLock();
static Lock readLock = reentrantReadWriteLock.readLock();
static Lock writeLock = reentrantReadWriteLock.writeLock();
// 获取一个key对应的value——读锁
public static final Object get(String key){
readLock.lock();
try {
return map.get(key);
}finally {
readLock.unlock();
}
}
// 设置key所对应的value,并返回旧的value
public static final Object put(String key,Object value){
writeLock.lock();
try {
return map.put(key, value);
}finally {
writeLock.unlock();
}
}
}
5、读写锁相关事项
①读锁和写锁是两个锁,但同一个线程是可以拥有两把锁的。
②若当前同步状态为S
写状态获取 S=S+1 写状态释放 S=S-1 低16位(直接加减)
读状态获取 S=S+(1<<16) 读状态释放S=S-(1<<16) 高16位(左移16位)
③读写锁应用
应用于“缓存”的实现(操作系统内存也是个缓存-多读少写)
6、锁降级
锁降级到底指什么?
锁降级:当前线程把持住(当前拥有的)写锁,再获取到读锁,随后释放(先前拥有的)写锁的过程。
获取读锁是必要的吗?