Java-多线程
基本概念
要了解多线程,需要先了解一下这些基本概念:
程序:由特定语言编写,完成某项功能的指令集合。
进程:程序的一次执行过程,或者是正在运行的程序;是一个动态的过程:有自身的产生、存在和消亡的过程。
线程:可以理解进程的执行单位,是操作系统能够进行运算调度的最小单位。它被包含在进程之中,是进程中的实际运作单位;
Java中,每个线程拥有独立的运行栈和程序计数器(pc)。
多线程:Java的main方法是一个线程,当你想main方法与别的方法同时执行时,就需要创建线程来执行对应的方法;
简而言之,多线程就是Java中多个线程共同运行,分别或共同完成特定的功能,充分利用CPU。
线程优先级:线程存在1-10的优先级。
线程的生命周期(几种状态):
新建 – 就绪 – 运行 – 阻塞 – 死亡
为什么需要多线程和多线程的优点
多线程在特定场景下可以提高系统的响应速度,完成一些高并发场景下的需求。
多线程的优点:
(1)提高应用程序的响应。图像化页面情况下可增强用户的体验;
(2)提高计算机系统CPU的利用率;
(3)改善程序结构;将既长又复杂的进程分为多个线程,独立运行,利于理解和修改。
Java创建多线程方式一:继承Thread类
- 创建一个继承于Thread类的子类
- 重写Thread类的run() –> 将需要此线程执行的操作声明在run()中
- 创建Thread类的子类的对象
- 通过此对象调用start()
1 | public class Demo { |
注意要调用start()方法后,该线程才算启动!
我们在程序里面调用了start()方法后,虚拟机会先为我们创建一个线程,然后等到这个线程第一次得到时间片时再调用run()方法。
注意不可多次调用start()方法。在第一次调用start()方法后,再次调用start()方法会抛出IllegalThreadStateException异常;因为线程的生命周期是个不可循环的过程,一个线程对象结束了不能再次start。
Java创建多线程方式二:实现Runnale接口
- 创建一个实现了Runnable接口的类
- 该实现类去实现Runnable中的抽象方法:run(),将需要此线程执行的操作声明在run()中
- 创建实现类的对象
- 将此对象作为参数传递到Thread类的构造器中,创建Thread类的对象
- 通过Thread类的对象调用start()方法
先来看下Runnable接口:
1 | @FunctionalInterface |
可以看到Runnable是一个函数式接口,这意味着我们可以使用Java 8的函数式编程来简化代码。
示例代码:
1 | public class Demo { |
Java创建多线程方式三:实现Callable接口
- 创建一个实现了Callable接口的MyCallable类
- MyCallable实现类去实现Callable中的抽象方法:call(),将需要此线程执行的操作声明在call()中,并且有返回值
- 创建MyCallable实现类的对象;
- 使用FutureTask类来包装Callable对象,该FutureTask对象封装了该Callable对象的call()方法的返回值。
- 将FutureTask对象作为参数传递到Thread类的构造器中,然后直接调用start()方法
- 可以通过futureTask.get()方法,获取线程的返回值
示例代码:
1 | public class CallableTest { |
输出结果如下:
1 | 0 |
Java创建多线程方式四:使用线程池(重要、推荐使用)
Java创建线程池总体上可以分为2类:
(1)一类是通过Executors创建的线程池;
(2)另一类是通过ThreadPoolExecutor创建的线程池。
ThreadPoolExecutor虽然是最原始的创建线程池的方式,它包含了 7 个参数可供设置,但是也是推荐使用的。
在阿里巴巴开发手册中,明确强制线程池不允许使用 Executors 去创建,而是通过 ThreadPoolExecutor 的方式,因为这样的处理方式可以让大家更加明确线程池的运行规则,规避资源耗尽的风险。
说明:Executors 返回的线程池对象的弊端如下:
(1) FixedThreadPool 和 SingleThreadPool:
允许的请求队列长度为 Integer.MAX_VALUE,可能会堆积大量的请求,从而导致 OOM。
(2) CachedThreadPool:
允许的创建线程数量为 Integer.MAX_VALUE,可能会创建大量的线程,从而导致 OOM。
那么怎么通过ThreadPoolExecutor去创建线程池呢?有人说直接创建对应的对象即可,那么创建对象的时候需要传什么参数进去呢?这些参数是什么意思呢?下面我们先来看看ThreadPoolExecutor最全的一个构造器的源码:
1 | public ThreadPoolExecutor(int corePoolSize, |
可以看到,ThreadPoolExecutor构造器的参数长达7个之多,我们需要完全了解这些参数的意义,才可以创建好自己想要的线程池:
corePoolSize:the number of threads to keep in the pool, even if they are idle, unless {@code allowCoreThreadTimeOut} is set (核心线程数大小:不管它们创建以后是不是空闲的。线程池需要保持 corePoolSize 数量的线程,除非设置了 allowCoreThreadTimeOut;就是线程池中始终存活的线程数)
maximumPoolSize:the maximum number of threads to allow in the pool。(最大线程数:线程池中最多允许创建 maximumPoolSize 个线程。)
keepAliveTime:when the number of threads is greater than the core, this is the maximum time that excess idle threads will wait for new tasks before terminating。(存活时间:如果经过 keepAliveTime 时间后,超过核心线程数的线程还没有接受到新的任务,那就回收。)
unit:the time unit for the {@code keepAliveTime} argument (keepAliveTime 的时间单位。)
workQueue:the queue to use for holding tasks before they are executed. This queue will hold only the {@code Runnable} tasks submitted by the {@code execute} method。(存放待执行任务的队列:当提交的任务数超过核心线程数大小后,再提交的任务就存放在这里。它仅仅用来存放被 execute 方法提交的 Runnable 任务。所以这里就不要翻译为工作队列了。)
threadFactory:the factory to use when the executor creates a new thread。(线程工程:用来创建线程工厂。比如这里面可以自定义线程名称,当进行虚拟机栈分析时,看着名字就知道这个线程是哪里来的。)
handler :the handler to use when execution is blocked because the thread bounds and queue capacities are reached。(拒绝策略:当队列里面放满了任务、最大线程数的线程都在工作时,这时继续提交的任务线程池就处理不了,应该执行怎么样的拒绝策略。)
workQueue包含以下 7 种类型:
ArrayBlockingQueue:
一个由数组结构组成的有界阻塞队列。LinkedBlockingQueue:
一个由链表结构组成的有界阻塞队列。SynchronousQueue:
一个不存储元素的阻塞队列,即直接提交给线程不保持它们。PriorityBlockingQueue:
一个支持优先级排序的无界阻塞队列。DelayQueue:
一个使用优先级队列实现的无界阻塞队列,只有在延迟期满时才能从中提取元素。LinkedTransferQueue:
一个由链表结构组成的无界阻塞队列。与SynchronousQueue类似,还含有非阻塞方法。LinkedBlockingDeque:
一个由链表结构组成的双向阻塞队列。
较常用的是 LinkedBlockingQueue
和 Synchronous
,线程池的排队策略与 BlockingQueue
有关。
handler拒绝策略,拒绝处理任务时的策略,系统提供了 4 种可选:
AbortPolicy:
拒绝并抛出异常。CallerRunsPolicy:
使用当前调用的线程来执行此任务。DiscardOldestPolicy:
抛弃队列头部(最旧)的一个任务,并执行当前任务。DiscardPolicy:
忽略并抛弃当前任务。
默认策略为 AbortPolicy
。
ThreadPoolExecutor线程池的参数我们都了解完了,是不是有点迷糊呢?那么我们来通过图例了解一下ThreadPoolExecutor线程池的执行流程,加深对参数、在实际使用中的理解:
当线程数小于核心线程数时,添加工作线程并执行。
当线程数大于等于核心线程数,且任务队列未满时,将任务放入任务队列。
当线程数大于等于核心线程数,且任务队列已满:
若线程数小于最大线程数,创建线程;
若线程数等于最大线程数,默认情况下抛出异常,拒绝任务。
下面来演示一下ThreadPoolExecutor线程池代码:
1 | public static void main(String[] args) { |
输出结果部分如下:
1 | ... |
Thread类的几个常用方法
currentThread():静态方法,返回对当前正在执行的线程对象的引用;
start():开始执行线程的方法,java虚拟机会调用线程内的run()方法;
yield():yield在英语里有放弃的意思,同样,这里的yield()指的是当前线程愿意让出对当前处理器的占用。这里需要注意的是,就算当前线程调用了yield()方法,程序在调度的时候,也还有可能继续运行这个线程的;
sleep():静态方法,使当前线程睡眠一段时间;
join():使当前线程等待另一个线程执行完毕之后再继续执行,内部调用的是Object类的wait方法实现的;
run():通常需要重写thread类中的此方法,将创建的线程要执行的操作写在此方法中;
getName():获取线程名字;
setName():设置线程名字;
stop():已过时;强制结束线程的生命周期;
isAlive():判断当前线程是否存活。
线程的生命周期
Thread类和Runnable接口方式的比较
对于这两种方法,在开发中优先选择:实现Runnable接口的方式;
原因:
- Runnable接口实现的方式没有类的单继承性的限制;
- Runnable接口实现的方式更适合来处理多个线程由共享数据的情况(继承方式的话共享数据需要声明为static,有时候还需要额外的处理)。
所以,我们通常优先使用“实现Runnable接口”这种方式来自定义线程类。
Runnable 和 Callable
Runnable
自 Java 1.0 以来一直存在,但Callable
仅在 Java 1.5 中引入,目的就是为了来处理Runnable
不支持的用例。**Runnable
接口**不会返回结果或抛出检查异常,但是 Callable
接口可以。所以,如果任务不需要返回结果或抛出异常推荐使用 Runnable
接口,这样代码看起来会更加简洁。
execute 和 submit
execute()
方法用于提交不需要返回值的任务,所以无法判断任务是否被线程池执行成功与否;submit()
方法用于提交需要返回值的任务。线程池会返回一个Future
类型的对象,通过这个Future
对象可以判断任务是否执行成功,并且可以通过Future
的get()
方法来获取返回值,get()
方法会阻塞当前线程直到任务完成,而使用get(long timeout,TimeUnit unit)
方法的话,如果在timeout
时间内任务还没有执行完,就会抛出java.util.concurrent.TimeoutException
。