文章目录
- 多线程
- 1.进程
- 进程属性
- 并发和并行
- 虚拟地址空间
- 2.线程
- 概念
- 线程的创建方式
- 3.Thread类
- 常见构造方法和属性
- 线程的状态
- 优先级
- 后台线程
- 线程是否存活
- start和run
- 4. 线程的一些基本操作
- 线程中断(interrupted)
- 线程等待join
- currentThread(获取当前线程引用)
- 线程休眠sleep
多线程
1.进程
进程是操作系统中非常核心的一个概念,进程也叫做“任务”,一个运行起来的程序就称为进程,像QQ安装后是一个存储在磁盘的一个可执行程序(静态的),当双击QQ运行的时候操作系统就会把文件中的核心数据加载到内存里,同时在系统中生成一个进程,同时给进程分配一定的系统硬件资源(CPU、内存、磁盘、网络带宽等…),在任务管理器中就可以查看到。
进程属性
同一时刻系统中运行的进程是有很多的,这么多的进程是如何被操作系统管理的呢?操作系统管理进程是通过描述+组织管理进程。
- 描述:详细的描述清楚一个进程有哪些属性/信息,操作系统通过一个PCB来描述一个进程。
- PCB:也叫进程的控制块,它是C语言的一个结构体,一个结构体对象就对应着一个进程
- 一个进程可能是一个PCB,也可能对应多个
- 组织:通过一定的数据结构,把若干个用来描述的实体,给放到一起,并且进行增删改查。
- 系统中通常会使用双向链表这样的结构来把这些PCB给组织在一起
- 创建一个进程,本质就是创建PCB,并且加入到链表上
- 销毁一个进程,本质上就是从链表上删除对应的PCB节点
- 查看任务管理器的进程列表,本质上就是在遍历这个链表
那么PCB里具体有哪些信息?(进程里面有哪些关键的要素)
-
pid:进程的身份标识,一个机器这些进程的pid是唯一的,通过pid来区分一个进程
-
内存指针:
- 一个可执行文件,双击后开始在内存中运行,操作系统把文件中的核心数据(要执行的指令、指令依赖的数据)加载到内存中
- 既然要创建进程,就要给进程分配内存空间,然后在这个内存空间上就有很多区域
- 内存指针就是指向进程持有的内存资源,在程序关闭时也方便释放内存资源。
-
文件描述符表
- 每个进程都可以打开一些文件(文件其实就是存在硬盘上的数据)
- 文件描述符表里面就记录了当前进程都打开了哪些文件(打开了之后就可以后续针对这些文件进行读写操作了)
下面的这些属性都是和进程调度相关的
-
进程状态
- 运行状态:进程正在CPU上运行
- 就绪状态:进程已经做好准备,随时准备被CPU调度执行
- 阻塞状态:进程在此状态下不能执行,只有等阻塞该进程的事假完成之后才能执行
-
进程的优先级
- 系统调度的时候,会根据优先级来给进程安排运行时间
- 进程优先级越高就越容易被CPU调度执行
- 创建进程的时候,可以通过一些系统调用来干预优先级
-
进程的上下文
- 进程在CPU上执行了一会之后,要切换给别的进程,就需要保存当前运行的中间结果(类似存档),下次进程再被调度执行的时候,恢复到之前的中间结果(类似读档),继续往下执行
- 对于进程来说,上下文就是CPU中的寄存器的值(寄存器的值就包含了运行的中间结果,需要把这这写结果保存到PCB的上下文信息中(内存))
- 进程的上下文主要是存储调度出CPU之前,寄存器中的信息(把寄存器信息保存到内存中),等到这个进程下次恢复到CPU上执行的时候,就把内存中保存好的数据恢复到寄存器中
-
进程的记账信息
- 记账信息主要是记录进程在CPU上执行多久了,用来辅助决定这个进程是继续执行,还是要被调度出CPU了
- 通过进程记账信息就可以让进程运行更加均衡,避免有进程完全到不了CPU上执行
并发和并行
电脑上有着几百个进程都在运行,但是电脑只有1个CPU,而且一般都是4核或者8核心的CPU,是不足以运行这么进程的。操作系统就采用了进程调度这样的机制来进行执行的。
并发执行
并发执行是指一个CPU运行多个进程,一个CPU先运行进程1、再运行进程2…,这样调度执行,虽然CPU在一直进行切换,但是在电脑前坐着的使用者是感受不到这个过程的。
并行执行
并行执行是指多个CPU运行着多个进程,比如CPU1运行进程1,CPU2运行进程2,进程1和进2无论是从微观还是宏观都是同时执行的,
虚拟地址空间
一个进程想要运行,就需要给它分配一些系统资源,其中内存就是最核心的资源。
虚拟地址空间是指一个进程可用的地址空间,它是在进程被创建时由操作系统给出的,它是一种特殊的地址空间,它使得每个进程都可以访问自己的一块独立的内存空间,而不需要关心实际的物理地址。
MMU是计算机硬件中用于管理虚拟内存和物理内存之间映射的芯片,MMU通过将虚拟地址从CPU发出的程序地址装换为物理地址,来管理内存和提供进程之间的保护。
也就是我们访问的内存是虚拟内存而不是真实的物理内存,MMU会对我们的内存访问进行校验,判断是否越界访问,只有合法访问才能正常访问内存,如果越界就会MMU就会给操作系统发送异常信息。
通过虚拟地址空间,操作系统可以管理活跃的进程和内存,同时也提供了更好的保护机制,从而确保系统的安全性和可靠性。
由于进程之间相互隔离,进程间的通讯又是一个新的问题。可以使用文件或者socket等两个进程都可以访问的公共资源。
2.线程
概念
线程就是一个“执行流”,可以理解为线程是一个“轻量级进程”。虽然进程已经可以实现“并发编程”,但是频繁创建和销毁进程,开销还是比较大的,引入多线程是对多进程程序的优化。
- 创建线程比创建进程更加高效
- 销毁线程比销毁进程更加高效
- 调度线程比调度进程更加高效
- 同一个进程中的这些线程之间,共用同一份系统资源(内存+文件描述符表)
创建线程并没有向操作系统申请资源,销毁线程也不需要释放资源,线程是产生在进程内部,共用之前的资源。进程包含了线程,一个线程对应一个PCB,一个进程对应一组PCB(内存指针和文件描述符表,都是一份,但状态、优先级、记账信息、上下文、每个线程都有独立的)。进程是操作系统分配资源的基本单位,线程是调度执行的基本单位。
创建线程其实就是在内核里创建了PCB
线程的创建方式
1.继承Thread类重写run方法
public class ThreadDemo {
static class MyThread extends Thread {
@Override
public void run() {
System.out.println("继承Thread重写run方法");
}
}
public static void main(String[] args) {
Thread thread = new MyThread();
thread.start();
}
}
2.实现Runnable接口,重写run方法
public class ThreadDemo {
static class MyRunnable implements Runnable {
@Override
public void run() {
System.out.println("实现Runnable接口重写run方法");
}
}
public static void main(String[] args) {
Thread thread = new Thread(new MyRunnable());
thread.start();
}
}
3.使用Thread匿名内部类
public static void main(String[] args) {
Thread thread = new Thread() {
@Override
public void run() {
System.out.println("使用匿名内部类");
}
};
thread.start();
}
4.使用Runnable匿名内部类
public static void main(String[] args) {
Thread thread1 = new Thread(new Runnable() {
@Override
public void run() {
System.out.println("Runnable匿名内部类");
}
});
thread1.start();
}
5.使用lambda表达式
public static void main(String[] args) {
Thread thread2 = new Thread(()->{
System.out.println("使用lambda表达式");
});
thread2.start();
}
6.使用Callable+FutureTak
- Callable和Runnable类似都是描述了一个过程,只不过Callable带有有返回值。Callable的泛型参数就是返回值
- Callable中包含call()方法,和Runnable的run()方法类似,不过call()方法是带有返回值的
- 通过FutureTask的get()方法来获取Callable的返回值,如果此时还没有获取到返回值,该方法就会阻塞.
public static void main(String[] args) throws ExecutionException, InterruptedException {
//这是一个能有返回值的线程,也是一个接口
Callable<Integer> callable = new Callable<Integer>() {
@Override
public Integer call() throws Exception {
//Thread.sleep(4000);
int sum = 0;
for (int i = 0; i <= 100000; i++) {
sum+=i;
}
return sum;
}
};
//通过 FutureTask 来接收 Callable的返回值
FutureTask<Integer> futureTask = new FutureTask<>(callable);
Thread t = new Thread(futureTask);
//调用t.start() 就会执行 FutureTask() 内部的 call 方法,完成计算,计算结果就会返回到 FutureTask对象中
t.start();
System.out.println("hhh");
//调用FutureTask的 get 方法就能获取到结果
//如果FutureTask没有接受到值就会阻塞等待
int tmp = futureTask.get();
System.out.println(tmp);
}
3.Thread类
常见构造方法和属性
方法 | 说明 |
---|---|
Thread() | 创建线程对象 |
Thread(Runnable target) | 使用 Runnable对象创建线程 |
Thread(String name) | 创建线程对象,并命名 |
Thread(Runnable target, String name) | 使用 Runnable 对象创建线程对象,并命名 |
Thread(ThreadGroup group,Runnable target) | 线程可以被用来分组管理,分好的组即为线程组 |
Thread常用方法
属性 | 获取的方法 |
---|---|
线程Id | getId() |
名称 | getName() |
状态 | getState() |
优先级 | getPriority() |
是否后台线程 | isDaemon() |
是否存活 | isAlive() |
是否被中断 | isInterrupted() |
获取当前线程对象 | Thread.currentThread() |
线程的状态
Java中线程的状态是和操作系统的状态不一样,这是Java自己的一套线程状态。Java中的线程状态其实主要是就绪状态和阻塞状态。
- NEW:Thread对象创建出来来,但是内核的PCB还没有创建(还没有真正创建线程)
- TERMINATED:内核的PCB销毁了,但是Thread对象还在
- RUNNABlE:就绪状态(线程正在CPU上运行或者是在就绪队列中排队)
- TIMED_WATING:安装一定的时间进行阻塞,sleep或者其它指定时间阻塞
- WAITING:特殊的阻塞状态,调用wait时
- BLOCKED:等待锁的时候进入的阻塞状态
优先级
优先级,也是和"进程的优先级”是类似的效果,此处的状态和优先级,和内核PCB中的状态优先级并不完全一致。
后台线程
关于后台线程(守护线程),我们创建的线程默认都是“前台线程”,前台线程会阻止进程退出,如果main运行完了,前台线程还没有执行完毕,进程是不会退出的。
如果是后台线程,后台线程是不阻止进程退出的,如果main等其他的前台线程执行完了,这个时候,即使后台线程没有执行完,进程也会退出。
线程是否存活
判断一个线程是否存活,最简单的方法就是看run方法是否已经结束。
public static void main(String[] args) {
Thread thread = new Thread() {
@Override
public void run() {
System.out.println("使用匿名内部类");
}
};
thread.start();
//下面还有一些逻辑
//......
}
比如上述代码,run方法执行完毕后,其实线程就销毁了,但是由于Thread对象是靠JVM的GC来进行销毁的,所以它和内核的线程生命周期是不一样的,它会比内核的线程存活时间更长,所以此时就可以使用isAlive()
方法来判断线程是否存活。
start和run
start()
方法会在内核创建新的线程,也就是创建了新的PCB,此时代码就是多线程的方式执行,而如果直接调用的是run()
方法,就并不会在内核创建新的线程,也就是说此时代码是串行执行,和多线程没有任何关系。
public static void main(String[] args) {
Thread thread = new Thread() {
@Override
public void run() {
System.out.println("使用匿名内部类");
}
};
}
4. 线程的一些基本操作
线程中断(interrupted)
如果一个线程的run方法执行完了,线程就已经结束了,但实际应用中可能是一没那么快结束,甚至可能是一个死循环,那么想让线程结束就需要用到线程中断了。
-
直接定义一个变量作为一个标记位判断线程是否结束(并不推荐)
public class Interrupted { private static boolean FLAG = true; public static void main(String[] args) throws InterruptedException { Thread thread = new Thread(()->{ while (FLAG) { System.out.println("test"); try { Thread.sleep(1000); } catch (InterruptedException e) { e.printStackTrace(); } } }); thread.start(); Thread.sleep(5000); FLAG = false; } }
-
使用标准库中的标记位
方法 说明 public void interrupt() 中断对象关联线程,如果线程正在阻塞,则以异常方式通知,否则设置标记位 public static boolean interrupted() 判断当前线程的中断标志位是否设置,调用后清除标志位(默认返回false) public boolean isInterrupted() 判断对象关联的线程的标志位是否设置,调用后不清除标志位(默认返回false) 代码示例:
public static void demo2() throws InterruptedException { Thread thread = new Thread(()->{ while (!Thread.interrupted()) { try { Thread.sleep(1000); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println("test"); } }); thread.start(); Thread.sleep(5000); thread.interrupt(); }
interrupt()
方法本来是会把isInterrupted()
的标志位修改为ture的,但这段代码在阻塞就会抛出一个异常,且线程不会停止循环继续运行。这里的 interrupt 方法有两种行为
1.如果当前线程正在运行中,此时就会修改 Thread.islnterruppted() 标记位为 true
2.如果当前线程 正在 sleep、wait、等待锁,此时就会触发 InterruptedException
如果要结束循环在catch加上brak即可
public static void demo2() throws InterruptedException { Thread thread = new Thread(()->{ while (!Thread.currentThread().isInterrupted()) { try { Thread.sleep(1000); } catch (InterruptedException e) { e.printStackTrace(); break; } System.out.println("test"); } }); thread.start(); Thread.sleep(5000); thread.interrupt(); }
还有一个静态的方法
interrupted()也是标志位
public static void demo3() throws InterruptedException { Thread thread = new Thread(()->{ while (!Thread.interrupted()) { try { Thread.sleep(1000); } catch (InterruptedException e) { e.printStackTrace(); break; } System.out.println("test"); } }); thread.start(); Thread.sleep(5000); thread.interrupt(); }
那么interrupted()
和isInterrupted()
方法有什么区别呢?
列如:调用 interrupt() 方法,把标记位设为 true,就应该结束循环
- 当调用 静态的 interrupted 来判定标记位的时候,就会返回 true,同时就会把标记位再改回 false,下次再调用interrupted() 就返回 false
- 如果是调用非静态的 isInterrupted() 来判断标记位,也会返回 true,但不会对标记位进行修改,后面再调用isInterrupted() 的时候仍然返回 true
线程等待join
线程之间的调度顺序,是不确定的。可以通过一些特殊的操作,来对线程的执行顺序,做出干预。其中
join就是一个办法,控制线程之间的结束顺序。
比如这里在main方法里调用join的效果就是等thread线程的代码执行完毕后才继续执行main方法里的逻辑,此时main方法的线程就进入阻塞状态,不参与cpu调度。
当然根据需要join可以设置指定时间的等待,正常使用一般不会死等的。
public static void main(String[] args) throws InterruptedException {
Thread thread = new Thread(()->{
for (int i = 0; i < 5; i++) {
System.out.println("线程执行中...");
try {
Thread.sleep(500);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
});
thread.start();
thread.join();
System.out.println("线程执行结束");
}
currentThread(获取当前线程引用)
currentThread 能够获取到当前线程对应的 Thread 实例的引用,相当于 this关键字
public static void demo() {
Thread thread = new Thread(){
@Override
public void run() {
System.out.println(this.getId());
System.out.println(Thread.currentThread().getId());
}
};
}
但是需要注意的是,如果是使用 Runnable 或者 lambda 的方式来创建的线程,就无法使用 this 了。
this指向的是 Runnable 实例,而不是Thread 实例了,此时也就没有 getId 方法了。
public static void demo1() {
Thread thread = new Thread(new Runnable() {
@Override
public void run() {
System.out.println(Thread.currentThread().getId());
//System.out.println(this.getId); 错误写法
}
});
}
线程休眠sleep
也是我们比较熟悉一组方法,有一点要记得,因为线程的调度是不可控的,所以,这个方法只能保证实际休眠时间是大于等于参数设置的休眠时间的
通过 sleep() 方法来休眠一个线程,sleep() 是一个类方法
public static void main(String[] args) {
Thread t = new Thread(new Runnable() {
@Override
public void run() {
try {
//休眠1秒
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
});
}
Sleep 这个方法,本质上就是把线程PCB给从就绪队列,移动到了阻塞队列,只有当 Sleep时间到了或者抛出异常了才会回到就绪队列中