Java八股文面试之多线程篇

简介: Java八股文面试之多线程篇

线程有哪几种状态。

(1)NEW

线程至今尚未启动

(2)RUNNABLE

线程正在 Java 虚拟机中执行

(3)BLOCKED

受阻塞并等待获得同步代码块的锁

(4)WAITING

无限期地等待另一个线程来执行某一特定操作

(5)TIMED_WAITING

在指定的时间内等待另一个线程来执行某一特定操作

(6)TERMINATED

线程已退出

注意:RUNNABLE 是否真的运行取决于调度器,由操作系统内核来决定,可以细分为Ready就绪和Running运行

创建多线程有几种方法?

通过继承Thread类或者实现Runnable接口、Callable接口(JDK>=1.5)都可以实现多线程,不过实现Runnable接口与实现Callable

接口的方式基本相同,只是Callable接口里定义的方法返回值,可以声明抛出异常而已。因此,将实现Runnable接口

和实现Callable接口可以归为一种方式。

(1)实现Runnable、Callable接口的方式创建线程的优缺点

①优点:线程类只是实现了Runnable接口或者Callable接口,还可以继承其他类。这种方式下,多个线程可以共享一个target对象,所以非常适合多个相同线程来处理同一份资源的情况,从而可以将CPU、代码和数据分开,形成清晰的模型,较好的体现了面向对象的思想。

②缺点:编程稍微复杂一些,如果需要访问当前线程,则必须使用Thread.currentThread()方法

(2)继承Thread类的方式创建线程的优缺点

①优点:编写简单,如果需要访问当前线程,则无需使用Thread.currentThread()方法,直接使用this即可获取当前线程

②缺点:因为线程类已经继承了Thread类,java语言是单继承的,所以就不能再继承其他父类了。

如何停止一个正在运行的线程

(1)当线程退出后,会自动正常退出,也就是当run方法完成后线程终止

(2)使用stop方法强行终止,但是不推荐;因为stop方法是个废弃方法

(3)使用interrupt方法中断线程,但并没有真正的结束线程,所以一般用于中断正在休眠的线程

sleep()和wait() 有什么区别?

(1)sleep()方法属于Thread类,可以在任何地方调用;wait()方法属于Object类,需要和 synchronized 一起用。

(2)sleep()方法不会释放锁,线程会一直占用CPU资源。wait()方法会释放锁,让出CPU资源,并且线程进入等待状态,直到被其他线程唤醒。

(3)sleep()方法会自动苏醒,不需要其他线程唤醒。wait()方法需要被notify()或notifyAll()方法唤醒。

(4)sleep()方法通常用于让线程暂停执行一段时间,例如模拟延迟操作。wait()方法通常用于线程间通信和协作,例如生产者-消费者模型。

Thread 类中的start() 和 run() 方法有什么区别?

start()方法被用来启动新创建的线程,而且start()内部调用了run()方法,这和直接调用run()方法的

效果不一样。当你调用run()方法的时候,只会是在原来的线程中调用,没有新的线程启动,start()

方法才会启动新线程。

Thread类中的yield方法有什么作用?

yield方法可以暂停当前正在执行的线程对象,让其它有相同优先级的线程执行。它是一个静态方法

而且只保证当前线程放弃CPU占用而不能保证使其它线程一定能占用CPU,执行yield()的线程有可

能在进入到暂停状态后马上又被执行。

乐观锁和悲观锁的理解以及实现

(1)**乐观锁:**每次去拿数据的时候都认为别人不会修改,所以不会上锁,但是在更新的时候会判断

一下在此期间别人有没有去更新这个数据,可以使用版本号等机制。

(2)乐观锁的实现方式:

①使用版本标识来确定读到的数据与提交时的数据是否一致。提交后修改版本标识,不一致时可

以采取丢弃和再次尝试的策略。

②java中的Compare and Swap即CAS ,当多个线程尝试使用CAS同时更新同一个变量时,只有其

中一个线程能更新变量的值,而其它线程都失败,失败的线程并不会被挂起,而是被告知这次竞争

中失败,并可以再次尝试。 CAS 操作中包含三个操作数 —— 需要读写的内存位置(V)、进行比

较的预期原值(A)和拟写入的新值(B)。如果内存位置V的值与预期原值A相匹配,那么处理器会自

动将该位置值更新为新值B。否则处理器不做任何操作

