⼩⽶⽇常实习⼀⾯(⼀⾯已过)
1.hashmap的结构
JDK1.8 之前 HashMap 由数组+链表组成的,数组是 HashMap 的主体,链表则是主要为了解决哈希冲突⽽存在的(“拉链法”解决冲突)。JDK1.8 以后的HashMap在解决哈希冲突时有了较⼤的变化,当链表长度⼤于阈值(默认为 8)(将链表转换成红⿊树前会判断,如果当前数组的长度⼩于 64,那么会选择先进⾏数组扩容,⽽不是转换为红⿊树)时,将链表转化为红⿊树,以减少搜索时间。
2.为什么负载因⼦是0.75(这⾥我提到了redis的负载因⼦是1或者5,但没有追问redis的hashmap)
loadFactor 加载因⼦是控制数组存放数据的疏密程度,loadFactor 越趋近于 1,那么数组中存放的数据(entry)也就越多,也就越密,也就是会让链表的长度增加,loadFactor 越⼩,也就是趋近于 0,数组中存放的数据(entry)也就越少,也就越稀疏。
loadFactor 太⼤导致查元素效率低,太⼩导致数组的利⽤率低,存放的数据会很分散。loadFactor 的默认值为 0.75f 是官⽅给出的⼀个⽐较好的临界值。
给定的默认容量为 16,负载因⼦为 0.75。Map 在使⽤过程中不断的往⾥⾯存放数据,当数量达到了 16 * 0.75 = 12 就需要将当前 16 的容量进⾏扩容,⽽扩容这个过程涉及到 rehash、复制数据等操作,所以⾮
常消耗性能。
3.volatile的作⽤和原理
我们先要从 CPU 缓存模型说起!
CPU Cache 缓存的是内存数据⽤于解决 CPU 处理速度和内存不匹配的问题,内存缓存的是硬盘数据⽤于解决硬盘访问速度过慢的问题。为了更好地理解,我画了⼀个简单的 CPU Cache ⽰意图如下(实际上,现代的 CPU Cache 通常分为三层,分别叫 L1,L2,L3 Cache):
CPU Cache 的⼯作⽅式:
先复制⼀份数据到 CPU Cache 中,当 CPU 需要⽤到的时候就可以直接从 CPU Cache 中读取数据,当运算完成后,再将运算得到的数据写回 Main Memory 中。但是,这样存在内存缓存不⼀致性的问题!⽐如我执⾏⼀个 i++操作的话,如果两个线程同时执⾏的话,假设两个线程从 CPU Cache 中读取的 i=1,两个线程做了 1++运算完之后再写回 Main Memory 之后 i=2,⽽正确结果应该是 i=3。
CPU 为了解决内存缓存不⼀致性问题可以通过制定缓存⼀致协议或者其他⼿段来解决。
讲⼀下 JMM(Java 内存模型)
Java 内存模型抽象了线程和主内存之间的关系,就⽐如说线程之间的共享变量必须存储在主内存中。Java 内存模型主要⽬的是为了屏蔽系统和硬件的差异,避免⼀套代码在不同的平台下产⽣的效果不⼀致。
在 JDK1.2 之前,Java 的内存模型实现总是从主存(即共享内存)读取变量,是不需要进⾏特别的注意的。⽽在当前的 Java 内存模型下,线程可以把变量保存本地内存(⽐如机器的寄存器)中,⽽不是直接在主存中进⾏读写。这就可能造成⼀个线程在主存中修改了⼀个变量的值,⽽另外⼀个线程还继续使⽤它在寄存器中的变量值的拷贝,造成数据的不⼀致。
主内存:所有线程创建的实例对象都存放在主内存中,不管该实例对象是成员变量还是⽅法中的本地变量(也称局部变量)
本地内存:每个线程都有⼀个私有的本地内存来存储共享变量的副本,并且,每个线程只能访问⾃⼰的本地内存,⽆法访
问其他线程的本地内存。本地内存是 JMM 抽象出来的⼀个概念,存储了主内存中的共享变量副本。
要解决这个问题,就需要把变量声明为volatile,这就指⽰ JVM,这个变量是共享且不稳定的,每次使⽤它都到主存中进⾏读取。
所以,volatile关键字除了防⽌ JVM 的指令重排,还有⼀个重要的作⽤就是保证变量的可见性。
volatile实现内存可见性原理
导致内存不可见的主要原因就是Java内存模型中的本地内存和主内存之间的值不⼀致所导致,例如上⾯所说线程A访问⾃⼰本地内存A的X值时,但此时主内存的X值已经被线程B所修改,所以线程A所访问到的值是⼀个脏数据。那如何解决这种问题呢?
volatile可以保证内存可见性的关键是volatile的读/写实现了缓存⼀致性,缓存⼀致性的主要内容为:
每个处理器会通过嗅探总线上的数据来查看⾃⼰的数据是否过期,⼀旦处理器发现⾃⼰缓存对应的内存地址被修改,就会将当前处理器的缓存设为⽆效状态。此时,如果处理器需要获取这个数据需重新从主内存将其读取到本地内存。
当处理器写数据时,如果发现操作的是共享变量,会通知其他处理器将该变量的缓存设为⽆效状态。
那缓存⼀致性是如何实现的呢?可以发现通过volatile修饰的变量,⽣成汇编指令时会⽐普通的变量多出⼀个Lock指令,这个Lock指令就是volatile关键字可以保证内存可见性的关键,它主要有两个作⽤:
将当前处理器缓存的数据刷新到主内存。
刷新到主内存时会使得其他处理器缓存的该内存地址的数据⽆效。
volatile实现有序性原理
4.什么是原⼦性
volatile的特性有哪些?
并发编程的三⼤特性为可见性、有序性和原⼦性。通常来讲volatile可以保证可见性和有序性。
可见性:volatile可以保证不同线程对共享变量进⾏操作时的可见性。即当⼀个线程修改了共享变量时,另⼀个线程可以读取到共享变量被修改后的值。
有序性:volatile会通过禁⽌指令重排序进⽽保证有序性。
原⼦性:对于单个的volatile修饰的变量的读写是可以保证原⼦性的,但对于i++这种复合操作并不能保证原⼦性。这句话的意思基本上就是说volatile不具备原⼦性了。
4.volatile能不能保证原⼦性
问题来了,既然它可以保证修改的值⽴即能更新到主存,其他线程也会捕捉到被修改后的值,那么为什么不能保证原⼦性呢?
⾸先需要了解的是,Java中只有对基本类型变量的赋值和读取是原⼦操作,如i = 1的赋值操作,但是像j = i或者i++这样的操作都不是原⼦操作,因为他们都进⾏了多次原⼦操作,⽐如先读取i的值,再将i的值赋值给j,两个原⼦操作加起来就不是原⼦操作了。
所以,如果⼀个变量被volatile修饰了,那么肯定可以保证每次读取这个变量值的时候得到的值是最新的,但是⼀旦需要对变量进⾏⾃增这样的⾮原⼦操作,就不会保证这个变量的原⼦性了。
举个栗⼦
⼀个变量i被volatile修饰,两个线程想对这个变量修改,都对其进⾏⾃增操作也就是i++,i++的过程可以分为三步,⾸先获取i的值,其次对i 的值进⾏加1,最后将得到的新值写会到缓存中。
线程A⾸先得到了i的初始值100,但是还没来得及修改,就阻塞了,这时线程B开始了,它也得到了i的值,由于i的值未被修改,即使是被volatile修饰,主存的变量还没变化,那么线程B得到的值也是100,之后对其进⾏加1操作,得到101后,将新值写⼊到缓存中,再刷⼊主存中。根据可见性的原则,这个主存的值可以被其他线程可见。
问题来了,线程A已经读取到了i的值为100,也就是说读取的这个原⼦操作已经结束了,所以这个可见性来的有点晚,线程A阻塞结束后,继续将100这个值加1,得到101,再将值写到缓存,最后刷⼊主存,所以即便是volatile具有可见性,也不能保证对它修饰的变量具有原⼦性。
5.数据库隔离级别
SQL 标准定义了四个隔离级别:
READ-UNCOMMITTED(读取未提交):最低的隔离级别,允许读取尚未提交的数据变更,可能会导致脏读、幻读或不可重复读。
READ-COMMITTED(读取已提交):允许读取并发事务已经提交的数据,可以阻⽌脏读,但是幻读或不可重复读仍有可能发⽣。
REPEATABLE-READ(可重复读):对同⼀字段的多次读取结果都是⼀致的,除⾮数据是被本⾝事务⾃⼰所修改,可以阻⽌脏读和不可重复读,但幻读仍有可能发⽣。
SERIALIZABLE(可串⾏化):最⾼的隔离级别,完全服从ACID的隔离级别。所有的事务依次逐个执⾏,这样事务之间就完全不可能产⽣⼲扰,也就是说,该级别可以防⽌脏读、不可重复读以及幻读。
MySQL InnoDB 存储引擎的默认⽀持的隔离级别是 REPEATABLE-READ(可重读)。我们可以通过SELECT @@tx_isolation;命令来查
看,MySQL 8.0 该命令改为SELECT @@transaction_isolation;
6.幻读举个例⼦
脏读(Dirty read): 当⼀个事务正在访问数据并且对数据进⾏了修改,⽽这种修改还没有提交到数据库中,这时另外⼀个事务也访问了这个数据,然后使⽤了这个数据。因为这个数据是还没有提交的数据,那么另外⼀个事务读到的这个数据是“脏数据”,依据“脏数据”所做的操作可能是不正确的。
丢失修改(Lost to modify): 指在⼀个事务读取⼀个数据时,另外⼀个事务也访问了该数据,那么在第⼀个事务中修改了这个数据后,第⼆个事务也修改了这个数据。这样第⼀个事务内的修改结果就被丢失,因此称为丢失修改。例如:事务1读取某表中的数据A=20,事务2也读取A=20,事务1修改A=A-1,事务2也修改A=A-1,最终结果A=19,事务1的修改被丢失。
不可重复读(Unrepeatableread): 指在⼀个事务内多次读同⼀数据。在这个事务还没有结束时,另⼀个事务也访问该数据。那么,在第⼀个事务中的两次读数据之间,由于第⼆个事务的修改导致第⼀个事务两次读取的数据可能不太⼀样。这就发⽣了在⼀个事务内两次读到的数据是不⼀样的情况,因此称为不可重复读。
幻读(Phantom read): 幻读与不可重复读类似。它发⽣在⼀个事务(T1)读取了⼏⾏数据,接着另⼀个并发事务(T2)插⼊了⼀些数据时。在随后的查询中,第⼀个事务(T1)就会发现多了⼀些原本不存在的记录,就好像发⽣了幻觉⼀样,所以称为幻读。
不可重复度和幻读区别:
不可重复读的重点是修改,幻读的重点在于新增或者删除。
例1(同样的条件, 你读取过的数据, 再次读取出来发现值不⼀样了):事务1中的A先⽣读取⾃⼰的⼯资为 1000的操作还没完成,事务2中的B先⽣就修改了A的⼯资为2000,导致A再读⾃⼰的⼯资时⼯资变为 2000;这就是不可重复读。
例2(同样的条件, 第1次和第2次读出来的记录数不⼀样):假某⼯资单表中⼯资⼤于3000的有4⼈,事务1读取了所有⼯资⼤于3000的⼈,共查到4条记录,这时事务2 ⼜插⼊了⼀条⼯资⼤于3000的记录,事务1再次读取时查到的记录就变为了5条,这样就导致了幻读。
7.innodb如何解决幻读
MySQL InnoDB 的 REPEATABLE-READ(可重读)并不保证避免幻读,需要应⽤使⽤加锁读来保证。⽽这个加锁度使⽤到的机制就是Next-Key Locks。
InnoDB 的三种⾏锁
记录锁:针对单个⾏记录添加锁。
间隙锁(Gap Locking):可以帮我们锁住⼀个范围(索引之间的空隙),但不包括记录本⾝。采⽤间隙锁的⽅式可以防⽌幻读情况的产⽣。
Next-Key 锁:帮我们锁住⼀个范围,同时锁定记录本⾝,相当于间隙锁 + 记录锁,可以解决幻读的问题。
出现幻读的原因是在读已提交的情况下,InnoDB 只采⽤记录锁(Record Locking)!
InnoDB 的解决⽅案
实际上,还是将锁的范围扩⼤到 间隙锁 + 记录锁 来解决幻读问题的。
在隔离级别为可重复读时,InnoDB 会采⽤ Next-Key 锁的机制,解决幻读问题。
<算法有哪些
标记-清除算法
该算法分为“标记”和“清除”阶段:⾸先标记出所有不需要回收的对象,在标记完成后统⼀回收掉所有没有被标记的对象。它是最基础的收集算法,后续的算法都是对其不⾜进⾏改进得到。这种垃圾收集算法会带来两个明显的问题:
1. 效率问题
2. 空间问题(标记清除后会产⽣⼤量不连续的碎⽚)
标记-复制算法
为了解决效率问题,“标记-复制”收集算法出现了。它可以将内存分为⼤⼩相同的两块,每次使⽤其中的⼀块。当这⼀块的内存使⽤完后,就将还存活的对象复制到另⼀块去,然后再把使⽤的空间⼀次清理掉。这样就使每次的内存回收都是对内存区间的⼀半进⾏回收。
标记-整理算法
根据⽼年代的特点提出的⼀种标记算法,标记过程仍然与“标记-清除”算法⼀样,但后续步骤不是直接对可回收对象回收,⽽是让所有存活的对象向⼀端移动,然后直接清理掉端边界以外的内存。
分代收集算法
当前虚拟机的垃圾收集都采⽤分代收集算法,这种算法没有什么新的思想,只是根据对象存活周期的不同将内存分为⼏块。⼀般将 java 堆分为新⽣代和⽼年代,这样我们就可以根据各个年代的特点选择合适的垃圾收集算法。
⽐如在新⽣代中,每次收集都会有⼤量对象死去,所以可以选择”标记-复制“算法,只需要付出少量对象的复制成本就可以完成每次垃圾收集。⽽⽼年代的对象存活⼏率是⽐较⾼的,⽽且没有额外的空间对它进⾏分配担保,所以我们必须选择“标记-清除”或“标记-整理”算法进⾏垃圾收集。
9.可达性分析
这个算法的基本思想就是通过⼀系列的称为 “GC Roots” 的对象作为起点,从这些节点开始向下搜索,节点所⾛过的路径称为引⽤链,当⼀个对象到 GC Roots 没有任何引⽤链相连的话,则证明此对象是不可⽤的,需要被回收。
10.判断gcroots的原则是什么
哪些对象可以作为 GC Roots 呢?
虚拟机栈(栈帧中的本地变量表)中引⽤的对象
本地⽅法栈(Native ⽅法)中引⽤的对象
⽅法区中类静态属性引⽤的对象
⽅法区中常量引⽤的对象
所有被同步锁持有的对象
11.算法题合并有序链表
描述
输⼊两个递增的链表,单个链表的长度为n,合并这两个链表并使新链表中的节点仍然是递增排序的。
数据范围: 0 \le n \le 10000≤n≤1000,-1000 \le 节点值 \le 1000−1000≤节点值≤1000
要求:空间复杂度 O(1)O(1),时间复杂度 O(n)O(n)
如输⼊{1,3,5},{2,4,6}时,合并后的链表为{1,2,3,4,5,6},所以对应的输出为{1,2,3,4,5,6},转换过程如下图所⽰:
/*
public class ListNode {
int val;
ListNode next = null;
ListNode(int val) {
this.val = val;
}
}*/
public class Solution {
public ListNode Merge(ListNode list1,ListNode list2) {
if(list1==null||list2==null)
return list1==null?list2:list1;
ListNode dum=new ListNode(0);
ListNode cur=dum;
while(list1!=null&&list2!=null)
{
if(list1.val<list2.val)
{
<=list1;
;
}
else
{
<=list2;
;
}
sql数据库迁移另一个硬盘
;
}
<=list1!=null?list1:list2;
;
}
}

版权声明:本站内容均来自互联网,仅供演示用,请勿用于商业和其他非法用途。如果侵犯了您的权益请与我们联系QQ:729038198,我们将在24小时内删除。