Java并发编程工具锁深入了解原理实现

目录

一、Lock接口

二、队列同步器AbstractQueuedSynchronizer

1.概述

2.接口与实例

3.同步器的实现分析

3.1 同步队列

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

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

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

三、重入锁ReentrantLock

1.概述

2.实现重进入获取和释放

3.公平和非公平锁的区别

四、Condition接口

1.Condition接口

2.Condition的实现分析

2.1等待队列

五、并发工具类

1.CountDownLatch

2.CyclicBarrier

3.Semaphore

4.Exchanger


一、Lock接口

锁是用来控制多个线程访问共享资源的方式,一般来说,一个锁能够防止多个线程同时访问共享资源(但是有些锁可以允许多个线程并发的访问共享资源,比如读写锁)。在Lock接口出现之前,Java程序是靠synchronized关键字实现锁功能的,而Java SE5之后,并发包中新增了Lock接口(以及相关实现类)用来实现锁功能,它提供了与synchronized关键字类似的同步功能,只是在使用时需要显式地获取和释放锁。虽然它缺少了(通过synchronized块或者方法所提供的)隐式获取释放锁的便捷性,但却拥有了锁获取与释放的可操作性、可中断的获取锁以及超时获取锁等多种synchronized关键字所不具备的同步特性。使用synchronized关键字将会隐式地获取锁,但是它将锁的获取和释放固化了,也就是先获取再释放。

Lock接口的使用很简单,如下所示:

Lock lock = new ReentrantLock();
lock.lock();
try {
} finally {
    lock.unlock();
}

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

Lock接口提供synchronized关键字所不具备的特性如下:

特性

描述

尝试非阻塞的获取锁

当前线程尝试获取锁,如果此时锁没有被其它线程持有,则获取并持有锁。

能被中断的获取锁

与synchronized不同,获取到的锁能够响应中断,当获取到锁的线程被中断时,中断异常被抛出,同时锁被释放。

超时获取锁

在指定的截止时间之前获取锁,如果时间到了仍没有获取到,则取消获取。

其接口的API方法功能如下:

方法名称

描述

void lock()

获取锁,调用该方法的当前线程将会获取锁,获得锁后,该方法返回结束

void lockInterruptibly()

可中断的获取锁,和lock方法不同之处在于该方法会响应中断,即获取锁过程中可以中断当前线程

boolean tryLock()

尝试非阻塞的获取锁,调用该方法后立马返回,如果可以获取返回true,不能返回false

boolean tryLock(long time, TimeUnit unit)

超时的获取锁,下列三种情况会返回:

1.当前线程在超时时间内获取到了锁;

2.当前线程在超时时间内获取锁时被中断;

3.超时时间结束,返回false

void unlock()

当前线程释放锁

Condition newCondition()

获取等待通知组件,该组件和当前锁绑定,当前线程只有获取了锁,才能调用该组件的wait()方法,而调用后,当前线程释放锁

二、队列同步器AbstractQueuedSynchronizer

1.概述

队列同步器AbstractQueuedSynchronizer(以下简称同步器),是用来构建锁或者其他同步组件的基础框架,它使用了一个int成员变量表示同步状态,通过内置的FIFO队列来完成资源获取线程的排队工作,并发包的作者(Doug Lea)期望它能够成为实现大部分同步需求的基础。

同步器的主要使用方式是继承,子类通过继承同步器并实现它的抽象方法来管理同步状态,在抽象方法的实现过程中免不了要对同步状态进行更改,这时就需要使用同步器提供的3个方法来进行操作,因为它们能够保证状态的改变是安全的。

子类推荐被定义为自定义同步组件的静态内部类,同步器自身没有实现任何同步接口,它仅仅是定义了若干同步状态获取和释放的方法来供自定义同步组件使用,同步器既可以支持独占式地获取同步状态,也可以支持共享式地获取同步状态,这样就可以方便实现不同类型的同步组件,如ReentrantLock、CountDownLatch等。

同步器是实现锁(也可以是任意同步组件)的关键,在锁的实现中聚合同步器,利用同步器实现锁的语义。可以这样理解二者之间的关系:锁是面向使用者的,它定义了使用者与锁交互的接口(比如可以允许两个线程并行访问),隐藏了实现细节;同步器面向的是锁的实现者,它简化了锁的实现方式,屏蔽了同步状态管理、线程的排队、等待与唤醒等底层操作。锁和同步器很好地隔离了使用者和实现者所需关注的领域。

2.接口与实例

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

