目录
一.进程
- 定义
- 进程在操作系统内核中管理形式
- PCB里进程的特征
- 进程运行形式
- 进程调度相关属性
二.线程
- 定义
- 线程优点
- 线程缺点
三. 多线程创建形式
四.进程与线程区别
五.Thread常见方法
六.Thread常见属性
七.线程状态
一.进程
-
定义:进程指一个跑起来的程序,程序是静态的,它作为系统的一种资源永远存在,而进程是动态的,它会随着程序的结束而销毁.进程不仅包含这个程序的实体,它还包含这个程序所占有的系统资源.(例如同一程序,同一时刻两次运行,就表示他们是两个独立的进程)
-
进程在操作系统内核里如何管理?
(描述+组织)描述指使用结构体PCB结构控制块来描述进程属性,组织指的是通过双向链表把PCB串在一起.
创建一个进程,本质上就是创建一个PCB这样的结构体对象,把它插入到链表中
销毁一个进程,本质上就是把链表上的PCB结点删除掉
任务管理器查看进程列表,本质上就是遍历这个链表 -
PCB里面有哪些描述了进程的特征?
1.PID:进程身份标识符(唯一的一个数字),一个主机同一时刻这些进程的PID是唯一的,通过PID来区分进程
2.内存指针:指向了自己的内存(进程所占的系统资源,指向该进程持有的一些重要数据在内存中的位置)
3.文件描述符表:硬盘上的文件等其他资源
一个进程可能是一个PCB,也可能是对应多个PCB,系统管理PCB链表也不一定是一个 -
进程运行的形式
1.并行:微观上同一时刻,两个核心进程同时执行
2.并发:微观上来说,同一时刻,一个核心上只能运行一个进程,但是如果进程之间快速切换,我们是感知不到的,就好像这几个进程同时进行.
在这里插入代码片
//串行与并行区别
public class Demo6 {
public static final long cont=100000000L;
public static void main(String[] args) {
//serial();
concurrency();
}
//串行任务
public static void serial(){
long beg=System.currentTimeMillis();
long a=0;
for (int i = 0; i < cont; i++) {
a++;
}
a=0;
for (int i = 0; i < cont; i++) {
a++;
}
long end=System.currentTimeMillis();
System.out.println("时间间隔:"+(end-beg));
}
//并发
public static void concurrency(){
long beg=System.currentTimeMillis();
Thread t=new Thread(()->{
long a=0;
for (int i = 0; i < cont; i++) {
a++;
}
});
Thread t1=new Thread(()->{
long a=0;
for (int i = 0; i < cont; i++) {
a++;
}
});
t.start();
t1.start();
long end=System.currentTimeMillis();
System.out.println("时间间隔:"+(end-beg));
}
}
使用两个线程最终消耗的时间不一定是一个线程消耗时间的50%,创建线程实例,也是有开销的,串行执行,没有额外创建线程,并发执行创建了两个线程.如果计算量小,计算的快,创建线程开销影响更大,多线程提升就不明显
-
进程调度相关属性
1.进程状态
就绪状态:进程随时准备好执行
运行状态:正在运行
阻塞状态:短时间内无法在CPU上执行
2.优先级
操作系统进行调度时也是有优先级的,创建进程时,可以通过一些系统调度相对的干扰优先级
3.上下文
本质上是存档内容,CPU的寄存器的值
操作系统在进程切换时,需要把进程执行的中间状态记录下来保存好,下次这个进程在CPU上运行时,就可以恢复上次状态继续执行
4.记账信息
操作系统统计每个进程在CPU上占用的时间和执行的指令数目,以便调整下次各个进程改如何调度 -
虚拟地址空间(避免进程之间相互产生影响)
程序所获取的地址并不是真实的物理内存地址,而是通过MMU映射到的物理地址,是抽象虚拟的.
一旦进程访问越界,操作系统发现当前地址超过进程1访问范围,直接向进程反馈一个错误,这样会防止干扰操作,提高系统稳定性,不会出现一个进程有bug而影响其他进程 -
进程之间是相互独立的,如果需要进程通信,就会引出进程间通信机制
指一个或多个进程都可以访问到公共空间,基于这个公共空间进行数据间的交换即可.
二.线程(thread)
-
定义:线程被包含在进程中,一个进程默认有一个线程,也可以有多个线程.一个线程就是一个 “执行流”. 每个线程之间都可以按照顺讯执行自己的代码. 多个线程之间 “同时” 执行着多份代码.
-
为什么要引入线程?
CPU进入多核新时代,如果用多进程去提高执行速度,会造成消耗资源多,速度慢,而频繁的创建与销毁进程这个操作比较低效. -
多线程的优点
1.可以充分利用多核CPU,提高效率
2.只是创建第一个进程时需要申请资源,后续在创建新线程,都是共用一份资源,节省申请资源的开销.销毁线程时,也只是销毁到最后一个才真正释放资源,前面的线程不必真正释放资源 -
多线程的缺点
1.当线程数目越来越多时,CPU核心数有限,当线程数目达到一定程度时,CPU核心已经被吃满,此时增加线程,无法提高效率,反而会因为线程太多,线程调度开销太大,影响效率.
2.线程之间可能会相互影响,如果两个线程同时修改变量,也容易产生线程不安全问题.
3.如果某个线程发生意外,很可能把整个线程带走,其他线程无法工作 -
操作系统内核:通过一组PCB来描述一个进程,每一个PCB对应一个线程.一个进程至少有一个线程,也可以是多个.这一组的内存指针和文件描述符表其实是同一份东西,而状态,优先级,上下文则是每一个线程都会有一个
三.谈谈线程和进程区别
1.进程包含线程
2.线程比进程更轻量(创建更快,销毁更快)
3.同一个进程多个线程共用一份内存/文件资源,进程与进程之间则是相互独立的
4.**进程是资源分配的基本单位,线程是调度执行的基本单位**
四.多线程创建方式
- 继承Thread类
在这里插入代码片
class MyThread extends Thread{
@Override
public void run() {
System.out.println("这里是线程运行实例");
}
}
public class Demo1 {
public static void main(String[] args) {
MyThread t=new MyThread();//创建一个Mythread实例,并不是在系统中直接创建
t.start();//调用start方法时,才真正创建出一个新线程
}
}
1.重写run方法,run是Thread父类里面已经有的方法,run里面的逻辑就是这个线程要执行的工作,Java运行一次程序,就是启动一个进程,一个进程里面至少会有一个线程,或着多个线程,main方法里面的线程也叫做主线程.
2.(main主线程与Mythread创建出来的新线程都是并发执行的)调用start方法另外启动一个线程在执行run方法,新线程是一个单独的执行流,和现有线程执行不想关.
在这里插入代码片
class MyThread extends Thread{
@Override
public void run() {
while (true){
System.out.println("hello");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
}
}
public class Demo1 {
public static void main(String[] args) {
MyThread t=new MyThread();
t.start();
while (true){
System.out.println("main");
try {
Thread.sleep(1000);//执行sleep,线程进入阻塞,sleep时间到,线程恢复就绪状态,但是哪个线程谁先谁后不一定,抢占式执行
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
}
}
- 实现Runable接口
在这里插入代码片
class MyRunnable implements Runnable{
@Override
public void run() {
while (true){
System.out.println("Hello");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
public class Demo2 {
public static void main(String[] args) throws InterruptedException {
MyRunnable runnable=new MyRunnable();
Thread t=new Thread(runnable);
t.start();
Thread t1=new Thread(runnable);
t1.start();
while (true){
System.out.println("main");
Thread.sleep(1000);
}
}
}
1.runnable当作t变量里面的参数,主要是把线程的任务与线程本身分开,使用runnable专门表示线程完成的任务.前面的继承thread写法,把线程要完成的工作与线程本身耦合在一起,如果要对代码进行调整,代码改动比较大,而runnable只需把它传给其他实体进行改动即可.
2.如果想要多个线程干一样的任务,runnable是最合适的.
- 继承Thread类,使用匿名内部类
在这里插入代码片
public class Demo3 {
public static void main(String[] args) {
Thread t=new Thread(){ //创建Thread子类,同时实例化出一个对象
@Override
public void run() {
while (true){
System.out.printf("hello");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
};
t.start();
}
}
- 实现Runnable接口,使用匿名内部类
在这里插入代码片
public class Demo4 {
public static void main(String[] args) {
Thread t=new Thread(new Runnable() {//匿名内部类实例,作为构造方法参数
@Override
public void run() {
while (true){
System.out.println("hello");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
}
});
t.start();
}
}
- 通过lambda表达式
在这里插入代码片
public class Demo5 {
public static void main(String[] args) {
Thread t=new Thread(()->{
while (true){
System.out.printf("hello");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
});
t.start();
}
}
lambda本质上是匿名函数,()表示函数形参,{}表示函数体,->表示它是一个lambda
三.Thread常见方法
1.给线程起名字
Thread t3 = new Thread(“这是我的名字”);
Thread t4 = new Thread(new MyRunnable(), “这是我的名字”);
2.创建一个实例对象
Thread t1 = new Thread();
Thread t2 = new Thread(new MyRunnable());
六.Thread常见属性
1.ID:这是java中给Thread对象安排的身份标识,和操作系统内核中的PCB中的PID,以及操作系统提供的线程API的线程id不是一回事.
2.isDaemon()判断是不是守护线程
1.默认创建的线程是前台线程,前台线程会阻止进程退出,如果是后台线程,后台线程不阻止进程退出,如果前台线程执行完,这个时候不论后台线程有没有执行完,程序都会退出
2.main运行完前台线程还没完,程序不会退出
setDaemon()设置成后台线程,但是必须在调用start开始之前设置,如果前台线程启动了,就改变不了了.
3.isAlive()内核线程是否存活
Thread对象虽然和内核中的线程是一 一对应关系,但是生命周期并不是完全对应的.
Thread对象实例化出来,但是内核线程不一定会有,调用start方法,内核线程才会有,当你内核线程执行完,run运行完,内核线程就销毁了,但是Thread对象还在.所以说Thread对象的生命周期比线程长.
4.start方法
直接调用run方法并没有创建线程,只是在原来线程里面运行代码,调用start,则是创建了线程,在新线程中执行代码(和原来线程是并发的)
```java
在这里插入代码片
class MyThread1 extends Thread{
@Override
public void run() {
while (true){
System.out.println("hello Thread");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
public class Demo7 {
public static void main(String[] args) throws InterruptedException {
MyThread1 t=new MyThread1();
t.run();
// t.start();
while (true){
System.out.println("main");
Thread.sleep(1000);
}
}
}
属性的展示:
在这里插入代码片
public class Demo8 {
public static void main(String[] args) {
Thread t=new Thread(()->{
while (true){
System.out.println("Thread");
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
},"这是线程");
t.start();
System.out.println(t.getId());
System.out.println(t.getName());
System.out.println(t.getPriority());
System.out.println(t.getState());
System.out.println(t.isDaemon());//是否是守护线程
System.out.println(t.isAlive());//是否存活
System.out.println(t.isInterrupted());//是否线程中断
}
}
5.线程中断
定于:线程中断本质上是让run方法尽快结束,而不是run执行一半强制结束(取决于run里面具体实现)
线程中断方式
- 定义一个标志位,作为线程是否结束的标记
在这里插入代码片
public class Demo9 {
public static boolean Quit=false;
public static void main(String[] args) throws InterruptedException {
Thread t=new Thread(()->{
while (!Quit){
System.out.println("Hello");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
System.out.println("线程执行完");
});
t.start();
Thread.sleep(5000);
Quit=true;
System.out.println("线程结束");
}
}
- 使用标准库自带的标志位
在这里插入代码片
public class Demo10 {
public static void main(String[] args) throws InterruptedException {
Thread t=new Thread(()->{
while (!Thread.currentThread().isInterrupted()){
System.out.println("Hello");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
System.out.println("线程执行完");
});
t.start();
Thread.sleep(3000);
t.interrupt();//通过它中断线程,设标志位为true
System.out.println("线程停止");
}
}
t.interrupt();方法行为有两种情况
-
t线程在运行状态,会设置,将它的标志位由原来flase改为true
-
t线程在阻塞状态,会触发异常,不会设置标志位(不是没设置,而是interrupt会设置标志位,但是sleep等这些阻塞方法会清除这个标志位,表面上看是没有设置标志位,实际上设置了**
代码只打印了日志,并没有结束循环,因此线程还在继续执行中,Java中中断线程并不是强制性的,可以有以下几种方法 -
立即结束线程,break
-
不理会
-
稍后处理sleep
6.线程等待join方法
线程之间调度顺序是不确定的,通过一些特殊操作,来对线程执行顺序进行干预
在这里插入代码片
public class Demo11 {
public static void main(String[] args) throws InterruptedException {
Thread t=new Thread(()->{
for (int i = 0; i < 5; i++) {
System.out.println("Thread");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
});
t.start();
System.out.println("main线程join之前");
t.join();
System.out.println("main线程join之后");
}
}
在main中调用了join,让main方法阻塞等待,等t执行完了,main函数继续执行
如果调用join之前,t线程已经结束了,main是不需要阻塞等待的
public void join(long millis) 等待线程结束,最多等 millis 毫秒,可以等待,但是是有时间限制,过期不候
7.sleep()指定休息时间,阻塞一会.
sleep将线程添加到阻塞队列里,当挪回就绪队列时,并不是立即去CPU上执行,还是得看系统啥时候调度到这个线程
七.线程状态
- NEW: 安排了工作, 还未开始行动 (Thread对象创建出来,内核PCB还未创建)
- RUNNABLE: 可工作的. 又可以分成正在工作中和即将开始工作. (cpu上运行+在就绪队列里面排队的)
- BLOCKED:这几个都表示排队等着其他事情 (等待锁)
- WAITING: 这几个都表示排队等着其他事情 (特殊阻塞状态,调用wait)
- TIMED_WAITING: 这几个都表示排队等着其他事情 (按照一定时间进行阻塞)
- TERMINATED: 工作完成了.(内核PCB销毁,但是Thread对象还在)