您的位置 首页 java

大厂面试题库之《Java多线程》含答案

1、 多线程 的几种实现方式

继承Thread、实现Runnable接口、创建有返回结果的 线程 (创建Callable线程,然后封装FutureTask任务,获取结果方法为阻塞)

2、什么是 线程安全

在多线程环境下,执行程序始终能得到预期的正确结果

3、volatile的原理,作用,能代替锁么

1)作用:保持内存可见性和防止指令重排序

2)原理:观察加入volatile关键字和没有加入volatile关键字时所生成的汇编代码发现,加入volatile关键字时,会多出一个 Lock 前缀指令”,lock前缀指令实际上相当于一个内存屏障(也称内存栅栏),提供保障有:

  • 确保指令重排序时不会将其后面的代码排到内存屏障之前
  • 确保指令重排序时不会将其前面的代码排到内存屏障之后
  • 确保在执行到内存屏障修饰的指令时前面的代码全部执行完成
  • 强制将线程工作内存中值的修改刷新到主内存中
  • 如果是写操作,则会导致其他线程工作内存中的缓存数据失效

3)不能代替锁,因为不能保证原子性,只能保证可见性和顺序性

4、画一个线程的生命周期状态图。

大厂面试题库之《Java多线程》含答案

线程生命周期图

5、sleep和wait的区别

1)。这两个方法来自不同的类,分别是Thread和Object

2)。最主要是sleep方法没有释放锁,而wait方法释放了锁,使得其他线程可以使用同步控制块或者方法。

3)。wait,notify和notifyAll只能在同步控制方法或者同步控制块里面使用,而sleep可以在

任何地方使用

  1. synchronized (x){
  2. x.notify()
  3. //或者wait()
  4. }

4)。sleep必须捕获异常,而wait,notify和notifyAll不需要捕获异常

5、sleep和sleep(0)的区别

Thread.Sleep(0) 并非是真的要线程挂起0毫秒,意义在于这次调用Thread.Sleep(0)的当前线程确实的被冻结了一下,让其他线程有机会优先执行。Thread.Sleep(0) 是你的线程暂时放弃cpu,也就是释放一些未用的时间片给其他线程或进程使用,就相当于一个让位动作。

6、Lock与Synchronized的区别

1)Lock是一个接口,而synchronized是 Java 中的关键字,synchronized是内置的语言实现;

  2)synchronized在发生异常时,会自动释放线程占有的锁,因此不会导致 死锁 现象发生;而Lock在发生异常时,如果没有主动通过unLock()去释放锁,则很可能造成死锁现象,因此使用Lock时需要在finally块中释放锁;

  3)Lock可以让等待锁的线程响应中断,而synchronized却不行,使用synchronized时,等待的线程会一直等待下去,不能够响应中断;

  4)通过Lock可以知道有没有成功获取锁,而synchronized却无法办到。

  5)Lock可以提高多个线程进行读操作的效率。

7、synchronized的原理是什么,一般用在什么地方

原理:通过反编译后代码可以看出:对于同步方法,JVM采用ACC_SYNCHRONIZED标记符来实现同步。 对于同步代码块。JVM采用monitorenter、monitorexit两个指令来实现同步。

方法级的同步是隐式的。同步方法的常量池中会有一个ACC_SYNCHRONIZED标志。当某个线程要访问某个方法的时候,会检查是否有ACC_SYNCHRONIZED,如果有设置,则需要先获得监视器锁,然后开始执行方法,方法执行之后再释放监视器锁。这时如果其他线程来请求执行方法,会因为无法获得监视器锁而被阻断住。值得注意的是,如果在方法执行过程中,发生了异常,并且方法内部并没有处理该异常,那么在异常被抛到方法外面之前监视器锁会被自动释放。

同步代码块使用monitorenter和monitorexit两个指令实现。可以把执行monitorenter指令理解为加锁,执行monitorexit理解为释放锁。 每个对象维护着一个记录着被锁次数的计数器。未被锁定的对象的该计数器为0,当一个线程获得锁(执行monitorenter)后,该计数器自增变为 1 ,当同一个线程再次获得该对象的锁的时候,计数器再次自增。当同一个线程释放锁(执行monitorexit指令)的时候,计数器再自减。当计数器为0的时候。锁将被释放,其他线程便可以获得锁。

无论是ACC_SYNCHRONIZED还是monitorenter、monitorexit都是基于Monitor实现的,在Java虚拟机(HotSpot)中,Monitor是基于C++实现的,由ObjectMonitor实现。

