多线程相关面试题

Posted 蒙面侠1024

tags:

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

多线程面试题

一、线程(一)

1. 什么是线程池?

线程池是一种线程的使用模式。创建若干个可执行的线程放入一个池(容器)中,有任务需要处理时,会提交到线程池中的任务队列,处理完之后线程并不会被销毁,而是仍然在线程池中等待下一个任务。

2. 线程池有什么优势?

第一:降低资源消耗。通过重复利用已创建的线程降低线程创建和销毁造成的消耗。

第二:提高响应速度。当任务到达时,任务可以不需要等到线程创建就能执行。

第三:提高线程的可管理性,线程是稀缺资源,如果无限制地创建,不仅会消耗系统资源,还会降低系统的稳定性,使用线程池可以进行统一分配、调优和监控。

3. Java中有几种线程池?

1、newFixedThreadPool创建一个指定工作线程数量的线程池。每当提交一个任务就创建一个工作线程,如果工作线程数量达到线程池初始的最大数,则将提交的任务存入到池队列中。

2、newCachedThreadPool创建一个可缓存的线程池。这种类型的线程池特点是:

1).工作线程的创建数量几乎没有限制(其实也有限制的,数目为Interger. MAX_VALUE), 这样可灵活的往线程池中添加线程。

2).如果长时间没有往线程池中提交任务,即如果工作线程空闲了指定的时间(默认为1分钟),则该工作线程将自动终止。终止后,如果你又提交了新的任务,则线程池重新创建一个工作线程。

3、newSingleThreadExecutor创建一个单线程化的Executor,即只创建唯一的工作者线程来执行任务,如果这个线程异常结束,会有另一个取代它,保证顺序执行(我觉得这点是它的特色)。单工作线程最大的特点是可保证顺序地执行各个任务,并且在任意给定的时间不会有多个线程是活动的 。

4、newScheduleThreadPool创建一个定长的线程池,而且支持定时的以及周期性的任务执行,类似于Timer。(这种线程池原理暂还没完全了解透彻)

4. 线程池的参数?

可以通过 ThreadPoolExecutor 类创建线程池。ThreadPoolExecutor 类有 4 个构造方法,其中最一般化的构造方法包含 7 个参数。

public ThreadPoolExecutor(int corePoolSize, int maximumPoolSize, 
long keepAliveTime, TimeUnit unit, BlockingQueue<Runnable> workQueue,
ThreadFactory threadFactory, RejectedExecutionHandler handler)

7 个参数的含义如下。

corePoolSize:核心线程数,定义了最少可以同时运行的线程数量,当有新的任务时就会创建一个线程执行任务,当线程池中的线程数量达到 corePoolSize 之后,到达的任务进入阻塞队列。

maximumPoolSize:最大线程数,定义了线程池中最多能创建的线程数量。

keepAliveTime:等待时间,当线程池中的线程数量大于 corePoolSize 时,如果一个线程的空闲时间达到 keepAliveTime 时则会终止,直到线程池中的线程数不超过 corePoolSize。

unit:参数 keepAliveTime 的单位。

workQueue:阻塞队列,用来存储等待执行的任务。

threadFactory:创建线程的工厂。

handler:当拒绝处理任务时的策略。

4. 多线程的优点和可能存在的问题

线程也称为轻量级进程,是程序执行的最小单位,线程间的切换和调度的成本远远小于进程,多个线程同时运行可以减少线程上下文切换的开销。多线程是开发高并发系统的基础,利用好多线程机制可以显著提高系统的并发能力和性能。

多线程并发编程并不总是能提高程序的执行效率和运行速度,而且可能存在一些问题,包括内存泄漏、上下文切换、死锁以及受限于硬件和软件的资源限制问题等。

二、线程(二)

1. 线程的创建方式?

有4种方式可以用来创建线程:

  • 继承Thread类
  • 实现Runnable接口
  • 应用程序可以使用Executor框架来创建线程池
  • 还有一种方式是实现Callable接口
    实现Runnable接口这种方式更受欢迎,因为这不需要继承Thread类。在应用设计中已经继承了别的对象的情况下,这需要多继承(而Java不支持多继承),只能实现接口。同时,线程池也是非常高效的,很容易实现和使用。
