(Java实习生)每日10道面试题打卡——Java多线程篇

Posted 兴趣使然の草帽路飞

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了(Java实习生)每日10道面试题打卡——Java多线程篇相关的知识,希望对你有一定的参考价值。

  • 临近秋招,备战暑期实习,祝大家每天进步亿点点!
  • 本篇总结的是Java多线程知识相关的面试题,后续会每日更新~

在这里插入图片描述


1、什么是进程、线程、协程,他们之间的关系是怎样的?

  • 进程: 本质上是一个独立执行的程序,进程是操作系统进行资源分配和调度的基本概念,操作系统进行资源分配和调度的一个独立单位。
  • 线程:是操作系统能够进行运算调度的最小单位。它被包含在进程之中,是进程中的实际运作单位。一个进程中可以并发多个线程,每条线程执行不同的任务,切换受系统控制。
  • 协程又称为微线程,是一种用户态的轻量级线程,协程不像线程和进程需要进行系统内核上的上下文切换,协程的上下文切换是由用户自己决定的,有自己的上下文,所以说是轻量级的线程,也称之为用户级别的线程就叫协程,一个线程可以多个协程,线程进程都是同步机制,而协程则是异步 。Java的原生语法中并没有实现协程,目前python、Lua和GO等语言支持
  • 三者的关系:
    • 一个进程可以有多个线程,它允许计算机同时运行两个或多个程序。线程是进程的最小执行单位,CPU的调度切换的是进程和线程,进程和线程多了之后调度会消耗大量的CPU,CPU上真正运行的是线程,线程可以对应多个协程:

在这里插入图片描述


2、请你说下并发和并行的区别,并举例说明?

  • 并发 concurrency一核CPU,模拟出来多条线程,快速交替执行
  • 并行 parallellism多核CPU ,多个线程可以同时执行
    • eg: 线程池!
  • 并发指在一段时间内宏观上去处理多个任务。并行指同一个时刻,多个任务确实真的同时运行。

举例:

并发:是一心多用,听课和看电影,但是CPU大脑只有一个,所以轮着来

并行:火影忍者中的影分身,有多个你出现,可以分别做不同的事情

3、请问java实现多线程有哪几种方式,有什么不同,比较常用哪种?

3.1 继承Thread

  • 继承 Thread,重写里面run()方法,创建实例,执行start()方法。
  • 优点:代码编写最简单直接操作。
  • 缺点:没返回值,继承一个类后,没法继承其他的类,拓展性差。
public class ThreadDemo1 extends Thread {
    @Override
    public void run() {
        System.out.println("继承Thread实现多线程,名称:"+Thread.currentThread().getName());
    }
}

public static void main(String[] args) {
      ThreadDemo1 threadDemo1 = new ThreadDemo1();
      threadDemo1.setName("demo1");
      // 执行start
      threadDemo1.start();
      System.out.println("主线程名称:"+Thread.currentThread().getName());
}

3.2 实现Runnable接口

  • 自定义类实现 Runnable,实现里面run()方法,创建 Thread 类,使用 Runnable 接口的实现对象作为参数传递给Thread 对象,调用strat()方法。
  • 优点:线程类可以实现多个几接口,可以再继承一个类。
  • 缺点:没返回值,不能直接启动,需要通过构造一个 Thread 实例传递进去启动。
public class ThreadDemo2 implements Runnable {
    @Override
    public void run() {
        System.out.println("通过Runnable实现多线程,名称:"+Thread.currentThread().getName());
    }
}

public static void main(String[] args) {
        ThreadDemo2 threadDemo2 = new ThreadDemo2();
        Thread thread = new Thread(threadDemo2);
        thread.setName("demo2");
    	// start线程执行
        thread.start();
        System.out.println("主线程名称:"+Thread.currentThread().getName());
}

