Java基础

Hashmap的原理,增删的情况后端数据结构如何位移

在JDK1.6,JDK1.7中,HashMap采用位桶+链表实现,即使用链表处理冲突,同一hash值的链表都存储在一个链表里。但是当位于一个桶中的元素较多,即hash值相等的元素较多时,通过key值依次查找的效率较低。而JDK1.8中,HashMap采用位桶+链表+红黑树实现,当链表长度超过阈值(8)时,将链表转换为红黑树,这样大大减少了查找时间。

hashMap 1.7和1.8区别

(1)JDK1.7用的是头插法,而JDK1.8及之后使用的都是尾插法
那么他们为什么要这样做呢?因为JDK1.7是用单链表进行的纵向延伸,当采用头插法时会容易出现逆序且环形链表死循环问题。但是在JDK1.8之后是因为加入了红黑树使用尾插法,能够避免出现逆序且链表死循环的问题。
(2)resize扩容时哈希值计算方式不同

  • 扩容后数据存储位置的计算方式也不一样:1. 在JDK1.7的时候是直接用hash值和需要扩容的二进制数进行&(这里就是为什么扩容的时候为啥一定必须是2的多少次幂的原因所在,因为如果只有2的n次幂的情况时最后一位二进制数才一定是1,这样能最大程度减少hash碰撞)(hash值 & length-1)
  • 而在JDK1.8的时候直接用了JDK1.7的时候计算的规律,也就是扩容前的原始位置+扩容的大小值=JDK1.8的计算方式,而不再是JDK1.7的那种异或的方法。但是这种方式就相当于只需要判断Hash值的新增参与运算的位是0还是1就直接迅速计算出了扩容后的储存方式。
    (3)数据结构不同
    JDK1.7的时候使用的是数组+ 单链表的数据结构。但是在JDK1.8及之后时,使用的是数组+链表+红黑树的数据结构(当链表的深度达到8的时候,也就是默认阈值,就会自动扩容把链表转成红黑树的数据结构来把时间复杂度从O(n)变成O(logN)提高了效率)(小于6时回退为链表)

put和resize过程

    hashmap在存数据的时候是基于hashing的原理,当我们调用put(key,value)方法的时候,其实我们会先对键key调用key.hashcode()方法,根据方法返回的hashcode来找到bucket的位置来存Entry对象。(Entry对象存有key和value)
    若hashcode相同,那么它们对应的bucket显然也是相同的,这个时候就会产生所谓的碰撞(hashmap的底层存储结构是 数组+链表)。每个bucket索引对应一个链表,这个时候系统就会找到对应的链表,在对应的链表逐一检查这个链表里有没存在相同的key对象,这个时候是通过equals()这个方法来对比的。如果有,则用新的value取代旧的value。然后在链表的尾部加上这个Entry对象。(1.7采用头插法)

其实get原理和put原理是差不多的,一个逆向的过程。
1、当我们调用get(key)的时候,会调用key的hashcode方法获得hashcode.
2、根据hashcode获取相应的bucket。
3、由于一个bucket对应的链表中可能存有多个Entry,这个时候会调用key的equals方法来找到对应的Entry
4、最后把值返回

JDK1.7和JDK1.8的区别

  1. 最重要的一点是底层结构不一样,1.7是数组+链表,1.8则是数组+链表+红黑树结构;
  2. jdk1.7中当哈希表为空时,会先调用inflateTable()初始化一个数组;而1.8则是直接调用resize()扩容;
  3. 插入键值对的put方法的区别,1.8中会将节点插入到链表尾部,而1.7中是采用头插;
  4. jdk1.7中的hash函数对哈希值的计算直接使用key的hashCode值,而1.8中则是采用key的hashCode异或上key的hashCode进行无符号右移16位的结果,避免了只靠低位数据来计算哈希时导致的冲突,计算结果由高低位结合决定,使元素分布更均匀;
  5. 扩容时1.8会保持原链表的顺序,而1.7会颠倒链表的顺序;而且1.8是在元素插入后检测是否需要扩容,1.7则是在元素插入前;
  6. jdk1.8是扩容时通过hash&cap==0将链表分散,无需改变hash值,而1.7是通过更新hashSeed来修改hash值达到分散的目的;
  7. 扩容策略:1.7中是只要不小于阈值就直接扩容2倍;而1.8的扩容策略会更优化,当数组容量未达到64时,以2倍进行扩容,超过64之后若桶中元素个数不小于7就将链表转换为红黑树,但如果红黑树中的元素个数小于6就会还原为链表,当红黑树中元素不小于32的时候才会再次扩容。