重写同步器指定的方法时,需要使用同步器提供的如下3个方法来访问或修改同步状态:

  1. getState():获取当前同步状态;
  2. setState(int newState):设置当前同步状态;
  3. compareAndSetState(int expect, int update):使用CAS设置当前状态,该方法能够保证状态设置的原子性。

同步器可重写的方法与描述如下表:

方法名称

描述

boolean tryAcquire(int arg)

独占式获取同步状态,实现该方法需要查询当前状态并判断同步状态是否符合预期,然后再使用CAS设置同步状态

boolean tryRelease(int arg)

独占式释放同步状态,等待获取同步状态的线程将有机会获取同步状态

int tryAcquireShared(int arg)

共享式获取同步状态,返回大于等于0的值,表示获取成功,反之则失败

boolean tryReleaseShared(int arg)

共享式释放同步状态

boolean isHeldExclusively()

当前同步器是否在独占模式下被线程占用,一般该方法表示是否被当前线程所占

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

方法名称

描述

void acquire(int arg)

独占式获取同步状态,如果当前线程获取同步状态成功,则方法返回,否则将进入同步队列等待,该方法将会调用被重写的tryAcquire方法

void acquireInterruptibly(int arg)

与acquire方法相同,但是该方法响应中断,如果当前线程被中断,则抛出InterruptedException异常

boolean tryAcquireNanos(int arg, int nanos)

在acquiredInterruptibly方法基础上增加了超时限制,如果当前线程在超时时间内获取到了同步状态返回true,否则返回false

void acquireShared(int arg)

共享式的获取同步状态,如果当前线程未获取到同步状态,将会进入同步队列等待,与独占式唯一的区别是共享式在同一时刻可能有多个线程获取到同步状态

void acquireSharedInterruptibly

(int arg)

与acquireShared相同,只是该方法能响应中断

boolean tryAcquireSharedNanos

(int arg, int nanos)

在acquireSharedInterruptibly的基础上增加超时限制

boolean release(int arg)

独占式的释放同步状态,该方法会在释放同步状态后,将同步队列的第一个节点包含的线程唤醒

boolean releaseShared(int arg)

共享式的释放同步状态

Collection<Thread> getQueuedThreads()

获取等待在同步线程中的线程集合

同步器提供的模板方法基本上分为3类:

  1. 独占式获取与释放同步状态;
  2. 共享式获取与释放同步状态;
  3. 查询同步队列中的等待线程情况。

自定义同步组件将使用同步器提供的模板方法来实现自己的同步语义,只有掌握了同步器的工作原理才能更加深入地理解并发包中其他的并发组件。

接下来我们写一个简单的锁:

public class Mutex implements Lock {
    /**
     * 实现自定义同步器
     */
    private static class Sync extends AbstractQueuedSynchronizer {
        /**
         * 是否处于占用状态
         *
         * @return
         */
        @Override
        protected boolean isHeldExclusively() {
            return getState() == 1;
        }
        /**
         * 当状态为0时获取锁
         *
         * @param arg
         * @return
         */
        @Override
        public boolean tryAcquire(int arg) {
            if (compareAndSetState(0, 1)) {
                setExclusiveOwnerThread(Thread.currentThread());
                return true;
            }
            return false;
        }
        /**
         * 释放锁,将状态设置为0
         *
         * @param arg
         * @return
         */
        @Override
        protected boolean tryRelease(int arg) {
            if (getState() == 0) {
                throw new IllegalMonitorStateException();
            }
            setExclusiveOwnerThread(null);
            setState(0);
            return true;
        }
        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();
    }
}
@Test
public void testCustomLock() throws InterruptedException {
    long startTime = System.currentTimeMillis();
    boolean flag = false;
    for (int n = 0; n < 10; n++) {
        final int[] i = {0};
        Mutex lock = new Mutex();
        Runnable casThread = () -> {
            for (int j = 0; j < 50000000; j++) {
                if (flag) {
                    if (lock.tryLock()) {
                        i[0]++;
                        lock.unlock();
                    } else {
                        j --;
                    }
                } else{
                    lock.lock();
                    i[0]++;
                    lock.unlock();
                }
            }
        };
        Thread t1 = new Thread(casThread, "thread-1");
        Thread t2 = new Thread(casThread, "thread-2");
        t1.start();
        t2.start();
        t1.join();
        t2.join();
        System.out.println("num " + n + ": final value:" + i[0]);
    }
    System.out.println("custom time : " + 
            (System.currentTimeMillis() - startTime));
}

