更多问题参考:
Java多线程面试题通关手册:https://mp.weixin.qq.com/s/w8KTx1K8RpcZl_In-CxIMQ
问题 | 答案 |
---|---|
线程与进程的区别? | 进程是操作系统分配资源的最小单元,线程是CPU调度和分派的最小单元。 一个线程可以创建和撤销另一个线程,同一个进程中的多个线程之间可以并发执行 进程有独立的地址空间,线程有自己的堆栈和局部变量,但线程之间没有单独的地址空间 |
守护线程 | Java分为两种线程:用户线程和守护线程 是个服务线程,准确地来说就是服务其他的线程(比如垃圾回收线程)
守护线程的退出:如果其他的线程(即用户自定义线程)都执行完毕,连main线程也执行完毕,那么jvm就会退出,此时会杀死会杀死进程中的所有守护线程 Thread对象的setDaemon(true) 设置守护线程。 |
线程的状态 参考: |
|
Thread 类中的 sleep、join、yield、interrupt object 类中的 wait、notify、notifyAll Condition 接口 参考: | sleep:相当于让线程进入阻塞状态,不会释放锁
join:线程A中调用另一个线程B的join方法,线程A将会等待线程B执行完毕后再执行。调用了Object的wait方法,让线程进入阻塞状态,会释放锁
yield:并不会让线程进入阻塞状态,而是让线程重回就绪状态,不会释放锁,让出CPU执行权给同等级的线程,如果没有相同级别的线程在等待CPU的执行权,则该线程继续执行。
interrupt:可以用来中断一个正处于阻塞状态的线程;直接调用interrupt方法不能中断正在运行中的线程。
wait()、notify()和notifyAll()方法是本地方法,并且为final方法,无法被重写。必须在同步块或者同步方法中进行。 wait:让当前线程阻塞,并且当前线程必须拥有此对象的monitor,会释放锁 (选择哪个线程取决于操作系统对多线程管理的实现) notify:能够唤醒一个正在等待这个对象的monitor的线程 notifyAll:能够唤醒所有正在等待这个对象的monitor的线程
sleep和wait区别:来自不同的类;sleep没有释放锁,wait释放锁;wait,notify只能用于同步方法,sleep任何地方使用;sleep必须捕获异常,wait不需要
wait,notify 为什么放在Object类中? 1.每个对象都可以成为锁,不仅仅是Thread对象 2.线程可以拥有多个对象锁,不知道操作的对象锁是哪个
Condition是个接口,基本的方法就是await()和signal()方法;
Conditon中的await()对应Object的wait(); Condition中的signal()对应Object的notify(); Condition中的signalAll()对应Object的notifyAll()。 |
线程中断 Java的中断是一种协作机制 参考: | Thread类中断方法 void interrupt():并不一定就立马中断了正在运行的线程,它只是要求线程自己在合适的时机中断自己。每个线程都有一个boolean的中断状态(这个状态不在Thread的属性上),interrupt方法仅仅只是将该状态置为true。 bool interrupted() : 测试当前线程是否已经中断,并清除中断状态 bool isInterrupted() 测试当前线程是否已经中断,不设置中断状态
一个方法声明抛出InterruptedException,表示该方法是可中断的,比如wait,sleep,join,中断方法会对interrupt调用做出响应(sleep响应interrupt的操作包括清除中断状态,抛出InterruptedException) Object.wait, Thread.sleep方法,会不断的轮询监听 interrupted 标志位,发现其设置为true后,会停止阻塞并抛出 InterruptedException异常 不可中断的操作:包括进入synchronized段以及Lock.lock(),inputSteam.read()等,调用interrupt()对于这几个问题无效,因为它们都不抛出中断异常。如果拿不到资源,它们会无限期阻塞下去。 可被中断的加锁操作:Lock.lockInterruptibly() 抛出中断异常 |
CAS 机制 参考来源: | Compare And Swap的缩写,翻译过来就是比较并替换,在无锁的情况下实现原子操作。 3个基本操作数:内存地址V,旧的预期值A,要修改的新值B。利用unsafe提供了原子性操作方法(硬件命令)。
CAS存在的问题? 1.ABA问题,利用版本号比较可以有效解决ABA问题 AtomicStampedReference 已解决 2.循环时间长开销大。自旋CAS如果长时间不成功,会给CPU带来非常大的执行开销。 3.只能保证一个共享变量的原子操作。AtomicReference类来保证引用对象之间的原子性,你可以把多个变量放在一个对象里来进行CAS操作。 |
Atomic包 | Atomic的包名为java.util.concurrent.atomic。这个包里面提供了一组原子变量的操作类,这些类可以保证在多线程环境下,当某个线程在执行atomic的方法时,不会被其他线程打断,而别的线程就像自旋锁一样,一直等到该方法执行完成,才由JVM从等待队列中选择一个线程执行 。 |
LongAdder(只能做自增、自减) 使用分段CAS以及自动分段迁移 参考:
| 代码思路:每一步都优先考虑代价相对小的操作 (1) 在开始并发比较低的时候先考虑在base上累加,有竞争失败才去创建cells (2) 如果有了cells之后,先考虑在对应的cell上进行cas操作,有竞争失败才会进入自旋 (3)内部实现了自动分段迁移的机制,也就是如果某个Cell的value执行CAS失败了,那么就会自动去找另外一个Cell分段内的value值进行CAS操作 要从LongAdder中获取当前累加的总值,就会把base值和所有Cell分段数值加起来返回给你
AtomicLong可不可以不要 ? incrementAndGet()有返回值 而 increment() 无返回值,需要再进行一次求和 为什么用Cell数组而不是long数组?扩容的时候,long是栈中存储,可能会发生数据丢失的问题 |
synchronized (非公平锁) synchronized关键字经过编译后,会在同步块的前后分别形成monitorenter和monitorexit这两个字节码指令,这两个字节码都需要一个reference类型的参数来指明要锁定和解锁的对象。 | 1.synchronized方法 一般方法 对象锁、 static方法 类锁(与对象锁不互斥) 2.synchronized代码块 对象锁 对于synchronized方法或者synchronized代码块,当出现异常时,JVM会自动释放当前线程占用的锁,因此不会由于异常导致出现死锁现象。 缺点:无法响应中断、无法实现读写锁 锁对象的对象头里面有一个 threadid 字段 再次进去,如果是自己则自己使用,否则锁升级为轻量级锁 3 锁升级 锁对象的对象头里面有一个 threadid 字段(1标识偏向锁,0表示无锁。00时表示轻量级锁,10表示重量级锁,10是GC标记) 第一次进去设置为自己id 持有偏向锁 再次进去,如果是自己则自己使用,否则锁升级为轻量级锁 通过自旋一定次数获取锁,没获取到,升级为重量级锁 |
Lock 声明了四个方法来获取锁
| 使用Lock必须在try{}catch{}块中进行,并且将释放锁的操作放在finally块中进行,以保证锁一定被被释放,防止死锁的发生。 ReentrantLock 如果锁具备可重入性,则称作为可重入锁。可重入性在我看来实际上表明了锁的分配机制:基于线程的分配,而不是基于方法调用的分配 ReentrantReadWriteLock 读写锁将对一个资源(比如文件)的访问分成了2个锁,一个读锁和一个写锁 |
Lock和synchronized的选择 | Lock是一个接口,而synchronized是Java中的关键字 synchronized在发生异常时自动释放锁,Lock自己处理释放锁 Lock可响应线程中断 lockInterruptibly(),synchronized 不行 Lock可以知道有没有成功获取锁,而synchronized却无法办到 最重要 读写锁、可以提高多个线程进行读操作的效率,synchronized不行 |
ThreadLocal 参考: | 线程局部变量。变量是同一个,但是每个线程都使用同一个初始值。 就是每个线程都维护了一个map,而这个map的key就是threadLocal,而值就是我们set的那个值,每次线程在get的时候,都从自己的变量中取值,既然从自己的变量中取值,那肯定就不存在线程安全问题,总体来讲,ThreadLocal这个变量的状态根本没有发生变化,他仅仅是充当一个key的角色,另外提供给每一个线程一个初始值。
ThreadLocalMap key是弱引用,容易被回收;回收后 键值为null,value值不回收;ThreadLocalMap 做了一些额外的回收工作。 ThreadLocal导致的内存泄露: 核心线程池中线程使用ThreadLocal ThreadLocal应用: 实现数据库连接Connection对象、保存上下文用户信息、 为什么不直接用线程id来作为ThreadLocalMap的key? 直接用线程id来作为ThreadLocalMap的key,无法区分放入ThreadLocalMap中的多个value |
volatile关键字及其应用
| |
创建线程有哪几种方式? |
|
Callable、Future和FutureTask 参考: | Future提供了三种功能: 1)判断任务是否完成; 2)能够中断任务; 3)能够获取任务执行结果。 |
简述Executor框架 参考: | Executor框架包括3大部分: (1)任务。也就是工作单元,包括被执行任务需要实现的接口:Runnable接口或者Callable接口; (2)任务的执行。也就是把任务分派给多个线程的执行机制,包括Executor接口及继承自Executor接口的ExecutorService接口。 (3)异步计算的结果。包括Future接口及实现了Future接口的FutureTask类。 execute() 没有返回结果 submit() 有返回结果,但实际调用execute()
|
线程池运行原理 参考: | ThreadPoolExecutor
|
线程池状态 | RUNNING 当创建线程池后,初始时,线程池处于RUNNING状态;
SHUTDOWN 调用了shutdown()方法,此时线程池不能够接受新的任务,它会等待所有任务执行完毕
STOP 调用了shutdownNow() 此时线程池不能接受新的任务,并且会去尝试终止正在执行的任务
TERMINATED(结束) 当线程池处于SHUTDOWN或STOP状态,并且所有工作线程已经销毁,任务缓存队列已经清空或执行结束后 |
线程池的分类 | Executors类中提供的几个静态方法来创建线程池:
Executors.newCachedThreadPool(); //将corePoolSize设置为0,将maximumPoolSize设置为Integer.MAX_VALUE,使用的SynchronousQueue(一个不存储元素的阻塞队,插入后必须等待删除,否则会处于阻塞),也就是说来了任务就创建线程运行,当线程空闲超过60秒,就销毁线程。
Executors.newSingleThreadExecutor(); //创建容量为1的缓冲池,corePoolSize和maximumPoolSize都设置为1,也使用的LinkedBlockingQueue
Executors.newFixedThreadPool(int); //创建固定容量大小的缓冲池,线程池corePoolSize和maximumPoolSize值是相等的,它使用的LinkedBlockingQueue
Executors.newSingleThreadScheduledExecutor() 创建一个单线程的线程池。此线程池支持定时以及周期性执行任务的需求 |
任务拒绝策略 | ThreadPoolExecutor.AbortPolicy:丢弃任务并抛出RejectedExecutionException异常。 默认
ThreadPoolExecutor.DiscardPolicy:也是丢弃任务,但是不抛出异常。
ThreadPoolExecutor.DiscardOldestPolicy:丢弃队列最前面的任务,然后重新尝试执行任务(重复此过程)
ThreadPoolExecutor.CallerRunsPolicy:由调用线程处理该任务 |
CountDownLatch CyclicBarrier Semaphore(信号量) | CountDownLatch(int count) 等待其他count个任务执行完毕之后才能执 await() throws InterruptedException // 挂起 countDown() // count 减1
1)CountDownLatch和CyclicBarrier都能够实现线程之间的等待,只不过它们侧重点不同: CountDownLatch一般用于某个线程A等待若干个其他线程执行完任务之后,它才执行; 而CyclicBarrier一般用于一组线程互相等待至某个状态,然后这一组线程再同时执行; 另外,CountDownLatch是不能够重用的,而CyclicBarrier是可以重用的。 2)Semaphore其实和锁有点类似,它一般用于控制对某组资源的访问权限。
Semaphore(int permits) //permits表示许可数目,即同时可以允许多少线程进行访问 acquire() throws InterruptedException // 获取锁 acquire(int permits) 获得多个 release() 释放锁 |
BlockingQueue
ReentrantLock // 可重入锁 Condition | boolean add(E e); // 加元素,超过最大值报错 boolean offer(E e); // 加元素 e的值不能为空,否则抛出空指针异常 void put(E e) throws InterruptedException; // 加元素,没有多余的空间,该方法会一直阻塞 E take() throws InterruptedException; // 取元素,如果队列中没有值,线程会一直阻塞 poll(long timeout, TimeUnit unit) throws InterruptedException; // 取元素,在给定的时间里,从队列中获取值,时间到了直接调用普通的poll方法
分类: ArrayBlockingQueue:是一个用数组实现的有界阻塞队列 ,支持公平锁和非公平锁 LinkedBlockingQueue:一个由链表结构组成的有界队列,此队列的长度为Integer.MAX_VALUE PriorityBlockingQueue: 一个支持线程优先级排序的无界队列,默认自然序进行排序 DelayQueue: 一个实现PriorityBlockingQueue实现延迟获取的无界队列,在创建元素时,可以指定多久才能从队列中获取当前元素。只有延时期满后才能从队列中获取元素。 |
DelayQueue 无界、延迟、阻塞队列
| 支持延时获取元素的无界阻塞队列 BlockingQueue+PriorityQueue(堆排序)+Delayed DelayQueue中存放的对象需要实现compareTo()方法和getDelay()方法,必须实现Delayed接口 getDelay方法返回该元素距离失效还剩余的时间,当<=0时元素就失效了 取数据:获取对重第一个元素 无数据:线程阻塞 有数据:过期时间没到,线程阻塞 有数据:过期时间到了,取数据,唤醒所有等待线程 存数据:堆排序存数据,如果存的数据在堆顶,则唤醒一个等待线程 |
Timer、TimerTask 参考: | Timer是jdk中提供的一个定时器工具,使用的时候会在主线程之外起一个单独的线程执行指定的计划任务,可以指定执行一次或者反复执行多次。 TimerTask是一个实现了Runnable接口的抽象类,代表一个可以被Timer执行的任务。 Timer 里面有 TaskQueue(存TimerTask);TimerThread 线程 如何终止Timer线程 调用timer的cancle方法;当所有任务执行结束后,删除对应timer对象的引用,线程也会被终止;调用System.exit方法终止程序 几个缺陷:
|
happens-before规则(八大原则) 先与什么发生,没有任何时间上的含义 参考: | 单线程happen-before原则:在同一个线程中,书写在前面的操作happen-before后面的操作。 锁的happen-before原则:同一个锁的unlock操作happen-before此锁的lock操作。 volatile的happen-before原则:对一个volatile变量的写操作happen-before对此变量的任意操作(当然也包括写操作了)。 happen-before的传递性原则:如果A操作 happen-before B操作,B操作happen-before C操作,那么A操作happen-before C操作。 线程启动的happen-before原则:同一个线程的start方法happen-before此线程的其它方法。 线程中断的happen-before原则:对线程interrupt方法的调用happen-before被中断线程的检测到中断发送的代码。 线程终结的happen-before原则:线程中的所有操作都happen-before线程的终止检测。 对象创建的happen-before原则:一个对象的初始化完成先于他的finalize方法调用 |
AQS (AbstractQueuedSynchronizer) 参考: | |
AQS内部核心变量
用来实现各种锁,各种同步组件的。它包含了state变量、加锁线程、等待队列等并发中的核心组件 | int state 初始化为0,加锁的状态 关键变量 记录当前加锁的是哪个线程 |
AQS实现原理 |
|
AQS实现公平锁与非公平锁 | 如上步骤6,线程2尝试CAS加锁,如果此时线程3也加锁 非公平锁:线程3直接获取锁成功,不一定说先来排队的线程就就先会得到机会加锁,而是出现各种线程随意抢占的情况。 公平锁:AQS的队列里真的有线程排着队,线程3也得排队 |
RateLimiter |
// 当前桶里存储的令牌数量 double storedPermits; // 桶可存储最大令牌数量 double maxPermits; // 生成一个令牌的间隔 微秒级别 double stableIntervalMicros; // 下一个请求允许获取到令牌的微秒数 private long nextFreeTicketMicros = 0L |