Analysis of the Principle of Java Thread Synchronizer

Analysis of CountDownLatch principle

In daily development, we often encounter a scenario where a thread needs to wait for some threads to end before it can continue to run downward. Before CountDownLatch appeared, the join method was usually used to implement it, but the join method was not flexible enough, so CountDownLatch was developed.

Example

public static void main(String[] args) throws InterruptedException {
    CountDownLatch countDownLatch = new CountDownLatch(2);

    ExecutorService executorService = Executors.newFixedThreadPool(2);
    // 添加任务
    executorService.execute(new Runnable() {
        @Override
        public void run() {
            try {
                // 模拟运行时间
                Thread.sleep(1000);
                System.out.println("thread one over...");
            } catch (InterruptedException e) {
                e.printStackTrace();
            }finally {
                // 递减计数器
                countDownLatch.countDown();
            }
        }
    });

    // 同上
    executorService.execute(new Runnable() {
        @Override
        public void run() {
            try {
                Thread.sleep(1000);
                System.out.println("thread two over...");
            } catch (InterruptedException e) {
                e.printStackTrace();
            }finally {
                countDownLatch.countDown();
            }
        }
    });

    System.out.println("wait all child thread over!");
    // 阻塞直到被interrupt或计数器递减至0
    countDownLatch.await();
    System.out.println("all child thread over!");
    executorService.shutdown();
}

The output is:

wait all child thread over!
thread one over...
thread two over...
all child thread over!

The advantages of CountDownLatch over the join method are roughly in two points:

  • After calling the join method of a child thread, the thread will block until the child thread finishes running, and CountDownLatch allows the child thread to finish running or decrement the counter during the running process, which means that the await method does not have to wait until the child thread finishes before returning .

  • Using thread pool to manage threads is generally to add Runnable directly to the thread pool. At this time, there is no way to call the join method of the thread, but the counter can still be decremented in the child thread, which means that CountDownLatch can be more flexible than the join method. Control the synchronization of threads.

Class diagram structure

As can be seen from the figure, CountDownLatch is implemented based on AQS.

As can be seen from the following code, the counter value of CountDownLatch is the state value of AQS .

public CountDownLatch(int count) {
    if (count < 0) throw new IllegalArgumentException("count < 0");
    this.sync = new Sync(count);
}
Sync(int count) {
    setState(count);
}

Source code analysis

void await()

When the thread calls the await method of CountDownLatch, the current thread will be blocked until the counter value of CountDownLatch decrements to 0 or other threads call the interrupt method of the current thread.

public void await() throws InterruptedException {
    // 允许中断(中断时抛出异常)
    sync.acquireSharedInterruptibly(1);
}

// AQS的方法
public final void acquireSharedInterruptibly(int arg)
        throws InterruptedException {
    if (Thread.interrupted())
        throw new InterruptedException();
    // state=0时tryAcquireShared方法返回1,直接返回
    // 否则执行doAcquireSharedInterruptibly方法
    if (tryAcquireShared(arg) < 0)
        // state不为0,调用该方法使await方法阻塞
        doAcquireSharedInterruptibly(arg);
}

// Sync的方法(重写了AQS中的该方法)
protected int tryAcquireShared(int acquires) {
    return (getState() == 0) ? 1 : -1;
}

// AQS的方法
private void doAcquireSharedInterruptibly(int arg)
    throws InterruptedException {
    final Node node = addWaiter(Node.SHARED);
    boolean failed = true;
    try {
        for (;;) {
            final Node p = node.predecessor();
            if (p == head) {
                // 获取state值,state=0时r=1,直接返回,不再阻塞
                int r = tryAcquireShared(arg);
                if (r >= 0) {
                    setHeadAndPropagate(node, r);
                    p.next = null; // help GC
                    failed = false;
                    return;
                }
            }
            // 若state不为0则阻塞调用await方法的线程
            // 等到其他线程执行countDown方法使计数器递减至0
            // (state变为0)或该线程被interrupt时
            // 该线程才能继续向下运行
            if (shouldParkAfterFailedAcquire(p, node) &&
                parkAndCheckInterrupt())
                throw new InterruptedException();
        }
    } finally {
        if (failed)
            cancelAcquire(node);
    }
}

