前言
操作系统是学习多线程前提,对操作系统中的相关概念有比较深入的了解,会对线程的学习有很大的帮助。本篇文章主要阐述操作系统中的各种重要的概念,并对线程做详细的介绍。
一、操作系统的基础概念介绍
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();
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++));
try {
TimeUnit.MILLISECONDS.sleep(139);
} catch (InterruptedException e) {
}
}
}
}
public static void main(String[] args) {
SomeThread st = new SomeThread();
st.start();
int i = 0;
while (true) {
System.out.println("我是主线程(执行流 A): " + (i++));
try {
TimeUnit.MILLISECONDS.sleep(257);
} 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初学者遇到的线程,对这一部分做了全面的论述和代码演示。希望对这一部分存在困惑的朋友有所帮助。
|