初始计算机操作系统——进程与线程,多线程以及Thread类的创建和属性

目录

通过前半篇文章需要了解 

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.进程(process/task):运行起来的可执行文件。

为啥要有进程

我们的系统支持多任务,程序员就需要并发编程。

通过多进程是完全可以实现并发编程的 

如果频繁的创建/销毁进程(创建进程就需要给进程分配资源1.内存,2.文件(对于资源的申请和释放本身就是一个比较低效的过程)),这件事情成本是比较高的如果需要频繁的调度进程,这件事成本也是比较高的,

如何解决这个问题?

 (1)进程池

虽然可以解决上述问题,提高效率,同时也有问题。即使不使用,也在消耗系统资源,消耗的系统资源太多

 (2)使用线程:

线程比进程更轻量,每个进程能够执行一个任务,每个线程也能执行一个任务(执行一段代码),也能够并发编程。创建线程的成本比创建进程的成本低很多,销毁线程的成本比销毁进程低很多,调度线程的成本比调度进程低很多。在Linux上也把线程称为轻量级进程。

为啥线程比进程更轻量?

进程重在资源申请与释放,线程是包含在进程中的,一个进程中包含多个线程,共同用同一份资源(同一份内存+文件),只是在创建进程的第一个线程的时候(由于要分配资源,成本是相对高的)。后序这个进程再创建其他进程的时候,成本都是要更低一些。(不必再分配资源了)。

但是多加线程,效率并不一定会高,当线程多了,这些线程可能会竞争同一个资源,整体的速度就会受到限制。

2.线程:线程是进程内的一部分......进程包含线程。

一个线程就是一个 "执行流 ". 每个线程之间都可以按照顺讯执行自己的代码 . 多个线程之间 " 同时 " 执行 着多份代码.

对于java代码来说,最终都是通过java进程(jvm)跑起来的。

进程与线程之间的区别与联系:

1.进程包含线程,一个进程之间可以有一个线程,也可以有多个线程;

2.进程和线程都是为了处理并发编程这样的场景;

但是进程在频繁创建和释放资源以及调度效率低,开销大。相比之下,线程更轻量,创建和释放的效率更高(为啥更轻量,为了申请释放资源的过程)

3.操作系统创建进程,要给进程分配资源,进程是操作系统资源分配(系统分配资源如:内存,文件资源)的基本单位,

操作系统创建的线程,是要在CPU上调度执行,线程是操作系统调度执行的基本单位(CPU),(一个线程就是一个pcb)

4.进程有独立性,每个进程有独自的虚拟地址空间,一个进程挂了不会影响其他进程;

同一个进程中的多个线程,共同用同一个内存空间,一个线程挂了,可能会影响其他线程

(同一个进程中的若干线程之间,共享着内存,资源,文件描述符表, 每一个线程被调度执行都有自己的状态,上下文,记账信息); 

3.操作系统如何管理进程?

(1)先描述一个进程(明 确出一个进程上面的相关属性);里面主要通过C/C++实现,操作系统中描述进程的这个结构体,称为PCB;

(2)再组织若干个进程(使用一些数据结构,把很多描述进程的信息放在一起,方便增删查改--eg:linux通过PCB通过双向链表将每个进程的PCB串起来);

4.注:

"创建进程":先创建出PCB,然后把PCB加到双向链表中;

"销毁进程":找到链表中的PCB,并且从链表中删除;

"查看任务管理器":遍历链表;

5.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的时候就可以从刚才的内存中回复这些数据到寄存器中,相当于读档, 存档+读档~,就称为上下文;

记账信息:统计了每个进程,都分别被执行了多久,分别执行了那些指令,分别排队等了多久.......给进程的调度提供了指导依据

6.进程之间的通信 

早期的操作系统,里面的进程都是访问同一个内存的地址空间,如果有一个进程出现bug,把内存的数据写错了,就可能引起其他进程崩溃。(如果将进程按照虚拟地址空间的方式划分出很多分,这时候虽然系统中有百八十个进程,但实际上从微观上看,同时执行的进程就6个)

