文章目录
- 面试技巧
- 1.JavaSE
- 1.1 集合(Collection,Set,List,Map)
- 1.1.1 HashMap和HashTable的区别
- 1.1.2 HashMap的put()和get()方法是怎么实现的
- 1.1.3 HashMap与HashSet的区别
- 1.1.4 HashMap的数据结构,线程是否安全,为什么?
- 1.1.5 HashMap的实现原理
- 1.1.6 JDK1.8的HashMap有哪些优化
- 1.1.7 ArrayList和LinkedList有什么区别
- 1.1.8 ArrayList去重
- 1.1.9 Set集合去重后会存在哪些问题?
- 1.1.10 HashMap和HashTable与ConcurrentMap的区别
- 1.1.11 String,StringBuilder,StringBuffer的区别
- 1.1.12 JAVA集合框架中的常用集合及其特点、适用场景、实现原理简介
- 1.1.13 Java8的新特性
- 1.1.14 解决hash碰撞
- 1.1.15 TreeMap和TreeSet在排序时如何比较元素,Collections工具类中的sort()方法如何比较元素?
- 1.2 线程
- 1.3 基础
- 2. Spring框架
- 3. HTTP/网络
- 4. 数据库
- 5. MyBatis
面试技巧
1. 自我介绍
- 基本信息(姓名,年龄,学历,学校,专业)
- 工作经验(工作年限,项目经验)
- 技术栈(结合做过的项目)
- 其他(特长,爱好)
2. 面试技巧
- 不能一问一答,主动找话题,将话题引向自己擅长的领域,技术。
- 如果答不上来问题,组织一下语言,重新说,实在想不起来就说自己忘了,不能支支吾吾。
- 听完问题再回答,如果没有听清楚,适当反问题目意思。
- 声音洪亮,自信。
- 回答问题:what是什么,how怎么用,why原理。
3. 注意事项
- 刷题,写简历,投简历,面试。
- 面试前,查询好去公司的路线,安排好时间。
- 认真阅读公司招聘需求,恶补需求技术点,不会的技术也要去了解。
1.JavaSE
1.1 集合(Collection,Set,List,Map)
1.1.1 HashMap和HashTable的区别
- 共同点:
- HashMap和HashTable都实现了Map接口,并且都是key-value的数据结构。
- 不同点
- HashMap是java1.2引进的Map接口的实现类,而HashTable是java1.1的继承自Dictionary的一个类
- HashMap是线程不安全的,HashTable是线程安全的(也称作线程同步)
- HashMap允许将null值作为key或value,而HashTable不允许,如果存储null的话会报空指针异常
- 使用ConcurrentMap兼顾线程安全和效率
1.1.2 HashMap的put()和get()方法是怎么实现的
- HashMap的put()方法实现:
2. 通过传递key-value数据时调用put方法的时候,HashMap使用key的hashcode()方法(使用了hashcode()方法此方法增加了高位运算防止hash冲突用来重新计算hash值)和哈希算法来找出存储key-value对的索引,如果索引处为空,则直接将数据插入到对应的数组中,否则判断是否是红黑树。若是,则以红黑树插入,否则遍历链表。若长度不小于8,则将链表转换为红黑树,转换成功后再插入。新加入的数据放到链表头,最先加入的数据放到链表尾。 - HashMap的get()方法的实现:
- 根据
key
的hashcode
算出元素在数组中的下标,之后遍历Entry
对象链表,直到找到元素为止。 - 如果使用对象作为key,需要重写key的hashcode()和equals()方法
- 如果出现了大量
hash冲突
,那么遍历链表的时候,会比较慢。JDK 1.8
里面,当链表的长度大于阀值(默认为8)的时候,会使用红黑树来存储数据,以便加快key
的查询速度。
- 根据
1.1.3 HashMap与HashSet的区别
HashMap不允许重复的键,HashSet不允许存储重复的值(对象)
- HashSet底层调用的是HashMap
- HashMap实现Map接口,HashSet实现Set接口
- HashMap存储key-value格式的数据,HashSet只能存储对象
- HashMap使用put()方法将元素存入map,HashSet使用add()方法将数据存入set
- HashMap中使用key来计算hashcode值,HashSet使用成员对象来计算hashcode值,使用equals()方法判断对象的相等性,如果两个对象不同返回false
- HashMap速度快因为其使用的是唯一的key来获取对象,HashSet较慢
1.1.4 HashMap的数据结构,线程是否安全,为什么?
-
HashMap数据结构
- JDK1.8以前是数组+链表
- JDK1.8以后是数组+链表+红黑树
-
线程不安全,可能造成死循环,具体表现链表的循环指向,应该使用ConcurrentHashMap,它是线程安全的。
1.1.5 HashMap的实现原理
- HashMap的主干是一个Entry数组,Entry数组时HashMap的基本组成单元。每一个Entry包含一个key-value键值对。
- HashMap由数组+链表+红黑树组成,数组是HashMap的主体,链表是为了解决hash冲突而存在的。
- 如果定位到的数组位置不含链表(当前entry的next指向null)那么对于查找,添加操作很快,仅需一次寻址即可;
- 如果定位的数组包含链表,对于添加操作,其时间复杂度为O(n),首先遍历链表,存在即覆盖,否则新增;对于查找操作来讲,仍需遍历链表,然后通过key对象的equals()方法注意比对查找。所以,出于对性能的考虑,HashMap中的链表出现的越少,性能越好。
1.1.6 JDK1.8的HashMap有哪些优化
- JDK1.8之前的HashMap的数据结构是数组+链表,JDK1.8之后HashMap的数据结构是数组+链表+红黑树
- 当一个链表太长的时候(链表长度大于8时),HashMap会动态的将它替换成一个红黑树,这话的话会将时间复杂度从O(n)降为O(logn)。
- 利用红黑树快速增删改查的特点提高了HashMap的性能。
1.1.7 ArrayList和LinkedList有什么区别
- ArrayList的底层是数组,查询速度快,而LinkedList底层是一个双向链表,增删速度快
- ArrayList允许重复的元素存储,而LinkedList不允许存储重复的元素。
1.1.8 ArrayList去重
- 利用HashSet去重(不保证去重后顺序一定)
- 利用LinkedList去重(去重后顺序一定)
1.1.9 Set集合去重后会存在哪些问题?
- 经过Set去重后,元素列表原始顺序被打乱,需要经过排序后才能与原来的元素列表比对。
- LinkedHashSet是有序的HashSet(按照存入集合的顺序打印)
1.1.10 HashMap和HashTable与ConcurrentMap的区别
- HashMap是线程不安全的,HashTable,ConcurrentMap是线程安全的
- HashMap的键和值都允许有null值都存在,而HashTable不允许
- HashMap的效率相对HashTable和ConcurrentMap较高
- HashMap根据键的hashCode值存储数据,大多数情况下可以直接定位到它的值,因而具有很快的访问速度,但遍历的顺序是不确定的
- HashMap最多只允许存储一条键为null的记录,多条值为null
- HashMap是非线程安全,如果为了满足线程安全,可以使用Collections的synchronizedMap,或者使用ConcurrentMap
1.1.11 String,StringBuilder,StringBuffer的区别
- 线程安全相关:
- StringBuilder线程不安全,StringBuffer线程安全。因为StringBuffer的所有公开方法都是
synchronized
修饰的,而StringBuilder并没有synchronized
修饰。String也是线程安全的,因为它的底层是使用final
关键字修饰的char[]
数组。 - 因为StringBuilder是线程不安全的,速度较String,StringBuffer快
- 使用场景:
- String类适用于操作较少字符串。
- StringBuilder类适用于操作单线程下在字符缓冲区进行大量字符串操作。
- StringBufffer类适用于操作多线程下在字符缓冲区进行大量字符串操作。
- String为什么要使用final来修饰(为什么底层是不可变)
- 主要是为了“效率” 和 “安全性” 的缘故。若 String允许被继承, 由于它的高度被使用率, 可能会降低程序的性能,所以String被定义成final。
- StringBuilder和StringBuffer继承自AbstractStringBuilder,而它没有使用final修饰,所以是可变的
1.1.12 JAVA集合框架中的常用集合及其特点、适用场景、实现原理简介
Collectin接口
定义了包含了一批对象的集合,存储数据的格式是value格式
List接口
继承自Collection,用于定义以列表形式存储的集合。
List接口常用类:
- ArrayList:ArrayList的底层是数组,查询速度快,允许重复的元素存储。
- LinkedList:LinkedList底层是一个双向链表,增删速度快,不允许存储重复的元素。可以用来进行去重操作,去重之后的list不保证元素的存放顺序。
Map接口
Map接口在Collection的基础上,为每个value对象指定了一个key,并使用Entry保存每个Key-Value键值对。通过key快速定位到对象(value)
1.1.13 Java8的新特性
- Lambda表达式
- 方法引用
- 函数式接口
- 默认方法
- 新的编译工具
- Steam API
- Option类
- Date Time API
- Nashorn JavaScript引擎
- 内置Base64编码
1.1.14 解决hash碰撞
- 开放地址法
- 再hash法
- 链地址法(拉链法)
- 建立一个公共溢出区
1.1.15 TreeMap和TreeSet在排序时如何比较元素,Collections工具类中的sort()方法如何比较元素?
TreeMap
TreeMap要求存放的键值对的键的类必须实现Comparable接口,重写compareTo()方法从而根据键对元素进行排序。
TreeSet
- 自然排序TreeSet要求存放的对象的类必须实现Comparable接口,重写compareTo(T o)方法进行排序。
- 比较器Comparator根据TreeSet构造器传入的比较容器中重写的compare(T o1,T o2)方法
Collections工具类中的sort()方法比较元素
- 使用自定义类型的集合进行排序
- 使用自然排序compareTo()方法
- 比较器Comparator的compare(T o1,T o2)方法。根据传入的比较值判断比较结果。
- 使用包装类的compareTo()方法。
1.2 线程
1.2.1 创建线程有几种方式?
- 实现
Runnable
接口来创建线程。多线程 - 实现
Callable
接口来创建线程。 - 使用
FutureTask
类来创建线程,与Callable
一样可以实现有返回值的线程创建,并且可以抛出异常。 - 通过继承
Thread
类来创建一个线程。单线程 - 创建线程池,使用
ThreadPoolExecutor
或Executors
。
1.2.2 Thread中的run()和start()方法有什么区别?
多线程
-
run()方法被调用时,会被当做一个普通的函数调用,程序中只有主线程,它是同步的,只调用run()方法无法达到多线程的目的
-
start()方法被调用时,线程处于就绪状态,可以被JVM来调度执行,在调度过程中,JVM通过调用线程类的run()方法来完成实际的业务逻辑,start()方法能够异步调用run()方法(无需等待run()方法中的代码执行完毕),所以start()方法可以实现多线程。
-
实现
Runnable
才是真正的多线程。
1.2.3 synchronized和Lock有什么区别?
类别 | synchronized | Lock |
---|---|---|
存在层次 | Java的关键字,基于JVM。 | 一个接口。 |
锁的释放 | 1,加锁的线程执行完代码后释放锁。2,线程执行中发生异常,JVM会让然线程释放锁。 | 使用Lock锁发生异常时,不会主动释放占有的锁,必须手动调用unlock()方法释放锁,一般都会用try-catch-finally结构,避免死锁的发生。 |
锁的获取 | 假设A线程获得锁,B线程等待。如果A线程阻塞,B线程会一直等待。 | 分情况而定,Lock锁有多个获取锁的方式,获得锁后,线程可以不用一直等待。 |
锁状态 | 无法判断。 | 可以判断。可以通过tryLock()方法判断是否获取了锁。 |
锁类型 | 可重入,不可中断,非公平。 | 可重入,可判断,公平。 |
性能 | 少量同步。 | 1,大量同步。大量资源同时竞争时(大量线程),Lock锁的性能要远远优于synchronized。2,Lock可以提高多个线程进行读操作的效率,可以通过ReadWriteLock接口实现读写分离。 |
是否响应中断 | 只能等待锁的释放,不能响应中断。 | Lock等待锁的过程中可以使用interrupt来中断等待 |
调度机制 | synchronized使用Object对象本身的wait,notify,notifyAll调度机制。 | Lock可以使用Condition进行线程之间的调度。 |
synchronized和Lock用途区别:
synchronized和ReentrantLock在一般情况下没有什么区别,但是在非常复杂的同步应用中,应该使用ReentrantLock
- 某个线程在等待一个锁的控制权的这段时间需要中断。
- 需要分开处理一些wait-notify,ReentrantLock里面的Condition应用,能够控制notify()具体的线程。
- 具有公平锁的功能,每个到来的线程都需要排队等候。
synchronized和Lock的性能区别
- synchronized是关键字,基于JVM。Lock锁是使用Java代码编写的控制锁的代码。
- 在Java1.5中,synchronized是性能低效的,以为这是一个重量级操作,需要调用操作接口,导致有可能加锁消耗的系统时间比加锁以外的操作还多,相比之下使用Java提供的Lock对象,性能更高。
- 在Java1.6中,synchronized在语义上很清晰,可以进行很多优化,有自适应自旋,锁消除,锁粗化,轻量级锁,偏向锁等等。导致Java1.6以上的synchronized的性能并不比Lock差。
Synchronized和Lock机制的区别
- synchronized原始采用的是CPU悲观锁,即获得线程的是独占锁。独占锁意味着其他线程只能依靠阻塞来等待线程释放锁。而在CPU转换线程阻塞时,会引起线程上下文切换,当有很多线程竞争锁的时候,会引起CPU频繁的上下文切换导致效率很低。
- 而Lock用得是乐观锁的方式。所谓乐观锁就是每次不加锁,而是假设没有冲突而去完成某项操作,如果因为冲突失败就重试,直到成功为止。乐观锁实现的机制就是CAS操作(Compare And Swap),一个线程的失败或挂起不应该影响其他线程的失败或挂起的算法。
1.2.4 乐观锁和悲观锁
悲观锁
-
当我们要对数据库中的一条数据进行修改的时候,为了避免同时被其他人修改,最好的办法就是直接对该数据尽心加锁以防止并发。这种借助数据库锁机制,在修改数据之前先锁定,再修改的方式被称为悲观并发控制(Pessimistic Concurrency Control)缩写PCC,又称悲观锁
-
**悲观锁具有强烈的独占和排他特性。**它指的是对数据被外界修改持保守态度,因此,在整个数据处理过程中,将数据处于锁定状态。
-
悲观锁的实现,往往依靠数据库提供的锁机制。
-
悲观锁是一种对数据的修改持有悲观态度的并发控制方式。总是假设最坏的情况,每次读取数据的时候默认其他线程会更改数据,因此需要进行加锁操作,当其他线程想要访问数据时,都需要阻塞挂起。悲观锁的实现:
- 传统的关系型数据库使用这种锁机制,比如行锁,表锁,写锁,都是在做操作之前先上锁。
- Java里面的锁使用
synchronized
关键字实现。
-
悲观锁主要分为共享锁和排它锁
- 共享锁(shared locks)又称为读锁,简称S锁。顾名思义,共享锁就是多个事务对同一数据可以共享同一把锁,都能访问到数据,只能读。
- 排它锁(exclusive locks)又称为写锁,简称X锁。排它锁就是不能与其它锁并存,如果一个事务获取了一个数据行的排它锁,其他事务就不能再获取该行的其他锁,包括共享锁和排它锁,但是获取排它锁的事务可以对数据进行读和写。
-
悲观锁说明:
悲观并发控制实际上是“先取锁再访问”的保守策略,为数据处理的安全提供了保证。但是在效率方面,处理加锁的机制会让数据产生额外的开销,还有增加产生死锁的可能。另外还有降低并行性。一个事务如果锁定了某行数据,其他事务就必须等待该事务处理完成后才可以处理被锁的数据。
乐观锁
-
乐观锁是相对于悲观锁而言,乐观锁**假设数据一般情况下不会造成冲突,所以在数据进行提交更新的时候,才会正式对数据的冲突与否进行检测,如果发现冲突了,则返回给用户错误的信息,让用户决定如何去做。**乐观锁适用于读操作多的场景,这样可以提高程序的吞吐量。
-
乐观锁避免了数据库幻读,业务处理时间过长等原因引起数据处理错误的一种机制,但乐观锁不会刻意使用数据库本身的锁机制,而是依据数据本身来保证数据的正确性。乐观锁的实现:
- CAS实现:Java自带类的原子变量使用了乐观锁的一种CAS实现方式。
- 版本号控制:一般是在数据表中加上一个数据版本号version字段,表示数据被修改的次数。当数据被修改时,version值会+1。当线程A需要更新数据值时,在读取数据的同时也会读取version值,在提交更新时,若刚才读取到的version值与当前数据库中的version值相等时才更新,否则重试更新操作,直到更新成功。
-
乐观锁说明:
乐观并发控制相信事务之间的数据竞争(data race)的概率是比较小的,因此尽可能直接执行下去,直到提交的时候才去锁定,所以不会产生任何锁和死锁。
1.2.5 Thread和Runnable的区别
- Thread类实现了Runnable接口。
- Runnable可以实现多个相同的程序代码的线程共享同一资源,而Thread不适合,因为Thread源码内部向下转型为Runnable,Thread内部资源共享依然是以Runnable的形式去实现资源共享。
- Runnable不能直接调用run()方法,调用run()方法并未启动新的线程,而是启动了一个普通函数而已。
Runnable和Thread相比优点有:
- 由于Java不允许多继承,因此实现了Runnable接口还可以再继承其他类,但是Thread不行。
- Runnable可以实现多个相同的程序代码的线程去共享统一资源,而Thread不适合。
1.2.6 sleep()和wait()的区别
sleep()
是Thread类的方法,wait()
方法是Object类的方法。sleep()
方法没有释放锁,wait()
方法释放了锁。sleep()
可以在任何地方使用,wait()
只能在同步控制方法或同步控制代码块中使用(使用synchronized修饰)。sleep()
可以指定时间唤醒线程,如果没到指定时间,可以使用interrupt()
强行唤醒线程。wait()
会等其他线程调用notify()/notifyAll()
唤醒等待池中的所有线程才会进入就绪队列,等待系统分配资源。- 使用
sleep()
必须捕获异常,使用wait()
不用捕获异常。
sleep()
和wait()
的作用
sleep()
的作用是让线程休眠指定的时间,在时间到达时恢复,也就是说sleep将在时间到达后恢复线程执行
wait()
会将调用者的线程挂起,直到其他线程调用同一个对象的notify()方法才会重新激活调用者。
1.2.7 synchronized加方法上锁对象是什么,加静态方法上锁是什么
synchronized加普通方法上是锁住该类的实例对象(多个),new 创建对象能创建多个。
synchronized加在静态方法上是锁住该类的.class对象(唯一)。
1.2.8 synchronized(this)和synchronized(User.class)的区别
对象锁和类锁
synchronized(this)对象锁
在Java中每个对象都有一个monitor对象,这个对象就是Java对象的锁,通常被称为内置锁或对象锁。类的对象可以有多个,所以每个对象都有其独立的对象锁,互不干扰。
synchronized(*.class)类锁
在Java中,针对每个类也有一个锁,可以称为类锁,类锁实际上是通过对象锁实现的,即类的Class对象锁。每个类只有一个Class对象,所以每个类只有一个类锁。
1.2.9 synchronized和volatitle关键字的区别
Java多线程中的原子性,可见性,有序性
- 原子性:是指多线程的多个操作是一个整体,不能被分割,要么就不执行,要么就全部执行完,中间不能被打断。
- 可见性:是指线程之间的可见性,就是一个线程修改后的结果,其他的线程能够立马知道。
- 有序性:为了提高执行效率,Java中的编译器和处理器可以对指令进行重新排序,重新排序会影响多线程并发的正确性,有序性就是要保证不进行重新排序(保证线程操作的执行顺序)。
synchronized
该关键字提供了一种同步锁,被修饰的代码块可以防止被多个线程同时运行,代码块运行时,相当于单线程操作,故而保证原子性,可见性和有序性。
volatile
volatile只保证可见性和有序性,被volatile修饰的共享变量必须在修改后及时刷新到主存中,并且禁止指令重新排序,故保证可见性和有序性。
synchronized和volatile的区别
- synchronized可以用在变量、方法、类、同步代码块中,使用范围广泛。而volatile只能作用于变量。
- synchronized可以保证线程的原子性,可见性,有序性。而volatile只能保证可见性和有序性。
- synchronized可能会造成线程阻塞,而volatile不会造成线程阻塞。
1.2.10 线程的5种状态
-
新建状态(New)创建了一个线程对象
-
就绪状态(Runnable)当前线程被其它线程调用start()方法后处于可运行线程池中,等待被线程调度选中,获得cpu的使用权。
-
运行状态(Running)就绪状态的线程获得了cpu的时间片timeslice(cpu使用权)执行程序代码。
-
阻塞状态(Blocked)阻塞状态是指线程因为某些原因放弃了cpu的使用权,让出了cpu的时间片timeslice,暂时停止运行。直到线程进入就绪状态,才有机会获得timeslice进入运行状态。
阻塞的三种情况:
- 等待阻塞:运行状态的线程执行wait()方法,JVM会把该线程放入等待队列(wait queue)中。
- 同步阻塞:运行状态的线程在获取对象的同步锁时,若该同步锁被别的线程占用,则JVM会把该线程放入锁池(Lock Pool)中。
- 其他阻塞:运行的线程执行Thread.sleep()方法时,或使用join()方法,或者发出了I/O请求时,JVM会把该线程设置为阻塞状态。当sleep()状态超时,join()等待线程终止或者超时,或者I/O处理完毕时,线程重新进入就绪状态。
-
死亡状态:线程run(),main()方法执行结束,或者因为异常退出了run()方法,则该线程结束生命周期,死亡的线程不能进入就绪状态。
1.3 基础
1.3.1 int,Integer,AtomicInteger的区别
- int是基本数据类型,Integer是引用数据类型。
- int和Integer之间可以自动拆装箱。
- int的默认值是0,Integer的默认值是null。
- 使用泛型时只能填入包装类Integer,不能填入基本类型int。
- Integer的valueOf()方法默认缓存值是-128~127
- AtomicInteger原子操作类适用于高并发场景下,线程安全。
1.3.2 Java中的四个基本流,处理文本用什么流,处理照片用什么流
Java中的四个基本流是
- InputStream 字节输入流
- OutputStream 字节输出流
- Reader 字符输入流
- Writer 字符输出流
能使用字节流一定能使用字符流,反之字节流能使用字符流处理的文件。
文本用字符流,照片(二进制文件)字节流。
1.3.3 使用反射创建实例的几种方式
- 对象调用getClass()方法。
- 类名.class()的方法。
- 通过class对象的forName()静态方法来获取,使用次数最多。
1.3.4 使用反射调用对象的方法
对象.getClass(),获取Class文件,再调用getMethod()获取Method方法对象,使用方法对象.invoke(obj),调用方法。
2. Spring框架
1.1 Spring
1.1.1 什么是Spring
Spring是一种轻量级框架,旨在提高开发人员的开发效率以及系统的可维护性。我们一般说的Spring框架就是SpringFramework,它是一个模块集合。
1.2 SpringMVC
3. HTTP/网络
3.1 TCP
3.1.1 Http三次握手,四次挥手
三次握手
四次挥手
3.1.2 Session和Cookie的区别
4. 数据库
4.1 MySQL
4.1.1 PrepareStatement和Statement的区别
- PrepareStatement是预编译的,批处理效率高。Statement对象只在一次性存取语句时使用。因为使用Statement对象开销比PrepareStatement大。
- 使用PrepareStatement可以防止SQL注入,如果使用Statement会有SQL注入的风险。
- PrepareStatement为每个参数保留一个
?
作为占位符。
4.1.2 为什么要用数据库连接池,数据库连接池的原理
-
资源重用:
数据库连接得到重用,避免了频繁创建、释放连接引起的大量的性能开销。减少系统资源的消耗的同时提高了系统运行环境的稳定性。(数据库建立连接的线程开销)。
-
更快的系统响应速度:
对于业务请求,可以直接使用连接池中的连接,不必重新创建连接,避免了连接创建的时间开销,缩短系统整体响应时间。
-
新的资源分配手段:
对于多应用共享同一数据库的系统而言,可以在应用层通过数据库连接池的配置,实现数据库连接技术。
-
统一的连接管理:
避免数据库连接泄漏。
原理
- 以空间换时间的思想,系统预先创建多个数据库连接对象,虽然会占用一定的内存空间,但是可以省去后面每次执行SQL的创建和释放连接的时间。
- 常用的数据库连接池:
- C3P0
- DBCP
- Druid(阿里)