java多线程(4)

120 阅读8分钟

在1.8中java并发包引入了很多的重要的工具类,这里就一些工具类和他们的原理进行一些讨论。

AQS

AQS实现ReentrantLock简介

AbstractQueuedSYchronizer是并发大师doug lea设计的抽象队列同步器 ,用来加强原有的锁的能力, ReentrantLock可重入锁就是用aqs原理实现的一个锁,我们在这里用这个锁来讲解AQS

abstract static class Sync extends AbstractQueuedSynchronizer 

Sync是ReentrantLock用来控制线程安全的方法,这方法是AQS的子类,AQ是通过内置的FIFO双向队列来完成线程的排队过程的。AQS内部通过节点head和tail来记录队首和队尾的元素

/**
 * 等待队列的头部节点懒加载,例如在初始化时候,只会被setHead方法修改
 *  提示:如果头部已经存在,这个节点的等待状态waitStatus不能为CANCELLED
 */
private transient volatile Node head;

/**
 *  等待队列的尾部节点,懒加载, 只有在加入新的阻塞节点时候修改
 */
private transient volatile Node tail;

其中Node中的thread用来存放进入AQS队列中的线程引用,Node结点内部的SHARED表示标记线程是因为获取共享资源失败被阻塞添加到队列中的;Node中的EXCLUSIVE表示线程因为获取独占资源失败被阻塞添加到队列中的。waitStatus表示当前线程的等待状态

static final class Node {
    /** Marker to indicate a node is waiting in shared mode */
    static final Node SHARED = new Node();
    /** Marker to indicate a node is waiting in exclusive mode */
    static final Node EXCLUSIVE = null;

    /** waitStatus value to indicate thread has cancelled */
    static final int CANCELLED =  1;
    /** waitStatus value to indicate successor's thread needs unparking */
    static final int SIGNAL    = -1;
    /** waitStatus value to indicate thread is waiting on condition */
    static final int CONDITION = -2;
    /**
     * waitStatus value to indicate the next acquireShared should
     * unconditionally propagate
     */
    static final int PROPAGATE = -3;
  • CANCELLED=1:表示线程因为中断或者等待超时,需要从等待队列中取消等待;

  • SIGNAL=-1:当前线程thread1占有锁,队列中的head(仅仅代表头结点,里面没有存放线程引用)的后继结点node1处于等待状态,如果已占有锁的线程thread1释放锁或被CANCEL之后就会通知这个结点node1去获取锁执行。

  • CONDITION=-2:表示结点在等待队列中(这里指的是等待在某个lock的condition上,关于Condition的原理下面会写到),当持有锁的线程调用了Condition的signal()方法之后,结点会从该condition的等待队列转移到该lock的同步队列上,去竞争lock。(注意:这里的同步队列就是我们说的AQS维护的FIFO队列,等待队列则是每个condition关联的队列)

