Java 面试题 - 多线程

多线程

1.并行和并发有什么区别

1)并行是指两个或者多个事件在同一时刻发生;而并发是指两个或多个事件在同一时间间隔发生。
2)并行是在不同实体上的多个事件,并发是在同一实体上的多个事件。
3)在一台处理器上“同时”处理多个任务,在多台处理器上同时处理多个任务。如hadoop分布式集群。
所以并发编程的目标是充分的利用处理器的每一个核,以达到最高的处理性能。

2.线程和进程的区别

简而言之,进程是程序运行和资源分配的基本单位,一个程序至少有一个进程,一个进程至少有一个线程。进程在执行过程中拥有独立的内存单元,而多个线程共享内存资源,减少切换次数,从而效率更高。线程是进程的一个实体,是cpu调度和分派的基本单位,是比程序更小的能独立运行的基本单位。同一进程中的多个线程之间可以并发执行。

3.守护线程是什么

守护线程,是个服务线程,准确地来说就是服务其他的线程。

4.创建线程有那几种方式

①. 继承Thread类创建线程类

  • 定义Thread类的子类,并重写该类的run方法,该run方法的方法体就代表了线程要完成的任务。因此把run()方法称为执行体。
  • 创建Thread子类的实例,即创建了线程对象。
  • 调用线程对象的start()方法来启动该线程。

②. 通过Runnable接口创建线程类

  • 定义runnable接口的实现类,并重写该接口的run()方法,该run()方法的方法体同样是该线程的线程执行体。
  • 创建 Runnable实现类的实例,并依此实例作为Thread的target来创建Thread对象,该Thread对象才是真正的线程对象。
  • 调用线程对象的start()方法来启动该线程。

③. 通过Callable和Future创建线程

  • 创建Callable接口的实现类,并实现call()方法,该call()方法将作为线程执行体,并且有返回值。
  • 创建Callable实现类的实例,使用FutureTask类来包装Callable对象,该FutureTask对象封装了该Callable对象的call()方法的返回值。
  • 使用FutureTask对象作为Thread对象的target创建并启动新线程。
  • 调用FutureTask对象的get()方法来获得子线程执行结束后的返回值。

5.说一下 runnable 和 callable 有什么区别

1)Runnable接口中的run()方法的返回值是void,它做的事情只是纯粹地去执行run()方法中的代码而已;
2)Callable接口中的call()方法是有返回值的,是一个泛型,和Future、FutureTask配合可以用来获取异步执行的结果。

6.线程有哪些状态

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

创建状态:在生成线程对象,并没有调用该对象的start方法,这是线程处于创建状态。
就绪状态:当调用了线程对象的start方法之后,该线程就进入了就绪状态,但是此时线程调度程序还没有把该线程设置为当前线程,此时处于就绪状态。在线程运行之后,从等待或者睡眠中回来之后,也会处于就绪状态。
运行状态:线程调度程序将处于就绪状态的线程设置为当前线程,此时线程就进入了运行状态,开始运行run函数当中的代码。
阻塞状态:线程正在运行的时候,被暂停,通常是为了等待某个时间的发生(比如说某项资源就绪)之后再继续运行。sleep,suspend,wait等方法都可以导致线程阻塞。
死亡状态:如果一个线程的run方法执行结束或者调用stop方法后,该线程就会死亡。对于已经死亡的线程,无法再使用start方法令其进入就绪

7.sleep() 和 wait() 有什么区别

sleep():方法是线程类(Thread)的静态方法,让调用线程进入睡眠状态,让出执行机会给其他线程,等到休眠时间结束后,线程进入就绪状态和其他线程一起竞争cpu的执行时间。因为sleep() 是static静态的方法,他不能改变对象的机锁,当一个synchronized块中调用了sleep() 方法,线程虽然进入休眠,但是对象的机锁没有被释放,其他线程依然无法访问这个对象。

wait():wait()是Object类的方法,当一个线程执行到wait方法时,它就进入到一个和该对象相关的等待池,同时释放对象的机锁,使得其他线程能够访问,可以通过notify,notifyAll方法来唤醒等待的线程。

8. notify()和 notifyAll()有什么区别

1)如果线程调用了对象的 wait() 方法,那么线程便会处于该对象的等待池中,等待池中的线程不会去竞争该对象的锁。