2. 线程的同步方式?
  • wait():使一个线程处于等待(阻塞)状态,并且释放所持有的对象的锁;
  • sleep():使一个正在运行的线程处于睡眠状态,是一个静态方法,调用此方法要处理InterruptedException异常;
  • notify():唤醒一个处于等待状态的线程,当然在调用此方法的时候,并不能确切的唤醒某一个等待状态的线程,而是由JVM确定唤醒哪个线程,而且与优先级无关;
  • notityAll():唤醒所有处于等待状态的线程,该方法并不是将对象的锁给所有线程,而是让它们竞争,只有获得锁的线程才能进入就绪状态;
3. 线程的状态有哪些

线程在运行的生命周期中的任何时刻只能是 6 种不同状态的其中一种。

  • 初始状态(NEW):线程已经构建,尚未启动。
  • 运行状态(RUNNABLE):包括就绪(READY)和运行中(RUNNING)两种状态,统称为运行状态。
  • 阻塞状态(BLOCKED):线程被锁阻塞。
  • 等待状态(WAITING):线程需要等待其他线程做出特定动作(通知或中断)。
  • 超时等待状态(TIME_WAITING):不同于等待状态,超时等待状态可以在指定的时间自行返回。
  • 终止状态(TERMINATED):当前线程已经执行完毕。
4. 怎么唤醒一个阻塞的线程

如果线程是因为调用了wait()、sleep()或者join()方法而导致的阻塞,可以中断线程,并且通过抛出InterruptedException来唤醒它;如果线程遇到了IO阻塞,无能为力,因为IO是操作系统实现的,Java代码并没有办法直接接触到操作系统。

5. 进程和线程的区别是什么?

进程是程序的一次执行过程,是系统运行程序的基本单位,因此进程是动态的。系统运行一个程序即为一个进程的创建、运行以及消亡的过程。

线程是比进程更小的执行单位。一个进程在其执行的过程中可以产生多个线程,多个线程共享进程的堆和方法区内存资源,每个线程都有自己的程序计数器、虚拟机栈和本地方法栈。由于线程共享进程的内存,因此系统产生一个线程或者在多个线程之间切换工作时的负担比进程小得多,线程也称为轻量级进程。

进程和线程最大的区别是,各进程是独立的,而各线程则不一定独立,因为同一进程中的多个线程极有可能会相互影响。线程执行开销小,但不利于资源的管理和保护,进程则相反。

6. 线程里的死锁是什么,如何避免?

线程死锁描述的是这样一种情况:多个线程同时被阻塞,它们中的一个或者全部都在等待某个资源被释放。由于线程被无限期地阻塞,因此程序不可能正常终止。

避免死锁就是在资源分配时,借助于算法(比如银行家算法)对资源分配进行计算评估,使其进入安全状态。

线程(三)

1. wait和sleep的区别是什么

sleep是线程类(Thread)的方法,导致此线程暂停执行指定时间,把执行机会给其他线程,但是监控状态依然保持,到时后会自动恢复。调用sleep不会释放对象锁。
wait是Object类的方法,对此对象调用wait方法导致本线程放弃对象锁,进入等待此对象的等待锁定池,只有针对此对象发出notify方法(或notifyAll)后本线程才进入对象锁定池准备获得对象锁进入运行状态。

2. lock和sychronized的区别

synchronized是Java的关键字,当它用来修饰一个方法或者一个代码块的时候,能够保证在同一时刻最多只有一个线程执行该段代码。JDK1.5以后引入了自旋锁、锁粗化、轻量级锁,偏向锁来有优化关键字的性能。
Lock是一个接口,而synchronized是Java中的关键字,synchronized是内置的语言实现;synchronized在发生异常时,会自动释放线程占有的锁,因此不会导致死锁现象发生;而Lock在发生异常时,如果没有主动通过unLock()去释放锁,则很可能造成死锁现象,因此使用Lock时需要在finally块中释放锁;Lock可以让等待锁的线程响应中断,而synchronized却不行,使用synchronized时,等待的线程会一直等待下去,不能够响应中断;通过Lock可以知道有没有成功获取锁,而synchronized却无法办到。