因此 ,现在的操作系统就是“进程的独立性”来保证的,就依仗了"虚拟地址空间"(进程之间现在通过"虚拟地址空间"隔开,但是在实际工作中,进程有的时候还是需要交互的);

两个进程之间,也是隔离开来的,也是不能直接交互的,操作系统也是提供了类似的”公共空间“,进程A就可以把数据放在公共空间上,进程B再取走。

进程间通信!操作系统中也提供了多种进程间通信机制。常用的1.文件操作;2.网络操作(socket);

7. Java 的线程 和 操作系统线程 的关系

线程是操作系统中的概念 . 操作系统内核实现了线程这样的机制 , 并且对用户层提供了一些 API 供用户使 用( 例如 Linux pthread ).
Java 标准库中 Thread 类可以视为是对操作系统提供的 API 进行了进一步的抽象和封装 .

8.Thread类

Java进行多线程编程 ,在Java标准库中就提供了Thread类,表示/操作线程 ;

Thread类也可以视为Java标准库提供的API ;

创建好的thread实例,和操作线程中的线程是一一对应的关系 ;

操作系统提供了一组关于线程的API,Java对这组API进一步封装就成了thread类;

可以通过jdk自带的工具jconsole查看当前Java进程中所有线程;

8.1继承thread类并且重写run方法

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是一个数据结构,体现的是进程或者线程是如何实现的,如何被描述出来的

8.2实现Runnable接口 

//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();
   
    }
}

解耦合,就是为了让线程和进程干的活分离开 

8.3匿名内部类,继承Thread

    public static void main(String[] args) {
        Thread t=new Thread(()->{
            System.out.println("hello");
        });
        t.start();
    }

8.4匿名内部类,实现Runnable

   public static void main(String[] args) {
        Thread t=new Thread(new Runnable() {
            @Override
            public void run() {
                System.out.println("hello");
            }
        });
        t.start();
    }

8.5使用Lambda表达式 

    public static void main(String[] args) {
        Thread t=new Thread(()->{
            System.out.println("hello");
        });
        t.start();
    }

8.6Thread中的常见属性 

 

 

8.6.1Thread 的几个基本属性

getId() Id  ID 是线程的唯一标识,不同线程不会重复
getName()名称
getState()状态
getPriority()优先级

8.6.2isDaemon()是否后台线程

是否是守护线程//

前台进程会阻止进程结束,前台进程工作没完,进程是完不了的;
后台进程不会阻止进程的结束,后台线程工作没完,进程也是可以结束的;
代码中手动创建的线程,默认都是前台的;包括main也是的其他jvm自带的线程都是后台的;
也可以 使用setDeamon设置成后台线程(守护线程)(被设置的这个线程就与t无关了) , 
关于后台线程,需要记住一点: JVM 会在一个进程的所有非后台线程结束后,才会结束运行
是否存活,即简单的理解,为 run 方法是否运行结束了

8.6.3isAlive() 是否存活

就是判断是否真的有线程,
调用start后,才会在内核创建一个PCB,此时的一个PCB才表示一个真正的线程,此时isAlive()才是true;
另外,如果内核把线程把run干完了,此时线程销毁,pcb随之销毁,但是Thread t这个对象不一定释放 此时isAlive()还是false;
t神魔时候不在了?当引用不指向这个对象,被GC回收;
run方法在执行的时候 isAlive ()->true;
执行完->false
public class demo1 {
    public static void main(String[] args) throws InterruptedException {
      Thread t=new Thread(()->{
          System.out.println("线程开始");
          try {
              Thread.sleep(1000);
          } catch (InterruptedException e) {
              throw new RuntimeException(e);
          }
          System.out.println("线程结束");
      });
        System.out.println(t.isAlive());
        t.start();
        System.out.println(t.isAlive());
        Thread.sleep(5000);
        System.out.println(t.isAlive());
    }
}

8.6.4isInterrupted() 是否被中断

中断一个线程,不是让线程立刻停止,而是通知线程需要停止了,是否真的停止,取决于具体代码的书写方式;

为true,表示被终止;

为false,表示为被终止;

想要销毁或者终止线程——>让run方法尽快执行完毕

(1)自定义方式

缺点就是不能及时响应,尤其是在sleep休眠的时候比较久的情况

