JavaEE--2.多线程
1.认识线程(Thread)
1.1概念
1)什么是线程
⼀个线程就是⼀个 "执行流". 每个线程之间都可以按照顺序执行自己的代码. 多个线程之间 "同时" 执行着多份代码.
2)为什么要有线程
首先, "并发编程" 成为 "刚需".
1. 单核 CPU 的发展遇到了瓶颈. 要想提⾼算⼒, 就需要多核 CPU. ⽽并发编程能更充分利⽤多核 CPU 资源.
2. 有些任务场景需要 "等待 IO", 为了让等待 IO 的时间能够去做⼀些其他的⼯作, 也需要⽤到并发编程
其次, 虽然多进程也能实现 并发编程, 但是线程比进程更轻量
创建线程比创建进程更快.
销毁线程比销毁进程更快.
调度线程比调度进程更快.
最后, 线程虽然比进程轻量, 但是⼈们还不满足, 于是又有了 "线程池"(ThreadPool) 和 "协程" (Coroutine)
多进程的问题就是太"重"
创建/销毁进程的开销比较大(时间,空间)
一旦需求常见,需要频繁地创建销毁进程,开销非常明显.
最典型的例子就是服务器的开发:针对每个发送请求的客户端都创建一个单独的进程,由这个进程负责给客户端进行服务
为了解决进程开销比较大的问题,发明了"线程"(Thread)
线程可以理解成更轻量的进程,也能实现并发编程,创建/销毁的开销比进程更低.
因此,多线程的编程就成了当下主流的并发编程方式.
所谓的进程,在系统中是以PCB(进程控制块)这样的结构体来描述的,通过链表的形式来组织的
对于系统中,线程同样也是通过PCB来描述的(Linux)
一个进程其实是一组PCB
一个线程是一个PCB
所以,其中存在"包含关系”,一个进程中,可以包含多个线程
此时每个线程都可以独立的到CPU上调度执行
线程是系统“调度执行”的基本单位
进程是系统“资源分配”的基本单位
解释:一个可执行程序,运行的时候(双击),操作系统就会创建进程,给这个程序分配各种系统资源(CPU,内存,硬盘,网络带宽......),同时,也会在这个进程中,创建出一个或者多个线程,这些线程再去CPU上调度执行.
上个文章提到的"进程调度"相当于"线程调度",针对"只包含一个线程的进程来说"
如果有多个线程在一个进程中,每个线程,都会有自己的状态,优先级,上下文,记账信息,每个都会各自独立的在CPU上调度执行
进程要么包含一个线程,要么包含多个线程,不能没有线程
同一个进程的这些线程,共用着同一个系统资源
线程比进程更轻量,主要就在与创建线程省去了"分配资源"过程,销毁线程也省去了"释放资源"过程.
分配资源这个操作的开销大
一旦创建进程,同时也会创建第一个线程=>就会负责分配资源
一旦后续创建第二个第三个线程,就不必再重新分配资源了....
从微观角度看,多个核心,每个核心都可以执行一个线程,这些核心之间的执行过程是"同时执行的",这时称之为"并行"
一个核心,也可以按照"分时复用",来切换多个线程,从微观上来看.多个线程是"一个接一个"的执行的,由于调度速度足够快,宏观上看起来好像"同时执行",这时称之为"并发"
提升效率的关键是在于充分利用多核心进行"并行执行"
如果只是"微观并发",速度是没有提升的
真正能提升速度的是"并行"
线程有关的问题:
1.线程数目太多
如果线程数目太多,比如超出了CPU核心数目.此时就无法在微观上完成"并行"执行,势必会存在严重的"竞争".
2.进程资源"冲突"
当线程数目多了之后,此时就容易发生"冲突",由于多个线程,使用的是同一份资源(内存资源)
内存资源指的是代码中定义的变量/对象,如果多个线程同时对一个变量进行读写操作(尤其是写操作)容易发生冲突
一旦发生冲突,就可能使程序出现问题("线程安全问题"[重点内容,难点内容])
3.线程异常
当一个进程中有多个线程的时候,一旦某个线程抛出异常,这个时候,如果未能妥善处理,可能导致整个进程崩溃,其他线程就会随之崩溃
总结下(这也是高频的面试题)
进程线程的概念和区别
1.进程包含线程
一个进程里可以有一个线程,也可以有多个线程,不能没有线程
2.进程是系统资源分配的基本单位
线程是系统调度执行的基本单位
3.同一个进程里的线程之间,共用着同一份系统资源(内存,硬盘,网络带宽等...)
尤其是"内存资源",就是代码中定义的变量/对象...
编程中,多个线程,是可以共用同一份变量的
4.线程是当下实现并发编程的主流方式,通过多线程,就可以充分利用好多核CPU
但是不是线程数目越多越好,线程数目达到一定程度,把多个核心都充分利用了之后,此时继续增加线程无法再提高效率,甚至可能会影响效率(线程调度也是有开销的)
5.多个线程之间,可能会互相影响,线程安全问题,一个线程抛出异常,也可能会把其他线程也一起带走
6.多个进程之间,一般不会相互影响,一个进程崩溃了,不会影响到其他进程(这一点也称为"进程的隔离性")
3)进程和线程的区别
进程是包含线程的,每个进程至少有一个线程,即主线程.
进程和进程之间不共享内存空间,同一进程的线程之间共享同一个内存空间
进程是资源分配的基本单位,线程是系统调度的基本单位
一个进程挂了一般不会影响到其他进程,一个线程挂了可能会把同进程的其他线程一起带走(整个进程崩溃).
4)Java的线程和操作系统线程的关系
线程是操作系统中的概念,操作系统内核实现了线程这样的机制,并且对用户提供了一些API供用户使用(例如Linux的pthread库).
Java标准库中的Thread类可以视为对操作系统提供的API进行了进一步的抽象和封装.
Java(JVM)把这些系统API封装好了,咱们不需要关注系统原生API,只需要了解好Java提供的这一套API就行了
1.2第一个多线程程序
Thread标准库:
这个类就负责完成多线程相关的开发.
此处Thread类可以直接使用,不需要导入任何的包
(因为有的类比较特殊,默认已经导入了,比如String)
此处的继承不是主要目的,更主要的目的是为了重写run方法
调用start就会在进程内部创建出新的线程,新的线程就会执行刚才run里面的代码
上述代码没有run方法没有手动的去调用但是最终也是执行了像run这种用户手动定义,但是没有用户手动调用,最终这个方法被系统/库/框架进行调用了此时,这样的方法被称为"回调函数"(callback)回调函数是编程中一个非常重要的概念
回调函数是一个非常重要的概念!!
回调函数概念其实目前至少遇到过两次
1.C语言中的函数指针
函数指针主要有两个用途
a)作为回调函数
b)实现转移表--降低代码的复杂程度
2.Java数据结构中的优先级队列
优先级队列必须先定义好对象的"比较规则" Comparable Compareto
Comparator Compare
自己定义了这些比较规则,但是并没有调用,此时都是标准库本身内部的逻辑负责调用
这个代码运行起来,是一个进程,但是这个进程实际上是包含了两个线程!
调用main方法的线程称为"主线程"
一个进程至少得包含一个线程,至少这一个线程就是主线程
t.start() 这里又手动的创建了新的线程
主线程和新线程都并发/并行的在CPU上执行
Thread,sleep();是Thread的静态方法 类名.方法名
Java标准库提供的方法
C语言中也有类似的,Sleep() (Windows的系统函数)
多线程之间谁先去CPU上调度执行,这个过程是"不确定的"(不是数学意义上的随机)
而是这个调度顺序取决于操作系统,内核里"调度器"的实现
调度器里有一套规则,咱们作为应用程序开发没办法进行干预,也感受不到
只能将这个过程近似的视为"随机" "抢占式执行"
借助第三方工具更直观的来看这两个线程的情况
在jdk的bin目录中打开jconsole
可以看出当前java进程中线程的情况,左下角可以看出一个java进程中不只有两个线程,还有别的
main是主线程,调用main方法的
Thread-0是代码中自己创建的线程,它的命名规律是Thread-数字
在IDEA的调试器,也能看到类似的信息
就可以手动的切换都某个线程上,看你需要关注的一些信息
创建线程的写法
1)继承Thread,重写run
上面的写法就是继承Thread类
1.继承 Thread 来创建⼀个线程类.
class MyThread extends Thread {@Overridepublic void run() {System.out.println("这⾥是线程运⾏的代码");}
}
2.创建 MyThread 类的实例
MyThread t = new MyThread();
3.调用 start 方法启动线程
t.start();
全部代码:
2)实现Runnable接口,重写run
1.实现 Runnable 接口
class MyRunnable implements Runnable {@Overridepublic void run() {System.out.println("这⾥是线程运⾏的代码");}
}
2.创建 Thread 类实例, 调⽤ Thread 的构造⽅法时将 Runnable 对象作为 target 参数.
Thread t = new Thread(new MyRunnable());
3.调用 start 方法
t.start();
全部代码:
Runnable就是用来描述"要执行的任务" 是什么
有人认为Runnable这种做法更有利于"解耦合",这只是一个任务,并不是和"线程"这样的概念强相关
后续执行这个任务的载体可以是线程也可以是别的
别的在这里比如后续会介绍的线程池来执行任务
再比如可以通过"虚拟线程"(协程)来执行~~
线程是轻量级进程,因为进程太重量了,随着对于性能要求进一步提高,开始嫌弃线程也太重量了
于是引入了"协程".(轻量级线程,纤程)
这个协程的概念在后续的java版本中就被引入了,起的名叫做"虚拟线程"
3)内部匿名类
内部匿名类没有类目
Runnable子类对象
Thread子类对象
1.定义匿名内部类,这个类是Thread的子类
2.类的内部重写的父类run方法
3.创建了一个子类的实例,并且把实例的引用赋值给了t
匿名内部类就是一次性使用的类,用完就丢
4.lambda表达式创建Runnable子类
Thread thread=new Thread(() -> {while(true){System.out.println("这是线程输出的");try {Thread.sleep(1000);} catch (InterruptedException e) {e.printStackTrace();}}});thread.start();while(true){System.out.println("这是main运行的代码");try {Thread.sleep(1000);} catch (InterruptedException e) {e.printStackTrace();}}
这也是推荐的写法
后续多以lambda表达式来创建多线程
2.1Thread的常见构造方法
方法 | 说明 |
Thread() | 创建线程对象 |
Tread(Runnable target) | 使用Runnable对象创建线程对象 |
Thread(String name) | 创建线程对象,并命名 |
Tread(Runnable target,String name) | 使用Runnable对象创建线程对象,并命名 |
[了解]Thread(ThreadGoup goup,Runnable target) | 线程可以被用来分组管理,方便统一的设置线程的一些属性 |
注:给线程起名字不会影响到线程的执行效果,但是有一个合适的名字有利于调试的结果
不起名字默认就是Thread-0,Thread-1...
现在很少会使用线程组,线程的相关属性用的也不多,现在更多的是会使用线程池
2.2Thread的几个常见的属性
属性 | 获取方法 |
ID | getId() |
名称 | getName() |
状态 | getState() |
优先级 | getPriority() |
是否后台线程 | isDaemon() |
是否存活 | isAlive() |
是否被中断 | isInterrupted() |
1.ID是线程的唯一标识,不同的线程不会重复
JVM自动分配的,Thread对象的身份标识
2.名称是各种调试工具用到的
3.状态表示线程当前所处的一个情况,下面我们会具体说明
就绪/阻塞状态 就绪状态的名字为RUNNABLE
通常情况下,一个Thread对象,就是对应到系统内部的一个线程(PCB),但是也可能会存在一个情况,Thread对象存在,但是系统内部的线程已经没了/还没有创建
4.优先级高的线程理论上来说更容易被调度到
设计不同的优先级影响系统的调度,这里的影响是基于"统计"规则的影响,直接肉眼观察河南观察到效果
5.关于后台线程,需要记住一点:JVM会在一个进程的所有非后台线程结束后才会结束运行
后台线程:如果这个线程执行的过程中不能阻止进程结束(虽然线程在执行,但是进程结束了,这个线程也会被随之带走),这样的线程就是"后台线程"
前台线程:如果某个线程在执行的过程中,能够阻止进程的结束,这个线程就是"前台进程 "
6.是否存活,即简单的理解run的方法是否结束了
true为存在,false为无了
由于主线程和thread线程并发执行,主线程执行上述4个操作,执行了一部分的时候thread线程就执行打印了
RUNNABLE正在运行,实际上Java没有RUNNABLE这个线程状态,把正在CPU上运行和随时调度到CPU上运行的都统称为RUNNABLE
后台线程:
1)进程要结束(前台线程要结束),无力阻止
2)后台线程先结束也不影响进程的结束(其他前台进程的结束)
前台进程:
1)前台线程结束,此时进程就结束,后台线程也就随之结束
2)前台线程不结束,后台线程结束了不影响
一个进程中,前台线程可以有很多个(创建的线程默认就是前台的),必须所有的前台线程都结束,进程才能结束
代码中,创建的new Thread对象生命周期和内核中的实际的线程是不一定一样的
可能会出现Thread对象仍然存在,但是内核中的线程不存在的情况但是不会出现(Thread对象不存在,线程还在的情况)
1.调用start之前,内核中还没创建线程
2.线程的run执行完毕,内核的线程就没了,但是Thread对象仍然存在
2.3启动一个线程
调用start方法,才真正的在操作系统的底层创建出一个线程.
经典问题:start和run之间的区别
start:调用系统函数,真正的在操作系统内核中创建线程(创建PCB,加入到链表中),此处的start会根据不同的系统,分别调出不同的api(windows,linux,mac...),创建好新的线程之后再单独执行run
run:描述了线程要执行的的任务,也可以成为"线程的入口"
start的执行速度一般是比较快的(创建线程 比较轻量)
一旦start执行完毕,新线程就会开始执行,调用start的线程也会继续执行main
两个线程分别个做个的
调用start不一定非得是main线程调用的,任何线程都可以创建其他线程
如图:这里就是由thread调用了thread1线程
如果系统资源充裕,就可以任意的创建线程,(当然,线程不是越多越好);
一个Thread对象只能调用一次start,如果多次调用start就会出现问题(一个Thread对象只能对应系统中的一个线程)
IllegalThreadStateException 报错
由于Java中希望一个Thread对象能够对应到一个系统中的线程,因此就会 start中根据线程状态做出判定:如果Thread对象是没有start,此时的状态是一个NEW状态,接下来可以顺利调用start,如果已经调用过start,就会进入其他状态,只要不是NEW状态,接下来执行start都会抛出异常
2.4程序的中断(程序的终止)
目前常见的有以下的两种方法:
1.通过共享的标记来进行沟通
2.调用interrupt()方法来停止
例如:A,B两个线程,B正在运行,A想让B结束,核心在于A让B的run方法直接完毕,此时B就自然结束了,而不是说B的run执行一般,A直接把B强制结束了
用共享标记结束,例如:
private static boolean isQuit=false;public static void main(String[] args) throws InterruptedException {Thread thread=new Thread(()->{while(!isQuit){System.out.println("hello thread");try {Thread.sleep(3000);} catch (InterruptedException e) {e.printStackTrace();}}});thread.start();Thread.sleep(3000);System.out.println("main方法开始中断程序");isQuit=true;}
用isQuit来作为标志位
如果将isQuit放在main方法里面,作为局部变量,代码会报错
变量捕捉,是lambda表达式/匿名内部类的一个语法规则
isQuit和lambda定义在一个作用域中,此时lambda是可以访问到lambda外部(和lambda同一个作用域的变量),Java的变量捕捉有特殊要求,要求捕捉的变量得是final/事实final
写成成员变量之后就可以了是因为此时走的语法是"内部类访问外部类的成员"
lambda表达式本质上是一个"函数式接口"产生的"匿名内部类"
2.调用interrupt来终止
1)interrupt方法能够设置标志位,也能唤醒sleep等阻塞的方法(会抛出异常)
2)sleep被唤醒后,会清空标志位
如图可以看出,标志位被sleep清空,并没有中断此线程
线程A和线程B,A希望B线程终止
1)如果B线程想无视A线程,就直接catch中啥也不做
B线程仍然会继续执行
sleep清除标志位,就可以使B能做出这种选择,如果sleep不清楚标志位,B势必会结束,无法写出继续让线程继续的代码
2)如果B线程想立即结束就直接在catch中写上return或者break
此时B线程就会立即结束
3)如果B想稍后再结束,就可以在catch中写上一些其他的逻辑(比如释放资源,清除硬一些数据,提交一些结果....收尾工作)
这些逻辑完成之后再进行return/break;
这些给了程序员更多的操作空间
2.5等待一个线程-join()
操作系统针对多个线程的执行是一个"随机调度,抢占式执行"的过程
线程等待就是在确定两个线程的结束顺序
无法确定两个线程调度执行顺序,但是可以控制谁先结束,谁后结束
让后结束的线程等待先结束的线程即可,此时后结束的线程会进入阻塞,一直到先结束的线程真的结束了,阻塞才结束
方法 | 说明 |
public void join() | 等待线程结束 |
public void join(long millis) | 等待线程结束,醉倒等millis毫秒 |
public boid join(long millis,int nanos) 一般不用,后面是精确到纳秒 | 同理,但可以更高精度 |
因为计算机很难做到精确计时,一般能精准到ms就不错了
和操作系统相关像windows,linux系统,线程调度开销比较大,操作系统中还有一类系统"实时操作系统"就能把调度开销尽可能降低,开销小于一定的误差要求,从而可以做到更精准
Thread thread1=new Thread(()->{for(int i=0;i<3;i++){System.out.println("Hello thread1");try {Thread.sleep(1000);} catch (InterruptedException e) {e.printStackTrace();}}});Thread thread2=new Thread(()->{for(int i=0;i<4;i++){System.out.println("Hello thread2");try {Thread.sleep(1000);} catch (InterruptedException e) {e.printStackTrace();}}});System.out.println("main开始");thread1.start();thread2.start();try {thread1.join();} catch (InterruptedException e) {e.printStackTrace();}try {thread2.join();} catch (InterruptedException e) {e.printStackTrace();}System.out.println("main结束");
此代码为例,就是main线程等待thread1和thread2结束后才往下运行
当然不一定是main去等待,thread1也可以去等待thread2结束后再继续
代码如下
public static void main(String[] args) {Thread thread1=new Thread(()->{for(int i=0;i<3;i++){System.out.println("Hello thread1");try {Thread.sleep(1000);} catch (InterruptedException e) {e.printStackTrace();}}});Thread thread2=new Thread(()->{try {thread1.join();} catch (InterruptedException e) {e.printStackTrace();}for(int i=0;i<4;i++){System.out.println("Hello thread2");try {Thread.sleep(1000);} catch (InterruptedException e) {e.printStackTrace();}}});System.out.println("main开始");thread2.start();thread1.start();try {thread2.join();} catch (InterruptedException e) {e.printStackTrace();}System.out.println("main结束");}
这里就是thread2中等待thread1
2.6获取当前线程的引用
方法 | 说明 |
public static Thread currentThread(); | 返回当前线程对象的引用 |
2.7休眠当前线程
线程执行sleep,就会使得这个线程不参与CPU调度,从而把资源让出来给别人使用
"放权"
方法 | 说明 |
public static void sleep(long millis) throws InterruptedException | 休眠当前线程 |
public static void sleep(long millis,int nanos) throws InterruptedException | 可以更高精度精确到纳秒 |
3.线程的状态
进程的状态:
就绪:正在CPU上执行,或者随时可以去CPU上执行
阻塞:暂时不能去参与CPU执行
Java的线程对于状态做了更详细的区分,不仅仅是就绪和阻塞了
六种状态
1.NEW 这种Thread对象虽然有了,但是内核的线程还没有(还没有调用start)
public static void main(String[] args) {//匿名内部类的lambda表达式创建Runnable子类对象,备份下Thread thread=new Thread(() -> {for (int i=0;i<3;i++){System.out.println("Hello Thread");try {Thread.sleep(1000);} catch (InterruptedException e) {e.printStackTrace();}}});System.out.println(thread.getState());thread.start();
}
2.TERMINATED 当前Thread对象虽然还在,但是内核的线程已经销毁(线程已经结束了)
public static void main(String[] args) {//匿名内部类的lambda表达式创建Runnable子类对象,备份下Thread thread=new Thread(() -> {for (int i=0;i<3;i++){System.out.println("Hello Thread");try {Thread.sleep(1000);} catch (InterruptedException e) {e.printStackTrace();}}});thread.start();try {Thread.sleep(5000);} catch (InterruptedException e) {e.printStackTrace();}System.out.println(thread.getState());}
3.RUNNABLE 就绪状态.正在CPU上运行+随时可以去CPU上去运行
public static void main(String[] args) {//匿名内部类的lambda表达式创建Runnable子类对象,备份下Thread thread=new Thread(() -> {while(true){}});thread.start();System.out.println(thread.getState());}
4.BKLOCKED 因为锁竞争引起的阻塞
public static void main(String[] args) throws InterruptedException {//匿名内部类的lambda表达式创建Runnable子类对象,备份下Thread thread1=new Thread(() -> {synchronized (Locker) {for (int i = 0; i < 50000; i++) {count++;}}});Thread thread2=new Thread(()->{synchronized(Locker){for(int i=0;i<50000;i++) {System.out.println(thread1.getState());count++;}}});thread1.start();thread2.start();thread1.join();thread2.join();System.out.println(count);}
5.TIMED_WAITING 有超时时间的阻塞等待,比如sleep或者join带参数版本
public static void main(String[] args) {//匿名内部类的lambda表达式创建Runnable子类对象,备份下Thread thread=new Thread(() -> {while(true){System.out.println("hello thread");try {Thread.sleep(3000);} catch (InterruptedException e) {e.printStackTrace();}}});thread.start();try {Thread.sleep(1000);} catch (InterruptedException e) {e.printStackTrace();}System.out.println(thread.getState());}
6.WAITING 没有超时时间的阻塞等待,join/wait
public static void main(String[] args) throws InterruptedException {//匿名内部类的lambda表达式创建Runnable子类对象,备份下Thread thread=new Thread(() -> {while(true){}});Thread thread2=new Thread(()->{try {thread.start();thread.join();} catch (InterruptedException e) {e.printStackTrace();}});thread2.start();Thread.sleep(3000);System.out.println(thread2.getState());}
学习线程状态主要就是为了调试,比如遇到某个代码功能没有执行就可以观察对应线程的状态,看看是否是因为一些原因阻塞了.