2)当有线程调用了对象的 notifyAll() 方法(唤醒所有 wait 线程)或 notify() 方法(只随机唤醒一个 wait 线程),被唤醒的的线程便会进入该对象的锁池中,锁池中的线程会去竞争该对象锁。也就是说,调用了 notify 后只要一个线程会由等待池进入锁池,而 notifyAll 会将该对象等待池内的所有线程移动到锁池中,等待锁竞争。

3)优先级高的线程竞争到对象锁的概率大,假若某线程没有竞争到该对象锁,它还会留在锁池中,唯有线程再次调用 wait() 方法,它才会重新回到等待池中。而竞争到对象锁的线程则继续往下执行,直到执行完了 synchronized 代码块,它会释放掉该对象锁,这时锁池中的线程会继续竞争该对象锁。

9. 线程的 run()和 start()有什么区别?

每个线程都是通过某个特定 Thread 对象所对应的方法 run() 来完成其操作的,方法 run() 称为线程体。通过调用 Thread 类的 start() 方法来启动一个线程。

start() 方法来启动一个线程,真正实现了多线程运行。这时无需等待 run 方法体代码执行完毕,可以直接继续执行下面的代码; 这时此线程是处于就绪状态, 并没有运行。 然后通过此 Thread 类调用方法 run() 来完成其运行状态, 这里方法 run() 称为线程体,它包含了要执行的这个线程的内容, Run 方法运行结束, 此线程终止。然后 CPU 再调度其它线程。

run() 方法是在本线程里的,只是线程里的一个函数,而不是多线程的。 如果直接调用 run() ,其实就相当于是调用了一个普通函数而已,直接待用 run() 方法必须等待 run() 方法执行完毕才能执行下面的代码,所以执行路径还是只有一条,根本就没有线程的特征,所以在多线程执行时要使用 start() 方法而不是 run() 方法。

10.线程池中 submit() 和 execute() 方法有什么区别

1)接收的参数一样
2)submit 有返回值,而 execute 没有
3)submit 方便 Exception 处理

11.在 java 程序中怎么保证多线程的运行安全

1)原子性:提供互斥访问,同一时刻只能有一个线程对数据进行操作(atomic,synchronization)
2)可见性:一个线程对主内存的修改可以即使地被其他线程看到(synchronization,volatile)
3)有序性:一个线程观察其他线程中的指令执行顺序,由于指令重排,该观察结果一般杂乱无章(happens-before 原则)

12.多线程锁升级原理是什么

在Java中,锁共有4种状态,级别从低到高依次为:无状态锁,偏向锁,轻量级锁和重量级锁状态,这几个状态会随着竞争情况逐渐升级。
锁可以升级但不能降级。
锁升级的图示过程:
请添加图片描述

13. 怎么防止死锁

死锁的四个必要条件:
1)互斥条件:进程对所分配到的资源不允许其他进程进行访问,若其他进程访问该资源,只能等待,直至占有该资源的进程使用完成后释放该资源
2)请求和保持条件:进程获得一定的资源之后,又对其他资源发出请求,但是该资源可能被其他进程占有,此事请求阻塞,但又对自己获得的资源保持不放
3)不可剥夺条件:是指进程已获得的资源,在未完成使用之前,不可被剥夺,只能在使用完后自己释放
4)环路等待条件:是指进程发生死锁后,若干进程之间形成一种头尾相接的循环等待资源关系
这四个条件是死锁的必要条件,只要系统发生死锁,这些条件必然成立,而只要上述条件之 一不满足,就不会发生死锁。

14. ThreadLocal 是什么,有哪些使用场景

线程局部变量是局限于线程内部的变量,属于线程自身所有,不在多个线程间共享。Java 提供 ThreadLocal 类来支持线程局部变量,是一种实现线程安全的方式。但是在管理环境下(如 web 服务器)使用线程局部变量的时候要特别小心,在这种情况下,工作线程的生命周期比任何应用变量的生命周期都要长。任何线程局部变量一旦在工作完成后没有释放,Java 应用就存在内存泄露的风险。

15.说一下 synchronized 底层实现原理

synchronized 可以保证方法或者代码块在运行时,同一时刻只有一个方法可以进入到临界区,同时它还可以保证共享变量的内存可见性。

