Java面试题——第四篇(多线程)

1. sleep(0)的意义

他的意义在于 调用Thread.sleep(0)的当前线程确实的被冻结了一下,让其他线程有机会优先执行。相当于一个让位动作
在线程没退出之前,线程有三个状态:就绪态、运行态、等待态。sleep(n) 之所以在n秒内不会参与CPU竞争,是因为,当线程调用sleep(n)的时候,线程是由运行态转入等待态,线程被放入等待队列中,等待定时器n秒后的中断事件,当到达n秒计时后,线程才重新由等待态转入就绪态,被放入就绪队列中,等待队列中的线程是不参与cpu竞争的。只有就绪队列中的线程才会参与cpu竞争,所谓cpu调度,就是根据一定的算法,从就绪队列中选择一个线程来分配cpu事件。
sleep(0) 之所以马上回去参与cpu竞争,是因为调用sleep(0)后,因为0的原因,线程直接回到就绪队列,参与cpu竞争。

2. synchronized原理及应用

synchronized可以保证方法或者代码块在运行时,同一时刻只有一个方法可以进入到临界区,同时还可以保证共享变量的内存可见性。
Java中每一个对象都可以作为锁,这是sychronized实现同步的基础。

  • 普通同步方法:锁的是当前实例对象
  • 静态同步方法:锁的是当前类的class对象
  • 同步方法块:锁的是括号里面的对象。

同步方法块时使用monitorenter和monitorexit指令来实现,而对于同步方法使用acc_sychronized来完成。无论采用哪种方式,本质都是对一个对象的监视器进行获取,而这个获取过程是排他的,也就是同一时刻只能有一个线程获取到由synchronized所保护对象的监视器。任何一个对象都有自己的监视器,执行方法的线程必须先获取到该对象的监视器才能进入方法块和同步方法,而没有获取到监视器的线程将会被阻塞。

Java对象头和monitor是实现synchronized的基础,Java对象头中主要包括两部分数据,标记字段和类型指针。标记字段用于存储对象自身的运行时数据,包括哈希码、GC分代年龄、锁状态标志、偏向线程ID等。虚拟机通过类型指针来确定对象是哪个类的实例。

锁的主要存在四种状态,依次是:无锁、偏向锁、轻量级锁、重量级锁。他们会随着竞争的激烈而逐渐升级,注意锁可以升级不可以降级,这种策略是为了提高获得锁和释放锁的效率。

3. 用过哪些原子类,他们的原理是什么

Atomic包下的类,基本都是使用Unsafe实现的包装类,在底层就是CAS操作实现的。

4. 线程池的关闭方式有几种,各自的区别是什么

可以调用线程池的shutdown或者shutdownNow方法来关闭线程池。他们的原理是遍历线程池中的工作线程,然后逐个调用线程的interrupt方法来中断线程,所以 无法响应中断的任务可能永远无法终止

5. Spring的Controller是单例还是多例,怎么保证并发的安全

Spring中的Controller默认是单例的,也就是singleton模式。所以如果Controller中有一个私有变量a,所有请求到同一个Controller时,使用的变量a都是公用的,即若是某个请求修改了这个变量a,则在别的请求中能够读到这个修改的内容。

为了保证并发安全,常见有两种解决方法

  • 在Controller中使用ThreadLocal变量
  • 在Spring配置文件Controller中声明为scope=“prototype”,每次都创建新的controller,不再使用单例模式。

6. 如何实现一个并发安全的链表

  • 采用粗粒度锁,完全锁住链表。
  • 采用细粒度锁,只锁住需要修改的节点。
  • 利用CAS来修改节点。

7. 有哪些无锁的数据结构,怎么做

要实现一个线程安全的队列有两种方式:阻塞和非阻塞。阻塞队列就是锁的应用如sychronized、ReentrantLock。而无阻塞就是CAS算法的应用(无锁),比较常见的是ConcurrentLinkedQueue,这是一个基于链表节点的无边界的线程安全队列,采用FIFO原则对元素进行排序,采用CAS算法实现。

