JavaLearn# (13)多线程:线程生命周期线程控制线程同步线程通信线程池ForkJoin框架
Posted LRcoding
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了JavaLearn# (13)多线程:线程生命周期线程控制线程同步线程通信线程池ForkJoin框架相关的知识,希望对你有一定的参考价值。
1. 进程和线程
-
程序:一段静态的代码,是应用程序执行的蓝本
-
进程:指一种正在运行的程序,有自己的地址空间
- 动态性(正在运行的程序)
- 并发性(同时运行)
- 独立性(QQ 和 微信互不干扰)
-
并发和并行的区别
- 并行(parallel):多个 CPU 同时执行多个任务,宏观和微观来看,都是同时执行
- 并发(concurrency):一个 CPU 同时执行多个任务(采用时间片轮转,A执行一段时间,B再执行一段时间)
-
线程
- 进程内部的一个执行单元,它是程序中一个单一的顺序控制流程,又被称为轻量级进程(lightweight process)
- 如果在一个进程中,同时运行了多个线程,用来完成不同的工作,则被称为多线程
- 线程特点:
- 轻量级进程
独立调度的基本单位
- 共享进程资源
- 可并发执行
-
线程和进程的区别:
区别 | 进程 | 线程 |
---|---|---|
根本区别 | 作为资源分配的单位(例:申请30M内存) | 调度和执行的单位(从30M中划分资源) |
开销 | 大:每个进程有自己独立的代码和数据空间 | 小:同一类线程共享代码和数据空间,每个线程有独立的运行栈和程序计数器 |
所处环境 | 在操作系统中能同时运行多个任务 | 在同一应用程序中有多个顺序流同时执行 |
分配内存 | 每个进程分配不同的内存区域 | 除了CPU,不会为线程分配资源,使用的都是进程的,线程组只能共享资源 |
包含关系 | 没有线程的进程可以被看作单线程的,如果一个进程内拥有多个线程,那么就是多个线程同时完成 | 线程是进程的一部分 |
2. 线程的定义方式
2.1 继承 Thread 类
- Thread 类常用方法:
- run():线程要执行的任务
- start():启动线程
- getName():获取线程名称
- getPriority():获取优先级
- Thread.currentThread():得到当前线程
- 启动 main 方法,自动创建 main 线程
创建一个线程类:(extends Thread
)
public class TreadDemo extends Thread{
/**
* 线程体:线程要执行的任务
*/
@Override
public void run() {
while (true) {
System.out.println("[A] 线程执行了" + this.getName() + " " + this.getPriority());
}
}
}
创建线程对象、启动线程:
// 创建线程对象
Tread treadDemo = new TreadDemo();
treadDemo.setName("设置线程名称"); // 设置线程的名称
treadDemo.setPriority(Thread.MAX_PRIORITY); // 设置线程的优先级
// 启动线程
treadDemo.start();
// treadDemo.run(); 此处为普通的方法调用,直接调用 run() 方法
2.2 实现 Runnable 接口
- 两种方式的比较:
- 继承 Thread 类:编程简单,但是单继承,无法继承其他类
- 实现 Runnable 接口:编程稍繁琐,可以再继承其他类,实现其他接口,
操作的为同一个任务
,便于多个线程共享同一个资源
定义一个线程类:(implements Runnable
)
public class RunnableDemo implements Runnable{
@Override
public void run() {
while (true) {
System.out.println("[A] 线程执行了" + Thread.currentThread().getName() + " "
+ Thread.currentThread().getPriority());
}
}
}
创建线程对象、启动线程
// 创建线程对象
Runnable runnable = new RunnableDemo();
Thread thread = new Thread(runnable); // 使用 runnable
// 启动线程
thread.setName("[A] 线程");
thread.setPriority(Thread.NORM_PRIORITY);
thread.start();
// 创建另一个线程对象
Thread thread1 = new Thread(runnable); // 使用同一个 runnable,操作的为同一个任务对象
thread1.setName("[A] 线程 2");
=====》 还可以直接使用匿名内部类的方式
Runnable runnable = new Runnable() {
@Override
public void run() {
while (true) {
System.out.println("[A] 线程执行了" + Thread.currentThread().getName() + " "
+ Thread.currentThread().getPriority());
}
}
};
// 创建一个线程
Thread thread = new Thread(runnable); // 使用 runnable
thread.setName("[A] 线程");
thread.setPriority(Thread.NORM_PRIORITY);
thread.start();
// 创建另一个线程
Thread thread1 = new Thread(runnable); // 使用同一个 runnable,操作的为同一个对象
thread1.setName("[A] 线程 2");
2.3 实现 Callable 接口
JDK 1.5后推出
- 可以有返回值,支持泛型的返回值
- 可以抛出检查异常
- 需要借助 FutureTask,获取返回结果等
定义一个线程类:(implements Callable
)
public class CallableDemo implements Callable<Integer> {
/**
* 有返回值,并且可以抛出异常
* @return
* @throws Exception
*/
@Override
public Integer call() throws Exception {
if (false) {
throw new Exception();
}
return new Random().nextInt(10);
}
}
创建线程对象、启动线程
// 创建线程
Callable<Integer> callable = new CallableDemo();
// 需要使用 FutureTask 进行操作
FutureTask<Integer> task = new FutureTask(callable);
// 最终放入的是 FutureTask 对象
Thread thread = new Thread(task);
// 启动线程
thread.start();
// 获取返回值
Integer i = task.get(); // 得不到返回值,就一直等待!!!!!!!!!!
task.get(3, TimeUnit.SECONDS); // 就等待 3 秒,获取不到就报错
System.out.println(i);
3. 线程的生命周期
- 新生状态:
- 用 new 创建一个线程对象后,该对象就进入了新生状态
- 在此时的线程,拥有自己的内存空间,通过
start()
方法进入就绪状态
- 就绪状态:
- 此时线程具备了运行条件,但还没分配到CPU
- 当系统选定一个等待执行的线程后,它就进入了执行状态,称为“CPU调度”
- 运行状态
- 执行自己的 run 方法中的代码,直到等待某资源而阻塞,或完成任务而死亡
- 如果在给定时间片内,没执行结束,就会被系统给换下来回到阻塞状态
- 阻塞状态
- 处于运行状态的线程,在某些情况下(执行了sleep方法、等待I/O设备等)将让出CPU 并暂停自己的运行,进入阻塞状态
- 只有当睡眠时间结束,或资源已获取到,才能进入就绪状态中等待,被系统选中后,从停止的位置继续运行
- 死亡状态
- 正常运行的线程,完成了所有的工作
- 线程被强制性的终止,如通过 stop 方法来终止一个线程【不推荐使用】
- 线程抛出未捕获的异常
4. 线程控制
可以对线程的生命周期进行干预
基础线程类:
public class MyThread extends Thread {
@Override
public void run() {
this.setPriority(6);
for (int i = 0; i < 100; i++) {
System.out.println("线程开始执行了:" + this.getName() + " " + this.getPriority());
}
}
}
4.1 join()
阻塞其他线程,等待该线程执行完之后,再执行其他线程
for (int i = 0; i < 100; i++) {
if (i == 50) {
Thread thread = new MyThread();
thread.setName("自定义线程类");
thread.start();
// 阻塞主线程,当 自定义线程 执行完,再执行主线程,【在 start() 方法之后】
thread.join();
}
System.out.println("主线程:" + Thread.currentThread().getName());
}
4.2 sleep()
让出 CPU,自身进入阻塞状态
Thread thread = new MyThread();
thread.setName("自定义线程类");
thread.start();
for (int i = 0; i < 100; i++) {
try {
Thread.sleep(1000); // 主线程休眠 1 秒,进入【阻塞状态】,时间到后再次进入【就绪状态】
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("主线程:" + Thread.currentThread().getName());
}
4.3 yield()
礼让 CPU,执行一会儿,然后退回到就绪状态,继续争抢CPU(可能会立即抢到,继续执行)
Thread thread = new MyThread();
thread.setName("自定义线程类");
thread.start();
for (int i = 0; i < 100; i++) {
Thread.yield(); // 主线程进行礼让,执行一会儿后,让出CPU,进入【就绪状态】
System.out.println("主线程:" + Thread.currentThread().getName());
}
4.4 setDaemon()
守护线程(寄生线程),启动它的线程执行完了,那么它也要停止执行
Thread thread = new MyThread();
thread.setName("自定义线程类");
thread.setDaemon(true); // 启动它的线程执行结束之后,它也停止执行,【在 start() 方法之前】
thread.start();
for (int i = 0; i < 100; i++) {
System.out.println("主线程:" + Thread.currentThread().getName());
}
4.5 interrupt()
并不是结束了线程,而是修改了线程的状态,需要线程类进行判断isInterrupted()
Thread thread = new MyThread();
thread.setName("自定义线程类");
thread.start();
for (int i = 0; i < 100; i++) {
System.out.println("主线程:" + Thread.currentThread().getName());
}
thread.interrupt(); // 修改线程的状态,告诉它该结束运行了
线程类修改为:
public class MyThread extends Thread {
@Override
public void run() {
while (this.isInterrupted()) { // 进行判断,是否该结束了
System.out.println("线程开始执行了:" + this.getName() + " " + this.getPriority());
}
}
}
5. 线程同步
5.1 问题的提出
-
场景:
-
多个用户同时操作一个银行账户。每次取款 400 元,取款前先检查余额是否足够,如果不够,放弃取款
例如 2 个人同时取 400,卡内还有 600 元
-
-
分析:
- 使用多线程解决
- 开发一个取款线程类,每个用户对应一个线程对象
- 多个线程共享同一个银行账户,使用 Runnable 方式
-
思路
- 创建银行账户类 Account
- 创建取款线程 AccountRunnable
- 创建测试类,进行测试
Account 类:
public class Account {
private int balance = 600;
/**
* 取款
* @param money
*/
public void withDraw(int money) {
this.balance -= money;
}
/**
* 查看余额
* @return
*/
public int getBalance() {
return balance;
}
}
AccountRunnable 类:
public class AccountRunnable implements Runnable {
private Account account = new Account();
/**
* 取款的步骤
*/
@Override
public void run() {
// 在这个流程中,需要保证【有一个人进入了,另一个人就无法进入】
// ======================== 取款开始 ===================================
if (account.getBalance() >= 400) {
try {
Thread.sleep(10); // 需要特别注意的地方
} catch (InterruptedException e) {
e.printStackTrace();
}
account.withDraw(400);
System.out.println(Thread.currentThread().getName() + " 取款成功,余额:" + account.getBalance());
} else {
System.out.println(Thread.currentThread().getName() + " 取款失败,余额:" + account.getBalance());
}
// ======================== 取款结束 ===================================
}
}
TestAccount 类:
public static void main(String[] args) {
// 创建 Runnable 对象
Runnable runnable = new AccountRunnable();
Thread user1 = new Thread(runnable, "张三");
user1.start();
Thread user2 = new Thread(runnable, "张三妻子");
user2.start();
}
可能出现的结果:
5.2 同步代码块
针对上面的问题,第一种解决方案:使用同步代码块
synchronized (account) { // 使用共享资源作为锁
// 在开始取款之前,先【加上一把锁】,保证【有一个人进入了,另一个人就无法进入】
// ======================== 取款开始 ===================================
if (account.getBalance() >= 400) {
try {
Thread.sleep(10); // 需要特别注意的地方
} catch (InterruptedException e) {
e.printStackTrace();
}
account.withDraw(400);
System.out.println(Thread.currentThread().getName() + " 取款成功,余额:" + account.getBalance());
} else {
System.out.println(Thread.currentThread().getName() + " 取款失败,余额:" + account.getBalance());
}
// ======================== 取款结束 ===================================
}
5.2.1 同步监视器(锁子)
synchronized (同步监视器) { 同步代码块 }
- 必须是引用数据类型,不能是基本数据类型
- 在同步代码块中,可以改变它的值,但是不能改变其引用,
建议使用 final 修饰同步监视器
- 一般使用共享资源作同步监视器即可
- 也可创建一个专门的同步监视器,没有任何业务意义
- 尽量不要使用 String 和包装类(Integer)作同步监视器
5.2.2 执行过程
- 第一个线程,来到同步代码块,发现同步监视器为 open 状态,需要改为 close,然后执行其中的代码
- 第一个线程,在执行过程中,发生了线程切换(阻塞),第一个线程就失去了 cpu,但是没有开锁 open
- 第二个线程,获取了cpu,来到同步代码块,发现同步监视器为 close 状态,无法执行其中的代码,也进入阻塞状态
- 第一个线程,再次获取 cpu,接着执行后续的代码;执行完成后,开锁 open
- 第二个线程,也再次获取到 cpu,来到同步代码块,发现为 open 状态,重复第一个线程的处理过程(加锁)
注意:同步代码块中,可以发生线程切换
,但是后续的线程,由于无法开锁,所以无法执行同步代码块
5.2.3 分析
-
加上锁之后,安全了,但是效率降低,而且可能出现死锁
-
多个代码块使用了同一个锁A,锁住一个代码块的同时,也把其他地方使用这把锁A的代码块锁住了
导致其他线程无法访问这些代码块
但是没有锁住使用其他锁B的代码块,其他线程可以执行这些代码块
5.3 同步方法
提取需要加锁的代码块,形成一个方法,给方法加锁(不能给 run() 方法加锁
)
public synchronized void withDraw() { // 【synchronized】 写在方法上
if (account.getBalance() >= 400) {
try {
Thread.sleep(10); // 需要特别注意的地方
} catch (InterruptedException e) {
e.printStackTrace();
}
account.withDraw(400);
System.out.println(Thread.currentThread().getName() + " 取款成功,余额:" + account.getBalance());
} else 以上是关于JavaLearn# (13)多线程:线程生命周期线程控制线程同步线程通信线程池ForkJoin框架的主要内容,如果未能解决你的问题,请参考以下文章