您的当前位置:首页正文

Java并发包下的锁(2)——队列同步器

2024-12-03 来源:个人技术集锦

同步器简介
1. 什么是同步器?

 同步器(AQS)是一个抽象类,子类通过继承同步器并实现它的抽象方法来管理同步状态。同步器提供了3个方法来保证同步状态的修改是安全的:

  • getState():得到当前线程的同步状态

  • setState(int i):为线程设置一个新的同步状态

  • compareAndSetState(int expect, int update):使用CAS算法保证设置状态过程是原子的

 同步器既可以支持独占式的获取同步状态,也可以支持共享式的获取同步状态,这样可以很方便的实现不同的类型的组件(典型的独占式组件:ReentrantLock;典型的共享式同步组件:ReentrantReadWriteLock)

同步器是实现锁(任意同步组建)的基石,同步器在锁中的实现,可以理解为:

  • 锁是向使用者的,它定义了使用者与锁交互的接口,隐藏了实现的细节

  • 同步器面向的是锁的实现者,它简化了锁的实现方式,屏蔽了同步状态管理、线程的排队、等待与唤醒等底层操作

2. 同步器的API展示

 同步器的设计基于了,使用者需要继承同步器并重写指定的方法,随后将同步器组合在自定义的同步组件的实现中,并调用同步器提供的模板方法,这些模板方法会调用使用者重写的方法。

同步器可重写的方法如下:

public abstract class AbstractQueuedSynchronizer
    extends AbstractOwnableSynchronizer
    implements java.io.Serializable {
    
    // 独占式的获取同步状态,实现该方法需要查询当前状态并判断同步状态
    // 是否符合预期,然后使用CAS设置同步状态
    protected boolean tryAcquire(int arg) {
        throw new UnsupportedOperationException();
    }
    
    // 独占式释放同步状态,等待获取同步状态的线程将有机会获取同步状态
    protected boolean tryRelease(int arg) {
        throw new UnsupportedOperationException();
    }
    
    // 共享式获取同步状态,返回大于等于0的值,表示获取成功;返回小于
    // 0的值,表示获取失败
     protected int tryAcquireShared(int arg) {
        throw new UnsupportedOperationException();
    }
    
    // 共享式释放同步状态
    protected boolean tryReleaseShared(int arg) {
        throw new UnsupportedOperationException();
    }
    
    // 返回一个boolean值,表示是否被当前线程所独占:当前同步器是否被
    // 当前线程所独占
    protected boolean isHeldExclusively() {
        throw new UnsupportedOperationException();
    }
}

实现自定义同步组件时,将会调用同步器提供的模板方法,同步器提供的模板方法如下:


