一、进程与线程基本概念
进程和线程不是一开始就有的,而是随着需要才出现的概念。
1、进程产生的背景
最初计算机只能接受一些特定的指令,用户输入一个指令,计算机就做一个操作。但是用户输入的速度是远远低于计算机运算的速度的,所以计算机有大量时间是在等待用户的输入,也就是 CPU 一直处于空闲状态,CPU 的利用率非常低。
批处理操作系统
后来有了批处理操作系统,把一些系列的指令写成一个清单,一次性把这个清单交给计算机,然后计算机会逐行读取指令,并把结果输出到另外一个磁盘中。
批处理操作系统的出现虽然提高了计算机的效率,但是由于批处理操作系统的指令运行方式仍然是串行的,内存中始终只有一个程序在运行。这种批处理操作系统并不理想,因为在同一时刻,CPU 只能执行一个程序的指令,也就是内存中只能运行一个程序。
进程的提出
由于内存中只能运行一个程序,于是计算机科学家们提出了进程的概念。
进程就是应用程序在内存中分配的空间,也就是在内存中可以同时存在多个程序在运行,并且各个应用程序(进程)之间互不干扰。每一个进程都保存程序运行的状态。
程序:指能完成某些功能的代码集合。
在计算机中的 CPU 采用时间片的方式运行每一个进程。CPU 为每一个进程会分配一个时间片,如果时间片结束时进程还在运行,则暂停这个进程的运行,并且 CPU 分配给另外一个进程(这个过程就是上下文切换)。如果进程在时间片结束前阻塞或结束,则 CPU 立即进行切换,不用等待时间片用完。
需要注意 CPU 进行上下文切换是非常耗时的操作,因为要在进程切换前会保存当前进程的状态(进程的标识,进程使用的资源等),方便在下次获取到 CPU 时间片之后根据之前保存的状态进行恢复,接着继续执行。
使用 CPU时间片 + 进程的方式,在宏观上觉得在同一时间段执行了多个任务,那是因为 CPU 的运算速度太快了,所以看上去像是在并发处理任务。其实在在单核 CPU 来说,任意时刻只有一个程序在执行。
并发:在同一时间段,处理多个任务。
并行:在同一时刻,处理多个任务。
对操作系统的要求进一步提高
虽然进程的出现,使得操作系统的性能大大提升,但是随着时间的推移,人们并不满足一个进程在一段时间只能做一件事情,如果一个进程有多个子任务时,只能逐个得执行这些子任务,很影响效率。
比如:我们电脑上的杀毒软件,我们在扫描病毒的同时不能进行垃圾扫描。我们必须要等待病毒扫描完成之后,才能进行垃圾扫描。
线程的提出
我们能不能让这些子任务同时执行呢?于是线程的概念就被提出来了。线程是比进程更小的单位,一个程序就是一个进程,而一个进程里面可以包含一个或多个线程。
![image-20230405200232690](https://i-blog.csdnimg.cn/blog_migrate/5fef3bba3b989612ed582bc41128fe3e.png)
比如:我们电脑上的杀毒软件,可以一边进行病毒扫描,一边进行垃圾扫描。
进程让操作系统的并发性成为了可能,而线程让进程的内部并发成为了可能。
既然进程也可以实现并发,那为啥还要提出进程呢?
- 进程的通信比较复杂,数据不容易共享,不同进程之间有内存屏障。而线程方便数据的共享,也方便线程之间的通信。
- 进程是重量级的,切换进程的开销比较大,不但需要保存寄存器和栈信息,还要进行资源的分配回收以及页调度。而线程只需要保存寄存器和栈信息,开销比较小。
二、多线程
上面我们讲了进程和线程的来由。那么在 Java 中如何创建线程呢?
1、继承Thread类
class MyThread extends Thread {
@Override
public void run() {
System.out.println(Thread.currentThread().getName());
}
}
public class Demo04 {
public static void main(String[] args) {
MyThread thread1 = new MyThread();
thread1.setName("子线程1");
thread1.start();
MyThread thread2 = new MyThread();
thread2.setName("子线程2");
thread2.start();
System.out.println(Thread.currentThread().getName());
}
}
注意:同一个线程不能多次调用start()
方法,否则会报 IllegalThreadStateException
异常。
start() 源码:
private volatile int threadStatus = 0;
public synchronized void start() {
/**
* This method is not invoked for the main method thread or "system"
* group threads created/set up by the VM. Any new functionality added
* to this method in the future may have to also be added to the VM.
*
* A zero status value corresponds to state "NEW".
*/
if (threadStatus != 0)
throw new IllegalThreadStateException();
/* Notify the group that this thread is about to be started
* so that it can be added to the group's list of threads
* and the group's unstarted count can be decremented. */
group.add(this);
boolean started = false;
try {
start0();
started = true;
} finally {
try {
if (!started) {
group.threadStartFailed(this);
}
} catch (Throwable ignore) {
/* do nothing. If start0 threw a Throwable then
it will be passed up the call stack */
}
}
}
private native void start0();
分析源码发现threadStatus = 0
,当调用本地方法start0()
之后会改变threadStatus
的值会改变,因此第二次调用start()
方法时会报IllegalThreadStateException
异常。
2、实现Runnable接口
在上面使用继承 Thread 类的方式来创建线程,但是我们知道在 Java 中是单继承,多实现。我们要是一个类继承了 Thread 类就不能显示的继承其它类了,那如果我们现在既要创建新的线程任务,也要继承其它类,那么我们要如何实现呢?
在 Thread 的构造器中,支持传递一个 Runnable 接口的实现类的方式创建线程。
class MyCustomThread implements Runnable {
@Override
public void run() {
System.out.println(Thread.currentThread().getName());
}
}
public class Demo05 {
public static void main(String[] args) {
Thread thread = new Thread(new MyCustomThread());
thread.start();
}
}
我们还可以对以上的代码进行简化,比如写成匿名内部类对象的方式,还可以写成 lamdab 表达式的方式。
何为匿名内部类对象?
匿名内部类是指没有名字的内部类,它在声明时直接实现一个接口或继承自一个类,并且在使用时直接进行创建和实例化。匿名内部类在Java中非常常用,它可以简化代码的书写,使代码更为简洁,并且也可以使程序员在一定程度上隐藏代码的实现细节。
格式:
new 父类构造器<实参列表> implements 接口名<泛型参数列表>{ 外部类成员变量、方法;[内部类成员变量] [内部类方法]}
在 JDK 8 之后,如果不修改外部变量,可以直接访问,不用加 final 修饰。
public class OuterClass { public void myMethod() { final int x = 3; // 将x声明为final new Thread(new Runnable() { @Override public void run() { x++; // 编译错误,无法修改x的值 System.out.println(x); } }).start(); } }
匿名内部类对象:
public class Demo05 {
public static void main(String[] args) {
Thread thread = new Thread(new Runnable {
@Override
public void run() {
System.out.println(Thread.currentThread().getName());
}
});
thread.start();
}
}
要区分匿名对象,匿名内部类对象,内部类三者的区别。
匿名对象:没有给创建的对象起一个名字。
例如:
new MyThread()
。内部类:在一个类的内部定义的类。
例如:
class OutterClass { // 外部类成员 class InnerClass { // 内部类成员 } }
匿名内部类对象:没有给内部类的实例起名字。
我们还可以进一步简化代码,写成 lamdab 表达式。
public class Demo05 {
public static void main(String[] args) {
new Thread(() -> System.out.println(Thread.currentThread().getName())).start();
}
}
如果对 lamdab 不熟悉的同学,可以去网上搜索学习。
要使用 lamdab,使用的 JDK 版本要求大于等于8。
更多内容欢迎访问我的博客。