1.final关键字
- final修饰类,表示类不可变,不可继承,比如String,不可变性
- final修饰方法,表示该方法不可重写,比如模板方法,可以固定我们的算法
- final修饰变量,这个变量就是常量
注意:
- 修饰的是基本数据类型,这个值本身不能修改
- 修饰的是引用类型,引用的指向不能修改
比如下面代码是可以的
final Student student=new Student(1,"Andy");
student.setAge(18);//注意这个是可以的
如下所示,在栈指向堆内存的对象,student.setAge(18)只是改变堆内存中Age的值,并没有改变引用的指向,所以是可以的。
final Student student=new Student(1,"Andy");
student=new Student();//注意这个是不可以的
如图所示,在堆内存重新new 了一个对象,改变了final的引用是不行的。
2.String,StringBuffer,StringBuilder区别
String跟其他两个类的区别是
- String是final类型,每次声明都是不可变的对象,所以每次操作都会产生新的String对象,然后将指针指向新的String对象。
StringBuffer,StringBuilder都是在原有对象上进行操作
- 所以,如果需要经常改变字符串内容,则建议采用这两者
StringBuffer vs StringBuilder
- 前者是线程安全的,后者是线程不安全的。
- 线程不安全性能更高,优先采用StringBuilder。
- StringBuilder>StringBuffer >String
- StringBuffer 每个方法都加了Synchronized
开发过程中优先采用StringBuilder,性能最高,那么是否有线程安全问题呢?
什么时候我们会考虑线程安全的问题?
- 多线程访问同一资源的时候需要考虑。
开发中,你用StringBuilder来解决什么问题?
字符拼接问题:由上图可以知道每个线程访问会创建一个StringBuilder,所以不会有线程安全的问题,
类似的类有ArrayList,HashMap
StringBuilder sb=new StringBuilder();
sb.append("");//每个线程访问一个StringBuilder
3. 接口和抽象类的区别
这个问题,要分JDK版本来区分回答:
JDK1.8之前:(具体实现)
语法:
- 抽象类:方法可以有抽象的,也可以有非抽象的,有构造器
- 接口:方法都是抽象,属性都是常量,默认有public static final修饰
设计:
- 抽象类:同一类事物的抽取,比如针对Dao层操作的封装,如,BaseDao,BaseServiceImpl
- 接口:通常更像是一种标准的制定,定制系统之间对接的标准
例子:
- 1.单体项目,分层开发,interface作为各层之间的纽带,在controller中注入IUserService,在Service注入IUserDao
- 2.分布式项目,面向服务的开发,抽取服务Service,这个时候,就会产生服务的提供者和服务的消费者两个角色
- 这两个角色之间的纽带,依然是接口
JDK1.8之后:
- 接口里面可以有实现的方法,注意要在方法的声明上加上default或者static(空实现)
最后区分几个概念:
多继承,多重继承,多实现
- 多重继承:A->B->C (爷孙三代的关系)
- 多实现:Person implements IRunable,IEatable(符合多项国际化标准)
- 多继承:接口可以多继承,类只支持单继承
4. 算法题-求N的阶乘
- 什么是递归
- 递归,就是方法内部调用方法自身
- 递归的注意事项:
- 找到规律,编写递归公式
- 找到出口(边界值),让递归有结束边界
注意:如果递归太多层,或者没有正确结束递归,则会出现栈内存溢出Error
- 问题:为什么会出现栈内存溢出,而不是堆内存溢出?
栈帧存放当前方法的变量,当方法反复调用就会不断创建栈帧,最终导致栈内存不足
- 这道题该怎么写?
规律:N!=(n-1)!*n
出口:n == 1或n == 0 return 1;
public static int getResult(int n){
if(n<0){
throw new ValidateException("非法参数");
}
if(n==1 ||n==0){
return 1;
}
return getResult(n-1)*n;
}
5. 算法题-求解菲波那切数列的第N个数是几?
如何实现递归求菲波那切数列第N个数字的值(传说中的不死神兔就是这个问题)
- 数字的规律:1,1,2,3,5,8,13,21…
- 所以,我们可以分析编写如下:
- 规律:每个数等于前两个数之和
- 出口:第一项和第二项都等于1
/*
规律:每个数等于前两个数之和
出口:第一项和第二项都等于1
*/
public static int getFeiBo(int n){
if(n<0){
throw new ValidateException("非法参数");
}
if(n==1||n==2){
return 1;
}else{
return getFeiBo(n-1)+getFeiBo(n-2);
}
}
6. Integer && Int
public class Main {
public static void main(String[] args) {
Integer i1=new Integer(12);
Integer i2=new Integer(12);
//i1,i2是引用类型,两个引用类型的比较,比较的是地址值
System.out.println(i1==i2);//false
Integer i3=126;//基本类型转成引用类型 装箱操作 自动装箱机制
Integer i4=126;//基本类型转成引用类型 装箱操作 自动装箱机制
//反编译
Integer.valueOf(126);
int i5=126;
System.out.println(i3==i4);//true
System.out.println(i3==i5);//true 自动拆箱 数值
Integer i6=128;
Integer i7=128;
int i8=128;
System.out.println(i6==i7);//false
System.out.println(i6==i8);//true 自动拆箱 数值
}
}
Integer.valueOf源码
IntegerCache
IntegerCache.low= -128 IntegerCache.high= 127
如果数值在-128到127之间,就在缓存中取数,所以 i3=i4=i5
public static Integer valueOf(int i) {
if (i >= IntegerCache.low && i <= IntegerCache.high)
return IntegerCache.cache[i + (-IntegerCache.low)];
return new Integer(i);
}
i6,i7 >127,所以开辟了新的内存,如下
Integer i6=128; //实际上等于Integer i6=new Integer(128);
Integer i7=128; //实际上等于Integer i7=new Integer(128);
结论:如果是引用类型比较,看边界
如果是引用类型跟数值比较,看数值
7,方法的重载和重写
- 重载:发生在一个类里面,方法名相同,参数列表不同(混淆点:跟返回类型没关系)
//以下不构成重载
public double add(int a,int b);
public int add(int a,int b);
- 重写:发生在父类子类之间的,方法名相同,参数列表相同
8,List和Set的区别
- List:有序,可重复 ArrayList LinkedList
- set:无序,不可重复 HashSet TreeSet
- 无序!=可排序
- 无序:无序就是加进去的顺序!=输出的顺序
拓展:Collections Vs Collection
Collections:工具类 java工具类命名+s结尾
Collection:集合类
9,谈谈ArrayList和LinkedList的区别
底层数据结构的差异
- ArrayList:数组,连续一块内存空间
- LinkedList:双向链表,不是连续的内存空间
一个常规的结论
- ArrayList,查找快,因为是连续的内存空间,方便寻址,但删除,插入慢,因为需要发生数据迁移。
- LinkedList,查找慢,因为需要通过指针一个个寻找,但删除,插入快,因为只要改变前后节点的指针指向即可。
ArrayList头部插入和中间插入,都要发生数据迁移,比较慢,ArrayList < LinkedList
ArrayList 和 LinkedList 在末尾插入数据效率是差不多的
- 确定要存储1000个对象的信息,ArrayList和LinkedList哪个更有优势?
- ArrayList更有优势,因为ArrayList是数组,只需要存放数据
- LinkedList是双向链表,除了要存储数据外还要存储前后指针,更加消耗内存
10. 说说如何实现IOC容器?知道怎么实现?
使用方式:
1.配置文件的方式
<bean>
<constructor-arg></constructor-arg>
</bean>
public class UserController{
private IUserService userService;
public void setUserService(IUserService userService){
this.userService=userService;
}
}
2.注解的方式
@Controller
public class UserController{
@Autowired
private IUserService userService;
}
@Service
public class UserService implements IUserService{
}
11,如何在双向链表A和B之间插入C?
可以使用伪代码的方式来实现,你的答案是什么?
假设我们定位到了A节点,那么A.next就是B节点,这个是前提
C.pre=A; --C指向A
C.next=A.next; --C指向B
A.next.pre=C; --B指向C
A.next=C; --A指向C
12. 谈谈HashSet的存储原理
HashSet的存储原理或者工作原理,主要是从如何保证唯一性来说起。
这里面主要有3个问题,需要回答?
第一,为什么要采用Hash算法?有什么优势,解决了什么问题?
第二,所谓哈希表是一张什么表?
第三,HashSet如何保证保存对象的唯一性?会经历一个什么样的运算过程?
首先,我们需要明确一点,HashSet底层采用的时HashMap来实现存储,其值作为HashMap的key
public boolean add(E e) {
return map.put(e, PRESENT)==null;
}
第一,为什么要采用Hash算法?有什么优势,解决了什么问题?
解决的问题是唯一性
- 存储数据,底层采用的是数组,
当我们放数据的时候,你如何判断是否唯一?
可以采用遍历的方式,逐个比较,但是这种效率低,尤其是数据很多的情况下
所以,为了解决这个效率低的问题,我们采用新的方式
- 采用hash算法,通过计算存储对象的hashcode,然后再根据数组长度-1做位运算,得到我们存储在数组的哪个下标下,如果此时计算的位置没有其他元素,直接存储,不用比较。
但是随着元素的不断添加,就可能出现哈希冲突,即不同的对象计算出来的hash值是相同的
这个时候,我们就需要比较,才需要使用到equals方法
- 如果equals方法相同,则不插入,如果不相等,则形成链表
第二,所谓哈希表是一张什么表?
- 本质是数组,数组的元素是链表
JDK1.7版本实现
JDK1.8做了优化
- 随着元素不断添加,链表可能会越来越长,会优化为红黑树
13,ArrayList vs Vector
- ArrayList:线程不安全的,效率高,常用
- Vector:线程安全的,效率低
public synchronized void ensureCapacity(int minCapacity) {
if (minCapacity > 0) {
modCount++;
ensureCapacityHelper(minCapacity);
}
}
14, HashTable & HashMap & ConcurrentHashMap
- HashTable是线程安全的,但是效率低
- HashMap是线程不安全的,但效率高
ConcurrentHashMap(分段锁)
- 兼顾了线程安全和效率的问题
- 分析:HashTable锁了整段数据(用户操作是不同的数据段,依然需要等待)
解决方案:把数据分段,执行分段锁(分离锁),核心把锁的范围变小,这样出现并发冲突的概率就变小
在保存的时候,计算所存储的数据是哪一段,只锁当前这一段
注意:分段锁(分离锁)是JDK1.8之前的一种方案,JDK1.8之后做了优化。
总结:
15,开发一个自己的栈,你会怎么写?
答案如下:我们分析JDK里面的Stack源码,会发现其实非常简单
首先,栈的特点是FILO(First In Last Out)
其次,底层的数据结构我们采用数组的方式
来,看几个关键的源码,一目了然
存:push
public E push(E item) {
addElement(item);
return item;
}
public synchronized void addElement(E obj) {
modCount++;
ensureCapacityHelper(elementCount + 1);
elementData[elementCount++] = obj;//底层的实现是数组
}
取:pop:取出最后一个并删除 peak:取出最后一个不删除
public synchronized E pop() {
E obj;
int len = size();
obj = peek();
removeElementAt(len - 1);
return obj;
}
public synchronized E peek() {
int len = size();
if (len == 0)
throw new EmptyStackException();
return elementAt(len - 1);
}
16,谈谈IO流的分类及选择
1,分类
- 按方向分:输入流,输出流
(注意:是站在程序的角度来看方向,输入流用于读取文件,输出流用于写文件)
- 按读取的单位分:字节流,字符流
- 按处理的方式分:节点流,处理流
比如,FileInputStream和BufferedInputStream(后者带有缓存区功能-byte[])
- IO流的4大基类:InputStream,OutputStream,Reader,Writer
2,选择
- 字节流可以读取任何文件
- 读取文本文件的时候:选择字符流(假如有解析文件的内容的需求,比如逐行处理,则采用字符流,比如txt文件)
- 读取二进制文件的时候,选择字节流(视频,音频,doc,ppt)
17, serialVersionUID的作用是什么?
- serialVersionUID:字面意思上是序列化版本号,凡是实现Serializable接口的类都有一个表示序列化版本标识符的静态变量。
serialVersionUID有两种生成方式:
- 采用第一种方式生成的serialVersionUID是1L,例如:
private static final long serialVersionUID = 1L;
- 采用第二种方式生成的serialVersionUID是根据类名,接口名,方法和属性等来生成的,例如:
import java.io.Serializable;
public class Test implements Serializable{
private static final long serialVersionUID = 3959055215634785113L;
....
}
当我们一个实体类中没有显式的定义一个名为serialVersionUID、类型为long的变量时,Java序列化机制会根据编译时的class自动生成一个serialVersionUID作为序列化版本比较,
- 如果没有显示指定serialVersionUID,会自动生成一个。
- 只有同一次编译生成的class才会生成相同的serialVersionUID。
- 但是如果出现需求变动,Bean类发生改变,则会导致反序列化失败。为了不出现这类的问题,所以我们最好还是显式的指定一个serialVersionUID。
18,请描述下java的异常体系
19,罗列常见的5个运行时异常
此类异常,编译时没有提示做异常处理,因此通常此类异常的正确理解应该是逻辑错误
- 算数异常
- 空指针
- 类型转换异常
- 数组越界
- NumberFormatException (数字格式异常,转换失败,比如 a12就会转换失败)
20,罗列常见的5个非运行时异常
- IOException
- SQLException
- FileNotFoundException
- NoSuchFileException
- NoSuchMethodExcpetion
21,throw和throws的区别
- throw,作用于方法内,用于主动抛出异常
- throws,作用于方法声明上,声明该方法有可能会抛出某些异常
针对项目中,异常的处理方式,我们一般采用层层往上抛,最终通过异常处理机制处理
( 展示异常页面,或返回统一的 json 信息 ),自定义异常一般继承 RuntimeException,
我们去看Hibernate 等框架,他们的异常体系都是最终继承自RuntimeException。
返回值是多少?
public static void main(String[] args) {
int result=test();
System.out.println(result);
}
public static int test(){
int i = 1;
try{
i = 1/0;
i++;
}catch (Exception e){
return ++i;
}finally {
return i++;
}
}
执行结果如下
22,创建线程的方式
- 继承Thread
- 实现Runable接口
- 实现Callable接口(可以获取线程执行后的返回值)
但实际后两种,更准确的理解是创建了一个可执行的任务,要采用多线程的方式执行
还需要通过创建Thread对象来执行,比如new Thread( new Runable(){} ).start(),这样的方式来执行
在实际开发中,我们通常采用线程池的方式来完成Thread的创建,更好管理线程资源。
案例1:如何正确启动线程
public static void main(String[] args) {
MyThread thread=new MyThread();
//正确启动线程的方式
//thread.run(); //调用方法并非开启新线程
thread.start();//开启子线程
}
static class MyThread extends Thread{
public void run(){
System.out.println(Thread.currentThread().getName()+": running...");
}
}
案例2:实现Runnable只是创建了一个可执行任务,并不是一个线程
public static void main(String[] args) {
Mytask task=new Mytask();
//task.start();//并不能直接以线程的方式启动
//它表达的是一个任务,需要启动一个线程来执行
new Thread(task).start();
}
static class Mytask implements Runnable{
@Override
public void run() {
System.out.println(Thread.currentThread().getName()+": running....");
}
}
案例3:Runable Vs Callable
class Mytask2 implements Callable<Boolean>{
@Override
public Boolean call() throws Exception {
return null;
}
}
23,请描述线程的生命周期
上述的图有些简略,下面详细说明下,线程共有6种状态:
new,runnable,blocked,waiting,timed waiting,terminated
1,当进入synchronized同步代码块或同步方法时,且没有获取到锁,线程就进入了blocked状态,直到锁被释放,重新进入runnable状态
2,当线程调用wait()或者join时,线程都会进入到waiting状态,当调用notify或notifyAll时,或者join的线程执行结束后,会进入runnable状态
3,当线程调用sleep(time),或者wait(time)时,进入timed waiting状态,
当休眠时间结束后,或者调用notify或notifyAll时会重新runnable状态。
4,程序执行结束,线程进入terminated状态
案例篇
/**
* @author huangguizhao
* 测试线程的状态
*/
public class ThreadStateTest {
public static void main(String[] args) throws InterruptedException {
Thread thread = new Thread(new Task());
System.out.println(thread.getState());//NEW
thread.start();
System.out.println(thread.getState());//RUNNABLE
//保险起见,让当前主线程休眠下
Thread.sleep(10);
System.out.println(thread.getState());//terminated
}
}
class Task implements Runnable{
@Override
public void run() {
for (int i = 0; i < 100; i++) {
System.out.println(i);
}
}
}
public class ThreadStateTest {
public static void main(String[] args) throws InterruptedException {
BlockTask task = new BlockTask();
Thread t1 = new Thread(task);
Thread t2 = new Thread(task);
t1.start();
t2.start();
//从严谨的角度来说,t1线程不一定会先执行,此处是假设t1先执行
System.out.println(t1.getState());//RUNNABLE
System.out.println(t2.getState());//BLOCKED
Thread.sleep(10);
System.out.println(t1.getState());//TIMED_WAITING
Thread.sleep(1000);
System.out.println(t1.getState());//WAITING
}
}
class BlockTask implements Runnable{
@Override
public void run() {
synchronized (this){
//另一个线程会进入block状态
try {
//目的是让线程进入waiting time状态
Thread.sleep(1000);
//进入waiting状态
wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
注意:
blocked,waiting,timed waiting 我们都称为阻塞状态
上述的就绪状态和运行状态,都表现为runnable状态
24,谈谈你对线程安全的理解?
如果这个是面试官直接问你的问题,你会怎么回答?
- 一个专业的描述是,当多个线程访问一个对象时,如果不用进行额外的同步控制或其他的协调操作,调用这个对象的行为都可以获得正确的结果,我们就说这个对象是线程安全的
那么我们如何做到线程安全?
实现线程安全的方式有多种,其中在源码中常见的方式是,采用synchronized关键字给代码块或方法加锁,比如StringBuffer
查看StringBuffer的源码,你会看到是这样的:
那么,我们开发中,如果需要拼接字符串,使用StringBuilder还是StringBuffer?
场景一:
如果是多个线程访问同一个资源,那么就需要上锁,才能保证数据的安全性。
这个时候如果使用的是非线程安全的对象,比如StringBuilder,那么就需要借助外力,给他加synchronized关键字。或者直接使用线程安全的对象StringBuffer
场景二:
如果每个线程访问的是各自的资源,那么就不需要考虑线程安全的问题,所以这个时候,我们可以放心使用非线程安全的对象,比如StringBuilder
比如在方法中,创建对象,来实现字符串的拼接。
看场景,如果我们是在方法中使用,那么建议在方法中创建StringBuilder,这时候相当是每个线程独立占有一个StringBuilder对象,不存在多线程共享一个资源的情况,所以我们可以安心使用,虽然StringBuilder本身不是线程安全的。
什么时候需要考虑线程安全?
1,多个线程访问同一个资源
2,资源是有状态的,比如我们上述讲的字符串拼接,这个时候数据是会有变化的
25,Sleep & Wait的区别
区别1:使用限制
- 使用sleep方法可以让当前线程休眠,时间一到当前线程继续往下执行,在任何地方都能使用,但需要捕获 InterruptedException 异常。
try{
Thread.sleep(3000L);
}catch(InterruptedException e){
e.printStackTrace();
}
- 而使用wait方法则必须放在 synchronized块里面,同样需要捕获
InterruptedException 异常,并且需要获取对象的锁。
synchonized(lock){
try{
lock.wait();
}catch(InterruptedException e){
e.printStackTrace();
}
}
- 而且wait 还需要额外的方法 notify/notifyAll 进行唤醒,它们同样需要放在 synchoronized块
里面,且获取对象的锁
synchonized(lock){
//随机唤醒
lock.notify();
//唤醒全部
lock.notifyAll();
}
当然也可以使用带时间的 wait(long millis) 方法,时间一到,无需其他线程唤醒,也会重新竞争获取对象的锁继续执行。
区别2:使用场景
- sleep 一般用于当前线程休眠,或者轮循暂停操作,wait 则多用于多线程之间的通信。
区别3:所属类
- sleep 是 Thread 类的静态本地方法,wait 则是 Object 类的本地方法。
java.lang.Thread#sleep
public static native void sleep(long millis) throws InterruptedException;
java.lang.Thread#wait
public static native void wait(long timeout) throws InterruptedException;
为什么要这样设计呢?
- 因为 sleep 是让当前线程休眠,不涉及到对象类,也不需要获得对象的锁,所以是线程类的方法。wait是让获得对象锁的线程实现等待,前提是要获得对象的锁,所以是类的方法。
区别4:释放锁
Object lock=new Object();
synchronized(lock){
try{
lock.wait(3000L);
Thread.sleep(2000L);
}catch(InterruptedException e){
e.printStackTrace();
}
}
如上代码所示,wait 可以释放当前线程对 lock 对象锁的持有,而 sleep 则不会。
区别5:线程切换
- sleep 会让出 CPU 执行时间且强制上下文切换,而 wait 则不一定,wait 后可能还是有机会重新竞争到锁继续执行的。
26,谈谈你对ThreadLocal的理解
ThreadLocal解决了什么问题?内部源码是怎么样的?
作用:
为每个线程创建一个副本
实现在线程的上下文传递同一个对象,比如connection
第一个问题:证明ThreadLocal为每个线程创建一个变量副本
public class ThreadLocalTest {
private static ThreadLocal<Long> threadLocal = new ThreadLocal<>();
public static void main(String[] args) throws InterruptedException {
//开启多个线程来执行任务
Task task = new Task();
new Thread(task).start();
Thread.sleep(10);
new Thread(task).start();
}
static class Task implements Runnable{
@Override
public void run() {
Long result = threadLocal.get();
if(result == null){
threadLocal.set(System.currentTimeMillis());
System.out.println(Thread.currentThread().getName()+"->"+threadLocal.get());
}
}
}
}
输出的结果是不同的
问题二:为什么可以给每个线程保存一个不同的副本
那我们来分析源码
Long result = threadLocal.get();
public T get() {
//1.获取当前线程
Thread t = Thread.currentThread();
//2,获取到当前线程对应的map
ThreadLocalMap map = getMap(t);
if (map != null) {
//3.以threadLocal为key,获取到entry
ThreadLocalMap.Entry e = map.getEntry(this);
if (e != null) {
@SuppressWarnings("unchecked")
//4.获取对应entry的value,就是我们存放到里面的变量的副本
T result = (T)e.value;
return result;
}
}
return setInitialValue();
}
我们需要结合set方法的源码分析,才可以更好理解
threadLocal.set(System.currentTimeMillis());
public void set(T value) {
//1.获取到当前线程
Thread t = Thread.currentThread();
//2.获取当前线程对应的map
ThreadLocalMap map = getMap(t);
if (map != null)
//3.往map存放一个键值对
//this ThreadLocal
//value 保存的副本
map.set(this, value);
else
createMap(t, value);
}
所以,我们得到结论:
每个线程都会有对应的map,map来保存键值对。
问题三:ThreadLocal这种特性,在实际开发中解决了什么问题?
比如:hibernate管理session,mybatis管理sqlsession,其内部都是采用ThreadLocal来实现的。
前提知识:不管是什么框架,最本质的操作都是基于JDBC,当我们需要跟数据库打交道的时候,都需要有一个connection。
那么,当我们需要在业务层实现事务控制时,该如何达到这个效果?
我们构建下代码如下:
public class UserService {
//省略接口的声明
private UserDao userDao = new UserDao();
private LogDao logDao = new LogDao();
//事务的边界放在业务层
//JDBC的封装,connection
public void add(){
userDao.add();
logDao.add();
}
}
public class UserDao {
public void add(){
System.out.println("UserDao add。。。");
//创建connection对象
//connection.commit();
//connection.rollback();
}
}
public class LogDao {
public void add(){
System.out.println("LogDao add。。。");
//创建connection对象
//connection.commit();
//connection.rollback();
}
}
如果代码按上面的方式来管理connection,我们还可以保证service的事务控制吗?
这是不行的,假设第一个dao操作成功了,那么它就提交事务了,而第二个dao操作失败了,它回滚了事务,但不会影响到第一个dao的事务,因为上面这么写是两个独立的事务
那么怎么解决。
上面的根源就是两个dao操作的是不同的connection
所以,我们保证是同个connection即可
//事务的边界放在业务层
//JDBC的封装,connection
public void add(){
Connection connection = new Connection();
userDao.add(connection);
logDao.add(connection);
}
上面的方式代码不够优雅
public class ConnectionUtils {
private static ThreadLocal<Connection> threadLocal = new ThreadLocal<>();
public static Connection getConnection(){
Connection connection = threadLocal.get();
if(connection == null){
connection = new Connection();
threadLocal.set(connection);
}
return connection;
}
}
public class UserDao {
public void add(){
System.out.println("UserDao add。。。");
//创建connection对象
//connection.commit();
//connection.rollback();
Connection connection = ConnectionUtils.getConnection();
System.out.println("UserDao->"+connection);
}
}
到此,我们可以保证两个dao操作的是同一个connection
27,谈谈你对ThreadLocal的理解
面试场景:
面试官第一问:
请问,我现在编写一个类,类全名如下:java.lang.String,
我们知道JDK也给我们听过了一个java.lang.String,
那么,我们编写的这个String类能否替换到JDK默认提供,也就是说程序实际运行的时候,会加载我们的String还是JDK的String?为什么?
如果,你无法确定?那么第二问:
了解类的加载机制吗?知道JDK的类加载器吗?双亲委托机制说说看
1,首先,什么是类的加载机制?
JVM使用Java类的流程如下:
1,Java源文件----编译---->class文件
2,类加载器ClassLoader会读取这个.class文件,并将其转化为java.lang.Class的实例。有了该实例,JVM就可以使用他来创建对象,调用方法等操作了。
那么ClassLoader是以一种什么机制来加载Class的?
这就是我们要谈的类的加载机制!
2,搞清楚这个问题,首先要知道,我们用到的Class文件都有哪些来源?
1,Java内部自带的核心类,位于$ JAVA_HOME/jre/lib,其中最著名的莫过于 rt.jar
2,Java的扩展类,位于 $ JAVA_HOME/jre/lib/ext 目录下
3,我们自己开发的类或项目开发用到的第三方jar包,位于我们项目的目录下,比如WEB-INF/lib目录
3,那么,针对这些Class,JDK是怎么分工的?谁来加载这些Class?
针对不同的来源,Java分了不同的ClassLoader来加载
1,Java核心类,这些Java运行的基础类,由一个名为BootstrapClassLoader加载器负责加载。这个类加载器被称为“根加载器或引导加载器”
注意:BootstrapClassLoader不继承ClassLoader,是由JVM内部实现。法力无边,所以你通过java程序访问不到,得到的是null。
2,Java扩展类,是由ExtClassLoader负责加载,被称为扩展类加载器。
3,项目中编写的类,是由AppClassLoader来负责加载,被称为系统类加载器。
4, 那凭什么,我就知道这个类应该由老大BootStrapClassLoader来加载?
这里面就要基于双亲委托机制?
-
所谓双亲委托机制,就是加载一个类,会先获取到一个系统类加载器AppClassLoader的实例,然后往上层层请求,先由BootstarpClassLoader去加载,
-
如果BootStrapClassLoader发现没有,再下发给ExtClassLoader去加载,还是没有,才由AppClassLoader去加载。
-
如果还是没有,则报错
5,所以,上述问题的答案你清楚了吗?
JDK提供java.lang.String类,默认在rt.jar这个包里面,所以,默认会由BootstarpClassLoader加载,
所以,我们自己编写的java.lang.String,都没有机会被加载到
6,给两段程序看看,类加载器的关系
案例1:创建一个自己的类,然后打印其类加载器
public class ClassLoaderTest {
public static void main(String[] args) throws ClassNotFoundException {
Class<?> clazz = Class.forName("com.huangguizhao.thread.client.Programmer");
ClassLoader classLoader = clazz.getClassLoader();
System.out.println(classLoader.getClass().getSimpleName());
}
}
案例2:打印其双亲类加载器信息
while(classLoader.getParent() != null){
classLoader = classLoader.getParent();
System.out.println("-->"+classLoader.getClass().getSimpleName());
}
28,谈谈Ajax的工作原理
谈这个问题的关键三要素,异步交互,XMLHttpRequest对象,回调函数。
下面,看图,传统模式跟Ajax工作模式的对比:
早期,预计是以XML为主要的传输数据格式,所以Ajax的最后一个字母就是代表XML的意思,不过现在基本是json为主。
29,浅谈JavaScript的原型机制
JavaScript的原型有一个关键的作用就是来扩展其原有类的特性,比如下面这段代码,给String扩展了hello方法。很多的框架就是采用这种方式来进行扩展,从而让框架更易用。
var str="abc";
String.prototype.hello=function(){
alert("通过原型的方式来扩展原有的类的方法或属性");
}
str.hello();
30,描述JSP和Servlet的区别
技术的角度:
- JSP本质就是一个Servlet
- JSP的工作原理:JSP->翻译->Servlet(java)->编译->class (最终跑的文件)
应用的角度:
- JSP=HTML+JAVA
- Servlet=Java+HTML
各取所长,JSP的特点在于实现视图,Servlet的特点在于实现控制逻辑
31,谈谈Servlet的生命周期
首先,要明确一点,Servlet是单实例的,这个很重要!
生命周期的流程:
- 创建对象–>初始化–>service()–>doXXX()–>销毁
创建对象的时机:
- 1,默认是第一次访问该Servlet的时候创建
- 2,也可以通过配置web.xml,来改变创建时机,比如在容器启动的时候去创建,DispatcherServlet (SpringMVC前端控制器)就是一个例子
<load-on-startup>1</load-on-startup>
执行的次数
- 对象的创建只有一次,单例
- 初始化一次
- 销毁一次
关于线程安全
构成线程不安全三个因素:
- 1,多线程的环境(有多个客户端,同时访问Servlet)
- 2,多个线程共享资源,比如一个单例对象(Servlet是单例的)
- 3,这个单例对象是有状态的(比如在Servlet方法中采用全局变量,并且以该变量的运算结果作为下一步操作的判断依据)
伪代码,演示线程不安全的操作方式
public class MyServlet extends HttpServlet{
private int ticket = 100;
public void doXXX(){
if(ticket > 0){
//......
ticket--;
}
}
}
所以,我们要避免在Servlet中做上述类似的操作!
分析Servlet内部源码,关于Service对请求的分发处理逻辑,会调用相应的doXXX方法
32,描述Session跟Cookie的区别(重要)
存储的位置不同
- Session:服务端
- Cookie:客户端
存储的数据格式不同
- Session:value为对象,Object类型
- Cookie:value为字符串,如果我们存储一个对象,这个时候,就需要将对象转换为JSON
存储的数据大小
- Session:受服务器内存控制
- Cookie:一般来说,最大为4k
生命周期不同
-
Session:服务器端控制,默认是30分钟,注意,当用户关闭了浏览器,session并不会消失
-
Cookie:客户端控制,其实是客户端的一个文件,分两种情况
1,默认的是会话级的cookie,这种随着浏览器的关闭而消失,比如保存sessionId的cookie
2,非会话级cookie,通过设置有效期来控制,比如这种 7天免登录 这种功能,
就需要设置有效期,setMaxAge
cookie的其他配置
httpOnly=true:防止客户端的XSS攻击
path="/" :访问路径
domain="":设置cookie的域名
cookie跟session之间的联系
http协议是一种无状态协议,服务器为了记住用户的状态,我们采用的是Session的机制
而Session机制背后的原理是,服务器会自动生成会话级的cookie来保存session的标识,如下图所示:
33,转发和重定向的区别
1,转发:
发生在服务器内部的跳转,所以,对于客户端来说,至始至终就是一次请求,所以这期间,保存在request对象中的数据可以传递
原理:
request.getRequestDispatcher("/跳转的地址").forward(request,response);
2,重定向:
发生在客户端的跳转,所以,是多次请求,这个时候,如果需要在多次请求之间传递数据,就需要用session对象
原理:
response.sendRedirect("跳转地址");
3,面试官的问题:
在后台程序,想跳转到百度,应该用转发还是重定向?
答案:重定向,因为转发的范围限制在服务器内部
34,谈谈三层架构
1,JavaEE将企业级软件架构分为三个层次:
Web层:负责与用户交互并对外提供服务接口
业务逻辑层:实现业务逻辑模块
数据存取层:将业务逻辑层处理的结果持久化,方便后续查询
2,看图:
3,每个层都有各自的框架
- WEB层:SpringMVC,Struts2,Struts1
- 业务逻辑层:Spring
- 数据持久层:Hibernate,MyBatis,SpringDataJPA,SpringJDBC
35,谈谈对MVC的理解(重要)
MVC是对Web层做了进一步的划分,更加细化
- Model(模型) - 模型代表一个存取数据的对象或 JAVA POJO。
- View(视图) - 视图代表模型包含的数据的可视化,比如HTML,JSP,Thymeleaf,FreeMarker等等
- Controller(控制器) -控制器作用于模型和视图上。它控制数据流向模型对象,并在数据变化时更新视图。它使视图与模型分离开,目前的技术代表是Servlet,Controller
常见的MVC框架有,Struts1,Struts2,SpringMVC
比如,SpringMVC分为两个控制器
DispatchServlet:前端控制器,由它来接收客户端的请求,再根据客户端请求的URL的特点,分发到对应的业务控制器,比如UserController
36,描述JSP的9大内置对象(不重要)
首先,对于我们现在来说,用JSP的内置对象来直接开发的基本没有了,除非是比较老旧的项目,jsp的内置对象,则是不需要在jsp页面中创建,直接可以使用。
其实,我们通过观察jsp生成的java文件可以发现,其背后是帮我们创建了这些对象,所以对象的创建方式还是没有改变的。
我们去观察背后生成java类,那么里面是有给各个对象创建及初始化的代码(截取部分代码片段)
public void _jspService(final javax.servlet.http.HttpServletRequest request,
final javax.servlet.http.HttpServletResponse response)
pageContext = _jspxFactory.getPageContext(this, request, response,
null, true, 8192, true);
_jspx_page_context = pageContext;
application = pageContext.getServletContext();
config = pageContext.getServletConfig();
session = pageContext.getSession();
out = pageContext.getOut();
_jspx_out = out;
....
来,如果面试官问到这个问题,记得多少回答多少即可。
内置对象名 类型
- request (HttpServletRequest)
- response (HttpServletResponse)
- config (ServletConfig)
- application (ServletContext)
- session (HttpSession)
- exception (Throwable)
- page (Object(this))
- out (JspWriter)
- pageContext (PageContext)
37,JSP的4大域对象
4大域对象
- ServletContext context域
- HttpSession session域
- HttpServletRequet request域
- PageContext page
4大域对象的作用范围
- page域: 只能在当前jsp页面使用 (当前页面)
- request域: 只能在同一个请求中使用 (转发才有效,重定向无效)
- session域: 只能在同一个会话(session对象)中使用 (私有的,多个请求和响应之间)
- context域: 只能在同一个web应用中使用 (全局的)
38,并发和并行的区别
- 并发:同一个CPU执行多个任务,按细分的时间片交替执行
- 并行:在多个CPU上同时处理多个任务
39,谈谈数据库设计的三大范式及反范式
1,数据库的三大范式
-
第一范式:列不可分
1NF:原子性,即字段不可以再分。 -
第二范式:要有主键
2NF:唯一性,不可以把多种数据保存在同一张表中,即一张表只能保存“一种”数据。
不符合第二范式的表:学号, 姓名, 年龄, 课程名称, 成绩, 学分; -
第三范式:不可存在传递依赖
3NF:直接性,每一列都和主键直接相关,而不能间接相关。(依赖不准传递)比如商品表里面关联商品类别表,那么只需要一个关联字段product_type_id即可,其他字段信息可以通过表关联查询即可得到
如果商品表还存在一个商品类别名称字段,如product_type_name,那就属于存在传递依赖的情况,第三范式主要是从空间的角度来考虑,避免产生冗余信息,浪费磁盘空间
2,反范式设计:(第三范式)
为什么会有反范式设计?
-
原因一:提高查询效率(读多写少)
比如上述的描述中,显示商品信息时,经常需要伴随商品类别信息的展示,所以这个时候,为了提高查询效率,可以通过冗余一个商品名称字段,这个可以将原先的表关联查询转换为单表查询
-
原因二:保存历史快照信息
比如订单表,里面需要包含收货人的各项信息,如姓名,电话,地址等等,这些都属于历史快照,需要冗余保存起来,不能通过保存用户地址ID去关联查询,因为用户的收货人信息可能会在后期发生变更
40,说说常用的聚合函数有哪些及作用?
列表如下:
基本使用语法:
select max(age) from t_student;
select min(age) from t_student;
聚合函数经常会结合分组查询
41,左连接,右连接,内连接,如何编写SQL,他们的区别是什么?
左连接:以左表为主
select a.*,b.* from a left join b on a.b_id = b.id;
右连接:以右表为主
select a.*,b.* from a right join b on a.b_id = b.id;
内连接:只列出两张表关联查询符合条件的记录
select a.*,b.* from a inner join b on a.b_id = b.id;
案例:
select t.id t_id,t.`name` t_name,c.id c_id,c.`name` c_name
from t_teacher t LEFT JOIN t_class c on t.id=c.t_id; #4条,以老师表为主
select t.id t_id,t.`name` t_name,c.id c_id,c.`name` c_name
from t_teacher t RIGHT JOIN t_class c on t.id=c.t_id; #4条,以班级表为主
select t.id t_id,t.`name` t_name,c.id c_id,c.`name` c_name
from t_teacher t INNER JOIN t_class c on t.id=c.t_id; #3条,只展示匹配条件的记录
42,如何解决SQL注入?
1,SQL注入,是指通过字符串拼接的方式构成了一种特殊的查询语句
比如:select * from t_user where usename='' and password=''
' or 1=1 #
select * from t_user where usename='' or 1=1 # ' and password=''
2,解决方案
-
采用预处理对象,采用PreparedStatement对象,而不是Statement对象
可以解决SQL注入的问题
另外也可以提高执行效率,因为是预先编译执行 -
SQL执行过程(语法校验->编译->执行)
延伸
MyBatis如何解决了SQL注入的问题?采用#
MyBatis的#和$的差异,#可以解决SQL注入,而?号不能解决
43,JDBC如何实现对事务的控制及事务边界
DBC对事务的操作是基于Connection来进行控制的,具体代码如下:
try {
//开启事务
connection.setAutoCommit(false);
//做业务操作
//doSomething();
//提交事务
connection.commit();
}catch(Exception e){
//回滚事务
try {
connection.rollback();
} catch (SQLException e1) {
e1.printStackTrace();
}
}
但,注意,事务的边界我们是放在业务层进行控制,因为业务层通常包含多个dao层的操作。
44,谈谈事务的特点
原子性是基础,隔离性是手段,一致性 是约束条件,而持久性是我们的目的。
简称,ACID
原子性( Atomicity )、一致性( Consistency )、隔离性( Isolation )和持久性( Durability )
原子性:
- 事务是数据库的逻辑工作单位,事务中包含的各操作要么都完成,要么都不完成
(要么一起成功,要么一起失败)
一致性:
- 事务一致性是指数据库中的数据在事务操作前后都必须满足业务规则约束。
比如A转账给B,那么转账前后,AB的账户总金额应该是一致的。
隔离性:
- 一个事务的执行不能被其它事务干扰。即一个事务内部的操作及使用的数据对其它并发事务是隔离的,并发执行的各个事务之间不能互相干扰。(设置不同的隔离级别,互相干扰的程度会不同)
持久性:
- 事务一旦提交,结果便是永久性的。即使发生宕机,仍然可以依靠事务日志完成数据的持久化。
日志包括回滚日志(undo)和重做日志(redo),当我们通过事务修改数据时,首先会将数据库变化的信息记录到重做日志中,然后再对数据库中的数据进行修改。这样即使数据库系统发生奔溃,我们还可以通过重做日志进行数据恢复。
45,谈谈事务的隔离级别
有以下4个级别:
- READ UNCOMMITTED 读未提交,脏读、不可重复读、幻读有可能发生。
- READ COMMITTED 读已提交,可避免脏读的发生,但不可重复读、幻读有可能发生。
- REPEATABLE READ 可重复读,可避免脏读、不可重复读的发生,但幻读有可能发生。
- SERIALIZABLE 串行化,可避免脏读、不可重复读、幻读的发生,但性能会影响比较大。
特别说明:
- 幻读,是指在本地事务查询数据时只能看到3条,但是当执行更新时,却会更新4条,所以称为幻读
来一张汇总表:
注意:
- 幻读和不可重复读是十分相似的,以致于很多人很难分辨它们,你只需要知道,它们最大的区别是:不可重复读读取到的是更新(update)数据,而幻读读取到的是插入(insert)数据。
45,synchronized和lock的区别
1,作用的位置不同
- synchronized可以给方法,代码块加锁
- lock只能给代码块加锁
2,锁的获取锁和释放机制不同
- synchronized无需手动获取锁和释放锁,发生异常会自动解锁,不会出现死锁。
- lock需要自己加锁和释放锁,如lock()和unlock(),如果忘记使用unlock(),则会出现死锁,
所以,一般我们会在finally里面使用unlock().
补充:
//明确采用人工的方式来上锁
lock.lock();
//明确采用手工的方式来释放锁
lock.unlock();
- synchronized修饰成员方法时,默认的锁对象,就是当前对象
- synchronized修饰静态方法时,默认的锁对象,当前类的class对象,比如User.class
- synchronized修饰代码块时,可以自己来设置锁对象,比如
synchronized(this){
//线程进入,就自动获取到锁
//线程执行结束,自动释放锁
}
46,说说TCP和UDP的区别
首先,两者都是传输层的协议。
其次,
- tcp提供可靠的传输协议,传输前需要建立连接,面向字节流,传输慢
- udp无法保证传输的可靠性,无需创建连接,以报文的方式传输,效率高
47,谈谈什么是TCP的三次握手
来,看图,个人觉得很完美的一张图,不接受反驳
48,谈谈什么是TCP的四次挥手?
大家可以思考一个问题,为什么要四次挥手,原因是TCP连接是一种双工的通信模式。
49,什么是死锁?如何防止死锁?
1,什么是死锁
死锁最初由一个悲惨的故事说起,话说一群哲学家一起聚餐,然后在每个人的左边和右边分别放着一根筷子,而只有同时抓到两根筷子,才能正常吃饭,于是,不幸的故事发生了,每位哲学家都只抓到一根筷子,且都不愿意释放手中的筷子,于是,最终一桌的饭菜就这么浪费了。
不知道这个故事是谁发明的,但确实形象说明了死锁的情况。
转换到线程的场景,就是线程A持有独占锁资源a,并尝试去获取独占锁资源b
同时,线程B持有独占锁资源b,并尝试去获取独占锁资源a
这样线程A和线程B相互持有对方需要的锁,从而发生阻塞,最终变为死锁。
public class Deadlock {
private static final Object a = new Object();
private static final Object b = new Object();
public static void main(String[] args){
new Thread(new Task(true)).start();
new Thread(new Task(false)).start();
}
static class Task implements Runnable{
private boolean flag;
public Task(boolean flag){
this.flag = flag;
}
@Override
public void run() {
if(flag){
synchronized (a){
System.out.println(Thread.currentThread().getName()+"->获取到a资源");
synchronized (b){
System.out.println(Thread.currentThread().getName()+"->获取到b资源");
}
}
}else{
synchronized (b){
System.out.println(Thread.currentThread().getName()+"->获取到b资源");
synchronized (a){
System.out.println(Thread.currentThread().getName()+"->获取到a资源");
}
}
}
}
}
}
//有可能会出现死锁,如果第一个线程已经走完,第二个线程才获取到执行权限,那么就不会出现死锁
2,如何防止死锁?(重点)
- 减少同步代码块嵌套操作
- 降低锁的使用粒度,不要几个功能共用一把锁
- 尽量采用tryLock(timeout)的方法,可以设置超时时间,这样超时之后,就可以主动退出,防止死锁(关键)
50,什么是反射?可以解决什么问题?
反射是指程序在运行状态中,
1,可以对任意一个类,都能够获取到这个类的所有属性和方法。
2,对于任意一个对象,都可以调用它的任意一个方法和属性
在java中,Class类就是关键API
public class Reflection {
public static void main(String[] args) throws ClassNotFoundException, NoSuchFieldException, NoSuchMethodException {
//1.以class对象为基础
Class<?> clazz = Class.forName("com.hgz.reflection.Student");
System.out.println(clazz);
//2.类中每一部分,都有对应的类与之匹配
//表示属性的类
Field nameField =
clazz.getField("name");
//表示方法的类
Method helloMethod = clazz.getDeclaredMethod("hello", String.class);
//表示构造方法的类
Constructor<?>[] constructors = clazz.getConstructors();
}
}
这种能力带来很多的好处,在我们的许多框架的背后实现上,都采用了反射的机制来实现动态效果。
框架是提供一种编程的约定
比如@Autowrie 就能实现自动注入
@Autowrie
private IUserService userService;
注解的解析程序,来扫描当前的包下面有哪些属性加了这个注解,一旦有这个注解,就要去容器里面获取对应的类型的实现,然后给这个属性赋值。
思考题:如何实现一个IOC容器?
51,谈谈你对Spring的认识
1,概览图如下:
2,说说上面的模块
核心的IOC容器技术(控制反转),帮助我们自动管理依赖的对象,不需要我们自己创建和管理依赖对象,从而实现了层与层之间的解耦,所以重点是解耦!
核心的AOP技术(面向切面编程),方便我们将一些非核心业务逻辑抽离,从而实现核心业务和非核心业务的解耦,比如添加一个商品信息,那么核心业务就是做添加商品信息记录这个操作,非核心业务比如,事务的管理,日志,性能检测,读写分离的实现等等
spring Dao,Spring web模块,更方便集成各大主流框架,比如ORM框架,hibernate,mybatis,比如MVC框架,struts2,SpringMVC
52,Spring的bean作用域有哪些?
- 1,默认是singleton,即单例模式
- 2,prototype,每次从容器调用bean时都会创建一个新的对象,比如整合Struts2框架的时候,spring管理action对象则需要这么设置。
- 3,request,每次http请求都会创建一个对象
- 4,session,同一个session共享一个对象
- 5,global-session
53,Spring的bean是线程安全的吗?
大家可以回顾下线程不安全构成的三要素:
- 1,多线程环境
- 2,访问同一个资源
- 3,资源具有状态性
那么Spring的bean模式是单例,而且后端的程序,天然就处于一个多线程的工作环境。
那么是安全的吗?
关键看第3点,我们的bean基本是无状态的,所以从这个点来说,是安全的。
所谓无状态就是没有存储数据,即没有通过数据的状态来作为下一步操作的判断依据
54,什么是事务的传播特性及Spring支持的特性有哪些?
1,什么是事务的传播特性?
我们一般都是将事务的边界设置在Service层,
那么当我们调用Service层的一个方法的时,它能够保证我们的这个方法中执行的所有的对数据库的更新操作保持在一个事务中,
在事务层里面调用的这些方法要么全部成功,要么全部失败。那么事务的传播特性也是从这里说起的。
如果你在你的Service层的这个方法中,还调用了本类的其他的Service方法,那么在调用其他的Service方法的时候,这个事务是怎么规定的呢?
必须保证在我方法里调用的这个方法与我本身的方法处在同一个事务中,否则无法保证事物的一致性。
事务的传播特性就是解决这个问题的
2,Spring支持的事务传播特性
在Spring中,针对传播特性的多种配置,我们大多数情况下只用其中的一种:PROPGATION_REQUIRED:
这个配置项的意思是说当我调用service层的方法的时候,开启一个事务,
那么在调用这个service层里面的其他的方法的时候,如果当前方法产生了事务就用当前方法产生的事务,否则就创建一个新的事务。
这个工作是由Spring来帮助我们完成的。
3,Spring支持的事务传播特性
- PROPAGATION_REQUIRED:支持当前事务,如果当前没有事务,就新建一个事务。这是最常见的选择。
- PROPAGATION_SUPPORTS:支持当前事务,如果当前没有事务,就以非事务方式执行。
- PROPAGATION_MANDATORY:支持当前事务,如果当前没有事务,就抛出异常。
- PROPAGATION_REQUIRES_NEW:新建事务,如果当前存在事务,把当前事务挂起
- PROPAGATION_NOT_SUPPORTED:以非事务方式执行操作,如果当前存在事务,就把当前事务挂起。
- PROPAGATION_NEVER:以非事务方式执行,如果当前存在事务,则抛出异常。
55,什么是悲观锁,什么是乐观锁?
-
1,悲观锁是利用数据库本身的锁机制来实现,会锁记录。
实现的方式为:select * from t_table where id = 1 for update
-
2,乐观锁是一种不锁记录的实现方式,采用CAS(compare and set/swap)模式,采用version字段来作为判断依据。每次对数据的更新操作,都会对version+1,这样提交更新操作时,如果version的值已被更改,则更新失败。
update t_product set store=store+1,version=version+1 where id=1 and version=old_version
-
3,乐观锁的实现为什么要选择version字段,如果选择其他字段,比如业务字段store(库存),那么可能会出现所谓的ABA问题
如下图所示:
56,MyBatis-缓存机制,从一级缓存到二级缓存
缓存,主要作用是提高了查询性能,减少了跟数据库交互的次数,从而也减轻了数据库承受的压力。
适用于读多写少的场景,如果数据变化频率非常高,则不适用。
MyBatis的缓存分为一级缓存和二级缓存。
下面,我们通过做实验,来掌握MyBatis的一级缓存和二级缓存的特点:
1,来,关门,上一级缓存
观察执行结果:
再做一次实验,中间修改对象的信息
再做一次实验,中间新增记录
一级缓存总结:
1,一级缓存模式是开启状态
2,一级缓存作用域在于SqlSession(大家可以关闭SqlSession,然后创建一个新的,再获取对象,观察实验结果)
3,如果中间有对数据的更新操作,则将清空一级缓存。
下面,我们来看二级缓存(重点)
要使用二级缓存,需要经历两个步骤
1,开启二级缓存(默认处于开启状态)< setting name=“cacheEnabled” value=“true”/>
2,在Mapper.xml中,配置二级缓存(也支持在接口配置)在标签< mapper>下面添加< cache/>标签即可
默认的二级缓存配置会有如下特点:
2.1 所有的Select语句将会被缓存
2.2 所有的更新语句(insert、update、delete)将会刷新缓存
2.3 缓存将采用LRU(Least Recently Used 最近最少使用)算法来回收
2.4 缓存会存储1024个对象的引用
回收算法建议采用LRU,当然,还提供了FIFO(先进先出),SOFT(软引用),WEAK(弱引用)等其他算法。
3,做实验,验证二级缓存的效果:
观察结果:
二级缓存关键说明:
- 当关闭了SqlSession之后,才会将查询数据保存到二级缓存中(SqlSessionFactory)中,所以才有了上述的缓存命中率。MyBatis的二级缓存默认采用的是Map的实现。
4,衍生
其实,我们在开发中,可以集成第三方的缓存来保存MyBatis的二级缓存,常用的有EhCache和Redis
4.1 EhCache
MyBatis提供了一个项目实现,ehcache-cache
学习地址:https://github.com/mybatis/ehcache-cache
57,MyBatis有哪些分页方式?
正常人,一般使用物理分页。
分为逻辑分页和物理分页
所谓逻辑分页,是指使用MyBatis自带的RowBounds进行分页,它会一次性查出多条数据,然后再检索分页中的数据,具体一次性查询多少条数据,受封装jdbc配置的fetch-size决定
而物理分页,是从数据库中查询指定条数的数据,而我们用的分页插件PageHelper实现的就是物理分页
那么问题来了,你清楚分页插件背后的原理吗?
58,说说MyBatis分页插件的原理是什么?
首先,在MyBatis内部定义了一个拦截器接口
所有的插件都要实现该接口,来,我们看看这个接口的定义
public interface Interceptor {
Object intercept(Invocation invocation) throws Throwable;
Object plugin(Object target);
void setProperties(Properties properties);
}
那么其中一个关键的方法就是intercept,从而实现拦截
分页插件的原理就是使用MyBatis提供的插件接口,实现自定义插件,在插件的拦截方法内,拦截待执行的SQL,然后根据设置的dialect(方言),和设置的分页参数,重写SQL ,生成带有分页语句的SQL,执行重写后的SQL,从而实现分页
所以原理还是基于拦截器
58,从浏览器输入URL到页面加载完毕,都经历了什么?
首先,需要经过DNS(域名解析服务)将URL转换为对应的ip地址,实际上域名只是方便我们记忆,在网络上的每台主机交互的地址都是IP。
其次,我们需要通过这个ip地址跟服务器建立TCP网络连接,随后向我们的服务器发出http请求。注意,http协议是tcp的上层协议
最后,服务器接收到我们的请求,处理完毕之后,将响应数据放入到http的响应信息中,然后返回给客户端。
客户端浏览器完成对服务器响应信息的渲染,将信息展现在用户面前。
常见的响应状态码:
200,500,404,400,405,301这些你知道什么意思吗?
59,说说synchronized的底层原理
synchronized是由一对 monitorenter 和 monitorexit 指令来实现同步的,在JDK6之前,monitor的实现是依靠操作系统内部的互斥锁来实现的,所以需要进行用户态和内核态的切换,所以此时的同步操作是一个重量级的操作,性能很低。
但是,JDK6带来了新的变化,提供了三种monitor的实现方式,分别是偏向锁,轻量级锁和重量级锁,即锁会先从偏向锁再根据情况逐步升级到轻量级锁和重量级锁。
这就是锁升级
-
在锁对象的对象头里面有一个threadid字段,默认情况下为空,当第一次有线程访问时,则将该threadid设置为当前的线程id,我们称为让其获取偏向锁,当线程执行结束,则重新将threadid设置为空。
-
之后,如果线程再次进入的时候,会先判断threadid与该线程的id是否一致,如果一致,则可以获取该对象,如果不一致,则发生锁升级,从偏向锁升级为轻量级锁
-
轻量级锁的工作模式是通过自旋循环的方式来获取锁,看对方线程是否已经释放了锁,如果执行一定次数之后,还是没有获取到锁,则发生锁升级,从轻量级锁升级为重量级锁。
-
使用锁升级的目的是为了减少锁带来的性能消耗。
通过反编译查看字节码,就可以看到相关的指令
javap -verbose Test.class
源码:就是写了synchronized同步代码块控制线程安全
Code:
stack=2, locals=4, args_size=1
0: new #2 // class java/lang/Object
3: dup
4: invokespecial #1 // Method java/lang/Object."<init>":()V
7: astore_1
8: aload_1
9: dup
10: astore_2
11: monitorenter
12: getstatic #3 // Field java/lang/System.out:Ljava/io/PrintStream;
15: ldc #4 // String 获得锁
17: invokevirtual #5 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
20: aload_2
21: monitorexit
22: goto 30
25: astore_3
26: aload_2
27: monitorexit
28: aload_3
29: athrow
30: return
synchronized如何保证可见性的?
首先,我们需要知道可见性原理
两个线程如何保证变量信息的共享可见性?需要经历以下的流程
线程A-》本地内存A(共享变量副本)-》主内存(共享变量)
如果有变更,需要将本地内存的变量写到主内存,对方才可以获取到更新。
这个是提前知识。
那么,synchronized是如何保证可见性的
就是当获取到锁之后,每次读取都是从主内存读取,当释放锁的时候,都会将本地内存的信息写到主内存,从而实现可见性
60,synchronized和volatile的区别
1,作用的位置不同
- synchronized是修饰方法,代码块
- volatile是修饰变量
2,作用不同
- synchronized,可以保证变量修改的可见性及原子性,可能会造成线程的阻塞
- volatile 仅能实现变量修改的可见性,但无法保证原子性,不会造成线程的阻塞
61, 谈谈我们为什么要做服务化?
高并发系统架构的目标,高可用,高性能,高可扩展性
单体架构:
特点:适合小成本快速试错,模块之间的调用,是进程内的通信
微服务架构:
特点:适合业务复杂的拆分场景,更灵活做子系统升级等等,系统之间的通信,变成为进程间的通信
62, 服务拆分的原则?
一般我们根据业务的边界来拆分+共性的基础服务(短信,邮件,日志)
63, 服务拆分&服务治理&服务间的通信
- 1,抽取公共的基础服务(短信,邮件,文件管理)
- 2,以业务为边界,拆分服务
- 3,要管理这么一堆服务,注册中心
Zookeeper - 4,服务之间需要通信,通信的方式
64,如何实现服务之间共享资源的安全访问控制?
1,synchronized?
- 控制范围是一个JVM实例中,但是分布式系统是N个JVM,所以控制不了
2,分布式锁
保证锁是共享的第三方资源
- 1,上锁
- 2,解锁
2.1 数据库的方式
- t_lock
- id lock(0)
- 上锁:将数值修改为1
- 解锁:将数值修改为0
2.2 Redis
- setnx:不存在,就设置成功,否则,设置失败
- 上锁:成功执行setnx key value
- 解锁:delete key
- 避免死锁:需要设置过期时间 expire key timeout
注意:需要将两个操作变成原子操作
- 4.0之前:Redis+lua脚本,lua脚本帮助我们扩充Redis指令
- 4.0之后:直接使用现成的原子指令,在设置数据的时候,同时设置时间
避免无锁:
- 释放锁的时候,检查这把锁是不是我的
2.3 zookeeper
-
以节点作为锁,创建成功,表示获取锁成功
-
上锁:创建节点lock
-
解锁:删除节点lock
-
避免死锁:节点,为临时节点类型,客户端如果挂掉,会自动删除
-
死锁:设置过期时间
-
setnx lock uuid
-
expire lock 1000
lua脚本----原子性操作
- 解锁:del lock
- 无锁:判断当前这把锁是不是我的,我的才删除
65,三种分布式锁的实现方式
保证锁是共享的第三方资源
- 1,首先,尝试获取锁
- 2,获取锁成功,则上锁
- 3,处理业务操作之后,则解锁
落地的方式:
1,数据库的方式
创建一张表,t_lock,其中包含字段 lock(1),默认为0,有客户端获取锁,则将其修改为1
2,Redis的方式
关键指令:setnx lock uuid (尝试获取锁并上锁)
- 如果key存在,则设置失败
- 如果key不存在,则设置成功,所以如果设置成功,则表示获取到锁,否则获取锁失败
delete key(释放锁)
存在的问题1
死锁的问题
解决办法
设置有效期:expire lock 1000
存在的问题2
原子性(setnx lock uuid ;expire lock 1000)
两条指令可能导致无法设置有效期
lua脚本,将上述两个操作合成一个指令,变为原子性操作
redis4.x以上版本已经提供了对应的指令
存在的问题3
无锁
- 判断当前这把锁是不是我的,我的才删除
3,zookeeper
前提知识:
zookeeper采用树状节点的方式来保存我们的服务的注册信息(znode)
我们创建znode的时候,有name属性,如果znode已经存在,那么会创建失败,否则创建成功。
获取锁
以节点作为锁,创建成功,表示获取锁成功
释放锁
则是删除节点即可
避免死锁
创建的节点类型必须为非持久性节点,这样我们的客户端失效之后,那么这个节点就会删除
节点类型:
- 持久性
- 非持久性(临时节点):客户端如果挂掉,会自动删除
羊群效应
一旦我们的这个锁失效之后,假设有上百个客户端都会去争抢这个锁,这个时候我们称为羊群效应
时序节点
zookeeper里面的节点还分时序节点和非时序节点
我们的节点是有序号的,
lock
1,2,3,4
66,如何解决服务之间的session共享问题?
目标:完成用户的状态认证
单机版:session中,服务器自动生成cookie(jsessionId–abcdef)
集群版或者分布式:
有状态的方式
无状态的方式
有状态
服务器依然要存储用户的信息,存储有效凭证
生成cookie(user_token:uuid),从而来帮助我们确定存储在redis中的用户凭证信息。
此处uuid等同于之前的sessionId
但是有状态的方式需要耗费一定的内存空间来存储用户信息。
客户端:
key–value
PC客户端(cookie)
user_token--------uuid
App客户端(H5)
服务端只需要给客户端一个凭证信息,比如uuid即可,对方保存的时候,key由它自己决定,如何存储由它自己决定
服务端Redis
key------value
user:token:uuid-—userinfo(password=null)
无状态
总结:令牌本质就是一串字符串
67,你是如何理解CAP的?
所以这里面就造就了一个矛盾:
如果节点多,那么可以提高可用性,但是由于节点多,数据之间同步速度会变慢,就影响了一致性,所以,我们选择的时候,就是在AP或CP做选择。
68,QPS,TPS,响应时间,吞吐量,并发量,PV,UV,日活,这些名词都解释下?
69,如何做好线上服务器容量评估(峰值QPS&压力测试)
70,我们该如何应对高并发
71,如何实现分布式系统的数据一致性问题?(分布式事务)
谈谈什么是分布式事务?
72,基于MQ的分布式事务(柔性事务)
73,谈谈对BASE理论的理解?
BASE理论的核心
基本可用
柔性状态
最终一致性
74. 什么是乐观锁和悲观锁?
悲观锁:
概念:当要对数据库中的一条数据进行修改的时候,为了避免同时被其他人修改,最好的办法就是直接对该数据进行加锁以防止并发。这种借助数据库锁机制,在修改数据之前先锁定,再修改的方式被称之为悲观并发控制【Pessimistic Concurrency Control,缩写“PCC”,又名“悲观锁”】。
悲观锁是利用数据库本身的锁机制来实现,会锁记录。
实现的方式为:
select* from t_table where id=1 for update;
乐观锁:
概念:乐观锁是相对悲观锁而言的,乐观锁假设数据一般情况下不会造成冲突
,所以在数据进行提交更新
的时候,才会正式对数据的冲突与否进行检测,如果发现冲突了
,则返回给用户错误的信息,让用户决定如何去做。乐观锁适用于读操作多
的场景,这样可以提高程序的吞吐量
。
乐观锁是一种不锁记录的实现方式,采用CAS
(compare and swap) 模式,采用version
字段来作为判断依据。每次对数据的更新操作,都会对version+1,这样提交更新操作时,如version的值已被更改,则更新失败。
#更新数据前查询version值
select version from xxx where id=1; #假设当前version值为6
update xx set store=stroe+1,version=version+1 where id=1 and version=6;
#假设version值一致,更新成功,不一致,有另一个线程修改了version的值,则更新失败
乐观锁的实现为什么要选择version字段,如果选择其他字段,比如业务字store(库存),那么可能会出现所谓的ABA问题。
75. Spring Bean的生命周期
Spring IOC容器对Bean的生命周期进行管理的过程:
- 通过构造器或工厂方法创建Bean实例
- 为Bean的属性设置值和对其他Bean的引用
- 调用Bean的初始化方法
- Bean可以使用了
- 当容器关闭时,调用Bean的销毁方法
- 在Bean的声明里设置了
init-method
和destroy-method
属性,为Bean指定初始化和销毁方法