上述示例中,独占锁Mutex是一个自定义同步组件,它在同一时刻只允许一个线程占有锁。Mutex中定义了一个静态内部类,该内部类继承了同步器并实现了独占式获取和释放同步状态。在tryAcquire方法中,如果经过CAS设置成功(同步状态设置为1),则代表获取了同步状态,而在tryRelease方法中只是将同步状态重置为0。用户使用Mutex时并不会直接和内部同步器的实现打交道,而是调用Mutex提供的方法,在Mutex的实现中,以获取锁的lock()方法为例,只需要在方法实现中调用同步器的模板方法acquire即可,当前线程调用该方法获取同步状态失败后会被加入到同步队列中等待,这样就大大降低了实现一个可靠自定义同步组件的门槛。

当然,简单写的锁相比Java自带的synchronized同步块性能肯定差了很多,自己测试过,性能预估差了将近三倍。

3.同步器的实现分析

接下来将从实现角度分析同步器是如何完成线程同步的,主要包括:同步队列、独占式同步状态获取与释放、共享式同步状态获取与释放以及超时获取同步状态等同步器的核心数据结构与模板方法。

3.1 同步队列

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

同步队列中的节点(Node)用来保存获取同步状态失败的线程引用、等待状态以及前驱和后继节点,节点的属性类型与名称以及描述。具体相关信息如下表:

属性类型与名称

描述

int waitStatus

等待状态,包含如下状态:

1.CANCELLED值为1,由于在同步队列中等待的线程等待超时或被中断,需要从同步队列中取消等待,节点进入该状态将不再变化;

2.SIGNAL值为-1,后继节点的线程处于等待状态,而当前线程如果释放了同步状态或被取消,将会通知后续节点,使后续节点继续运行;

3.CONDITION值为-2,节点在等待队列中,节点线程等待在Condition上,当其他线程对Condition调用了signal()方法后,该节点将会从等待队列移到同步队列中,加入到对同步状态的获取中;

4.PROPAGATE值为-3表示下一次共享式同步状态将被无条件的传播下去;

5.INITIAL值为0,初始状态

Node pre

前驱节点,当节点加入同步队列时被设置(尾部添加)

Node next

后继节点

Node nextWaiter

等待队列中的后继节点。如果当前节点是共享的,那么这个字段将是一个Node类型的SHARED常量,也就是说节点类型和等待队列中的后继节点共用同一个字段

Thread thread

获取同步状态的线程

节点是构成同步队列的基础,同步器拥有首节点(head)和尾节点(tail),没有成功获取同步状态的线程将会成为节点加入该队列的尾部。基本结构图如下:

同步器包含了两个节点类型的引用,一个指向头节点,而另一个指向尾节点。试想一下,当一个线程成功地获取了同步状态(或者锁),其他线程将无法获取到同步状态,转而被构造成为节点并加入到同步队列中,而这个加入队列的过程必须要保证线程安全,因此同步器提供了一个基于CAS的设置尾节点的方法:compareAndSetTail,它需要传递当前线程“认为”的尾节点和当前节点,只有设置成功后,当前节点才正式与之前的尾节点建立关联。

同步队列遵循FIFO,首节点是获取同步状态成功的节点,首节点的线程在释放同步状态时,将会唤醒后继节点,而后继节点将会在获取同步状态成功时将自己设置为首节点。示意图如下:

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

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

获取:通过调用同步器的acquire方法可以获取同步状态,该方法对中断不敏感,也就是由于线程获取同步状态失败后进入同步队列中,后续对线程进行中断操作时,线程不会从同步队列中移出。

获取方法源码如下:

public final void acquire(int arg) {
    if (!tryAcquire(arg) 
            && acquireQueued(addWaiter(Node.EXCLUSIVE), arg)) {
        selfInterrupt();
    }
}

上述代码主要完成了同步状态获取、节点构造、加入同步队列以及在同步队列中自旋等待的相关工作。方法中各方法的大致作用流程如下:

  1. tryAcquire方法:保证线程安全的获取同步状态;
  2. addWaiter方法:如果同步状态获取失败,则构造同步节点(Node.EXCLUSIVE表示独占式),同一时刻只能有一个线程成功获取同步状态,随后将该节点加入到同步队列的尾部;
  3. acquireQueued方法:使得该节点以“死循环”的方式获取同步状态;
  4. selfInterrupt方法:获取失败则暂停线程。

释放:当前线程获取同步状态并执行了相应逻辑之后,就需要释放同步状态,使得后续节点能够继续获取同步状态。通过调用同步器的release方法可以释放同步状态,该方法在释放了同步状态之后,会唤醒其后继节点(进而使后继节点重新尝试获取同步状态)。

释放方法源码如下:

public final boolean release(int arg) {
    if (tryRelease(arg)) {
        Node h = head;
        if (h != null && h.waitStatus != 0)
            unparkSuccessor(h);
        return true;
    }
    return false;
}

