《Java并发编程的艺术》第五章 Java中的锁

第五章 Java中的锁

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

5.1 Lock 接口

Lock 接口提供了与 synchronized 关键字类似的同步功能,只是在使用时需要显示地获取和释放锁。虽然 Lock 接口缺少了 synchronized 方法隐式获取释放锁的便捷性,但是却拥有了锁获取与释放的可操作性、可中断的获取锁以及超时获取锁等多种 synchronized 关键字所不具备的同步特性。

Lock 接口的 API:

方法名 描述
void lock() 获取锁
void lockInterruptibly() throws InterruptedException 可中断的获取锁
boolean tryLock() 尝试非阻塞的获取锁
boolean tryLock(long time, TimeUnit unit) throws InterruptedException 超时获取锁
void unlock() 释放锁
Condition newCondition() 获取等待通知组件

5.2 队列同步器

队列同步器(AbstractQueuedSynchronizer),是用来构建锁或者其他同步组件的基础框架,通过内置 FIFO 队列来完成资源获取线程的排队工作。

同步器是实现锁的关键: – 锁面向使用者,定义了使用者与锁交互的接口,隐藏了实现细节; – 同步器面向锁的实现者,简化了锁的实现方式,屏蔽了同步状态管理、线程的排队、等待与唤醒等底层操作。

5.2.1 同步器接口

队列同步器提供了三个抽象方法供实现类实现:

  1. getState():获取当前同步状态
  2. setState(int newState):设置当前同步状态
  3. compareAndSetState(int expect,int update):使用CAS设置当前状态

同时,同步器提供了多个可重写的方法:

  1. tryAcquire(int arg):独占式获取同步状态
  2. tryRelease(int arg):独占式释放同步状态
  3. tryAcquireShared(int arg):共享式获取同步状态
  4. tryReleaseShared(int arg):共享式释放同步状态
  5. isHeldExclusively():当前同步器是否被线程独占
import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.AbstractQueuedSynchronizer;
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.Lock;

/**
 * 使用 Mutex 时并不会直接与内部同步器的实现打交道,而是调用 Mutex 提供的方法.
 * 如 获取锁的lock() 方法,只需要在方法实现中调用同步器的 acquire(int arg) 即可。
 * 当前线程后去同步状态失败后,会被加入到同步队列中等待
 */
public class Mutex implements Lock {

    private static class Sync extends AbstractQueuedSynchronizer {
        // 是否处于占用状态
        @Override
        protected boolean isHeldExclusively() {
            return getState() == 1;
        }

        // 当状态为0时获取锁
        @Override
        protected boolean tryAcquire(int arg) {
            // 通过CAS设置state
            if(compareAndSetState(0,1)){
                setExclusiveOwnerThread(Thread.currentThread());
                return true;
            }
            return false;
        }

        // 释放锁,将状态设置为0
        @Override
        protected boolean tryRelease(int arg) {
            if(getState() == 0) throw new IllegalMonitorStateException();
            setExclusiveOwnerThread(null);
            setState(0);
            return true;
        }

        // 返回一个 Condition ,每个Condition 都包含了一个 condition 队列
        Condition newCondition() {
            return new ConditionObject();
        }
    }

    private final Sync sync = new Sync();

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

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

    @Override
    public boolean tryLock() {
        return sync.tryAcquire(1);
    }

    @Override
    public boolean tryLock(long time, TimeUnit unit) throws InterruptedException {
        return sync.tryAcquireNanos(1,unit.toNanos(time));
    }

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

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

5.2.2 队列同步器的实现分析

1. 同步队列

同步器内部使用一个同步队列(FIFO)来完成同步状态的管理,当线程获取同步状态失败后,会将当前线程及其等待状态等信息构造成为一个节点(Node)并加入同步队列,同时阻塞当前线程。

当同步状态释放时,会将首节点唤醒,再次尝试获取同步状态。

节点(Node)的属性类型与名称描述

属性类型与名称 描述
int waitStatus 等待状态
Node prev 前驱节点
Node next 后继节点
Node nextWaiter 等待队列中的后继节点
Thread 获取同步状态的线程

类似于双向链表

2. 独占式同步状态获取与释放

调用同步器的 acquire(int arg) 方法,可以获取同步状态,JDK 中acquire的实现如下:

    /**
     * Acquires in exclusive mode, ignoring interrupts.  Implemented
     * by invoking at least once {@link #tryAcquire},
     * returning on success.  Otherwise the thread is queued, possibly
     * repeatedly blocking and unblocking, invoking {@link
     * #tryAcquire} until success.  This method can be used
     * to implement method {@link Lock#lock}.
     *
     * @param arg the acquire argument.  This value is conveyed to
     *        {@link #tryAcquire} but is otherwise uninterpreted and
     *        can represent anything you like.
     */
    public final void acquire(int arg) {
        if (!tryAcquire(arg) &&
            acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
            selfInterrupt();
    }

其主要逻辑为:1. tryAcquire 尝试获取同步状态,如果失败的话则构造同步节点,并通过addWirter方法将该节点加入到同步队列的尾部;2. 调用 acquireQueued 方法,使该节点以死循环的方式获取同步状态,如果获取不到则阻塞节点中的线程。

/**
     * Inserts node into queue, initializing if necessary. See picture above.
     * @param node the node to insert
     * @return node's predecessor
     */
    private Node enq(final Node node) {
        for (;;) {
            Node t = tail;
            if (t == null) { // Must initialize
                if (compareAndSetHead(new Node()))
                    tail = head;
            } else {
                node.prev = t;
                if (compareAndSetTail(t, node)) {
                    t.next = node;
                    return t;
                }
            }
        }
    }

    /**
     * Creates and enqueues node for current thread and given mode.
     *
     * @param mode Node.EXCLUSIVE for exclusive, Node.SHARED for shared
     * @return the new node
     */
    private Node addWaiter(Node mode) {
        Node node = new Node(Thread.currentThread(), mode);
        // Try the fast path of enq; backup to full enq on failure
        Node pred = tail;
        if (pred != null) {
            node.prev = pred;
            if (compareAndSetTail(pred, node)) {
                pred.next = node;
                return node;
            }
        }
        enq(node);
        return node;
    }

独占式同步状态获取流程

3. 共享式同步状态获取与释放

共享式与独占式最主要的区别在于,同一时刻能否有多个线程同时获取到同步状态。

主要方法有:acquireShared(int arg)、tryAcquireShared(int arg)、releaseShared(int arg)

4. 独占式超时获取同步状态

同步器的 doAcquireNanos(int arg, long nanosTimeout) 方法可以超时获取同步状态,即在指定的时间段内获取同步状态,成功返回true。

独占式超时获取同步状态流程

5.3 重入锁 ReentrantLock

重入锁,表示该锁能够支持一个线程对资源的重复加锁。即 ReentrantLock 在调用 lock() 方法时已经获取到锁的线程,再次调用 lock() 方法依旧能够获取锁而不被阻塞。

此外,重入锁还支持获取锁时的公平性与非公平性选择。

synchronized 关键字隐式的支持重入

1. 实现重进入

重进入,指任意线程在获取锁之后能够再次获取该锁而不被阻塞,实现该特性需要解决两个问题:

  1. 线程再次获取锁:即锁需要识别获取的锁是否为当前占据锁的线程。
  2. 锁的最终释放:通过计数自增的方式表示线程重复n次加锁,锁释放也是同理。

2. 公平与非公平获取锁的区别

如果一个锁是公平的,那么锁的获取顺序就应该符合FIFO原则。

一般情况下,非公平锁的效率是要高于公平锁的。但是非公平锁可能使线程“饥饿”,即先来的线程因优先级低一直处于等待状态。

5.4 读写锁

上面的锁基本都是排他锁,在同一时刻只允许一个线程进行访问。而读写锁能在同一时刻允许多个读线程访问,但在写线程访问时,所有读、写线程均被阻塞

5.4.1 ReentrantReadWriteLock 的特性与API

特性 说明
公平性选择 支持公平与非公平的锁获取方式
重进入 支持重进入,即写锁能支持一个线程多次获取,读锁也是
锁降级 写锁能够降级为读锁
方法名称 描述
int getReadLockCount() 返回当前读锁被获取的次数
int getReadHoldCount() 返回当前线程获取读锁的次数
boolean isWriteLocked() 判断写锁是否被获取
int getWriteHoldCount() 返回当前写锁被获取的次数

5.4.2 读写锁的实现分析

1. 读写状态的设计

读写锁同样依赖于自定义同步器来实现同步功能,但是读写锁的同步器需要维护多个读线程和一个写线程

2. 写锁的获取与释放

写锁是一个支持重入的排他锁,只能被一个线程获取及重入。

如果当前线程在获取写锁时,读锁已经被获取(state!=0)或者该线程不是获取写锁的线程,则进入等待状态。

写锁的释放与 ReentrantLock 类似,都是维护一个 写状态属性,为0时表示写锁被释放,同时写锁的修改对其后的读锁可见。

3. 读锁的获取与释放

读锁是一个支持重入的共享锁,能被多个线程同时获取。

如果当前没有其他写线程,则读锁总会被成功获取。

读锁的每次释放均减少读状态。

4. 锁降级

写锁降级为读锁,指当前拥有写锁的线程,再获取读锁,随后释放之前拥有的写锁的过程。

ReentrantReadWriteLock 不支持锁升级。

5.5 LockSupport 工具

当需要阻塞或唤醒一个线程的时候,会使用 LockSupport 工具类来完成相应的工作。

方法名称 描述
void park() 阻塞当前线程
void parkNanos(long nanos) 阻塞当前线程nanos纳秒
void parkUntil(long deadline) 阻塞当前线程知道deadline
void unpark(Thread thread) 唤醒处于阻塞状态的线程

5.6 Condition 接口

Object 对象拥有一组监视器方法,包含:wait()、wait(long timeout)、notify()及notifyAll() ,这些方法与 synchronized 关键字配合,实现 等待/通知模式。

Condition 也提供了类似的方法,与Lock 配合实现 等待/通知 模式。

文章作者: koral
文章链接: http://luokaiii.github.io/2019/06/10/读书笔记/《Java并发编程的艺术》/5.Java中的锁/
版权声明: 本博客所有文章除特别声明外,均采用 CC BY-NC-SA 4.0 许可协议。转载请注明来自