Java | 一分钟掌握异步编程 | 3 - 线程异步

时间:2022-05-30 01:26:04

 作者:Mars酱

 声明:本文章由Mars酱编写,部分内容来源于网络,如有疑问请联系本人。

 转载:欢迎转载,转载前先请联系我!

前言

前两篇介绍了理论,这篇讲具体的实现方式。前言都是废话,直接上车~

简单粗暴

创建一个对象,继承Thread类,实现run函数,这个线程异步就做完了:

/**
 * @author mars酱
 */
public class OrderThread extends Thread {
    @Override
    public void run() {
        System.out.println("----------------------------");
        System.out.println("老板: 那小子已经下单了,开工做面!");
        System.out.println("----------------------------");
    }
}

主函数代码:

/**
 * @author mars酱
 */
public class NoodlesRestaurant {
    public static void main(String[] args) {
        System.out.println("你:老板,两碗面条。(潇洒扫一扫,6元巨款两碗面)");
        OrderThread order = new OrderThread();
        order.start();

        System.out.println("你:我俩等下吃碗面,就去看个电影,然后再那个###酒店吧");
        System.out.println("女友:你好坏坏");
    }
}

运行一下:

Java | 一分钟掌握异步编程 | 3 - 线程异步

线程异步就是这么简单粗暴,干净又卫生~

开门接客

如果来一对小情侣就创建一个对象,事情处理完还要销毁对象,这样反复创建、销毁还是很费体力的,我们遇到这种情况可以使用线程池处理,JDK里面的线程池是java.util.concurrent.ThreadPoolExecutor对象。

我们平常工作中呢一般这样写:

ExecutorService executor = Executors.newFixedThreadPool(3);

ExecutorService executor = Executors.newCachedThreadPool();

ExecutorService executor = Executors.newSingleThreadExecutor();

但是实际上呢,这三种方式最终都是通过java.util.concurrent.ThreadPoolExcutor来完成的,所以我一步到位了。

Executors 中创建三种线程:

newFixedThreadPool: 固定大小的线程池;

newCachedThreadPool: 线程数根据任务动态调整的线程池;

newSingleThreadExecutor: 单线程用的线程池;

看下java.util.concurrent.ThreadPoolExcutor的构造函数吧:

Java | 一分钟掌握异步编程 | 3 - 线程异步

构造函数的参数我给大家翻译翻译,体现一下我英语bia级水平:

corePoolSize: 池中存活的线程数,范围在0 ~ Integer.MAX_VALUE之间,假设现在设置为10,那么新任务来不足10个线程就会创建,否则超过10个线程就会根据情况销毁到只剩10为止;

maximumPoolSize: 最大允许的线程数,

keepAliveTime: 当线程数大于核心数时,keepAliveTime是多余的空闲线程在终止之前等待新任务的最长时间;

unit: 是修饰keepAliveTime的单位,时分秒那种修饰,具体可以看看TimeUnit里面的内容;

workQueue: 队列对象。newSingleThreadExecutor() 和 newFixedThreadPool() 使用的是LinkedBlockingQueue对象队列,newCachedThreadPool()使用的是SynchronousQueue队列,因为newCachedThreadPool()的需求是可以动态调整队列大小的,而SynchronousQueue队列它没有容量,放入一个元素之后只有等它被取出来才能再放入一个;

threadFactory: ThreadFactory对象是线程工厂,工厂就是来创建线程的;

handler: 拒绝策略。当到达线程边界和队列容量而被阻止执行时要使用的处理策略,这里可用的四种策略:

四种拒绝策略:

  1. CallerRunsPolicy:让提交任务的线程自己去执行这个任务,也就意味着这个线程会因此而阻塞;
  2. AbortPolicy:拒绝执行这个任务,并且在提交任务的线程中抛出 java.util.current.RejectedExecutionException异常;
  3. DiscardPolicy:同样拒绝执行这个任务,但是不抛出异常;
  4. DiscardOldestPolicy:从任务队列中把最早加入的任务丢弃,然后把当前任务加进任务队列;

