Java多线程 高频面试题

本文详细探讨了Java中的进程、线程概念,JVM内存区域,多线程的并发与并行,上下文切换,死锁防范,以及多线程的实现方法、synchronized和Lock比较,线程池原理及使用技巧,包括生产者消费者模式实例。通过理解这些概念,提升程序性能和线程管理能力。
摘要由CSDN通过智能技术生成

之前以为面经只是死记硬背的东西,后来发现记住了它们,对自己对知识的理解确实有帮助,难怪语文的文章老是要求背背背。

前言

这次的面经整理分为以下几个部分,希望对大家的工作有帮助。

内容链接地址
Java 基础
Java 集合
Java 多线程
Java 虚拟机
计算机网络
数据结构和算法
数据库
JavaWeb
设计模式
Spring、MyBatis

1 程序、进程和线程

  • 程序:程序是存储在磁盘或者其它的数据存储设备中的含有指令和数据的文件,也就是说程序是静态的代码;
  • 进程:进程是程序的一次执行过程,系统运行一个程序即是一个进程从创建到运行再到消亡的过程;
  • 线程:线程是一个比进程更小的执行单元,一个进程在执行的过程中可以产生多个线程。

(总结:进程就是程序的一次执行过程,线程则是比进程更小的执行单元,一个进程可以产生多个线程。)

线程和进程最大的不同在于各进程基本上是独立的,而同类的多个线程共享进程的堆和方法区资源,不过每个线程有自己的程序计数器、虚拟机栈和本地⽅法栈。同时,系统产⽣⼀个线程,或是在各个线程之间作切换⼯作时,负担要⽐进程⼩得多,也正因为如此,线程也被称为轻量级进程。

 

2 从 JVM 角度看进程和线程之间的关系

Java 的内存区域如下:
请添加图片描述
从上图可以看出:⼀个进程中可以有多个线程,多个线程共享进程的堆和⽅法区 (JDK1.8 之后叫做元空间)资源,但是每个线程有⾃⼰的程序计数器、虚拟机栈和本地⽅法栈。

2.1 程序计数器为什么是私有的

程序计数器有以下两个作用:

  1. 字节码解释器通过改变程序计数器来依次读取指令,从⽽实现代码的流程控制,如:顺序执⾏、选择、循环、异常处理;
  2. 在多线程的情况下,程序计数器⽤于记录当前线程执⾏的位置,从⽽当线程被切换回来的时候能够知道该线程上次运⾏到哪⼉了。

所以,程序计数器私有主要是为了线程切换后能恢复到正确的执行位置。

2.2 虚拟机栈和本地方法栈为什么是私有的

  • 虚拟机栈: 每个 Java ⽅法在执⾏的同时会创建⼀个栈帧⽤于存储局部变量表、操作数栈、常量池引⽤等信息。每一 Java 方法从调⽤⾄执⾏完成的过程,就对应着⼀个栈帧在 Java 虚拟机栈中入栈和出栈的过程。
  • 本地⽅法栈: 和虚拟机栈所发挥的作⽤⾮常相似,区别是: 虚拟机栈为虚拟机执⾏ Java ⽅法 (也就是字节码)服务,⽽本地⽅法栈则为虚拟机使⽤到的 Native ⽅法服务。

所以,虚拟机栈和本地方法栈是私有的是为了保证线程中的局部变量不被别的线程访问到。

 

3 多线程的概念

3.1 并发和并行的区别

  • 并发:同一时间段内,多个任务都在执行。(单位时间内不一定同时执行)
  • 并行:单位时间内,多个任务同时执行。

3.2 上下文切换

多线程编程中⼀般线程的个数都⼤于 CPU 核⼼的个数,⽽⼀个 CPU 核⼼在任意时刻只能被⼀个线程使⽤,为了让这些线程都能得到有效执⾏,CPU 采取的策略是为每个线程分配时间⽚并轮转的形式。当⼀个线程的时间片用完的时候就会重新处于就绪状态让给其他线程使⽤,这个过程就属于⼀次上下⽂切换。

概括来说就是:当前任务在执⾏完 CPU 时间⽚切换到另⼀个任务之前会先保存⾃⼰的状态,以便下次再切换回这个任务时,可以再加载这个任务的状态。任务从保存到再加载的过程就是⼀次上下⽂切换