该方法执行时,会唤醒头节点的后继节点线程,unparkSuccessor方法使用LockSupport来唤醒处于等待状态的线程。

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

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

共享式获取与独占式获取最主要的区别在于同一时刻能否有多个线程同时获取到同步状态。以文件的读写为例,如果一个程序在对文件进行读操作,那么这一时刻对于该文件的写操作均被阻塞,而读操作能够同时进行。写操作要求对资源的独占式访问,而读操作可以是共式访问,两种不同的访问模式在同一时刻对文件或资源的访问情况。示例如下图所示:

左半部分,共享式访问资源时,其他共享式的访问均被允许,而独占式访问被阻塞,右半部分是独占式访问资源时,同一时刻其他访问均被阻塞。

获取:通过调用同步器的acquireShared方法可以共享式地获取同步状态,方法源码如下:

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);
                if (r >= 0) {
                    setHeadAndPropagate(node, r);
                    p.next = null;
                    if (interrupted)
                        selfInterrupt();
                    failed = false;
                    return;
                }
            }
            if (shouldParkAfterFailedAcquire(p, node) &&
                    parkAndCheckInterrupt())
                interrupted = true;
        }
    } finally {
        if (failed)
            cancelAcquire(node);
    }
}

acquireShared方法中,同步器调用tryAcquireShared方法尝试获取同步状态,tryAcquireShared方法返回值为int类型,当返回值大于等于0时,表示能够获取到同步状态。因此,在共享式获取的自旋过程中,成功获取到同步状态并退出自旋的条件就是tryAcquireShared方法返回值大于等于0。可以看到,在doAcquireShared方法的自旋过程中,如果当前节点的前驱为头节点时,尝试获取同步状态,如果返回值大于等于0,表示该次获取同步状态成功并从自旋过程中退出。

释放:与独占式一样,共享式获取也需要释放同步状态,通过调用releaseShared方法可以释放同步状态,方法源码如下:

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

该方法在释放同步状态之后,将会唤醒后续处于等待状态的节点。对于能够支持多个线程同时访问的并发组件(比如Semaphore),它和独占式主要区别在于tryReleaseShared方法必须确保同步状态(或者资源数)线程安全释放,一般是通过循环和CAS来保证的,因为释放同步状态的操作会同时来自多个线程。

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

通过调用同步器的doAcquireNanos方法可以超时获取同步状态,即在指定的时间段内获取同步状态,如果获取到同步状态则返回true,否则返回false。该方法提供了传统Java同步操作(比如synchronized关键字)所不具备的特性。

超时获取同步状态过程可以被视作响应中断获取同步状态过程的“增强版”,doAcquireNanos方法在支持响应中断的基础上,增加了超时获取的特性。针对超时获取,主要需要计算出需要睡眠的时间间隔nanosTimeout,为防止过早通知,nanosTimeout计算公式为:nanosTimeout-=now-lastTime,其中now为当前唤醒时间,lastTime为上次唤醒时间,如果naosTimeout大于0则表示超时时间未到,需要继续睡眠nanosTimeout纳秒,反之表示已经超时。方法源码如下:

private boolean doAcquireNanos(int arg, long nanosTimeout)
        throws InterruptedException {
    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();
            if (nanosTimeout <= 0L)
                return false;
            if (shouldParkAfterFailedAcquire(p, node) &&
                nanosTimeout > spinForTimeoutThreshold)
                LockSupport.parkNanos(this, nanosTimeout);
            if (Thread.interrupted())
                throw new InterruptedException();
        }
    } finally {
        if (failed)
            cancelAcquire(node);
    }
}

该方法在自旋过程中,当节点的前驱节点为头节点时尝试获取同步状态,如果获取成功则从该方法返回,这个过程和独占式同步获取的过程类似,但是在同步状态获取失败的处理上有所不同。如果当前线程获取同步状态失败,则判断是否超时(nanosTimeout小于等于0表示已经超时),如果没有超时,重新计算超时间隔nanosTimeout,然后使当前线程等待nanosTimeout纳秒(当已到设置的超时时间,该线程会从LockSupport.parkNanos方法返回)。

如果nanosTimeout小于等于spinForTimeoutThreshold(1000纳秒)时,将不会使该线程进行超时等待,而是进入快速的自旋过程。原因在于,非常短的超时等待无法做到十分精确,如果这时再进行超时等待,相反会让nanosTimeout的超时从整体上表现得反而不精确。因此,在超时非常短的场景下,同步器会进入无条件的快速自旋。

三、重入锁ReentrantLock

1.概述

