概述
多线程在多核处理器上的并发能力往往比单线程程序更好,但是编写稳定可靠的多线程程序往往并非易事。所幸JDK提供了很多并发包,同时也有很多开箱即用第三方工具可供选择。作为开发人员,我们需要掌握一些多线程的核心知识。在这篇博客中我们主要了解多线程的基本概念和使用方法。
使用场景
当需要运行不希望阻塞主线程的任务时,我们可以考虑另起一个线程。比如Android开发中的消息发送,Web后端开发中的定时任务等。此外在Web请求中,每一次请求就对应一个线程。可以说多线程无处不在。
使用方法
使用方法很简单,我们可以直接通过new一个Thread对象,来运行我们的任务。
Java示例:
public static void main(String[] args) {
Runnable task1 = () -> {
System.out.println("hellotask1");
System.out.println(Thread.currentThread().getName());
};
Runnable task2 = () -> {
System.out.println("hellotask2");
System.out.println(Thread.currentThread().getName());
};
new Thread(task1, "thread-1").start();
new Thread(task2, "thread-2").start();
// hello task1 // thread-1 // hello task2 // thread-2
}
当然在实际项目中我们更推荐线程池的方式来创建和管理线程。我们采用阿里Java手册推荐的方式,即创建ThreadPoolExecutor对象来实现线程池。上面task1和task2是我们采用函数式编程自定义的Runnable类型的函数表达式,用来代表两个需要在其它线程中执行的任务,然后通过new一个Thread对象来指定实现Runnable接口的任务以及线程名字,最后调用start方法执行任务。
ThreadPoolExecutor构造函数的主要参数:
- corePoolSize 线程池核心线程数,也就是一旦创建了ThreadPoolExecutor,就会有corePoolSize个内驻线程。
- maximumPoolSize 线程池允许的最大线程数,表示当需要的线程超过了corePoolSize,且workQueue已满,线程池会新建线程,直到线程数达到了maximumPoolSize。
- keepAliveTime 表示超过了 corePoolSize的线程的最大空闲时长。如果超过线程的空闲时长超过keepAliveTime,就会被终结。
- unit (TimeUnit) 空闲时长的单位。
- workQueue(BlockingQueue<Runnable>) 被submit的任务的存放队列。
- threadFactory (ThreadFactory) 线程的生产工厂,我们可以在这里定义线程的创建过程。
- handler(RejectedExecutionHandler) 拒绝策略,比如workQueue放不下新的任务了,且没有空闲线程,就会触发拒绝策略。
当然我们也可以结合一张图来理解ThreadPoolExecutor线程池的工作方式
Java示例代码:
public class ThreadExample {
public static void main(String[] args) {
Runnable task1 = () -> {
System.out.println("hello task1");
System.out.println(Thread.currentThread().getName());
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
};
Runnable task2 = () -> {
System.out.println("hello task2");
System.out.println(Thread.currentThread().getName());
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
};
// 由于是LinkedBlockingQueue,工作队列永远不会满,所以maximumPoolSize应该设置的和corePoolSize一样大
ThreadPoolExecutor threadPoolExecutor = new ThreadPoolExecutor(
2,
2,
0,
TimeUnit.SECONDS,
new LinkedBlockingQueue<>(),
r -> new Thread(r, "my-pool-thread"));
threadPoolExecutor.submit(task1);
threadPoolExecutor.submit(task2);
threadPoolExecutor.submit(task1);
threadPoolExecutor.submit(task2);
threadPoolExecutor.submit(task1);
threadPoolExecutor.submit(task2);
threadPoolExecutor.submit(task1);
threadPoolExecutor.submit(task2);
// hello task1
// my-pool-thread
// hello task2
// my-pool-thread
// hello task1
// my-pool-thread
// hello task2
// my-pool-thread
// hello task1
// my-pool-thread
// hello task2
// my-pool-thread
// hello task1
// my-pool-thread
// hello task2
// my-pool-thread
}
}
注意点
上述代码使用了默认的拒绝策略(由于是链表阻塞任务队列,拒绝策略永远不会执行),并且自定义了线程创建工厂,为线程命名,方便日后追踪问题。从运行结果可以看出,每隔一秒会同时输出task1和task2的运行结果。结果顺序可能不一样。
一般情况下,能不用多线程就尽量不要使用,或者在使用的时候尽量使用Java提供的并发工具类,此外在使用线程池的时候,要根据业务需要合理设置corePoolSize,另外,线程池的shutdown方法只会拒绝新提交的任务,在任务队列的任务还是会正常消费。
结语
多线程的创建和使用很简单,但是一定要正确区分使用场景,本篇博客只是简单介绍了入门的使用方法,供大家参考。