上下⽂切换通常是计算密集型的。也就是说,它需要相当可观的处理器时间,在每秒⼏⼗上百次的切换中,每次切换都需要纳秒量级的时间。所以,上下⽂切换对系统来说意味着消耗⼤量的 CPU 时间,事实上,可能是操作系统中时间消耗最⼤的操作。

Linux 相⽐与其他操作系统(包括其他类 Unix 系统)有很多的优点,其中有⼀项就是,其上下⽂切换和模式切换的时间消耗⾮常少。

3.3 为什么使用多线程

  1. 在多 CPU 系统中,可以利用多线程将计算逻辑分配到多个处理器核心上,从而提高 CPU 的利用率;
  2. 改善程序结构。就是将一个即长又复杂的进程划分为多个线程来处理,使程序便于理解和修改。

多编程的⽬的就是为了能提⾼程序的执⾏效率、提⾼程序运⾏速度,然而多线程的缺点也有:线程的滥用会给系统带来上下文切换的额外负担,并且线程间的共享变量可能造成死锁的出现。

1 什么时候需要用到多线程

我认为当程序需要提高运行速度,同时改善程序结构时需要利用到多线程。

2 什么是线程安全

线程安全指的是当一个线程在操作一个方法或者语句时,其它线程不能对其进行操作,只能等到该线程结束后才可以进行访问。

3 能想到某个业务场景来需要线程安全吗?

一:比如生产者和消费者模式中,产品的数量就需要保证线程安全,否则当多个线程进行操作时,就可能导致脏读的发生,因为多个线程可能读到的是相同的值,从而导致产品总量超出。

3.4 线程的生命周期和状态

请添加图片描述

  • 一个线程在创建之后就会处于 NEW(新建)状态,此时调用 start() 方法后线程会进入到 RUNNABLE(运行)状态;
  • 当线程执行 wait() 方法后会进入到 WITTING(等待)状态,进入等待状态的线程需要依靠其它线程的通知才能够返回到运行状态;
  • 当使用 sleep(long time) 或者 wait(long time) 方法可以将线程置于 TIME_WAITING(超时等待)状态,等时间一结束就会自动进入到运行状态;
  • 当线程调用同步方法时,如果该线程没有获得锁的话,该线程就会进入到 BLOCK(阻塞)状态;
  • 一个线程结束之后就会进入到 TERMINATED(终止)状态;

3.5 什么是线程死锁?如何避免死锁?

死锁是指两个或多个线程互相持有对方所需要的资源,并请求锁定对方的资源,导致这些线程一直处于等待其它线程释放资源的状态。

死锁的必要条件

  1. 互斥条件:某个资源在任意⼀个时刻只由⼀个线程占用;
  2. 请求与保持条件:⼀个线程因请求资源⽽阻塞时,对已获得的资源保持不放;
  3. 不剥夺条件:线程已获得的资源在末使⽤完之前不能被其他线程强⾏剥夺,只有自己使⽤完毕后才释放资源;
  4. 循环等待条件:若干线程之间形成⼀种头尾相接的循环等待资源关系。

如何避免死锁

我上⾯说了产⽣死锁的四个必要条件,为了避免死锁,我们只要破坏产⽣死锁的四个条件中的其中⼀个就可以了。现在我们来挨个分析⼀下:

  1. 破坏互斥条件 :这个条件我们没有办法破坏,因为我们⽤锁本来就是想让他们互斥的;
  2. 破坏请求与保持条件 :⼀次性申请所有的资源;
  3. 破坏不剥夺条件 :占⽤部分资源的线程进⼀步申请其他资源时,如果申请不到,可以主动释放它占有的资源;
  4. 破坏循环等待条件:通过按照顺序来申请资源,按某⼀顺序申请资源,释放资源则反序释放。

 

4 多线程的使用

4.1 多线程的实现方法

创建多线程的方式有四种,分别是:继承 Thread 类、实现 Runnable 接口、实现 Callable 接口、使用线程池。

四种方式的差异如下:

