Java笔试面试-线程与死锁
线程
线程(Thread)是程序运行的执行单元,依托于进程存在。一个进程中可以包含多个线程,多线程可以共享一块内存空间和一组系统资源,因此线程之间的切换更加节省资源、更加轻量化,因而也被称为轻量级的进程。
进程
进程(Processes)是程序的一次动态执行,是系统进行资源分配和调度的基本单位,是操作系统运行的基础,通常每一个进程都拥有自己独立的内存空间和系统资源。简单来说,进程可以被当做是一个正在运行的程序。
为什么需要线程
程序的运行必须依靠进程,进程的实际执行单元就是线程。
为什么需要多线程
多线程可以提高程序的执行性能。例如,有个 90 平方的房子,一个人打扫需要花费 30 分钟,三个人打扫就只需要 10 分钟,这三个人就是程序中的“多线程”。
线程使用
线程的创建,分为以下三种方式:
- 继承 Thread 类,重写 run 方法
- 实现 Runnable 接口,实现 run 方法
- 实现 Callable 接口,实现 call 方法
1.继承 Thread 类
class ThreadTest {
public static void main(String[] args) throws Exception {
MyThread thread = new MyThread();
thread.start();
}
}
class MyThread extends Thread {
@Override
public void run() {
System.out.println("Thread");
}
}
以上程序执行结果如下:
Thread
2.实现 Runnable 接口
class ThreadTest {
public static void main(String[] args) throws Exception {
MyRunnable runnable = new MyRunnable();
runnable.run();
}
}
class MyRunnable implements Runnable {
@Override
public void run() {
System.out.println("Runnable");
}
}
以上程序执行结果如下:
Runnable
3.实现 Callable 接口
class ThreadTest {
public static void main(String[] args) throws Exception {
MyCallable callable = new MyCallable();
Object result = callable.call();
System.out.println(result);
}
}
class MyCallable implements Callable {
@Override
public Object call() throws Exception {
System.out.println("Callable");
return "Success";
}
}
以上程序执行结果如下:
Callable
Success
4.JDK 8 创建线程
JDK 8 之后可以使用 Lambda 表达式很方便地创建线程,请参考以下代码:
new Thread(() -> System.out.println("Lambda Of Thread.")).start();
线程高级用法
线程等待
使用 wait() 方法实现线程等待,代码如下:
System.out.println(LocalDateTime.now());
Object lock = new Object();
Thread thread = new Thread(() -> {
synchronized (lock){
try {
// 1 秒钟之后自动唤醒
lock.wait(1000);
System.out.println(LocalDateTime.now());
} catch (InterruptedException e) {
e.printStackTrace();
}
}
});
thread.start();
以上程序执行结果如下:
2019-06-22T20:53:08.776
2019-06-22T20:53:09.788
注意:当使用 wait() 方法时,必须先持有当前对象的锁,否则会抛出异常 java.lang.IllegalMonitorStateException
。
线程唤醒
使用 notify()/notifyAll() 方法唤醒线程。notify() 方法随机唤醒对象的等待池中的一个线程;notifyAll() 唤醒对象的等待池中的所有线程。
Object lock = new Object();
lock.wait();
lock.notify();
// lock.notifyAll();
线程休眠
// 休眠 1 秒
Thread.sleep(1000);
等待线程执行完成
Thread joinThread = new Thread(() -> {
try {
System.out.println("执行前");
Thread.sleep(1000);
System.out.println("执行后");
} catch (InterruptedException e) {
e.printStackTrace();
}
});
joinThread.start();
joinThread.join();
System.out.println("主程序");
以上程序执行结果:
执行前
执行后
主程序
yield 交出 CPU 执行权
new Thread(){
@Override
public void run() {
for (int i = 1; i < 10; i++) {
if (i == 5) {
// 让同优先级的线程有执行的机会
this.yield();
}
}
}
}.start();
注意:yield 方法是让同优先级的线程有执行的机会,但不能保证自己会从正在运行的状态迅速转换到可运行的状态。
线程中断
使用 System.exit(0) 可以让整个程序退出;要中断单个线程,可配合 interrupt() 对线程进行“中断”。
Thread interruptThread = new Thread() {
@Override
public void run() {
for (int i = 0; i < Integer.MAX_VALUE; i++) {
System.out.println("i:" + i);
if (this.isInterrupted()) {
break;
}
}
}
};
interruptThread.start();
Thread.sleep(10);
interruptThread.interrupt();
线程优先级
在 Java 语言中,每一个线程有一个优先级,默认情况下,一个线程继承它父类的优先级。可以使用 setPriority 方法设置(1-10)优先级,默认的优先级是 5,数字越大表示优先级越高,优先级越高的线程可能优先被执行的概率就越大。
设置优先级的代码如下:
Thread thread = new Thread(() -> System.out.println("Java"));
thread.setPriority(10);
thread.start();
死锁
死锁是指两个或两个以上的进程在执行过程中,由于竞争资源或者由于彼此通信而造成的一种阻塞的现象,若无外力作用,它们都将无法推进下去。比如,当线程 A 持有独占锁 a,并尝试去获取独占锁 b 的同时,线程 B 持有独占锁 b,并尝试获取独占锁 a 的情况下,就会发生 A B 两个线程由于互相持有对方需要的锁,而发生的阻塞现象,我们称为死锁。
死锁示意图如下所示:
死锁代码:
Object obj1 = new Object();
Object obj2 = new Object();
// 线程1拥有对象1,想要等待获取对象2
new Thread() {
@Override
public void run() {
synchronized (obj1) {
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
synchronized (obj2) {
System.out.println(Thread.currentThread().getName());
}
}
}
}.start();
// 线程2拥有对象2,想要等待获取对象1
new Thread() {
@Override
public void run() {
synchronized (obj2) {
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
synchronized (obj1) {
System.out.println(Thread.currentThread().getName());
}
}
}
}.start();
笔试面试题
1.线程和进程有什么区别和联系?
答:从本质上来说,线程是进程的实际执行单元,一个程序至少有一个进程,一个进程至少有一个线程,它们的区别主要体现在以下几个方面:
- 进程间是独立的,不能共享内存空间和上下文,而线程可以;
- 进程是程序的一次执行,线程是进程中执行的一段程序片段;
- 线程占用的资源比进程少。
2.如何保证一个线程执行完再执行第二个线程?
答:使用 join() 方法,等待上一个线程的执行完之后,再执行当前线程。
示例代码:
Thread joinThread = new Thread(() -> {
try {
System.out.println("执行前");
Thread.sleep(1000);
System.out.println("执行后");
} catch (InterruptedException e) {
e.printStackTrace();
}
});
joinThread.start();
joinThread.join();
System.out.println("主程序");
3.线程有哪些常用的方法?
答:线程的常用方法如下:
- currentThread():返回当前正在执行的线程引用
- getName():返回此线程的名称
- setPriority()/getPriority():设置和返回此线程的优先级
- isAlive():检测此线程是否处于活动状态,活动状态指的是程序处于正在运行或准备运行的状态
- sleep():使线程休眠
- join():等待线程执行完成
- yield():让同优先级的线程有执行的机会,但不能保证自己会从正在运行的状态迅速转换到可运行的状态
- interrupted():是线程处于中断的状态,但不能真正中断线程
4.wait() 和 sleep() 有什么区别?
答:wait() 和 sleep() 的区别主要体现在以下三个方面。
- 存在类的不同:sleep() 来自 Thread,wait() 来自 Object。
- 释放锁:sleep() 不释放锁;wait() 释放锁。
- 用法不同:sleep() 时间到会自动恢复;wait() 可以使用 notify()/notifyAll() 直接唤醒。
5.守护线程是什么?
答:守护线程是一种比较低级别的线程,一般用于为其他类别线程提供服务,因此当其他线程都退出时,它也就没有存在的必要了。例如,JVM(Java 虚拟机)中的垃圾回收线程。
6.线程有哪些状态?
答:在 JDK 8 中,线程的状态有以下六种。
- NEW:尚未启动
- RUNNABLE:正在执行中
- BLOCKED:阻塞(被同步锁或者 IO 锁阻塞)
- WAITING:永久等待状态
- TIMED_WAITING:等待指定的时间重新被唤醒的状态
- TERMINATED:执行完成
7.线程中的 start() 和 run() 有那些区别?
答:start() 方法用于启动线程,run() 方法用于执行线程的运行时代码。run() 可以重复调用,而 start() 只能调用一次。
8.产生死锁需要具备哪些条件?
答:产生死锁的四个必要条件如下。
- 互斥条件:一个资源每次只能被一个进程使用
- 请求与保持条件:一个进程因请求资源而阻塞时,对已获得的资源保持不放。
- 不剥夺条件:进程已获得的资源,在末使用完之前,不能强行剥夺。
- 循环等待条件:若干进程之间形成一种头尾相接的循环等待资源关系。
这四个条件是死锁的必要条件,只要系统发生死锁,这些条件必然成立,而只要上述条件之一不满足,就不会发生死锁。
9.如何预防死锁?
答:预防死锁的方法如下:
- 尽量使用 tryLock(long timeout, TimeUnit unit) 的方法 (ReentrantLock、ReentrantReadWriteLock),设置超时时间,超时可以退出防止死锁。
- 尽量使用 Java. util. concurrent 并发类代替自己手写锁。
- 尽量降低锁的使用粒度,尽量不要几个功能用同一把锁。
- 尽量减少同步的代码块。
10.thread.wait() 和 thread.wait(0) 有什么区别?代表什么含义?
答:thread.wait() 和 thread.wait(0) 是相同的,使用 thread.wait() 内部其实是调用的 thread.wait(0),源码如下:
public final void wait() throws InterruptedException {
wait(0);
}
wait() 表示进入等待状态,释放当前的锁让出 CPU 资源,并且只能等程序执行 notify()/notifyAll() 方法才会被重写唤醒。
11.如何让两个程序依次输出 11/22/33 等数字,请写出实现代码?
答:使用思路是在每个线程输出信息之后,让当前线程等待一会再执行下一次操作,具体实现代码如下:
new Thread(() -> {
for (int i = 1; i < 4; i++) {
System.out.println("线程一:" + i);
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}).start();
new Thread(() -> {
for (int i = 1; i < 4; i++) {
System.out.println("线程二:" + i);
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}).start();
程序执行结果如下:
线程一:1
线程二:1
线程二:2
线程一:2
线程二:3
线程一:3
12.说一下线程的调度策略?
答:线程调度器选择优先级最高的线程运行,但是如果发生以下情况,就会终止线程的运行:
- 线程体中调用了 yield() 方法,让出了对 CPU 的占用权;
- 线程体中调用了 sleep() 方法,使线程进入睡眠状态;
- 线程由于 I/O 操作而受阻塞;
- 另一个更高优先级的线程出现;
- 在支持时间片的系统中,该线程的时间片用完。