- 写SQL很常考察group by、内连接和外连接。
- 手写代码:手写代码一般考单例、排序、线程、消费者生产者。我建议排序算法除了冒泡排序,最好还能手写一种其他的排序代码。试想:如果一般面试者都写的冒泡排序,而你写的是快速排序/堆排序,肯定能给面试官留下不错的印象。
面试常问的知识点?
1)集合相关问题(必问):
1.1HashMap、LinkedHashMap、ConcurrentHashMap、ArrayList、LinkedList的底层实现。
1.2HashMap和Hashtable的区别。
1.3ArrayList、LinkedList、Vector的区别。
1.4HashMap和ConcurrentHashMap的区别。
HashMap
- 底层数组+链表实现,可以存储null键和null值,线程不安全
- 初始size为*1***6,扩容:newsize = oldsize*2,size一定为2的n次幂
- 扩容针对整个Map,每次扩容时,原来数组中的元素依次重新计算存放位置,并重新插入
- 插入元素后才判断该不该扩容,有可能无效扩容(插入后如果扩容,如果没有再次插入,就会产生无效扩容)
- 当Map中元素总数超过Entry数组的75%,触发扩容操作,为了减少链表长度,元素分配更均匀
- 计算index方法:index = hash & (tab.length – 1)
HashMap的初始值还要考虑加载因子:
- 哈希冲突:若干Key的哈希值按数组大小取模后,如果落在同一个数组下标上,将组成一条Entry链,对Key的查找需要遍历Entry链上的每个元素执行equals()比较。
- 加载因子:为了降低哈希冲突的概率,默认当HashMap中的键值对达到数组大小的75%时,即会触发扩容。因此,如果预估容量是100,即需要设定100/0.75=134的数组大小。
- 空间换时间*:如果希望加快Key查找的时间,还可以进一步降低加载因子,加大初始大小,以降低哈希冲突的概率。*
ConcurrentHashMap
- 底层采用分段的数组+链表实现,线程安全
- 通过把整个Map分为N个Segment,可以提供相同的线程安全,但是效率提升N倍,默认提升16倍。(读操作不加锁,由于HashEntry的value变量是 volatile的,也能保证读取到最新的值。)
- Hashtable的synchronized是针对整张Hash表的,即每次锁住整张表让线程独占,ConcurrentHashMap允许多个修改操作并发进行,其关键在于使用了锁分离技术
- 有些方法需要跨段,比如size()和containsValue(),它们可能需要锁定整个表而而不仅仅是某个段,这需要按顺序锁定所有段,操作完毕后,又按顺序释放所有段的锁
- 扩容:段内扩容(段内元素超过该段对应Entry数组长度的75%触发扩容,不会对整个Map进行扩容),插入前检测需不需要扩容,有效避免无效扩容
ConcurrentHashMap是使用了锁分段技术来保证线程安全的。
锁分段技术:首先将数据分成一段一段的存储,然后给每一段数据配一把锁,当一个线程占用锁访问其中一个段数据的时候,其他段的数据也能被其他线程访问。
ConcurrentHashMap提供了与Hashtable和SynchronizedMap不同的锁机制。Hashtable中采用的锁机制是一次锁住整个hash表,从而在同一时刻只能由一个线程对其进行操作;而ConcurrentHashMap中则是一次锁住一个桶。
ConcurrentHashMap默认将hash表分为16个桶,诸如get、put、remove等常用操作只锁住当前需要用到的桶。这样,原来只能一个线程进入,现在却能同时有16个写线程执行,并发性能的提升是显而易见的。
1.5HashMap和LinkedHashMap的区别。
1.6HashMap是线程安全的吗。
HashMap在put的时候,插入的元素超过了容量(由负载因子决定)的范围就会触发扩容操作,就是rehash,这个会重新将原数组的内容重新hash到新的扩容数组中,在多线程的环境下,存在同时其他的元素也在进行put操作,如果hash值相同,可能出现同时在同一数组下用链表表示,造成闭环,导致在get时会出现死循环,所以HashMap是线程不安全的。
1.7ConcurrentHashMap是怎么实现线程安全的。
ConcurrentHashMap是使用了锁分段技术来保证线程安全的。
锁分段技术:首先将数据分成一段一段的存储,然后给每一段数据配一把锁,当一个线程占用锁访问其中一个段数据的时候,其他段的数据也能被其他线程访问。
ConcurrentHashMap提供了与Hashtable和SynchronizedMap不同的锁机制。Hashtable中采用的锁机制是一次锁住整个hash表,从而在同一时刻只能由一个线程对其进行操作;而ConcurrentHashMap中则是一次锁住一个桶。
ConcurrentHashMap默认将hash表分为16个桶,诸如get、put、remove等常用操作只锁住当前需要用到的桶。这样,原来只能一个线程进入,现在却能同时有16个写线程执行,并发性能的提升是显而易见的。
2)多线程并发相关问题(必问):
2.1创建线程的3种方式。
- 继承Thread类创建线程类
(1)定义Thread类的子类,并重写该类的run方法,该run方法的方法体就代表了线程要完成的任务。因此把run()方法称为***执行体(线程体)***。
(2)创建Thread子类的实例,即创建了线程对象。
(3)调用线程对象的start()方法来启动该线程。
- 实现Runnable接口
(1)定义runnable接口的实现类,并重写该接口的run()方法,该run()方法的方法体同样是该线程的线程执行体。
(2)创建 Runnable实现类的实例,并以此实例作为Thread的target来创建Thread对象,该Thread对象才是真正的线程对象。
(3)调用线程对象的start()方法来启动该线程。
- 通过Callable和Future创建线程
(1)创建Callable接口的实现类,并实现call()方法,该***call()方法将作为线程执行体,并且有返回值***。
public interface Callable
{
V call() throws Exception;
}
(2)创建Callable实现类的实例,使用FutureTask类来包装Callable对象,该FutureTask对象封装了该Callable对象的call()方法的返回值。(FutureTask是一个包装器,它通过接受Callable来创建,它***同时实现了Future和Runnable接口***。)
(3)使用FutureTask对象作为Thread对象的target创建并启动新线程。
(4)调用FutureTask对象的get()方法来获得子线程执行结束后的返回值。
2.1.1创建线程的三种方式的对比
*1、采用实现Runnable、Callable接口的方式创建多线程*
优势:
线程类只是实现了Runnable接口或Callable接口,还可以继承其他类。
在这种方式下,多个线程可以共享同一个target对象,所以非常适合多个相同线程来处理同一份资源的情况,从而可以将CPU、代码和数据分开,形成清晰的模型,较好地体现了面向对象的思想。
****劣势****:
编程稍微复杂,如果要访问当前线程,则必须***使用Thread.currentThread()方法***。
2、使用继承Thread类的方式创建多线程
优势:
编写简单,如果需要访问当前线程,则无需使用Thread.currentThread()方法,直接使用this即可获得当前线程。
劣势:
线程类已经继承了Thread类,所以不能再继承其他父类。
3、Runnable和Callable的区别
(1) Callable规定(重写)的方法是call(),Runnable规定(重写)的方法是run()。
(2) Callable的任务执行后可返回值,而Runnable的任务是不能返回值的。
(3) call方法可以抛出异常,run方法不可以。
(4) 运行Callable任务可以拿到一个Future对象,表示异步计算的结果。它提供了检查计算是否完成的方法,以等待计算的完成,并检索计算的结果。通过Future对象可以了解任务执行情况,可取消任务的执行,还可获取执行结果future.get()。
2.2什么是线程安全。
java中的线程安全是什么:
就是线程同步的意思,就是当一个程序对一个线程安全的方法或者语句进行访问的时候,其他的不能再对他进行操作了,必须等到这次访问结束以后才能对这个线程安全的方法进行访问
什么叫线程安全:
如果你的代码所在的进程中有多个线程在同时运行,而这些线程可能会同时运行这段代码。如果每次运行结果和单线程运行的结果是一样的,而且其他的变量的值也和预期的是一样的,
就是线程安全的。
或者说(接口的幂等性):一个类或者程序所提供的接口对于线程来说是原子操作或者多个线程之间的切换不会导致该接口的执行结果存在二义性,也就是说我们不用考虑同步的问题。
线程安全问题都是由全局变量及静态变量引起的。
若每个线程中对全局变量、静态变量只有读操作,而无写操作,一般来说,这个全局变量是线程安全的;若有多个线程同时执行写操作,一般都需要考虑线程同步,否则就可能影响线程安全。
存在竞争的线程不安全,不存在竞争的线程就是安全的
怎么解决线程安全问题?
要想解决线程安全的问题,我们就需要解决一个问题,就是线程之间进行同步交互。了解可见性后,我们知道是没有办法相互操作对方的工作内存的。
一般有如下几种方法
synchronized关键字(放在方法上)
同步代码块
jdk1.5的Lock
2.3Runnable接口和Callable接口的区别。
(1) Callable规定(重写)的方法是call(),Runnable规定(重写)的方法是run()。
(2) Callable的任务执行后可返回值,而Runnable的任务是不能返回值的。
(3) call方法可以抛出异常,run方法不可以。
(4) 运行Callable任务可以拿到一个Future对象,表示异步计算的结果。它提供了检查计算是否完成的方法,以等待计算的完成,并检索计算的结果。通过Future对象可以了解任务执行情况,可取消任务的执行,还可获取执行结果future.get()。
2.4wait方法和sleep方法的区别。
2.5synchronized、Lock、ReentrantLock、ReadWriteLock。
- synchronized同步锁
- synchronized属于悲观锁,直接对区域或者对象加锁,性能稳定,可以使用大部分场景。
- ReentrantLock可重入锁(Lock接口)
- 相对于synchronized更加灵活,可以控制加锁和放锁的位置
- 可以使用Condition来操作线程,进行线程之间的通信
- 核心类AbstractQueuedSynchronizer,通过构造一个基于阻塞的CLH队列容纳所有的阻塞线程,而对该队列的操作均通过Lock-Free(CAS)操作,但对已经获得锁的线程而言,ReentrantLock实现了偏向锁的功能。
- ReentrantReadWriteLock可重入读写锁(ReadWriteLock接口)
- 相对于ReentrantLock,对于大量的读操作,读和读之间不会加锁,只有存在写时才会加锁,但是这个锁是悲观锁
- ReentrantReadWriteLock实现了读写锁的功能
- ReentrantReadWriteLock是ReadWriteLock接口的实现类。ReadWriteLock接口的核心方法是readLock(),writeLock()。实现了并发读、互斥写。但读锁会阻塞写锁,是悲观锁的策略。
- StampedLock戳锁
- ReentrantReadWriteLock虽然解决了大量读取的效率问题,但是,由于实现的是悲观锁,当读取很多时,读取和读取之间又没有锁,写操作将无法竞争到锁,就会导致写线程饥饿。所以就需要对读取进行乐观锁处理。
- StampedLock加入了乐观读锁,不会排斥写入
- 当并发量大且读远大于写的情况下最快的的是StampedLock锁
2.6介绍下CAS(无锁技术)。
一:悲观锁与乐观锁
数据库有两种锁,悲观锁的原理是每次实现数据库的增删改的时候都进行阻塞,防止数据发生脏读;乐观锁的原理是在数据库更新的时候,用一个version字段来记录版本号,然后通过比较是不是自己要修改的版本号再进行修改。这其中就引出了一种比较替换的思路来实现数据的一致性,事实上,cas也是基于这样的原理。
二:CAS技术原理
2.1:cas是什么?
cas的英文翻译全称是compare and set ,也就是比较替换技术,·它包含三个参数,CAS(V,E,N),其中V(variate)表示欲更新的变量,E(Excepted)表示预期的值,N(New)表示新值,只有当V等于E值的时候吗,才会将V的值设为N,如果V值和E值不同,则说明已经有其它线程对该值做了更新,则当前线程什么都不做,直接返回V值。
举个例子,假如现在有一个变量int a=5;我想要把它更新为6,用cas的话,我有三个参数cas(5,5,6),我们要更新的值是5,找到了a=5,符合V值,预期的值也是5符合,然后就会把N=6更新给a,a的值就会变成6;
2.2:cas的优点
2.2.1cas是以乐观的态度运行的,它总是认为当前的线程可以完成操作,当多个线程同时使用CAS的时候只有一个最终会成功,而其他的都会失败。这种是由欲更新的值做的一个筛选机制,只有符合规则的线程才能顺利执行,而其他线程,均会失败,但是失败的线程并不会被挂起,仅仅是尝试失败,并且允许再次尝试(当然也可以主动放弃)
2.2.2:cas可以发现其他线程的干扰,排除其他线程造成的数据污染
2.7volatile关键字的作用和原理。
1.保证可见性,不保证原子性
(1)当写一个volatile变量时,JMM会把该线程本地内存中的变量强制刷新到主内存中去;
(2)这个写会操作会导致其他线程中的volatile变量缓存无效。
2.禁止指令重排
重排序是指编译器和处理器为了优化程序性能而对指令序列进行排序的一种手段。重排序需要遵守一定规则:
(1)重排序操作不会对存在数据依赖关系的操作进行重排序。
比如:a=1;b=a; 这个指令序列,由于第二个操作依赖于第一个操作,所以在编译时和处理器运行时这两个操作不会被重排序。
(2)重排序是为了优化性能,但是不管怎么重排序,单线程下程序的执行结果不能被改变
volatile原理
volatile可以保证线程可见性且提供了一定的有序性,但是无法保证原子性。在JVM底层volatile是采用“内存屏障”来实现的。观察加入volatile关键字和没有加入volatile关键字时所生成的汇编代码发现,加入volatile关键字时,会多出一个lock前缀指令,lock前缀指令实际上相当于一个内存屏障(也成内存栅栏),内存屏障会提供3个功能:
(1)它确保指令重排序时不会把其后面的指令排到内存屏障之前的位置,也不会把前面的指令排到内存屏障的后面;即在执行到内存屏障这句指令时,在它前面的操作已经全部完成;
(2)它会强制将对缓存的修改操作立即写入主存;
(3)如果是写操作,它会导致其他CPU中对应的缓存行无效。
2.8什么是ThreadLocal。
首先,它是一个数据结构,有点像HashMap,可以保存"key : value"键值对,但是一个ThreadLocal只能保存一个,并且各个线程的数据互不干扰。
ThreadLocal<String> localName = new ThreadLocal();localName.set("占小狼");String name = localName.get();
在线程1中初始化了一个ThreadLocal对象localName,并通过set方法,保存了一个值 占小狼
,同时在线程1中通过 localName.get()
可以拿到之前设置的值,但是如果在线程2中,拿到的将是一个null。这是为什么,如何实现?不过之前也说了,ThreadLocal保证了各个线程的数据互不干扰。
每个线程中都有一个 ThreadLocalMap
数据结构,当执行set方法时,其值是保存在当前线程的 threadLocals
变量中,当执行set方法中,是从当前线程的 threadLocals
变量获取。
所以在线程1中set的值,对线程2来说是摸不到的,而且在线程2中重新set的话,也不会影响到线程1中的值,保证了线程之间不会相互干扰。
那每个线程中的 ThreadLoalMap
究竟是什么?
从名字上看,可以猜到它也是一个类似HashMap的数据结构,但是在ThreadLocal中,并没实现Map接口。
在ThreadLoalMap中,也是初始化一个大小16的Entry数组,Entry对象用来保存每一个key-value键值对,只不过这里的key永远都是ThreadLocal对象,是不是很神奇,通过ThreadLocal对象的set方法,结果把ThreadLocal对象自己当做key,放进了ThreadLoalMap中。
2.9创建线程池的4种方式。
1、new Thread
执行一个异步任务你还只是如下new Thread吗?
new Thread(new Runnable() {
@Override
public void run() {
// TODO Auto-generated method stub
}
}
).start();
那你就out太多了,new Thread的弊端如下:
a. 每次new Thread**新建对象性能差**。 b. 线程缺乏统一管理,可能无限制新建线程,相互之间竞争,及可能占用过多系统资源导致死机或oom(out of memory)。 c. 缺乏更多功能,如定时执行、定期执行、线程中断。
相比new Thread,Java提供的四种线程池的好处在于:
a. 重用存在的线程,减少对象创建、消亡的开销,性能佳。
b. 可有效控制最大并发线程数,提高系统资源的使用率,同时避免过多资源竞争,避免堵塞。
c. 提供定时执行、定期执行、单线程、并发数控制等功能。
2、Java 线程池
Java通过Executors提供四种线程池,分别为:
newCachedThreadPool 创建一个可缓存的线程池,如果线程池长度超过处理需求,可灵活回收空闲线程,若无可回收,则新建线程;线程池为无限大,当执行第二个任务时第一个任务已经完成,会复用执行第一个任务的线程,而不用每次新建线程。
newFixedThreadPool 创建一个定长线程池,可控制线程最大并发数,超出的线程会在队列中等待;定长线程池的大小最好根据系统资源进行设置。
newScheduledThreadPool 创建一个定长线程池,支持定时及周期性任务执行。
newSingleThreadExecutor 创建一个单线程化的线程池,它只会唯一的工作线程来执行任务,保证所有任务按照指定顺序(FIFO,LIFO,优先级)执行。
2.9.1为什么要用线程池:
1.减少了创建和销毁线程的次数,每个工作线程都可以被重复利用,可执行多个任务。
2.可以根据系统的承受能力,调整线程池中工作线线程的数目,防止因为消耗过多的内存,而把服务器累趴下(每个线程需要大约1MB内存,线程开的越多,消耗的内存也就越大,最后死机)。
Java里面线程池的顶级接口是Executor,但是严格意义上讲Executor并不是一个线程池,而只是一个执行线程的工具。真正的线程池接口是ExecutorService。
2.10ThreadPoolExecutor的内部工作原理。
2.11分布式环境下,怎么保证线程安全。
3)JVM相关问题:
3.1介绍下垃圾收集机制(在什么时候,对什么,做了什么)。
Java虚拟机中的内存分为程序计数器、虚拟机栈、本地方法栈、Java堆和方法区等几部分,在哪些部分回收内存呢?
确定了要回收的内存,内存中必然存在着很多内容,如何判定这些内容就是不需要的垃圾了呢?
程序不断运行,垃圾收集不可能也随着程序一直运行,那什么时候进行垃圾收集操作呢?
最重要的问题是,怎么回收?
3.1.1回收区域
在前面几篇中可以知道,Java内存中的程序计数器、虚拟机栈和本地方法栈是线程私有的,线程结束也就没了。其中程序计数器负责指示下一条指令,栈中的栈帧随着方法的进入和退出不停的入栈出栈。每一个栈帧的大小在编译时就基本已经确定。所以这几个区域就不需要考虑内存回收,因为方法结束或线程停止,内存就回收了。
和上述三个区域不同的是,Java堆和方法区是线程共享的。在Java堆中存放着所有线程在运行时创建的对象,在方法区中存放着关于类的元数据信息。我们在程序运行时才能确定需要加载哪些类的元数据信息到方法区,创建哪些对象到堆中,也就是说,这部分的内存分配和回收都是动态的。也因为这样,这两个部分是垃圾收集器所关注的地方。
3.1.2谁才是垃圾?可达性分析算法(Reachability Analysis)
首先考虑一下存放对象的Java堆。
程序中创建的对象绝大多数都会在Java堆中,而程序的运行也会创建大量的对象。但这些对象并不总是使用,这样就产生了一些不会再使用的垃圾。这些垃圾占据着宝贵的内存空间,所以需要回收这些空间。不过,怎么才能确定堆中的对象是垃圾呢?
一种常见的算法是引用计数算法,它基于这样的考虑,给对象添加一个引用计数器,每当有一个地方引用它时,计数器的值就加1;当引用实效时,计数器的值就减1。当计数器的值为0时,对象就不可能再被使用。
引用计数算法实现简单,判定效率也高。不过,主流的Java虚拟机中并没有使用引用计数算法来管理内存,因为这个算法很难解决对象之间相互循环引用的问题。
可达性分析算法
可达性分析算法的基本思想是通过一系列称为“GC Roots”的对象作为起始点,从这些节点开始向下搜索,搜索所走过的路径称为引用链(Reference Chain),当一个对象到GC Roots没有任何引用链相连时,即从GC Roots到这个对象不可达,就说明这个对象是不可用的。如下图,左面的四个对象都有引用链到GC Roots,因此是可用的;右面的三个对象到GC Roots不可达,所以是不可用的。
在Java中,下面几种对象可以作为GC Roots:
- 虚拟机栈(栈帧中的本地变量表)中引用的对象;
- 方法区中类静态属性引用的对象;
- 方法区中常量引用的对象;
- 本地方法栈中JNI(即Native方法)引用的对象;
其实这两种方法都涉及到了对象的引用,也就是说对象是否是垃圾都与引用有关,因此有必要全面的理解一下Java中的引用。
其实Java中的引用一共有四种。这是JDK 1.2 之后对引用概念的扩充,分别是强引用(Strong Reference)、软引用(Soft Reference)、弱引用(Weak Reference)和虚引用(Phantom Reference),这四种引用的强度依次逐渐减弱。
(1)强引用
强引用就是程序中普遍存在的,类似“Object obj=new Object()”这类的引用,只要强引用还存在,垃圾回收器就不会回收被引用的对象。
(2)软引用
软引用用来描述一些还有用但不是必须的对象。对于软引用关联的对象,在系统将要发生内存溢出异常之前,将会把这些对象列入回收范围进行第二次回收。如果这次回收还没有足够的内存,才会抛出内存溢出异常。SoftReference类来实现软引用。
(3)弱引用
弱引用也用来描述非必须的对象,但是强度比软引用还弱,被引用的对象只能存活到下一次垃圾收集之前。当下一次垃圾收集器工作时,不论内存是否足够,都会回收这些对象。WeakReference类实现了弱引用。
(4)虚引用
虚引用是最弱的一种引用,也叫幽灵引用或幻影引用。一个对象是否有虚引用存在不会对其生存时间产生影响,也无法通过虚引用来取得一个对象实例。虚引用的唯一目的就是当被虚引用关联的对象被收集器收集时收到一个系统通知。PhantomReference类实现了虚引用。
3.1.3回收方法区
除了Java堆,方法区中也存在垃圾收集。只不过这里的收集效率比较低。
方法区,在HotSpot虚拟机中叫永久代,GC收集两部分内容,废弃常量和无用的类。收集废弃常量与收集Java堆中的对象类似。以常量池中字面量的收集为例,假如一个字符串“ABC”已经在常量池中,但是当前系统中没有任何一个String对象是“ABC”,即没有对象引用常量池中的“ABC”,也没有其他地方引用了这个字面量,如果这时发生内存回收,而且必要的话,“ABC”就会被清理出常量池。常量池中的其他类(接口)、方法和字段的符号引用也类似。
不过要判断一个类是否无用就麻烦很多了。要同时满足如下三个条件一个类才是无用的类:
- 该类的所有实例都已经被回收,也就是Java堆中不存在该类的任何的实例;
- 加载该类的ClassLoader已经被回收;
- 该类对应的java.lang.Class对象没有在任何地方被引用,无法在任何地方通过反射访问到该类的方法。
满足上面的三个条件,虚拟机就可以回收。不过,对于HotSpot虚拟机来说,是否回收通过-Xnoclassgc参数来设置。
3.1.4垃圾收集算法(放在3.2)
3.2垃圾收集有哪些算法,各自的特点。
现在我们知道了在哪里收集垃圾以及如何判定一个对象是否是垃圾。接下来就要考虑如何收集垃圾,即垃圾收集算法。不过由于垃圾收集算法涉及到大量的程序细节,所以这里仅仅介绍算法的基本思想及其发展过程。
(1)标记-清除算法
标记-清除(Mark-Sweep)算法是最基础的收集算法,算法名字表明这个算法的垃圾收集过程包括两步:标记和清除。前面介绍的判定垃圾的过程就是标记过程,在标记过后的清除过程中会清理标记为垃圾的对象。后序的垃圾收集算法都是在这个算法的基础上改进而成的。这个算法有两个不足:一个就是标记和清除的效率不高;第二个是空间问题,标记清除后会产生大量不连续的内存碎片,空间碎片太多的话可能导致以后分配大块内存时失败的问题,这样就会触发另一次垃圾收集操作。算法的执行过程如下图:
(2)复制算法
复制算法是为了解决标记-清除算法效率不高的问题的,它将可用内存按照容量分为大小相等的两部分,每次只使用其中的一块。当一块的内存用完了,就将还存活的对象复制到另一块,然后再把已经使用过的内存空间一次性清理掉。这样使得每次是对整个半区进行内存回收,内存分配时也不需要考虑内存碎片的问题,只要移动堆顶指针,按顺序进行分配就好。算法的执行过程如下图:
不过这个算法使得内存只能一半能用,代价太高了。现在的虚拟机都采用这种方法来回收新生代,不过不是1:1分配的,而是将堆内存分为以块较大的Eden空间和两块较小的Survivor空间,每次使用Eden和一个Survivor空间。当回收时,将Eden和Survivor中还存活的对象复制到另一块Survivor中,然后清理Eden和使用过的Survivor空间。HotSpot虚拟机默认的Eden和Survivor比例是8:1,即Eden占堆的80%空间,Survivor占10%的空间,每次只能使用90%的堆空间。
不过,我们并不能保证每次回收只有不多于10%的对象存活,当Survivor空间不够时,需要使用其他内存空间(老年代)进行分配担保,即如果Survivor空间不够,存活的对象直接进入老年代。
(3)标记-整理算法
复制收集算法在对象存活率较高时就需要进行较多的复制操作,效率就会降低。更关键的是,如果不想浪费50%的空间,就需要有额外的空间进行分配担保,以应对被使用的内存中所有对象都存活的极端情况,所以在老年代中一般不使用这种算法。
根据老年代的特点,可以使用另一种标记-整理(Mark-Compact)算法,标记过程和标记-清除算法一样,但后续步骤不是直接对可回收对象进行清理,而是整理存活的对象,将存活的对象都向一端移动,然后直接清理掉边界外的内存。算法的执行过程如下:
这样,也没有了内存碎片的问题。
(4)分代收集算法
现在的虚拟机都使用“分代收集”算法,这种算法只是根据对象的存活周期的不同将内存划分为几块。一般把Java堆空间分为新生代和老年代,这样就可以根据各个年代的特点采用最适合的收集算法。在新生代,每次垃圾收集都会有大量的对象死去,只有少量存活,这样就可以选择复制算法,只需复制少量存活的对象就可以完成垃圾收集。在老年代中,对象的存活率高、没有额外的空间对它进行分配担保,就必须采用标记-清除或标记-整理算法来进行回收。
3.3类加载的过程。
3.4双亲委派模型。
从JVM的角度来看,只存在两种类加载器:
- ****启动类加载器****(Bootstrap ClassLoader):由C++语言实现(针对HotSpot),负责将存放在<JAVA_HOME>\lib目录或-X bootclasspath参数指定的路径中的类库加载到内存中。
- 其他类加载器:由Java语言实现,继承自抽象类ClassLoader。
如:****扩展类加载器****(Extension ClassLoader):负责加载<JAVA_HOME>\lib\ext目录或java.ext.dirs系统变量指定的路径中的所有类库。
****应用程序类加载器****(Application ClassLoader)。负责加载用户类路径(classpath)上的指定类库,我们可以直接使用这个类加载器。一般情况,如果我们没有自定义类加载器默认就是用这个加载器。
3.4.1双亲委派模型
双亲委派模型****工作过程****:
如果一个类加载器收到类加载的请求,它首先不会自己去尝试加载这个类,而是把这个请求委派给父类加载器完成。每个类加载器都是如此,只有当父加载器在自己的搜索范围内找不到指定的类时(即ClassNotFoundException),子加载器才会尝试自己去加载。
简单的说:
某个特定的类加载器在接到加载类的请求时,首先将加载任务委托给父类加载器,依次递归,如果父类加载器可以完成类加载任务,就成功返回;只有父类加载器无法完成此加载任务时,才自己去加载。
3.4.2为什么使用双亲委派模型
简单的来说:一个是安全性,另一个就是性能;(避免重复加载 和 避免核心类被篡改)
用户自定义一个java.lang.String类,该String类具有系统的String类一样的功能,只是在某个函数稍作修改。比如equals函数,这个函数经常使用,如果在这这个函数中,黑客加入一些“病毒代码”。并且通过自定义类加载器加入到JVM中。此时,如果没有双亲委派模型,那么JVM就可能误以为黑客自定义的java.lang.String类是系统的String类,导致“病毒代码”被执行。
而有了双亲委派模型,黑客自定义的java.lang.String类永远都不会被加载进内存。因为首先是最顶端的类加载器加载系统的java.lang.String类,最终自定义的类加载器无法加载java.lang.String类。
3.5有哪些类加载器。
主要有一下四种类加载器:
(1)启动类加载器(Bootstrap ClassLoader)用来加载java核心类库,无法被java程序直接引用。
(2)扩展类加载器(extensions class loader):它用来加载Java的扩展库。Java虚拟机的实现会提供一个扩展库目录。
该类加载器在此目录里面查找并加载 Java类。
(3)系统类加载器(system class loader)也叫应用类加载器:它根据Java应用的类路径(CLASSPATH)来加载Java类。一般来说,Java应用的类都是由它来完成加载的。可以通过ClassLoader.getSystemClassLoader0来获取它。
(4)用户自定义类加载器,通过继承 java.lang.ClassLoader类的方式实现。
3.6能不能自己写一个类叫java.lang.String。
可以写,但不建议。双亲委派机制,还是先逐级向上加载父类加载器,不会加载你自己写的类,保证系统加载的安全性。
4)设计模式相关问题(必问):
4.1先问你熟悉哪些设计模式
4.2然后再具体问你某个设计模式具体实现和相关扩展问题。
5)数据库相关问题,针对Mysql(必问):
-
给题目让你手写SQL。
-
有没有SQL优化经验。
-
Mysql索引的数据结构。
-
SQL怎么进行优化。
5.5SQL关键字的执行顺序。
5.6有哪几种索引。
索引类型
1.普通索引
是最基本的索引,它没有任何限制。它有以下几种创建方式:
(1)直接创建索引
CREATE INDEX index_name ON table(column(length))
(2)修改表结构的方式添加索引
ALTER TABLE table_name ADD INDEX index_name ON (column(length))
(3)创建表的时候同时创建索引
CREATE TABLE table
( id
int(11) NOT NULL AUTO_INCREMENT , title
char(255) CHARACTER NOT NULL , content
text CHARACTER NULL , time
int(10) NULL DEFAULT NULL , PRIMARY KEY (id
), INDEX index_name (title(length)) )
(4)删除索引
DROP INDEX index_name ON table
2.唯一索引
与前面的普通索引类似,不同的就是:索引列的值必须唯一,但允许有空值。如果是组合索引,则列值的组合必须唯一。它有以下几种创建方式:
(1)创建唯一索引
CREATE UNIQUE INDEX indexName ON table(column(length))
(2)修改表结构
ALTER TABLE table_name ADD UNIQUE indexName ON (column(length))
(3)创建表的时候直接指定
CREATE TABLE table
( id
int(11) NOT NULL AUTO_INCREMENT , title
char(255) CHARACTER NOT NULL , content
text CHARACTER NULL , time
int(10) NULL DEFAULT NULL , UNIQUE indexName (title(length)) );
3.主键索引
是一种特殊的唯一索引,一个表只能有一个主键,不允许有空值。一般是在建表的时候同时创建主键索引:
CREATE TABLE table
( id
int(11) NOT NULL AUTO_INCREMENT , title
char(255) NOT NULL , PRIMARY KEY (id
) );
4.组合索引
指多个字段上创建的索引,只有在查询条件中使用了创建索引时的第一个字段,索引才会被使用。使用组合索引时遵循最左前缀集合
ALTER TABLE table
ADD INDEX name_city_age (name,city,age);
5.全文索引
主要用来查找文本中的关键字,而不是直接与索引中的值相比较。fulltext索引跟其它索引大不相同,它更像是一个搜索引擎,而不是简单的where语句的参数匹配。fulltext索引配合match against操作使用,而不是一般的where语句加like。它可以在create table,alter table ,create index使用,不过目前只有char、varchar,text 列上可以创建全文索引。值得一提的是,在数据量较大时候,现将数据放入一个没有全局索引的表中,然后再用CREATE index创建fulltext索引,要比先为一张表建立fulltext然后再将数据写入的速度快很多。
(1)创建表的适合添加全文索引
CREATE TABLE table
( id
int(11) NOT NULL AUTO_INCREMENT , title
char(255) CHARACTER NOT NULL , content
text CHARACTER NULL , time
int(10) NULL DEFAULT NULL , PRIMARY KEY (id
), FULLTEXT (content) );
(2)修改表结构添加全文索引
ALTER TABLE article ADD FULLTEXT index_content(content)
(3)直接创建索引
CREATE FULLTEXT INDEX index_content ON article(content)
5.6什么时候该(不该)建索引。
哪些情况下需要创建索引:
- 主键自动建立唯一索引。
- 频繁作为查询条件的字段应该创建索引。
- 查询中与其他表关联的字段,外键关系建立索引。
- 单键/组合索引的选择问题,组合索引性价比更高。
- 查询中排序的字段,排序字段若通过索引去访问将大大提高排序速度。
- 查询中统计或者分组字段。
哪些情况不适合建索引:
-
表记录太少。
-
经常增删改的表或者字段。
-
Where条件里用不到的字段不创建索引。
-
过滤性不好的不适合建索引。
-
Explain包含哪些列。
6)框架相关问题:
6.1Hibernate和Mybatis的区别。
6.2Spring MVC和Struts2的区别。
6.3Spring用了哪些设计模式。
工厂设计模式
Spring使用工厂模式可以通过 BeanFactory
或 ApplicationContext
创建 bean 对象。
两者对比:
BeanFactory
:延迟注入(使用到某个 bean 的时候才会注入),相比于BeanFactory
来说会占用更少的内存,程序启动速度更快。ApplicationContext
:容器启动的时候,不管你用没用到,一次性创建所有 bean 。BeanFactory
仅提供了最基本的依赖注入支持,ApplicationContext
扩展了BeanFactory
,除了有BeanFactory
的功能还有额外更多功能,所以一般开发人员使用ApplicationContext
会更多。
单例设计模式
在我们的系统中,有一些对象其实我们只需要一个,比如说:线程池、缓存、对话框、注册表、日志对象、充当打印机、显卡等设备驱动程序的对象。事实上,这一类对象只能有一个实例,如果制造出多个实例就可能会导致一些问题的产生,比如:程序的行为异常、资源使用过量、或者不一致性的结果。
使用单例模式的好处:
- 对于频繁使用的对象,可以省略创建对象所花费的时间,这对于那些重量级对象而言,是非常可观的一笔系统开销;
- 由于 new 操作的次数减少,因而对系统内存的使用频率也会降低,这将减轻 GC 压力,缩短 GC 停顿时间。
Spring 中 bean 的默认作用域就是 singleton(单例)的。
代理设计模式
代理模式在 AOP 中的应用
AOP(Aspect-Oriented Programming:面向切面编程)能够将那些与业务无关,却为业务模块所共同调用的逻辑或责任(例如事务处理、日志管理、权限控制等)封装起来,便于减少系统的重复代码,降低模块间的耦合度,并有利于未来的可拓展性和可维护性。
Spring AOP 就是基于动态代理的,
Spring 中 jdbcTemplate
、hibernateTemplate
等以 Template 结尾的对数据库操作的类,它们就使用到了模板模式。一般情况下,我们都是使用继承的方式来实现模板模式,但是 Spring 并没有使用这种方式,而是使用Callback 模式与模板方法模式配合,既达到了代码复用的效果,同时增加了灵活性。
观察者模式
观察者模式是一种对象行为型模式。它表示的是一种对象与对象之间具有依赖关系,当一个对象发生改变的时候,这个对象所依赖的对象也会做出反应。
适配器模式
适配器模式(Adapter Pattern) 将一个接口转换成客户希望的另一个接口,适配器模式使接口不兼容的那些类可以一起工作,其别名为包装器(Wrapper)。
spring AOP中的适配器模式
我们知道 Spring AOP 的实现是基于代理模式,但是 Spring AOP 的增强或通知(Advice)使用到了适配器模式,与之相关的接口是AdvisorAdapter
。Advice 常用的类型有:BeforeAdvice
(目标方法调用前,前置通知)、AfterAdvice
(目标方法调用后,后置通知)、AfterReturningAdvice
(目标方法执行结束后,return之前)等等。每个类型Advice(通知)都有对应的拦截器:MethodBeforeAdviceInterceptor
、AfterReturningAdviceAdapter
、AfterReturningAdviceInterceptor
。Spring预定义的通知要通过对应的适配器,适配成 MethodInterceptor
接口(方法拦截器)类型的对象(如:MethodBeforeAdviceInterceptor
负责适配 MethodBeforeAdvice
)。
spring MVC中的适配器模式
在Spring MVC中,DispatcherServlet
根据请求信息调用 HandlerMapping
,解析请求对应的 Handler
。解析到对应的 Handler
(也就是我们平常说的 Controller
控制器)后,开始由HandlerAdapter
适配器处理。HandlerAdapter
作为期望接口,具体的适配器实现类用于对目标类进行适配,Controller
作为需要适配的类。
为什么要在 Spring MVC 中使用适配器模式? Spring MVC 中的 Controller
种类众多,不同类型的 Controller
通过不同的方法来对请求进行处理。
装饰者模式
装饰者模式可以动态地给对象添加一些额外的属性或行为。相比于使用继承,装饰者模式更加灵活。简单点儿说就是当我们需要修改原有的功能,但我们又不愿直接去修改原有的代码时,设计一个Decorator套在原有代码外面。其实在 JDK 中就有很多地方用到了装饰者模式,比如 InputStream
家族,InputStream
类下有 FileInputStream
(读取文件)、BufferedInputStream
(增加缓存,使读取文件速度大大提升)等子类都在不修改InputStream
代码的情况下扩展了它的功能。
6.4Spring中AOP主要用来做什么。
1.Spring声明式事务管理配置。
2.Controller层的参数校验。
3.使用Spring AOP实现MySQL数据库读写分离案例分析
4.在执行方法前,判断是否具有权限。
5.对部分函数的调用进行日志记录。监控部分重要函数,若抛出指定的异常,可以以短信或邮件方式通知相关人员。
6.信息过滤,页面转发等等功能
6.5Spring注入bean的方式。
6.6什么是IOC,什么是依赖注入。
6.7Spring是单例还是多例,怎么修改。
6.8Spring事务隔离级别和传播性。
6.9介绍下Mybatis/Hibernate的缓存机制。
Hibernate与MyBatis都可以是通过SessionFactoryBuider由XML配置文件生成SessionFactory,然后由SessionFactory 生成Session,最后由Session来开启执行事务和SQL语句。
而MyBatis的优势是MyBatis可以进行更为细致的SQL优化,可以减少查询字段,并且容易掌握。
Hibernate的优势是DAO层开发比MyBatis简单,Mybatis需要维护SQL和结果映射。数据库移植性很好,MyBatis的数据库移植性不好,不同的数据库需要写不同SQL。有更好的二级缓存机制,可以使用第三方缓存。MyBatis本身提供的缓存机制不佳。
6.10Mybatis的mapper文件中#和$的区别。
6.11Mybatis的mapper文件中resultType和resultMap的区别。
7)其他遇到问题:
7.1介绍下栈和队列。
7.2IO和NIO的区别。
Java是一种动态链接的语言,常量池的作用非常重要,常量池中除了包含代码中所定义的各种基本类型(如int、long等等)和对象型(如String及数组)的常量值外,还包含一些以文本形式出现的符号引用,比如:
类和接口的全限定名;
字段的名称和描述符;
方法的名称和描述符。
在C语言中,如果一个程序要调用其它库中的函数,在链接时,该函数在库中的位置(即相对于库文件开头的偏移量)会被写在程序中,在运行时,直接去这个地址调用函数;
而在Java语言中不是这样,一切都是动态的。编译时,如果发现对其它类方法的调用或者对其它类字段的引用的语句,记录进class文件中的只能是一个文本形式的符号引用,在连接过程中,虚拟机根据这个文本信息去查找对应的方法或字段。
所以,与Java语言中的所谓“常量”不同,class文件中的“常量”内容很丰富,这些常量集中在class中的一个区域存放,一个紧接着一个,这里就称为“常量池”。
-
==和equals的区别。
-
重载和重写的区别。
-
String和StringBuilder、StringBuffer的区别。
7.6静态变量、实例变量、局部变量线程安全吗,为什么。
*静态变量:线程非安全。*
静态变量即类变量,位于方法区,为所有对象共享,共享一份内存,一旦静态变量被修改,其他对象均对修改可见,故线程非安全。
*实例变量:单例模式(只有一个对象实例存在)线程非安全,非单例线程安全。*
实例变量为对象实例私有,在虚拟机的堆中分配,若在系统中只存在一个此对象的实例,在多线程环境下,“犹如”静态变量那样,被某个线程修改后,其他线程对修改均可见,故线程非安全;如果每个线程执行都是在不同的对象中,那对象与对象之间的实例变量的修改将互不影响,故线程安全。
*局部变量:线程安全。*
每个线程执行时将会把局部变量放在各自栈帧的工作内存中,线程间不共享,故不存在线程安全问题。
7.7try、catch、finally都有return语句时执行哪个。
任何执行try 或者catch中的return语句之前,都会先执行finally语句,如果finally存在的话。
如果finally中有return语句,那么程序就return了,所以finally中的return是一定会被return的,
编译器把finally中的return实现为一个warning。
7.8介绍下B树、二叉树。
7.9ajax的4个字母分别是什么意思。
Asynchronous JavaScript and XML 的缩写,异步的JavaScript和XML。在不重新加载整个页面的情况下 ,AJAX 与服务器交换数据并更新部分网页。
7.10xml全称是什么。
XML英文全称为Extensible Markup Language,可扩展标记语言。主要用于保存和处理数据同时,保存和处理数据之间的关系。XML的实质是一段字符串,根据这一特点,XML具有跨平台,跨语言特性。
7.11分布式锁的实现。
基于数据库实现分布式锁;
基于缓存(Redis等)实现分布式锁;
基于Zookeeper实现分布式锁;
7.12分布式session存储解决方案。
方案一:客户端存储
直接将信息存储在cookie中
cookie是存储在客户端上的一小段数据,客户端通过http协议和服务器进行cookie交互,通常用来存储一些不敏感信息
缺点:
- 数据存储在客户端,存在安全隐患
- cookie存储大小、类型存在限制
- 数据存储在cookie中,如果一次请求cookie过大,会给网络增加更大的开销
方案二:session复制
session复制是小型企业应用使用较多的一种服务器集群session管理机制,在真正的开发使用的并不是很多,通过对web服务器(例如Tomcat)进行搭建集群。
存在的问题:
- session同步的原理是在同一个局域网里面通过发送广播来异步同步session的,一旦服务器多了,并发上来了,session需要同步的数据量就大了,需要将其他服务器上的session全部同步到本服务器上,会带来一定的网路开销,在用户量特别大的时候,会出现内存不足的情况
优点:
- 服务器之间的session信息都是同步的,任何一台服务器宕机的时候不会影响另外服务器中session的状态,配置相对简单
- Tomcat内部已经支持分布式架构开发管理机制,可以对tomcat修改配置来支持session复制,在集群中的几台服务器之间同步session对象,使每台服务器上都保存了所有用户的session信息,这样任何一台本机宕机都不会导致session数据的丢失,而服务器使用session时,也只需要在本机获取即可
如何配置:
在Tomcat安装目录下的config目录中的server.xml文件中,将注释打开,tomcat必须在同一个网关内,要不然收不到广播,同步不了session
在web.xml中开启session复制:<distributable/>
方案三:session绑定:
Nginx介绍:
Nginx是一款自由的、开源的、高性能的http服务器和反向代理服务器
Nginx能做什么:
反向代理、负载均衡、http服务器(动静代理)、正向代理
如何使用nginx进行session绑定
我们利用nginx的反向代理和负载均衡,之前是客户端会被分配到其中一台服务器进行处理,具体分配到哪台服务器进行处理还得看服务器的负载均衡算法(轮询、随机、ip-hash、权重等),但是我们可以基于nginx的ip-hash策略,可以对客户端和服务器进行绑定,同一个客户端就只能访问该服务器,无论客户端发送多少次请求都被同一个服务器处理
在nginx安装目录下的conf目录中的nginx.conf文件
upstream aaa {
Ip_hash;
server 39.105.59.4:8080;
Server 39.105.59.4:8081;
}
server {
listen 80;
server_name www.wanyingjing.cn;
#root /usr/local/nginx/html;
#index index.html index.htm;
location / {
proxy_pass http:39.105.59.4;
index index.html index.htm;
}
}
123456789101112131415
缺点:
- 容易造成单点故障,如果有一台服务器宕机,那么该台服务器上的session信息将会丢失
- 前端不能有负载均衡,如果有,session绑定将会出问题
优点:
- 配置简单
方案四:基于redis存储session方案
基于redis存储session方案流程示意图
引入pom依赖:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-session-data-redis</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-data-starter-redis</artifactId>
</dependency>
12345678
配置redis
#redis数据库索引(默认是0)
spring.redis.database=0
spring.redis.host=127.0.0.1
spring.redis.port=6379
#默认密码为空
spring.redis.password=
#连接池最大连接数(负数表示没有限制)
spring.redis.jedis.pool.max-active=1000
#连接池最大阻塞等待时间(负数表示没有限制)
spring.redis.jedis.pool.max-wait=-1ms
#连接池中的最大空闲连接
spring.redis.jedis.pool.max-idle=10
#连接池中的最小空闲连接
spring.redis.jedis.pool.min-idle=2
#连接超时时间(毫秒)
spring.redis.timeout=500ms
12345678910111213141516
优点:
- 这是企业中使用的最多的一种方式
- spring为我们封装好了spring-session,直接引入依赖即可
- 数据保存在redis中,无缝接入,不存在任何安全隐患
- redis自身可做集群,搭建主从,同时方便管理