文章目录
🎑线程是什么?
也把线程称为轻量级进程
如果把进程想象成工厂,线程就是工厂中的"流水线"
使用多线程:
- 能够充分利用多核CPU,能够提高效率~
- 只是创建第一个线程的时候,需要申请资源,后续再创建新的线程,都是共用同一份资源
节省了申请资源的开销
销毁线程的时候,也只是销毁到最后一个的时候,才真正的释放资源
前面的线程销毁,都不必真正释放资源
操作系统内核,是通过PCB来描述进程的~~
更准确的说法 : 是一组PCB来描述一个进程,每个PCB对应一个线程
一个进程至少有一个线程,也可以有多个~
这一组PCB上的内存指针,和文件描述符表,其实是同一份~
而状态,上下文,优先级,几张信息,则是每个PCB(每个线程)自己有一份
进程是 资源分配的基本单位
线程是 调度执行 的基本单位
❗ ❗面频面试题(面试必考):
谈谈进程和线程之间的区别?
- 进程包含线程
- 线程比进程更轻量,创建更快,销毁更快
- 同一个进程的多个线程之间共用同一份内存/文件资源,而进程和进程之间则是独立的内存/文件资源
- 进程是资源分配的基本单位,线程是调度执行的基本单位
- 线程之间可能会相互影响到.
如果两个线程同时修改同一个变量,容易产生"线程不安全"的问题- 如果某个线程出现异常,并且异常没有处理好的话,整个进程都会随之崩溃!!这个时候后续其他线程自然难以进行运行
线程数目不是越多越好,CPU核心数是有限的,当线程数目达到一定程度的时候,CPU核心数已经被吃满了!!此时继续增加线程,也无法再提升效率了~反而会因为线程太多,线程调度开销太大,影响了效率
!
❓如果CPU是6核心,此时6个线程就是吃满了吗?
不一定,一个线程可能在占用CPU,也可能在等待~
另外,现代的CPU都有"超线程技术",一个核心可以并行跑两个线程(比如12核24线程)
🎎一、线程创建
🧨1.继承Thread类,重写Thread里的run方法
class MyThread extends Thread {
@Override
public void run() {
System.out.println(1);
}
}
Thread相当于操作系统中的线程进行的封装
run
是Thread
父类已经有的方法了
但是这里要重写一下~
run
里面的逻辑,就是这个线程要执行的工作!!
创建子类,并且重写run
方法,相当于"安排任务"
创建一个MyThread
实例,创建实例并不会在系统中真的创建一个线程!!
调用start
方法的时候,才是真正创建出一个新的线程~~
Thread myThread = new MyThread();
myThread.start();
新的线程就会执行run
里面的逻辑~直到run
里的代码执行完,新的线程就运行结束了
运行了一次Java程序,就是启动了一个进程~
一个进程里至少会有一个线程~默认的线程正是main
方法所在的线程(也叫做主线程)
main
主线程,和MyThread
创建出来的新线程,是一个并发执行(宏观的,其实是并发+并行)的关系!
并发执行的意思是两边同时执行,各自执行各自的!
另外启动一个线程,来执行Thread
中run
方法!!
新的线程是一个单独的执行流,和现有进程的执行流不相关
多个线程调度的时候,会发生抢占式执行!
“抢占式执行” :
操作系统在调动线程的时候,是一个"随机"的进程!!
执行sleep
,线程进入阻塞
sleep
时间到,线程恢复就绪状态,参与线程调度当两个线程都参与调度的时候,谁先谁后,不确定!
是多线程编程的"万恶之源,罪魁祸首"
就给多线程程序的执行,带来了非常多的变数
写代码的时候,可能某些调度顺序下,代码没问题,某些调度顺序下,代码就出bug!!
编写多线程代码,就需要考虑所有可能的调度情况,保证每种情况下,都不出问题…
耦合性比较高,假设未来要对这个代码进行调整(不用多线程了,用其他方式),代码改动就比较大
🧨2.通过重写Runnable接口,重写run
class MyRunnable implements Runnable {
@Override
public void run() {
System.out.println(2);
}
}
public class TestDemo {
public static void main(String[] args) {
Runnable myRunnable = new MyRunnable();
Thread thread = new Thread(myRunnable);
thread.start();
}
}
把线程要干的活和线程本身分开了~
使用Runnable
来专门表示"线程要完成的工作"
把任务提取出来,目的是为了解耦合,改成其他方式只需要把Runnable
传给其他的实体就行~
如果想使多个线程都干一样的活,也更适合使用Runnable
🧨3.使用匿名内部类,来创建Thread的子类
匿名内部类的实例,作为构造方法的参数!!
Thread myThread0 = new MyThread() {
@Override
public void run() {
System.out.println(3);
}
};
创建Thread的子类,同时实例化出一个对象!
🧨4.使用匿名内部类,实现Runnable接口的方式
在构造方法的参数里面就地创建一个匿名内部类
匿名内部类的实例,作为构造方法的参数!
Thread thread1 = new Thread(new Runnable() {
@Override
public void run() {
System.out.println(4);
}
});
🧨5.lambda表达式
Thread thread2 = new Thread(()->{
while (true) {
System.out.println(5);
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
});
thread.start();
复习lambda表达式:
本质上是一个"匿名函数"
()函数的形参 {}函数体
-> 特殊语法,表示它是一个lambda
❗ 线程创建不止这五种!
🎎二、线程中断
run
方法执行完了,线程就结束了
❓有没有方法让线程提前一点结束呢?
答案是通过线程中断(本质仍然是让run方法尽快结束,而不是run执行一半,强制结束)
这就取决于run里面具体是如何实现的了
- 直接自己定义一个标志位,作为线程是否结束的标记~
public class TestDemo {
private static boolean isQuit = false;
public static void main(String[] args) throws InterruptedException {
Thread thread = new Thread(()-> {
while (!isQuit) {
System.out.println("isQuit为假");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
});
thread.start();
Thread.sleep(1000);
System.out.println("中断线程");
isQuit = true;
System.out.println("isQuit 为真");
}
}
- 使用标准库里自带的标志位
Thread.currentThread().isInterrupted();
currentThread()
是Thread
类的静态方法, 通过这个方法,就可以拿到当前线程的实例(拿到当前线程对应的Thread
对象)
isInterrupted()
判定标志位:
在主线程中,通过thread.Interrupt()
来中断这个线程,设置标志位为true
!!
interrupt
方法的行为有两种情况:
thread
线程在运行状态时,isInterrupted()
会设置Thread.currentThread().isInterrupted();
的标志位为true
thread
线程在阻塞状态(sleep
)会抛出异常,会清除这个标志位(设置了又清除了),并触发一个InterruptedException
异常,这个异常会把sleep
提前唤醒!
但是由于代码中只是打印了日志,而没有结束循环,因此线程还是在继续执行中!
在java中,中断线程并非是强制的!!
是由线程自身的代码来进行判定处理的!!
线程自身能怎么处理呢?
- 立即结束线程
- 不理会
- 稍后理会
🎎 三、线程等待
join()
阻塞等待,线程的结束
class MyRunnable implements Runnable {
public static final long COUNT = 10_0000_0000L;
@Override
public void run() {
int x = 0;
for (int i = 0; i < COUNT; i++) {
x++;
}
}
}
public static void fun2() {
Runnable myRunnable = new MyRunnable();
Thread thread = new Thread(myRunnable);
Thread thread1 = new Thread(myRunnable);
long beg = System.currentTimeMillis();
thread.start();
thread1.start();
thread.join();
thread1.join();
long end = System.currentTimeMillis();
System.out.println(end-beg + "ms");
}
线程之间的调度顺序,是不确定的!!
可以通过一些特殊操作,来对线程的执行顺序,做出干预
其中join
是一个方法,控制线程之间的结束顺序!!
💥上面的代码如果不加thread.join(); thread1.join();
是有问题的,因为’main
’线程和’thread
,thread1
是并发执行的,thread
,thread1
还没有执行完的时候就计算时间了…
要让main
线程等待thread
和thread1
执行完了,才能停止计时~
在main
中调用thread.join
效果就是让main
线程阻塞,一直到thread
执行完run
,main
才继续执行
在main
中调用thread1.join
效果就是让main
线程阻塞,一直到thread1
执行完run
,main
才继续执行
(执行join的线程(比如main线程)就阻塞,阻塞到调用join的线程执行完毕之后才继续执行)
thread
和thread1
是并发执行(宏观上的同时),而不是先执行完thread
,才执行thread1
此处写两个join
就是两个线程都执行完,才继续执行计时操作!!
Java中的多线程方法,只要阻塞就都可能触发InterruptedException
异常
如果是调用join
之前,线程已经结束了,此时join
还需要阻塞等待吗?
🎫答案:不需要
🎎四、线程休眠
sleep();
指定休眠的时间~~让线程休息一会儿(阻塞一会儿)
操作系统管理这些线程的PCB时,是有多个链表的~~
当1号线程,sleep
的时间到了!!就会被移动回之前的就绪队列~~
挪回了就绪队列,不代表立刻就能上CPU上执行,还得看系统什么时候调度到这个线程!!
❔sleep(1000
),不一定就真的是休眠了1000,一般是要略多于1000,具体多了多少,得看调度的时间开销…(可能快,比如<1ms,也可能慢,比如10ms左右)
通常的操作系统,调度开销都是不可预期的,可能快可能慢
有些场景,不希望调度开销太大,速度太慢!!(比如航空航天等精密的工作)
因此为了解决这个问题,可以使用"实时操作系统"
特点就是任务调度的开销,是可预期的(做到实时,也要付出代价,所以普通工作不用)
🎎五、获取线程实例
public static Thread currentThread();
Thread.currentThread();
获取到当前这个线程对应的Thread
对象的引用
哪个线程里面调用,得到的就是哪个线程的引用