面试问题积累

问题来源:https://blog.csdn.net/xiaofeng10330111/article/details/100706167  + 自己思考的一些问题

上述问题,要求自己有思考后的精简总结。

HashMap和ConcurrentHashMap区别

ConcurrentHashMap是线程安全的,至于原因,要从数据结构角度看:

HashMap:jdk1.7数组+链表,jdk1.8数组+链表/红黑树

ConcurrentHashMap:jdk1.7(synchronized+segment数组|分段锁)+数组+链表,jdk1.8(synchronized/CAS+Node数组)+链表/红黑树

另外HashMap可以有key为null,value为null的键值对,但key为null的只能有一个,应为key不允许重复

ConcurrentHashMap则不支持key为null,value为null的键值对

高并发HashMap的环是如何产生的

HashMap的数组在达到阈值后会扩容,其中存放的数据会重新refresh,重新hash存放,多线程情况下,jdk1.7使用链表头插法,此时若有数据存放,存在数据头尾相连,造成闭环的可能。

jdk1.7和1.8,HashMap的区别,从数据结构上、Hash值的计算上、链表数据的插入方法、内部Entry类的实现上分析?

jdk1.7:数组+链表,hash值计算用了9次扰动处理=4次位运算+5次异或,头插法,数组和链表节点的实现类是Entry类

jdk1.8:数组+链表/红黑树,hash值计算用了2次扰动处理=1次位运算+1次异或,尾插法,数组和链表节点的实现类是Node类

HashMap如果我想要让自己的Object作为K应该怎么办?

重写hashcode,并且 重写equals()方法

HashSet和HashMap的区别

HashSet集合中不允许存在重复的元素

jdk1.7 HashMap是基于数组和链表实现的,为什么不用双链表?

使用链表是为了解决哈希冲突,单链表即可达到目的,双链表增加了存储空间和复杂度

jdk1.8 HashMap中引入红黑树的原因是?为什么要用红黑树而不是平衡二叉树?

引入红黑树是为了提高hashmap 增删改查性能,单链表越长,索引效率越慢。

相比平衡二叉树,红黑树是一个折中的选择,其平衡度比平衡二叉树要低,删除、插入数据后重新构造数的开销要更低,而查询效率比普通二叉树要高。

二叉查找树、平衡二叉树、红黑树(变色、旋转)的特性要对比下

volatile与synchronized的区别是什么?volatile作用

volatile作用于变量,保证变量的可见性和有序性。例:共享变量x,对a、b线程可见,那么x由volatile字段修饰后,线程a对变量x的修改对b是立马可见的。

volatile,禁止指令重排序,保证工作内存中的变量值改变后能够立即刷新到主内存,保证可见和有序性。它不保证原子性,因此不是线程安全的,但保证以下两个条件可认为线程安全:

  1. 运算结果并不依赖变量的当前值,或者能够确保只有单一的线程修改变量的值。
  2. 变量 不需要与 其他状态变量 共同 参与 不变约束。 --- 这句话理解语境是:多个变量一起使用时

参考:https://blog.csdn.net/pzxwhc/article/details/38236031

https://blog.csdn.net/xiaofeng10330111/article/details/105360860

https://www.cnblogs.com/monkeysayhi/p/7654460.html

synchronize是java内置的变量,可以修饰变量、方法、类、同步代码块,保证可见性、有序性和原子性。

synchronized和Lock的区别

synchronize是java内置的变量,可以修饰变量、方法、类、同步代码块,保证可见性、有序性和原子性

lock是java的接口类,比较灵活,需要手工加锁和释放,子类有Reentrantlock(可重入锁)等

java并发编程有哪些特性

原子性(原子操作):指一个操作是不可中断的,要么全部执行成功要么全部执行失败,有着“同生共死”的感觉。及时在多个线程一起执行的时候,一个操作一旦开始,就不会被其他线程所干扰。

可见性:是指当一个线程修改了共享变量后,其他线程能够立即得知这个修改

有序性:如果在本线程内观察,所有的操作都是有序的;如果在一个线程观察另一个线程,所有的操作都是无序的。

线程安全需要保证三大特性。

HashMap、HashTable、ConcurrentHashMap的原理与区别

HashTable:

  1. 底层数组+链表实现,无论key还是value都不能为null,线程安全,实现线程安全的方式是在修改数据时锁住整个HashTable,效率低,ConcurrentHashMap做了相关优化
  2. 初始size为11,扩容:newsize = olesize*2+1
  3. 计算index的方法:index = (hash & 0x7FFFFFFF) % tab.length

HashMap:

  1. 底层数组+链表实现,可以存储null键和null值,线程不安全
  2. 初始size为16,扩容:newsize = oldsize*2,size一定为2的n次幂
  3. 扩容针对整个Map,每次扩容时,原来数组中的元素依次重新计算存放位置,并重新插入
  4. 插入元素后才判断该不该扩容,有可能无效扩容(插入后如果扩容,如果没有再次插入,就会产生无效扩容)
  5. 当Map中元素总数超过Entry数组的75%,触发扩容操作,为了减少链表长度,元素分配更均匀
  6. 计算index方法:index = hash & (tab.length – 1)