为什么会线程不安全

1、put的时候导致的多线程数据不一致。
这个问题比较好想象,比如有两个线程A和B,首先A希望插入一个key-value对到HashMap中,首先计算记录所要落到的桶的索引坐标,然后获取到该桶里面的链表头结点,此时线程A的时间片用完了,而此时线程B被调度得以执行,和线程A一样执行,只不过线程B成功将记录插到了桶里面,假设线程A插入的记录计算出来的桶索引和线程B要插入的记录计算出来的桶索引是一样的,那么当线程B成功插入之后,线程A再次被调度运行时,它依然持有过期的链表头但是它对此一无所知,以至于它认为它应该这样做,如此一来就覆盖了线程B插入的记录,这样线程B插入的记录就凭空消失了,造成了数据不一致的行为。

2、另外一个比较明显的线程不安全的问题是HashMap的get操作可能因为resize而引起死循环(cpu100%)(jdk1.8采用尾插法后解决了该问题)

默认初始化大小是多少,为啥是这么多,为啥大小都是2的整数次幂

默认初始化大小16

1、保证得到的新的数组索引和老数组索引一致
16的二进制表示为 10000,那么length-1就是15,二进制为01111,同理扩容后的数组长度为32,二进制表示为100000,length-1为31,二进制表示为011111。从下图可以我们也能看到这样会保证低位全为1,而扩容后只有一位差异,也就是多出了最左位的1,这样在通过 h & (length-1)的时候,只要h对应的最左边的那一个差异位为0,就能保证得到的新的数组索引和老数组索引一致(大大减少了之前已经散列良好的老数组的数据位置重新调换)。
2、获得的数组索引index更加均匀
数组长度保持2的次幂,length-1的低位都为1
3、唯一性
&运算,高位是不会对结果产生影响的,所以只关注低位,如果低位全部为1,那么对于h低位部分来说,任何一位的变化都会对结果产生影响,也就是说,要得到index=21这个存储位置,h的低位只有这一种组合。
如果不是2的次幂,也就是低位不是全为1此时,要使得index=21,h的低位部分不再具有唯一性了,哈希冲突的几率会变的更大,同时,index对应的这个bit位无论如何不会等于1了,而对应的那些数组位置也就被白白浪费了
4、更高效的哈希算法
方便进行位与运算,效率较高。

HashMap和LinkedHashMap的区别

HashMap
是一个最常用的Map,它根据键的HashCode 值存储数据,根据键可以直接获取它的值,具有很快的访问速度。HashMap最多只允许一条记录的键为Null;允许多条记录的值为 Null;HashMap不支持线程的同步,即任一时刻可以有多个线程同时写HashMap;可能会导致数据的不一致。如果需要同步,可以用 Collections的synchronizedMap方法使HashMap具有同步的能力。

LinkedHashMap
LinkedHashMap也是一个HashMap,但是内部维持了一个双向链表,可以保持顺序

HashSet

(HashSet内部通过使用HashMap的键来存储集合中的元素,而且内部的HashMap的所有值
都是null。(因为在为HashSet添加元素的时候,内部HashMap的值都是PRESENT),而PRESENT在实例域的地方直接初始化了,而且不允许改变。)

1、元素没有顺序(现在知道为什么没有顺序了把,因为底层用的是HashMap,HashMap本身中的元素度没有顺序,那么HashSet更不可能有了)

2、元素不能重复(这个也很好理解了,因为HashSet中存放的元素,度是当作HashMap的key来使用,HashMap中key是不能相同的,所以HashSet的元素也不能重复了)

3、非线程安全。

Object类知道的方法

Object是java所有类的基类,是整个类继承结构的顶端,也是最抽象的一个类。大家天天都在使用toString()、equals()、hashCode()、waite()、notify()...
2.Object方法详解 Object中含有: registerNatives()、getClass()、hashCode()、equals()、clone()、toString()、notify()...
https://blog.csdn.net/javarrr/article/details/92619598

重写equals和hashcode

1.equals与hashCode的定义必须一致,两个对象equals为true,就必须有相同的hashCode。反之,如果两个对象的hashcode相同,equals不一定相同。
2.当重写equals方法时,一定要同时把hashcode方法一并重写,因为要保证在实现hash表的扩展性

Java语言规范要求equals需要具有如下的特性:
自反性:对于任何非空引用 x,x.equals() 应该返回 true。
对称性:对于任何引用 x 和 y,当且仅当 y.equals(x) 返回 true,x.equals(y) 也应该返回 true。
传递性:对于任何引用 x、y 和 z,如果 x.equals(y)返回 true,y.equals(z) 也应返回同样的结果。
一致性:如果 x 和 y 引用的对象没有发生变化,反复调用 x.equals(y) 应该返回同样的结果。
对于任意非空引用 x,x.equals(null) 应该返回 false。

