banner
NEWS LETTER

JUC面试题

Scroll down

1.线程池的作用?线程池参数?线程池拒绝策略和等待队列类型之间的关系?

线程池的作用:

1)通过复用已创建的线程降低资源的消耗。
2)任务到达时,任务可以无需创建线程就能运行。
3)通过线程池对线程进行分配,调优,监控可以提高线程的管理性。

线程池参数:

1)corePoolSize:核心线程数,最小可以同时运行线程数量
2)maxmumPoolSize:线程池能够容纳的最大线程数
3)keepAliveTime:多余空闲线程的存活时间,当该线程的时间到达keepAlieveTime值时,∴多余的线程会被销毁
4)unit:keepAliveTime的单位
5)workQueue:任务队列,被提交但未执行的任务
6)threadFactory:表示生成线程中生成线程的工厂,一般用默认的
7)handler:拒绝策略

线程池拒绝策略和等待队列之间的关系:

当向线程池提交任务时,先判断核心线程是否以满,没满就创建核心线程,满了就判断等待队列满了没,没满就加入等待队列。等待队列满了就判断线程池是否达到最大线程数,没到达就创建非核心线程。达到就按照拒绝策略处理。

1.1线程池的使用方法和实现原理
使用方法
1)通过构造方法创建
2)通过Executors 来实现

  • FixTreadPool:返回一个固定线程数量的线程池
  • SingleThreadExecutor:返回只有一个线程的线程池
  • CacheThreadPool:返回一个根据实际线程

实现原理
当向线程池提交任务时,先判断核心线程是否以满,没满就创建核心线程,满了就判断等待队列满了没,没满就加入等待队列。等待队列满了就判断线程池是否达到最大线程数,没到达就创建非核心线程。达到就按照拒绝策略处理。上述不论是创建核心线程还是创建非核心线程都是通过addWorker()方法。线程池将线程封装成一个Worker类,这个Worker继承了Runnable接口,所以Worker也是一个线程任务。这个Work启动后会run方法中的runWorker方法。runWork方法是一个while循环体。当执行完当前线程的任务后,worker的生命周期没有结束,worker会通过循环不断调用getTask()从阻塞队列中获取任务,从而达到线程复用的目的。getask也是这个循环体的条件。她的作用除了从阻塞队列中获取线程,它还会判如果是非核心线程,断当等待时间超过keepAliveTime,就会返回null,循环会终止,并销毁这个线程。

1.2 如何选择合适的线程数量

通过预估任务数量,观察线程状态,选择合适的线程数。

2.ThreadLocal使用场景和问题

如果想要每一个线程都有自己的本地变量可以使用ThreadLocal来实现。ThreadLocal存在内存泄露问题,ThreadLocal作为key是弱引用,而value是强引用。所以ThreadLocal在没有外部强引用的情况下,在垃圾回收时有可能被清理掉,而value没有被清理掉。ThreadLocal考虑了这种情况,通过调用set(), get(), remove()方法可以清理key为null的记录。

2.1 ThreadLocal为什么key使用弱引用

  • 如果key使用强引用:引用ThreadLocal的对象被回收了,但是ThreadLcoalMap还持有ThreadLocal的强引用,如果没有手动删除,ThreadLocal不会被回收,导致内存泄露
  • 如果key使用弱引用,引用ThreadLocal的对象回收了,由于ThreadLocalMap持有ThreadLocal的弱引用,即使没有手动删除,ThreadLocal也会被回收。value在下一次ThreadLocalMap调用Set,get的时候会被清除

2.1ThreadLocal原理

Thread有一个ThreadLocalMap类型的变量,而ThreadLocal调用get方法的过程是先获取当前线程,然后通过当前线程和获取ThreadLocalMap变量,然后根据key从ThreadLocalMap变量中获取value。变量并不是放在了ThreadLocal上,ThreadLocal只是ThreadLocalMap的封装。

3.乐观锁与悲观锁

1)乐观锁:乐观锁总是假设对访问的共享数据没有冲突,线程可以不停的执行,无需加锁也无需等待。一旦多个线程发生冲突,乐观锁使用CAS来保证线程安全。(因为乐观锁无锁,所以不会发生死锁问题)
2)悲观锁:悲观锁认为每次访问共享资源都会发生冲突,因此每次对数据上锁,保证临界区的程序一段时间内只有一个线程在执行。

