当多个线程同时参与某项任务时,如何知道他们都已经执行完毕

前几天被问到,多个线程同时查询一张表,最后汇总查询结果返回,那么就存在一个问题,如何判断多个线程是否全部已经处理完成。

网上整理资料,总体有两种方法:

第一种:自己实现,即自己实现计数器,具体操作为:

其实这类问题的核心论点都是:如何在一个线程中得知其他线程是否执行完毕。

假设现在有 3 个线程在运行,需要在主线程中得知他们的运行结果;可以分为以下几步:

  • 定义一个计数器为 3。
  • 每个线程完成任务后计数减一。
  • 一旦计数器减为 0 则通知等待的线程。

所以也很容易想到可以利用等待通知机制来实现,阻塞队列的类似。

按照这个思路自定义了一个 MultipleThreadCountDownKit 工具,构造函数如下:

考虑到并发的前提,这个计数器自然需要保证线程安全,所以采用了 AtomicInteger

所以在初始化时需要根据线程数量来构建对象。

计数器减一

当其中一个业务线程完成后需要将这个计数器减一,直到减为0为止。

    /**     * 线程完成后计数 -1     */    
    public void countDown(){
        if (counter.get() <= 0){ 
                   return;     
         }
        int count = this.counter.decrementAndGet();
        if (count < 0){
                    throw new RuntimeException("concurrent error") ;
         }
        if (count == 0){ 
                   synchronized (notify){
                                   notify.notify();  
                   }    
        }
    }

利用 counter.decrementAndGet() 来保证多线程的原子性,当减为 0 时则利用等待通知机制来 notify 其他线程。

等待所有线程完成

而需要知道业务线程执行完毕的其他线程则需要在未完成之前一直处于等待状态,直到上文提到的在计数器变为 0 时得到通知。

    /**     * 等待所有的线程完成     * @throws InterruptedException     */        
    public void await() throws InterruptedException {
        synchronized (notify) {
            while (counter.get() > 0) {
                notify.wait();
            }
            if (notifyListen != null) {
                notifyListen.notifyListen();
            }
        }
    }

原理也很简单,一旦计数器还存在时则会利用 notify 对象进行等待,直到被业务线程唤醒。

同时这里新增了一个通知接口可以自定义实现唤醒后的一些业务逻辑,后文会做演示。

并发测试

主要就是这两个函数,下面来做一个演示。

  • 初始化了三个计数器的并发工具 MultipleThreadCountDownKit
  • 创建了三个线程分别执行业务逻辑,完毕后执行 countDown()
  • 线程 3 休眠了 2s 用于模拟业务耗时。
  • 主线程执行 await() 等待他们三个线程执行完毕。

通过执行结果可以看出主线程会等待最后一个线程完成后才会退出;从而达到了主线程等待其余线程的效果。

MultipleThreadCountDownKit multipleThreadKit = new MultipleThreadCountDownKit(3); 
   multipleThreadKit.setNotify(() -> LOGGER.info("三个线程完成了任务"));

也可以在初始化的时候指定一个回调接口,用于接收业务线程执行完毕后的通知。

当然和在主线程中执行这段逻辑效果是一样的(和执行 await() 方法处于同一个线程)。

第二种方法:CountDownLatch

当然我们自己实现的代码没有经过大量生产环境的验证,所以主要的目的还是尝试窥探官方的实现原理。

所以我们现在来看看 juc 下的 CountDownLatch 是如何实现的。

通过构造函数会发现有一个 内部类 Sync,他是继承于 AbstractQueuedSynchronizer ;这是 Java 并发包中的基础框架,都可以单独拿来讲了,所以这次重点不是它,今后我们再着重介绍

countDown

其实他的核心逻辑和我们自己实现的区别不大。

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

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

利用这个内部类的 releaseShared 方法,我们可以理解为他想要将计数器减一。

看到这里有没有似曾相识的感觉。

没错,在 JDK1.7 中的 AtomicInteger 自减就是这样实现的(利用 CAS 保证了线程安全)。

只是一旦计数器减为 0 时则会执行 doReleaseShared 唤醒其他的线程。