ConcurrentHashMap:

  1. 底层采用分段的数组+链表实现,线程安全
  2. 通过把整个Map分为N个Segment,可以提供相同的线程安全,但是效率提升N倍,默认提升16倍。(读操作不加锁,由于HashEntry的value变量是 volatile的,也能保证读取到最新的值。)
  3. Hashtable的synchronized是针对整张Hash表的,即每次锁住整张表让线程独占,ConcurrentHashMap允许多个修改操作并发进行,其关键在于使用了锁分离技术
  4. 有些方法需要跨段,比如size()和containsValue(),它们可能需要锁定整个表而而不仅仅是某个段,这需要按顺序锁定所有段,操作完毕后,又按顺序释放所有段的锁
  5. 扩容:段内扩容(段内元素超过该段对应Entry数组长度的75%触发扩容,不会对整个Map进行扩容),插入前检测需不需要扩容,有效避免无效扩容

Atomic类如何保证原子性(CAS操作)

Atomic类内部使用volatile关键字来保证内存可见性,使用CAS保证原子性。Atomic内部调用native方法CAS。

CAS(CompareAndSwap,比较并替换)是一种硬件层面的手段,用于保证原子性。

CAS需要有3个操作数:内存地址V,旧的预期值A,即将要更新的目标值B。CAS指令执行时,当且仅当内存地址V的值与预期值A相等时,将内存地址V的值修改为B,否则就什么都不做。整个比较并替换的操作是一个原子操作。

CAS利用CPU调用底层指令实现。两种方式:总线加锁或者缓存加锁保证原子性。

Java不可重入锁与可重入锁的区别如何理解?

可重入锁:

可重入锁就是一个类的A、B两个方法,A、B都有获得统一把锁,当A方法调用时,获得锁,在A方法的锁还没有被释放时,调用B方法时,B方法也获得该锁。这种情景,可以是不同的线程分别调用这个两个方法。也可是同一个线程,A方法中调用B方法,这个线程调用A方法。synchronized和java.util.concurrent.locks.ReentrantLock是可重入锁

不仅判断锁有没有被锁上,还会判断锁是谁锁上的,当就是自己锁上的时候,那么他依旧可以再次访问临界资源,并把加锁次数加一。

不可重入锁:

不可重入锁就是一个类的A、B两个方法,A、B都有获得统一把锁,当A方法调用时,获得锁,在A方法的锁还没有被释放时,调用B方法时,B方法也获得不了该锁,必须等A方法释放掉这个锁。

只判断这个锁有没有被锁上,只要被锁上申请锁的线程都会被要求等待。

java锁分类

乐观锁与悲观锁:并不是特指某两种类型的锁,是人们定义出来的概念或思想,主要是指看待并发同步的角度。

乐观锁:每次去拿数据的时候都认为别人不会修改,所以不会上锁,但是在更新的时候会判断一下在此期间别人有没有去更新这个数据,常见CAS。

悲观锁:每次去拿数据的时候都认为别人会修改,所以每次在拿数据的时候都会上锁,这样别人想拿这个数据就会阻塞直到它拿到锁,常见synchronized。

-----------

独享锁和共享锁也是比较广义的讲法

独享锁:指该锁一次只能被一个线程所持有,如ReentrantLock,写锁

共享锁:指该锁可被多个线程所持有,如ReadWriteLock,读锁

独享锁与共享锁也是通过AQS来实现的,通过实现不同的方法,来实现独享或者共享。

-----------

互斥锁:在Java中的具体实现就是ReentrantLock

在访问共享资源之前对其进行加锁操作,在访问完成之后进行解锁操作。 加锁后,任何其他试图再次加锁的线程会被阻塞,直到当前进程解锁。

如果解锁时有一个以上的线程阻塞,那么所有该锁上的线程都被编程就绪状态, 第一个变为就绪状态的线程又执行加锁操作,那么其他的线程又会进入等待。 在这种方式下,只有一个线程能够访问被互斥锁保护的资源

读写锁:在Java中的具体实现就是ReadWriteLock

读写锁既是互斥锁,又是共享锁,read模式是共享,write是互斥(排它锁)的。

读写锁有三种状态 :读加锁状态、写加锁状态和不加锁状态

-----------

可重入锁与不可重入锁

-----------

公平锁是指多个线程按照申请锁的顺序来获取锁

非公平锁是指多个线程获取锁的顺序并不是按照申请锁的顺序,有可能后申请的线程比先申请的线程优先获取锁

对于Java ReetrantLock而言,通过构造函数指定该锁是否是公平锁,默认是非公平锁。非公平锁的优点在于吞吐量比公平锁大。

