多线程学习
多线程的实现方案一:继承Thread类
1、为什么不直接调用了run方法,而是调用start启动线程。
- 直接调用run方法会当成普通方法执行,此时相当于还是单线程执行。
多线程的实现方案二:实现Runnable接口
- 定义一个线程任务类MyRunnable实现Runnable接口,重写run()方法
- 创建MyRunnable任务对象
- 把MyRunnable任务对象交给Thread处理。
- 调用线程对象的start()方法启动线程
优点:
线程任务类只是实现接口,可以继续继承类和实现接口,扩展性强。
缺点:
编程多一层对象包装,如果线程有执行结果是不可以直接返回的。
多线程的实现方案三:Callable和FutureTask
前2种线程创建方式都存在一个问题:
- 他们重写的run方法均不能直接返回结果。
- 不适合需要返回线程执行结果的业务场景。
使用方法:
-
得到任务对象
定义类实现Callable接口,重写call方法,封装要做的事情。
用FutureTask把Callable对象封装成线程任务对象。
-
把线程任务对象交给Thread处理。
-
调用Thread的start方法启动线程,执行任务、线程执行完毕后、通过FutureTask的get方法去获取任务执行的结果。
方式三优缺点:
-
·优点:
线程任务类只是实现接口,可以继续继承类和实现接口,扩展性强。
可以在线程执行完毕后去获取线程执行的结果。
-
缺点:
编码复杂一点。
Thread常用的构造器
解决线程安全的方法
方式一:同步代码块
- 作用:把出现线程安全问题的核心代码给上锁。
- 原理:每次只能一个线程进入,执行完毕后自动解锁,其他线程才可以进来执行。
synchronized(同步锁对象){
操作共享资源的核心代码(核心代码)
}
-
锁对象要求
锁对象只要对于当前同时执行的线程来说是同一个对象即可。
规范上:建议使用共享资源作为锁对象
锁对象不可以使用任意唯一的对象,这样会影响到其他无关线程的执行
对实例方法可以用调用该锁地址(this)作为锁对象
对于静态方法建议使用字节码(类名.class)对象作为锁对象。
-
同步代码块是如何实现线程安全的?
对出现问题的核心代码使用synchronized进行加锁
每次只能一个线程占锁进入访问
方式二:同步方法
同步方法
- 作用:把出现线程安全问题的核心方法给上锁。
- 原理:每次只能一个线程进入,执行完毕以后自动解锁,其他线程才可以进来执行。
同步方法底层原理
- 同步方法其实底层也是有隐式锁对象的,只是锁的范围是整个方法代码。
- 如果方法是实例方法:同步方法默认用this作为的锁对象。但是代码要高度面向对象!
- 如果方法是静态方法:同步方法默认用类名.class作为的锁对象。
同步方法和同步代码块的差别
- 同步代码块锁的范围更小
- 同步方法锁的范围更大
方式三:Lock锁
- 为了更清晰的表达如何加锁和释放锁,JDK5以后提供了一个新的锁对象Lock,更加灵活、方便。
- Losk实现提供比使用synchronized方法和语句可以获得更广泛的锁定操作。
- Lock是接口不能直接实例化,这里采用它的实现类ReentrantLock来构建Lock锁对象
线程通信
什么是线程通信、如何实现?
- 所谓线程通信就是线程间相互发送数据,线程通信通常通过共享一个数据的方式实现。
- 线程间会根据共享数据的情况决定自己该怎么做,以及通知其他线程怎么做。
线程通信常见模型
- 生产者与消费者模型:生产者线程负责生产数据,消费者线程负责消费数据。
- 要求:生产者线程生产完数据后,唤醒消费者,然后等待自己;消费者消费完该数据后,唤醒生产者,然后等待自己。
Object类的等待和唤醒方法:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-c71sN0Wq-1660659468969)(https://gitee.com/mazzz777/pic-go-warehouse/raw/master/202208151711514.png)]
注意
:上述方法应该由锁对象去调取
线程池
什么是线程池?
-
线程池就是一个可以
复用线程
的技术。
不使用线程池的问题
-
如果用户每发起一个请求,后台就创建一个新线程来处理,下次新任务来了又要创建新线程,而创建新线程的开销是很大的,这样会严重影响系统的性能。
例如
淘宝系统,有100W人同时来访问,如果不使用线程池就意味着要创建100W个线程,这个并不可能,并且极大的浪费了系统的资源。
谁代表线程池?
- JDK5.0起提供了代表线程池的接口:ExecutorService
如何得到线程池对象
-
方式一
(更通用):使用ExecutorService的实现类ThreadPoolExecutor自创建一个线程池对象
ExecutorService pool = new ThreadPoolExecutor(3, 5, 6,
TimeUnit.SECONDS,
new ArrayBlockingQueue<>(5),
Executors.defaultThreadFactory(),
new ThreadPoolExecutor.AbortPolicy());
- 方式二:使用Executors(线程池的工具类)调用方法返回不同特点的线程池对象
临时线程什么时候创建啊?
- 新任务提交时发现核心线程都在忙 ,任务队列也满了,并且还可以创建临时线程,此时才会创建临时线程。
什么时候会开始拒绝任务?
- 核心线程和临时线程都在忙,任务队列也满了,新的任务过来的时候才会开始任务拒绝。
ExecutorService常用方法
新任务拒绝策略
ExecutorService方法
一般核心线程回去处理自己的任务和等待队列中的任务,如果核心线程正在忙,并且等待队列已满,就会触发创建临时线程来参与工作。如果当创建临时线程达到上限之后,依然在提供任务,就会触发任务拒绝,并且抛出异常。
Exccutors创建线程池对象(动态线程池)
常用方法
使用固定线程池的方法:
Executors.newFixedThreadPool(n) n为固定线程数量
使用Executors可能存在的风险
- 第一个:任务队列没有限制,可能存在内存溢出的风险
- 第二个:同样没有任务队列限制,可能存在内存溢出的风险
- 第三个:任务没有限制,创造线程也没有限制,内存溢出
- 第四个:限制了核心线程数量,但是没有限制临时线程的数量,总成内存溢出
推荐的模式
阿里的规范文档建议我们使用ThreadPoolExecutor的方式去创建线程池,参数可控,并且可以直观看线程池创建的规则。
定时器
- 定时器是一种控制任务延时调用,或者周期调用的技术。
- 作用:闹钟、定时邮件发送。
定时器的实现方式
- 方式一:Timer
- 方式二:ScheduledExecutorService
Timer定时器
Timer定时器的特点和存在的问题
1、Timer是
单线程
,处理多个任务按照顺序执行,存在延时与设置定时器的时间出入。
2、可能因为其中的某个任务的异常使Timer线程死掉,从而影响后续任务执行。
ScheduledExecutorService定时器
- ScheduledExecutorService是jdk1.5中引入了并发包,目的是为了弥补Timer的缺陷ScheduledExecutorService内部为线程池。
ScheduledExecutorService的优点
- 基于线程池,某个任务的执行情况不会影响其他定时任务的执行。
- 如果有任务出现了问题,就会对问题线程进行拦截,不影响别的线程继续运行
并发与并行
- 正在运行的程序(软件)就是一个独立的进程,线程是属于进程的,多个线程其实是并发与并行同时进行的。
并发的理解:
- CPU同时处理线程的数量有限。
- CPU会轮询为系统的每个线程服务,由于CPU切换的速度很快,给我们的感觉这些线程在同时执行,这就是并发。
并行的理解
- 在同一时刻上,同时多个线程在被CPU处理并执行
简单说说并发和并行的含义
·并发:CPU分时轮询的执行线程。
·并行:同一个时刻同时在执行。
线程的生命周期
线程的状态
- 线程的状态:也就是线程从生到死的过程,以及中间经历的各种状态及状态转换。
- 理解线程的状态有利于提升并发编程的理解能力。
Java线程的状态
- Java总共定义了6种状态。
- 6种状态都定义在Thread类的内部枚举类中。