请大概讲一下hashMap和hashTable的区别
Hashtable和hashMap都是常见的一些map实现,是以键值对的形式存储和操作数据的容器类型。
他们之间的不同是,hashtable是java类库早期提供的一个哈希表实现,本身是同步的,不支持null键,但是会支持null值,而且hashtable本身是同步的,开销是比较是大的,所以现在已经很少被推荐使用。而HashMap是应用更加广泛的哈希表实现,行为上和hashtable基本一致,最大的区别是hashmap不是同步的,而且支持null值和null建,而且hashmap的get和put操作,绝大多数时候都可以达到常数时间。
而且我们也应该注意到,hashtable是扩展了Dictionary类的,而hashmap等其他map是扩展了abstractMap类,这其实也是个应该注意的点。
而且hashtable和hashmap的初始容量是不同的,hashmap的初始容量是16,扩容的时候容量会翻倍。而hashtable的初始容量是11,扩容的时候是翻倍后加1。
而且hashmap的底层和hashtable的底层还是有一点不同的。我们都知道,hashtable和hashmap的底层是一个数组,而数组中的每一个节点都是一个链表,但是在java1.8之后,当链表的长度超过8之后,hashmap会将链表转化为红黑树,而hashtable则没有这个机制。
HashMap的底层原理?ConcurrentHashMap的底层原理?(在jdk7和jdk8中的区别)
该如何来讲解hashmap呢?我们都知道,在我们实际的开发过程中,使用的hashmap的次数是非常之多的,虽然hashmap不是线程安全的,但是hashmap的性能非常高,基本上是常数级别的。
Hashmap的底层是数组+链表的形式。一个数据进入到hashmap的时候,我们会根据它的key值计算它的hash值,获取到它在数组的哪一个位置,然后将value值存储到数组中。但是我们都知道,hash算法的一大问题就是hash冲突,而且这个问题还无法避免,所以如果两个不同的数据的key值计算出来的hash值一样,那么意味着这两条数据会存储在数组的同一个节点,于是我们将这个节点设置为一个链表,这两条数据可以组成为一个链表。
但是在jdk1.8之后有一点变动,我们知道,链表的特点是插入快,但是查询慢,于是hash冲突的次数多,导致链表的长度变得很长,链表的查询的时间复杂度是O(n),这意味着链表长度越长,查询就越慢。于是在jdk1.8之后,hashmap内部的链表,当它的长度超过了8之后,这个链表就会转换为红黑树,方便我们更快的查询。但是要注意的是,当链表的长度减少到8以下,红黑树会退化为链表。
Hashmap底层的数组,默认长度是16,当数组数量达到了负载因子0.75以后,数组会进行动态扩容,容量变成之前的一倍。
但是要注意的是,hashmap不是线程安全的,如果我们需要线程安全的时候,使用synchornized来保持线程安全的话,同步的粒度非常大,那么就会有着巨大的性能开销,划不来,而且更重要的是jdk为我们提供了ConcurrentHashMap,我们为什么要去重复造轮子呢?当然我们也可以使用hashtable来满足我们的需求,但是hashtable实现同步的方式非常暴力,就是加synchornized来修饰各种方法,锁的粒度非常大,性能太低。
ConcurrentHashmap是在1.7的时候是基于分利锁实现的线程安全。在jdk1.8之前,为了实现线程的安全,ConcurrentHashmap是内部的数组拆分为一个一个的segment,当一个数据进来的时候需要加锁,我们就通过这条数据的key值计算出来hash值,然后通过hash值定位到一个segment,然后给这个segment加锁,这样就可以将锁细粒化,减少性能上的开销。
到了jdk1.8的时候,ConcurrentHashmap又进行了一次优化,segment依然存在,但是不再是为了实现同步,而是仅仅为了兼容序列化而已,而实现同步的方式改为了CAS乐观锁,当有数据进来的时候,会先判断容器是否为null,如果是则使用volatile的sizeCtl作为互斥手段,如果有竞争的初始化操作,那么就暂停,等待条件恢复。如果容器不为null,就判断存放的链表是否为null,如果为null就使用CAS设置新节点,如果不是null就使用Sychornized对这个链表加锁,进行后续操作。
jdk1.8中HashMap为什么要用数组+链表+红黑树?直接用数组+红黑树不好吗?
这个问题的本质就是,链表和红黑树的对比。
因为在jdk1.8之后,HashMap中的数组中的一个节点就是一个链表,那么当这个链表的长度大于8的时候,那么这个链表就会转换为一个红黑树,而且红黑树不会因为长度变短而退化为链表。
首先是时间复杂度,红黑树因为有类似索引结构,所以查询的时间复杂度为O(log),而链表的查询的时间复杂度为O(n)。
但是要注意的是,每一条数据插入到红黑树的时候,红黑树是会有一个自旋平衡的过程的,这会导致新增插入成本高,当这个节点的数据小于8的时候,使用链表结构其实查询的成本还在接受范围内的(最多查询8次),但是新增的成本却是O(1),这是很低的且综合算起来是比较划算的。如果在数据量小于8的时候,依然使用红黑树,提升的那点查询性能,完全比不上新增插入的时候自旋平衡带来的性能损耗。
hashmap怎么减少hash碰撞?
Hashmap使用了两种方案来减少hash碰撞。
第一种,我们知道,hashmap的底层是数组+链表,当我们存储一条数据的时候,这条数据的key值经过hash计算,到了数组的同一个下标节点,那么就会和之前到这个节点的数据组成一个链表,就不会因为hash值的一样导致了之前的数据被覆盖掉了。
第二种,hashmap在计算hash值的时候,它的hash算法是将高位数据移位到低位进行了异或运算的,这样做的的目的就可以有效的减少hash碰撞的发生。
Java 并发中的 cas 是什么?会有什么问题?Java怎么解决这种问题?
cas的意思是比较并且交换,是为了保证原子性而出现的一种理论。在我们的实际开发中,使用CAS理论的也有不少,比如说我们有时候为了保证幂等性而在sql语句中出现,如下所示:
update set 字段1=A from 表1 where 字段1=B。
在java中比较常用CAS理论的是Automic原子类。
CAS会有一个比较经典的问题,就是ABA问题,就是在实际的开发过程中,一个字段的值,可能从A变成B了,但是又从B变成A了。
要解决这种问题,会使用版本号的形式来解决,在Automic原子类就是这样实现的。
同步锁Synchornized跟 Lock的区别?
- 首先是实现原理的不同,Synchornized是通过管程理论实现的,而Lock是通过信号量的形式实现的。
- Synchornized内部自带加锁解锁,而Lock需要开发人员手动加锁解锁。
- Synchornized没有办法中断阻塞,只能一直等待,直到获取到锁为止。而Lock的可以在陷入到阻塞的时候,中断阻塞状态。
谈了下java锁的使用优点跟缺点
java里面的锁有很多种,其中有重量级锁,轻量级锁,偏向锁,自旋锁。
什么是重量级锁呢?重量级锁就是说,底层是通过使用操作系统的mutex lock实现的,这就意味着所有的对象,都有一个对应的互斥锁的标记,这个标记用来保证在某一个时刻,只有一个线程可以访问这个对象,这就意味着如果多个线程来竞争这个对象的时候,这些线程会进行大量的线程状态的切换, 由于java的线程都是映射到操作系统的原生线程之上的,着就意味着如果要阻塞或者唤醒一个线程,都需要操作系统来帮忙,而线程状态的转换需要耗费极大的处理器时间,而且还会引起操作系统的内核态和用户态的切换,因为用户态只是我们的JVM的一个状态,没有办法访问到所有的程序,而且也没有CPU的占用能力,这个时候就必须切换到内核态了,这种切换带来的性能开销也是非常大的。所以这个锁就是一个重量级锁了。
那么jdk1.6之后新加入的偏向锁是什么呢?我们知道,在实际的开发中,可能对一个资源的竞争的线程并不是很多,甚至有的时候都只有一个线程在竞争。在这个假设的前提下,如果还是使用操作系统底层的互斥量,那肯定是极大的浪费。我们可以设定,当一个线程获得了锁以后,锁就会进入到偏向模式,这个时候对象头的Mark Word里面的部分字节更新为线程栈中的锁的地址,这个同步操作是相当耗时的,当这个线程再次请求这个锁的时候,不需要再做一系列的同步操作,就可以直接获取到锁,这样就可以大大的减少对性能的消耗。
但是如果一旦线程的数量变多,对锁的竞争变得激烈了,那么我们的偏向锁就会失效,变成了轻量级锁。
轻量级锁有一个假设,就是对于大部分锁来说,整个同步周期内都不存在竞争。在这个假设前提下,当有一个线程获取锁的时候,不需要调用操作系统的底层获取互斥量,而是只需要在我们的对象头的Mark Word里面的部分字节通过CAS的形式更新为线程栈中的锁的地址,如果更新成功,那就会是轻量级锁,但是如果发现对象里面已经有了其他线程更新的轻量级锁(当然会判断是否就是当前线程拥有了这个锁,如果是,那还是轻量级锁,会直接进入到同步代码块。),那么接下来就会因为对资源的竞争,膨胀为了重量级锁了。如果线程竞争严重的情况下,轻量级锁的性能,因为额外发生了CAS的操作,会比重量级锁还要差。
而自旋锁呢?我们知道线程竞争有一个很大的性能消耗点,刚刚我也说过了,是内核态和用户态之间的切换。
因此我们可以设定,当我们的线程,竞争不到锁的时候,不会直接陷入到阻塞状态,而是自旋一会儿,等待一会儿,在这个等待的时间里面,重新去竞争锁,如果竞争失败了,那么才会去陷入到阻塞状态。这样的操作,可以有效的解决在一个锁持有时间短,且锁竞争不激烈的环境中,减少线程状态的切换,但是要注意的是,锁的自旋如果无限下去,也是很耗费时间的,所以一般都是设置自旋10次,当然我们也可以自己设置。但是在实际的JVM运行过程中,自旋这个操作,其实是动态的。比如上一次自旋了很久没有获取到锁,那么下一次JVM就不会让线程自旋等待了。
ThreadLocal原理
ThreadLocal也就是我们说的线程本地变量。
我们知道,线程并发的根本问题,是对共享变量的修改导致的,那么ThreadLocal就是为一个线程提供了一个和其他线程隔离的共享变量副本,这样可以避免线程之间的操作乱套。
ThreadLocal,其底层是一个map对象,key就是threadLocal实例,value就是我们要隔离的变量副本。要注意的是,在这个map对象里面,key是一个弱引用,当没有其他对象引用它的时候,会被直接回收,但是value不同,value是一个强引用,是直接和这个线程Thread关联的。
但是我们都知道,在实际的开发过程中,如何要用到多线程,基本上都是通过线程池,那么在线程池中,线程其实是很少被回收的,那么这个时候map对象的value就基本上无法回收了,如果持续运行下去,很有可能导致java栈的内存溢出。
当然,ThreadLocal的开发者没有那么傻,当map的key对象被回收的时候,这个key管理的value也会在ThreadLocal的get(),set()等方法中进行判断,如果key已经被回收,那么value也会被回收掉。
但是这种方案的问题在于,是惰性删除,如果这个key永远不会访问,那么它对应的value也永远不会被删除。
也因此,如果我们使用ThreadLocal的时候,结束使用后要记得调用remove()方法进行删除。
Spring的@Transaction事务声明的注解中就使用ThreadLocal保存了当前的Connection对象,避免在本次调用的不同方法中使用不同的Connection对象。
然后我们会将一些登录信息放到ThreadLocal存储,方便我们使用。
请讲一下ArrayList和LikedList的区别?
ArrayList 是应用更加广泛的动态数组实现,它本身不是线程安全的,所以性能要好很多。与 Vector 近似,ArrayList 也是可以根据需要调整容量,不过两者的调整逻辑有所区别,Vector 在扩容时会提高 1 倍,而 ArrayList 则是增加 50%。
LinkedList 顾名思义是 Java 提供的双向链表,所以它不需要像上面两种那样调整容量,它也不是线程安全的。
Jdk6,jdk7,jdk8之间有哪些区别
从JVM层面来看,jdk1.6还是之前的老版本,有实现了方法区的永久代,字符串常量池,静态变量等都放到了永久代里面,而到了jdk1.7的时候,永久代还在,但是已经开始进行去永久代的操作了,字符串常量池和静态变量已经不保存在永久代了,而是放到了堆里面,而且JVM里面出现了G1垃圾收集器,但是还是处于试验状态。到了jdk1.8的时候,永久代已经去除了,类型信息,字段,方法,常量都保存在本地内存的元空间中了,字符串常量池和静态变量仍然在堆中,而且G1垃圾收集器已经成为了推荐使用的垃圾收集器了。
从java提供的类库来看,改变还是很大的,hashmap和ConcurrentHashMap有了很大的优化,HashMap底层的链表长度达到了8以后会转换为红黑树,而ConcurrentHashMap底层的锁,从锁住一个segment改成了锁住一个链表。而且jdk8对lambda表达式进行了支持,让java语言有了流畅的函数式表达能力。
常用的设计模式有哪些?
常见的设计模式主要有单例模式,工厂模式,观察者模式,建造者模式,代理模式,迭代器模式以及其他的一些模式。
我大概讲一下我们常见的单例模式,以及我本人常用的建造者模式。
单例模式,理解起来很简单,就是一个类只允许创造一个对象。单例模式的实现方法有两者,一种是饿汉模式,就是在创建的时候就生成了对象。一种是懒汉模式,懒汉模式就是在使用的时候才会生成对象,而且还会获取对象的方法用synchornized加锁。不过在实际的开发中,是很少使用单例模式,主要的原因是单例模式对代码的扩展性不友好,我们都知道单例模式只有一个对象,而且单例模式不支持有参数的构造函数,倘若哪天我们的需求有变动,想去修改这个单例模式,那就会很麻烦。
java服务启动慢如何排查
Java服务启动慢的原因很多,可能是由于硬件问题、网络问题、代码问题或者配置问题等等。以下是一些排查方法:
- 确认硬件性能:检查服务器的硬件性能是否满足服务的要求,比如CPU、内存、磁盘等。如果硬件性能不足,可能导致服务启动缓慢。
- 检查网络连接:如果服务依赖于其他服务或者数据库,可以检查网络连接是否正常,网络带宽是否足够,网络延迟是否过高等。
- 检查日志输出:查看服务的日志输出,了解服务启动过程中哪些操作较为耗时,是否有异常或错误信息。
- 检查代码逻辑:检查服务代码中是否存在死循环、阻塞或者长时间等待的操作,这些操作可能会导致服务启动缓慢。
- 检查配置文件:检查服务的配置文件,确保配置文件的参数正确、合理,是否有影响服务启动的配置错误。
- 使用性能分析工具:使用性能分析工具(如VisualVM、JProfiler等),对服务进行分析,找出启动慢的原因,进行优化。
以上方法可以帮助我们快速定位服务启动慢的原因,并进行针对性的优化,从而提高服务的启动速度。