终于写完了,
也算为假期画上了一个完美的句号~~~
目录
1.进程
1.1什么是进程
1.2操作系统是如何管理进程的
1.3进程中PCB的相关属性
1.3进程的调度
1.4进程的独立性
1.5进程的通信
2.线程
2.1什么是线程
2.2为什么要有线程
2.3为什么线程比进程更轻量
2.4线程和进程的区别与联系(经典面试题)
3.Java中多线程编程Thread的基本用法
3.1通过Thread来创建线程
3.1.2创建子类,继承Thread
3.1.3通过Runnable来进行实现
3.1.4通过匿名内部类来实现
3.1.5通过lambda表达式来实现
3.2多线程能够提供运行的效率
3.3Thread类的常见属性和方法
3.4线程的终止
3.5线程的等待
3.6线程获取引用
3.7线程的休眠
1.进程
1.1什么是进程
①什么是程序?
进程是跑起来的程序,将运行起来的可执行文件称之为进程。
②什么是可执行文件?
可执行文件都是文件,是静静地躺在硬盘上的,在双击之前,不会对系统有任何影响。但是一旦双击,操作系统就会把这个exe文件加载到内存中,并且让CPU开始执行exe内部的一些指令(即一些对应的二进制),这是就开始进行了一些具体的工作。把这些运行起来的可执行文件,称为"进程"
③下面就是电脑上进程运行的图:
1.2操作系统是如何管理进程的
①先描述一个进程(明确给出一个进程上面的一些相关属性):
a.操作系统里面主要是通过C++/C来实现的,此处的描述其实就是用的C语言中的结构体
b.操作系统中描述这个结构体,称为"PCB"(进程控制块)。注意!!!这里的PCB不是硬盘上的PCB
②再组织若干个进程(使用一些数据结构,把描述很多进程的信息给放到一起,方便进行增删查改)a.典型的实现,就是使用双链表,来把每个PCB给串起来
"创建进程":就是先创建出PCB,然后把PCB加到双向链表中
"销毁进程":就是找到链表上的PCB,并且从链表上删除
"查看任务管理器":遍历链表b.操作系统的种类是很多的,我们这里讨论的是以Linux为例的
1.3进程中PCB的相关属性
①pid(进程id):
进程的身份标识
②内存指针:
指明了这个进程要执行的代码/指令在内存的位置,以及这个进程中依赖的数据都在哪里。当运行一个exe,此时的操作系统就会把这个exe加载到内存中,变成进程
③文件描述符:
程序运行的过程中,是必定会和文件相关的。(这个文件描述表就可以视为一个数组,里面的每个元素又是一个结构体,就对应一个文件的相关信息)。
一个进程一旦启动,就会默认打开三个文件(系统自动打开):标准输入(system.in),标准输出(system.out),标准错误(system.err)
1.3进程的调度
上面的属性是一些基础的属性,而下面的属性是为了能够实现进程的调度
①什么是进程的调度?
而对于现在的系统而言,一般都是“多任务操作系统”,即一个系统在同一段时间内,执行了很多任务,比如现在我的系统上同时运行着QQ,画图,CSDN等等,任务数量可能有几十个,几百个,但CPU却只有6核,这就是所谓的“进程调度”一般都是“多任务操作系统”,即一个系统在同一段时间内,执行了很多任务,比如现在我的系统上同时运行着QQ,画图,CSDN等等,任务数量可能有几十个,几百个,但CPU却只有6核,这就是所谓的“进程调度”。
②并行,并发的理解:
a.并行:
微观上而言,两个CPU核心,同时执行两个任务的代码的操作
b.并发:
微观上而言,一个CPU核心,先执行一会任务1,再执行一会任务2,即多个任务交替执行,只要切换得足够快,那么宏观上看起来就像是多任务在同时执行一样
对于并行和并发这两件事,只是微观上有所区分(操作系统自行调度的结果),而在宏观上是区分不了的。所以我们只在研究操作系统进程调度的时候,稍作区分,而其他情况下,通常说的并发=并行+并发
③四大属性:
a.状态:
状态描述了当下进程接下来应该如何去调度。这里举两个常常涉及到的状态:
就绪状态(随时有空,可以去CPU上执行)
阻塞状态/睡眠状态(暂时可以不去CPU上执行)b.优先级:
分配的先后顺序以及分配的时间/空间的多少c.记账信息:
统计每个进程都分别执行了多久,分别执行了哪些指令,分别排队等了多久,这是用来给进程调度提供指导依据的d.上下文:(存档+读档=“上下文”)
对调度出的CPU的时候,进程执行的状态。分为三种情况:
(1)下次进程上CPU的时候,就可以恢复之前的状态,然后继续向下执行
(2)进程被调度出CPU之前,要先把CPU中的所有寄存器中的数据保存在内存中(PCB的上下文字段中)这里相当于存档
(3)下次进程再次调度上CPU的时候,就可以从刚才的内存中,恢复这些数据到寄存器中,这里相当于读档④举一个通俗的例子,对上述三点来进行解释:
所谓的调度就是“时间管理”:
假设A同学是一个非常完美的男生。原则上来说A同学在同一时刻,只能谈一个女朋友。但是他想女朋友长得漂亮带出去有面的同时也希望女朋友成绩好可以辅导他。这个时候,他谈了两个对象。
对象1:外在形象气质佳
对象2:学习能力极强。
因此,他就需要合理规划他的时间,避免两个女朋友同时碰面。他就安排了一个时间表。此时在宏观上看来,他谈了两个女朋友,但在微观上每个时刻看来,他只谈了一个女朋友。
并发:规划时间表的过程,就是“调度”的过程状态:正常情况下,A同学可以在任何时间叫任意的女朋友陪他,他的每个女朋友都处于就绪状态。假设1要去外地参加一个舞蹈比赛,那么我们就认为1是阻塞状态/休眠状态。所以在她外出的这段时间,A同学就可以不安排和1相处的时间
优先级:当A同学有空且女朋友1,2都有空时,他可以根据自己的想法决定哪段时间和哪个女朋友在一起
记账信息:若是A同学经常和1在一起,那么2就会不开心,A就会发现他给2的时间排的太少了,他就会适当增加一些,以便留住2的心
上下文:1,2女生的生日和喜好是不同的,A同学需要将其各自需求记在一个本子上,这样等到需要的时候就可以准确地对上号,以免尴尬
以上只是一个恰当的例子,但是在现实生活中,我们还是要做一个专情的人奥~
1.4进程的独立性
①进程独立性的引入:
进程的调度,其实就是操作系统在考虑CPU资源如何给各个进程分配。
内存资源又是如何分配的呢?
是根据虚拟地址空间来进行分配的。由于操作系统上同时运行着多个进程,如果某个进程出现了bug是否会影响到其它进程呢?
如何能够做到这一点呢?这就要靠“进程的独立性”来保证,就依仗了“虚拟地址空间”。②当需要交互时如何进行操作:
进程之间现在通过虚拟地址空间,已经各自隔开了,但在实际工作中,进程之间有的时候是需要相互交互的。类似的,咱们的两个进程之间,也是隔离开的,也是不能直接交互的,操作系统也是提供了类似的“公共空间”进程A就可以把数据放到公共空间上,进程B再取走
1.5进程的通信
现在我们经常使用到的两种通信方式:
1.文件操作
2.网络操作
2.线程
2.1什么是线程
线程是进程的一部分,进程包含线程。如果把进程想象成一个工厂,那么线程就是工厂里的生产线,一个工厂里面可以有一个生产线,也可以有多个
2.2为什么要有线程
①进程执行并发编程时的问题:
因为系统支持多任务,而程序猿需要“并发编程”,因为多进程是可以实现并发编程的,但这个时候就会出现一些问题:因为需要频繁的创建/销毁进程,这个成本是比较高的,同时,要是频繁地调度进程,成本也是比较高的
②如何解决上述问题呢?
a.进程池(数据库连接池,字符串常量池)
进程池虽然能够解决上述问题,但是本身也存在着问题,当池子里的进程闲置时,不使用也会对系统资源进行消耗。那么消耗的系统资源就太多了。b.使用线程来实现并发编程
线程比进程更轻量,每个进程可以执行一个任务,而每个线程也能执行一个任务(一段代码),也能够并发编程。而无论是创建/销毁/调度线程的成本都比对应操作进程低很多,所以有了线程
2.3为什么线程比进程更轻量
①进程的重量重在哪里?
重在资源申请释放(在仓库里找东西......)
②为什么线程比进程更轻量?
线程是包含在进程中的,一个进程中的多个线程,共用同一份资源(内存+文件)
只是创建第一个线程的时候由于要分配资源,进而成本相对较高,而后续这个进程中再创建其它线程的时候,这个时候成本都更低一些,不必再分配资源了
2.4线程和进程的区别与联系(经典面试题)
①进程包含线程,一个进程里既可以有一个线程,也可以有多个
②进程和线程都是为了处理并发编程这样的场景但是进程有问题,在频繁创建和释放的时候,效率很低,相比之下,线程更轻量,创建和释放的效率更高(为啥更轻量?少了申请释放资源的过程)
③操作系统创建进程,要给进程分配资源,进程是操作系统分配资源的基本单位
操作系统创建的线程,是要在CPU上调度执行,线程是操作系统调度执行的基本单位(前面讲的时间管理,当时的调度的进程,更准确的来说是调度的线程④进程具有独立性,每个进程有各自的虚拟空间地址,一个进程挂了,不影响其他进程。而同一个进程里的多个线程,共有一个内存空间,一个线程挂了,可能会影响到其他的线程,导致整个进程的崩溃
3.Java中多线程编程Thread的基本用法
3.1通过Thread来创建线程
通常认为
Runnable
这种写法更好一点,能够做到让线程和线程执行的任务,更好的进行解耦。
3.1.2创建子类,继承Thread
代码:
class MyThread extends Thread{ @Override public void run() { System.out.println("hello"); } } public class demo { public static void main(String[] args) { Thread t=new MyThread(); //线程开始运行 t.start(); } }
对各部分的解释:
3.1.3通过Runnable来进行实现
通过Runnable来描述任务内容,再进一步把描述好的任务交给Thread实例
代码:
class MyRunnable implements Runnable{ @Override public void run() { System.out.println("hi"); } } public class demo1 { public static void main(String[] args) { Thread t=new Thread(new MyRunnable()); t.start(); } }
3.1.4通过匿名内部类来实现
a.创建一个匿名内部类,继承自Thread类,同时重写run方法,同时再new出这个匿名内部类的实例
public class demo3 { public static void main(String[] args) { Thread t=new MyThread(){ @Override public void run() { System.out.println("hello hi"); } }; t.start(); } }
b.new的Runnable,针对这个创建的匿名内部类,同时new出的Runnable实例传给Thread的构造方法
public class demo4 { public static void main(String[] args) { Thread t=new Thread( new Runnable(){ @Override public void run() { System.out.println("what"); } }); t.start(); } }
3.1.5通过lambda表达式来实现
是使用lambda代替Runnable而已
public class demo5 { public static void main(String[] args) { Thread t=new Thread(()->{ System.out.println("hello Tim"); }); t.start(); } }
3.2多线程能够提供运行的效率
①进程并发性的体现:
在一个java进程中,至少会有一个main方法的线程(这个线程不是你手动创造的)。而自己手动创建的t线程和自动创建的main线程,就是并发(此处的并发=并行+并发)执行的关系(宏观上看来两者是同时进行执行的)
代码:
public class demo6{ public static void main(String[] args) { Thread t=new Thread(new Runnable() { @Override public void run() { int i=10; System.out.println("这里是手动创建的t线程的打印结果"); try { Thread.sleep(1000); } catch (InterruptedException e) { e.printStackTrace(); } } }); t.start(); //main 线程 System.out.println("这里是自动创建main的打印结果"); try { Thread.sleep(1000); } catch (InterruptedException e) { e.printStackTrace(); } } }
打印结果:
②有两个整数变量,分别要对这俩变量自增10亿次,分别使用一个线程,和两个线程。
我们来观察串行执行和并发执行的效率serial
串行的完成一系列运算
.
concurrency
使用两个线程并行的完成同样的运算
.a.串行:(单线程)通过这一个单线程来执行了a和b自加的操作public class demo7{ private static final long count = 1_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(); } }
b.并行:(多线程)
通过两个线程,来实现a和b自加的操作
public class demo8{ private static final long count = 20_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(); t1.join(); t2.join(); //当两个进程结束后再获取时间 long end = System.currentTimeMillis(); System.out.println("消耗时间:" +(end - begin) + "ms"); } public static void main(String[] args) throws InterruptedException { concurrency(); } }
两者结果比较
a.基于private static final long count = 20_0000_0000
b.基于private static final long count = 1000_0000;
由此我们可以发现,当任务量较大时,多线程大大提高了效率,而当任务量较小时,由于多线程本身存在更多的开销,则这个时候单线程更为高效
3.3Thread类的常见属性和方法
①Thread(String name):这个方法是给线程取一个名字,而取什么名字并不影响程序本身,仅仅是可以在调试中更方便的看到。
命名格式:
而我们在哪里看这个名字呢?我们可以通过jconsole来看
a.找到我的电脑中的JDK
b.双击后点自己改的,就可以看到你需要的相关属性了
②几个常用的属性:
介绍几个上述属性中用的较多的属性:
a.isDaemon()是否后台线程
如果线程是后台线程,就不影响进程退出。如果进程不是后台线程(前台线程),就会影响到进程退出。
举个例子:
要是创建的t1,t2线程默认是前台线程,那么即使main方法执行完毕,进程也不能退出。得等t1,t2都执行完成后,整个进程才能退出。而如果t1,t2是后台线程,此时如果main执行完毕,整个进程就直接退出,t1,t2就被强行终止了b.isAlive()是否存活
Thread t对象的生命周期和内核中对应的线程,生命周期并不完全一致,创建出t对象之后,在调用start之前,系统中是没有对应线程的。在run方法执行完了之后,系统中的线程就销毁了,但是t这个对象可能还存在。
就可以通过isAlive()来判定当前系统的线程的运行情况如果,调用start之后,run执行完之前,isAlive()就返回true
如果,调用start之前,run执行完之后,isAlive()就返回false③一些重要方法:
a.启动一个线程(start):
决定了是否真的创建了线程
区分一下run和start:
run单纯地只是一个普通方法,描述了任务的内容。
start则是一个特殊的方法,内部会在系统中创建线程。
你在main线程里调用run,其实并没有创建出新的线程,这个循环仍然在main线程中执行的
既然是在一个线程中执行,代码就得按照先后顺序,先运行第一个循环,后运行第二个循环如图所示,run这里就是普通的方法调用,执行完上面的循环后再开始执行下面的循环
而对于下图而言,我们可以看到两个线程是交替执行的
3.4线程的终止
中断线程就是让一个线程停下来。线程停下来的关键,是要让线程对应的run方法执行完(还有一个特殊的,是main这个线程.对于main来说,得是main方法执行完,线程就完了)。
我们在这主要介绍两种方法:
①手动设置一个标志位(自己创建的变量,Boolean),来控制线程是否要执行结束
在其他线程中控制这个标志位,就能影响到这个线程的结束。因为多个线程共用同一个虚拟地址空间。因此,
main
线程修改的isQuit
和t
线程判定的isQuit
,是同一个值。代码:
public class demo3 { 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线程"); } }
② 更好的做法是通过Thread来内置一个标志位来进行判定可以通过
a.
Thread.interrupted()
(这是一个静态方法)
b.Thread.currentThread().isInterrupted()
(这是一个实例方法,其中currentThread
能够获取到当前线程的实例) 一般无脑用这个方法就好。下面进行演示代码①:使用t.interrupt()方法时,线程处于就绪状态,这个时候标志位被设置为true,徐一直执行while死循环打印“hello thread”
public class demo4 { public static void main(String[] args) { Thread t = new Thread(()->{ //中断一个线程 while (!Thread.currentThread().isInterrupted()) { System.out.println("hello thread"); } }); t.start(); try {//main线程休眠5秒 Thread.sleep(5000); } catch (InterruptedException e) { e.printStackTrace(); } //如果t线程是就绪状态,就是设置标志位为true,循环就会一直进行下去 t.interrupt(); } }
代码②:使用t.interrupt()方法时,线程处于休眠(阻塞)状态,就会触发一个interruptException异常,用来唤醒它的休眠(sleep)。触发异常后就会进入catch语句中打印一个日志,然后就直接运行
public class demo4 { 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(); } } }); t.start(); try { Thread.sleep(5000); } catch (InterruptedException e) { e.printStackTrace(); } //如果t线程是就绪状态,就是设置标志位为true,循环就会一直进行下去 t.interrupt(); } }
结果如下:
解决方案:在读取日志之后,加一个break,使其跳出循环。而达到终止的目的
代码:
public class demo4 { 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是休眠状态,就会触发异常,唤醒阻塞 t.interrupt(); } }
运行结果:
3.5线程的等待
①什么是线程的等待?
对于多个线程之间,调度顺序是不确定的,线程之间的执行时按照调度器来进行安排的,这个过程可以视为“无序,随机”,但是有些时候,我们希望能够控制线程之间的顺序。为了达到这种目的,我们这里采用的方式是线程等待,此处的等待,主要是控制线程结束的先后顺序。我们用join来进行控制,哪个线程调用的join,哪个线程就会阻塞等待,知道对应的线程执行完毕为止(即对应线程的run执行完)
②怎么让线程等待?
首先调用这个方法的线程是main线程,针对t这个线程对象调用的,此时就是让main等待t。当调用了join之后,main线程就会进入阻塞状态(暂时无法在cpu上执行),即代码执行到这一行,暂时就不向下执行,那什么时候恢复成就绪状态呢?这个得等t线程执行完毕(即t的run方法跑完了)
这种通过线程等待,就是控制让t先结束,main后结束,这个在一定程度上干预了两个线程的执行顺序。
代码:加了join后要等到它执行完再执行main
public class demo7 { public static void main(String[] args) { 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(); try { t.join(); } catch (InterruptedException e) { e.printStackTrace(); } for(int i=0;i<3;i++){ System.out.println("你好"); } } }
运行结果:(如果不加的话,下面的内容应该是交替出现的)
③join的另一个版本,在规定时间内实在等不到,就不再等待了
比如将上述代码等待时间改成4s就会出现以下结果:
3.6线程获取引用
①怎样获取引用?
Thread.currentThread()就能够获取到当前线程的引用(Thread实例的引用)
哪个线程调用的这个currentThread,就获取到的是哪个线程的实例②例子:
a.简单地获取以下线程的名字
代码:
public class demo8 { public static void main(String[] args) { Thread t=new Thread(()->{ System.out.println(Thread.currentThread().getName()); }); t.start(); System.out.println(Thread.currentThread().getName()); } }
结果如下:
b.利用this引用来进行获取
代码:(输出结果与上面内容是一致的,需要注意的是,这个不是任何时候都能用,万能用法是a那种)
public class demo8 { public static void main(String[] args) { Thread t=new Thread(){ @Override public void run() { System.out.println(this.getName()); } }; t.start(); System.out.println(Thread.currentThread().getName()); } }
3.7线程的休眠
我们前面进程讲过:PCB+双向链表;这个说法是针对只有一个线程的进程。
如果是一个进程有多个线程, 此时每个线程都有一个PCB,一个进程对应的就是一组PCB了。PCB上有一个字段tgroupld
,这个id其实就相当于进程的id.同一个进程中的若干个线程的tgroupld
是相同的。而当前这个PCB都有各自的阻塞/就绪状态,当某个线程调用了sleep方法,这个PCB就会进入阻塞的状态,而这个时候,当操作系统调度线程的时候,就只会从就绪队列中挑选合适的PCB,而在阻塞队列中的PCB就只能够处于等待的时间。而当睡眠时间到了,才会从阻塞队列到就绪队列。
本节内容就到这里结束啦~感谢观看~