ReentrantLock 和 公平锁
一、基本介绍
- ReentrantLock(重入锁) 是一个独占式锁,具有和synchronize的监视器锁基本相同的行为和语意。但和synchronized相比,它更加的灵活、强大、增加了轮询、超时、中断等高级功能以及可以创建公平和非公平锁。
- ReentrantLock是基于Lock实现得可重入锁,所有的Lock都是基于AQS实现的,AQS和Condition各自维护不同得对象,在使用Lock和Condition时,其实就是两个队列得相互移动。它锁提供的共享锁、互斥锁都是基于对state的操作。而它的可重入是因为实现了同步器Sync,在Sync的两个实现类中包括了公平锁和非公平锁。
- 在使用ReentrantLock时一定要在finally中进行unlock的操作,否则其他线程访问时会永远阻塞。
- 可重入的作用就是,在一个被锁保护的代码里可以调用另一个被相同锁保护的方法。
二、ReentrantLock公平锁代码
-
初始化ReentrantLock
// 选择公平锁还是非公平锁 public ReentrantLock(boolean fair) { sync = fair ? new FairSync() : new NonfairSync(); } // 默认无参构造器就是非公平锁 public ReentrantLock() { sync = new NonfairSync(); } // 一般情况下不需要完全保持顺序执行的就不需要选择公平锁,因为公平锁只是听起来公平,实际上我们也无法保证线程调度器是否是公平的。如果线程调度器选择忽略一个线程,而该线程为了这个锁已经等待了很长时间,那么就没有机会公平的处理这个锁了。
-
公平锁和非公平锁,主要是在方法tryAcquire中,是否有!hasQueuedThreads()的判断。
// 公平锁 protected final boolean tryAcquire(int acquires) { final Thread current = Thread.currentThread(); int c = getState(); if (c == 0) { if (!hasQueuedPredecessors() && compareAndSetState(0, acquires)) { setExclusiveOwnerThread(current); return true; } } else if (current == getExclusiveOwnerThread()) { int nextc = c + acquires; if (nextc < 0) throw new Error("Maximum lock count exceeded"); setState(nextc); return true; } return false; } // 该方法通过比较头节点和尾节点以及头节点的下一个节点来判断当前线程是否有排在自己前面的其他线程在等待获取锁。 public final boolean hasQueuedPredecessors() { Node t = tail; Node h = head; Node s; return h != t && ((s = h.next) == null || s.thread != Thread.currentThread()); }
三、什么是公平锁
- CLH就是一种基于单向链表的高性能、公平的自旋锁。AQS中的队列是CLH变体的虚拟双向队列,AQS是通过将每条请求共享资源的线程封装成一个节点来实现锁的分配。
- 公平锁的实现
- CLH是一种基于链表的可拓展、高性能、公平的自旋锁。
- 分别有三种实现,CLH、MCS、Ticket
- 这三种,CLH适合多核多处理器,其他两种都适合单核处理器,了解即可,公平锁消耗性能极大,要确保自己的逻辑一定要顺序执行时再去使用公平锁。
四、总结
-
ReentrantLock的基本使用
// 一般要在一个类中定位为全局变量 public class Test { private final static Lock reLock = new ReentrantLock(); private static int count = 0; public void add() { reLock.lock(); // 加锁逻辑一定不可以写在try内部 try { count++; // 执行完输出一句话 say(); } finally { // 一定要在finally中增加解锁操作,否则可能会造成死锁。 reLock.unlock(); } } public void say() { // 也可以再加锁,此时如果同一线程获取到锁,就是重入,holdCount会进行++操作,标识当前线程获取到锁的次数 reLock.lock(); // 如果是不加锁的情况下,此时在main函数中的输出应该是 1,1 try { Thread.sleep(100L); System.out.println(count); } finally { reLock.unlock(); } } public static void main(String[] args) { Test test = new Test(); new Thread(() -> { test.say(); // 输出 0 }).start(); new Thread(() -> { test.add(); // 输出 1 }).start(); } }
条件对象
如果是需要进行某些判断的情况下去使用锁时,例如下方的代码片段,此时将lock写在if内部,它可能会出现,AB线程同时访问该代码块,此时AB都已执行通过了if判断,但此时,A线程已经完成减值操作,剩余值不足以被B线程所减,也就是A线程执行结束后的结果已经不满足B线程的条件了,这时候就出现了BUG。如何解决这个问题
public void sub(int i) { if (count >= i) { reLock.lock(); try { count -= i; }finally { reLock.unlock(); } } }
可以修改为这种方式
public class Test { private final static Lock reLock = new ReentrantLock(); private Condition reCondition; private static int count = 10000; public Test() { this.reCondition = reLock.newCondition(); } public void add(int i) { reLock.lock(); // 加锁逻辑一定不可以写在try内部 try { count+=i; // 执行完输出一句话 say(); // 增加结束后唤醒其他线程 reCondition.signalAll(); } finally { // 一定要在finally中增加解锁操作,否则可能会造成死锁。 reLock.unlock(); } } public void sub(int i) { reLock.lock(); try { // 如果扣减余额较大,则先等待一会,一定要在其他地方增加唤醒线程操作,否则可能会出现死锁 while (i >= count) { reCondition.await(); } count -= i; reCondition.signalAll(); } catch (InterruptedException e) { throw new RuntimeException(e); } finally { reLock.unlock(); } } }
-
实际开发中使用synchronized还是ReentrantLock
在实际开发过程中,ReentrantLock是属于颗粒度更小,控制更精细的控制锁,但也更加容易出现死锁的情况,如果真的需要进行对象锁操作还是推荐使用synchronized 由JVM委托控制的锁操作,不容易出现死锁的情况导致程序宕机。