比较:
乐观锁通常用于读多写少的数据,避免频繁加锁影响性能;悲观锁用于读少写多的数据,避免频繁地失败与重试;

3.1产生死锁的四个必要条件

(1)互斥条件:进程对所分配到的资源不允许其他进程进行访问,若其他进程访问该资源,只能等待,直至占有该资源的进程使用完成后释放该资源
(2)请求和保持条件:进程获得一定的资源之后,又对其他资源发出请求,但是该资源可能被其他进程占有,此事请求阻塞,但又对自己获得的资源保持不放
(3)不可剥夺条件:是指进程已获得的资源,在未完成使用之前,不可被剥夺,只能在使用完后自己释放
(4)循环等待条件:是指进程发生死锁后,必然存在一个进程–资源之间的环形链

4.CAS原子性如何保证,ABA问题如何解决

CAS的全称是比较并交换。它有三个值V:要更新的变量,E:预期值,N:新值,先判断V是否等于E,如果等于则将V替换成N,如果不等于则说明有其他线程更新了V,当前线程放弃更新说什么也不做。java通过Unsafe和Atmoic的一些类实现原子操作。Unsafe类有一些本地方法实现了CAS操作,atmoic将cas与while循环搭配使用,在失败后不断重试。

ABA问题:
所谓ABA问题就是,就是一个值原来是A,变成了B,又变成A,这个时候CAS察觉不出来的,但实际上CAS被更新了两次。ABA的解决思路是在变量前面加上版本号和时间戳。JDKatmoic包中提供了一个类atmoicStamppedReference,它的compareAndSet方法首先检查当前引用是否等于预期引用,并检查当前标志是否等于预期标志,如果两者都相等,才使用CAS更新当前的值和标志。

4.1CAS的性能一定比锁好吗

CAS通常用于读多写少的数据,大多数情况下读是不需要考虑线程问题的,频繁加锁的话会影响性能;对于读少写多的数据最好加锁,若果用乐观锁频繁失败重试影响性能。

5.volatile的作用,JMM模型和内存屏障

JMM模型指的是java内存模型。java内存模型中,所有的共享变量必须放在主存中,每个线程都保留了一份共享变量的副本。JMM规定线程对共享变量的访问必须在本地内存中进行,不能从主内存中直接读取。
volatile是轻量级锁。volatile保证了内存的可见性。当一个线程对volatile修饰的变量进行写操作时,JMM会立刻把本地内存对应的共享变量刷新到主存去;当一个线程对volatile修饰的变量进行读操作时,JMM会立刻把线程对应的本地内存设置为无效,然后从主内存中读取共享变量的值。
volatile还可以防止重排序,保证了多线程的有序性。例如创建一个对象,可以分为以下步骤,一是分配内存,二是初始化对象,三是让一个引用指向分配的地址。如果发生了JVM重排序 二和三的位置调换,而二还没有执行完,就会拿到一个未初始化完成的对象。volatile是通过内存屏障防止重排序的。内存屏障可以对编译器和处理器对指令重排作出一些限制,例如一条内存屏障可以禁止编译器和处理器将内存屏障后面的指令移到前面去。

6.ReentrantLock的源码分析

