创建线程的几种方式1️⃣ 继承 Thread 类
继承 Thread 类创建线程的步骤为:
1)创建一个类继承Thread类,重写run()方法,将所要完成的任务代码写进run()方法中;
2)创建Thread类的子类的对象;
3)调用该对象的start()方法,该start()方法表示先开启线程,然后调用run()方法;
@Slf4jpublic class ExtendsThread { static class T extends Thread { @Override public void run() { log.debug("hello"); } } public static void main(String[] args) { T t = new T(); t.setName("t1"); t.start(); }}
也可以直接使用Thread 类创建线程:
public static void main(String[] args) { Thread t1 = new Thread(new Runnable() { @Override public void run() { log.debug("hello"); } }, "t1"); }
看看 Thread 类的构造器,Thread 类有多个构造器来创建需要的线程对象:
// Thread.javapublic Thread() {}public Thread(Runnable target) {}public Thread(String name) {}public Thread(Runnable target, String name) {}// 还有几个使用线程组创建线程的构造器,就不列举了
2️⃣ Runnable 接口配合 Thread
实现 Runnable 接口创建线程的步骤为:
1)创建一个类并实现 Runnable 接口;
2)重写 run() 方法,将所要完成的任务代码写进 run() 方法中;
3)创建实现 Runnable 接口的类的对象,将该对象当做 Thread 类的构造方法中的参数传进去;
4)使用 Thread 类的构造方法创建一个对象,并调用 start() 方法即可运行该线程;
@Slf4jpublic class ImplRunnable { static class T implements Runnable { @Override public void run() { log.debug("hello"); } } public static void main(String[] args) { Thread t1 = new Thread(new T(), "t1"); t1.start(); }}
也可以写成这样:
public static void main(String[] args) { Runnable task = new Runnable() { @Override public void run() { log.debug("hello"); } }; Thread t2 = new Thread(task, "t2"); t2.start(); }
Java 8 以后可以使用 lambda 精简代码(IDEA会有提示可将匿名内部类换成 Lambda 表达式):
public static void main(String[] args) { Runnable task = () -> log.debug("hello"); Thread t2 = new Thread(task, "t2"); t2.start(); }
查看一下 Runnable 接口的源码,可以看到 Runnable 接口中只有一个抽象方法 run(),这种只有一个抽象方法的接口会加上一个注解:@FunctionalInterface
,那只要带有这个注解的接口就可以被Lambda表达式来简化。
@FunctionalInterfacepublic interface Runnable { public abstract void run();}
分析一下源码:
public static void main(String[] args) { Runnable task = () -> log.debug("hello"); Thread t2 = new Thread(task, "t2"); t2.start();}?;public Thread(Runnable target, String name) {// 调用Thread的指定构造器 init(null, target, name, 0);}?;private void init(ThreadGroup g, Runnable target, String name, long stackSize) { init(g, target, name, stackSize, null, true);}?;private void init(ThreadGroup g, Runnable target, String name, long stackSize, AccessControlContext acc, boolean inheritThreadLocals) { if (name == null) { throw new NullPointerException("name cannot be null"); } this.name = name; ....this.target = target;// target是Thread类中的成员变量,private Runnable target ....}?;@Overridepublic void run() { if (target != null) { target.run(); }}
Thread类是实现了Runnable接口的,那Thread类中重写了Runnable接口的run()方法,在创建Thread类的对象时,如果传进来的形参target不为空,那他就会去执行target的run(),也就是我们创建实现Runnable的实现类时重写的run()方法。
3️⃣ FutureTask 配合 Thread
这种通过实现 Callable 接口来实现的,步骤为:
1)创建一个类并实现 Callable 接口;
2)重写call()方法,将所要完成的任务的代码写进call()方法中,需要注意的是call()方法有返回值,并且可以抛出异常;
3)如果想要获取运行该线程后的返回值,需要创建 Future 接口的实现类的对象,即 FutureTask 类的对象,调用该对象的 get() 方法可获取call() 方法的返回值;
4)使用 Thread 类的有参构造器创建对象,将 FutureTask 类的对象当做参数传进去,然后调用 start() 方法开启并运行该线程;
@Slf4jpublic class ImplCallable { static class MT implements Callable { @Override public String call() throws Exception { return Thread.currentThread().getName() + " finished!"; } } public static void main(String[] args) throws ExecutionException, InterruptedException { // 创建 FutureTask 的对象 FutureTask task = new FutureTask(new MT()); // 创建 Thread 类的对象 Thread t1 = new Thread(task, "t1"); t1.start(); // 获取 call() 方法的返回值 String ret = task.get(); log.debug(ret); }}// --------------运行结果:00:33:29.628 [main] DEBUG cn.peng.create.ImplCallable - t1 finished!
FutureTask 能够接收 Callable 类型的参数,用来处理线程执行结束,有返回结果的情况
上面代码同样可以使用 Lambda 表达式简写:
public static void main(String[] args) throws ExecutionException, InterruptedException {// 匿名内部类方式 /* FutureTask task = new FutureTask(new Callable() { @Override public Integer call() throws Exception { log.debug("call() running...."); return 5 + 3; } }); */ // Lambda 简写 FutureTask task = new FutureTask(() -> { log.debug("call() running...."); return 5 + 3; }); new Thread(task, "t2").start(); Integer ret = task.get(); log.debug("ret: {}", ret); }// -------运行结果00:41:45.300 [main] DEBUG cn.peng.create.ImplCallable - ret: 8
看下源码,Callable 接口中也只有一个抽象方法 call(),并且call()方法支持返回值,含有注解:@FunctionalInterface
,可以被Lambda表达式简化。
@FunctionalInterfacepublic interface Callable { V call() throws Exception;}
FutureTask 类实现了 RunnableFuture
接口,而 RunnableFuture
接口又同时多继承了 Runnable, Future
接口,因此可以作为Thread 类的target。
public class FutureTask implements RunnableFuture {...}public interface RunnableFuture extends Runnable, Future { void run();}
三种方式对比
采用实现Runnable、Callable接口的方式创建线程时,
- 线程类只是实现了Runnable接口或Callable接口,还可以继承其他类;
- 在这种方式下,多个线程可以共享同一个target对象,非常适合多个相同线程来处理同一份资源的情况;
- 实现 Callable 接口执行 call() 方法有返回值,而实现Runnable 接口执行 run() 方法无返回值;
- 编程稍微复杂,如果要访问当前线程,则必须使用
Thread.currentThread()
方法;
而使用继承 Thread 类的方式创建多线程时,
- 线程类已经继承了Thread类,所以不能再继承其他父类(Java单继承);
- 编写简单,如果需要访问当前线程,则无需使用
Thread.currentThread()
方法,直接使用this
即可获得当前线程;
4️⃣ 线程池
查看下ThreadPoolExecutor
类中参数最全的构造方法,共有七大参数;
public ThreadPoolExecutor(int corePoolSize, int maximumPoolSize, long keepAliveTime, TimeUnit unit, BlockingQueue workQueue, ThreadFactory threadFactory, RejectedExecutionHandler handler) {...}
corePoolSize 核心线程数目 (最多保留的线程数)
maximumPoolSize 最大线程数目
keepAliveTime 生存时间 – 针对救急线程
unit 时间单位 – 针对救急线程
workQueue 阻塞队列
threadFactory 线程工厂 – 可以为线程创建时起个好名字
handler 拒绝策略
根据这个构造方法,JDK Executors
类中提供了众多工厂方法来创建各种用途的线程池。
newFixedThreadPool
public static ExecutorService newFixedThreadPool(int nThreads) { return new ThreadPoolExecutor(nThreads, nThreads, 0L, TimeUnit.MILLISECONDS, new LinkedBlockingQueue());}// 这种通过 ThreadFactory 指定线程名称public static ExecutorService newFixedThreadPool(int nThreads, ThreadFactory threadFactory) { return new ThreadPoolExecutor(nThreads, nThreads, 0L, TimeUnit.MILLISECONDS, new LinkedBlockingQueue(), threadFactory);}
创建一个固定大小的线程池,通过
Executors.newFixedThreadPool(线程个数)
即可创建。线程池特点:
- 核心线程数 == 最大线程数(没有救急线程被创建),因此也无需超时时间,时间被设置成了0;
- 阻塞队列
LinkedBlockingQueue
是无界的,可以放任意数量的任务;适用场景:
这种线程池适用于任务量已知,相对耗时的任务;
newCachedThreadPool
public static ExecutorService newCachedThreadPool() { return new ThreadPoolExecutor(0, Integer.MAX_VALUE, 60L, TimeUnit.SECONDS, new SynchronousQueue());}// 这种通过 ThreadFactory 指定线程名称public static ExecutorService newCachedThreadPool(ThreadFactory threadFactory) { return new ThreadPoolExecutor(0, Integer.MAX_VALUE, 60L, TimeUnit.SECONDS, new SynchronousQueue(), threadFactory);}
创建一个带缓冲功能的线程池,通过
Executors.newCachedThreadPool(线程个数)
即可创建。线程池特点:
- 核心线程为0,救急线程为 int 最大值,该线程池创建出的线程全是救急线程;
- 每个救急线程空闲存活时间 60s;
- 队列采用了
SynchronousQueue
,实现特点是,它没有容量,没有线程来取是放不进去的(一手交钱、一手交货);适用场景:
线程池表现为线程数会根据任务量不断增长,没有上限;当任务执行完毕,空闲 1分钟后释放线程。适合任务数比较密集,但每个任务执行时间较短的情况。
newSingleThreadExecutor
public static ExecutorService newSingleThreadExecutor() { return new FinalizableDelegatedExecutorService (new ThreadPoolExecutor(1, 1, 0L, TimeUnit.MILLISECONDS, new LinkedBlockingQueue()));}public static ExecutorService newSingleThreadExecutor(ThreadFactory threadFactory) { return new FinalizableDelegatedExecutorService (new ThreadPoolExecutor(1, 1, 0L, TimeUnit.MILLISECONDS, new LinkedBlockingQueue(), threadFactory));}
创建一个单线程的线程池,通过
Executors.newSingleThreadExecutor()
即可创建。线程池特点:
- 只有1个核心线程,没有救急线程,所以救急线程空闲存活时间为0;
- 阻塞队列
LinkedBlockingQueue
是无界的,可以放任意数量的任务;- 任务执行完毕,这唯一的线程也不会被释放。
适用场景:
希望多个任务排队串行执行。(线程数固定为 1,任务数多于 1 时,会放入无界队列排队)
看看面试题
Q:单线程的线程池和自己创建一个线程执行任务上的区别?
A:自己创建一个单线程(如实现Runnable接口)串行执行任务,如果任务执行失败导致线程终止,那么没有任何补救措施;而单线程的线程池
newSingleThreadExecutor
出现了这种情况时,还会新建一个线程,保证线程池的正常工作。
举个例子:
@Slf4jpublic class TestExecutors { public static void main(String[] args) { test(); } private static void test() { ExecutorService pool = Executors.newSingleThreadExecutor(); // 任务1 pool.execute(() -> { log.debug("1"); int i = 1 / 0; // 手动写异常 }); // 任务2 pool.execute(() -> { log.debug("2"); }); // 任务3 pool.execute(() -> { log.debug("3"); }); }}//-----运行结果:任务1中出现异常,但任务2和任务3都执行完了,任务执行完毕,这唯一的线程也不会被释放。13:47:26.448 [pool-1-thread-1] DEBUG cn.peng.threadpool.TestExecutors - 113:47:26.453 [pool-1-thread-2] DEBUG cn.peng.threadpool.TestExecutors - 213:47:26.453 [pool-1-thread-2] DEBUG cn.peng.threadpool.TestExecutors - 3Exception in thread "pool-1-thread-1" java.lang.ArithmeticException: / by zeroat cn.peng.threadpool.TestExecutors.lambda$test$0(TestExecutors.java:25)
Q:单线程的线程池
newSingleThreadExecutor
和创建固定数量为1的线程池newFixedThreadPool(1)
有什么区别?A:看两者的构造方法,
newFixedThreadPool
返回的是 ThreadPoolExecutor 对象,可以强转后调用 setCorePoolSize 等方法进行修改线程池的配置;- 而
newSingleThreadExecutor
在 ThreadPoolExecutor 的外层做了一个包装,FinalizableDelegatedExecutorService
应用的是装饰器模式,只对外暴露了 ExecutorService 接口,因此不能调用 ThreadPoolExecutor 中特有的方法,即不能修改线程池的配置。
来看看 Executor
、Executors
、ExecutorService
、ThreadPoolExecutor
这几个有什么联系。
Executor
是一个接口,内部只有一个抽象方法execute()
;void execute(Runnable command);
ExecutorService
也是一个接口,继承自Executor
,定义了一些操作线程池的抽象方法:void shutdown();// 执行任务List shutdownNow();boolean isShutdown();boolean isTerminated(); Future submit(Callable task);......
Executors
是一个工具类,内部提供了众多工厂方法来创建各种用途的线程池;ThreadPoolExecutor
是一个类,对于不同的业务可以自定义不同的线程池。上面三个官方自带的线程池都是利用ThreadPoolExecutor
来实现所需要的功能池。
newScheduledThreadPool
public static ScheduledExecutorService newScheduledThreadPool(int corePoolSize) { return new ScheduledThreadPoolExecutor(corePoolSize);}public static ScheduledExecutorService newScheduledThreadPool( int corePoolSize, ThreadFactory threadFactory) { return new ScheduledThreadPoolExecutor(corePoolSize, threadFactory);}
创建带有任务调度功能的线程池,线程池支持定时以及周期性执行任务。ScheduledThreadPoolExecutor
继承了ThreadPoolExecutor
,间接实现了 ExecutorService
接口,在ExecutorService
的基础上新增了一些方法,如 schedule()、scheduleAtFixedRate() 等。
public class ScheduledThreadPoolExecutor extends ThreadPoolExecutor implements ScheduledExecutorService { // 构造方法 public ScheduledThreadPoolExecutor(int corePoolSize) { super(corePoolSize, Integer.MAX_VALUE, 0, NANOSECONDS, new DelayedWorkQueue()); } public ScheduledFuture schedule(Runnable command, long delay, TimeUnit unit) {} public ScheduledFuture schedule(Callable callable, long delay, TimeUnit unit) {} // scheduleAtFixedRate(任务对象,延时时间,执行间隔,时间单位) // 表示线程第一次执行时,过了延时时间后再去处理参数一中的任务 public ScheduledFuture scheduleAtFixedRate(Runnable command, long initialDelay, long period, TimeUnit unit) {}
通过
Executors.newScheduledThreadPool(核心线程数)
来创建任务调度线程池;适用场景:
- 整个线程池表现为:线程数固定,任务数多于线程数时,会放入无界队列排队;
任务执行完毕,线程池中的线程也不会被释放,用来执行延迟或反复执行的任务。(只有核心线程、没有救急线程)
总结
前三种方法创建关闭频繁会消耗系统资源影响性能,而使用线程池可以不用线程的时候放回线程池,用的时候再从线程池取,项目开发中主要使用线程池。
前三种一般推荐采用实现接口的方式来创建线程。