操作系统中的概念详解


前言

操作系统是学习多线程前提,对操作系统中的相关概念有比较深入的了解,会对线程的学习有很大的帮助。本篇文章主要阐述操作系统中的各种重要的概念,并对线程做详细的介绍。


一、操作系统的基础概念介绍

1.并行与并发

并行(parallel):进程为真的同时在执行相关操作,也就是在微观视角下的同一时刻,是有多个指令在同时执行,所以并行只会发生在多CPU这种多核场景下。
并发:进程为假的同时在执行相关操作,也就是说在微观视角下,表现为一次只执行一个进程,但是在宏观角度,也就是用户看到的,是多个进程在“同时”执行。

2.用户态与内核态

用户态(user space):指令只能访问自己的内存。
内核态(kernel space):指令可以访问所有内存。
两者比较,用户态的性能较好,内核态的性能较差。

3.执行流

执行流(excution flow):拥有独立PC的一套指令;不同的执行流从现象上看起来是完全独立的。

二、内存管理

记住一个概念,内存管理都是从空间上划分的。
大致划分结果:[内核使用的内存][分配普通进程使用的内存][空闲空间]
但是空间划分不保证是连续的。

1.Java程序员眼中的内存

JVM的内存空间分为:堆区、栈区、方法区…这一块是属于Java应用和JVM中的概念,但是JVM又是作为了操作系统中的一个普通进程,所以,程序员操作的内存就指的是这一块,说白了就是内存划分中分配普通进程的内存。下面,用一张图说明这个概念:
在这里插入图片描述

2.线性地址和物理地址

在这里插入图片描述
由上图,我们可以看到:
线性地址:就是物理地址被操作系统转换之后的地址,是虚拟地址。
物理地址:是真实存在于内存中的地址。
由此引申出以下知识点:

  • 没有线性地址的情况,同一个程序的多次运行,会生成不同的进程,那我们能否保证同一个进程就一定能被放到内存中的同一个位置呢?
    答案显然是不能的,因为如果程序员可以直接看到物理地址,那就意味着程序员必须关心一个问题——不同进程如果出现在内存的不同位置,那么在程序中处理地址时,必须考虑地址不同带来的复杂性,这也就说明如果没有线性地址,进程的处理一定会出错。所以说,引入线性地址之后,程序员根本就不会再考虑这类复杂性了。
  • 操作系统分配出来的空间只是线性地址空间,实际的物理内存是不会真实的反映出来的,只有在访问这段内存时才会被分配。

3.进程间通信

  • 概念引入:理论上,不同的进程之间是独立的,但是实际上,往往是多个进程之间相互配合,来完成复杂度工作。例如,使用MySql的时候,需要通过workbench进程和MySql服务器进程进行通信,来实现对数据的增删查改。所以,就有了就有了进程之间交换数据的必要性。
  • 问题引入:操作系统进行资源分配是以进程为基本单位进行分配的, 也包括内存。现在有两个进程A和B,操作系统会将内存分配个A进程,不会分配给B进程。所以,进程A、B通过内存来进行数据间的交换的可能性就完全不存在了。也就是说,此时进程A和进程B是隔离的。为此,操作系统专门提供了一套机制,用于进程之间进行必要的数据交换——进程间通信机制。
  • 进程间通信的常见方式:管道(pipe)、消息队列(message queue)、信号量(semaphore)、信号(signal)、共享内存(shared memory)、网络(network)。
  • 内存管理中主要研究的问题:管理哪些内存已经被分配,哪些内存暂未分配?已经分配出的内存,何时进行回收,如何进行回收?物理地址与线性地址的转换;内存碎片等。

三、研究操作系统实现时,面临的问题

1.死锁问题:再分配资源时,如何避免死锁问题

  • 经典问题:
    描述死锁:哲学家问题,哲学家大部分时间在思考问题,在思考间隙,需要吃饭,在哲学家的左边有一只筷子,右边有一只筷子,哲学家需要同时拿到左边的筷子(请求资源)和右边的筷子(请求资源)才能吃饭。这个时候会遇到多个哲学家同时拿筷子(请求资源)的情况,就会发生死锁。
    解决方法:银行家算法(打破死锁的充分条件)。

  • 注意:此处所说的死锁与多线程中的死锁,有内在概念上的关联性,但是严格意义上说,不是一回事。

四、线程

1.进程与线程的问题讨论

  • 目前讨论的都是操作系统层面上的线程(thread)。

  • 进程(process)和线程(thread)的关系
    进程与线程是1:m的关系:
    一个线程一定属于一个进程;一个进程下可以允许有多个线程。
    一个进程内至少有一个线程,通常这个一开始就存在的线程称为主线程(main thread)。
    主线程和其他线程之间地位是完全相等的,没有任何特殊性。

  • 为什么操作系统要引出线程(thread)这一概念?
    由于进程这一个概念本来就是资源隔离的,所以进程之间进行数据通信注定是一个高成本的工作。在现实中,一个任务需要多个执行流一起配合完成工作,是非常常见的,所以就需要一种方便数据通信的执行流,线程就承担了这一职责。

  • 什么是线程?
    线程是操作系统进行调度(分配CPU)的基本的单位;
    在这一概念中,线程变成了独立执行流的承载概念;而进程退化成了只是资源(不含CPU)的承载概念。

  • 进程和线程概念的区别剖析
    进程:操作系统进行资源分配的基本单位(不含CPU资源)。
    线程:操作系统进行调度的基本单位(CPU资源),也就是执行流的承载单位。
    例如,运行一个程序,没有线程之前,OS创建进程,分配资源,给定一个唯一的PC,进行运行。
    有了线程之后,OS创建进程,分配资源。创建线程(主线程),给定一个唯一的PC,进行运行。
    程序的一次执行过程表现为一个进程,main所在的线程就是主线程。主线程中可以运行对应的操作来创建运行其他线程。
    由于进程把调度单位这一个职责让渡给线程了,所以,使得单纯进程的创建销毁适当简单;
    由于线程的创建和销毁不涉及资源分配、回收的问题,所以,通常理解,线程的创建/销毁成本要低于进程的成本。

