day12_多线程
1. 多线程相关概念
1.1 进程
-
进程是程序执行的过程,包括了动态创建、调度和消亡的整个过程
-
操作系统会以进程为单位,分配系统资源(CPU时间片、内存等资源),进程是资源分配的最小单位
1.2 线程
- 线程被包含在进程之中,是进程中的实际运作单位,一个进程内可以包含多个线程
- 是操作系统调度(CPU调度)执行的最小单位
2. Java多线程的创建
2.1 继承Thread类
- Java是通过
java.lang.Thread
类的对象来代表线程的 - 特点
- 编码简单
- 线程类已经继承Thread,无法继承其他类,不利于功能的扩展
- 实现
- 定义一个子类
MyThread
继承线程类java.lang.Thread
,重写run()
方法 - 创建
MyThread
类的对象 - 调用线程对象的
start()
方法启动线程
public class Test {
public static void main(String[] args) {
new MyThread().start();
}
}
class MyThread extends Thread {
@Override
public void run() {
System.out.println("MyThread start...");
}
}
- 注意
- 只有调用start方法才是启动一个新的线程执行
- 启动线程必须是调用start方法,不是调用run方法,直接调用run方法会当成普通方法执行,此时相当于还是单线程执行
- 不要把主线程任务放在启动子线程之前,这样主线程一直是先跑完的,相当于是一个单线程的效果了
2.2 实现Runnable接口
-
通过实现
Runnable
接口实现多线程 -
特点
- 任务类只是实现接口,可以继续继承其他类、实现其他接口,扩展性强
-
Thread类提供的构造器 说明 public Thread(Runnable target)
封装Runnable对象成为线程对象 -
实现
- 定义一个线程任务类
MyRunnable
实现Runnable
接口,重写run()
方法 - 创建
MyRunnable
任务对象 - 把
MyRunnable
任务对象交给Thread
处理 - 调用线程对象的
start()
方法启动线程
public class Test {
public static void main(String[] args) {
MyRunnable mr = new MyRunnable();
Thread thread = new Thread(mr);
thread.start();
}
}
class MyRunnable implements Runnable {
@Override
public void run() {
System.out.println("MyRunnable start...");
}
}
- 匿名内部类写法
/*
new Thread(new Runnable() {
@Override
public void run() {
System.out.println("Thread start...");
}
}).start();
*/
new Thread(() -> {
System.out.println("Thread start...");
}).start();
2.3 实现Callable接口
- 利用
Callable
接口、FutureTask
类来实现多线程 - 特点
- 线程任务类只是实现接口,可以继续继承类和实现接口,扩展性强
- 可以返回线程执行完毕后的结果
- 编码复杂
FutureTask
的API
FutureTask 提供的构造器 | 说明 |
---|---|
public FutureTask<>(Callable call) | 把Callable对象封装成FutureTask 对象 |
FutureTask 提供的方法 | 说明 |
---|---|
public V get() throws Exception | 获取线程执行call方法返回的结果 |
- 实现
- 定义一个类实现
Callable
接口,重写call()
方法 - 把Callable类型的对象封装成
FutureTask
,即线程任务对象 - 把线程任务对象交给
Thread
对象 - 调用
Thread
对象的start()
方法启动线程 - 线程执行完毕后、通过
FutureTask
对象的的get方法去获取线程任务执行的结果
public class Test {
public static void main(String[] args) throws Exception {
FutureTask<Integer> threadTask = new FutureTask<>(new MyCallable(1, 1));
new Thread(threadTask).start();
System.out.println(threadTask.get()); // 2
}
}
class MyCallable implements Callable<Integer> {
private Integer a;
private Integer b;
public MyCallable(Integer a, Integer b) {
this.a = a;
this.b = b;
}
@Override
public Integer call() throws Exception {
int ans = a + b;
System.out.println(a + " + " + b + " = " + ans);
return ans;
}
}
3. Thread
构造方法
Thread提供的常见构造器 | 说明 |
---|---|
public Thread(String name) | 可以为当前线程指定名称 |
public Thread(Runnable target) | 封装Runnable对象成为线程对象 |
public Thread(Runnable target, String name) | 封装Runnable对象成为线程对象,并指定线程名称 |
常用方法
Thread提供的常用方法 | 说明 |
---|---|
public void run() | 线程的任务方法 |
public void start() | 启动线程 |
public String getName() | 获取当前线程的名称,线程名称默认是Thread-索引 |
public void setName(String name) | 为线程设置名称 |
public static Thread currentThread() | 获取当前执行的线程对象 |
public static void sleep(long time) | 让当前执行的线程休眠多少毫秒后,再继续执行 |
public final void join() | 让调用当前这个方法的线程先执行完 |
4. 线程安全
4.1 什么是线程安全
- 多个线程,同时操作同一个共享资源的时候,可能会出现业务安全问题
出现线程安全的原因:
- 存在多个线程在同时执行
- 同时访问一个共享资源
- 存在修改该共享资源
4.2 线程同步
- 解决线程安全问题的方案
- 让多个线程实现先后依次访问共享资源,这样就解决了安全问题
常见的线程同步方案:加锁
- 每次只允许一个线程加锁,加锁后才能进入访问,访问完毕后自动解锁,然后其他线程才能再加锁进来
- 让多个线程实现先后依次访问共享资源,这样就解决了安全问题
4.3 方式一:同步代码块
- 原理
- 每次只允许一个线程加锁后进入,执行完毕后自动解锁,其他线程才可以进来执行
- 作用
- 把访问共享资源的核心代码给上锁,以此保证线程安全
- 实现
public void test() {
synchronized(同步锁) {
// 访问共享资源的核心代码
}
}
- 注意
- 对于当前同时执行的线程来说,同步锁必须是同一个对象
- 建议使用共享资源作为锁对象,对于实例方法建议使用
this
作为锁对象 - 对于静态方法建议使用字节码(类名.class)对象作为锁对象
4.4 方式二:同步方法
- 原理
- 每次只能一个线程进入,执行完毕以后自动解锁,其他线程才可以进来执行
- 作用
- 把访问共享资源的核心方法给上锁,以此保证线程安全
- 实现
public synchronized void test() {
// 访问共享资源的代码
}
- 底层
- 同步方法其实底层也是有隐式锁对象的,只是锁的范围是整个方法代码
- 如果方法是实例方法:同步方法默认用this作为的锁对象
- 如果方法是静态方法:同步方法默认用类名.class作为的锁对象
4.5 方式三:Lock锁
- 通过Lock创建出锁对象进行加锁和解锁
- 可以采用它的实现类
ReentrantLock
来构建Lock锁对象
构造方法
构造器 | 说明 |
---|---|
public ReentrantLock() | 获得Lock锁的实现类对象 |
常用方法
方法名称 | 说明 |
---|---|
void lock() | 获得锁 |
void unlock() | 释放锁 |
boolean trylock() | 尝试获取锁 |
5. 线程通信
- 当多个线程共同操作共享的资源时,线程间通过某种方式互相告知自己的状态,以相互协调,并避免无效的资源争夺
常用方法
方法名称 | 说明 |
---|---|
void wait() | 当前线程等待,直到另一个线程调用notify() 或notifyAll() 唤醒自己 |
void notify() | 唤醒正在等待对象监视器(锁对象)的单个线程 |
void notifyAll() | 唤醒正在等待对象监视器(锁对象)的所有线程 |
- 注意:上述方法应该使用当前同步锁对象进行调用
6. 线程的生命周期
- 线程的生命周期
- 指的是线程从生到死的过程中,经历的各种状态及状态转换
- Java的线程生命周期有6种,都定义在Thread类的内部枚举类中
线程状态 | 说明 |
---|---|
NEW(新建) | 线程刚被创建,但是并未启动 |
Runnable(可运行) | 线程已经调用了start(),等待CPU调度 |
Blocked(锁阻塞) | 线程在执行的时候未竞争到锁对象,则该线程进入Blocked状态;。 |
Waiting(无限等待) | 一个线程进入Waiting状态,另一个线程调用notify或者notifyAll方法才能够唤醒 |
Timed Waiting(计时等待) | 同waiting状态,有几个方法(sleep,wait)有超时参数,调用他们将进入Timed Waiting状态 |
Terminated(被终止) | 因为run方法正常退出而死亡,或者因为没有捕获的异常终止了run方法而死亡 |
7. 线程池
7.1 概述
- 线程池是一个容器,可以保存一些长久存活的线程对象,负责创建、复用、管理线程
优势:
- 降低资源消耗,重复利用线程池中线程,不需要每次都创建、销毁
- 便于线程管理,线程池可以集中管理并发线程的数量
使用步骤:
- 创建线程池
- 创建任务
- 提交任务
7.2 线程池的创建
- 提交Runnable任务
public class Test1 {
public static void main(String[] args) {
MyRunnable mr = new MyRunnable();
ExecutorService pool = Executors.newFixedThreadPool(3);
pool.submit(mr);
pool.shutdown();
}
}
class MyRunnable implements Runnable {
@Override
public void run() {
System.out.println("MyRunnable Start...");
}
}
- 提交Callable任务
public class Test2 {
public static void main(String[] args) throws Exception {
ExecutorService pool = Executors.newFixedThreadPool(3);
MyCallable mc = new MyCallable(1, 100);
Future<Integer> future = pool.submit(mc);
System.out.println(future.get());
pool.shutdown();
}
}
class MyCallable implements Callable<Integer> {
private int a;
private int b;
public MyCallable(int a, int b) {
this.a = a;
this.b = b;
}
@Override
public Integer call() throws Exception {
int sum = 0;
for (int i = a; i <= b; i++) {
sum += i;
}
return sum;
}
}
7.3 自定义线程池
- 使用
ThreadPoolExecutor
自定义线程池
public ThreadPoolExecutor(
int corePoolSize, // 核心线程数
int maximumPoolSize, // 最大线程数
long keepAliveTime, // 临时线程存活时间
TimeUnit unit, // 临时线程存活时间单位
BlockingQueue<Runnable> workQueue, // 阻塞等待队列
ThreadFactory threadFactory, // 创建线程的工厂
RejectedExecutionHandler handler // 拒绝策略
)
- 拒绝策略
策略 | 详解 |
---|---|
ThreadPoolExecutor.AbortPolicy | 丢弃任务并抛出异常。是默认的策略 |
ThreadPoolExecutor.DiscardPolicy | 丢弃任务,但是不抛出异常 这是不推荐的做法 |
ThreadPoolExecutor.DiscardOldestPolicy | 抛弃队列中等待最久的任务 然后把当前任务加入队列中 |
ThreadPoolExecutor.CallerRunsPolicy | 由主线程负责调用任务的run()方法从而绕过线程池直接执行 |