mirror of
https://github.com/Snailclimb/JavaGuide
synced 2025-07-20 03:13:00 +08:00
197 lines
12 KiB
Markdown
197 lines
12 KiB
Markdown
|
||
## 1. 简述线程,程序、进程的基本概念。以及他们之间关系是什么?
|
||
|
||
**线程**与进程相似,但线程是一个比进程更小的执行单位。一个进程在其执行的过程中可以产生多个线程。与进程不同的是同类的多个线程共享同一块内存空间和一组系统资源,所以系统在产生一个线程,或是在各个线程之间作切换工作时,负担要比进程小得多,也正因为如此,线程也被称为轻量级进程。
|
||
|
||
**程序**是含有指令和数据的文件,被存储在磁盘或其他的数据存储设备中,也就是说程序是静态的代码。
|
||
|
||
**进程**是程序的一次执行过程,是系统运行程序的基本单位,因此进程是动态的。系统运行一个程序即是一个进程从创建,运行到消亡的过程。简单来说,一个进程就是一个执行中的程序,它在计算机中一个指令接着一个指令地执行着,同时,每个进程还占有某些系统资源如CPU时间,内存空间,文件,文件,输入输出设备的使用权等等。换句话说,当程序在执行时,将会被操作系统载入内存中。
|
||
|
||
**线程** 是 **进程** 划分成的更小的运行单位。线程和进程最大的不同在于基本上各进程是独立的,而各线程则不一定,因为同一进程中的线程极有可能会相互影响。从另一角度来说,进程属于操作系统的范畴,主要是同一段时间内,可以同时执行一个以上的程序,而线程则是在同一程序内几乎同时执行一个以上的程序段。
|
||
|
||
**线程上下文的切换比进程上下文切换要快很多**
|
||
|
||
- 进程切换时,涉及到当前进程的CPU环境的保存和新被调度运行进程的CPU环境的设置。
|
||
- 线程切换仅需要保存和设置少量的寄存器内容,不涉及存储管理方面的操作。
|
||
|
||
## 2. 线程有哪些基本状态?这些状态是如何定义的?
|
||
|
||
Java 线程在运行的生命周期中的指定时刻只可能处于下面 6 种不同状态的其中一个状态(图源《Java 并发编程艺术》4.1.4 节)。
|
||
|
||

|
||
|
||
线程在生命周期中并不是固定处于某一个状态而是随着代码的执行在不同状态之间切换。Java 线程状态变迁如下图所示(图源《Java 并发编程艺术》4.1.4 节):
|
||
|
||