package thread;


public class demo1 {


    //使用标志
    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);
            }
            System.out.println("线程结束");
        });
        t.start();
        Thread.sleep(3000);
        flg=false;
        System.out.println("修改flg为false");
    }
}

如果将代码改为

所以报错原因不是因为作用域问题,而是不能修改的问题。lambda表达式可以进行变量捕获,也就是把当前作用域的变量在表达式内部复制了一份。变量捕获就只能捕获是final类型的,

(2)使用Thread自带的标志位

上面的方法存在一种情况:还没执行完就退出了,而这种方法这种的是可以唤醒sleep的。

使用 Thread.interrupted() 或者 Thread.currentThread().isInterrupted() 代替自定
义标志位。Thread.currentTnread可以获取到当前线程的实例。
(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的方法里了
    }

🤔interrupt()做的事情:

如果线程在sleep中休眠 ,此时调用interrupt会把t唤醒 ,从sleep中提前返回 ;

interrupt会做两件事情 ,

1.把线程内部的标志位设置成true;

2.如果线程在进行sleep ,就会触发异常,把sleep唤醒;

但是sleep在唤醒的时候 ,还会把刚才设置的标志位再设置回false(清空标志位)

像wait,join这样的造成代码暂停的方法都会有类似的清除标志位的设定

🤔观察报错后继续执行+真相大白:

所以这个时候执行 ,还可能会导致sleep中被interrupted之后 ,还会继续执行,即线程t忽略了终止请求,有如下报错后继续执行;

interrupt使得sleep发生异常终止,sleep清除后续的true标志位,导致线程又继续执行流。这样的目的是线程收到要中断信息时,能自由决定后续操作。有更多操作空间。

🤔解决办法:

catch里面可以加上break(也可以根据需求添加sleep)/catch里面改成throw new RuntimeException(e);让线程t立即相应你的终止请求

🤔end:

so , 唤醒之后到底让线程立即终止还是稍后 ,就把选择权交给程序员自己了 。

为啥不涉及成A让B终止 ,B就终止呢 ?

A,B之间是并发执行的,随即调度的 ,导致B这个线程执行到哪了,A都是不清楚的 。



8.7等待线程(join)

线程是一个随机调度的过程,等待线程就是控制两个线程之间的结束顺序 。等待线程就是让一个线程等待另一个线程执行结束再继续执行。t.join()就是主线程等待t线程先结束,一般不会进行死等,

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 也不一定必须得等待 ;


8.8获取当前线程的引用

public static Thread currentThread();
返回当前线程对象的引用
public class ThreadDemo {
    public static void main(String[] args) {
        Thread thread = Thread.currentThread();
        System.out.println(thread.getName());
   }
}

8.9线程的休眠sleep

1.原本链表中的PCB都是随叫随到的就绪状态

2.线程A调用sleep, A就会进入休眠状态,把A从上述链表拎出来,放到另一个链表上,这个链表的PCB都是“阻塞状态”,暂时不参与CPU的调度执行。

pcb是使用链表来组织的(并不具体)

3.操作系统每次需要调度一个线程去执行,就是从就绪队列中选一个就好

一旦线程进入阻塞状态,对应的PCB就进入阻塞队列,就暂时无法参与调度了

sleep方法在继承Thread的重写run方法中你只能try catch,不能throws,因为父类没进行throw。

阻塞——>就绪

eg: 调用sleep(1000) , 对应的线程PCB就要再阻塞队列中待1000ms ,当PCB回到了就绪队列,会被立即调度吗?

虽然sleep(1000),考虑到调度的开销,对应的线程是无法在唤醒之后立即被执行的,实际的时间大概率要大于1000ms的 。

8.10start和run的区别

本质区别在于:是否在系统内部创建出了线程

start是真正创建了一个线程,线程是独立的执行流;start方法内部会调用系统api,在系统内核创建线程。

run只是描述了该线程执行神魔内容,会在start创建好线程后自动调用。

9.明天 

学线程的状态以及多线程带来的风险(线程安全)。

  • 14
    点赞
  • 13
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 11
    评论
评论 11
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

sqyaa.

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值