首页 > 编程语言 >Java面试题汇总-集合篇+JUC篇

Java面试题汇总-集合篇+JUC篇

时间:2025-01-18 15:57:39浏览次数:3  
标签:JUC 面试题 Java hashmap ThreadLocal 线程 哈希 java

其他面试题

Java面试题汇总-Java基础篇(共50道题)-CSDN博客

Java集合

目录

1.请你说说java中hashmap的原理

2.HashMap的put(key,value)和get(key)过程

3.在使用hashmap时,有哪些提升性能的技巧?

4.什么是哈希碰撞?怎么解决哈希碰撞?

5.谈一谈hashmap的扩容操作?

6.hashmap的初始容量和负载因子越大越好吗?

7.为什么jdk1.8之后要对hashmap进行了红黑树的改动?

8.jdk1.8对hashmap除了对红黑树的改动还有哪些改动呢?

9.扩容是在原有的hashmap中扩还是新建一个hashmap对象?如果是后者?旧hashmap怎么处理?

10.hashmap有什么缺点呢?

11.java中有哪些集合类?

12.请说一下ArrayList和LinkedList的区别?

13.讲讲ArrayList的扩容机制

14.ConcurrentHashMap和hashmap的区别是什么?


1.请你说说java中hashmap的原理

在jdk1.7前,hashMap底层是由数组和链表实现的,key值会通过hashCode()方法计算出哈希值并映射到数组的槽位,当多个key值映射到同一个槽位时,则会以链表形式放到同一个槽位。

Jdk1.8时得到优化,链表长度超过8时将链表转换为红黑树,提高查询性能。(延伸→为什么要转换为红黑树?跳转至第七题)

Hashmap的默认初始容量为 16,负载因子为 0.75。也就是说,当存储的元素数量超过 16x0.75=12 个时, Hashap会触发扩容操作,容量x2并重新分配元素位置。这种扩容是比较耗时的操作,频繁扩容会影响性能。

2.HashMap的put(key,value)和get(key)过程

在put时,

  1. 我们将key/value传给put,它调用hashCode计算hash从而得到插槽位置,

  2. 判断这个插槽位置是否已经被占用,没有的话就直接存入,

  3. 有的话就遍历这个位置的链表或红黑树看看是否有相同的键,

  4. 有的话就更新这个键的值,没有的话就直接存入到链表或数据结构;

  5. 然后hashmap开始判断链表的长度是否超过阈值,

  6. 是的话就转换为红黑树进行存储;

  7. 之后hashmap检查键值对的数量与数组长度比值是否达到负载因子,是的话就进行扩容。

在get时,

  1. 我们将key传给get,它调用hashCode计算hash从而得到插槽位置,

  2. 进一步调用equals方法确定键值对,从而取出value值。

3.在使用hashmap时,有哪些提升性能的技巧?

1.合理设置hashmap的初始容量负载因子,尽量减少扩容操作 2.尽量让hash值均匀分布,避免哈希碰撞 3.选择合适的数据类型,使用合适的数据结构:LinkedHashMap(插入顺序存储)、ConcurrentHashMap(高并发线程安全)、TreeMap (可以排序)

4.什么是哈希碰撞?怎么解决哈希碰撞?

Hash 碰撞是指不同的数据通过哈希算法计算后得到相同的哈希值,映射到了哈希表的同一个位置,发生”碰撞”解决方案: 1.拉链法(链地址法): 将哈希表的每一个槽的位置变成一个链表,将哈希值相同的键存储到同一个链表中 2.开放寻址法: 如果出现碰撞,就寻找哈希表下一个可用的位置. 3.再哈希法(双重哈希): 在出现碰撞时,使用第二个哈希函数计算新的索引位置, 减少碰撞的概率

5.谈一谈hashmap的扩容操作?

hashMap默认的负载因子是0.75,即如果hashmap中的元素个数超过了总容量75%,,则会触发扩容,扩容分为两个步骤:

  • 第1步是对哈希表长度的扩展(2倍)

  • 第2步是将旧哈希表中的数据放到新的哈希表中。

因为hashmap的初始容量是16,转换为2进制就是有四位(2的四次方),每次扩容都是乘以2,也就是位数+1,每次扩容的时候元素的hash值也会发生变化,这时候只需要看hash值新增的那个位是0还是1即可,如果是1那该元素在新hashmap的新位置为:旧位置+旧hashmap容量。

因此,hashmap扩容之后,部分元素的位置会发生变化,但不需要去重新计算哈希值,但即使如此hashmap的扩容操作还是一个比较耗时的操作,我们要尽可能减少hashmap的扩容。

  • 延伸→怎么减少扩容?→(设置大一点的初始容量和负载因子)

  • 延伸→初始容量和负载因子越大越好吗?(问题6)

  • 延伸→扩容是在原有的hashmap中扩还是新建一个hashmap对象?如果是后者?旧hashmap怎么处理?(问题9)

6.hashmap的初始容量和负载因子越大越好吗?

不是的,初始容量设置的大一点确实是可以减少扩容操作,但同时带来的问题就是内存空间的浪费,而负载因子大一点确实也能减少扩容操作,但太大也会影响性能,最极端的例子就是当负载因子为1时,虽然可以减少扩容次数,提高内存利用率,但它增加了哈希冲突的可能性。冲突过多会导致桶内链表或红黑树变长,降低查找插入和删除的效率。因此,负载因子1.0会使得时间复杂度劣化为 0(n),不利于 Hashap的高效运行。

7.为什么jdk1.8之后要对hashmap进行了红黑树的改动?

