目录
CPU:本质上是通过一些基础的门电路构成的:
电子开关=>基础门电路=>半加器=>全加器=>加法器=>进行加减乘除
CPU跑的快主要是因为当前这个CPU集成程度非常高.里面包含了非常多的门电路。
一、进程
操作系统是一个管理的软件。
对下,要管理好各种硬件设备;
对上,操作系统要给各种软件提供稳定的运行环境。
操作系统中提供的功能,是非常复杂的,这里重点介绍操作系统中针对"进程"这个软件资源的管理。
进程是操作系统对一个正在运行的程序的一种抽象,换言之,可以把进程看做程序的一次运行过程;同时,在操作系统内部,进程又是操作系统进行资源分配的基本单位。
进程(process)又叫任务(task)。
1.1 操作系统是如何管理进程的?(进程是如何管理的)
- 先描述一个进程(明确出一个进程上的一些相关属性)
操作系统里面主要都是通过C/C++
来实现的.此处的描述其实就是用的C语言中的"结构体"(也就和Java的类差不多)。操作系统中描述进程的这个结构体,称为"PCB
" (process control block
进程控制块)。
- 再组织若干个进程(使用一些数据结构,把很多描述进程的信息放在一起,方便进行增删改查)
典型的实现就是使用一个双向链表,把每个进程的PCB给串起来。操作系统的种类是很多,内部的实现也是各有不同,此处所讨论的情况是以Linux
这个系统为例。
所谓的“创建进程",就是先创建出PCB,然后把PCB加到双向链表中;
所谓的"销毁进程",就是找到链表上的PCB,并且从链表上删除;
所谓的“查看任务管理器"就是遍历链表。
1.2 PCB(进程控制块)中的一些属性
-
pid
(进程id):进程的身份标识—进程的身份证号; -
内存指针
:指明了进程要执行的指令/代码在内存哪里,以及进程中依赖的数据都在哪里。当运行一个exe
,此时操作系统就会把这个exe
加载到内存中,变成进程. -
文件描述符表
:程序运行过程中经常要和文件打交道(文件是在硬盘上),进程每次打开一个文件,就会在文件描述符表上多增加一项(这个文件描述符表就可以视为一个数组,里面的每个元素又是一个结构体,就对应一个文件的相关信息)。
一个进程只要一启动,不管代码中是否写了打开/操作文件的代码,都会默认的打开三个文件(系统自动打开的):标准输入(System.in),标准输出(System.out),标准错误(System.err)
;这个文件描述符表的下标,就称为文件描述符。
1.3 进程调度的基本过程
上面的属性是一些基础的属性.下面的一组属性,主要是为了能够实现进程调度:
状态:这个状态就描述了当前这个进程接下来应该怎么调度
就绪状态:随时可以去CPU上执行.
阻塞状态/睡眠状态:暂时不可以去CPU上执行.(Linux中的进程状态还有很多其他的…)
优先级:先给谁分配时间,后给谁分配时间,以及给谁分的多,给谁分的少
记账信息:统计每个进程,都分别被执行了多久,分别都执行了哪些指令,分别都排队等了多久;给进程调度提供指导依据的。
上下文:表示了上次进程被调度出CPU的时候,当时程序的执行状态,下次进程上CPU的时候,就可以恢复之前的状态,然后继续往下执行。
进程被调度出CPU之前,要先把CPU中的所有的寄存器中的数据都给保存到内存中(PCB的上下文字段中)相当于存档了.下次进程再被调度上CPU的时候,就可以从刚才的内存中恢复这些数据到寄存器中.相当于读档了.
存档+读档~~ 存档存储的游戏信息,就称为"上下文"。
1.4 进程间通信
进程的调度,其实就是操作系统在考虑CPU资源如何给各个进程分配
内存资源又是如何分配的呢?通过虚拟地址空间
由于操作系统上,同时运行着很多个进程,如果某个进程,出现了bug,进程崩溃了,是否会影响到其他进程呢?现代的操作系统(windows, linux, mac…)能够做到这一点,就是“进程的独立性"来保证的,就依仗了"虚拟地址空间"。
进程之间现在通过虚拟地址空间,已经各自隔离开了,但是在实际工作中,进程之间有的时候还是需要相互交互的。这就是进程间通信。
操作系统中提供了多种这样的进程间通信机制(有些机制是属于历史遗留的,已经不适合于现代的程序开发)
现在最主要使用的进程间通信方式,两种:
1.文件操作
2.网络操作(socket)
二、线程
1.线程是什么?
一个线程就是一个 “执行流”. 每个线程之间都可以按照顺序执行自己的代码. 多个线程之间 “同时” 执行着多份代码。
例如:一家公司要去银行办理业务,既要进行财务转账,又要进行福利发放,还得进行缴社保。如果只有张三一个会计就会忙不过来,耗费的时间特别长。为了让业务更快的办理好,张三又找来两位同事李四、王五一起来帮助他,三个人分别负责一个事情,分别申请一个号码进行排队,自此就有了三个执行流共同完成任务,但本质上他们都是为了办理一家公司的业务。此时,我们就把这种情况称为多线程,将一个大任务分解成不同小任务,交给不同执行流就分别排队执行。其中李四、王五都是张三叫来的,所以张三一般被称为主线程(Main Thread)。
2.为什么要有线程?
我们的系统支持多任务了,程序猿也就需要"并发编程",通过多进程,是完全可以实现并发编程的,但是如果需要频繁的创建/销毁进程,这个事情成本是比较高的.如果需要频繁的调度进程,这个事情成本也是比较高的。
解决上述这个问题的方法:
1.进程池(数据库连接池.字符串常量池)
进程池虽然能解决上述问题,提高效率,同时也有问题.池子里的闲置进程,不使用的时候也在消耗系统资源.消耗的系统资源太多。
2.使用线程来实现并发编程,线程比进程更轻量.每个进程可以执行一个任务.每个线程也能执行一个任务(执行一段代码),也能够并发编程.
创建线程比创建进程更快.
销毁线程比销毁进程更快.
调度线程比调度进程更快.
3. 为啥线程比进程更轻量?(进程重量是重在哪里?)
重在资源申请释放(在仓库里找东西…)
线程是包含在进程中的.一个进程中的多个线程,共用同一份资源(同一份内存+文件)
只是创建进程的第一个线程的时候(由于要分配资源)成本是相对高的,后续这个进程中再创建其他线程,这个时候成本都是要更低一些,不必再分配资源。
4. (重点)线程与进程的区别?
- 进程包含线程,一个进程里面可以有一个线程,也可以有多个线程
- 进程和线程都是为了处理并发编程这样的场景,但是进程在频繁创建和销毁中,开销更高.线程开销更低.(线程比进程更轻量)
- 进程是系统分配资源(内存,文件资源…)基本单位.线程是系统调度执行的基本单位(CPU)。
- 进程具有独立性,每个进程有各自独立的独立的虚拟地址空间,一个进程挂了,不会影响其他进程。同一个进程有多个线程,共用同一块内存空间,一个线程挂了,可能影响到其他线程。
5. Java 的线程和操作系统线程的关系
线程是操作系统中的概念. 操作系统内核实现了线程这样的机制, 并且对用户层提供了一些 API 供用户使用(例如 Linux 的 pthread 库).
Java 标准库中 Thread
类可以视为是对操作系统提供的 API
进行了进一步的抽象和封装.
线程之间是并发执行的
在一个进程中,至少会有一个线程.在一个java进程中,也是至少会有一个调用main方法的线程(这个线程不是你手动搞出来的)
如下面例子中,自己创建的thread 线程和自动创建的main线程,就是并发执行的关系(宏观上看起来是同时执行)。
class MyThread2 extends Thread{
@Override
public void run() {
//run方法中得逻辑,是在新创建出来的线程中被执行的代码
while (true){
System.out.println("hello thread");
try {
//这个休眠操作,就是强制的让线程进入阻塞状态,单位是ms 1s之内这个线程不会到cpu上执行
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
public class Test02 {
public static void main(String[] args) {
Thread thread = new MyThread2();
thread.start();
while (true){
//一个进程中,至少会有一个线程
System.out.println("hello main");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
执行结果:
现在两个线程,都是打印一条,就休眠个1s,每一轮1s时间到了之后,到底是先唤醒main还是 thread,这是不确定的(随机的)。
对操作系统来说,内部对于线程之间的调度顺序,在宏观上可以认为是随机的(抢占式执行)。这个随机性,会给多线程编程带来很多麻烦。
6. 创建线程的5种写法
方法1 继承 Thread 类
创建子类,继承自Thread
,并且重写run
方法.
run
方法中描述了这个线程内部要执行哪些代码.每个线程都是并发执行的. (各自执行各自的代码)因此就需要告知这个线程,你执行的代码是啥。并不是我一定义这个类,一写run方法,线程就创建出来,需要调用start方法,才是真正的在系统中创建了线程,才是真正开始执行上面的run操作。在调用start之前,系统中是没有创建出线程的。
class MyThread extends Thread{
@Override
public void run() {
//run方法中得逻辑,是在新创建出来的线程中被执行的代码
System.out.println("hello");
}
}
public class Test01 {
public static void main(String[] args) {
Thread t = new MyThread();
//执行到这里,才真正在系统中创建了线程
t.start();
}
}
方法2 实现 Runnable 接口
创建一个类,实现Runnable
接口,再创建Runnable
实例传给Thread
实例。
//通过Runnable来描述任务的内容,然后进一步再把描述好的任务交给Thread
class MyRunnable implements Runnable{
@Override
public void run() {
System.out.println("hello");
}
}
public class Test03 {
public static void main(String[] args) {
Thread thread = new Thread(new MyRunnable());
thread.start();
}
}
方法3/4:使用匿名内部类
创建了一个匿名内部类,继承自Thread
类.同时重写run
方法.同时再new
出这个匿名内部类的实例。并调用satrt
来开启线程。
public class Test04 {
public static void main(String[] args) {
Thread t = new Thread(){
@Override
public void run() {
System.out.println("hello run");//hello run
}
};
t.start();
}
}
new的 Runnable
,针对这个创建的匿名内部类.同时new
出的Runnable
实例传给Thread
的构造方法。
public class Test05 {
public static void main(String[] args) {
Thread t = new Thread(new Runnable() {
@Override
public void run() {
System.out.println("hello runnable");
}
});
t.start();
}
}
通常认为Runnable
这种写法更好一点,能够做到让线程和线程执行的任务,更好的进行解耦。
方法5:使用lambda表达式
使用lambda
代替了Runnable
public class Test04 {
public static void main(String[] args) {
//方法5 使用lambda代替Runnable
Thread t = new Thread(()->{
System.out.println("hello thread");
});
t.start();
}
}
7. 多线程的优势-提高任务完成的效率
例如:有两个整数变量,分别要对这俩变量自增10亿次,分别使用一个线程,和两个线程。
可以观察多线程在一些场合下是可以提高程序的整体运行效率的。
串行执行
public class Test06 {
//在写一个比较长的整数常量的时候,可以通过下划线来进行分隔
private static final long count = 10_0000_0000;
//serial 串行执行
public static void serial(){
long a = 0;
long b = 0;
long begin = System.currentTimeMillis();
for (long i = 0; i < count; i++) {
a++;
}
for (long i = 0; i < count; i++) {
b++;
}
long end = System.currentTimeMillis();
System.out.println("消耗时间:" +(end - begin) + "ms");
}
public static void main(String[] args) throws InterruptedException {
serial();
}
}
执行结果:
并行执行
public class Test06 {
//在写一个比较长的整数常量的时候,可以通过下划线来进行分隔
private static final long count = 10_0000_0000;
//concurrency 并发性
public static void concurrency() throws InterruptedException {
long begin = System.currentTimeMillis();
Thread t1 = new Thread(()->{
long a = 0;
for (long i = 0; i < count; i++) {
a++;
}
});
t1.start();
Thread t2 = new Thread(()->{
long b = 0;
for (long i = 0; i < count; i++) {
b++;
}
});
t2.start();
//让main线程等待t1
t1.join();
//让main线程等待t2
t2.join();
long end = System.currentTimeMillis();
System.out.println("消耗时间:" +(end - begin) + "ms");
}
public static void main(String[] args) throws InterruptedException {
concurrency();
}
}
执行结果:
比较执行结果,可明显看出并行执行较串行执行效率显著提升。
三、Thread 类及常见方法
3.1 Thread 的常见构造方法
例:
Thread t1 = new Thread();
Thread t2 = new Thread(new MyRunnable());
Thread t3 = new Thread("这是我的名字");
Thread t4 = new Thread(new MyRunnable(), "这是我的名字");
//创建线程对象,并命名
Thread(String name)
这个是给线程(thread 对象)起一个名字,起一个啥样的名字,不影响线程本身的执行,
仅仅只是影响到程序猿调试.可以借助一些工具看到每个线程以及名字.
它很容易在调试中对线程做出区分,可以使用jconsole
来观察线程的名字。
3.2 Thread 的几个常见属性
//是否后台线程
isDaemon()
如果线程是后台线程,就不影响进程退出.
如果线程不是后台线程(前台线程),就会影响到进程退出
如:创建的t1和t2默认都是前台的线程,即使main方法执行完毕,进程也不能退出,得等t1和t2都执行完,整个进程才能退出!!!
如果t1和t2是后台线程,此时如果main 执行完毕,整个进程就直接退出, t1和t2就被强行终止了。
//是否存活
isAlive()
操作系统中对应的线程是否正在运行.
Thread t
对象的生命周期和内核中对应的线程,生命周期并不完全一致。创建出t
对象之后,在调用start之前,系统中是没有对应线程的,在run方法执行完了之后,系统中的线程就销毁了。但是t
这个对象可能还存在,通过isAlive()
就能判定当前系统的线程的运行情况。
如果调用start之后, run 执行完之前, isAlive
就是返回true
;
如果调用start之前, run执行完之后, isAlive
就返回false
;
例:
public class Test12 {
public static void main(String[] args) {
Thread thread = new Thread(() -> {
for (int i = 0; i < 10; i++) {
try {
System.out.println(Thread.currentThread().getName() + ": 我还活着");
Thread.sleep(1 * 1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
System.out.println(Thread.currentThread().getName() + ": 我即将死去");
});
System.out.println(Thread.currentThread().getName()
+ ": ID: " + thread.getId());
System.out.println(Thread.currentThread().getName()
+ ": 名称: " + thread.getName());
System.out.println(Thread.currentThread().getName()
+ ": 状态: " + thread.getState());
System.out.println(Thread.currentThread().getName()
+ ": 优先级: " + thread.getPriority());
System.out.println(Thread.currentThread().getName()
+ ": 后台线程: " + thread.isDaemon());
System.out.println(Thread.currentThread().getName()
+ ": 活着: " + thread.isAlive());
System.out.println(Thread.currentThread().getName()
+ ": 被中断: " + thread.isInterrupted());
thread.start();
while (thread.isAlive()) {}
System.out.println(Thread.currentThread().getName()
+ ": 状态: " + thread.getState());
}
}
执行结果:
3.3 启动一个线程-start()
调用 start 方法, 才真的在操作系统的底层创建出一个线程.
start和run的区别
run 单纯的只是一个普通的方法,描述了任务的内容.
start则是一个特殊的方法,内部会在系统中创建线程.
3.4 中断一个线程
中断线程就是让一个线程停下来。
线程停下来的关键,是要让线程对应的run方法执行完(还有一个特殊的,是main这个线程.对于main来说,得是main方法执行完,线程就完了)。
目前常见的有以下两种方式:
1. 通过共享的标记来进行沟通
可以手动的设置一个标志位(自己创建的变量, boolean
),来控制线程是否要执行结束。
public class Test07 {
public static boolean isQuite = false;
public static void main(String[] args) {
Thread t = new Thread(()->{
while (!isQuite){
System.out.println("hello thread");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
});
t.start();
try {
Thread.sleep(5000);
} catch (InterruptedException e) {
e.printStackTrace();
}
isQuite = true;
System.out.println("终止t线程");
}
}
输出结果:
在其他线程中控制这个标志位,就能影响到这个线程的结束.
此处因为多个线程共用同一个虚拟地址空间。因此, main
线程修改的isQuit
和t
线程判定的isQuit
,是同一个值。
2. 调用 interrupt() 方法来通知
使用 Thread.interrupted()
(这是一个静态方法)
或者 Thread.currentThread().isInterrupted()
(这是一个实例方法,其中currentThread
能够获取到当前线程的实例) 代替自定义标志位.
推荐使用: Thread.currentThread().isInterrupted()
这个方法。
public class Test08 {
public static void main(String[] args) {
Thread t = new Thread(()->{
//中断一个线程
while (!Thread.currentThread().isInterrupted()){
System.out.println("hello thread");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
//做一些收尾工作
System.out.println("收尾工作");
//当触发异常之后,立即退出循环
break;
}
}
});
t.start();
try {
Thread.sleep(5000);
} catch (InterruptedException e) {
e.printStackTrace();
}
//调用这个方法可能产生:
//如果t线程是就绪状态,就是没有设置标志位为true
//如果t线程处于阻塞状态(sleep休眠了),就会触发一个InterruptedException
t.interrupt();
}
}
调用这个interrupt
方法,就会让sleep触发一个异常从而导致线程从阻塞状态被唤醒。当下的代码,一旦触发了异常之后,就进入了catch
语句,在catch
中,就单纯的只是打了一个日志,printStackTrace
是打印当前出现异常位置的代码调用栈,打完日志之后,就直接继续运行。
运行结果:
3.5 等待一个线程-join()
多个线程之间,调度顺序是不确定的,线程之间的执行是按照调度器来安排的.这个过程可以视为是"无序,随机"这样不太好.有些时候,我们需要能够控制线程之间的顺序.
线程等待就是其中一种控制线程执行顺序的手段,此处的线程等待,主要是控制线程结束的先后顺序。
public class Test09 {
public static void main(String[] args) throws InterruptedException {
Thread t = new Thread(()->{
for (int i = 0; i < 5; i++) {
System.out.println("hello thread");
}
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
});
t.start();
//首先,调用这个方法的线程,是main 线程.针对t这个线程对象调用的.
//此时就是让main等待t
t.join(1000);//指定最大等待时间
}
}
执行结果:
调用join之后, main线程就会进入阻塞状态(暂时无法在cpu上执行)
代码执行到join这一行,就暂时停下了,不继续往下执行了,然后join 啥时候能继续往下走,恢复成就绪状态呢?就是等到t线程执行完毕(t的run方法跑完了)。
通过线程等待,就是在控制让t先结束, main后结束,一定程度上的干预了这两个线程的执行顺序。
在上述二的第7小节中就用到了join。
public void join(long millis) //等待线程结束,最多等 millis 毫秒
进入join 也会产生阻塞.这个阻塞不会一直持续下去.如果在最大等待时间之内, t线程结束了,此时join直接返回,如果在最大等待时间之后,t仍然不结束, 此时 join 也就直接返回。
3.6 获取当前线程实例的引用
//返回当前线程对象的引用
public static Thread currentThread();
例:
public class Test10 {
public static void main(String[] args) {
Thread t = new Thread(){
@Override
public void run() {
//同样可以拿到当前Thread的实例
// System.out.println(Thread.currentThread().getName());//Thread-0
//拿到当前Thread的实例
System.out.println(this.getName());//Thread-0
}
};
t.start();
//拿到main线程的实例
System.out.println(Thread.currentThread().getName());//main
}
}
Runnable
和lambda
表达式都不能获得Thread的实例,Runnable
只是一个单纯的任务,没有name属性,lambda同理。
3.7 休眠当前线程
因为线程的调度是不可控的,所以,这个方法只能保证实际休眠时间是大于等于参数设置的休眠时间。
进程:PCB+双向链表;这个说法是针对只有一个线程的进程。
如果是一个进程有多个线程, 此时每个线程都有一个PCB,一个进程对应的就是一组PCB了。
PCB上有一个字段tgroupld
,这个id其实就相当于进程的id.同一个进程中的若干个线程的tgroupld
是相同的!!!
如果某个线程调用了sleep方法,这个PCB就会进入到阻塞队列,操作系统调度线程的时候,就只是从就绪队列中挑选合适的PCB到CPU上运行,阻塞队列里的PCB就只能干等着。当睡眠时间到了,系统就会把刚才这个PCB从阻塞队列挪回到就绪队列。
在前面的案例中大多都有用到sleep方法。