创建多线程的方式特征
继承Thread类每次使用时,只需要实现继承了 Thread 类的子类就行:HelloThread h1 = new HelloThread();
实现Runnable接口使用时首先创建实现了接口的类的对象,然后将此对象作为参数传递到 Thread 类的构造器中,从而创建Thread类的对象 Window1 w = new Window1(); Thread t1 = new Thread(w);。相比于继承Thread类,1)实现的方式没有类的单继承性的局限性;2)降低了线程对象与线程任务之间的耦合性,增强了程序的可扩展性。
实现Callable接口相比于实现 Runnable 接口,1)call()可以有返回值的;2)call()可以抛出异常,被外面的操作捕获,获取异常的信息。而之前的方法只能是 try catch 捕获异常;3)Callable 是支持泛型的。
使用线程池用从学校到西湖举例,前三种方法创建多线程就是每次去都要造一辆自行车,骑到西湖后就把自行车销毁,而线程池的方法就是搭乘公共交通去西湖。因此,这一种是最常用的方法,好处是:1)提高响应速度(减少了创建新线程的时间);2)降低资源消耗(重复利用线程池中线程,不需要每次都创建);3)便于线程管理。

可以。所以,如果任务不需要返回结果或抛出异常推荐使用 Runnable 接⼝,这样代码看起来会更加简洁。

参考文献:传送门

4.2 sleep() ⽅法和 wait() 方法的区别

sleep()wait()
来源来自 Thread 类来自 Object 类
对锁的影响没有释放锁释放锁
使用范围任何地方只能在同步控制方法或者同步控制块里面使用,否则会抛 IllegalMonitorStateException
恢复方式时间到了之后线程会⾃动苏醒需要其他线程调用同一对象的 notify()/nofityAll() 才能重新恢复

1 sleep() 和 wait() 的区别

  1. sleep() 是 Tread 类的方法,而 wait() 是 Object 类的方法;
  2. sleep() 不会释放锁,而 wait() 会释放锁;
  3. sleep() 在任何方法里都可以使用,而 wait() / notify() 只能在同步方法中使用。

4.3 sleep() 方法和 yield() 方法的区别

线程执行 sleep() 方法后进入超时等待状态,而执行 yield() 方法后进入就绪状态。

sleep() 方法给其他线程运行机会时不考虑线程的优先级,因此会给低优先级的线程运行的机会;yield() 方法只会给相同优先级或更高优先级的线程以运行的机会。

4.4 join() 方法的作用

线程中的 join() 用来等待指定线程终止,然后继续执行当前线程。

比如线程 A 执行了 threadB.join() 之后,其含义就是线程 A 等待 threadB 线程终止之后才从 threadB.join() 继续往下执行自己的代码。

4.5 Thread 调用 start() 方法和调用 run() 方法的区别

  • run() :普通的方法调用,在主线程中执行,不会新建一个线程来执行;
  • start():新启动一个线程,这时此线程处于就绪(可运行)状态,一旦得到 CPU 时间片,就开始执行 run() 方法。

总结: 调⽤ start() ⽅法⽅可启动线程并使线程进⼊就绪状态,直接执⾏ run() ⽅法的话不会以多线程的⽅式执⾏。

4.6 synchronized 关键字

参考 Java 基础部分对其的讲解;
构造⽅法不能使⽤ synchronized 关键字修饰。构造⽅法本身就属于线程安全的,不存在同步的构造⽅法⼀说。

4.7 synchronized 和 Lock 的区别

  • Lock 是一个接口,而 synchronized 是 Java 中的关键字,synchronized 是内置的语言实现;
  • Lock 在发生异常时,如果没有主动通过 unLock() 去释放锁,很可能会造成死锁现象,因此使用 Lock 时需要在 finally 块中释放锁;synchronized 在发生异常时,会自动释放锁,因此不会导致死锁现象发生;
  • Lock 的使用更加灵活,可以有响应中断、有超时时间等;而 synchronized 却不行,使用 synchronized 时,等待的线程会一直等待下去,直到获取到锁;
  • 在性能上,随着近些年 synchronized 的不断优化,Lock 和 synchronized 在性能上已经没有很明显的差距了,所以性能不应该成为我们选择两者的主要原因。官方推荐尽量使用 synchronized,除非 synchronized 无法满足需求时,则可以使用 Lock。