在 JDK 1.8之前, Hashmap 使用链表来解决哈希冲突。当哈希冲突较多时,链表中的元素增多,查找、插入和删除的时间复杂度从 O(1) 退化为 O(n)。 因此在JDK1.8引入红黑树,将链表长度超过一定阈值(默认 8)时的链表转换为红黑树,避免性能急剧下降。当链表长度降到6以下时红黑树会重新退化为链表,保持简单高效。 红黑树是一种平衡二叉搜索树,插入、删除、查找操作的时间复杂度为 Ò(log n),在元素多的情况下远优于链表的 O(n)。

延伸→既然红黑树效率这么高,那为什么不直接用红黑树就好了,还要用链表呢?

因为红黑树节点的大小是普通节点大小的两倍,所以为了节省内存空间不会直接只用红黑树,只有当节点到达一定数量才会转成红黑树,这里定义的是 8。 为什么是 8 呢?通过计算表明这个长度是时间和空间的平衡点。

8.jdk1.8对hashmap除了对红黑树的改动还有哪些改动呢?

1.优化了哈希值的计算 2.将头插法改成了尾插法 3.优化了扩容机制,在jdk1.8之前,是将所有元素重新计算哈希值,然后根据哈希值重新寻找插入的哈希桶,在idk1.8之后是如果是旧数组中对应的新数组的位置,就直接复制到新数组当中,如果不对应,就计算元素的哈希值,并找到插入的哈希桶

9.扩容是在原有的hashmap中扩还是新建一个hashmap对象?如果是后者?旧hashmap怎么处理?

在Java中的HashMap实现中,当HashMap需要扩容时,它会创建一个新的HashMap实例,并将旧HashMap中的所有元素重新计算哈希值后插入到新的HashMap中。这个过程称为rehashing。

具体来说,当HashMap中的元素数量超过其容量(capacity)和加载因子(load factor)的乘积时,HashMap会进行扩容。扩容操作通常包括以下几个步骤:

  1. 创建一个新的HashMap实例,其容量通常是原HashMap容量的两倍。

  2. 遍历原HashMap中的所有元素。

  3. 对于每个元素,根据新的容量重新计算其哈希值。(jdk1.8前)

  4. 将元素插入到新的HashMap中。

  5. 将原HashMap中的所有引用指向新的HashMap。

这个过程中,旧的HashMap实例不会被修改,而是被新的HashMap实例替代。这样做的好处是可以减少在扩容过程中对HashMap操作的影响,因为元素的插入和查找操作不需要在扩容的同时进行。不过,这也意味着在扩容过程中,HashMap可能会暂时占用更多的内存,因为旧的HashMap实例和新的HashMap实例会同时存在,直到所有的元素都被迁移到新的HashMap中。

总而言之就是:是新建一个容量为旧容量两倍的hashmap,然后把旧hashmap的元素迁移过去,旧hashmap之后会被JVM的垃圾回收机制回收。

(这里可能会被面试官延伸到JVM和垃圾回收机制那边)

10.hashmap有什么缺点呢?

hashmap比较明显的两个缺点就是扩容操作开销大线程不安全

hashmap不是线程安全的,hashmap在多线程会存在下面的问题: JDK 1.7 HashMap 采用数组 +链表的数据结构,多线程背景下,在数组扩容的时候,存在 Entry 链死循环和数据丢失问题。 JDK 1.8 HashMap 采用数组 + 链表+ 红黑二叉树的数据结构,优化了 1.7 中数组扩容的方案,解决了Entry 链死循环和数据丢失问题。但是多线程背景下,put 方法存在数据覆盖的问题。

如果要保证线程安全,可以使用ConurrentHashMap这个数据结构。

(这里可能可以把面试官引入ConurrentHashMap这个知识点来)

11.java中有哪些集合类?

Java 中的集合类主要分为两大类:Collection 接口Map接囗 前者是存储对象的集合类,后者存储的是键值对(key-value)

Collection 接口下又分为 ListSetQueue 接口,每个接口有其具体实现类,

Map 接口下有很多接口,比如HashMap , inkedHashMap , TreeMap , Hashtable , ConcurrentHashMap

  • List下有ArrayList(基于动态数组,查询速度快,插入、删除慢),LinkedList(基于双向链表,插入、删除快,查询速度慢)Vector(线程安全的动态数组,类似于 ArrayList,但开销较大);

  • Set下有HashSet(基于哈希表,元素无序,不允许重复),LinkedHashSet(基于链表和哈希表,维护插入顺序,不允许重复)TreeSet(基于红黑树,元素有序,不允许重复);

  • Queue下有PriorityQueue(基于优先队列,元素有序),LinkedList(可以作为常规队列使用,先进先出)

关于hashmap的知识点请参考以下文章:每日速记10道java面试题05-CSDN博客

12.请说一下ArrayList和LinkedList的区别?

两者的底层数据结构不一样,ArrayList是用数组实现的,LinkedList是用链表实现的。

插入和删除的效率不同,ArrayList删除和插入元素效率较低,LinkedList删除和插入元素效率比较高,只需要移动指针即可。

随机访问的效率不同,ArrayList底层是由数组实现的,因此随机访问效率高,时间复杂度为O(1),LinkedList需要从头开始遍历链表,时间复杂度为O(n)

13.讲讲ArrayList的扩容机制

1.ArrayList 构造函数允许指定初始容量。如果不指定,ArrayList 默认的初始容量为 10。 2.当 ArrayList 中的元素数量达到当前数组的容量时,需要添加更多元素,ArrayList 会进行扩容操作。扩容操作通常涉及创建一个新的数组,其容量是原数组容量的 1.5 倍(newCapacity = oldCapacity + (oldCapacity >> 1)),即原容量加上原容量的一半。 3.创建新数组后,ArrayList 会将原数组中的所有元素复制到新数组中。 4.复制完成后,ArrayList 会将内部的数组引用指向新的数组

延伸→那旧数组最终去哪了呢?→被JVM内部的垃圾回收机制给回收掉了。