重入锁ReentrantLock,顾名思义就是支持重进入的锁,它表示该锁能够支持一个线程对资源的重复加锁。除此之外,该锁的还支持获取锁时的公平和非公平性选择。

回想我们在说明Lock接口时举得Mutex例子,如果一个线程通过调用它的lock方法先获得了锁,后面又继续调用了一次lock方法,它将会自己阻塞自己,因为那个例子没有考虑该线程拿到锁会再次去获取锁,ReentrantLock就是为了解决这种场景的。synchronized关键字隐式的支持重进入,比如一个synchronized修饰的递归方法,在方法执行时,执行线程在获取了锁之后仍能连续多次地获得该锁,而不像Mutex由于获取了锁,而在下一次获取锁时出现阻塞自己的情况。

ReentrantLock虽然没能像synchronized关键字一样支持隐式的重进入,但是在调用lock()方法时,已经获取到锁的线程,能够再次调用lock()方法获取锁而不被阻塞。

这里提到一个锁获取的公平性问题,如果在绝对时间上,先对锁进行获取的请求一定先被满足,那么这个锁是公平的,反之是不公平的。公平的获取锁,也就是等待时间最长的线程最优先获取锁,也可以说锁获取是顺序的。ReentrantLock提供了一个构造函数,能够控制锁是否是公平的。事实上,公平的锁机制往往没有非公平的效率高,但是,并不是任何场景都是以TPS(系统吞吐量)作为唯一的指标,公平锁能够减少“饥饿”发生的概率,等待越久的请求越是能够得到优先满足。

2.实现重进入获取和释放

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

  1. 线程再次获取锁:锁需要去识别获取锁的线程是否为当前占据锁的线程,如果是,则再次成功获取;
  2. 锁的最终释放:线程重复n 次获取了锁,随后在第n 次释放该锁后,其他线程能够获取到该锁。锁的最终释放要求锁对于获取进行计数自增,计数表示当前锁被重复获取的次数,而锁被释放时,计数自减,当计数等于0时表示锁已经成功释放。

ReentrantLock是通过组合自定义同步器来实现锁的获取与释放,以非公平性(默认的)实现为例,获取实现方法源码如下:

final boolean nonfairTryAcquire(int acquires) {
    final Thread current = Thread.currentThread();
    int c = getState();
    if (c == 0) {
        if (compareAndSetState(0, acquires)) {
            setExclusiveOwnerThread(current);
            return true;
        }
    }
    else if (current == getExclusiveOwnerThread()) {
        int nextc = c + acquires;
        if (nextc < 0) // overflow
            throw new Error("Maximum lock count exceeded");
        setState(nextc);
        return true;
    }
    return false;
}

获取:该方法增加了再次获取同步状态的处理逻辑:通过判断当前线程是否为获取锁的线程来决定获取操作是否成功,如果是获取锁的线程再次请求,则将同步状态值进行增加并返回true,表示获取同步状态成功。

成功获取锁的线程再次获取锁,只是增加了同步状态值,这也就要求ReentrantLock在释放同步状态时减少同步状态值。实现方法源码如下:

protected final boolean tryRelease(int releases) {
    int c = getState() - releases;
    if (Thread.currentThread() != getExclusiveOwnerThread())
        throw new IllegalMonitorStateException();
    boolean free = false;
    if (c == 0) {
        free = true;
        setExclusiveOwnerThread(null);
    }
    setState(c);
    return free;
}

释放:如果该锁被获取了n次,那么前(n-1)次tryRelease方法必须返回false,而只有同步状态完全释放了,才能返回true。可以看到,该方法将同步状态是否为0作为最终释放的条件,当同步状态为0时,将占有线程设置为null,并返回true,表示释放成功。

3.公平和非公平锁的区别

公平性与否是针对获取锁而言的,如果一个锁是公平的,那么锁的获取顺序就应该符合请求的绝对时间顺序,也就是FIFO。

对于非公平锁,只要CAS设置同步状态成功,则表示当前线程获取了锁,而公平锁则不同。公平锁实现方法源码如下:

protected final boolean tryAcquire(int acquires) {
    final Thread current = Thread.currentThread();
    int c = getState();
    if (c == 0) {
        if (!hasQueuedPredecessors() &&
            compareAndSetState(0, acquires)) {
            setExclusiveOwnerThread(current);
            return true;
        }
    }
    else if (current == getExclusiveOwnerThread()) {
        int nextc = c + acquires;
        if (nextc < 0)
            throw new Error("Maximum lock count exceeded");
        setState(nextc);
        return true;
    }
    return false;
}

