写在前面:本文为个人八股复习所用,整合了其他平台的答案加自己的理解,希望能对大家的八股复习有所帮助,答案可能存在出入,请大家理性食用~~
1. 进程和线程的区别
进程:
进程是操作系统资源分配的基本单位,是程序的一次执行过程,进程之间是相互独立的,各自有各自的内存空间,它由操作系统进行调度。
线程:
线程是CPU执行调度的基本单位,一个进程在其执行的过程中会产生多个线程,与进程不同的是同类的多个线程共享进程的堆和方法区资源,但每个线程有自己的程序计数器、虚拟机栈和本地方法栈,所以系统在产生一个线程,或是在各个线程之间做切换工作时,负担要比进程小得多,也正因为如此,线程也被称为轻量级进程。
下面来思考这样一个问题:为什么程序计数器、虚拟机栈和本地方法栈是线程私有的呢?为什么堆和方法区是线程共享的呢?
1.1. 程序计数器为什么是私有的?
程序计数器主要有下面两个作用:
- 字节码解释器通过改变程序计数器来依次读取指令,从而实现代码的流程控制,如:顺序执行、选择、循环、异常处理。
- 在多线程的情况下,程序计数器用于记录当前线程执行的位置,从而当线程被切换回来的时候能够知道该线程上次运行到哪儿了。
需要注意的是,如果执行的是 native 方法,那么程序计数器记录的是 undefined 地址,只有执行的是 Java 代码时程序计数器记录的才是下一条指令的地址。
所以,程序计数器私有主要是为了线程切换后能恢复到正确的执行位置。
1.2. 虚拟机栈和本地方法栈为什么是私有的?
- 虚拟机栈: 每个 Java 方法在执行之前会创建一个栈帧用于存储局部变量表、操作数栈、常量池引用等信息。从方法调用直至执行完成的过程,就对应着一个栈帧在 Java 虚拟机栈中入栈和出栈的过程。
- 本地方法栈: 和虚拟机栈所发挥的作用非常相似,区别是:虚拟机栈为虚拟机执行 Java 方法 (也就是字节码)服务,而本地方法栈则为虚拟机使用到的 Native 方法服务。 在 HotSpot 虚拟机中和 Java 虚拟机栈合二为一。
所以,为了保证线程中的局部变量不被别的线程访问到,虚拟机栈和本地方法栈是线程私有的。
1.3. 一句话简单了解堆和方法区
堆和方法区是所有线程共享的资源,其中堆是进程中最大的一块内存,主要用于存放新创建的对象 (几乎所有对象都在这里分配内存),方法区主要用于存放已被加载的类信息、常量、静态变量、即时编译器编译后的代码等数据。
2. 线程的生命周期和状态
Java 线程在运行的生命周期中的指定时刻只可能处于下面 6 种不同状态的其中一个状态:
- 新建(New):线程对象被创建出来,但还未调用start()启动的状态。
- 就绪(Runnable):线程已经被启动,等待系统分配资源来运行。处于就绪状态的线程可能正在等待CPU时间片或等待某些条件满足。使用时间片轮转、优先级队列等调度算法来运行线程。
- 运行(Running):线程获得CPU资源正在执行任务
- 阻塞(Blocked):线程在等待某个锁或其他条件,如等待IO完成。
-
- 比如,线程A与线程B代码中使用同一锁,如果线程A获取到锁,线程A进入到Runnable状态,那么线程B就进入到Blocked锁阻塞状态。
- 等待(Waiting):线程等待其他线程通知调度器一个特定条件,通常是由于调用了
Object.wait()
、Thread.join()
或者LockSupport.park()
等方法。 - 终止(Terminated):线程执行完毕或者因为异常退出了
run()
方法。
3. 切换线程状态的方法
线程的状态是由JVM根据线程的行为和调度来管理和切换的,并且程序员可以通过合适的方法来影响线程状态的切换。
- Thread 类的方法:
-
- start():启动一个线程,使其处于就绪状态,等待系统调度执行。
- sleep(long millis):让当前线程休眠指定的时间,线程状态从运行转为阻塞或超时等待状态。
- join():等待线程终止,使当前线程进入等待状态。
- interrupt():中断线程,让线程抛出 InterruptedException 异常,处于终止状态。
- Object 类的方法:
-
- wait():让当前线程等待,使其进入等待状态,直到其他线程调用相同对象的
notify()
或notifyAll()
方法唤醒它。 - notify() 和 notifyAll():用于唤醒一个或所有等待中的线程,使其从等待状态转为就绪状态。
- wait():让当前线程等待,使其进入等待状态,直到其他线程调用相同对象的
- LockSupport 类的方法:
-
- park() 和 parkNanos(long nanos):阻塞当前线程,使其进入等待状态,可以通过 unpark(Thread thread) 方法唤醒指定线程。
- Thread 类的静态方法:
-
- yield():提示调度器当前线程愿意放弃当前的 CPU 执行时间,使当前线程从运行状态转为就绪状态,但不一定会立即执行。
4. Thread.sleep() 方法和 Object.wait() 方法对比
共同点:两者都可以暂停线程的执行。
区别:
- sleep() 方法没有释放锁,而 wait() 方法释放了锁 。
- wait() 通常被用于线程间交互/通信,sleep()通常被用于暂停执行。
- wait() 方法被调用后,线程不会自动苏醒,需要别的线程调用同一个对象上的 notify()或者 notifyAll() 方法。sleep()方法执行完成后,线程会自动苏醒,或者也可以使用 wait(long timeout) 超时后线程会自动苏醒。
- sleep() 是 Thread 类的静态本地方法,wait() 则是 Object 类的本地方法。为什么这样设计呢?下一个问题就会聊到。
5. 为什么 wait() 方法不定义在 Thread 中?
wait()
是让获得对象锁的线程实现等待,会自动释放当前线程占有的对象锁。每个对象(Object)都拥有对象锁,既然要释放当前线程占有的对象锁并让其进入 Waiting状态,自然是要操作对应的对象(Object)而非当前的线程(Thread)。
类似的问题:为什么 sleep() 方法定义在 Thread 中?
因为 sleep() 是让当前线程暂停执行,不涉及到对象类,也不需要获得对象锁。
6. 可以直接调用 Thread 类的 run 方法吗?
new 一个 Thread,线程进入了新建状态。调用 start()方法,会启动一个线程并使线程进入了就绪状态,当分配到时间片后就可以开始运行了。 start() 会执行线程的相应准备工作,然后自动执行 run() 方法的内容,这是真正的多线程工作。 但是,直接执行 run() 方法,会把 run() 方法当成一个 main 线程下的普通方法去执行,并不会在某个线程中执行它,所以这并不是多线程工作。
总结:调用 start() 方法方可启动线程并使线程进入就绪状态,直接执行 run() 方法的话不会以多线程的方式执行。
7. 创建线程的方法
7.1. 继承Thread类
- 定义一个类,继承自 Thread 类。
- 重写 run() 方法,该方法包含线程的代码逻辑。
- 创建该类的实例,并调用 start() 方法启动线程。
示例代码:
//1.继承Thread类
public class Client {
public static void main(String[] args) {
Thread thread = new MyThread();
thread.start();
}
}
class MyThread extends Thread {
@Override
public void run() {
System.out.println("继承Thread类创建线程");
}
}
7.2. 实现Runnable接口
- 定义一个类,实现 Runnable 接口。
- 实现 run() 方法,该方法包含线程的代码逻辑。
- 创建 Runnable 实例,将其作为参数传递给 Thread 类的构造函数。
- 调用 Thread 实例的 start() 方法启动线程。
示例代码:
//2.实现Runnable接口
public class Client {
public static void main(String[] args) {
Runnable runnable = new MyRunnable();
Thread thread = new Thread(runnable);
thread.start();
}
}
class MyRunnable implements Runnable {
@Override
public void run() {
System.out.println("实现Runnable接口创建线程");
}
}
7.3. 实现Callable接口(带返回值的线程)
- 定义一个类,实现 Callable 接口,并指定泛型为希望返回的类型。
- 实现 call() 方法,该方法包含线程的代码逻辑,并返回一个值。
- 创建 Callable 实例,并使用 ExecutorService 的 submit() 方法提交任务。
- 调用 Future 对象的 get() 方法获取线程执行结果。
import java.util.concurrent.*;
class MyCallable implements Callable<String> {
public String call() {
// 线程执行的代码逻辑
return "MyCallable is running";
}
}
public class Client {
public static void main(String[] args) throws InterruptedException, ExecutionException {
//Executors 类:这是一个工厂类,提供了创建不同类型线程池的静态方法
ExecutorService executor = Executors.newSingleThreadExecutor();
Future<String> future = executor.submit(new MyCallable());
try {
String result = fu.get();//获取线程执行结果
} catch (InterruptedException e) {
throw new RuntimeException(e);
} catch (ExecutionException e) {
throw new RuntimeException(e);
}finally {
service.shutdown();//关闭线程池
}
}
}
汇总:
public class Client {
public static void main(String[] args) {
//1.继承Thread类
Thread t1 = new Thread() {
@Override
public void run() {
System.out.println("继承Thread类创建线程");
}
};
t1.start();
//2.实现Runnable接口
Thread t2 = new Thread(new Runnable() {
@Override
public void run() {
System.out.println("实现Runnable接口");
}
});
t2.start();
//3.实现Callable接口
ExecutorService executor = Executors.newSingleThreadExecutor();
Future<String> future = executor.submit(new Callable<String>(){
@Override
public String call() throws Exception {
return null;
}
});
try {
String result = future.get();
} catch (InterruptedException e) {
throw new RuntimeException(e);
} catch (ExecutionException e) {
throw new RuntimeException(e);
}finally {
executor.shutdown();//关闭线程池
}
}
}
8. 面试题:双线程交替打印数字
思路:
双线程交替打印有两种思路,一种是设置共享变量,控制当前线程应该打印哪个数,另一种是通过锁的互斥机制实现。第一种方法具备通用性,适用于各种线程同步问题,其原理是多个线程持有同一个锁,每个线程内部判断是否轮到自己执行,否则就等待;第二种方式只适用于两个线程交替执行的任务,其原理是通过wait和notify来实现线程间的通信,但两个线程都实现同一个Runnable接口,在Runnable接口中控制执行流程。
方法一(通用性方法):
public static void main(String[] args){
public static int state = 0;
public static Object lock = new Object();
@Test
public void testThread() {
Thread t1 = new Thread(new Runnable() {
@Override
public void run() {
try {
synchronized (lock) {
while (true) {
while (state % 2 != 0)
lock.wait();
System.out.println(Thread.currentThread().getName() + ":" + state);
state++;
Thread.sleep(1000);
lock.notifyAll();
}
}
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
});
Thread t2 = new Thread(new Runnable() {
@Override
public void run() {
try {
synchronized (lock) {
while (true) {
while (state % 2 != 1)
lock.wait();
System.out.println(Thread.currentThread().getName() + ":" + state);
state++;
Thread.sleep(1000);
lock.notifyAll();
}
}
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
});
t1.start();
t2.start();
}
方法二:
public static void main(String[] args){
Runnable runnable = new MyRunnable();
Thread t1 = new Thread(runnable);
Thread t2 = new Thread(runnable);
t1.start();
t2.start();
}
class MyRunnable implements Runnable {
private static int i = 1;
@Override
public void run() {
while (true) {
synchronized (this) {
notify();
System.out.println(Thread.currentThread().getName() + ": " + i++);
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
try {
wait();
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
}
}
}
9. 面试题:交替打印ABC
9.1. 方法一:使用synchronized和wait/notify
synchronized是Java中的一个关键字,用于实现对共享资源的互斥访问。wait和notify是Object类中的两个方法,用于实现线程间的通信。wait方法会让当前线程释放锁,并进入等待状态,直到被其他线程唤醒。notify方法会唤醒一个在同一个锁上等待的线程。
我们可以使用一个共享变量state来表示当前应该打印哪个字母,初始值为0。当state为0时,表示轮到A线程打印;当state为1时,表示轮到B线程打印;当state为2时,表示轮到C线程打印。每个线程在打印完字母后,需要将state加1,并对3取模,以便循环。同时,每个线程还需要唤醒下一个线程,并让自己进入等待状态。
public class PrintABC {
// 共享变量,表示当前应该打印哪个字母
private static int state = 0;
// 共享对象,作为锁和通信的媒介
private static final Object lock = new Object();
public static void main(String[] args) {
// 创建三个线程
Thread threadA = new Thread(new Runnable() {
@Override
public void run() {
try {
// 循环100次
for (int i = 0; i < 100; i++) {
// 获取锁
synchronized (lock) {
// 判断是否轮到自己执行
while (state % 3 != 0) {
// 不是则等待
lock.wait();
}
// 打印字母
System.out.println("A");
// 修改状态
state++;
// 唤醒下一个线程
lock.notifyAll();
}
}
} catch (InterruptedException e) {
e.printStackTrace();
}
}
});
Thread threadB = new Thread(new Runnable() {
@Override
public void run() {
try {
for (int i = 0; i < 100; i++) {
synchronized (lock) {
while (state % 3 != 1) {
lock.wait();
}
System.out.println("B");
state++;
lock.notifyAll();
}
}
} catch (InterruptedException e) {
e.printStackTrace();
}
}
});
Thread threadC = new Thread(new Runnable() {
@Override
public void run() {
try {
for (int i = 0; i < 100; i++) {
synchronized (lock) {
while (state % 3 != 2) {
lock.wait();
}
System.out.println("C");
state++;
lock.notifyAll();
}
}
} catch (InterruptedException e) {
e.printStackTrace();
}
}
});
// 启动三个线程
threadA.start();
threadB.start();
threadC.start();
}
}
9.2. 方法二:使用ReentrantLock和Condition
ReentrantLock是Java中的一个类,用于实现可重入的互斥锁。Condition是ReentrantLock中的一个接口,用于实现线程间的条件等待和唤醒。ReentrantLock可以创建多个Condition对象,每个Condition对象可以绑定一个或多个线程,实现对不同线程的精确控制。
我们可以使用一个ReentrantLock对象作为锁,同时创建三个Condition对象,分别绑定A、B、C三个线程。每个线程在打印字母之前,需要调用对应的Condition对象的await方法,等待被唤醒。每个线程在打印字母之后,需要调用下一个Condition对象的signal方法,唤醒下一个线程。
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.ReentrantLock;
public class PrintABC {
// 共享变量,表示当前应该打印哪个字母
private static int state = 0;
// 可重入锁
private static final ReentrantLock lock = new ReentrantLock();
// 三个条件对象,分别绑定A、B、C三个线程
private static final Condition A = lock.newCondition();
private static final Condition B = lock.newCondition();
private static final Condition C = lock.newCondition();
public static void main(String[] args) {
// 创建三个线程
Thread threaA = new Thread(new Runnable() {
@Override
public void run() {
try {
// 循环100次
for (int i = 0; i < 100; i++) {
// 获取锁
lock.lock();
try {
// 判断是否轮到自己执行
while (state % 3 != 0) {
// 不是则等待
A.await();
}
// 打印字母
System.out.println("A");
// 修改状态
state++;
// 唤醒下一个线程
B.signal();
} finally {
// 释放锁
lock.unlock();
}
}
} catch (InterruptedException e) {
e.printStackTrace();
}
}
});
Thread threaB = new Thread(new Runnable() {
@Override
public void run() {
try {
for (int i = 0; i < 100; i++) {
lock.lock();
try {
while (state % 3 != 1) {
B.await();
}
System.out.println("B");
state++;
C.signal();
} finally {
lock.unlock();
}
}
} catch (InterruptedException e) {
e.printStackTrace();
}
}
});
Thread threaC = new Thread(new Runnable() {
@Override
public void run() {
try {
for (int i = 0; i < 100; i++) {
lock.lock();
try {
while (state % 3 != 2) {
C.await();
}
System.out.println("C");
state++;
A.signal();
} finally {
lock.unlock();
}
}
} catch (InterruptedException e) {
e.printStackTrace();
}
}
});
// 启动三个线程
threaA.start();
threaB.start();
threaC.start();
}
}