这里我们只需要关心红框部分(其他的暂时不用关心,这里涉及到了 AQS 中的队列相关),最终会调用 LockSupport.unpark 来唤醒线程;就相当于上文调用 object.notify()

所以其实本质上还是相同的。

await

其中的 await() 也是借用 Sync 对象的方法实现的。

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

    public final void acquireSharedInterruptibly(int arg) throws InterruptedException {
        if (Thread.interrupted())
            throw new InterruptedException();
        //判断计数器是否还未完成            
        if (tryAcquireShared(arg) < 0) doAcquireSharedInterruptibly(arg);
    }

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

一旦还存在未完成的线程时,则会调用 doAcquireSharedInterruptibly 进入阻塞状态。

     private final boolean parkAndCheckInterrupt() {
        LockSupport.park(this);
        return Thread.interrupted();
    }

同样的由于这也是 AQS 中的方法,我们只需要关心红框部分;其实最终就是调用了 LockSupport.park 方法,也就相当于执行了 object.wait() 。

  • 所有的业务线程执行完毕后会在计数器减为 0 时调用 LockSupport.unpark 来唤醒线程。
  • 等待线程一旦计数器 > 0 时则会利用 LockSupport.park 来等待唤醒。

这样整个流程也就串起来了,它的使用方法也和上文的类似。

就不做过多介绍了。

实际案例

同样的来看一个实际案例。

在上一篇《一次分表踩坑实践的探讨》提到了对于全表扫描的情况下,需要利用多线程来提高查询效率。

比如我们这里分为了 64 张表,计划利用 8 个线程来分别处理这些表的数据,伪代码如下:

    CountDownLatch count = new CountDownLatch(64);
    ConcurrentHashMap total = new ConcurrentHashMap();
    for(Integer i = 0;i<=63;i++){
        executor.execute(new Runnable() {
            @Override
            public void run() {
                List value = queryTable(i);
                total.put(value, NULL);
                count.countDown();
            }
        });
    }
    count.await();
    System.out.println("查询完毕");

这样就可以实现所有数据都查询完毕后再做统一汇总;代码挺简单,也好理解(当然也可以使用线程池的 API)。

总结

CountDownLatch 算是 juc 中一个高频使用的工具,学会和理解他的使用会帮助我们更容易编写并发应用。

 

 

 

  • 1
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
在Qt中,可以使用QThread类来实现多线程。要让多个线程执行同一个任务,可以将任务封装成一个QObject派生类,并将该类对象移动到多个QThread线程中。 为了避免死锁,需要注意以下几点: 1.不要在QThread线程中直接调用GUI线程中的函数,因为GUI线程可能会阻塞,导致死锁。可以通过信号槽机制来解决。 2.确保线程安全。多个线程访问同一个变量或资源,需要使用互斥锁等机制来保护。 3.不要在多个线程中同访问同一个QObject对象,因为QObject不是线程安全的。可以通过将该对象放入一个单独的线程中,或者使用线程安全的数据结构来解决。 下面是一个简单的示例代码,演示如何在Qt中实现多线程并避免死锁: ``` // Worker类,封装了一个任务 class Worker : public QObject { Q_OBJECT public: Worker(QObject *parent = nullptr) : QObject(parent) {} public slots: void doWork() { // 执行任务 } }; // 主线程中创建多个QThread线程,并将任务分配给它们执行 int main(int argc, char *argv[]) { QApplication a(argc, argv); // 创建多个QThread线程 QList<QThread *> threads; for (int i = 0; i < 5; i++) { QThread *thread = new QThread(); threads.append(thread); thread->start(); } // 创建任务 Worker *worker = new Worker(); // 将任务分配给多个线程执行 foreach (QThread *thread, threads) { worker->moveToThread(thread); QObject::connect(thread, &QThread::started, worker, &Worker::doWork); } return a.exec(); } ``` 在上面的示例代码中,我们创建了一个Worker类,封装了一个任务,并在主线程中创建了多个QThread线程。然后将任务分配给多个线程执行,使用了QObject::moveToThread()方法将Worker对象移动到各个线程中,并使用信号槽机制来触发任务执行。这样就可以实现多个线程执行同一个任务了。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值