boolean await(long timeout, TimeUnit unit)

Compared with the await method above, the calling thread is blocked for up to timeout time (unit specified by unit) after calling this method. Even if the counter is not decremented to 0 or the calling thread is not interrupted, the calling thread will continue to run down.

public boolean await(long timeout, TimeUnit unit)
    throws InterruptedException {
    return sync.tryAcquireSharedNanos(1, unit.toNanos(timeout));
}

void countDown()

Decrement the counter. When the value of the counter is 0 (that is, state=0), all threads blocked by calling the await method will be awakened.

public void countDown() {
    // 将计数器减1
    sync.releaseShared(1);
}

// AQS的方法
public final boolean releaseShared(int arg) {
    // 当state被递减至0时tryReleaseShared返回true
    // 会执行doReleaseShared方法唤醒因调用await方法阻塞的线程
    // 否则如果state不是0的话什么也不做
    if (tryReleaseShared(arg)) {
        doReleaseShared();
        return true;
    }
    return false;
}

// Sync重写的AQS中的方法
protected boolean tryReleaseShared(int releases) {
    for (;;) {
        int c = getState();
        // 如果state已经为0,没有递减必要,直接返回
        // 否则会使state变成负数
        if (c == 0)
            return false;
        int nextc = c-1;
        // 通过CAS递减state的值
        if (compareAndSetState(c, nextc))
            // 如果state被递减至0,返回true以进行后续唤醒工作
            return nextc == 0;
    }
}

 

 

The principle of CyclicBarrier

The counter of CountDownLatch is one-off, which means that when the counter reaches 0, calling the await and countDown methods will return directly. CyclicBarrier solves this problem. CyclicBarrier means a loopback barrier. It can make a group of threads all reach a state and then execute them all at the same time, and then reset their own state and use it for the next state synchronization.

Example

Suppose a task is composed of phase 1, phase 2, and phase 3. Each thread must execute phase 1, phase 2, and phase 3 serially. When multiple threads execute the task, it is necessary to ensure that all threads execute phase 1 Phase 2 can only be entered after completion, and Phase 3 can only be entered when all threads of Phase 2 have been executed. The following code can be used to achieve:

public static void main(String[] args) {
    // 等待两个线程同步
    CyclicBarrier cyclicBarrier = new CyclicBarrier(2);

    ExecutorService executorService = Executors.newFixedThreadPool(2);
    // 运行两个子线程,当两个子线程的step1都执行完毕后才会执行step2
    // 当两个子线程的step2都执行完毕后才会执行step3
    for(int i = 0; i < 2; i++) {
        executorService.execute(new Runnable() {
            @Override
            public void run() {
                try{
                    System.out.println(Thread.currentThread() + " step1");
                    cyclicBarrier.await();
                    System.out.println(Thread.currentThread() + " step2");
                    cyclicBarrier.await();
                    System.out.println(Thread.currentThread() + " step3");
                }catch (Exception e){
                    e.printStackTrace();
                }
            }
        });
    }
    executorService.shutdown();
}

The output is as follows:

Thread[pool-1-thread-1,5,main] step1
Thread[pool-1-thread-2,5,main] step1
Thread[pool-1-thread-1,5,main] step2
Thread[pool-1-thread-2,5,main] step2
Thread[pool-1-thread-2,5,main] step3
Thread[pool-1-thread-1,5,main] step3

Class diagram structure

CyclicBarrier is based on ReentrantLock and is essentially based on AQS. Parties are used to record the number of threads, indicating how many threads call the await method before all threads will break through the barrier and run down. At the beginning, count is equal to parties. When the await method is called by the thread, it will be decremented by 1. When the count becomes 0, the barrier point is reached. All threads calling await will execute down together. At this time, the CyclicBarrier must be reset, and count=parties again. .