Java中每一个对象都可以作为锁,这是 synchronized 实现同步的基础:
1)普通同步方法,锁是当前实例对象
2)静态同步方法,锁是当前类的 class 对象
3)同步方法块,锁是括号里面的对象

16. synchronized 和 volatile 的区别是什么

介绍
volatile 本质是在告诉jvm当前变量在寄存器(工作内存)中的值是不确定的,需要从主存中读取;
synchronized 则是锁定当前变量,只有当前线程可以访问该变量,其他线程被阻塞住。
区别
1)volatile 仅能使用在变量级别;synchronized 则可以使用在变量、方法、和类级别的。
2)volatile 仅能实现变量的修改可见性,不能保证原子性;而 synchronized 则可以保证变量的修改可见性和原子性。
3)volatile 不会造成线程的阻塞;synchronized 可能会造成线程的阻塞。
4)volatile 标记的变量不会被编译器优化;synchronized 标记的变量可以被编译器优化。

17. synchronized 和 Lock 有什么区别

首先 synchronized 是 java 内置关键字,在 jvm 层面,Lock 是个 java 类;
synchronized 无法判断是否获取锁的状态,Lock 可以判断是否获取到锁;
synchronized 会自动释放锁(a 线程执行完同步代码会释放锁 ;b 线程执行过程中发生异常会释放锁),Lock 需在 finally 中手工释放锁(unlock()方法释放锁),否则容易造成线程死锁;
用 synchronized 关键字的两个线程1和线程2,如果当前线程1获得锁,线程2线程等待。如果线程1阻塞,线程2则会一直等待下去,而 Lock 锁就不一定会等待下去,如果尝试获取不到锁,线程可以不用一直等待就结束了;
synchronized 的锁可重入、不可中断、非公平,而 Lock 锁可重入、可判断、可公平(两者皆可);
Lock 锁适合大量同步的代码的同步问题,synchronized 锁适合代码少量的同步问题。

18. synchronized 和 ReentrantLock 区别是什么

synchronized 是和if、else、for、while一样的关键字,ReentrantLock 是类,这是二者的本质区别。既然 ReentrantLock 是类,那么它就提供了比 synchronized 更多更灵活的特性,可以被继承、可以有方法、可以有各种各样的类变量,ReentrantLock 比 synchronized 的扩展性体现在几点上:

ReentrantLock 可以对获取锁的等待时间进行设置,这样就避免了死锁
ReentrantLock 可以获取各种锁的信息
ReentrantLock 可以灵活地实现多路通知
另外,二者的锁机制其实也是不一样的: ReentrantLock 底层调用的是 Unsafe 的 park 方法加锁,synchronized 操作的应该是对象头中 mark word。

19. 说一下 atomic 的原理

Atomic 包中的类基本的特性就是在多线程环境下,当有多个线程同时对单个(包括基本类型及引用类型)变量进行操作时,具有排他性,即当多个线程同时对该变量的值进行更新时,仅有一个线程能成功,而未成功的线程可以向自旋锁一样,继续尝试,一直等到执行成功。

Atomic 系列的类中的核心方法都会调用 unsafe 类中的几个本地方法。我们需要先知道一个东西就是 Unsafe 类,全名为:sun.misc.Unsafe,这个类包含了大量的对 C 代码的操作,包括很多直接内存分配以及原子操作的调用,而它之所以标记为非安全的,是告诉你这个里面大量的方法调用都会存在安全隐患,需要小心使用,否则会导致严重的后果,例如在通过 unsafe 分配内存的时候,如果自己指定某些区域可能会导致一些类似 C++ 一样的指针越界到其他进程的问题。

20.并发的三大特性

1)原子性
原子性是指在一个操作中cpu不可以在中途暂停然后再调度,即不被中断操作,要不全部执行完成,要不都不执行。
2)可见性
当多个线程访问同一个变量时,一个线程修改了这个变量的值,其他线程能够立即看得到修改的值。
3)有序性
虚拟机在进行代码编译时,对于那些改变顺序之后不会对最终结果造成影响的代码,虚拟机不一定会按照我们写的代码的顺序来执行,有可能将他们重排序

