1.Hashmap和treemap的异同
异:
- Hashmap中元素无序,通过hashcode查找,treemap中元素按照某一顺序排列
- Hashmap基于hash表实现,treemap基于红黑树实现
- Hashmap适用于map插入、删除、定位元素,treemap适用于按自然顺序遍历
同:
1.都是线程不安全的
2.说说多态
1.多态是同一个对象,在不同的时刻表现出来的不同状态
2.前提:(1)有继承或者实现(2)有方法的重写(3)父类的引用指向子类对象
3.多态特点:表现为父类,但是调用的是子类重写后的方法
4.利弊:好处:提高代码的可维护性和可扩展性,坏处:无法调用子类独有的方法,需要向下转型或者重新创建子类对象
3.线程相关
3.1线程和进程的区别
1.根本区别:进程是操作系统分配资源的基本单位,线程是处理器任务调度和执行的基本单位
2.包含关系:线程是进程的一部分,一个进程内可以有多个线程
3.影响关系:一个进程奔溃后,其他进程可以不受影响,但是一个线程奔溃整个进程都会死掉,所以多进程比多线程强壮
3.2线程的状态
1.新建
2.运行
3.等待:调用wait方法后,会进入等待状态(需要先获取锁,调用wait方法后,释放锁)
Sleep() wait()两个方法之间的异同
同:两种方法都可以让进程进入等待状态
异:1.sleep()方法是thread类的静态方法,wait()方法是object实例方法;2.调用wait()方法前提是必须获取到锁,调用wait之后释放锁。Sleep()方法可以再任何时候使用,调用sleep()方法不会释放锁;3.sleep()方法到时间后可以自动唤醒,wait()方法必须有notify()或者notifyall()方法才能唤醒
4.超时等待:和等待差不多,但是有一个时间限制,如果超过这个时间,自动唤醒
5.阻塞:线程没有获取到锁,会进入阻塞状态
6.终止
3.3创建线程的四种方式
- 继承thread类,重写run方法
- 实现runnable接口myrunnable,重写run方法,将myrunnable作为参数创建thread对象
- 实现callable接口mycallable,重写call方法,将mycallable作为参数创建futuretask对象,将futuretask对象作为参数创建thread对象
- 使用线程池executor创建
3.4创建线程池的四种方式
1.Newfixedthreadpool(n)创建一个固定大小的线程池,可控制线程最大并发数,超过的就排队等待
2.Newcachedthreadpool创建一个可缓存线程池,可以灵活的回收空线程,线程最大并发数不可控制
3.Newscheduledthreadpool创建一个定时线程池,支持定时和周期任务执行
4.Newsinglethreadexecutor创建只有一个线程的线程池
3.5yield关键字
表示在可以让出当前获得的cpu时间片,并且只会让给具有相同线程优先级的线程,但是下一次如果抢到了cpu,就不会让了
3.6线程越多越好吗?
不是,cpu核数有限,1核1000线程是没有意义的。同时线程切换也会有很多的消耗。
3.7多线程和单线程的比较
多线程一定比单线程快吗?
不一定。涉及到几个因素:CPU核数,并发量,以及线程的上下文切换消耗。
什么时候用多线程?
1.高并发的时候,高并发的时候可以体现出多线程的优势
2.有大任务的时候,有大任务,会让单线程一直执行那个任务,导致后面所有的任务都卡死停住了
3.CPU是多核的时候
4.创建对象的几种方式
1.new
2.通过反射。使用 类名.class.newinstance() 创建该类的对象(使用该方法的前提是类里面必须有public的无参构造器)
3.和上一种差不多。通过类名.class.get(declared)constructors.newinstance()。这种方法可以同时适用于有参和无参的构造,并且通过declared可以获取private构造
4.通过序列化和反序列化创建,前提是实现serializable接口
5.String stringbuilder stringbuffer
String 是不可变的对象, 因此在每次对 String 类型进行改变的时候其实都等同于生成了一个新的 String 对象,然后将指针指向新的 String 对象。Stringbuffer和stringbuilder对象可以被多次修改,并且不产生新的未被使用的对象。Stringbuilder相比于stringbuffer,有速度优势,但是是线程不安全的。
1 String a="123";
创建了1个对象,在字符串常量池中
jvm在编译阶段会判断常量池中是否有 "123" 这个常量对象如果有,a直接指向这个常量的引用,如果没有会在常量池里创建这个常量对象。
2 String a=new String("123");
创建了2个对象,字符串常量池中有一个,堆中有一个
同情况1,jvm编译阶段判断常量池中 "123"存在与否,进而来判断是否创建常量对象,然后运行阶段通过new关键字在java heap创建String对象。
3 String a="123"+"456";
创建了1个对象,在字符串常量池中
jvm编译阶段过编译器优化后会把字符串常量直接合并成"123456",所有创建对象时最多会在常量池中创建1个对象。
4 String a="123"+new String("456");
创建了4个对象,字符串常量池中2个,堆中2个
常量池对象"123" ,"456",new String("456")创建堆对象,还有一个堆对象"123456"
6.LinkedList<>几种实现
1.栈stack Stack<Integer> stack = new Stack<>();
push() pop() peek() empty()
2.队列queue Queue<Integer> queue = new LinkedList<>();
offer() poll() peek() isEmpty()
- 双向队列deque
addFirst() addLast() removeFirst() removeLast() isEmpty()
7.原子性、可见性、有序性
7.1原子性
一个操作要么都执行,要么都不执行
比如,i++这种操作就不具有原子性。假设有两个线程同时执行i++的操作。
线程1执行increase方法先读取变量i的值,发现是5,此时切换到线程2执行increase方法读取变量i的值,发现也是5。
线程1执行将变量i的值加1的操作,得到结果是6,线程二也执行这个操作。
线程1将结果赋值给变量i,线程2也将结果赋值给变量i。
在这两个线程都执行了一次increase方法之后,最后的结果竟然是变量i从5变到了6,而不是我们想象中的7。。
解决方案:通过加锁lock或者synchronized
7.2可见性
可见性是指当一个线程修改了共享变量的值,其他线程能够立即得知这个修改。
多线程变量不可见:当一个线程对一变量a修改后,还没有来得及将修改后的a值回写到主存,而被线程调度器中断操作(或收回时间片),然后让另一线程进行对a变量的访问修改,这时候,后来的线程并不知道a值已经修改过,它使用的仍旧是修改之前的a值,这样修改后的a值就被另一线程覆盖掉了。
解决方案:通过volatile关键字。被volatile修饰的成员变量在每次被线程访问时,都强迫从内存中重读该成员变量的值;而且,当成员变量发生变化时,强迫线程将变化值回写到共享内存。或者lock或者synchronized
7.3有序性
重排序:一般来说,处理器为了提高程序运行效率,可能会对输入代码进行优化,它不保证程序中各个语句的执行先后顺序同代码中的顺序一致,但是它会保证程序最终执行结果和代码顺序执行的结果是一致的。在Java内存模型中,允许编译器和处理器对指令进行重排序,但是重排序过程不会影响到单线程程序的执行,却会影响到多线程并发执行的正确性。
解决方案:通过lock synchronized,此外,java自带的happen-before原则也可以保证有序性
1.程序次序规则:一个线程内,按照代码顺序,书写在前面的操作先行发生于书写在后面的操作。
2.锁定规则:一个unLock操作先行发生于后面对同一个锁额lock操作。
3.volatile变量规则:对一个变量的写操作先行发生于后面对这个变量的读操作。
4.传递规则:如果操作A先行发生于操作B,而操作B又先行发生于操作C,则可以得出操作A先行发生于操作C。
5.线程启动规则:Thread对象的start()方法先行发生于此线程的每个一个动作。
6.线程中断规则:对线程interrupt()方法的调用先行发生于被中断线程的代码检测到中断事件的发生。
7.线程终结规则:线程中所有的操作都先行发生于线程的终止检测,我们可以通过Thread.join()方法结束、Thread.isAlive()的返回值手段检测到线程已经终止执行。
8.对象终结规则:一个对象的初始化完成先行发生于他的finalize()方法的开始。
8.双亲委派机制
有几个类加载器:
bootstrap class loader
extension class loader
application class loader
user class loader
当加载一个类时,首先问问自己的父类,是否有加载过,如果有加载过,就不管它了,如果没有,在问问父类的父类,这个不断往上查找的过程是一个询问的过程,并不会真正的加载,直到达到最顶端bootstrap class loader,然后bootstrap class loader开始加载,如果bootstrap class loader无法加载的,再下派到子加载器去加载,直到达到最底层,如果没有任何加载器可以加载,就会报class not found exception。
好处:
1.保证核心类不会被篡改,比如string类
因为无论哪个类,最先开始加载动作的都是bootstrap class loader,这个加载器会加载string类,当加载过之后,其他加载器就不会加载
2.防止重复加载同一个类
在双亲委派机制中,加载类的时候会先向上问一问,如果加载过了就不加载了,避免了重复加载
如果问到双亲是谁:extension class loader 和 application class loader
9.进程间通信
进程间为什么要通信:每个进程有各自的空间,每个进城内部的数据另外一个进程是看不到的,所以进程A需要先将自己的数据拷贝到内存缓冲区,然后进程B再从内存缓冲区中读取数据,实现进程间通信。
- 匿名管道pipe。只适用于父子进程之间通信,并且是单向的,一端写,一端读,速度比较慢。
- 有名管道named pipe。同样是双半工的方式(单向)。但是避免了只能父子进程的局限性。
- 消息队列。消息队列是消息的链表,存放在内核中,并且有一个标识符进行标识。消息队列独立于发送进程和接收进程,当发送进程结束后,消息队列内的消息不会被删除。消息队列可以随机查询,不一定要按照先进先出的方式进行查询。
- 信号量。信号量是一个计数器,主要实现锁的功能。防止某一个进程访问共享资源按的时候,其他进程也在访问。作为不同进程间的同步手段。
- 共享内存。共享内存是由一个进程创建出来的内存共享区域,可以由多个其他进程访问。通常配合其他方式一起使用,比如信号量。
- 套接字socket。与其他通信方式不同的是,套接字可以实现不同机器进程之间的通信。
10.线程间通信
共享内存、消息传递、管道流,java中主要使用共享内存方式进行通信
1.共享内存:有工作内存和主内存,线程通过主内存共享状态。volatile,线程1对变量a更新后,使得变量a强制对线程2可见。
2.消息传递:线程之间没有公共的空间,通过明确的发送消息进行通信。wait/notify等待通知方式。
3.管道流:管道流输入输出的方式。
11.Hash冲突
什么是hash冲突:哈希算法计算前的数据是无限的,但是计算后的结果范围是有限的。因此,总会存在不同的值,计算结果相同。
如何解决hash冲突:
- 开放地址法:发生哈希冲突后,按照某一次序找到下一个空闲的单元,把冲突的元素放入。又可分为:线性探查法,平方探查法,双散列函数探查法
- 拉链法:将哈希值相同的元素构成一个链表,如果长度少于6个是链表,如果长度超过8个转为红黑树。
- 再哈希法:碰到哈希后值相同的情况,使用其他的哈希函数进行计算。
- 创建公共溢出区:将哈希表分为哈表表和溢出表。溢出的数据就放在溢出表。
12.接口和抽象类的区别
(1)接口是多实现,抽象类单继承。
(2)接口中只有抽象方法,抽象类中可以有抽象方法,也可以有非抽象方法。
(3)接口中只能是常量,抽象类中可以有常量,可以有变量。
(4)接口中没有构造方法,抽象类中有构造方法。
何时用接口,何时用抽象类:
接口是对动作的抽象,抽象类是对根源的抽象。
接口表示具体的动作,抽象类表示完成这个动作的主体是谁。
具体的说,比如人吃东西,人就是抽象类,吃东西就是接口。在人这个抽象类里面又可分为男人、女人。因为类是单继承的,接口是多实现的。男人只能继承人类这个类,它不可能又继承人类这个类,又继承动物这个类。但是接口是多实现的,比如男人可以实现吃东西接口,可以实现睡觉接口。
13.Scala和Java有什么区别
1.val var
2.void unit
3.scala 会将函数的最后一行当做return语句
4.循环 scala: for(i <- 1 to 5) 从1到5(包括1包括5)
5. java没有元组 scala 元组tuple val tuple = (100,"Java","Scala","Spark") 通过._1开始调用
6.scala trait特质类似Java的接口,可以多实现,但是trait里面可以包括抽象方法和普通方法
7.scala没有静态方法,静态变量,但是scala里面有object单例对象,起到Java static作用,比如object Dog相当于static class Dog
14.hashmap数据结构
hashmap:数组+链表/红黑树
使用链表的目的:解决哈希冲突
使用红黑树的目的:在解决哈希冲突的情况下,提高性能。
为什么用红黑树,不用avl树?红黑树相比于avl树,查找的速度差不多,都是通过平衡二分查找,但是插入和删除的效率更高,红黑树不追求完全平衡,因此在插入和删除的时候省去了很多再平衡的操作,红黑树插入查找删除时间复杂度都是logn
为什么用红黑树,不用B+树?b+树主要用于数据存储在磁盘上的场景,而红黑树多用于内存中排序。
15. 重写equals和hashcode的联系
重写equals就得重写hashcode
好处:
1.使用hashcode提前校验,避免多次调用equals方法,提高效率。就比如set集合,要求是不可重复的,每插入一个元素,都会和之前的元素进行比较,比如set集合里面已经有了10000个元素,然后插入第10001个,需要做10000次equals比较,效率比较低,重写,可以先利用hash算法,计算hash值,如果hash地址值上已经有元素了,再用equals进行比较
2.保证是同一个对象,如果重写了equals,没有重写hashcode,会出现equals相等,但是hashcode不相等的情况,重写hashcode就是为了避免这种情况。
15.AQS
Lock锁
Lock 接口的实现类主要有:
重入锁(ReentrantLock)
读锁(ReadLock)
写锁(WriteLock)
Lock 接口的实现基本都是通过聚合了一个同步器(AbstractQueuedSynchronizer 缩写为 AQS)的子类来完成线程访问控制的。
AQS同步器(AbstractQueuedSynchronizer)
锁是面向使用者,屏蔽了一些使用锁的细节,使用起来更方便
同步器面向锁的实现者,简化了锁的实现方式,屏蔽了同步状态的管理,等待、唤醒等底层操作
如何使用AQS:
定义一个类,继承AQS,重写AQS里面protected修饰的方法,有独占式方法,共享式方法。例如tryacquire(独占式获取同步状态)
同步组件通过重写AQS的方法,告诉AQS如何判断当前的同步状态是否成功或者成功释放,实现自己想要表达的同步语义(自定义处理逻辑,表示什么时候返回true,什么时候返回false),而AQS只需要同步组件表达的true和false即可,AQS会针对true和false不同的情况做不同的处理
16. ArrayList和LinkedList的区别
相同:arraylist和linkedlist都是list接口的两种实现
区别:
1.arraylist本质是动态数组,linkedlist本质是双向链表
2.对于查询:arraylist时间复杂度为O(1),因为有索引,linkedlist时间复杂度为O(n),因为需要遍历链表(如果查询的是头或者尾,Linkedlist时间复杂度为O(1),底层有first和last方法)
3.对于添加:如果是在末尾添加,arraylist和linkedlist时间复杂度都为O(1)。如果是在中间添加,arraylist需要先找到添加的位置,然后将后面的元素再向后移动一个单位,linkedlist需要先遍历链表,找到位置,再添加。两者时间复杂度都为O(n).
4.对于删除:删除时间复杂度和添加一样。如果是在末尾,两者时间复杂度都为O(1)。如果是中间的元素,两者时间复杂度均为O(n)。
5.总之,如果是查询,arraylist更快,如果是删除或者添加,Linkedlist更有优势,因为arraylist需要移动数据
6.还有arraylist的空间浪费主要体现在末尾的预留空间上,初始容量为10,每次扩容大概增长50%,Linkedlist的花费体现在每个元素都有更多的开销。
17. 重载和重写的区别
重载:方法名称相同,方法参数类型不同(包括参数类型、参数个数、参数顺序),方法返回值类型可以相同,可以不同
重写:方法名称、方法参数类型、方法返回值类型都相同
18.final
修饰变量:final修饰的变量仅可赋值一次,如果是基本数据类型,值无法被更改,如果是引用数据类型,引用不会更改,但是引用所指向的对象里面的数据可以更改
修饰方法:final修饰的方法不可以被重写,但是可以被重载
修饰类:final修饰的类无法被继承
19.nio多路复用非阻塞IO
里面几个核心概念:
1.通道channel
通道,顾名思义,就是一个提供传输的地方。通道里面装载的都是缓冲区Buffer,通过通道,可以读取缓冲区数据,也可以写入缓冲区数据
2.缓冲区buffer
在nio中,所有数据都是用缓冲区处理的。在读取数据时,会读取缓冲区中的数据;在写入数据时,会将数据写入缓冲区中。缓冲区实质上是一个数组,它有几个关键的属性。mark <= position <= limit <= capacity
mark:记录当前position的位置的前一个位置
position:下一个要操作的位置
limit:下一个不可操作的位置
capacity:数组的最大长度
初始时,缓冲区数组长度为0,然后不断的往缓冲区里面写数据,position也会不断的后移。当长度到达一定值时,会执行buffer.flip()方法,将buffer中的数据写入到channel管道中,然后再执行buffer.clear()方法,恢复到初始位置。
3.选择器selector
Selector 可以同时监控多个channel,也就是说,利用Selector 可使一个单独的线程管理多个Channel,检查状态。
4.NIO和IO区别:
20.阻塞IO/非阻塞IO/异步
举个你去饭堂吃饭的例⼦,你好⽐⽤户程序,饭堂好⽐操作系统。阻塞 I/O 好⽐,你去饭堂吃饭,但是饭堂的菜还没做好,然后你就⼀直在那⾥等啊等,等了好⻓⼀段时间终于等到饭堂阿姨把菜端了出来(数据准备的过程),但是你还得继续等阿姨把菜(内核空间)打到你的饭盒⾥(⽤户空间),经历完这两个过程,你才可以离开。⾮阻塞 I/O 好⽐,你去了饭堂,问阿姨菜做好了没有,阿姨告诉你没,你就离开了,过⼏⼗分钟,你⼜来,饭堂问阿姨,阿姨说做好了,于是阿姨帮你把菜打到你的饭盒⾥,这个过程你是得等待的。基于⾮阻塞的 I/O 多路复⽤好⽐,你去饭堂吃饭,发现有⼀排窗⼝,饭堂阿姨告诉你这些窗⼝都还没做好菜,等做好了再通知你,于是等啊等( select 调⽤中),过了⼀会阿姨通知你菜做好了,但是不知道哪个窗⼝的菜做好了,你⾃⼰看吧。于是你只能⼀个⼀个窗⼝去确认,后⾯发现 5 号窗⼝菜做好了,于是你让 5 号窗⼝的阿姨帮你打菜到饭盒⾥,这个打菜的过程你是要等待的,虽然时间不⻓。打完菜后,你⾃然就可以离开了。异步 I/O 好⽐,你让饭堂阿姨将菜做好并把菜打到饭盒⾥后,把饭盒送到你⾯前,整个过程你都不需要任何等待。
21.CAS ABA
CAS
CAS是英文单词CompareAndSwap的缩写,中文意思是:比较并替换。CAS需要有3个操作数:内存地址V,旧的预期值A,即将要更新的目标值B。CAS指令执行时,当且仅当内存地址V的值与预期值A相等时,将内存地址V的值修改为B,否则就什么都不做。整个比较并替换的操作是一个原子操作。
ABA
带有标记的原子引用类“AtomicStampedReference”,它可以通过控制变量值的版本来保证CAS的正确性。A-B-A 就会变成1A-2B-3A
22.可重入锁/不可重入锁
可重入锁
可重入锁就是一个类的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方法释放掉这个锁。
23.synchronized用法
四种用法
1.普通同步方法
public synchronized int get(){}
2.静态同步方法
public static synchronized int get(){}
3.同步代码块(实例对象)
private final Object lock = new Object();
public void set(){
synchronized(lock){}
}
4.同步代码块(类对象/类.class)
public void set(){
synchronized(SynchronizedExample.class){}
}
24.几种锁
- 乐观锁、悲观锁
乐观锁
在 Java 中是通过使用无锁编程来实现,最常采用的是CAS 算法,Java 原子类中的递增操作就通过 CAS 自旋实现的。
悲观锁
synchronized
Lock 实现类
- 公平锁、非公平锁
实现
ReentrantLock
ReentrantReadWriteLock
- 可重入锁、不可重入锁
可重入锁
ReentrantLock
synchronized
ReentrantReadWriteLock 的读写锁
非可重入锁
暂无
- 排它锁、共享锁
实现
排他锁
synchronized
Lock 相关实现类,ReentrantReadWriteLock 写锁
Lock 相关实现类,ReentrantLock
共享锁
ReentrantReadWriteLock 读锁
25.object()方法
toString()
euqals()
hashcode()
wait()
notify()
notifyAll()
26.hashmap为什么线程不安全
jdk 1.7是从头部插入导致数据丢失或者死循环
jdk 1.8插入是从尾部插入造成数据覆盖
jdk1.8造成线程不安全分2中情况;
1.并发执行put操作时会出现hashcode冲突从而导致数据覆盖,造成线程不安全;
2.jdk1.8在(++size>threshold)代码片段,如果并发操作,可能导致两次扩容,但最终结果只有一次扩容的效果,从而线程不安全
27.死锁
(1)什么是死锁:
线程A获得锁A,等待锁B,线程B获得锁B,等待锁A,两者进入僵持状态
(2)死锁发生的条件:
互斥条件:进程要求对所分配的资源进行排它性控制,即在一段时间内某资源仅为一进程所占用。
请求和保持条件:当进程因请求资源而阻塞时,对已获得的资源保持不放。
不剥夺条件:进程已获得的资源在未使用完之前,不能剥夺,只能在使用完时由自己释放。
环路等待条件:在发生死锁时,必然存在一个进程--资源的环形链
28.Hashmap详解
1.说说你对hash算法的理解
任意长度的输入转换为固定长度的输出
追问:hash算法任意长度的输入 转化为了 固定长度的输出,会不会有问题呢?
会碰到,两个不同的对象,经过Hash算法之后,算出同样的Hash值,也就是hash冲突
追问:hash冲突能避免么?
不能完成避免,只能说尽量避免。就比如有10个苹果,有9个抽屉,那么必然会有一个抽屉会放2个以上的苹果
2.你认为好的hash算法,应该考虑点有哪些呢?
首先得效率很高,因为任意长度的输入都要经过计算Hash值,我希望在输入比较长的时候,他也可以计算的很快
然后计算出来的hash值,不能根据这个Hash值逆推出原文
再然后两个输入,只要有一点不同,hash值就得是不同的
其次要尽可能的分散,因为在table中slot大部分都处于空闲状态时,要尽可能的降低hash冲突
3.HashMap中存储数据的结构是什么样的呢?
以jdk8为例,hashmap是数组+链表+红黑树,每个数据单元都是一个Node结构,node结构中有key字段,value字段,next字段,hash字段。next字段表示当发生Hash冲突时,当前桶位中的Node要与冲突Node连成一个链表
4.创建HashMap时,不指定散列表数组长度,初始长度是多少呢?
初始长度16
追问:散列表是new HashMap() 时创建的么?
不是,散列表是懒加载机制,只有当Put数据时才会创建
5.默认负载因子是多少呢,并且这个负载因子有什么作用?
默认负载因子是0.75,也就是75%。负载因子的作用是计算扩容阈值用的,就比如使用无参构造方法创建Hashmap,它的默认情况扩容阈值就是16*0.75=12
6.链表转化为红黑树,需要达到什么条件呢?
链表转红黑树需要两个条件,一个是当前链表长度到达8,另外一个是当前散列表数组长度达到64。否则的话,如果仅仅是链表的长度达到8,数组长度不达标,也不会转红黑树,仅仅会发生一次resize,散列表扩容
7.Node对象内部的hash字段,这个hash值是key对象的hashcode()返回值么?
不是
追问:这个hash值是怎么得到呢?
这个Hash值是key的Hashcode二次加工得到的
追问:hash字段为什么采用高低位异或?
8.HashMap put 写数据的具体流程,尽可能的详细点!
写数据主要分为几种情况吧,主要分为四种情况。根据hash算法,得到一个槽位下标。
第一种就是slot==null。这种情况其实没什么好说的,就直接占用这个slot就行了,把当前Put方法传进来的key和value包装为一个Node对象,放到这个slot中就可以了
第二种就是slot!=null,并且引用的Node还没有链化。这种情况的话,首先比较Node当中的key与当前put进来的key,是否完全相等,如果完全相等,这个put操作其实就是replace操作,就是替换操作,把新的value替换为当前Node中的value。如果不相等的话,其实就是hash冲突了,Hash冲突的话,就是在slot的node后面再加一个Node,采用尾插法
第三种就是slot内Node已经链化。第三种其实和第二种有点接近,首先还是先看链表上元素的key,与当前传过来的key是否一致,如果一致的话,其实就还是一个replace操作,替换value。否则的话,就是迭代到链表的尾部,都没有重复的Key,就将当前的key和value,包装成Node,追加到链表的尾部。此外,追加之后还没完,还需要判断一下链表的长度,是否达到了树化的阈值,如果达到树化阈值的话,就调用一个树化方法
第四种就是冲突比较严重了,链表转为了红黑树
9.红黑树的写入操作,是怎么找到父节点的,找父节点流程?
首先说下TreeNode的数据结构,TreeNode结构就是在Node结构基础上,增加了几个字段,分别是指向父节点的parent,指向左子节点的left,指向右节点的right,还有表示颜色的,这就是TreeNode的基本结构。
然后说下红黑树的插入操作,首先是找到一个合适的插入节点,也就是插入节点的父节点,然后这个红黑树的排序特性和二叉排序树是一致的,所以找父节点的操作和二叉排序树里面一致的,这个二叉排序树,也就是左子节点的值小于当前节点,右子节点的值大于当前节点,每次向下查找都可以排除一半的数据。
查找过程也是分情况的,第一种就是一直向下查找,直到查找道左子树或者右子树为Null,说明树种没有发现treenode的key和要put进来的key一致的,此时探测节点就是要插入的父节点所在了,然后就是将要插入的节点插入到父节点的左子树或者右子树,但是插入会打破平衡,此时需要一个红黑树的平衡算法。
第二种就是发现treenode的key和要插入的key是一致的,此时插入就是一次replace操作,替换掉value就行了
10.TreeNode数据结构,简单说下。
11.红黑树的原则有哪些呢?
(1)第一个就是所有节点,要是是黑色,要么是红色。
(2)第二个就是根节点必须是黑色的。
(3)第三个就是叶子结点一定是黑色的(NIL节点)。
(4)第四个就是所有的从根节点到叶子结点的路径上,黑色节点个数都是一样的。
(5)第五个就是两个红色节点不能相连,所以可以推倒除红色节点的子节点一定是黑色的。
12.JDK8 hashmap为什么引入红黑树?解决什么问题?
为了解决7,或者说7之前吧,hash冲突链化比较严重的问题。因为散列表理想情况下,查询效率是O(1),如果链化比较严重的话,查询效率会退化到O(n)。红黑树的话,是一种二叉搜索数,查询效率比链化快很多。
追问:为什么hash冲突后性能变低了?【送分题】
因为链表不是数组,内存中链表不是连续的。如果要查询链表的话,需要从前往后一个一个数,如果要查询的数据在链表的末尾,需要通过next字段挨个往后查找,就比较耗时间。
13.hashmap 什么情况下会触发扩容呢?
往里面写数据的时候可能会出发扩容,达到扩容阈值后进行扩容。
追问:触发扩容后,会扩容多大呢?算法是什么?
追问:为什么采用位移运算,不是直接*2?
14.hashmap扩容后,老表的数据怎么迁移到扩容后的表的呢?
15.hashmap扩容后,迁移数据发现该slot是颗红黑树,怎么处理呢?
29.Concurrenthashmap详解
concurrenthashmap是如何保证线程安全的:
1.put
(1)数组初始化时的线程安全:通过自旋+CAS+双重check保证初始化时的安全。
具体而言,数组初始化时,首先通过自旋保证一定可以初始化成功,然后通过CAS设置sizectl的值,保证同一时刻只有一个线程对数组进行初始化,CAS成功后,还会再次判断当前数组是否已经初始化完成,如果已经初始化完成,就不会再次初始化。
(2)新增槽点值时的线程安全:自旋+CAS+锁。
为了保证新增时的线程安全,有四处优化:
1)通过自旋死循环保证一定可以新增成功。失败的话,会重复新增,直到成功为止。
2)当槽点为空时,通过CAS新增。在槽点为空时,没有直接赋值。因为在判断槽点为空时,有可能槽点被其他线程赋值了,所以采用了CAS算法,如果槽点确实为空,就进行赋值;如果恰好被其他线程赋值,CAS操作失败,会再次执行for自旋,再进行槽点有值的Put流程。
3)当前槽点有值,说明是hash冲突了,可能是链表或者红黑树,需要锁住当前槽点,通过synchronized实现。
4)如果是红黑树,就锁住红黑树的根节点。
(3)扩容时的线程安全
concurrenthashmap中的扩容方法叫做transfer,分为以下几步:
1)首先需要把老数组的值全部拷贝到扩容之后的新数组上,先从数组的队尾开始拷贝;
2)拷贝数组的槽点时,先把原数组槽点锁住,保证原数组槽点不能操作,成功拷贝到新数组时,把原数组槽点赋值为转移节点;
3)这时如果有新数据正好需要 put 到此槽点时,发现槽点为转移节点,就会一直等待,所以在扩容完成之前,该槽点对应的数据是不会发生变化的;
4)从数组的尾部拷贝到头部,每拷贝成功一次,就把原数组中的节点设置成转移节点;
5)直到所有数组数据都拷贝到新数组时,直接把新数组整个赋值给数组容器,拷贝完成。
2.get
get操作和hashmap差不多。都是先获取数组下标,然后看下标的key是否和传入的key相等,如果是链表或者红黑树的话,用相应的查找方法。
30.什么是自旋锁?
自旋锁(spinlock):是指当一个线程在获取锁的时候,如果锁已经被其它线程获取,那么该线程将循环等待,然后不断的判断锁是否能够被成功获取,直到获取到锁才会退出循环。自旋锁与互斥锁有点类似,只是自旋锁不会引起调用者睡眠,如果自旋锁已经被别的执行单元保持,调用者就一直循环在那里看是否该自旋锁的保持者已经释放了锁,"自旋"一词就是因此而得名。由于自旋锁使用者一般保持锁时间非常短,因此选择自旋而不是睡眠是非常必要的,自旋锁的效率远高于互斥锁。
自旋锁存在的问题
如果某个线程持有锁的时间过长,就会导致其它等待获取锁的线程进入循环等待,消耗CPU。使用不当会造成CPU使用率极高。
上面Java实现的自旋锁不是公平的,即无法满足等待时间最长的线程优先获取锁。不公平的锁就会存在“线程饥饿”问题。
自旋锁的优点
自旋锁不会使线程状态发生切换,一直处于用户态,即线程一直都是active的;不会使线程进入阻塞状态,减少了不必要的上下文切换,执行速度快
非自旋锁在获取不到锁的时候会进入阻塞状态,从而进入内核态,当获取到锁的时候需要从内核态恢复,需要线程上下文切换。 (线程被阻塞后便进入内核(Linux)调度状态,这个会导致系统在用户态与内核态之间来回切换,严重影响锁的性能)
31. lock和synchronized的区别
1.首先synchronized是java内置关键字,在jvm层面,Lock是个java类;
2.synchronized无法判断是否获取锁的状态,Lock可以判断是否获取到锁;
lock()、tryLock()、tryLock(long time, TimeUnit unit) 和 lockInterruptibly()都是用来获取锁的。
(1)lock()方法是平常使用得最多的一个方法,就是用来获取锁。如果锁已被其他线程获取,则进行等待。
(2)tryLock()方法是有返回值的,它表示用来尝试获取锁,如果获取成功,则返回true,如果获取失败(即锁已被其他线程获取),则返回false,也就说这个方法无论如何都会立即返回。在拿不到锁时不会一直在那等待。
3.synchronized会自动释放锁(a 线程执行完同步代码会释放锁 ;b 线程执行过程中发生异常会释放锁),Lock需在finally中手工释放锁(unlock()方法释放锁),否则容易造成线程死锁;
4.用synchronized关键字的两个线程1和线程2,如果当前线程1获得锁,线程2线程等待。如果线程1阻塞,线程2则会一直等待下去,而Lock锁就不一定会等待下去,如果尝试获取不到锁,线程可以不用一直等待就结束了;
5.synchronized的锁可重入、不可中断、非公平,而Lock锁可重入、可判断、可公平(两者皆可)
6.Lock锁适合大量同步的代码的同步问题,synchronized锁适合代码少量的同步问题。
32. 对象的创建过程
Student stu =new Student("张三","18");
就拿上面这句代码来说,虚拟机首先会去检查Student这个类有没有被加载,如果没有,首先去加载这个类到方法区,然后根据加载的Class类对象创建stu实例对象,需要注意的是,stu对象所需的内存大小在Student类加载完成后便可完全确定。内存分配完成后,虚拟机需要将分配到的内存空间的实例数据部分初始化为零值,这也就是为什么我们在编写Java代码时创建一个变量不需要初始化。紧接着,虚拟机会对对象的对象头进行必要的设置,如这个对象属于哪个类,如何找到类的元数据(Class对象),对象的锁信息,GC分代年龄等。设置完对象头信息后,调用类的构造函数。
1、相应类加载检查过程
虚拟机首先会去检查Student这个类有没有被加载,如果没有,首先去加载这个类到方法区,然后根据加载的Class类对象创建stu实例对象
2、为对象分配内存
对象所需内存的大小在类加载完成后便完全确定,为对象分配内存相当于把一块确定大小的内存从Java堆里划分出来
3、对象内存初始化为零
对象内存初始化为零,但不包括对象头;
4、对对象的对象头进行必要的设置
主要设置对象头信息,包括类元数据引用、对象的哈希码、对象的GC分代年龄等
5、调用类的构造函数
33.使用线程池的好处
降低资源消耗:通过重复利用已经创建的线程,降低创建线程和销毁线程的损耗。
提高响应速度:当任务到达时,不需要等待线程的创建就可以直接执行任务。
提高线程的可管理性:线程是稀缺资源,如果无限制的创建,不仅会消耗系统资源,还会降低系统的稳定性,使用线程池可以统一的对线程进行分配,调优和监控。
34. 为什么没有多继承
假设类A中有一个public方法fun(),然后类B和类C同时继承了类A,类B或类C中各自对fun()进行了覆盖,这时类D通过多继承同时继承了类B和类C,这样便导致砖石危机了,程序在运行的时候对应方法fun()该如何判断?
35. 为什么要有Java内存模型JMM
Java内存模型规定了所有的变量都存储在主内存中,每条线程还有自己的工作内存,线程的工作内存中保存了该线程中是用到的变量的主内存副本拷贝,线程对变量的所有操作都必须在工作内存中进行,而不能直接读写主内存。不同的线程之间也无法直接访问对方工作内存中的变量,线程间变量的传递均需要自己的工作内存和主存之间进行数据同步进行。而JMM就作用于工作内存和主存之间数据同步过程。他规定了如何做数据同步以及什么时候做数据同步。所以,总结下:JMM是一种规范,目的是解决由于多线程通过共享内存进行通信时,存在的本地内存数据不一致、编译器会对代码指令重排序、处理器会对代码乱序执行等带来的问题。
36. Java基本类型以及占的空间
byte 1字节 字节型
short 2字节 短整型
int 4字节 整型
long 8字节 长整型
float 4字节 单精度浮点型
double 8字节 双精度浮点型
boolean 1字节 布尔型(值有两种:false,true)
char 2字节 字符型