介绍完了,写代码玩玩:

/**
 * @author mars酱
 */
public class NoodlesRestaurant {
    public static void main(String[] args) {
    	// 1. 创建一个线程池,活跃线程5, 最大线程10,等待时长5秒,使用LinkedBlockingQueue队列,拒绝策略使用CallerRunsPolicy
        ThreadPoolExecutor threadPoolExecutor = new ThreadPoolExecutor(5,
                10,
                5,
                TimeUnit.SECONDS,
                new LinkedBlockingQueue<>(),
                new ThreadPoolExecutor.CallerRunsPolicy());
        // 2. 接待一个用户的下单
        threadPoolExecutor.execute(new OrderThread());
    }
}

执行后的结果:

Java | 一分钟掌握异步编程 | 3 - 线程异步

很好,接1个客没问题,那么我们开始大量接客:

改造下单代码,增加桌号,以方便知道拿桌下单了:

public class OrderThread extends Thread {
    Integer deskNum;

    public OrderThread(Integer deskNum) {
        this.deskNum = deskNum;
    }

    @Override
    public void run() {
        System.out.println(">> 老板: " + deskNum + "号桌已下单,开工做面!");
    }

}

餐厅代码也改造一下:

/**
 * @author mars酱
 */
public class NoodlesRestaurant {
    public static void main(String[] args) {
//        System.out.println("你:老板,两碗面条。(潇洒扫一扫,6元巨款两碗面)");
//        OrderThread order = new OrderThread();
//        order.start();
//
//        System.out.println("你:我俩等下吃碗面,就去看个电影,然后再那个###酒店吧");
//        System.out.println("女友:你好坏坏");

        // 1. 创建一个线程池,活跃线程5, 最大线程10,等待时长5秒,使用LinkedBlockingQueue队列,拒绝策略使用CallerRunsPolicy
        ThreadPoolExecutor threadPoolExecutor = new ThreadPoolExecutor(5,
                10,
                5,
                TimeUnit.SECONDS,
                new LinkedBlockingQueue<>(),
                new ThreadPoolExecutor.CallerRunsPolicy());

        // 2. 模拟10桌客人轮流下单点餐
        for (int i = 0; i < 10; i++) {
            System.out.println(i + "桌 | 潇洒扫一扫~");
            threadPoolExecutor.execute(new OrderThread(i));
            System.out.println(i + "桌 | 点餐完成!");
        }
    }
}

运行结果:

Java | 一分钟掌握异步编程 | 3 - 线程异步

最后

废话很少,让你拥有充足的一分钟,连代码中互撩的环节也省去了。实际工作中,当应用结束的时候,有两个问题要考虑:

  1. 线程池队列当中未被处理的任务要如何处理?
  2. 正在执行的那些任务要如何处理?

我认为在设计上应该保证,队列中未被处理的任务是可以随时丢弃的。下次启动线程池后,这些未处理的任务可以重新再提交给线程池,业务上不受影响。另外,对于正在执行的任务,要保证在执行任务过程中,一旦进程被结束,那么要有应对的处理策略:

  1. 所有已经作出的状态变更都会回滚,下次执行这个任务又可以再开始。例如:使用数据库事务
  2. 这个任务可以从中间状态继续执行,比如:下载文件中的断点续传。

但不管怎么样,当进程结束时,还是希望应该尽可能等待正在处理的任务执行完成,以减少出错的可能性。ThreadPoolExecutor 提供这样一种方式:

// 告诉线程池不再接受新的任务,也不再处理队列中的任务 
executor.shutdownNow(); 

// 等待线程池中正在执行的任务都处理完毕,最多1小时 
executor.awaitTermination(1, TimeUnit.HOURS);

再最后

多线程异步虽然有缺陷,但是如果你不关心返回结果,而且任务之间相互没有依赖关系,那么多线程异步是个简单粗暴的可靠选择。