// JDK8之后采用lambda表达式
public static void main(String[] args) {
    Thread thread = new Thread(() -> {
        System.out.println("通过Runnable实现多线程,名称:"+Thread.currentThread().getName());
    });
    thread.setName("demo2");
    // start线程执行
    thread.start();
    System.out.println("主线程名称:"+Thread.currentThread().getName());
}

3.3 实现Callable接口

  • 创建 Callable 接口的实现类,并实现call()方法,结合 FutureTask 类包装 Callable 对象,实现多线程。
  • 优点:有返回值,拓展性也高
  • 缺点:Jdk5以后才支持,需要重写call()方法,结合多个类比如 FutureTaskThread
public class MyTask implements Callable<Object> {
    @Override
    public Object call() throws Exception {
        System.out.println("通过Callable实现多线程,名称:"+Thread.currentThread().getName());
        return "这是返回值";
    }
}

public static void main(String[] args) {
    	// JDK1.8 lambda表达式
        FutureTask<Object> futureTask = new FutureTask<>(() -> {
          System.out.println("通过Callable实现多线程,名称:" +
                        			Thread.currentThread().getName());
            return "这是返回值";
        });

     	// MyTask myTask = new MyTask();
		// FutureTask<Object> futureTask = new FutureTask<>(myTask);
        // FutureTask继承了Runnable,可以放在Thread中启动执行
        Thread thread = new Thread(futureTask);
        thread.setName("demo3");
    	// start线程执行
        thread.start();
        System.out.println("主线程名称:"+Thread.currentThread().getName());
        try {
            // 获取返回值
            System.out.println(futureTask.get());
        } catch (InterruptedException e) {
            // 阻塞等待中被中断,则抛出
            e.printStackTrace();
        } catch (ExecutionException e) {
            // 执行过程发送异常被抛出
            e.printStackTrace();
        }
}

3.4 通过线程池创建线程

  • 自定义 Runnable 接口,实现 run()方法,创建线程池,调用执行方法并传入对象。
  • 优点:安全高性能,复用线程。
  • 缺点: Jdk5后才支持,需要结合 Runnable 进行使用。
public class ThreadDemo4 implements Runnable {
    @Override
    public void run() {
        System.out.println("通过线程池+runnable实现多线程,名称:" +
                           Thread.currentThread().getName());
    }
}

public static void main(String[] args) {
    	// 创建线程池
        ExecutorService executorService = Executors.newFixedThreadPool(3);
        for(int i=0;i<10;i++){
            // 线程池执行线程任务
            executorService.execute(new ThreadDemo4());
        }
        System.out.println("主线程名称:"+Thread.currentThread().getName());
        // 关闭线程池
        executorService.shutdown();
}
  • 一般常用的 Runnable 和 第 四种线程池 + Runnable,简单方便扩展,和高性能 (池化的思想)。

3.5 Runable Callable Thread 三者区别?

  • Thread 是一个抽象类,只能被继承,而 Runable、Callable 是接口,需要实现接口中的方法。
  • 继承 Thread 重写run()方法,实现Runable接口需要实现run()方法,而Callable是需要实现call()方法。
  • ThreadRunable 没有返回值,Callable 有返回值。
  • 实现 Runable 接口的类不能直接调用start()方法,需要 new 一个 Thread 并发该实现类放入 Thread,再通过新建的 Thread 实例来调用start()方法。
  • 实现 Callable 接口的类需要借助 FutureTask (将该实现类放入其中),再将 FutureTask 实例放入 Thread,再通过新建的 Thread 实例来调用start()方法。获取返回值只需要借助 FutureTask 实例调用get()方法即可!

4、请你说一下线程的几个状态(生命周期)?