1 synchronized 和 ReentrantLock 的异同

  • ReentrantLock 是 Lock 接口的一个实现类,而 synchronized 是 Java 中的关键字;
  • ReentrantLock 在发生异常时,需要手动声明释放锁,所以最好在finally中声明释放锁,而synchronized 在发生异常时,会自动释放锁,因此不会导致死锁现象发生;
  • ReentrantLock 更加灵活,提供的方法也更多。

4.8 synchronized 关键字和 volatile 关键字的区别

synchronized 关键字和 volatile 关键字是两个互补的存在,⽽不是对⽴的存在。

  • volatile 关键字是线程同步的轻量级实现,所以 volatile 性能肯定⽐ synchronized 关键字要好;
  • volatile 关键字只能⽤于变量,⽽ synchronized 关键字还可以修饰⽅法以及代码块;
  • volatile 关键字能保证数据的可⻅性,但不能保证数据的原⼦性。 synchronized 关键字两者都能保证;
  • volatile 关键字主要⽤于保证变量在多个线程之间的可⻅性,⽽ synchronized 关键字解决的是多个线程之间访问资源的同步性。

4.9 为什么要弄⼀个 CPU 高速缓存

CPU 缓存则是为了解决 CPU 处理速度和内存处理速度不对等的问题,

缓存是内存中少部分数据的复制品。

 

5 线程池

5.1 为什么使用线程池

池化技术的思想主要是为了减少每次获取资源的消耗,提⾼对资源的利⽤率。

使⽤线程池的好处:

  • 降低资源消耗:通过重复利用已创建的线程降低线程创建和销毁造成的消耗;
  • 提高响应速度:当任务到达时,任务可以不需要等到线程创建就能⽴即执⾏;
  • 提高线程的可管理性:线程是稀缺资源,如果⽆限制的创建,不仅会消耗系统资源,还会降低系统的稳定性,使⽤线程池可以进⾏统⼀的分配,调优和监控。

5.2 线程池的原理

请添加图片描述

5.3 线程池的代码实现

/**
 * 创建线程的方式四:使用线程池
 * 好处:
 * 1.提高响应速度(减少了创建新线程的时间)
 * 2.降低资源消耗(重复利用线程池中线程,不需要每次都创建)
 * 3.便于线程管理
 *      corePoolSize:核心池的大小
 *      maximumPoolSize:最大线程数
 *      keepAliveTime:线程没有任务时最多保持多长时间后会终止
 */
public class Pool {
    public static void main(String[] args) {
        //1. 提供指定线程数量的线程池
        ExecutorService service = Executors.newFixedThreadPool(10);
//        service1.setCorePoolSize(15);
//        service1.setKeepAliveTime();
        
        //2.执行指定的线程的操作。需要提供实现Runnable接口或Callable接口实现类的对象
        service.execute(new NumberThread());//适合适用于Runnable
        service.execute(new NumberThread1());//适合适用于Runnable
        
        //3.关闭连接池
        service.shutdown();
    }
}

class NumberThread implements Runnable{
    @Override
    public void run() {
       // 逻辑代码
    }
}

class NumberThread1 implements Runnable{
    @Override
    public void run() {
		// 逻辑代码
    }
}

5.4 线程池的核心属性

  • hreadFactory(线程工厂):用于创建工作线程的工厂;
  • corePoolSize(核心线程数):当线程池运行的线程少于corePoolSize 时,将创建一个新线程来处理请求,即使其他工作线程处于空闲状态;
  • workQueue(队列):用于保留任务并移交给工作线程的阻塞队列;
  • maximumPoolSize(最大线程数):线程池允许开启的最大线程数;
  • handler(拒绝策略):往线程池添加任务时,将在下面两种情况触发拒绝策略:1)线程池运行状态不是 RUNNING;2)线程池已经达到最大线程数,并且阻塞队列已满时;
  • keepAliveTime(保持存活时间):如果线程池当前线程数超过 corePoolSize,则多余的线程空闲时间超过 keepAliveTime 时会被终止。

