Java 多线程实现方式、如何使用 CompletableFuture 完成异步任务

多线程实现方式、CompletableFuture 异步任务

java

一、异步和多线程有什么区别?

其实,异步和多线程并不是一个同等关系,异步是目的,多线程只是我们实现异步的一个手段.

什么是异步?

异步是当一个调用请求发送给被调用者,而调用者不用等待其结果的返回.实现异步可以采用多线程技术或则交给另外的进程来处理

多线程和异步操作两者都可以达到避免调用线程阻塞的目的,从而提高软件的可响应性。甚至有些时候我们就认为多线程和异步操作是等同的概念。但是,多线程和异步操作还是有一些区别的。而这些区别造成了使用多线程和异步操作的时机的区别。

异步和多线程是两个不同的概念,不能这样比较.异步请求一般用在IO等耗时操作上,他的好处是函数调用立即返回,相应的工作线程立即返还给系统以供重用。由于系统的线程资源是非常宝贵的,通常有一定的数目限制,如.net默认是25。若使用异步方式,用这些固定数目的线程在固定的时间内就可以服务更多的请求,而如果用同步方式,那么每个请求都自始至终占用这一个线程,服务器可以同时服务的请求数就少了。当异步操作执行完成后,系统会从可用线程中选取一个执行回调程序,这时的这个线程可能是刚开始发出请求的那个线程,也可能是其他的线程,因为系统选取线程是随机的事情,所以不能说绝对不是刚开始的那个线程。多线程是用来并发的执行多个任务。

不过有个问题,异步有时优先级比主线程还高。这个特点和多线程不同。

详见:https://blog.csdn.net/qq_36936155/article/details/78991050

二、多线程方式

java多线程实现方式主要有三种:继承Thread类、实现Runnable接口、使用ExecutorServiceCallableFuture实现有返回结果的多线程。

其中前两种方式线程执行完后都没有返回值,只有最后一种是带返回值的。

1、继承Thread类实现多线程

继承Thread类的方法尽管被我列为一种多线程实现方式,但Thread本质上也是实现了Runnable接口的一个实例,它代表一个线程的实例,并且,启动线程的唯一方法就是通过Thread类的start()实例方法。start()方法是一个native方法,它将启动一个新线程,并执行run()方法。这种方式实现多线程很简单,通过自己的类直接extend Thread,并复写run()方法,就可以启动新线程并执行自己定义的run()方法。例如:

 public class MyThread extends Thread {
   
     @Override
     public void run() {
   
         System.out.println(Thread.currentThread().getName() + ": 使用thread初始化了一个线程");
     }
 }

在合适的地方启动线程:

// 启动MyThread线程
for (int i = 0; i < 10; i++) {
   
    new MyThread().start();
}

2、实现Runnable接口方式实现多线程

如果自己的类已经extends另一个类,就无法直接extends Thread,此时,必须实现一个Runnable接口,如下:

public class MyRunnable extends OtherClass implements Runnable {
   
    @Override
    public void run() {
   
        System.out.println(Thread.currentThread().getName() + ": 使用runnable初始化一个线程");
    }
}

为了启动MyThread,需要首先实例化一个Thread,并传入自己的MyRunnable实例

 for (int i = 0; i < 10; i++) {
   
     new Thread(new MyRunnable()).start();
 }

匿名内部类的方式

 for (int i = 0; i < 10; i++) {
   
     new Thread(new Runnable() {
   
         @Override
         public void run() {
   
             System.out.println(Thread.currentThread().getName() + ": 使用runnable初始化一个线程");
         }
     }).start();
 }
 // Thread本质上也是实现了Runnable接口的一个实例(匿名内部类简化)
 for (int i = 0; i < 10; i++) {
   
     new Thread(() -> {
   
         System.out.println(Thread.currentThread().getName() + ": 使用runnable匿名内部类初始化一个线程");
     }).start();
 }

3、实现Callable接口通过FutureTask包装器来创建Thread线程

public class MyCallable implements Callable<String> {
   
    @Override
    public String call() {
   
        System.out.println(Thread.currentThread().getName() + ": 使用Callable初始化一个线程");
        return "zhangsan";
    }
}

调用返回Future对象的get()方法,从Future对象上获取任务的返回值,会阻塞直到计算完成。

