进程是操作系统中非常核心的一个概念,进程也叫做“任务”,一个运行起来的程序就称为进程,像QQ安装后是一个存储在磁盘的一个可执行程序(静态的),当双击QQ运行的时候操作系统就会把文件中的核心数据加载到内存里,同时在系统中生成一个进程,同时给进程分配一定的系统硬件资源(CPU、内存、磁盘、网络带宽等…),在任务管理器中就可以查看到。
同一时刻系统中运行的进程是有很多的,这么多的进程是如何被操作系统管理的呢?操作系统管理进程是通过描述+组织管理进程。
那么PCB里具体有哪些信息?(进程里面有哪些关键的要素)
pid:进程的身份标识,一个机器这些进程的pid是唯一的,通过pid来区分一个进程
内存指针:
文件描述符表
下面的这些属性都是和进程调度相关的
进程状态
进程的优先级
进程的上下文
进程的记账信息
电脑上有着几百个进程都在运行,但是电脑只有1个CPU,而且一般都是4核或者8核心的CPU,是不足以运行这么进程的。操作系统就采用了进程调度这样的机制来进行执行的。
并发执行
并发执行是指一个CPU运行多个进程,一个CPU先运行进程1、再运行进程2…,这样调度执行,虽然CPU在一直进行切换,但是在电脑前坐着的使用者是感受不到这个过程的。
并行执行
并行执行是指多个CPU运行着多个进程,比如CPU1运行进程1,CPU2运行进程2,进程1和进2无论是从微观还是宏观都是同时执行的,
一个进程想要运行,就需要给它分配一些系统资源,其中内存就是最核心的资源。
虚拟地址空间是指一个进程可用的地址空间,它是在进程被创建时由操作系统给出的,它是一种特殊的地址空间,它使得每个进程都可以访问自己的一块独立的内存空间,而不需要关心实际的物理地址。
MMU是计算机硬件中用于管理虚拟内存和物理内存之间映射的芯片,MMU通过将虚拟地址从CPU发出的程序地址装换为物理地址,来管理内存和提供进程之间的保护。
也就是我们访问的内存是虚拟内存而不是真实的物理内存,MMU会对我们的内存访问进行校验,判断是否越界访问,只有合法访问才能正常访问内存,如果越界就会MMU就会给操作系统发送异常信息。
通过虚拟地址空间,操作系统可以管理活跃的进程和内存,同时也提供了更好的保护机制,从而确保系统的安全性和可靠性。
由于进程之间相互隔离,进程间的通讯又是一个新的问题。可以使用文件或者socket等两个进程都可以访问的公共资源。
线程就是一个“执行流”,可以理解为线程是一个“轻量级进程”。虽然进程已经可以实现“并发编程”,但是频繁创建和销毁进程,开销还是比较大的,引入多线程是对多进程程序的优化。
创建线程并没有向操作系统申请资源,销毁线程也不需要释放资源,线程是产生在进程内部,共用之前的资源。进程包含了线程,一个线程对应一个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
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);
}
方法 | 说明 |
---|---|
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中的线程状态其实主要是就绪状态和阻塞状态。
优先级,也是和"进程的优先级”是类似的效果,此处的状态和优先级,和内核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()
方法会在内核创建新的线程,也就是创建了新的PCB,此时代码就是多线程的方式执行,而如果直接调用的是run()
方法,就并不会在内核创建新的线程,也就是说此时代码是串行执行,和多线程没有任何关系。
public static void main(String[] args) {
Thread thread = new Thread() {
@Override
public void run() {
System.out.println("使用匿名内部类");
}
};
}
如果一个线程的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,就应该结束循环
线程之间的调度顺序,是不确定的。可以通过一些特殊的操作,来对线程的执行顺序,做出干预。其中
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 能够获取到当前线程对应的 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() 是一个类方法
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时间到了或者抛出异常了才会回到就绪队列中