动态代理

动态代理:无需声明代理类。是使用反射和字节码的技术,在运行期创建指定接口或类的子类(即动态代理类)以及其实例对象的技术。通过动态代理技术可以无侵入地对代码进行增强。

动态代理实现的方式主要有两种:
1.JDK原生动态代理
2.CGLib动态代理(CG:Code Generation)

两种动态代理的最大的区别是:
JDK动态代理要求被代理对象必须基于接口来实现。动态代理类和被代理类必须继承同一个接口。动态代理只能对接口中声明的方法进行代理。对那些没有实现接口的bean。JDK动态代理无法代理。而CGLib通过继承被代理类的方式实现代理。

Java动态代理使用Java原生的反射API进行操作,在生成类上比较高效;CGLIB使用ASM框架直接对字节码进行操作,在类的执行过程中比较高效

Spring 注解默认使用JDK动态代理来实现。也可以通过修改配值,改为使用CGLib动态代理来实现。因而建议Spring注解不要写在Interface上。

java中exception和error有什么区别,运行时异常和一般异常有什么区别

1.exception和error都是继承了throwable类,在java中只有throwable类型的实例才可以被抛出(throw)或者捕获(catch),它是异常处理机制的基本组成类型

2.exception和error体现了java平台设计者对不同异常情况的分类。exception是程序正常运行中,可以预料的意外情况,并且应该被捕获,进行相应的处理

3.error是指在正常情况下,不大可能出现的情况,绝大部分的error都会导致程序(比如jvm自身)处于非正常的、不可恢复的状态。既然是非正常情况,所以不便于也不需要捕获,常见的比如OutOfMemoryError之类,都是Error的子类

4.Exception又分为可检查异常和不可检查异常,可检查异常在源代码里必须显示的进行捕获处理,这是编译期检查的一部分。不可检查异常就是所谓的运行时异常,类似NullPointerException、ArrayIndexOutOfBoundsException之类,通常是可以编码避免的逻辑错误,具体可以根据需要来判断是否需要捕获,并不会在编译期强制要求

ConcurrentHashMap

https://www.jianshu.com/p/865c813f2726*

使用无界阻塞队列会出现什么问题?

当线程在执行任务时需要调用远程服务,当调用远程服务异常时,就会导致线程处理每个任务都需要等待很长的时间;
处理任务的速度慢,产生任务的速度快,而任务队列是没有边界的,就会导致队列变得越来越大,从而导致内存飙升,还可能导致OOM内存溢出。

为什么要使用线程池

  • 提高程序的执行效率
    由于创建和销毁线程需要和底层操作系统交互,大量时间都耗费在创建和销毁线程上,因而比较浪费时间,系统效率很低
    而线程池里的每一个线程任务结束后,并不会死亡,而是再次回到线程池中成为空闲状态,等待下一个对象来使用,因而借助线程池可以提高程序的执行效率
  • 控制线程的数量,防止程序崩溃
    如果不加限制地创建和启动线程很容易造成程序崩溃,比如高并发1000W个线程,JVM就需要有保存1000W个线程的空间,这样极易出现内存溢出
    线程池中可以控制线程数量
    (线程复用
    控制最大并发数
    管理线程)

使用线程池的风险

  • 死锁
    虽然任何多线程程序中都有死锁的风险,但线程池却引入了另一种死锁可能,线程在等待等待队列中的执行结果,但等待队列却因为无法获取线程而不能运行。
  • 资源不足
    线程消耗包括内存和其它系统资源在内的大量资源。如果线程池太大,那么被那些线程消耗的资源可能严重地影响系统性能。在线程之间进行切换将会浪费时间,而且使用超出比您实际需要的线程可能会引起资源匮乏问题,因为池线程正在消耗一些资源,而这些资源可能会被其它任务更有效地利用。
  • 并发错误
    线程池和其它排队机制依靠使用 wait() 和 notify() 方法,这两个方法都难于使用。如果编码不正确,那么可能丢失通知,导致线程保持空闲状态
  • 线程泄漏

请求过载