|
||
|
||
|
||
|
||
由上图可以看出:线程创建之后它将处于 **NEW(新建)** 状态,调用 `start()` 方法后开始运行,线程这时候处于 **READY(可运行)** 状态。可运行状态的线程获得了 CPU 时间片(timeslice)后就处于 **RUNNING(运行)** 状态。
|
||
|
||
> 操作系统隐藏 Java 虚拟机(JVM)中的 RUNNABLE 和 RUNNING 状态,它只能看到 RUNNABLE 状态(图源:[HowToDoInJava](https://howtodoinjava.com/):[Java Thread Life Cycle and Thread States](https://howtodoinjava.com/java/multi-threading/java-thread-life-cycle-and-thread-states/)),所以 Java 系统一般将这两个状态统称为 **RUNNABLE(运行中)** 状态 。
|
||
|
||

|
||
|
||
当线程执行 `wait()`方法之后,线程进入 **WAITING(等待)**状态。进入等待状态的线程需要依靠其他线程的通知才能够返回到运行状态,而 **TIME_WAITING(超时等待)** 状态相当于在等待状态的基础上增加了超时限制,比如通过 `sleep(long millis)`方法或 `wait(long millis)`方法可以将 Java 线程置于 TIMED WAITING 状态。当超时时间到达后 Java 线程将会返回到 RUNNABLE 状态。当线程调用同步方法时,在没有获取到锁的情况下,线程将会进入到 **BLOCKED(阻塞)** 状态。线程在执行 Runnable 的` run() `方法之后将会进入到 **TERMINATED(终止)** 状态。
|
||
|
||
## 3. 何为多线程?
|
||
|
||
多线程就是多个线程同时运行或交替运行。单核CPU的话是顺序执行,也就是交替运行。多核CPU的话,因为每个CPU有自己的运算器,所以在多个CPU中可以同时运行。
|
||
|
||
|
||
## 4. 为什么要使用多线程?
|
||
|
||
先从总体上来说:
|
||
|
||
- **从计算机底层来说:**线程可以比作是轻量级的进程,是程序执行的最小单位,线程间的切换和调度的成本远远小于进程。另外,多核 CPU 时代意味着多个线程可以同时运行,这减少了线程上下文切换的开销。
|
||
- **从当代互联网发展趋势来说:**现在的系统动不动就要求百万级甚至千万级的并发量,而多线程并发编程正是开发高并发系统的基础,利用好多线程机制可以大大提高系统整体的并发能力以及性能。
|
||
|
||
再深入到计算机底层来探讨:
|
||
|
||
- **单核时代:** 在单核时代多线程主要是为了提高 CPU 和 IO 设备的综合利用率。举个例子:当只有一个线程的时候会导致 CPU 计算时,IO 设备空闲;进行 IO 操作时,CPU 空闲。我们可以简单地说这两者的利用率目前都是 50%左右。但是当有两个线程的时候就不一样了,当一个线程执行 CPU 计算时,另外一个线程可以进行 IO 操作,这样两个的利用率就可以在理想情况下达到 100%了。
|
||
- **多核时代:** 多核时代多线程主要是为了提高 CPU 利用率。举个例子:假如我们要计算一个复杂的任务,我们只用一个线程的话,CPU 只会一个 CPU 核心被利用到,而创建多个线程就可以让多个 CPU 核心被利用到,这样就提高了 CPU 的利用率。
|
||
## 5 使用多线程常见的三种方式
|
||
|
||
### ①继承Thread类
|
||
|
||
MyThread.java
|
||
|
||
```java
|
||
public class MyThread extends Thread {
|
||
@Override
|
||
public void run() {
|
||
super.run();
|
||
System.out.println("MyThread");
|
||
}
|
||
}
|
||
```
|
||
Run.java
|
||
|
||
```java
|
||
public class Run {
|
||
|
||
public static void main(String[] args) {
|
||
MyThread mythread = new MyThread();
|
||
mythread.start();
|
||
System.out.println("运行结束");
|
||
}
|
||
|
||
}
|
||
|
||
```
|
||
运行结果:
|
||

|
||
从上面的运行结果可以看出:线程是一个子任务,CPU以不确定的方式,或者说是以随机的时间来调用线程中的run方法。
|
||
|
||
### ②实现Runnable接口
|
||
推荐实现Runnable接口方式开发多线程,因为Java单继承但是可以实现多个接口。
|
||
|
||
MyRunnable.java
|
||
|
||
```java
|
||
public class MyRunnable implements Runnable {
|
||
@Override
|
||
public void run() {
|
||
System.out.println("MyRunnable");
|
||
}
|
||
}
|
||
```
|
||
|
||
Run.java
|
||
|
||
```java
|
||
public class Run {
|
||
|
||
public static void main(String[] args) {
|
||
Runnable runnable=new MyRunnable();
|
||
Thread thread=new Thread(runnable);
|
||
thread.start();
|
||
System.out.println("运行结束!");
|
||
}
|
||
|
||
}
|
||
```
|
||
运行结果:
|
||

|
||
|
||
### ③使用线程池
|
||
|
||
**在《阿里巴巴Java开发手册》“并发处理”这一章节,明确指出线程资源必须通过线程池提供,不允许在应用中自行显示创建线程。**
|
||
|
||
**为什么呢?**
|
||
|
||
> **使用线程池的好处是减少在创建和销毁线程上所消耗的时间以及系统资源开销,解决资源不足的问题。如果不使用线程池,有可能会造成系统创建大量同类线程而导致消耗完内存或者“过度切换”的问题。**
|
||
|
||
**另外《阿里巴巴Java开发手册》中强制线程池不允许使用 Executors 去创建,而是通过 ThreadPoolExecutor 的方式,这样的处理方式让写的同学更加明确线程池的运行规则,规避资源耗尽的风险**
|
||
|
||
> Executors 返回线程池对象的弊端如下:
|
||
>
|
||
> - **FixedThreadPool 和 SingleThreadExecutor** : 允许请求的队列长度为 Integer.MAX_VALUE,可能堆积大量的请求,从而导致OOM。
|
||
> - **CachedThreadPool 和 ScheduledThreadPool** : 允许创建的线程数量为 Integer.MAX_VALUE ,可能会创建大量线程,从而导致OOM。
|
||
|
||
对于线程池感兴趣的可以查看我的这篇文章:[《Java多线程学习(八)线程池与Executor 框架》](http://mp.weixin.qq.com/s?__biz=MzU4NDQ4MzU5OA==&mid=2247484042&idx=1&sn=541dbf2cb969a151d79f4a4f837ee1bd&chksm=fd9854ebcaefddfd1876bb96ab218be3ae7b12546695a403075d4ed22e5e17ff30ebdabc8bbf#rd) 点击阅读原文即可查看到该文章的最新版。
|
||
|
||
|
||
## 6 线程的优先级
|
||
|
||
每个线程都具有各自的优先级,**线程的优先级可以在程序中表明该线程的重要性,如果有很多线程处于就绪状态,系统会根据优先级来决定首先使哪个线程进入运行状态**。但这个并不意味着低
|
||
优先级的线程得不到运行,而只是它运行的几率比较小,如垃圾回收机制线程的优先级就比较低。所以很多垃圾得不到及时的回收处理。
|
||
|
||
**线程优先级具有继承特性。** 比如A线程启动B线程,则B线程的优先级和A是一样的。
|
||
|
||
**线程优先级具有随机性。** 也就是说线程优先级高的不一定每一次都先执行完。
|
||
|
||
Thread类中包含的成员变量代表了线程的某些优先级。如**Thread.MIN_PRIORITY(常数1)**,**Thread.NORM_PRIORITY(常数5)**,
|
||
**Thread.MAX_PRIORITY(常数10)**。其中每个线程的优先级都在**Thread.MIN_PRIORITY(常数1)** 到**Thread.MAX_PRIORITY(常数10)** 之间,在默认情况下优先级都是**Thread.NORM_PRIORITY(常数5)**。
|
||
|
||
学过操作系统这门课程的话,我们可以发现多线程优先级或多或少借鉴了操作系统对进程的管理。
|
||
|
||
|
||
## 7 Java多线程分类
|
||
|
||
### 用户线程
|
||
|
||
运行在前台,执行具体的任务,如程序的主线程、连接网络的子线程等都是用户线程
|
||
|
||
### 守护线程
|
||
|
||
运行在后台,为其他前台线程服务.也可以说守护线程是JVM中非守护线程的 **“佣人”**。
|
||
|
||
|
||
- **特点:** 一旦所有用户线程都结束运行,守护线程会随JVM一起结束工作
|
||
- **应用:** 数据库连接池中的检测线程,JVM虚拟机启动后的检测线程
|
||
- **最常见的守护线程:** 垃圾回收线程
|
||
|
||
|
||
**如何设置守护线程?**
|
||
|
||
可以通过调用 Thead 类的 `setDaemon(true)` 方法设置当前的线程为守护线程。
|
||
|
||
注意事项:
|
||
|
||
1. setDaemon(true)必须在start()方法前执行,否则会抛出IllegalThreadStateException异常
|
||
2. 在守护线程中产生的新线程也是守护线程
|
||
3. 不是所有的任务都可以分配给守护线程来执行,比如读写操作或者计算逻辑
|
||
|
||
|
||
## 8 sleep()方法和wait()方法简单对比
|
||
|
||
- 两者最主要的区别在于:**sleep方法没有释放锁,而wait方法释放了锁** 。
|
||
- 两者都可以暂停线程的执行。
|
||
- Wait通常被用于线程间交互/通信,sleep通常被用于暂停执行。
|
||
- wait()方法被调用后,线程不会自动苏醒,需要别的线程调用同一个对象上的notify()或者notifyAll()方法。sleep()方法执行完成后,线程会自动苏醒。
|
||
|
||
|
||
## 9 为什么我们调用start()方法时会执行run()方法,为什么我们不能直接调用run()方法?
|
||
|
||
这是另一个非常经典的java多线程面试问题,而且在面试中会经常被问到。很简单,但是很多人都会答不上来!
|
||
|
||
new一个Thread,线程进入了新建状态;调用start()方法,会启动一个线程并使线程进入了就绪状态,当分配到时间片后就可以开始运行了。
|
||
start()会执行线程的相应准备工作,然后自动执行run()方法的内容,这是真正的多线程工作。 而直接执行run()方法,会把run方法当成一个main线程下的普通方法去执行,并不会在某个线程中执行它,所以这并不是多线程工作。
|
||
|
||
**总结: 调用start方法方可启动线程并使线程进入就绪状态,而run方法只是thread的一个普通方法调用,还是在主线程里执行。**
|
||
|
||
|
||
|
||
|