个人整理 - Java 后端面试题 - 多线程篇

本贴最后更新于 1722 天前,其中的信息可能已经斗转星移
  • 标 ★ 号的知识点为重要知识点

介绍一下 Syncronized 锁,如果用这个关键字修饰一个静态方法,锁住了什么?如果修饰成员方法,锁住了什么?

- Synchronized修饰成员方法的话锁住的是实例对象。修饰静态方法的话,锁住的是类对象。
(可以类比数据库中的表锁和行锁的机制)

★ 请你介绍一下 volatile?

- 使用volatile修饰的变量会强制将修改的值立即写入主存,主存中值的更新会使缓存中的值失效
- (非volatile变量不具备这样的特性,非volatile变量的值会被缓存,线程A更新了这个值,
- 线程B读取这个变量的值时可能读到的并不是是线程A更新后的值)。
- volatile会禁止指令重排 volatile具有可见性、有序性,不具备原子性。 注意,volatile不具备原子性。(不能保证并发累加问题)

★Synchronized 和 Lock 的区别?

  • synchronized :
    • 是一个 Java 的关键字,JVM 隐式锁。
    • 会自动释放锁。
    • 去获得锁的时候,会阻塞。
    • 不可获取锁的状态。
    • 可重入,不可中断,非公平锁
    • 性能没有 Lock 好。(但是随着 JDK 版本更迭,其实 synchronized 的性能也不错)
  • Lock:
    • 是 java 的一个类。
    • 需要手动释放锁。一般在 finally 中释放。(避免异常的时候没有释放锁)
    • 线程可以尝试获得锁,线程可以不用一直阻塞等待。
    • 可以获取锁的状态,通过 trylock()方法。
    • 可重入,可中断,可公平。
    • 性能良好,适合大量并发。

.概括的解释下线程的生命周期周期状态。

1.新建状态(New):new Thread(runable); new操作完成后,此时线程刚被创建是新建状态。
2.就绪状态(Runnable): 当调用thread.start()方法被调用的时候,线程进入就绪状态,可以开始抢占cpu
3.运行状态(Running): 当线程分配到了时间片之后,开始进入运行状态。
4.阻塞状态(Blocked): 当线程还未执行结束前,让出cpu时间片(主动或者被动的),线程进入阻塞状态
5.死亡状态(Dead):线程正常运行结束或者遇到一个未捕获的异常,线程进入死亡状态。

多线程的实现方式?

最基本的是两种:一、通过继承 Thread 类创建线程。二、通过实现 Runnable 接口创建线程执行代码,并放入线程类当中。

创建线程的方法,哪个更好,为什么?

- 继承Thread类
- 实现Runnable接口。较灵活。推荐使用。
- 实现Callable接口。有返回值。

1.继承 Thread 类,重写 run 方法
2.实现 Runnable 接口,重写 run 方法,实现 Runnable 接口的实现类的实例对象作为 Thread 构造函数的 target
3.通过 Callable 和 FutureTask 创建线程
4.通过线程池创建线程

以上其实本质上就是 Thread 和 Runable 的实现方式

★wait 的底层实现?

  • lock.wait()方法最终通过 ObjectMonitor 的 void wait(jlong millis, bool interruptable, TRAPS)方法实现
  1. 将当前线程封装成 ObjectWaiter 对象 node

  2. 通过 ObjectMonitor::AddWaiter 方法将 node 添加到_WaitSet 列表中

  3. 通过 ObjectMonitor::exit 方法释放当前的 ObjectMonitor 对象,这样其它竞争线程就可以获取该 ObjectMonitor 对象

  4. 最终底层的 park 方法会挂起线程

★ 多线程中的 i++ 线程安全吗?为什么?

不安全。就算i变量加上volatile修饰符的话一样是线程不安全的,因为这里的线程不安全并不是内存可见性造成的。
是因为i++不是一个原子性操作。i++分为两步操作,一步是i+1,一步是把结果再赋值给i。假设两个线程同时并发执行
i++操作的话,就会导致少加1的情况。

★AtomicInteger 和 volatile 等线程安全操作的关键字的理解和使用

volatile 关键字保证了可见性。
Atomicinteger 利用 volatile 保证可见性再利用 CAS 机制来保证原子性。

什么叫线程安全?举例说明

线程安全是编程中的术语,指某个函数、函数库在并发环境中被调用时,能够正确地处理多个线程之间的共享变量,使程序功能正确完成。

说说线程安全问题,什么是线程安全,如何实现线程安全?

  1. 使用 synchronized 或者 Lock 自己实现。
  2. 使用线程安全的类。
  3. 不使用全局变量。
  4. 使用 LocalThread 类。
  5. 使用 CAS 更新机制。

BlockingQueue 的使用。(take,poll 的区别,put,offer 的区别)。

take 和 put 是阻塞的。
poll 和 offer 是非阻塞的。

定时线程的使用

可使用 Timer 也可使用 ScheduleThreadPool

如何线程安全的实现一个计数器?