该方法与nonfairTryAcquire比较,唯一不同的位置为判断条件多了hasQueuedPredecessors()方法,即加入了同步队列中当前节点是否有前驱节点的判断,如果该方法返回true,则表示有线程比当前线程更早地请求获取锁,因此需要等待前驱线程获取并释放锁之后才能继续获取锁。

回顾nonfairTryAcquire方法,当一个线程请求锁时,只要获取了同步状态即成功获取锁。在这个前提下,刚释放锁的线程再次获取同步状态的几率会非常大,使得其他线程只能在同步队列中等待。

现在测试一下公平锁和非公平锁,测试场景:10个线程,每个线程获取100000次锁。

性能差别:在测试中公平性锁与非公平性锁相比,总耗时是其94.3倍,总切换次数是其133倍。可以看出,公平性锁保证了锁的获取按照FIFO原则,而代价是进行大量的线程切换。非公平性锁虽然可能造成线程“饥饿”,但极少的线程切换,保证了其更大的吞吐量。

四、Condition接口

任意一个Java对象,都拥有一组监视器方法(定义在Object上),主要包括wait、wait、notify以及notifyAll方法,这些方法与synchronized同步关键字配合,可以实现等待/通知模式。Condition接口也提供了类似Object的监视器方法,与Lock配合可以实现等待/通知模式,但是这两者在使用方式以及功能特性上还是有差别的。

Object和Condition特性功能对比:

对比项

Object Monitor Method

Condition

前置条件

获取对象锁

调用lock方法获取锁

调用newCondition获取Condition

调用方式

直接调用

如:object.wait()

直接调用

如:condition.await()

等待队列个数

一个

多个

当前线程释放锁并进入等待状态

支持

支持

当前线程释放锁并进入等待状态,在等待状态中不响应中断

不支持

支持

当前线程释放锁并进入超时等待状态

支持

支持

当前线程释放锁并进入等待状态直到将来的某一时刻

不支持

支持

唤醒等待队列的一个线程

支持

支持

唤醒等待队列的全部线程

支持

支持

1.Condition接口

Condition定义的部分方法及描述:

方法名称

描述

void await()

当前线程进入等待状态直到被通知(SIGNAL)或中断,当前线程将进入运行状态且退出的情况有

1.其它线程调用该线程的signal()或signalAll()方法,而当前线程被唤醒;

2.其它线程调用interrupt()方法中断当前线程;

3.如果当前线程从await()方法返回,表明当前对象已获得Condition对象的锁

void awaitUninterruptibly()

当前线程进入等待状态直到被通知,并且线程不能被中断

long awaitNanos

(long nanosTimeout)

当前线程进入等待状态直到被通知、中断或超时。返回值表示剩余时间,如果返回值是0或负数,则代表已超时

boolean awaitUntil(Date deadline)

当前线程进入等待状态直到被通知、中断或到某个时间。如果没到某个时间就被通知返回true,否则返回false

void signal()

唤醒一个等待在Condition上的线程,该线程从等待方法返回前必须获得与Condition关联的锁

void signalAll()

唤醒所有等待在Condition上的线程,该线程从等待方法返回前必须获得与Condition关联的锁

获取一个Condition必须通过Lock的newCondition()方法。下面通过一个有界队列的示例来深入了解Condition的使用方式。有界队列是一种特殊的队列,当队列为空时,队列的获取操作将会阻塞获取线程,直到队列中有新增元素,当队列已满时,队列的插入操作将会阻塞插入线程,直到队列出现“空位”。代码示例如下:

public class BoundedQueue<T> {
    private Object[] items;
    // 添加的下标,删除的下标和数组当前数量
    private int addIndex, removeIndex, count;
    private Lock lock = new ReentrantLock();
    private Condition notEmpty = lock.newCondition();
    private Condition notFull = lock.newCondition();
    public BoundedQueue(int size) {
        items = new Object[size];
    }
    // 添加一个元素,如果数组满,则添加线程进入等待状态,直到有"空位"
    public void add(T t) throws InterruptedException {
        lock.lock();
        try {
            while (count == items.length)
                notFull.await();
            items[addIndex] = t;
            if (++addIndex == items.length)
                addIndex = 0;
            ++count;
            notEmpty.signal();
        } finally {
            lock.unlock();
        }
    }
    // 由头部删除一个元素,如果数组空,则删除线程进入等待状态,直到有新添加元素
    @SuppressWarnings("unchecked")
    public T remove() throws InterruptedException {
        lock.lock();
        try {
            while (count == 0)
                notEmpty.await();
            Object x = items[removeIndex];
            if (++removeIndex == items.length)
                removeIndex = 0;
            --count;
            notFull.signal();
            return (T) x;
        } finally {
            lock.unlock();
        }
    }
}