8. 对AbstractQueuedSychronizer的了解,其加锁以及解锁流程,独占锁和公平锁加锁有什么不同

  • AQS即队列同步器,是用来构建锁或者其他同步组件的基础框架,使用了一个int成员变量表示同步状态,通过内置的FIFO队列来完成资源获取线程的排队工作。
  • AQS使用一个int成员变量state来表示同步状态,当state>0时表示已经获取了锁,当state=0时表示释放了锁。他提供了三个方法getState()、setState(int newState)、compareAndSetState(int expect,int update)来对同步状态state进行操作,当然AQS对state的操作是安全的。
  • AQS通过内置的FIFO同步队列来完成线程获取资源的排队工作,如果当前线程获取同步状态失败时,AQS会将当前线程以及等待状态等信息构造成一个节点(Node)并将其加入队列,同时会阻塞当前线程,当同步状态释放时,则会把节点中的线程唤醒,使其在次尝试获取同步状态。

共享式获取和独占式获取最主要的区别在于同一时刻能否有多个线程同时获取到同步状态。读写锁ReentrantReadWriteLock,他的读取锁ReadLock是共享式的,但是他的WriteLock就是独占式的。

公平锁和非公平锁,公平的获取锁也就是等待时间最长的线程最优先获得锁。其关键在于获取锁的时候是否按照FIFO的顺序来。

9. ConcurrentLinkedQueue和LinkedBlockingQueue的用处和不同之处

ConcurrentLinkedQueue采用CAS操作Node节点(Node里的元素也使用volatile修饰)来保证元素的一致性。LinkedBlockingQueue使用一个独占锁来保证线程安全,然后用Condition来做阻塞操作。实现了先进先出特性,是作为生产者消费者的首选。

10. 谈谈对读写锁的理解

  • 和排他锁ReentrantLock不同,读写锁ReentrantReadWriteLock在同一时刻可以允许多个读线程访问,但是在写线程访问时,所有的读线程和其他写线程均被阻塞。读写锁维护了一个对锁,一个读锁和一个写锁,通过分离读锁和写锁,使得并发性相比一般的排他锁有了很大的提升。
  • 写锁是一个支持可重入的排他锁,写锁的状态获取最终会调用tryAcquire(int arg)方法,在判断重入时加入了一项条件:读锁是否存在,因为要确保写锁的操作对读锁是可见的,因此只有等读锁完全释放后,写锁才能够被当前线程所获取。一旦写锁获取了,所有其他读、写线程均会被阻塞。
  • 读锁是一个可重入的共享锁,他能够被多个线程同时获取,在没有其他写线程访问时,读锁总是获取成功。

11. 可以创建volatile数组吗

Java中可以创建volatile数组,不过只是一个指向数组的引用,而不是整个数组。如果改变引用指向的数组,将会收到volatile保护,但是,如果多个线程同时改变数组的元素,volatile标识符就不能起到保护作用。
同理,对于Pojo类,使用volatile修饰,只能保证这个引用的可见性,不能保证其内部的属性。

12. 一个线程池设计的最大线程数应该考虑哪些因素

要想合理的配置线程池的大小,首先要分析任务的特性,可以从以下几个角度分析

  • 任务的性质:计算密集型任务、IO密集型任务
  • 任务的优先级:高、中、低
  • 任务的执行时间:长、中、短
  • 任务的依赖性:是否依赖其他系统资源,如数据库操作。
  1. 在有N个CPU的系统上,计算密集型任务应配置尽可能少的线程,可以将线程池大小设置为N+1.
  2. IO密集型任务应该配置尽可能多的线程,因为IO操作不占用CPU,应加大线程数量,如线程大小设置为2N+1.
  3. 结论:线程等待时间所占比例越高,需要越多线程。线程CPU时间所占比例越高,需要越少线程。