14.ConcurrentHashMap和hashmap的区别是什么?

  • HashMap非线程安全的,这意味着在多线程环境中,如果多个线程同时对 HashMap 进行读写操作,可能会导致数据不一致或抛出 ConcurrentModificationException

  • ConcurrentHashMap线程安全的,它允许多个线程同时对容器进行读写操作而不会导致数据不一致。

延伸→那ConcurrentHashMap 是如何保证线程安全的呢?

分段锁机制(Java 7)

在 Java 7 中,ConcurrentHashMap 使用分段锁技术,将哈希表分成多个段(Segment),每个段相当于一个小的 HashMap,并且继承自 ReentrantLock,即每个段拥有一个独立的锁。这样,当多个线程访问 ConcurrentHashMap 时,只要它们访问的不是同一个段,就可以实现真正的并发访问,从而减少锁竞争,提高并发性能。

CAS + synchronized 机制(Java 8)

在 Java 8 中,ConcurrentHashMap 对实现方式进行了改进,放弃了分段锁,而是采用了“CAS + synchronized”的机制来保证线程安全。如果某个桶没有元素,那么使用 CAS 操作来添加新节点;如果有元素,则使用 synchronized 锁住当前桶,再次尝试 put 操作。这样可以避免分段锁机制下的锁粒度太大,提高了并发性能。

Java并发

目录

1.什么是java中的线程同步?

2.java中的Synchronized是怎么实现的?

3.java中的ReentrantLock是怎么实现的?

4.java中Synchronized和ReentrantLock有什么区别?

5.什么是AQS?

6.线程的生命周期在java中是怎样的?

7.java中如何创建多线程?

8.你了解java线程池的原理吗?

9.换做是你,你会如何设置java线程池的数量?

10.java线程池有哪些拒绝策略?

11.java并发库中提供了哪些线程池?

12.java线程池中shutdown和shutdownNow的区别?

13.Java 中的 DelayQueue 和 ScheduledThreadPool 有什么区别?

14.你了解java中的读写锁吧?

15.说一说读写锁的原理

16.什么是java内存模型?

17.什么是java的原子性、可见性、有序性?

18.Java 中的 final 关键字是否能保证变量的可见性?

19.为什么在 Java 中需要使用 ThreadLocal?

20.Java 中的 ThreadLocal 是如何实现线程资源隔离的?

21.为什么 Java 中的 ThreadLocal 对 key 的引用为弱引用?

22.既然说了那么多ThreadLocal,那TheadLocal有什么缺点呢?

23.如何将当前线程的局部变量传递到另一个线程?

24.Java中Thread.sleep 和 Thread.yield 的区别?

25.Java 中 Thread.sleep(0)的作用是什么?

26.ThreadLocal的原理(简易版)

27.线程中blocked和waiting有什么区别?

28.怎么保证多线程安全?

29.Java中有哪些常用的锁,在什么场景下使用?

30.怎么理解可重入锁?

31.Java中想实现一个乐观锁,都有哪些方式?

32.java中volatile关键字有什么作用?

33.volatile可以替代synchronized吗?

34.什么是java中的ABA问题?


1.什么是java中的线程同步?

当一个共享资源被多个线程访问时进行保护的行为叫线程同步,让线程有序访问,目的是为了避免出现数据不一致等问题。

  • 延伸→如何保证线程同步?→常见的线程同步的方式1.Synchronized 2.ReentrantLock

  • 这里可能会给自己挖坑,把面试官待到Synchronized 和ReentrantLock两个知识点来

  • 延伸→java中的Synchronized是怎么实现的?→问题2

  • 延伸→java中的ReentrantLock是怎么实现的?→问题3

  • 延伸→java中Synchronized和ReentrantLock有什么区别?→问题4

2.java中的Synchronized是怎么实现的?

synchronized 实现原理依赖于JVM 的 Monitor(监视器锁)和对象头(Object Header)。

synchronized 修饰在方法或代码块上时,会对特定的对象或类加锁,从而确保同一时刻只有一个线程能执行加锁的代码块。当synchronized 修饰方法:方法的常量池会增加一个ACCSYNCHRONIZED 标志,当某个线程访问这个方法检查是否有 ACC SYNCHRONIZED 标志,若有则需要获得监视器锁才可执行方法,此时就保证了方法的同步。

synchronized 修饰代码块:会在代码块的前后插入 monitorenter和 monitorexit 字节码指令。可以把 monitorenter 理解为加锁,monitorexit 理解为解锁。

3.java中的ReentrantLock是怎么实现的?

1.ReentrantLock是基于AQS实现的支持可重入,公平/非公平,可中断的锁。 2.内部通过一个state变量两个队列(同步队列和等待队列)来实现 3.同步队列存放的是需要竞争这个锁的线程,是双向链表,等待队列存放的是需要满足condition才执行的线程,是单向链表。 4.锁是否公平的区别在于:竞争锁时,是否需要判断自身是否是队列中的第一个线程,公平锁需要判断,非公平锁则不用,直接尝试获取即可,非公平锁和公平锁获取失败都需要加入同步队列。 5.公平锁在获取锁时会判断当前线程是否已经是同步队列的第一个线程,或同步队列为空

延伸→什么是AQS?问题5

4.java中Synchronized和ReentrantLock有什么区别?

锁的获取方式:synchronized 是隐式获取锁的,即在进入 synchronized 代码块或方法时自动获取锁退出时自动释放锁;而ReetrantLock 需要程序显式地获取锁和释放锁

锁的性质:synchronized 是可重入的互斥锁,即同一个线程可以多次获得同一把锁,而且锁的释放也只能由获得锁的线程来释放,ReetrantLock 可以是可重入的互斥锁,也可以是非可重入的互斥锁,还可以是读写锁。

锁的粒度:synchronized 是以代码块和方法为单位进行加锁和解锁,而 ReetrantLock 可以精确地控制锁的范围,可以支持多个条件变量。

性能:

