经典Java面试题(第1季)
一、JavaSE 面试题
1、自增变量
代码的执行结果是什么?
@Test
public void test1(){
int i = 1;
i = i++;
int j = i++;
int k = i + ++i * i++;
System.out.println("i = " + i);
System.out.println("j = " + j);
System.out.println("k = " + k);
}
结果如下:
i = 4
j = 1
k = 11
接下来逐步分析
第一步: i = i++;
第二步:int j = i++;
第三部: int k = i + ++i * i++;
总结
1、赋值 =,最后计算
2、= 右边的从左到右加载值以此压入操作数栈
3、实际先算哪个,看运算符优先级
4、自增、自减操作都是直接修改变量的值,不经过操作数栈
5、最后的赋值之前,临时结果也是存储在操作数栈中
2、单例设计模式
2.1、什么是Singleton [英 /ˈsɪŋɡltən/]
- Singleton:在Java中即指单例设计模式,它是软件开发中最常用的设计模式之一。
- 单:唯一
- 例:实例
- 单例设计模式,即某个类在整个系统中只能有一个实例对象可被获取和使用的代码模式
- 例如:代表 JVM 运行环境的Runtime类
2.2、要点
- 一是某个类只能有一个实例;
- 构造器私有化
- 二是它必须自行创建这个实例;
- 含有一个该类的静态变量来保存这个唯一的实例
- 三是它必须自行向整个系统提供这个实例
- 对外提供获取该实例对象的方式
- (1)直接暴露 (2)用静态变量的 get() 方法获取
- 对外提供获取该实例对象的方式
3、几种常见形式
- 饿汉式:直接创建对象,不存在线程安全问题
- 直接实例化饿汉式(简洁直观)
- 枚举式(最简洁)
- 静态代码块饿汉式(适合复杂实例化)
- 懒汉式:延迟创建对象
- 线程不安全(适用于单线程)
- 线程安全(适用于多线程)
- 静态内部类形式(适用于多线程)
饿汉式:直接创建对象,不存在线程安全问题
1)饿汉式: 直接创建实例对象
/**
* 饿汉式:
* 类初始化时,直接创建实例对象,不管你是否需要这个对象都会创建
*
* (1)构造器私有化
* (2)自行创建,并且用静态变量保存
* (3)向外部提供这个实例
* (4)强调这是一个单例,我们可以使用final修饰
*
*/
public class Singleton01{
public static final Singleton01 INSTANCE = new Singleton01();
private Singleton01(){
}
}
2)饿汉式: 枚举式(最简洁)
/**
* 饿汉式:
* 枚举类型:表示该类型的对象是有限的几个
* 我们可以限定为一个,就成了单例
*/
public enum Singleton02{
INSTANCE
}
3)饿汉式: 静态代码块饿汉式
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import lombok.ToString;
import java.io.IOException;
import java.util.Properties;
@ToString
@NoArgsConstructor
@AllArgsConstructor
@Data
public class Singleton03 {
public static final Singleton03 INSTANCE;
private String info;
static {
try {
Properties pro = new Properties();
pro.load(Singleton03.class.getClassLoader().getResourceAsStream("singleton.properties"));
INSTANCE = new Singleton03(pro.getProperty("info"));
} catch (IOException e) {
throw new RuntimeException();
}
}
public static void main(String[] args) {
Singleton03 instance = Singleton03.INSTANCE;
System.out.println("instance = " + instance);
}
}
打印结果: instance = Singleton03(info=Hallo Word)
singleton.properties 文件内容
info=Hallo Word
懒汉式:延迟创建对象
1)饿汉式: 线程不安全(适用于单线程)
/**
* 懒汉式:
* 延迟创建这个实例对象
* (1)构造器私有化
* (2)用一个静态变量保存这个实例
* (3)提供一个静态方法,获取这个实例对象
*/
public class Singleton04 {
private static Singleton04 instance;
private Singleton04(){
}
public static Singleton04 getInstance(){
if (instance == null) {
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
instance = new Singleton04();
}
return instance;
}
public static void main(String[] args) throws Exception {
/*Singleton04 s1 = Singleton04.getInstance();
Singleton04 s2 = Singleton04.getInstance();
System.out.println("s1 = " + s1);
System.out.println("s2 = " + s2);
System.out.println("result:"+(s1 == s2));*/
/**
* 单线程下
* 执行结果:
* s1 = com.feimai.test.firstInterview.P1Demo.Singleton04@4f023edb
* s2 = com.feimai.test.firstInterview.P1Demo.Singleton04@4f023edb
* result:true
*/
Callable<Singleton04> callable = () -> Singleton04.getInstance();
ExecutorService es = Executors.newFixedThreadPool(2);
Future<Singleton04> f1 = es.submit(callable);
Future<Singleton04> f2 = es.submit(callable);
Singleton04 s1 = f1.get();
Singleton04 s2 = f2.get();
System.out.println("s1 = " + s1);
System.out.println("s2 = " + s2);
System.out.println("result:" + (s1 == s2));
es.shutdown();
/**
* 多线程下
* 执行结果:
* s1 = com.feimai.test.firstInterview.P1Demo.Singleton04@76fb509a
* s2 = com.feimai.test.firstInterview.P1Demo.Singleton04@76fb509a
* result:true
*/
/**
* getInstance 方法中加个休眠方法后
* s1 = com.feimai.test.firstInterview.P1Demo.Singleton04@76fb509a
* s2 = com.feimai.test.firstInterview.P1Demo.Singleton04@300ffa5d
* result:false
*/
}
}
2)饿汉式: 双重检查(线程安全,适用于多线程)
/**
* 双重检查(线程安全,适用于多线程)
*/
public class Singleton05 {
// 加 volatile 作用:防止指令重排, 当实例变量有修改时,能刷到主存中去是一个原子操作,并且保证可见性。
private static volatile Singleton05 instance;
private Singleton05() {
}
public static Singleton05 getInstance() {
if (instance == null) {
synchronized (Singleton05.class) {
if (instance == null) {
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
instance = new Singleton05();
}
}
}
return instance;
}
public static void main(String[] args) throws Exception {
Callable<Singleton05> callable = () -> Singleton05.getInstance();
ExecutorService es = Executors.newFixedThreadPool(2);
Future<Singleton05> f1 = es.submit(callable);
Future<Singleton05> f2 = es.submit(callable);
Singleton05 s1 = f1.get();
Singleton05 s2 = f2.get();
System.out.println("s1 = " + s1);
System.out.println("s2 = " + s2);
System.out.println("result:" + (s1 == s2));
es.shutdown();
/**
* 执行结果:
* s1 = com.feimai.test.firstInterview.P1Demo.Singleton04@76fb509a
* s2 = com.feimai.test.firstInterview.P1Demo.Singleton04@300ffa5d
* result:false
*/
}
}
3)饿汉式: 静态内部类模式(线程安全,适用于多线程)
/**
* 静态内部类模式(适用于多线程)
*/
public class Singleton06 {
/**
* 1、内部类被加载和初始化时,才创建singleton实例对象
* 2、静态内部类不会自动创建,不会随着外部类的加载初始化而初始化,他是要单独去加载和实例化的
* 3、因为是在内部类加载和初始化时,创建的,因此线程安全
*/
private Singleton06() {
}
private static class Inner{
private static volatile Singleton06 singleton = new Singleton06();
}
public static Singleton06 getInstance(){
return Inner.singleton;
}
public static void main(String[] args) throws Exception {
Callable<Singleton06> callable = () -> Singleton06.getInstance();
ExecutorService es = Executors.newFixedThreadPool(2);
Future<Singleton06> f1 = es.submit(callable);
Future<Singleton06> f2 = es.submit(callable);
Singleton06 s1 = f1.get();
Singleton06 s2 = f2.get();
System.out.println("s1 = " + s1);
System.out.println("s2 = " + s2);
System.out.println("result:" + (s1 == s2));
es.shutdown();
/**
* 执行结果:
* s1 = com.feimai.test.firstInterview.P1Demo.Singleton06@76fb509a
* s2 = com.feimai.test.firstInterview.P1Demo.Singleton06@76fb509a
* result:true
*/
}
}
总结:
1、如果是饿汉式,枚举形式最简单
2、如果是懒汉式,静态内部类最简单
3、类初始化和实例初始化
首先看一道题,题目如下:
3.1、考点
1)类初始化过程
2)实例初始化过程
3)方法的重写
3.2、类初始化过程
-
1)、一个类要创建实例需要先加载并初始化该类
- main方法所在的类需要先加载和初始化
-
2)、一个子类要初始化需要先初始化父类
-
3)、一个类初始化就是执行 <clinit>() 方法
- <clinit>() 方法由静态类变量显示赋值代码 和 静态代码块 组成
- 类变量显示赋值代码 和 静态代码块 从上到下 顺序执行
- <clinit>() 方法只执行一次
3.3、实例初始化过程
- 1)、实例初始化就是执行<init>() 方法
- <init>() 方法可能重载有多个,有几个构造器就有几个<init>() 方法
- <init>() 方法由非静态实例变量显示赋值代码 和 非静态代码块、对应构造器代码块组成
- 非静态实例变量显示赋值代码 和 非静态代码块代码从上到下顺序执行,而对应构造器的代码最后执行
- 每次创建实例对象,调用对应构造器,执行的就是对应的<init>() 方法
- <init>() 方法的首行是 super() 或 super(实参列表),即对应父类的 <init>() 方法
3.4、方法的重写
- 1)、哪些方法不可以被重写
- final 方法
- 静态方法
- private 等子类中不可见方法
- 2)、对象的多态性
- 子类如果重写了父类的方法,通过子类对象调用的一定是子类重写过得代码
- 非静态方法默认的调用对象就是 this
- this 对象在构造器或者说 <init> 方法中就是正在创建的对象
3.5、 代码
Father 类
/**
* 父类的初始化 <clinit>()
* (1) j = method(); 静态变量
* (2) 父类的静态代码块
*
*
* 父类的实例方法:
* (1) super(); 在首行
* (2) i = test(); 非静态变量
* (3) 父类的非静态代码块
* (4) 父类的无参构造(最后)
*
*
* 非静态方法前面其实有一个默认的对象是 this
* this 在构造器(或 <init>() )它表示的是正在创建的对象,
* 因为这里是在创建 Son 对象,所以 test() 执行的是
* 子类重写的代码(面向对象 多态)
*
* 这里 i = test() 执行的是 子类重写的 test() 方法
*/
public class Father {
private int i = test();
private static int j = method();
static {
System.out.print("(1)");
}
public Father() {
System.out.print("(2)");
}
{
System.out.print("(3)");
}
public int test() {
System.out.print("(4)");
return 1;
}
public static int method() {
System.out.print("(5)");
return 1;
}
}
Son类
/**
* 子类的初始化 <clinit>()
* (1) j = method(); 静态变量
* (2) 子类的静态代码块
*
* 先初始化父类:(5)(1)
* 初始化子类:(10)(6)
*
* 子类的实例方法 <init>:
* (1) super(); 在首行 (9)(3)(2)
* (2) i = test(); 非静态变量 (9)
* (3) 子类的非静态代码块 (8)
* (4) 子类的无参构造(最后) (7)
*
* 因为 创建了两个 Son 对象,因此 子类的实例方法 <init> 执行了两次
* 所以 顺序为 (9)(3)(2)(9)(8)(7)
*/
public class Son {
private int i = test();
private static int j = method();
static {
System.out.print("(6)");
}
public Son() {
/**
* super();
* 写或者不写都在,在子类构造器中一定会调用父类的构造器
*/
System.out.print("(7)");
}
{
System.out.print("(8)");
}
public int test() {
System.out.print("(9)");
return 1;
}
public static int method() {
System.out.print("(10)");
return 1;
}
public static void main(String[] args) {
Son s1 = new Son();
System.out.println();
Son s2 = new Son();
}
}
3.6、 运行结果
执行结果
(5)(1)(10)(6)(9)(3)(2)(9)(8)(7)
(9)(3)(2)(9)(8)(7)
3.7、进阶要求
-
Override 和 Overload 区别 ?
-
Override 重写的要求
- 方法名
- 形参列表
- 返回值类型
- 抛出的异常列表
- 修饰符
- 了解《JVM虚拟机规范》中关于和方法的说明、invokespecial指令
4、方法的参数传递机制
首先看一道题,题目如下:
4.1、考点
- 1)、方法的参数传递机制
- 2)、String、包装类等对象的不可变性
4.2、分析图
或者
4.3、方法的参数传递机制
- 1)、形参是基本数据类型
- 传递数据值
- 2)、实参是引用数据类型
- 传递地址值
- 特殊的类型:String、包装类等对象不可变性
4.4、代码
/**
* 方法的参数传递机制
*/
public class demo4{
public static void main(String[] args) {
int i = 1;
String str = "hello";
Integer num = 200;
int[] arr = {1, 2, 3, 4, 5};
MyData myData = new MyData();
change(i, str, num, arr, myData);
System.out.println("i = " + i); // i = 1; j = 2
System.out.println("str = " + str); // str = hello ; s = helloworld
System.out.println("num = " + num); // num = 200 ; n = 201
System.out.println("arr = " + Arrays.toString(arr)); // 2, 2, 3, 4, 5
System.out.println("my.a = " + myData.a); // 11
}
public static void change(int j, String s, Integer n, int[] a, MyData m) {
j += 1;
s += "world";
n += 1;
a[0] += 1;
m.a += 1;
}
}
class MyData {
int a = 10;
}
4.5、运行结果
i = 1
str = hello
num = 200
arr = [2, 2, 3, 4, 5]
my.a = 11
5、递归 与 迭代
首先先看一刀编程面试图。题目如下:
编程题: 有n步台阶,一次只能上 1步 或 2步,共有多少种走法?
5.1、递归
分析下图,当 n 等于 1 或者 2 时,走法就等于 n ,从第三层台阶开始,每一层台阶为 前两层台阶 的走法之和。
5.2、循环迭代
用 one、tow 这两个变量赖存储 n 的 最后走一步 和 最后做两步,从第三层开始走,用 sum 来保存 前两次走法的次数 , sum = two + one ,然后 two 移到 one,one 移到 sum 上,循环迭代。
5.3 代码
public class TestStep {
@Test
public void test(){
Instant ins1 = Instant.now();
//System.out.println("f(40) = " + f(40)); // 165580141
System.out.println("f(40) = " + loop(40)); // 165580141
Instant ins2 = Instant.now();
long millis = Duration.between(ins1, ins2).toMillis();
// System.out.println("f(40) ->millis = " + millis); // 431 ms
System.out.println("loop(40) ->millis = " + millis); // 0 ms
}
// 递归: 实现 f(n) :求 n 步台阶,一共有几种走法
public int f(int n){
if (n < 1) {
throw new IllegalArgumentException(n + "不能小于1");
}
if (n == 1 || n == 2) {
return n;
}
return f(n-2) + f(n-1);
}
// 循环迭代
public int loop(int n){
if (n < 1) {
throw new IllegalArgumentException(n + "不能小于1");
}
if (n == 1 || n == 2) {
return n;
}
int one = 2; // 初始化为走到第二级台阶的走法
int two = 1; // 初始化为走到第一级台阶的走法
int sum = 0;
for (int i = 3; i <= n ; i++) {
// 最后跨 2 步 + 最后跨 1 步的走法
sum = two + one;
two = one;
one = sum;
}
return sum;
}
}
5.4、 总结
- 1)、方法调用自身称为 递归,利用变量的 原值 推出 新值 称为 迭代
- 2)、递归
- 优点: 大问题转化为小问题, 可以减少代码量,同时代码精简,可读性好;
- 缺点: 递归调用浪费空间,而且递归太深容易造成堆栈的溢出。
- 3)、迭代
- 优点: 代码运行效率好,因为时间只因 循环次数 增加而增加,而且没有额外的空间开销;
- 缺点: 代码不如递归简洁,可读性不好。
6、成员变量和局部变量
首先看一道题,题目如下:
6.1、考点
- 1)、就近原则
- 2)、变量的分类
- 成员变量: 类变量、实例变量
- 局部变量
- 3)、非静态代码块的执行:每次创建实例对象都会执行
- 4)、方法的调用规则:调用一次 执行一次
6.2、画图分析
6.3、局部变量和成员变量的区别:
- 1)、声明的位置:
- 局部变量: 方法体 { } 中,形参,代码块 { } 中
- 成员变量:类中,方法外
- 类变量:有 static 修饰
- 实例变量: 没有 static 修饰
- 2)、 修饰符
- 局部变量: final
- 成员变量:public、protected、private、final、static、volatile、transient
- 3)、值 存储的位置
- 局部变量: 栈
- 实例变量:堆
- 类变量:方法区
- 4)、作用域
- 局部变量:从声明处开始,到所属的 } 结束
- 实例变量:在当前类中 " this. "(有时this. 可以缺省),在其他类中 " 对象名. " 访问
- 类变量:在当前类中 " 类名. "(有时类名. 可以缺省),在其他类中 " 类名 . " 或者 " 对象名 . " 访问
- 5)、生命周期
- 局部变量:每一个线程,每一次调用执行都是新的生命周期
- 实例变量:随着对象的创建二初始化,随着对象的被回收而消亡,每一个对象的实例变量是独立的
- 类变量:随着类的初始化而初始化,随着类的卸载而消亡,该类的所有对象的类变量是共享的
6.4、当局部变量与 xx 变量重名时,如何区分
- 1)、局部变量 与 实例变量重名
- 在实例变量前面加 " this . "
- 2)、局部变量 与 类变量重名
- 在 类变量 前面加 " 类名 . "
6.4、代码
public class Exam5 {
static int s;//成员变量,类变量
int i;//成员变量,实例变量
int j;//成员变量,实例变量
{
int i = 1;//非静态代码块中的局部变量 i
i++;
j++;
s++;
}
public void test(int j){//形参,局部变量,j
j++;
i++;
s++;
}
public static void main(String[] args) {//形参,局部变量,args
Exam5 obj1 = new Exam5();//局部变量,obj1
Exam5 obj2 = new Exam5();//局部变量,obj1
obj1.test(10);
obj1.test(20);
obj2.test(30);
System.out.println(obj1.i + "," + obj1.j + "," + obj1.s); // 2 1 5
System.out.println(obj2.i + "," + obj2.j + "," + obj2.s); // 1 1 5
}
}
二、SSM 面试题
1、Spring Bean 的作用域 之间有什么区别?
在 Spring 的配置文件中,给 bean 加上 scope 属性 来指定 bean 的作用域 如下:
- singleton: 默认值的,当IOC容器一创建就会创建bean的实例,而且是单例的,每次得到的都是同一个。
- prototype: 原型的。当IOC容器一创建不再实例化该bean,每次调用getBean方法时再实例化该bean,而且每调用一次创建一个对象
- request: 每一次 HTTP 请求都会产生一个新的 bean ,该 bean 仅在当前 HTTP request 内有效。
- session: 在一次会话中共享一个bean。
配置文件如下:
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:context="http://www.springframework.org/schema/context"
xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd
http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context-4.0.xsd">
<!-- ★bean的作用域
可以通过scope属性来指定bean的作用域
-singleton:默认值。当IOC容器一创建就会创建bean的实例,而且是单例的,每次得到的都是同一个
-prototype:原型的。当IOC容器一创建不再实例化该bean,每次调用getBean方法时再实例化该bean,而且每调用一次创建一个对象
-request:每次请求实例化一个bean
-session:在一次会话中共享一个bean
-->
<bean id="book" class="com.atguigu.spring.beans.Book" scope="prototype">
<property name="id" value="8"></property>
<property name="title" value="红高粱"></property>
<property name="author" value="莫言"></property>
<property name="price" value="10.00"></property>
<property name="sales" value="800"></property>
</bean>
</beans>
2、Spring 支持的常用数据库 事务传播行为 和 事务的隔离级别?
声明式事务:
在 Spring 中,声明式事务是用 事务参数 来定义的。一个事务参数 就是对 事务策略 应该如何应用到某个方法的 一段描述, 如下图所示,一个事务参数 共有 5个方面组成。
1、事务的传播行为
事务的传播行为:指的就是当一个事务方法被另一个事务方法调用时,这个事务方法应该如何进行。Spring 中定义了 7种 传播行为。
支持当前事务的情况:
传播行为 | 意义 |
---|---|
@Transactional(propagation=Propagation.REQUIRED) | 如果当前方法存在一个事务,则将该方法置于同一个事物中,如果之前不存在事务,则另新开启一个事物(delete ,insert update)。 |
@Transactional(propagation=Propagation.SUPPORTS) | 如果当前方法存在一个事务,则将该方法置于同一个事物中,如果之前不存在事务,则进行非事务执行(select) |
@Transactional(propagation=Propagation.MANDATORY) | 如果已经存在一个事务,支持当前事务。如果没有一个活动的事务,则抛出异常。 |
不支持当前事务的情况:
传播行为 | 意义 |
---|---|
@Transactional(propagation=Propagation.REQUIRES_NEW) | 表示当前方法必须在它自己的事务里运行。一个新的事务将被启动,而且如果有一个现有事务在运行的话,则将在这个方法运行期间被挂起。 |
@Transactional(propagation=Propagation.NOT_SUPPORTED) | 表示该方法不应该在一个事务中运行。如果一个现有事务正在进行中,它将在该方法的运行期间被挂起。 |
@Transactional(propagation=Propagation.NEVER) | 表示当前的方法不应该在一个事务中运行。如果一个事务正在进行,则会抛出一个异常。 |
其他情况:
传播行为 | 意义 |
---|---|
@Transactional(propagation=Propagation.NESTED) | 表示如果当前正有一个事务在进行中,则该方法应当运行在一个嵌套式事务中。被嵌套的事务可以独立于封装事务进行提交或回滚。如果封装事务不存在,行为就像PROPAGATION_REQUIRES一样。 |
2、事务的隔离级别
隔离级别定义 一个事务可能受其他 并发事务活动影响的程度。
在一个典型的应用程序中,多个事务同时运行,经常会为了完成他们的工作而操作同一个数据。并发虽然是必需的,但是会导致一些问题:
- 脏读(Dirty read 读未提交): 脏读发生在一个事务读取了 被 另一个事务改写 但尚未提交的 数据时。如果这些改变在稍后被回滚了,那么第一个事务 读取的数据 就是无效的。
- 不可重复读(Nonrepeatable read): 不可重复读发生在一个事务执行 相同的查询两次 或两次以上,但 每次查询结果都不相同时。这通常是由于 另一个并发事务 在两次查询之间 更新了数据。
- 幻影读(Phantom reads): 幻影读和不可重复读相似。当一个事务(T1)读取几行记录后,另一个并发事务(T2)插入了一些记录时,幻读就发生了。在后来的查询中,第一个事务(T1)就会发现一些原来没有的额外记录。
在理想状态下,事务之间将完全隔离,从而可以防止这些问题发生。然而,完全隔离会影响性能,因为隔离经常牵扯到锁定在数据库中的记录(而且有时是锁定完整的数据表)。侵占性的锁定会阻碍并发,要求事务相互等待来完成工作。
考虑到完全隔离会影响性能,而且并不是所有应用程序都要求完全隔离,所以有时可以在事务隔离方面灵活处理。因此,就会有好几个隔离级别。
隔离级别 | 含义 |
---|---|
@Transactional(isolation=Isolation.DEFAULT) | 使用后端数据库默认的隔离级别,Mysql 默认采用的 REPEATABLE_READ隔离级别 Oracle 默认采用的 READ_COMMITTED隔离级别. |
@Transactional(isolation=Isolation.READ_UNCOMMITTED) | 最低的隔离级别,允许读取尚未提交的数据变更,可能会导致脏读、幻读或不可重复读 |
@Transactional(isolation=Isolation.READ_COMMITTED) | 允许读取并发事务已经提交的数据,可以阻止脏读,但是幻读或不可重复读仍有可能发生 |
@Transactional(isolation=Isolation.REPEATABLE_READ) | 当一个事务读取这个数据时会对其进行加锁处理,防止其他事务对数据进行修改。重复读取(REPEATABLE_READ)的意思,读取了一条数据,这个事务不结束,别的事务就不可以改这条记录;假如通过其他手段改了,读取的依旧是原来的那个值,除非数据被当前事务本身改变,值才会变。 这样就解决了脏读、不可重复读的问题,但是幻读的问题还是无法解决 |
@Transactional(isolation=Isolation.SERIALIZABLE) | 最高的隔离级别,完全服从ACID的隔离级别。所有的事务依次逐个执行,这样事务之间就完全不可能产生干扰,也就是说,该级别可以防止脏读、不可重复读以及幻读。但是这将严重影响程序的性能。通常情况下也不会用到该级别。 |
3、只读
声明式事务的第三个特性是它是否是一个只读事务。如果一个事务只对后端数据库执行读操作,那么该数据库就可能利用那个事务的只读特性,采取某些优化 措施。通过把一个事务声明为只读,可以给后端数据库一个机会来应用那些它认为合适的优化措施。由于只读的优化措施是在一个事务启动时由后端数据库实施的, 因此,只有对于那些具有可能启动一个新事务的传播行为(PROPAGATION_REQUIRES_NEW、PROPAGATION_REQUIRED、 ROPAGATION_NESTED)的方法来说,将事务声明为只读才有意义。
此外,如果使用Hibernate作为持久化机制,那么把一个事务声明为只读,将使Hibernate的flush模式被设置为FLUSH_NEVER。这就告诉Hibernate避免和数据库进行不必要的对象同步,从而把所有更新延迟到事务的结束。
4、事务超时
为了使一个应用程序很好地执行,它的事务不能运行太长时间。因此,声明式事务的下一个特性就是它的超时。
假设事务的运行时间变得格外的长,由于事务可能涉及对后端数据库的锁定,所以长时间运行的事务会不必要地占用数据库资源。这时就可以声明一个事务在特定秒数后自动回滚,不必等它自己结束。
由于超时时钟在一个事务启动的时候开始的,因此,只有对于那些具有可能启动一个新事务的传播行为(PROPAGATION_REQUIRES_NEW、PROPAGATION_REQUIRED、ROPAGATION_NESTED)的方法来说,声明事务超时才有意义。
5、回滚规则
事务五边形的对后一个边是一组规则,它们定义哪些异常引起回滚,哪些不引起。在默认设置下,事务只在出现运行时异常(runtime exception)时回滚,而在出现受检查异常(checked exception)时不回滚(这一行为和EJB中的回滚行为是一致的)。
不过,也可以声明在出现特定受检查异常时像运行时异常一样回滚。同样,也可以声明一个事务在出现特定的异常时不回滚,即使那些异常是运行时一场。
3、Spring MVC 如何解决 POST 请求中文乱码问题?
1、解决 POST 请求中文乱码问题
修改项目中web.xml文件
<filter>
<filter-name>CharacterEncodingFilter</filter-name>
<filter-class>org.springframework.web.filter.CharacterEncodingFilter</filter-class>
<init-param>
<param-name>encoding</param-name>
<param-value>UTF-8</param-value>
</init-param>
<init-param>
<param-name>forceEncoding</param-name>
<param-value>true</param-value>
</init-param>
</filter>
<filter-mapping>
<filter-name>CharacterEncodingFilter</filter-name>
<url-pattern>/*</url-pattern>
</filter-mapping>
2、解决 GET 请求中文乱码问题
修改tomcat中server.xml文件
<Connector URIEncoding="UTF-8" port="8080" protocol="HTTP/1.1"
connectionTimeout="20000"
redirectPort="8443" />
4、 Spring MVC 的工作流程?
4.1、工作原理图:
4.2 Spring MVC 执行过程
1、用户发送请求至 前段控制器 DispatcherServlet
2、DispatcherServlet 收到请求 调用 HandlerMapping处理器映射器
3、处理器映射器 找到具体的处理器(可以根据 xml 配置、注解 进行查找),生成处理器以及处理器拦截器(如果有,则生成)一并返回给DispatcherServlet。
4、DispatcherServlet 调用 HandlerAdapter 处理器适配器。
5、HandlerAdapter 经过适配调用具体的适配器(Controller,也叫后端控制器)。
6、Controller 执行完成后,返回ModelAndView。
7、HandlerAdapter 将 Controller 执行结果 ModelAndView 返回给 DispatcherServlet。
8、DispatcherServlet 将 ModelAndView 传给 ViewReslover 视图解析器。
9、ViewReslover 解析后 具体返回 View,这个 View 不是完整的,仅仅是一个页面(视图)名字,且没有后缀名。
10、DispatcherServlet 根据 View 进行渲染视图(即 将模型数据填充至 视图中)。
11、DispatcherServlet 响应用户,把有数据的视图(页面),返回给用户。
图中可以看到,DispatcherServlet(前端控制器)占据了很大的一部分,事实也是这样,springMVC中,DispatcherServlet是他的核心。
接下来再看一下springMVC组件及其核心步骤的说明:
黄色字体和绿色字体是对组件作用的说明,其中黄色字体说明的组件是框架提供,绿色字体说明的部分是要由工程师实现,红色字体是核心架构的具体流程步骤
5、Mybatis 中当实体类中的属性名和表中的字段不一样,怎么解决?
解决方案有三种如下:
1、写 SQL 语句的时候 写别名
2、在MyBatis的全局配置文件中开启驼峰命名规则,前提是符合驼峰命名规则
<!-- 开启驼峰命名规则,可以将数据库中下划线映射为驼峰命名
列如 last_name 可以映射为 lastName
-->
<setting name="mapUnderscoreToCameLCase" value="true" />
3、在Mapper映射文件中使用 resultMap 自定义映射
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper
PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<!-- namespace属性:必须是接口的全类名 -->
<mapper namespace="com.atguigu.mybatis.mapper.EmployeeMapper">
<!--
id属性:必须是接口中方法的方法名
resultType属性:必须是方法的返回值的全类名
-->
<select id="getEmployeeById" resultMap="myMap">
select * from employees where id = #{id}
</select>
<!-- 自定义高级映射 -->
<resultMap type="com.atguigu.mybatis.entities.Employee" id="myMap">
<!-- 映射主键 -->
<id column="id" property="id"/>
<!-- 映射其他列 -->
<result column="last_name" property="lastName"/>
<result column="email" property="email"/>
<result column="salary" property="salary"/>
<result column="dept_id" property="deptId"/>
</resultMap>
</mapper>
三、Java 高级
1、Linux 常用服务类相关命令?
常用基本命令 - 进程类,centos 6 和 centos 7 及以上命令有些不同
1、centos 6
service 服务名 start
service 服务名 stop
service 服务名 restart
service 服务名 reload
service 服务名 status
#查看服务的方法 /etc/init.d/ 服务名
#通过 chkconfig 命令设置自启动
#查看服务 chkconfig -list l grep XXX
chkconfig -level 5 服务名 on/off
2、运行级别(centos 6)
Linux 系统有 7 种运行级别 (runlevel) : 常用的是级别 3 和 5 。
- 运行级别0: 系统停机状态,系统默认运行级别不能设为0,否则不能正常启动
- 运行级别1: 单用户工作状态,root权限,用于系统维护,禁止远程登陆
- 运行级别2: 多用户状态(没有NFS),不支持网络
- 运行级别3: 完全的多用户状态(有NFS),登陆后进入控制台命令行模式
- 运行级别4: 系统未使用,保留
- 运行级别5: X11控制台,登陆后进入图形GUI模式
- 运行级别6: 系统正常关闭并重启,默认运行级别不能设为6,否则不能正常启动
3、centos 7
systemctl start 服务名(xxx.service)
systemct restart 服务名(xxxx.service)
systemctl stop 服务名(xxxx.service)
systemctl reload 服务名(xxxx.service)
systemctl status 服务名(xxxx.service)
#查看服务的方法 /usr/lib/systemd/system
#查看服务的命令
systemctl list-unit-files
systemctl --type service
#通过systemctl命令设置自启动
自启动 systemctl enable service_name
不自启动 systemctl disable service_name
2、Git 分支相关命令?
1、创建分支
git branch <分支名>
git branch -v 查看分支
2、切换分支
git checkout <分支名>
git checkout -b <分支名> 一步完成 新建分支 并 切换到新分支
3、合并分支
git checkout master 先切换到 主干
git merge <分支名>
4、删除分支
git checkout master 先切换到 主干
git branch -D <分支名>
5、工作流介绍
简单来说就是 master 分支上线,如果 master 出问题,会创建一个 hotfix 分支进行解决 bug ,解决完后合并到 master 分支和 develop 分支,保持一个同步,有新的分支开发完成就会和 develop 分支合并,然后创建一个 release 分支进行测试,完成后在合并到 master 和 develp ,保持一致。
3、redis 持久化有几种类型,它们的区别是?
Redis提供了2个不同形式的持久化方式:
1、RDB(Redis DataBase):
1.1、解释:
在指定的时间间隔内,将内存中的数据集 快照写入磁盘,也就是行话讲的Snapshot快照,它恢复时是将快照文件直接读到内存里。
1.2、备份是如何执行的:
Redis 会单独 创建(fork) 一个 子进程 来进行持久化,会先将数据写入到一个 临时文件 中,待持久化过程都结束了,再用这个 临时文件 替换 上次持久化好的文件 。整个过程中,主进程是不进行任何 IO 操作的,这就确保了极高的性能。 如果需要进行大规模数据的恢复,且对于数据恢复的完整性不是非常敏感,那 RDB 方式 要比 AOF 方式 更加高效。
1.3、RDB 的工作流程图
1.4、RDB的备份
先通过config get dir 查询rdb文件的目录
将*.rdb的文件拷贝到别的地方
1.5、RDB的恢复
关闭Redis
先把备份的文件拷贝到工作目录下
启动Redis,备份数据会直接加载
1.6、RDB的优点
1)节省磁盘空间
2)恢复速度快
1.7、RDB的缺点
1)虽然Redis在fork时使用了写时拷贝技术,但是如果数据庞大时还是比较消耗性能
2)在备份周期在一定间隔时间做一次备份,所以如果Redis意外down调的话,就会丢失最后一次快照后的所有修改
2、AOF(Append Of File):
2.1、解释:
以日志的形式来记录每个写操作, 将 Redis 执行过的 所有写指令 记录下来(读操作不记录),只许追加文件 但不可以改文件,Redis 启动之初 会读取该文件 重新构建数据,也就是说, Redis 重启的话,就会根据日志文件的内容将写指令,从前到后都执行一次,以完成数据的恢复工作。
1.2、RDB 的工作流程图
1.3、AOF的优点
1)备份机制更稳健,丢失数据概率更低。
2)可读的日志文本,通过操作AOF文件,可以处理误操作
1.4、AOF的缺点
1)比起RDB占用更多的磁盘空间
2)恢复备份速度要慢
3)每次读写都同步的话,有一定的性能压力
4)存在个别bug,造成恢复不能
4、MySQL 什么时候适合创建索引,什么时候不适合创建索引?
1、什么时候适合创建索引
1)主键自动建立唯 一 索引
2)频繁作为查询条件的字段应该创建索引
3)查询中与其它表关联的字段,外键关系建立索引
4)频繁更新的字段不适合创建索引,因为每次更新不单是更新了记录还会更新索引
5)单键组索引的选择问题,who? 在高并发下领向创建组合索引
6)意询中排序的字段,排序字段若通过索引法访问将大大提高排序速度
7)查询中统计或者分组字段
2、什么时候不适合创建索引
1)表记录太少
2)经常增删改的表因为更新表时,MySQL不仅要保存数据,还要保存一下索引文件数据重复且分布平均的表字段,因此应该只为最经常查询和最经常排序的数据列建立索引。
3)注意,如果某个数据列包含许多重复的内容,为它建立索弓|就没有太大的实际效果。
5、JVM 垃圾回收机制,GC 发生在 JVM 哪部分,有几种 GC,他们的算法是什么?
1、GC发生在JVM哪部分?
GC是发生在堆内
2、GC是什么??
GC是分代收集算法,在堆内不同的区域有不同的策略
3、有几种GC?
有两种GC:Minor GC、Full GC
- 次数上频繁收集Young区 Minor GC
- 次数上较少收集Old区 Full GC
- 基本不动perm区(永久区)
4、它们的算法是什么?
GC的4大算法:
1)、引用计数法: 只要有对象被引用,GC就不进行回收,这种方式已被淘汰(JVM的实现一般不采用这种方式,缺点:1)每次对对象赋值时均要维护引用计数器,且计数器本身也有一定的消耗;2)较难处理循环引用【A引用B,B引用A】)。
2)、复制算法(copying):年轻代中使用的是Minor GC,这种GC算法采用的是复制算法(copying):1)从根集合(GC Root)开始,通过Tracing从From中找到存活对象,拷贝到To中;2)From、To交换身份,下次内存分配从To开始。
3、标记算法(Mark-Sweep): 老年代一般是由标记清除或者是标记清除与标记整理的混合实现。
- 标记(mark):从根集合开始扫描,对存活的对象进行标记。
- 清除(Sweep):扫描整个内存空间,回收未被标记的对象,使用free-list记录可以区域
- 优点:不需要额外的空间,在同一块内存空间操作
- 缺点:两次扫描,耗时严重;会产生内存碎片
4、标记压缩(Mark-Compact): 老年代一般是由标记清除或者是标记清除与标记整理的混合实现。
- 标记(Mark):与标记清除一样。
- 压缩(Compact):再次扫描并往一端滑动存活对象(在整理压缩阶段,不在对标记的对象做回收,而是通过所有存活对象都向一端移动,然后直接清除边界以外的内存)
- 优点:没有内存碎片
- 缺点:需要移动对象的成本
5、标记-清除-压缩(Mark-Sweep-Compact): 老年代Full GC有两种算法结合使用
原理:
- Mark-Sweep和Mark-Compact结合
- 和Mark-Sweep一致,当进行多次GC后才Compact
Full GC两种算法结合使用,先标记进行清除,清除多次并产生很多内存碎片之后,再做压缩
优点:减少移动对象成本
四、项目面试题
1、redis 在项目中的使用场景?
数据类型 | 使用场景 |
---|---|
String | 比如说 ,我想知道什么时候封锁一个IP地址。Incrby命令 |
String | 存储用户信息【id,name,age】 Hset(key,field,value) Hset(userKey,id,101) Hset(userKey,name,admin) Hset(userKey,age,23) ----修改案例---- Hget(userKey,id) Hset(userKey,id,102) 为什么不使用String 类型来存储 Set(userKey,用信息的字符串) Get(userKey) 不建议使用String 类型 |
List | 实现最新消息的排行,还可以利用List的push命令,将任务存在list集合中,同时使用另一个命令,将任务从集合中取出[pop]。 Redis—list数据类型来模拟消息队列。【电商中的秒杀就可以采用这种方式来完成一个秒杀活动】 |
Set | 特殊之处:可以自动排重。比如说微博中将每个人的好友存在集合(Set)中,这样求两个人的共通好友的操作。我们只需要求交集即可。 |
Zset | 以某一个条件为权重,进行排序。 京东:商品详情的时候,都会有一个综合排名,还可以按照价格进行排名。 |
2、Elasticsearch 与 solr 的区别?
1)、背景
他们都是基于 Lucene 搜索服务器基础上开发,一款优秀的,高性能的企业级搜索服务器,【是因为他们都是基于分词技术构建的倒排索引的方式进行查询】。
2)、开发语言
Java
3)、诞生时间
Solr:2004年诞生
ES:2010年诞生
4)、主要区别
- 当实时建立索引的时候,solr 会产生 io 阻塞,而 es 不会,es 查询性能要高于 solr。
- 在不断动态添加数据的时候,solr 的检索效率会变得低下,而 es 没有什么变化。
- Solr 利用 zookeeper 进行分布式管理,而 es 自带有分布式系统的管理功能,Solr 一般都要部署到 web 服务器上,比如 tomcat,启动 tomcat 的时候需要配置 tomcat 和 solr 的 关联 【 Solr 的本质,是一个动态的 web项目】。
- Solr支持更多格式的数据 【xml、json、csv 】等,而 es 仅仅支持 json 文件格式。
- Solr 是传统搜索应用的有利解决方案,但是 es 更加适用于新兴的是是搜索应用。
- 单纯的对已有的数据进行检索, solr 效率更好,高于 es。
- Solr 官网提供的功能更多哦,而 es 本身更加注重于核心功能,高级功能都有第三方插件完成。
3、单点登录
单点登录实现过程:
单点登录:一处登录多处使用!
前提:单点登录多使用在分布式系统中。
4、购物车实现过程
购物车:
- 购物车跟用户的关系?
a) 一个用户必须对应一个购物车【一个用户不管买多少商品,都会存在属于自己的购物车中。】
b) 单点登录一定在购物车之前。 - 跟购物车有关的操作有哪些?
a) 添加购物车
i. 用户未登录状态
1. 添加到什么地方?未登录将数据保存到什么地方?
a) Redis? — 京东
b) Cookie? — 自己开发项目的时候【如果浏览器禁用cookie】
ii. 用户登录状态
1. Redis 缓存中 【读写速度快】
a) Hash :hset(key,field,value)
i. Key:user:userId:cart
ii. Hset(key,skuId,value);
2. 存在数据库中【oracle,mysql】
b) 展示购物车
i. 未登录状态展示
1. 直接从cookie 中取得数据展示即可
ii. 登录状态
1. 用户一旦登录:必须显示数据库【redis】+cookie 中的购物车的数据
a) Cookie 中有三条记录
b) Redis中有五条记录
c) 真正展示的时候应该是八条记录
5、消息队列在项目中的使用
背景:在分布式系统中是如何处理高并发的。
由于在高并发的环境下,来不及同步处理用户发送的请求,则会导致请求发生阻塞。比如说,大量的insert,update之类的请求同时到达数据库MYSQL,直接导致无数的行锁表锁,甚至会导致请求堆积很多。从而触发 too many connections 错误。使用消息队列可以解决【异步通信】
1)、 异步
2)、 并行
3)、 排队
消息队列电商使用场景:
消息队列的弊端:
消息的不确定性:延迟队列,轮询技术来解决该问题即可!
文章声明:
此文章内容参考B站视频内容
参考:B站视屏链接
此外还参考了CSDN某些文章
参考:文章链接 1