**(3)悲观锁:**总是假设最坏的情况,每次去拿数据的时候都认为别人会修改,所以每次在拿数据

的时候都会上锁,这样别人想拿这个数据就会阻塞直到它拿到锁。

传统的关系型数据库里边就用到了很多这种锁机制,比如行锁,表锁等,读锁,写锁等,都是在做

操作之前先上锁。再比如Java里面的同步原语synchronized关键字的实现也是悲观锁。

(4)悲观锁的实现方式:synchronized和lock

①synchronized是Java关键字,而lock是一个类

②synchronized获取锁的时候,假设A线程获得锁,B等待,若A阻塞,B会自治等待;而lock在这种情况下

B会尝试去获取锁

③synchronized的锁的状态是无法判断的,而lock是可以判断的

④synchronized和lock都是对象锁,都支持重入锁

⑤lock可以实现synchronized不具备的特性,如响应中断,支持超时,公平锁和共享锁

(共享锁:在同一时刻,可以有多个线程用有锁;独占锁:synchronized,在任一时刻,只有一个线程可以拥有此锁)

有三个线程T1,T2,T3如何保证顺序执行

可以使用线程类的join()方法在一个线程中启动另一个线程,另外一个线程完成该线程继续执行。为了确保三个线程的

顺序应该先启动最后一个(T3调用T2,T2调用T1),这样T1就会先完成而T3最后完成。先启动三个线程中哪一个都行,因

为在每个线程的run()方法中都用join方法限定了三个线程的执行顺序。