常用的几种线程池

  • newCachedThreadPool
    创建一个可缓存线程池,如果线程池长度超过处理需要,可灵活回收空闲线程,若无可回收,则新建线程。
    这种类型的线程池特点是:
    工作线程的创建数量几乎没有限制(其实也有限制的,数目为Interger. MAX_VALUE), 这样可灵活的往线程池中添加线程。
    如果长时间没有往线程池中提交任务,即如果工作线程空闲了指定的时间(默认为1分钟),则该工作线程将自动终止。终止后,如果你又提交了新的任务,则线程池重新创建一个工作线程。
    在使用CachedThreadPool时,一定要注意控制任务的数量,否则,由于大量线程同时运行,很有会造成系统瘫痪。
  • newFixedThreadPool
    创建一个指定工作线程数量的线程池。每当提交一个任务就创建一个工作线程,如果工作线程数量达到线程池初始的最大数,则将提交的任务存入到池队列中。
    FixedThreadPool是一个典型且优秀的线程池,它具有线程池提高程序效率和节省创建线程时所耗的开销的优点。但是,在线程池空闲时,即线程池中没有可运行任务时,它不会释放工作线程,还会占用一定的系统资源。
  • newSingleThreadExecutor
    创建一个单线程化的Executor,即只创建唯一的工作者线程来执行任务,它只会用唯一的工作线程来执行任务,保证所有任务按照指定顺序(FIFO, LIFO, 优先级)执行。如果这个线程异常结束,会有另一个取代它,保证顺序执行。单工作线程最大的特点是可保证顺序地执行各个任务,并且在任意给定的时间不会有多个线程是活动的。
  • newScheduleThreadPool
    创建一个定长的线程池,而且支持定时的以及周期性的任务执行,支持定时及周期性任务执行。

Java线程池七个参数详解

https://blog.csdn.net/ye17186/article/details/89467919

线程池执行流程

https://www.cnblogs.com/javazhiyin/p/11364027.html
https://blog.csdn.net/lchq1995/article/details/85230399

线程池的拒绝策略

https://www.jianshu.com/p/a55da1c8bb93

线程池execute 和 submit的区别

1、接收的参数不一样
execute只能提交Runnable类型的任务,而submit既能提交Runnable类型任务也能提交Callable类型任务。
2、submit有返回值,而execute没有
用到返回值的例子,比如说我有很多个做validation的task,我希望所有的task执行完,然后每个task告诉我它的执行结果,是成功还是失败,如果是失败,原因是什么。
然后我就可以把所有失败的原因综合起来发给调用者。
个人觉得cancel execution这个用处不大,很少有需要去取消执行的。
而最大的用处应该是第二点。
3、submit方便Exception处理
execute会直接抛出任务执行时的异常,submit会吃掉异常,可通过Future的get方法将任务执行时的异常重新抛出。
https://blog.csdn.net/dufufd/article/details/84320004

Runnable和Callable的区别
https://blog.csdn.net/const_/article/details/88105721

volatile和synchronized区别

1)本质:volatile本质是在告诉jvm当前变量在寄存器中的值是不确定的,需要从主存中读取,synchronized则是锁定当前变量,只有当前线程可以访问该变量,其他线程被阻塞住.
2)作用范围:volatile仅能使用在变量级别,synchronized则可以使用在变量,方法,代码块
3)原子性:volatile仅能实现变量的修改可见性,而synchronized则可以保证变量的修改可见性和原子性.
  《Java编程思想》上说,定义long或double变量时,如果使用volatile关键字,就会获得(简单的赋值与返回操作)原子性。
4)阻塞:volatile不会造成线程的阻塞,而synchronized可能会造成线程的阻塞.
5、当一个域的值依赖于它之前的值时,volatile就无法工作了,如n=n+1,n++等。如果某个域的值受到其他域的值的限制,那么volatile也无法工作,如Range类的lower和upper边界,必须遵循lower<=upper的限制。
6、使用volatile而不是synchronized的唯一安全的情况是类中只有一个可变的域。

6、synchronized和lock区别

1.Lock是一个接口,而synchronized是Java中的关键字,synchronized是内置的语言实现;
2.synchronized无法判断是否获取锁的状态,Lock可以判断是否获取到锁;
3.synchronized会自动释放锁(a 线程执行完同步代码会释放锁 ;b 线程执行过程中发生异常会释放锁),Lock需在finally中手工释放锁(unlock()方法释放锁),否则容易造成线程死锁;
4.Lock可以让等待锁的线程响应中断,而synchronized却不行,使用synchronized时,等待的线程会一直等待下去,不能够响应中断;
5.synchronized的锁可重入、不可中断、非公平,而Lock锁可重入、可中断、可公平(两者皆可)
6.Lock锁适合大量同步的代码的同步问题,synchronized锁适合代码少量的同步问题。

CyclicBarrier 使用

