多线程(五)Lock
ReentrantLock VS ReentrantReadWriteLock VS StampedLock
锁 | 特性 | 是否支持重入 | 是否支持锁升级 | 是否支持Condition | 适合场景 |
ReentrantLock | 独占可重入 | 是 | 无 | 是 | 纯写入 |
ReentrantReadWriteLock | 非独占可重读,读写锁,悲观锁 | 是 | 否 | 是 | 读写均衡 |
StampedLock | 非独占不可重入,多模式锁,乐观锁 | 否 | 是 | 否 | 读多写少 |
ReentrantLock
可重入锁,顾名思义,这个锁可以被线程多次重复进入进行获取操作。ReentantLock继承接口Lock并实现了接口中定义的方法,除了能完成synchronized所能完成的所有工作外,还提供了诸如可响应中断锁、可轮询锁请求、定时锁等避免多线程死锁的方法。
Lock实现的机理依赖于特殊的CPU指定,可以认为不受JVM的约束,并可以通过其他语言平台来完成底层的实现。在并发量较小的多线程应用程序中,ReentrantLock与synchronized性能相差无几,但在高并发量的条件下,synchronized性能会迅速下降几十倍,而ReentrantLock的性能却能依然维持一个水准。
ReentrantLock引入两个概念:公平锁与非公平锁。锁的实现方式是基于如下几点:表结点Node和状态state的volatile关键字;sum.misc.Unsafe.compareAndSet的原子操作。公平和非公平锁的队列都基于锁内部维护的一个双向链表,表结点Node的值就是每一个请求当前锁的线程。公平锁则在于每次都是依次从队首取值。而非公平锁在等待锁的过程中, 如果有任意新的线程妄图获取锁,都是有很大的几率直接获取到锁的,如果被加入了等待队列后则跟公平锁没有区别。
ReentrantLock通过方法lock()与unlock()来进行加锁与解锁操作,与synchronized会被JVM自动解锁机制不同,ReentrantLock加锁后需要手动进行解锁。为了避免程序出现异常而无法正常解锁的情况,使用ReentrantLock必须在finally控制块中进行解锁操作。
synchronized与wait()和nitofy()/notifyAll()方法相结合可以实现等待/通知模型,ReentrantLock同样可以,但是需要借助Condition,且Condition有更好的灵活性,具体体现在:
- 一个Lock里面可以创建多个Condition实例,实现多路通知
- notify()方法进行通知时,被通知的线程时Java虚拟机随机选择的,但是ReentrantLock结合Condition可以实现有选择性地通知
ReentrantLock实例
public class TestReentrantLock { private Lock lock = new ReentrantLock(); private Condition condition = lock.newCondition(); private Queue<String> queue = new LinkedList<>(); public static void main(String[] args) throws InterruptedException { TestReentrantLock rl = new TestReentrantLock(); new Thread() { @Override public void run() { for (Integer i = 0; i < 5; i++) { rl.getTask(); } } }.start(); Thread.sleep(100); new Thread() { @Override public void run() { for (Integer i = 0; i < 5; i++) { rl.addTask(Math.random() + ""); } } }.start(); } public void addTask(String s) { lock.lock(); try { queue.add(s); System.out.println("addTask:" + s); condition.signalAll(); } finally { lock.unlock(); } } public String getTask() { lock.lock(); try { while (queue.isEmpty()) { try { condition.await(); } catch (InterruptedException e) { // TODO Auto-generated catch block e.printStackTrace(); } } String result = queue.remove(); System.out.println("getTask:" + result); return result; } finally { lock.unlock(); } } }
执行结果(虽然getTask的线程先执行,但是由于await方法阻塞线程,等待被唤醒):
addTask:0.1532530140339844 addTask:0.9855533133771119 getTask:0.1532530140339844 getTask:0.9855533133771119 addTask:0.6634186213426154 addTask:0.24053864333002573 getTask:0.6634186213426154 getTask:0.24053864333002573 addTask:0.2089403771816799 getTask:0.2089403771816799
ReentrantLock源码分析
public class ReentrantLock implements Lock, java.io.Serializable { private static final long serialVersionUID = 7373984872572414699L; /** Synchronizer providing all implementation mechanics */ private final Sync sync; /** * Base of synchronization control for this lock. Subclassed into fair and nonfair versions below. Uses AQS state to * represent the number of holds on the lock. */ abstract static class Sync extends AbstractQueuedSynchronizer { private static final long serialVersionUID = -5179523762034025860L; /** * Performs {@link Lock#lock}. The main reason for subclassing is to allow fast path for nonfair version. */ abstract void lock(); /** * 尝试获取非公平锁 */ 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);// 更新state值为新的重入次数 return true; } return false; } //释放当前线程占用的锁 protected final boolean tryRelease(int releases) { int c = getState() - releases; //如果不是当前线程占用锁,则抛出异常 if (Thread.currentThread() != getExclusiveOwnerThread()) throw new IllegalMonitorStateException(); boolean free = false; if (c == 0) { free = true; setExclusiveOwnerThread(null);//清空独占线程 } setState(c); return free; } protected final boolean isHeldExclusively() { return getExclusiveOwnerThread() == Thread.currentThread(); } final ConditionObject newCondition() { return new ConditionObject(); } // Methods relayed from outer class final Thread getOwner() { return getState() == 0 ? null : getExclusiveOwnerThread(); } final int getHoldCount() { return isHeldExclusively() ? getState() : 0; } final boolean isLocked() { return getState() != 0; } /** * Reconstitutes the instance from a stream (that is, deserializes it). */ private void readObject(java.io.ObjectInputStream s) throws java.io.IOException, ClassNotFoundException { s.defaultReadObject(); setState(0); // reset to unlocked state } } /** * Sync object for non-fair locks 非公平锁 */ static final class NonfairSync extends Sync { private static final long serialVersionUID = 7316153563782823691L; /** * Performs lock. Try immediate barge, backing up to normal acquire on failure. */ final void lock() { if (compareAndSetState(0, 1)) //cas设置state状态,如果原值为0,置为1 setExclusiveOwnerThread(Thread.currentThread()); else acquire(1); // 调用的是tryAcquire方法 } protected final boolean tryAcquire(int acquires) { return nonfairTryAcquire(acquires); } } /** * Sync object for fair locks */ static final class FairSync extends Sync { private static final long serialVersionUID = -3000897897090466540L; final void lock() { acquire(1); //调用的是tryAcquire } /** * 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(); 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; } } /** * Creates an instance of {@code ReentrantLock}. This is equivalent to using {@code ReentrantLock(false)}. 默认是非公平锁 */ public ReentrantLock() { sync = new NonfairSync(); } /** * Creates an instance of {@code ReentrantLock} with the given fairness policy. 参数true是公平锁 * @param fair {@code true} if this lock should use a fair ordering policy */ public ReentrantLock(boolean fair) { sync = fair ? new FairSync() : new NonfairSync(); } /** * Acquires the lock. */ public void lock() { sync.lock(); } /** * Acquires the lock unless the current thread is {@linkplain Thread#interrupt interrupted}. * @throws InterruptedException if the current thread is interrupted */ public void lockInterruptibly() throws InterruptedException { sync.acquireInterruptibly(1); } /** * Acquires the lock only if it is not held by another thread at the time of invocation. */ public boolean tryLock() { return sync.nonfairTryAcquire(1); } /** * Acquires the lock if it is not held by another thread within the given waiting time and the current thread has not been * {@linkplain Thread#interrupt interrupted}. */ public boolean tryLock(long timeout, TimeUnit unit) throws InterruptedException { return sync.tryAcquireNanos(1, unit.toNanos(timeout)); } /** * Attempts to release this lock. * @throws IllegalMonitorStateException if the current thread does not hold this lock */ public void unlock() { sync.release(1); } /** * Returns a {@link Condition} instance for use with this {@link Lock} instance. * @return the Condition object */ public Condition newCondition() { return sync.newCondition(); } /** * Queries the number of holds on this lock by the current thread. * @return the number of holds on this lock by the current thread,or zero if this lock is not held by the current thread */ public int getHoldCount() { return sync.getHoldCount(); } /** * Queries if this lock is held by the current thread. * @return {@code true} if current thread holds this lock and {@code false} otherwise */ public boolean isHeldByCurrentThread() { return sync.isHeldExclusively(); } /** * Queries if this lock is held by any thread. This method is designed for use in monitoring of the system state,not for synchronization control. * @return {@code true} if any thread holds this lock and {@code false} otherwise */ public boolean isLocked() { return sync.isLocked(); } /** * Returns {@code true} if this lock has fairness set true. * @return {@code true} if this lock has fairness set true */ public final boolean isFair() { return sync instanceof FairSync; } /** * Returns the thread that currently owns this lock, or {@code null} if not owned. * @return the owner, or {@code null} if not owned */ protected Thread getOwner() { return sync.getOwner(); } /** * Queries whether any threads are waiting to acquire this lock.查询是否有线程正在等待获取此锁 * @return {@code true} if there may be other threads waiting to acquire the lock */ public final boolean hasQueuedThreads() { return sync.hasQueuedThreads(); } /** * Queries whether the given thread is waiting to acquire this lock. 查询是否有指定线程正在获取此锁 * @param thread the thread * @return {@code true} if the given thread is queued waiting for this lock * @throws NullPointerException if the thread is null */ public final boolean hasQueuedThread(Thread thread) { return sync.isQueued(thread); } /** * Returns an estimate of the number of threads waiting to acquire this lock. * @return the estimated number of threads waiting for this lock */ public final int getQueueLength() { return sync.getQueueLength(); } /** * Returns a collection containing threads that may be waiting to acquire this lock. * @return the collection of threads */ protected Collection<Thread> getQueuedThreads() { return sync.getQueuedThreads(); } /** * Queries whether any threads are waiting on the given condition associated with this lock. * @param condition the condition */ public boolean hasWaiters(Condition condition) { if (condition == null) throw new NullPointerException(); if (!(condition instanceof AbstractQueuedSynchronizer.ConditionObject)) throw new IllegalArgumentException("not owner"); return sync.hasWaiters((AbstractQueuedSynchronizer.ConditionObject)condition); } /** * Returns an estimate of the number of threads waiting on the given condition associated with this lock. */ public int getWaitQueueLength(Condition condition) { if (condition == null) throw new NullPointerException(); if (!(condition instanceof AbstractQueuedSynchronizer.ConditionObject)) throw new IllegalArgumentException("not owner"); return sync.getWaitQueueLength((AbstractQueuedSynchronizer.ConditionObject)condition); } /** * Returns a collection containing those threads that may be waiting on the given condition associated with this lock. */ protected Collection<Thread> getWaitingThreads(Condition condition) { if (condition == null) throw new NullPointerException(); if (!(condition instanceof AbstractQueuedSynchronizer.ConditionObject)) throw new IllegalArgumentException("not owner"); return sync.getWaitingThreads((AbstractQueuedSynchronizer.ConditionObject)condition); } /** * Returns a string identifying this lock, as well as its lock state.*/ public String toString() { Thread o = sync.getOwner(); return super.toString() + ((o == null) ? "[Unlocked]" : "[Locked by thread " + o.getName() + "]"); } }
总结:
- 每一个ReentrantLock自身维护一个AQS队列记录申请锁的线程信息;
- 通过大量CAS保证多个线程竞争锁的时候的并发安全;
- 可重入的功能是通过维护state变量来记录重入次数实现的。
- 公平锁需要维护队列,通过AQS队列的先后顺序获取锁,缺点是会造成大量线程上下文切换;
- 非公平锁可以直接抢占,所以效率更高;
- AbstractQueuedSynchronizer简称AQS,是一个用于构建锁和同步容器的框架。AQS解决了在实现同步容器时设计的大量细节问题。AQS使用一个FIFO的队列表示排队等待锁的线程,队列头节点称作“哨兵节点”或者“哑节点”,它不与任何线程关联。其他的节点与等待线程关联,每个节点维护一个等待状态waitStatus。AQS中还有一个表示状态的字段state,例如ReentrantLocky用它表示线程重入锁的次数,Semaphore用它表示剩余的许可数量,FutureTask用它表示任务的状态。对state变量值的更新都采用CAS操作保证更新操作的原子性。
ReadWriteLock
ReentrantLock保证了只有一个线程可以执行临界区代码,但是有些时候,这种保护有点过头。任何时刻,只允许一个线程修改,也就是调用inc()方法是必须获取锁,但是,get()方法只读取数据,不修改数据,它实际上允许多个线程同时调用。使用ReadWriteLock可以解决这个问题。使用ReadWriteLock
时,适用条件是同一个数据,有大量线程读取,但仅有少数线程修改
ReadWriteLock实例
public class TestReadWriteLock { public static void main(String[] args) { PricesInfo pricesInfo = new PricesInfo(); for (Integer i = 0; i < 3; i++) { new Thread(new Reader(pricesInfo), "ReadLock" + i).start(); } new Thread(new Writer(pricesInfo), "WriterLock").start(); } } class PricesInfo { private double price; private ReadWriteLock lock; public PricesInfo() { this.price = 1.0; this.lock = new ReentrantReadWriteLock(); } public double getPrice() { lock.readLock().lock(); System.out.printf("%s : Price 开始读了!\n", Thread.currentThread().getName()); double value = price; System.out.printf("%s : Price 读取完毕 : %f\n", Thread.currentThread().getName(), value); lock.readLock().unlock(); return value; } public void setPrices(double price) { lock.writeLock().lock(); System.out.printf("Writer:Attempt to modify the price.\n"); this.price = price; for (Integer i = 0; i < Integer.MAX_VALUE; i++) { } System.out.printf("Writer:Prices have been modified.%s \n", price); lock.writeLock().unlock(); } } class Reader implements Runnable { private PricesInfo priceInfo; public Reader(PricesInfo priceInfo) { this.priceInfo = priceInfo; } @Override public void run() { for (int i = 0; i < 5; i++) { priceInfo.getPrice(); try { Thread.sleep(2); } catch (InterruptedException e) { e.printStackTrace(); } } } } class Writer implements Runnable { private PricesInfo pricesInfo; public Writer(PricesInfo pricesInfo) { this.pricesInfo = pricesInfo; } @Override public void run() { for (int i = 0; i < 3; i++) { pricesInfo.setPrices((i + 1) * 10); try { Thread.sleep(2); } catch (InterruptedException e) { e.printStackTrace(); } } } }
执行结果如下:
ReadLock1 : Price 开始读了! ReadLock1 : Price 读取完毕 : 1.000000 ReadLock2 : Price 开始读了! ReadLock0 : Price 开始读了! ReadLock0 : Price 读取完毕 : 1.000000 ReadLock2 : Price 读取完毕 : 1.000000 Writer:Attempt to modify the price. Writer:Prices have been modified.10.0 ReadLock1 : Price 开始读了! ReadLock1 : Price 读取完毕 : 10.000000 ReadLock0 : Price 开始读了! ReadLock0 : Price 读取完毕 : 10.000000 ReadLock2 : Price 开始读了! ReadLock2 : Price 读取完毕 : 10.000000 Writer:Attempt to modify the price. Writer:Prices have been modified.20.0 ReadLock2 : Price 开始读了! ReadLock1 : Price 开始读了! ReadLock1 : Price 读取完毕 : 20.000000 ReadLock0 : Price 开始读了! ReadLock0 : Price 读取完毕 : 20.000000 ReadLock2 : Price 读取完毕 : 20.000000 ReadLock2 : Price 开始读了! ReadLock2 : Price 读取完毕 : 20.000000 Writer:Attempt to modify the price. Writer:Prices have been modified.30.0 ReadLock0 : Price 开始读了! ReadLock1 : Price 开始读了! ReadLock1 : Price 读取完毕 : 30.000000 ReadLock2 : Price 开始读了! ReadLock2 : Price 读取完毕 : 30.000000 ReadLock0 : Price 读取完毕 : 30.000000 ReadLock1 : Price 开始读了! ReadLock1 : Price 读取完毕 : 30.000000 ReadLock0 : Price 开始读了! ReadLock0 : Price 读取完毕 : 30.000000
总结:
- Java并发库中ReetrantReadWriteLock实现了ReadWriteLock接口并添加了可重入的特性
- ReetrantReadWriteLock读写锁的效率明显高于synchronized关键字
- ReetrantReadWriteLock读写锁的实现中,读锁使用共享模式;写锁使用独占模式,换句话说,读锁可以在没有写锁的时候被多个线程同时持有,写锁是独占的
- ReetrantReadWriteLock读写锁的实现中,需要注意的,当有读锁时,写锁就不能获得;而当有写锁时,除了获得写锁的这个线程可以获得读锁外,其他线程不能获得读锁
StampedLock
ReadWriteLock
可以解决多线程同时读,但只有一个线程能写的问题。但是它有个潜在的问题:如果有线程正在读,写线程需要等待读线程释放锁后才能获取写锁,即读的过程中不允许写,这是一种悲观的读锁。要进一步提升并发执行效率,Java 8引入了新的读写锁:StampedLock
。StampedLock
和ReadWriteLock
相比,改进之处在于:读的过程中也允许获取写锁后写入!这样一来,我们读的数据就可能不一致,所以,需要一点额外的代码来判断读的过程中是否有写入,这种读锁是一种乐观锁。乐观锁的意思就是乐观地估计读的过程中大概率不会有写入,因此被称为乐观锁。反过来,悲观锁则是读的过程中拒绝有写入,也就是写入必须等待。显然乐观锁的并发效率更高,但一旦有小概率的写入导致读取的数据不一致,需要能检测出来,再读一遍就行。StampedLock是不可重入锁。
StampedLock实例
public class TestStampedLock { private double x, y; private final StampedLock stampedLock = new StampedLock(); //写锁的使用 void move(double deltaX, double deltaY){ long stamp = stampedLock.writeLock(); //获取写锁 try { x += deltaX; y += deltaY; } finally { stampedLock.unlockWrite(stamp); //释放写锁 } } //乐观读锁的使用。乐观读锁获取失败则获取悲观读锁 double distanceFromOrigin() { long stamp = stampedLock.tryOptimisticRead(); //获得一个乐观读锁 double currentX = x; double currentY = y; if (!stampedLock.validate(stamp)) { //检查乐观读锁后是否有其他写锁发生,有则返回false stamp = stampedLock.readLock(); //获取一个悲观读锁 try { currentX = x; } finally { stampedLock.unlockRead(stamp); //释放悲观读锁 } } return Math.sqrt(currentX*currentX + currentY*currentY); } //悲观读锁以及读锁升级写锁的使用 void moveIfAtOrigin(double newX,double newY) { long stamp = stampedLock.readLock(); //悲观读锁 try { while (x == 0.0 && y == 0.0) { long ws = stampedLock.tryConvertToWriteLock(stamp); //读锁转换为写锁 if (ws != 0L) { //转换成功 stamp = ws; //票据更新 x = newX; y = newY; break; } else { stampedLock.unlockRead(stamp); //转换失败释放读锁 stamp = stampedLock.writeLock(); //强制获取写锁 } } } finally { stampedLock.unlock(stamp); //释放所有锁 } } }
总结:
- 所有获取锁的方法,都返回一个邮戳(stamp),stamp为0表示获取失败,其余都表示成功;
- 所有释放锁的方法,都需要一个邮戳(stamp),这个stamp必须是和成功获取锁时得到的stamp一致;
- StampedLock是不可重入的;(如果一个线程已经持有了写锁,再去获取写锁的话就会造成死锁)
- StampedLock有三种访问模式:
- Reading(读模式):功能和ReentrantReadWriteLock的读锁类似
- Writing(写模式):功能和ReentrantReadWriteLock的写锁类似
- Optimistic reading(乐观读模式):这是一种优化的读模式。
- StampedLock支持读锁和写锁的相互转换
我们知道RRW中,当线程获取到写锁后,可以降级为读锁,但是读锁是不能直接升级为写锁的。
StampedLock提供了读锁和写锁相互转换的功能,使得该类支持更多的应用场景。 - 无论写锁还是读锁,都不支持Conditon等待