很简单的生产消费模型,其实现逻辑便不详细说明。在添加和删除方法中使用while循环而非if判断,目的是防止过早或意外的通知,只有条件符合才能够退出循环。回想之前提到的等待/通知的经典范式,二者是非常类似的。

2.Condition的实现分析

ConditionObject是同步器AbstractQueuedSynchronizer的内部类,其实现了Condition接口,因为Condition的操作需要获取相关联的锁,所以作为同步器的内部类也较为合理。每个Condition对象都包含着一个队列(以下称为等待队列),该队列是Condition对象实现等待/通知功能的关键。

下面将分析Condition的实现,主要包括:等待队列、等待和通知,下面提到的Condition如果不加说明均指的是ConditionObject 。

2.1等待队列

等待队列是一个FIFO的队列,在队列中的每个节点都包含了一个线程引用,该线程就是在Condition对象上等待的线程,如果一个线程调用了Condition.await()方法,那么该线程将会释放锁、构造成节点加入等待队列并进入等待状态。事实上,节点的定义复用了同步器中节点的定义,也就是说,同步队列和等待队列中节点类型都是同步器的静态内部类AbstractQueuedSynchronizer.Node。

一个Condition包含一个等待队列,Condition拥有首节点firstWaiter和尾节点lastWaiter。当前线程调用Condition.await()方法,将会以当前线程构造节点,并将节点从尾部加入等待队列,其基本结构如下图所示:

如图所示,Condition拥有首尾节点的引用,而新增节点只需要将原有的尾节点nextWaiter指它,并且更新尾节点即可。上述节点引用更新的过程并没有使用CAS保证,原因在于调用await()方法的线程必定是获取了锁的线程,也就是说该过程是由锁来保证线程安全的。

其具体的实现和上面的有界队列差不多,便不做过多的叙述。在监视器模型上,一个对象拥有一个同步队列和等待队列,而并发包中的Lock(更确切地说是同步器)拥有一个同步队列和多个等待队列。模型结构图如下:

五、并发工具类

在JDK的并发包(java.util.concurrent)中为我们提供了一些非常重要的并发工具类,接下来简单列举一下四个实用工具类。

1.CountDownLatch

其关键部分源码如下:

public class CountDownLatch {
    private static final class Sync extends AbstractQueuedSynchronizer {
        private static final long serialVersionUID = 4982264981922014374L;
        Sync(int count) {
            setState(count);
        }
        int getCount() {
            return getState();
        }
        protected int tryAcquireShared(int acquires) {
            return (getState() == 0) ? 1 : -1;
        }
        protected boolean tryReleaseShared(int releases) {
            for (;;) {
                int c = getState();
                if (c == 0)
                    return false;
                int nextc = c-1;
                if (compareAndSetState(c, nextc))
                    return nextc == 0;
            }
        }
    }
    private final Sync sync;
    public CountDownLatch(int count) {
        if (count < 0) throw new IllegalArgumentException("count < 0");
        this.sync = new Sync(count);
    }
}

CountDownLatch,类似于计数器的方式,用于等待一个或多个线程执行完操作开始自身代码的执行。

其构造函数接收一个int类型的整数作为计数器而使用,例如如果想等待N个线程执行完毕就传入N,当每调用一次countDown函数,表示某一个线程执行完毕。实际上,这个N并不是与线程绑定,也就是说并不是一定和线程的数量一致,只需要countDown函数执行N次,当前等待的线程就会开始执行。

注意点:

  1. 如果传入的参数大于2,那么主线程将会一直等待;
  2. 计数器必须大于0,如果为0,调用await方法不会阻塞当前线程。

应用场景:当遇到一个比较耗时的计算量较大的任务时,我们则可以考虑使用多线程来操作,将一个大任务拆分成多个小任务(一个任务相当于一个线程),当每个小任务执行完毕返回结果后,再由某一主线程对结果进行统计。

2.CyclicBarrier

其关键部分源码如下: 

public class CyclicBarrier {
    private final ReentrantLock lock = new ReentrantLock();
    private final int parties;
    private int count;
    private final Runnable barrierCommand;
    public CyclicBarrier(int parties, Runnable barrierAction) {
        if (parties <= 0) throw new IllegalArgumentException();
        this.parties = parties;
        this.count = parties;
        this.barrierCommand = barrierAction;
    }
    public CyclicBarrier(int parties) {
        this(parties, null);
    }
}

CyclicBarrier即同步屏障,它主要功能是让一组线程达到一个屏障(也可以称为同步点)是被阻塞,直到最后一个线程达到屏障是,屏障才被打开,所有被拦截的线程才会继续执行。