- 使用原子变量
- 使用锁机制
- 自己实现CAS操作

如何写一个线程安全的单例?

public class Singleton {

    private static volatile Singleton instance;

    private Singleton() {}

    public static Singleton getInstance() {
        if (instance == null) {
            synchronized (Singleton.class) {
                //再检查一次
                if (instance == null){
                    instance = new Singleton();
                }
            }
        }
        return instance;
    }

}

或者使用枚举类型的单例,让 JVM 层面来保证单一实例。

★ 多线程同步的方法,如何进行线程间的同步?

使用synchronized修饰符
使用wait\notify机制
使用jdk自带的可重入锁Lock+Condition

介绍一下生产者消费者模式?

生产者生产商品进入线程安全的并发队列当中,消费者从这个队列中获取商品进行消费。
实现并发与解耦。本质上和我们的消息中间件MQ是一样的。

通过三种方式实现生产者消费者模式?

  1. synchronized 加非同步队列的锁
  2. Lock 加 condition
  3. 使用同步队列 BlockingQueue

线程创建有很大开销时,应该怎么优化?

使用线程池进行优化。

★ 请简述一下线程池的运行流程,使用参数以及方法策略等

- 运行流程

如果此时线程池中的数量小于corePoolSize,即使线程池中的线程都处于空闲状态,也要创建新的线程来处理被添加的任务。
如果此时线程池中的数量等于corePoolSize,但是缓冲队列workQueue未满,那么任务被放入缓冲队列。
如果此时线程池中的数量大于等于corePoolSize,缓冲队列workQueue满,并且线程池中的数量小于maximumPoolSize,
建新的线程来处理被添加的任务。
如果此时线程池中的数量大于corePoolSize,缓冲队列workQueue满,并且线程池中的数量等于maximumPoolSize,
那么通过 handler所指定的策略来处理此任务。
当线程池中的线程数量大于 corePoolSize时,如果某线程空闲时间超过keepAliveTime,线程将被终止。这样,
线程池可以动态的调整池中的线程数。


- 使用参数

CorePoolSize:核心线程池大小
MaximumPoolSize:最大线程数
WorkQueue:任务缓存队列
ThreadFactory:线程工厂,主要用来创建线程
keepAliveTime:线程最大的存活时间
Handler:饱和处理策略 

- 饱和处理策略

ThreadPoolExecutor.AbortPolicy:丢弃任务并抛出RejectedExecutionException异常;也是默认的处理方式。
ThreadPoolExecutor.DiscardPolicy:丢弃任务,但是不抛出异常。
ThreadPoolExecutor.DiscardOldestPolicy:丢弃队列最前面的任务,然后重新尝试执行任务(重复此过程)
ThreadPoolExecutor.CallerRunsPolicy:由调用线程处理该任务

★ 简述 ThreadPoolExecutor 内部工作原理?

先查看当前运行状态,如果不是 RUNNING 状态会拒绝执行任务,如果是 RUNNING 状态,就会查看当前运行的线程数量,如果小于核心线程数,
会创建新的线程来执行这个任务,如果不小于核心线程,会将这个任务放到阻塞队列去等代执行,直到上一个任务执行完再来执行这个任务。
如果失败会创建一个非核心线程来执行这个任务如果当前线程数大于最大线程数,会直接拒绝该任务。

★ 常用的线程池模式以及不同线程池的使用场景

newSingleThreadPool:串行化执行的任务
newFixedThreadPool:希望固定线程数的任务,一般设置为 Runtime.getRuntime().availableProcessors()
newCachedThreadPool:适合异步小任务
newScheduledThreadPool:适合周期性的任务

newFixedThreadPool 此种线程池如果线程数达到最大值后会怎么办。

放入等待队列,当线程池中有线程空闲就执行等待队列中的任务

一个线程池正在处理服务如果忽然断电该怎么办?

队列实现持久化储存,下次启动自动载入。
但是实际需要看情况,大体思路是这样。
添加标志位,未处理 0,处理中 1,已处理 2。每次启动的时候,把所有状态为 1 的,置为 0。或者定时器处理
关键性的应用就给电脑配个 UPS。

★ 讲一下 AQS 吧。

AQS的英文是抽象队列同步器的意思。
首先AQS当中主要有三个东西,一个是state变量,一个是当前线程,一个是等待队列。
它的运作机制主要是线程想获取锁的话,先用CAS的机制将state加1,如果成功
的话,在将当前线程变量赋值为自身。由于AQS是可重入的,所以第二次加锁的时候
先判断当前线程变量是否是自身,如果是的话。state变量再进行加1。
在并发的时候,其他线程想加锁的时候,CAS操作state会失败,进入等待队列。
如果之前的线程执行完毕的话,会唤醒等待队列中的线程

通过 AQS 实现一个自定义的 Lock?

自定义独占锁只需要定义一个非公开的内部类继承 AbstractQueuedSynchronizer 类
重写 tryAcquire 和 tryRelease 就可以了。

