目录
Java多线程(一)
线程与进程基本介绍
并发和并行基本介绍
CPU调度基本介绍
主线程基本介绍
创建线程对象与相关方法
继承Thread类创建线程对象
多线程在内存中运行的原理
Thread类中常用的方法
Thread类中关于线程优先级的方法
守护线程与Thread类中关于守护线程的方法
礼让线程与Thread类中关于礼让线程的方法
插入线程与Thread类中关于插入线程的方法
实现Runnable接口创建线程对象
使用匿名内部类创建线程对象
使用继承or接口创建线程对象
线程安全
线程安全引入
解决线程安全
同步代码块解决线程不安全
同步方法解决线程不安全
死锁
线程状态
Java多线程(一)
本章中的概念部分都只是为了后面的程序执行更好理解,更深层的概念移步到Linux进程部分(待更新)
线程与进程基本介绍
进程:在内存中运行的程序实例,一般一个程序代表一个进程
线程:进程中最小的执行单元,一般线程负责进程中程序的运行,一个线程至少存在一个线程,也可以有多个线程,当存在多个线程时,一般称为多线程程序
可以简单理解为:当一个程序加载到内存中后就会开启一个进程,当程序需要执行某一个功能时就会开辟一个线程,该线程就是程序与CPU交流的通道,一个功能对应着一个线程,一个线程对应着一个通道
并发和并行基本介绍
并发:在同一个时刻,多个CPU(多核CPU)同时执行指令任务
并行:在同一个时刻,一个CPU执行多个指令任务
在CPU是单核时,CPU看似在同一时刻执行多个任务,实际上是CPU在执行任务中进行的高速切换,因为速度快所以人很难感知到任务执行的先后顺序
现在CPU基本上都是多核,可以理解为多个CPU,所以可以同一时间处理多个任务,每一个CPU管一个任务,但是依旧存在着高速切换,只是频率相对于单核CPU会变小,所以现在的CPU在执行指令时一般都是并行和并发同时存在
CPU调度基本介绍
CPU调用一般分为两种:
- 分时调度:让所有线程轮流获取到CPU的调度权,并且相对平均分配每个线程占用的CPU时间片
- 抢占式调度:多个线程轮流抢占CPU的使用权(哪个线程抢到了CPU的使用权,哪个线程先执行),一般都是优先级高的线程抢到的概率大,但不代表使用权一定属于优先级高的线程
Java程序都是抢占式调用
主线程基本介绍
主线程:CPU和内存之间专门为Java中的main
函数服务开辟的线程
创建线程对象与相关方法
在Java中,创建线程对象一共有两种方式:
- 普通类继承
Thread
类,重写Thread
中的run
方法 - 普通类实现
Runnable
接口,重写接口中的run
方法
继承Thread
类创建线程对象
继承Thread
类后重写Thread
中的run
方法,该方法用于线程中执行的任务,例如循环等。创建完自定义线程类后就可以通过自定义类创建一个线程对象,使用该对象调用start()
方法启动线程,例如:
// 自定义线程类
public class Thread01 extends Thread {@Overridepublic void run() {for (int i = 0; i < 10; i++) {System.out.println("Thread01..." + i);}}
}// 主线程
public class Test {public static void main(String[] args) {// 创建自定义线程类对象Thread01 t1 = new Thread01();// 调用start方法启动线程t1.start();// 在主线程中执行其他任务for (int i = 0; i < 10; i++) {System.out.println("main..." + i);}}
}
因为Java程序都是抢占式调用,所以会出现交替执行的情况,也会出现主线程先执行完,再执行自定义线程的任务
需要注意,不要对同一个线程对象多次调用start
方法,也不要显式调用run
方法,直接调用run
方法就不会被认为是线程启动执行任务
多线程在内存中运行的原理
在Java程序中,当存在多个线程时,对于主线程来说是一个栈空间,而其余线程相当于其他的栈空间,如下图所示:
两个线程相互抢占使用权,但是因为main
函数有更大的概率抢到,所以可能出现主线程任务先执行完再执行自定义线程任务
Thread
类中常用的方法
void start()
方法:启动进程,JVM会自动调用对应线程的run
方法void run()
方法:设置线程中的任务,该方法是Thread
类实现了Runnable
接口后重写的方法String getName()
方法:获取调用对象的线程名称,默认情况下线程名称组成为:Thread+编号
void setName(String name)
方法:设置调用对象的线程名称static Thread currentThread()
:获取当前已经获取到CPU使用权的线程static void sleep(long millis)
:设置线程睡眠,参数表示睡眠毫秒数
需要注意,Thread
中的sleep
方法会抛出异常,如果在自定义线程类中使用sleep
方法时,不可以使用throws
处理异常,只能使用try...catch
,但是如果在主线程则可以直接使用
基本使用实例:
// 自定义线程
public class Thread01 extends Thread {@Overridepublic void run() {for (int i = 0; i < 10; i++) {System.out.println(getName() + "..." + i);}}
}// 主线程
public class Test {public static void main(String[] args) throws InterruptedException {// 创建自定义线程类对象Thread01 t1 = new Thread01();// 调用start方法启动线程t1.start();// 在主线程中执行其他任务for (int i = 0; i < 10; i++) {Thread.sleep(1000L);System.out.println(Thread01.currentThread().getName() + "..." + i);}}
}
Thread
类中关于线程优先级的方法
void setPriority(int newPriority)
:设置调用对象的线程优先级,线程优先级越高,抢到CPU使用权的概率越大,但是概率大不代表一定可以抢到。Java中线程优先级有10个等级,其中1表示最小优先级,10表示最大优先级,默认优先级为5int getPriority()
:获取调用对象的线程优先级
基本使用示例:
// 自定义线程类
public class Thread01 extends Thread {@Overridepublic void run() {for (int i = 0; i < 10; i++) {System.out.println(getName() + "..." + i);}}
}// 主线程
public class Test {public static void main(String[] args) throws InterruptedException {// 创建自定义线程类对象Thread01 t1 = new Thread01();Thread01 t2 = new Thread01();// 设置/获取线程优先级t1.setPriority(1);t2.setPriority(10);System.out.println("t1.getPriority() = " + t1.getPriority());System.out.println("t2.getPriority() = " + t2.getPriority());// 调用start方法启动线程t1.start();t2.start();}
}
守护线程与Thread
类中关于守护线程的方法
守护进程:守护线程表示当前线程的任务会随着所有非守护线程结束而结束,但是在非守护线程结束时,守护线程一般不会是立即结束,因为在非守护线程结束时需要与守护线程进行结束信号的通信,这段时间中守护线程依旧在执行
需要注意,当出现一个守护线程,多个非守护线程时,守护线程会等到所有非守护线程结束才会结束
在Java中,可以使用void setDaemon(boolean on)
将调用对象所在的线程设置为守护线程或者取消设置守护线程,参数取值只有两种:true
(开启守护线程)和false
(关闭守护线程)
基本使用实例:
// 自定义线程
public class Thread01 extends Thread {@Overridepublic void run() {for (int i = 0; i < 100; i++) {System.out.println(getName() + "..." + i);}}
}// 主线程
public class Test {public static void main(String[] args) throws InterruptedException {// 创建自定义线程类对象Thread01 t1 = new Thread01();Thread01 t2 = new Thread01();// 设置t1进程为守护进程t1.setDaemon(true);// 调用start方法启动线程t1.start();t2.start();// 在主线程中执行其他任务for (int i = 0; i < 10; i++) {System.out.println(Thread01.currentThread().getName() + "..." + i);}}
}
礼让线程与Thread
类中关于礼让线程的方法
礼让线程:默认情况下Java的线程对CPU使用权是抢占式,而礼让线程是为了让正在抢夺使用权的线程尽可能相对平衡(不是绝对平衡),从而达到二者交替执行
在Java中,设置礼让线程的方法为:static void yield()
基本使用实例:
// 自定义线程
public class Thread01 extends Thread {@Overridepublic void run() {for (int i = 0; i < 10; i++) {// 设置礼让线程Thread.yield();System.out.println(getName() + "..." + i);}}
}// 主线程
public class Test {public static void main(String[] args) throws InterruptedException {// 创建自定义线程类对象Thread01 t1 = new Thread01();Thread01 t2 = new Thread01();// 调用start方法启动线程t1.start();t2.start();}
}
插入线程与Thread
类中关于插入线程的方法
插入线程:让调用对象所在线程尽可能优先执行完,再执行其他进程
在Java中对应插入线程的方法为:void join()
基本使用实例:
// 自定义线程
public class Thread01 extends Thread {@Overridepublic void run() {for (int i = 0; i < 10; i++) {System.out.println(getName() + "..." + i);}}
}// 主线程
public class Test {public static void main(String[] args) throws InterruptedException {// 创建自定义线程类对象Thread01 t1 = new Thread01();Thread01 t2 = new Thread01();// 调用start方法启动线程t1.start();// 阻塞当前线程,等待t1线程执行完毕t1.join();// 在主线程中执行其他任务for (int i = 0; i < 10; i++) {System.out.println(Thread01.currentThread().getName() + "..." + i);}}
}
实现Runnable
接口创建线程对象
本方法创建线程对象与继承Thread
方式类似,但因为Runnable
是接口,所以必须重写对应的run
方法,使用实现类创建对象(目前不是线程对象),将该对象使用Thread
中的构造方法:Thread(Runnable target)
创建线程对象
// 自定义线程
public class Thread02 implements Runnable{@Overridepublic void run() {for (int i = 0; i < 10; i++) {System.out.println(Thread.currentThread().getName() + "..." + i);}}
}// 主线程
public class Test01 {public static void main(String[] args) {// 创建实现类对象Thread02 t = new Thread02();// 实现类通过Thread构造函数创建线程类对象Thread t1 = new Thread(t);t1.start();for (int i = 0; i < 10; i++) {System.out.println(Thread.currentThread().getName()+"..."+i);}}
}
如果想为线程设置名字,可以使用void setName(String name)
方法,也可以使用构造函数,例如:
public class Test01 {public static void main(String[] args) {// 创建实现类对象Thread02 t = new Thread02();// 实现类通过Thread构造函数创建线程类对象Thread t1 = new Thread(t, "线程1");t1.start();for (int i = 0; i < 10; i++) {System.out.println(Thread.currentThread().getName()+"..."+i);}}
}
使用匿名内部类创建线程对象
基本使用方式如下:
public class Test02 {public static void main(String[] args) {// 使用对象名调用start方法Runnable r = new Runnable() {@Overridepublic void run() {for (int i = 0; i < 10; i++) {System.out.println(Thread.currentThread().getName() + "..." + i);}}};Thread t1 = new Thread(r);t1.start();// 使用匿名内部类new Thread(new Runnable() {@Overridepublic void run() {for (int i = 0; i < 10; i++) {System.out.println(Thread.currentThread().getName() + "..." + i);}}}).start();for (int i = 0; i < 10; i++) {System.out.println(Thread.currentThread().getName()+"..."+i);}}
}
使用继承or接口创建线程对象
如果当前自定义线程类已经继承了其他类,则选择通过实现Runnable
接口创建线程对象,否则使用继承创建线程对象,因为Java不支持多继承
如果需要多个线程对象使用共享同一个资源时,可以考虑使用实现Runnable
接口的方式创建线程对象
线程安全
线程安全引入
当同一个数据被多个线程获取到时,就会出现线程安全问题
例如,在买票的过程中,一共有三个人一起买票,如果至少两个人同时拿到同一张票就代表出现了线程不安全
// 自定义线程类
public class Thread03 extends Thread{static int tickets = 10;@Overridepublic void run() {while (tickets > 0) {System.out.println("线程" + Thread.currentThread().getName() + "获取到第" + tickets + "张票");tickets--;}}
}// 主线程
public class Test03 {public static void main(String[] args) {Thread03 t1 = new Thread03();Thread03 t2 = new Thread03();Thread03 t3 = new Thread03();t1.start();t2.start();t3.start();}
}
例如下面的情况:
解决线程安全
在Java中,解决线程安全的方式就是给有线程不安全的代码加锁,并且必须是同一把锁,否则该锁无效。给线程加锁的方式有两种:
- 使用同步代码块,使用格式如下:
synchronized (唯一任意对象){// 出现线程不安全的代码
}
- 同步方法:包括静态同步方法和非静态同步方法,使用格式如下:
// 静态方法
权限修饰符 static synchronized 返回值类型 方法名 {// 方法体
}// 非静态方法
权限修饰符 synchronized 返回值类型 方法名 {// 方法体
}
给线程不安全的代码加锁后,当一个线程进入后就会「加锁」,此时其他线程无法再进入对应的代码,当前面的线程执行完毕后离开,该锁就会「解锁」,此时其他线程就会进入重复上面的过程,在此过程中,哪一个线程先执行取决于哪一个线程先抢到CPU的使用权
同步代码块解决线程不安全
以前面的买票为例,解决方案如下:
// 修改后的自定义线程类(使用继承+同步代码块)
public class Thread03 extends Thread {static int tickets = 100;// 任意对象加锁static Object obj = new Object();@Overridepublic void run() {while (true) {synchronized (obj) {if (tickets > 0) {System.out.println(Thread.currentThread().getName() + "..." + tickets);tickets--;}else {break;}}}}
}// 主线程
public class Test03 {public static void main(String[] args) {Thread03 t1 = new Thread03();Thread03 t2 = new Thread03();Thread03 t3 = new Thread03();t1.start();t2.start();t3.start();}
}
修改后的代码就可以解决线程不安全的问题
上面的代码也可以通过实现Runnable
类的方式创建线程对象实现,例如下面的代码:
// 使用接口实现+同步代码块
public class Thread04 implements Runnable{int tickets = 100;Object obj = new Object();@Overridepublic void run() {while (true) {synchronized (obj) {if(tickets > 0) {System.out.println(Thread.currentThread().getName() + "..." + tickets);tickets--;}else {break;}}}}
}// 主进程
public class Test03 {public static void main(String[] args) {// 使用实现+同步代码块Thread04 tickets = new Thread04();new Thread(tickets).start();new Thread(tickets).start();new Thread(tickets).start();}
}
使用接口实现与继承的不同的是,锁对象和票成员不需要使用static
修饰,因为此时三个线程共用一个tickets
和obj
成员,示意图如下:
同步方法解决线程不安全
- 静态同步方法
以继承+同步方法为例
对于静态同步方法来说,其默认锁是对象类
// 使用继承+静态同步方法
public class Thread05 extends Thread{static int tickets = 100;// 静态方法public static synchronized void sale() {if(tickets > 0) {System.out.println(Thread.currentThread().getName() + "..." + tickets);tickets--;}}@Overridepublic void run() {while (true) {sale();if(tickets <= 0) {break;}}}
}
- 非静态同步方法
非静态同步方法只能使用接口的方式创建线程对象,因为使用继承无法保证 this
只指向一个对象
对于非静态同步方法,其默认锁是this
// 使用实现+非静态同步方法
public class Thread06 implements Runnable{static int tickets = 100;// 非静态同步方法public synchronized void sale() {if(tickets > 0) {System.out.println(Thread.currentThread().getName() + "..." + tickets);tickets--;}}@Overridepublic void run() {while (true) {sale();if (tickets <= 0) {break;}}}
}
死锁
前面解决线程安全时涉及到加锁,但是如果出现锁嵌套,就容易出现死锁问题,例如下图:
代码实现:
// 锁1
public class LockA {public static LockA lockA = new LockA();
}// 锁2
public class LockB {public static LockB lockB = new LockB();
}// 死锁
public class DieLock implements Runnable{private boolean flag;public DieLock(boolean flag) {this.flag = flag;}@Overridepublic void run() {if (flag){synchronized (LockA.lockA){System.out.println("if...lockA");synchronized (LockB.lockB){System.out.println("if...lockB");}}}else{synchronized (LockB.lockB){System.out.println("else...lockB");synchronized (LockA.lockA){System.out.println("else...lockA");}}}}
}// 主线程
public class Test05 {public static void main(String[] args) {DieLock dieLock1 = new DieLock(true);DieLock dieLock2 = new DieLock(false);new Thread(dieLock1).start();new Thread(dieLock2).start();}
}
线程状态
在Java中,并不是所有进程都在开始运行之后直接进入运行状态,常见的状态有6种,见下面表格:
线程状态 | 导致状态发生条件 |
| 线程刚被创建,但是并未启动。还没调用 |
| 线程可以在Java虚拟机中运行的状态,可能正在运行自己代码,也可能没有,这取决于操作系统处理器。 |
| 当一个线程试图获取一个对象锁,而该对象锁被其他的线程持有,则该线程进入 |
| 一个线程在等待另一个线程执行一个(唤醒)动作时,该线程进入 |
| 同 |
| 因为 |
对应状态图如下: