1. ConcurrentHashMap的读是否要加锁,为什么?
读操作没有加锁,目的是为了进一步降低锁冲突的概率,为了保证读到刚修改的数据,搭配了volatile 关键字;
2. 介绍下 ConcurrentHashMap 的锁分段技术?
这个是 Java1.7 中采取的技术,Java1.8 中已经不再使用了,简单的说就是把若干个哈希桶分成一个 "段"(Segment),针对每个段分别加锁,目的也是为了降低锁竞争的概率,当两个线程访问的数据恰好在同一个段上的时候,才触发锁竞争;
3. ConcurrentHashMap在jdk1.8做了哪些优化?
取消了分段锁,直接给每个哈希桶(每个链表)分配了一个锁(就是以每个链表的头结点对象作为锁对 象),将原来 数组 + 链表 的实现方式改进成 数组 + 链表 / 红黑树 的方式,当链表较长的时候(大于等于 8 个元素,并且数组长度大于等于 64)就转换成红黑树;
4. Hashtable 和 HashMap、ConcurrentHashMap 之间的区别?
HashMap: 线程不安全,key 允许为 null,
Hashtable: 线程安全,使用 synchronized 锁 Hashtable 对象,效率较低,key 不允许为 null,
ConcurrentHashMap: 线程安全,使用 synchronized 锁每个链表头结点,锁冲突概率低,充分利用 CAS 机制,优化了扩容方式,key 不允许为 null;
5. 谈谈 volatile 关键字的用法?
volatile 能够保证内存可见性,强制从主内存中读取数据,此时如果有其他线程修改被 volatile 修饰的变量,可以第⼀时间读取到最新的值,volatile 还可以避免指令重排序引起的问题;
6. Java多线程是如何实现数据共享的?
JVM 把内存分成了这几个区域: 方法区,堆区,栈区,程序计数器,其中方法区和堆区是多个线程之间共享的,只要把某个数据放到堆内存中,就可以让多个线程都能访问到;
7. Java 创建线程池的接口是什么?参数的含义?
在这两篇博客中都有涉及:Java 多线程(01)—— 认识线程和创建线程,线程池拒绝策略创建线程池主要有两种方式:通过 Executors 工厂类创建,创建方式比较简单,但是定制能力有限;通过ThreadPoolExecutor 创建.创建方式比较复杂,但是定制能力强;
8. LinkedBlockingQueue 的作用是什么?
LinkedBlockingQueue 表示线程池的任务队列,用户通过 submit / execute向这个任务队列中添加任务,再由线程池中的工作线程来执行任务;
9. Java线程共有几种状态?状态之间怎么切换的?
NEW:安排了工作还未开始行动,新创建的线程,还没有调用 start 方法时处在这个状态;
RUNNABLE:可工作的,又可以分成正在工作中和即将开始工作,调用 start 方法之后,并正在CPU上 运行 / 在即将准备运行的状态
BLOCKED:使用 synchronized 时,如果锁被其他线程占用,就会阻塞等待,并进入该状态;
WAITING:调用 wait 方法会进入该状态;
TIMED_WAITING:调用 sleep 方法或者 wait(超时时间) 会进入该状态;
TERMINATED:工作完成了,当线程 run 方法执行完毕后,会处于这个状态;
10. 在多线程下,如果对一个数进行叠加,该怎么做?
使用 synchronized/ReentrantLock 加锁,使用 AtomicInteger 原子操作;
11. Servlet是否是线程安全的?
Servlet 本身是工作在多线程环境下,如果在 Servlet 中创建了某个成员变量,此时如果有多个请求到达服务器,服务器就会在多线程进行操作,是可能出现线程不安全的情况的;
12. Thread 和 Runnable 的区别和联系?
Thread 类描述了一个线程,Runnable描述了一个任务;在创建线程的时候需要指定线程完成的任务,可以直接重写 Thread 的 run 方法,也可以使用 Runnable 来描述这个任务;
13. 多次 start ⼀个线程会怎么样?
第一次调用 start 可以成功,后续再调用 start 会抛出 java.lang.IllegalThreadStateException 异常
14. 有 synchronized 两个方法,两个线程分别同时用这个方法,请问会发生什么?
synchronized 加在非静态方法上,相当于针对当前对象加锁;
如果这两个方法属于同一个实例:线程1 能够获取到锁,并执行方法,线程2 会阻塞等待,直到线程1 执行完毕,释放锁,线程2 获取到锁之后才能执行方法内容;
如果这两个方法属于不同实例:两者能并发执行,互不干扰;
15. 进程和线程的区别?
进程是包含线程的,每个进程至少有一个线程存在,即主线程;进程和进程之间不共享内存空间.同一个进程的线程之间共享同一个内存空间;进程是系统分配资源的最小单位,线程是系统调度的最小单位。
16. 线程同步的方式有哪些?
synchronized,ReentrantLock,Semaphore 等都可以用于线程同步;
17. 为什么有了 synchronized 还需要 juc 下的 lock?
以 juc 的 ReentrantLock 为例,synchronized 使用时不需要手动释放锁,ReentrantLock 使用时需要手动释放,使用起来更灵活;synchronized 在申请锁失败时,会死等,ReentrantLock 可以通过trylock 的方式等待一段时间就放弃;synchronized 是非公平锁,ReentrantLock 默认是非公平锁,但可以通过构造方法传入一个 true 开启公平锁模式;synchronized 是通过 Object 的 wait/notify 实现等待,唤醒,每次唤醒的是一个随机等待的线程,ReentrantLock 搭配 Condition 类实现等待唤醒,可以更精确控制唤醒某个指定的线程;
18. AtomicInteger 的实现原理是什么?
基于 CAS 机制,详细信息可以看这篇博客:Java多线程(04)
19. 信号量听说过么?之前都用在过哪些场景下?
信号量,用来表示 "可用资源的个数",本质上就是一个计数器,使用信号量可以实现 "共享锁" ,比如某个资源允许 3 个线程同时使用,那么就可以使用 P 操作作为加锁,V操作作为解锁,前三个线程的P操作都能顺利返回,后续线程再进行 P 操作就会阻塞等待,直到前面的线程执行了 V 操作;
20. 什么是偏向锁?
偏向锁不是真的加锁,而只是在锁的对象头中记录一个标记(记录该锁所属的线程),如果没有其他线程参与竞争锁,那么就不会真正执行加锁操作,从而降低程序开销,一旦真的涉及到其他的线程竞争,再取消偏向锁状态,进入轻量级锁状态;
21. 讲解下你自己理解的 CAS 机制
全称Compareandswap,即"比较并交换",相当于通过一个原⼦的操作,同时完成 "读取内存,比较是否相等,修改内存" 这三个步骤,本质上需要 CPU 指令的支撑;
22. ABA 问题怎么解决?
给要修改的数据引入版本号;在 CAS 比较数据当前值和旧值的同时,也要比较版本号是否符合预期,如果发现当前版本号和之前读到的版本号一致,就真正执行修改操作,并让版本号自增,如果发现当前版本号比之前读到的版本号大,就认为操作失败;
23. 你是怎么理解乐观锁和悲观锁的,具体怎么实现呢?
悲观锁认为多个线程访问同一个共享变量冲突的概率较大,会在每次访问共享变量之前都去真正加锁.;乐观锁认为多个线程访问同一个共享变量冲突的概率不大,并不会真的加锁,而是直接尝试访问数据.,在访问的同时识别当前的数据是否出现访问冲突;悲观锁的实现就是先加锁(比如借助操作系统提供的 mutex),获取到锁再操作数据,获取不到锁就等待,乐观锁的实现可以引入一个版本号;借助版本号识别出当前的数据访问是否冲突;
24. 介绍下读写锁
读写锁就是把读操作和写操作分别进行加锁,读锁和读锁之间不互斥,写锁和写锁之间互斥,写锁和读锁之间互斥,读写锁最主要用在 "频繁读,不频繁写" 的场景中;
25. 什么是自旋锁,为什么要使用自旋锁策略呢,缺点是什么?
如果获取锁失败,立即再尝试获取锁,无限循环,直到获取到锁为止,第一次获取锁失败,第二次的尝试会在极短的时间内到来,一旦锁被其他线程释放,就能第一时间获取到锁,
相比于挂起等待锁,优点:没有放弃CPU资源,一旦锁被释放就能第一时间获取到锁,更高效,在锁持有时间比较短的场景下非常有用;缺点:如果锁的持有时间较长,就会浪费 CPU 资源;
26. synchronized 是可重入锁么?
是可重入锁,可重入锁指的就是连续两次加锁不会导致死锁,实现的方式是在锁中记录该锁持有的线程身份,以及⼀个计数器(记录加锁次数),如果发现当前加锁的线程就是持有锁的线程,则直接计数自增;