★Java 中有几种线程池?

1.newFixedThreadPool 这个线程池的corePoolSize和maximumPoolSize大小是一样的
采用的是LinkedBlockingQueue无界阻塞队列。
2.newCachedThreadPool
这个线程池的corePoolSize是0,maximumPoolSize是int最大值
采用的是SynchronousQueue无缓冲等待队列。
3.newScheduledThreadPool
这个线程池采用的是DelayedWorkQueue延迟工作队列。
4.newSingleThreadExecutor
这个线程池corePoolSize和maximumPoolSize都是1,
采用的是LinkedBlockingQueue无界阻塞队列

newFixedThreadPool是固定线程数的线程池,适用于执行长任务,固定线程数避免
线程切换带来的损耗。
newCachedThreadPool是缓冲线程池,适用于执行小任务,这些小任务都可以在
一个时间片内执行完
newScheduledThreadPool适用于周期性执行任务。
newSingleThreadExecutor适用于串行化执行任务。

★SimpleDateFormat 是线程安全的吗?如何解决?

不是线程安全的,解决方案是不要共享 simpleDateFormat 实例,而使用临时生成 SimpleDateFormat 实例,
或者通过 ThreadLocal 对每个线程绑定各自的 SimpleDateFormat.

★threadlocal 为什么会出现 oom?

答:ThreadLocal 里面使用了一个存在弱引用的 map, map 的类型是 ThreadLocal.ThreadLocalMap. Map 中的 key 为一个 threadlocal 实例。这个 Map 的确使用了弱引用,不过弱引用只是针对 key。每个 key 都弱引用指向 threadlocal。 当把 threadlocal 实例置为 null 以后,没有任何强引用指向 threadlocal 实例,所以 threadlocal 将会被 gc 回收。
但是,我们的 value 却不能回收,而这块 value 永远不会被访问到了,所以存在着内存泄露。因为存在一条从 current thread 连接过来的强引用。只有当前 thread 结束以后,current thread 就不会存在栈中,强引用断开,Current Thread、Map value 将全部被 GC 回收。最好的做法是将调用 threadlocal 的 remove 方法。

在 ThreadLocal 的 get(),set(),remove()的时候都会清除线程 ThreadLocalMap 里所有 key 为 null 的 value,但是这些被动的预防措施并不能保证不会内存泄漏:

(1)使用 static 的 ThreadLocal,延长了 ThreadLocal 的生命周期,可能导致内存泄漏。
(2)分配使用了 ThreadLocal 又不再调用 get(),set(),remove()方法,那么就会导致内存泄漏,因为这块内存一直存在。

★ 线程池的好处?

a. 重用存在的线程,减少对象创建、消亡的开销,性能佳。
b. 可有效控制最大并发线程数,提高系统资源的使用率,同时避免过多资源竞争,避免堵塞。
c. 提供定时执行、定期执行、单线程、并发数控制等功能。

线程池启动线程 submit 和 execute 有什么不同?

execute 提交的是 runable 接口的对象,获取不到线程的执行结果。
submit 提交的是 callable 接口的对象,可以获取到线程的执行结果,可以
通过 Future.get 抛出的异常进行捕获。

cyclicbarrier 和 countdownlatch 的区别

CountDownLatch: 一个线程(或者多个), 等待另外N个线程完成某个事情之后才能执行。
类似于赛跑运动中,所有运动员都跑完了才开始颁奖仪式。
CyclicBrrier: N个线程相互等待,任何一个线程完成之前,所有的线程都必须等待。
类似于赛跑运动中,所有运动员互相等待其他所有人都准备好了,才可以起跑。这场跑完,下一场
也是需要所有运动员都准备好了,才开始下一轮起跑。(可重复)

如何理解 Java 多线程回调方法?

java当中的多线程回调主要是利用Callable和Future这两个组件。
在A线程中调用了一个B线程执行一个操作,在这个操作还未执行完成之前,A线程先去做别的事情,
等到预估B线程快执行好了之后,A线程去调用get()方法阻塞获取B线程的执行结果。

★ 同步方法和同步代码块的区别是什么?

1锁粒度不同
2代码块里可以指定同步的标识

★ 在监视器(Monitor)内部,是如何做线程同步的?

AQS的实现原理和底层的Monitor是相似的。
Monitor是虚拟机底层用C++实现的。
- _owner:指向持有ObjectMonitor对象的线程
- _WaitSet:存放处于wait状态的线程队列
- _EntryList:存放处于等待锁block状态的线程队列
- _recursions:锁的重入次数
- _count:用来记录该线程获取锁的次数 

当多个线程同时访问一段同步代码时,首先会进入_EntryList队列中,当某个线程获取到对象的monitor后进入_Owner区域并把monitor中的_owner变量设置为当前线程,同时monitor中的计数器_count加1。即获得对象锁。
若持有monitor的线程调用wait()方法,将释放当前持有的monitor,_owner变量恢复为null,_count自减1,同时该线程进入_WaitSet集合中等待被唤醒。若当前线程执行完毕也将释放monitor(锁)并复位变量的值,以便_EntryList队列中其他线程进入获取monitor(锁)

一旦方法或者代码块被 synchronized 修饰, 那么这个部分就放入了监视器的监视区域, 
确保一次只能有一个线程执行该部分的代码, 线程在获取锁之前不允许执行该部分的代码 。

★ 请说明一下 sleep() 和 wait() 有什么区别?

sleep是让线程休眠让出cpu。在一定时间后会自动恢复运行。但是这个操作是不会释放锁操作的。
wait是让线程等待,一定需要唤醒,才会继续后面的操作。wait是释放锁的。
这两个方法来自不同的类分别是Thread和Object,sleep方法属于Thread类中的静态方法,wait属于Object的成员方法,
对此对象调用wait方法导致本线程放弃对象锁。
wait,notify和notifyAll只能在同步控制方法或者同步控制块(对某个对象同步,然后在块中调用wait或者notify)里面使用,
而sleep可以在任何地方使用(使用范围)。

★ 唤醒一个阻塞的线程

如因为 Sleep,wait,join 等阻塞,可以使用 interrupted exception 异常唤醒。

同步和异步有何异同,在什么情况下分别使用他们?举例说明。

同步:在同一个线程中,语句B必须等待语句A执行完之后,才能继续执行。  例如一条转账记录的生成。A先减钱,扣好再执行B加钱。
异步:在一个线程中,执行A语句用另外一个线程运行,A语句还未运行完成前,就直接运行语句B。  例如用户支付时,还未支付成功前
就直接返回客户端正在支付中的页面,而不是等待支付成功才返回支付成功的页面。

启动一个线程是用 run()还是 start()?

start()

请说出你所知道的线程同步的方法 ?

synchronized隐式锁
Lock显式锁
volatile可见性(但不保证原子性)
ThreadLocal线程局部变量
CAS机制

★★ThreadLocal 什么时候会出现 OOM 的情况?为什么?

1. ThreadLocal的实现是这样的:每个Thread 维护一个 ThreadLocalMap 映射表,这个映射表的 key
是 ThreadLocal实例本身,value 是真正需要存储的 Object。

2. 也就是说 ThreadLocal 本身并不存储值,它只是作为一个 key 来让线程从 ThreadLocalMap 获取 value。
值得注意的是图中的虚线,表示 ThreadLocalMap 是使用 ThreadLocal 的弱引用作为 Key 的,弱引用的对象在 
GC 时会被回收。

3. ThreadLocalMap使用ThreadLocal的弱引用作为key,如果一个ThreadLocal没有外部强引用来引用它,那么
系统 GC 的时候,这个ThreadLocal势必会被回收,这样一来,ThreadLocalMap中就会出现key为null的Entry,
就没有办法访问这些key为null的Entry的value,如果当前线程再迟迟不结束的话,这些key为null的Entry的value
就会一直存在一条强引用链:Thread Ref -> Thread -> ThreaLocalMap -> Entry -> value永远无法回收,造成内存泄漏。

4. 总的来说就是,ThreadLocal里面使用了一个存在弱引用的map, map的类型是ThreadLocal.ThreadLocalMap. Map
中的key为一个threadlocal实例。这个Map的确使用了弱引用,不过弱引用只是针对key。每个key都弱引用指向threadlocal。
当把threadlocal实例置为null以后,没有任何强引用指向threadlocal实例,所以threadlocal将会被gc回收。

但是,我们的value却不能回收,而这块value永远不会被访问到了,所以存在着内存泄露。因为存在一条从current thread
连接过来的强引用。只有当前thread结束以后,current thread就不会存在栈中,强引用断开,Current Thread、Map value
将全部被GC回收。最好的做法是将调用threadlocal的remove方法,这也是等会后边要说的。

5. 其实,ThreadLocalMap的设计中已经考虑到这种情况,也加上了一些防护措施:在ThreadLocal的get(),set(),remove()
的时候都会清除线程ThreadLocalMap里所有key为null的value。这一点在上一节中也讲到过!

6、但是这些被动的预防措施并不能保证不会内存泄漏:
(1)使用static的ThreadLocal,延长了ThreadLocal的生命周期,可能导致内存泄漏。
(2)分配使用了ThreadLocal又不再调用get(),set(),remove()方法,那么就会导致内存泄漏,因为这块内存一直存在。

★ 谈谈对 synchronized 的偏向锁、轻量级锁、重量级锁的理解,以及升级过程?

  • 重量级锁:Monitor 同步成本高
  • 自旋锁: 自旋,不需要从用户态转换为核心态
  • 轻量级锁: CAS 原理
  • 偏向锁: 消除数据在无竞争情况下的同步原语

★ 深入分析 ThreadLocal 的实现原理?

每一个线程里面都有一个TheadLocalMap的数据结构。threadLocal实例调用get的时候,
是先通过本线程为key获取当前线程的TheadLocalMap,再以threadLocal实例为key,去获取对应的值。
数据库连接、Session管理等等都使用到了TheadLocal。

