Java面试题之synchronized平台级锁和Lock实现的锁区别

一、Lock类层次结构及相关API

1、Lock类层级结构

ReentrantLockReentrantReadWriteLock都是java.util.concurrent并发包下的工具类,ReentrantLock实现了Lock接口,ReentrantReadWriteLock实现了ReadWriteLock接口,而其中的ReadLockWriteLock又实现了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实现的平台级锁的优点:

  1. 使用简单,语义清晰,在方法上加上synchronized关键字或者使用同步代码块即可。
  2. 由JVM提供,提供了多种优化方案,如锁粗化、锁消除、偏向锁、轻量级锁、重量级锁等,关于这些优化特性请参考:Java面试题之synchronized关键字原理以及锁相关
  3. 锁的释放由JVM完成,不用人工干预,也降低了死锁的可能性。

再说说它的缺点:
无法实现一些锁的高级功能,如超时锁中断锁读写锁共享锁公平锁

2、Lock实现的锁优缺点

Lock实现的锁主要弥补了synchronized的缺点,比如上面提到的锁的高级功能,如超时锁中断锁读写锁共享锁公平锁这些。

再说一下它的缺点:

  1. 需手动释放锁,新手使用不当可能造成死锁。
  2. 没有synchronized实现的锁那么多优化项。


三、手撸一把简单的ReentrantLock

1、ReentrantLock实现简单流程

先介绍一下一些关键属性,如下:

  • waiters代表锁池,说白了就是抢锁失败线程的等待队列。
  • owner代表成功获取到锁的线程。
  • count用来标记锁的可重入次数。

在这里插入图片描述先描述下加锁流程:

  1. 如果可重入次数为0代表锁还没有被任何线程持有,这时可以通过CAS(0, count + 1)操作进行抢锁。
  2. 如果可重入次数不为0,则判断当前抢锁的线程是不是持有锁的线程,如果是则将可重入次数+1即可。
  3. 如果如上两种条件都不满足,则直接算抢锁失败。
  4. 抢锁失败的线程直接进入等待队列,并阻塞等待。

再描述下解锁流程:

  1. 如果调用解锁方法的线程不是持有锁的线程,则抛出IllegalMonitorStateException,否则第2步。
  2. 可重入次数-1,如果可重入次数为0则代表解锁成功。
  3. 解锁成功后唤醒队列头部等待的抢锁线程。

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

在这里插入图片描述

猜你喜欢

转载自blog.csdn.net/lingbomanbu_lyl/article/details/125481636