2.JVM中规定的线程

  • “Java线程”与“操作系统线程(原生线程)”
    不同JVM有不同的实现,它们的外在表现基本一致,除了极个别的几个现象。Java线程,一个线程异常关闭,不会连坐。我们使用的HotSpot实现(JVM)采用,使用一个OS线程来实现一个Java线程。
    Java 中由于有JVM 的存在,所以使得Java中做多进程级别的开发基本很少。Java中的线程还克服了很多OS线程的缺点。所以,在Java开发中,我们使用多线程模型来进行开发,很少使用多进程模型。

  • Java线程在代码中如何体现?
    调用java.lang.Thread类(包括其子类)的一个对象

  • 如何在代码中创建线程?
    (1)通过继承Thread类,并且重写run方法。实例化该类的对象->Thread对象。
    (2)通过实现Runhable接口,并且重写run方法。实例化Runnable对象。利用Runnable对象去构建一个Thread对象。
    Runable——让这个线程去完成的工作。

  • 启动线程
    当有一个Thread对象时,调用其start()方法。

  • 注意
    (1)一个已经调用过start(),不能再调用start()了,start()只允许工作在“新建”状态下,再调用就会有异常发生;
    (2)干万不要调用成 run()。因为调用run方法,就和线程没关系了,完全是在主线程下在运行代码。

  • 多线程模型图
    在这里插入图片描述

  • 线程代码示例

public class MyThread extends Thread {
    @Override
    public void run() {
        System.out.println("正在的执行起来");
    }
}
public class Main {
    public static void main(String[] args) {
        MyThread t = new MyThread();

        // 通过调用 Thread 对象的 start 方法,来开始线程的运行(线程中的代码运行起来)
        t.start();
    }
}

(1)如何理解t.start()做了什么?
t.start()只做了一件事情:把线程的状态从新建变成了就绪。不负责分配CPU。
(2)如下代码是先执行主线程还是先执行子线程?

public class AboutThread {
    static class SomeThread extends Thread {
        @Override
        public void run() {
            int i = 0;
            while (true) {
                System.out.println("我是另一个线程(执行流 B): " + (i++));   // 语句1
                try {
                    TimeUnit.MILLISECONDS.sleep(139);   // 语句2
                } catch (InterruptedException e) {
                }
            }
        }
    }

    public static void main(String[] args) {
        // 在主线程中,利用 SomeClass 对象,创建一个新的线程出来
        SomeThread st = new SomeThread();
        st.start();

        int i = 0;
        while (true) {
            System.out.println("我是主线程(执行流 A): " + (i++));     // 语句1
            try {
                TimeUnit.MILLISECONDS.sleep(257);   //  语句2
            } catch (InterruptedException e) {
            }
        }
    }
}

分析,线程把加入到线程调度器(不区分是OS还是JVM 实现的)的就绪队列中,等待被调度器选中分配CPU。从子线程进入到就绪队列这一刻起,子线程和主线程在地位上就完全平等了。先执行子线程中的语句还是主线程中的语句理论上都是可能的,所以,哪个线程会被选中分配CPU,完全是随机的。
但是t.start()是主线程的语句。换言之,这条语句被执行了,说明主线程现在正在CPU上(主线程是运行状态)。所以,主线程刚刚执行完t.start()就马上发生线程调度的概率不大,大概率还是t.start()的下一条语句就先执行了,也就是主线程中的语句会先被打印出来。

  • 什么情况下出现线程调度?
    (1)CPU 空闲
    当前运行着的CPU执行结束了 运行->结束
    当前运行着的CPU等待外部条件 运行->阻塞
    当前运行着的CPU主动放弃 运行->就绪
    (2)被调度器主动调度
    高优先级线程抢占
    时间片耗尽(这个情况最常见)

  • 注意:在多线程中,明明代码是固定的,但会出现现象是随机的可能性,主要原因就是调度的随机性体现在线程的运行过程中。

  • 我们写的无论是Thread的子类还是Runnable的实现类,只是给线程启动的“程序"所以,同一个程序,可以启动多个线程。
    代码示例:

public class Main {
    public static void main(String[] args) {
        MyThread t1 = new MyThread();   // 用同一个“程序”启动了多个线程
        t1.start();

        MyThread t2 = new MyThread();   // 用同一个“程序”启动了多个线程
        t2.start();

        MyThread t3 = new MyThread();   // 用同一个“程序”启动了多个线程
        t3.start();

        MyThread t4 = new MyThread();   // 用同一个“程序”启动了多个线程
        t4.start();
    }
}

总结

本篇文章主要对操作系统中涉及的相关概念进行了详细的解释,然后就是JVM线程,也就是Java初学者遇到的线程,对这一部分做了全面的论述和代码演示。希望对这一部分存在困惑的朋友有所帮助。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值