低并发的情况下,sychronized 的性能优于ReetrantLock,因为 ReetrantLock 需要显式地获取和释放锁,而synchronized 是在JVM 层面实现的;

高并发的情况下,ReetrantLock 的性能可能优于 synchronized,因为 ReetrantLock 可以更好地支持高并发和读写分离的场景。

5.什么是AQS?

AQS 是 AbstractQueuedSynchronizer 的缩写,它是 Java 并发包 java.util.concurrent.locks 中的一个抽象类,提供了一个用于构建锁和其他同步器的框架。

简单来说 AQS 就是起到了一个抽象、封装的作用,将一些排队、入队、加锁、中断等方法提供出来,便于其他相关 JUC 锁的使用,具体加锁时机、入队时机等都需要实现类自己控制。 常见的实现类有ReentrantLock、CountDownLatch、Semaphore等等

6.线程的生命周期在java中是怎样的?

在 Java 中,线程的生命周期可以细化为以下几个状态:

  • New(初始状态):线程对象创建后,但未调用 start()方法。

  • Runnable(可运行状态):调用 start()方法后,线程进入就绪状态,等待 CPU 调度。

  • Blocked(阻塞状态):线程试图获取一个对象锁而被阻塞。

  • Waiting(等待状态):线程进入等待状态,需要被显式唤醒才能继续执行。

  • Timed Waiting(含等待时间的等待状态):线程进入等待状态,但指定了等待时间,超时后会被唤醒。

  • Terminated(终止状态):线程执行完成或因异常退出。

7.java中如何创建多线程?

1)继承Thread类:用户自定义继承Java.lang.Thread类,重写run方法,在run方法里面定义线程的具体实现,创建该类的实例后,通过start方法启动线程。 优点:编写简单 缺点:继承了Thread类,所以不能再继承其他类,功能比较局限

2)实现Runnable接口:用户自定义实现java.lang.Runnable接口,重写run方法,此后将runnable对象作为参数传递给thread类的构造器,创建thread对象后再调用start方法启动线程。 优点:可以继承其他的类,而且在这种方式下,可以多个线程共享同一个目标对象,非常适合多个相同线程处理同一份资源的情况,体现了面向对象的思想。 缺点:相比较第一种方法编程略微复杂。

3)实现callable接口与FutureTask:Callable接口与runnable类似,但callable的call方法可以有返回值且可以抛出异常,要执行callable任务,需要把他包装进一个FutureTask中来实现。 优点缺点也是和runnable类似,优点是可以实现多线程处理同一份资源,缺点是编程稍微复杂。

4)使用线程池(Executor框架):可以通过Executors类的静态方法创建不同类型的线程池。

优点:线程池可以重用预先创建的线程,避免了线程创建和销毁的开销,提高了程序的性能;而且通过合理配置线程池大小,可以最大化CPU利用率和系统吞吐量

缺点:增加了程序的复杂度。

延伸→这边面试官可能就会问你关于线程池的问题了。

8.你了解java线程池的原理吗?

线程池使用原理:先使用核心线程数量,当核心线程数量用完后,使用队列存储线程,不同的线程池内置的队列长度不同,存在无限制长的队列也存在容量为0的队列,当队列长度消耗光的时候会去使用最大线程池数量最大线程池数量用完后会触发线程池拒绝策略,一共有4种拒绝策略,默认抛出异常,第二种会将当前任务交给父线程执行,阻塞向线程池添加的速度,第三种是抛弃最早的线程重新加入线程队列,第四种是直接抛弃新来的线程调最早的线程。

用通俗易懂的话来理解就是:

线程池可以理解为,去银行办理业务:默认有6个柜台,当没有人去银行办理业务时候,柜台小姐姐都是拉呱玩耍的,有人去办理业务,先开三个柜台来办理业务,若三个柜台都有人在办理业务,那来的人先去等候区,若此时等候区也满了,就去新开另外三个柜台,随着办理业务人员增多,新开的柜台也都有人在办理业务,排队等候区也满了,那你还来办理业务??想搞事呢??明天再来或者去前面找个人插队,把第一个等候区的人赶走……,这就是经理的拒绝策略。

延伸→这时候面试官可能会问:换做是你,你会如何设置java线程池的数量?

9.换做是你,你会如何设置java线程池的数量?

对于不同类型的任务,线程数设置不同

如果是cpu密集型的任务,一般i/o开销较小,可以充分利用cpu资源,线程数就是cpu核心数+1

IO密集型任务,会因为i/o阻塞,导致无法高效利用cpu,如果任务较多,就需要更多的线程,一般就是cpu核心数*2

以上的公式都只是一个理论值,实际情况还得具体情况具体分析,例如机器性能、预期CPU利用率等等,并不是写死的一个数。

10.java线程池有哪些拒绝策略?

  • abort: 默认。抛异常。适用于要通知调用者任务没有被执行的场景。

  • callerRuns:调用者处理任务,哪个线程提交的任务,哪个线程自己执行。

  • DiscardOldest:扔掉任务队列里面排队时间最长的。适用于丢弃老的任务,处理重要的新任务的场景。

  • discard: 直接静悄悄的扔掉。适用于丢掉任务没有任何影响的场景。

11.java并发库中提供了哪些线程池?

Executors类提供五种静态工厂方法用于创建不同类型的线程池。

  • 固定线程池的核心和最大线程数相同,队列无界,适合于数量确定的稳定任务,但可能导致内存溢出。

  • 工作窃取池是在JDK8引入的,利用ForkoinPool实现,能在处理完自己的任务后去其他线程的队列中窃取任务。

  • 单线程池中只有一个线程,能按顺序执行任务,适合需要顺序执行的场景。

  • 缓存线程池的核心线程数为0,最大线程数可以无限,适合短时间大量短任务的场景,但可能导致频繁的上下文切换。

  • 计划线程池用于需要定时或周期性执行任务,底层使用DelavedWorkQueue实现延时任务。