关键字:volatile、synchronized
volatile 本身就包含了禁止指令重排序的语义,而 synchronized 关键字是由“一个变量在同一时刻只允许一条线程对其进行 lock 操作”这条规则明确的。
synchronized关键字同时满足以上三种特性(原子性,可见性,有序性),但是 volatile 关键字不满足原子性。
在某些情况下,volatile 的同步机制的性能确实要优于锁(使用 synchronized 关键字或 java.util.concurrent 包里面的锁),因为volatile的总开销要比锁低。
我们判断使用 volatile 还是加锁的唯一依据就是 volatile 的语义能否满足使用的场景(原子性)

21.volatile

  1. 保证被volatile修饰的共享变量对所有线程总是可见的,也就是当一个线程修改了一个被volatile修饰共享变量的值,新值总是可以被其他线程立即得知。
//线程1
boolean stop = false;
while(!stop){
doSomething();
}
//线程2
stop = true;

如果线程2改变了stop 的值,线程1一定会停止吗?不一定。当线程2更改了 stop 变量的值之后,但是还没来得及写入主存当中,线程2转去做其他事情了,那么线程1由于不知道线程2对 stop 变量的更改,因此还会一直循环下去。

  1. 禁止指令重排序优化。
int a = 0;
bool flag = false;

public void write() {
    a = 2; //1
    flag = true; //2
}
public void multiply() {
    if (flag) { //3
        int ret = a * a;//4
    }
}

write 方法里的1和2做了重排序,线程1先对 flag 赋值为 true,随后执行到线程2,ret 直接计算出结果,再到线程1,这时候a才赋值为2,很明显迟了一步。
但是用 volatile 修饰之后就变得不一样了

第一:使用 volatile 关键字会强制将修改的值立即写入主存;
第二:使用 volatile 关键字的话,当线程2 进行修改时,会导致线程1的工作内存中缓存变量 stop 的缓存行无效(反映到硬件层的话,就是CPU的L1 或者 L2 缓存中对应的缓存行无效);
第三:由于线程1的工作内存中缓存变量stop的缓存行无效,所以线程1再次读取变量 stop 的值时会去主存读取。
inc++; 其实是两个步骤,先加加,然后再赋值。不是原子性操作,所以 volatile 不能保证线程安全。

22.为什么用线程池?解释下线程池参数

1、降低资源消耗;提高线程利用率,降低创建和销毁线程的消耗。
2、提高响应速度;任务来了,直接有线程可用可执行,而不是先创建线程,再执行。
3、提高线程的可管理性;线程是稀缺资源,使用线程池可以统一分配调优监控。

  • corePoolSize 代表核心线程数,也就是正常情况下创建工作的线程数,这些线程创建后并不会消除,而是一种常驻线程
  • maxinumPoolSize 代表的是最大线程数,它与核心线程数相对应,表示最大允许被创建的线程数,比如当前任务较多,将核心线程数都用完了,还无法满足需求时,此时就会创建新的线程,但是线程池内线程总数不会超过最大线程数
  • keepAliveTime 、 unit 表示超出核心线程数之外的线程的空闲存活时间,也就是核心线程不会消除,但是超出核心线程数的部分线程如果空闲一定的时间则会被消除,我们可以通过 setKeepAliveTime 来设置空闲时间
  • workQueue 用来存放待执行的任务,假设我们现在核心线程都已被使用,还有任务进来则全部放入队列,直到整个队列被放满但任务还再持续进入则会开始创建新的线程
  • ThreadFactory 实际上是一个线程工厂,用来生产线程执行任务。我们可以选择使用默认的创建工厂,产生的线程都在同一个组内,拥有相同的优先级,且都不是守护线程。当然我们也可以选择自定义线程工厂,一般我们会根据业务来制定不同的线程工厂
  • Handler 任务拒绝策略,有两种情况,第一种是当我们调用 shutdown 等方法关闭线程池后,这时候即使线程池内部还有没执行完的任务正在执行,但是由于线程池已经关闭,我们再继续想线程池提交任务就会遭到拒绝。另一种情况就是当达到最大线程数,线程池已经没有能力继续处理新提交的任务时,这是也就拒绝

23.线程池中阻塞队列的作用?为什么是先添加队列而不是先创建最大线程

