一.ArrayList和LinkedList区别及使用场景
-
LinkedList和ArrayList的差别主要来自于Array和LinkedList数据结构的不同。ArrayList是基于数组实现的,LinkedList是基于双链表实现的。另外LinkedList类不仅是List接口的实现类,可以根据索引来随机访问集合中的元素,除此之外,LinkedList还实现了Deque接口,Deque接口是Queue接口的子接口,它代表一个双向队列,因此LinkedList可以作为双向队列 ,栈(可以参见Deque提供的接口方法)和List集合使用,功能强大。
-
因为Array是基于索引(index)的数据结构,它使用索引在数组中搜索和读取数据是很快的,可以直接返回数组中index位置的元素,因此在随机访问集合元素上有较好的性能。Array获取数据的时间复杂度是O(1),但是要插入、删除数据却是开销很大的,因为这需要移动数组中插入位置之后的的所有元素。
-
相对于ArrayList,LinkedList的随机访问集合元素时性能较差,因为需要在双向列表中找到要index的位置,再返回;但在插入,删除操作是更快的。因为LinkedList不像ArrayList一样,不需要改变数组的大小,也不需要在数组装满的时候要将所有的数据重新装入一个新的数组,这是ArrayList最坏的一种情况,时间复杂度是O(n),而LinkedList中插入或删除的时间复杂度仅为O(1)。ArrayList在插入数据时还需要更新索引(除了插入数组的尾部)。
-
LinkedList需要更多的内存,因为ArrayList的每个索引的位置是实际的数据,而LinkedList中的每个节点中存储的是实际的数据和前后节点的位置。
使用场景:
(1)如果应用程序对数据有较多的随机访问,ArrayList对象要优于LinkedList对象;
(2)如果应用程序有更多的插入或者删除操作,较少的数据读取,LinkedList对象要优于ArrayList对象;
(3)不过ArrayList的插入,删除操作也不一定比LinkedList慢,如果在List靠近末尾的地方插入,那么ArrayList只需要移动较少的数据,而LinkedList则需要一直查找到列表尾部,反而耗费较多时间,这时ArrayList就比LinkedList要快。
二.hashmap如何解决hash冲突,为什么hashmap中的链表需要转成红黑树?
1.hash值冲突是发生在put()时,先查询是否存在该hash值,若不存在,则直接以Entry<V,V>的方式存放在数组中,若存在,则再对比key是否相同,若hash值和key都相同,则替换value,若hash值相同,key不相同,则形成一个单链表,将hash值相同,key不同的元素以Entry<V,V>的方式存放在链表中,这样就解决了hash冲突
2.在jdk1.8版本后,Java对HashMap做了改进,在链表长度大于8的时候,将后面的数据存在红黑树中,以加快检索速度。
三.JDK1.7 并发的HashMap为什么会引起死循环?
线程不安全的HashMap, HashMap在并发执行put操作时会引起死循环,是因为多线程会导致HashMap的Entry链表形成环形数据结构,查找时会陷入死循环。
table这个Entry数组是线程共享的,而next,e是线程私有的。一个map中存有3->7->null两个元素,当两个线程put第三个元素8时,如果出发了resize(),A线程走到transfer()方法时B线程进来了,
B线程会把老的table在自己线程中变成一个新的table写入内存,修改了node的指向关系,变成了7->3->null;这时A线程再拿到B修改过的table去变成新的table,在循环读取元素时,先拿到3放入新的table,
线程A再复制元素 7 ,当前 e = 7 ,而next值由于线程 B 修改了它的引用,所以next 为 3,由于上面取到的next = 3, 接着while循环,即当前处理的结点为3, next就为null ,退出while循环。
这时node的指向关系是,7->3->7形成循环,在get操作时会死循环
四.hashmap的数组长度为什么要保证是2的幂
关键代码:tab[i = (n - 1) & hash] 这里是计算数组索引下标的位置
&为二进制中的与运算
运算规则:0&0=0; 0&1=0; 1&0=0; 1&1=1;
即:两位同时为“1”,结果才为“1”,否则为0
因为hashMap 的数组长度都是2的n次幂 ,那么对于这个数再减去1,转换成二进制的话,就肯定是最高位为0,其他位全是1 的数。
不为2的n次幂 的话,对应的二进制数肯定有一位为0 , 这样不管你的hashCode 值对应的该位,是0还是1 ,
最终得到的该位上的数肯定是0,这带来的问题就是HashMap上的数组元素分布不均匀,而数组上的某些位置,永远也用不到。
五.如何用LinkedHashMap实现LRU
LinkedHashMap默认的元素顺序是put的顺序,但是如果使用带参数的构造函数,那么LinkedHashMap会根据访问顺序来调整内部 顺序。 LinkedHashMap的get()方法除了返回元素之外还可以把被访问的元素放到链表的底端,这样一来每次顶端的元素就是remove的元素。
public LinkedHashMap (int initialCapacity, float loadFactor, boolean accessOrder);
initialCapacity 初始容量
loadFactor 加载因子,一般是 0.75f
accessOrder false 基于插入顺序 true 基于访问顺序(get一个元素后,这个元素被加到最后,使用了LRU 最近最少被使用的调度算法)
LinkedHashMap中有一个方法removeEldestEntry(entry) 我们只需要覆盖这个方法,根据我们自己的需求在一定条件下返回true,这样就实现了LruCache
六.ConcurrentHashMap是如何在保证并发安全的同时提高性能
HashTable容器在竞争激烈的并发环境下表现出效率低下的原因,是因为所有访问HashTable的线程都必须竞争同一把锁,那假如容器里有多把锁,每一把锁用于锁容器其中一部分数据,
那么当多线程访问容器里不同数据段的数据时,线程间就不会存在锁竞争,从而可以有效的提高并发访问效率,这就是ConcurrentHashMap所使用的锁分段技术,首先将数据分成一段一段的存储,
然后给每一段数据配一把锁,当一个线程占用锁访问其中一个段数据的时候,其他段的数据也能被其他线程访问
七.ConcurrentHashMap是如何让多线程同时参与扩容
先判断table的长度是否小于64,如果小于,则优先使用扩容来解决问题,扩容为原来的一倍,调整某一个桶中元素过多的问题(超出了8个)),会触发某些桶中的元素重新分配,避免在一个桶中有太多的元素影响访问效率
在多线的环境下,用volatile的方式读取sizectrl属性的值,来判断map所处的状态,通过cas修改操作来告诉其它线程Map的状态类型。
未初始化
等于0,表示未指定初始化容量,则使用默认容量
大于0,为指定的初始化容量
初始化中
等于-1,表示正在初始化,并且通过cas告诉其它线程
正常状态
等于原table长度n*0.75,扩容阀值
扩容中
小于0,表示有其他线程正在执行扩容操作
等于(resizeStamp(n) << RESIZE_STAMP_SHIFT) + 2表示此时只有一个线程在执行扩容
并且多线程分段扩容
八.ReentrantLock如何实现公平和非公平锁是如何实现
公平锁的lock方法在进行cas判断时多了一个hasQueuedPredecessors()方法,当阻塞队列中没有线程时才尝试获取锁
九.CountDownLatch和CyclicBarrier的区别?各自适用于什么场景?
CountDownLatch和CyclicBarrier都是java.util.concurrent包下面的多线程工具类。
区别:CountDownLatch,当计数为0的时候,下一步的动作实施者是main函数;对于CyclicBarrier,下一步动作实施者是“其他线程”
对于CountDownLatch,其他线程为游戏玩家,比如英雄联盟,主线程为控制游戏开始的线程。在所有的玩家都准备好之前,主线程是处于等待状态的,也就是游戏不能开始。当所有的玩家准备好之后,下一步的动作实施者为主线程,即开始游戏。
公司要求所有人在翻越当前障碍物之后再开始翻越下一个障碍物,也就是所有人翻越第一个障碍物之后,才开始翻越第二个,以此类推。类比地,每一个员工都是一个“其他线程”。当所有人都翻越的所有的障碍物之后,程序才结束。而主线程可能早就结束了,这里我们不用管主线程
十.往线程池里提交一个任务会发生什么
当调用execute()方法添加一个任务时,线程池会做如下判断:
a. 如果正在运行的线程数量小于corePoolSize,那么马上创建线程运行这个任务
b. 如果正在运行的线程数量大于或等于corePoolSize,那么将这个任务放入队列
c. 如果这时候队列满了,而且正在运行的线程数量小于maximunPoolSize,那么还是要创建非核心线程立刻运行这个任务
d. 如果队列满了,而且正在运行的线程数量大于或等于maximunPoolSize,那么线程池会抛出RejectedExecutionException
十一.线程池的几个参数如何设置
corePoolSize 核心线程池大小
maximumPoolSize 线程池最大容量大小
keepAliveTime 线程池空闲时,线程存活的时间
TimeUnit 线程活动保持时间的单位
BlockingQueue<Runnable> 任务队列,用于保存等待执行的任务的阻塞队列
ThreadFactory 用于设置线程的工厂
RejectedExecutionHandler 饱和策略
十二.线程池的非核心线程什么时候会被释放
阻塞队列里没有任务,那么非核心线程也要在等到keepAliveTime时间后才会释放
十三.synchronized与ReentrantLock的区别
sychronized有三条原则:
当一个线程访问“某对象”的“synchronized方法”或者“synchronized代码块”时,其他线程对“该对象”的该“synchronized方法”或者“synchronized代码块”的访问将被阻塞。
当一个线程访问“某对象”的“synchronized方法”或者“synchronized代码块”时,其他线程仍然可以访问“该对象”的非同步代码块。
当一个线程访问“某对象”的“synchronized方法”或者“synchronized代码块”时,其他线程对“该对象”的其他的“synchronized方法”或者“synchronized代码块”的访问将被阻塞。
synchronized会在进入同步块的前后分别形成monitorenter和monitorexit字节码指令.在执行monitorenter指令时会尝试获取对象的锁,如果此没对象没有被锁,或者此对象已经被当前线程锁住,
那么锁的计数器加一,每当monitorexit被锁的对象的计数器减一.直到为0就释放该对象的锁.由此synchronized是可重入的,不会出现自己把自己锁死.
ReentrantLock:
除了synchronized的功能,多了三个高级功能
等待可中断:在持有锁的线程长时间不释放锁的时候,等待的线程可以选择放弃等待,tryLock(long timeout, TimeUnit unit)
公平锁:按照申请锁的顺序来一次获得锁称为公平锁,synchronized的是非公平锁,ReentrantLock可以通过构造函数实现公平锁。new RenentrantLock(boolean fair)
绑定多个Condition:通过多次newCondition可以获得多个Condition对象,可以简单的实现比较负责的线程同步的功能,通过await()等待,signal()唤醒;
十四.乐观锁和悲观锁的区别
乐观锁:相信事务之间的数据竞争(data race)的概率是比较小的,因此尽可能直接做下去,直到提交的时候才去锁定,所以不会产生任何锁和死锁。乐观锁不是数据库自带的,需要我们自己去实现。
悲观锁:就是在操作数据时,认为此操作会出现数据冲突,所以在进行每次操作时都要通过获取锁才能进行对相同数据的操作。悲观锁是由数据库自己实现了的。
使用select…for update会把数据给锁住,不过我们需要注意一些锁的级别,MySQL InnoDB默认行级锁。行级锁都是基于索引的,如果一条SQL语句用不到索引是不会使用行级锁的,会使用表级锁把整张表锁住,这点需要注意。
十五.如何实现一个乐观锁
比如多线程实现a++操作。如果简单循环创建线程执行a++,最终结果不一定是预期的样子,因为a++操作需要三步完成:1.从主内存中读取a的值到线程内存2.对a进行加1操作3.把值写入主内存。所以可能一个线程
还没写回主内存,其他线程就拿到旧的值进行加1。可以参考AtomicInteger的实现就利用了乐观锁。原理是自增操作主要是使用了unsafe的getAndAddInt方法,其内部又调用了Unsafe.compareAndSwapInt方法。这个机制叫做CAS机制。
CAS 即比较并替换,实现并发算法时常用到的一种技术。CAS操作包含三个操作数——内存位置、预期原值及新值。执行CAS操作的时候,将内存位置的值与预期原值比较,如果相匹配,那么处理器会自动将该位置值更新为新值,否则,处理器不做任何操作。
十六.Java中的强引用、软引用、弱引用、虚引用
强引用:如果一个对象具有强引用,不会被垃圾回收器回收。当内存空间不足,Java虚拟机宁愿抛出OutOfMemoryError错误,使程序异常终止,也不回收这种对象。
软引用:软引用关联着的对象,只有在内存不足的时候JVM才会回收该对象
Obj obj = new Obj();
SoftReference<Obj> sr = new SoftReference<Obj>(obj);
obj = null;
System.out.println(sr.get());
弱引用:当JVM进行垃圾回收时,无论内存是否充足,都会回收被弱引用关联的对象
WeakReference<String> sr = new WeakReference<String>(new String("hello"));
System.out.println(sr.get()); // 输出hello
System.gc(); //通知JVM的gc进行垃圾回收
System.out.println(sr.get()); // 输出null
虚引用:不影响对象的生命周期。在java中用java.lang.ref.PhantomReference类表示。如果一个对象与虚引用关联,则跟没有引用与之关联一样,在任何时候都可能被垃圾回收器回收。虚引用主要用来跟踪对象被垃圾回收的活动。
虚引用必须和引用队列关联使用,当垃圾回收器准备回收一个对象时,如果发现它还有虚引用,就会把这个虚引用加入到与之 关联的引用队列中。程序可以通过判断引用队列中是否已经加入了虚引用,来了解被引用的对象是否将要被垃圾回收
ReferenceQueue<String> queue = new ReferenceQueue<String>();
PhantomReference<String> pr = new PhantomReference<String>(new String("hello"), queue);
利用软引用和弱引用解决OOM问题:假如有一个应用需要读取大量的本地图片,如果每次读取图片都从硬盘读取,则会严重影响性能,但是如果全部加载到内存当中,又有可能造成内存溢出,此时使用软引用可以解决这个问题。
十七.java NIO与BIO的区别
NIO:同步非阻塞io模式,核心通过selector管理多个连接,达到一个线程处理多个事件
BIO:同步阻塞IO模式,数据的读取写入必须阻塞在一个线程内等待其完成。
十八.同步阻塞、同步非阻塞、异步的区别
异步:当一个异步过程调用发出后,调用者不会立即得到结果。而是在“发出后”,“被调用者“通过状态,来通知调用者,或通过回调函数处理这个调用
同步阻塞:调用者发出请求后会一直等待结果
同步非阻塞:调用者发出请求后就去执行其他任务,过一会再询问被调用者执行结果
十九.JVM内存模型
根据java虚拟机规范,java虚拟机管理的内存将分为下面五大区域
1.程序计数器是一块很小的内存空间,它是线程私有的,可以认作为当前线程的行号指示器。
2.线程私有栈描述的是Java方法执行的内存模型
3.本地方法栈是与虚拟机栈发挥的作用十分相似,区别是虚拟机栈执行的是Java方法,而本地方法栈则为虚拟机使用到的native方法服务,也是线程私有
4.堆是java虚拟机管理内存最大的一块内存区域,因为堆存放的对象是线程共享的,所以多线程的时候也需要同步机制
5.方法区同堆一样,是所有线程共享的内存区域,用于存储已被虚拟机加载的类信息、常量、静态变量,如static修饰的变量加载类的时候就被加载到方法区中。
二十.为什么要划分成年轻代和老年代,年轻代为什么被划分成eden、survivor区域
年轻代用来存放新近创建的对象,老年代中存放的对象是存活了很久的。
每次新生代的使用,会是eden区和一块survivor区。当我们进行垃圾回收的时候,清除正在使用的区域,将其中的存货对象,
放入到另一个survivor区域,并进行整理,保证空间的连续。如果对象长时间存活,则将对象移动到老年区
二十一.java动态代理和cglib动态代理的区别
java动态代理是利用反射机制生成一个实现代理接口的匿名类,在调用具体方法前调用InvokeHandler来处理。JDK动态代理只能对实现了接口的类生成代理,而不能针对类
cglib动态代理是利用asm开源包,对代理对象类的class文件加载进来,通过修改其字节码生成子类来处理。CGLIB是针对类实现代理,主要是对指定的类生成一个子类,覆盖其中的方法
二十二.spring中bean的生命周期是怎样的
- 实例化一个Bean,也就是我们通常说的new
- 按照Spring上下文对实例化的Bean进行配置,也就是IOC注入
- 如果这个Bean实现了BeanNameAware接口,会调用它实现的setBeanName(String beanId)方法,此处传递的是Spring配置文件中Bean的ID
- 如果这个Bean实现了BeanFactoryAware接口,会调用它实现的setBeanFactory(),传递的是Spring工厂本身(可以用这个方法获取到其他Bean)
- 如果这个Bean实现了ApplicationContextAware接口,会调用setApplicationContext(ApplicationContext)方法,传入Spring上下文,该方式同样可以实现步骤4,但比4更好,以为ApplicationContext是BeanFactory的子接口,有更多的实现方法
- 如果这个Bean关联了BeanPostProcessor接口,将会调用postProcessBeforeInitialization(Object obj, String s)方法,BeanPostProcessor经常被用作是Bean内容的更改,并且由于这个是在Bean初始化结束时调用After方法,也可用于内存或缓存技术
- 如果这个Bean在Spring配置文件中配置了init-method属性会自动调用其配置的初始化方法
- 如果这个Bean关联了BeanPostProcessor接口,将会调用postAfterInitialization(Object obj, String s)方法
注意:以上工作完成以后就可以用这个Bean了,那这个Bean是一个single的,所以一般情况下我们调用同一个ID的Bean会是在内容地址相同的实例 - 当Bean不再需要时,会经过清理阶段,如果Bean实现了DisposableBean接口,会调用其实现的destroy方法
- 最后,如果这个Bean的Spring配置中配置了destroy-method属性,会自动调用其配置的销毁方法
二十三.属性注入和构造器注入哪种会有循环依赖的问题
Spring容器对构造函数配置Bean进行实例化有一个前提,即Bean构造函数入参引用的对象必须已经准备就绪。
由于这个机制,如果两个Bean都相互引用,都采用构造函数注入方式,就会发生类似于线程死锁的循环依赖问题。
可以指定一个bean设置懒加载属性
二十四.redis性能为什么高
1.完全基于内存,绝大部分请求是纯粹的内存操作
2.数据存在内存中,类似于HashMap,HashMap的优势就是查找和操作的时间复杂度都是O(1)
3.采用单线程,避免了不必要的上下文切换和竞争条件,也不存在多进程或者多线程导致的切换而消耗 CPU
4.使用多路I/O复用模型,非阻塞IO。
二十五.单线程的redis如何利用多核cpu机器
redis的单一线程只能用到一个cpu核心,所以可以在同一个多核的服务器中,可以启动多个实例,组成master-master或者master- slave的形式
二十六.redis的缓存淘汰策略
1.定时过期:每个设置过期时间的key都需要创建一个定时器,到过期时间就会立即清除。该策略可以立即清除过期的数据,对内存很友好;但是会占用大量的CPU资源去处理过期的数据
2.惰性过期:只有当访问一个key时,才会判断该key是否已过期,过期则清除。该策略可以最大化地节省CPU资源,却对内存非常不友好。极端情况可能出现大量的过期key没有再次被访问,从而不会被清除,占用大量内存。
3.定期过期:每隔一定的时间,会扫描一定数量的数据库的expires字典中一定数量的key,并清除其中已过期的key。expires字典会保存所有设置了过期时间的key的过期时间数据
二十七.有海量key和value都比较小的数据,在redis中如何存储才更省内存
原理:通过大幅减少key的数量来降低内存的消耗
实现:hash,在客户端通过分组将海量的key根据一定的策略映射到一组hash对象中
二十八.如何保证redis和DB中的数据一致性
1.读的时候先访问缓存,缓存没有访问数据库,然后插入缓存
2.写的时候先写数据库,再写到缓存
二十九.如何解决缓存穿透和缓存雪崩
缓存穿透是指查询一个一定不存在的数据,由于缓存不命中,接着查询数据库也无法查询出结果,因此也不会写入到缓存中,这将会导致每个查询都会去请求数据库,造成缓存穿透
解决:当存储层不命中后,即使返回的空对象也将其缓存起来,同时会设置一个过期时间,之后再访问这个数据将会从缓存中获取,保护了后端数据源;
缓存雪崩是指,由于缓存层承载着大量请求,有效的保护了存储层,但是如果缓存层由于某些原因整体不能提供服务,于是所有的请求都会达到存储层,存储层的调用量会暴增,造成存储层也会挂掉的情况。
解决:使用redis集群部署;对缓存数据设置过期时间增加随机因子,防止缓存同时集体失效
三十.redis如何实现分布式锁
1.分布式锁需要解决的问题
互斥性:任意时刻只能有一个客户端拥有锁,不能同时多个客户端获取
安全性:锁只能被持有该锁的用户删除,而不能被其他用户删除
死锁:获取锁的客户端因为某些原因而宕机,而未能释放锁,其他客户端无法获取此锁,需要有机制来避免该类问题的发生
容错:当部分节点宕机,客户端仍能获取锁或者释放锁
2.通过setNX,并设置失效时间,在finally块释放锁
三十一.Mysql(innondb) 有哪几种事务隔离级别
1.read uncommitted(读取未提交数据):即便是事务没有commit,但是我们仍然能读到未提交的数据,级别最低,造成脏读
2.read committed(可以读取其他事务提交的数据):当前会话只能读取到其他事务提交的数据,未提交的数据读不到。会造成不可重复读
3.repeatable read(可重读)---MySQL默认的隔离级别:当前会话可以重复读,就是每次读取的结果集都相同,而不管其他事务有没有提交。会造成幻读
4.serializable(串行化):其他会话对该表的写操作将被挂起,隔离级别中最严格的,但是这样做势必对性能造成影响
三十二.mysql的行锁、表锁、间隙锁、意向锁分别是做什么的
行锁:mysql的行锁是通过索引加载的,即是行锁是加在索引响应的行上的,要是对应的SQL语句没有走索引,则会全表扫描
表锁:就是对没有索引字段上进行事务操作,会导致表锁
间隙锁:当我们用范围条件而不是相等条件检索数据,InnoDB会给符合条件的已有数据记录的索引项加锁;对于键值在条件范围内并不存在的记录,叫做间隙
意向锁:意向锁产生的主要目的是为了处理行锁和表锁之间的冲突
三十三.mysql的读锁和写锁
MySQL的表级锁有两种模式:读锁和写锁。读的时候可以读,读的时候不能写,写的时候不能读,写的时候不能写(读就是共享读,其他的可以读,不能写,写是独立写,其他的不能读也不能写)
三十四.mysql的最左索引匹配原则
最左优先,以最左边的为起点任何连续的索引都能匹配上。同时遇到范围查询(>、<、between、like)就会停止匹配。
三十五.mysql如何优化慢查询
1.索引没起作用的情况:
在使用LIKE关键字进行查询的查询语句中,如果匹配字符串的第一个字符为“%”,索引不会起作用。只有“%”不在第一个位置索引才会起作用。
满足最左匹配原则
2.优化数据库结构
将字段很多的表分解成多个表
对于需要经常联合查询的表,可以建立中间表以提高查询效率
3.分解关联查询
将一个大的查询分解为多个小查询是很有必要的。
4.优化LIMIT分页
select id,title from collect limit 90000,10; -> select id,title from collect where id>=(select id from collect order by id limit 90000,1) limit 10;
三十六.分库分表如何选择分表键
1.通过redis维护当前最大id返回
2.可以通过设置数据库 sequence 或者表的自增字段步长来进行水平伸缩。每个服务的初始ID不同
3.UUID,因为其无序性,导致索引B+树不能连续维护索引,降低效率
4.当前系统时间戳,高并发情况下不理想
5.snowflake 算法:snowflake 算法是 twitter 开源的分布式 id 生成算法,采用Scala语言实现,是把一个64位的long型的id,1个 bit是不用的+用其中的41bit作为毫秒数+用10bit作为工作机器id+12bi作为序列号。
三十七.分库分表的情况下,查询时一般是如何做排序的
借鉴三十六.5
网友评论