// 独占式获取同步状态,该方法会调用重写的 tryAcquire(int arg) 方法。
// 如果当前线程获取同步状态成功,则返回;如果不成功,进入同步队列等待
public final void acquire(int arg) {
        if (!tryAcquire(arg) &&
            acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
            selfInterrupt();
}

// 该方法与acquire(int arg)相同,区别是该方法响应中断
// 当前线程因未获取到同步状态而进入同步队列中,如果被中断
// 该方法会抛出 InterruptedException 异常并返回
public final void acquireInterruptibly(int arg)
            throws InterruptedException {
        // 如果中断抛出异常    
        if (Thread.interrupted())
            throw new InterruptedException();
        // 尝试中断的获取同步状态
        if (!tryAcquire(arg))
            doAcquireInterruptibly(arg);
}

// 在 acquireInterruptibly(int arg) 上加上了超时限制
// 如果当前线程在超时时间内没有获取到同步状态,将会返回false
// 获取到了则返回true
public final boolean tryAcquireNanos(int arg, long nanosTimeout)
            throws InterruptedException {
        if (Thread.interrupted())
            throw new InterruptedException();
        return tryAcquire(arg) ||
            doAcquireNanos(arg, nanosTimeout);
}

// 共享式的获取同步状态,如果当前线程没有获取到同步状态,将会进入
// 同步队列等待,与独占式的区别:同一时刻可以有多个线程获取到同步状态
public final void acquireShared(int arg) {
        if (tryAcquireShared(arg) < 0)
            doAcquireShared(arg);
}

// 响应中断的获取同步状态
public final void acquireSharedInterruptibly(int arg)
            throws InterruptedException {
        if (Thread.interrupted())
            throw new InterruptedException();
        if (tryAcquireShared(arg) < 0)
            doAcquireSharedInterruptibly(arg);
}

// 超时限制
public final boolean tryAcquireSharedNanos(int arg, long nanosTimeout)
            throws InterruptedException {
        if (Thread.interrupted())
            throw new InterruptedException();
        return tryAcquireShared(arg) >= 0 ||
            doAcquireSharedNanos(arg, nanosTimeout);
}

// 调用重写的tryRelease()方法,独占式的释放同步状态,
// 在释放同步状态之后,将同步队列中的第一个节点包含的线程唤醒
public final boolean release(int arg) {
        if (tryRelease(arg)) {
            Node h = head;
            if (h != null && h.waitStatus != 0)
                // 唤醒同步队列中的下一个节点
                unparkSuccessor(h);
            return true;
        }
        return false;
}

// 共享式的释放同步状态
public final boolean releaseShared(int arg) {
    if (tryReleaseShared(arg)) {
        doReleaseShared();
        return true;
    }
    return false;
}

// 获取等待在同步队列上的线程集合
public final Collection<Thread> getQueuedThreads() {
        ArrayList<Thread> list = new ArrayList<Thread>();
        for (Node p = tail; p != null; p = p.prev) {
            Thread t = p.thread;
            if (t != null)
                list.add(t);
        }
        return list;
}

模板方法分为3类:

  • 独占式获取与释放同步状态:同一时刻只能有一个线程获取到锁,其他线程只能在同步队列中等待获取。在获取到锁的线程释放锁之后,后继线程才能获取锁

  • 共享式获取与释放同步状态:同一时刻可以有多个线程获取到同步状态,如: 读锁

  • 查询同步队列中等待的线程情况

3. 自定义同步组件

下面的代码展示了如何自定义一个同步组件:

// 自定义同步组建
public class MyAQS implements Lock {
    // 静态内部类,自定义同步器
    private static class Sync extends AbstractQueuedSynchronizer {

        // 当状态为0时,获取锁
        protected boolean tryAcquire(int acquires) {
            if (compareAndSetState(0, 1)) {
                setExclusiveOwnerThread(Thread.currentThread());
                return true;
            }
            return false;
        }

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

        // 当前线程是否被独占
        protected boolean isHeldExclusively() {
            return getState() == 1;
        }

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

    private 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();
    }
}

测试上面的自定义同步组件:


// 线程安全的修改一个共享变量
public class TestMyAQS {
    // 构建一个同步器对象
    private static MyAQS lock = new MyAQS();

    public static void main(String[] args) {
        Job job = new Job();

        // 开启5个线程去修改mutext的值
        for (int i = 0; i < 5; i++) {
            Thread thread = new Thread(job, "thread-" + i);
            thread.start();
        }

    }

    public static class Job implements Runnable {

        private int mutext = 1;

        @Override
        public void run() {
            while (true) {
                // 加锁
                lock.lock();
                try {
                    write();
                    TimeUnit.SECONDS.sleep(1);
                    System.out.println(Thread.currentThread().getName());
                    System.out.println("当前值为:" + read());
                } catch (Exception e) {
                    e.printStackTrace();
                } finally {
                    // 释放锁
                    lock.unlock();
                }
            }
        }

        public void write() {
            mutext++;
        }

        public int read() {
            return mutext;
        }
    }
}

// 结果如下:
thread-0
当前值为:2
thread-1
当前值为:3
thread-1
当前值为:4
thread-1
当前值为:5
thread-2
当前值为:6
thread-3
当前值为:7
thread-3
当前值为:8
thread-4
当前值为:9
thread-4
当前值为:10
同步器的实现

上面的内容了解了什么是同步器,简单介绍了同步器的几个API,并自定义了一个同步组件去加深对同步器的理解,接下来将会去深入的了解同步器是如何完成线程同步的,主要有以下几个内容:

  • 同步队列:同步器实现的核心

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

  • 独占式超时获取同步状态

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

1. 同步队列

 同步器依赖内部的同步队列(一个FIFO的双向队列)来完成同步状态的管理,当前线程获取同步状态失败时,同步器会将当前线程以及等待状态信息构建成一个节点(Node),并将其加入到同步队列,同时会阻塞当前线程。 当同步状态释放时,会把首节点中的线程唤醒,使其再次尝试获取同步状态。

 同步队列中的Node用来保存获取同步状态失败的线程引用,等待状态以及前驱和后继节点。Node是AQS的一个静态内部类,其源码如下:

static final class Node {

        /**
        等待状态
        1. CANCELLED:值为1,由于同步队列中等待的线程等待超时或者被中断,需要从
        同步队列中取消等待
        2. SIGNAL:值为-1,后继节点的线程处于等待状态,而当前节点的线程如果释放
        了同步状态或者被取消,将会通知后继节点,使后继节点的线程得以云行
        3. CONDITION:值为-2,节点在等待队列中,节点线程等待在Condition上,当其
        他线程对Condition调用了signal()方法后,该节点将会从等待队列转移到同步队
        列,加入到对同步状态的获取中
        4. PROPAGATE:值为-3,表示下一次共享式同步状态的获取将会无条件的传播下去
        5. INITIAL:值为0,初始状态
        */
        volatile int waitStatus;
        
        // 指向前驱节点
        volatile Node prev;
        
        // 指向后继节点
        volatile Node next;
        
        // 获取同步状态的线程
        volatile Thread thread;
        
        // 指向等待队列中的节点
        Node nextWaiter;
        
        // 是否是共享式获取锁
        final boolean isShared() {
            return nextWaiter == SHARED;
        }
}

节点是构成同步队列的基础,其抽象结构如下图所示:

 同步器拥有首节点(head)和尾节点(tail),没有成功获取同步状态的线程将会成为节点加入该队列的尾部。其加入队列的过程是基于CAS设置尾节点的方法:compareAndSetTail(Node expect, Node update),保证了线程安全性。只有节点设置成功后,同步器的tail节点才会和尾节点建立关联。

 同步队列是遵循FIFO的双向队列,首节点是获取同步状态成功的节点,首节点的线程在释放同步状态时,将会唤醒后继节点,后继将会在获取同步状态成功时将自己设置为首节点。上图红色虚线框中的节点和黑色虚线框中的节点演示就是上述过程。

 设置首节点是通过获取同步状态成功的线程来完成的,由于只有一个线程能成功获取到同步状态,因此设置头结点的方法不需要CAS的保证,只需要将首节点设置为为原首节点的后继节点并断开原首节点的next引用就行

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

&emso;使用acquire(int arg)方法可以获取同步状态,该方法对中断不敏感:线程进入同步队列之后,后续对线程进行中断操作时,线程不会从同步队列中移出。

/**
该代码完成的功能如下:

==> 完成对同步状态的获取
==> 节点构造
==> 加入同步队列
==> 在同步队列中自旋

*/
public final void acquire(int arg) {
        // 线程安全的获取同步状态
        if (!tryAcquire(arg) &&
            acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
            selfInterrupt();
}

 上面代码的运行逻辑为:先调用自定义同步器实现的 tryAcquire(arg) 方法,保证线程安全的获取同步状态,如果获取失败,则构造同步节点(独占式:Node.EXCLUSIVE),并通过 addWaiter(Node node) 方法将该节点加入同步队列的尾部,最后调用 acquireQueued() 方法,使得该节点以死循环的方式获取同步状态。如果获取不到则阻塞节点中的线程,被阻塞线程的唤醒主要靠前驱节点的出队或者阻塞线程被中断来实现。下面的源码展示了所分析的逻辑:

// 添加节点到同步队列的尾部
private Node addWaiter(Node mode) {
        // 将当前线程构造成一个节点
        Node node = new Node(Thread.currentThread(), mode);
        // 尝试在尾部快速添加
        Node pred = tail;
        if (pred != null) {
            node.prev = pred;
            // CAS设置尾节点,保证线程安全
            if (compareAndSetTail(pred, node)) {
                pred.next = node;
                return node;
            }
        }
        // 如果添加不成功则进入死循环
        enq(node);
        return node;
}

// 通过死循环来保证节点的正确添加
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;
            }
        }
    }
}