对于Synchronized而言,也是一种非公平锁。由于其并不像ReentrantLock是通过AQS的来实现线程调度,所以并没有任何办法使其变成公平锁。

-----------

分段锁其实是一种锁的设计,并不是具体的一种锁,对于ConcurrentHashMap而言,其并发的实现就是通过分段锁的形式来实现高效的并发操作。

-----------

偏向锁/轻量级锁/重量级锁

这三种锁是指锁的状态,并且是针对Synchronized。在Java 5通过引入锁升级的机制来实现高效Synchronized。这三种锁的状态是通过对象监视器在对象头中的字段来表明的。

偏向锁是指一段同步代码一直被一个线程所访问,那么该线程会自动获取锁。降低获取锁的代价。

轻量级锁是指当锁是偏向锁的时候,被另一个线程所访问,偏向锁就会升级为轻量级锁,其他线程会通过自旋的形式尝试获取锁,不会阻塞,提高性能。

重量级锁是指当锁为轻量级锁的时候,另一个线程虽然是自旋,但自旋不会一直持续下去,当自旋一定次数的时候,还没有获取到锁,就会进入阻塞,该锁膨胀为重量级锁。重量级锁会让他申请的线程进入阻塞,性能降低。

-----------

自旋锁:

在Java中,自旋锁是指尝试获取锁的线程不会立即阻塞,而是采用循环的方式去尝试获取锁,这样的好处是减少线程上下文切换的消耗,缺点是循环会消耗CPU

参考文档:

https://www.cnblogs.com/hustzzl/p/9343797.html

https://www.cnblogs.com/2019wxw/p/11656736.html

AQS理论的数据结构是什么样的?

AQS(AbstractQueuedSynchronizer)抽象的队列式同步器,是一种锁机制。

其数据结构包含:

volatile修饰的state共享变量(用于计数器,类似gc的回收计数器)

状态符(线程标记)用于标记锁是哪个线程加的

阻塞队列

详细参考:https://www.cnblogs.com/tinghao/p/12575814.html

多线程中sleep与wait的区别是什么?

sleep和wait都能使线程处于阻塞状态

sleep是线程中的方法,但是wait是Object中的方法。
sleep方法不会释放lock,但是wait会释放,而且会加入到等待队列中。
sleep方法不依赖于同步器synchronized,但是wait需要依赖synchronized关键字。
sleep不需要被唤醒(休眠之后推出阻塞),但是wait需要(不指定时间需要被别人中断)

ps:线程几种状态:新建、就绪、运行、阻塞、死亡

(一). 等待阻塞:运行(running)的线程执行o.wait()方法,JVM会把该线程放入等待队列(waitting queue)中。
(二). 同步阻塞:运行(running)的线程在获取对象的同步锁时,若该同步锁被别的线程占用,则JVM会把该线程放入锁池(lock pool)中。
(三). 其他阻塞:运行(running)的线程执行Thread.sleep(long ms)或t.join()方法,或者发出了I/O请求时,JVM会把该线程置为阻塞状态。当sleep()状态超时、join()等待线程终止或者超时、或者I/O处理完毕时,线程重新转入可运行(runnable)状态。

final、finnally、finalize的区别是什么?

final 表示不可修改的,可以用来修饰类,方法,变量。

finally是Java的异常处理机制中的一部分。finally块的作用就是为了保证无论出现什么情况,finally块里的代码一定会被执行。

finalize是Object类的一个方法,是GC进行垃圾回收前要调用的一个方法。

  • ThreadLocal的原理和实现

 

  • ThreadLocal为什么要使用弱引用和内存泄露问题
  • 为什么要使用线程池
  • 线程池的线程数量怎么确定
  • 线程池的五种运行状态
  • 线程池的关闭(shutdown或者shutdownNow方法)
  • 如何控制线程池线程的优先级
  • 线程之间如何通信
  • 核心线程池ThreadPoolExecutor的参数
  • 常见线程池的创建参数是什么样的?
  • ThreadPoolExecutor的工作流程
  • Java线程池的调优经验有哪些?(线程池的合理配置)
  • 怎么对线程池进行有效监控?
  • Boolean占几个字节
  • Exception和Error
  • Object类内的方法
  • Jdk1.8/Jdk1.7都分别新增了哪些特性?
  • SpringBuffer和SpringBuilder的区别是什么?性能对比?如何鉴定线程安全?
  • String str="hello world"和String str=new String("hello world")的区别?
  • Array和ArrayList有什么区别?使用时注意事项有哪些?
  • LRU算法是怎么实现的?大致说明下
  • CAS?CAS 有什么缺陷,如何解决?
  • ScheduledThreadPoolExecutor中的使用的是什么队列?内部如何实现任务排序的?

 

 

 

 

 

 

 

 

 

 

 

 

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值