一个进程一般由三部分组成:程序段、数据段、进程控制块
一个标准的线程主要由三部分组成:线程描述信息、程序计数器、栈内存
创建线程的几种方式:
前两种的区别:前者是直接创建一个线程类;后者是创建的线程的target执行目标类,需要将其实例作为参数传入线程类的构造器,才能真正创建线程。
实现Runnable接口,不能直接访问Thread的实例方法,须通过Thread.currentThread()获取当前线程,然后才能访问当前线程。
共同之处:都有run方法,且都没有异步执行的结果返回,如果需要有异步执行结果返回,则可用第三种方式创建线程
实现Runnable的优点:
第三种创建方式:
calllable类似于Runnable,也是一个函数式接口,内部只有一个call() 方法,不同的是call方法有返回值;
callable与Thread的关联是通过RunnableFuture来桥接的,之前的Runnable可以作为Thread的target来执行,因为target是Runnable类型的;而callable与Runnable没有继承关系,所以不能作为target来执行,只能依赖于RunnableFuture;RunnableFuture继承了Runnable和Future,所以它可以当做target来执行。
Future接口的功能:
注:在同步线程中获取异步线程的执行结果时,如果call执行完成返回结果,则同步线程直接获取返回的结果,如果call还未执行完成,即outcome为空时,同步线程会阻塞,知道异步线程执行返回结果后再继续执行。
先了解下JUC工具包:
JUC(java.util.concurrent)于JDK1.5开始加入,是用于完成高并发、处理多线程的一个工具包。其线程池的类与接口架构图如下:
//创建只有一个线程的线程池
public class singleThreadPoolTest {
public static final int SLEEPTIME = 1000;
public static class TargetTask implements Runnable {
static AtomicInteger taskNo = new AtomicInteger(1);
private String taskName;
public TargetTask(){
taskName = "task-" + taskNo.get();
taskNo.incrementAndGet();
}
@Override
public void run() {
System.out.println("任务:" + taskName + "doing");
try {
Thread.sleep(SLEEPTIME);
System.out.println("任务:" + taskName + "执行结束");
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
public static void main(String[] args) {
ExecutorService pool = Executors.newSingleThreadExecutor();
for (int i = 0; i < 5; i++){
pool.execute(new TargetTask());
pool.submit(new TargetTask());
}
//关闭线程池
pool.shutdown();
}
}
如上四种通过Executors快捷创建线程池的方法可以快速创建线程池,但在生产场景中许多公司尤其大厂不允许使用快捷方式创建线程池,而是使用ThreadPoolExecutor构造器去创建线程池;其实Executors快捷创建线程池也是调用的ThreadPoolExecutor(定时任务调用的ScheduledThreadPoolExecutor)的构造方法完成的。
ThreadPoolExecutor构造方法有多个重载版本,其中比较重要的是:
public ThreadPoolExecutor(
int corePoolSize,
int maximumPoolSize,
long keepAliveTime, TimeUnit unit,
BlockingQueue<Runnable> workQueue,
ThreadFactory threadFactory,
RejectedExecutionHandler handler
)
参数说明:
1、核心和最大线程数
corePoolSize是核心线程数,maximumPoolSize是最大线程数
在线程池接收到新任务时,如果当前工作线程数小于核心线程数时,即使有空闲线程也会创建新线程来处理新任务,直到线程数达到核心线程数。
在线程接收到新任务时,如果当前工作线程数大于核心线程数,但小于最大线程数,则仅当任务队列满时才会创建新线程执行新任务,
2、keepAliveTime、TimeUnit 用于控制空闲线程存活时长
3、BlockingQueue 阻塞队列,线程池工作线程已满时,新任务户被存放在阻塞队列中等待
4、ThreadFactory 新线程的产生方式
5、最后一个参数是拒绝策略
拒绝策略包括如下:
- AbortPolicy 使用此策略,若线程池队列已满,新任务就会被拒绝,并抛出异常RejectedException,是线程池的默认拒绝策略
- DiscardPolicy 若队列已满,新任务就会被丢弃掉,且不会抛出异常
- DiscardOldestPolicy 抛弃最老策略,若队列已满,新任务进来时会抛弃老任务
- CallerRunsPolicy 如果新任务进入队列失败,则提交任务的线程会自己去执行该任务,不会再有线程池去调度处理
- 也可自定义拒绝策略,实现RejectedExecutionHandler接口的rejectedExecution方法即可
Java阻塞队列 BlockingQueue
与普通队列相比的特点:
在阻塞队列为空时,会阻塞当前线程的元素获取操作,直到阻塞队列中有元素为止,当阻塞队列中有了元素后,线程自动被唤醒,不用用户代码实现。
其实现类有:
创建示例:
ThreadPoolExecutor threadPoolExecutor = new ThreadPoolExecutor(
10,
10000,
10L,
TimeUnit.SECONDS,
new ArrayBlockingQueue<>(20),
Executors.defaultThreadFactory(),
new ThreadPoolExecutor.AbortPolicy()
);
创建完成后即可使用。
ThreadPoolExecutor 线程池调度器为每个任务执行前后都提供了钩子方法:
beforeExecute | afterExecute | terminated
默认不执行任何操作,需子类重写。
向线程池提交任务的两种方式及其区别:
execute、submit是向线程池提交任务的两种方式
区别是:
1、execute只能接收Runnable类型的参数,submit可以接收callable、Runnable两种类型的参数
2、submit提交任务后会有返回值,而execute没有(submit返回一个Future对象)
线程池的任务调度流程:
线程池的状态:
关闭线程池:
shutdown、shutdownNow、awaitTermination
awaitTermination(long timeout, TimeUnit unit)指定超时时间去关闭线程池,如果返回false,则可调用shutdownNow去关闭线程池。
示例:
/**
* 关闭线程池
* 先使用shutdown, 停止接收新任务并尝试完成所有已存在任务.
* 如果超时, 则调用shutdownNow, 取消在workQueue中Pending的任务,并中断所有阻塞函数.
* 如果仍超時,則強制退出.
* 另对在shutdown时线程本身被调用中断做了处理.
*/
public static void shutdownAndAwaitTermination(ExecutorService pool) {
if (pool != null && !pool.isShutdown()) {
pool.shutdown();
try {
if (!pool.awaitTermination(120, TimeUnit.SECONDS)) {
pool.shutdownNow();
if (!pool.awaitTermination(120, TimeUnit.SECONDS)) {
logger.info("Pool did not terminate");
}
}
}
catch (InterruptedException ie) {
pool.shutdownNow();
Thread.currentThread().interrupt();
}
}
}