// 节点添加到同步队列之后,尝试获取同步状态
final boolean acquireQueued(final Node node, int arg) {
    boolean failed = true;
    try {
        boolean interrupted = false;
        for (;;) {
            final Node p = node.predecessor();
            if (p == head && tryAcquire(arg)) {
                setHead(node);
                p.next = null; // help GC
                failed = false;
                return interrupted;
            }
            if (shouldParkAfterFailedAcquire(p, node) &&
                parkAndCheckInterrupt())
                interrupted = true;
        }
    } finally {
        if (failed)
            cancelAcquire(node);
    }
}

前驱节点是头结点才能尝试获取同步状态,原因如下:

  • 头结点是成功获取到同步状态的节点,头结点的线程释放了同步状态之后,将会唤醒其后继节点,后继节点的线程被唤醒后需要检查自己的前驱节点是不是头结点

  • 维护同步队列的FIFO原则:在同步队列等待的非首节点线程会不断的循环检查自己是否满足获取同步状态的条件,节点之间基本不相互通信,只是简单判断自己的前驱是不是头结点,这就要求节点的释放需要满足FIFO规则。

下图表示节点自旋获取同步状态:

下图展示了独占式同步状态的获取流程:

 在获取同步状态时,同步器维护一个同步队列,获取状态失败的线程都会被加入到队列中进行自旋;移除队列(停止自旋)的条件是前驱节点为头结点且成功获取同步状态。在释放同步状态时,同步器调用tryRelease()方法释放同步状态,然后唤醒头结点的后继节点。