  • PROPAGTE=-3:表示下一次共享状态获取将会传递给后继结点获取这个共享同步状态。

AQS中维持了一个单一的volatile修饰的状态信息state(AQS通过Unsafe的相关方法,以原子性的方式由线程去获取这个state)。AQS提供了getState()、setState()、compareAndSetState()函数修改值(实际上调用的是unsafe的compareAndSwapInt方法)。下面是AQS中的部分成员变量以及更新state的方法


/**
 * The synchronization state.
 */
private volatile int state;

/**
 * Returns the current value of synchronization state.
 * This operation has memory semantics of a {@code volatile} read.
 * @return current state value
 */
protected final int getState() {
    return state;
}

/**
 * Sets the value of synchronization state.
 * This operation has memory semantics of a {@code volatile} write.
 * @param newState the new state value
 */
protected final void setState(int newState) {
    state = newState;
}

/**
 * Atomically sets synchronization state to the given updated
 * value if the current state value equals the expected value.
 * This operation has memory semantics of a {@code volatile} read
 * and write.
 *
 * @param expect the expected value
 * @param update the new value
 * @return {@code true} if successful. False return indicates that the actual
 *         value was not equal to the expected value.
 */
protected final boolean compareAndSetState(int expect, int update) {
    // See below for intrinsics setup to support this
    return unsafe.compareAndSwapInt(this, stateOffset, expect, update);
}

简单的写一个可重入锁的例子:

private static void doTask1(){
    try {
        reentrantLock.lock();
        System.out.println("doTask1");
        Thread.sleep(3 * 1000);
        doTask2();
    }catch (Exception e){
        e.printStackTrace();
    }finally {
        reentrantLock.unlock();
    }
}

private static void doTask2(){
    try {
        reentrantLock.lock();
        System.out.println("doTask2");
        Thread.sleep(10 * 1000);
    }catch (Exception e){
        e.printStackTrace();
    }finally {
        reentrantLock.unlock();
    }
}

因为doTask1()方法和doTask2()方法都是同一个线程同一把锁,所以运行结果如下

屏幕快照 2022-02-03 下午5.22.09.png

那么具体的可重入锁的实现呢,就是跟 state 和 exclusiveOwnerThread 有关 控制ReentrantLock的状态是AQS内int型的state,表示加锁的状态,初始状态值为0;另外 AQS 还维护了一个很重要的变量exclusiveOwnerThread,它表示的是获得锁的线程,也叫独占线程。还有用来存储获取锁失败线程的队列,以及head 和 tail 结点,如下图所示

image.png

首先来看非公平锁的实现

final void lock() {
    if (compareAndSetState(0, 1))
        setExclusiveOwnerThread(Thread.currentThread());
    else
        acquire(1);
}

在lock的实现中,使用了CAS操作,设置了标识的state为0,如果为0就可以设置成功返回true,然后就设置exclusiveOwnerThread 为当前线程。 而当线程执行到doTask2的时候, 执行 lock 发现 state 已经不是0而是1了,然后检查ExclusiveOwnerThread是不是和获取锁的线程是同一个,结果发现是同一个,所以 state+1 = 2,这就是可重入的核心原理。

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;
}

源码可以得知两种情况都可以加锁成功-一种state为 0, 一种state不为0,但是为同一线程,就把state加一并且使用锁。 如果这两个条件都不满足,线程2就会进入等待队列,等待线程1释放锁再重新加锁

public final void acquire(int arg) {
//判断是否加锁成功
    if (!tryAcquire(arg) &&
    //等待队列
        acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
        selfInterrupt();
}
/**
 * 创建等待节点的方法
 */
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;
}

这个方法会使用cas创建尾节点的方法把创建失败的方法放置在等待队列的尾部, 再看看acquireQueuedshouldParkAfterFailedAcquire的方法

final boolean acquireQueued(final Node node, int arg) {
        try {
            boolean interrupted = false;
            for (;;) {
            //判断之前的结点是不是头结点 head,如果是头结点就尝试去获取锁,
                final Node p = node.predecessor();
                if (p == head && tryAcquire(arg)) {
                //获取锁成功的话,就把当前线程设置为head
                    setHead(node);
                    //断开之前头结点
                    p.next = null; // help GC
                    return interrupted;
                }
                //如果之前的不是头结点,那么就要等待了,等候之前的线程释放锁后,调用 LockSupport来唤醒,
                if (shouldParkAfterFailedAcquire(p, node) &&
                    parkAndCheckInterrupt()) // 这里很重要,待该线程被唤醒时继续走for循环,设置自己为head结点
                    interrupted = true;
            }
        } catch (Throwable t) {
            cancelAcquire(node);
            throw t;
        }
    }
 private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) {
	    // 注意Node的waitStatus字段我们在上面创建Node的时候并没有指定 ,默认值是0    
	    // waitStatus 的4种状态  
	    //static final int CANCELLED =  1;    
	    //static final int SIGNAL    = -1;  //表示有后继结点等待被唤醒  
	    //static final int CONDITION = -2;   //条件锁使用
	    //static final int PROPAGATE = -3; //共享锁时使用
        int ws = pred.waitStatus;
        if (ws == Node.SIGNAL)           
            return true;
        // 如果 ws > 0,则表示是取消状态,然后通过while循环 把所有是取消状态的线程从等待队列中删除
        if (ws > 0) {           
            do {
                node.prev = pred = pred.prev;
            } while (pred.waitStatus > 0);
            pred.next = node;
        } else {//如果不是取消状态,则通过cas操作将该线程的waitStatus设置为等待唤醒状态           
            pred.compareAndSetWaitStatus(ws, Node.SIGNAL);
        }
        return false;
    }

