004-多线程
多线程
-
什么是线程?
-
线程(Thread)是一个程序内部的一条执行流程、
-
如果程序中只有一条执行流程,那这个程序就是单线程程序
-
-
什么是多线程?
- 多线程是指从软硬件上实现的多条执行流程的技术(多条线程由cpu负责调度执行)
- 多线程用在哪里?
- 抢票系统、上传和下载并行......
创建线程
-
方法一:实现Thread类
-
步骤
-
定义一个子类继承Thread类,成为一个线程类
-
重写Thread类的run方法
-
在run方法中编写线程的任务代码(线程要干的活)
-
创建线程类对象代表线程
-
调用线程类对象的start方法,启动线程。(自动调用run方法)
-
-
优缺点
- 优点:编码简单
- 缺点:线程类已经继承Thread,无法继承其他类,不利于功能的扩展
-
注意事项
- 启动线程必须是调用start方法,不是调用run方法
- 直接调用run方法会当成普通方法执行,此时相当于还是单线程执行
- 只有调用start方法才是启动一个新的线程
- 不要把主线程任务放在启动子线程之前
- 直到启动线程,线程才开始启动,这样没有和主线程同时执行的感觉
- 启动线程必须是调用start方法,不是调用run方法
-
-
方法二:实现Runnable接口
-
步骤
-
定义一个线程任务类实现Runnable接口
-
重写run方法,设置线程任务
-
创建线程任务类对象代表一个线程任务
-
把线程任务对象交给一个线程对象来处理
-
调用线程对象的start方法启动线程
-
-
优缺点
- 优点:人物类只是实现接口,可以继续继承其他类、实现其他接口,扩展性强
- 缺点:需要多一个Runnable对象
-
匿名内部类写法
-
可以创建Runnable的匿名内部类对象
-
再交给Thread线程对象
-
再调用线程对象的start启动线程
-
-
-
方法三:利用Callable接口、FutureTask类来实现
-
前两种方法存在的问题:加入线程执行完毕后有一些数据要返回,他们重写的run方法均不能直接接返回结果
- 一个错误的解决方法:用一个静态变量去记住run方法里的数据。
- 错误原因是无法断定此时线程中的内容已经执行完了,线程还在和主线程同时运行的话,此时再主线程里是取不到线程结果的
- 如何解决?
- JDK5.0提供了Callable接口和FutureTask类来实现(多线程的第三种创建方式)
- 这种方式的最大优点:可以返回线程执行完毕后的结果
- 一个错误的解决方法:用一个静态变量去记住run方法里的数据。
-
步骤
-
创建任务对象
-
定义一个类实现Callable接口,重写call方法,封装要做的事情和要返回的数据
-
把Callable类型的对象封装成FutureTask(线程任务对象),FutureTask实现了Runnable,FutureTask对象本质上就是Runnable对象,可以交给Thread线程对象处理
-
-
把线程任务对象交给Thread对象
-
调用Thread对象的start方法启动线程
-
线程执行完毕后,通过FutureTask对象的get方法去获取线程任务执行的结果
-
-
注意事项
- 多个线程分开try-catch,这样一个线程异常不影响其他线程跑
- 调用某个线程任务的get方法时,如果发现这个线程还没执行完毕,就会让出CPU,等该线程执行完毕后才会往下执行
-
优缺点
- 优点:线程任务类只是实现接口,可以继续继承类和实现接口,扩展性强;可以在线程执行完毕后去获取线程执行的结果
- 缺点:编码复杂一点
-
-
三种方法对比
线程常用方法
- join方法调用时,线程插队,全部跑完后再执行主线程
线程安全
-
什么是线程安全问题?
-
多个线程,同时操作同一个共享资源的时候,可能会出现业务安全问题
-
线程同步
-
线程同步是线程安全问题的解决方案
-
线程同步的核心思想
- 让多个线程先后一次访问共享资源,这样就可以避免出现线程安全问题
-
线程同步的常见方案
- 加锁:每次只允许一个线程加锁,加锁后才能进入访问,访问完毕后自动解锁,然后其他线程才能再加锁进来
方式一:同步代码块
-
作用:把访问共享资源的核心代码都上锁,以此保证线程安全
-
原理:每次只允许一个线程加锁后进入,执行完毕后自动解锁,其他线程才可以进来执行
-
同步锁的注意事项
- 对于当前同步执行的线程来说,同步锁必须是同一把(同一个对象),否则会出bug
- 括号里的同步锁必须对于线程来说是唯一对象,例如可以填一个双引号字符串,而不能是new对象
-
快捷键:ctrl+alt+t有将选中代码包入同步锁块中的选项
-
随便使用一个唯一对象真的好吗(例如使用双引号字符串)?
-
不好!会影响其他无关线程的执行
-
例如取钱示例中,小明和小红有一个公共账户,小黑和小白也有个公共账户,那么原本只想在小明小红账户上加的锁实际上加到所有账户上了,这导致其他用户想访问自己账户的时候也要被这个锁卡住
-
锁对象的使用规范:
-
建议使用共享资源作为锁对象,对于实例方法建议使用
this
作为锁对象 -
对于静态方法建议使用字节码(
类名.class
)对象作为锁对象
-
-
示例
-
方式二:同步方法
-
作用:把访问共享资源的核心方法给上锁,以此保证线程安全
-
原理:每次只能一个线程进入,执行完毕以后自动解锁,其他线程才可以进来执行
-
示例
-
同步方法底层原理
- 同步方法其实底层也是隐式锁对象的,只是锁的范围是整个方法代码
- 如果方法时实例方法:同步方法默认用
this
作为锁对象 - 如果方法时静态方法:同步方法默认用
类名.class
作为锁对象
-
同步代码块好还是同步方法好?
- 同步代码块性能好,但现在的计算机不在乎这点性能
- 在可读性方面,同步方法更好
方式三:Lock锁
-
Lock锁是JDK5开始提供的一个新的锁定操作,通过它可以创建出锁对象进行加锁和解锁,更灵活、更方便、更强大。
-
Lock是接口,不能直接实例化,可以采用它的实现类ReentrantLock来构建Lock对象
-
Lock的常用方法
-
示例
- 锁对象不能用static,不然无法保证每个账户一把锁,这样所有账户就公用一把锁了
- 使用final是保证锁不会被人撬了
- 解锁操作一般放在finally中,用try包裹要锁的代码操作,在方法中一般把异常抛给上层,不用写catch
线程池
-
线程池十一个可以复用线程的技术
-
不使用线程池的问题
- 用户每发起一个请求,后台就需要创建一个新线程来处理,下次新任务来了肯定又要创建新线程来处理,创建新线程的开销是很大的,并且请求过多时,肯定会产生大量的线程出来,这样会严重影响系统性能
-
线程池工作原理
- 工作线程在任务队列中处理任务,处理完后再到后面找任务去执行
创建线程池
- JDK5.0起提供了代表线程池的接口:ExecutorService
方式一:使用ExecutorService的实现类ThreadPoolExecutor自创建一个线程池对象
-
示例
方式二:使用Executors(线程池的工具类)调用方法返回不同特点的线程池对象
-
Executors是一个线程池的工具类,提供了很多静态方法用于返回不同特点的线程池对象
-
注意:这些方法的底层,都是通过线程池的实现类ThreadPoolExecutor创建的线程池对象
-
Executors使用可能存在的陷阱
-
大型并发系统环境中使用Executors如果不注意可能会出现系统风险
OOM是溢出的意思
-
处理Runnable任务
-
ExecutorService的常用方法
线程池的注意事项
-
什么时候开始创建临时线程?
- 新任务提交时发现核心线程都在忙,任务队列也满了,并且还可以创建临时线程,此时才会创建临时线程
-
什么时候会拒绝新任务?
- 核心线程和临时线程都在忙,任务队列也满了,新任务过来的时候才会开始拒绝任务
-
任务拒绝策略
处理Callable任务
-
ExecutorService常用方法
并发、并行
- 进程
- 正在运行的程序(软件)就是一个独立的进程
- 线程是属于进程的,一个进程中可以同时运行很多个线程
- 进程中的多个线程其实是并发和并行执行的
- 并发的含义
- 进程中的线程是由CPU负责调度执行的,但CPU能同时处理线程的数量有限,为了保证全部线程都能往前执行,CPU会轮询为系统的每个线程服务,由于CPU切换的速度很快,给我们的感觉这些线程在同时执行。这就是并发
- 并行的理解
- 在同一个时刻上,同时有多个线程在被CPU调度执行