线程面试题总结

78 阅读6分钟

1.线程

1.1 Java线程和os的现场关系

Java 线程和操作系统线程的关系:现在的 Java 线程的本质其实就是操作系统的线程一个 Java 程序的运行是 main 线程和多个其他线程同时运行

[5] Attach Listener //添加事件
[4] Signal Dispatcher // 分发处理给 JVM 信号的线程
[3] Finalizer //调用对象 finalize 方法的线程
[2] Reference Handler //清除 reference 线程
[1] main //main 线程,程序入口

1.2 线程和进程

线程与进程相似,但线程是一个比进程更小的执行单位。一个进程在其执行的过程中可以产生多个线程。与进程不同的是同类的多个线程共享进程的方法区资源,但每个线程有自己的程序计器虚拟机栈本地方法栈,所以系统在产生一个线程,或是在各个线程之间做切换工作时,负担要比进程小得多,也正因为如此,线程也被称为轻量级进程。

用户线程和内核线程分为三种模式:

  1. 一对一(一个用户线程对应一个内核线程)
  2. 多对一(多个用户线程映射到一个内核线程)
  3. 多对多(多个用户线程映射到多个内核线程)

其中在 Windows 和 Linux 等主流操作系统中,Java 线程采用的是一对一的线程模型,也就是一个 Java 线程对应一个系统内核线程


1.3进程线程关系和优缺点

image.png

线程是进程划分成的更小的运行单位。线程和进程最大的不同在于基本上各进程是独立的,而各线程则不一定,因为同一进程中的线程极有可能会相互影响。线程执行开销小,但不利于资源的管理和保护;而进程正相反。

  • 程序计数器独立是因为每个线程执行的位置不同,切换时候要获取当前的执行位置
  • 虚拟机栈: 每个 Java 方法在执行之前会创建一个栈帧用于存储局部变量表、操作数栈、常量池引用等信息。从方法调用直至执行完成的过程,就对应着一个栈帧在 Java 虚拟机栈中入栈和出栈的过程。
  • 本地方法栈: 和虚拟机栈所发挥的作用非常相似,区别是:虚拟机栈为虚拟机执行 Java 方法 (也就是字节码)服务,而本地方法栈则为虚拟机使用到的 Native 方法服务。 在 HotSpot 虚拟机中和 Java 虚拟机栈合二为一。
  • 为了保证线程中的局部变量不被别的线程访问到,虚拟机栈和本地方法栈是线程私有的
  • 堆和方法区是所有线程共享的资源,其中堆是进程中最大的一块内存,主要用于存放新创建的对象 (几乎所有对象都在这里分配内存),方法区主要用于存放已被加载的类信息、常量、静态变量、即时编译器编译后的代码等数据。

1.4 如何创建线程

继承Thread类,实现Runnable接口,实现callable接口,使用线程池,使用CompletableFuture类等

不管哪种方式,最终还是依赖于new Thread().start()创建线程

  • 线程和线程体:线程是个容器,线程体才是可运行的任务,类似runnable接口等是在创建线程体即多线程任务,这个任务需要绑定到Thread上,调用start执行
public class ExtendsThread extends Thread {
    @Override
    public void run() {
        System.out.println("1......");
    }

    public static void main(String[] args) {
        new ExtendsThread().start();
    }
}

public class ImplementsRunnable implements Runnable {
    @Override
    public void run() {
        System.out.println("2......");
    }

    public static void main(String[] args) {
        ImplementsRunnable runnable = new ImplementsRunnable();
        new Thread(runnable).start();
    }
}

public class ImplementsCallable implements Callable<String> {
    @Override
    public String call() throws Exception {
        System.out.println("3......");
        return "zhuZi";
    }

    public static void main(String[] args) throws Exception {
        ImplementsCallable callable = new ImplementsCallable();
        FutureTask<String> futureTask = new FutureTask<>(callable);
        new Thread(futureTask).start();
        System.out.println(futureTask.get());
    }
}

ThreadPoolExecutor poolB = new ThreadPoolExecutor(2, 3, 0,
                TimeUnit.SECONDS, new LinkedBlockingQueue<Runnable>(3),
                Executors.defaultThreadFactory(), new ThreadPoolExecutor.AbortPolicy());
        poolB.submit(()->{
            System.out.println("4B......");
        });
        poolB.shutdown();
        
        
        
  public static void main(String[] args) throws InterruptedException {
        CompletableFuture<String> cf = CompletableFuture.supplyAsync(() -> {
            System.out.println("5......");
            return "zhuZi";
        });
        // 需要阻塞,否则看不到结果
        Thread.sleep(1000);
    }