13. 为什么线程池中线程等待时间所占比例越高,需要越多线程,CPU所占比例越高,需要越少线程

1. 线程等待时间所占比例高,需要越多线程

  • 减少等待延迟:当线程执行的任务包含大量的等待时间(如IO操作、数据库查询、网络通信等),单个线程在执行过程中会有大量时间处于非CPU执行状态。这种情况下,增加线程数量可以让更多的线程在等待时同时执行,从而减少因等待导致的延迟和资源闲置。
  • 提高系统吞吐量:在多线程环境下,每个线程都可能在等待外部资源时暂停执行,通过增加线程数量,即使每个线程都在等待,也能有更多的线程在处理不同的任务,从而增加系统的整体吞吐量。
  • 并行执行优势:在IO密集型应用中,并行执行多个线程可以减少总体执行时间,因为多个IO操作可以同时进行,不会相互阻塞。

2. 线程CPU时间所占比例高,需要越少线程

  • 避免上下文切换开销:当线程主要进行CPU密集型任务时,增加线程数量会增加CPU上下文切换的频率。上下文切换是CPU从一个线程切换到另一个线程时保存和恢复状态的过程,这一过程需要消耗一定的时间。
  • 提高CPU利用率:在CPU密集型任务中,每个线程都能有效利用CPU资源。此时,较少的线程就能保持较高的CPU利用率,无需引入额外的线程来减少等待时间。
  • 简化系统复杂性:过多的线程在CPU密集型任务中不仅不会带来性能提升,反而会增加系统的复杂性和管理难度。

14. Java中用到的线程调度算法是什么

分时调度模型和抢占式调度模型

  • 分时调度模型:指让所有的线程轮流获得cpu的使用权,并且平均分配每个线程占用的cpu的时间片。
  • 抢占式调度模型:优先让可运行池中优先级高的线程占用cpu,如果可运行池中的线程优先级相同,就随机选择一个线程,使其占用cpu。

15. 并发编程的三个重要特性

并发编程的三个重要特性分别为:原子性、可见性、有序性

16. 多线程中i++线程安全吗

不安全。i++不是原子性操作,i++分为读取i值,对i值加1,再赋值给i++。执行期中任何一步都有可能被其他线程抢占。

17. Java中创建线程有哪几种方式

  1. 继承Thread类创建线程
  2. 通过Runnable接口创建线程
  3. 通过Callable和Future创建线程
    在这里插入图片描述

18. Runnable和Callable有什么区别

Runnable接口不会返回值或者抛出检查异常,但是Callable接口可以。

19. 线程类的构造方法、静态块是被哪个线程调用的

假设Thread2中new了Thread1、main函数中new了Thread2.

  1. Thread2的构造方法、静态块是被main函数调用的,Thread2中的run方法是线程自己调用的。
  2. Thread1的构造方法、静态块是被Thread2调用的,Thread1中的run方法是线程自己调用的。

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

  1. 使用退出标志,使线程正常退出,也就是当run方法完成后线程终止。
  2. 使用stop方法强行终止,但是不推荐这个方法,因为stop和suspend、resume一样都是过期的方法。
  3. 使用interrupt方法中断线程。

21. stop和suspend方法为何不推荐使用