The lock is used to ensure the atomicity of the update counter count. The condition variable trip of lock is used to support communication between threads using await and signalAll.

The following is the constructor of CyclicBarrier:

public CyclicBarrier(int parties) {
    this(parties, null);
}

// barrierAction为达到屏障点(parties个线程调用了await方法)时执行的任务
public CyclicBarrier(int parties, Runnable barrierAction) {
    if (parties <= 0) throw new IllegalArgumentException();
    this.parties = parties;
    this.count = parties;
    this.barrierCommand = barrierAction;
}

The definition of Generation is as follows:

private static class Generation {
    // 记录当前屏障是否可以被打破
    boolean broken = false;
}

Source code analysis

int await()

The current thread will block when calling this method and will not return until one of the following conditions is met:

  • The parties have called the await method, which means they have reached the barrier point

  • Other threads call the interrupt method of the current thread

  • The broken flag of the Generation object is set to true, and BrokenBarrierExecption is thrown

  • public int await() throws InterruptedException, BrokenBarrierException {
        try {
            // false表示不设置超时时间,此时后面参数无意义
            // dowait稍后具体分析
            return dowait(false, 0L);
        } catch (TimeoutException toe) {
            throw new Error(toe); // cannot happen
        }
    }

    boolean await(long timeout, TimeUnit unit) Compared to await(), the wait timeout will return false.  

public int await(long timeout, TimeUnit unit)
    throws InterruptedException,
            BrokenBarrierException,
            TimeoutException {
           // 设置了超时时间
           // dowait稍后分析     
    return dowait(true, unit.toNanos(timeout));
}

int dowait(boolean timed, long nanos)

private int dowait(boolean timed, long nanos)
    throws InterruptedException, BrokenBarrierException,
            TimeoutException {
    final ReentrantLock lock = this.lock;
    lock.lock();
    try {
        final Generation g = generation;
        // 屏障已被打破则抛出异常
        if (g.broken)
            throw new BrokenBarrierException();

        // 线程中断则抛出异常
        if (Thread.interrupted()) {
            // 打破屏障
            // 会做三件事
            // 1\. 设置generation的broken为true
            // 2\. 重置count为parites
            // 3\. 调用signalAll激活所有等待线程
            breakBarrier();
            throw new InterruptedException();
        }

        int index = --count;
        // 到达了屏障点
        if (index == 0) {
            boolean ranAction = false;
            try {
                final Runnable command = barrierCommand;
                if (command != null)
                    // 执行每一次到达屏障点所需要执行的任务
                    command.run();
                ranAction = true;
                // 重置状态,进入下一次屏障
                nextGeneration();
                return 0;
            } finally {
                if (!ranAction)
                    breakBarrier();
            }
        }

        // 如果index不为0
        // 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) {
                // 执行此处时,有可能其他线程已经调用了nextGeneration方法
                // 此时应该使当前线程正常执行下去
                // 否则打破屏障
                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();
    }
}

// 打破屏障
private void breakBarrier() {
    // 设置打破标志
    generation.broken = true;
    // 重置count
    count = parties;
    // 唤醒所有等待的线程
    trip.signalAll();
}

private void nextGeneration() {
    // 唤醒当前屏障下所有被阻塞的线程
    trip.signalAll();
    // 重置状态,进入下一次屏障
    count = parties;
    generation = new Generation();
}

 

Semaphore principle research

Semaphore semaphore is also a synchronizer. Unlike CountDownLatch and CyclicBarrier, its internal counter is incremented, and the initial value of the counter (usually 0) can be specified during initialization, but it is not necessary to know the number of threads that need to be synchronized. Instead, specify the number of threads that need to be synchronized when calling the acquire method where synchronization is needed.

Example

public static void main(String[] args) throws InterruptedException {
    final int THREAD_COUNT = 2;
    // 初始信号量为0
    Semaphore semaphore = new Semaphore(0);
    ExecutorService executorService = Executors.newFixedThreadPool(THREAD_COUNT);

    for (int i = 0; i < THREAD_COUNT; i++){
        executorService.execute(new Runnable() {
            @Override
            public void run() {
                System.out.println(Thread.currentThread() + " over");
                // 信号量+1
                semaphore.release();
            }
        });
    }

    // 当信号量达到2时才停止阻塞
    semaphore.acquire(2);
    System.out.println("all child thread over!");

    executorService.shutdown();
}