https://www.jianshu.com/p/333fd8faa56e

读写锁写饥饿

比如在读线程非常多,写线程很少的情况下,很容易导致写线程“饥饿”,虽然使用“公平”策略可以一定程度上缓解这个问题,但是“公平”策略是以牺牲系统吞吐量为代价的。
StampedLock是Java8引入的一种新的锁机制,简单的理解,可以认为它是读写锁的一个改进版本,读写锁虽然分离了读和写的功能,使得读与读之间可以完全并发,但是读和写之间依然是冲突的,读锁会完全阻塞写锁,它使用的依然是悲观的锁策略.如果有大量的读线程,他也有可能引起写线程的饥饿
而StampedLock则提供了一种乐观的读策略,这种乐观策略的锁非常类似于无锁的操作,使得乐观锁完全不会阻塞写线程

StampedLock控制锁有三种模式(写,读,乐观读)

(1)写入(Writing):writeLock是一个独占锁,也是一个悲观锁。

(2)读取(Reading):readLock这时候是一个悲观锁。

(3)乐观读取(Optimistic Reading):提供了tryOptimisticRead方法返回一个非0的stamp,只有当前同步状态没有被写模式所占有时才能获取到。乐观读取模式仅用于短时间读取操作时经常能够降低竞争和提高吞吐量。同时使用的时候一般需要读取并存储到另外一个副本,以用做对比使用。下面干脆使用代码来实现一下这几种锁的实现。

多线程状态图

https://blog.csdn.net/qq_30604989/article/details/80163366

java线程安全的实现方法

https://blog.csdn.net/make__It/article/details/86578443

CAP

https://baijiahao.baidu.com/s?id=1650890231453975345&wfr=spider&for=pc

如何中断一个线程

方法一:调用interrupt方法,通知线程应该中断了:
A.如果线程处于被阻塞状态,那么线程将立即退出被阻塞状态,并抛出了一个InterruptedException异常。
B.如果线程处于正常活动状态,那么会将该线程的中断标志设置为true。被设置中断标志的线程将正常运行,不受影响。

方法二:使用volatile boolean类型变量控制;

await/wait 与sleep、yield间的区别

https://blog.csdn.net/jiyiqinlovexx/article/details/51052592

CAS aba问题

https://www.cnblogs.com/androidsuperman/p/9249180.html
https://baijiahao.baidu.com/s?id=1648077822185803003&wfr=spider&for=pc

动态代理是基于什么原理

反射机制是java语言提供的一种基础功能,赋予程序在运行时自省的能力。通过反射我们可以直接操作类或者对象,比如获取某个对象的类定义,获取类声明的属性和方法,调用方法或者构造对象,甚至可以运行时修改类定义。

动态代理是一种方便运行时动态构建代理、动态处理代理方法调用的机制,很多场景都是利用类似机制做到的,比如用来包装RPC调用、面向切面的编程。

实现动态代理的方式很多,比如JDK自身提供的动态代理,就是主要利用了上面提到的反射机制。还有其他的实现方式,比如利用传说中更高性能的字节码操作机制,类似ASM、aglib、Javassist等。

线程池工作过程

线程池的工作过程如下:

1、线程池刚创建时,里面没有一个线程。任务队列是作为参数传进来的。不过,就算队列里面有任务,线程池也不会马上执行它们。
2、当调用 execute() 方法添加一个任务时,线程池会做如下判断:

  • 如果正在运行的线程数量小于 corePoolSize,那么马上创建线程运行这个任务;
  • 如果正在运行的线程数量大于或等于 corePoolSize,那么将这个任务放入队列;
  • 如果这时候队列满了,而且正在运行的线程数量小于 maximumPoolSize,那么还是要创建非核心线程立刻运行这个任务;
  • 如果队列满了,而且正在运行的线程数量大于或等于 maximumPoolSize,那么线程池会抛出异常RejectExecutionException。

3、当一个线程完成任务时,它会从队列中取下一个任务来执行。
4、当一个线程无事可做,超过一定的时间(keepAliveTime)时,线程池会判断,如果当前运行的线程数大于 corePoolSize,那么这个线程就被停掉。所以线程池的所有任务完成后,它最终会收缩到 corePoolSize 的大小。

NIO与IO的区别

https://blog.csdn.net/u010031673/article/details/51755075
https://www.cnblogs.com/xiaoxi/p/6576588.html

BIO与NIO、AIO的区别

https://blog.csdn.net/ty497122758/article/details/78979302

CyclicBarrier和CountDownLatch

image.png

d

你可能感兴趣的:(Java基础)