ReentrantLock实现了公平锁与非公平锁,也是独占锁。ReentrantLock有两个嵌套类FairSync和NoFairSync分别对应公平锁和不公平锁的实现。在ReentrantLock中公平锁和非公平锁的区别是lock(),加锁方法,tryAcquire(),尝试获取锁的方法。非公平锁的lock方法通过CAS设置同步状态来获取锁,成功了就设置当前线程为独占线程,否则就调用tryAcquire()方法再次尝试和获取锁,不成功则加入队列。公平锁的lock方法直接直接调用tryAcquire方法,尝试获取锁,不成功则加入队列。公平锁与非公平锁的tryAcquire()方法的主要区别是公平锁会判断当前线程的前驱节点是否为头节点,如果是就尝试获取锁,但由于一开始头节点和尾结点都为空所以先直接加入队列;非公平不会判断前驱节点是否为头节点,直接cas获取锁,失败加入队列。
加入队列是adwaiter()方法,将该线程加入到队列尾部,这个队列是双向列表,头结点是虚节点,没有任何实际意义。然后在调用acquireQueue()方法。这个方法是,通过自旋获取锁,这个自旋过程会判断它的前驱节点是否题头节点,是的话就调用tryAcquire()尝试获取锁。公平锁如果前驱节点不是头节点或者获取锁1失败就靠前驱节点判断,当前线程是否被阻塞。(shouldParkAfterFailedAcquire)如果是就parkAndCheckIerrupt()阻塞这个线程。
如果调用unlock唤醒方法,它会先通过CAS方法释放资源,然后调用unparkSuccessor(),唤醒队列中第一个waitStatus < 0 的线程。被唤醒线程回到之前调用的parkAndCheckIerrupt,这个方法还会调用Thread.interrupted(),方法回的是当前执行线程的中断状态,并清除中断位。如果该线程处于中断状态,那么等他获取到锁后,通过调用selfInterrupt,让他重新回到中断状态。

7.i++与++i是原子操作吗

通过查看i++的字节码指令发现i++执行多步操作:
1)从变量i中读取i的值
2)值加1
3)将加1后的值写回i
由于操作是多步的的,在多线程中会出现问题

8.synchronized锁原理,特性

synchronized有三种使用方式
1)修饰实例方法,给当前对象加锁,监视器是当前对象实例
2)修饰静态方法,给当前class类上锁,监视器是当前class类对象
3)修饰代码块,给代码块上锁,监视器可以任意指定

synchronized关键字底层原理属于JVM层面

1)同步代码块上
使用synchronized修饰同步代码块,会在代码块的字节码指令前后分别加上monitorenter和monitorexit指令。在执行monitorenter指令时,会尝试获取对象的锁,如果锁的计数器为0,就可以获取,然后锁的计数器加1。执行monitorexit方法,计数器减1

2)修饰方法
JVM会给方法添加ACC_SYNCHRONIZED访问标识,标明这是一个同步方法。

8.1synhronized是重量级锁吗

JDK1.6以前是重量级锁,之后内部有了个优化,不再是简单的重量锁,有了个锁升级的流程。

对象头有个叫MarkWord,存储了锁有关的信息,其中就有占有偏向锁的线程id
1)当一个线程准备获取共享资源时,检查MarkWord里面放的是不是自己的线程ID,如果是则表明该线程已经获取到了锁,这个锁仍然是偏向锁。(这个MarkWord就是锁对象对象头中的锁信息)
2)如果不是则尝试CAS替换MarkWord里的线程id为新的线程id,如果替换成功该锁仍然是偏向锁,如果失败则锁升级,锁升级为轻量级锁。根据MarkWord的线程id,通知这个线程暂停并将MarkDown的内容清空。
3)两个线程都把锁对象的HashCode复制到自己线程栈帧中由于存储锁记录的空间,接着通过CAS操作把锁对象的MarkWord的内容修改为指向自己存储锁记录空间的指针。
4)成功的或得锁,失败的进入自旋
5)自旋的线程在自旋的过程中成功获得资源则锁仍是轻量级锁,自选失败则锁升级为重量级锁,自旋的线程阻塞,并加入到一个队列中,等待唤醒。

8.2reentrantLock与syhronized的区别

1)reentranLock的实现依赖于Api,syhronized依赖JVM,而JVM中是通过操作系统中 mutex lock 实现的,因此锁的状态是由操作系统维护,每次获取锁都要切换到内核态。而reentrantLock的锁状态是通过一个state属性维护的,不需要切换到内核态。
2)reentrantLock既实现了公平锁也实现了非公平锁,syhronized是非公平锁。
3)reentrantLock可选择性通知(Condtion实现),而syhronized有notify()和notifyall()要么随机唤醒一个要么唤醒全部
4)reentrantLock实现了可等待中断,也就是说正在等待的线程和以放弃等待,改为处理其他事。

9.sleep()与wait()的区别

