AQS & ReentrantLock 实现原理

参考链接

1 AQS (AbstractQuenedSynchronizer)

在这里插入图片描述

  • 基于 AQS 的同步器类,包含“获取”和“释放”操作(“获取”需要依赖状态,通常会阻塞;“释放”不会阻塞)
    • 对于 ReentrantLockSemaphore,“获取”和“释放”比较直观
    • 对于 CountDownLatch,“获取”表示“等待直到闭锁到达结束状态”,即 countDownLatch.await()
    • 对于 FutureTask,“获取”表示“等待直到任务完成”,即 futureTask.get()
boolean acquire() throws InterruptedException {
	while (当前状态不允许获取) {
		if (需要阻塞获取请求) {
			将当前线程加入队列并阻塞
		} else {
			return false;
		}
	}
	更新同步器状态
	如果当前线程位于队列,将线程移出队列
	return true;
}

void release() {
	更新同步器状态
	if (当前状态允许某些被阻塞的线程获取) {
		解除队列中若干个线程的阻塞状态
	}
}

AQS 的主要数据结构

  1. 状态 volatile int state
    • ReentrantLock 将它作为锁的重入次数(因此 ReentrantLock 还要记录锁的持有者)
    • Semaphore 将它作为剩余的许可数量
    • FutureTask 用它表示任务的状态:尚未开始、正在运行、已完成、已取消
    • CountDownLatich 将它作为计数值
  2. 同步队列
    • 存放等待获取锁的线程
  3. 条件队列
    • 存放等待被唤醒的线程,线程被唤醒后,加入到同步队列的队尾
    • java.util.concurrent.locks.Condition 接口相关联的队列,相关方法 await()/signal()/signalAll(),它允许更细粒度的线程间协调

和synchronized的实现相比,Monitor包含的数据结构:

  • owner:当前拥有锁的线程
  • count:重入次数
  • EntryList:抢夺锁的线程队列
  • WaitSet:等待被 notify()/notifyAll() 唤醒的线程队列
  • 线程通过 CAS 改变状态符 state,成功则获取锁成功,失败则进入等待队列,等待被唤醒
  • AQS 采用 自旋锁 的机制

2 Lock 接口与显式条件

public interface Lock {
	/**
	* 阻塞直到加锁成功
	**/
	void lock();

	/**
	* 解锁
	**/
	void unlock();

	/**
	* lock方法的响应中断版本:阻塞直到加锁成功或被中断
	**/
	void lockInterruptibly() throws InterruptedException;

	/**
	* 非阻塞(调用后立即返回),成功获取锁返回true,否则返回false
	**/
	boolean tryLock();

	/**
	* tryLock的延时等待版本:等待时间内可以响应中断,如果发生中断则抛出异常
	**/
	boolean tryLock(long time, TimeUnit unit) throws InterruptedException;

