Java 同步器源码剖析

博客文章地址:Java 同步器源码剖析

个人博客主页:www.samsa-blog.top 欢迎各位掘友交流

一、CountDownLatch

CountDownLatch主要的应用场景是:在主线程中开启多线程去并行执行任务,然后主线程需要等待所有子线程执行完后再进行汇总的场景。

1.1 应用场景

当任务A,B执行完毕之后,主线程再进行任务。

在任务A,B没有执行完之前,也就是都没有调用 countDown方法将计数器减为0之前,主线程调用的await方法会挂起等待任务执行完之后,才会执行。

JAVA 复制代码
public class CountDownLatchTest {
    // 创建一个CountDownLatch 实例
    private static CountDownLatch countDownLatch = new CountDownLatch(2);
​
    public static void main(String[] args) throws InterruptedException {
        ExecutorService executorService = Executors.newFixedThreadPool(2) ;
​
        //将任务A加入线程池
        executorService.submit(new Runnable() {
            @Override
            public void run() {
                try {
                    Thread .sleep(1000) ;
                } catch (InterruptedException e) {
                    e.printStackTrace();
                } finally {
                    countDownLatch.countDown();
                    System.out.println("child TaskOne over!");
                }
            }
        });
​
        //将任务B加入线程池
        executorService.submit(new Runnable() {
            @Override
            public void run() {
                try {
                    Thread .sleep(1000) ;
                } catch (InterruptedException e) {
                    e.printStackTrace();
                } finally {
                    countDownLatch.countDown();
                    System.out.println("child TaskTwo over!");
                }
            }
        });
​
        System.out.println("wait all child task over");
        countDownLatch.await();
        System.out.println ("all child task over");
        executorService.shutdown();
    }
}
​
// 执行结果:
    wait all child task over
    child TaskTwo over!
    child TaskOne over!
    all child task over

1.2 源码剖析

从类图可以看出 CountDownLatch 是使用 AQS 实现的;其计数器原理就是计数器的值赋给了 AQS 的状态变量 state

JAVA 复制代码
public class CountDownLatch {
    public CountDownLatch(int count) {
        if (count < 0) throw new IllegalArgumentException("count < 0");
        this.sync = new Sync(count); 
    }
    
    private static final class Sync extends AbstractQueuedSynchronizer {
        Sync(int count) {
            // 设置计数值,这里跟进去就会进入AQS,将count值赋值给state
            setState(count); 
        }
    }
}

1.2.1 void await()方法

当线程调 CountDownLatch 对象的 await 方法后,当前线程会被直到下面的情况之一发生才会返回:

  • 当所有线程都调用了CountDownLatch对象的countDown方法后,也就是计数器值为0时;
  • 其他线程调用了当前线程的interrupt()方法中断了当前线程;
  • 当前线程就会抛出 InterruptedException 异常 然后返回。
JAVA 复制代码
// (1)java.util.concurrent.CountDownLatch#await()
public void await() throws InterruptedException {
    sync.acquireSharedInterruptibly(1);
}
​
// java.util.concurrent.locks.AbstractQueuedSynchronizer#acquireSharedInterruptibly
public final void acquireSharedInterruptibly(int arg) throws InterruptedException {
    if (Thread.interrupted()) throw new InterruptedException(); // 如果线程被中断则抛出异常
    if (tryAcquireShared(arg) < 0) // (2)这里仅仅是判断 计数器是否为0,为0:直接返回
        // (3)不为0,进入AQS队列等待
        doAcquireSharedInterruptibly(arg);
}
​
// java.util.concurrent.CountDownLatch.Sync#tryAcquireShared
protected int tryAcquireShared(int acquires) {
    return (getState() == 0) ? 1 : -1;
}

1.2.2 void countDown()方法

线程调用该方法后 ,计数器的值递减,递减后如果数器值为0,则唤醒所有因调用await方法而被阻塞的线程,否则什么都不做。

