javaSE-多线程
多线程
什么是线程?
- 线程(thread)是一个程序内部的一条执行路径
- main方法的执行其实就是一条单独的执行路径
- 程序中如果只有一条执行路径,那么这个程序就是单线程的程序
多线程是什么?
- 多线程是指从软硬件上实现多条执行流程的技术
多线程的创建
Thread类
- java是通过java.lang.Thread类来代表线程的
- 按照面向对象的思想,Thread类应该提供了多线程实现的方法
多线程的实现方案一:继承Thread类
- 定义一个子类MyThread继承线程类java.lang.Thread,重写run方法
- 创建MyThread类的对象
- 调用线程类对象的start()方法启动线程(启动后还是执行run方法的)
/MyThread 1
2
3
4
5
6
7
8
9
10
11//1. 定义一个子类MyThread继承线程类java.lang.Thread
public class MyThread extends Thread {
// 重写run方法
public void run() {
for (int i = 0; i < 5; i++) {
System.out.println("子线程执行"+i);
}
}
}运行结果/ThreadDemo1 1
2
3
4
5
6
7
8
9
10
11public static void main(String[] args) {
// 2. 创建MyThread类的对象
MyThread myThread = new MyThread();
// 3. 调用线程类对象的start()方法启动线程(启动后还是执行run方法的)
myThread.start();
for (int i = 0; i < 5; i++) {
System.out.println("主线程执行"+i);
}
}方式一优缺点1
2
3
4
5
6
7
8
9
10子线程执行0
主线程执行0
子线程执行1
主线程执行1
子线程执行2
主线程执行2
子线程执行3
主线程执行3
子线程执行4
主线程执行4
- 优点:编码简单
- 缺点:线程类已经继承Thread,无法继承其他类,不利于扩展
为什么不直接调用run方法,而是调用start启动线程
- 直接调用run方法会被当成普通方法来执行,此时相当于还是单线程执行
把子线程放在主线程之前执行
- 如果把主线程任务放在子线程之前,主线程一直是先跑完的,相当于是一个单线程的效果
多线程实现方式二:实现Runnable接口
- 定义一个线程任务类MyRunnable实现Runnable接口,重写run()方法。
- 创建MyRunnable任务对象
- 把MyRunnable任务对象交给Thread处理
- 调用线程对象的start()方法启动线程
Thread的构造器
1 | // 定义一个线程任务类,实现Runnable接口 |
1 | public static void main(String[] args) { |
运行结果
1 | 子线程在执行:0 |
多线程的实现方式二:实现Runnable接口(匿名内部类形式)
- 创建Runnable的匿名内部类对象
- 交给Thread处理
- 调用线程对象的start()启动线程运行结果
/ThreadDemo2 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17public static void main(String[] args) {
Runnable target = new Runnable() {
public void run() {
for (int i = 0; i < 5; i++) {
System.out.println("子线程正在执行:"+i);
}
}
};
new Thread(target).start();
for (int i = 0; i < 5; i++) {
System.out.println("主线程正在执行:"+i);
}
}方式二优缺点:1
2
3
4
5
6
7
8
9
10子线程正在执行:0
主线程正在执行:0
子线程正在执行:1
主线程正在执行:1
子线程正在执行:2
主线程正在执行:2
子线程正在执行:3
主线程正在执行:3
子线程正在执行:4
主线程正在执行:4
- 优点:线程任务类知识实现接口,可以继续继承类和实现接口,扩展性强
- 缺点:变成多一层对象包装,如果线程的执行结果有返回值是不可以直接返回的
前两种线程创建方式都存在一个问题
- 他们重写的run方法均不能直接返回结果
- 不适合需要返回线程执行结果的业务场景
怎么解决这个问题?
- JDK5.0提供了Callable和FutureTask来实现
多线程的创建方式三:利用Callable、FutureTask接口实现
- 得到任务对象
- 定义类实现Callable接口,重写call方法,封装要做的事情。
- 用FutureTask把Callable对象封装成线程任务对象
- 把线程任务对象交给Thread处理
- 调用Thread的start方法启动线程,执行任务
- 线程执行完毕之、通过FutureTask的get方法去获取任务执行的结果
/myCallable 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18// 定义一个任务类实现Callable接口 申明任务执行结果的数据类型
public class MyCallable implements Callable<String>{
private int n;
public MyCallable(int n) {
this.n = n;
}
// 重写call方法(任务方法)
public String call() throws Exception {
int sum = 0;
for (int i = 1; i <= n; i++) {
sum +=i;
}
return "1-n的累加结果为:"+sum;
}
}运行结果/ThreadDemo3 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27public static void main(String[] args) {
// 创建Callable任务对象
Callable<String> myCallable = new MyCallable(100);
// 把Callable任务对象交给FutureTask对象
FutureTask<String> stringFutureTask = new FutureTask<>(myCallable);
// 交给线程处理
Thread thread = new Thread(stringFutureTask);
// 启动线程
thread.start();
Callable<String> myCallable2 = new MyCallable(10);
FutureTask<String> stringFutureTask2 = new FutureTask<>(myCallable2);
Thread thread2 = new Thread(stringFutureTask2);
thread2.start();
try {
String sum = stringFutureTask.get();
System.out.println("子线程一:"+sum);
String sum2 = stringFutureTask2.get();
System.out.println("子线程二:"+sum2);
} catch (Exception e) {
e.printStackTrace();
}
System.out.println("主线程执行");
}FutureTask的API1
2
3子线程一:1-n的累加结果为:5050
子线程二:1-n的累加结果为:55
主线程执行
方法三优缺点:
- 优点:线程任务类知识实现接口,可以继续继承类和实现接口,扩展性强
- 可以在线程执行完毕后去获取线程执行的结果
- 缺点:编码复杂一点
三种方式对比
Thread常用API
当有很多线程在执行的时候,我们怎么去区分这些线程
- 此时需要使用Thread类的常用返回:getName(),setName(),currentThread()等
Thread获取和设置线程名称
1 | public class MyThread extends Thread { |
1 | public static void main(String[] args) { |
运行结果
1 | Thread-0 |
Thread类的线程休眠方法
间隔三秒打印一次结果
1 | public static void main(String[] args) throws Exception { |
线程安全
线程安全问题
- 多个线程同时操作同一个共享资源的时候就可能会出现业务安全问题,称为线程安全问题
线程安全问题出现的原因
- 存在多线车呢个并发
- 同时访问共享资源
- 存在修改共享资源
取钱模型演示**
- 需求:小明和小红有一个共同账户,余额是十万元
- 如果小明和小红同时来取钱,两人都要取钱十万元,可能会出现什么样的问题呢?
代码演示
1 | public class Account { |
1 | /* |
1 | public class ThreadSafeDemo { |
运行结果
1 | 小红来取钱,当前余额100000.0 取钱成功,取出100000.0元 |
线程同步
- 为了解决线程安全问题
线程同步的核心思想 - 加锁,把共享资源上锁,每次只能一个线程进入访问,访问完毕后解锁,然后其他线程才能进入
方式一:同步代码块
- 作用:把出现线程安全问题的核心代码给上锁
- 原理:每次只能一个线程进入,执行完毕后自动解锁,其他系acne很难过才可以进来执行
锁对象要求 - 理论上,锁对象只要对于当前同时执行的线程来说是用一个对象即可运行结果
/Account 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17public void drawMoney(Double money) {
// 先获取是谁来取钱
String name = Thread.currentThread().getName();
synchronized ("synchronized") {
// 判断账户中的余额是否足够
if (this.money >= money){
// 取钱操作
System.out.println(name +"来取钱,当前余额"+this.money+ " 取钱成功,取出"+money+"元");
// 更新余额
this.money -= money;
System.out.println(name+"取走钱后余额为:"+this.money);
}else {
System.out.println(name+"来取钱,余额不足");
}
}
}锁对象用任意唯一的对象好不好呢1
2
3小明来取钱,当前余额100000.0 取钱成功,取出100000.0元
小明取走钱后余额为:0.0
小红来取钱,余额不足 - 不好,会影响其他无关线程的执行
锁对象的规范要求
- 规范上:建议使用共享资源作为锁对象
- 对于实例方法建议使用this作为锁对象
1
synchronized (this)
- 对于静态方法建议使用字节码(类名.class)对象作为锁对象
方式二:同步方法
- 作用:把出现线程安全问题的核心方法给上锁
- 原理:每次只能一个线程进入,执行完毕后自动解锁,其他线程才可以进入
格式
1 | public synchronized void drawMoney(Double money) {……核心代码……} |
同步方法的底层原理
- 同步方法其实底层也是有隐式锁对象的,知识锁的范围是整个方法
- 如果方法是实例方法:同步方法默认用this作为锁对象。但是代码要高度面向对象
- 如果方法是静态方法:同步方法默认用类名.class作为锁对象
是用同步代码块好还是同步方法好一点?
- 同步代码块锁的范围更小,同步方法锁的范围更大
- 每次只能一个线程占锁进入访问
线程池
什么是线程池
- 线程池就是一个可以复用线程的技术
不使用线程池的问题
- 如果用户每发起一个请求,后台就创建一个线程来处理,下次新任务来了又要创建新线程,而创建新线程的开销是很大的,这样会严重影响系统的性能
睡代表线程池
- 线程池接口:ExecutorService
如何得到线程池对象
- 方式一:使用ExecutorService实现类ThreadPoolExecutor自创建一个线程池对象
- 方式二:使用Executors(线程池的工具类)调用方法返回不同特点的线程池对象
ThreadPoolExecutor构造器的参数说明
线程池常见面试题
临时线程什么时候创建?
- 新任务提交的时候发现核心线程都在忙,任务队列也满了,而且还可以创建临时线程,此时才会创建临时线程
什么时候会开始拒绝任务? - 核心线程和临时线程都在忙,任务队列也满了,新的任务过来的时候才会开始拒绝任务
线程池处理Runnable任务
ThreadPoolExecutor创建线程池对象示例
ExecutorService的常用方法
新任务拒绝策略
案例演示
1 | public class MyRunnable implements Runnable { |
1 | public static void main(String[] args) { |
运行结果
1 | Exception in thread "main" java.util.concurrent.RejectedExecutionException: Task threadPool.MyRunnable@6e8cf4c6 rejected from java.util.concurrent.ThreadPoolExecutor@12edcd21[Running, pool size = 5, active threads = 5, queued tasks = 2, completed tasks = 0] |
线程池如何处理Runnable任务
- 使用ExecutorService的方法
- void execute(Runnable target)
线程池如何处理Callable任务
1 | // 定义一个任务类实现Callable接口 申明任务执行结果的数据类型 |
1 | public static void main(String[] args) throws ExecutionException, InterruptedException { |
运行结果
1 | pool-1-thread-1 执行1-1结果为:1 |
Executors得到线程池对象的常用方法
- Executor:线程池的工具类通过调用返回值不同类型的线程池对象
注意:Executor的底层也是基于线程池的实现类ThreadPoolExecutor创建线程池对象的
1 | public static void main(String[] args) { |
Executors使用可能存在的陷阱
- 大型并发系统环境使用Executors如果不注意可能会出现系统风险
并发和并行
- 正在运行的程序就是一个独立的进程,线程是属于进程的,多个线程其实是并行与并发同时进行的
并发的理解:
- CPU同时处理线程的数量有限
- CPU会轮询为系统的每个线程服务,由于CPU切换的速度很快,给我们的感觉是这些线程咋同时执行,这就是并发
并行的理解:
- 在同一个时刻上,同时又多个线程在被CPU处理并执行
线程的声明周期
线程的状态
- 线程的状态:也就是线程从生到死的过程,以及中间经历的各种状态及状态转换
java线程的状态
- java一共定义了6种状态
- 6种状态都定义在了Thread类的内部枚举类中
线程的六种状态
- 新建状态(NEW)
- 就绪状态(RUNNABLE)
- 阻塞状态(BLOCKED)
- 等待状态(WAITING)
- 计时等待(TIMED_WAITING)
- 结束状态(TERMINATE)
本博客所有文章除特别声明外,均采用 CC BY-NC-SA 4.0 许可协议。转载请注明来自 Abulivyet!
评论