线程通常有五种状态,新建、就绪、运行、阻塞和死亡状态:

  • 新建状态New):线程刚被创建,但尚未启动。如:Thread t = new Thread();
  • 就绪状态(Runnable):当调用线程对象的start()方法后,线程即进入就绪状态。处于就绪状态的线程,只是说明此线程已经做好了准备,随时等待CPU调度执行,并不是说执行了t.start()此线程立即就会执行。
  • 运行状态Running):当 CPU 开始调度处于就绪状态的线程时,此时线程才得以真正执行,即进入到运行状态。
  • 阻塞状态Blocked):处于运行状态中的线程由于某种原因,暂时放弃对CPU的使用权,停止执行,此时进入阻塞状态,直到其进入到就绪状态,才有机会再次被CPU调用以进入到运行状态。根据阻塞产生的原因不同,阻塞状态又可以分为三种:
    • 等待阻塞 :运行的线程执行wait()方法,该线程会释放占用的所有资源,JVM会把该线程放入“等待池”中。进入这个状态后,是不能自动唤醒的,必须依靠其他线程调用notify()notifyAll()方法才能被唤 醒,wait()Object 类的方法。
    • 同步阻塞:当线程处于运行状态时,试图获得某个对象的同步锁时,如果该对象的同步锁已经被其他线程占用,Java虚拟机就会把这个线程放到这个对象的锁池中。
    • 其他阻塞状态:当前线程执行了sleep()方法,或者调用了其他线程的join()方法,或者发出了 I/O 请求时,就会进入这个状态。线程会进入到阻塞状态。当sleep()状态超时、join()等待线程终止或者超时、或者 I/O处理完毕时,线程重新转入就绪状态。
  • 死亡状态Dead):线程执行完了或者因异常退出了run()方法,该线程结束生命周期。

5、请你说一下线程状态转换相关方法:sleep/yield/join wait/notify/notifyAll 的区别?

Thread下的方法:

  • sleep():属于线程 Thread 的方法,让线程暂缓执行,等待预计时间之后再恢复,交出CPU使用权,不会释放锁,抱着锁睡觉!进入超时等待状态TIME_WAITGING,睡眠结束变为就绪Runnable
  • yield():属于线程 Thread 的方法,暂停当前线程的对象,去执行其他线程,交出CPU使用权,不会释放锁,和sleep()类似,让相同优先级的线程轮流执行,但是不保证一定轮流
    • 注意:不会让线程进入阻塞状态 BLOCKED,直接变为就绪 Runnable,只需要重新获得CPU使用权。
  • join():属于线程 Thread 的方法,在主线程上运行调用该方法,会让主线程休眠,不会释放锁,让调用join()方法的线程先执行完毕,再执行其他线程。类似让救护车警车优先通过!!

Object下的方法:

  • wait():属于 Object 的方法,当前线程调用对象的 wait()方法,会释放锁,进入线程的等待队列,需要依靠notify()或者notifyAll()唤醒,或者wait(timeout)时间自动唤醒。
  • notify():属于 Object 的方法,唤醒在对象监视器上等待的单个线程,随机唤醒。
  • notifyAll():属于Object 的方法,唤醒在对象监视器上等待的全部线程,全部唤醒

线程状态转换流程图


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

run():普通的方法调用run()函数,在主线程中执行,不会新建一个线程来执行

start()新启动一个线程,这时此线程处于就绪(可运行)状态,并没有真正运行,一旦得到 CPU 时间片,就调用 run() 方法执行线程任务。


7、线程池的核心属性有哪些?

使用线程池的好处:

重用存在的线程,减少对象创建销毁的开销,有效的控制最大并发线程数,提高系统资源的使用率,同时避免过多资源竞争,避免堵塞,且可以定时定期执行、单线程、并发数控制,配置任务过多任务后的拒绝策略等功能。

类别:

  • newFixedThreadPool :一个定长线程池,可控制线程最大并发数。
  • newCachedThreadPool:一个可缓存线程池。
  • newSingleThreadExecutor:一个单线程化的线程池,用唯一的工作线程来执行任务。
  • newScheduledThreadPool:一个定长线程池,支持定时/周期性任务执行。

【阿里巴巴编码规范】 线程池不允许使用 Executors 去创建,要通过 ThreadPoolExecutor的方式原因?