12.java线程池中shutdown和shutdownNow的区别?

1.shutdown会关闭线程池,拒绝接收新的任务,会把任务队列中的任务执行完再关闭。适用于程序需要平滑停止线程池的场景,如应用程序正常退出时。保证所有已提交的任务都能执行完毕,避免任务丢失。 2.shutdownNow:强制关闭线程池,将任务队列的任务返回,清空任务队列,强制中断当前执行的任务,但是不一定保证百分百中断成功 。适用于紧急情况或需要立即停止线程池的场景,如应用程序异常退出时。快速清理资源,但可能导致部分任务未完成。

shutdown就好比餐厅快打烊了,拒绝接新客,但会把已经在店的客人招待完了才关门。

shutdownNow就好比餐厅着火了,直接把客人们赶走关门了,但可能会导致部分客人没吃完饭。

13.Java 中的 DelayQueue 和 ScheduledThreadPool 有什么区别?

DelayQueue 是一个阻塞队列,而 ScheduledThreadPool是线程池,不过内部核心原理都是差不多的。 DelayQueue 是利用优先队列存储元素,当从队列中获取任务的时候,如果最老的任务已经到了执行时间,可以从队列中出队一个任务,反之可以获得 null 或者阻塞等待任务到时。 ScheduledThreadPool内部也使用的一个优先队列 DelayedWorkQueue 且可以内部多线程执行任务,支持定时执行的任务,即每隔一段时间执行一次的任务。

14.你了解java中的读写锁吧?

读写锁,它允许多个线程同时读取共享资源,而在写操作时确保只有一个线程能够进行写操作(读读操作不互斥,读写互斥、写写互斥)。这种机制适合于读多写少的场景,因为它提高了系统的并发性和性能。Java 中的 ReadwriteLock 是通过 ReentrantReadwriteLock 实现的,它提供了以下两种锁模式:

  • 读锁(共享锁)允许多个线程同时获取读锁,只要没有任何线程持有写锁。适合读操作频繁而写操作较少的场景。

  • 写锁(独占锁)写锁是独占的,当有线程持有写锁时,其他线程既不能获取写锁,也不能获取读锁。写锁用于保证写操作的独占性,防止数据不一致。

这里面试官大概率会延伸问读写锁的原理,如果没有延伸,自己能说出来就是加分!

15.说一说读写锁的原理

可以参考以下博文:读写锁详解_读写锁的实现原理-CSDN博客

16.什么是java内存模型?

1.JMM内存模型是java虚拟机定义的一种规范,是为了消除早期不同硬件和操作系统访问内存时的差异。 2.JMM内存模型规范了内存中的变量存储传递的规则。即线程何时从主内存中读取数据,何时把工作内存中数据写回主内存。 3.保证了多线程环境下的可见性原子性有序性

  • 可见性:即一个线程对变量的修改对其他线程来说是可见的,可以通过volatile关键字实现。

  • 原子性:线程的操作是一个不可分割的一系列操作,不能被中断,分割,要么全部失败要么全部成功。

  • 有序性:线程的执行是有一定顺序的,JMM允许通过指令重排序来提高性能,指令重排序会保证线程内的操作顺序不会被破坏,不会影响线程执行最终的结果,并通过happens-before规则来保证其有序性。

17.什么是java的原子性、可见性、有序性?

1)原子性(Atomicity) 原子性指的是一个操作或一系列操作要么全部执行成功,要么全部不执行,期间不会被其他线程干扰。

原子类与锁:Java 提供了 java.util.concurrent.atomic包中的原子类,如 AtomicInteger,Atomiclong ,来保证基本类型的操作具有原子性。此外,synchronized 关键字和 Lock 接口也可以用来确保操作的原子性。 CAS(Compare-And-Swap):Java 的原子类底层依赖于 CAS 操作来实现原子性。CAS 是一种硬件级的指令,它比较内存位置的当前值与给定的旧值,如果相等则将内存位置更新为新值,这一过程是原子的。CAS 可以避免传统锁机制带来的上下文切换开销。

2)可见性(Visibility) 可见性指的是当一个线程修改了某个共享变量的值,其他线程能够立即看到这个修改。

volatile: volatile关键字是Java 中用来保证可见性的轻量级同步机制。当一个变量被声明为 volatile时,所有对该变量的读写操作都会直接从主内存中进行,从而确保变量对所有线程的可见性。synchronized:synchronized 关键字不仅可以保证代码块的原子性,还可以保证进入和退出 synchronized 块的线程能够看到块内变量的最新值。每次线程退出 synchronized 块时,都会将修改后的变量值刷新到主内存中,进入该块的线程则会从主内存中读取最新的 Java Memory Model(JMM):JMM 规定了共享变量在不同线程间的可见性和有序性规则。它定义了内存屏障的插入规则,确保在多线程环境下的代码执行顺序和内存可见性。

3)有序性(Ordering) 有序性指的是程序执行的顺序和代码的先后顺序一致。但在多线程环境下,为了优化性能,编译器和处理器可能会对指令进行重排序。

指令重排序:为了提高性能,处理器和编译器可能会对指令进行重排序。尽管重排序不会影响单线程中的执行结果,但在多线程环境下口能会导致严重的问题。例如,经典的双重检查锁定(DCL)模式在没有正确同步的情况下,由于指令重排序可能导致对象尚未完全初始化就被另一个线程访问。

18.Java 中的 final 关键字是否能保证变量的可见性?

final关键字的作用主要是用于声明常量和防止重写,它与变量的可见性有关,但不是直接保证可见性。当一个对象的final字段被初始化后,其他线程可以安全地访问这个字段,前提是对象的构造完成后被共享。JMM规定,一旦构造函数完成,final字段的值对其他线程是可见的。

19.为什么在 Java 中需要使用 ThreadLocal?

