目录
通过前半篇文章需要了解
1.进程(process/task):运行起来的可执行文件。
为啥要有进程?
如何解决这个问题?
(1)进程池:
(2)使用线程:
为啥线程比进程更轻量?
2.线程:线程是进程内的一部分......进程包含线程。
进程与线程之间的区别与联系:
3.操作系统如何管理进程?
4.注:
5.PCB中的一些基本属性
6.进程之间的通信
7. Java 的线程 和 操作系统线程 的关系
8.Thread类
继承thread类并且重写run方法
实现Runnable接口
匿名内部类,继承Thread
匿名内部类,实现Runnable
使用Lambda表达式
Thread中的常见属性
Thread 的几个基本属性
isDaemon()是否后台线程
isAlive() 是否存活
isInterrupted() 是否被中断
等待线程(join)
获取当前线程的引用
线程的休眠sleep
start和run的区别
9.明天
1.进程是啥?
2.进程时如何进行管理的?描述+组织
3.进程的PCB里有啥?
4.进程的调度咋进行?时间管理大师~
5.进程的独立性是咋回事?
6.进程之间如何进行通信?
7.进程和线程的区别和练习
操作系统居然是搞管理的软件,对上,要管理好各种硬件设备,对下,要对各种软件提供稳定的运行环境。
为啥要有进程?
我们的系统支持多任务,程序员就需要并发编程。
通过多进程是完全可以实现并发编程的
如果频繁的创建/销毁进程(创建进程就需要给进程分配资源1.内存,2.文件(对于资源的申请和释放本身就是一个比较低效的过程)),这件事情成本是比较高的如果需要频繁的调度进程,这件事成本也是比较高的,
如何解决这个问题?
(1)进程池:
虽然可以解决上述问题,提高效率,同时也有问题。即使不使用,也在消耗系统资源,消耗的系统资源太多
(2)使用线程:
线程比进程更轻量,每个进程能够执行一个任务,每个线程也能执行一个任务(执行一段代码),也能够并发编程。创建线程的成本比创建进程的成本低很多,销毁线程的成本比销毁进程低很多,调度线程的成本比调度进程低很多。在Linux上也把线程称为轻量级进程。
为啥线程比进程更轻量?
进程重在资源申请与释放,线程是包含在进程中的,一个进程中包含多个线程,共同用同一份资源(同一份内存+文件),只是在创建进程的第一个线程的时候(由于要分配资源,成本是相对高的)。后序这个进程再创建其他进程的时候,成本都是要更低一些。(不必再分配资源了)。
但是多加线程,效率并不一定会高,当线程多了,这些线程可能会竞争同一个资源,整体的速度就会受到限制。
一个线程就是一个 "执行流 ". 每个线程之间都可以按照顺讯执行自己的代码 . 多个线程之间 " 同时 " 执行 着多份代码.对于java代码来说,最终都是通过java进程(jvm)跑起来的。
1.进程包含线程,一个进程之间可以有一个线程,也可以有多个线程;
2.进程和线程都是为了处理并发编程这样的场景;
但是进程在频繁创建和释放资源以及调度效率低,开销大。相比之下,线程更轻量,创建和释放的效率更高(为啥更轻量,为了申请释放资源的过程)
3.操作系统创建进程,要给进程分配资源,进程是操作系统资源分配(系统分配资源如:内存,文件资源)的基本单位,
操作系统创建的线程,是要在CPU上调度执行,线程是操作系统调度执行的基本单位(CPU),(一个线程就是一个pcb)
4.进程有独立性,每个进程有独自的虚拟地址空间,一个进程挂了不会影响其他进程;
同一个进程中的多个线程,共同用同一个内存空间,一个线程挂了,可能会影响其他线程
(同一个进程中的若干线程之间,共享着内存,资源,文件描述符表, 每一个线程被调度执行都有自己的状态,上下文,记账信息);
(1)先描述一个进程(明 确出一个进程上面的相关属性);里面主要通过C/C++实现,操作系统中描述进程的这个结构体,称为PCB;
(2)再组织若干个进程(使用一些数据结构,把很多描述进程的信息放在一起,方便增删查改--eg:linux通过PCB通过双向链表将每个进程的PCB串起来);
"创建进程":先创建出PCB,然后把PCB加到双向链表中;
"销毁进程":找到链表中的PCB,并且从链表中删除;
"查看任务管理器":遍历链表;
pid(进程id): 进程的身份标识~进程的身份账号;
内存指针:知名这个进程要执行的代码/指令在内存的哪里,以及这个进程执行中依赖的数据在哪。当运行一个exe,操作熊会把这个exe加载到内存中,变成进程。内存中包含了进程要执行的二进制指令(通过编译器生成),除了指令之外还有一些数据。
文件描述符表:程序运行过程中,经常要和文件打交道,文件是在硬盘上的。
进程每次打开一个文件,就会再文件描述表上多增加一项;这个文件描述表就可以视为一个数组,里面的每个元素又是一个结构体,就对应一个文件的相关信息。
一个进程只要一启动,不管代码中是否写了打开/操作文件的代码,都会默认打开三个文件(系统自动打开的,分别是:标准输入System.in,标准输出System.out,标准错误Systmp.err);
这个文件描述符表的下标,就被称为文件描述符。
要想让一个进程正常工作就得给他分配资源:如内存,硬盘,CPU;
上面的属性是一些基础属性,下面的属性主要是为了能够实现进程调度(是理解进程管理的重要话题)。(现在的操作系统一般是"多任务操作系统")
进程调度:系统上任务的数量(进程的数量)很多,但CPU核数有限。
并行:微观上,两个CPU核心,同时执行两个任务的代码;
并发:微观上,一个CPU核心,先执行一会任务1,在执行一会任务2......再继续执行任务1,只要切换足够快,宏观上看起来,就好像多任务在同时执行......
实际上通常使用并发来代指并行+并发,只在研究操作系统稍作区分。
所谓"调度"就是“时间管理”, 在并发过程中,规划时间表的过程,就是调度的过程。
状态:描述了当前这个进程接下来应该如何 调度;
就绪状态:随时可以去CPU上执行;
阻塞状态/睡眠状态:暂时不可以去CPU上执行;
优先级:先给谁分配时间,后给谁分配时间,以及给谁分的多,给谁分的少
上下文: 示了上次进程被调度出CPU时,当时程序的执行状态,下次进CPU就可以恢复之前的状态,然后继续向下执行。
进程被调度出CPU之前,要先把CPU中所有的寄存器中的始数据都保存在内存中(PCB当中的上下文字段中)相当于存档;
下次进程在被调度上CPU的时候就可以从刚才的内存中回复这些数据到寄存器中,相当于读档, 存档+读档~,就称为上下文;
记账信息:统计了每个进程,都分别被执行了多久,分别执行了那些指令,分别排队等了多久.......给进程的调度提供了指导依据
早期的操作系统,里面的进程都是访问同一个内存的地址空间,如果有一个进程出现bug,把内存的数据写错了,就可能引起其他进程崩溃。(如果将进程按照虚拟地址空间的方式划分出很多分,这时候虽然系统中有百八十个进程,但实际上从微观上看,同时执行的进程就6个)
因此 ,现在的操作系统就是“进程的独立性”来保证的,就依仗了"虚拟地址空间"(进程之间现在通过"虚拟地址空间"隔开,但是在实际工作中,进程有的时候还是需要交互的);
两个进程之间,也是隔离开来的,也是不能直接交互的,操作系统也是提供了类似的”公共空间“,进程A就可以把数据放在公共空间上,进程B再取走。
进程间通信!操作系统中也提供了多种进程间通信机制。常用的1.文件操作;2.网络操作(socket);
线程是操作系统中的概念 . 操作系统内核实现了线程这样的机制 , 并且对用户层提供了一些 API 供用户使 用( 例如 Linux 的 pthread 库 ).Java 标准库中 Thread 类可以视为是对操作系统提供的 API 进行了进一步的抽象和封装 .
Java进行多线程编程 ,在Java标准库中就提供了Thread类,表示/操作线程 ;
Thread类也可以视为Java标准库提供的API ;
创建好的thread实例,和操作线程中的线程是一一对应的关系 ;
操作系统提供了一组关于线程的API,Java对这组API进一步封装就成了thread类;
可以通过jdk自带的工具jconsole查看当前Java进程中所有线程;
class Mythread extends Thread{
@Override
public void run() {//当run方法执行完毕,新的这个线程自然销毁;
super.run();
System.out.println("hello thread");
//run方法里的逻辑是在新创建出来的线程里被执行的代码
//‘分配活‘
//这里的创建线程都是在同一个进程内部创建的
}
}
public class Demo1 {
public static void main(String[] args) {
Thread t=new Mythread();
t.start();
//start里面没有调用run,start是创建了一个新的线程,新的线程负责执行run方法;
//新的线程就是调用操作系统的API,通过操作系统内核创建新线程的PCB,并且把要执行的指令交给这个PCB,
//当PCB被调度到CPU上执行的时候,也就到了线程run方法中的代码了
//操作系统调度线程的时候是抢占式执行的
}
}//new thread()并不是创建线程,说的线程是系统内核里的PCB;
//调用start才是创建pcb(真正的线程)
//一个线程对应一个pcb;
//同一个进程中PCB的pid相同,不同进程的pid是不同的
//PCB不是一个简称,PCB是一个数据结构,体现的是进程或者线程是如何实现的,如何被描述出来的
//Runnable是描述一个“要执行的任务”,run方法就是任务的执行细节
class MyRunnable implements Runnable{
@Override
public void run() {
System.out.println("hello");
}
}
public class Demo3 {
public static void main(String[] args) {
//这只是描述了这个任务
Runnable runnable=new MyRunnable();
//把任务交给线程来执行
Thread t=new Thread(runnable);
t.start();
}
}
解耦合,就是为了让线程和进程干的活分离开
public static void main(String[] args) {
Thread t=new Thread(()->{
System.out.println("hello");
});
t.start();
}
public static void main(String[] args) {
Thread t=new Thread(new Runnable() {
@Override
public void run() {
System.out.println("hello");
}
});
t.start();
}
public static void main(String[] args) {
Thread t=new Thread(()->{
System.out.println("hello");
});
t.start();
}
Thread 的几个基本属性
getId() Id ID 是线程的唯一标识,不同线程不会重复getName()名称getState()状态getPriority()优先级isDaemon()是否后台线程
是否是守护线程//
前台进程会阻止进程结束,前台进程工作没完,进程是完不了的;后台进程不会阻止进程的结束,后台线程工作没完,进程也是可以结束的;代码中手动创建的线程,默认都是前台的;包括main也是的其他jvm自带的线程都是后台的;也可以 使用setDeamon设置成后台线程(守护线程)(被设置的这个线程就与t无关了) ,关于后台线程,需要记住一点: JVM 会在一个进程的所有非后台线程结束后,才会结束运行是否存活,即简单的理解,为 run 方法是否运行结束了
isAlive() 是否存活
就是判断是否真的有线程,调用start后,才会在内核创建一个PCB,此时的一个PCB才表示一个真正的线程,此时isAlive()才是true;另外,如果内核把线程把run干完了,此时线程销毁,pcb随之销毁,但是Thread t这个对象不一定释放 此时isAlive()还是false;t神魔时候不在了?当引用不指向这个对象,被GC回收;run方法在执行的时候 isAlive ()->true;执行完->false
isInterrupted() 是否被中断
中断一个线程,不是让线程立刻停止,而是通知线程需要停止了,是否真的停止,取决于具体代码的书写方式;
为true,表示被终止;
为false,表示为被终止;
1.自定义方式
缺点就是不能及时响应,尤其是在sleep休眠的时候比较久的情况
//使用标志 public static boolean flg=true; public static void main(String[] args) throws InterruptedException { Thread t=new Thread(()->{ while(flg){ System.out.println("hello thread"); } try{ Thread.sleep(1000); } catch (InterruptedException e) { throw new RuntimeException(e); } }); t.start(); Thread.sleep(3000); flg=false; }
2.使用Thread自带的标志位
这种的是可以唤醒sleep的
使用 Thread.interrupted() 或者 Thread.currentThread().isInterrupted() 代替自定义标志位.(Thread.currentThread()是Thread的静态方法,通过这个对象可以获取到当前的线程,哪个方法调用线程,得到的就是哪个线程对象的调用)Thread 内部包含了一个 boolean 类型的变量作为线程是否被中断的标记 .在main中调用t.interrupted(),就相当于main通知t要终止了;public static void main(String[] args) throws InterruptedException { Thread t=new Thread(()->{ while(!Thread.currentThread().isInterrupted()){ System.out.println("hello thread"); try{ Thread.sleep(1000); } catch (InterruptedException e) { e.printStackTrace(); } } }); t.start(); Thread.sleep(3000); t.interrupt();//把boolean操作封装到Thread的方法里了 }
如果线程在sleep中休眠 ,此时调用interrupt会把t唤醒 ,从sleep中提前返回 ;
interrupt会做两件事情 ,
1.把线程内部的标志位设置成true;
2.如果线程在进行sleep ,就会触发异常,把sleep唤醒;
但是sleep在唤醒的时候 ,还会把刚才设置的标志位再设置回false(清空标志位);
像wait,join这样的造成代码暂停的方法都会有类似的清除标志位的设定
所以这个时候执行 ,还可能会导致sleep中被interrupted之后 ,还会继续执行,即线程t忽略了终止请求;
在catch里面可以加上break(也可以根据需求添加sleep)/catch里面改成throw new RuntimeException(e);让线程t立即相应你的终止请求
so , 唤醒之后到底让线程立即终止还是稍后 ,就把选择权交给程序员自己了 。
为啥不涉及成A让B终止 ,B就终止呢 ? A,B之间是并发执行的,随即调度的 ,导致B这个线程执行到哪了,A都是不清楚的 。
等待线程(join)
线程是一个随机调度的过程,等待线程就是控制两个线程之间的结束顺序 。
public class demo5 { public static void main(String[] args) throws InterruptedException { Thread t=new Thread(()->{ for (int i = 0; i < 3; i++) { System.out.println("hello thread"); try { Thread.sleep(1000); } catch (InterruptedException e) { throw new RuntimeException(e); } } }); t.start(); System.out.println("join之前"); t.join(); //这里的join是让main等待t执行结束;(等待t的run执行完) //本身执行完start之后,t线程和main线程就并发执行,分头执行main继续向下执行,t也继续向下执行就会发生阻塞block //一直阻塞到t线程执行完,main才能从join里面恢复过来 //必然是t先结束 System.out.println("join之后"); } }
——> main线程中的打印“join之前”执行,遇到join,等待t线程执行完后才执行后续的;
如果执行join的时候 ,t 已经结束了,join 就不会阻塞 ,立即返回 ;
so,有 join 也不一定必须得等待 ;
public static Thread currentThread(); 返回当前线程对象的引用
public class ThreadDemo {
public static void main(String[] args) {
Thread thread = Thread.currentThread();
System.out.println(thread.getName());
}
}
1.原本链表中的PCB都是随叫随到的“就绪状态”;
2.线程A调用sleep, A就会进入休眠状态,把A从上述链表拎出来,放到另一个链表上,这个链表的PCB都是“阻塞状态”,暂时不参与CPU的调度执行。
pcb是使用链表来组织的(并不具体)
3.操作系统每次需要调度一个线程去执行,就是从就绪队列中选一个就好
一旦线程进入阻塞状态,对应的PCB就进入阻塞队列,就暂时无法参与调度了
eg: 调用sleep(1000) , 对应的线程PCB就要再阻塞队列中待1000ms ,当PCB回到了就绪队列,会被立即调度吗?
虽然sleep(1000),考虑到调度的开销,对应的线程是无法在唤醒之后立即被执行的,实际的时间大概率要大于1000ms的 。
start是真正创建了一个线程,线程是独立的执行流;
run只是描述了要干什莫活,如果直接在main中调用run,此时没有创建新的线程,全是main在干活
学线程的状态以及多线程带来的风险(线程安全)。