多线程
1.为啥要有多进程
因为并发编程的刚需!
CPU单个核心已经发挥到极致了,要想提升算力,就得使用多个核心
引入并发编程,最大的目的就是为了能够充分的利用好CPU的多核资源
使用多进程这种编程模型,是完全可以做到并发编程的,并且也能使CPU多核被充分利用
但是,在 有些场景 下,会存在问题:
如果需要频繁地创建/销毁进程,这个时候就会比较低效
因为,创建/销毁进程本身就是一个比较低效的操作,具体需要完成:
- 创建PCB
- 分配系统资源(尤其是内存资源) 这个比较消耗时间,因为是在系统内核资源管理模块,进行一系列遍历操作的
- 把PCB加入到内核的双向链表当中
那么为了提高这个场景下的效率,就引入了”线程“
2.进程和线程之间的区别
-
进程是包含线程的,线程是在进程内部的,每个进程至少有一个线程存在,即主线程。
-
进程和进程之间不共享内存空间. 同一个进程的线程之间共用同一份系统资源。(每个进程有独立的虚拟地址空间,也有自己独立的文件描述符集,同一个进程的多个线程之间,则共用这一份虚拟地址空间和文件描述符集)
-
进程是操作系统中资源分配的基本单位,线程是操作系统中调度执行的基本单位
-
多个进程同时执行的时候,如果一个进程挂了,一般不会影响到别的进程;同一个进程里的多个线程之间,如果一个线程挂了,很可能把整个进程带走,其它同进程中的线程也就没了
每个线程其实也都有自己的 PCB,一个进程里面就可能对应多个PCB
同一个进程的线程之间共用同一份系统资源(意味着:新创建的线程,不必重新给它分配系统资源,只需要复用前面的即可)
因此,比起创建进程,创建线程只需要:
- 创建PCB
- 把PCB加入到内核的链表中
这是线程相对于进程做出的重大改进,也就是线程更轻量的原因
3.观察线程
![image-20220722160246701](https://i-blog.csdnimg.cn/blog_migrate/deb714e3f86fba5287442922084866d7.png)
这个run方法重写的目的是,为了明确咱们新创建出来的线程需要干什么
![image-20220722160349604](https://i-blog.csdnimg.cn/blog_migrate/2051bccce8244d5ae77b37b3411cb743.png)
光创建了这个类,还不算创建线程,还得创建实例
class MyThread extends Thread {
@Override
public void run() {
System.out.println("hello thread! ");
}
}
public class Demo1 {
//创建于1个线程
//Java中 创建线程,离不开 thread 类
//一个创建线程的 朴素方法,学一个子类,继承Thread,重写其中的 run 方法
public static void main(String[] args) {
Thread t = new MyThread(); //向上转型的写法
t.start(); //这才是真正开始创建线程(在操作系统内核中,创建出对应线程的PCB,然后让这个PCB加入到系统链表中,参与调度)
System.out.println("hello main!");
}
}
在这个代码中,虽然先启动的线程,后打印的 hello main
但是实际执行的时候,看到的却是先打印了 hello main 后打印了 hello thread
这说明什么呢?
-
每个线程是独立的执行流! main对应的线程是一个执行流, MyThread是另一个执行流。这两个执行流之间是并发的执行关系。
-
此时两个线程执行的先后顺序,取决于操作系统调度器具体实现(我们可以认为是随机调度的),因此先打印哪个,是随机的,虽然咱们反复运行多次,可能打印的结果一样,但是顺序仍然是不可确定的!当前看到的先打印 hello main,大概率是受到创建线程自身的开销影响 (哪怕1000次都是先打印main,也不能保证1001次还是这个结果)
此处不想让进程结束这么快,我们就可以这么做
此时就可以查看 Java 里进行的线程
![image-20220722161807680](https://i-blog.csdnimg.cn/blog_migrate/b73c4e62f12c06d07968b5657607b8d4.png)
双击运行
![image-20220722161838306](https://i-blog.csdnimg.cn/blog_migrate/2f964125d35247d2b7c30acccb70987d.png)
如果不显示进程列表,别担心,关闭之后,右键,以管理员身份运行
![image-20220722162144011](https://i-blog.csdnimg.cn/blog_migrate/e3039a08bea53b76a759e1bc8b363a60.png)
这里的调用栈非常有用!
未来调试一个"卡死"的程序的时候,就可以看下每个线程的调用栈是啥,就可以初步的确认卡死的原因。
刚才的死循环代码,打印的太多太快
有的时候不希望它们打这么快(不方便来观察),可以使用sleep来让线程适当的"休息"一下
使用Thread.sleep
的方式进行休眠,sleep
是Thread
的静态成员方法,sleep
的参数是一个时间, 单位是ms
4.一个经典面试题
谈谈 Thread 的 run 和 start 的区别
![image-20220722162620138](https://i-blog.csdnimg.cn/blog_migrate/f2a4e9f95cd3f117a70814fbcad0c5da.png)
-
使用start,可以看到两个线程并发的执行,两组打印交替出现。
-
使用run,可以看到只是在打印thread,没有打印main。
-
直接调用run,并没有创建新的线程,而只是在之前的线程中,执行了run 里的内容.
-
使用start,则是创建新的线程,新的线程里面会调用run (新线程和旧线程之间是并发执行的关系)
5.创建线程的几种常见写法
- 创建一个类继承Thread,重写run(这个写法,线程和任务内容是绑定在一起的)
class MyThread extends Thread {
@Override
public void run() {
while(true){
System.out.println("hello thread! ");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
public class Demo1 {
//创建于1个线程
//Java中 创建线程,离不开 thread 类
//一个创建线程的 朴素方法,学一个子类,继承Thread,重写其中的 run 方法
public static void main(String[] args) {
Thread t = new MyThread(); //向上转型的写法
t.start(); //这才是真正开始创建线程(在操作系统内核中,创建出对应线程的PCB,然后让这个PCB加入到系统链表中,参与调度)
while(true) {
System.out.println("hello main!");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
- 创建一个类,实现Runnable接口,重写 run
class MyRunnable implements Runnable {
@Override
public void run(){
while(true){
System.out.println("hello thread! ");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
public class Demo2 {
public static void main(String[] args) {
//创建线程
//第二种方法 创建一个类,实现 Runnable接口,重写run
Runnable runnable = new MyRunnable();
Thread t = new Thread(runnable);
t.start();
while(true) {
System.out.println("hello main!");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
此处创建的Runnable ,相当于是定义了一个"任务" (代码要干啥),还是需要Thread实例,把任务交给Thread,还是Thread.start来创建具体的线程
这个写法,线程和任务是分开的(更好的解耦合)【咱们写代码的时候要追求:低耦合,高内聚】
- 仍然是使用继承 Thread类,但不再显式继承,而是使用“匿名内部类”
public class Demo3 {
public static void main(String[] args) {
//第三种:匿名内部类的写法
Thread t = new Thread(){
@Override
public void run() {
while(true){
System.out.println("hello thread! ");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
};
t.start();
}
}
在start之前,线程只是准备好了.并没有真正被创建出来。执行了start方法,才真正在操作系统中创建了线程!
- 使用Runnable,以匿名内部类的方式使用
public class Demo4 {
public static void main(String[] args) {
/* 方法一
Runnable runnable = new Runnable() {
@Override
public void run() {
while(true){
System.out.println("hello thread! ");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
};
*/
// 2.
Thread t = new Thread(new Runnable() {
@Override
public void run() {
while(true){
System.out.println("hello thread! ");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}) ;
t.start();
}
}
- 使用lambda表达式,来定义任务(推荐)
public class Demo5 {
Thread t = new Thread( ()->{
while(true){
System.out.println("hello thread! ");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
});
}
6.使用多线程带来的好处
使用多线程,能够更充分的利用CPU多核资源
看一个代码 (完成 20 亿次自增)
public class Demo6 {
//1.单个线程,串行的,完成 20 亿次自增
//2.两个线程,并发的,完成 20 亿次自增
private static final long COUNT = 20_0000_0000;
/**
* 串行的
*/
private static void serial(){
//需要把方法执行的时间记录下来
long beg = System.currentTimeMillis();//记录当前的毫秒级时间戳
int a = 0;
for(long i =0; i< COUNT;i++){
a++;
}
a = 0;
for(long i = 0 ; i < COUNT; i++){
a++;
}
long end = System.currentTimeMillis();
System.out.println("单线程消耗的时间:" + (end-beg) + "ms");
}
/**
* 并发的
*/
private static void concurrency(){
long beg = System.currentTimeMillis();//记录当前的毫秒级时间戳
Thread t1 = new Thread( ()-> {
int a = 0;
for(long i =0; i< COUNT;i++){
a++;
}
});
Thread t2 = new Thread( ()->{
int a = 0;
for(long i =0; i< COUNT;i++){
a++;
}
});
t1.start();
t2.start();
try {
t1.join();
t2.join();
} catch (InterruptedException e) {
e.printStackTrace();
}
long end = System.currentTimeMillis();
System.out.println("并发执行消耗的时间:" + (end-beg) + "ms");
}
}
一些解释
-
concurrency的代码设计到三个线程:t1,t2,main,三个线程都是并发执行的
-
如果没有调用join,只调用start,虽然 t1、t2 是会开始执行,同时不等它们执行完,main线程就往下走了,于是就结束计时
-
正确的计时,应该是等到 t1和t2 都执行完,才停止!
-
join是等待线程结束(等待线程把自己的run方法执行完),在主线程中调用 t1.join ,意思就是让main线程等待t1执行完
下面我们运行程序,测一下单线程和多线程运行的时间(注:此为联想拯救者R9000P野兽模式下测得数据)
相比之下,多线程的效率确实提高不少!
但为什么时间不刚好是单线程的一半呢?
- 创建线程自身,也是有开销的!
- 两个线程在CPU上不一定是纯并行,也可能是并发,部分时间里是并行了,部分时间里是并发的。
- 线程的调度,也是有开销的。 (但是当前场景中,开销应该是非常小的)
7.多线程的使用场景
1.在CPU密集型场景
代码中大部分工作,都是在使用CPU进行运算(就像刚才这个反复自增)
使用多线程,就可以更好的利用CPU多核计算资源,从而提高效率!
2.在 I0密集型场景
读写硬盘,读写网卡…这些操作都算I0,这些场景里, 就需要花很大的时间等待!
像这些I0操作,都是几乎不消耗CPU就能完成快速读写数据的操作。
既然CPU在摸鱼,就可以给他找点活干,也可以使用多线程,避免CPU过于闲置