本文参考:https://blog.youkuaiyun.com/arjun_yu/article/details/112993354
java
多线程实现方式主要有三种:继承Thread
类、实现Runnable
接口、使用ExecutorService
、Callable
、Future
实现有返回结果的多线程。
其中前两种方式线程执行完后都没有返回值,只有最后一种是带返回值的。
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
等。
个人推荐使用guava
的ThreadFactoryBuilder()
来创建线程池:
/**
* ThreadFactory 为线程池创建的线程命名
*/
private static ThreadFactory threadFactory = new ThreadFactoryBuilder().setNameFormat("demo-pool-%d").build();
public static void main(String[] args) {
// 线程池创建 指定属性
ThreadPoolExecutor pool = new ThreadPoolExecutor(3, 5, 60, TimeUnit.SECONDS,
new ArrayBlockingQueue<Runnable>(100), threadFactory, new ThreadPoolExecutor.AbortPolicy());
for (int i = 0; i < 10; i++) {
pool.execute(() -> System.out.println("测试一下guava命名的线程:" + Thread.currentThread().getName()));
}
}
使用上面的方法创建线程池不仅可以避免
OOM
的问题,还可以自定义线程名称,更加方便出错时溯源。
6、定时任务
开发中,往往遇到另起线程执行其他代码的情况,用java
定时任务接口ScheduledExecutorService
来实现。
ScheduledExecutorService
是基于线程池设计的定时任务类,每个调度任务都会分配到线程池中的一个线程去执行,也就是说,任务是并发执行,互不影响。
注意,只有当调度任务来的时候,ScheduledExecutorService
才会真正启动一个线程,其余时间ScheduledExecutorService
都是处于轮询任务的状态。
// 创建一个支持定时及周期性的任务执行的线程池,多数情况下可用来替代Timer类。
ScheduledExecutorService scheduledExecutorService = Executors.newScheduledThreadPool(3);
// scheduleAtFixedRate() 每次执行时间为上一次任务开始起向后推一个时间间隔,是基于固定时间间隔进行任务调度
scheduledExecutorService.scheduleAtFixedRate(() -> {
System.out.println(Thread.currentThread().getName() + ": 定时执行任务!" + new Date());
}, 5, 10, TimeUnit.SECONDS);
// scheduleWithFixedDelay() 每次执行时间为上一次任务结束起向后推一个时间间隔,取决于每次任务执行的时间长短
scheduledExecutorService.scheduleWithFixedDelay(() -> {
System.out.println(Thread.currentThread().getName() + ": 定时执行任务!" + new Date());
}, 5, 10, TimeUnit.SECONDS);
// 只执行一次延时任务
ScheduledExecutorService scheduledThreadPoolExecutor = new ScheduledThreadPoolExecutor(1,
new BasicThreadFactory.Builder().namingPattern("example-schedule-pool-%d").daemon(true).build());
scheduledThreadPoolExecutor.schedule(() -> {
System.out.println(Thread.currentThread().getName() + ": 定时执行任务!");
}, 20, TimeUnit.SECONDS);
ScheduleAtFixedRate
每次执行时间为上一次任务开始起向后推一个时间间隔,即每次执行时间为initialDelay,initialDelay+period,initialDelay+2*period。。。。。
ScheduleWithFixedDelay
每次执行时间为上一次任务结束起向后推一个时间间隔,即每次执行时间为:initialDelay,initialDelay+executeTime+delay,initialDelay+2*executeTime+2*delay。。。。。
由此可见,
ScheduleAtFixedRate
是基于固定时间间隔进行任务调度,ScheduleWithFixedDelay
取决于每次任务执行的时间长短,是基于不固定时间间隔进行任务调度。