多线程基础
为什么要使用多线程
- 充分利用CPU资源
现代计算机几乎都是多核CPU,如果是单线程那便只能使用其中的一个核心,而其它核心处于空闲状态.一核有难,七核围观就是指一个8核的CPU只有一个核在使用,无法充分利用CPU的资源 - 防止卡顿
程序在运行中经常会遇到阻塞操作,比如网络请求,文件读写.这些操作会让当前的线程处于等待模式(一直等着外部响应,在获取响应数据前线程无法处理其它任务).如果是使用单线程,就会导致在等待期间程序卡死,无法对外界行为做出反应.但使用多线程时,可以让等待操作在后台执行,主程序仍然可以响应外部操作.你也不希望你刷短视频刷一半,突然断网程序就一直在等待网络,正在看的视频也被终止,必须等到网络重新连接上才能继续刷视频吧
进程与线程的概念与区别
进程和线程的概念
- 进程是一个独立运行的程序实例.是操作系统资源分配的基本单位.
- 线程是进程内的执行单元,是CPU调度的基本单位
对于下面最简单的输出代码,就有一个线程
java
public class Test {
public static void main(String[] args) {
System.out.println("hallo world");
}
}
在运行这个代码时,会先创建一个java进程.然后在java进程中有一个线程会调用main方法
进程和线程的区别
| - | 进程 | 线程 |
|---|---|---|
| 资源分配 | 拥有独立的内存,文件 | 共享所属进程的资源,仅拥有独立的栈和程序计数器 |
| 独立性 | 进程之间完全独立,一个进程崩溃不会影响其它进程 | 线程依赖进程,同一进程内的线程共享资源,一个线程崩溃可能导致整个进程崩溃 |
| 开销 | 创建,销毁,切换的开销大(需要分配资源和释放资源) | 开销小(仅需要维护少量独立资源,共享进程资源) |
| 数量 | 系统中进程数量较少 | 一个进程可包含多个线程,数量通常远多于进程 |
| 总结 |
- 进程包含线程
一个进程中可以有一个线程,也可以有多个线程.但一个进程中不会有0个进程
- 进程是资源分配的基本单位;线程是调度执行的基本单位
- 每个进程都有自己独立的资源;一个进程的多个线程之间,共用同一份资源
- 进程和进程之间是隔离 的,因此一个进程崩溃不容易影响到别的进程.同一个进程的线程和线程之间是共享资源的.共享资源的好处是节省资源申请和销毁的开销,坏处是容易冲突,一个线程出问题容易影响到其它的线程
因此,线程也称为"轻量级进程"
创建线程的多种方式
Threand是一个在java.lang中的类.要使用Thread类得先继承thread类并且重写run()方法.还有一种是实现Runnable接口
继承Thread类并重写run()方法
- 创建一个类继承Thread类
java
class MyThreand extends Thread{
}
- 重写Thread类的run()方法.重写的逻辑就是要执行的逻辑
java
class MyThreand extends Thread{
@Override
public void run() {
while (true) {
System.out.println("hallo Thread");
try {
Thread.sleep(500);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
}
}
- 创建该子类的实例
java
public class Test {
public static void main(String[] args) {
MyThreand myThreand = new MyThreand();
}
}
- 调用实例的start()方法启动线程(不是直接调用run()方法)
java
public class Test {
public static void main(String[] args) {
MyThreand myThreand = new MyThreand();
myThreand.start();
}
}
最后的代码如下
java
class MyThreand extends Thread{
@Override
public void run() {
while (true) {
System.out.println("hallo Thread");
try {
Thread.sleep(500);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
}
}
public class Test {
public static void main(String[] args) {
MyThreand myThreand = new MyThreand();
myThreand.start();
}
}
运行之后会每0.5秒打印一次hallo Thread
实现Runnable接口
- 创建一个类实现Runnable接口
java
class MyRunnable implements Runnable{
@Override
public void run() {
}
}
- 实现Runnable的run()方法(定义线程逻辑)
java
class MyRunnable implements Runnable{
@Override
public void run() {
while(true){
System.out.println("hallo Thread");
try {
Thread.sleep(500);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
}
}
- 创建Runnable实现类的实例,并将该实例作为参数传入Thread构造器,创建Thread实例
java
public class Test1 {
public static void main(String[] args) {
MyRunnable myRunnable = new MyRunnable();
Thread thread = new Thread(myRunnable);
}
}
- 调用实例的start()方法启动线程
java
public class Test1 {
public static void main(String[] args) {
MyRunnable myRunnable = new MyRunnable();
Thread thread = new Thread(myRunnable);
thread.start();
}
}
运行结果同继承Thread类,每0.5秒打印一次hallo Thread
使用Thread的注意点
- 线程启动必须调用start()方法(start()方法会自动调用run()方法),不是直接调用run()方法.调用start()方法会真正的创建一个线程,而调用run()方法则是只调用该方法,不会创建一个线程.相当于还是单线程.
- 一个线程只能启动一次.线程的生命周期是一次性的.调用start()后无法再对start()重新调用(即使run()方法执行完毕).
- 线程的执行顺序由操作系统调度决定.即使先调用thread1.steart()也有可能会是后调用的thread2.start()先执行
两种方式的优缺点
| 方式 | 优点 | 缺点 |
|---|---|---|
| 继承Thread | 代码简单 | 受限于单继承,任务与线程耦合 |
| 实现Runnable | 避免单继承限制,任务与线程解耦 | 需通过Thread.cuurentThread()获取线程 |
使用匿名内部类
使用匿名内部类的好处是可以简化代码(不用单独定义一个Thread子类),适合创建简单的线程
- 通过匿名内部类继承Thread
new Thread(){...}表示创建一个Thread的匿名类实例.这个匿名子类没有类名,只能能在创建时使用一次
java
public class Test2 {
public static void main(String[] args) {
Thread thread = new Thread(){
@Override
public void run(){
}
};
}
}
- 重写run()方法
java
public class Test2 {
public static void main(String[] args) {
Thread thread = new Thread(){
@Override
public void run(){
while(true){
System.out.println("hallo Thread");
try {
Thread.sleep(500);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
}
};
}
}
- 调用匿名内部类实例的start()方法
就是简单的加上一个thread.start();
java
public class Test2 {
public static void main(String[] args) {
Thread thread = new Thread(){
@Override
public void run(){
while(true){
System.out.println("hallo Thread");
try {
Thread.sleep(500);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
}
};
thread.start();
}
}
- 使用匿名内部类的特点
- 代码更加简洁,可读性高
- 一次性
使用匿名内部类(基于Runnable)
- 创建Runnable的匿名实例
java
public class Test3 {
public static void main(String[] args) {
Runnable runnable = new Runnable() {
@Override
public void run() {
}
};
}
}
- 重写run()方法
java
public class Test3 {
public static void main(String[] args) {
Runnable runnable = new Runnable() {
@Override
public void run() {
while (true){
System.out.println("hallo thread");
try {
Thread.sleep(500);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
}
};
}
}
- 将runnable传入Thread并调用实例后的start()方法
runnable只是一个接口,不包含Thread的start()方法.所以要将runnable中重写的run()方法交给Thread执行
java
public class Test3 {
public static void main(String[] args) {
Runnable runnable = new Runnable() {
@Override
public void run() {
while (true){
System.out.println("hallo thread");
try {
Thread.sleep(500);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
}
};
Thread t = new Thread(runnable);
t.start();
}
}
- 特点
- 代码简洁,不用定义类
- 避免单继承:java中一个类只能继承一个类,但可以实现多个接口
- 符合面向对象设计原则
使用匿名内部类(基于lambda表达式)
lambda表达式是一个语法糖(简化写法).使用有两个前提
- 匿名内部类
- 函数式接口:只包含一个抽象方法的接口,例如Runnable只有run()方法,Comparator只有compare()方法
Thread的构造器也可以使用
使用方法:
- 用lambda表达式替代匿名内部类
()->{...}是匿名内部类的语法
java
public class Test4 {
public static void main(String[] args) {
Thread thread = new Thread(() -> {
});
}
}
Thread thread = new Thread((这里面可以加入函数的参数列表) -> {函数体});
- 添加一次性代码的逻辑
java
public class Test4 {
public static void main(String[] args) {
Thread thread = new Thread(() -> {
while (true){
System.out.println("hallo thread");
try {
Thread.sleep(500);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
});
}
}
- 运行
java
public class Test4 {
public static void main(String[] args) {
Thread thread = new Thread(() -> {
while (true){
System.out.println("hallo thread");
try {
Thread.sleep(500);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
});
thread.start();
}
}
进一步简化代码,直接将lambda表达式作为参数传给Thread
java
public static void main(String[] args) {
new Thread(() -> {
while (true){
System.out.println("hallo thread");
try {
Thread.sleep(500);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
}).start();
}
- 特点
- 更加简洁:省略new 接口名(),@Override,方法名等代码
- 只能用于函数式接口或Thread构造器
- 本质仍然是匿名内部类:lambda表达式编译后会自动生成匿名内部类
Thread方法的用法和示例
Thread的构造方法
| 方法 | 说明 |
|---|---|
| Thread() | 创建线程对象 |
| Thread(Runnable target) | 使用Runnbale对象创建线程对象 |
| Thread(String name) | 创建线程对象并命名 |
| Thread(Runnable target,String name) | 使用Runnable对象创建线程对象并命名 |
| *Thread(ThreadGroup group,Runnable target) | 线程可以被分组管理,分好的组即为线程组 |
线程组了解即可,在现在的开发中线程组已经被替代为更优的线程池
Thread() 创建线程对象
在前面的"使用Thread创建线程"已经详细解释过,在此不多加赘述
Thread(Runnable target) 使用Runnable创建对象
在前面的"使用Thread创建线程"已经详细解释过,在此不多加赘述
Thread(String name) 创建线程对象并命名
- 创建MyThread类并添加构造函数,用来接收并设置线程名
Java
class MyThread extends Thread {
public MyThread(String name){
super(name);
}
}
- 重写run()方法,即线程的主逻辑
java
class MyThread extends Thread{
public MyThread(String name){
super(name);
}
@Override
public void run() {
while (true) {
System.out.print(Thread.currentThread().getName() + ":");
System.out.println("hallo Thread");
try {
Thread.sleep(500);
} catch (InterruptedException e) {
}
}
};
}
- 创建线程实例,并传入线程名
java
public static void main(String[] args) {
MyThread myThread = new MyThread("这是MyThread线程");
myThread.start();
}
最后的代码如下:
java
class MyThread extends Thread{
public MyThread(String name){
super(name);
}
@Override
public void run() {
while (true) {
System.out.print(Thread.currentThread().getName() + ":");
System.out.println("hallo Thread");
try {
Thread.sleep(500);
} catch (InterruptedException e) {
}
}
};
public static void main(String[] args) {
MyThread myThread = new MyThread("这是MyThread线程");
myThread.start();
}
}
- 注意:必须添加构造函数MyThread(String name)并调用父类的构造函数来命名.如果不添加该构造函数会默认使用Thread()构造方法.无法添加重命名
Thread(Ruable target,String name) 使用Runnable创建对象并命名
Java
public static void main(String[] args) {
Runnable runnable = new Runnable() {
@Override
public void run() {
while (true) {
System.out.print(Thread.currentThread().getName() + ":");
System.out.println("hallo Thread");
try {
Thread.sleep(500);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
}
};
Thread t = new Thread(runnable, "这是Thread线程");
t.start();
}
Thread.currentThread().getName()方法是获取当前线程名
查看输出内容会发现获取到的线程名就是我们设置的线程名
Thread类的常见属性和对应属性的获取方法
| 属性 | 获取方式 |
|---|---|
| ID | getId() |
| 名称 | getName() |
| 状态 | getState() |
| 优先级 | getPriority() |
| 是否后台线程 | isDaemon() |
| 是否存活 | isAlive() |
| 是否被中断 | isInterrupted() |
以下是示例的MyThread类的构造
java
class MyThreand extends Thread{
@Override
public void run() {
while (true) {
System.out.println("hallo Thread");
try {
Thread.sleep(500);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
}
}
ID,getId()
是JVM为线程生成的一个Id编号.注意是JVM内,和系统内核中的PCB的PID不是同一个东西
- 使用getId()方法获取Id
java
public class Test {
public static void main(String[] args) {
MyThreand myThreand = new MyThreand();
myThreand.start();
System.out.println(myThreand.getId());
}
}
输出内容为:
24
hallo Thread
hallo Thread
hallo Thread
...
名称,getName()
java
public class Test {
public static void main(String[] args) {
MyThreand myThreand = new MyThreand();
myThreand.start();
System.out.println(myThreand.getName());
}
}
输出内容为:
Thread-0
hallo Thread
hallo Thread
hallo Thread
...
状态,getState()
线程状态能反映线程在生命周期中的当前行为,java中一共定义了6种线程状态,getState()的返回值就是其中之一
| 状态 | 含义 |
|---|---|
| NEW | 线程已经创建,但未调用(start()方法) |
| RUNNABLE | 线程已经启动,正在等待CPU调度(就绪状态)或正在执行run()方法(运行状态) |
| BLOCKED | 线程因竞争同步锁而被阻塞(等待锁释放) |
| WAITING | 线程因调用wait(),join()等方法,进入无限期等待状态(需要其它线程唤醒) |
| TIMED_WAITING | 线程因调用sleep(),wait()等方法,进入限期等待状态(超时自动唤醒) |
| TERMINATED | 线程执行完毕或因异常终止 |
java
public class Test {
public static void main(String[] args) {
MyThreand myThreand = new MyThreand();
myThreand.start();
System.out.println(myThreand.getState());
}
}
输出内容为:
RUNNABLE
hallo Thread
hallo Thread
hallo Thread
...
优先级,getPriority()
在java中线程优先级是一个整数,范围是1~10之间.数值越大代表进程的优先级越高.getPriority()是获取当前优先级.setPriority()是设置优先级
java
public class Test5 {
public static void main(String[] args) {
Thread thread = new Thread(()-> {
while (true){
System.out.println("MyThread正在运行");
try {
Thread.sleep(500);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
},"MyThread");
System.out.println("初始优先级是:" + thread.getPriority());
thread.setPriority(10);
System.out.println("设置后的优先级是:" + thread.getPriority());
thread.start();
}
}
输出内容是:
初始优先级是:5
设置后的优先级是:10
MyThread正在运行
MyThread正在运行
MyThread正在运行
- 优先级的作用:
优先级的主要作用是向操作系统的线程调度器提供一个"建议",表明该线程的重要性.通俗来说就是
优先级高的线程获得CPU执行时间片的机会更多,更有可能被优先调度,反之亦然 - 注意:
- 真正的决定权在操作系统:java的优先级只是给操作系统一个建议.不同的操作系统有不同的调度算法.有可能会忽略java的游戏机设置
- 不能保证执行顺序:优先级高的进程也不一定会比优先级低的进程更快执行
- 可能导致线程"饥饿
是否后台线程,isDaemon()
该方法用来判断线程是否为后台线程(守护线程).返回true就是后台线程,false就是前台线程(用户线程)
- 后台线程与前台线程的区别
类型|特点
-|-
后台线程(守护线程Daemon)|依赖前台进程存在.当所有前台线程结束后,后台线程会被JVM强行终止
前台线程(用户线程User)|独立存在,当所有前台线程结束后,JVM就会退出,即程序终止
就好比一棵树.前台进程就是树干,后台进程就是树叶.一棵树可以没有树叶只有树干,也可以只有树干而没有树叶,但不会只有树叶而没有树干
java
public class Test5 {
public static void main(String[] args) {
Thread thread = new Thread(()-> {
while (true){
System.out.println("MyThread正在运行");
try {
Thread.sleep(500);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
},"MyThread");
System.out.println(thread.isDaemon());
thread.start();
}
}
输出内容是:
false
MyThread正在运行
MyThread正在运行
由此可见我们默认创建的线程默认是前台进程
- 设置后台线程
isDaemon()只用来查询状态,药创建后台进程,需要使用**Thread.setDaemon(boolean on)**方法来设置
java
public class Test5 {
public static void main(String[] args) {
Thread thread = new Thread(()-> {
while (true){
System.out.println("MyThread正在运行");
try {
Thread.sleep(500);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
},"MyThread");
System.out.println(thread.isDaemon());
thread.setDaemon(true);
thread.start();
System.out.println(thread.isDaemon());
}
}
输出内容:
false
true
MyThread正在运行
进程已结束,退出代码0
对于上段代码,第一行输出是MyThread默认为前台线程.第二行输出是使用setDaemon()方法设置为后台线程.注意到这次MyThread线程只进行了一次打印.原因是将该线程设置为后台线程后,当唯一的前台线程main执行完毕后会自动销毁.此时没有前台线程,作为后台线程的MyThread就被JVM强制关闭了
注意:setDeamon()方法必须在start()方法前使用,否则抛异常
是否存活,isAlive()
- 什么是存活?
线程的存活指的是:线程已经启动(start()调用,且线程还未终止)
对于存活,包含以下状态
- RUNNABLE(就绪或正在运行)
- BLOCKED(竞争说阻塞)
- WAITING(无限期等待)
- TIMED_WAITING(限期等待)
对于非存活,包含以下状态 - NEW(未启动线程)
- TERMINATED(线程已终止)
- isAlive()用法示例
java
public class Test5 {
public static void main(String[] args) {
Thread thread = new Thread(()-> {
while (true){
System.out.println("MyThread正在运行");
try {
Thread.sleep(500);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
},"MyThread");
System.out.print("线程状态:" + thread.getState());
System.out.println("是否存活:" + thread.isAlive());
thread.start();
System.out.print("线程状态:" + thread.getState());
System.out.println("是否存活:" + thread.isAlive());
}
}
输出内容:
线程状态:NEW是否存活:false
线程状态:RUNNABLE是否存活:true
MyThread正在运行
MyThread正在运行
MyThread正在运行
...
是否被中断,isInterrupted()
- 什么是中断?
java中的线程中断不是强制终止线程,是通过设置线程的"中断标志位"来通知线程"你要中断了",然后线程才开始判断是否中断
isInterrupted()方法的作用就是查询线程的"中断标志位,以此来决定后续行为 - 什么是中断标志位?
中断标志位是线程内的一个布尔型变量,用于标记线程是否收到中断信号.中断标志位的初始状态是false,即未中断.我们可以使用interrupt()来设置中断标记为true
注意 :当线程处于阻塞状态时(sleep(),wait()等等),会抛出异常,并且自动清除中断标志位.清楚中断标志位的原因就是让线程有足够的时间退出循坏,释放资源等等
java
public class Test6 {
public static void main(String[] args) {
Thread thread = new Thread(()-> {
while(!Thread.currentThread().isInterrupted()) {//判断线程是否收到中断信号
System.out.println("线程运行中");
try {
Thread.sleep(500);//sleep被中断会抛出InterruptedException
} catch (InterruptedException e) {
System.out.println("线程休眠时被中断,正在退出线程");
System.out.println("sleep被中断后线程的中断标志位是" + Thread.currentThread().isInterrupted());
Thread.currentThread().interrupt();//重新标记中断信号
}
}
System.out.println("线程是否被中断:" + Thread.currentThread().isInterrupted());
});
thread.start();
System.out.println("main线程开始");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
System.out.println("main线程结束,结束前设置中断标记位");
thread.interrupt();
}
}
输出内容:
main线程开始
线程运行中
线程运行中
main线程结束,结束前设置中断标记位
线程休眠时被中断,正在退出线程
sleep被中断后线程的中断标志位是false
线程是否被中断:true
进程已结束,退出代码0
由代码和输出内容"sleep被中断后线程的中断标志位是false"可知sleep被中断时会抛出异常并且恢复中断标记位