大家好,欢迎来到IT知识分享网。
文章目录
-
- 1. 谈谈进程和线程之间的区别【高频】
- 2. java中有哪些方式来创建线程?
- 3. run和start的区别【经典面试题】
- 4. Java线程的状态
- 5. 【线程不安全的原因】
- 6. 就以count++为例:一个线程加锁、一个线程不加锁,此时能否保证线程的安全呢?
- 7. 要加锁的代码如果不是在一个方法里,怎么办呢?
- 8. synchronized的特性:
- 9. Java库中常见安全线程和不安全线程
- 10. volatile的特性
- 11. 在面试中一旦谈到了volatile,多半是脱离不了JMM(Java Memory Model:java内存模型)。
- 12. 【面试题】 wait和sleep的对比:
- 13. wait和notify
- 14. 【单例模式】懒汉和饿汉模式,谁才是线程安全的?
- 15. 如何解决指令重排序带来的问题呢?
- 16. 理解双重 if 判定 / volatile:
- 17. 【了解】非先进先出队列
- 18. 阻塞队列特点及典型应用场景
- 19. 生产者-消费者模型/阻塞队列的好处
- 20. 阻塞队列的具体使用
- 21. 标准库中的定时器Timer
- 22. (了解)小结定时器实现:
- 23. 为啥从池子里取比创建新线程快?
- 24. (了解)Executors.newCachedThreadPool()创建线程池
- 25. 如何把N个任务分配给M个线程呢?
- 26. 标准库里提供的ThreadPoolExecutor的构造方法以及拒绝策略
- 27. 延伸问题:线程池不是可以自定义线程数目吗,那么在实际开发中,线程池的数目如何确定?设定为几计较合适呢?2?4?…
- 28. 多线程的案例:单例模式、阻塞队列、定时器、线程池都是日常开发中常用的多线程相关的基础组件,务必要重点掌握。
- 29. 描述一下线程池的执行流程和拒绝策略有哪些?【面试题!】
- 30. 锁相关
- 31. 你是怎么理解乐观锁和悲观锁的,具体怎么实现呢?
- 32. 介绍下读写锁?
- 33. 什么是自旋锁,为什么要使用自旋锁策略呢,缺点是什么?
- 34. synchronized 是可重入锁么?
- 35. 【补】synchronized特点
- 36. CAS了解
- 37. CAS的ABA问题
- 38. synchronized
- 39. 什么是偏向锁?
- 40. synchronized 实现原理 是什么?
- 41. 介绍下 Callable 是什么?
- 42. 面试中常见问题:说说synchronized和ReentrantLock的区别
- 43. Executors (工厂类)创建线程池的几种方式
- 44. 信号量Semaphore
- 45. JUC:标准库提供的多线程安全相关的包
- 46. 线程同步的方式有哪些?
- 47. 为什么有了 synchronized 还需要 juc 下的 lock?
- 48. AtomicInteger 的实现原理是什么?
- 49. 信号量听说过么?之前都用在过哪些场景下?
- 50. 多线程环境使用哈希表
- 51. ConcurrentHashMap的优化策略
- 52. HashMap、HashTable、ConcurrentHashMap的区别:(顺序一定要搞对,①②最重要!!)
- 53. ConcurrentHashMap的读是否要加锁,为什么?
- 54. 介绍下 ConcurrentHashMap的锁分段技术?
- 55. ConcurrentHashMap在jdk1.8做了哪些优化?
- 56. 【教科书给出死锁的四个必要条件】
- 57. 死锁相关
- 58. 谈谈 volatile关键字的用法?
- 59. Java多线程是如何实现数据共享的?
- 60. Java创建线程池的接口是什么?参数LinkedBlockingQueue 的作用是什么?
- 61. Java线程共有几种状态?状态之间怎么切换的?
- 62. 在多线程下,如果对一个数进行叠加,该怎么做?
- 63. Servlet是否是线程安全的?
- 64. Thread和Runnable的区别和联系?
- 65. 多次start一个线程会怎么样?
- 66. 有synchronized两个方法,两个线程分别同时用这个方法,请问会发生什么?
- 67. 进程和线程的区别?
努力经营当下 直至未来明朗
1. 谈谈进程和线程之间的区别【高频】
2. java中有哪些方式来创建线程?
3. run和start的区别【经典面试题】
4. Java线程的状态
5. 【线程不安全的原因】
① 抢占式执行:多个线程调度执行过程可以视为是“全随机”的(也不能理解成纯随机的,但是确实在应用层程序上是没有规律的)(所以:在写代码的时候,就需要考虑到在任意一种调度的情况下都是能够运行出正确结果的)
(内核实现的,我们无能为力)
② 多个线程修改同一个变量:
【String是不可变对象(也就是不能修改String的内容,这并不是说用final修饰,而是把set系列方法给藏起来了),设计成不可变的好处之一就是“线程安全”】
(有时候可以通过调整代码来规避线程安全问题,但是普适性不高)
③ 修改操作不是原子的:
CPU执行指令都是以“ 一个指令”为单位进行执行,一个指令就相当于CPU上的“最小单位”了,不会说该条指令还没执行完就把线程调度走了。
(eg. count++就是三条指令
而像是有的修改操作如int的赋值就是单个CPU指令,安全一些)
注:解决线程安全问题最常见的方法就是从这里入手:把多个操作通过特殊手段打包成一个原子操作 (一个线程是否安全的判定是复杂的)
④ 内存可见性问题:JVM的代码优化(逻辑等价条件下提高效率)引入的bug
⑤ 指令重排序
(以上并不是线程不安全的全部原因)
6. 就以count++为例:一个线程加锁、一个线程不加锁,此时能否保证线程的安全呢?
7. 要加锁的代码如果不是在一个方法里,怎么办呢?
8. synchronized的特性:
- 互斥:也就是 加锁/解锁。
(上一个线程解锁之后, 下一个线程并不是立即就能获取到锁.) - 刷新内存(存疑)
- 可重入【不好理解】:
1) 一个线程针对同一把锁连续加锁两次就可能造成死锁。
而在加锁两次之后不会产生死锁的就叫做“可重入锁”,会产生死锁的叫“不可重入锁”。
2)可重入锁的底层实现其实很简单:只要 让锁里面记录好是哪个线程持有的这把锁。
如:t线程尝试对this对象来加锁,this这个锁里面就记录了是t线程持有了它;第二次进行加锁的时候如果该this锁发现是原来的t线程,则直接通过,没有任何负面影响,不会阻塞等待。
3) 那么在何时进行解锁又是一个问题:引入计数器。
每次加锁,计数器++, 每次解锁,计数器–。 只有在计数器为0的时候才能够真正加锁和解锁
可重入锁的实现要点:
①让锁里持有线程对象,记录是谁加了锁;
②维护一个计数器,用来衡量啥时候真加锁,啥时候真解锁,啥时候又是直接放行。
4) 在加锁代码中出现异常,如果没人catch就会脱离之前的代码块,脱离一层代码块计数器就-1,然后经过多次脱离 则计数器会最终减到0;所以是不会在加锁代码中出现异常时死锁的,无论如何解锁代码都是可以执行到的。
5) 可重入已经在synchronized中处理好了,也就不会发生死锁了。
9. Java库中常见安全线程和不安全线程
10. volatile的特性
- synchronized(加锁)保证了线程的可重入性和原子性!! 而volatile是保证“内存可见性”的,但是不保证原子性。
- 针对一个线程读,一个线程修改 的场景,使用volatile是合适的; 针对两个线程修改的场景,volatile是无能为力的,因为没有原子性
(volatile可以多加,但是千万不要少加)
11. 在面试中一旦谈到了volatile,多半是脱离不了JMM(Java Memory Model:java内存模型)。
12. 【面试题】 wait和sleep的对比:
13. wait和notify
14. 【单例模式】懒汉和饿汉模式,谁才是线程安全的?
15. 如何解决指令重排序带来的问题呢?
答:办法就是:禁止指令重排序。那么如何禁止呢?就是使用volatile关键字,既能保证内存可见性(读、修改线程并发,但是其实细想是不会存在的:因为每个线程有各自工作的一套CPU寄存器,有各自的上下文),又能禁止指令重排序(避免得到不完全对象,内存数据无效)。
16. 理解双重 if 判定 / volatile:
17. 【了解】非先进先出队列
18. 阻塞队列特点及典型应用场景
- 阻塞队列是一个特殊的队列,但是其确实是 “先进先出” 的。
- 阻塞队列特点:
① 线程安全
② 带有阻塞功能:
A)如果队列满了还继续入队列,此时入队操作就会阻塞;直到队列不满,入队列才能成功
B)如果队列空了还继续出队列,此时出队操作就会阻塞;直到队列不空,出队列才能成功 - 阻塞队列的典型应用场景:生产者-消费者模型(描述的是多线程协同工作的一种方式),该模型能够较好地解决锁冲突问题。 (举例:包饺子)
19. 生产者-消费者模型/阻塞队列的好处
① 使用阻塞队列,有利于代码 “解耦合”
耦合:两个模块之间的关联关系,关系越紧密则耦合性越高。
如果使用阻塞队列,当流量骤增的时候,生产者和阻塞队列就承受了压力,而其余消费者还是按照原来的节奏来消费数据,即对消费者的冲击就不大。
20. 阻塞队列的具体使用
(队列有三个基本操作:入队列、出队列、取 队首元素)
- 阻塞队列提供给了带有阻塞的入队列和出队列方法,但是没有提供带有阻塞的取队首元素方法。
- 标准库的阻塞队列
1)BlockingQueue 也有 offer, poll, peek 等方法, 但是这些方法不带有阻塞特性。而put、take方法带有阻塞。
2) BlockingQueue是接口。 - 自己实现一个阻塞队列
1) 先实现一个普通队列
(队列的实现有两个版本:基于链表、基于数组)
① 循环队列:下标head/tail 如果head == tail就是null/满,如果存入数据则tail++,并且队列中有效元素范围是[head,tail); 而如果到达末尾就又回到开头,达到循环
② 如何区分该队列是空还是满呢?空和满都是head==tail,即指针重合,区分方法就是:要么加一个记录个数的变量,要么浪费一个空间
2)加上线程安全 (线程安全就是加锁synchronized)
3)加上阻塞实现(队列为空则出队列阻塞,队列满则入队列阻塞)
这里注意稳妥的写法,wait不一定是另一个线程中的notify来唤醒的,也可能是interrupt来唤醒的,如果是interrupt唤醒可能条件就还不成熟,所以需要循环再判断。
(wait被唤醒之后也是要去竞争锁的)
21. 标准库中的定时器Timer
- 标准库中的定时器 Timer类(使用的是java.util中的)。
- 而在Timer类中的一个重要的方法是
schedule(TimerTask task, long delay)。
① 参数1 TimerTask task是要安排的任务,其实就是一个Runnable接口,我们要做的就是继承这个TimerTask,然后重写run方法,从而指定要执行的任务。【注意!参数1是 new TimerTask() {…重写run方法},不是new Runnable() {…}】
② 参数2 long delay 就是指经过delay (ms)之后开始执行参数1任务task。 - 一个定时器可以同时安排多个任务!
- 执行代码会发现,在执行完task任务之后进程并没有退出,理由:Timer内部需要一组线程来执行注册的任务task,而这里的线程是前台线程,会影响进程退出。
22. (了解)小结定时器实现:
23. 为啥从池子里取比创建新线程快?
24. (了解)Executors.newCachedThreadPool()创建线程池
- 使用
Executors.newCachedThreadPool()来创建线程池,使用ExecutorService来接收
- 线程池使用submit方法把任务提交到线程池中即可,线程池中就会有一些线程来完成这里的任务。
25. 如何把N个任务分配给M个线程呢?
26. 标准库里提供的ThreadPoolExecutor的构造方法以及拒绝策略
标准库里提供的ThreadPoolExecutor其实是更复杂一些的,尤其是构造方法,可以支持很多参数,可以支持很多选项,让我们创建出不同风格的线程池
- 构造方法【常见面试题!!】
1) 查看ThreadPoolExecutor里的构造方法:java.util.concurrent(并发) -> ThreadPoolExecutor (线程池)
2) 此处只分析最后一个构造方法:
① corePoolSize:核心线程数
② maximumPoolSize:最大线程数
(任务数量是不太确定的,有时候任务多了,核心线程处理不过来,此时就需要更多的线程来帮助一起处理任务;当任务处理完之后,这些除了核心线程外的线程在一定时间的空闲之后就可以销毁了;但是核心线程即使空闲也不会销毁。
灵活调配这两个数值,可以做到既能够处理任务巅峰,又能够在空闲的时候节省资源。)
③ keepAliveTime:运行的额外线程空闲的最大时间,也就是空闲上限。
④ unit:时间的单位
⑤ workQueue:手动给线程池传入一个任务队列。其实在线程池中是有自己的队列的(如果不自己手动传入就会在线程池内部自己创建),但是有时候代码的业务逻辑中本身就有一个队列来保存这里的任务,此时如果把自己队列中的任务再拷贝到线程池内部就是画蛇添足了,直接就让线程池消费业务逻辑中已有的队列即可!
⑥ threadFactory:描述了线程是如何创建的。工厂对象就负责创建线程,程序员可以手动指定线程的创建策略。
⑦ RejectedExecutionHandler handler:【重点!常考!】线程池的拒绝策略。线程池的任务队列已经满了(工作线程忙不过来了),如果又添加了别的新任务,那该怎么办呢?
——这个拒绝策略对于实现“高并发”服务器也是非常有意义的。 - 以下就是标准库中提供的拒绝策略:
① AbortPolicy:中断策略,直接抛异常 handler(回调,处理方法)
② CallerRunsPolicy:调用者来执行,而不是被调用者来执行(按理来说是被调用者执行);如果调用者也不执行就丢弃该任务
③ DiscardOldestPolicy:丢弃最老的未处理请求
④ DiscardPolicy:直接丢弃最新的任务
(实际开发中,需要根据请求来决定使用哪种策略)
【面试官考察你对于ThreadPoolExecutor的理解,其实主要就是在考察拒绝策略】
27. 延伸问题:线程池不是可以自定义线程数目吗,那么在实际开发中,线程池的数目如何确定?设定为几计较合适呢?2?4?…
28. 多线程的案例:单例模式、阻塞队列、定时器、线程池都是日常开发中常用的多线程相关的基础组件,务必要重点掌握。
29. 描述一下线程池的执行流程和拒绝策略有哪些?【面试题!】
30. 锁相关
- 锁的核心特性 “原子性”, 这样的机制追根溯源是 CPU 这样的硬件设备提供的。
- 操作系统默认的锁的调度是非公平锁。但是如果要想实现公平锁就需要引入额外的数据结构来记录加锁的顺序,此时就需要一定的额外开销。
- Java里只要以Reentrant开头命名的锁都是可重入锁,而且JDK提供的所有现成的Lock实现类,包括synchronized关键字锁都是可重入的;而 Linux 系统提供的 mutex 是不可重入锁。
- 锁内容参考:锁和CAS
31. 你是怎么理解乐观锁和悲观锁的,具体怎么实现呢?
32. 介绍下读写锁?
33. 什么是自旋锁,为什么要使用自旋锁策略呢,缺点是什么?
34. synchronized 是可重入锁么?
35. 【补】synchronized特点
- 对于synchronized:
① 开始时是乐观锁, 如果锁冲突频繁, 就转换为悲观锁.
② 开始是轻量级锁实现, 如果锁被持有的时间较长, 就转换成重量级锁
③乐观锁/轻量级锁的部分是基于自旋锁实现的,悲观锁的部分是基于挂起等待锁实现的
④不是读写锁,而是普通互斥锁
⑤是非公平锁
⑥是可重入锁
(在标准库中是有另外的其他锁能够实现④⑤的) - 所以:
synchronized是自适应的:初始使用的时候是乐观锁/轻量级锁/自旋锁,如果锁竞争不激烈就保持上述状态不变;但是如果锁竞争激烈了,synchronized就会自动升级成悲观锁/重量级锁/挂起等待锁。
36. CAS了解
- CAS 可以视为是一种乐观锁. (或者可以理解成 CAS 是乐观锁的一种实现方式)
- CAS即:【 compare and swap】即比较并交换:把内存中的某个值M和CPU寄存器A中的值进行比较,如果两个值相同,就把另一个寄存器B中的值和内存的值M进行交换(把内存的值M放到寄存器B,同时把寄存器B的值写给内存;其实这是一个“写内存”操作,更关心的是交换后内存值M)
(返回值是是否操作成功) - CAS的操作是由CPU的一条指令原子性完成的,所以是线程安全的,效率也较高。(高效是因为没有锁冲突和线程等待)
- CAS应用:实现原子类、实现自旋锁。
37. CAS的ABA问题
- CAS的ABA问题【面试的时候谈到CAS,十有八九就会谈到ABA】
1)这是CAS的一个小缺陷
2)在CAS中进行比较的时候,如果此时的寄存器A和内存M的值相同,你无法判定是内存M的值始终不变还是M变了,但是又变回来了
3)CAS的ABA问题其实在大部分情况下也不是事儿,不会出现bug;但是在极端情况下是会出现问题的。
4)ABA在极端情况下是啥效果?
举例:可能会导致一次取钱,两次扣款(也就是在第一个线程完成取款之后,又有人转账,而第二个线程进行CAS检查时候发现数值相同,就进行扣款操作) - 如何解决ABA带来的问题?
只要有一个记录能够记录上内存的变化就可以解决ABA问题了。
那么如何进行记录呢?
——另外搞一个内存,保存内存M的“修改次数”(版本号)或者是“上次修改时间”,通过这两种方法都是可以解决ABA问题的。
“修改次数”“上次修改时间”都是【只增不减】,也就是说无法再跳回去。
此时比较的就不是寄存器A和内存M了,而是比较版本号/上次修改时间。 - 相关面试题
1)讲解下你自己理解的 CAS 机制
答:CAS全称 compare and swap, 即 “比较并交换”。 相当于通过一个原子的操作, 同时完成 “读取内存, 比较是否相等, 修改内存” 这三个步骤。本质上需要 CPU 指令的支撑。
2)ABA问题怎么解决?
答: ① 给要修改的数据引入版本号。在 CAS 比较数据当前值和旧值的同时, 也要比较版本号是否符合预期。
② 如果发现当前版本号和之前读到的版本号一致, 就真正执行修改操作, 并让版本号自增; 如果发现当前版本号比之前读到的版本号大, 就认为操作失败。
38. synchronized
- JVM 将 synchronized 锁分为 无锁、偏向锁、轻量级锁、重量级锁 状态。会根据情况,进行依次升级。
- synchronized加锁的具体过:
1)涉及锁升级/锁膨胀: 无竞争,偏向锁;有竞争,轻量级锁; 竞争激烈,重量级锁。
2)锁自适应:实现了轻量级锁和重量级锁的“自适应”操作。
- synchronized的优化手段:
1)锁消除:锁消除是一种编译器优化的行为,但是编译器的判定不是特别准,此时如果不是编译器有十足/100%的把握都是不会进行synchronized的自动消除的。 也就是说:锁消除只是在编译器/JVM有十足的把握的时候才进行。
2)锁粗化:实际开发过程中, 使用细粒度锁, 是期望释放锁的时候其他线程能使用锁。但是实际上可能并没有其他线程来抢占这个锁, 这种情况 JVM 就会自动把锁粗化, 避免频繁申请释放锁。(锁粗化的前提是代码的逻辑不变)
3)锁升级/膨胀
39. 什么是偏向锁?
答: 偏向锁不是真的加锁, 而只是在锁的对象头中记录一个标记(记录该锁所属的线程)。 如果没有其他线程参与竞争锁, 那么就不会真正执行加锁操作, 从而降低程序开销。 一旦真的涉及到其他的线程竞争, 再取消偏向锁状态, 进入轻量级锁状态。
40. synchronized 实现原理 是什么?
答: 参考【synchronized原理】所有内容:特点+加锁过程+优化手段。
41. 介绍下 Callable 是什么?
答: ① Callable 是一个 interface 。相当于把线程封装了一个 “返回值”, 方便程序员借助多线程的方式计算结果。
② Callable 和 Runnable 相对, 都是描述一个 “任务”。Callable 描述的是带有返回值的任务, Runnable 描述的是不带返回值的任务。
③ Callable 通常需要搭配 FutureTask 来使用, FutureTask 用来保存 Callable 的返回结果。 因为Callable 往往是在另一个线程中执行的, 啥时候执行完并不确定。 FutureTask 就可以负责这个等待结果出来的工作。
42. 面试中常见问题:说说synchronized和ReentrantLock的区别
- ReentrantLock类的核心用法(三个方法):
①lock()加锁;
②unlock()解锁;
③tryLock()试试看能否加锁,成功即加锁,不成功就不加 - ReentrantLock的 缺点:如果在lock() 和 unlock() 之间有return或者是有异常,就可能执行不到unlock了;而synchronized没有该风险,只要代码出了代码块就一定执行解锁。
(即:synchronized 使用时不需要手动释放锁。ReentrantLock 使用时需要手动释放. 使用起来更灵活, 但是也容易遗漏 unlock) - 有些特定的功能是synchronized做不到的。也就是ReentrantLock优势:
1)tryLock()试试看能否加锁,试成功即加锁,试失败就放弃不加;并且还可以设定加锁的等待超时时间。(实际开发中使用“死等”策略要慎重)
(即:synchronized 在申请锁失败时, 会死等。ReentrantLock 可以通过 trylock 的方式等待一段时间就放弃)
2)ReentrantLock 可以实现公平锁,本来默认是非公平锁。传入一个参数true就是公平锁(“先来后到”)
3)synchronized是搭配wait/notify实现等待通知机制的,唤醒操作是随机唤醒一个等待的线程。而 ReentrantLock搭配Condition类实现的,唤醒操作是可以指定哪个线程等待的线程被唤醒的。 - 网上资料还有一个区别:synchronized是java关键字,底层是JVM实现的(也就是大概率通过C++实现的);而ReentrantLock是标准库中的一个类,底层是基于java实现的。
43. Executors (工厂类)创建线程池的几种方式
Executors 本质上是 ThreadPoolExecutor 类的封装
44. 信号量Semaphore
- 信号量本身是一个计数器,表示可用资源的个数。
① P操作申请一个资源,可用资源数-1
② V操作释放一个资源,可用资源数+1. - 当计数为0的时候继续P操作就会产生阻塞,阻塞等待到其他线程V操作了为止(基于信号量也是可以实现“生产者消费者模型”)
- 当计数为0的时候继续P操作就会产生阻塞,阻塞等待到其他线程V操作了为止(基于信号量也是可以实现“生产者消费者模型”)
- 注意:标准库中的acquire就是P操作申请资源, release就是V操作释放资源
- 当需求中就是有多个可用资源的时候,就要记得使用信号量Semaphore
45. JUC:标准库提供的多线程安全相关的包
- CountDownLatch
1)使用CountDownLatch就是类似于跑步比赛,使用的时候先设定选手/线程的个数,每个选手撞线(完成工作)就调用以下countDown方法,当撞线次数达到选手的个数就结束比赛。(也就是说,要等到最后一个选手到达/最后一个线程也完成任务 才真正结束)
2)如使用多线程完成任务:要下载一个很大的文件,此时就切分成很多个部分,每个线程负责下载其中的一部分,当所有的线程都下载完毕,整个文件才算下载完成。
46. 线程同步的方式有哪些?
答:synchronized, ReentrantLock, Semaphore 等都可以用于线程同步。
47. 为什么有了 synchronized 还需要 juc 下的 lock?
48. AtomicInteger 的实现原理是什么?
49. 信号量听说过么?之前都用在过哪些场景下?
答:① 信号量, 用来表示 “可用资源的个数”, 本质上就是一个计数器。
② 使用信号量可以实现 “共享锁”, 比如某个资源允许 3 个线程同时使用, 那么就可以使用 P 操作作为加锁, V 操作作为解锁, 前三个线程的 P 操作都能顺利返回, 后续线程再进行 P 操作就会阻塞等待, 直到前面的线程执行了 V 操作。
50. 多线程环境使用哈希表
- HashTable是线程安全的,但是不推荐使用,其是属于无脑给各种方法加synchronized
- ConcurrentHashMap线程安全的,推荐使用,背后做了很多优化。(多线程下直接无脑使用,单线程使用HashMap)
- HashTable只是简单的把关键方法加上了 synchronized 关键字。
51. ConcurrentHashMap的优化策略
- 锁粒度的控制
1) HashTable直接在方法上加synchronized,相当于是对this加锁,即相当于是针对哈希表对象来加锁的,所以,一个哈希表只有一个锁。多个线程的时候,无论这些线程是如何操作这个哈希表的,都会产生锁冲突。
2) HashTable只有一把锁,而ConcurrentHashMap不是一把锁,而是多把锁,然后给每个哈希桶(哈希桶就是哈希表下的一个链表)都加一把锁。
3) 如果两个线程同时访问一个哈希桶的时候才会锁冲突,但如果不是同一个哈希桶就不会锁冲突了。
4)由于哈希桶个数很多,此时恰好两个线程操作同一个哈希桶的概率就大大降低了,因此锁冲突的概率就降低了,大大提升了性能。(锁冲突对性能影响是很大的。)
ConcurrentHashMap的每个哈希桶都有自己的锁,大大降低了锁冲突的概率,性能也就大大提升了。 - ConcurrentHashMap做了一个激进的操作:只给写操作加锁,读操作不加锁。
1)也就是说:两个线程同时修改才会有锁冲突。两个线程同时读是没有锁冲突的; 一个线程读、一个线程写也是没有锁冲突的。
2) 那么 “一个线程读、一个线程写没有锁冲突” 这个操作是否会有线程安全问题?
- 充分利用了CAS的特性
1)比如像维护元素个数,都是通过CAS来实现的,而不是加锁; 包括还有一些地方也是直接使用CAS实现的轻量级锁来实现。
2)虽然synchronized内部已经有很多的优化了,但是终究这里的优化是JVM内部的,程序员不可控; 而ConcurrentHashMap的思路就是能不加锁就不加锁)
3)则ConcurrentHashMap的核心优化思路:尽一切可能降低锁冲突的概率!
但凡涉及到很多加锁操作,代码就基本和“高性能”/运行效率/高并发 无缘了。
(性能固然重要,但是相比之下,代码的正确性、开发效率才是更重要的) - ConcurrentHashMap对于扩容操作也进行了特殊的优化:化整为零(有点儿类似拷贝)
1)HashTable的扩容:当put元素的时候,如果发现当前的负载因子(元素个数/哈希桶的个数)已经超过阈值就需要进行扩容,即申请一个更大的数组,把之前旧的数据给搬运到新的数组上。有一个大问题:如果元素个数很多,则搬运操作就会开销很大。执行一个put操作,正常一个put瞬间完成(哈希表的O(1)特性),但是触发扩容的这一下put可能就会卡很久。
2)ConcurrentHashMap在扩容的时候就不是一次性搬运了,而是一次搬运一点儿。
在触发扩容的时候创建一个新的数组,在扩容期间内,旧的和新的会同时存在一段时间,每次进行哈希表操作的时候都会把旧的内存上的元素搬运一部分到新的空间上,直到最终搬运完成,然后再释放旧的空间。
在这个过程中如果要查询元素,则旧的和新的一起查;如果是插入元素,直接往新的上面插;如果是删除元素,直接删了就不用搬运了。
52. HashMap、HashTable、ConcurrentHashMap的区别:(顺序一定要搞对,①②最重要!!)
答:① HashMap线程不安全,HashTable、ConcurrentHashMap 线程安全。
② HashTable、ConcurrentHashMap虽然都是线程安全,但是有很多差别:锁粒度的控制(一把、很多锁),ConcurrentHashMap写操作加锁、读操作不加锁,ConcurrentHashMap利用了CAS特性,ConcurrentHashMap扩容优化:化整为零。
③ 旧版本(jdk1.8之前不包含1.8)的ConcurrentHashMap的实现是分段锁,而新版本(jdk1.8开始)的ConcurrentHashMap是每个链表分一个锁。 【分段锁:好几个链表共用同一把锁。 但是分段锁的锁冲突概率要比每个链表加一把锁更高,代码实现也更复杂】
④ HashMap的 key 允许为null(HashMap是无序的!!TreeMap是有序的),HashTable、ConcurrentHashMap的key不能为null。
53. ConcurrentHashMap的读是否要加锁,为什么?
答: 读操作没有加锁。目的是为了进一步降低锁冲突的概率, 为了保证读到刚修改的数据, 搭配了volatile 关键字。
54. 介绍下 ConcurrentHashMap的锁分段技术?
55. ConcurrentHashMap在jdk1.8做了哪些优化?
56. 【教科书给出死锁的四个必要条件】
① 互斥使用:锁A被线程1占用,线程2就用不了 (打破不了,锁的基本特性)
② 不可抢占:锁A被线程1占用,线程2就不能把锁A给抢过来,除非线程1释放锁(打破不了,锁的基本特性)
③ 请求和保持:有多把锁,线程1拿到锁A之后,不想释放锁A,还想请求再拿到一个锁B(取决于代码:获取锁B的时候是否释放锁A,有可能打破,但是不普适。主要看需求场景是否允许这么写)
④ 循环等待:线程1等待线程2释放锁,线程2释放锁得等待线程3释放锁,线程3释放锁得等待线程1释放锁 (有把握打破:约定好加锁顺序就可以打破循环等待)
57. 死锁相关
- 死锁:一个线程加上锁之后解不开了,也就是程序僵住了。
- 死锁多个线程、多把锁,更容易死锁。 描述该死锁场景的一个典型问题:哲学家就餐。
- 学校操作系统针对死锁给出的方案是“银行家算法”(把所有资源统一进行统筹分配),也能避免死锁,是一个更普适的方案,但是比较复杂,不太适合实际开发
58. 谈谈 volatile关键字的用法?
答:volatile 能够保证内存可见性, 强制从主内存中读取数据。 此时如果有其他线程修改被 volatile 修饰的变量, 可以第一时间读取到最新的值。
59. Java多线程是如何实现数据共享的?
60. Java创建线程池的接口是什么?参数LinkedBlockingQueue 的作用是什么?
61. Java线程共有几种状态?状态之间怎么切换的?
62. 在多线程下,如果对一个数进行叠加,该怎么做?
答:① 使用 synchronized / ReentrantLock 加锁
② 使用 AtomInteger 原子操作
63. Servlet是否是线程安全的?
64. Thread和Runnable的区别和联系?
65. 多次start一个线程会怎么样?
66. 有synchronized两个方法,两个线程分别同时用这个方法,请问会发生什么?
67. 进程和线程的区别?
免责声明:本站所有文章内容,图片,视频等均是来源于用户投稿和互联网及文摘转载整编而成,不代表本站观点,不承担相关法律责任。其著作权各归其原作者或其出版社所有。如发现本站有涉嫌抄袭侵权/违法违规的内容,侵犯到您的权益,请在线联系站长,一经查实,本站将立刻删除。 本文来自网络,若有侵权,请联系删除,如若转载,请注明出处:https://haidsoft.com/113816.html