★ 为什么线程的 stop()和 suspend()方法为何不推荐使用?

stop方法不推荐使用的主要原因是stop停止线程的是非优雅停止。它会放弃锁,并立马停止线程。
如果线程当中正在执行一段事务性的操作。这个操作涉及两条语句A、B,在执行到语句A后stop,线程就立马停止,导致语句B未执行。
使得这个操作处于一种不一致的状态。
suspend()方法容易发生死锁。因为一旦调用suspend的时候,线程会暂停,但不放弃锁。(这和wait()方法不一样。wait()方法
是放弃锁。)想要恢复线程的话必须调用resume()方法,但如果调用resume()方法前又需要获取之前的锁的话,这时候就引发了
死锁。

★ 出现死锁,如何排查定位问题?

推荐使用jstack jconsole或者jvisualvm来检测死锁。

线程的 sleep()方法和 yield()方法有什么区别?

yield()方法是只会给相同优先级或更高优先级的线程以运行的机会。sleep让出运行的机会没有线程优先级的区别。
sleep是进入阻塞状态,yield是进入就绪状态。(有可能下次就进去运行态)
sleep()方法声明抛出InterruptedException,而yield()方法没有声明任何异常。
sleep()方法比yield()方法(跟操作系统CPU调度相关)具有更好的可移植性。(不是很理解)

当一个线程进入一个对象的 synchronized 方法 A 之后,其它线程是否可进入此对象的 synchronized 方法 B?

不能,因为方法B也需要持有该对象的锁才能运行。所以在这期间只能运行该对象的非synchronized方法。

★JVM 层面分析 sychronized 如何保证线程安全的?

需要从对象的锁头结构进行分析

每个java对象都可以用做一个实现同步的锁,这些锁成为内置锁。线程进入同步代码块或方法的时候会自动获得该锁,
在退出同步代码块或方法时会释放该锁。获得内置锁的唯一途径就是进入这个锁的保护的同步代码块或方法。而Java
的内置锁又是一个互斥锁,这就是意味着最多只有一个线程能够获得该锁,当线程A尝试去获得线程B持有的内置锁时,
线程A必须等待或者阻塞,知道线程B释放这个锁,如果B线程不释放这个锁,那么A线程将永远等待下去。
从synchronized修饰的同步代码块编译的字节码,我们可以看到同步代码块前后加上monitorenter和monitorexit。
他们分别代表了代表了锁的获取和释放。

请说出与线程同步以及线程调度相关的方法。

wait():使一个线程处于等待(阻塞)状态,并且释放所持有的对象的锁;
sleep():使一个正在运行的线程处于睡眠状态,是一个静态方法,调用此方法要处理InterruptedException异常;
notify():唤醒一个处于等待状态的线程,当然在调用此方法的时候,并不能确切的唤醒某一个等待状态的线程,
而是由JVM确定唤醒哪个线程,而且与优先级无关;
notityAll():唤醒所有处于等待状态的线程,该方法并不是将对象的锁给所有线程,而是让它们竞争,只有获得锁的线程才能进入就绪状态;

说说线程的基本状态以及状态之间的关系?

线程有五个状态:new创建状态,Runnable就绪状态,Running运行状态,Dead消亡状态,Blocked阻塞状态。
创建线程通过start方法进入就绪状态,获取cpu的执行权进入运行状态,失去cpu执行权会回到就绪状态,
运行状态完成进入消亡状态,运行状态通过sleep方法和wait方法进入阻塞状态,
休眠结束或者通过notify方法或者notifyAll方法释放锁进入就绪状态

讲一下非公平锁和公平锁在 Reentrantlock 里的实现?

ReentranLock的底层是AQS。通过一个自定义的同步器来实现锁的获取和释放。默认是非公平的。
公平锁在源码当中,获取锁的时候会去判断队列中是否有等待的线程,没有的话才会去获取锁。
非公平锁在源码中,是直接与所有线程去竞争获取锁。

公平锁是FIFO的,效率较非公平锁低,但为了防止线程一直饥饿。会采用公平锁。
非公平锁,效率较高,但线程获取锁是随机的,极端情况下会导致某个线程一直处于获取不到锁的情况。

请说明一下 synchronized 的可重入怎么实现。

底层是对象监视器。会在对象头部有个区域,专门记录锁信息。包括持有锁的线程,锁的计数器,锁的状态这些。 线程在尝试获取对象锁时,先看看锁计数器是不是为0,为零就说明锁还在,于是获取锁,计数器变成1,并记录下持有锁的线程,当有线程再来请求同步方法时,先看看是不是当前持有锁的线程,是的话,那就直接访问,锁计数器+1,如果不是的话就进入阻塞状态。当退出同步块时,计数器-1,变成0时,释放锁。

为什么 wait, notify 和 notifyAll 这些方法在 Object 类里面?