上面的shouldParkAfterFailedAcquire方法只是将waitStatus设置为SIGNAL,但是并没有阻塞操作,真正的阻塞操作在下面的方法parkAndCheckInterrupt,如下:

/**
 * 阻塞当前的线程
 *
 * @return {@code true} if interrupted
 */
private final boolean parkAndCheckInterrupt() {
    LockSupport.park(this);
    //返回线程是否被中断的状态
    return Thread.interrupted();
}

锁的释放,就是直接调用AQS的释放方法release

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

//AQS的释放方法
public final boolean release(int arg) {
    if (tryRelease(arg)) {
        Node h = head;
        if (h != null && h.waitStatus != 0)
            unparkSuccessor(h);
        return true;
    }
    return false;
}

这个方法会尝试一直把AQS的state --直到所有使用该锁的线程全部释放以后,即计数器清零以后才释放锁,并且会把等待队列中的头节点提到前面去执行。

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;
}

总结:

  • 线程1调用lock方法时候会先查看AQS状态是否为0,如果为0则通过CAS操作把State设为0,并且创建锁

  • 如果这时候线程1同时调用另外一个lock方法,那么线程1会发现 state = 1,它再去看独占线程是不是就是自己的线程,如果是的话 state + 1 ,获取锁成功。

  • 如果线程1的锁还没有释放,此时线程2调用lock方法,那么state不会等于0,且独占线程是线程1而不是自己(线程2),所以AQS会把线程2放到等待队列的尾部,如果线程2的前置结点是头结点head,那么线程2会通过死循环一直去获取锁,如果还是获取不到锁,那么会阻塞住线程2。如果不是头结点那么就会阻塞线程2,等待线程1释放锁且唤醒它。

  • 可重入锁的释放时候会等待全部线程执行释放之后才能释放。

semaphore 信号量

信号量模型 : 一个计数器,一个等待队列,三个方法: init () down(), up();

image.png java的semaphore的实现方法和前文的ReentrantLock大致上原理相同,这里只 举一个方法的例子nonfairTryAcquireShared使用信号量

protected int tryAcquireShared(int arg) {
    throw new UnsupportedOperationException();
}

final int nonfairTryAcquireShared(int acquires) {
    for (;;) {
    //无限循环等待信号量的数量available -acquires相减 如果有可以创建的数量调用Aqs的compareAndSetState创建信号量方法
        int available = getState();
        int remaining = available - acquires;
        if (remaining < 0 ||
            compareAndSetState(available, remaining))
            return remaining;
    }
}

信号量最大的需求就是在于多个线程访问同一临界区,对于各种池化资源的支持,semaphore可以简单的实现一个限流器

CountDownLatch 与cyclicBrirrer

在并发编程的过程中常常有这么一种场景,我们同时有多个任务一起并发执行,在多个任务都执行完毕以后再进行下一步的任务CountDownLatchcyclicBrirrer都可以帮助完成这样的任务。 CountDownLatch主要用在一个线程等待多个线程执行完毕的情况,而CyclicBarrier用在多个线程互相等待执行完毕的情况。

使用CountDownLatch实现线程的等待

CountDownLatch的代码实现非常的简单 ,使用AQS的状态机保存了一个数字作为计数器,首先在初始化CountDownLatch的时候传入了一个值count, 设置AQS的state为这个count值

Sync(int count) {
    setState(count);
}

int getCount() {
    return getState();
}

protected int tryAcquireShared(int acquires) {
    return (getState() == 0) ? 1 : -1;
}

每次调用countDown()方法都调用了AQS的releaseShared,在里面cas的方式给计数器减一

public void countDown() {
    sync.releaseShared(1);
}

public long getCount() {
    return sync.getCount();
}

protected boolean tryReleaseShared(int releases) {
    // Decrement count; signal when transition to zero
    for (;;) {
        int c = getState();
        if (c == 0)
            return false;
        int nextc = c-1;
        if (compareAndSetState(c, nextc))
            return nextc == 0;
    }
}