释放同步状态的方法源码如下:

public final boolean release(int arg) {
    if (tryRelease(arg)) {
        Node h = head;
        if (h != null && h.waitStatus != 0)
            // 唤醒后继节点
            unparkSuccessor(h);
        return true;
    }
    return false;
}
3. 独占式超时获取同步状态

响应中断的同步状态获取——acquireInterruptibly(int arg)

 在JDK1.5之前,当一个线程获取不到锁而被阻塞在synchronized之外时,对该线程进行中断操作,此时线程的中断标志位被修改,但线程依然会阻塞在synchronized上,等待获取锁。而在JDK1.5中,同步器提供了 acquireInterruptibly(int arg) 方法,该方法在获取同步状态时,如果当前线程被中断,会立即返回并抛出 InterruptedException 异常。

public final void acquireInterruptibly(int arg)
            throws InterruptedException {
        // 响应中断
        if (Thread.interrupted())
            throw new InterruptedException();
        if (!tryAcquire(arg))
            doAcquireInterruptibly(arg);
}

增强版的 acquireInterruptibly(int arg)——tryAcquireNanos(int arg, long nanosTimeout)

 通过调用同步器的 tryAcquireNanos() 方法可以尝试超时获取同步状态:在指定的时间内获得同步状态,如果获得则返回true,否则返回false。

public final boolean tryAcquireNanos(int arg, long nanosTimeout)
            throws InterruptedException {
        // 响应中断
        if (Thread.interrupted())
            throw new InterruptedException();
        // 超时获取同步状态
        return tryAcquire(arg) ||
            doAcquireNanos(arg, nanosTimeout);
}

 从上面的源码我们可以得出很重要的一点:该方法获取同步状态时对中断状态敏感,也就是说该方法在等待获取同步状态时,如果当前线程被中断,会立即返回并抛出 InterruptedException 异常。我们会发现,该方法就是一个增强版的 acquireInterruptibly(int arg),不但具有响应中断的能力,还具备了超时获取同步状态的功能。

 那同步器是使用什么方法来完成超时获取与响应中断的呢?doAcquireNanos(int arg, long nanosTimeout) 方法就是哪个重要的核心角色,我们来看看它的源码:

 private boolean doAcquireNanos(int arg, long nanosTimeout)
            throws InterruptedException {
        // 超时时间小于等于0,超时,返回false
        if (nanosTimeout <= 0L)
            return false;
        // 截止时间
        final long deadline = System.nanoTime() + nanosTimeout;
        // 将节点以独占的方式加入同步队列
        final Node node = addWaiter(Node.EXCLUSIVE);
        boolean failed = true;
        try {
            // 与独占获取同步状态类似:自旋获取同步状态
            // 当节点的前驱节点为头结点时尝试获取,如果成功则返回
            // 如果失败判断是否超时
            for (;;) {
                final Node p = node.predecessor();
                if (p == head && tryAcquire(arg)) {
                    setHead(node);
                    p.next = null; // help GC
                    failed = false;
                    return true;
                }
                // 超时时间
                nanosTimeout = deadline - System.nanoTime();
                // 超时则返回false,获取同步状态失败
                if (nanosTimeout <= 0L)
                    return false;
                // 如果没有超时,则继续睡眠,直到nanosTimeout=0时返回
                if (shouldParkAfterFailedAcquire(p, node) &&
                    nanosTimeout > spinForTimeoutThreshold)
                    // nanosTimeout=0时从该方法返回
                    LockSupport.parkNanos(this, nanosTimeout);
                // 如果发生中断,抛出异常并出队列
                if (Thread.interrupted())
                    throw new InterruptedException();
            }
        } finally {
            if (failed)
                cancelAcquire(node);
        }
}