不管是异常还是正常,只要运行完毕了,isDone()方法结果一样是true

 for (int i = 0; i < 10; i++) {
   
     FutureTask<String> futureTask = new FutureTask<>(new MyCallable());
     new Thread(futureTask).start();
     //System.out.println(futureTask.get());  // 阻塞
     while (!futureTask.isDone()) {
    // 轮询
         System.out.println("有结果了吗?");
     }
     System.out.println("对方同意了!");
     System.in.read();
 }

4、使用ExecutorService、Callable、Future实现有返回结果的多线程

ExecutorService、Callable、Future这个对象实际上都是属于Executor框架中的功能类。

**ExecutorService提供了submit()方法,传递一个Callable,或Runnable,返回Future。**如果Executor后台线程池还没有完成Callable的计算,这调用返回Future对象的get()方法,会阻塞直到计算完成。

 // 创建固定数目线程的线程池
 ExecutorService executorService = Executors.newFixedThreadPool(3);
 for (int i = 0; i < 10; i++) {
   
     executorService.submit(() -> {
   
         System.out.println(Thread.currentThread().getName() + ": 线程池执行任务!");
     });
 }
 // 如果Executor后台线程池还没有完成Callable的计算,这调用返回Future对象的get()方法,会阻塞直到计算完成。
 for (int i = 0; i < 10; i++) {
   
     Future<String> submit = executorService.submit(new MyCallable());
     System.out.println(submit.get().toString());
 }
 // 关闭线程池
 executorService.shutdown();

5、通过线程池创建线程

避免使用Executors创建线程池主要是为了避免其中的默认实现,可以改用ThreadPoolExecutor构造方法指定参数即可。

需要指定核心线程池的大小、最大线程池的数量、保持存活的时间、等待队列容量的大小。在这种情况下一旦提交的线程数超过当前可用的线程数时就会抛出拒绝执行的异常java.util.concurrent.RejectedExecutionException 有界队列已经满了便无法处理新的任务。

上述代码中Executors类,提供了一系列工厂方法用于创建线程池,返回的线程池都实现了ExecutorService接口。

创建固定数目线程的线程池。
public static ExecutorService newFixedThreadPool(int nThreads)
创建一个可缓存的线程池,调用execute 将重用以前构造的线程(如果线程可用)。如果现有线程没有可用的,则创建一个新线程并添加到池中。终止并从缓存中移除那些已有 60 秒钟未被使用的线程。
public static ExecutorService newCachedThreadPool()
创建一个单线程化的Executor。
public static ExecutorService newSingleThreadExecutor()
创建一个支持定时及周期性的任务执行的线程池,多数情况下可用来替代Timer类。
public static ScheduledExecutorService newScheduledThreadPool(int corePoolSize)

 long start = System.currentTimeMillis();
 ThreadPoolExecutor threadPoolExecutor = new ThreadPoolExecutor(3, 10, 30, TimeUnit.SECONDS, new ArrayBlockingQueue<>(500));
 for (int i = 0; i < 100; i++) {
   
     threadPoolExecutor.execute(() -> {
   
         try {
   
             Thread.sleep(500);
         } catch (InterruptedException e) {
   
             e.printStackTrace();
         }
         System.out.println(Thread.currentThread().getName() + ": 自定义线程池执行任务");
     });
 }
 // 关闭线程池 - 执行后停止接受新任务,会把队列的任务执行完毕。
 threadPoolExecutor.shutdown();
 // 关闭线程池 - 也是停止接受新任务,但会中断所有的任务,将线程池状态变为 stop。
 //threadPoolExecutor.shutdownNow();
 // 会每隔一秒钟检查一次是否执行完毕(状态为 TERMINATED),当从 while 循环退出时就表明线程池已经完全终止了。
 while (!threadPoolExecutor.awaitTermination(1, TimeUnit.SECONDS)) {
   
     LOGGER.info("线程还在执行。。。");
 }
 long end = System.currentTimeMillis();
 LOGGER.info("一共处理了【{}】", (end - start));

使用工具类来创建线程池:

除了自己定义的ThreadPool之外,还可以使用开源库apache guava等。

个人推荐使用guavaThreadFactoryBuilder() 来创建线程池:

 /**
  * ThreadFactory 为线程池创建的线程命名
  */
 private static ThreadFactory threadFactory = new ThreadFactoryBuilder().setNameFormat("demo-pool-%d").build();

 public static void main(String[] args) 
  • 2
    点赞
  • 6
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值