并发编程之线程与线程池的工作原理
在日常开发工作中,并发编程的概念是互联网行业人人皆知的,但是它背后的辛秘又会有多少人全部掌握了呢?我知道我目前掌握的或许也有些遗漏,但是我想把自己认识以及认知的
线程和多线程
,想进一步总结下,进而查漏补缺。
文章目录
1、线程与进程的区别
- 本质区别
进程
是操作系统资源分配的基本单位;线程
是处理器任务调度和执行的基本单位。
- 包含关系
一个进程至少包含一个线程,而线程是进程的一部分。
- 资源开销
每个进程都有独立的资源空间,进程之间的切换会有较大的开销,同一进程内的线程共享进程的地址空间,每个线程都有自己独立的运行栈和程序计数器,线程之间的切换开销小。
- 影响关系
一个进程崩溃后,在保护模式下其他进程不会被影响;但是一个线程崩溃可能导致整个进程被操作系统杀掉,所以多进程比多线程健壮。
- 案例对比
进程 就 类似于 应用程序(微信、QQ、王者荣耀);而线程则是进程中的其中一个线程 类似于 微信中的语音或视频聊天 在聊天的同时还可以跟另一个人进行聊天。
2、线程的生命周期
状态 | 描述 |
---|---|
NEW | 初始化状态,线程被创建,但还没调用start()方法。 |
RUNNABLE | 可运行状态,可运行状态包括 运行中状态和就绪状态。 |
BLOCKED | 阻塞状态,处于这个状态的线程需要等待其他线程释放锁或者等待进入synchronized. |
WAITING | 等待状态,处于这个状态的线程需要等待其他线程进行通知或中断操作,进而进入下一个状态. |
TIME_WAITING | 超时等待状态,例如 执行Thread.sleep(s) ,可以在一定时间自行返回。 |
TERMINATED | 终止状态,当前线程执行完毕。 |
3、线程的3种创建方式
3.1 继承Thread类
- 代码案例
/**
* 1、继承Thread
*/
public static class OneThread extends Thread {
@Override
public void run() {
System.out.println(Thread.currentThread().getName() + ":" + this.getState());
try {
// 延迟5s 模拟业务逻辑执行
Thread.sleep(6000);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
System.out.println(Thread.currentThread().getName() + ":" + this.getState());
System.out.println("OneThread线程执行完毕..." + Thread.currentThread().getName());
}
}
- 运行验证
public static void main(String[] args) throws Exception {
System.out.println("当前线程:" + Thread.currentThread().getName());
// 创建线程
OneThread oneThread = new OneThread();
System.out.println(oneThread.getName() + ":" + oneThread.getState());
// 启动线程
oneThread.start();
}
3.2 实现Runnable接口
- 代码实现
/**
* 实现Runnable接口
*/
public static class TwoThread implements Runnable {
@Override
public void run() {
System.out.println(Thread.currentThread().getName() + ":" + Thread.currentThread().getState());
try {
// 延迟5s 模拟业务逻辑执行
Thread.sleep(5000);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
System.out.println(Thread.currentThread().getName() + ":" + Thread.currentThread().getState());
System.out.println("TwoThread线程执行完毕..." + Thread.currentThread().getName());
}
}
- 运行验证
public static void main(String[] args) throws Exception {
// TwoThread 为实现Runnable接口
TwoThread twoThread = new TwoThread();
// 将实现放入Thread执行
Thread thread = new Thread(twoThread);
thread.start();
}
3.3 实现Callable接口
- 代码实现
/**
* 实现Callable接口
*/
public static class ThreeThread implements Callable<Integer> {
@Override
public Integer call() throws Exception {
System.out.println(Thread.currentThread().getName() + ":" + Thread.currentThread().getState());
try {
// 延迟5s 模拟业务逻辑执行
Thread.sleep(7000);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
System.out.println(Thread.currentThread().getName() + ":" + Thread.currentThread().getState());
System.out.println("ThreeThread线程执行完毕..." + Thread.currentThread().getName());
return 100000;
}
}
- 运行实现
public static void main(String[] args) throws Exception {
// 创建Callable接口对应的线程
FutureTask<Integer> futureTask = new FutureTask<>(new ThreeThread());
Thread threeThread = new Thread(futureTask);
threeThread.start();
// 阻塞等待获取响应业务结果
Integer result = futureTask.get();
System.out.println(result);
}
4、手动创建线程的弊端
- 1、每次创建线程都需要通道NEW对象,并且调用start()方法
- 2、线程不能统一管理,频繁的创建和销毁会过多的占用系统资源,导致应用宕机或OOM问题。
- 3、其他(需要各位踊跃的补充哈!!!)。
此处可能会有朋友想到,跟之前数据库的连接/释放操作遇到的问题瓶颈是一样,于是就采用池化的思想进行优化,因此对于线程也是类似,同理~!
5、线程池好处
- 1、循环利用存在的线程,避免线程频繁的创建和销毁带来的开销。
- 2、可以控制最大并发数,提供系统资源的利用率。
- 3、可以对线程池的使用情况实时监控,方便调整优化,便面过多的占用系统资源。
6、线程池
6.1 创建线程池常用的方式
Executors.newFixedThreadPool(10); // 创建固定数量的线程池
Executors.newSingleThreadExecutor();// 创建单线程线程池
Executors.newCachedThreadPool();// 创建一个可缓存的线程池
Executors.newScheduledThreadPool(3);// 创建一个定时的线程池,支持周期性的执行任务
上述创建线程池的方式不推荐使用,并且阿里巴巴开发文档也是推荐开发者使用自定义线程池的形式进行创建。
阿里巴巴Java开发手册(黄山版)
6.2 ThreadPoolExecutor的七大核心参数
/**
* corePoolSize: 核心线程数,表示线程池启动后一直处于工作状态的线程数
* maximumPoolSize: 最大工作线程数,表示线程池最大工作线程池的最大线程数
* keepAliveTime: 存活时间,表示非核心线程在停止工作状态时,继续存活的时间
* unit: 时间单位
* workQueue: 待工作的阻塞队列,当待工作线程数大于核心线程且小于阻塞队列的大小时,新增的线程会添加至阻塞队列中
* threadFactory: 创建线程的工厂,自定义工厂
* handler: 拒绝执行异常处理器 ,最大线程数 = 最大工作线程数 + 阻塞队列的大小,如果大于最大线程数,此时会触发拒绝策略。
* 1、AbortPolicy: 总是会抛出异常RejectedExecutionException。
* 2、CallerRunsPolicy: 由调用者线程执行任务,除非执行器已经关闭,在这种情况下任务会被丢弃。
* 3、DiscardOldestPolicy: 丢弃最早未处理任务的请求
* 4、DiscardPolicy: 什么也不做
*/
public ThreadPoolExecutor(int corePoolSize,
int maximumPoolSize,
long keepAliveTime,
TimeUnit unit,
BlockingQueue<Runnable>workQueue,
ThreadFactory threadFactory,
RejectedExecutionHandler handler);
6.3 ThreadPoolExecutor的execute执行逻辑
public void execute(Runnable command) {
// 线程为空抛出NullPointerException异常
if (command == null)
throw new NullPointerException();
int c = ctl.get();
// 若工作线程数小于核心线程数
if (workerCountOf(c) < corePoolSize) {
// addWorker 如果为true 则表示线程创建核心线程成功
if (addWorker(command, true))
return;
c = ctl.get();
}
//(工作线程数 大于 核心线程数) , 若线程池处于运行状态 且 向任务队列添加成功时
if (isRunning(c) && workQueue.offer(command)) {
// 用于二次检查
int recheck = ctl.get();
// 若线程池处于非运行状态,且从remove移除线程command成功后,执行拒绝策略.
if (! isRunning(recheck) && remove(command))
reject(command);// 执行拒绝策略
// 若工作线程数等于0,则开始创建非核心线程
else if (workerCountOf(recheck) == 0)
addWorker(null, false);
}
// 任务队列已满,如果创建非核心线程失败时,则执行拒绝策略。
else if (!addWorker(command, false))
reject(command);//执行拒绝策略
}
7、小结
以上是对线程与线程池的理解进行的火力输出,相当于唤醒大家对线程与线程池知识的补充,而且我觉得上述只是概念方面的总结和理解比较片面,接下来呢,我会对此更近一步的知识挖掘与输出,今天先聊到这里,如果存在错误的地方还烦请各位小伙伴批评指正!!!