专栏原创出处:github-源笔记文件 (opens new window)github-源码 (opens new window),欢迎 Star,转载请附上原文出处链接和本声明。

Java 并发编程专栏系列笔记,系统性学习可访问个人复盘笔记-技术博客 Java 并发编程 (opens new window)

# 为什么要设计 Lock 接口

锁是用来控制多个线程访问共享资源的方式,一般来说,一个锁能够防止多个线程同时访问共享资源 (但是有些锁可以允许多个线程并发的访问共享资源,比如读写锁)。

在 Lock 接口出现之前,Java 程序是靠 synchronized 关键字实现锁功能的,而 Java SE 5 之后,并发包中新增了 Lock 接口 (以及相关实现类) 用来实现锁功能,它提供了与 synchronized 关键字类似的同步功能, 只是在使用时需要显式地获取和释放锁。虽然它缺少了 (通过 synchronized 块或者方法所提供的) 隐式获取释放锁的便捷性,但是却拥有了锁获取与释放的可操作性、可中断的获取锁以及超时获取锁等多种 synchronized 关键字所不具备的同步特性。

使用 synchronized 关键字将会隐式地获取锁,但是它将锁的获取和释放固化了,也就是先获取再释放。当然,这种方式简化了同步的管理,可是扩展性没有显示的锁获取和释放来的好。

例如,针对一个场景,手把手进行锁获取和释放,先获得锁 A,然后再获取锁 B,当锁 B 获得后,释放锁 A 同时获取锁 C,当锁 C 获得后,再释放 B 同时获取锁 D,以此类推。这种场景下,synchronized 关键字就不那么容易实现了,而使用 Lock 却容易许多。

有关并发关键字及锁的比较参考前面文章《并发操作比较(CAS、volatile、synchronized、Lock)》,有关锁的内存语义参考前面文章《锁的内存语义》

# 如何使用 Lock

  • 不要将获取锁的过程写在 try 块中,因为如果在获取锁 (自定义锁的实现) 时发生了异常,异常抛出的同时,也会导致锁无故释放。
  • 在 finally 块中释放锁,目的是保证在获取到锁之后,最终能够被释放。

正确的使用方式:

Lock lock = new ReentrantLock();
lock.lock();
try {
 // 业务逻辑处理
} finally {
  lock.unlock();
}

# Lock 的 API 说明

public interface Lock {

    // 阻塞地获取锁,直到获取到锁才返回,而且是不可中断的。
    void lock();

    // 可中断地获取锁,与 lock() 方法的不同之处,在于该方法在阻塞等待锁的过程中会响应中断。
    void lockInterruptibly() throws InterruptedException;

    // 尝试非阻塞地获取锁,即调用该方法后,立刻返回,成功获取锁,返回 true,失败则返回 false。
    boolean tryLock();
    
    // 可中断的超时获取锁,在以下 3 种情况下会返回:
    // 1. 当前线程在指定时间内获得了锁;
    // 2. 当前线程在指定时间内被中断;
    // 3. 指定时间结束(超时结束),返回 false;
    boolean tryLock(long time, TimeUnit unit) throws InterruptedException;
    
    // 释放锁
    void unlock();

    // 等待通知组件,当前线程只有获得了锁,才能调用该组件的 wait() 方法,调用后,线程将会释放锁
    Condition newCondition();
}

# Lock 接口的实现及依赖

图例:

  • +红色线表示内部类
  • 绿色虚线表示接口实现关系
  • 蓝色实线表示类继承关系

Lock 接口的实现类主要有:

  • 重入锁(ReentrantLock)
  • 读锁(ReadLock)
  • 写锁(WriteLock)

Lock 接口的实现基本都是通过聚合了一个同步器(AbstractQueuedSynchronizer 缩写为 AQS)的子类来完成线程访问控制的。《并发编程-锁》中我们逐个分析上图中的内容。

最后修改时间: 2/17/2020, 4:43:04 AM