JAVA 复制代码
// java.util.concurrent.CountDownLatch#countDown
public void countDown() {
    sync.releaseShared(1);
}
​
// java.util.concurrent.locks.AbstractQueuedSynchronizer#releaseShared
public final boolean releaseShared(int arg) {
    if (tryReleaseShared(arg)) {  // (4)调用sync实现AQS的tryReleaseShared方法
        // (5)AQS释放资源的方法,就是让当前调用await()方法等待计数器为0的线程释放,执行任务
        doReleaseShared();
        return true;
    }
    return false;
}
​
// java.util.concurrent.CountDownLatch.Sync#tryReleaseShared
protected boolean tryReleaseShared(int releases) {
    // 这里是无锁并发,基于CAS实现对计数器-1操作;
    // 利用循环CAS,保证当前线程成功完成 计数器-1操作。
    for (;;) {
        int c = getState();
        // (4.1)如果当前计数器为0,再减就变成负数了,直接false
        if (c == 0) 
            return false;
        int nextc = c-1;
        // (4.2)利用CAS让计数器-1,如果CAS-1之后为0,那么返回true,则会进入到(5)方法里面
        if (compareAndSetState(c, nextc)) 
            return nextc == 0;
    }
}

二、回环屏障CyclicBarrier

上面的CountDownLatch计数器是一次性的,也就是等到计数器值变为0后,再调用CountDownLatch的await、countdown方法都会立刻返回,这就起不到线程同步的效果了。

而CyclicBarrier类(回环屏障)它可以让一组线程全部达到一个状态后再全部同时执行。

也就是说 new CyclicBarrier(3):线程调用await方法后会被阻塞,这个阻塞点就屏障点;这里当有三个线程都调用await方法之后,就会冲破屏障,继续下运行。

2.1 应用场景

  • 场景一:

    用两个线程去执行一个被分解成两个子任务的任务,当两个子线程把自己的子任务都执行完毕后再对它们的结果进行汇总处理。

    JAVA 复制代码
    public class CycleBarrierTest1 {
        // 创建一个CyclicBarrier实例,添加一个所有子线程全部到达屏障后执行的任务
        private static CyclicBarrier cyclicBarrier= new CyclicBarrier(2, new Runnable() {
            public void run() {
                System.out.println(Thread.currentThread() +"task1 merge result");
                System.out.println("-------------------------");
            }
        });
    ​
        public static void main(String[] args) {
            ExecutorService executorService = Executors.newFixedThreadPool(2) ;
            // 任务 1-1
            executorService.submit(new Runnable() {
                @SneakyThrows
                @Override
                public void run() {
                    System.out.println(Thread.currentThread( ) + "task1-1" ) ;
                    System.out.println(Thread.currentThread() + "enter in barrier");
                    cyclicBarrier.await();
                    System.out.println(Thread . currentThread() + "enter out barrier");
                }
            });
            // 任务 1-2
            executorService.submit(new Runnable() {
                @SneakyThrows
                @Override
                public void run() {
                    System.out.println(Thread.currentThread( ) + "task1-2" ) ;
                    System.out.println(Thread.currentThread() + "enter in barrier");
                    cyclicBarrier.await();
                    System.out.println(Thread . currentThread() + "enter out barrier");
                }
            });
    ​
            executorService.shutdown();
        }
    }
    ​
    // 执行结果:
        Thread[pool-1-thread-1,5,main]task1-1
        Thread[pool-1-thread-1,5,main]enter in barrier
        Thread[pool-1-thread-2,5,main]task1-2
        Thread[pool-1-thread-2,5,main]enter in barrier
        Thread[pool-1-thread-2,5,main]task1 merge result
        -------------------------
        Thread[pool-1-thread-2,5,main]enter out barrier
        Thread[pool-1-thread-1,5,main]enter out barrier
  • 场景二:

    现在有两个任务,每个任务都有三个阶段:阶段1、阶段2和阶段3组成,每个线程要串行地执行阶段1、阶段2和阶段3;同时需要满足当多个线程执行任务时,必须要保证所有线程执行完阶段1之后才能进入阶段2,所有线程执行完阶段2之后才能进入阶段3。

    利用CyclicBarrier完成:

    JAVA 复制代码
    public class CycleBarrierTest2 {
        //创建一个CyclicBarrier
        private static CyclicBarrier cyclicBarrier = new CyclicBarrier(2);
    ​
        public static void main(String[] args) {
            ExecutorService executorService = Executors.newFixedThreadPool(2);
    ​
            //将任务A加到线程池,任务A分为三个步骤 1~3
            executorService.submit(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();
                    }
                }
            });
    ​
            //将任务B加到线程池,任务B分为三个步骤 1~3
            executorService.submit(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();
        }
    }
    ​
    // 执行结果:
        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-1,5,main] step3
        Thread[pool-1-thread-2,5,main] step3