	/**
	* 新建一个显式条件
	**/
	Condition newCondition();
}
  • Condition 接口的关键方法:await()signal()/signalAll()
    • 类比 wait()notify()/notifyAll()
    • 调用 await()/signal() 要先加锁,否则会抛出异常(类比 wait()/notify() 要在对应的同步代码块中调用)
    • 同样,从 await() 返回后,线程重新获得锁,但需要检查等待条件(因此总是在循环中检查条件,调用 await()
  • 显式条件和显式锁配合,synchronizedwait/notify 配合
  • 类比 wait()/notify() 的 Demo
    static void awaitSignalTest() throws InterruptedException {
        WaitThreadWithLock thread = new WaitThreadWithLock();
        thread.start();
        Thread.sleep(2000L);
        thread.setFlagTrue();  // 主线程调用setFlagTrue()
    }

class WaitThreadWithLock extends Thread {
    private boolean flag = false;
    private final Lock lock = new ReentrantLock();
    private final Condition condition = lock.newCondition();

    @Override
    public void run() {
        lock.lock();
        try {
            while (!flag) {
                try {
                    System.out.println("sub thread call await()");
                    condition.await();
                    System.out.println("sub thread return from await()");
                } catch (InterruptedException e) {
                    System.out.println("await interrupted");
                }
            }
        } finally {
            lock.unlock();
            System.out.println("sub thread terminate");
        }
    }

    public void setFlagTrue() {  // 由主线程调用
        lock.lock();
        try {
            flag = true;
            condition.signal();
        } finally {
            lock.unlock();
            System.out.println("main thread call signal()");
        }
    }
}

结果:
sub thread call await()
main thread call signal()
sub thread return from await()
sub thread terminate

3 转账 Demo:解决死锁的两种方案

  • 账户类定义(不考虑余额不足的情况)
class BankAccount {
    private final long uid;
    private volatile long balance;
    private final Lock lock = new ReentrantLock();

    public BankAccount(long uid, long initBalance) {
        this.uid = uid;
        this.balance = initBalance;
    }

    public void add(long money) {
        accountTryLock();
        try {
            balance += money;
        } finally {
            accountUnlock();
        }
    }

    public void reduce(long money) {
        accountTryLock();
        try {
            balance -= money;
        } finally {
            accountUnlock();
        }
    }

    public boolean accountTryLock() {
        return lock.tryLock();
    }

    public void accountLock() {
        lock.lock();
    }

    public void accountUnlock() {
        lock.unlock();
    }

    public long getUid() {
        return uid;
    }

    public long getBalance() {
        return balance;
    }
}
  • 转账方案1,无同步措施
	/**
     * 无同步转账
     */
    static void naiveTransfer(BankAccount from, BankAccount to, long money) {
        from.accountLock();
        try {
            to.accountLock();
            try {
                from.reduce(money);
                to.add(money);
            } finally {
                to.accountUnlock();
            }
        } finally {
            from.accountUnlock();
        }
    }
  • 转账方案2,固定加锁顺序,先获取 id 更小的账户的锁
	/**
     * 确定加锁顺序的转账
     */
    static void transferWithOrder(BankAccount from, BankAccount to, long money) {
        // 按照uid指定加锁顺序,先获取uid更小的账户的锁
        BankAccount first = from.getUid() < to.getUid() ? from : to;
        BankAccount second = from.getUid() > to.getUid() ? from : to;
        first.accountLock();
        try {
            second.accountLock();
            try {
                from.reduce(money);
                to.add(money);
            } finally {
                second.accountUnlock();
            }
        } finally {
            first.accountUnlock();
        }
    }
  • 转账方案3,使用 tryLock() 转账
    • 需要注意的是,每执行一次 transferWithTryLock 不一定成功转账(因为 tryLock() 没有获取锁时,直接从方法返回)
	/**
     * 使用tryLock的转账
     */
    static boolean transferWithTryLock(BankAccount from, BankAccount to, long money) {
        if (from.accountTryLock()) {
            try {
                if (to.accountTryLock()) {
                    try {
                        from.reduce(money);
                        to.add(money);
                        return true;
                    } finally {
                        to.accountUnlock();
                    }
                }
            } finally {
                from.accountUnlock();
            }
        }
        return false;
    }

4 ReentrantLock 非公平锁加锁流程

  • 非公平锁是指新来的线程跟 AQS 同步队列头部的线程竞争锁,队列其他的线程还是正常排队
  • 公平锁严格执行 FIFO,新线程只能加入队尾

在这里插入图片描述

  1. 非公平锁尝试加锁,即执行 tryAcquire() 的流程是:检查state字段,若为0,表示锁未被占用,尝试占用锁;若不为0,检查当前锁是否被自己占用,若被自己占用,则更新 state 字段,重入次数加 1
  2. 如果以上两点都没有成功,则获取锁失败,进入同步队列
  3. 进入同步队列的线程尝试获取锁(最靠前的线程才有资格尝试),如果获取成功则成为队列新的头节点,获取失败则尝试挂起
  4. 线程入队后能够挂起的前提是,它的前驱节点的状态为 SIGNAL,状态为 SIGNAL 的节点在出队后会唤醒后面紧邻的节点

5 ReentrantLock 和 synchronized 的异同

在这里插入图片描述

  • 都提供了互斥性内存可见性
  • 优先使用 synchronized,不满足要求时考虑 ReentrantLock
  • 响应中断
    • 如果某一线程A正在执行锁中的代码,另一线程B正在等待获取该锁,可以让它中断自己或者在别的线程中中断它
    • Lock 等待锁过程中可以用 interrupt() 来中断等待
    • Lock 接口提供的定时加锁方法 tryLock(time, timeUnit)lockInterruptibly() 具有响应中断的能力
  • 超时等待
    • 规定超时等待时间,避免线程无限期的等待获取锁
    • Lock 接口提供的定时加锁方法 tryLock(time, timeUnit)
  • 公平锁与非公平锁
    • 公平锁是指多个线程同时尝试获取同一把锁时,按照线程达到的先后顺序获取锁
    • 而非公平锁则允许线程“插队”,新线程和队首的线程竞争锁
  • 自动释放
    • 内置锁以代码块为单位加锁,离开同步代码块自动释放锁
    • Lock 接口必须在 finally 块中释放锁,而不会自动释放
    	private final Lock lock = new ReentrantLock();
    
    	public void test() {
        	lock.lock();
        	try {
            	// ...
        	} finally {
            	lock.unlock();
        	}
    	}
    	```
    
  • 设计思维
    • synchronized 是一种阻塞式算法,线程得不到锁的时候进入锁等待队列,等待其它线程唤醒,有上下文切换开销
    • 基于 CAS 的算法(AQS、原子变量等)是非阻塞的,如果发生更新冲突只是返回失败,不会阻塞,没有上下文切换的开销

6 ReentrantReadWriteLock

  • ReadWriteLock 接口暴露两个锁对象,读不互斥,写互斥,读写互斥
  • 这种锁设计旨在提高读操作的并发性,同时保证写操作的独占性,尤其适用于读多写少的场景
  • 支持锁降级,即线程可以先获取写锁,然后获取读锁,最后释放写锁
  • 读锁和写锁都支持线程重入
public interface ReadWriteLock {
    /**
     * Returns the lock used for reading.
     *
     * @return the lock used for reading
     */
    Lock readLock();

    /**
     * Returns the lock used for writing.
     *
     * @return the lock used for writing
     */
    Lock writeLock();
}
  • 用读写锁包装 Map
class ReentrantReadWriteLockMap<K, V> {

    private final Map<K, V> map = new HashMap<>();
    private final ReentrantReadWriteLock lock = new ReentrantReadWriteLock();
    private final Lock readLock = lock.readLock();
    private final Lock writeLock = lock.writeLock();

    public V put(K key, V value) {
        writeLock.lock();
        try {
            return map.put(key, value);
        } finally {
            writeLock.unlock();
        }
    }

    public V get(K key) {
        readLock.lock();
        try {
            return map.get(key);
        } finally {
            readLock.unlock();
        }
    }
}
  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值