// 超时公式如下:
nanosTimeout -= now -lastTime

nanosTimeout: 时间间隔
now: 当前唤醒时间
lastTime:上次唤醒时间

nanosTimeout > 0 表示时间未到,还需要睡眠nanosTimeout秒
nanosTimeout <= 0 表示时间已到,唤醒

 独占式超时获取同步状态和独占式获取同步状态的流程很相似,区别在于未获取到同步状态时的处理逻辑:acquire(int args) 在未获取到同步状态时,将会使当前线程一直处于等待状态;doAcquireNanos(int arg, long nanosTimeout) 会使当前线程等待 nanosTimeout 纳秒,如果在 nanosTimeout 纳秒内没有获取到同步状态,将会从等待逻辑中自动返回。

下图是独占式超时获取同步状态的流程:

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

 共享式获取与独占式获取最主要的区别在于同一时刻能否有多个线程同时获取到同步状态。例如:一个线程对共享资源区进行读操作,那么所有的写线程将会被阻塞;如果一个写线程对共享资源区进行写操作,那么所有的读线程将会被阻塞。在这里,读线程之间是共享的,同一时刻允许有多个读线程对共享资源区进行读取;读线程和写线程之间是互斥的,同一时刻只能有一种线程访问资源,另一种线程必须阻塞;写线程之间也是互斥的,同一时刻只能有一个线程修改数据,其他线程必须同步阻塞。如下图所示:

 共享式同步状态获取使用到了同步器的acquireShared(int arg)方法,其核心是doAcquireShared(int arg)方法,下面我们来看看这两个方法的源码:

public final void acquireShared(int arg) {
    // 尝试获取同步状态
    if (tryAcquireShared(arg) < 0)
        doAcquireShared(arg);
}

private void doAcquireShared(int arg) { 
        // 共享节点
        final Node node = addWaiter(Node.SHARED);
        boolean failed = true;
        try {
            boolean interrupted = false;
            // 自旋获取
            for (;;) {
                final Node p = node.predecessor();
                if (p == head) {
                    int r = tryAcquireShared(arg);
                    // 如果返回值大于等于0,获取共享状态成功,返回
                    if (r >= 0) {
                        setHeadAndPropagate(node, r);
                        p.next = null; // help GC
                        if (interrupted)
                            selfInterrupt();
                        failed = false;
                        return;
                    }
                }
                if (shouldParkAfterFailedAcquire(p, node) &&
                    parkAndCheckInterrupt())
                    interrupted = true;
            }
        } finally {
            if (failed)
                cancelAcquire(node);
        }
}

共享式释放同步状态的方法是 releaseShared(int arg),其源码如下:

public final boolean releaseShared(int arg) {
        if (tryReleaseShared(arg)) {
            doReleaseShared();
            return true;
        }
        return false;
}

该方法在释放同步状态后,将会唤醒后续处于等待状态的节点。

LockSupport工具

 LockSupport 是一个工具类,为阻塞或唤醒线程提供了一组最基本的工具方法,LockSupport 也成为了构建同步组建的基础工具,其中同步器的实现也得到了 LockSupport 的支持。LockSupport 定义了一组以 park 开头的方法来阻塞当前线程,使用 unpark(Thread) 来唤醒一个被阻塞的线程。其API如下:

// 阻塞当前线程,如果调用unpark(Thread thread)方法或者当前线程
// 中断,将从该方法返回
public static void park() {
        UNSAFE.park(false, 0L);
}

// 阻塞当前线程 nanos 纳秒,超时限制
// 参数 blocker 表示当前线程在等待的对象:阻塞对象
public static void parkNanos(Object blocker, long nanos) {
        if (nanos > 0) {
            Thread t = Thread.currentThread();
            setBlocker(t, blocker);
            UNSAFE.park(false, nanos);
            setBlocker(t, null);
        }
}

// 阻塞当前线程,直到 deadline 时间(从1970年到deadline的毫秒数)
public static void parkUntil(Object blocker, long deadline) {
        Thread t = Thread.currentThread();
        setBlocker(t, blocker);
        UNSAFE.park(true, deadline);
        setBlocker(t, null);
}

// 唤醒处于阻塞状态的线程
public static void unpark(Thread thread) {
        if (thread != null)
            UNSAFE.unpark(thread);
}
参考

《Java并发编程的艺术》

《Java并发编程实战》

显示全文