2.2 源码剖析

  • CyclicBarrier 基于独占锁实现,本质底层还是基于AQS的。
  • parties:用来记录线程个数,这里表示多少线程调用 await 后,所有线程才会冲破屏障继续往下进行。
  • count:count一开始等于parties,每当有线程调用 await 方法就-1,count=0表示所有线程都到了屏障点;由于CycleBarirer 可以被复用的,所以parties始终用来记录总的线程个数,当 count 计数器值变为0时会将 parties 的值赋给 count,而进行复用。
  • barrierCommand:当所有线程到达屏障点之后,执行的任务。
  • lock:保证更新计数器count的原子性。
  • trip:lock的条件变量,支持线程await,signal操作进行同步。
  • Generation.broken:记录当前屏障是否被打破。

2.2.1 int await()方法

JAVA 复制代码
// java.util.concurrent.CyclicBarrier#await()
public int await() throws InterruptedException, BrokenBarrierException {
    try {
        return dowait(false, 0L);
    } catch (TimeoutException toe) {
        throw new Error(toe); // cannot happen
    }
}
​
// java.util.concurrent.CyclicBarrier#dowait
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) // 如果为true,说明已经到达屏障被打破
            throw new BrokenBarrierException();
        if (Thread.interrupted()) { // 线程被中断,屏障标志设置为打破,抛出异常。
            breakBarrier();
            throw new InterruptedException();
        }
​
        int index = --count;
        //  (1)index==0 则说明所有线程都到了屏障点,此时执行初始化时传递的任务
        if (index == 0) {  // tripped
            boolean ranAction = false;
            try {
                final Runnable command = barrierCommand;
                if (command != null)  // (2)如果到达屏障点,有设置任务要执行,这里就执行
                    command.run();
                ranAction = true;
                // (3) 释放其他因调用await方法而被阻塞的线程,即冲破屏障点,并重置CyclicBarrier
                nextGeneration();
                return 0;
            } finally {
                if (!ranAction)
                    breakBarrier();
            }
        }
​
        // loop until tripped, broken, interrupted, or timed out
        // (4) 如果index!=0
        for (;;) {
            try {
                // (5),(6) 调用await(),就是来到了屏障点,等待所有线程都来到屏障点,那么 index=0,进入(1),到打(3)
                // (5)没有设置超时间
                if (!timed)
                    trip.await();
                // (6)设置了超时时间
                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) {  // 带超时时间的await()方法会做这个判断
                breakBarrier();
                throw new TimeoutException();
            }
        }
    } finally {
        lock.unlock();
    }
}
​
// java.util.concurrent.CyclicBarrier#nextGeneration
// 重置 CyclicBarrier
private void nextGeneration() {
    trip.signalAll();             // (7)唤醒条件队列里面阻塞线程
    count = parties;              // (8)重置count = parties
    generation = new Generation(); // (9)重新设置屏障标志位false
}

await()方法执行流程图:

三、信号量Semaphore

Semaphore它内部的计数器是递增的,并且在开始初始化Semaphore时可以指定一个初始值 ,但是并不需要 知道需要同步的线程个数,而是在需要同步的地方调用 acquire 方法时指定需要同步的线程个数

其实我们可以这样理解信号量:等红绿灯,三个灯分别为:绿灯,黄灯,红灯;车不会管红绿灯内部系统如果实现灯的转换的,只要出现红灯这个标的时候就停车,也就是需要同步的地方调用acquire方法时指定需要同步的线程个数。

3.1 应用场景

A的两个子任务执行完毕之后,同步执行(1),B的两个子任务执行完毕之后,同步执行(2)。