1、一般的队列只能保证作为一个有限长度的缓冲区,如果超出了缓冲长度,就无法保留当前的任务了,阻塞队列通过阻塞可以保留住当前想要继续入队的任务。
阻塞队列可以保证任务队列中没有任务时阻塞获取任务的线程,使得线程进入wait状态,释放cpu资源。
阻塞队列自带阻塞和唤醒的功能,不需要额外处理,无任务执行时,线程池利用阻塞队列的take方法挂起,从而维持核心线程的存活、不至于一直占用cpu资源
2、在创建新线程的时候,是要获取全局锁的,这个时候其它的就得阻塞,影响了整体效率。
就好比一个企业里面有10个(core)正式工的名额,最多招10个正式工,要是任务超过正式工人数(task > core)的情况下,工厂领导(线程池)不是首先扩招工人,还是这10人,但是任务可以稍微积压一下,即先放到队列去(代价低)。10个正式工慢慢干,迟早会干完的,要是任务还在继续增加,超过正式工的加班忍耐极限了(队列满了),就的招外包帮忙了(注意是临时工)要是正式工加上外包还是不能完成任务,那新来的任务就会被领导拒绝了(线程池的拒绝策略)。

24.线程池中线程复用原理

线程池将线程和任务进行解耦,线程是线程,任务是任务,摆脱了之前通过 Thread 创建线程时的一个线程必须对应一个任务的限制。
在线程池中,同一个线程可以从阻塞队列中不断获取新任务来执行,其核心原理在于线程池对 Thread 进行了封装,并不是每次执行任务都会调用 Thread.start() 来创建新线程,而是让每个线程去执行一个“循环任务”,在这个“循环任务”中不停检查是否有任务需要被执行,如果有则直接执行,也就是调用任务中的 run 方法,将 run 方法当成一个普通的方法执行,通过这种方式只使用固定的线程就将所有任务的 run 方法串联起来。

25.CAS

CAS是一个原子操作是用于实现多线程同步的原子指令,它比较一个内存位置的值并且只有相等时修改这个内存位置的值为新的值,保证了新的值总是基于最新的信息计算的,如果有其他线程在这期间修改了这个值则CAS失败。CAS返回是否成功或者内存位置原来的值用于判断是否CAS成功

CAS 的 ABA问题
如线程1从内存X中取出A,这时候另一个线程2也从内存 X 中取出 A,并且线程2进行了一些操作将内存X中的值变成了 B,然后线程2又将内存 X 中的数据变成 A,这时候线程1进行 CAS 操作发现内存 X 中仍然是A,然后线程1操作成功。虽然线程1的 CAS 操作成功,但是整个过程就是有问题的。比如链表的头在变化了两次后恢复了原值,但是不代表链表就没有变化。

所以 JAVA 中提供了 AtomicStampedReference / AtomicMarkableReference 来处理会发生 ABA 问题的场景,主要是在对象中额外再增加一个标记来标识对象是否有过变更。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
1、面向对象的特征有哪些方面? 答:面向对象的特征主要有以下几个方面: - 抽象:抽象是将一类对象的共同特征总结出来构造类的过程, 包括数据抽象和行为抽象两方面。抽象只关注对象有哪些属性和行为, 并不关注这些行为的细节是什么。 - 继承:继承是从已有类得到继承信息创建新类的过程。 提供继承信息的类被称为父类(超类、基类); 得到继承信息的类被称为子类(派生类)。继承让变化中的软件系统有了一定的延续 性, 同时继承也是封装程序中可变因素的重要手段 (如果不能理解请阅读阎宏博士的《Java与模式》或《设计模式精解》中关于桥梁模 式的部分)。 - 封装:通常认为封装是把数据和操作数据的方法绑定起来, 对数据的访问只能通过已定义的接口。面向对象的本质就是 将现实世界描绘成一系列完全自治、封闭的对象。 我们在类中编写的方法就是对实现细节的一种封装; 我们编写一个类就是对数据和数据操作的封装。 可以说,封装就是隐藏一切可隐藏的东西, 只向外界提供最简单的编程接口 (可以想想普通洗衣机和全自动洗衣机的差别,明显全自动洗衣机封装更好因此操作 起来更简单; 我们现在使用的智能手机也是封装得足够好的,因为几个按键就搞定了所有的事 情)。 - 多态性:多态性是指允许不同子类型的对象对同一消息作出不同的响应。 简单的说就是用同样的对象引用调用同样的方法但是做了不同的事情。 多态性分为编译时的多态性和运行时的多态性。 总共90多道题目,包含面向对象、算法、多线程面试题及详解 大厂面试题集,纯人工手写,分享不易,有问题敬请谅解 。。。。。。。。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值