因为在 java 中每个对象都拥有锁机制对应的监视器,监视器记录了当前持有该对象锁的线程,通过这个对象锁,来协调多线程。所以 wait,notify,notifyAll 放置在 Object 类中。

notify 和 notifyAll 的区别?

notify会选取等待池中的一个线程移入到锁池。
notifyAll会将所有等待池中的线程移入到锁池。

什么是死锁(deadlock)?

两个或两个以上的进程在执行过程中,因争夺资源而造成的一种互相等待的现象。
死锁发生的必要条件: 1.互斥 2.保持锁并请求锁(持有锁并等待) 3.不可抢夺 4.循环等待。
最重要的还是避免循环等待。

如何确保 N 个线程可以访问 N 个资源同时又不导致死锁?

破坏四个条件中其中一个即可。
1.破坏条件二:限制线程不能在持有锁的同时还等待锁。
2.破坏条件四:资源有序分配法。限制所有线程必须先请求A锁再请求B锁。  如果有的线程先请求A锁再请求B锁,
其他线程先请求B锁再请求A锁的话就会造成死锁。

请谈谈什么是进程,什么是线程?

好比电脑上开了一个QQ和一个迅雷。这两个属于进程。
而QQ中的聊天窗口,语音传输,文件传输相当于一个个线程。
主要是因为Cpu太快了,所以才会有线程这个粒度更小的执行单位。并行处理的效率才能更高。

启动线程是用 start()方法还是 run()方法?

start 是启动线程,run 其实就是直接调用方法,并没有多线程的概念。

java 并发包下有哪些类?

答:ConcurrentHashMap,ConcurrentSkipListMap,ConcurrentNavigableMap

CopyOnWriteArrayList

BlockingQueue,BlockingDeque (ArrayBlockingQueue,LinkedBlockingDeque,LinkedBlockingQueue,DelayQueue,PriorityBlockingQueue,SynchronousQueue)

ConcurrentLinkedDeque,ConcurrentLinkedQueue,TransferQueue,LinkedTransferQueue)

CopyOnWriteArraySet,ConcurrentSkipListSet

CyclicBarrier,CountDownLatch, Semaphore, Exchanger

Lock(ReetrantLock,ReetrantReadWriteLock)

Atomic 包

★AQS,抽象队列同步器

AQS 定义 2 种资源共享方式:独占与 share 共享

独占:只能有 1 个线程运行

share 共享:多个线程可以同 p 执行如 samphore/countdownlanch

AQS 负责获取共享 state 的入队和/唤醒出队等,AQS 在顶层已经实现好了,AQS 有几种方法:acquire()是独占模式下线程共享资源的顶层入口,如获取到资源,线程直接返回,否则进入等待队列,直到获取到资源为止。tryAcquire()将线程加入等待队列的尾部,并标志为独占。acquireQueued()使线程在等待队列中获取资源,一直到获取资源后不返回,如果过程被中断也返回 true,否则 false。

线程在等待过程中被中断是不响应的,获取资源才补上中断。将线程添加到队列尾部用了 CAS 自旋(死循环直到成功),类似于 AutomicInteger 的 CAS 自旋 volatile 变量。

start->tryAcquire -> 入队 -> 找安全点 -> park 等待状态 -> 当前节点成对头 -> End

多线程同步锁

A,RentrantLock,可重入的互斥锁,可中断可限时,公平锁,必须在 finally 释放锁,而 synchronize 由 JVM 释放。可重入但是要重复退出,普通的 lock()不能响应中断,lock.lockInterruptbly()可响应中断,可以限时 tryLock(),超时返回 false,不会永久等待构成死锁。

B,Confition 条件变量,signal 唤醒其中 1 个在等待的线程,signalall 唤醒所有在等待的线程 await()等待并释放锁,与 lock 结合使用。

C,semaphore 信号量,多个线程比(额度=10)进入临界区,其他则阻塞在临界区外。

D,ReadWriteLock,读读不互斥,读写互斥,写写互斥。

E,CountDownLantch 倒数计时器,countdown()和 await()

F,CyCliBarrier

G,LockSupport,方法 park 和 unpark

★ThreadLocal 用过么,原理是什么,用的时候要注意什么

Thread 类中有一个 threadLocals 和 inheritableThreadLocals 都是 ThreadLocalMap 类型的变量,而 ThreadLocalMap 是一个定制化的 Hashmap,默认每个线程中这个两个变量都为 null,只有当前线程第一次调用了 ThreadLocal 的 set 或者 get 方法时候才会进行创建。其实每个线程的本地变量不是存放到 ThreadLocal 实例里面的,而是存放到调用线程的 threadLocals 变量里面。也就是说 ThreadLocal 类型的本地变量是存放到具体的线程内存空间的。ThreadLocal 就是一个工具壳,它通过 set 方法把 value 值放入调用线程的 threadLocals 里面存放起来,当调用线程调用它的 get 方法时候再从当前线程的 threadLocals 变量里面拿出来使用。如果调用线程一直不终止那么这个本地变量会一直存放到调用线程的 threadLocals 变量里面,所以当不需要使用本地变量时候可以通过调用 ThreadLocal 变量的 remove 方法,从当前线程的 threadLocals 里面删除该本地变量。另外 Thread 里面的 threadLocals 为何设计为 map 结构那?很明显是因为每个线程里面可以关联多个 ThreadLocal 变量。