其构造函数默认也是接收一个int类型的参数N作为屏障拦截的线程数量,每个线程调用await方法表示到达了屏障点,然后被阻塞。

注意点:

  1. 构造函数中的N必须为线程的总数,当最后一个线程调用await方法(到达屏障)时,屏障才会打开,被阻塞的线程才会执行,这里的N表示的含义和CountDownLatch传入的N是不一样的;
  2. 当所有线程都到达屏障时,当屏障打开,接下来会优先执行哪个线程呢?答案是不确定的。但是CyclicBarrier为我们提供了一个更高级的用法,即构造函数还支持传递一个Runnable对象,当屏障打开时,优先执行Runnable中的run方法。(这一功能十分强大,完全可以替代CountDownLatch了)。

应用场景:同CountDownLatch。与CountDownLatch区别则是CountDownLatch计数器只能使用一次,而CyclicBarrier的计数器可以使用 reset方法重置,所以适合更复杂的业务场景。

3.Semaphore

其关键源码:

public class Semaphore implements java.io.Serializable {
    private final Sync sync;
    abstract static class Sync extends AbstractQueuedSynchronizer {
        private static final long serialVersionUID = 1192457210091910933L;
        Sync(int permits) {
            setState(permits);
        }
        final int getPermits() {
            return getState();
        }
        final int nonfairTryAcquireShared(int acquires) {
            for (;;) {
                int available = getState();
                int remaining = available - acquires;
                if (remaining < 0 ||
                    compareAndSetState(available, remaining))
                    return remaining;
            }
        }
        protected final boolean tryReleaseShared(int releases) {
            for (;;) {
                int current = getState();
                int next = current + releases;
                if (next < current) // overflow
                    throw new Error("Maximum permit count exceeded");
                if (compareAndSetState(current, next))
                    return true;
            }
        }
        final void reducePermits(int reductions) {
            for (;;) {
                int current = getState();
                int next = current - reductions;
                if (next > current) // underflow
                    throw new Error("Permit count underflow");
                if (compareAndSetState(current, next))
                    return;
            }
        }
        final int drainPermits() {
            for (;;) {
                int current = getState();
                if (current == 0 || compareAndSetState(current, 0))
                    return current;
            }
        }
    }
    static final class NonfairSync extends Sync {
        private static final long serialVersionUID = -2694183684443567898L;
        NonfairSync(int permits) {
            super(permits);
        }
        protected int tryAcquireShared(int acquires) {
            return nonfairTryAcquireShared(acquires);
        }
    }
    static final class FairSync extends Sync {
        private static final long serialVersionUID = 2014338818796000944L;
        FairSync(int permits) {
            super(permits);
        }
        protected int tryAcquireShared(int acquires) {
            for (;;) {
                if (hasQueuedPredecessors())
                    return -1;
                int available = getState();
                int remaining = available - acquires;
                if (remaining < 0 ||
                    compareAndSetState(available, remaining))
                    return remaining;
            }
        }
    }
    public Semaphore(int permits) {
        sync = new NonfairSync(permits);
    }
    public Semaphore(int permits, boolean fair) {
        sync = fair ? new FairSync(permits) : new NonfairSync(permits);
    }
}

Semaphore即信号量,主要用来控制并发访问特定资源的线程数量,协调各个线程合理使用公共资源。

构造函数同样也是接收一个int类型的参数N作为入参,用来限制访问某一公共资源最大的线程并发数,通过acquire来获取许可证,release释放许可证。

注意点:线程的并发数永远不会大于N(N为入参)。

应用场景:我们有大量的线程在完成一个巨量任务的时候,但是某一公共资源却有限定了线程的链接树,这时候就需要对这些大量线程访问这一公共资源做控制。例如当我们有上百个线程需要处理本地上G的数据文件,每个线程处理完成之后需要把结果写到数据库,而数据库只支持十个线程的并发链接,此时,对数据库的链接我们就可以通过Semaphore来控制最大连接数。

4.Exchanger

其关键源码如下:

public class Exchanger<V> {
    public Exchanger() {
        participant = new Participant();
    }
    static final class Participant extends ThreadLocal<Node> {
        public Node initialValue() { return new Node(); }
    }
}

Exchanger(交换者),它是用于线程间的协作工具类,主要用于线程间数据的交换。它提供了一个同步点,在这个同步点,两个线程可以交换彼此的数据。

注意点:

  1. Exchanger只能作用于两个线程之间,如果作用于第三个线程,第三个线程一直处于等待中;
  2. Exchange中还有一个重载函数,接收一个等待时长,用于避免一直等待。

 

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值