Java并发系列(6)——AQS与显式锁的使用

接上一篇《Java并发系列(5)——CAS与Java原子操作类

5 AQS 与显式锁

5.1 显式锁

Java 提供了两种加锁的方式:

  • 一种是 synchronized,即内部锁,是 JVM 原生支持的,写在了 JVM 规范里面的 monitorenter 和 monitorexit 这样一对指令;
  • 另一种是 Lock,即显式锁,是以 CAS 为基础通过 Java 编码实现的。
5.1.1 ReentrantLock

ReentrantLock 是 Java 提供的一个 Lock 实现类——可重入锁。

可重入意味着,当试图获取锁的时候,如果发现锁已经被某个线程持有,但这个线程恰好是自己,那么仍然可以获得锁,而不会自己跟自己死锁。所以 synchronized 也可以算是可重入锁。

用法示例:

public class ReentrantLockTest {

    private static Lock lock = new ReentrantLock();

    public static void main(String[] args) throws InterruptedException {
        new Thread(() -> {
            System.out.println("thread2 begin");
            try {
                lock.lock();
                System.out.println("thread2 lock acquired");
                Thread.sleep(2000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            } finally {
                System.out.println("thread2 lock released");
                lock.unlock();
            }
            System.out.println("thread2 end");
        }).start();
        new Thread(() -> {
            System.out.println("thread1 begin");
            try {
                lock.lock();
                System.out.println("thread1 lock acquired");
            } finally {
                lock.unlock();
            }
            System.out.println("thread1 end");
        }).start();
    }
}

通过 lock 和 unlock 方法就实现了类似于 synchronized 代码块的功能。

5.1.2 Condition

在 synchronized 中线程协作靠 wait 和 notify,在显式锁场景下,线程协作就要依赖 Condition 了。

用法示例:

public class ConditionTest {

    private static Lock lock = new ReentrantLock();
    private static Condition condition1 = lock.newCondition();
    private static Condition condition2 = lock.newCondition();