需要注意内存泄露问题:
由于 ThreadLocalMap 是以弱引用的方式引用着 ThreadLocal,换句话说,就是 ThreadLocal 是被 ThreadLocalMap 以弱引用的方式关联着,因此如果 ThreadLocal 没有被 ThreadLocalMap 以外的对象引用,则在下一次 GC 的时候,ThreadLocal 实例就会被回收,那么此时 ThreadLocalMap 里的一组 KV 的 K 就是 null 了,因此在没有额外操作的情况下,此处的 V 便不会被外部访问到,而且只要 Thread 实例一直存在,Thread 实例就强引用着 ThreadLocalMap,因此 ThreadLocalMap 就不会被回收,那么这里 K 为 null 的 V 就一直占用着内存。
综上,发生内存泄露的条件是
ThreadLocal 实例没有被外部强引用,比如我们假设在提交到线程池的 task 中实例化的 ThreadLocal 对象,当 task 结束时,ThreadLocal 的强引用也就结束了
ThreadLocal 实例被回收,但是在 ThreadLocalMap 中的 V 没有被任何清理机制有效清理
当前 Thread 实例一直存在,则会一直强引用着 ThreadLocalMap,也就是说 ThreadLocalMap 也不会被 GC
也就是说,如果 Thread 实例还在,但是 ThreadLocal 实例却不在了,则 ThreadLocal 实例作为 key 所关联的 value 无法被外部访问,却还被强引用着,因此出现了内存泄露。

★concurrenthashmap 具体实现及其原理,jdk8 下的改版

jdk7:

在jdk7中是以数组+分段锁(segement)+链表的方式实现,Segment继承自ReentrantLock

使用分段锁的方式是将数据分成一块一块的存储,然后给每一块数据配一把锁,当一个线程访问一块数据的时候,其它线程也可以访问其它块的数据,实现并发访问,大大提高并发访问的能力。

jdk8:

在jdk8中是以数组+链表+红黑树的方式实现,彻底放弃Segement分段存储
其中内部大量采用synchronized和CAS操作,key-value值都是用volatile修饰,保证值的并发可见性

★cas 是什么,他会产生什么问题

CompareAndSet,底层需要依赖 cpu 的原子指令。
会产生 ABA 问题的解决,如加入修改次数、版本号。

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

ConcurrentLinkedQueue 基于 CAS 的无锁技术,不需要在每个操作时使用锁,所以扩展性表现要更加优异,在常见的多线程访问场景,一般可以提供较高吞吐量。
LinkedBlockingQueue 内部则是基于锁,并提供了 BlockingQueue 的等待性方法。

★concurrent 包中使用过哪些类?分别说说使用在什么场景?为什么要使用?

原子类:提供原子累加的数值功能
锁类:提供 ReentranLock 锁和读写锁
并发集合类:提供 Map 或者 List 的并发实现
并发工具类:等待多线程完成的 CountDownLatch、同步屏障 CylicBarriar、控制并发数的 Semaphore、线程间交换数据的 Exchanger
阻塞队列:提供多种形式的阻塞队列

★Condition 接口及其实现原理

在经典的生产者-消费者模式中,可以使用 Object.wait()和 Object.notify()阻塞和唤醒线程,但是这样的处理下只能有一个等待队列。在可重入锁 ReentrantLock 中,使用 AQS 的 condition 提供了类似 object.wait 和 notify 的线程通信机制,可以实现设置多个等待队列,使用 Lock.newCondition 就可以生成一个等待队列。

★Fork/Join 框架的理解

ForkJoin 框架的作用其实和并行流的作用类似。适合于分而治之的场景。比如累加 1 到 100,开启 10 个线程,分别累加 1-10,11-20....等 10 个段再进行汇总。ForkJoin 的底层还有一个 workStealing 的思想。

★sleep 和 sleep(0)的区别。

sleep 的作用是在未来的 n 毫秒内,不参与到 CPU 竞争。
sleep(0)的作用是触发操作系统立刻重新进行一次 CPU 竞争。

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

CountDownLatch 闭锁
CyclicBarrier 循环栅栏
.Exchanger 线程交换器
Semaphore 信号量

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

shutdown:(非阻塞等待所有线程执行完毕,此方法就已执行)
1、调用之后不允许继续往线程池内继续添加线程;
2、线程池的状态变为 SHUTDOWN 状态;
3、所有在调用 shutdown()方法之前提交到 ExecutorSrvice 的任务都会执行;
4、一旦所有线程结束执行当前任务,ExecutorService 才会真正关闭。