3. 说说synchronized关键字是怎么样保证线程安全的

在Java中,synchronized关键字是用来控制线程同步的,就是在多线程的环境下,控制synchronized代码段不被多个线程同时执行。
synchronized既可以加在一段代码上,也可以加在方法上。

4. volatile关键字的作用

对于可见性,Java提供了volatile关键字来保证可见性。
当一个共享变量被volatile修饰时,它会保证修改的值会立即被更新到主存,当有其他线程需要读取时,它会去内存中读取新值。
从实践角度而言,volatile的一个重要作用就是和CAS结合,保证了原子性,详细的可以参见java.util.concurrent.atomic包下的类,比如AtomicInteger。

5. CAS原理

CAS是compare and swap的缩写,即我们所说的比较交换。
cas是一种基于锁的操作,而且是乐观锁。在java中锁分为乐观锁和悲观锁。悲观锁是将资源锁住,等一个之前获得锁的线程释放锁之后,下一个线程才可以访问。而乐观锁采取了一种宽泛的态度,通过某种方式不加锁来处理资源,比如通过给记录加version来获取数据,性能较悲观锁有很大的提高。
CAS 操作包含三个操作数 —— 内存位置(V)、预期原值(A)和新值(B)。如果内存地址里面的值和A的值是一样的,那么就将内存里面的值更新成B。CAS是通过无限循环来获取数据的,若果在第一轮循环中,a线程获取地址里面的值被b线程修改了,那么a线程需要自旋,到下次循环才有可能机会执行。
java.util.concurrent.atomic 包下的类大多是使用CAS操作来实现的( AtomicInteger,AtomicBoolean,AtomicLong)。

6. synchronized、volatile、CAS比较

synchronized是悲观锁,属于抢占式,会引起其他线程阻塞。
volatile提供多线程共享变量可见性和禁止指令重排序优化。
CAS是基于冲突检测的乐观锁(非阻塞)

7. 什么是AQS

AQS是AbustactQueuedSynchronizer的简称,它是一个Java提高的底层同步工具类,用一个int类型的变量表示同步状态,并提供了一系列的CAS操作来管理这个同步状态。

AQS是一个用来构建锁和同步器的框架,使用AQS能简单且高效地构造出应用广泛的大量的同步器,比如我们提到的ReentrantLock,Semaphore,其他的诸如ReentrantReadWriteLock,SynchronousQueue,FutureTask等等皆是基于AQS的。

8. 什么是自旋?

很多synchronized里面的代码只是一些很简单的代码,执行时间非常快,此时等待的线程都加锁可能是一种不太值得的操作,因为线程阻塞涉及到用户态和内核态切换的问题。既然synchronized里面的代码执行得非常快,不妨让等待锁的线程不要被阻塞,而是在synchronized的边界做忙循环,这就是自旋。如果做了多次忙循环发现还没有获得锁,再阻塞,这样可能是一种更好的策略。

9. 什么是乐观锁和悲观锁?

1)乐观锁:就像它的名字一样,对于并发间操作产生的线程安全问题持乐观状态,乐观锁认为竞争不总是会发生,因此它不需要持有锁,将比较-替换这两个动作作为一个原子操作尝试去修改内存中的变量,如果失败则表示发生冲突,那么就应该有相应的重试逻辑。

2)悲观锁:还是像它的名字一样,对于并发间操作产生的线程安全问题持悲观状态,悲观锁认为竞争总是会发生,因此每次对某资源进行操作时,都会持有一个独占的锁,就像synchronized,不管三七二十一,直接上了锁就操作资源了。

以上是关于多线程相关面试题的主要内容,如果未能解决你的问题,请参考以下文章

多线程系列面试题4 - 常见的锁有哪些,使用过么,简单说说

IOS面试题(多线程) --- 锁

记一个Java多线程相关的面试题

金九银十:线程多线程,线程池面试题十连问!

JAVA面试题:多线程

Java面试题(多线程篇)