线程与线程池
文章目录
为什么要使用线程
不会影响主线程,多个任务“同时”进行。
因为计算机运算速度很快,而且采用的时间片技术和各种调度算法,可以保证每个程序都能得到执行,时延非常短,我们是感觉不出来的。比如几个程序在一秒钟之内就轮换执行了200多次,我们只会以为每一个程序都是没有被中断过的,是单独一直在执行的。
线程
线程的状态
1、新建
2、就绪 start()
3、运行 run()
4、阻塞 调用sleep()或yield();wait();该线程与另一线程join()在一起;
5、消亡
线程创建三种方式
- 继承Thread类
- 实现Runnable方法(常用) (没有返回值)
- 实现callable接口 (有返回值和异常)
线程池
在一个应用程序中,我们需要多次使用线程,也就意味着,我们需要多次创建并销毁线程。而创建并销毁线程的过程势必会消耗内存。而在Java中,内存资源是及其宝贵的,所以,我们就提出了线程池的概念。
创建线程池?
Java中已经提供了创建线程池的一个类:Executor
而我们创建时,一般使用它的子类:ThreadPoolExecutor
Executor (接口) 跟 Execotors(类) 区别
Executor 是接口,能执行我们的线程任务,这个接口
只有一个execute()方法
Execotors 工具类,工具类中的不同方法按照我们的需求创建不同的线程池
注意:Executors只是一个工具类,它所有的返回方法,都是ThreadPoolExecutor 或者 ScheduledThreadPoolExecutor两个类的实例!
ThreadPoolExecutor创建线程池–核心参数
public class ThreadPoolExecutorDemo {
private static final int CORE_POOL_SIZE = 5; //核心线程数线程数定义了最小可以同时运行的线程数量。
private static final int MAX_POOL_SIZE = 10; // 当队列中存放的任务达到队列容量的时候,当前可以同时运行的线程数量变为最大线程数。
private static final int QUEUE_CAPACITY = 100;// 当新任务来的时候会先判断当前运行的线程数量是否达到核心线程数,如果达到的话,新任务就会被存放在队列中。
private static final Long KEEP_ALIVE_TIME = 1L; // 当线程池中的线程数量大于 corePoolSize 的时候,如果这时没有新的任务提交,核心线程外的线程不会立即销毁,而是会等待,直到等待的时间超过了 keepAliveTime才会被回收销毁。
public static void main(String[] args) {
//通过ThreadPoolExecutor构造函数自定义参数创建
ThreadPoolExecutor executor = new ThreadPoolExecutor(
CORE_POOL_SIZE,
MAX_POOL_SIZE,
KEEP_ALIVE_TIME,
// 参考的时间单位
TimeUnit.SECONDS,
// 当新任务来的时候会先判断当前运行的线程数量是否达到核心线程数,如果达到的话,新任务就会被存放在队列中。
// 等待队列为 ArrayBlockingQueue,并且容量为 100;
new ArrayBlockingQueue<>(QUEUE_CAPACITY),
// 饱和策略
new ThreadPoolExecutor.CallerRunsPolicy());
// 提交任务到线程池中
// 我们模拟了10个任务,我们配置的核心线程数为 5 、等待队列容量为 100 ,所以每次只可能存在 5 个任务同时执行,
// 剩下的 5 个任务会被放到等待队列中去。当前的5个任务中如果有任务被执行完了,线程池就会去拿新的任务执行。
for (int i = 0; i < 10; i++) {
executor.execute(new RunnableImpl());
}
executor.shutdown();// 终止线程池
while (!executor.isTerminated()) {
}
System.out.println("Finished all threads");
}
}
线程池执行流程图
ExecutorService接口
该接口继承了Executor接口并进行了扩展,提供了更多方法,我们能获得任务执行的状态并且可以获取任务的返回值。
ExecutorService的实现类有两个
1. ThreadPoolExecutor
2. ScheduledThreadPoolExecutor
ExecutorService还继承了Executor接口,注意区分Executor接口和Executors工厂类,Executor接口只有一个execute()
方法
继承树
ScheduledThreadPoolExecutor
该接口是ExecutorService的另一个实现类,该类直接实现ScheduleExecutorService接口,该类的主要体现在这个接口上,最主要的功能就是可以对其中的任务进行调度,比如延迟执行、定时执行等等。
四种常见线程池
1、newFixedThreadPool
创建一个固定大小的线程池,因为采用*的阻塞队列,所以实际线程数量永远不会变化,适用于负载较重的场景,对当前线程数量进行限制。(保证线程数可控,不会造成线程过多,导致系统负载更为严重)
2、newCachedThreadPool
用来创建一个可以无限扩大的线程池,适用于负载较轻的场景,执行短期异步任务。(可以使得任务快速得到执行,因为任务时间执行短,可以很快结束,也不会造成cpu过度切换)
3、newScheduledThreadPool
适用于执行延时或者周期性任务。
4、newSingleThreadExecutor
创建一个单线程的线程池,适用于需要保证顺序执行各个任务。
volatile关键字
- 防止指令重排序
并发原子类 AtomicInteger
并发机制的三个特性:原子性、可见性、有序性;synchronized关键字可以保证可见性和有序性却无法保证原子性,而这个AtomicInteger的作用就是为了保证原子性。
incrementAndGet(); // 自增操作
例子
public class Test { static volatile int a = 0; public static void main(String[] args) { Test test = new Test(); Thread[] threads = new Thread[5]; for (int i = 0; i < 5; i++) { threads[i] = new Thread(()->{ try{ for (int j = 0; j < 10; j++) { System.out.println(a++); Thread.sleep(500); } }catch(Exception e){ } }); threads[i].start(); } } }
我们用了5个线程分别对a进行++操作,为保证有序性,使用volatile来修饰a,5个线程,每个线程加10,结果应该是50,测试一下。
... 36 35 35 Process finished with exit code 0
结果显示会出现相同的值,这是因为变量a虽然保证了可见性和有序性,但是缺没有保证原子性。
对于a++的操作,其实可以分解为3个步骤
- 从主存中读取a的值
- 对a进行加1操作
- 把a重新刷新到主存
这三个步骤在单线程中一点问题都没有,但是到了多线程就出现了问题了。比如说有的线程已经把a进行了加1操作,但是还没来得及重新刷入到主存,其他的线程就重新读取了旧值。因为才造成了错误。
如何解决呢?
使用AtomicInteger来修饰变量a
使用了AtomicInteger来定义a,而且使用了AtomicInteger的函数incrementAndGet来对a进行自增操作
static AtomicInteger a = new AtomicInteger(); // 从0开始 ... System.out.println(a.incrementAndGet()); ...
**结果为50 ! **