shutdownNow():
1、该方法返回尚未执行的 task 的 List;
2、线程池的状态变为 STOP 状态;
3、阻止所有正在等待启动的任务, 并且停止当前正在执行的任务。

★ 假如有一个第三方接口,有很多个线程去调用获取数据,现在规定每秒钟最多有 10 个线程同时调用它,如何做到。

可以使用 ScheduledThreadPool 的 scheduleAtFixedRate 方法

★countdowlatch 和 cyclicbarrier 的内部原理和用法,以及相互之间的差别(比如 countdownlatch 的 await 方法和是怎么实现的)。

CountDownLatch 原理

CountDownLatch 是使用一组线程来等待其它线程执行完成,这个场景类似于一群人考试,先做的人先交了,但是在考试时间没到的前提下,老师必须额等待最后一个学生完成交卷老师才能走,CountDownLatch 使用 Sync 继承 AQS。构造函数很简单地传递计数值给 Sync,并且设置了 state,这个 state 的值就是倒计时的数值,每当一个线程完成了自己的任务(学生完成交卷),那么就使用 CountDownLatch.countdown()方法来做一次 state 的减一操作,在内部是通过 CAS 完成这个更新操作,直到所有的线程执行完毕,也就是说计数值变成 0,那么就然后在闭锁上等待的线程就可以恢复执行任务。

CyclicBarrier 原理 栅栏

CyclicBarrier 的字面意思是可循环(Cyclic)使用的屏障(Barrier)。它要做的事情是,让一组线程到达一个屏障(也可以叫同步点)时被阻塞,直到最后一个线程到达屏障时,屏障才会开门,所有被屏障拦截的线程才会继续干活。线程进入屏障通过 CyclicBarrier 的 await()方法。
实现原理:在 CyclicBarrier 的内部定义了一个 Lock 对象,其实就是 ReenTrantLock 对象,每当一个线程调用 CyclicBarrier 的 await 方法时,将剩余拦截的线程数减 1,然后判断剩余拦截数是否为 0,如果不是,进入 Lock 对象的条件队列等待。如果是,执行 barrierAction 对象的 Runnable 方法,然后将锁的条件队列中的所有线程放入锁等待队列中,这些线程会依次的获取锁、释放锁,接着先从 await 方法返回,再从 CyclicBarrier 的 await 方法中返回。

在 CyclicBarrier 类的内部有一个计数器,每个线程在到达屏障点的时候都会调用 await 方法将自己阻塞,此时计数器会减 1,当计数器减为 0 的时候所有因调用 await 方法而被阻塞的线程将被唤醒。这就是实现一组线程相互等待的原理。

★ 用过读写锁吗,原理是什么,一般在什么场景下用。一般在读多写少的场景下进行使用。

(1)只要没有线程占用写锁,那么任意数目的线程都可以持有这个读锁。
(2)只要没有线程占用读写锁,那么才能为一个线程分配写锁。
读锁相当于一个共享锁,写锁 i 相当于独占锁。

★ 开启多个线程,如果保证顺序执行,有哪几种实现方式,或者如何保证多个线程都执行完再拿到结果。(或者用三个线程按顺序循环打印 abc 三个字母,比如 abcabcabc)

使用 synchronized, wait 和 notifyAll
使用 Lock 和 Condition
使用 Semaphore
使用 AtomicInteger

可以使用 CountDownLatch 进行实现

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

延迟队列是无界阻塞队列,通过计算当前时间与任务时间的差值,小于 0 的话则取出。
而时间轮算法的是借鉴钟表盘的原理,由一个进程推进时间格前进,例如我们要在晚上八点执行任务的话,只需要表盘走过一圈,并到达第八格时,即可执行。

如何解决死锁?

可以使用有序资源分配法或者银行家算法进行避免死锁。

线程池如何调优

可以根据实际情况从最大线程数、等待队列、拒绝策略等参数进行调优。

★ 对 AbstractQueuedSynchronizer 了解多少,讲讲加锁和解锁的流程,独占锁和公平锁加锁有什么不同。

AbstractQueuedSynchronizer 会把所有的请求线程构成一个 CLH 队列,当一个线程执行完毕(lock.unlock())时会激活自己的后继节点,但正在执行的线程并不在队列中,而那些等待执行的线程全部处于阻塞状态.
线程的显式阻塞是通过调用 LockSupport.park()完成,而 LockSupport.park()则调用 sun.misc.Unsafe.park()本地方法,再进一步,HotSpot 在 Linux 中中通过调用 pthread_mutex_lock 函数把线程交给系统内核进行阻塞。

转自我的 github

技术讨论群 QQ:1398880
  • 面试

    面试造航母,上班拧螺丝。多面试,少加班。

    325 引用 • 1395 回帖

相关帖子

欢迎来到这里!

我们正在构建一个小众社区,大家在这里相互信任,以平等 • 自由 • 奔放的价值观进行分享交流。最终,希望大家能够找到与自己志同道合的伙伴,共同成长。

注册 关于
请输入回帖内容 ...