前言👀~
上一章我们介绍了什么是进程,对于进程就了解那么多即可,我们作为java程序员更关注线程,线程内容比较多,所以我们要分好几部分才能讲完
目录
进程的缺点
多线程(重要)
进程和线程的区别(经典面试题)
java如何进行多线程编程?
创建线程的方式(面试题)
继承Thread类(来自java.lang包下)
使用匿名内部类继承Thread类重写run
实现Runnable接口
使用匿名内部类实现Runnable接口重写run
使用lambda表达式创建线程(推荐)
Thread 类及常见方法
Thread类的构造方法
Thread类的常见属性
启动线程
中断线程
等待线程
查看线程状态
如果各位对文章的内容感兴趣的话,请点点小赞,关注一手不迷路,如果内容有什么问题的话,欢迎各位评论纠正 🤞🤞🤞
个人主页:N_0050-CSDN博客
相关专栏:java SE_N_0050的博客-CSDN博客 java数据结构_N_0050的博客-CSDN博客 java EE_N_0050的博客-CSDN博客
进程的缺点
多进程编程的缺点:进程太重量效率不高,创建进程和销毁进程和调度进程消耗的时间都是比较多的(消耗在申请资源上),因为我们知道进程是系统资源分配的基本单元,所以在给进程分配资源的时候是一个大活。拿分配内存说,操作系统内部也有一定的数据结构,用来管理空闲的内存,当进程申请内存空间的时候,操作系统就会从这个数据结构中找到大小合适空闲的内存返回给进程。这里的数据结构可以提高一定的效率,但是总体来说和线程相比还是比较耗时的。同时频繁创建和销毁进程和进程切换是一个开销很大的操作,多进程和多线程都能实现并发编程,线程比进程更轻量。有些任务场景需要 “等待 IO”, 为了让等待 IO 的时间能够去做⼀些其他的工作, 也需要用到并发编程. 其次,虽然多进程也能实现 并发编程, 但是线程比进程更轻量
多线程(重要)
进程想要执行任务就需要依赖线程。线程不能独立存在,需要依附于进程(进程包含线程,进程可包含一个线程也可包含多个线程),一个进程在最开始的时候,至少要有一个线程(主线程),这个线程负责完成执行代码的工作,我们也可以根据需要创建多个线程,从而实现"并发编程"的效果换句话说,就是进程中的最小执行单位就是线程
线程也称轻量级线程(创建、销毁、调度都比进程快),每个线程就是一个独立的 "执行流"(因为在执行用户写的代码)可以独立的执行一些代码,每一个线程可以执行一系列的操作(也就是代码)
更好的理解线程,还是拿之前在进程举的演员的例子,一个舞台可以有多个演员,但是呢这个多个演员来自不同的剧组,这里面的演员就是线程,剧组就是进程。在我们之前谈的进程调度都是基于一个进程只有一个线程,可以理解为之前每个剧组都只有一个演员。实际上,一个进程可以有多个线程,每个线程都可以独立进行调度。之后谈到进程调度的话,不是调度整个进程,而是调度进程中的每一个线程。就比如说这个导演叫这个剧组的所有人来拍戏,所有的演员由导演进行分配角色、上场时间、台词等,相当于线程也有状态、优先级、上下文、记账信息
下面有图更好理解多线程
一个工厂代表一个进程,一个生产线代表一个线程,我们之前提到的多进程是下面这样子的
下面这样是我们说的多线程,一个进程中有多个线程,同个工厂(共用资源)两个生产线(多线程)提高了效率
下面这么多人吃100个坤,适当的线程数目(里面的人)能提高效率,但是线程数目过多的情况,效率会降低并且造成线程冲突(线程不安全)
当人数过多且有人吃不到坤的时候,生气了把鸡全扔了,此时引发线程异常,如果我们没有处理好,可能会导致整个进程崩了,其他线程也会随之消失
总结:一个进程使用PCB来表示,一个进程可以使用一个PCB表示也可与使用多个PCB表示,每个TCB对应一个线程(可以理解PCB中包含TCB),并且每个线程都有这些信息(状态、优先级、上下文、记账信息,但每个线程有自己的程序计数器、虚拟机栈和本地方法栈)辅助调度,除此之外,前面说到的属性pid是相同的,内存指针、文件描述符表也是共用一份的,根据这些信息我们可以得出线程的特点:每个线程都可以独立去cpu上调度执行、同一个线程的多个进程之间共用一份内存空间和文件资源,所以我们创建线程的时候不需要像进程一样申请资源(但是呢创建第一个线程的时候,相当于和进程一起创建的,所以我们要去申请资源,这里申请资源算在进程头上。后续再创建线程的话就是共享同一份了),我们直接用系统给进程分配好的资源,这样大大提高了我们的效率和节省开销。综上所述我们可以得出进程是资源分配的基本单位,线程是CPU 调度执行的基本单位
每个线程都是独立调度的,在调度的过程中,系统就不考虑 进程 这样的概念了。所以就是不同进程中的线程可以被轮番调度,每个线程只有获得 CPU 的使用权才能执行指令。所谓多线程的并发运行,其实是指从宏观上看,各个线程轮流获得 CPU 的使用权,分别执行各自的任务。
进程和线程的区别(经典面试题)
1.线程比进程更轻量、高效,线程不需要申请资源,和同一进程共用一份,省去了申请资源的开销
2.同一个进程内线程和线程之间会有影响(线程不安全和线程异常),一个线程崩了可能导致其他线程受到影响最终导致进程崩了。进程和进程之间具有独立性
3.线程依附于进程,一个进程至少有一个线程(主线程),也可以有多个线程
4.线程是调度执行的基本单位,进程是资源分配的基本单位
java如何进行多线程编程?
线程是操作系统的概念,操作系统提供了一套API来操作线程,java对操作系统提供的API进行封装(跨平台),我们学java的只需要掌握java封装过后的这套API就可以操作线程了。
进程和进程之间能并发执行实现并发编程,线程和线程之间也能实现并发执行实现并发编程,我们学java的更侧重线程之间的并发执行
创建线程的方式(面试题)
继承Thread类(来自java.lang包下)
创建Thread对象,我们就可以操作 操作系统内部的线程了。以及重写入口方法run(描述了该线程要执行的任务)
class MyThread extends Thread {@Overridepublic void run() {System.out.println("我开始工作了");System.out.println("我结束工作了");}
}public class test1 {public static void main(String[] args) {Thread thread = new MyThread();thread.start();System.out.println("我是主线程");}
}
输出:不确定
再来看下面这段代码,猜一下输出顺序
class MyThread extends Thread {@Overridepublic void run() {while (true) {System.out.println("thread线程正在工作");}}
}public class test1 {public static void main(String[] args) {Thread thread = new MyThread();thread.start();while (true) {System.out.println("主线程正在工作");}}
}
输出:交替输出
为什么呢?我们也不知道这两个线程是同时执行的还是交替执行的(同一个核心上执行,还是分别在两个核心上执行),所以我们统称并发(并行+并发),实现并发编程的效果,为什么要实现并发编程?(充分利用多核cpu的资源)。
注意:打印顺序不一定,操作系系统对于多个线程的调度顺序是不确定的,随机的。虽然有先后顺序但是谁先谁后我们是不确定的(重要),这个随机取决于操作系统对于线程调度的模块(调度器)的具体实现
接着再来看下面这段代码,猜一下输出顺序
class MyThread extends Thread {@Overridepublic void run() {while (true) {System.out.println("thread线程正在工作");}}
}public class test1 {public static void main(String[] args) {Thread thread = new MyThread();thread.run();while (true) {System.out.println("主线程正在工作");}}
}
输出:thread线程正在工作 不只一条哈
原因:T.run 这种时候只有一个主线程,因为我们没有创建一个新的进程。等run这个方法结束后,才会执行后面的代码,相当于只有一个执行流,只能依次执行循环。相当于就是main线程在执行它的run方法,就跟我们在main方法中平常创建一个类然后调用方法一样,main线程在工作
不信的话代码拿去自己试试,然后用jdk中自带的工具jconsole,里面其他线程是JVM创建的
使用匿名内部类继承Thread类重写run
同样创建Thread对象,我们就可以操作 操作系统内部的线程了。以及重写入口方法run(描述了该线程要执行的任务)
public class test1 {public static void main(String[] args) {Thread thread = new Thread() {@Overridepublic void run() {System.out.println("我是匿名内部类");}};thread.start();System.out.println("我是主线程");}
}
实现Runnable接口
实现Runnable接口,它就表示的是一个可以运行的任务,所以还是需要创建线程来完成这个任务。这样理解我写了个任务,丢给线程去完成,但是我们要先把线程创建出来才能去完成
class MyRunnable implements Runnable {@Overridepublic void run() {while (true) {System.out.println("我是Runnable接口");}}
}public class test1 {public static void main(String[] args) {MyRunnable myRunnable = new MyRunnable();Thread thread = new Thread(myRunnable);thread.start();while (true) {System.out.println("主线程正在工作");}}
}
使用Runnable接口和继承Thread类的区别:主要是为了解耦合,使用Runnable接口相当于跟线程拆分开,把任务抽离出来,可以把这个任务丢给任意一个线程去完成,可以用在需要重复完成这个任务的场景,直接继承Thread类就不行,它更适合完成一次性的任务
还有关于创建一个线程的时候,有两个关键的操作。一个是明确线程要执行的任务,我们更关注任务本身,如果这个任务就只是执行一段简单的代码至于用什么方式实现这个任务没什么区别。如果遇到复杂的任务有些方式可能就完成不了,这时候我们需要用其他的方式去完成,这时候我们把任务提取出来,我们可以自己选择指定的方式去完成这个任务 。另外一个操作就是通过调用系统API创建出线程。
使用匿名内部类实现Runnable接口重写run
public class test1 {public static void main(String[] args) {Thread thread = new Thread(new Runnable() {@Overridepublic void run() {System.out.println("我是匿名内部类");}}) {};thread.start();System.out.println("我是主线程");}
}
使用lambda表达式创建线程(推荐)
public class test1 {public static void main(String[] args) {Thread thread = new Thread(() -> {System.out.println("我是lambda表达式");});thread.start();System.out.println("我是主线程");}
}
除了上面的方式之外,还有其他的方式,后续讲解,还有一个点需要清楚就是创建Thread类的时候并没有真正创建线程,只有在调用start方法的时候调用系统API去创建线程的时候才算认识
Thread 类及常见方法
Thread类的构造方法
在Thread类源码里有个构造方法可以设置线程的名字,其他的就没什么好介绍的了
public class test1 {public static void main(String[] args) {Thread thread = new Thread(() -> {while (true) {System.out.println("我是lambda表达式");}}, "我叫线程A");thread.start();}
}
为什么这里没有显示main线程呢?因为main线程执行完了,线程的入口方法执行完了,这个线程自然就销毁了。对于主线程来说,入口方法就是main方法,它调用系统API去创建完线程之后就执行完了。所以如果线程都执行完了,进程也就结束了但只要有一个线程还在执行,进程就不会结束
Thread类的常见属性
1.ID:线程的身份标识就是用来区分线程的,类似进程的pid,只不过这里的ID是java提供的,不是系统api提供的
2.getState()方法:获取线程状态,后面有讲到
3.isDaemon()方法:判断是否为后台线程(守护线程),守护线程就是用来告诉JVM,我的这个线程不重要不需要等待它运行完才退出,让JVM喜欢什么时候退出就退出。前台线程(非守护线程)就是告诉JVM,这个线程没执行完成之前,你不能退出。默认情况一个线程是前台线程守护进程(后台进程),默认情况一个线程是前台线程,我们可以通过setDameon()方法去设置,这么设置之后主线程执行完后,没有其他前台线程了,这个进程自然也就结束了
public class test1 {public static void main(String[] args) {Thread thread = new Thread(() -> {while (true) {System.out.println("我是lambda表达式");}}, "我叫线程A");thread.setDaemon(true);//设置thread线程为守护线程thread.start();}
}
4.isAlive()方法:Thread对象的生命周期要比系统内核中的线程更长,线程没了Thread对象还在,我们要以系统内核中的线程为主。所以我们使用isAlive()进行判断,判断系统内核中的线程有没有结束,结束返回false,没结束返回true。简单的理解为 run 方法是否运行结束了
public class test1 {public static void main(String[] args) throws InterruptedException {Thread thread = new Thread(() -> {try {Thread.sleep(1000);} catch (InterruptedException e) {throw new RuntimeException(e);}System.out.println("我是lambda表达式");}, "我叫线程A");System.out.println(thread.isAlive());thread.start();System.out.println(thread.isAlive());Thread.sleep(2000);System.out.println(thread.isAlive());}
}
输出:false true 我是lambda表达式 false
5.isInterrupted()方法:用来判断线程是否被中断,怎么判断呢?Thread内部有一个标志位进行判断。下面会讲到
启动线程
使用start()方法创建线程
start方法和run方法的区别:非常直白的说,你可以把run看作是任务,start是叫人过来完成这个任务的。专业点说就是start方法通过调用系统API在系统内核中创建线程然后执行run方法中的代码。run方法会在线程创建好后自动被被调用
中断线程
中断一个线程(终止/打断),让一个线程停止运行(销毁),在java中销毁/终止一个线程做法比较唯一(这个唯一不是说只有一个方法,而是说销毁一个进程是让run方法快点执行完),让run方法快点执行完。在C++中是可以直接强制终止一个线程的运行,就比如你打开一个文本编辑器输入一段信息,输入一半直接给你干没了
先补充两个方法currentThread()方法和sleep()方法后面要用到
获取当前线程引用:currentThread()方法返回当前线程对象的引用,哪个线程调用这个方法就获取哪个线程对象的引用
休眠当前线程:sleep()方法让线程睡觉的,你可以设置睡多久,然后时间到了系统把它叫醒(阻塞->就绪),注意睡醒之后不会马上回到cpu上运行,要排队,这里会涉及调度所以会有一定的开销。就是你睡醒了需要一点时间缓缓才能去工作。
interrupt()方法:使用interrupt()方法设置标志位,前面说了Thread内部有一个标志位用来判断线程是否结束,调用这个方法就把这个标志位设置成true。这样即使我们还在执行sleep方法,它也会被强制唤醒。
来猜猜下面执行代码线程会是什么状态以及输出什么
public class test1 {public static void main(String[] args) throws InterruptedException {Thread thread = new Thread(() -> {while (!Thread.currentThread().isInterrupted()) {System.out.println("hello");try {Thread.sleep(1000);} catch (InterruptedException e) {e.printStackTrace();}}});thread.start();Thread.sleep(3000);thread.interrupt();}
}
答案是输出几个hello后报异常,然后说说过程,这个线程先执行sleep方法,然后这个线程处于睡眠,然后你使用了interrupt()方法设置把标志位为true,把这个线程唤醒了,那么这个线程就会继续工作,除非你不让它睡眠并且设置标志位为true。举个例子本来你在上班,然后突然有点困了,你同事让你睡会,结果领导来了你同事赶紧把你叫醒,你立马起来了。
使用interrupt()方法搭配sleep方法这样设置的标志位就像没效果一样,没有把你的线程中断,为什么这样设定呢?java期望线程收到中断的信息的时候,我们能够自己决定接下来要怎么处理,这样可以让我们在开发中有更多的操作空间,前提是通过异常的方式去唤醒。比如你打游戏女朋友叫你陪她去逛街,你可以直接关掉游戏陪她去,也可以说等我打完这把,还可以当个聋子啥也没听见。
public class test1 {public static void main(String[] args) throws InterruptedException {Thread thread = new Thread(() -> {while (!Thread.currentThread().isInterrupted()) {System.out.println("hello");try {Thread.sleep(1000);} catch (InterruptedException e) {e.printStackTrace();
// 第一种 System.out.println("一直工作");
//
// 第二种 System.out.println("工作一会休息了");
// break;
//
// 第三种 break;}}});thread.start();Thread.sleep(3000);thread.interrupt();}
}
等待线程
使用join()方法,让一个线程等待另外一个线程执行结束再执行。前面说线程并发执行的时候,执行的顺序是不确定的随机的,此时我们可以通过这个方法来控制线程结束的顺序。并且我们可以设置等待时间,如果没有设置等待时间,默认的话这个线程会一直等到那个线程执行结束后才会执行自己的任务,类似舔狗有一直舔的也有舔一段时间不舔了
join方法的工作过程:
1.如果主线程正在运行,主线程中调用了A.join方法,此时主线程进入阻塞状态,A线程执行完,主线程才会解除完成接下来的任务
public class test1 {public static void main(String[] args) throws InterruptedException {Thread thread = new Thread(() -> {for (int i = 0; i < 5; i++) {System.out.println("我先干完活,你再干");}});thread.start();System.out.println("我要干活了");thread.join();System.out.println("轮到我干活了");}
}
输出结果
2.如果主线程任务已经执行完了,再调用A.join方法,就不用进入阻塞状态了,直接结束了
public class test1 {public static void main(String[] args) throws InterruptedException {Thread thread = new Thread(() -> {for (int i = 0; i < 5; i++) {System.out.println("我先干完活,你再干");}});thread.start();System.out.println("我马上干完不给你机会干");thread.join();}
}
输出结果
查看线程状态
和进程一样,线程也有运行、就绪、阻塞状态。真正在系统调度的还是线程,线程是调度执行的基本单位
在java中,给线程赋予了一些其他的状态:
1.NEW:Thread对象已经存在了,但是系统线程还没创建,也就是还没调用start方法
2.RUNNABLE:就绪状态,这里有两种表示,一种是在cpu上执行了,另一种是正等着去cpu上执行
3.TIMED_WATING:阻塞,被sleep这种固定时间的方式打断产生的阻塞
4.WATING:阻塞,被wait这种不固定时间的方式打断产生的阻塞
5.BLOCKED:阻塞,等待锁导致的阻塞,后续死锁讲解
6.TERMINATED:Thread对象还在,操作系统内核的线程结束了,也这样理解意味着该线程已经完成了其run方法的执行
public class test1 {public static void main(String[] args) throws InterruptedException {Thread t = new Thread(() -> {while (!Thread.currentThread().isInterrupted()) {System.out.println("java");try {Thread.sleep(2000);} catch (InterruptedException e) {break;}}});System.out.println(t.getState());//只创建了Thread对象的NEW状态t.start();System.out.println(t.getState());//创建完线程t就是这个RUNNABLE状态Thread.sleep(1000);System.out.println(t.getState());//t线程处于睡眠t.interrupt();Thread.sleep(1000);System.out.println(t.getState());//t线程执行完了}
}
输出结果
后续线程出现卡死的情况,我们可以通过上述后面三种状态去确定卡死的原因是什么,最后两个状态后续再讲解
上述方法是多线程中非常常用的方法,要好好掌握,今天的内容就先到这,后面我们接着讲解线程还有不少的知识点等着💕