5.5 线程池的拒绝策略(饱和策略)

  • AbortPolicy:中止策略。默认的拒绝策略,直接抛出 RejectedExecutionException。调用者可以捕获这个异常,然后根据需求编写自己的处理代码。

  • DiscardPolicy:抛弃策略。什么都不做,直接抛弃被拒绝的任务。

  • DiscardOldestPolicy:抛弃最老策略。抛弃阻塞队列中最老的任务,相当于就是队列中下一个将要被执行的任务,然后重新提交被拒绝的任务。如果阻塞队列是一个优先队列,那么“抛弃最旧的”策略将导致抛弃优先级最高的任务,因此最好不要将该策略和优先级队列放在一起使用。

  • CallerRunsPolicy:调用者运行策略。在调用者线程中执行该任务。该策略实现了一种调节机制,该策略既不会抛弃任务,也不会抛出异常,而是将任务回退到调用者(调用线程池执行任务的主线程),由于执行任务需要一定时间,因此主线程至少在一段时间内不能提交任务,从而使得线程池有时间来处理完正在执行的任务。

5.6 一些面试问题

1 线程池是什么

线程池是指在初始化应用程序的过程中创建的一个线程集合,之后每次执行新的任务的时候重用这些线程而非新建一个线程。作用的话就是提高资源的利用率、提高线程的可管理性。

2 线程池的过程

  1. 提交任务,判断核心线程数是否达到最大;
  2. 如果未达到最大,则执行任务;如果达到最大,则判断任务队列是否已满;
  3. 如果任务队列未满,则将任务添加到任务队列中等待线程执行;如果任务队列已满,则判断线程数是否达到最大线程数;
  4. 如果未达到最大线程数,则创建非核心线程执行任务;反之,则执行饱和策略。

3 线程池的构造方法有哪些参数

  • corePoolSize:核心线程数
  • maximumPoolSize:最大线程数
  • keepAliveTime:当核心线程已满时,闲置线程最长可以存活的时间
  • 保存任务的队列;
  • 饱和策略。

4 线程池的饱和策略

  • 终止策略,默认。抛出异常,让调用者捕获异常来处理;
  • 抛弃策略,直接抛弃被拒绝的任务;
  • 抛弃最老策略:抛弃下一个将被执行的任务;
  • 调用者运行策略。

5 线程池的种类

  • newCachedThreadPool:可缓存线程池;
  • newFixedThreadPool:可指定工作线程数量的线程池;
  • newScheduleThreadPool:定长的线程池;

 

6 生产者消费者模式

**生产者和消费者问题是线程模型中的经典问题:**生产者和消费者在同一时间段内共用同一个存储空间,生产者往存储空间中添加产品,消费者从存储空间中取走产品,当存储空间为空时,消费者阻塞,当存储空间满时,生产者阻塞。该储存空间可以当做是一个缓冲区。

关于生产者和消费者的参考文献:传送门

import java.lang.*;

/**
 * 生产者和消费者,wait()和notify()的实现
 */
public class Main {
    private static int count = 0;
    private static final int FULL = 10;
    private static final String LOCK = "lock";

    public static void main(String[] args) {
        Main test1 = new Main();
        // 如果下面两个类用静态内部类,那么下面的这条语句就可以修改为 new Thread(new Producer()).start();
        // 多个生产者和消费者共同作用
        new Thread(test1.new Producer()).start();
        new Thread(test1.new Consumer()).start();
        new Thread(test1.new Producer()).start();
        new Thread(test1.new Consumer()).start();
    }

    class Producer implements Runnable {
        @Override
        public void run() {
            // 如果是 synchronized(this) 表示用当前对象当锁
            while (true) {
                // 延时其实可以表示生产者生产的速度
                try {
                    Thread.sleep(50);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }

                synchronized (LOCK) {
                    // 反正用 if 的时候出错了
                    while (count == FULL) {
                        try {
                            // wait()和notify()都会释放锁,而sleep()不会
                            LOCK.wait();
                        } catch (Exception e) {
                            e.printStackTrace();
                        }
                    }
                    count++;
                    System.out.println(Thread.currentThread().getName() + "生产者生产,目前总共有" + count);
                    LOCK.notifyAll();
                }
            }
        }
    }

    class Consumer implements Runnable {
        @Override
        public void run() {
            while (true) {
                try {
                    Thread.sleep(50);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }

                synchronized (LOCK) {
                    while (count == 0) {
                        try {
                            LOCK.wait();
                        } catch (Exception e) {
                            e.printStackTrace();
                        }
                    }
                    count--;
                    System.out.println(Thread.currentThread().getName() + "消费者消费,目前总共有" + count);
                    LOCK.notifyAll();
                }
            }
        }
    }
}
评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值