因为在多线程编程中,多个线程可能会同时访问和修改共享变量,导致线程安全问题。 ThreadLocal 提供了一种简单的解决方案,使每个线程都有自己的独立变量副本,避免了多线程间的变量共享和竞争,从而解决了线程安全问题。与通过加锁、同步块等传统方式来保证线程安全相比。 Threadlocal不需要对变量访问进行同步,减少了上下文切换、锁竞争的性能损耗。

以上是某面试网站的答案,但我认为ThreadLocal最主要的作用是方便每个线程处理自己的状态而引入的一种机制,比如当前登录用户的id,如果没有ThreadLocal,我们每次要用都只能在发过来的请求中提取,特别的麻烦。TheadLocal确实有解决共享变量的作用,但我们一般不会专门用TheadLocal来解决共享变量,一般会加锁来解决这个问题。

20.Java 中的 ThreadLocal 是如何实现线程资源隔离的?

每一个线程都有自己的ThreadLocalMap,设置值是设置到自己的map里面取的话也是从自己的map里面取,从而实现了线程隔离。

ThreadLocal原理: ThreadLocalMap里面有个Entry数组,Entry的keyThreadLocal对象,key是通过弱引用指向ThreadLocal对象的value是设置的值。 get方法:get方法会先获取当前线程,然后得到当前线程的ThreadLocalMap,然后将当前的ThreadLocal对象作为key从map里面拿到Entry,如果找到的话,直接返回这个entry中的value;没有找到的话,就会调用初始化方法,返回初始值。

21.为什么 Java 中的 ThreadLocal 对 key 的引用为弱引用?

垃圾回收弱引用允许垃圾回收器(GC)在没有强引用指向这些对象时回收它们。如果 ThreadLocal 对象没有被外部强引用,那么将其设置为弱引用可以确保垃圾回收器能够回收这些对象,即使它们仍然被某些线程的 ThreadLocalMap 引用。

内存泄漏问题:如果 ThreadLocalMap 的 key 是强引用,当线程结束时,由于线程对象还持有对 ThreadLocal 实例的强引用,这时候ThreadLocal对象不能被GC(垃圾回收器)视为“垃圾”,因此无法被正确回收,导致 ThreadLocalMap 中 的entry 无法及时被清理,从而造成内存泄漏

弱引用解决方案:将 ThreadLocalMap中 的 key 设为 弱引用可以有效避免这个问题,因为 弱引用 的对象在下一次垃圾回收时就会被回收,这样一来,即使线程结束后,ThreadLcoal 对象也能够被正确的回收,从而避免内存泄漏;

延伸→这里可能会被提问到JVM的垃圾回收机制。

22.既然说了那么多ThreadLocal,那TheadLocal有什么缺点呢?

1.内存泄漏风险 ThreadLocal变量会在整个线程生命周期内存在,除非明确地被设置为null或者线程结束。如果线程长时间运行而没有适当地清理 ThreadLocal 变量,那么这些变量会一直占据内存,从而导致内存泄漏。 2.不适合共享数据 ThreadLocal的设计初衷是为了每个线程提供独立的数据副本,因此不适合用来在多个线程之间共享数据。如果需要多个线程共享数据,应该考虑使用其他同步机制,如synchronized、Lock 或 Atomic 类型。 3.难以调试 由于 ThreadLocal 变量是线程私有的,因此在调试时很难追踪每个线程的状态。如果多个线程在操作 ThreadLocal 变量时出现问题,调试起来可能会比较困难。 4.静态 ThreadLocal 变量的问题 如果 ThreadLocal 变量是静态的,那么这个变量会伴随整个应用程序的生命周期,除非显式地调用 remove() 方法。这意味着即使线程结束了,ThreadLocal 变量仍然存在,可能会导致内存泄漏。 5.性能开销 虽然 ThreadLocal 可以减少锁的竞争,但是它也有一定的性能开销。每次访问 ThreadLocal 变量时都需要查找线程的本地存储空间,这比直接访问普通变量要慢一些。

23.如何将当前线程的局部变量传递到另一个线程?

我们可以使用阿里开源的一个组件:TransmittableThreadLocal(TTL),TransmittableThreadLocal是一种扩展了Java 的 ThreadLocal 的功能的类,主要解决在多线程环境中,特别是在线程池中,如何将线程局部变量从一个线程传递到另一个线程的问题特性: 1.允许在任务从一个线程(例如,一个线程池中的线程)转移到另一个线程时,保持线程局部变量的值 2.TransmittableThreadLocal 能够捕获当前线程的局部变量并在新线程中恢复这些变量,支持异步执行 3.TransmittableThreadLocal 提供了对当前线程的值进行快照的能力,并在新的线程中恢复这些值适用场景: 4 .使用 TransmittableThreadLocal 能确保ThreadLocal在执行过程中能够传递所需的上下文信息 5.在进行异步处理或回调时,TransmittableThreadLocal 确保在异步执行上下文中,可以访问到原线程的上下文信息

24.Java中Thread.sleep 和 Thread.yield 的区别?

暂停时间Thread.sleep 可以让线程暂停一段确定的时间,而 Thread.yield 不会暂停线程,只是建议调度器让出CPU。

响应中断Thread.sleep 可以响应中断,而 Thread.yield 不会抛出异常,也不会改变线程的中断状态。

锁的管理:两者都不会导致线程释放锁。

调度行为Thread.sleep 会导致线程进入TIMED_WAITING状态,而 Thread.yield 只是向调度器发出一个调度建议,不会改变线程的状态。

