公平锁是怎么实现公平的
那么下面看看公平锁是怎么实现公平的。公平锁的话只需要看FairSync重写的tryAcquire方法。
/** * Sync object for fair locks */ static final class FairSync extends Sync { private static final long serialVersionUID = -3000897897090466540L; final void lock() { acquire(1); } /** * Fair version of tryAcquire. Don't grant access unless * recursive call or no waiters or is first. */ protected final boolean tryAcquire(int acquires) { final Thread current = Thread.currentThread(); int c = getState(); // 7 当前state的状态为0 if (c == 0) { // 8 公平策略 if (!hasQueuedPredecessors() && compareAndSetState(0, acquires)) { setExclusiveOwnerThread(current); return true; } } // 9 当前线程是锁的持有者 else if (current == getExclusiveOwnerThread()) { int nextc = c + acquires; if (nextc < 0) throw new Error("Maximum lock count exceeded"); setState(nextc); return true; } return false; } }
如以上代码所示,公平的tryAcquire策略与非公平的类似,不同之处在于,代码(8)在设置CAS前添加了hasQueuedPredecessors方法,该方法是实现公平性的核心代码,
public final boolean hasQueuedPredecessors() { // The correctness of this depends on head being initialized // before tail and on head.next being accurate if the current // thread is first in queue. Node t = tail; // Read fields in reverse initialization order Node h = head; Node s; return h != t && ((s = h.next) == null || s.thread != Thread.currentThread()); }
在如上代码中,如果当前线程节点有前驱节点则返回true,否则如果当前AQS队列为空或者当前线程节点是AQS的第一个节点则返回false。
其中如果h==t则说明当前队列为空,直接返回false;
如果h!=t并且s==null则说明有一个元素将要作为AQS的第一个节点入队列( 回想一下 enq函数的第一个元素入队列是两步操作:首先创建一个哨兵头节点,然后将第一个元素插入哨兵节点后面),那么返回true
如果h!=t并且s!=null和s.thread != Thread.currentThread()则说明队列里面的第一个元素不是当前线程,那么返回true。
void lockInterruptibly() 方法
该方法与lock()方法类似,它的不同在于,它对中断进行响应,就是当前线程在调用该方法时,如果其他线程调用了当前线程的interrupt()方法,则当前线程会抛出InterruptedException异常,然后返回。
/** * Acquires the lock unless the current thread is * {@linkplain Thread#interrupt interrupted}. * * <p>Acquires the lock if it is not held by another thread and returns * immediately, setting the lock hold count to one. * * <p>If the current thread already holds this lock then the hold count * is incremented by one and the method returns immediately. * * <p>If the lock is held by another thread then the * current thread becomes disabled for thread scheduling * purposes and lies dormant until one of two things happens: * * <ul> * * <li>The lock is acquired by the current thread; or * * <li>Some other thread {@linkplain Thread#interrupt interrupts} the * current thread. * * </ul> * * <p>If the lock is acquired by the current thread then the lock hold * count is set to one. * * <p>If the current thread: * * <ul> * * <li>has its interrupted status set on entry to this method; or * * <li>is {@linkplain Thread#interrupt interrupted} while acquiring * the lock, * * </ul> * * then {@link InterruptedException} is thrown and the current thread's * interrupted status is cleared. * * <p>In this implementation, as this method is an explicit * interruption point, preference is given to responding to the * interrupt over normal or reentrant acquisition of the lock. * * @throws InterruptedException if the current thread is interrupted */ public void lockInterruptibly() throws InterruptedException { sync.acquireInterruptibly(1); }
/** * Acquires in exclusive mode, aborting if interrupted. * Implemented by first checking interrupt status, then invoking * at least once {@link #tryAcquire}, returning on * success. Otherwise the thread is queued, possibly repeatedly * blocking and unblocking, invoking {@link #tryAcquire} * until success or the thread is interrupted. This method can be * used to implement method {@link Lock#lockInterruptibly}. * * @param arg the acquire argument. This value is conveyed to * {@link #tryAcquire} but is otherwise uninterpreted and * can represent anything you like. * @throws InterruptedException if the current thread is interrupted */ public final void acquireInterruptibly(int arg) throws InterruptedException { // 若果当前线程被打断,直接抛出异常 if (Thread.interrupted()) throw new InterruptedException(); // 尝试获取资源 if (!tryAcquire(arg)) // 调用AQS可被中断的方法 doAcquireInterruptibly(arg); }
boolean tryLock() 方法
尝试获取锁,如果当前该锁没有被其他线程持有,则当前线程获取该锁并返回true,否则返回false。注意,该方法不会引起当前线程阻塞。
/** * Acquires the lock only if it is not held by another thread at the time * of invocation. * * <p>Acquires the lock if it is not held by another thread and * returns immediately with the value {@code true}, setting the * lock hold count to one. Even when this lock has been set to use a * fair ordering policy, a call to {@code tryLock()} <em>will</em> * immediately acquire the lock if it is available, whether or not * other threads are currently waiting for the lock. * This "barging" behavior can be useful in certain * circumstances, even though it breaks fairness. If you want to honor * the fairness setting for this lock, then use * {@link #tryLock(long, TimeUnit) tryLock(0, TimeUnit.SECONDS) } * which is almost equivalent (it also detects interruption). * * <p>If the current thread already holds this lock then the hold * count is incremented by one and the method returns {@code true}. * * <p>If the lock is held by another thread then this method will return * immediately with the value {@code false}. * * @return {@code true} if the lock was free and was acquired by the * current thread, or the lock was already held by the current * thread; and {@code false} otherwise */ public boolean tryLock() { return sync.nonfairTryAcquire(1); }
/** * Performs non-fair tryLock. tryAcquire is implemented in * subclasses, but both need nonfair try for trylock method. */ final boolean nonfairTryAcquire(int acquires) { final Thread current = Thread.currentThread(); int c = getState(); 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; }
如上代码与非公平锁的tryAcquire()方法代码类似,所以tryLock()使用的是非公平策略。
boolean tryLock(long timeout, TimeUnit unit)
尝试获取锁,与tryLock()的不同之处在于,它设置了超时时间,如果超时时间到没有获取到该锁则返回false。
public boolean tryLock(long timeout, TimeUnit unit) throws InterruptedException { // 调用AQS的tryAcquireNanos return sync.tryAcquireNanos(1, unit.toNanos(timeout)); }
释放锁
void unlock() 方法
- 尝试释放锁,如果当前线程持有该锁,则调用该方法会让该线程对该线程持有的AQS状态值减1,如果减去1后当前状态值为0,则当前线程会释放该锁,否则仅仅减1而已。
- 如果当前线程没有持有该锁而调用了该方法则会抛出
IllegalMonitorStateException
异常
/** * Attempts to release this lock. * * <p>If the current thread is the holder of this lock then the hold * count is decremented. If the hold count is now zero then the lock * is released. If the current thread is not the holder of this * lock then {@link IllegalMonitorStateException} is thrown. * * @throws IllegalMonitorStateException if the current thread does not * hold this lock */ public void unlock() { sync.release(1); }
/** * Releases in exclusive mode. Implemented by unblocking one or * more threads if {@link #tryRelease} returns true. * This method can be used to implement method {@link Lock#unlock}. * * @param arg the release argument. This value is conveyed to * {@link #tryRelease} but is otherwise uninterpreted and * can represent anything you like. * @return the value returned from {@link #tryRelease} */ public final boolean release(int arg) { if (tryRelease(arg)) { Node h = head; if (h != null && h.waitStatus != 0) unparkSuccessor(h); return true; } return false; }
protected final boolean tryRelease(int releases) { int c = getState() - releases; // 11 如果不是锁持有者调用 抛出IllegalMonitorStateException if (Thread.currentThread() != getExclusiveOwnerThread()) throw new IllegalMonitorStateException(); boolean free = false; // 12 若果可重入次数为0 ,则清空锁持有线程 if (c == 0) { free = true; setExclusiveOwnerThread(null); } // 13 设置可重入次数为原始值减1 setState(c); return free; }
代码(11)所示,如果当前线程不是该锁持有者则直接抛出异常
否则查看状态值是否为0,为0则说明当前线程要放弃对该锁的持有权,则执行代码(12)把当前锁持有者设置为null。
如果状态值不为0,则仅仅让当前线程对该锁的可重入次数减1。
Demo : 使用ReentrantLock来实现一个简单的线程安全的list
import java.util.ArrayList; import java.util.concurrent.locks.ReentrantLock; /** * @author 小工匠 * @version 1.0 * @description: TODO * @date 2021/12/4 22:05 * @mark: show me the code , change the world */ public class ReentrantLockList { //线程不安全的List private ArrayList<String> list = new ArrayList<String>(); //独占锁,默认是非公平锁,传入true可以是公平锁 private volatile ReentrantLock lock = new ReentrantLock(); //往集合中添加元素 public void add(String str) { lock.lock(); try { list.add(str); } finally { lock.unlock(); } } //删除集合中的元素 public void remove(String str) { lock.lock(); try { list.remove(str); } finally { lock.unlock(); } } //根据索引获取集合中某个元素 public String get(int index) { lock.lock(); try { return list.get(index); } finally { lock.unlock(); } } }
如上代码通过在操作array元素前进行加锁保证同一时间只有一个线程可以对array数组进行修改,但是也只能有一个线程对array元素进行访问。
同样最后使用图 来加深理解。
假如线程Thread1、Thread2和Thread3同时尝试获取独占锁ReentrantLock,假设Thread1获取到了,则Thread2和Thread3就会被转换为Node节点并被放入ReentrantLock对应的AQS阻塞队列,而后被阻塞挂起。 如上图。
假设Thread1获取锁后调用了对应的锁创建的条件变量1,那么Thread1就会释放获取到的锁,然后当前线程就会被转换为Node节点插入条件变量1的条件队列。
由于Thread1释放了锁,所以阻塞到AQS队列里面的Thread2和Thread3就有机会获取到该锁,假如使用的是公平策略,那么这时候Thread2会获取到该锁,从而从AQS队列里面移除Thread2对应的Node节点。 如下图
小结
我们梳理了ReentrantLock的实现原理,ReentrantLock的底层是使用AQS实现的可重入独占锁。
在这里AQS状态State值为0表示当前锁空闲,为大于等于1的值则说明该锁已经被占用。该锁内部有公平与非公平实现,默认情况下是非公平的实现。
另外,由于该锁是独占锁,所以某时只有一个线程可以获取该锁。