java多线程(并发)夯实之路- ReentrantLock深入浅出(解决哲学家问题)

ReentrantLock

特点

相对于synchroized的特点:

可中断

可设置超时时间

可阻塞为公平锁

支持多个条件变量

与synchroized相同,支持可重入

可重入:线程获得了一把锁,可以再次获得锁

不可重入:线程获得了一把锁,不能再次获得锁

lockInterruptibly

reentrantLock.lockInterruptibly()方法:线程尝试获取lock对象锁,其他线程已经获得lock锁时该线程进入阻塞队列,可以被其他线程用interrupt方法打断,防止无限制的等待,避免死锁,如下图,抛出异常返回:

tryLock

reentrantLock.lock()方法不可打断

reentrantLock.tryLock()方法尝试获取锁,获取到了返回真,否则返回假(不会等待)。可以设置获取锁的超时时间(被interrupt打断会抛异常)

锁超时:

公平锁

reentranLock默认是不公平的

设为公平锁:根据进入阻塞队列的顺序决定谁先获得锁

公平锁一般没有必要,会降低并发度,使用tryLock更好,也能解决饥饿问题

条件变量(await和signal)

synchronized中也有条件变量,即waitSet  

ReentrantLock支持多个条件变量,相当于可以有多个休息室

使用await方法前需要先获得锁,方法执行之后会释放锁,进入condtionObject等待,被唤醒(打断,超时)重新竞争锁,成功获得锁后继续执行。

使用condition.signal或condition.signalAll方法唤醒条件变量(休息室)内随机一个或全部线程

ReentrantLock解决哲学家就餐问题

哲学家就餐问题

该情景可能产生死锁,可能每个哲学家都拿着左手的餐叉,永远都在等右边的餐叉(或者相反)。

活跃性问题

多把锁

可以使用多把锁将锁的粒度细分

好处:可以增强并发度

坏处:如果一个线程需要同时获得多把锁,就容易发生死锁

活跃性-死锁

如果一个线程需要同时获得多把锁,就容易发生死锁 t1线程获得了A对象的锁,接下来想获得B对象的锁 t2线程获得了B对象的锁,接下来想获得A对象的锁

这样就发生了死锁

定位死锁:可以使用jconsole工具,或者用jps定位进程id,再用jstack定位死锁

活跃性-活锁

如果两个线程互相改变对方的结束条件,最后谁也无法结束,就说明产生了活锁,如

活锁是因为两个线程互相改变了对方的结束条件导致CPU一直在运行,死锁是两个线程互相持有对方想要的锁,导致双方都阻塞无法运行。

解决活锁:使两个线程交错运行,可以添加随机的睡眠时间

活跃性-饥饿

一个线程的优先级太低,始终得不到CPU的调度,也不能够结束(读写锁会遇到饥饿问题)

解决死锁:顺序加锁(哲学家就餐中可以规定优先获取序号小的筷子,从小到大获取)


但是顺序加锁容易导致饥饿问题,如哲学家就餐问题会有死锁产生,用顺序加锁解决死锁问题后,会出现饥饿问题(一些哲学家总是优先抢到锁,一直进餐,导致其他哲学家一直不能进餐)

饥饿问题可以用ReentrantLock来解决

解决哲学家就餐问题:

1.顺序加锁解决死锁

改为

2. ReentrantLock解决饥饿

改为

ReentrantLock原理

默认为非公平锁实现

加锁成功:没有竞争时直接获得锁,state值设为1。

加锁失败:有竞争时,cas修改state值失败,进入tryAcquire逻辑,结果失败。接下来进入 addWaiter逻辑,构造Node队列(Node的waitStatus为0默认为正常状态,第一个Node为Dummy(哑元)或哨兵,用来占位,并不关联线程,waitStatus为-1将有责任唤醒后面的节点)。

然后线程进入acquireQueue逻辑,acquireQueue会在第一个死循环中不断尝试获得锁,失败后进入park阻塞:如果自己紧邻着head(排第二位),那么再次tryAcquire,如果失败,进入 shouldParkAferFiledAcquire逻辑,将前驱node,即head的waitStatus改为-1,返回false。再次回到 acquireQueue逻辑,如果head的waitStatus为-1,返回true

进入parkAndCheckInterrupt,线程park

有多个线程竞争失败

Thread-0释放锁,进入tryRelease流程,如果成功,设置exclusiveOwnerThread为null,state=0当前队列不为null,head的waitSates=-1,进入unparkSuccessor流程,找到当前离head最近的node,unpark(Thread-1),如果加锁成功设置exclusiveOwnerThread为Thread-1,state=1,head指向之前的  Thread-1的node,该node清空Thread。原来的head连接的node从链表断开,可被垃圾回收

如果这时有其它线程来竞争(非公平的体现),并且被它先获得了锁,设置了 exclusiveOwnerThread和state,Thread-1再次进入acquireQueue流程,进入park阻塞

可重入原理

锁重入:如果已经获得了锁,线程还是当前线程,表示发生锁重入,state++释放锁:state--,直到state为0才释放锁

可打断原理

不可打断模式:park后parkAndCheckInterrupt打断仍会驻留在AQS队列中,获得锁后才能继续运行再被打断(打断标记设为真)

可打断模式:调用了aquireInterruptibly方法,被打断会抛出异常,不会再次进入for循环,停止等

待锁

公平锁实现原理

非公平锁:不去检查AQS队列,直接尝试cas获得锁

公平锁:先检查AQS队列是否有前驱节点,如果队列中有第二个节点,并且第二个节点不是当前线程,就不能获取锁

条件变量实现原理

await流程:当前线程持有锁,调用await方法,进入CondtionObject的addCondtionWaiter流程,创建新node状态为2(Node.CONDITON),关联该线程,加入等待队列尾部。接下来进入fullyRelease流程(state减为0为止),释放同步器上的锁(之后park阻塞),AQS队列中的下一个节点去竞争锁,如果没有其它竞争线程则获得锁

single流程:检查调用single方法的线程是不是锁的持有者,不是则抛异常,然后检查条件变量的第一个节点,不为空调用doSingle方法,没有下一个节点把lastWaiter设为null,有就把fisrtWaiter设为下一个节点,然后执行transferForSingle流程,将node加入AQS队尾,将线程waitStatus改为0,将前一个节点 waitStatus改为-1(transferForSingle失败,如果firstWaiter有下一个节点,会尝试唤醒这个节点)。最后持有锁的线程释放锁,进入unlock流程

  • 18
    点赞
  • 24
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值