1.5 线程的生命周期和状态

New -> Runnable -> (Blocked/Waiting/Timed Waiting) -> Terminated

new新建状态:即new Thread()

Runnable可运行状态: thread.start(); // 线程现在处于RUNNABLE状态,但可能未立即运行,因为
时间片很短切换太快所以区分**ready和running没什么意义**

Blocked(阻塞状态):比如进入同步代码块synchronized时候,如果获取不到锁,就进入该状态

Waiting(等待状态):Object.wait()或Thread.join()并等待另一个线程的执行结果时进入waiting状态
,直到有其他线程调用notify()或notifyAll()来唤醒它。

Timed Waiting(超时等待状态):Thread.sleep()或Object.wait(long timeout)方法时,它进入
TIMED_WAITING状态,直到超时时间结束或被唤醒。

Terminated(终止状态):线程执行完成或者抛出未捕获的异常后会进入TERMINATED状态,表示线程的生命周期结束
/

image.png

1.6什么是线程上下文切换

线程有自己的运行信息(上下文),比如私有的程序计数器和栈,所以当线程退出占用cpu时候需要保存上下文,以便下次占用cpu执行时恢复现场

常见的线程退出cpu情境如下:

  • 主动让出cpu,比如sleep和wait
  • 时间片用完
  • 调用阻塞类型的系统中断,比如请求IO
  • 被终止或结束运行

1.7 Thread.sleep()和Object.wait()对比

Thread.java
 public static native void sleep(long millis) throws InterruptedException;
 
 Object.java
 public final native void wait(long timeout) throws InterruptedException;
Thread.sleep()Object.wait()
作用暂停当前线程指定时间进入wait状态,直到其他线程调用notify()notifyAll()唤醒它
场景模拟延时,不适合锁和线程间通信的场景生产者-消费者模式,或其他线程同步场景
锁机制不释放锁释放锁
恢复条件超时notify()notifyAll(),使用带时间参数的方法时超时恢复
使用位置不要求比如要同步块或方法中使用,因为会释放锁,所以线程必须先持有锁,否则报IllegalMonitorStateException
public class WaitNotifyExample {
    private static final Object lock = new Object();

    public static void main(String[] args) {
        Thread waitingThread = new Thread(() -> {
            synchronized (lock) {
                try {
                    System.out.println("Thread is waiting");
                    lock.wait(); // 线程等待
                    System.out.println("Thread resumed");
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        });

        Thread notifyingThread = new Thread(() -> {
            synchronized (lock) {
                System.out.println("Thread is notifying");
                lock.notify(); // 唤醒等待线程
            }
        });

        waitingThread.start();
        try {
            Thread.sleep(2000); // 主线程休眠2秒以确保waitingThread先运行
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        notifyingThread.start();
    }
}

Thread is waiting
(wait后释放对象锁,所以notifyingThread 可以占用锁lock进入同步代码块执行notify)
Thread is notifying
Thread resumed

1.8 为什么wait不放在Thread方法里

对象头里有锁信息(markword里有锁标志) ,因为锁是和对象关联的,wait要释放锁必然要操作对象头的锁信息操作,放在Object.java可以知道释放的是哪个对象的锁

而sleep只是让线程等待,不涉及对象和对象锁,所以放在Thread.java

1.9 可以直接调用Thread类的run方法吗?

new Thread后调用start进入就绪状态,线程被分配到时间片后就自动执行run()。

如果直接执行run方法相当于main线程下执行普通方法不是多线程执行

代码如下,直接调用run相当于在main线程执行

public class RunTest {
    public static void main(String[] args) {
        MyThread t1 = new MyThread();
//        t1.run(); // 直接调用 run()
        t1.start();
        Thread.sleep(1000);
        System.out.println("Main thread: " + Thread.currentThread().getName());
    }

}
class MyThread extends Thread {
    @Override
    public void run() {
        System.out.println("Running in: " + Thread.currentThread().getName());
    }
}
Running in: Thread-0
Main thread: main

public class RunTest {
    public static void main(String[] args) throws InterruptedException {
        MyThread t1 = new MyThread();
        t1.run(); // 直接调用 run()
//        t1.start();
        Thread.sleep(1000);
        System.out.println("Main thread: " + Thread.currentThread().getName());
    }

}
class MyThread extends Thread {
    @Override
    public void run() {
        System.out.println("Running in: " + Thread.currentThread().getName());
    }
}

Running in: main
Main thread: main

参考: javaguide.cn/java/concur…