25.Java 中 Thread.sleep(0)的作用是什么?

  1. 提示线程调度器Thread.sleep(0) 可以被看作是一种提示,告诉线程调度器当前线程愿意让出CPU给其他同优先级的线程。尽管这个调用不会使线程实际休眠,但它会触发线程调度器重新考虑线程的调度。

  2. 释放CPU:如果当前线程是运行状态,并且执行了Thread.sleep(0),它会释放CPU给其他线程,即使没有其他同优先级的线程可以运行,当前线程也可能在下一次调度时被延迟执行。

  3. 响应中断:尽管Thread.sleep(0)不会使线程进入长时间的休眠状态,但如果在调用Thread.sleep(0)之前线程的中断状态已经被设置,那么这个方法会抛出InterruptedException,并且清除中断状态。这可以作为一种快速响应中断的方式

延伸→那这个作用不是和Thread.yield一样吗?这两者有什么区别呢?

两者都不会进入阻塞状态且愿意让出CPU给其他线程,但是:

Thread.sleep(0) 会检查线程的中断状态。如果线程在调用之前被中断,Thread.sleep(0) 会抛出InterruptedException,并且清除中断状态。

Thread.yield 不会抛出异常,也不会改变线程的中断状态,它只是单纯地提示线程调度器当前线程愿意让出CPU。

延伸→这两者的应用场景分别在哪些方面呢?

Thread.sleep(0)可以在循环中使用 ,提供一个快速检查线程中断状态的机会,尤其是在需要频繁响应中断信号的场合。

在实时性要求不高的场景下,Thread.yield() 可以用来让当前线程偶尔让出CPU,给其他线程执行的机会。

26.ThreadLocal的原理(简易版)

Thread Local内部有个map集合,map里面存放了一系列的key/value,key值保存Thread Local本身,当调用Thread Local的get方法时,首先回去map集合中找有无当前Thread Local的key,有的话直接返回,没有就进行创建并放入,当调用set方法时,就会在map中存储对应的键值对,当调用remove时,则从map中移除与该Thread Local对象相关的键值对。

27.线程中blocked和waiting有什么区别?

触发条件:线程进入阻塞状态通常是想要获取一个对象的锁,但该锁已经被另一个线程占有,这时线程就会进入阻塞状态直到锁可用。而进入等待状态一般是等待另一个线程进行某些操作,这时线程不会占用CPU资源不会参与锁的竞争

唤醒机制:当一个线程阻塞等待锁时,一旦锁释放,线程就会抢占锁并从阻塞状态变化为运行状态。而线程进入等待状态时需要被显式唤醒,比如线程调用了object.wait方法,就需要等待其他线程的notify或者notifyAll来唤醒。

延伸→notify 和 notifyAll 的区别?

回答:

区别在于:

  1. notify:唤醒一个线程,其他线程依旧处于wait的等待唤醒状态

  2. notifyAll:所有线程退出wait状态,开始竞争锁

延伸→notify选择哪个线程?

notify的源码注释提到说notify选择唤醒的线程是任意的,但是依赖于具体实现的jvm,目前比较流行的jvm实现是hotspot,hotspot是采用队列也就是先进先出的顺序唤醒。

28.怎么保证多线程安全?

1)synchronized关键字:可以使用synchronized关键字来同步代码块或方法,确保同一时刻只有一个线程可以访问这些代码。(实现原理见→ 每日速记10道java面试题06-CSDN博客

2)volatile关键字:volatile关键字用于变量,确保所有线程看到的是该变量最新的值。(见问题7)

3)原子类Java并发库提供了原子类,比如atomic Integeratomic Long可以用于更新基本数据类型的变量而无需额外的同步

4)线程局部变量:ThreadLocal类可以为每个线程提供变量副本,这样每个线程都有自己的变量,消除了竞争条件。(实现原理见问题1)

5)使用并发集合:使用ConcurrentHashMap等一些实现了线程安全逻辑的集合。

29.Java中有哪些常用的锁,在什么场景下使用?

  1. 内置锁:Java中的synchronized关键字是内置锁机制的基础,当线程进入同步代码块或方法时,就会获取锁,离开代码块时,锁就会被释放。其他线程想要获取该锁时会被阻塞直到锁释放

  2. 读写锁:允许多个读者访问共享资源,但只允许一个写者,一般适用于读操作远多于写操作的场景。

  3. 乐观锁悲观锁:悲观锁是指当其他线程访问数据前就锁定资源不让进synchronized就是悲观锁的例子。乐观锁通常不锁定资源,而是在更新数据时检查数据是否被其他线程修改修改了就不允许更新,没修改就可以更新,通常使用版本号时间戳来实现乐观锁。(怎么实现乐观锁? 问题6)

  4. 自旋锁:线程在等待锁时循环检查锁是否可用而不是直接放弃CPU阻塞等待。适用在所等待时间很短的场景,可以提高性能,但过度自旋会浪费CPU

30.怎么理解可重入锁?

可重入锁就是同一个线程在获取了锁之后,再次重复获取该锁可以成功获取且不会导致死锁等其他问题。

