文章目录
- 前言
- 一、JAVA基础知识
- 1 什么面向对象?
- 2 JVM为什么能实现JAVA跨平台?
- 3 ==和equals区别
- 4 final关键字
- 5 如何确定一个对象是否是线程安全的?
- 6 String、StringBuffer和StringBuilder
- 7 接口和抽象类的区别
- 8 重载和重写的区别
- 9 List和Set的区别
- 10 ArrayList和LinkedList的区别
- 11 HashSet
- 12 HashMap为什么是线程不安全的?
- 13 简述Java异常体系
- 14 throw跟throws的区别
- 15 try-catch-finally执行
- 16 怎么理解线程安全?
- 17 sleep()和wait()的区别
- 18 ThreadLocal
- 19 类加载机制(双亲委托)
- 20 Ajax的工作原理
- 21 Servlet的生命周期
- 22 转发和重定向
- 23 Session跟Cookie的区别
- 24 理解MVC
- 25 数据库设计三范式
- 26 SQL注入
- 27 JDBC如何控制事务及事务边界
- 28 数据库事务特点ACID
- 29 数据库 事务隔离级别
- 30 synchronized与lock
- 31 TCP和UDP的区别
- 32 TCP三次挥手
- 33 TCP四次挥手
- 34 什么是死锁?如何预防死锁?
- 35 什么是反射
- 36 对Spring的认识
- 37 Spring的bean作用域
- 38 Spring的Bean是线程安全的吗
- 39 synchronized底层实现
前言
本章主要是通过在网上收集的一些面经经验总结出来的面试(属于初级程序员知识储备)一、JAVA基础知识
1 什么面向对象?
面向对象是一种思想,在Java编程中,万事万物皆对象,也俗称(OOP),相对于传统的面向过程编程,不使用硬编码,可扩展,已维护,不是简单的以当前的业务需求一步一步的编程,而是更多的考虑使用封装、继承、类、多态、抽象等思想。
- 封装就是说把对象封装成为一个比较独立,相对封闭的个体,一般来说就是外部不能直接获取类内部的属性值,因为属性包含的信息比较重要,需要隐藏起来,当外部需要访问的时候,通过调用类里面的getting()方法来获取值。
- 抽象,使用关键字abstract来修饰的类或方法,意义在于找出一些是事物的相似之处,比如说男生,女生的话,都属于人类,有共同的一些特征,鼻子,眼睛之类的,有一些共同的行为,如说吃饭,工作之类的。把这些抽象出来作为一个类,即为抽象类,这个类只考虑这些事物之间存在的相似度。
- 继承,使用关键字extend,父类是存在一定属性和行为的,而子类继承父类的话就会获得父类的属性和行为作为自身的属性和行为,同时还可以继承在自身中添加一些新的内容,或对继承过来的进行一些修改操作,比如说赋值或重写操作。
- 多态,说的是用一个父类或者接口类型的引用变量指向子类或实现类的对象,其中程序调用的方法在运行的时候才会动态绑定,就是子类或实现类中的方法,而不是引用变量的类型中定义的方法。
2 JVM为什么能实现JAVA跨平台?
2.1 概念
jvm是Java运行环境的一部分,是通过在实际的计算机上模拟各种计算机功能实现的。
2.2 为什么要JVM?
操作系统只能识别机器指令,无法识别java高级编程语言,需要通过jvm这个媒介把字节码文件----解释器----->汇编指令-----转译----->CPU可以识别的机器指令
2.3 执行机制
java源文件----编辑器----->class字节码文件------运行----->不同操作系统的jvm上-----翻译成机器码---->执行操作
3 ==和equals区别
3.1 ==比较的是值
- 如果比较的是基本数据类型的话,则比较的是数值
- 如果比较的是引用类型数据的话,则比较的是内存地址
3.2 equals默认比较的是内存地址
public class Object {
public boolean equals(Object obj) {
return (this == obj);
}
}
如果需要比较里面的内容的话则需要重写equals方法的比较规则
public final class String implements Serializable, Comparable<String>, CharSequence {
public boolean equals(Object var1) {
//如果内存地址都一样,那就直接返回true
if (this == var1) {
return true;
} else {
//instanceof是Java中的二元运算符,左边是对象,右边是类;当对象是右边类或子类所创建对象时,返回true;否则,返回false。
if (var1 instanceof String) {
String var2 = (String)var1;
int var3 = this.value.length;
//比较长度
if (var3 == var2.value.length) {
char[] var4 = this.value;
char[] var5 = var2.value;
//转成char[],一个一个元素的匹配
for(int var6 = 0; var3-- != 0; ++var6) {
if (var4[var6] != var5[var6]) {
return false;
}
}
return true;
}
}
return false;
}
}
}
4 final关键字
- final修饰类,表示类不可变,不可继承(String)
- final修饰方法,表示该方法不可重写
- final修饰变量,这个变量就是常量
注意:
修饰的是基本数据类型,这个值本身不能修改
final int k = 1;
修饰的是引用类型,引用的指向不能修改
final Student student = new Student(1,"Andy");
student.setAge(18);
final和static的区别
都可以修饰类,方法,成员变量
加载机制的不同
- static修饰的属性,在类加载时被创建并进行初始化,只会被创建一次
- final标记的成员变量必须在声明的同时赋值,或在该类的构造方法中赋值,不可以重新赋值
修饰类的不同
- static只能修饰内部类,final能修饰外部类
修饰变量的不同
- static修饰的变量可以重新赋值
- final修饰表示常量、一旦创建不可改变
- static不可以修饰方法内的局部变量,final可以
修饰方法的不同
- static方法中不能用this和super关键字
- static方法必须被实现,而不能是抽象的abstract
- static方法只能被static方法覆盖
- final方法不能被子类重写
- final类中的方法默认是final的
- final不能用于修饰构造方法
5 如何确定一个对象是否是线程安全的?
单线程的情况下不会发生线程安全问题,一般线程安全问题都是发生在多线程环境下,且多线程同时操作一个资源的时候存在边界、互斥问题等情况下,对这个对象不需要加入额外的同步控制,操作的数据的结果依然是正确的。
6 String、StringBuffer和StringBuilder
String与其它两个的不同点在于
- String被final修饰,每次声明都是不可变的对象
- 每次操作都会产生新的String类型的对象,指针指向新的内存地址
- 所以String还有一个名称叫只读字符串
StringBuffer和StringBuilder
- StringBuffer是线程安全的,String也是,因为它的不可变性
- StringBuffer和StringBuilder都是在原有对象上进行操作,常用于改变字符串的内容
三者选择问题
线程不安全性能更高,所以在开发中,优先采用StringBuilder.
StringBuffer有多线程问题需要处理且经常性的改变字符串内容,效率比String好一些
在开发中,我们经常在方法里面定义一个StringBuilder用于字符串的拼接,这是线程安全的,因为一个线程去调用一个方法的时候会创建一个栈帧,用来存储局部变量表、操作栈、动态链接、方法出口等信息。调用方法时执行入栈,方法返回时执行出栈。这是线程独享的所以不存在线程安全问题,同理(ArrayList和HashMap在方法中的应用也是一样的)
7 接口和抽象类的区别
语法区别
- 抽象类:方法可以有抽象的,也可以有非抽象, 有构造器,(可以没有抽象方法)
- 接口:方法一般是抽象,属性都是常量,默认有public static final修饰,可以有实现的方法,注意要在方法的声明上加上default或者static
设计区别
- 抽象类:同一类事物的抽取,并进行封装,比如说BaseDao定义了基本的crud操作,子类可以继承它,可以使用它的方法,也可以根据实际需要去覆写它的方法
接口:通常更像是一种标准的制定,定制系统之间对接的标准(注重的是分层开发,高内聚低耦合)
概念区别
- 多重继承:A->B->C(爷孙三代的关系)
- 多实现:Person implements impl1,impl2
- 多继承:接口可以多继承,类只支持单继承
8 重载和重写的区别
- 重载:发生在一个类里面,方法名相同,参数列表不同(混淆点:跟返回类型没关系)
- 重写:发生在父类子类之间的,方法名相同,参数列表相同
9 List和Set的区别
- List是有序的,可重复的
输入的顺序等于输出的顺序 - Set是无序的,不可重复的
a. 基本数据类型会比较数值是否相等,相等则会去掉重复
b. 引用数据类型会先调用hashCode()比较哈希值,如果一样则会调用equals() 比较值,如果重复则去掉
c.HashSet:元素乱序;LinkedHashSet:保证元素添加顺序;TreeSet:元素按自然顺序排序
10 ArrayList和LinkedList的区别
底层数据结构区别
- ArrayList,数组,连续一块内存空间(动态扩容,扩容机制:当元素的元素长度超过阈值时会创建一个新的数组,长度为元数组的1.5倍,然后将旧数组的数据迁移到新的数组)
- LinkedList,双向链表,不是连续的内存空间
查询效率比较
- 如果是按位置查找则ArrayList比较有优势,因为ArrayList可以通过下标直接定位到查询位置的元素
- 如果是按值查找则双方都没有效率上的区别,都要从头节点开始比较
添加效率比较
- 如果添加在中部,则LinkedList比较有优势,因为只要涉及到前节点和后节点的指针指向的更改即可(迭代器在指定元素后面插入元素),而ArrayList还要涉及到后面元素的移动问题add(int index, E element)
- 如果添加在头部或末尾,ArrayList和LinkedList的效率都差不多
LinkedList<Integer> list = new LinkedList<>();
list.add(2);
list.add(4);
list.addFirst(1);
list.addLast(2);
System.out.println(list.getFirst());
System.out.println(list.getLast());
System.out.println(list.removeFirst());
System.out.println(list.removeLast());
System.out.println(list.size());
list.add(list.size(),666);
list.set(list.size()-1,888);
System.out.println(list);
11 HashSet
HashSet底层是通过HashMap的key进行存储值
12 HashMap为什么是线程不安全的?
put的时候导致的多线程数据不一致
- HashMap的put()并没有加锁或者做其它线程安全的设置,当有多个线程同时往HashMap添加元素的时候可能会发生线程A获取到添加位置索引坐标时时间片用完,而此刻线程B也获取到了该位置的索引坐标并成功执行插入操作,当线程A重新获得时间片调度执行未完成的操作时,因为无法判断索引位置已经被其它线程使用,所以会直接覆盖其它线程的操作结果,造成了数据的不一致性。
- 设想两个线程同时需要执行resize操作,可能局部会出现环形链表,在调用get()方法的时候可能会发生死循环。
解决方法1:Collections.synchronizedMap();本质加synchronized
解决方法2:ConcurrentHashMap采用分段锁
13 简述Java异常体系
public class Throwable implements Serializable {}//异常基类
public class Error extends Throwable {}//非检查型异常,JVM异常:StackOverFlowError(递归异常)、堆异常(内存泄漏)
public class Exception extends Throwable {}//程序报的异常
public class RuntimeException extends Exception {}//运行时异常/逻辑异常/非检查型异常
非运行时异常{}//检查类型异常
检查型异常和非检查型异常的区别
非检查型异常也就是运行时异常,对于运行时异常即使我们在一个方法上throws声明可能抛出异常,编译器也会检测到是运行时异常而不要求我们在主方法中必须处理;对于检查型异常我们throws声明之后也必须处理才可以编译通过。
运行时异常
- ArrayIndexOutOfBoundsException(数组越界访问)
- ArrayStoreException(数据存储异常)
- ArithmeticException(算术运算中,被0除或模除)
- ClassCastException (类型转换异常)
- IllegalThreadStateException(试图非法改变线程状态)
- NumberFormatException(数据格式异常)
- NullPointerException(空指针)
- OutOfMemoryException(内存溢出)
- IndexOutOfBoundsException(指示某排序索引(例如对数组、字符串或向量的排序)超出范围时抛出)
非运行时异常
- IOException
- SQLException
- NoSuchFileException
- NoSuchMethodException
- FileNotFoundException
- ExecutionException
14 throw跟throws的区别
- throw,作用于方法内,用于主动抛出异常
- throws, 作用于方法声明上,声明该方法有可能会抛些某些异常
自定义异常需要继承RunTimeException,在项目中比较合适的做法是将异常层层上抛,并做同一异常处理:返回错误页面 / 错误指定格式的json信息
15 try-catch-finally执行
无异常执行:try-finally
- 如果retrun在try里面则返回try里面的操作结果,如果在finally里面则返回该域里面的操作结果
- 放在外面也会以finally里面的操作结果为主
异常执行:catch-finally
- 如果finally里面有retrun则优先返回finally里面的操作结果,没有则会返回catch或外部的return结果
总结
- 如果finally中有return语句,try、catch中的异常被消化掉了,屏蔽了异常的发生
- 在try里面return的结果只会跟try这个局部操作结果有关,finally里面的操作不会影响try里面的return返回结果
- 如果return 在try-catch-finally的后面,则操作结果与try域,catch域,finally域里面的操作有关
- 一般不会在finally里面try-catch和return,一般只是用于关闭流、数据库连接等。
16 怎么理解线程安全?
16.1 线程安全概念
一个专业的描述是,当多个线程访问一个对象时,如果不用进行额外的同步控制或其他的协调操作,调用这个对象的行为都可以获得正确的结果,我们就说这个对象是线程安全的(StringBuffer内部上锁)
16.2 如何做到线程安全
主要是需要结果并发编程的三大问题:原子性、可见性和有序性问题
- 可以通过加synchronized关键字的方法给方法或代码块加锁,(JVM层面实现,效率不高,可自动释放锁)
- 可以通过Lock锁机制,通过创建Lock对象,采用lock()加锁,unlock()解锁,来保护指定的代码块(Java中的JUC编程实现,需要在代码中的finally子句中显式释放锁lock.unlock())
16.3 什么时候需要考虑线程安全
- 多个线程访问同一个资源(一般为成员变量,局部变量不涉及线程安全问题)
- 一般需要操作资源状态的改变
17 sleep()和wait()的区别
所属类的区别
- sleep方法是定义在Thread上(线程睡眠,当前线程会休眠一段时间,休眠期间,会暂时释放cpu,但并不释放对象锁,也就是说,在休眠期间,其他线程依然无法进入被同步保护的代码内部,当前线程休眠结束时,会重新获得cpu执行权,从而执行被同步保护的代码)
- wait方法是定义在Object上(对象锁,方法会释放占有的对象锁,当前线程进入等待池,释放cpu,而其他正在等待的线程即可抢占此锁,获得锁的线程即可运行程序)
对于锁资源的处理方式不同
- sleep不会释放锁
- wait会释放锁
使用范围
- sleep可以使用在任何代码块
- wait必须在同步方法或同步代码块执行
与wait配套使用的方法
- notify():会唤醒因为调用对象的wait()而处于等待状态的线程,从而使得该线程有机会获取对象锁。调用notify()后,当前线程并不会立即释放锁,而是继续执行当前代码,直到synchronized中的代码全部执行完毕,才会释放对象锁。JVM会在等待的线程中调度一个线程去获得对象锁,执行代码
- notifyAll():唤醒所有等待的线程
wait():当前的线程等待,直到其他线程调用此对象的notify( ) 方法或 notifyAll( ) 方法
生命周期
- 当线程调用wait()或者join时,线程都会进入到waiting状态,当调用notify或notifyAll时,或者join的线程执行结束后,会进入runnable状态
当线程调用sleep(time),或者wait(time)时,进入timed waiting状态
问题一:为什么wait要定义在Object中,而不定义在Thread中?
同步代码块中,需要一个对象锁来实现多线程的互斥效果,Java的锁是对象级别的,而不是程序级别的
问题二:为什么wait必须写在同步代码块中?
原因是避免CPU时间片切换到其他线程,而其他线程又提前执行了notify()方法获取因为wait()而处于等待状态的线程,所以需要一个同步锁来保护。
18 ThreadLocal
作用
- 为每一个线程创建一个变量副本
- 实现在线程的上下文传递一个对象,比如connection
18.1 验证ThreadLocal为每个线程创建一个变量副本
public class ThreadLocalTest02 implements Runnable{
public static ThreadLocal<Long> local = new ThreadLocal<>();
public static void main(String[] args) {
Thread[] threads = new Thread[2];
for (int i = 0; i < threads.length; i++) {
//创建任务
threads[i] = new Thread(new ThreadLocalTest02());
//开启线程任务
threads[i].start();
}
}
@Override
public void run() {
Long i = local.get();
if(i == null){
local.set(System.currentTimeMillis());
}
System.out.println(Thread.currentThread().getName() + "---->" + local.get());
}
}
/*
Thread-1---->1623681259650
Thread-0---->1623681259649
*/
18.2 源码分析ThreadLocal为什么能让每一个线程有自己的一份变量副本
public T get() {
//获取当前线程
Thread t = Thread.currentThread();
//根据当前线程获取ThreadLocalMap
ThreadLocalMap map = getMap(t);
if (map != null) {
//this == threadLocal
ThreadLocalMap.Entry e = map.getEntry(this);
if (e != null) {
@SuppressWarnings("unchecked")
T result = (T)e.value;
return result;
}
}
return setInitialValue();
}
public void set(T value) {
Thread t = Thread.currentThread();
ThreadLocalMap map = getMap(t);
if (map != null)
// this == threadLocal
// value == 变量副本值
map.set(this, value);
else
createMap(t, value);
}
分析得出:每一线程都有自己的独一份ThreadLocalMap
18.3 ThreadLocal实现数据库连接一致
实现原理:线程执行Service层逻辑与数据库交互时共用一个connection,保持多个DAO层操作属于同一个事务内。
public class ConnectionUtils {
private static ThreadLocal<Connection> connectionThreadLocal = new ThreadLocal<>();
public static Connection getConnection(){
Connection connection = connectionThreadLocal.get();
if(connection == null){
connection = new Connection(){
.....
};
connectionThreadLocal.set(connection);
}
return connection;
}
}
public class ThreadLocalTest03 extends Thread{
public static void main(String[] args) {
Thread[] threads = new Thread[2];
for (int i = 0; i < threads.length; i++) {
threads[i] = new Thread(new ThreadLocalTest03());
threads[i].start();
}
}
@Override
public void run() {
Connection connection1 = ConnectionUtils.getConnection();
Connection connection2 = ConnectionUtils.getConnection();
System.out.println(Thread.currentThread().getName() + "---->" + connection1);
System.out.println(Thread.currentThread().getName() + "---->" + connection2);
}
}
public class ThreadLocalTest03 implements Runnable{
public static void main(String[] args) {
Thread[] threads = new Thread[3];
for (int i = 0; i < threads.length; i++) {
threads[i] = new Thread(new ThreadLocalTest03());
threads[i].start();
}
}
@Override
public void run() {
//模拟一个线程处理复杂请求事务问题
Connection connection1 = ConnectionUtils.getConnection();
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
Connection connection2 = ConnectionUtils.getConnection();
System.out.println(Thread.currentThread().getName() + "---->" + connection1);
System.out.println(Thread.currentThread().getName() + "---->" + connection2);
}
}
/*
结果:每一个线程共用一个connection
Thread-1---->com.mxf.gupao.concurrent_program.ConnectionUtils$1@34c05d0e
Thread-0---->com.mxf.gupao.concurrent_program.ConnectionUtils$1@69656e74
Thread-2---->com.mxf.gupao.concurrent_program.ConnectionUtils$1@bc499b1
Thread-0---->com.mxf.gupao.concurrent_program.ConnectionUtils$1@69656e74
Thread-1---->com.mxf.gupao.concurrent_program.ConnectionUtils$1@34c05d0e
Thread-2---->com.mxf.gupao.concurrent_program.ConnectionUtils$1@bc499b1
*/
19 类加载机制(双亲委托)
19.1 JVM加载Java源文件
- Java源文件----编译---->class文件
- 类加载器ClassLoader读取这个.class文件,并将其转化为java.lang.Class的实例。然后JVM就可以使用他来创建对象,调用方法等操作
19.2 Class文件来源
- Java内部自带的核心类,位于$JAVA_HOME/jre/lib,其中最著名的莫过于rt.jar
- Java的扩展类,位于$JAVA_HOME/jre/lib/ext目录下
- 我们自己开发的类或项目开发用到的第三方jar包,位于我们项目的目录下,比如WEB-INF/lib目录
19.3 类加载器分类
- BootStrapClassLoader(根加载器,由JVM内部实现,非java语言编写)
- ExtClassLoader(扩展类加载器,Java中的ext包下的拓展类加载器)
- AppClassLoader(系统类加载器,项目中编写的类加载器)
19.4 双亲委托机制
- 概念:
双亲委托机制,就是加载一个类,会先获取到一个系统类加载AppClassLoader的实例,然后往上层层请求,先由BootstarpClassLoader去加载,如果BootStrapClassLoader发现没有,再下发给ExtClassLoader去加载,还是没有,才由AppClassLoader去加载。- 原因:
防止Java内部核心代码被外部代码污染,提高Java的安全性
20 Ajax的工作原理
客户端发送请求到Ajax引擎进行数据校验和处理,代为向服务端发送请求,然后通过回调函数实现异步局部刷新页面
21 Servlet的生命周期
生命周期的流程:
- 创建对象–>初始化–>service()分发请求–>doXXX()–>销毁
创建对象的时机:
- 默认是第一次访问该Servlet的时候创建
也可以通过配置web.xml,来改变创建时机执行的次数:
- 对象的创建只有一次,单例
- 初始化一次,销毁一次
Servlet是否是线程安全的
- 线程不安全的因素:多线程环境(多个客户端同时访问),共享资源(Servlet本是单例),单例对象有状态(取决于有没有全局变量)
- 如果在Servlet定义了全局变量的话,多线程的情况下可能操作相同资源变量,可能会引起线程安全问题。
22 转发和重定向
22.1 概念
转发
- 发生在服务器内部,客户端地址没有改变,request对象中的数据是可以传递的
重定向- 发生在客户端的跳转,属于多次请求,需要在多次请求之间传递数据,就需要用Session对象进行内存存储信息
22.2 java实现
// 转发
request.getRequestDispatcher("地址").forward(request, response);
// 重定向
response.sendRedirect("地址");
23 Session跟Cookie的区别
存储的位置
- Session:存储在服务端的内存中
- Cookie:存储在浏览器中
存储的数据格式不同
- Session:value为对象,Object类型
- Cookie:value为字符串,存储对象可转换为JSON
存储的数据大小
- Session:受服务器内存控制
- Cookie:一般来说,最大为4k
生命周期不同
- Session:服务器端控制,默认是30分钟,注意,当用户关闭了浏览器,session并不会消失,长时间没有使用Session,服务器才会清除Session
- 默认的是会话级的cookie,随着浏览器的关闭而消失:保存sessionId的cookie,非会话级cookie,可通过设置有效期来控制
cookie的其他配置
- httpOnly=true:防止客户端的XSS攻击
- path="/" :访问路径
- domain="":设置cookie的域名
session和cookie比较
- session比cookie更加的安全,将关键数据存在服务端,cookie可以通过浏览器控制台信息看到
session和cookie关联
- HTTP协议是无状态的,Session不能依据HTTP连接来判断是否为同一客户,因此服务器向客户端浏览器发送一个名为sessionid的Cookie,它的值为该Session的id(HttpSession.getId())。Session依据该Cookie来识别是否为同一用户。
// Session操作
request.getSession().setAttribute("xxx",xxx);//储存到session中
request.getSession().getAttribute("xxx",xxx);//读取session中的值
request.getSession().removeAttribute("xxx");//移除
request.getSession().invalidate();//注销该request的所有session
request.getSession().setMaxInactiveInterval(-1);//永不过期
//Cookie操作
Cookie c = new Cookie("name","xiaoming");
c.setMaxAge(60)//参数单位为秒
c.setMaxAge(0)// 表示会删除浏览器指定的cookie
c.setMaxAge(-1)// 代表关闭浏览器cookie就会失效
如果不设置过期时间则一般只存储在内存中,当关闭浏览器窗口时,cookie就会消失
24 理解MVC
使用的是分层思想,分工明确易于解耦
- Model:代表一个存取数据的对象或 JAVA POJO
- View:代表模型包含的数据的可视化,例如:HTML,JSP,Thymeleaf,FreeMarker,vue
- Controller:作用于模型和视图上,控制数据流向模型对象,并在数据变化时更新视图,使视图与模型分离开,例如:Servlet,Controller
常见的MVC框架
- Struts1,Struts2
- SpringMVC
SpringMV可分为两种控制器
- DispatchServlet:前端控制器,由它来接收客户端的请求
- 根据客户端请求的URL的特点,分发到对应的业务控制器
25 数据库设计三范式
三范式
- 第一范式:列不可分
- 第二范式:要有主键
- 第三范式:不可存在传递依赖
反三范式设计,可存在传递依赖
- 提高查询效率,如查询商品信息的时候,经常伴随着商品类别信息的展示,如果没有商品类别信息在商品表中进行存储的话,可能就需要关联查询才能查询出结果,效率不高,如果能通过一个冗余字段就能实现单表查询出结果的话能提高不小的速率
- 保存订单快照信息,如订单表里面存在收货人的相关地址,联系方式等,如果通过关联id进行存储的话,在后面用户对收货地址进行更改的话,前面的订单收获地址信息也会跟着改变,这是不合理的
26 SQL注入
26.1 概念
- 通过输入某些参数,达到改变sql语句本身的含义,这种情况就称为sql注入
26.2 解决方案
- 通过JDBC提供的PreparedStatement实现预编译可以方式防止SQL注入,禁用SQL语句字符串拼接
- MyBatis框架使用#{}占位符的方式处理SQL注入问题,${}则不行
- 通过正则表达式过滤存过来的参数,转义特殊字符
- 前端js校验用户输入是否存在非法字符
27 JDBC如何控制事务及事务边界
JDBC对事务的操作是基于Connection来进行控制的,一般放在业务层
try {
//默认为true,自动提交事务,所以需要先改为手动提交事务
connection.setAutoCommit(false);
//做业务操作
//doSomething1();
//doSomething2();
//统一提交事务
connection.commit();
}catch(Exception e){
//回滚事务
try {
connection.rollback();
} catch (SQLException e1) {
e1.printStackTrace();
}
}
28 数据库事务特点ACID
- 原子性( Atomicity )描述的是数据库的逻辑工作单位,一个事务包含的左右操作都要完成,或者所有操作统一做回滚操作
- 一致性( Consistency )描述的是数据库的的数据在事务操作前后必须要满足业务规则约束,比如说转账前后的金额总和一致,贩卖商品数量不可能为负数等
- 隔离性( Isolation )描述的是在并发执行过程,事务之间操作的数据是相互隔离的,并发事务之间互不干扰,这也是数据库事务隔离级别需要解决的问题
- 持久性( Durability )描述的是一个事务提交后,操作结果会被保存到系统磁盘中,即使中途发生宕机,也能通过事务日志完成数据的恢复,日志主要包括回滚日志(undo)和重做日志(redo)
29 数据库 事务隔离级别
英文 | 中文 | 描述 | 事务影响 |
---|---|---|---|
READ UNCOMMITTED | 读未提交 | 一个事务未提交的数据对其它事务可见 | 脏读、不可重复读、幻读有可能发生 |
READ COMMITTED | 读已提交 | 一个已提交的更新、删除事务对其它事务可见 | 可避免脏读的发生,但不可重复读、幻读有可能发生 |
REPEATABLE READ | 可重复读 | 在一个事务中多次读取同样的数据结果是一致 的 | 可避免脏读、不可重复读的发生,但幻读有可能发生(INNODB为发生幻读) |
SERIALIZABLE | 串行化 | 不存在并性行为,强制事务串行执行 | 可避免脏读、不可重复读、幻读的发生,但性能会影响比较大 |
30 synchronized与lock
原文链接【Java并发编程:Lock,作者: Matrix海子】
synchronized作用:如果一块代码被synchronized修饰了,当一个线程去获取对应的锁,并执行代码方法时,其它线程就只能一直等待,等待当前线程释放锁后再抢占锁资源
抢占锁的线程释放锁的条件:
- 获取锁的线程执行完该代码块,然后释放锁
- 发生异常,JVM让线程自动释放锁
synchronized导致坏的结果:
- 如果一个抢占锁的线程由于要等待IO调度或其它原因被阻塞了,没有释放锁,导致其它线程也只能长时间的等待(Lock能响应中断)
- 并发安全问题一般会发生在(读-写操作)或者(写-写操作)中,(读-读操作不会发生并发问题),如果使用synchronized实现同步的话则所有的情况都会加锁,阻塞其它线程的执行
Lock和synchronized的实现
- synchronized是基于JVM的实现;Lock是java并发包的一个接口,通过这个类可以实现同步访问
- synchronized可以自动释放锁;lock需要用户手动释放锁,否则可能会出现死锁现象
- Lock可以提高多个线程进行读操作的效率
- 通过Lock可以知道有没有成功获取锁,而synchronized却无法办到
Lock获取锁的四个方法
- lock() – 如果锁已被其他线程获取,则进行等待
- tryLock() – 用来尝试获取锁-true,则返回true,如果获取失败-false,不会等待
- tryLock(long time, TimeUnit unit) – 先等待一段时间,然后再返回结果
- lockInterruptibly() – 如果线程正在等待获取锁,则这个线程能够响应中断等待状态
Lock释放锁
- unlock()
31 TCP和UDP的区别
相同点
- 两者都是传输层的协议
不同点
- TCP提供可靠的传输协议,传输前需要建立连接,面向字节流,传输慢
- UDP无法保证传输的可靠性,无需创建连接,以报文的方式传输,效率高
32 TCP三次挥手
目的在于让彼此确认对方已经收到自己发的消息,才同意创建安全连接
- 客户端给服务端发送创建连接的请求
- 服务端受到客户端连接请求之后,返回确认字符给客户端
- 客户端接收到服务端的确认字符后,在通知服务端自己已经收到服务端发送过来的确认字符消息
33 TCP四次挥手
- 客户端发送断开连接的请求给服务端
- 服务端接收到客户端的请求后,关闭输出流,并返回确认字符信息给客户端
- 过了一段时间后,服务端就会关闭输入流,再次返回确认字符信息给客户端
- 客户端在接收到两次来自服务端的确认字符后,就会返回确认字符信息给服务端
34 什么是死锁?如何预防死锁?
概念: 指一组线程因为竞争资源而相互等待,导致永久阻塞的现象叫死锁,线程A独占资源a,线程B独占资源b,然后线程A想再获取资源b,需要等待线程B释放资源b,而线程B再想获得资源a,需要等待线程A释放资源a,因为相互阻塞而导致永久导致阻塞
破坏死锁条件: 链接
35 什么是反射
概念: 反射指程序在运行状态中,可以动态获取类的所有内容,包括属性、构造器、方法,并看可以给属性赋值,调用方法,让java程序具备动态性
Class代表所有的字节码对象的抽象类
Class clazz = Class.forName("类全路径名");
Spring通过@Autowrie 就能实现自动注入
本质上也是通过java反射机制扫描主程序下包类上、属性上、方法上的注解,然后获取对应的类型进行进行实例化,然后给属性赋值
36 对Spring的认识
Spring 的框架结构主要包括数据访问层,Web层,AOP以及Spring核心IOC
- Spring 是一个轻量级的 DI / IoC 和 AOP 容器的开源框架
- 低侵入 / 低耦合 (降低组件之间的耦合度,实现软件各层之间的解耦)
- 声明式事务管理,面向切面编程
- 方便集成其他框架
- 降低 Java 开发难度
- DI:若要使用某个对象,只需要从 Spring 容器中获取需要使用的对象,不关心对象的创建过程,也就是把创建对象的控制权反转给了Spring框架
- AOP:在面向切面编程的思想里面,把功能分为核心业务功能,和周边功能。
37 Spring的bean作用域
构成应用程序主干并由Spring IoC容器管理的对象称为bean。bean是一个由Spring IoC容器实例化、组装和管理的对象
配置文件设置scope
- 默认是singleton,即单例模式(Spring默认)
- prototype,每次从容器调用bean时都会创建一个新的对象,比如整合Struts2框架的时候,spring管理action对象则需要这么设置。
- request,每次http请求都会创建一个对象
- session,同一个session共享一个对象
- global-session
38 Spring的Bean是线程安全的吗
与Bean的作用域有关
- prototype,每一个线程之间并不存在Bean共享,线程安全
- singleton,共享一个Bean,如果Controller、Service、Dao存在实例变量的对象,则说明这个资源是有状态的,所以可能会存在线程安全问题
对于有状态的bean,可以通过ThreadLocal去解决线程安全问题(在多线程的环境下,能为每一个线程保存私有的数据TreadLocalMap)
也可以通过加锁的方式解决线程安全问题
39 synchronized底层实现
synchronized是由 一对monitorenter和monitorexit指令来实现同步的
- 在JDK6之前,monitor的实现是依靠操作系统内部的互斥锁来实现的,需要进行用户态和内核态的切换,是一个重量级的操作,性能很低
- JDK6带了锁升级的实现,偏向锁–>轻量级锁–>重量级锁
- 锁对象的对象头里面threadid字段,当为空的情况下,线程第一次访问,则将threadid设置为当前线程id,称为获取偏向锁,当线程执行完操作之后,会将threadid置为空
- 当threadid不为空的时候,又有线程来尝试获取锁,会先比较当前线程id是否与threadid一致,如果一致则可以获取该锁对象,如果不一致,则发生锁升级,从偏向锁升级为轻量级锁
- 轻量级的锁是通过自旋循环的工作方式来获取锁的,看当前占有锁的线程是否释放锁,执行一定次数之后,如果还没有获取到锁的话,则发生锁升级,从轻量级锁升级为重量级锁
使用锁升级的目的主要是为了减少锁带来的性能开销