前言
本文主要讲解Java程序中进程与线程的相关的基础面试知识点,将重要知识进行整理,并通过直观的Demo和图示进行讲解。关于Java进程和线程更深层次的原理,我将在下一篇中进行讲解。
进程与线程
进程:进程独占内存空间,保存各自运行状态,相互间不干扰可以互相切换,为并发处理任务提供了可能。
线程:共享进程的内存资源,相互间切换更快速,支持更细粒度的任务控制,使进程内的子任务得以并发执行。
进程和线程的区别
- 进程是资源分配的最小单位,线程是CPU调度的最小单位。
- 所有进程相关的资源都被记录在PCB(进程控制块)中。
- 进程是抢占处理机的调度单位;线程属于某个进程,共享其资源。
- 线程只由堆栈寄存器、程序计数器和TCB(线程控制块)组成。
- 线程不能看作独立应用,而进程可以看作独立应用。
- 进程有独立的地址空间,相互不影响,线程只是进程的不同执行路径。
- 线程没有独立地址空间,多进程的程序比多线程程序健壮。
- 进程的切换比线程切换开销大。
Java中进程和线程的关系
- Java对操作系统提供的功能进行封装,包括进程和线程。
- 每运行一个程序产生一个进程,进程包含至少一个线程。
- 每个进程对应一个JVM实例,多个线程共享JVM里的堆。
- Java采用单线程编程模型,程序会自动创建主线程。
- 主线程可以创建子线程,原则上要后于子线程完成执行。
线程的状态
线程有六种状态:
- 新建(New):创建后尚未启动的线程的状态。
- 运行(Runnable):Runnable包含Running和Ready两个子状态。处于此状态的线程,有可能正在被执行,也有可能等待CPU为其分配执行时间。处于Running状态的线程位于可运行线程中等待被线程调度选中,获取CPU的使用权,处于Ready状态的线程位于线程池中。处于Ready状态的线程在获得CPU时间后会转换为Running状态。
- 无限等待(Waiting):不会被分配CPU时间,需要显示被唤醒。以下两种情况会让线程位于无限等待状态:
- 没有设置
Timeout
参数的Object.wait()
方法。 - 没有设置
Timeout
参数的Thread.join()
方法。 - 调用了
LockSupport.part()
方法。
- 没有设置
- 限期等待(Timed Waiting):在一定时间后会由系统自动唤醒。
Thread.sleep()
方法- 设置了
Timeout
参数的Object.wait()
方法。 - 设置了
Timeout
参数的Thread.join()
方法。 LockSupport.parkNanos()
方法。LockSupport.parkUntil()
方法。
- 阻塞(Blocked):等待获取排它锁。例如使用
synchronized
关键字修饰的线程在被其他线程访问时就会阻塞。 - 结束(Terminated):已终止线程的状态,线程已经结束执行。
Thread中的start和run方法的区别
- 调用
start()
方法会创建一个新的子线程并启动。 run()
方法只是Thread的一个普通方法的调用,并不会创建新的线程。
从JVM源码层面上分析,调用Thread中的start()方法时,会调用JVM中的JVM_StartThread
方法创建一个新的子线程,该方法又会调用thread_entry
方法,thread_entry会执行call_virtual
调用虚拟机,最终调用run()
方法中的内容。
Thread和Runnable是什么关系
- Thread是实现了Runnable接口的类,使得run方法支持多线程
- 因类的单一继承原则,推荐多使用Runnable接口。这样能够赋予子类较高的拓展性,便于后续将普通类升级成多线程。
如何给run()方法传参
- 构造函数传参
- 成员变量传参
- 回调函数传参
如何实现处理线程的返回值
在JDK1.5之前,线程是没有返回值的。开发者需要需要返回值时很不方便,需要使用其他方法或干脆不使用多线程。
-
主线程等待法:在主线程中使用while循环直到返回值不为空。这样做的缺点也很明显,我们需要手动去编写循环的逻辑,并且我们无法知道线程执行完成的具体时间,设置的
sleep
时长不精确。并且,当成员变量多的时候,我们的程序会显得异常臃肿无法维护。 -
使用Thread类的
join()
方法阻塞当前线程以等待子线程处理完毕。这样做的缺点是粒度不够细,无法精确控制成员之间的依赖关系。 -
通过Callable接口实现
-
FutureTask
Demo:
MyCallable.java
package com.yeliheng.threads; import java.util.concurrent.Callable; public class MyCallable implements Callable<String> { /** * Computes a result, or throws an exception if unable to do so. * * @return computed result * @throws Exception if unable to compute a result */ @Override public String call() throws Exception { String value = "test"; Thread.sleep(5000); return value; } }
FutureTaskTest.java
package com.yeliheng.threads; import java.util.concurrent.ExecutionException; import java.util.concurrent.FutureTask; public class FutureTaskTest { public static void main(String[] args) throws ExecutionException, InterruptedException { FutureTask<String> futureTask = new FutureTask<>(new MyCallable()); new Thread(futureTask).start(); if(!futureTask.isDone()) { System.out.println("线程正在执行..."); } System.out.println("返回值:" + futureTask.get()); } }
-
线程池
Demo:
MyCallable.java与第一点内容完全相同。
ThreadPoolTest.java
package com.yeliheng.threads; import java.util.concurrent.ExecutionException; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; import java.util.concurrent.Future; public class ThreadPoolTest { public static void main(String[] args) { ExecutorService newCachedThreadPool = Executors.newCachedThreadPool(); Future<String> future = newCachedThreadPool.submit(new MyCallable()); if(!future.isDone()) { System.out.println("线程正在执行..."); } try { System.out.println("返回值: " + future.get()); } catch (InterruptedException | ExecutionException e) { e.printStackTrace(); } finally { newCachedThreadPool.shutdown(); } } }
-
sleep和wait的区别
基本区别:
sleep
是Thread
类的方法,wait
是Object
类的方法。sleep
方法可以在任何地方使用。wait
方法只能在synchronized
方法或synchronized
块中使用。
最主要的本质区别:
-
Thread.sleep()
只会让出CPU,不会导致锁行为的改变。 -
Object.wait()
不仅会让出CPU,还会释放已经占有的同步资源锁。
Demo:
package com.yeliheng.threads;
public class SleepWaitTest {
public static void main(String[] args) {
final Object lock = new Object();
// 线程A
new Thread(() -> {
System.out.println("线程A正在等待获得锁...");
synchronized (lock) {
try {
System.out.println("线程A休眠20毫秒");
Thread.sleep(20);
System.out.println("线程A执行wait方法");
// 此时,lock会让出CPU并释放资源锁
// B线程可以获取到锁
lock.wait(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}).start();
// 线程B
new Thread(() -> {
System.out.println("线程B正在等待获得锁...");
synchronized (lock) {
try {
System.out.println("线程B获得锁");
Thread.sleep(10);
System.out.println("线程B执行完毕");
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}).start();
}
}
运行此段代码将输出:
线程A正在等待获得锁...
线程A休眠20毫秒
线程B正在等待获得锁...
线程A执行wait方法
线程B获得锁
线程B执行完毕
Process finished with exit code 0
可以看出,在线程A休眠后,线程B尝试获得锁,但此时锁并未被释放。直到线程A执行wait()
方法,锁才被释放,线程b获得锁并执行相关逻辑。
notify和notifyAll的区别
notifyAll
会让所有处于等待池中的线程全部进入锁池去竞争获取锁的机会。notify
只会随机选取一个处于等待池中的线程进入锁池去竞争获取锁的机会。
锁池:假设线程A已经拥有了某个对象(不是类)的锁,而其它线程B、C想要调用这个对象的某个synchronized方法(或者块),由于B、C线程在进入对象的synchronized方法(或者块)之前必须先获得该对象锁,而恰巧该对象的锁目前正在被A线程占用,此时B、C线程就会被阻塞,进入到一个地方去等待锁的释放,这个地方便是该对象的锁池。
等待池:假设线程A调用了某个对象的wait方法,线程A就会释放该对象的锁,同时线程A就进入到了该对象的等待池中,进入到等待池中的线程不会去竞争该对象的锁。
yield是什么
当调用Thread.yield()
函数时,会给线程调度器一个当前线程愿意让出CPU使用的暗示,但是线程调度器可能会忽略这个暗示。
如何中断线程
-
已经弃用的方法:通过stop()方法停止线程。
-
调用
interrupt()
,通知线程应该终端了- 如果线程处于被阻塞状态,那么线程将立即退出被阻塞状态,并抛出一个
InterruptedException
异常。 - 如果线程处于正常活动状态,那么会将该线程的中断标志设置为
true
。被设置中断标志的线程将继续正常运行,不受影响。
调用
interrupt
不会立即中断线程,需要被调用的线程配合中断。在正常运行任务时,经常检查本线程的中断标志位,如果被设置了中断标志就自行停止线程。 - 如果线程处于被阻塞状态,那么线程将立即退出被阻塞状态,并抛出一个
总结
针对以上讲解的内容,我画了一张图进行总结。图中包含了进程状态的转换以及上文讲述到的方法内容。
从图中可以清晰地看出进程状态的相互转换。
下一篇,我们将对Java进程和线程的进阶原理进行讲解。