Executors创建的线程池底层也是调用 ThreadPoolExecutor,只不过使用不同的参数、队列、拒绝策略等如果使用不当,会造成资源耗尽问题。直接使用ThreadPoolExecutor让使用者更加清楚线程池允许规则,常见参数的使用,避免风险。

常见的线程池问题:
1.newFixedThreadPool和newSingleThreadExecutor: 
	队列使用LinkedBlockingQueue,队列长度为 Integer.MAX_VALUE,可能造成堆积,导致OOM
2.newScheduledThreadPool和newCachedThreadPool:
    线程池里面允许最大的线程数是Integer.MAX_VALUE,可能会创建过多线程,导致OOM

ThreadPoolExecutor构造函数里面的参数,能否解释下各个参数的作用?

public ThreadPoolExecutor(int corePoolSize,
                          int maximumPoolSize,
                          long keepAliveTime,
                          TimeUnit unit,
                          BlockingQueue<Runnable> workQueue,
                          ThreadFactory threadFactory,
                          RejectedExecutionHandler handler)
  • corePoolSize核心线程数,线程池也会维护线程的最少数量,默认情况下核心线程会一直存活,即使没有任务也不会受存 keepAliveTime 控制!
    :在刚创建线程池时线程不会立即启动,到有任务提交时才开始创建线程并逐步线程数目达到 corePoolSize

  • maximumPoolSize线程池维护线程的最大数量,超过将被阻塞!
    :当核心线程满,且阻塞队列也满时,才会判断当前线程数是否小于最大线程数,才决定是否创建新线程

  • keepAliveTime非核心线程的闲置超时时间,超过这个时间就会被回收,直到线程数量等于 corePoolSize

  • unit:指定 keepAliveTime 的单位,如 TimeUnit.SECONDS、TimeUnit.MILLISECONDS

  • workQueue线程池中的任务队列,常用的是 ArrayBlockingQueue、LinkedBlockingQueue、SynchronousQueue

  • threadFactory创建新线程时使用的工厂

  • handlerRejectedExecutionHandler 是一个接口且只有一个方法,线程池中的数量大于 maximumPoolSize,对拒绝任务的处理策略,默认有 4 种策略:

    • AbortPolicy
    • CallerRunsPolicy
    • DiscardOldestPolicy
    • DiscardPolicy

8、你知道线程池有哪些拒绝策略吗?

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

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

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

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

9、请你简单描述一下线程池的运作流程

图片参考:https://joonwhee.blog.csdn.net/article/details/115364158


10、请问Java中可以有哪些方法来保证线程安全?

  • 加锁:比如synchronize/ReentrantLock。
  • 使用 volatile 声明变量,轻量级同步,不能保证原子性(需要解释)。
  • 使用线程安全类,例如原子类 AtomicXXX等。
  • 使用线程安全集合容器,例如:CopyOnWriteArrayList/ConcurrentHashMap等。
  • ThreadLocal本地私有变量/信号量 Semaphore等。

总结的面试题也挺费时间的,文章会不定时更新,有时候一天多更新几篇,如果帮助您复习巩固了知识点,还请三连支持一下,后续会亿点点的更新!

在这里插入图片描述


为了帮助更多小白从零进阶 Java 工程师,从CSDN官方那边搞来了一套 《Java 工程师学习成长知识图谱》,尺寸 870mm x 560mm,展开后有一张办公桌大小,也可以折叠成一本书的尺寸,有兴趣的小伙伴可以了解一下,当然,不管怎样博主的文章一直都是免费的~
在这里插入图片描述

以上是关于(Java实习生)每日10道面试题打卡——Java多线程篇的主要内容,如果未能解决你的问题,请参考以下文章

(Java实习生)每日10道面试题打卡——Java简单集合篇

(Java实习生)每日10道面试题打卡——Java简单集合篇

(Java实习生)每日10道面试题打卡——JavaWeb篇

(Java实习生)每日10道面试题打卡——JavaWeb篇

(Java实习生)每日10道面试题打卡——JVM篇

(Java实习生)每日10道面试题打卡——JVM篇