在Java中,stop和suspend方法是Thread类的两个方法,但是都不推荐使用,主要原因是他们可能导致严重的安全问题和死锁问题。

  1. stop方法不推荐使用原因
  • 不安全性:stop方法用于立即停止一个线程,但这会导致线程突然停止而无法完成清理工作。当线程被强制停止时,他可能正在执行关键任务。
  • 破坏对象状态:停止线程会使它解锁它已锁定的所有监视器。如果先前有这些监视器保护的任何对象处于不一致状态,则其他线程可能会以不一致状态查看这些对象,这可能导致程序出现行为异常。
  • 无法恢复:一旦线程被stop()方法停止。他就不能被重新启动或者恢复执行。
  1. suspend方法不推荐使用原因
  • 死锁风险:suspend方法用于暂停线程的执行,但他不会释放线程所持有的锁。如果一个线程在执行suspend时持有某个锁,而另一个线程在等待这个锁以继续执行,那么这两个线程可能会陷入思索状态,因为被暂停的线程不会释放锁,而等待的线程又无法继续执行。
  1. 使用interrupt方法:interrupt()方法用于中断线程,但他是安全的,因为他不会立即停止线程的执行,而是向线程发送一个中断信号。线程可以通过检查其中断状态来响应中断,并有序的清理资源和退出。

22. 谈谈对线程中断的理解

在Java中认为,一个线程不应该由其他线程来强制中断或者停止,所以一些会强制中断线程的方法如Thread.stop()、Thread.suspend()方法都已经被废弃。一般是通过调用thread.interrupt()方法来设置线程的中断标识。

  1. 如果线程是处于阻塞状态、会抛出InterruptedException异常,代码可以进行捕获,进行一些处理,例如Object.wait()、Thread.sleep()、BlockingQueue.put()、BlockingQueue.take()等。
  2. 如果线程是处于Runnable状态、也就是正常运行,调用thread.interrupt()只是会设置中断标识位,不会有什么其他操作。

23. 线程sleep()和yield()方法有什么区别

  1. sleep()方法给其他线程运行机会时不考虑线程的优先级,因此会给低优先级的线程以运行的机会,yield()方法只会给相同优先级或者更高优先级的线程以运行的机会。
  2. 线程执行sleep()进入阻塞状态,而执行yield方法后转入就绪状态。
  3. sleep()方法比yield()方法具有更好的可移植性。

24. 死锁和活锁的区别

死锁:指两个或者两个以上的线程在执行过程中,因争夺资源而导致的一种互相等待的现象,若无外力作用,他们都将无法推进下去。
活锁:任务或者执行者没有被阻塞、由于某些条件没有满足,导致一直重复尝试、失败、尝试、失败。

25. 什么是无锁

无锁,即没有对资源进行锁定,即所有的线程都能访问并修改同一个资源,但同时只有一个线程能修改成功。无锁典型的特点是一个修改操作在一个循环内进行,线程会不断的尝试修改共享资源。如果没有冲突就修改成功并退出否则就会继续下一次循环尝试。所以,如果有多个线程修改同一个值必定会有一个线程能修改成功。而其他修改失败的线程会不断尝试直到修改成功。

26. 乐观锁有什么缺点

  1. ABA问题
    如果一个变量V初次读取的时候是A值,并且在准备赋值的时候检查到它仍然是A值,那么我们就能说明它没有被其他线程修改过吗?很明显是不可能的。因为在这段时间内可能被修改为其他值,然后又改回A,那么CAS操作就会误认为它从来没有被修改过。
  2. 循环时间长开销大
    自旋CAS如果长时间不成功,会给CPU带来非常大的执行开销。如果JVM能支持处理器提供的pause指令那么效率就会有所提升。
  3. 只能保证一个共享变量的原子性操作
    CAS只对单个共享变量有效,当操作涉及多个共享变量时CAS无效,但是从JDK1.5开始,提供了AtomicReference类来保证引用对象之间的原子性。

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

在Java中,锁共有4种状态,级别从低到高分别为:无状态锁、偏向锁、轻量级锁、重量级锁。

  • synchronized锁升级原理在锁对象的对象头里有一个threadid字段,在第一次访问时候threadid为空,jvm会让其持有偏向锁,并将threadid设置为其线程id。再次进入的时候会判断threadid是否与其线程id一致。如果一致,则可以直接使用此对象,如果不一致,则升级偏向锁为轻量级锁,通过自旋循环一定次数来获取此锁,执行一定次数后,如果还没有正常获取到要使用的对象,此时就会把锁从轻量级升级为重量级锁,此过程就构成了synchronized锁升级
  • 锁升级目的:锁升级是为了降低锁带来的性能消耗。

