Day11、线程概述-线程并发并行
线程
什么是线程?
线程( thread ) 是一个程序内部的一条执行路径。
我们之前启动程序执行后, main 方法的执行其实就是一条单独的执行路径。
public static void main(String[] args) {
//代码
for (int i = 0 ; i < 10 ; i++) {
System.out.println(i);
}
//代码
}
程序中如果只有一条执行路径, 那么这个程序就是单线程的程序。
多线程是什么?
![image-20220516110830717]()
关于多线程要学的东西
![image-20220516111039029]()
多线程的创建
Thread 类
Java 是java.lang.Thread 类来代表线程的。
按照面向对象的思想, Thread 类应该提供了实现多线程的方式。
多线程的实现方案一: 继承Thread 类
定义一个子类MyThread 继承线程类Java.lang.Thread, 重写run() 方法
创建MyThread 类的对象
调用线程对象的start() 方法启动线程( 启动后还是执行run 方法的)
方式一优缺点:
优点: 编码简单
缺点: 线程类已经继承Thread , 无法继承其他类, 不利于扩展。
小结:
1 、为什么不直接调用了run 方法, 而是调用start 启动线程。
直接调用run 方法会当成普通方法执行, 此时相当于还是单线程执行。
只有调用start方法才是启动一个新的线程执行。
2 、把主线程任务放在子线程之前了。
这样主线程一直是先跑完的, 相当于是一个单线程的效果了。
总结:
![image-20220516161144567]()
public class ThreadDemo01 {
public static void main(String[] args) {
//3.new一个新线程对象
Thread t = new MyThread();
//4.调用start方法启动线程(执行的还是run方法)
t.start();
for (int i = 0; i < 5; i++) {
System.out.println("主线程执行输出:" + i);
}
}
}
//1.定义一个线程类继承Thread类
class MyThread extends Thread{
//2.重写run()方法
//里面是定义线程以后要干啥
@Override
public void run() {
for (int i = 0; i < 5; i++) {
System.out.println("子线程执行输出:" + i);
}
}
}
方式二优缺点:
优点: 线程任务类只是实现接口, 可以继续继承类和实现接口, 扩展性强。
缺点: 编程多一层对象包装, 如果线程有执行结果是不可以直接返回的。
总结:
![image-20220516162117846]()
public class ThreadDemo02 {
public static void main(String[] args) {
//3.n创建一个任务对象
Runnable r = new MyRunnable();
//4.把任务对象交给Thread处理
//new Thread(r).start();
Thread t = new Thread(r);
//5.启动线程
t.start();
for (int i = 0; i < 5; i++) {
System.out.println("主线程执行输出:" + i);
}
}
}
//1.定义一个线程任务类,实现Runnable接口
//extends是继承亲爹
//implement是继父
class MyRunnable implements Runnable {
//2.重写run()方法
//定义线程的执行任务
@Override
public void run() {
for (int i = 0; i < 10; i++) {
System.out.println("子线程执行输出:" + i);
}
}
}
多线程的实现方案二: 实现Runnable 接囗( 匿名内部类形式)
可以创建Runnable 的匿名内部类对象。
交给Thread 处理。
调用线程对象的start() 启动线程。
public class ThreadDemo02Other {
public static void main(String[] args) {
//3.创建一个任务对象
Runnable target = new Runnable() {
@Override
public void run() {
for (int i = 0; i < 5; i++) {
System.out.println("子线程1执行输出:" + i);
}
}
};
//4.把任务对象交给Thread处理
//new Thread(r).start();
Thread t = new Thread(target);
//5.启动线程
t.start();
new Thread(new Runnable() { //匿名写法
@Override
public void run() {
for (int i = 0; i < 5; i++) {
System.out.println("子线程2执行输出:" + i);
}
}
}).start();
new Thread(() -> {
for (int i = 0; i < 5; i++) {
System.out.println("子线程3执行输出:" + i);
}
}).start();
for (int i = 0; i < 5; i++) {
System.out.println("主线程执行输出:" + i);
}
}
}
小结:
1、前2 种线程创建方式都存在一个问题:
他们重写的run 方法均不能直接返回结果。
不适合需要返回线程执行结果的业务场景。
2 、怎么解决这个问题昵?
JDK 5·0 提供了Callable 和FutureTask 来实现。
这种方式的优点是: 可以得到线程执行的结果。
多线程的实现方案三: 利用callable. FutureTask 接囗实现。
定义类实现Cable 接口, 重写call()方法, 封装要做的事情。
用FutureTask 把Callable 对象封装成线程任务对象。
把线程任务对象交给Thread 处理。
调用Thread 的start 方法启动线程, 行任务
线程执行完毕后、通过FutureTask 的get 方法去获取任务执行的结果。
FutureTask的API
| 方法名称 |
说明 |
| public FutureTask<>(CaIIabIe call) |
把Callable 对象封装成FutureTask 对象。 |
| public V get() throws Exception |
获取线程执行c 方法返回的结果 |
方式三优缺点:
优点: 线程任务类只是实现接口, 可以继续继承类和实现接口, 扩展性强。
可以在线程执行完毕后去获取线程执行的结果。
缺点: 编码复杂一点。
public class ThreadDemo03 {
public static void main(String[] args) {
//3.创建callable任务对象
Callable<String> call1 = new MyCallable(100);
//4.把Callable任务对象 交给FutureTas对象
//FutureTask对象的作用1:是Runnable的对象(实现了Runnable接口) 可以交给Thread了
//FutureTask对象的作用2:可以在线程执行通过之后调用其get方法得到线程执行完成的结果
FutureTask<String> f1 = new FutureTask<>(call1);
//5.交给线程处理
Thread t1 = new Thread(f1);
//6.启动线程
t1.start();
Callable<String> call2 = new MyCallable(50);
FutureTask<String> f2 = new FutureTask<>(call2);
Thread t2 = new Thread(f2);
t2.start();
try {
String s1 = f1.get();
System.out.println("第一个线程结果:" + s1);
} catch (Exception e) {
e.printStackTrace();
}
try {
String s2 = f2.get();
System.out.println("第一个线程结果:" + s2);
} catch (Exception e) {
e.printStackTrace();
}
}
}
//1.定义一个任务类 实现Callable接口 应该申明线程任务执行完毕后的结果的数据类型
class MyCallable implements Callable<String> {
private int n;
public MyCallable(int n){
this.n = n;
}
//2.重写call方法(任务方法)
@Override
public String call() throws Exception {
int sum = 0;
for (int i = 0; i <= n; i++) {
sum += i;
}
return "子线程执行的结果是:" + sum;
}
}
总结:3 种方式对比
| 方式 |
优点 |
缺点 |
| 继承Thread 类 |
编程比较简单,可以直接使用Thread |
扩展性较差,不能再继承其他的类,不能返回线程执行的结果 |
| 实现RunnabIe 接口 |
扩展性强,实现该接口的同时还可以继承其他的类。 |
编程相对复杂,不能返回线程执行的结果 |
| 实现CaIIabIe 接口 |
扩展性强,实现该接口的同时还可以继承其他的类。可以得到线程执行的结果 |
编程相对复杂 |
Thread的常用方法
Thread 常用API 说明
Thread 常用方法: 获取线程名称getName() 、设置名称setName() 、获取当前线程对象currentThread()。
至于Thread 类提供的诸如: yield 、join 、interrupt 、不推荐的方法st0p 、守护线程、线程优先级等线程的控制方法, 在开发中很少使用, 这些方法会在高级篇以及后续需要用到的时候再为大家讲解。
多线程执行时,如何区分线程
此时需要使用Thread的常用方法:getName()、setName()、currentThread()等。
Thread 类获得当前线程的对象
| 方法名称 |
说明 |
| public static Thread currentThread(); |
返回对当前正在执行的线程对象的引用 |
注意:
此方法是Thread 类的静态方法, 可以直接使用Thread 类调用。
这个方法是在哪个线程执行中调用的, 就会得到哪个线程对象。
Thread的构造器
| 方法名称 |
说明 |
| public Thread(String name) |
可以为当前线程指定名称 |
| public Thread(Runnable target) |
封装Runnable 对象成为线程对象 |
| public Thread(Runnable target,String name) |
封装Runnable 对象成为线程对象, 并指定线程名称 |
Thread 类的线程休眠方法
| 方法名称 |
说明 |
| public static void sleep(long time) |
让当前线程休目民指定的时间后再继续执行, 单位为毫秒。 |
总结:
Thread常用方法、构造器
| 方法名称 |
说明 |
| String getName() |
获取当前线程的名称, 默认线程名称是Thread-索引 |
| void setName(String name) |
设置线程名称 |
| public static Thread currentThread() |
返回对当前正在执行的线程对象的引用 |
| public static void sleep(long time) |
让线程休眠指定的时间, 单位为毫秒。 |
| public void run() |
线程任务方法 |
| public void start() |
线程启动方法 |
| 构造器 |
说明 |
| public Thread(String name) |
可以为当前线程指定名称 |
| public Thread(Runnable target ) |
把Runnable 对象交给线程对象 |
| public Thread(Runnable target,String name) |
把Runnable 对象交给线程对象,并指定线程名称 |
线程安全
线程安全问题出现的原因?
存在多线程并发
同时访问共享资源
存在修改共享资源
案例:取钱业务
![image-20220516191151697]()
线程安全问题发生的原因是什么?
线程同步:为了解决安全问题
1 、取钱案例出现问题的原因?
2 、如何才能保证线程安全昵?
让多个线程实现先后依次访问共享资源, 这样就解决了安全问题
线程同步的核心思想
加锁,把共享资源进行上锁,每次只能一个线程进入访问完毕以后解锁,然后其他线程才能进来。
同步代码块
. 作用: 把出现线程安全问题的核心代码给上锁。
. 原理: 每次只能一个线程进入, 执行完毕后自动解锁, 其他线程才可以进来执行。
synchronized( 同步锁对象) {
操作共享资源的代码( 核心代码)
}
锁对象要求
理论上: 锁对象只要对于当前同时执行的线程来说是同一个对象即可。
锁对象不能用任意唯一的对象,会影响其他无关线程的执行
建议使用共享资源作为对象锁
对于实例方法建议使用this作为锁对象
静态方法建议使用字节码(类名.class)对象作为锁对象
同步方法
作用: 把出现线程安全问题的核心方法给上锁。
原理: 每次只能一个线程进入, 执行完毕以后自动解锁, 其他线程才可以进来执行。
格式
修饰符synchronized 返回值类型方法名称( 形参列表) {
操作共享资源的代码
}
同步方法底层原理
同步方法其实底层也是有隐式锁对象的, 只是锁的范围是整个方法代码。
如果方法是实例方法: 同步方法默认用this 作为的锁对象。但是代码要高度面向对象!
如果方法是静态方法: 同步方法默认用类名· class 作为的锁对象。
同步代码块好还是同步方法好?
总结:
![image-20220516201834201]()
Lock 锁
为了更清晰的表达如何加锁和释放锁, JDK5以后提供了一个新的锁对象Lock , 更加灵活、方便。
Lock 实现提供比使用synchronized 方法和语句可以获得更广泛的锁定操作。
Lock 是接口不能直接实例化, 这里采用它的实现类ReentrantLock 来构建Lock 锁对象。
| 方法名称 |
说明 |
| public ReentrantLock() |
获得Lock锁的实现类对象 |
Lock的API
| 方法名称 |
说明 |
| void lock() |
获得锁 |
| void unlock() |
释放锁 |
线程通信
什么是线程通信、如何实现?
所谓线程通信就是线程间相互发送数据, 线程通信通常通过共享一个数据的方式实现。
线程间会根据共享数据的情况决定自己该怎么做, 以及通知其他线程怎么做。
线程通信常见模型
生产者与消费者模型: 生产者线程负责生产数据, 消费者线程负责消费数据。
要求: 生产者线程生产完数据后, 唤醒消费者, 然后等待自己; 消费者消费完该数据后, 唤醒生产者, 然后等待自己。
![image-20220517095751495]()
Object 类的等待和唤醒方法:
| 方法名称 |
说明 |
| void wait() |
让当前线程等待并释放所占锁, 直到另一个线程调用notify()方法或notifyAll() 方法 |
| void notify() |
唤醒正在等待的单个线程 |
| void notifyAll() |
唤醒正在等待的所有线程 |
注意:
线程池:dagger:
什么是线程池?
不使用线程池的问题
如果用户每发起一个请求,后台就创建一个新线程来处理,下次新任务来了又要创建新线程,而创建新线程的开销是很大的, 这样会严重影响系统的性能。
线程池的工作原理:
![image-20220517105524123]()
谁代表线程池?
JDK 5.0 起提供了代表线程池的接口:ExecutorService
如何得到线程池对象
方式一: 使用ExecutorService 的实现类ThreadPooIExecutor 自创建一个线程池对象
ExecutorService→ThreadPoolExecutor.
方式二: 使用Executors ( 线程池的工具类) 调用方法返回不同特点的线程池对象
![image-20220517105709704]()
面试题:常问
![image-20220517110209233]()
ThreadPoolExecutor 创建线程池对象示例
ExecutorService pools = new ThreadPoolExecutor(3,5,8, TimeUnit SECONDS, new ArrayBlockingQueue<>(6),Executors.defaultThreadFactory( ),new ThreadPoolExecutor.AbortPolicy());
ExecutorService 的常用方法
| 方法名称 |
说明 |
| void execute(Runnable command ) |
执行任务/ 命令, 没有返回值, 一般用来执行Runnable 任务 |
| Future submit(Callable task) |
执行任务, 返回未来任务对获取线程结果, 一般韋来执行callable 任务 |
| Void shutdown() |
等任务执行完毕后关闭线程池 |
| List shutdownNow() |
立刻关闭, 停止正在执行的任务, 返回队列中未执行的任务 |
新任务拒绝策略
| 策略 |
详解 |
ThreadPoolExecutor.AbortPolicy |
被拒绝的任务的处理程序,抛出一个 RejectedExecutionException 。 是默认的策略 |
| ThreadPoolExecutor.CallerRunsPolicy |
丢弃任务,不抛异常。不推荐 |
| ThreadPoolExecutor.DiscardOldestPolicy |
抛弃队列中等待最久的任务,然后把当前任务加入队列中 |
ThreadPoolExecutor.CallerRunPolicy |
由主线程负责调用任务的run()方法从而绕过线程池直接执行 |
ExcutorService的常用方法
| 方法名称 |
说明 |
| void execute(Runnable command) |
执行任务/命令,没有返回值,一般用来执行Runnable任务 |
| Future submit(Callable task) |
执行Callable任务,返回未来任务对象获取线程结果 |
| void shutdown() |
等任务执行完毕后关闭线程池 |
| List shutdownNow() |
立刻关闭,停止正在执行的任务,并返回队列中未执行的任务 |
总结:
线程池如何处理Callable任务,并得到任务执行完后返回的结果
使用ExcutorSerive的方法:
Future submit(Callable command)
###Executors 得到线程池对象的常用方法
Executors : 线程池的工具类通过调用方法返回不同类型的线程池对象。
| 方法名称 |
说明 |
| public static ExecutorService newCachedThreadPool() |
线程数量随着任务增加而增加, 如果线程任务执行完毕且空闲了一段时间则会被回收掉 |
| public static ExecutorService newFixedThreadPool ( int nThreads ) |
创建固定线程数量的线程池如果某个线程因为执行异常而结束, 那么线程池会补充一一个新线程替代它。 |
| public static ExecutorService newSingleThreadExecutor() |
创建只有一个线程的线程池对, 如果该线程出现异常而结束, 那么线程池会补充一个新线程。 |
| public static Scheduled ExecutorService newScheduledThreadPool(int corePoolSize) |
创建一个线程池, 可以实现在给定的延迟后运行任务, 或者定期执行任务。 |
注意: Executor的底层其实也是基于线程池的实现类ThreadPoolExecutor创建线程池对象的。
Executors使用可能存在的陷阱
大型并发系统环境中使用Executors 如果不注意可能会出现系统风险。
| 方法名称 |
存在问题 |
| public statlc ExecutorService newFixedThreadPool ( int nThreads ) |
允许请求的任务队列长度是Interger.MAX_VALUE, 可能出现OOM错误(java.lang.OutOfMemoryError ) |
| public statlc ExecutorService newSingleThreadExecutor() |
|
| public statlc ExecutorService newCachedThreadPool() |
创建的线程数量最大上限是lInterger.MAX_VALUE,线程数可能会随着任务1 : 1 增长也可能出现OOM错误(java.lang.OutOfMemoryError) |
| public statlc ScheduledExecutorService newScheduledThreadPool(int corePoolSize) |
|
Excutors使用可能存在的陷阱
大型并发系统环境中使用Executors如果不注意可能会出现系统风险。
![image-20220521204537775]()
定时器
定时器
定时器是一种控制任务延时调用或者周期调用的技术。
作用: 闹钟、定时邮件发送。
定时器的实现方式
方式一:Timer
方式二:ScheduledExecutorService
Timer 定时器
| 构造器 |
说明 |
| public Timer() |
创建Timer 定时器对象 |
| 方法 |
说明 |
| public void schedule (TimerTask task, long delay,long period ) |
开启一个定时器, 按照计划处理TimerTask 任务 |
Timer 定时器的特点和存在的问题
Timer 是单线程, 处理多个任务按照顺序执行, 存在延时与设置定时器的时间有出入。
可能因为其中的某个任务的异常使Timer 线程死掉, 从而影响后续任务执行。
ScheduledExecutorService 定时器
ScheduledExecutorService 是jdk1.5 中引入了并发包, 目的是为了弥补Timer 的缺陷, ScheduledExecutorService 内部为线程池。
| Executors 的方法 |
说明 |
| public static ScheduledExecutorService newScheduledThreadPool ( int corePoolSize ) |
得到线程池对象 |
| ScheduledExecutorService 的方法 |
说明 |
| public ScheduledFuture<?> scheduleAtFixedRate(Runnable command, long initialDelay,long periods,TimeUnit unit) |
周期调度方法 |
ScheduIedExecutorService 的优点
基于线程池某个任务的执行情况不会影响其他定时任务的执行。
补充知识:并发并行
并发与并行
正在运行的程序( 软件) 就是一个独立的进程,线程是属于进程的,多个线程其实是并发与并行同时进行的。
并发的理解:
CPU 同时处理线程的数量有限。
CPU 会轮询为系统的每个线程服务,由于CPU 切换的速度很快,给我们的感觉这些线程在同时执行, 这就是并发。
并行的理解:
在同一个时刻上,同时有多个线程在被cpu处理并执行
补充知识:线程的生命周期
线程的状态
线程的状态: 也就是线程从生到死的过程, 以及中间经历的各种状态及状态转换。
理解线程的状态有利于提升并发编程的理解能力。
Java线程的状态
Java 总共定义了6 种状态
6 种状态都定义在Thread 类的内部枚举类中。
public class Thread{
...
public enum State {
NEW,
RUNNABLE,
BLOCKED,
WAITING,
TIMED_WAITING,
TEMINATED;
}
}
![image-20220521213712764]()
线程的6 种状态总结
| 线程状态 |
描述 |
| NEW( 新建) |
线程刚被创建,但是并未启动。 |
| RunnabIe( 可运行) |
线程已经调用了start() 等待CPU 调度 |
| Blocked ( 锁阻塞) |
线程在执行的时候未竞争到锁对象, 则该线程进入BIocked 状态; |
| Waiting( 无限等待) |
一个线程进入Waiting 状态, 另一个线程调用notify 或者notifyAII 方法才能够唤醒 |
| Timed Waiting( 计时等待) |
同waiting 状态, 有几个方法有超时参数, 调用他们将进入Timed Waiting 状态。带有超时参数的常用方法有Thread.sIeep 、Object.wait。 |
| Teminated(被终止) |
因为run 方法正常退出而死亡/ 或者因为没有捕获的异常终止了run 方法而死亡。 |