zoukankan      html  css  js  c++  java
  • 在 Java 中完全同时启动两个线程-Java快速入门教程

    1. 概述

    多线程编程允许我们并发运行线程,每个线程可以处理不同的任务。因此,它可以最佳地利用资源,特别是当我们的计算机具有多个多核 CPU 或多个 CPU 时。有时,我们想控制多个线程同时启动。

    在本教程中,我们将首先了解要求,尤其是“完全相同的时间”的含义。此外,我们将讨论如何在 Java 中同时启动两个线程。

    2. 了解需求

    我们的要求是:“同时启动两个线程。”

    这个要求看起来很容易理解。但是,如果我们仔细考虑一下,甚至可以完全 同时启动两个线程吗?

    首先,每个线程都会消耗CPU时间来工作。因此,如果我们的应用程序运行在具有单核 CPU 的计算机上,则不可能完全 同时启动两个线程

    如果我们的计算机有一个多核CPU或多个CPU,两个线程可以在可能开始确切同一时间。但是,我们无法在 Java 端对其进行控制。

    这是因为当我们在 Java 中使用线程时,Java 线程调度依赖于操作系统的线程调度因此,不同的操作系统可能会以不同的方式处理它。

    此外,如果我们以更严格的方式讨论“完全相同的时间”,根据爱因斯坦的狭义相对论

    如果两个不同的事件在空间上是分开的,就不可能在绝对意义上说这两个不同的事件同时发生。

    无论我们的 CPU 位于主板上或位于 CPU 中的内核多近,总有空间。因此,我们不能确保两个线程完全同时启动。

    那么,这是否意味着该要求无效?

    不,这是一个有效的要求。即使我们不能让两个线程开始EXACT同一时间,我们可以得到非常接近通过一些同步技术。

    当我们需要两个线程“同时”启动时,这些技术可以在大多数实际情况下帮助我们。

    在本教程中,我们将探索两种解决此问题的方法:

    • 使用CountDownLatch
    • 使用CyclicBarrier
    • 使用Phaser

    所有方法都遵循相同的想法:我们不会真正同时启动两个线程。相反,我们在线程启动后立即阻塞线程并尝试同时恢复它们的执行。

     

    由于我们的测试将与线程调度相关,因此值得一提的是本教程中运行测试的环境:

    • CPU:Intel(R) Core(TM) i7-8850H CPU。处理器时钟在 2.6 和 4.3 GHz 之间(4.1 4 核,4 GHz 6 核)
    • 操作系统:64 位 Linux 内核版本 5.12.12
    • Java:Java 11

    现在,让我们看看CountDonwLatchCyclicBarrier的作用。

    3. 使用CountDownLatch

     

    CountDownLatch是 Java 5 中作为java.util.concurrent的一部分引入的同步器通常,我们使用CountDownLatch来阻塞线程,直到其他线程完成它们的任务。

    简单地说,我们闩锁对象中设置一个计数并将闩锁对象与一些线程相关联当我们启动这些线程时,它们将被阻塞,直到闩锁的计数变为零。

    另一方面,在其他线程中,我们可以控制在什么情况下我们减少计数,让被阻塞的线程恢复,例如,当主线程中的某些任务完成时。

    3.1. 工作线程

     

    现在,让我们看看如何使用CountDownLatch解决我们的问题

    首先,我们将创建我们的Thread类。我们称之为WorkerWithCountDownLatch

    public class WorkerWithCountDownLatch extends Thread {
        private CountDownLatch latch;
    
        public WorkerWithCountDownLatch(String name, CountDownLatch latch) {
            this.latch = latch;
            setName(name);
        }
    
        @Override public void run() {
            try {
                System.out.printf("[ %s ] created, blocked by the latch...
    ", getName());
                latch.await();
                System.out.printf("[ %s ] starts at: %s
    ", getName(), Instant.now());
                // do actual work here...
            } catch (InterruptedException e) {
                // handle exception
            }
        }
    

    我们在WorkerWithCountDownLatch 类中添加了一个闩锁对象首先我们来了解一下latch对象的作用。

    run()方法中,我们调用了latch.await()方法。 这意味着,如果我们启动工作线程,它将检查闩锁的计数。 线程将被阻塞,直到计数为零。

    这样,我们就可以在主线程中创建一个count=1CountDownLatch(1)闩锁,并将闩锁对象与我们想要同时启动的两个工作线程相关联

    当我们希望两个线程继续执行它们的实际工作时,我们通过 在主线程中调用latch.countDown()释放闩锁

    接下来我们来看看主线程是如何控制这两个工作线程的。

    3.2. 主线程

    我们将在usingCountDownLatch()方法中实现主线程

    private static void usingCountDownLatch() throws InterruptedException {
        System.out.println("===============================================");
        System.out.println("        >>> Using CountDownLatch <<<<");
        System.out.println("===============================================");
    
        CountDownLatch latch = new CountDownLatch(1);
    
        WorkerWithCountDownLatch worker1 = new WorkerWithCountDownLatch("Worker with latch 1", latch);
        WorkerWithCountDownLatch worker2 = new WorkerWithCountDownLatch("Worker with latch 2", latch);
    
        worker1.start();
        worker2.start();
    
        Thread.sleep(10);//simulation of some actual work
    
        System.out.println("-----------------------------------------------");
        System.out.println(" Now release the latch:");
        System.out.println("-----------------------------------------------");
        latch.countDown();
    }
    

    现在,让我们从main()方法调用上面的usingCountDownLatch ()方法。当我们运行 main()方法时,我们会看到输出:

    ===============================================
            >>> Using CountDownLatch <<<<
    ===============================================
    [ Worker with latch 1 ] created, blocked by the latch
    [ Worker with latch 2 ] created, blocked by the latch
    -----------------------------------------------
     Now release the latch:
    -----------------------------------------------
    [ Worker with latch 2 ] starts at: 2021-06-27T16:00:52.268532035Z
    [ Worker with latch 1 ] starts at: 2021-06-27T16:00:52.268533787Z
    

    如上面的输出所示,两个工作线程几乎同时启动。两个开始时间之间的差异小于两微秒

    4. 使用CyclicBarrier 类

    所述的CyclicBarrier类是Java 5.基本上引入另一个同步器,CyclicBarrier允许等待线程的固定数量为互相继续执行之前到达一个公共点

    接下来,让我们看看如何使用CyclicBarrier解决我们的问题

    4.1. 工作线程

    我们先来看看我们的工作线程的实现:

    public class WorkerWithCyclicBarrier extends Thread {
        private CyclicBarrier barrier;
    
        public WorkerWithCyclicBarrier(String name, CyclicBarrier barrier) {
            this.barrier = barrier;
            this.setName(name);
        }
    
        @Override public void run() {
            try {
                System.out.printf("[ %s ] created, blocked by the barrier
    ", getName());
                barrier.await();
                System.out.printf("[ %s ] starts at: %s
    ", getName(), Instant.now());
                // do actual work here...
            } catch (InterruptedException | BrokenBarrierException e) {
                // handle exception
            }
        }
    }
    

    实现非常简单。我们将屏障对象与工作线程相关联当线程启动时,我们立即调用barrier.await() 方法。

    这样,工作线程就会被阻塞,等待各方调用barrier.await()恢复。

    4.2. 主线程

    接下来我们看看主线程中如何控制两个工作线程的恢复:

    private static void usingCyclicBarrier() throws BrokenBarrierException, InterruptedException {
        System.out.println("
    ===============================================");
        System.out.println("        >>> Using CyclicBarrier <<<<");
        System.out.println("===============================================");
    
        CyclicBarrier barrier = new CyclicBarrier(3);
    
        WorkerWithCyclicBarrier worker1 = new WorkerWithCyclicBarrier("Worker with barrier 1", barrier);
        WorkerWithCyclicBarrier worker2 = new WorkerWithCyclicBarrier("Worker with barrier 2", barrier);
    
        worker1.start();
        worker2.start();
    
        Thread.sleep(10);//simulation of some actual work
    
        System.out.println("-----------------------------------------------");
        System.out.println(" Now open the barrier:");
        System.out.println("-----------------------------------------------");
        barrier.await();
    }
    

    我们的目标是让两个工作线程同时恢复。所以,加上主线程,我们一共有三个线程。

    如上方法所示,我们在主线程中创建了一个包含三方屏障对象。接下来,我们创建并启动两个工作线程。

    正如我们之前所讨论的,两个工作线程被阻塞并等待屏障打开以恢复。

    在主线程中,我们可以做一些实际的工作。当我们决定打开barrier时,我们调用barrier.await() 方法让两个worker继续执行。

    如果我们在 main()方法中调用usingCyclicBarrier(),我们将得到输出:

    ===============================================
            >>> Using CyclicBarrier <<<<
    ===============================================
    [ Worker with barrier 1 ] created, blocked by the barrier
    [ Worker with barrier 2 ] created, blocked by the barrier
    -----------------------------------------------
     Now open the barrier:
    -----------------------------------------------
    [ Worker with barrier 1 ] starts at: 2021-06-27T16:00:52.311346392Z
    [ Worker with barrier 2 ] starts at: 2021-06-27T16:00:52.311348874Z

    我们可以比较两个工人的开始时间。即使两个工作人员没有在完全相同的时间开始,我们也非常接近我们的目标:两个开始时间之间的差异小于三微秒。

    5. 使用Phaser

    移相器类是引入了同步Java 7已经很类似的CyclicBarrierCountDownLatch但是,Phaser类更灵活。

    例如,与CyclicBarrierCountDownLatch不同Phaser允许我们动态注册线程方。

    接下来,让我们使用Phaser解决问题

    5.1. 工作线程

    像往常一样,我们先看一下实现,然后了解它是如何工作的:

    public class WorkerWithPhaser extends Thread {
        private Phaser phaser;
    
        public WorkerWithPhaser(String name, Phaser phaser) {
            this.phaser = phaser;
            phaser.register();
            setName(name);
        }
    
        @Override public void run() {
            try {
                System.out.printf("[ %s ] created, blocked by the phaser
    ", getName());
                phaser.arriveAndAwaitAdvance();
                System.out.printf("[ %s ] starts at: %s
    ", getName(), Instant.now());
                // do actual work here...
            } catch (IllegalStateException e) {
                // handle exception
            }
        }
    }

    当工作线程被实例化时,我们通过调用phaser.register()将当前线程注册到给定的 Phaser对象 这样,当前的工作就变成了移相器屏障的一个线程方 

    接下来,当工作线程启动时,我们立即调用phaser.arriveAndAwaitAdvance()因此,我们告诉 phaser当前线程已经到达,并将等待其他线程方的到达继续进行。当然,在其他线程方到来之前,当前线程是被阻塞的。

    5.2. 主线程

    接下来,我们继续看主线程的实现:

    private static void usingPhaser() throws InterruptedException {
        System.out.println("
    ===============================================");
        System.out.println("        >>> Using Phaser <<<");
        System.out.println("===============================================");
    
        Phaser phaser = new Phaser();
        phaser.register();
    
        WorkerWithPhaser worker1 = new WorkerWithPhaser("Worker with phaser 1", phaser);
        WorkerWithPhaser worker2 = new WorkerWithPhaser("Worker with phaser 2", phaser);
    
        worker1.start();
        worker2.start();
    
        Thread.sleep(10);//simulation of some actual work
    
        System.out.println("-----------------------------------------------");
        System.out.println(" Now open the phaser barrier:");
        System.out.println("-----------------------------------------------");
        phaser.arriveAndAwaitAdvance();
    }

    在上面的代码中,我们可以看到,主线程将自己注册为Phaser对象的线程方

    在我们创建并阻塞了两个工作线程之后,主线程也调用了phaser.arriveAndAwaitAdvance()这样我们可以让两个工作线程可以同时恢复。

    最后,让我们调用main()方法中的usingPhaser ()方法:

    ===============================================
            >>> Using Phaser <<<
    ===============================================
    [ Worker with phaser 1 ] created, blocked by the phaser
    [ Worker with phaser 2 ] created, blocked by the phaser
    -----------------------------------------------
     Now open the phaser barrier:
    -----------------------------------------------
    [ Worker with phaser 2 ] starts at: 2021-07-18T17:39:27.063523636Z
    [ Worker with phaser 1 ] starts at: 2021-07-18T17:39:27.063523827Z
    

    同样,两个工作线程几乎同时启动。两个开始时间之间的差异小于两微秒

    六,结论

    在本文中,我们首先讨论了要求:“同时启动两个线程”。

    接下来,我们讨论了同时启动三个线程的两种方法:使用CountDownLatch、  CyclicBarrierPhaser

     

    他们的想法很相似,阻塞两个线程并试图让它们同时恢复执行。

    尽管这些方法不能保证两个线程完全同时启动,但对于现实世界中的大多数情况,结果非常接近且足够。

  • 相关阅读:
    static关键字用法(转载)
    浮点数资源(转)
    关于指针
    使用animate()的时候,有时候会出现移进移出的闪动问题
    要求开启密码大写提示
    如果layer层在iframe下不居中滚动
    自动适应iframe右边的高度
    jquery实现输入框实时输入触发事件代码
    使得最右边的元素右边框为0
    交互过程中封装了一些常用的函数(不断添加中)
  • 原文地址:https://www.cnblogs.com/BlogNetSpace/p/15189356.html
Copyright © 2011-2022 走看看