28. wait方法和sleep方法的区别

  1. sleep是Thread类的静态方法、wait来自Object类。
  2. 最主要的是 sleep方法没有释放锁,而wait方法释放了锁。使得其他线程可以使用同步控制块或者方法。
  3. wait、notify、notifyAll只能在同步控制方法或者同步控制块里面使用,而sleep可以在任何地方使用。
  4. sleep必须捕获异常,wait、notify等不需要。

29. 为什么wait()、notify()、notifyAll()必须在同步方法或者代码块中被调用

当一个线程需要调用对象的wait方法时,这个线程必须拥有该对象的锁 ,接着他就会释放这个对象锁并进入等待状态直到其他线程调用这个对象上的notify()方法。同样的,当一个线程需要调用对象的notify()方法时,他会释放这个对象的锁。以便其他在等待的线程就可以得到这个对象锁。由于所有的这些方法都需要线程持有对象的锁,就只能通过同步来实现。

30. JUC包中的原子类是哪4类

  1. 基本类型
  • AtomicInteger
  • AtomicLong
  • AtomicBoolean
  1. 数组类型
  • AtomicIntegerArray
  • AtomicLongArray
  • AtomicReferenceArray
  1. 引用类型
  • AtomicReference
  • AtomicStampedReference
  1. 对象的属性修改类型
  • AtomicIntegerFiledUpdater
  • AtomicLongFieldUpdater

31. 线程池的状态

线程池有5种状态,Running、ShutDown、Stop、Tidying、Terminated

  • Running(运行状态): 正常状态,接收新的任务,处理等待队列中的任务。
  • ShutDown (关闭状态) : 不接受新的任务提交,但是会继续处理等待队列中的任务。
  • Stop(停止状态):不接受新的任务,不再处理等待队列中的任务,中断正在执行任务的线程。
  • Tidying(整理状态): 当线程池中所有任务都已经终止,线程数为0时,线程池就会进入Tidying状态。在这个过程中,线程会执行terminated钩子方法,terminated方法在ThreadPoolExecutor类中是空的,但用户可以在子类中重载该方法以实现特定的清理逻辑。
  • terminated(终止状态):当terminated()方法执行结束后,线程池就会进入terminated状态,表示线程池已经彻底关闭,且不能再重新启动。

32. 线程池ThreadpoolExecutor拒绝策略有哪些

  1. AbortPolicy:抛出异常,拒绝新任务
  2. CallerRunsPolicy:调用执行自己的线程运行任务,这种策略会降低对于新任务提交的速度,影响程序整体性能。
  3. DiscardPolicy:不处理新任务,直接丢弃
  4. DiscardOldestPolicy:丢弃最早的未处理新任务。

33. 高并发、任务执行时间短的业务怎样使用线程池

线程数设置为N+1,避免线程上下文的切换。

34. 并发不高、任务执行时间长的业务怎样使用线程池

  1. 假如任务执行时间长集中在IO操作上,增加线程池中线程数量,降低平均等待时间。
  2. 假如业务时间长集中在计算操作上,可以将线程池中的线程数设置的少一点,减少线程上下文切换。

35. ThreadLocal造成内存泄漏的原因?

ThreadLocalMap中使用的key为ThreadLocal弱引用,而value是强引用。所以,如果ThreadLocal没有被外部强引用的情况下,在垃圾回收的时候,key会被清理掉。value不会被清理掉。 这样一来,ThreadLocalMap就会出现key为null的Entry。ThreadLocalMap已经考虑了这种情况,在调用set()、get()、remove()时,会清理掉key为null的记录。使用完ThreadLocal方法后,最好手动调用remove方法

  • 8
    点赞
  • 26
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值