目录
什么是多线程?
概念:
一个线程就是一个执行流,每个线程之间都可以按照自己的顺序执行自己的代码,多个线程之间"同时"执行多份代码。多线程编程其实也可以称作为"并发编程"。
**并发编程:**对于进程也可以实现并发编程,但是和线程相比,线程更轻量
-
创建线程比创建进程更快
-
销毁线程比销毁进程更快
-
调度线程比调度进程更快
优点:
-
多线程可以充分利用CPU资源去处理一些复杂业务,从而提升业务的效率
-
一定程度上可以提高程序处理任务效率,创建线程的个数,根据CPU逻辑处理器的数量作为参考
-
线程个数<逻辑处理器个数:会提升程序处理任务效率
-
线程个数>逻辑处理器个数:由于过多的线程,导致有较多线程处于阻塞状态,并且线程创建和销毁也会一定程度加重系统开销,可能会降低程序处理任务效率
-
线程和进程区别:(面试常考题)
-
进程包含线程,每一个进程至少有一个线程,即主线程
-
进程和进程之间不共享内存空间,同一个进程之间的线程共享内存空间
-
进程是系统分配资源的最小单位,线程是系统调度的最小单位
-
一个进程挂了一般不会影响其它进程,但是一个线程挂了,可能导致整个进程的崩溃
Java线程和操作系统线程的关系:
-
线程是操作系统中的概念。操作系统内核实现了线程这样的机制,并且对用户层提供了一些API供用户使用
-
Java标准库中的Thread类可以视为是对操作系统提供的API进一步的抽象和封装
多线程创建
-
Java中线程参与调度执行的步骤:Java中创建一个线程对象->JVM调用系统的API->创建系统中的线程->最终参与CPU调度
-
线程的执行顺序并没有什么规律,这和CPU的调度有关,由于CPU的调度是"抢占式"执行的,所以哪个线程当前占用CPU资源是不确定的
方法1继承Thread类
-
Thread用来描述一个线程,创建的每一个线程都是Thread的对象
-
继承Thread类,直接使用this就表示当前对象的引用
java
class MyThread extends Thread{
//必须要重写Thread类中的run()方法,run()内可以更具业务需求,进行调整
@Override
public void run() {
System.out.println("Mythread");
}
}
public class Test1 {
public static void main(String[] args) {
//创建MyThread的实例
MyThread t1 = new MyThread();
//调用start()启动线程,线程真正开始运行
t1.start();
}
}
方法2实现Runnable接口
-
其中只有一个run()方法,面对多个线程时和Thread类相比方法更方便,多个线程执行同一个任务就使用Runnable()接口
-
实现Runnable接口,this表示的是MyRunnable()的引用,需要使用Thread.currentThread()
java
class MyRunnable implements Runnable{
@Override
//重写run()方法
public void run() {
System.out.println("MyRunnable");
}
}
public class Test2 {
public static void main(String[] args) {
//创建Thread实例,调用Thread构造方法时将Runnable对象作为参数传入
Thread t1 = new Thread(new MyRunnable());//根据线程需要,传入对应参数
//调用start启动线程
t1.start();
}
}
star()和run()的区别:(经典面试题)
-
start()真实申请系统线程的PCB,从而启动一个线程,参与CPU调度
-
run()定义线程的时候指定线程要执行的任务,如果直接调用,就是Java一个对象中普通的方法
其他形变
匿名内部类创建Thread子类对象
java
Thread t1 = new Thread(){
@Override
public void run() {
System.out.println("匿名内部类创建线程");
}
};
匿名内部类创建Runnable子类对象
java
Thread t2 = new Thread(new Runnable() {
@Override
public void run() {
System.out.println("匿名内部类创建线程");
}
});
lambda表达式创建Runnable子类对象
-
最推荐的编码方式
-
Runnable接口是一个函数式接口,可以通过Lambda表达式创建,本质上就是实现了Runnable接口
-
函数接口:接口中只定义了一个方法
java
Thread t3 = new Thread(()->{
System.out.println("Lambda表达式创建线程");
});
Thread类及其常见方法
-
Thread类是JVM用来管理线程的一个类,每一个线程都有唯一的Thread对象与之相关联
-
JVM会将这些Thread对象组织起来,用于线程调度,线程管理
Thread的常见构造方法:
方法 | 说明 |
---|---|
Thread() | 创建线程对象 |
Thread(Runnable target) | 使用Runnable对象创建线程对象 |
Thread(String name) | 创建线程对象,并命名 |
Thread(Runnable target, String name) | 使用Runnable对象创建线程对象,并命名 |
Thread常见属性
属性 | 获取方法 |
---|---|
ID | getId() |
名称 | getName() |
状态 | getState() |
优先级 | getPriority() |
是否后台线程 | isDaemon() |
是否存活 | isAlive() |
是否被中断 | isInterrupted() |
-
ID:线程的唯一标识,不同线程不会重复,JVM默认为Thread对象生成的一个编号,是Java层面的,与PCB区分开
- Thread是Java中的一个类:创建的Thread对象->调用start方法->JVM调用系统API生成一个PCB->PCB与Thread对象一一对应
-
名称:线程的名称
-
状态:表示线程所处的情况
-
优先级高的线程理论上来说更容易被调度到
-
关于后台线程:JVM会在一个进程的所有非后台进程结束后,才会停止运行,前台线程可以阻止线程的退出
-
是否存活,可以简单理解为run()方法是否执行结束
启动一个线程--start()
-
覆写run()方法仅仅是提供了线程的任务清单
-
调用start方法,才真正申请系统线程PCB,从而启动一个线程,参与CPU调度
中断线程
-
线程执行到一半需要停止,通过一个信号使线程退出
-
方案:
-
通过共享标记来进行沟通
-
调用interrupt()方法来通知
-
使用共享标记来中断
java
public class Test {
//设置标志位,变量用volatile修饰,保证内存可见性,后续再线程安全解决会提到
public static volatile boolean isQuit = false;
public static void main(String[] args) {
//线程中断演示
Thread t1 = new Thread(()->{
while(!isQuit){
System.out.println(Thread.currentThread().getName()+"正常工作,没有被中断!");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
System.out.println(Thread.currentThread().getName()+"被中断,停止任务进行!");
});
System.out.println(Thread.currentThread().getName()+"发出中断指令!");
isQuit = true;
System.out.println(Thread.currentThread().getName()+"结束发出中断命令!");
t1.start();
}
}
调用interrupt()方法来通知
java
public class Test2 {
public static void main(String[] args) throws InterruptedException {
//线程中断演示
Thread t1 = new Thread(() -> {
while (!Thread.interrupted()) {
System.out.println(Thread.currentThread().getName() + "正常工作,没有被中断!");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
System.out.println(Thread.currentThread().getName() + "被中断,停止任务进行!");
});
//启动线程
t1.start();
//线程休眠
Thread.sleep(10000);
//发出中断信号
t1.interrupt();
}
}
注意 :
-
如果线程处于运行状态,直接中断线程,不会报异常,符合程序预期
-
如果线程处于等待状态,就会报一个中断异常
-
下图所示第一张为上述代码所报异常
- 修改catch处理逻辑后代码
总结:
-
调用interrupt()方法来通知,如果线程因为调用wait/join/sleep等方法而阻塞挂起,则以InerruptedException异常的形式通知
-
出现异常时候,要不要结束线程取决于catch中代码的写法,可以选择忽略这个异常,也可以跳出循环过程
等待线程--join()
- 等待一个线程执行完毕
方法 | 说明 |
---|---|
public void join() | 等待线程到结束 |
public void join(long millis) | 最多等待millis毫秒 |
public void join(long millis,int nanos) | 更高精度 |
获取当前线程的引用
java
public static void main(String[] args) {
Thread t1 = Thread.currentThread();//获取当前线程对象的引用
System.out.println(t1.getName());
}
//一般可以连起来使用Thread.currentThread()+方法
Thread.currentThread().getName()
休眠当前线程
- 因为线程的调度是不可控的,所以,这个方法只能保证实际休眠时间是大于等于参数设置的休眠时间的
java
public static void main(String[] args) throws InterruptedException {
//获取当前毫秒
System.out.println(System.currentTimeMillis());
Thread.sleep(3*1000);
System.out.println(System.currentTimeMillis());
}
线程的状态
- 线程的状态是一个枚举类型Thread.State
java
public static void main(String[] args) {
for (Thread.State state:Thread.State.values()) {
System.out.println(state);
}
}
**面试题:**共六种线程状态
-
**NEW:**安排了工作,但是还未执行,创建好了一个线程对象,没有调用start()方法之前是不会创建PCB的
-
RUNNABLE:可工作的,包含正在工作中和即将开始工作->运行+就绪,此时系统中有很多PCB
-
BLOCKED:等待锁的状态,阻塞的一种
-
WAITING:没有等待时间,处于一直死等的状态
-
TIMED_WATING:设置等待时间的等待状态,过时不候
-
**TERMINATED:**线程执行完成,PCB已经销毁,但是Java对象还在
**注意:**对于isAlive()方法,可以认为处于不是NEW和TERMINATED状态的都是活着的
线程安全(重点)
线程不安全:
- 如果多线程状态下代码运行的结果是符合我们预期的,即在单线程环境应该的结果,则说明这个程序是线程安全的
造成线程不安全的原因:
线程调度是随机的
-
这是线程安全的罪魁祸首
-
随机调度使一个线程在多线程环境下,执行顺序存在很多变数
-
代码编写人员必须保证在任意执行顺序之下,代码都能正常运行
多个线程修改了同一变量
- 多个线程修改不同变量,不会出现线程安全问题,一个线程修改一个变量,也不会出现线程安全问题
无法保证原子性
-
原子性:要么全部执行,要么全部不执行
-
例如:count++这个操作,对应几条CPU指令
-
LOAD:从内存或者寄存器中读取count的值
-
ADD:执行自增
-
STORE:把计算结果写回寄存器或内存
-
-
如果能保证原子性,当执行count++代码的时候,上述三条指令连续执行,不会被打断
-
-
无法保证原子性可能带来的问题:可能会发生覆盖现象
-
如果一个线程正在对一个变量操作,这时中途插入其他线程,这个操作会被打断,结果就可能产生覆盖
- 这点也和线程抢占式调度密切相关,如果线程不是抢占式的,就算没有原子性,也问题不大
-
-
一条Java语句不一定使原子的,也不一定使一条指令
无法保证可见性
-
可见性:一个线程对共享变量值的修改,能够及时地被其它线程看到,可以通过某种方式,让线程之间相互通信
-
**Java内存模型(JMM):**Java虚拟机规范中定义了Java内存模型
-
线程之间的共享变量存储在主内存中
-
每一个线程都有自己的"工作内存"
-
当线程要读取一个共享变量的时候,会先把变量从主内存中读取到自己的工作内存中,再从工作内存中读取数据
-
当线程要修改一个共享变量的时候,会先修改工作内存中的副本,再同步回主内存
-
观于JMM面试题:
-
所有线程不可以直接修改内存中的变量
-
如果要修改,需要把这个变量从主内存中复制到自己的工作内存中
-
各个线程之间无法相互通信,做到了内存级别的线程隔离
为什么要整这么多内存?
-
实际上没有这么内存,这只是Java规范中的一个术语,是属于"抽象"的叫法
-
所谓"主内存"才是真正硬件角度的"内存",而所谓"工作内存",则是指CPU的寄存器和高速缓存
为啥要这么麻烦地拷来拷去?
-
因为CPU访问自身寄存器速度以及高速缓存 的速度,远远超过访问内存地速度(快了3-4个数量级,也就是几千倍,上万倍)
-
那访问寄存器这么好,要啥内存?--因为太贵
-
价格排序:CPU寄存器>内存>硬盘
-
访问速度:CPU寄存器>内存>硬盘
-
指令重排序
-
由于一条Java语句可能对应多条机器指令
-
我们写的代码在编译之后可能会与代码对应的指令顺序不同,这个过程就是指令的重排序
-
编译器对于指令重排序的前提是"保持逻辑不发生变化",这一点在单线程下比较容易判断,但是在多线程环境下就没有那么容易,多线程代码执行复杂程度高,编译器很难在编译阶段对代码的执行效果进行预测,因此激进的重排序很容易导致优化后的逻辑和之前不等价
xdm多线程需要解决的内容非常多,一篇文章不足已掌握,关于线程安全的解决请听下回分解!!