目录
一、程序,进程,线程
- 程序:为完成特定任务,用某种语言编写的一组指令的集合,指静态的代码
- 进程:程序的一次执行过程,或是正在运行的一个程序
- 线程:程序内部的执行路径,是操作系统能够进行运算调度的最小单位
一个进程可以由多个线程组成,至少得包含一个线程
当进程内的多个线程同时运行,这种运行方式称为并发运行
二、创建线程
有四种方式
1、继承Thread类
- 继承Thread类需要重写run()方法
- 直接调用start()方法来启动线程
public class PrintThread extends Thread{
@Override
public void run() {
//当前线程的名称
String threadName = Thread.currentThread().getName();
System.out.println("线程"+threadName+"开始计时:");
for(int i = 1;i<=5;i++) {
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("第"+i+"秒");
}
System.out.println("线程"+threadName+"结束计时");
}
}
public static void main(String[] args) {
PrintThread pt = new PrintThread();
pt.setName("《计时器》");
pt.start();
}
线程《计时器》开始计时:
第1秒
第2秒
第3秒
第4秒
第5秒
线程《计时器》结束计时
2、实现Runnable接口
- 实现Runnable接口
- 重写run方法
- 需要借助Thread类
- 将对象作为Thread构造方法的参数,调用start
public class PrintRunThread implements Runnable{
@Override
public void run() {
String threadName = Thread.currentThread().getName();
System.out.println("线程"+threadName+"开始计时:");
for(int i = 1;i<=5;i++) {
System.out.println("第"+i+"秒");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
System.out.println("线程"+threadName+"结束计时");
}
}
static void test() {
PrintRunThread pr = new PrintRunThread();
Thread t = new Thread(pr);
t.setName("《计时器2》");
t.start();
}
3、实现Callable接口
与使用runnable方式相比,callable功能更强大些:
- runnable重写的run方法不如callaalbe的call方法强大,call方法可以有返回值
- 方法可以抛出异常
- 支持泛型的返回值
注意事项:
- 实现Callable接口
- 重写call()方法
- 需要借助FutureTask类
- 对象作为Future构造方法的参数
- 还需要借助Thread类
- FutureTask类的对象作为Thread类构造方法的参数
- FutureTask对象.get()可以获取call方法的返回结果
public class PrintCallThread implements Callable{
@Override
public Object call() throws Exception {
int sum = 0;
String threadName = Thread.currentThread().getName();
System.out.println("线程"+threadName+"开始计数:");
for(int i = 1;i<=5;i++) {
System.out.println("加"+(i*10));
sum+=i*10;
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
System.out.println("线程"+threadName+"结束计时");
return sum;
}
}
void thread3() {
PrintCallThread pc = new PrintCallThread();
FutureTask ft = new FutureTask(pc);
Thread t = new Thread(ft);
t.setName("《求和计算器》");
t.start();
try {
System.out.println("最后结果为"+ft.get());//FutureTask对象.get()可以获取重写的call方法的返回值
} catch (InterruptedException e) {
e.printStackTrace();
} catch (ExecutionException e) {
e.printStackTrace();
}
}
输出结果:
线程《求和计算器》开始计数:
加10
加20
加30
加40
加50
线程《求和计算器》结束计时
最后结果为150
4、线程池
不建议单纯使用继承Thread或者实现Runnable接口的方式来创建线程,那样势必有创建及销毁线程耗费资源、线程上下文切换问题。同时创建过多的线程也可能引发资源耗尽的风险,这个时候引入线程池比较合理,方便线程任务的管理。所以线程池的优势就是不用反复创建和销毁线程,能提高响应速度,减少资源消耗。
两个重要的API:ExecutorService 和 Executors,用来创建线程池的
ExecutorService有个重要的子类ThreadPoolExecutor,也可以直接new ThreadPoolExecutor()来创建线程池,我们这里先只讲用Executors来创建
(1)Executors有四个创建线程池的静态方法:
newFixedThreadPool(线程个数); | 创建固定大小的线程池 |
newSingleThreadExecutor(); | 创建只有一个线程的线程池 |
newCachedThreadPool() | 创建一个不限线程数上限的线程池,任何提交的任务都将立即执行 |
newScheduledThreadPool() | 创建一个不限线程数上限的线程池,提交的任务将定时周期性执行 |
newScheduledThreadPool
返回值都是ExecutorService
ExecutorService executorService =Executors.newFixedThreadPool(10);
//创建一个容量为10的线程池,固定容量
(2)executorService有两个重要的方法
execute(线程对象); | 参数是实现了Runnable接口的线程 |
submit(线程对象); | 参数是实现了Callable接口的线程 |
(3)executorService关闭线程池的方法
shutdownNow() | 立即关闭线程池(暴力),正在执行中的及队列中的任务会被中断 |
shutdown() | 平滑关闭线程池,正在执行中的及队列中的任务能执行完成,后续进来的任务会被执行拒绝策略 |
isTerminated() | 当正在执行的任务及对列中的任务全部都执行(清空)完就会返回true |
利用上述的两个线程类来测试创建的线程池:
static void usePool() {
ExecutorService excutor = Executors.newFixedThreadPool(10);
PrintRunThread run = new PrintRunThread();
PrintCallThread call = new PrintCallThread();
//执行线程
excutor.execute(run);
excutor.submit(call);
//关闭线程
excutor.shutdown();
}
5、Springboot中使用线程池
(1)配置类中构建线程池实例,方便调用
@Configuration
public class ThreadPoolConfig {
@Bean(value = "threadPoolInstance")
public ExecutorService createThreadPoolInstance() {
//通过guava类库的ThreadFactoryBuilder来实现线程工厂类并设置线程名称
ThreadFactory threadFactory = new ThreadFactoryBuilder().setNameFormat("thread-pool-%d").build();
ExecutorService threadPool = new ThreadPoolExecutor(10, 16, 60L, TimeUnit.SECONDS, new ArrayBlockingQueue<Runnable>(100), threadFactory, new ThreadPoolExecutor.AbortPolicy());
return threadPool;
}
}
(2) 引用线程池实例
(3)executorService.execute(new Runnable(){});
@Resource(name = "threadPoolInstance")
private ExecutorService executorService;
@Override
public void spikeConsumer() {
//TODO
executorService.execute(new Runnable() {
@Override
public void run() {
//TODO
}});
}
三、线程的生命周期
新建、就绪、运行、阻塞、死亡
1、介绍五种状态
(1)新建:
仅仅分配了内存,还不能运行
(2)就绪:
具备了运行的条件,在等待CPU
(3)运行:
获得了CPU的使用权,并开始执行run()方法中的线程执行体。
(4)阻塞:
被迫(同步阻塞)或者主动(sleep)让出CPU的使用权并暂时中止自己的执行,进人阻塞状态。
线程进人阻塞状态后,就不能进入排队队列。只有当引起阻塞的原因被消除后,线程才可以转入就绪状态,是不能从阻塞直接转为运行的
当sleep结束或者因为wait()进入阻塞状态,但是被notify()唤醒的时候,会从阻塞状态进入就绪
(5)死亡:
一旦进入死亡状态,线程将不再拥有运行的资格,也不能再转换到其他状态。也就是说线程的生命周期结束了
2、五种状态之间的转换
(1)新建-->就绪
new出线程对象之后,通过start()方法启动线程
(2)就绪-->运行
获得CPU
因为就绪状态表示,除了CPU以外的其他资源都已经备好了
注意:只有处于就绪状态的线程才能转为运行状态
(3)运行-->死亡
run()方法正常执行完毕、stop()方法强行终止线程或者捕获到某种异常的时候线程就进入死亡状态。
(4)运行-->就绪
如果被迫失去CPU的使用权时,会进入就绪状态,如果是因为其他资源被剥夺,通过wait(),sleep()等方式,将会进入阻塞状态
(5)运行-->阻塞
有三种情况:
- 同步阻塞:当前线程所需的某种资源被其他线程占用的时候,也称IO阻塞
- 等待阻塞:线程调用wait()方法,使本线程进入到等待状态;调用join()方法,使本线程进入到另一种等待状态:等待线程终止或者超时的状态
- 其他阻塞:执行sleep(时间)方法的时候,线程主动放弃对CPU的使用权,停止执行
(6)阻塞-->就绪
根据(5)的三种阻塞情况可知,对应以下三种情况可让线程从阻塞装成就绪
- 当别的线程释放了正在阻塞的线程需要的资源的时候,这个阻塞的线程将能转成就绪状态
- 使用notify()方法唤醒线程
- sleep()时间结束