是否了解LinkedHashMap
LinkedHashMap保存了记录的插入顺序,在用Iterator遍历LinkedHashMap时,先得到的记录肯定是先插入的。
在遍历的时候会比HashMap慢TreeMap能够把它保存的记录根据键排序,默认是按升序排序,也可以指定排序的比较器。当用Iterator遍历TreeMap时,得到的记录是排过序的。
是否了解CopyOnWriteArrayList
一、CopyOnWriteArrayList介绍
①、CopyOnWriteArrayList,写数组的拷贝,支持高效率并发且是线程安全的,读操作无锁的ArrayList。所有可变操作都是通过对底层数组进行一次新的复制来实现。
②、CopyOnWriteArrayList适合使用在读操作远远大于写操作的场景里,比如缓存。它不存在扩容的概念,每次写操作都要复制一个副本,在副本的基础上修改后改变Array引用。CopyOnWriteArrayList中写操作需要大面积复制数组,所以性能肯定很差。
③、CopyOnWriteArrayList 合适读多写少的场景,不过这类慎用 ,因为谁也没法保证CopyOnWriteArrayList 到底要放置多少数据,万一数据稍微有点多,每次add/set都要重新复制数组,这个代价实在太高昂了。在高性能的互联网应用中,这种操作分分钟引起故障。
二、CopyOnWriteArrayList 有几个缺点:
1、由于写操作的时候,需要拷贝数组,会消耗内存,如果原数组的内容比较多的情况下,可能导致young gc或者full gc。
(1、young gc :年轻代(Young Generation):对象被创建时,内存的分配首先发生在年轻代(大对象可以直接被创建在年老代),大部分的对象在创建后很快就不再使用,因此很快变得不可达,于是被年轻代的GC机制清理掉(IBM的研究表明,98%的对象都是很快消亡的),这个GC机制被称为Minor GC或叫Young GC。
2、年老代(Old Generation):对象如果在年轻代存活了足够长的时间而没有被清理掉(即在几次Young GC后存活了下来),则会被复制到年老代,年老代的空间一般比年轻代大,能存放更多的对象,在年老代上发生的GC次数也比年轻代少。当年老代内存不足时,将执行Major GC,也叫 Full GC
)
2、不能用于实时读的场景,像拷贝数组、新增元素都需要时间,所以调用一个set操作后,读取到数据可能还是旧的,虽然CopyOnWriteArrayList 能做到最终一致性,但是还是没法满足实时性要求;可能有朋友说ArrayList是非线程安全的容器,换成Vector就没问题了,实际上换成Vector还是会出现这种错误。
原因在于,虽然Vector的方法采用了synchronized进行了同步,但是实际上通过Iterator访问的情况下,每个线程里面返回的是不同的iterator,也即是说expectedModCount是每个线程私有。假若此时有2个线程,线程1在进行遍历,线程2在进行修改,那么很有可能导致线程2修改后导致Vector中的modCount自增了,线程2的expectedModCount也自增了,但是线程1的expectedModCount没有自增,此时线程1遍历时就会出现expectedModCount不等于modCount的情况了。
是否了解ConcurrentModificationException异常
Java ConcurrentModificationException异常原因和解决方法
在前面一篇文章中提到,对Vector、ArrayList在迭代的时候如果同时对其进行修改就会抛出java.util.ConcurrentModificationException异常。下面我们就来讨论以下这个异常出现的原因以及解决办法。
以下是本文目录大纲:
一.ConcurrentModificationException异常出现的原因
二.在单线程环境下的解决办法
三.在多线程环境下的解决方法
ArrayList<Integer> list = new ArrayList<>();
list.add(2);
Iterator<Integer> iterator = list.iterator();
while(iterator.hasNext()){
Integer integer = iterator.next();
if(integer == 2){
list.remove(integer);
}
}
先在next()方法中会调用checkForComodification()方法,然后根据cursor的值获取到元素,接着将cursor的值赋给lastRet,并对cursor的值进行加1操作。初始时,cursor为0,lastRet为-1,那么调用一次之后,cursor的值为1,lastRet的值为0。注意此时,modCount为0,expectedModCount也为0。
final void checkForComodification() {
if (modCount != expectedModCount)
throw new ConcurrentModificationException();
通过remove方法删除元素最终是调用的fastRemove()方法,在fastRemove()方法中,首先对modCount进行加1操作(因为对集合修改了一次),然后接下来就是删除元素的操作,最后将size进行减1操作,并将引用置为null以方便垃圾收集器进行回收工作。
那么注意此时各个变量的值:对于iterator,其expectedModCount为0,cursor的值为1,lastRet的值为0。
对于list,其modCount为1,size为0。
接着看程序代码,执行完删除操作后,继续while循环,调用hasNext方法()判断,由于此时cursor为1,而size为0,那么返回true,所以继续执行while循环,然后继续调用iterator的next()方法:
注意,此时要注意next()方法中的第一句:checkForComodification()。
关键点就在于:调用list.remove()方法导致modCount和expectedModCount的值不一致。
在单线程环境下的解决办法
在这个方法中,删除元素实际上调用的就是list.remove()方法,但是它多了一个操作:
expectedModCount = modCount;
因此,在迭代器中如果要删除元素的话,需要调用Itr类的remove方法。
在多线程环境下的解决方法
一般有2种解决办法:
1)在使用iterator迭代的时候使用synchronized或者Lock进行同步;
2)使用并发容器CopyOnWriteArrayList代替ArrayList和Vector。
ConcurrentHashMap
ConcurrentHashMap
-
在理想状态下,ConcurrentHashMap 可支持
16个线程
执行并发写操作,及任意数量线程的读操作。 -
关于它的存储结构
- JDK 1.7 中使用分段锁(ReentrantLock + Segment + HashEntry),相当于把一个 HashMap 分成多个段,每段分配一把锁,这样支持多线程访问。锁粒度:
基于 Segment
,包含多个 HashEntry。 - JDK 1.8 中使用 CAS + synchronized + Node + 红黑树。锁粒度:
Node(首结点)
(实现 Map.Entry<K,V>)。锁粒度降低了。
- JDK 1.7 中使用分段锁(ReentrantLock + Segment + HashEntry),相当于把一个 HashMap 分成多个段,每段分配一把锁,这样支持多线程访问。锁粒度:
抽象类和接口
抽象类可以提供成员方法的实现细节
,而接口中没有;但是JDK1.8之后,在接口里面可以定义default方法,default方法里面是可以具备方法体的。- 抽象类中的成员变量可以是
各种类型
的,而接口中的成员变量只能是public static final
类型; - 接口中每一个方法也是隐式指定为 public abstract,不能含有静态代码块以及静态方法,而抽象类可以有
静态代码块和静态方法
; - 一个类只能继承
一个
抽象类,而一个类却可以实现多个
接口。
动态代理
- java的动态代理技术的实现主要有两种方式:
- JDK原生动态代理
- CGLIB动态代理
- JDK原生动态代理是Java原生支持的,不需要任何外部依赖,但是它只能基于接口进行代理(需要代理的对象
必须实现于某个接口
) - CGLIB通过继承的方式进行代理(让需要代理的类成为Enhancer的父类),无论目标对象
有没有实现接口都可以代理
,但是无法处理final的情况
类加载器有哪些
1.根类加载器
2.扩展类加载器
3.系统类加载器
常见树知道哪些
关于synchronized
和volatile
的比较:
- 关键字
volatile
是线程同步的轻量级实现,所以volatile
性能肯定比synchronized
要好,并且只能修改变量,而synchronized
可以修饰方法,以及代码块。 - 多线程访问
volatile
不会发生阻塞,而synchronized
会出现阻塞 volatile
能保证数据的可见性,但不能保证原子性;而synchronized
可以保证原子性,也可以间接保证可见性,因为它会将私有内存和公共内存中的数据做同步- 关键字
volatile
解决的下变量在多线程之间的可见性;而synchronized
解决的是多线程之间资源同步问题
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-Wx6DarbF-1619110176381)(面试宝典问题.assets/20200503100214689.jpg)]
Threadlocal关键字
callable
- callable需要配合线程池使用
- callable比runnable功能复杂一些
- Callable的call方法有返回值并且
可以抛异常
,而Runnable的run方法就没有返回值也没有抛异常,也就是可以知道执行线程的时候除了什么错误。 - Callable运行后可以拿到一个Future对象,这个对象表示异步计算结果,可以从通过Future的
get方法
获取到call方法返回的结果。但要注意调用Future的get方法时,当前线程会阻塞,直到call方法返回结果。
- Callable的call方法有返回值并且
多线程 wait(),notify()方法,案例总结
submit()和 execute()
handlerMapping和HandlerAdapter的区别
Git Flow 的正确使用姿势
mysql自定义函数和存储过程的区别
redis的如何做事务支持
问题回答
- Redis的事务实质上是命令的集合,在一个事务中要么所有命令都被执行,要么所有事物都不执行。
- 事务从开始到执行会经历以下
三个阶段
,MULTI 开始到 EXEC结束前,中间所有的命令都被加入到一个命令队列中;当执行 EXEC命令后,将QUEUE中所有的命令执行。也就是。- MULTI开始事务。
- 命令入队列(QUEUE)。
- EXEC触发执行事务。
- Redis的事务没有关系数据库事务提供的回滚(rollback),所以开发者必须在事务执行失败后进行后续的处理
- 如果在一个事务中的命令出现错误,那么所有的命令都不会执行;
- 如果在一个事务中出现运行错误,那么正确的命令会被执行。
- 此外我们可以使用DISCARD取消事务。
JVM
Eden(伊甸)
空间,From Survivor (幸存)
空间,To Survivor
空间(8:1:1)。
- 老生代和新生代,新生代默认的空间占比总空间的 1/3,老生代的默认占比是 2/3
- 每次在 From Survivor 到 To Survivor 移动时都存活的对象,年龄就 +1,当年龄到达 15(默认配置是 15)时,升级为老生代。大对象也会直接进入老生代。
PreparedStatement和Statement的区别?
谈谈对sql注入的理解。
查看错误日志
tail -fn 100 catalina.out 循环实时查看最后100行记录(最常用的)
配合着grep用, 例如 : tail -fn 100 catalina.out | grep -- '关键字'
#vim
1、进入vim编辑模式:vim filename
2、输入“/关键字”,按enter键查找
3、查找下一个,按“n”即可
#less
less log.log
shift + G 命令到文件尾部 然后输入 ?加上你要搜索的关键字例如 ?1213
shift+n 关键字之间进行切换
软件并发
我们首先应该知道的常见软件的并发能力
- tomcat 调优后500左右
- mysql并发 2000左右
- nginx 1w左右
- redis 5w左右
排序算法比较
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-p9JM1eyi-1619110176385)(面试宝典问题.assets/20180807094112221.png)]
冒泡排序经过优化以后,最好时间复杂度可以达到O(n)。设置一个标志位,如果有一趟比较中没有发生任何交换,可提前结束,因此在正序情况下,时间复杂度为O(n)。
选择排序在最坏和最好情况下,都必须在剩余的序列中选择最小(大)的数,与已排好序的序列后一个位置元素做交换,依次最好和最坏时间复杂度均为O(n^2)。
插入排序是在把已排好序的序列的后一个元素插入到前面已排好序(需要选择合适的位置)的序列中,在正序情况下时间复杂度为O(n)。
堆是完全二叉树,因此树的深度一定是log(n)+1,最好和最坏时间复杂度均为O(n*log(n))。
归并排序是将大数组分为两个小数组,依次递归,相当于二叉树,深度为log(n)+1,因此最好和最坏时间复杂度都是O(n*log(n))。
快速排序在正序或逆序情况下,每次划分只得到比上一次划分少一个记录的子序列,用递归树画出来,是一棵斜树,此时需要n-1次递归,且第i次划分要经过n-i次关键字比较才能找到第i个记录,因此时间复杂度是\sum_{i=1}{n-1}(n-i)=n(n-1)/2,即O(n2)。