文章目录
- 多线程经典面试题
- 基本概念
- 线程的生命周期
- Java 线程的 6 个状态
- 线程状态转换
- 创建线程的方法
- 继承Thread类
- 实现Runnable接口
- Callable、Future 与 FutureTask
- Callable 接口
- Future 接口
- FutureTask 类
- FutureTask 的几个状态
- 创建线程的三种方式的对比
- 线程组和线程的优先级
- 线程组 (ThreadGroup)
- 线程的优先级
- 线程的几个主要的概念
- 线程同步
- 线程间通信
- 线程死锁
- 线程控制
- 多线程的使用
- 参考文献
多线程经典面试题
- 多进程的方式也可以实现并发,为什么我们要使用多线程?
多进程方式确实可以实现并发,但使用多线程,有以下几个好处:
- 进程间的通信比较复杂,而线程间的通信比较简单,通常情况下,我们需要使用共享资源,这些资源在线程间的通信比较容易。
- 进程是重量级的,而线程是轻量级的,故多线程方式的系统开销更小。
- 进程和线程的区别是什么?
进程是一个独立的运行环境,而线程是在进程中执行的一个任务,它们两个本质的区别是否单独占有内存地址空间及其他系统资源(比如I/O)
- 进程单独占有一定的内存地址空间,所以进程间存在内存隔离,数据是分开的,数据共享复杂但是数据同步简单,各个进程之间互不干扰;而线程共享所属进程占有的内存地址空间和资源,数据共享简单,但是同步复杂。
- 进程单独占有一定的内存地址空间,一个进程出现问题不会影响其他进程,不影响主进程的稳定性,可靠性高;一个线程崩溃可能影响整个程序的稳定性,可靠性较低。
- 进程单独占有一定的内存地址空间,进程的创建和销毁不仅需要保存寄存器和栈信息,还需要资源的分配回收以及页调度,开销巨大;线程只需要保存寄存器和栈信息,开销较小。
- 实现一个自定义的线程类,可以有继承Thread类或实现Runnable接口这两种方式,他们之间有什么优劣呢?
- 由于Java “单继承,多实现” 的特性,Runnable接口使用起来比Thread更灵活。
- Runnable 接口出现更符合面向对象,将线程单独进行对象的封装。
- Runnable 接口出现,降低了线程对象和线程任务的耦合性。
- 如果使用线程时不需要使用Thread类的诸多方法,显然使用Runnable接口更为轻量。
- FutureTask 类有什么用?为什么要有一个 FutureTask 类?
Future 只是一个接口,而它里面的 cancel、get、isDone 等方法要自己实现起来都是非常复杂的。所以JDK提供了一个 FutureTask 类来供我们使用。 - 能否采用线程优先级来指定线程执行的先后顺序?
不能,Java中的优先级不是特别可靠,Java程序中对线程所设置的优先级只是给操作系统一个建议,操作系统不一定会采纳,而真正的调用顺序,是由操作系统的线程调度算法决定的。 - 一个线程存在于一个线程组中,当线程和线程组的优先级不一致的时候将会怎么样呢?
如果某个线程优先级大于线程所在线程组的最大优先级,那么该线程的优先级将会失效,取而代之的是线程组的最大优先级。 - 反复调用同一个线程的 start() 方法是否可行?假如一个线程执行完毕(此时处于TERMINATED状态),再次调用这个线程的 start() 方法是否可行?
查看 start() 的源码后,两个问题的答案都是不可行的,在调用一次 start() 之后,threadStatus 的值会改变(threadStatus != 0), 所以不能再次调用,否则会抛出 IllegalThreadStateException 异常。
基本概念
时间片:CPU为每个进程分配一个时间段,称作它的时间片。
上下文: 是指某一时间点CPU寄存器和程序计数器的内容。
上下文切换:(有时候也称做进程切换或任务切换)是指CPU从一个进程(或线程)切换到另一个进程(或线程)。如果在时间片结束时进程还在运行,则暂停这个进程的运行,并且CPU分配给另一个进程,这个过程就叫做上下文交换。
线程:指的是进程中一个单一顺序的控制流,一个进程中可以并发多个线程,每条线程并行执行不同的任务。
进程:一个进程包括由操作系统分配的内存空间,包含一个或多个线程,一个线程不能独立的存在,它必须是进程的一部分,一个进程一直运行,直到所有的非守护线程都结束运行后才能结束。
线程的生命周期
线程是一个动态执行的过程,它也有一个从产生到死亡的过程
Java 线程的 6 个状态
// Thread.State 源码
public enum State {
NEW,
RUNNABLE,
BLOCKED,
WAITING,
TIMED_WAITING,
TERMINATED;
}
新建状态 (NEW)
使用 new 关键字和 Thread 类或其子类建立一个线程对象后,该线程对象就处于新建状态,它保持这个状态直到程序 start() 这个线程。
处于 NEW 状态的线程此时尚未启动。这里的尚未启动指的是还没有调用 Thread 实例的start() 方法。
// example:
private void testStateNew() {
Thread thread = new Thread(() -> {});
System.out.println(thread.getState()); // 输出 NEW
}
// start() 源码
public synchronized void start() {
if (threadStatus != 0)
throw new IllegalThreadStateException();
group.add(this);
boolean started = false;
try {
start0();
started = true;
} finally {
try {
if (!started) {
group.threadStartFailed(this);
}
} catch (Throwable ignore) {
}
}
}
// Thread.getState方法源码:
public State getState() {
// get current thread state
return sun.misc.VM.toThreadState(threadStatus);
}
// sun.misc.VM 源码:
public static State toThreadState(int var0) {
if ((var0 & 4) != 0) {
return State.RUNNABLE;
} else if ((var0 & 1024) != 0) {
return State.BLOCKED;
} else if ((var0 & 16) != 0) {
return State.WAITING;
} else if ((var0 & 32) != 0) {
return State.TIMED_WAITING;
} else if ((var0 & 2) != 0) {
return State.TERMINATED;
} else {
return (var0 & 1) == 0 ? State.NEW : State.RUNNABLE;
}
}
就绪状态和运行状态 ( RUNNABLE )
当线程对象调用了start()方法之后,该线程就进入了就绪状态。就绪状态的线程处于就绪队列中,要等待JVM里线程调度器的调度。
如果就绪状态的线程获取CPU资源,就可以执行 run(), 此时线程便处于运行状态。处于运行状态的线程最为复杂,它可以变为阻塞状态、就绪状态和死亡状态。
RUNNABLE 状态,表示线程在Java虚拟机中运行,也有可能在等待CPU分配资源。
阻塞状态 (BLOCKED)
如果一个线程执行了sleep(睡眠)、suspend(挂起)等方法。失去所占用的资源之后,该线程就从运行状态进入阻塞状态。在睡眠时间已到或获得设备资源后可以重新进入就绪状态。可以分为三种:
- 等待阻塞:运行状态中的线程执行 wait() 方法,使线程进入等待阻塞状态。
- 同步阻塞:线程在获取 synchronized 同步锁失败(因为同步锁被其他线程占用)。
- 其他阻塞:通过调用线程的 sleep() 或 join() 发出了 I/O 请求时,线程就会进入到阻塞状态,当 sleep() 状态超时,join() 等待线程终止或超时,或者 I/O 处理完毕,线程重新转入就绪状态。
example:
假如今天你下班后准备去食堂吃饭。你来到食堂仅有的一个窗口,发现前面已经有个人在窗口前了,此时你必须等前面的人从窗口离开才行。
假设你是线程t2,你前面的那个线程是t1,此时t1占有了锁(食堂的唯一窗口),t2正在等待锁的释放,所以t2就处于 BLOCKED 状态。
等待状态 (WAITING)
等待状态,处于等待状态的线程变成RUNNABLE状态需要其他线程唤醒。
调用如下3个方法会使线程进入等待状态:
- Object.wait(): 使当前线程处于等待状态直到另一个线程唤醒;
- Thread.join(): 等待线程执行完毕,底层调用的是Object实例的wait方法;
- LockSupport.park(): 除非获得调用许可,否则禁用当前线程进行线程调度;
example:
你等待好几分钟现在终于轮到你了,突然你们有一个“不懂事”的经理突然来了。你看到他你就有一种不祥的预感,果然,他是来找你的。
他把你拉到一旁叫你待会儿再吃饭,说他下午要去作报告,赶紧来找你了解一下项目的情况。你心里虽然有一万个不愿意但是你还是从食堂窗口走开了。
此时,假设你还是线程t2,你的经理是线程t1。虽然你此时都占有锁(窗口)了,“不速之客”来了你还是要放弃锁。此时你t2的状态就是WAITING。然后经理t1获得锁,进入RUNNABLE状态。
要是经理t1不主动唤醒你t2(notify、notifyAll、...),可以说你t2只能一直等待了。
超时等待状态 (TIMED_WAITING)
超时等待状态,线程等待一个具体的时间,时间到后会被自动唤醒。
调用以下方法会让线程进入超时等待状态:
- Thread.sleep(): 使当前线程睡眠指定时间;
- Object.wait(long timeout): 线程休眠指定时间,等待期间可以通过notify()、notifyAll()唤醒;
- Thread.join(long millis): 等待当前线程最多执行millis毫秒,如果millis为0,则会一直执行;
- LockSupport.parkNanos(long nanos): 除非获得调用许可,否则禁用当前线程进行线程调度指定时间;
- LockSupport.parkUntil(long deadline): 同上,也是禁止线程进行调度指定时间。
example:
到了第二天中午,又到了饭点,你还是到了窗口前。
突然间想起你的同事叫你等他一起,他说让你等他十分钟他改个bug。
好吧,你说那你就等等吧,你就离开了窗口,很快十分钟过去了,你见他还没来,你想都等了这么久了还不来,那你还是先去吃饭好了。
这时你还是线程t1,你改bug的同事是线程t2,t2让t1等待了指定时间,此时t1等待期间就属于 TIMED_WAITING 状态。
死亡状态 (TERMINATED)
一个运行状态的线程完成任务或在其他终止条件发生时,该线程就切换到终止状态。
线程状态转换
创建线程的方法
Java 提供了三种创建线程的方法
- 通过继承Thread 类本身
- 通过实现Runnable接口
- 通过 Callable 和 Future 创建线程
继承Thread类
example:
public class Demo {
public static class MyThread extends Thread {
@Override
public void run() {
System.out.println("MyThread");
}
}
public static void main(String[] args) {
Thread myThread = new MyThread();
myThread.start();
}
}
调用了start方法后,虚拟机会为我们先创建一个线程,然后等到这个线程第一次得到时间片时,再调用run方法。
实现Runnable接口
// Runnable接口(JDK 1.8 +):
@FunctionalInterface
public interface Runnable {
public abstract void run();
}
可以看到Runnable接口是一个函数式接口,这意味着我们可以使用Java 8 的函数式编程来简化代码。
example:
public class Demo {
public static class MyThread implements Runnable {
@Override
public void run() {
System.out.println("MyThread");
}
}
public static void main(String[] args) {
new Thread(new MyThread()).start();
// Java 8 函数式编程,可以省略MyThread类
new Thread(() -> {
System.out.println("Java 8 匿名内部类");
}).start();
}
}
Callable、Future 与 FutureTask
通常来说,我们使用 Runnable 和 Thread 来创建一个新的线程。但是它们有个弊端,就是run方法是没有返回值的。而有时侯我们希望开启一个线程去执行一个任务,并且这个任务执行完成后有一个返回值。
JDK提供了 Callable 接口与 Future 接口为我们解决这个问题,这就是所谓的“异步”模型。
Callable 接口
Callable 和 Runnable 类似,同样只有一个函数式接口。不同的是,Callable 提供的方法是有返回值的,而且支持泛型。
// Callable 接口
@FunctionalInterface
public interface Callable<V> {
V call() throws Exception;
}
example:
// 自定义Callable
class Task implements Callable<Integer>{
@Override
public Integer call() throws Exception {
// 模拟计算需要一秒
Thread.sleep(1000);
return 2;
}
public static void main(String args[]) throws Exception {
// 使用
ExecutorService executor = Executors.newCachedThreadPool();
Task task = new Task();
Future<Integer> result = executor.submit(task);
// 注意调用get方法会阻塞当前线程,直到得到结果。
// 所以实际编码中建议使用可以设置超时时间的重载get方法。
System.out.println(result.get());
}
}
Future 接口
Future 接口有几个比较简单的方法
// Future 接口
public abstract interface Future<V> {
public abstract boolean cancel(boolean paramBoolean);
public abstract boolean isCancelled();
public abstract boolean isDone();
public abstract V get() throws InterruptedException, ExecutionException;
public abstract V get(long paramLong, TimeUnit paramTimeUnit)
throws InterruptedException, ExecutionException, TimeoutException;
}
cancel 方法是试图取消一个线程的执行
注意是试图取消,并不一定能取消成功。因为任务可能已完成、已取消、或者一些其他因素不能取消,存在取消失败的可能。Boolean 类型的返回值是“是否取消成功”的意思。参数 paramBoolean 表示是否采用中断的方式取消线程的执行。
所以有时候,为了让任务有能够取消的功能,就是用 Callable 来代替 Runnable。如果为了取消性而使用 Future 但又不提供可用的结果,则可以声明 Future<?> 形式类型,并返回 null 作为底层任务的结果。
FutureTask 类
Future 接口有一个实现类叫 FutureTask。FutureTask 是实现的 RunnableFuture 接口的,而 RunnableFuture 接口同时继承了 Runnable 接口和 Future 接口。
// RunnableFuture 接口
public interface RunnableFuture<V> extends Runnable, Future<V> {
/**
* Sets this Future to the result of its computation
* unless it has been cancelled.
*/
void run();
}
example:
// 自定义Callable,与上面一样
class Task implements Callable<Integer>{
@Override
public Integer call() throws Exception {
// 模拟计算需要一秒
Thread.sleep(1000);
return 2;
}
public static void main(String args[]) throws Exception {
// 使用
ExecutorService executor = Executors.newCachedThreadPool();
FutureTask<Integer> futureTask = new FutureTask<>(new Task());
executor.submit(futureTask);
System.out.println(futureTask.get());
}
}
FutureTask 的几个状态
/**
*
* state可能的状态转变路径如下:
* NEW -> COMPLETING -> NORMAL
* NEW -> COMPLETING -> EXCEPTIONAL
* NEW -> CANCELLED
* NEW -> INTERRUPTING -> INTERRUPTED
*/
private volatile int state;
private static final int NEW = 0;
private static final int COMPLETING = 1;
private static final int NORMAL = 2;
private static final int EXCEPTIONAL = 3;
private static final int CANCELLED = 4;
private static final int INTERRUPTING = 5;
private static final int INTERRUPTED = 6;
state 表示任务的运行状态,初始状态为 NEW。运行状态只会在set、setExecution、cancel 方法中终止。COMPLETING、INTERRUPTING 是任务完成后的瞬时状态。
创建线程的三种方式的对比
- 采用实现 Runnable、Callable 接口的方式创建多线程时,线程类只是实现了Runnable接口或Callable接口,还可以继承其他类。
- 使用继承 Thread 类的方式创建多线程时,编写简单,如果需要访问当前线程,则无需使用 Thread.currentThread() 方法,直接使用 this 即可获得当前线程。
线程组和线程的优先级
总结来说:线程组是一个树状结构,每个线程组下面可以有多个线程或者线程组,线程组可以起到统一控制线程的优先级和检查线程的权限的作用。
线程组 (ThreadGroup)
Java 中 ThreadGroup 来表示线程组,我们可以使用线程组对线程进行批量控制。
ThreadGroup 和 Thread 的关系如同字面意思一样简单粗暴,每个 Thread 必然存在于一个 ThreadGroup 中,Thread 不能独立于 ThreadGroup 存在。执行 main() 方法线程的名字是 main, 如果在 new Thread 时没有显式指定,那么默认将父线程(当前执行new Thread的线程)线程组设置为自己的线程组。
example:
public class Demo {
public static void main(String[] args) {
Thread testThread = new Thread(() -> {
System.out.println("testThread当前线程组名字:" +
Thread.currentThread().getThreadGroup().getName());
System.out.println("testThread线程名字:" +
Thread.currentThread().getName());
});
testThread.start();
System.out.println("执行main所在线程的线程组名字: " + Thread.currentThread().getThreadGroup().getName());
System.out.println("执行main方法线程名字:" + Thread.currentThread().getName());
}
}
ThreadGroup 管理着它下面的Thread,ThreadGroup 是一个标准的向下引用的树状结构,这样设计的原因是:防止“上级”线程被“下级”线程引用而无法有效地被GC回收。
线程的优先级
每一个Java线程都有一个优先级,这样有助于操作系统确定线程的调度顺序。
Java 线程的优先级是一个整数,其取值范围是 1(Thread.Min_Priority)-10(Thread.Max_Priority)。
默认情况下,每一个线程都会分配一个优先级 Normal_Priority(5)。
通常情况下,高优先级的线程将会比低优先级的线程有更高的几率得到执行。但是,优先级不能保证线程执行的顺序,而且非常依赖于平台。可以使用 Thread 类的 setPriority() 实例方法来设定线程的优先级。
public class Demo {
public static void main(String[] args) {
Thread a = new Thread();
System.out.println("我是默认线程优先级:"+a.getPriority());
Thread b = new Thread();
b.setPriority(10);
System.out.println("我是设置过的线程优先级:"+b.getPriority());
}
}
Java 提供一个线程调度器来监视和控制处于RUNNABLE状态的线程,线程的调度策略采用抢占式,优先级高的线程比优先级低的线程会有更大的几率优先执行。在优先级相同的情况下,按照“先到先得”的原则。每个 Java 程序都有一个默认的主线程,就是通过 JVM 启动的第一个线程 main 线程。
还有一种线程称为“守护线程(Daemon)”,守护线程的优先级比较低。
如果某线程是守护线程,那如果所有的非守护线程都结束了,这个守护线程也会自动结束。
应用场景是:当所有非守护线程结束时,结束其余的子线程(守护线程)自动关闭,就免去了还要继续关闭子线程的麻烦。
一个线程默认是非守护线程,可以通过 Thread 的 setDaemon(Boolean on) 来设置。
线程的几个主要的概念
在多线程编程时,你需要了解一下几个概念
- 线程同步
- 线程间通信
- 线程死锁
- 线程控制:挂起、停止和恢复
线程同步
为什么要线程同步
因为当我们有多个线程要同时访问一个变量或对像时,如果这些线程中既有读又有写操作时,就会导致变量值或对象的状态出现混乱,从而导致程序异常。
实现线程同步的方式
- 同步方法
synchronized 关键字修饰方法,由于Java的每个对像都有一个内置锁,当用此关键字修饰方法时,内置锁会保护整个方法。在调用该方法前,需要获得内置锁,否则就会处于阻塞状态。 - 同步代码块
即用synchronized修饰的语句块。被该关键字修饰的语句块会自动被加上内置锁,从而实现同步。 - 使用特殊域变量(Volatile)实现线程同步
每次线程要访问volatile修饰的变量时都是从内存中读取,而不是从缓存中读取,因此每个线程访问到的变量值都是一样的,这样就保证了同步。Volatile关键字为域变量的访问提供了一种免锁机制。使用Volatile修饰域相当于告诉虚拟机该域可能会被其他线程更新,因此每次使用该域就要重新计算,而不是使用寄存器中的值。Volatile不会提供任何原子操作,它也不能用来修饰final类型的变量。 - 使用重入锁实现线程同步
在javaSE5中新增了一个java.util.concurrent包来支持同步。ReentrantLock类是可重入、互斥、实现了lock接口的锁,它与使用synchronized方法具有相同的基本行为和语义,并且扩展了其能力。
ReentrantLock类的常用方法有:ReentrantLock(): 创建一个ReentrantLock实例; lock(): 获得锁; unlock(): 释放锁; ReentrantLock()还可以创建公平锁的构造方法,但由于能大幅度降低程序运行效率,不推荐使用。 - 使用局部变量实现线程同步
ThreadLock的原理:如果使用ThreadLock管理变量,则每一个使用该变量的线程都获得该变量的副本,副本之间相互独立,这样每一个线程都可以随意修改自己的变量副本,而不会对其他线程产生影响。
线程间通信
线程通信的目标是使线程间能够互相发送信号。另一方面,线程通信使线程能够等待其他线程的信号。
线程死锁
多个线程同时被阻塞时,它们中的一个或者全部都在等待某个资源被释放。由于线程被无限期地阻塞,因此程序不可能正常终止。
java 死锁产生的四个必要条件:
- 互斥使用,即当资源被一个线程使用(占用)时,别的线程不能使用
- 不可抢占,资源请求者不能强制从资源占有者手中夺取资源,资源只能由资源持有者主动释放
- 请求和保持,即当资源请求者在请求其他的资源的同时保持对原有资源的占有
- 循环等待,即存在一个等待队列:P1占有P2的资源,P2占有P3的资源,P3占有P1的资源。这样就形成了一个等待环路。
线程控制
一个线程的控制简单包括3种:
- 开启线程
- 暂停线程
- 停止线程
多线程的使用
有效利用多线程的关键是理解程序是并发执行而不是串行执行。
参考文献
菜鸟教程 java多线程RedSpider社区