📜  Java并发编程的不同方法

📅  最后修改于: 2022-05-13 01:55:04.732000             🧑  作者: Mango

Java并发编程的不同方法

本文展示了如何使用Java线程框架执行并发编程。我们先来分析一下并发编程:

并发编程:这意味着任务看起来是同时运行的,但实际上,系统可能真的在任务之间来回切换。并发编程的要点是即使在单处理器机器上也是有益的。

单处理器机器上的并发编程:

  1. 假设用户需要下载五张图片,每张图片来自不同的服务器,每张图片需要 5 秒,现在假设用户下载了所有的第一张图片,需要 5 秒,然后所有的第二张图片,它又需要 5 秒,以此类推,到时间结束时,需要 25 秒。下载一点点图像一,然后一点点图像二、三、四、五,然后再回来一点点图像一,以此类推,速度更快。
    Tasks overlap in time
    
    Task1   ------   ------   ------    ------ 
    
    Task2       ------   ------   ------   ------ 
    
            ------------------------------------------>
                                 Time
    
  2. 如果每个需要 5 秒,然后将其分成小块,那么总和仍然是 25 秒。那为什么同时下载它会更快。
  3. 这是因为当调用来自第一台服务器的图像时,需要 5 秒,不是因为传入的带宽被最大化,而是因为服务器需要一段时间才能将其发送给用户。基本上,用户大部分时间都在等待。因此,当用户在等待第一个图像时,他不妨开始下载第二个图像。因此,如果服务器很慢,通过同时在多个线程中执行它,可以下载额外的图像而无需太多额外的时间。
  4. 现在最终,如果一个人同时下载大量图像,传入带宽可能会被最大化,然后添加更多线程不会加快速度,但在某种程度上,它是免费的。
  5. 除了速度之外,另一个优点是减少了延迟。一次做一点可以减少延迟,因此用户可以在事情进行时看到一些反馈。

并发编程的需要

  • 仅当任务相对较大且几乎是自包含的时,线程才有用。当用户在大量的单独处理之后只需要执行少量的组合时,启动和使用线程会有一些开销。因此,如果任务非常小,则永远不会为开销而得到回报。
  • 此外,如上所述,线程在用户等待时最有用。例如,当一个服务器在等待一个服务器时,另一个可能正在从另一台服务器读取数据。

并发编程的基本步骤

  1. 首先对任务进行排队。调用执行器服务点新的固定线程池并提供大小。此大小表示同时任务的最大数量。例如,如果将一千个事物添加到队列中,但池大小为 50,那么任何时候只有 50 个事物会运行。只有当前 50 个中的一个完成执行时,才会占用第 51 个执行。像 100 这样的数字作为池大小不会使系统过载。
    ExecutorService taskList = Executors.newFixedThreadPool(poolSize);
    
  2. 然后,用户必须将一些可运行类型的任务放入任务队列。 Runnable 只是一个单一的接口,它有一个名为 run 的方法。系统在适当的时候通过启动一个单独的线程在任务之间来回切换时调用run方法。
    taskList.execute(someRunnable)
  3. Execute 方法有点用词不当,因为当一个任务被添加到上面使用 executors dot new 固定线程池创建的队列中的任务时,它不一定立即开始执行它。当同时执行的其中一个(池大小)完成执行时,它开始执行。

有五种不同的方法来实现并发编程,各有优缺点。我们将在本文中讨论第一种方法,在后续文章中讨论其余方法。

方法一:实现 Runnable 的单独类

  1. 首先要做的是创建一个单独的类,并且是一个完全独立的类,它实现了可运行接口。
    public class MyRunnable implements Runnable {
             public void run() { ... }  
    }
  2. 其次制作主类的一些实例并将它们传递给执行。让我们应用第一种方法来制作只计数的线程。因此,每个线程都会打印线程名称、任务号和计数器值。
  3. 在此之后使用 pause 方法坐下来等待,以便系统来回切换。打印语句将因此被交错。
  4. 将构造函数参数传递给 Runnable 的构造函数,以便不同的实例计算不同的次数。
  5. 调用关闭方法意味着关闭正在监视的线程以查看是否添加了任何新任务。

实际实施

import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
  
/**
* @author evivehealth on 08/02/19.
*/
// Java program depicting 
// concurrent programming in action.
  
// Runnable Class that defines the logic
// of run method of runnable interface
public class Counter implements Runnable 
{
    private final MainApp mainApp;
    private final int loopLimit;
    private final String task;
  
    // Constructor to get a reference to the main class
    public Counter
          (MainApp mainApp, int loopLimit, String task)
    {
        this.mainApp = mainApp;
        this.loopLimit = loopLimit;
        this.task = task;
    }
  
    // Prints the thread name, task number and 
    // the value of counter
    // Calls pause method to allow multithreading to occur
    @Override
    public void run()
    {
        for (int i = 0; i < loopLimit; i++) 
        {
            System.out.println("Thread: " +
            Thread.currentThread().getName() + " Counter: "
                             + (i + 1) + " Task: " + task);
            mainApp.pause(Math.random());
        }
    }
}
class MainApp 
{
  
    // Starts the threads. Pool size 2 means at any time
    // there can only be two simultaneous threads
    public void startThread()
    {
        ExecutorService taskList = 
                         Executors.newFixedThreadPool(2);
        for (int i = 0; i < 5; i++) 
        {
            // Makes tasks available for execution.
            // At the appropriate time, calls run 
            // method of runnable interface
            taskList.execute(new Counter(this, i + 1,
                                    "task " + (i + 1)));
        }
  
        // Shuts the thread that's watching to see if 
        // you have added new tasks.
        taskList.shutdown();
    }
  
    // Pauses execution for a moment
    // so that system switches back and forth
    public void pause(double seconds)
    {
        try 
        {
            Thread.sleep(Math.round(1000.0 * seconds));
        }
        catch (InterruptedException e)
        {
            e.printStackTrace();
        }
    }
  
    // Driver method
    public static void main(String[] args)
    {
        new MainApp().startThread();
    }
}
输出:
Thread: pool-1-thread-1 Counter: 1 Task: task 1
Thread: pool-1-thread-2 Counter: 1 Task: task 2
Thread: pool-1-thread-2 Counter: 2 Task: task 2
Thread: pool-1-thread-1 Counter: 1 Task: task 3
Thread: pool-1-thread-2 Counter: 1 Task: task 4
Thread: pool-1-thread-1 Counter: 2 Task: task 3
Thread: pool-1-thread-1 Counter: 3 Task: task 3
Thread: pool-1-thread-1 Counter: 1 Task: task 5
Thread: pool-1-thread-2 Counter: 2 Task: task 4
Thread: pool-1-thread-2 Counter: 3 Task: task 4
Thread: pool-1-thread-1 Counter: 2 Task: task 5
Thread: pool-1-thread-2 Counter: 4 Task: task 4
Thread: pool-1-thread-1 Counter: 3 Task: task 5
Thread: pool-1-thread-1 Counter: 4 Task: task 5
Thread: pool-1-thread-1 Counter: 5 Task: task 5

好处:

  • 松散耦合由于可以重用单独的类,因此它促进了松散耦合。
  • 构造函数:参数可以传递给不同情况的构造函数。例如,描述线程的不同循环限制。
  • 竞争条件:如果数据已共享,则不太可能使用单独的类作为方法,如果它没有共享数据,则无需担心竞争条件。

缺点:
回调主应用程序有点不方便。必须通过构造函数传递引用,即使可以访问引用,也只能调用主应用程序中的公共方法(给定示例中的暂停方法)。