线程
如何手动使用线程?
java只能通过Thread类来创建线程, 也只有这样这种方式. Thread类本身继承Runable接口, 规定了run()方法, run()方法也就是你的线程到底要做什么(实现什么功能).
Thread类只有两种实现方式
- (1) 创建Thread类的时候, 接受一个人实现了Runable接口的类作为参数
- (2) 直接创建一个类继承Thread类. 然后用该类的实例使用线程
也就是Thread是一个载体, 我必须把具体的任务提供给Thread, 他才有意义.
线程在执行完代码后会自行销毁
线程需要返回一个结果(本质属于方式1创建线程)
利用Callable接口, 需要执行的方法在call()方法中, 返回值利用 FutureTask 封装, 如果要获得返回值, 就从FutureTask对象里获取. 注意Callable接口只是规定了个call()方法而已, 实际上的异步是通过FutureTask的异步来完成的, 因为FutureTask继承了Runable接口
(1) 实现Callable接口, 实现call()方法
public class MyCallable implements Callable<Integer> {
@Override
public Integer call() {
return 123;
}
}
(2) 创建Thread类, 启动线程
MyCallable mc = new MyCallable();
FutureTask<Integer> ft = new FutureTask<>(mc);
Thread thread = new Thread(ft);
thread.start();
System.out.println(ft.get()); // 返回值从ft中获取
FutureTask
FutureTask就是一个异步任务的实现, FutureTask的实例提供了一些方法, 可以来控制异步任务. 异步任务的状态分为未启动和已启动. 已启动的任务又有三种状态正常结束, 取消而结束, 异常而结束. 这几个状态就对应于异步任务执行的不同阶段.
控制异步任务的方法:
- (1) isCancelled() 判断异步任务是否取消而结束
- (2) isDone() 判断任务是否正常结束
- (3) cancel() 主动取消这个异步任务, 让它取消而结束
- (4) get() 获取异步任务的执行结果, 如果异步任务有结果了, 该方法调用会立即返回, 但是如果异步任务还没结束, 就会阻塞调用get()方法的线程, 直到异步任务结束.
线程无需返回结果
实现Runable接口(方式1)
(1) 实现run()方法
public class MyRunnable implements Runnable {
@Override
public void run() {
// ...
}
}
(2) 创建Thread类
MyRunnable instance = new MyRunnable();
Thread thread = new Thread(instance);
thread.start();
接口的方式使用更方便扩展, 因为java可以多继承
继承Thread类(方式2)
(1) 继承Thread类
public class MyThread extends Thread {
@Override
public void run() {
// ...
}
}
(2) 无需再创建Thrad类, 直接通过类的实例使用线程
MyThread mt = new MyThread();
mt.start();
Executor (也就是线程池)
如果每次都手动创建Thread使用, 然后等线程执行完自行销毁, 那么当线程使用比较多的时候, 系统就会花费很大的代价.
因此自然就会想到线程执行完任务后, 并不销毁, 而是存放在池子里等待再次使用, 毕竟线程没有非要销毁的意义. 这种思想就是一个线程池的概念.
java 提供了Executor接口, ExecutorService接口, Exectors工具类.
ExecutorService继承了Executor接口, 这两个接口定义了很多线程池应该具有的方法.
你想用现成的线程池可以使用Exectors工具类创建线程池.
也可以直接创建ThreadPoolExecutor来创建一个自定义的线程池
线程池的创建
线程池的创建分为两种方式 (1) 通过Exectors工具类创建 (2) 自定义线程池
创建出来的线程池一般都是通过ExecutorService的引用来使用, 也就是会让接口的引用指向具体的实现类.
即ExecutorService executorService = xxxx实现类.
通过Exectors工具类创建线程池
Exectors创建的线程池也是通过自定义线程池中那两个类创建的, 只不过是帮你提前创建好适用于某些场景的线程池
Exectors创建的ThreadPoolExecutor类线程池
(1) FixedThreadPool
- 固定线程数目的线程池
- corePool: 自己设置
- maximumPool: 等同corePool
- BlockingQueue: LinkedBlockingQueue 无界队列
- keepAliveTime: 0
- 适用于需要严格限制服务器线程使用的线程池
- ExecutorService executor = Exectors.newFixedThreadPool()
(2) CachedThreadPool
- 可以无限扩大线程数目的线程池
- corePool: 0
- maximumPool: Integer.MAX_VALUE
- BlockingQueue: SynchronnousQueue 无界队列
- keepAliveTime: 60s
- 适用于短期异步任务频繁的系统
- ExecutorService executor = Exectors.newCachedThreadPool()
(3) SingleThreadExecutor
- 单个线程的线程池
- corePool: 1
- maximumPool: 1
- BlockingQueue: LinkedBlockingQueue 无界队列
- keepAliveTime: 0
- 适用于需要顺序执行任务的场景
- ExecutorService executor = Exectors.newSingleThreadExecutor()
Exectors创建的ScheduledThreadPoolExetor类线程池
(1) ScheduledThreadPool 可以创建多个周期性运行线程的线程池 适用于需要多个后台线程执行周期任务的场景 ExecutorService executor = Exectors.newScheduledThreadPool
自定义线程池
(1) 利用ThreadPoolExecutor类
- corePool: 核心线程池大小
- maximumPool: 最大线程池大小
- BlockingQueue: 任务阻塞队列(直接握手队列, 无界队列, 有界队列)
- keepAliveTime: 线程存活时间(通常用于杀死超过非核心线程数目的线程)
- 拒绝策略: 抛异常, 直接丢弃等
(2) 利用ScheduledThreadPoolExetor类
继承自ThreadPoolExecutor类, 拥有上面的基本设置
特别的是采用DelayQueue实现等待队列. 每次新建任务的时候, 需要添加一个实现了RunnableScheduleFutur接口的futureTask, 定时就是通过添加任务实现了定时接口, 才能够定时执行的.
线程池的关闭
如需自动关闭, 需要设置相关参数, 这样线程池会在没有线程, 或者没有使用线程的时候关闭(但是自动关闭了, 就不会自动开启了, 一般不用)
手动关闭 executorService.shutdown(); // 告诉线程池要关闭了, 正常会等待线程池中所有线程执行完毕才一一关闭
executorService.awaitTermination(30, TimeUnit.SECONDS); // 等待30s后, 为所有线程设置中断标志位(啥时候真正中断 不确定), 才真正关闭线程池
executorService.shutdownNow(); // 立即关闭所有线程(通过设置中断标志位, 具体的中断时间不确定)
线程池创建的线程不会随着你程序的停止还关闭, 必须要通过线程池进行关闭才是真正的关闭. 可以通过实现Closeable接口, 来实现自动关闭的功能.
线程池如何让核心线程不被销毁
正常的线程, 在执行完任务后, 不会尝试去获取新的任务, 而是自行销毁, 但是线程池中的线程, 在执行完一个任务后, 会尝试获取下一个任务(如果获取不到, 会阻塞住), 这样线程就不会被销毁了
常见问题
(1) 线程池中ExecutorService中execute()和submit()方法的区别?
execute(Runnable command)是执行不带返回值的线程任务的
Future<?> submit(Runnable task) 是执行带返回值的线程任务的
(2) 线程池的拒绝策略?
当线程池的任务缓存队列已满并且线程池中的线程数目达到maximumPoolSize时,如果还有任务到来就会采取任务拒绝策略.
通常有以下四种策略:
- ThreadPoolExecutor.AbortPolicy:丢弃任务并抛出RejectedExecutionException异常。
- ThreadPoolExecutor.DiscardPolicy:丢弃任务,但是不抛出异常。
- ThreadPoolExecutor.DiscardOldestPolicy:丢弃队列最前面的任务,然后重新提交被拒绝的任务
- ThreadPoolExecutor.CallerRunsPolicy:由调用线程(提交任务的线程)处理该任务
默认采用的是AbortPolicy, 即丢弃任务并抛出异常
如果想选择不同的拒绝策略, 在创建的时候, 在构造函数传入相应的RejectedExecutionHandler即可
Daemon守护线程
首先线程是依托于进程创建的, 在进程中创建出来的线程有两种, 一种是普通线程, 一种是守护线程, 守护线程有个特点, 就是当有普通线程(只要是所属进程中存在即可, 不用是创建守护线程的普通线程)存在的时候, 守护线程才能存活(当然守护线程正常执行完, 也就自己结束的). 一旦进程中不存在普通线程, 那么会杀死所有守护线程.
如何设置一个线程为守护线程
thread.setDaemon(true)
守护线程的特点
- (1) 你不能把正在运行的常规线程设置为守护线程。
- thread.setDaemon(true)必须在thread.start()之前设置,否则会抛出一个IllegalThreadStateException异常。
- (2) 在Daemon线程中产生的新线程也是Daemon的(默认情况)
sleep()线程休眠
Thread.sleep(millisec) 方法会休眠当前正在执行的线程,millisec 单位为毫秒。 线程会进入阻塞状态.
中断线程
线程正常情况是下, 是在线程执行完任务后自行结束, 但是某些时候, 我们会需要控制线程在未执行完的时候, 就进行中断, 然后终止线程(例如线程执行任务的时间超出预期)
如何优雅中断线程
thread.interrupt();
方法会将线程的中断标志位, 设置为ture, 这个方法只会这样, 然后剩下的就交给线程来决定什么时候中断
运行时中断
如果线程一直处于运行状态, 你修改了中断标志位, 线程也不会去尝试获取标志位的内容, 因此根本就中断不了. 除非你自己在线程的代码里, 不断获取中断状态来判断
阻塞式中断
如果一个线程处于阻塞状态, 那么分情况会自动响应中断标志位
(1) 线程处于I/O阻塞, 等待synchronized锁阻塞的情况 线程不会去响应中断标志位, 线程仍旧是继续阻塞
(2) 其它情况下的阻塞, 例如wait(), sleep()等进入的阻塞 线程在这种阻塞状态下, 一旦线程的中断标志位被修改, 会立马抛出异常, 中断当前线程.
线程协作
(1) join()方法
如果某个线程A持有另一个线程的引用B, 那么线程A在执行的时候, 调用B.join(), 线程A会进入阻塞状态, 直到线程B执行结束并销毁后(B异常结束也算是结束), A才会变成待运行状态
(2) wait() 和 notify()方法
两个线程如果在执行同步方法时(因为wait()方法只能在synchronized关键字锁住的代码块里才能使用), 可以通过这两个方法, 进行协作
(3) await() signal()方法
java.util.concurrent 类库中提供了 Condition 类来实现线程之间的协调,可以在 Condition 上调用 await() 方法使线程等待,其它线程调用 signal() 或 signalAll() 方法唤醒等待的线程。
需要搭配高级锁Lock使用, 等待和唤醒的含义和wait()和notify()一致, 只是相比于 wait() 这种等待方式,await() 可以指定等待的条件,因此更加灵活。
协程
协程是一种比线程更加轻量级的存在,正如一个进程可以拥有多个线程一样,一个线程可以拥有多个协程。 跟IO多路复用的想法差不多.
协程的特点
(1) 协程不是被操作系统内核所管理的,而是完全由程序所控制,也就是在用户态执行。这样带来的好处是性能大幅度的提升,因为不会像线程切换那样消耗资源。
(2)协程不是进程也不是线程,而是一个特殊的函数,这个函数可以在某个地方挂起,并且可以重新在挂起处外继续运行。 线程和进程的切换都是操作系统内核控制的, 而协程需要用户自己控制切换和使用.
(3) 一个线程内可以由多个这样的特殊函数在运行,但是有一点必须明确的是,一个线程的多个协程的运行是串行的。
如果是多核CPU,多个进程或一个进程内的多个线程是可以并行运行的,但是一个线程内协程却绝对是串行的,无论CPU有多少个核。毕竟协程虽然是一个特殊的函数,但仍然是一个函数。一个线程内可以运行多个函数,但这些函数都是串行运行的。当一个协程运行时,其它协程必须挂起。
协程如何知道切换?
协程的使用就跟你自己控制执行了一系列函数一样, 但是这个函数你可以执行了一半挂起, 去执行别的函数. 这样的好处就是当我某一个函数需要等待某个资源的时候, 我就记录一个事件(相当于AIO), 然后主线程就可以去继续执行别的协程.
当事件就绪后被触发, 重新执行之前的协程.