public class JoinTest2 {
// 1.现在有T1、T2、T3三个线程,你怎样保证T2在T1执行完后执行,T3在T2执行完后执行
public static void main(String[] args) {
  final Thread t1 = new Thread(new Runnable() {
    @Override
    public void run() {
      System.out.println("t1");
  }
});
  final Thread t2 = new Thread(new Runnable() {
    @Override
    public void run() {
      try {
    // 引用t1线程,等待t1线程执行完
        t1.join();
      } catch (InterruptedException e) {
        e.printStackTrace();
      }
        System.out.println("t2");
    }
  });
  Thread t3 = new Thread(new Runnable() { 
  @Override
  public void run() {
    try {
      // 引用t2线程,等待t2线程执行完
      t2.join();
    } catch (InterruptedException e) {
      e.printStackTrace();
    }
    System.out.println("t3");
  }

线程池是什么?线程池ThreadPoolExecutor的参数分别表示什么?

https://editor.csdn.net/md/?articleId=128745907

常用的线程池有哪些?分别有什么特点?

(1)newSingleThreadExecutor:创建一个单线程池,此线程池保证所有任务的执行顺序按照任务的提交顺序执行

(2)newFixedThreadPool:是一种固定线程数量的线程池

特点:核心线程和最大线程数量都是一个固定的值。如果任务比较多工作线程处理不过来,就会加入到阻塞队列

里面等待。

(3)newCachedThreadPool:是一种可以缓存的线程池,它可以用来处理大量短期的突发流量

特点:①最大线程数是Interger.MaxValue②线程存活时间是60秒③阻塞队列用的是SynchronousQueue

(4)newScheduledThreadPool:具有延迟执行功能的线程池,可以用来实现实时调度

(5)newWorkStealingPool:Java8里面新加入的一个线程池,它内部会创建一个ForkJoinPool,利用工作窃取的算法并行处理请求

线程池的拒绝策略

JDK 提供了 4 种内置的拒绝策略:AbortPolicy、CallerRunsPolicy、DiscardOldestPolicy 和

DiscardPolicy;

①AbortPolicy:这是线程池默认的拒绝策略,当任务不能再提交的时候抛出异常,让开发人员及时知道程序运行状态,这样能在系统不能承载更大的并发量时,及时通过异常信息发现;

②DiscardPolicy:当任务不能再提交的时候直接丢弃任务,不抛出异常,这样可能会使我们无法发现系统的异

常状态,对于一些无关紧要的业务可以采用此策略;

③DiscardOldestPolicy:当任务不能再提交的时候丢弃任务队列中靠最前的任务,然后重新提交被拒绝的任务,对于一些允许丢弃老任务的业务可以采用此策略;

④CallerRunsPolicy: 当任务不能再提交的时候交由任务的调用线程(提交任务的线程,如main线程)来执行当前任务;这种拒绝策略会让所有任务都能得到执行,适合大量计算类型的任务执行。

⑤除了上面的四种拒绝策略,还可以通过实现 RejectedExecutionHandler 接口,实现自定义的拒绝策略

线程池的工作流程

提交任务后:

(1)判断当前线程池里的核心线程是否达到最大。

①如果不是,创建一个新的工作线程来执行任务。

②否则,则进入下个流程。

(2)判断线程池任务队列是否已满。

①如果任务队列没有满,则将任务添加到任务队列中等待线程执行。

②如果任务队列已满,则进入下个流程。

(3)判断线程池里的线程是否达到最大线程数。

①如果没有,则创建非核心线程执行任务。

②如果已满,则执行饱和策略。

ThreadLocal

说说ThreadLocal原理?

(1)ThreadLocal相当于一个线程本地变量,他会在每个线程中都创建一个副本;在线程之间访问内部

副本变量,就可以做到线程之间互相隔离,相比于synchronized它是在用空间来换时间。

(2)ThreadLocal中有一个静态内部类ThreadLocalMap,ThreadLocalMap又存在一个Entry数组,Entry具备保存key、value键值对的能力。Entry是一个弱引用,他的key是指向ThreadLocal的弱引用,

(3)使用弱引用的目的是为了防止内存泄露,如果使用的是强引用的话,除非线程结束否则ThreadLocal对象始终无

法被回收,使用弱引用的话会则在下一次GC的时候被回收,但是这样还是会存在内存泄露的问题。假如key和

ThreadLocal对象被回收之后,entry中存在key为null,但是value有值的entry对象,就会造成永远没办法被

访问到,除非线程运行结束。但是只要我们合理使用,在每次使用完之后都调用remove方法删除

Entry对象,实际上是不会出现这个问题的。

Thread、ThreadLocal、ThreadLocalMap关系

https://editor.csdn.net/md/?articleId=137903828

ThreadLocal会造成内存泄漏吗?如何避免内存泄漏

(1)ThreadLocal会造成内存泄漏吗?

①ThreadLocalMap内部维护了一个Entry类来存储键值对的映射关系,Entry将ThreadLocal作为Key,值作为value保存,它继承自弱引用WeakReference。由于ThreadLocal对象是弱引用,如果外部没有强引用指向它,它就会被GC回收,导致Entry的Key为null,如果这时value外部也没有强引用指向它,那么value就永远也访问不到了,按理也应该被GC回收,但是由于Entry对象还在强引用value,导致value无法被回收,这时「内存泄漏」就发生了,value成了一个永远也无法被访问,但是又无法被回收的对象。

(2)如何避免内存泄漏:

①将ThreadLocal变量存储的对象设置是不可变,因为不可变的对象不需要频繁更新

②尽量避免存储大对象,如果非要存,那么尽量在访问完成后及时调用remove()删除掉。

ThreadLocal的使用场景

(1)当我们在写API接口的时候,通常Controller层会接受来自前端的入参,当这个接口功能比较复杂的时候,可能我们调用的Service层内部还调用了 很多其他的很多方法,通常情况下,我们会在每个调用的方法上加上需要传递的参数。

但是如果我们将参数存入ThreadLocal中,那么就不用显式的传递参数了,而是只需要ThreadLocal中获取即可。

(2)当用户登录后,会将用户信息存入Token中返回前端,当用户调用需要授权的接口时,需要在header中携带 Token,然后拦截器中解析Token,获取用户信息,调用自定义的类(AuthNHolder)存入ThreadLocal中,当请求结束的时候,将ThreadLocal存储数据清空, 中间的过程无需在关注如何获取用户信息,只需要使用工具类的get方法即可。

(3)我们项目有个DateUtils工具类,这个工具类主要是对时间进行格式化,格式化/转化的实现是用的SimpleDateFormat。SimpleDateFormat不是线程安全的,所以我们就用ThreadLocal来让每个线程装载着自己的SimpleDateFormat对象

多线程有什么用?

(1)发挥多核CPU的优势

随着工业的进步,现在的笔记本、台式机乃至商用的应用服务器至少也都是双核的,4核、8核甚至

16核的也都不少见,如果是单线程的程序,那么在双核CPU上就浪费了50%,在4核CPU上就浪费

了75%。单核CPU上所谓的"多线程"那是假的多线程,同一时间处理器只会处理一段逻辑,只不过

线程之间切换得比较快,看着像多个线程"同时"运行罢了。多核CPU上的多线程才是真正的多线

程,它能让你的多段逻辑同时工作,多线程,可以真正发挥出多核CPU的优势来,达到充分利用

CPU的目的。

(2)防止阻塞

从程序运行效率的角度来看,单核CPU不但不会发挥出多线程的优势,反而会因为在单核CPU上运

行多线程导致线程上下文的切换,而降低程序整体的效率。但是单核CPU我们还是要应用多线程,

就是为了防止阻塞。试想,如果单核CPU使用单线程,那么只要这个线程阻塞了,比方说远程读取

某个数据吧,对端迟迟未返回又没有设置超时时间,那么你的整个程序在数据返回回来之前就停止

运行了。多线程可以防止这个问题,多条线程同时运行,哪怕一条线程的代码执行读取数据阻塞,

也不会影响其它任务的执行。

(3)便于建模

这是另外一个没有这么明显的优点了。假设有一个大的任务A,单线程编程,那么就要考虑很多,

建立整个程序模型比较麻烦。但是如果把这个大的任务A分解成几个小任务,任务B、任务C、任务

D,分别建立程序模型,并通过多线程分别运行这几个任务,那就简单很多了。

死锁是什么?什么条件下会产出死锁,如何避免死锁?

(1)死锁,是指两个或者两个以上的线程在执行过程中,去争夺同一份共享资源导致相互等待无法继续执行。

(2)产生死锁的原因:

①第一个:互斥条件,共享资源a和b只能被一个线程占用;

②第二个:请求和保持条件,线程T1已经获取共享资源a,在等待共享资源b的时候,不释放共享资源a;

③第三个:不可抢占条件,其他线程不能强行抢占线程T1占有的资源;

④第四个:循环等待条件,线程T1等待线程T2占有的资源,线程T2等待线程T1占有的资源,这形成了循环等待。

(4)线程产生死锁之后,只能通过外部干预来解决问题,比如重启程序,或者Kill线程。所以,我们只能在写代码时规避死锁的产生。那么如何避免死锁产生呢?根据产生死锁的四个必要条件,我们只需要破坏其中任何一 个条件就可以解决。

①第一个互斥条件是没有办法被破坏的,因为它是互斥锁的基本约束。其他三个条件都可以通过人工干预来破坏。

②第二个请求保持条件,我们可以在首次执行一次性申请所有的资源,这样就不存在等待锁的问题了。

③第③个,对于不可抢占条件来说,占用部分资源的线程在进一步申请其他资源的时候如果申请不到,我们可以主动释放它占有的资源。这样不可抢占这个条件就被破坏了。

④第④个,对于循环等待条件来说,可以通过按序申请资源来预防死锁的产生。所谓按序申请,就是给资源编号,所有线程可以按照线性化的序号顺序去申请共享资源,先申请序号小的,再申请序号大的,这样循环等待自然就不存在了。

锁的升级过程(无锁->偏向锁->轻量级锁->重量级锁)

(1)偏向锁

大多数情况下,锁不仅不存在多线程竞争,而且总是由同一线程多次获得,为了让线程获得锁的代价更低而引入偏向锁。只有第一次使用CAS将线程ID设置到对象的Mark Word头,之后发现这个线程ID是自己就表示没有竞争,不用重新CAS,以后只要不发生竞争,这个对象就归该线程所有

(2)轻量级锁:

轻量级锁是由偏向所升级来的,如果一个对象虽然有多线程访问,但多线程访问的时间是错开的(也就是没有竞争),那么可以使用轻量级锁来优化。轻量级锁对使用者是透明的,即语法仍然是synchronized

(3)重量级锁

如果在尝试加轻量级锁的过程中,CAS操作无法成功,说明有其他线程为此对象已经加上了轻量级锁(有竞争),这时需要进行锁膨胀,将轻量级锁变为重量级锁

synchronized和lock锁的区别

(1)synchronized是关键字,Lock类是一个接口

(2)Lock只有代码块锁,synchronized有代码块锁和方法锁

(3)synchronized无法判断是否获取锁的状态,Lock可以判断是否获取到锁

(4)synchronized会自动释放锁(a:线程执行完同步代码会释放锁 ;b:线程执行过程中发生异常会释放锁);Lock需在finally中手工释放锁(unlock()方法释放锁),否则容易造成线程死锁

(5)用synchronized关键字修饰的两个线程1和线程2,如果线程1阻塞,线程2则会一直等待下去。Lock锁可以设置超时时间,不用一直等待下去。

(6)synchronized的锁是可重入、不可中断、非公平。Lock锁可重入、可中断、可公平(也可非公平)

(7)synchronized锁适合代码少量的同步问题,Lock锁适合大量代码的同步问题

CountDownLatch(倒计时锁)

用来进行线程同步协作,等待所有线程完成倒计时才恢复运行

public static void main(String[] args) throws InterruptedException {
  CountDownLatch latch = new CountDownLatch(3);
  ExecutorService service = Executors.newFixedThreadPool(4);
  service.submit(() -> {
    log.debug("begin...");
    sleep(1);
    latch.countDown();
    log.debug("end...{}", latch.getCount());
  });
  service.submit(() -> {
    log.debug("begin...");
    sleep(1.5);
    latch.countDown();
    log.debug("end...{}", latch.getCount());
  });
  service.submit(() -> {
    log.debug("begin...");
    sleep(2);
    latch.countDown();
    log.debug("end...{}", latch.getCount());
  });
  service.submit(()->{
    try {
      log.debug("waiting...");
      latch.await();
      log.debug("wait end...");
    } catch (InterruptedException e) {
      e.printStackTrace();
    }
  });
}

并发编程为什么有可见性问题

(1)一个线程对共享变量的修改,另外一个线程能够立刻看到,我们称为可见性.

(2) 在单核时代,线程都是在一个CPU 上执行,操作的是同一个 CPU 的缓存,一个线程对缓存的写,对另外一个线程来说一定是可见的.

(2) 在多核时代,线程会在不同的 CPU 上执行时,操作的是不同的 CPU 缓存;当出现 线程 A 操作的是 CPU-1上的缓存,而线程 B 操作的是 CPU-2上的缓存,这个时候线程 A 对共享变量 V 的操作对于线程 B 而言就不具备可见性了

volatile关键字如何解决的可见性问题

(1)Java代码的运行是先将Java代码编译成字节码,然后JVM加载字节码最终转换成汇编指令在CPU上运行,被volatile修饰的字段会在其对应的汇编操作指令上加个lock前缀指令,这个指令就可以解决上述的两种可见性问题。

(2)处理器在接收到lock指令的时候会做两件事:

①将缓存行缓存的数据写回到系统内存中。

②这个写回到系统内存中的数据,如果在其他CPU的缓存行中存在相同的数据,则将其置为失效状态。

目录
相关文章
|
1天前
|
Java 开发者 UED
Java中的并发编程:解锁多线程的力量
【7月更文挑战第7天】在Java的世界中,掌握并发编程是提升应用性能和响应能力的关键。本文将深入探讨如何在Java中高效地使用多线程,包括创建和管理线程、同步机制、以及避免常见的并发陷阱。我们将一起探索锁、线程池、并发集合等工具,并了解如何通过这些工具来优化程序的性能和稳定性。
|
3天前
|
存储 算法 Java
Java面试之SpringCloud篇
Java面试之SpringCloud篇
17 1
|
3天前
|
SQL 关系型数据库 MySQL
java面试之MySQL数据库篇
java面试之MySQL数据库篇
7 0
java面试之MySQL数据库篇
|
22小时前
|
Java
实现Java多线程中的线程间通信
实现Java多线程中的线程间通信
|
3天前
|
存储 Java Linux
Java面试之Linux和docker
Java面试之Linux和docker
10 0
|
3天前
|
缓存 NoSQL Java
Java面试之分布式篇
Java面试之分布式篇
9 0
|
10天前
|
算法 Java 调度
《面试专题-----经典高频面试题收集四》解锁 Java 面试的关键:深度解析并发编程进阶篇高频经典面试题(第四篇)
《面试专题-----经典高频面试题收集四》解锁 Java 面试的关键:深度解析并发编程进阶篇高频经典面试题(第四篇)
18 0
|
17天前
|
设计模式 SQL JavaScript
java面试宝典全套含答案
java面试宝典全套含答案
|
17天前
|
存储 设计模式 Java
java实习生面试题_java基础面试_java面试题2018及答案_java面试题库
java实习生面试题_java基础面试_java面试题2018及答案_java面试题库