Class diagram structure

It can be seen from the figure that Semaphore is still implemented using AQS, and a fairness strategy can be selected (the default is unfair).

Source code analysis

void acquire()

Indicates that the current thread wants to obtain a semaphore resource. If the current semaphore is greater than 0, the current semaphore count is decreased by 1, and the method returns directly. Otherwise, if the current semaphore is equal to 0, it is blocked.

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

public final void acquireSharedInterruptibly(int arg)
        throws InterruptedException {
    // 可以被中断
    if (Thread.interrupted())
        throw new InterruptedException();
    // 调用Sync子类方法尝试获取,这里根据构造函数决定公平策略
    if (tryAcquireShared(arg) < 0)
        // 将当前线程放入阻塞队列,然后再次尝试
        // 如果失败则挂起当前线程
        doAcquireSharedInterruptibly(arg);
}

tryAcquireShared is implemented by a subclass of Sync to take corresponding actions based on fairness.

The following is the implementation of the non-fair strategy NofairSync:

protected int tryAcquireShared(int acquires) {
    return nonfairTryAcquireShared(acquires);
}

final int nonfairTryAcquireShared(int acquires) {
    for (;;) {
        int available = getState();
        int remaining = available - acquires;
        // 如果剩余信号量小于0直接返回
        // 否则如果更新信号量成功则返回
        if (remaining < 0 ||
            compareAndSetState(available, remaining))
            return remaining;
    }
}

Suppose thread A calls the acquire method to try to acquire the semaphore but is blocked due to insufficient semaphore. At this time, thread B increases the semaphore through release. At this time, thread C can call the acquire method to successfully acquire the semaphore (if the semaphore is sufficient) ), this is a manifestation of unfairness.

The following is the realization of fairness:

protected int tryAcquireShared(int acquires) {
    for (;;) {
        // 关键在于先判断AQS队列中是否已经有元素要获取信号量
        if (hasQueuedPredecessors())
            return -1;
        int available = getState();
        int remaining = available - acquires;
        if (remaining < 0 ||
            compareAndSetState(available, remaining))
            return remaining;
    }
}

The hasQueuedPredecessors method (refer to the analysis of lock principle in Java concurrent package in Chapter 6) is used to determine whether the predecessor node of the current thread is also waiting to acquire the resource, if so, it will give up the acquired permission, and then the current thread will be put into AQS , Otherwise try to get it.

void acquire(int permits)

Multiple semaphores can be obtained.

public void acquire(int permits) throws InterruptedException {
    if (permits < 0) throw new IllegalArgumentException();
    sync.acquireSharedInterruptibly(permits);
}

void acquireUninterruptibly()

Does not respond to interrupts.

public void acquireUninterruptibly() {
    sync.acquireShared(1);
}

void acquireUninterruptibly(int permits)

Does not respond to interrupts and can acquire multiple semaphores.

public void acquireUninterruptibly(int permits) {
    if (permits < 0) throw new IllegalArgumentException();
    sync.acquireShared(permits);
}

void release()

Add 1 to the semaphore. If a thread is currently blocked by calling the acquire method and is put into the AQS, a thread whose number of semaphores can be satisfied will be selected for activation according to the fairness strategy.

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

public final boolean releaseShared(int arg) {
    // 尝试释放资源(增加信号量)
    if (tryReleaseShared(arg)) {
        // 释放资源成功则根据公平性策略唤醒AQS中阻塞的线程
        doReleaseShared();
        return true;
    }
    return false;
}

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

void release(int permits)

Multiple semaphores can be added.

public void release(int permits) {
    if (permits < 0) throw new IllegalArgumentException();
    sync.releaseShared(permits);
}

 

Guess you like

Origin blog.csdn.net/u011694328/article/details/113103368