九、多线程
9.1 多线程概述
- 充分的利用计算机资源,执行不同的操作
- 同时运行多个独立的任务,每个任务对应一个进程,每个进程可产生多个线程
9.1.1 进程与线程
1. 进程
- 是指一个内存中运行的应用程序,每个进程都有一个独立的内存空间
- 有独立的内存空间和系统资源
- 程序就是进程
2. 线程
-
CPU调度和分派的基本单位
-
是进程中的一个执行路径,共享一个内存空间,线程之间可以自由切换,并发执行,一个进程最少有一个线程
-
线程实际上是在进程基础之上的进一步划分,一个进程启动之后,里面的若干执行路径又可以划分成若干个线程。
-
进程中执行运算的最小单位,可完成一个独立的顺序控制流程
3. 进程与线程的关系
9.1.2 多线程的运行机制
- 多线程是指一个进程同时运行了多个线程,并发执行的方式,来完成不同的工作
- 多线程是并发运行的,而非并行运行,多个线程交替执行,并非同时执行
- 多个线程交替占用CPU资源,而非真正的并行执行
9.1.3 多线程的优势
1.充分利用CPU资源,在CPU处于空闲状态时,运行其他线程,提高资源利用率
- 简化编程模式,将一个复杂的进程分为多个线程,每个线程实现简单的流程,简化程序逻辑,方便编码和维护
- 用户良好体验。多个线程交替运行,减少或避免了程序阻塞或意外情况发生造成的响应时间过慢现象,减少用户等待时间,提升用户体验
9.2 多线程编程
- Java 语言提供了 java.lang.Thread 类支持多线程编码
9.2.1 Thread 类介绍
- Thread 类提供了大量的方法来控制和操作线程
方法 | 描述 | 类型 |
---|---|---|
Thread() | 创建Thread对象 | 构造方法 |
Thread(Runnable target) | 创建Thread对象,target为run()方法被调用的对象 | 构造方法 |
Thread(Runnable target , String name) | 创建Thread对象,target为run()方法被调用的对象,name为新线程的名称 | 构造方法 |
void run() | 执行任务操作的方法 | 实例方法 |
void start() | 使该线程开始执行,JVM将调用该线程的run()方法 | 实例方法 |
void sleep(long millis) | 在指定的毫秒数内让当前正在执行的线程休眠(暂停执行),需要异常处理 | 静态方法 |
Thread currentThread() | 返回当前线程对象的引用 | 静态方法 |
-
在 Java 程序启动时,一个线程立即随之启动,这个线程称之为主线程
- main 方法就是主线程入口
- 主线程是产生其他子线程的线程
- 主线程通常必须最后完成执行,因为他执行各种关闭动作
-
使用Thread 类的方法获取主线程信息
public class Test01 {
public static void main(String[] args) {
//获取线程信息
Thread t=Thread.currentThread();
//获取当前线程名字
System.out.println("当前线程:"+t.getName());
//修改线程名
t.setName("MainThread");
System.out.println("当前线程:"+t.getName());
}
}
- 运行结果
当前线程:main
当前线程:MainThread
- Java 语言中,实现多线程分两种方式
- 继承 Thread 类
- 实现 Runnable 接口
9.2.2 继承 Thread 类创建线程类
- 继承 Thread 类实现线程,自定义线程类时,必须满足如下要求
- 此类必须继承 Thread 类
- 重写 Thread 类的 run() 方法,将线程执行的代码下载 run() 方法中
- 线程从 run() 方法开始执行,它是线程执行的起点
- 线程对象调用 start() 方法时启动线程,会自动调用 run()方法
- 实际运行中应调用 start() 方法
- 演示代码
创建自定义线程
/*
*创建自定义线程
*/
public class MyThread extends Thread{
//设置线程名字
public MyThread(String name){
super.setName(name);
}
//重写 Thread 类的 run() 方法
@Override
public void run() {
//执行线程任务的代码
for (int i = 0; i < 25; i++) {
System.out.println("我是线程"+getName()+"==>"+i);
}
}
}
线程测试类
/*
*线程测试类
*/
public class Test {
public static void main(String[] args) {
//线程1
MyThread t1=new MyThread("线程1");
//使用 start() 方法启动线程
t1.start();
//线程2
MyThread t2=new MyThread("线程2");
t2.start();
}
}
运行结果
我是线程线程2==>0
我是线程线程1==>0
我是线程线程2==>1
我是线程线程1==>1
我是线程线程2==>2
我是线程线程2==>3
我是线程线程2==>4
我是线程线程2==>5
我是线程线程2==>6
我是线程线程2==>7
我是线程线程2==>8
我是线程线程2==>9
我是线程线程2==>10
我是线程线程2==>11
我是线程线程2==>12
我是线程线程2==>13
我是线程线程2==>14
我是线程线程2==>15
我是线程线程2==>16
我是线程线程2==>17
我是线程线程2==>18
我是线程线程2==>19
我是线程线程2==>20
我是线程线程2==>21
我是线程线程2==>22
我是线程线程2==>23
我是线程线程2==>24
我是线程线程1==>2
我是线程线程1==>3
我是线程线程1==>4
我是线程线程1==>5
我是线程线程1==>6
我是线程线程1==>7
我是线程线程1==>8
我是线程线程1==>9
我是线程线程1==>10
我是线程线程1==>11
我是线程线程1==>12
我是线程线程1==>13
我是线程线程1==>14
我是线程线程1==>15
我是线程线程1==>16
我是线程线程1==>17
我是线程线程1==>18
我是线程线程1==>19
我是线程线程1==>20
我是线程线程1==>21
我是线程线程1==>22
我是线程线程1==>23
我是线程线程1==>24
- 已启动的线程不能重复调用 start() 方法,否则会抛出异常
- sleep() 方法在 run() 方法内使用,用来控制程序线程的休眠时间,sleep() 方法必须进行异常处理
注意 线程对象调用 start() 方法和 run() 方法截然不同,前者是启动线程,后者是调用实例方法,在实际应用中切勿混淆
9.2.3 实现 Runnable 接口创建线程类
- 使用 Thread 类的方式创建线程,子类无法在继承其他父类
- 可以通过实现 Runnable 接口的方式创建线程
- Runnable 接口只有一个抽象类 run ,其他方法都要借助于 Thread 类
- 演示代码
实现 Runnable 接口方式创建线程类
/*
* 实现 Runnable 接口方式创建线程类
* */
public class MyRunnable implements Runnable{
@Override
public void run() {
//线程内容
}
}
测试类
/*
* 测试类
* */
public class Test03 {
public static void main(String[] args) {
MyRunnable mr=new MyRunnable();
//通过 Thread 类创建线程对象
Thread t=new Thread(mr);
//启动线程
t.start();
}
}
9.3 线程的转换状态
- 新建的线程通常会在五种状态中转换: 新建、就绪、运行、阻塞、死亡
- 五种状态组成了线程的生命周期
9.3.1 线程的生命周期
线程的转换状态
(1)新建状态: 一个 Thread 类或子类的对象被声明并创建,此对象已经分配了内存空间和资源,但是还未被调度
**(2)就绪状态:**也称为可运行状态,也就是调用 start() 方法后,此时已经具备了运行的条件,但是还未被运行,进入线程列队,等待使用 CPU 资源
**(3)运行状态:**当就绪状态被调度并且获得处理器资源后便进入运行状态,表示线程正在运行,拥有了 CPU 的占用权,如果线程不让出 CPU 的控制权,则会一直运行完毕
让出 CPU 控制权的情况
- 线程运行完毕
- 有比当前线程优先级更高的线程抢占了 CPU
- 线程休眠
- 线程因等待某个资源而处于阻塞状态
**(4)阻塞状态:**一个正在运行的线程因某种特殊情况需要让出 CPU 并暂停时终止运行,线程处于不可运行的状态被称为阻塞状态,线程当被阻塞时不能进入就绪状态的排队队列,只有当阻塞的运用被取消时,线程才可以转为就绪状态
**(5)死亡状态:**一个线程的 run() 方法运行完毕,表示该线程已死亡,死亡状态的线程将不具备运行能力。
导致线程死亡的原因
- 正常运行的线程完成了全部工作,即运行完 run() 方法的最后一条语句
- 当进程停止运行时,该进程中的线程将被强行终止
9.4 线程调度相关方法
9.4.1 常用的线程操作方法
方 法 | 说 明 |
---|---|
int getPriority() | 返回线程的优先级 |
void setPrority(int newPriority) | 更改线程的优先级 |
boolean isAlive() | 测试线程是否处于活动状态 |
void join() | 进程中的其它线程必须等待该线程终止后才能执行,需要异常处理 |
void interrupt() | 中断线程 |
void yield() | 暂停当前正在执行的线程对象,并执行其他线程 |
9.4.2 线程的优先级
- 每个线程运行时都具有一定的优先级,优先级高的线程获得较多的运行机会
- 每个线程的默认优先级是相同的
- 默认情况下,主线程 main 具有普通优先级
- Thread 类 提供了 getPriority() 方法设置线程的优先级,范围是1~10
- 也可以使用Thread 类的三个静态常量设置线程的优先级
- MAX_PRIORITY:值是10,表示优先级最高
- MIN_PRIORITY:值是1,表示优先级最低
- NORM_PRIORITY:值是5,表示普通优先级
- 代码演示
使用 实现 Runnable 接口创建多线程
/*
* 使用 实现 Runnable 接口创建多线程
* */
public class MyRunnable implements Runnable{
@Override
public void run() {
//输出0-25
for (int i = 0; i <=25 ; i++) {
System.out.println("我是"+Thread.currentThread().getName()+":"+i);
}
}
}
测试类
/*
* 测试类
* */
public class Test {
public static void main(String[] args) {
//通过构造方法设置多线程名称
Thread t1=new Thread(new MyRunnable(),"线程1");
Thread t2=new Thread(new MyRunnable(),"线程2");
Thread t3=new Thread(new MyRunnable(),"线程3");
//设置优先级
//优先级最高 10
t1.setPriority(Thread.MAX_PRIORITY);
//优先级最低 1
t2.setPriority(Thread.MIN_PRIORITY);
//普通优先级 5
t3.setPriority(Thread.NORM_PRIORITY);
//启动线程
t1.start();
t2.start();
t3.start();
}
}
9.4.2 线程的强制运行
- 在线程操作中,可以使用 join() 方法 让一个线程强制运行
- 强制运行期间,其他线程无法运行,必须等此线程运行完毕才可以继续运行
- join() 方法需要进行异常处理 InterruptedExcpetion 异常
- 代码演示
线程类
/*
* 线程类
* */
public class MyThread extends Thread {
public MyThread(String name){
super.setName(name);
}
@Override
public void run() {
for (int i = 0; i <= 25; i++) {
System.out.println("我是"+getName()+":"+i);
}
}
}
测试类
/*
* 测试类
* */
public class Test {
public static void main(String[] args) {
Thread t=new MyThread("线程1");
//子线程 线程1输出
t.start();
try {
//强制运行线程1
t.join();
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
//主线程 main 输出
for (int i = 0; i <= 25; i++) {
System.out.println("我是"+Thread.currentThread().getName()+":"+i);
}
}
}
运行结果
我是线程1:0
我是线程1:1
我是线程1:2
我是线程1:3
我是线程1:4
我是线程1:5
我是线程1:6
我是线程1:7
我是线程1:8
我是线程1:9
我是线程1:10
我是线程1:11
我是线程1:12
我是线程1:13
我是线程1:14
我是线程1:15
我是线程1:16
我是线程1:17
我是线程1:18
我是线程1:19
我是线程1:20
我是线程1:21
我是线程1:22
我是线程1:23
我是线程1:24
我是线程1:25
我是main:0
我是main:1
我是main:2
我是main:3
我是main:4
我是main:5
我是main:6
我是main:7
我是main:8
我是main:9
我是main:10
我是main:11
我是main:12
我是main:13
我是main:14
我是main:15
我是main:16
我是main:17
我是main:18
我是main:19
我是main:20
我是main:21
我是main:22
我是main:23
我是main:24
我是main:25
由此可见,代码是先运行完 “线程1” 之后才开始运行 主线程的
9.4.3 线程的礼让
- 当程序运行中执行了 Thread 类的 yield() 静态方法 后,系统将会选择其他相同或优先级更高的线程运行
- 执行Thread 类的 yield() 静态方法会优先让其他线程运行,但不会停止自己的线程,不能保证一定会实现礼让
- 代码演示
线程类
/*
* 线程类
* */
public class MyThread extends Thread{
public MyThread(String name){
super.setName(name);
}
@Override
public void run() {
for (int i = 0; i <=25; i++) {
System.out.println("我是"+getName()+"==>"+i);
//如果是5的倍数 则让线程礼让
if(i%5==0){
Thread.yield();
}
}
}
}
测试类
/*
* 测试类
* */
public class Test06 {
public static void main(String[] args) {
MyThread t1=new MyThread("线程1");
t1.start();
//主线程
for (int i = 0; i <=25; i++) {
System.out.println("我是"+Thread.currentThread().getName()+"==>"+i);
//如果是5的倍数 则让线程礼让
if(i%5==0){
Thread.yield();
}
}
}
}
运行结果
我是main==>0
我是线程1==>0
我是main==>1
我是main==>2
我是main==>3
我是main==>4
我是线程1==>1
我是线程1==>2
我是线程1==>3
我是main==>5
我是线程1==>4
我是线程1==>5
我是main==>6
我是main==>7
我是main==>8
我是main==>9
我是main==>10
我是线程1==>6
我是线程1==>7
我是线程1==>8
我是线程1==>9
我是线程1==>10
我是线程1==>11
我是线程1==>12
我是线程1==>13
我是main==>11
我是线程1==>14
我是线程1==>15
我是线程1==>16
我是线程1==>17
我是线程1==>18
我是线程1==>19
我是线程1==>20
我是线程1==>21
我是线程1==>22
我是线程1==>23
我是线程1==>24
我是线程1==>25
我是main==>12
我是main==>13
我是main==>14
我是main==>15
我是main==>16
我是main==>17
我是main==>18
我是main==>19
我是main==>20
我是main==>21
我是main==>22
我是main==>23
我是main==>24
我是main==>25
每当到5的倍数时,线程就有可能礼让,让其他线程运行
9.5 线程同步
9.5.1 为什么需要线程同步
- 线程都是独立且异步运行的,每个线程都包含了运行时所需要的数据和方法,不必关系其他线程的状态和行为
- 当两个线程需要操作共同的数据时,就要考虑其他线程的状态和行为
- 当两个线程运行条件都需要同一个资源时,多个线程操作同一共享资源时,则会带来数据不安全问题的原因
- 需要使用线程同步技术来解决,多个线程操作同一共享资源时,将引发数据不安全问题
9.5.2 实现线程的同步
- 当两个或多个线程需要访问同一个资源时,需要以某种顺序来确保该资源一时刻只能被一个线程使用
- 相当于为资源加上了一把锁,一旦被一个线程调用,别的线程将无法调用
- 实现线程同步有两种方法,同步代码块和同步方法
(1)同步代码块
-
使用 synchronized 关键字修饰的代码块被称为同步代码块
-
语法
//obj 为指定上锁的对象
public synchronized(obj){
//需要同步的代码块
}
- 不添加同步,模拟银行取钱,代码演示
账户类
/*
* 账户类
* */
public class User {
//余额
private double balance;
public User(double balance){
this.balance=balance;
}
//取款方法
public void getMoney(){
if(balance<=0){
System.out.println("没钱了");
return;
}
System.out.println("账户余额:"+this.balance);
this.balance-=1000;
System.out.println("取款1000,剩余余额:"+this.balance);
}
}
线程类
/*
* 线程类
* */
public class MyThread extends Thread{
//账户类
User user;
//确保是同一个账户
public MyThread(User user){
this.user=user;
}
@Override
public void run() {
//去取方法
this.user.getMoney();
}
}
测试类
/*
* 测试类
* */
public class Test07 {
public static void main(String[] args) {
User user=new User(1000);
//线程1
MyThread t1=new MyThread(user);
MyThread t2=new MyThread(user);
t1.start();
t2.start();
}
}
运行结果
账户余额:1000.0
账户余额:1000.0
取款1000,剩余余额:-1000.0
取款1000,剩余余额:0.0
-
由此可见,两个线程是同时调用资源的,当余额小于等于0时,并没有执行if内的操作,因为两个线程同时执行时,余额还没有被减少
-
添加同步代码块,模拟银行取钱,代码演示
账户类
/*
* 账户类
* */
public class User {
//余额
private double balance;
public User(double balance){
this.balance=balance;
}
//取款方法
public void getMoney(){
//表示 当有线程调用此方法时,锁定该类的状态,不能被其他线程所修改
synchronized(this){
if(balance<=0){
System.out.println("没钱了");
return;
}
System.out.println("账户余额:"+this.balance);
this.balance-=1000;
System.out.println("取款1000,剩余余额:"+this.balance);
}
}
}
线程类
/*
* 线程类
* */
public class MyThread extends Thread{
//账户类
User user;
//确保是同一个账户
public MyThread(User user){
this.user=user;
}
@Override
public void run() {
//去取方法
this.user.getMoney();
}
}
测试类
/*
* 测试类
* */
public class Test07 {
public static void main(String[] args) {
User user=new User(1000);
//线程1
MyThread t1=new MyThread(user);
MyThread t2=new MyThread(user);
t1.start();
t2.start();
}
}
运行结果
账户余额:1000.0
取款1000,剩余余额:0.0
没钱了
synchronized(this) 中 this 关键字引用的是当前对象,如果当前对象没有被其他线程所占用,则会开始执行synchronized(this) 关键字大括号内的同步代码
(2)同步方法
- 如果一个方法内的所有代码都需要被同步,则可以之间使用 synchronized 关键字来修饰整个方法
- 语法
访问修饰符 synchronized 返回值类型 方法名(参数列表){
//方法体
}
- 演示代码
代码和上方的一致,这边之间修改账户类内的方法
/*
* 账户类
* */
public class User {
//余额
private double balance;
public User(double balance){
this.balance=balance;
}
//取款方法
public synchronized void getMoney(){
//表示 当有线程调用此方法时,锁定该类的状态,不能被其他线程所修改
if(balance<=0){
System.out.println("没钱了");
return;
}
System.out.println("账户余额:"+this.balance);
this.balance-=1000;
System.out.println("取款1000,剩余余额:"+this.balance);
}
}
9.5.3 线程同步的特征
- 不同的线程在执行以同一个对象作为锁标记的同步代码块或同步方法时,因为要获得这个对象的锁而相互牵制
- 多个并发线程访问同一资源的同步代码块或同步方法时,同一刻只能有一个线程运行
- 同一时刻只能有一个线程进入synchronized(this)同步代码块
- 当一个线程访问一个synchronized(this)同步代码块时,其他synchronized(this)同步代码块同样被锁定
- 当一个线程访问一个synchronized(this)同步代码块时,其他线程可以访问该资源的非synchronized(this)同步代码
- 如果多个线程访问的不是同一共享资源,无需同步
9.5.4 线程安全的类型
- 线程的安全类型
方法是否同步 | 效率比较 | 适合场景 | |
---|---|---|---|
线程安全 | 是 | 低 | 多线程并发共享资源 |
非线程安全 | 否 | 高 | 单线程 |
- 如果程序所在的进程中,有多个线程同时运行,每次运行结果和单线程时运行结果是一样的,且其他变量的值也和预期相同,则当前程序是线程安全的
stem.out.println(“账户余额:”+this.balance);
this.balance-=1000;
System.out.println(“取款1000,剩余余额:”+this.balance);
}
}
### 9.5.3 线程同步的特征
- 不同的线程在执行以同一个对象作为锁标记的同步代码块或同步方法时,因为要获得这个对象的锁而相互牵制
- 多个并发线程访问同一资源的同步代码块或同步方法时,同一刻只能有一个线程运行
- 同一时刻只能有一个线程进入synchronized(this)同步代码块
- 当一个线程访问一个synchronized(this)同步代码块时,其他synchronized(this)同步代码块同样被锁定
- 当一个线程访问一个synchronized(this)同步代码块时,其他线程可以访问该资源的非synchronized(this)同步代码
- 如果多个线程访问的不是同一共享资源,无需同步
### 9.5.4 线程安全的类型
- **线程的安全类型**
| | 方法是否同步 | 效率比较 | 适合场景 |
| -------------- | ------------ | -------- | ---------------------- |
| **线程安全** | **是** | **低** | **多线程并发共享资源** |
| **非线程安全** | **否** | **高** | **单线程** |
- 如果程序所在的进程中,有多个线程同时运行,每次运行结果和单线程时运行结果是一样的,且其他变量的值也和预期相同,则当前程序是线程安全的