目录
一、Lock类层次结构及相关API
1、Lock类层级结构
ReentrantLock和ReentrantReadWriteLock都是java.util.concurrent
并发包下的工具类,ReentrantLock实现了Lock接口,ReentrantReadWriteLock实现了ReadWriteLock接口,而其中的ReadLock和WriteLock又实现了Lock接口。
2、Lock接口相关API
为了区别synchronized和ReentrantLock,我们先了解一下Lock接口相关的API。
结论:
1、lock()最常用。
2、lockInterruptibly()方法一般更昂贵,有的impl可能没有实现lockInterruptibly,只有真的需要响应中断时才使用。
3、关于Condition
Object中的wait()
、notify()
、notifyAll()
只能和synchronized
关键字配合使用,可以唤醒一个或全部线程。Condition需要与Lock配合使用,提供多个等待集合,更精确的控制。
备注:如果说Lock代替了同步代码块或同步方法的加解锁逻辑,那么Condition则是代替了Object的等待和唤醒逻辑。
二、synchronized VS Lock
1、synchronized实现的锁优缺点
我们先说说synchronized实现的平台级锁的优点:
- 使用简单,语义清晰,在方法上加上synchronized关键字或者使用同步代码块即可。
- 由JVM提供,提供了多种优化方案,如锁粗化、锁消除、偏向锁、轻量级锁、重量级锁等,关于这些优化特性请参考:Java面试题之synchronized关键字原理以及锁相关。
- 锁的释放由JVM完成,不用人工干预,也降低了死锁的可能性。
再说说它的缺点:
无法实现一些锁的高级功能,如超时锁
、中断锁
、读写锁
、共享锁
、公平锁
。
2、Lock实现的锁优缺点
Lock实现的锁主要弥补了synchronized的缺点,比如上面提到的锁的高级功能,如超时锁
、中断锁
、读写锁
、共享锁
、公平锁
这些。
再说一下它的缺点:
- 需手动释放锁,新手使用不当可能造成死锁。
- 没有synchronized实现的锁那么多优化项。
三、手撸一把简单的ReentrantLock
1、ReentrantLock实现简单流程
先介绍一下一些关键属性,如下:
waiters
代表锁池,说白了就是抢锁失败线程的等待队列。owner
代表成功获取到锁的线程。count
用来标记锁的可重入次数。
先描述下加锁流程:
- 如果可重入次数为0代表锁还没有被任何线程持有,这时可以通过
CAS(0, count + 1)
操作进行抢锁。 - 如果可重入次数不为0,则判断当前抢锁的线程是不是持有锁的线程,如果是则将可重入次数+1即可。
- 如果如上两种条件都不满足,则直接算抢锁失败。
- 抢锁失败的线程直接进入等待队列,并阻塞等待。
再描述下解锁流程:
- 如果调用解锁方法的线程不是持有锁的线程,则抛出
IllegalMonitorStateException
,否则第2步。 - 将可重入次数-1,如果可重入次数为0则代表解锁成功。
- 解锁成功后唤醒队列头部等待的抢锁线程。
2、代码示例
public class NicksReentrantLock implements Lock {
// 用来标识哪个线程获取到锁
private Thread owner;
// 重入次数
private AtomicInteger counter = new AtomicInteger(0);
// 等待队列
private BlockingQueue<Thread> waiters = new LinkedBlockingQueue<>();
@Override
public void lock() {
if (!tryLock()) {
waiters.offer(Thread.currentThread());
// 循环解决伪唤醒问题
while (true) {
// 如果队列头部是当前线程说明可以抢锁
if (Thread.currentThread() == waiters.peek()) {
// 若抢锁成功则出队列
if (tryLock()) {
waiters.poll();
return;
}
}
// 若当前线程不在队列头部或者抢锁失败则挂起
LockSupport.park();
}
}
}
@Override
public void lockInterruptibly() throws InterruptedException {
}
@Override
public boolean tryLock() {
int ct = counter.get();
if (ct == 0) {
// 重入次数为0说明当前线程可以通过CAS操作获取锁
if (counter.compareAndSet(0, ct + 1)) {
owner = Thread.currentThread();
return true;
}
return false;
}
// 不为0则判断获取锁的线程是否是当前线程,如果是当前线程,则将可重入次数加1
if (owner == Thread.currentThread()) {
counter.set(ct + 1);
return true;
}
return false;
}
@Override
public boolean tryLock(long time, TimeUnit unit) throws InterruptedException {
return false;
}
@Override
public void unlock() {
// 解锁成功应该唤醒队列头部的线程
if (tryUnlock()) {
Optional.ofNullable(waiters.peek()).ifPresent(LockSupport::unpark);
}
}
public boolean tryUnlock() {
// 如果当前解锁的线程不是获取到锁的线程则抛异常
if (Thread.currentThread() != owner) {
throw new IllegalMonitorStateException();
}
int ct = counter.get();
int nextCt = ct - 1;
counter.set(nextCt);
if (nextCt == 0) {
owner = null;
return true;
}
return false;
}
@Override
public Condition newCondition() {
return null;
}
}
3、测试用例
public class Test {
private static int count = 0;
public static void main(String[] args) {
NicksReentrantLock lock = new NicksReentrantLock();
for (int index = 0; index < 10000; index++) {
new Thread(() -> {
try {
lock.lock();
count++;
} finally {
lock.unlock();
}
}).start();
}
LockSupport.parkNanos(1000 * 1000 * 1000);
System.out.println("累加后的值为:" + count);
}
}
备注:控制台输出为:
累加后的值为:10000
。