ReentrantLock实现可重入锁是基于内部的计数器。(具体来说一个state变量+两个队列具体看以下文章:每日速记10道java面试题06-CSDN博客

当一个线程获取锁时,计数器+1,同一个线程重复获取同一个锁,计数器会+1,当线程释放锁时,计数器-1,只有当计数器减到0时,才会彻底释放锁。

延伸→synchronized能否作为可重入锁?

支持重入的,具体是由底层的线程IDstatus实现的,当一个线程成功获取锁时,会记录该线程的线程ID,之后每次线程尝试获取锁,会比较线程ID如果相同则允许获取status+1,在释放锁时,每一次退出方法status-1,直到status为0,锁才完全释放

31.Java中想实现一个乐观锁,都有哪些方式?

版本号控制:增加一个版本号字段记录数据更新时候的版本每次更新成功时递增版本号,在更新的时候判断当前版本号是否与更新前版本号一致是的话则更新不是的话就不更新

时间戳:使用时间戳记录数据更新时间,每次更新数据时,比较时间戳,如果当前时间戳大于数据时间戳,则说明数据在更新前已经被其他线程更新了,这时就停止更新操作。

32.java中volatile关键字有什么作用?

保证变量第所有线程的可见性:如果一个变量被声明为volatile时,这时对该变量的所有修改都会被其他线程看到

禁止指令重排序优化

写写屏障:在对volatile变量执行写操作时,会插入一个写屏障,确保之前的一切的普通写操作已完成,不会在写完volatile变量之后又写

读写屏障执行读操作,会插入一个读屏障,保证volatile读之后的其他普通读操作不会被放到volatile读之前执行,保证读到的数据是最新的。

写读屏障:这是最重要的一个屏障,在volatile写后和volatile读前,确保写前不读,读后不写。

33.volatile可以替代synchronized吗?

不可以,volatile关键字可以保证可见性,但不能保证原子性,因此不能替代synchronized。Volatile关键字用来修饰变量,使得变量在一个线程修改可以被其他线程看到这次修改,也就是他的可见性。但是它不能保证原子性,所以如果要保证线程安全的话,最好还是使用synchronized关键字。(synchronized是如何保证原子性的?→每日速记10道java面试题06-CSDN博客

34.什么是java中的ABA问题?

CAS里面一个线程将一个数据从A变成了B,又变回了A,这时候,另一个线程会认为这个数据没有发生变化,继续进行操作,可能导致错误。其实现在的A已经不是当初的A了。

解决办法:加版本号或者时间戳 修改数据的时候,给数据添加一个版本号;在进行CAS操作的时候,除了比较内存中的实际值与期望值外,还比较版本号。版本号相同就修改,否则失败重试。

标签:JUC,面试题,Java,hashmap,ThreadLocal,线程,哈希,java
From: https://blog.csdn.net/csdn3043663729/article/details/145130181

相关文章

  • leetcode——分割两个字符串得到一个回文字符串(java)
    给你两个字符串a和b,它们长度相同。请你选择一个下标,将两个字符串都在相同的下标分割开。由a可以得到两个字符串:aprefix和asuffix,满足a=aprefix+asuffix,同理,由b可以得到两个字符串bprefix和bsuffix,满足b=bprefix+bsuffix。请你判断aprefix+bsu......
  • java流程控制及其循环语句
    用户交互Scanner(IO流跟电脑打交道)基础语法:Scanners=newScanner(System.in);//表示系统数据输入​Scanners=newScanner(System.out);//表示系统数据输出​scanner.close:表示关闭数据流通过Scanner类的n......
  • 科普文:算法和数据结构系列【高效的字符串检索结构:字典树Trie树原理、应用及其java示例
    概叙科普文:算法和数据结构系列【算法和数据结构概叙】-CSDN博客科普文:算法和数据结构系列【非线性数据结构:树Tree和堆Heap的原理、应用、以及java实现】-CSDN博客科普文:算法和数据结构系列【树:4叉树、N叉树】_动态维护四叉树-CSDN博客科普文:算法和数据结构系列【二叉树总结......
  • NB!一款基于java开发的漏洞检测工具,集合了泛微、用友、大华、海康、致远、红帆、万户
    1、工具介绍基于https://github.com/yhy0/ExpDemo-JavaFX上添加poc2、工具下载链接:工具下载:工具下载3、新增检测漏洞用友NC-Cloud系统接口getStaffInfo存在SQL注入漏洞用友U8-CloudReleaseRepMngAction存在SQL注入漏洞复现(CNVD-2024-33023)用友U8-CRM系统getDeptName......
  • Java使用sql查询mongodb
    概述MongoDB是一种NoSQL数据库,它使用文档存储数据,与传统的关系型数据库不同。尽管MongoDB不使用SQL进行查询,但有时在熟悉SQL语法的团队中,能够使用SQL查询MongoDB可以大大简化开发工作。本文将详细介绍如何在Java中使用SQL查询MongoDB。工具与依赖要实现这一......
  • JavaScript中通过array.map()实现数据转换、创建派生数组、异步数据流处理、复杂API请
    目录JavaScript中通过array.map()实现数据转换、创建派生数组、异步数据流处理、复杂API请求、DOM操作、搜索和过滤等,array.map()的使用详解(附实际应用代码)一、什么时候该使用Array.map(),与forEach()的区别是什么?1、什么时候该用Array.map()2、Array.map()与Array.forEach()的......
  • 1055 集体照(java)
    拍集体照时队形很重要,这里对给定的N个人K排的队形设计排队规则如下:每排人数为N/K(向下取整),多出来的人全部站在最后一排;后排所有人的个子都不比前排任何人矮;每排中最高者站中间(中间位置为m/2+1,其中m为该排人数,除法向下取整);每排其他人以中间人为轴,按身高非增序,先......
  • 2025.1.18 JavaScript基础
    1、变量的定义var变量名例如:<html> <body> <scripttype="text/javascript"> functionzhaoling(){ n=Number(document.form1.txt1.value); if(n!=parseInt(n/1)||n<1||n>100) { alert("请输入一个1-100的整数"); ......
  • JVM 八股文面试题
    目录1.前言2.JVM简介3.JVM内存划分3.1 为什么要进行内存划分3.2 内存划分的核心区域3.2.1核心区域一:程序计数器3.2.2核心区域二:元数据区3.2.3核心区域三:栈3.2.4核心区域四:堆4.JVM类加载机制4.1类加载的步骤 4.1.1步骤一:加载4.1.2步骤......
  • springboot+vue+java大学生健康管理系统 867i9
    目录系统实现截图开发核心技术介绍技术栈核心代码部分展示操作手册视频演示/源码获取系统实现截图开发核心技术介绍springboot+Element-UI+vue本系统采用MVVM模式,开发框架使用SpringBoot框架,开发工具使用IDEA,VisualStudioCode,Web服务器使用Tomcat,数据库服......