JAVA 复制代码
public class SemaphoreTest2 {
​
    // 创建一个Semaphore实例
    private static volatile Semaphore semaphore = new Semaphore(0);
​
    public static void main(String[] args) throws InterruptedException {
        ExecutorService executorService = Executors.newFixedThreadPool(2);
​
        // 将任务A-1 添加到线程池
        executorService.submit(new Runnable() {
            @Override
            public void run() {
                System.out.println(Thread.currentThread() + "A-1 task over");
                semaphore.release();
            }
        });
​
        // 将任务A-2 添加到线程池
        executorService.submit(new Runnable() {
            @Override
            public void run() {
                System.out.println(Thread.currentThread() + "A-2 task over");
                semaphore.release();
            }
        });
​
        // (1)等待线程执行任务A完毕,返回
        semaphore.acquire(2);
        System.out.println("A task over");
​
        // 将任务B-1 添加到线程池
        executorService.submit(new Runnable() {
            @Override
            public void run() {
                System.out.println(Thread.currentThread() + "B-1 task over");
                semaphore.release();
            }
        });
​
        // 将任务B-2 添加到线程池
        executorService.submit(new Runnable() {
            @Override
            public void run() {
                System.out.println(Thread.currentThread() + "B-2 task over");
                semaphore.release();
            }
        });
​
        // (2)等待线程执行任务B完毕,返回
        semaphore.acquire(2);
        System.out.println("B task over");
        //关闭线程池
        executorService .shutdown();
    }
}
​
// 执行结果:
    Thread[pool-1-thread-1,5,main]A-1 task over
    Thread[pool-1-thread-2,5,main]A-2 task over
    A task over
    Thread[pool-1-thread-1,5,main]B-1 task over
    Thread[pool-1-thread-2,5,main]B-2 task over
    B task over

3.2 源码剖析

Semaphore 还是使用 AQS 实现的。 Sync 只是对 AQS 个修饰,并且Sync有两个实现类,用来指定获取信号量时是否采用公平策略。

3.2.1 release()方法

下面两个方法的作用是把 Semaphore 信号量值 +1,或 +permits,如果当前有线程因为调用acquire方法被阻塞放入AQS阻塞队列,则会根据公平策略选择一个信号量个数被满足的线程进行激活,激活的线程会试获取刚增加的信号。

JAVA 复制代码
public void release() {
    sync.releaseShared(1);  // (1.1)arg=l
}

public void release(int permits) {  // (1.2)arg=permits
    if (permits < 0) 
        throw new IllegalArgumentException();
    sync.releaseShared(permits);
}
JAVA 复制代码
// java.util.concurrent.locks.AbstractQueuedSynchronizer#releaseShared
public final boolean releaseShared(int arg) {
    // (2) 这里是 只要CAS更新信号量成功,就进入doReleaseShared() 
    //     不论 信号量是否加到了 满足某一个线程同步满足的信号量值,
    //     由调用acquire的线程自己检查当前信号量值是否满足自己的要求。
    if (tryReleaseShared(arg)) {  
        doReleaseShared();  // AQS 调用park方法唤醒AQS队列里面最先挂起的线程
        return true;
    }
    return false;
}

// java.util.concurrent.Semaphore.Sync#tryReleaseShared
protected final boolean tryReleaseShared(int releases) {
    for (;;) {
        int current = getState();  // (4)获取当前信号量的值
        int next = current + releases;  // (5)将当前信号量值增加releases (+1 或 +permits)
        if (next < current) // overflow
            throw new Error("Maximum permit count exceeded");
        if (compareAndSetState(current, next)) // (6)使用CAS保证更新信号量值的原子性
            return true;
    }
}

3.2.2 acquire()方法

该方法的作用是:当前线程调用该方法的目的是希望获取信号量的值。

如果当前信号量个数大于0,当前信号量的计数会减1或减permits,然后该方法直接返回。否则如果当前信号量个数等于0 ,则当前线程会被放入 AQS 的阻塞队列。

JAVA 复制代码
public void acquire() throws InterruptedException {
	sync.acquireSharedInterruptibly(1);
}