1)sleep方法没有释放锁,wait方法释放了锁
2)sleep()是通过Thread类调用,wait()是通过Object类调用
3)sleep用于线程的暂停执行,wait用于线程的通信
4)使用范围不同,sleep()可以在任意地方使用,而wait()和notify()必须在同步代码块中使用
5)sleep方法执行完后自动苏醒,wait方法调用后线程不会自动苏醒,需要另外一个线程调用notify()或者notifyAll()方法,

10.ConcurrentHashMap源码分析

JDK1.7中ConcurrentHashMap使用分段锁(Reentrant Lock),将数据分成一段一段存储,然后每一段数据配一把锁。当一个线程占有一个数据段访问一个数据段时其他的数据段也能被其他的线程访问。它有一个Segment 类型的数组。Segment就数据段,segment是一个类似HashMap的结构,Segment数组大小默认是16个,且初始化后就不能扩容了。每次调用put()方法首先通过key获取Segment的位置,获取分段锁后,再进行操作。
JDK1.8中使用Synchronized加CAS机制,结构类似HashMap,也是数组加链表加红黑树。put方法是个自旋过程,没回put()方法通过key找到数组位置,判断该位置是否需要初始化,或者如果当前位置为空就通过cas写入,如果不为空则通过synchronized 写入数据。

1.8相较于1.7,前者的锁力度更小,1,8只是每个哈希桶(发生hash冲突时才用synchronized ),后者锁住一个段

注:hashTable就是hashMap的涉及多线程的方法加了synchronized修饰

11.什么是可重入锁,什么是公平锁

可重入锁就是一个线程获取某个锁后,在次获取该锁不会发生死锁(锁相关的片段可能需要再次访)。
公平锁就是多个线程按照自己申请锁的顺序依次获取锁

12.interrupt() 、interrputed()和isInterrupted()

  • interrupt()中断本线程,本质是将中断标记设置为false
  • interrupted()返回中断标记外,还会清除中断标记(将中断标记设置为false)
  • isInterrupted()返回中断标记

13.并行与并发的区别

  • 并发:同一段时间内多个任务同时执行,宏观上是多个任务一起执行,微观上是多个任务·快速交替执行。
  • 并行:同一时刻多个任务同时执行。

14.请问java多线程有几种实现方式

1)继承Thread,重写里面run()方法,创建实例,执行start()

优点:代码编写简单
缺点:没有返回值,继承一个类后,没法继承其他的类

2)实现Runnable接口

自定义实现Runnable接口,实现里面的run方法
优点:可以实现多个接口,还可以再继承一个类
缺点:没有返回值,不能直接启动,需要构造一个Thread实例传递进去启动。

3)实现Callable接口

自定义实现Callable接口的实现类,并实现call()方法,并结合FutureTask类包装Callable对象
优点:有返回值
缺点:需要结合FutureTask类或者Thread类

4)线程池

通过提前创建线程,
优点:略,见1
缺点:必须结合Runnable或者Callable使用 (如果使用execute()只能搭配Runnable使用,且没有返回值,如果想要有返回值搭配Callable使用)

15.线程的状态

image.png
1)新建状态(New):新建了个线程对象,但没有调用start方法
2)就绪状态(Runnable):调用线程的start方法,此线程进入就绪转态
3)运行状态(Running):当线程获得CPU时间后,它才进入运行状态,真正执行run()方法
4)阻塞状态(Blocked):遇到io事件时暂时让出cpu
5)等待状态(Waiting):相比于阻塞状态,等待状态是主动让出cpu,阻塞时候被动让出cpu。进入等待状态的方法有Thread.sleep(),Object.wait(), Thread.join()

运行状态如何变成就绪状态?
时间片用完或者调用yeild(礼让)

等待状态如何变成Runnable?
notify()方法或者sleep结束

16.多线程的三要素

1)原子性:相关的操作,不会被其他的线程干扰
2)可见性:一个线程修改了某个共享变量,能够立刻被其他线程知晓
3)有序性:保证线程内串行语义避免指令重排等

其他文章
cover
java基础面试题
  • 22/11/16
  • 13:02
  • java资料
cover
从0开始搭建自己的halo博客
  • 22/11/13
  • 18:29
  • 小白教程