【多线程的概念】
之前写过的所有代码,都只能使用“一个核心”,此时无论如何优化代码,最多只能用一个cpu核心,把这个核心跑满了,其他核心也是空闲着,通过写特殊的代码,把多个cpu核心都能应用起来,此为“并发编程”
之前使用的并发模式为“多进程编程”,其在创建/销毁进程时开销较大,一旦需求的场景需要频繁地创建/销毁进程,开销就十分明显了(最典型的就是服务器开发),为了解决这个问题,发明了“线程”(可以理解为更轻量的进程,也可以解决并发编程的问题,开销比进程低)
因此,多线程的编程成为了当前并发编程的主要模式
线程比进程更轻量,主要在于创建线程,省去了“分配资源”过程,销毁线程,省去了“释放资源”过程(一旦创建进程,同时也会创建第一个线程,就会负责分配资源,一旦后续创建第二个第三个等进程,就不必再重新分配资源了)
多进程:100只鸡分配到两个房间中,两个人在各自房间中吃鸡
多线程:100只鸡在一个房间中,两个人一起吃鸡(省去了再分配一个房间,再回收一个房间的操作)
【具体解析】
进程在系统中通过“PCB”这样的结构体来进行描述,通过“链表”这样的数据结构形式来组织的
而线程同样也通过“PCB”来描述(仅限于Linux)
一个进程,是一组PCB,一个线程,是一个PCB,两者之间是包含关系:一个进程中,可以包含多个线程,此时每个线程,都可以独立的到cpu上调度执行
线程是系统“调度执行”的基本单位
进程是系统“资源分配”的基本单位
【执行流程】
一个可执行程序运行时,操作系统会创建进程,给这个程序分配各种系统资源(cpu 内存,硬盘……),同时也会在这个进程中,创建一个或多个线程,这些线程再去cpu上调度执行
//如果有多个线程在一个进程中,每个线程都会有自己的状态,优先级,上下文,记账信息,每个都会各自独立的在cpu上调度执行
//进程包含线程,而包含,要么是包含一个,要么多个,不能没有(进程中不能没有线程)
【注意】
能够提高效率,关键是充分利用多核心进行“并发执行”,如果只是“微观并发”,速度不会提升,真正能提升速度的是“并行”,凡事有度,尽管线程越多处理越快,但如果线程数目太多,比如超出了cpu核心数目,此时就无法在微观上完成所有线程的“并行”执行,势必存在严重的“竞争”,可能会影响效率
//n个人吃一堆鸡,人如果太多,那就有人吃不到
【线程安全问题】
同一个进程中的线程之间,共用的是同一份资源(内存资源,指代码中定义的变量/对象),如果多个线程针对同一个变量进行读写操作(尤其是写操作),就容易发生冲突,一旦发生冲突,就可能使程序出现问题,此为“线程安全问题”
//两个人看上去同一只鸡,开始争抢,打起来了
【异常与崩溃】
当一个进程中有多个线程时,一个某个线程抛出异常,此时如果能妥善处理那还好,一旦处理不当可能导致整个进程都崩溃,因此其他线程会随之崩溃
但多个进程之间,一般不会相互影响,这一点也称为“进程的隔离性”
//一个人突然犯病,于是想掀桌,其他人来镇压,镇压成功无事发生,镇压失败则掀桌,所有人吃不了鸡
【注意2】
核心,cpu
“核心”是cpu的核心,是硬件设备
如果把cpu当成工厂,cpu的核心就是干活工人,核心数量越多,干活工人越多
进程
进程是系统管理的软件资源,进程也叫“任务”,这些任务要交给这些核心来进行
【总结:进程和线程的概念区别】(重点掌握)
1.进程包含线程
一个进程中可以有一个线程,也可以有多个线程,不能没有线程
2.线程是系统“调度执行”的基本单位,进程是系统“资源分配”的基本单位
3.同一个进程里的线程之间,共用同一份系统资源(内存,硬盘,网络带宽等),尤其是“内存资源”,就是代码中定义的变量/对象,编程中,多个线程可以共用同一份变量
4.线程是当下实现并发编程的主流方式,通过多线程,可以充分利用好多核CPU,但也不是线程数目越多就越好,线程数量达到一定程度,把多个核心都利用充分后,此时继续增加线程,无法再提高效率,甚至可能会影响效率(线程调度,也是有开销的)
5.多个线程之间共享资源,共享意味着可能会相互影响,线程安全问题中,一个线程抛出异常,可能会带走其他线程
6.多个进程之间一般不会相互影响,一个进程崩溃了也不会影响到其他进程,即“进程的隔离性”
【在Java中编写多线程程序】
线程本身是操作系统提供的概念,操作系统提供API,供程序员调用,而Java(JVM)把这些系统API封装好了,因此无需关心原生API,只需要了解Java提供的这套API即可
class MyThread extends Thread
{
}
此处Thread类可以直接使用,而无需导包(这个类在Java.lang这个包中,Java.lang中的类都是默认无需导包的)
【创建线程】
继承Thread类不是主要目的,最主要的目的是重写Thread类中的run方法,在run方法中的代码,就是即将创建出的线程要执行的逻辑
class MyThread extends Thread
{
public void run()
{
System.out.println("hello world");
}
}
class Demo
{
public static void main(String[] args)
{
MyThread t = new MyThread();//真正的创建线程
t.start();
}
}
这是一个最简单的多线程代码,它分为两部分
第一部分:MyThread继承父类Thread,并重写其内部的run方法,第二部分
第二部分:在main方法中创建MyThread类的实例,再使用它提供的start方法来去创建线程
调用start就会在进程内部创建出一个新的线程,新的线程就会去执行刚才run方法中的代码
注意:像run这种,用户手动定义了,但是没有手动调用,最终这个方法被系统/库/框架在合适的时机进行自行调用,此时这样的方法就称为“回调函数”
这个代码执行起来是一个进程,但这个进程中包含了两个线程,其中调用main方法会自动创建一个线程,这个线程为“主线程”,一个进程中至少要有一个线程,至少的这一个线程,就是主线程
t.start();
而这条代码相当于创建了一个新的线程,主线程和新线程会并发/并行的在cpu上执行
【并发执行】
class MyThread extends Thread
{
public void run()
{
while(true) {
System.out.println("hello A");
}
}
}
class Demo
{
public static void main(String[] args)
{
MyThread t = new MyThread();//真正的创建线程
t.start();
while(true){
System.out.println("hello B");
}
}
}
执行该代码后,可以看到在控制台中高频快速地进行二者输出的交叉式打印,这就是“并发进行”
【休眠操作】
这种短时间内高频快速地进行二者输出的交叉式打印对机器会造成大量负荷,因此在循环中可以加上休眠操作,让循环每循环一次都休息一会儿,避免cpu消耗过大
这是一个静态方法,需要通过类名Thread去调用
public void run()
{
while(true) {
System.out.println("hello A");
try {
Thread.sleep(1000);//休眠操作
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
class Demo
{
public static void main(String[] args)
{
MyThread t = new MyThread();//真正的创建线程
t.start();
while(true){
System.out.println("hello B");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
这里处理异常只能try/catch而不能throw的原因是:这个代码在重写的run方法内部,父类中的run没有throws这样的异常声明,因此子类重写时不能增加throws,只能try/catch
多线程之间,谁先去CPU上调度执行,这个过程是“不确定的”,不是数学意义上的随机,而是这个调度顺序取决于操作系统内核中“调度器”的实现,调度器中内有一套规则,但程序员无法干预也无法感受,只能把这个过程视为 “随机”
【创建线程其他的写法】
【Runnable】
实现Runnable,重写run
Runnable是一个接口,因此需要用到interface,以实现的方式进行表述
class MyRunnable implements Runnable
{
public void run()
{
while(true) {
System.out.println("hello A");
Thread.sleep(1000);
}
}
}
public class Demo2
{
public static void main(String[] args) {
MyRunnable runnable = new MyRunnable();
Thread t = new Thread(runnable);
t.start();
while(true){
System.out.println("hello B");
Thread.sleep(1000);
}
}
}
Runnable用来描述要执行的任务是什么,通过Thread创建线程,线程要描述的任务是通过Runnable来描述的,而不是通过Thread自己来描述的,其与第一种方式的区别就在这里
【匿名内部类】
Thread t = new Thread()//定义匿名内部类,这个类是Threaad类的子类
{
public void run()
{
}
};
本质上和第一种方法一样,继承Thread,重写run,通过匿名内部类来实现
匿名内部类,一般就是“一次性使用”的类,用完就丢,但是很方便
【基于匿名内部类的方式】
Thread t = new Thread(new Runnable() {
@Override
public void run() {
while(true){
System.out.println("hello B");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
);
这个写法,在Thread这个构造方法中直接写new Runnable,去定义匿名内部类,整体作为Thread方法的参数
【基于lambda的方式】
Thread t = new Thread(() ->{
while(true){
System.out.println("hello B");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
});
【Thread类其他的属性和方法】
给线程命名的方法,不起名字则默认叫做Thread-0,Thread-1……
【线程的属性】
ID,名称,状态,优先级:JVM自动分配,不能手动设置
状态:java中把线程的状态,分为阻塞与就绪
优先级:设置不同的优先级,会影响到系统的调度,这里的影响是基于“统计”规则的影响,直接肉眼观察,很难观察到结果
【前台,后台线程】
如果这个线程在执行过程中,能够阻止进程结束此时这个线程就是“前台线程”
如果这个线程在执行过程中,不能阻止进程结束(虽然线程在执行着,但是进程要结束了,此时这个线程也会随之被带走),这样的线程就是“后台线程”
【前台,后台线程】
如果这个线程在执行过程中,能够阻止进程结束,此时这个线程就是“前台线程”
如果这个线程在执行过程中,不能阻止进程结束(虽然线程在执行着,但是进程要结束了,此时这个线程也会随之被带走),这样的线程就是“后台线程”
举个例子:
上述过程中,主持人决定了这场宴席什么时候结束,当他宣布结束时,宴席就散了,若“我”是个只会干饭的小透明,无法决定宴席何时结束,在主持人还没有宣布宴席结束时溜了也不影响宴席继续
那么“我”就是“后台线程”
1.进程要结束,(前台线程要结束),后台线程无力阻止
2.后台线程先结束了,也不影响进程的结束
主持人就是“前台线程”
1.前台线程宣布结束,此时进程就结束,后台线程也会随之结束
2.前台线程不结束,后台线程结束了也不影响