ObjectMonitor类中提供了几个方法,如enter、exit、wait、notify、notifyAll等。sychronized加锁的时候,会调用objectMonitor的enter方法,解锁的时候会调用exit方法。

主要有两种用法,分别是同步方法和同步代码块

8、解释以下名词:重排序,自旋锁,偏向锁,轻量级锁,可重入锁,公平锁,非公平锁,乐观锁,悲观锁。

9、用过哪些原子类,他们的原理是什么

CAS利用CPU调用底层指令实现。

两种方式:总线加锁或者缓存加锁保证原子性。

  • 总线加锁

如i=0初始化,多处理器多线程环境下进行i++操作,处理器A和处理器B同时读取i值到各自缓存中,分别进行递增操作,i的值为1。处理器提供LOCK#信号对总线进行加锁后,处理器A读取i的值并递增,此时处理器B被阻塞,无法读取内存中的值。

  • 缓存加锁

总线加锁,在LOCK#信号下,其他线程无法操作内存,性能较差,缓存加锁能较好处理该问题。

缓存加锁,处理器A和B同时读取i值到缓存,处理器A提前完成递增,数据立即回写到主内存,并让处理器B缓存该数据失效,处理器B需重新读取i值。

CAS机制通常也存在以下缺点:

(1)ABA问题

如果V的初始值是A,在准备赋值的时候检查到它仍然是A,那么能说它没有改变过吗?也许V经历了这样一个过程:它先变成了B,又变成了A,使用CAS检查时

以为它没变,其实却已经改变过了。

(2)CPU开销较大

在并发量比较高的情况下,如果许多线程反复尝试更新某一个变量,却又一直更新不成功,循环往复,会给CPU带来很大的压力。

(3)不能保证代码块的原子性

CAS机制所保证的只是一个变量的原子性操作,而不能保证整个代码块的原子性。比如需要保证3个变量共同进行原子性的更新,就不得不使用Synchronized了。

10、JUC下研究过哪些并发工具,讲讲原理。

ConcurrentHashMap、ConcurrentSkipListMap、ConcurrentSkipListSet, CopyOnWriteArrayList 和 CopyOnWriteArraySet

CountDownLatch(闭锁)

创建执行线程的方式三

同步锁(Lock)

ReadWriteLock(读写锁)

线程八锁

线程池

Fork/Join 框架

11、用过线程池吗,如果用过,请说明原理,并说说newCache和newFixed有什么区别,构造函

数的各个参数的含义是什么,比如coreSize,maxsize等。

原理:预先创建若干线程用来处理提交到线程池的任务,并根据任务数量自动扩缩 线程数 量,并通过任务队列对超过最大线程数限制的任务进行管理,频繁创建线程和销毁线程带来的开销

corePoolSize:核心池的大小:在创建了线程池后,线程池中的线程数为0,当有任务来之后,就会创建一个线程去执行任务,当线程池中的线程数目达到corePoolSize后,就会把到达的任务放到缓存队列当中

maximumPoolSize:线程池最大线程数,这个参数也是一个非常重要的参数,它表示在线程池中最多能创建多少个线程;

keepAliveTime:表示线程没有任务执行时最多保持多久时间会终止。默认情况下,只有当线程池中的线程数大于corePoolSize时,keepAliveTime才会起作用,直到线程池中的线程数不大于corePoolSize,即当线程池中的线程数大于corePoolSize时,如果一个线程空闲的时间达到keepAliveTime,则会终止,直到线程池中的线程数不超过corePoolSize。

Java通过Executors提供四种线程池,分别为:

newCachedThreadPool创建一个可缓存线程池,如果线程池长度超过处理需要,可灵活回收空闲线程,若无可回收,则新建线程。

newFixedThreadPool 创建一个定长线程池,可控制线程最大并发数,超出的线程会在队列中等待。

newScheduledThreadPool 创建一个定长线程池,支持定时及周期性任务执行。

newSingleThreadExecutor 创建一个单线程化的线程池,它只会用唯一的工作线程来执行任务,保证所有任务按照指定顺序(FIFO, LIFO, 优先级)执行。

11、用三个线程按顺序循环打印abc三个字母,比如abcabcabc。

开放题,没有最好,只有更好的方案,可参考两种方案:

1、使用wait notifyAll进行三个线程间协调进行打印

2、利用JUC locks包下的condition实现类似wait notifyAll的功能

