在Android开发的面试中,Java多线程的问题是绕不开的。这个系列主要介绍面试过程中涉及到的多线程的知识点,以及相关的面试题。这是本系列的第二篇,介绍Java中多线程的旧版api,及其对应的常见的面试题。
java创建线程的方式
java创建线程有三种方式,分别是:
- 通过Runnable创建线程
java
public class RunnableTest implements Runnable {
@Override
public void run() {
}
}
public static void main(String[] args) throws Exception {
Runnable runnable = new RunnableTest();
Thread thread = new Thread(runnable);
thread.start();//启动线程
}
2.继承Thread创建线程
java
public class MyThread extends Thread {
public MyThread(String name) {
super(name);
}
@Override
public void run() {
}
}
public static void main(String[] args) throws Exception {
Thread thread = new MyThread("线程");
thread.start();
}
- 使用Callable和Future创建线程
arduino
复制代码
public class MyCallable implements Callable<String> {
@Override
public String call() throws Exception {
}
}
public static void main(String[] args) throws Exception {
Callable<String> callable = new MyCallable();
FutureTask<String> task = new FutureTask<>(callable);
Thread t = new Thread(task);
t.start();
System.out.println(task.get());
}
面试题1:一个线程两次调用start()方法会出现什么情况
Java的线程是不允许启动两次的,第二次调用会抛出 IllegalThreadStateException 异常。
面试题2:runnable 和 callable 有什么区别?
runnable 没有返回值,callable 可以拿到有返回值,callable 可以看作是 runnable 的补充。
面试题3: 线程的 run() 和 start() 有什么区别?
start() 方法用于启动线程,run() 方法用于执行线程的运行时代码。run() 可以重复调用,而 start() 只能调用一次。
线程的生命周期
线程有六种状态,分别如下:
- NEW(初始化状态)
- RUNNABLE(可运行 / 运行状态)
- BLOCKED(阻塞状态)
- WAITING(无时限等待)
- TIMED_WAITING(有时限等待)
- TERMINATED(终止状态)
可以通过getState()
获取线程所处的状态。状态之间的切换原因如下:
状态 | 切换原因 |
---|---|
NEW(初始化状态) | 创建Thread |
RUNNABLE(可运行 / 运行状态) | 调用start方法 |
BLOCKED(阻塞状态) | 只有线程等待synchronized锁时 |
WAITING(无时限等待) | 调用wait、join、LockSupport.park 时 |
TIMED_WAITING(有时限等待) | 调用sleep,带时间的wait、join、LockSupport.parkNanos |
TERMINATED(终止状态) | 执行完run方法后,自动切换到TERMINATED状态 |
面试题4:如果当前线程等待 CPU 使用权或者等待 I/O ,这时线程的状态是什么?
线程等待 CPU 使用权与等待 I/O 时也是RUNNABLE状态
wait、notify、notifyAll
java
synchronized (obj) {
try {
obj.wait();
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
调用 wait 方法会让线程进入等待状态(WAITING),同时释放掉锁。如果需要唤醒线程,需要调用 notify 或者 notifyAll 方法。notify 方法是随机唤醒一个线程,而 notifyAll 方法是会唤醒所有等待线程
sleep
java
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
调用sleep方法会让线程休眠一段时间,进入有时限等待状态(TIMED_WAITING)。需要注意,sleep 方法不会释放锁,但会释放CPU资源
面试题5:wait与sleep区别有哪些
- wait释放锁,sleep不释放锁
- wait需要被唤醒,sleep不需要
- wait需要获取到监视器(在synchronized代码块里面),否则抛异常,sleep不需要
- wait是object顶级父类的方法,sleep则是Thread的方法
join
java
//主线程执行
Thread thread = new Thread(runnable);
thread.start();
try {
thread.join();
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
join 方法的作用是等待指定的线程的结束。如上面的代码所示,主线程会等待 thread 线程执行结束后再执行。这时主线程会变成 WAITING 状态。需要注意,join方法只有在start方法之后调用才有效。
线程的中断
java
void interrupt() //请求线程中断,线程不一定会中断
boolean isInterrupted() //检测当前线程是否中断,不会改变中断状态,能多次判断
static boolean interrupted() //检测当前线程是否中断,会重置中断状态为false,只能判断一次
我们调用 interrupt 方法来请求线程中断,这时中断的操作只会改变中断状态,并不会中断程序, 这时需要我们使用 if(Thread.currentThread().isInterrupted())
来判断是否处于中断状态,自己来处理,这样增加了灵活性。
面试题6:isInterrupted 和 interrupted 的区别
- isInterrupted:是成员方法,作用是检测当前线程是否中断,不会改变中断状态,因此可以多次判断是否中断
- interrupted:是静态方法,作用是检测当前线程是否中断,它会重置中断状态为false,因此只能判断一次是否中断
面试题7:中断操作在线程所有状态下都生效吗
当线程处于New
或Terminated
状态时,中断操作无效,即中断状态不会改变; 当线程处于Runnable
或Blocked
时,中断操作会改变中断状态;当线程处于 WAITING
或 TIMED_WAITING
时,中断操作会抛出异常,isInterrupted() 不会返回 true,并且结束程序。
需要注意:当线程 A 处于 WAITING、TIMED_WAITING 状态时,如果其他线程调用线程 A 的 interrupt() 方法,会使线程 A 返回到 RUNNABLE 状态,同时线程 A 的代码会触发InterruptedException 异常。ReentranLocak.lock方法会进入waiting状态,但是不会被interrupt打断
面试题8:终止线程为什么不用 stop
stop 方法会直接杀死线程,如果此时线程持有 ReentrantLock 锁,被 stop 的线程并不会自动调用 ReentrantLock 的 unlock() 去释放锁,此时其他线程就再也没机会获得 ReentrantLock 锁。
线程优先级
每一个线程都有一个优先级,在默认的情况下,一个线程继承它父类线程的优先级。 可以使用setPriority(int newPriority)
设置优先级。
优先级的等级在MIN_PRIORITY
(默认值为1)和MAX_PRIORITY
(默认值为10)之间;默认为 NORM_PRIORITY
(默认值为5).
守护线程
java
public static void main(String[] args) {
Runnable runnable = new Runnable() {
@Override
public void run() {
while (true);
}
};
Thread thread = new Thread(runnable);
thread.start();
}
当我们执行上面代码的时候,由于子线程会一直执行,这时即使主线程结束了,该进程也不会结束。如果我们希望主线程结束了,子线程也会结束,我们可以通过 thread.setDaemon(true);
让子线程成为守护线程。
守护线程的唯一用途是为其他线程通过服务。注意:守护线程应该永远不去 访问固有资源,如文件,数据库,因为它会在任何时刻甚至在一个操作中间发生中断
ThreadLocal
ThreadLocal 是指线程本地变量,它可以让每个线程都有自己独立的变量副本,互不干扰。
csharp
public class RunnableTest implements Runnable {
private final ThreadLocal<Integer> value = new ThreadLocal<>();
@Override
public void run() {
//设置value
value.set(124);
//获取value
value.get();
}
}
实现原理如下图所示:
在 Thread 类中,有 ThreadLocalMap 的属性,当我们通过 ThreadLocal 的 set 方法设置 value 时,它会往当前 Thread 中的 ThreadLocalMap 插入 key 为 ThreadLocal,value 为指定值的一项。
面试题9:为什么ThreadLocal会造成内存泄漏问题
ThreadLocal 虽然是弱引用,但是其对应的value为强引用。如果 ThreadLocal 被垃圾回收,这时 key 为 null,但是 value 不会为null。如果该线程生命周期比较长(可能是线程池的核心线程),这时value这块内存就会一直在内存中,造成内存泄漏。
synchronized
synchronized 是旧版java中来在多线程中保持同步的关键字。synchronied 可以修饰普通方法,等价于synchronized(this){}
,是给当前类的对象加锁;synchronied 修饰静态方法,等价于synchronized(class){}
,是给当前类对象加锁。
需要注意,synchronized(obj){} 中的obj不要使用 String、Integer 等的对象,这是因为它们内部会缓存常量字符串、数字,会对同步造成影响
同步容器
同步容器是早期Java解决多线程操作容器的方案,同步容器有 Hashtable、Vector、Stack。它们的实现原理很简单就是给方法加上 synchronized 关键字,但是这会造成性能的问题。新版的java中提供了新的并发容器来解决这个问题,在下一篇文章会介绍新版的多线程接口。
参考
- 14个Java并发容器,Java高手都知道!-阿里云开发者社区 (aliyun.com)
- Java面试题|多线程22道必看面试题 - 知乎 (zhihu.com)
- 【建议收藏】106道Android核心面试题及答案汇总(总结最全面的面试题)
- 面试官问我什么是JMM - 知乎 (zhihu.com)
- Java 并发编程实战 (geekbang.org)
- final保证可见性和this引用逃逸 - 知乎 (zhihu.com)
- Synchronized的底层实现原理(原理解析,面试必备)_synchronized底层实现原理-CSDN博客
- 线程间到底共享了哪些进程资源 - 知乎 (zhihu.com)
- stackoverflow.com/questions/1...
- spotcodereviews.com/articles/co...
- Linux内核同步机制之(三):memory barrier (wowotech.net)
- 万字长文!一文彻底搞懂Java多线程 - 掘金 (juejin.cn)