面试常见问题总结
java面试之【java中的锁】
-
java中的锁
-
公平锁/非公平锁
-
公平锁是指多个线程按照申请锁的顺序来获取锁。 非公平锁是指多个线程获取锁的顺序并不是按照申请锁的顺序,有可能后申请的线程比先申请的线程优先获取锁,就有可能造成优先级反转或者饥饿现象。 ReentrantLock:通过构造函数指定该锁是否是公平锁,默认是非公平锁。非公平锁的优点在于吞吐量比公平锁大。 Synchronized也是一种非公平锁。由于其并不像ReentrantLock是通过AQS(AbstractQueuedSynchronized)来实现线程调度,所以并没有任何办法使其变成公平锁。
-
-
可重入锁
-
可重入锁又名递归锁,是指在同一个线程在外层方法获取锁的时候,在进入内层方法会自动获取锁,可重入锁的一个好处是可一定程度避免死锁。
-
-
独享锁/共享锁
-
独享锁是指该锁一次只能被一个线程所持有。 共享锁是指该锁可被多个线程所持有。 ReentrantLock是独享锁,另一个ReadWriteLock,其读锁是共享锁,其写锁是独享锁。 读锁的共享锁可保证并发读是非常高效的,读写,写读 ,写写的过程是互斥的。 独享锁与共享锁也是通过AQS(AbstractQueuedSynchronized)来实现的,通过实现不同的方法,来实现独享或者共享。 Synchronized也是独享锁。
-
-
互斥锁/读写锁
-
上面讲的独享锁/共享锁就是一种广义的说法,互斥锁/读写锁就是具体的实现。 互斥锁在Java中的具体实现就是ReentrantLock 读写锁在Java中的具体实现就是ReadWriteLock
-
-
乐观锁/悲观锁
-
乐观锁与悲观锁不是指具体的什么类型的锁,而是指看待并发同步的角度。 悲观锁认为对于同一个数据的并发操作,一定是会发生修改的,哪怕没有修改,也会认为修改。因此对于同一个数据的并发操作,悲观锁采取加锁的形式。悲观的认为,不加锁的并发操作一定会出问题。 乐观锁则认为对于同一个数据的并发操作,是不会发生修改的。 在更新数据的时候,会采用尝试更新,不断重新的方式更新数据。乐观的认为,不加锁的并发操作是没有事情的。从上面的描述我们可以看出,悲观锁适合写操作非常多的场景,乐观锁适合读操作非常多的场景,不加锁会带来大量的性能提升。悲观锁在Java中的使用,就是利用各种锁。乐观锁在Java中的使用,是无锁编程。
-
-
分段锁
-
分段锁其实是一种锁的设计,并不是具体的一种锁,对于ConcurrentHashMap而言,其并发的实现就是通过分段锁的形式来实现高效的并发操作。 分段锁的含义以及设计思想:ConcurrentHashMap中的分段锁称为Segment,它即类似于HashMap(JDK7与JDK8中HashMap的实现)的结构,即内部拥有一个Entry数组,数组中的每个元素又是一个链表;同时又是一个ReentrantLock(Segment继承了ReentrantLock)。当需要put元素的时候,并不是对整个hashmap进行加锁,而是先通过hashcode来知道他要放在那一个分段中,然后对这个分段进行加锁,所以当多线程put的时候,只要不是放在一个分段中,就实现了真正的并行的插入。但是,在统计size的时候,可就是获取hashmap全局信息的时候,就需要获取所有的分段锁才能统计。 分段锁的设计目的是细化锁的粒度,当操作不需要更新整个数组的时候,就仅仅针对数组中的一项进行加锁操作。
-
-
自旋锁
-
在Java中,自旋锁是指尝试获取锁的线程不会立即阻塞,而是采用循环的方式去尝试获取锁,这样的好处是减少线程上下文切换的消耗,缺点是循环会消耗CPU。
-
-
偏向锁/轻量级锁/重量级锁
-
这三种锁是指锁的状态,并且是针对Synchronized。 偏向锁:是指一段同步代码一直被一个线程所访问,那么该线程会自动获取锁。降低获取锁的代价。轻量级锁:是指当锁是偏向锁的时候,被另一个线程所访问,偏向锁就会升级为轻量级锁,其他线程会通过自旋的形式尝试获取锁,不会阻塞,提高性能。 重量级锁:是指当锁为轻量级锁的时候,另一个线程虽然是自旋,但自旋不会一直持续下去,当自旋一定次数的时候,还没有获取到锁,就会进入阻塞,该锁膨胀为重量级锁。重量级锁会让其他申请的线程进入阻塞,性能降低。
-
-
以上均为总结的面试经验,如果哪个地方有问题,欢迎指正。
java面试之【hashmap原理分析】
-
什么是HashMap
-
基于哈希表的 Map 接口的实现。此实现提供所有可选的映射操作,并允许使用 null 值和 null 键。(除了非同步和允许使用 null 之外,HashMap 类与 Hashtable 大致相同。)此类不保证映射的顺序,特别是它不保证该顺序恒久不变。 此实现假定哈希函数将元素适当地分布在各桶之间,可为基本操作(get 和 put)提供稳定的性能。迭代 collection 视图所需的时间与 HashMap 实例的“容量”(桶的数量)及其大小(键-值映射关系数)成比例。所以,如果迭代性能很重要,则不要将初始容量设置得太高(或将加载因子设置得太低)。
-
-
HashMap原理分析
-
数据结构(哈希桶数组)
-
数组(数组的特点是:寻址容易,插入和删除困难)+链表(链表的特点是:寻址困难,插入和删除容易)+红黑树(结合上面两种存储结构的优势)
-
hash 函数
-
用来获取数组下标,把数据放在对应下标元素的链表上
-
-
几个参数
-
length:Node[]初始化长度 默认为16
-
loadFactor:负载因子 默认0.75 负载因子越大,所能容纳的键值对个数越多
-
threshold:所能存储的最大Node个数(键值对)threshold = length * loadFactor (当threshold>length * loadFactor时执行resize扩容,扩容至原来两倍)
-
HashMap的存取实现
-
-
-
static class Node<K,V> implements Map.Entry<K,V> { final int hash; //用来定位数组索引位置 final K key; V value; Node<K,V> next; //链表的下一个node }
当链表长度>8时转换为红黑树
// 存储时: int hash = key.hashCode(); // 这个hashCode方法这里不详述,只要理解每个key的hash是一个固定的int值 int index = hash % Entry[].length; Entry[index] = value; // 取值时: int hash = key.hashCode(); int index = hash % Entry[].length; return Entry[index];
-
put()
-
如果两个key通过hash%Entry[].length得到的index相同,会不会有覆盖的危险?
-
这里HashMap里面用到链式数据结构的一个概念。上面我们提到过Entry类里面有一个next属性,作用是指向下一个Entry,也就是说数组中存储的是最后插入的元素
-
-
public V put(K key, V value) { if (key == null) return putForNullKey(value); //null总是放在数组的第一个链表中 int hash = hash(key.hashCode()); int i = indexFor(hash, table.length); //遍历链表 for (Entry<K,V> e = table[i]; e != null; e = e.next) { Object k; //如果key在链表中已存在,则替换为新value if (e.hash == hash && ((k = e.key) == key || key.equals(k))) { V oldValue = e.value; e.value = value; e.recordAccess(this); return oldValue; } } modCount++; addEntry(hash, key, value, i); return null; } void addEntry(int hash, K key, V value, int bucketIndex) { Entry<K,V> e = table[bucketIndex]; table[bucketIndex] = new Entry<K,V>(hash, key, value, e); //参数e, 是Entry.next //如果size超过threshold,则扩充table大小。再散列 if (size++ >= threshold) resize(2 * table.length); }
-
get()
public V get(Object key) { if (key == null) return getForNullKey(); int hash = hash(key.hashCode()); //先定位到数组元素,再遍历该元素处的链表 for (Entry<K,V> e = table[indexFor(hash, table.length)]; e != null; e = e.next) { Object k; if (e.hash == hash && ((k = e.key) == key || key.equals(k))) return e.value; } return null; }
-
null key/value 的存取 null key总是存放在Entry[]数组的第一个元素。
private V putForNullKey(V value) { for (Entry<K,V> e = table[0]; e != null; e = e.next) { if (e.key == null) { V oldValue = e.value; e.value = value; e.recordAccess(this); return oldValue; } } modCount++; addEntry(0, null, value, 0); return null; } private V getForNullKey() { for (Entry<K,V> e = table[0]; e != null; e = e.next) { if (e.key == null) return e.value; } return null; }
-
获取节点在数组中的位置 HashMap存取时,都需要计算当前key应该对应Entry[]数组哪个元素,即计算数组下标;算法如下:
/** * Returns index for hash code h. */ static int indexFor(int h, int length) { return h & (length-1); }
-
hash 冲突解决 开放定址法(线性探测再散列,二次探测再散列,伪随机探测再散列) 再哈希法 链地址法 建立一个公共溢出区
Java中hashmap的解决办法就是采用的 链地址法。
java面试之【redis】
-
redis cluster集群方式
-
Redis Sentinal着眼于高可用,在master宕机时会自动将slave提升为master,继续提供服务
-
Redis Cluster着眼于扩展性,在单个redis内存不足时,使用Cluster进行分片存储
-
-
Redis有哪些数据结构
-
字符串String
-
字典Hash
-
列表List
-
集合Set
-
有序集合SortedSet
如果你是Redis中高级用户,还需要加上下面几种数据结构HyperLogLog、Geo、 Pub/Sub。Redis Module,像BloomFilter,RedisSearch,Redis-ML
-
-
Redis分布式锁
-
先拿setnx来争抢锁,抢到之后,再用expire给锁加一个过期时间防止锁忘记了释放
-
线上使用keys会发生什么
-
redis关键的一个特性:redis的单线程的。keys指令会导致线程阻塞一段时间,线上服务会停顿,直到指令执行完毕,服务才能恢复,这个时候可以使用scan指令,scan指令可以无阻塞的提取出指定模式的key列表,但是会有一定的重复概率,在客户端做一次去重就可以了,但是整体所花费的时间会比直接用keys指令长
-
-
-
Redis做异步队列
-
使用list结构作为队列,rpush生产消息,lpop消费消息。
-
当lpop没有消息的时候,要适当sleep一会再重试
-
list还有个指令叫blpop,在没有消息的时候,它会阻塞住直到消息到来
-
-
-
redis如何实现延时队列
-
使用sortedset,拿时间戳作为score,消息内容作为key调用zadd来生产消息,消费者用zrangebyscore指令获取N秒之前的数据轮询进行处理
-
-
Redis如何做持久化的 (redis 同步的两种方式,利弊)
-
bgsave做镜像全量持久化
-
bgsave 原理
-
fork和cow。fork是指redis通过创建子进程来进行bgsave操作,cow指的是copy on write,子进程创建后,父子进程共享数据段,父进程继续提供读写服务,写脏的页面数据会逐渐和子进程分离开来
-
-
-
aof做增量持久化
-
因为bgsave会耗费较长时间,不够实时,在停机的时候会导致大量丢失数据,所以需要aof来配合使用
-
在redis实例重启时,会使用bgsave持久化文件重新构建内存,再使用aof重放近期的操作指令来实现完整恢复重启之前的状态
-
-
redis aof同步时间
-
取决于aof日志sync属性的配置,如果不要求性能,在每条写指令时都sync一下磁盘,就不会丢失数据。但是在高性能的要求下每次都sync是不现实的,一般都使用定时sync,比如1s1次,这个时候最多就会丢失1s的数据
-
-
Redis的同步机制 (redis集群同步方式)
-
Redis可以使用主从同步,从从同步
-
第一次同步时,主节点做一次bgsave,并同时将后续修改操作记录到内存buffer,待完成后将rdb文件全量同步到复制节点,复制节点接受完成后将rdb镜像加载到内存
-
加载完成后,再通知主节点将期间修改的操作记录同步到复制节点进行重放就完成了同步过程
-
-
-
redis常见的性能问题及解决方案
-
Master最好不要做任何持久化工作,如RDB内存快照和AOF日志文件
-
如果数据比较重要,某个Slave开启AOF备份数据,策略设置为每秒同步一次
-
为了主从复制的速度和连接的稳定性,Master和Slave最好在同一个局域网内
-
尽量避免在压力很大的主库上增加从库
-
主从复制不要用图状结构,用单向链表结构更为稳定,即:Master <- Slave1 <- Slave2 <- Slave3… 这样的结构方便解决单点故障问题,实现Slave对Master的替换。如果Master挂了,可以立刻启用Slave1做Master,其他不变
-
java面试之【mysql】
-
事务的基本要素 1、 原子性(Atomicity):事务开始后所有操作,要么全部做完,要么全部不做,不可能停滞在中间环节。事务执行过程中出错,会回滚到事务开始前的状态,所有的操作就像没有发生一样。也就是说事务是一个不可分割的整体。 2、 一致性(Consistency):事务开始前和结束后,数据库的完整性约束没有被破坏 。 3、 隔离性(Isolation):同一时间,只允许一个事务请求同一数据,不同的事务之间彼此没有任何干扰。 4、 持久性(Durability):事务完成后,事务对数据库的所有更新将被保存到数据库,不能回滚。
-
事务的并发问题 1、 脏读:事务A读取了事务B更新的数据,然后B回滚操作,那么A读取到的数据是脏数据 2、 不可重复读:事务 A 多次读取同一数据,事务 B 在事务A多次读取的过程中,对数据作了更新并提交,导致事务A多次读取同一数据时,结果 不一致。 3、 幻读:系统管理员A将数据库中所有学生的成绩从具体分数改为ABCDE等级,但是系统管理员B就在这个时候插入了一条具体分数的记录,当系统管理员A改结束后发现还有一条记录没有改过来,就好像发生了幻觉一样,这就叫幻读。
-
数据库事务隔离级别
事务隔离级别 | 脏读 | 不可重复读 | 幻读 |
---|---|---|---|
读未提交(read-uncommitted) | 是 | 是 | 是 |
不可重复读(read-committed) | 否 | 是 | 是 |
可重复读(repeatable-read) | 否 | 否 | 是 |
串行化(serializable) | 否 | 否 | 否 |
-
存储引擎
-
事物回滚怎么实现的
java面试之【volatile】
-
volatile
-
JVM就会向处理器发送一条Lock前缀的指令,将这个变量所在缓存行的数据写回到系统内存。但是就算写回到内存,如果其他处理器缓存的值还是旧的,再执行计算操作就会有问题,所以在多处理器下,为了保证各个处理器的缓存是一致的,就会实现缓存一致性协议,每个处理器通过嗅探在总线上传播的数据来检查自己缓存的值是不是过期了,当处理器发现自己缓存行对应的内存地址被修改,就会将当前处理器的缓存行设置成无效状态,当处理器要对这个数据进行修改操作的时候,会强制重新从系统内存里把数据读到处理器缓存里。
-
java面试之【一致性hash】
-
一致性hash
java 面试之【内存溢出及解决方案】
-
java.lang.OutOfMemoryError: Java heap space -java堆内存不够
-
原因
-
真的是堆内存不够
-
程序中有死循环
-
-
方案
-
-Xms 调整堆的最小值
-
-Xmx 调整堆的最大值
-
-
-
java.lang.OutOfMemoryError: GC overhead limit exceeded -当GC为释放很小空间占用大量时间时抛出
-
原因
-
堆太小,没有足够的内存
-
-
方案
-
查看系统是否有使用大内存的代码或死循环
-
通过添加JVM配置,来限制使用内存 < jvm-arg>-XX:-UseGCOverheadLimit< /jvm-arg>
-
-
-
java.lang.OutOfMemoryError: PermGen space
-
原因
-
Perm区内存不够
-
-
方案
-
< jvm-arg>-XX:MaxPermSize=128m< /jvm-arg>
< jvm-arg>-XXermSize=128m< /jvm-arg>
JVM的Perm区主要用于存放Class和Meta信息的,Class在被Loader时就会被放到PermGen space,这个区域成为年老代,GC在主程序运行期间不会对年老区进行清理,默认是64M大小,当程序需要加载的对象比较多时,超过64M就会报这部分内存溢出了,需要加大内存分配,一般128m足够。
-
-
-
java.lang.StackOverflowError - 栈内存溢出
-
原因
-
方法调用层次过多(比如存在无限递归调用)
-
线程栈太小
-
-
方案
-
优化程序设计,减少方法调用层次
-
调整-Xss参数增加线程栈大小
-
-
-
java.lang.OutOfMemoryError: unable to create new native thread
-
原因
-
Stack空间不足以创建额外的线程
-
创建的线程过多
-
Stack空间确实小了
-
-
方案
-
通过 -Xss启动参数减少单个线程栈大小,这样便能开更多线程(当然不能太小,太小会出现StackOverflowError)
-
通过-Xms -Xmx 两参数减少Heap大小,将内存让给Stack(前提是保证Heap空间够用)。
由于JVM没有提供参数设置总的stack空间大小,但可以设置单个线程栈的大小;而系统的用户空间一共是3G,除了Text/Data/BSS /MemoryMapping几个段之外,Heap和Stack空间的总量有限,是此消彼长的
-
-
文章不定期更新,大家如果有好的提议欢迎