12、ThreadLocal用过么,用途是什么,原理是什么,用的时候要注意什么。

13、如果让你实现一个并发安全的链表,你会怎么做?

开放题,没有最好,只有更好的解决方案,以下供参考:

方案一:粗粒度锁,完全锁住链表

方案二:细粒度锁,锁住需要修改的节点

14、无锁数据结构的实现原理

无锁数据结构的实现主要基于两个方面:原子性操作和内存访问控制方法

15、CAS中的ABA问题如何解决

ABA问题我们可以使用JDK的并发包中的AtomicStampedReference和 AtomicMarkableReference来解决。

16、多线程如果线程挂起与恢复

suspend方法与resume方法

17、countdowlatch和cyclicbarrier的内部原理和用法

1)CountDownLatch减计数,是一次性的,用给定的计数初始化CountDownLath。调用countDown()方法计数减 1,在计数被减到 0之前,调用await方法会一直阻塞。减为 0之后,则会迅速释放所有阻塞等待的线程,并且调用await操作会立即返回。CountDownLatch强调的是一个线程(或多个)需要等待另外的n个线程干完某件事情之后才能继续执行。

2)CyclicBarrier加计数,可以重用,用计数 N 初始化CyclicBarrier, 每调用一次await,线程阻塞,并且计数+1(计数起始是0),当计数增长到指定计数N时,所有阻塞线程会被唤醒。继续调用await也将迅速返回。CyclicBarrier强调的是n个线程,大家相互等待,只要有一个没完成,所有人都得等着。

3)countdownlatch的await方式通过队列同步器实现的共享锁来控制count

CyclicBarrier的await方式通过ReentrantLock

18、简述ConcurrentLinkedQueue和LinkedBlockingQueue的用处和不同之处

ConcurrentLinkedQueue:是Queue的一个安全实现.Queue中元素按FIFO原则进行排序.采用CAS操作,来保证元素的一致性。

LinkedBlockingQueue:由于LinkedBlockingQueue实现是线程安全的(使用JDK的可重入锁ReentrantLock),实现了先进先出等特性,是作为生产者消费者的首选,LinkedBlockingQueue 可以指定容量,也可以不指定,不指定的话,默认最大是Integer.MAX_VALUE,其中主要用到put和take方法,put方法在队列满的时候会阻塞直到有队列成员被消费,take方法在队列空的时候会阻塞,直到有队列成员被放进来。

19、导致线程死锁的原因?怎么解除线程死锁。

产生死锁的原因:(1)竞争系统资源 (2)进程的推进顺序不当

产生死锁的必要条件:

互斥条件:进程要求对所分配的资源进行排它性控制,即在一段时间内某资源仅为一进程所占用。

请求和保持条件:当进程因请求资源而阻塞时,对已获得的资源保持不放。

不剥夺条件:进程已获得的资源在未使用完之前,不能剥夺,只能在使用完时由自己释放。

环路等待条件:在发生死锁时,必然存在一个进程–资源的环形链。

解决死锁

加锁顺序(线程按照一定的顺序加锁)

加锁时限(线程尝试获取锁的时候加上一定的时限,超过时限则放弃对该锁的请求,并释放自己占有的锁)

死锁检测

20、非常多个线程(可能是不同机器),相互之间需要等待协调,才能完成某种工作,问怎么设计这种协调方案。

1)object类的wait/notify/notifyAll方法

2)JUC包Condition对象

3)使用Thread类的join()方法

4)使用CountDownLatch类

21、开启多个线程,如果保证顺序执行

1)信号量Sephmore的acquire和release方法

2)ReetrantLock和Condition

3)通过join方法去保证多线程顺序执行

4)使用单例线程池

22、延迟队列的实现方式,delayQueue和时间轮算法的异同。

DelayQueue本质是封装了一个PriorityQueue,使之线程安全,加上Delay功能,也就是说,消费者线程只能在队列中的消息“过期”之后才能返回数据获取到消息,不然只能获取到null。

DelayQueue的数据结构,时间轮在算法复杂度上有一定优势。DelayQueue由于涉及到排序,需要调堆,插入和移除的复杂度是O(lgn),而时间轮在插入和移除的复杂度都是O(1)。

文章来源:智云一二三科技

文章标题:大厂面试题库之《Java多线程》含答案

文章地址:https://www.zhihuclub.com/170464.shtml

关于作者: 智云科技

热门文章

网站地图