上一章节中,我们知道,进程是可以很好的解决并发编程这样的问题,但在一些特定的情况下,进程的表现是不尽人意的。比如:有些场景下,需要频繁的创建和销毁进程,此时使用多进程编程,系统开销就会很大
在早期Web开发中,就是基于多进程的编程模式来开发的。服务器同一时刻会收到很多请求,针对每个请求,它都会创建出一个进程,给这个请求提供一定的服务,然后返回对应的响应,一旦这个请求处理完了,此时这个进程就要销毁掉
开销比较大最关键的原因:
结论:进程在进行频繁创建和销毁时,开销是很大的,所以引进了线程这一概念
前面提到在操作系统内核中创建出线程,但什么是内核呢?
内核,是操作系统中,最核心的功能模块,其作用是:管理硬件,给软件提供稳定的运行环境
举一个例子,张三去银行办理业务,他只能通过工作人员去代办他的需求,他不能进入办事窗口自己办理业务(如果自己能进去,那警察叔叔可就要上门请你喝茶了)
上述例子中,工作人员所待的办事窗口,在操作系统中称为 ”内核空间(内核态)“,而张三所待的大堂,在操作系统中称为 ”用户空间(用户态)“
我们平时运行的普通程序如:QQ音乐,微信等程序都是运行在 用户态 的,这些程序在有的场景下,需要针对一些系统提供的 软硬件资源 进行操作
如:QQ音乐在播放音乐时,需要对扬声器进行操作;打微信视频电话时,需要对摄像头进行操作
这些操作,都 不是应用程序直接操作 的,此时需要调用系统所提供的 api,进一步在内核中完成这样的操作
问题:为什么要划分出内核态和用户态呢?
答:最主要的目的还是为了 “稳定”。如果给应用程序的权限太大,使它可以直接操作你的硬件,如果运行过程中出现了bug,就很可能导致将硬件给干烧了,直接用不了了
所以,操作系统封装了一些 api,这些 api 属于 “合法” 操作,应用程序只能调用这些 ”合法“ 的 api,这样就不至于对系统/硬件设备造成危害
线程,也可以称为 “轻量级进程” ,其优点在于:
保持了独立调度执行,但同时省去了 “分配资源” 和 “释放资源” 带来的额外开销
一个线程就是一个 “执行流”,每个线程之间都可以按照顺序执行自己的代码。多个线程之间 “同时” 执行着多份代码
设想如下场景:
一家公司要去银行办理业务,既要进行财务转账,又要进行福利发放,还得缴社保
如果只有张三一个会计就会忙不过来,耗费的时间特别长。为了让业务更快的办理好,张三又找来两位同事李四、王五一起来帮助他,三个人分别负责⼀个事情,分别申请⼀个号码进⾏排队,⾃此就有了三个执⾏流共同完成任务,但本质上他们都是为了办理同一个业务。此时,我们就把这种情况称为多线程,将⼀个⼤任务分解成不同⼩任务,交给不同执⾏流就分别排队执⾏。其中李四、王五都是张三叫来的,所以张三⼀般被称为主线程(Main Thread)
为啥要有线程
进程和线程的区别
比如,张三、李四、王五,其中李四挂了,李四所负责的任务就要落到其他两人身上,其他两人不堪重负,也纷纷挂掉了,这个业务就没法执行了!
线程多了也不是一件好事
Java 的线程 和 操作系统线程 的关系
代码:
class MyThread extends Thread {
@Override
public void run() {
// run 方法就是该线程的入口方法
System.out.println("hello world");
}
}
public class ThreadDemo1 {
public static void main(String[] args) {
// 根据刚才的类,创建出实例(线程实例,才是真正的线程)
Thread thread = new MyThread();
// 调用 Thread 的 start 方法,才会真正调用系统 api,在系统内核中创建出 线程
thread.start();
}
}
run()
方法,不需要程序员手动调用,它会在合适的时机(线程创建好之后),由 jvm 自动调用执行。这种风格的函数,称为 “回调函数(callback)”
compareTo()
和 compare
就属于 “回调函数”,它会在我们插入元素时,自动调用这些方法run()
方法,类似于 main 方法,是一个 Java 进程的入口方法start()
方法时,就会自动调用 run()
方法代码:
class MyThread2 extends Thread {
@Override
public void run() {
while (true) {
System.out.println("hello world");
}
}
}
public class ThreadDemo2 {
public static void main(String[] args) {
Thread thread = new MyThread2();
thread.start();
while (true) {
System.out.println("hello main");
}
}
}
while(true)
死循环如图:主线程 main 和其 子线程thread0,此时就是互不干扰,各搞各的
注意! 当有多个线程时,这些线程执行的先后顺序,是不确定的! 因为在操作系统内核中,有一个 “调度器” 模块,这个模块的实现方式,会呈现出一种 “随机调度” 的效果
上述代码中,俩循环都是死循环,而且没有加任何条件,一旦程序运行起来,这俩循环就会执行得飞快,导致CPU占用率比较高,所以我们可以在循环中加上 sleep()
方法来降低循环速度
C语言中用的是 Windows api 中提供的 Sleep
函数
在Java中,我们使用的是封装后的版本,是Thread类提供的静态方法
注意:1s = 1000ms,这个方法本身也没有非常精确,精度误差就在毫秒级,所以用第一个方法就好
但是,直接用的话,会报异常,如图:这个异常,意味着 sleep(1000)
的过程中,可能会被提前唤醒
代码:
class MyThread2 extends Thread {
@Override
public void run() {
while (true) {
System.out.println("hello world");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
public class ThreadDemo2 {
public static void main(String[] args) throws InterruptedException {
Thread thread = new MyThread2();
thread.start();
while (true) {
System.out.println("hello main");
Thread.sleep(1000);
}
}
}
运行结果:可以看到,两个语句是同时出现,也就是说可以认为两个循环是同时执行,即两个线程是同时执行的,而且打印也是 “随机” 的
但是!即便是 “随机” 的,也是主线程 main 先打印出语句
这是因为:主线程main在调用 start()
方法后,就立即往下执行打印语句了;于此同时,内核就要通过 刚才线程的 api 构建出线程,然后执行 run()
,由于创建线程本身也有开销,所以在第一轮打印时子线程要稍慢一些
run()
方法中只有一个 try() catch()
改错方案,不能有 throws
,但是下面的 main()
方法中就可以有两种改错方案?run()
方法加上 throws
,就修改了方法签名,此时就无法构成 “重写”,父类的 run()
没有 throws
这个异常,子类重写的时候,就也不能 throws
异常在 JDK 中,有一个 jconsole 工具,可以更直观地看到多个线程,地址在:Java-jdk-bin-jconsole.exe
如果打开后,发现进程一栏是空,那么需要以管理员方式打开
当然,使用IDEA调试器,也可以看线程情况(打断点)
该方法就是上面的写法
Runnable
接口代码:
class MyThread3 implements Runnable {
@Override
public void run() {
while (true) {
System.out.println("hello runnable");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
public class ThreadDemo3 {
public static void main(String[] args) {
// 两种写法
// 1.
/*Runnable runnable = new MyThread3();
Thread thread = new Thread(runnable);*/
// 2.
Thread thread = new Thread(new MyThread3());
thread.start();
while (true) {
System.out.println("hello main");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
内部类:在一个类里面定义的类,没有名字,不能重复使用,用一次就扔了
Runnable
接口,但是使用 匿名内部类代码:
public class ThreadDemo4 {
public static void main(String[] args) {
Thread thread = new Thread(new Runnable() {
@Override
public void run() {
while (true) {
System.out.println("runnable");
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();
}
}
}
}
代码:
public class ThreadDemo5 {
public static void main(String[] args) throws InterruptedException {
Thread thread = new Thread(() -> {
while (true) {
System.out.println("lambda");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
});
thread.start();
while (true) {
System.out.println("main");
Thread.sleep(1000);
}
}
}
为什么编译器正好知道我们要重写的是 run()
方法?
因为Thread 中的构造方法有好几个版本,在编译器编译的时候,就会一个个往里面匹配,其中匹配到 Runnable
这个版本时,发现这里有个 run()
方法,无参数,正好能和现在的 lambda 对上
如:此时在 jconsole 工具中就能看到我们所命名的线程
Thread thread = new Thread(new Runnable() {
@Override
public void run() {
System.out.println("runnable");
}
},"这是一个线程");
注意:第五个构造方法,Java中的线程组和系统内核中的线程组不是一个东西,了解即可
getId()
getState()
getPriority()
isDaemon()
:是否是 ”后台线程“(和手机上的 前台app,后台app概念是不同的)
如图:当前代码执行时,thread 子线程一直在执行,但是 main 主线程已经结束了,但是编译器依旧在打印子线程语句
所以说,该代码创建的线程,默认是前台线程, 会阻止进程结束,只要前台线程没执行完,进程就不会结束,即使主线程已经执行完毕
所以我们就可以设置,线程为 ”前台线程“ 或 ”后台线程“
如图:setDaemon
,设置为 true 时,就是将该线程设置为 “后台线程”,此时主线程默认是前台线程,主线程结束后,该子后台线程也只能结束
isAlive()
:表示,内核中的线程是否还存在在之前的代码中,我们循环条件里填的就是 true
,直接设定成死循环,现在我们添加一个 boolean
类型的变量,来操控这个循环的终止
代码:通过添加一个 布尔类型的变量 isQuit
,当我们想要结束该线程时,就在主线程main中将这个 isQuit
修改
public static boolean isQuit = false;
public static void main(String[] args) throws InterruptedException {
Thread thread = new Thread(() -> {
while (!isQuit) {
System.out.println("thread");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
System.out.println("线程执行完毕");
},"测试线程1");
thread.start();
// 运行三秒
Thread.sleep(3000);
System.out.println("提前终止线程");
isQuit = true;
}
问题1:isQuit
变量,为什么放在 main 方法当中就不可行?写作 成员变量就可以了呢?
答:涉及到变量捕获;lambda表达式本质上是 “函数式接口” => 匿名内部类,内部类访问外部类成员这个事情本身就是可以的,就不受到变量捕获的影响了
问题2:为什么 Java 对于变量捕获有 final 限制?
答:每个线程都有其独立的栈帧,各自栈帧的生命周期不一样。这就可能导致主线程执行完,栈帧销毁了,但子线程还在,还想用主线程里面的变量。---- Java 中的做法就非常的简单粗暴,变量捕获本质上就是 “传参” ,换句话说,就是让 lambda 表达式在自己的栈帧中创建一个新的 isQuit
并把外面的 isQuit
值拷贝过来(为了避免 里外 的 isQuit
的值不同步,Java干脆就不让修改了)
相比之下:JS 里的变量捕获就很复杂,JS改变了变量的生命周期:某个局部变量被其他 “匿名函数” 捕获,此时这个变量就脱离原有的函数级别的生命周期了(这背后就涉及到一个非常复杂的 “作用域链” 问题/闭包)
方法一,代码不够简洁,还需要我们手动添加一个布尔类型变量
Thread
类中,就内置了这样一个变量,如图
代码:
public class ThreadDemo8 {
public static void main(String[] args) {
Thread thread = new Thread(() -> {
while (!Thread.currentThread().isInterrupted()) {
System.out.println("thread");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
});
thread.start();
try {
Thread.sleep(3000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("我要终止这个线程");
thread.interrupt();
}
}
观察发现这里抛一个 InterruptedException 异常,说 sleep interrupted,就是 sleep 被提前唤醒了,可见是 sleep()
引起的问题
sleep()
被提前唤醒会做两件事:
我们通过 thread.interrupt()
方法已经把标志位设为 true
了
但是 sleep
提前唤醒,操作之后,又把 标志位改回了 false
,所以循环又继续了(注意,不止是 sleep 有这样一个操作,其他方法也会这样)
注意! 在Java中,线程的终止是一种 “软性” 操作,必须要对应的线程配合,才能把该线程给提前终止;相比之下,系统原生的 api 还提供了 “强制终止线程” 的操作,无论线程是否愿意配合,无论该线程执行到哪个代码,都能强行把这个线程给干掉
强行干掉线程,在Java中是没有提供相应 api 的,这种操作弊大于利,如果强行干掉一个线程,很可能线程执行到一半,就会出现一些残留的临时性质的“错误”数据
sleep(1000)
,但现在还没到1s呢,就要终止线程了break
break
break
start()
方法,多次调用会抛异常!所以要想启动更多线程,就是需要创建更多新的线程实例
start()
会调用系统的 api,来完成 创建线程 的操作线程中我们经常需要去处理抛的异常,一般有以下处理方法
catch
中尝试重连网络join()
让一个线程等待另一个线程的结束
注意:多个线程的执行顺序是不确定的!(随机调度,抢占式执行)
虽然线程底层的调度是无序的,但可以在应用程序中,通过一些api来影响到线程执行的顺序
join()
就是一种方式。比如:
join
是可能会使 t2线程 阻塞!join()
在 main 线程中调用 thread.join()
,就是让 main 线程等待 thread 线程结束。哪个线程调用 join()
方法,那么调用这个方法的线程就进入阻塞等待状态
代码:
public class ThreadDemo9 {
public static void main(String[] args) throws InterruptedException {
Thread thread = new Thread(() -> {
for(int i = 0; i < 5; i++) {
System.out.println("子线程正在工作中...");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
});
thread.start();
thread.join();
System.out.println("这是主线程,等待子线程结束后,该日志才能打印");
}
}
打印结果:可以看到,主线程 main
调用了 thread,所以 主线程 main 处于阻塞等待状态(下面简称为 “等待状态” )
直接上结论:thread.join()
方法,只会使主线程(或者调用thread.join()
的线程)进入等待池,并等待 thread
线程执行完毕后才会被唤醒。并不影响同一时刻处在运行状态的其他线程
打个比方,join有连接、汇合的意思,主线程main
调用子线程的join()
方法时,好比用一条绳子将两个线程连在一块了,主线程的速度是比子线程快的,这条绳子就相当于把主线程捆住了,需要等子线程跑完,然后主线程才能继续跑
join()
的种类如图:可以看到有 不带参数 和 带参数的 join()
方法
死等(不带参数的)
带有超时时间的等(等,但是有时间限制)
当然,interrupt()
方法,就可以把 等待池 里的线程提前唤醒
代码:
class MyThread5 extends Thread {
@Override
public void run() {
System.out.println(this.getId() + " " + this.getName());
}
}
public class ThreadDemo10 {
public static void main(String[] args) throws InterruptedException {
MyThread5 thread1 = new MyThread5();
MyThread5 thread2 = new MyThread5();
thread1.start();
thread2.start();
Thread.sleep(1000);
System.out.println(thread1.getId() + " " + thread1.getName());
System.out.println(thread2.getId() + " " + thread2.getName());
}
}
运行结果:可见,如果是继承 Thread 的类,就可以直接使用 this
拿到线程实例
但如果是 Runnable 或 lambda 的方式,this
就拿不到了,因为此时 this
已经不再指向 Thread
实例了,就只能用以下方法
Thread.currentThread()
方法,获取当前线程引用代码:
public static void main(String[] args) {
Thread thread1 = new Thread(() -> {
System.out.println(Thread.currentThread().getName());
});
Thread thread2 = new Thread(() -> {
System.out.println(Thread.currentThread().getName());
});
thread1.start();
thread2.start();
}
运行结果:如果在 lambda 表达式中改为 this
去引用,势必会报错
Java中,线程有以下六种状态:
start()
方法,系统中**还没有创建出线程**sleep()
或 带有超时时间的 join()
方法,就会进入这个状态