欢迎来到致知知识服务平台!

请登录 免费注册

服务热线:13164506307

当前位置:首页 > 编程/算法实现 > java

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

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

价格 50
评分 5.0 (0人评分) 销量: 收藏商品
数量
加入购物车 购买服务
服务详情


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

![在这里插入图片描述](https://img-blog.csdnimg.cn/20210522175819650.jpg?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3dlaXhpbl80MzU5MTk4MA==,size_16,color_FFFFFF,t_70#pic_center =400x400)

---

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

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

![在这里插入图片描述](https://img-blog.csdnimg.cn/20210219180203873.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3dlaXhpbl80MzU5MTk4MA==,size_16,color_FFFFFF,t_70#pic_center)

---

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

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

举例:

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

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

---

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

### 3.1 继承Thread

* 继承 *Thread*,重写里面`run()`方法,创建实例,执行`start()`方法。
* 优点:代码编写最简单直接操作。
* 缺点:**没返回值,继承一个类后,没法继承其他的类**,拓展性差。

```java
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*  实例传递进去启动。

```java
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()`方法,结合多个类比如 **FutureTask** 和 *Thread* 类

```java
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* 进行使用。

```java
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()`方法。
* *Thread* 和 *Runable* 没有返回值,*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* 的方法,唤醒在对象监视器上等待的全部线程,全部唤醒

### 线程状态转换流程图

![](https://img-blog.csdnimg.cn/img_convert/beaa7e48bc313a6c743066b8bd497414.png)

---

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

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

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

---

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

> 使用线程池的好处:

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

> 类别:

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

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

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

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

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

```java
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`:**创建新线程时使用的工厂**

* `handler`:*RejectedExecutionHandler* 是一个接口且只有一个方法,线程池中的数量大于 *maximumPoolSize*,对拒绝任务的处理策略,默认有 *4* 种策略:
  * `AbortPolicy`
  * `CallerRunsPolicy`
  * `DiscardOldestPolicy`
  * `DiscardPolicy`

---

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

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

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

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

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

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

图片参考:[https://joonwhee.blog.csdn.net/article/details/115364158]()

![](https://img-blog.csdnimg.cn/20200608092639652.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3YxMjM0MTE3Mzk=,size_16,color_FFFFFF,t_70)

---

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

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

---

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

![在这里插入图片描述](https://img-blog.csdnimg.cn/20210522180031226.gif#pic_center)

---

<font color=orange size=4>为了帮助更多小白从零进阶 Java 工程师,从CSDN官方那边搞来了一套 《Java 工程师学习成长知识图谱》,尺寸 `870mm x 560mm`,展开后有一张办公桌大小,也可以折叠成一本书的尺寸,有兴趣的小伙伴可以了解一下,当然,不管怎样博主的文章一直都是免费的~</font>
![在这里插入图片描述](https://img-blog.csdnimg.cn/20210524111209407.jpg?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3dlaXhpbl80MzU5MTk4MA==,size_16,color_FFFFFF,t_70#pic_center =400x)

服务评价

综合得分 5.0

服务态度: 5

工作速度: 5

完成质量: 5

(0人评论)