另外就是await() 方法,调用了 AQS的Interrupte方法打断

public void await() throws InterruptedException {
    sync.acquireSharedInterruptibly(1);
}

使用cyclicBrirrer实现线程的同时执行

简单看一下cyclicBrirrer 用以实现计数器以及回调函数的方法是使用了一个ReentrantLock可重入锁,一个Generation的实例,以及一个 Condition,

private static class Generation {
    boolean broken = false;
}

/** The lock for guarding barrier entry */
private final ReentrantLock lock = new ReentrantLock();
/** Condition to wait on until tripped */
private final Condition trip = lock.newCondition();
/** The number of parties */
private final int parties;
/* The command to run when tripped */
private final Runnable barrierCommand;
/** The current generation */
private Generation generation = new Generation();

//计数器的数字
private int count;

初始化的CyclicBarrier:parties为计数器入参数barrierAction 作为传入的Runnable参数,可以作为异步调用的方法。

public CyclicBarrier(int parties, Runnable barrierAction) {
    if (parties <= 0) throw new IllegalArgumentException();
    //将parties和count都置为计数器的值
    this.parties = parties;
    this.count = parties;
    this.barrierCommand = barrierAction;
}

主要的调用过程dowait方法:

private int dowait(boolean timed, long nanos)
    throws InterruptedException, BrokenBarrierException,
           TimeoutException {
          // 使用ReentrantLock 锁住
    final ReentrantLock lock = this.lock;
    lock.lock();
    try {
    //初始化判断器
        final Generation g = generation;

        if (g.broken)
            throw new BrokenBarrierException();

        if (Thread.interrupted()) {
        //如果线程被中断调用breakBarrier方法结束整个计数器
            breakBarrier();
            throw new InterruptedException();
        }
         //内部计数器减一
         
        int index = --count;
        if (index == 0) {  // tripped
            boolean ranAction = false;
            try {
                final Runnable command = barrierCommand;
                if (command != null)
                    command.run();
                ranAction = true;
                nextGeneration();
                return 0;
            } finally {
                if (!ranAction)
                    breakBarrier();
            }
        }
        //等待队列 如果被中断了也实现通知breakBarrier
        // loop until tripped, broken, interrupted, or timed out
        for (;;) {
            try {
                if (!timed)
                    trip.await();
                else if (nanos > 0L)
                    nanos = trip.awaitNanos(nanos);
            } catch (InterruptedException ie) {
                if (g == generation && ! g.broken) {
                    breakBarrier();
                    throw ie;
                } else {
                    // We're about to finish waiting even if we had not
                    // been interrupted, so this interrupt is deemed to
                    // "belong" to subsequent execution.
                    Thread.currentThread().interrupt();
                }
            }

            if (g.broken)
                throw new BrokenBarrierException();

            if (g != generation)
                return index;

            if (timed && nanos <= 0L) {
                breakBarrier();
                throw new TimeoutException();
            }
        }
    } finally {
        lock.unlock();
    }
}

总结:两个工具类都是使用AQS技术实现的计数器,双方都有其优劣。CyclicBarrier在功能上更加强大,可以自动重置自己的计数器,在归0以后可以重新使用, 可以回调其他线程。但是从代码上就可以看出CountDownLatch在性能上有着极大的优势,毕竟CyclicBarrier使用了锁和condition等等去完成他的方法,而CountDownLatch仅仅使用了AQS的计数器。 所以还是要基于不同的业务场景去选取使用的多线程工具类。

phaser

Phaser是java 7 引入的新的并发API。他引入了新的Phaser的概念,我们可以将其看成一个一个的阶段,每个阶段都有需要执行的线程任务,任务执行完毕就进入下一个阶段。所以Phaser特别适合使用在重复执行或者重用的情况。

在CyclicBarrier、CountDownLatch中,我们使用计数器来控制程序的顺序执行,同样的在Phaser中也是通过计数器来控制。在Phaser中计数器叫做parties, 我们可以通过Phaser的构造函数或者register()方法来注册。

通过调用register()方法,我们可以动态的控制phaser的个数。如果我们需要取消注册,则可以调用arriveAndDeregister()方法。