    public static void main(String[] args) throws InterruptedException {
        //起一个线程,在 condition1 上 await
        new Thread(() -> {
            try {
                lock.lock();
                System.out.println("thread1 acquired lock");
                System.out.println("thread1 await on condition1");
                try {
                    condition1.await();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                System.out.println("thread1 wake up");
            } finally {
                System.out.println("thread1 will release lock");
                lock.unlock();
            }
        }).start();
        //起一个线程在 condition2 上 await
        new Thread(() -> {
            try {
                lock.lock();
                System.out.println("thread2 acquired lock");
                System.out.println("thread2 await on condition2");
                try {
                    condition2.await();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                System.out.println("thread2 wake up");
            } finally {
                System.out.println("thread2 will release lock");
                lock.unlock();
            }
        }).start();
        Thread.sleep(10);
        //起一个线程唤醒
        new Thread(() -> {
            try {
                lock.lock();
                System.out.println("thread3 acquired lock");
                System.out.println("thread3 will signal condition1");
                //唤醒在 condition1 上 await 的线程
                condition1.signal();
                Thread.sleep(2000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            } finally {
                System.out.println("thread3 will release lock");
                lock.unlock();
            }
            try {
                lock.lock();
                System.out.println("thread3 acquired lock");
                System.out.println("thread3 will signal condition2");
                //唤醒在 condition2 上 await 的线程
                condition2.signal();
                Thread.sleep(2000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            } finally {
                System.out.println("thread3 will release lock");
                lock.unlock();
            }
        }).start();
    }
}

输出如下:

thread1 acquired lock
thread1 await on condition1
thread2 acquired lock
thread2 await on condition2
thread3 acquired lock
thread3 will signal condition1
thread3 will release lock
thread3 acquired lock
thread3 will signal condition2
thread3 will release lock
thread1 wake up
thread1 will release lock
thread2 wake up
thread2 will release lock

通过 await 和 signal 方法,就实现了类似于 wait 和 notify 的功能。

所不同的是:

  • 在 synchronized 场景下,wait 都是 wait 在同一个 monitor 上,notify 无法 notify 指定的线程;
  • 在显式锁场景下,同一个 Lock 上可以建立多个 Condition,可以 signal 在指定 Condition 上的 await 的线程。

比如上面的例子,线程 1 和线程 2 竞争同一把锁,但 await 在不同的 Condition 上,这在 synchronized 场景下是做不到的。

5.1.3 公平锁与非公平锁

ReentrantLock 的构造函数可以传一个 boolean 参数:

Lock lock = new ReentrantLock(true);
  • true:公平锁;
  • false:非公平锁,默认。

公平锁:当线程获取锁时,不管锁有没有被其它线程持有,只要有其它线程在排队等锁,当前线程就必须到队列末尾排队;

非公平锁:当线程获取锁时,先抢一次锁,能抢到就直接得到锁,抢不到再去队列末尾排队等锁。

synchronized 可以认为是非公平锁。因为 JVM 规范对 monitorenter 指令的描述就是“如果 monitor 的计数器为 0,那么线程可以成功进入 monitor”,它不会管是不是有其它线程正在等着。

5.1.4 synchronized 与显式锁对比
  • 易用性:synchronized 更容易使用,显式锁都必须 try…finally,必须 new 出 Lock 对象,显式调用至少两次方法(获得锁和释放锁),使用上更麻烦一些;
  • 功能性:synchronized 能做到的,显式锁都能做到,同时显式锁可以实现更丰富的功能:
    • 可以尝试性的拿锁,不管拿不拿得到,都立即返回,不阻塞;
    • 可以设置获取锁失败时阻塞超时时间;
    • 可以在获取锁失败阻塞时被打断;
    • 可以实现为共享锁;
    • 更丰富的线程间协作方式;
    • 等…
  • 性能:据说在低版本的 JDK 中,synchronized 随着线程数量增大性能下降很快,显式锁性能更优(本人没有测试过);但可以确定的是,在高版本比如 JDK1.8 中,synchronized 有了较大的优化,synchronized 不等同于重量级锁,在运行时可能是轻量级锁、自适应锁、偏向锁,甚至不必要的锁操作 JIT 编译之后会直接去掉,在性能上应该与显式锁不相伯仲(未经测试,JVM 参数与实际运行环境可能对 synchronized 的性能有较大影响)。

个人总结:synchronized 能解决的问题,没必要用显式锁。

5.1.5 ReentrantReadWriteLock

ReentrantLock 大体上是复制了 synchronized 的功能。

另一种很常见的场景是:并发读写。

如果没有写线程更改数据,那么读线程可以随便并发,根本不需要加锁。在这种场景下,如果用 synchronized 或者 ReentrantLock 将所有线程全部加锁,效率就很低了。因此 JDK 直接提供了读写锁的实现。

读写锁将锁分为读锁和写锁,读锁为共享锁,写锁为独占锁。只要没有其它线程持有独占锁,那么多个线程可以同时获得共享锁。

读写锁的行为表现为:

  • 读写互斥:已经有线程获得读锁,则其它线程无法获得写锁;已经有线程获得写锁,则其它线程无法获得读锁;
  • 写写互斥:已经有线程获得写锁,则其它线程无法获得写锁;
  • 读读共享:已经有线程获得读锁,其它线程仍然可以获得读锁。

用法示例(读写互斥):

public class ReentrantRWLockTest {

    private static ReadWriteLock readWriteLock = new ReentrantReadWriteLock();
    private static Lock readLock = readWriteLock.readLock();
    private static Lock writeLock = readWriteLock.writeLock();

    public static void main(String[] args) throws InterruptedException {
        new Thread(() -> {
            System.out.println("read thread begin");
            readLock.lock();
            System.out.println("read thread acquired lock");
        }).start();
        new Thread(() -> {
            System.out.println("write thread begin");
            writeLock.lock();
            System.out.println("read thread acquired lock");
        }).start();
    }
}

输出:

read thread begin
read thread acquired lock
write thread begin

read 线程得到了锁,write 线程就无法得到锁。

注意:这里程序运行不会结束,write 线程死锁了,因为已经执行完的 read 线程没有释放锁。使用显式锁必须显式调用 unlock 释放锁。

5.2 AQS

5.2.1 什么是 AQS

AQS,指的是 JDK 中的 AbstractQueuedSynchronizer 抽象类。它是显式锁,以及 CountDownLatch,Semaphore 这些 API 的实现基础。只要是需要手工实现同步的地方,基本都可以借助于这个抽象类来方便地完成。

5.2.2 为什么需要 AQS

考虑加锁和释放锁的过程中需要做哪些事情:

加锁过程:

  • 需要决定哪些线程能获得锁,哪些线程不能获得锁;
  • 获得锁的线程去跑自己的逻辑;
  • 没有获得锁的线程进入阻塞状态;
  • 如果某个线程获得了共享锁,它同时还要唤醒其它试图获得共享锁的线程。

释放锁过程:

  • 线程释放锁(未持有锁不能释放锁);
  • 线程成功释放锁之后要唤醒其它线程。

在上面列举的这些行为中,只有两步是由具体应用场景决定的:

  • 需要决定哪些线程能获得锁,哪些线程不能获得锁;
  • 线程释放锁。

比如独占锁和共享锁对上面这两步的实现就不一样。

除上面两步以外的其它步骤全是通用逻辑,对于大多数应用场景来说都是不需要特别实现的,因此 JDK 将这些步骤抽象出来统一实现在了 AQS 类里面,所以 AQS 就出现了。

5.2.3 AQS 负责做什么

所有涉及到同步操作的场景中的通用逻辑,都由 AQS 代理,比如:

  • 管理竞争锁失败后等待获取锁的线程;
  • 有线程释放锁之后,唤醒其它等待获取锁的线程来拿锁;
  • 管理在 Condition 上 await 的线程;
  • 在 signal 之后,通知到相应的线程。

5.3 AQS 快速上手

5.3.1 基于 AQS 实现 Lock

作为 demo,实现一个简单的不可重入非公平锁。

package per.lvjc.concurrent.aqs.my;

import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.AbstractQueuedSynchronizer;
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.Lock;

/**
 * 不可重入非公平锁
 */
public class LvjcNonfairLock implements Lock {

    private final Sync sync;

    public LvjcNonfairLock() {
        sync = new Sync();
    }

    @Override
    public void lock() {
        sync.acquire(1);
    }

    @Override
    public void lockInterruptibly() throws InterruptedException {
        sync.acquireInterruptibly(1);
    }

    @Override
    public boolean tryLock() {
        throw new UnsupportedOperationException();
    }

    @Override
    public boolean tryLock(long time, TimeUnit unit) throws InterruptedException {
        throw new UnsupportedOperationException();
    }

    @Override
    public void unlock() {
        sync.release(1);
    }

    @Override
    public Condition newCondition() {
        return sync.new ConditionObject();
    }

    /**
     * 实现自己的同步器
     */
    static final class Sync extends AbstractQueuedSynchronizer {

        @Override
        protected boolean tryAcquire(int arg) {
            int state = getState();
            //state 初始值为 0,不等于 0 表示锁已经被某线程持有
            if (state != 0) {
                //不可重入,直接返回获取锁失败,当前线程会进入阻塞
                return false;
            }
            //state == 0,锁没有被持有,抢锁
            if (compareAndSetState(0, 1)) {
                //设置锁被当前线程持有,释放锁时会校验
                setExclusiveOwnerThread(Thread.currentThread());
                return true;
            }
            //没抢到锁,还是返回 false
            return false;
        }

        @Override
        protected boolean tryRelease(int arg) {
            if (Thread.currentThread() != getExclusiveOwnerThread()) {
                //锁没有被当前线程持有,报错
                throw new IllegalMonitorStateException();
            }
            //锁被当前线程持有,直接释放;
            //不需要 cas,因为独占锁这里不存在并发
            setState(0);
            //不可重入锁,不用计数,直接返回 true,让其它线程可以抢锁
            return true;
        }
    }

    private static final LvjcNonfairLock lock = new LvjcNonfairLock();

    public static void main(String[] args) {
        Runnable runnable = () -> {
            String threadName = Thread.currentThread().getName();
            System.out.println(threadName + " begin");
            try {
                lock.lock();
                System.out.println(threadName + " acquired lock");
                TimeUnit.SECONDS.sleep(1);
            } catch (InterruptedException e) {
                e.printStackTrace();
            } finally {
                System.out.println(threadName + " will unlock");
                lock.unlock();
            }
        };
        for (int i = 0; i < 5; i++) {
            new Thread(runnable, "thread-" + i).start();
        }
    }
}

实现步骤:

  • 定义自己业务需要的方法:比如这里就是 Lock 接口的 lock 和 unlock 方法;
  • 实现方法,调用 AQS 的方法组装自己的逻辑:比如这里的 lock 和 unlock 方法只要简单地调用 AQS 的 acquire 和 release 方法即可;
  • AQS 中有几个没有实现的方法,扩展 AQS 实现用到的方法:比如这里调用的 AQS 的 acquire 和 release 方法中 tryAcquire 和 tryRelease 是 AQS 没有实现的,自己实现即可。

这里由于实现的是不可重入锁,所以连续调用两次 lock 方法会自己锁自己。

5.3.2 支持 Condition

要支持 Condition 功能,实现起来非常简单,代码片段:

    @Override
    public Condition newCondition() {
        return sync.new ConditionObject();
    }

    static final class Sync extends AbstractQueuedSynchronizer {

        @Override
        protected boolean isHeldExclusively() {
            return Thread.currentThread() == getExclusiveOwnerThread();
        }
    }

两步:

  • 返回一个 ConditionObject 对象(AQS 已经帮我们实现了 Condition 接口);
  • 实现 AQS 的 isHeldExclusively 方法(这个方法会被 signal 方法调用)。

需要注意的是:使用 Condition 需要持有独占锁,持有共享锁或没有获得锁的线程调用 Condition 的 await 和 signal 方法会抛出 IllegalMonitorStateException。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 3
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值