public void acquire(int permits) throws InterruptedException {
    if (permits < 0) 
        throw new IllegalArgumentException();
    sync.acquireSharedInterruptibly(permits);
}
JAVA 复制代码
// java.util.concurrent.locks.AbstractQueuedSynchronizer#acquireSharedInterruptibly
public final void acquireSharedInterruptibly(int arg) throws InterruptedException {
    if (Thread.interrupted())   // (1)如果线程被中断,则抛出中断异常
        throw new InterruptedException();
    // (2)调用Sync子类方法尝试获取,根据构造函数传递的fair字段,确定使用公平策略还是非公平策略唤醒线程
    if (tryAcquireShared(arg) < 0) 
        // (3)如果获取失败则放入阻塞队列。然后再次尝试,如果失败则调用park方法挂起当前线程,继续等待唤醒;
        //    等待唤醒之后,还会根据 公平,非公平策略进入到tryAcquireShared(int acquires)中尝试。
        doAcquireSharedInterruptibly(arg);
}

// 公平方式
protected int tryAcquireShared(int acquires) {
    for (;;) {
        // 公平方式,从阻塞队列中唤醒一个线程尝试
        if (hasQueuedPredecessors())
            return -1;
        // 获取当前信号量
        int available = getState();
        // 计算当前剩余值
        int remaining = available - acquires;
        // 这里就是 调用acquire的线程自己去检查,当前的信号量是否满足自身。
        // 如果当前剩余值小于0或者CAS成功则返回
        // -> remaining<0:说明当前信号量值不满足该线程的同步条件,则进入(3)继续挂起
        // -> remaining>=0 & cas 成功:满足该线程同步掉件,返回。
        if (remaining < 0 || compareAndSetState(available, remaining))
            return remaining;
    }
}

// 非公平方式: 就是有可能这个时候有另外一个不在阻塞队列的线程获取信号量,将信号量 -1或-acquires
protected int tryAcquireShared(int acquires) {
    return nonfairTryAcquireShared(acquires);
}
final int nonfairTryAcquireShared(int acquires) {
    for (;;) {
        int available = getState();
        int remaining = available - acquires;
        if (remaining < 0 || compareAndSetState(available, remaining))
            return remaining;
    }
}

acquire()方法代码执行流程图:

四、总结

CountDownLatch:只要检测到计数器值为0 ,就可以往下执行;虽然join也可以达到线程同步协作效果,但是CountDownLatch更为灵活,且可以和线程池配合使用。

CyclicBarrier:回环屏障,可以达到CountDownLatch的效果,但是CountDownLatch在计数器值为0后,就不能在使用,但是CyclicBarrier是可以复用的,即:所有线程到达屏障点之后会重置count值为parties

Semaphore:信号量,采用递增策略。开始并不需要关注同步的线程个数,等调用acquire方法时再指定需要同步的个数,并且提供了获取信号量的公平性策略。

相关推荐
工业互联网专业2 分钟前
毕业设计选题:基于springboot+vue+uniapp的驾校报名小程序
vue.js·spring boot·小程序·uni-app·毕业设计·源码·课程设计
Monodye10 分钟前
【Java】网络编程:TCP_IP协议详解(IP协议数据报文及如何解决IPv4不够的状况)
java·网络·数据结构·算法·系统架构
一丝晨光16 分钟前
逻辑运算符
java·c++·python·kotlin·c#·c·逻辑运算符
无名指的等待71240 分钟前
SpringBoot中使用ElasticSearch
java·spring boot·后端
Tatakai251 小时前
Mybatis Plus分页查询返回total为0问题
java·spring·bug·mybatis
武子康1 小时前
大数据-133 - ClickHouse 基础概述 全面了解
java·大数据·分布式·clickhouse·flink·spark
.生产的驴1 小时前
SpringBoot 消息队列RabbitMQ 消费者确认机制 失败重试机制
java·spring boot·分布式·后端·rabbitmq·java-rabbitmq
Code哈哈笑2 小时前
【C++ 学习】多态的基础和原理(10)
java·c++·学习
chushiyunen2 小时前
redisController工具类
java
A_cot2 小时前
Redis 的三个并发问题及解决方案(面试题)
java·开发语言·数据库·redis·mybatis