1. 面向对象VS面向过程
场景:程序要连接数据库
面向对象:市面上大家都使用的是什么组件,或那个框架完成的,我直接拿来使用。如何使用对象,组织者
面向过程:我该怎么连接数据库,怎么具体实现。如何创建对象,执行者
1.1 java的三大特性:封装、继承、多态
封装:隐藏内部实现,对外提供接口,提高安全性,复用性
继承:公用的内容,抽取作为父类,提高复用性
多态:包含重写,重载。
- 多态是同一个行为具有多个不同的表现形式或形态的能力;
- 多态就是同一个接口使用不同的实例而执行不同操作;
2. JDK、JRE、JVM
jdk:开发工具包,提供运行环境(jre)和开发环境,包含编译java源文件的编译器javac,还有调试和分析工具。
jre:java运行环境仅包含java虚拟机(jvm)和一些基础类库
jvm:java虚拟机,运行字节码文件
2.2. java的跨平台
主要是通过jvm实现的,在不同平台(win,mac,linux…)都有自己的jvm,而只要是编译后的字节码文件都可以通过任何平台的jvm运行,从而实现跨平台
3. ==和equals的区别
== : 比较的的是具体的值 (基本数据类型) ,如果比较引用数据类型 也是比较引用地址
equals :默认比较的是引用指向的地址(object类的方法),可以进行重写
3.1 new之后的对象,在栈中都是指向堆内存
4. final关键字
修饰类:类不可被继承,最终类
修饰方法:该方法不可被重写
修饰变量:那么这个变量就成为一个常量
-
修饰基本数据类型,那么这个值不可改变
-
修饰引用数据类型,指向的堆内存不可改变
5. String,StringBuffer,StringBuilder
String跟后两者的区别:String 是final类每次声明都是不可变对象,每次操作都会new新的对象
StringBuffer StringBuilder都是在原有对象进行操作 如果要经常改变字符串的内容 使用这两者
StringBuffer是线程安全的,StringBuilder是不安全的
效率:StringBuilder > StringBuffer > String
5.1 线程安全与不安全
线程安全:在多环境下,这个对象的访问不需要加入额外的同步控制,操作的数据依然是正确的
线程不安全:反之,数据不正确
StringBuffer安全是因为源码每个方法中都加入了synchronized关键字
在开发中,优先选用StringBuilder(性能高),要解决线程安全(高并发、多线程情况下)——>多线程要考虑,栈内存中数据是共享,还是每个线程独享,如果是线程独享,不必考虑安全问题
6. 抽象类和接口的区别
1,语法层面
1.1,JDK1.8之前
抽象类:方法可以有抽象的,也可以有非抽象(正常的方法,具体的实现), 有构造器
接口:方法都是抽象,属性都是常量,默认有public static final修饰
1.2,JDK1.8之后
接口里面可以有实现的方法,注意要在方法的声明上加上default或者static
interface IEatable{
public default void eat(){}
}
2,开发设计层面
抽象类:同一类事物的抽取,比如针对Dao层操作的封装,如,BaseDaoImpl,BaseServiceImpl
接口:通常更像是一种标准的制定,定制系统之间对接的标准规范 (解耦)
-
-
- 例子:
- 1,单体项目,分层开发,接口作为各层之间交互的纽带,在controller中注入IUserService,在Service注入IUserDao
- 2,分布式项目,面向服务的开发,抽取服务service,这个时候,就会产生服务的提供者和服务的消费者两个角色
- 这两个角色之间的纽带,依然是接口
- 例子:
-
最后区分几个概念:
-
多继承,多重继承,多实现
-
- 多继承:接口可以多继承,类只支持单继承
- 多重继承:A->B->C(爷孙三代的关系)
- 多实现:Person implements IRunable,IEatable(记忆联想-符合多项国际化标准)
- 多继承:接口可以多继承,类只支持单继承
7. 递归
递归:方法内部调用方法自身
- 找到规律
- 找到递归出口
7.1 递归为什么会栈内存溢出
在调用方法时候会在栈中开辟空间 栈帧,调用一次,会创建一个,所以,如果递归次数太多,会导致栈内存溢出。
7.2 N的阶乘
N! = (N-1)! * n;
5! = 1*2*3*4*5;
4! = 1*2*3*4;
public static int getResult(int n){
if(n < 0){
throw new RuntimeException("非法参数");
}
if(n == 1 || n == 1){
return 1; //递归出口
}
return getResult(n-1) * n;
}
7.3 斐波那契
数字:1,1,2,3,5,8,13,21 … 求,第n个数是多少
规律:每个数字都是前两个数字之和
递归出口:第一项和第二项都是1
public int getFeiBo(int n){
if(n < 0){
throw new RuntimeException("非法参数");
}
if(n == 1 || n == 2){
return 1;
}else{
return getFeiBo(n-2) + getFeiBo(n-1);
}
}
8. 什么是向上转型?什么是向下转型?
向上转型:父类引用指向子类对象。(安全)
向下转型:父类对象强转为子类对象。(不安全)
9.Integer int
Integer 引用数据类型(包装类);
int 基本数据类型
Integer i1 = new Integer(66);
Integer i2 = new Integer(66);
System.out.println(i1 == i2); //false 两个都是new出来的 所以指向的堆内存不同 false
// 自动装箱范围:-128 -- +127 之间 也就不是新new出来的对象 查看Integer.valueof源码
Integer i3 = 66;
Integer i4 = 66;
int i5 = 66;
System.out.println(i3 == i4); //true i3,i4都是使用基本数据类型进行赋值 进行自动装箱 (jdk1.5之后)Integer.valueof();
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
10. 重写和重载的区别
重载:发生在一个类中,多个方法 方法名相同,参数列表不同(顺序,类型,都可)就叫做重载。与返回值无关
重写:发生在两个父类跟子类之间,子类对父类方法的重新编写,满足:方法名相同,参数列表相同,子类方法访问权限只能大于父类
11. List和Set的区别
list:有序,可重复
set:无序,不可重复 (无序:加入集合的顺序 != 遍历的顺序)
12.ArrayList 和LinkedList的区别
-
底层数据结构
- ArrayList 底层是数组结构,是一块连续的内存空间
- LinkedList底层是双向链表,是不连续的内存
-
常规答案
-
ArrayList查询快,因为内存是连续的,方便寻址,但是增删慢,因为需要发生数据迁移
-
LinkedList查询慢,通过指针查找,但增删快,只需要改变前后结点的指针指向
通过下标查询,ArrayList快,如果指定元素,查询所在位置,两者相同 ArrayList有下标
增删:
-
如果是中间或者非结尾
- ArrayList因为底层为数组保证内存连续,所以,插入位置后的所有元素都要移动,导致速度变慢
- LinkedList因为底层是链表,只需要改变所在位置前后元素的结点指向即可
-
如果是结尾
-
ArrayList因为底层为数组,可计算位置,快速定位直接操作
-
LinkedList有两个指针,头指针,尾指针,要在末尾找会从尾指针开始找
末尾插入,两者类似
-
-
-
在向List中存入固定个数的元素 ArrayList更节省内存(初始化容量)
-
ArrayList 扩容
- 创建一个新的数组,长度是原数组的1.5倍(位运算)
- 将原数组迁移到当前数组
-
双向链表中在元素A和B中间插入元素C
伪代码:
C.pre = A
C.next = A.next
A…next.pre = C
A.next = C
13. HashSet 存储原理
HashSet 不可重复的原因:hashSet.add方法使用的是map集合,将add的东西作为key,所以不可重复
HashSet底层使用HashMap实现存储
13.1 为什么使用hash算法,有什么优势,解决了什么问题?
主要是解决数据的唯一性判断的效率问题,保证为O(1)的复杂度
- 存储数据,底层用的是数组,数组实现唯一性就是遍历,这种效率比较低,所以采用hashcode
- 采用hash算法,通过存储对象的hashcode,然后在跟数组长度-1做运算,得到我们要的存储在数组中的下表,如果此时这个下表没有其他元素,那么就不用比较,直接存储。
- hash冲突:不同的元素,或对象,hash值相同这时候就需要比较,使用equals,如果equals相等 则是重复的 不插入,如果不相等,就将当前位置的next指针指向要插入的元素,从而形成链表
13.2 什么是hash表
本质是一个数组,数组元素构成链表
13.3 链表的优化
jdk1.8之后,防止链表过长,会将链表优化为红黑树,使用二分查找比较,这样效率就更快了
14. ArrayList 和 Vector
区别:前者线程安全,后者不安全
类似于StringBuffer 和 StringBuilder的区别
15. HashTable & HashMap & ConcurrentHashMap
- HashTable 线程安全,内部有锁的控制
- HashMap 线程不安全,内部没有锁
- 优点:效率高
- 缺点:如果有多个线程同时操作,并且线程共享栈帧,那么就会产生线程安全的问题,甚至会出现死锁
- ConcurrentHashMap
- 使用的是分段锁,降低锁粒 jdk8 以前
- 将数据分段,执行分段锁(分离锁),核心把锁的范围变小,这样出现并发冲突的概率就变小
- 在put时候,key会首先进过hash运算,拿到所在的hash段,然后只锁当前段
- 相当于是折中
- jdk7 和 jdk8中的区别
- jdk1.7采用的是链表 jdk1.8采用的链表+红黑树
- 发生hash冲突时候
- jdk1.7 采用链表 jdk1.8默认使用链表 链表长度超过8且数组容量超过64时时候使用红黑树
- 并发安全的实现
- JDK1.7采用分段锁的方式,而JDK1.8采用CAS和synchronized的组合模式
- 查询时间复杂度
- JDK1.7采用链表的方式,时间复杂度为O(n),而JDK1.8在采用红黑树的方式时,时间复杂度为O(log(n))
- 使用的是分段锁,降低锁粒 jdk8 以前
- 开发中选择
- HashMap 不是多线程的情况
- 如果HashMap是个全局对象,并且线程共享map对象 选择ConcurrentHapMap
- HashTable 不选择 效率低
16. 栈 stack 是如何实现的
特点:FILO(First In Last Out)先进后出
底层是使用数组
入栈:数组尾部直接插入
public E push(E item) {
addElement(item);
return item;
}
public synchronized void addElement(E obj) {
modCount++;
ensureCapacityHelper(elementCount + 1); // 判断数组容量是否充足
elementData[elementCount++] = obj;
}
出栈:尾部出
第一种:获取并从栈中删除
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);
}
17. IO流的分类,以及选择
17.1 分类
按方向:
输入流、输出流(站在程序的角度)
读取单位:
字节流、字符流
处理方式;
节点流、处理流
比如FileInputStream和BufferedInputStream,处理流是带缓冲区的,缓冲区本质:数组
IO流的4大基类:
字节流 字符流
InputStream、OutputStream、Reader、Writer
17.2 如何选
- 字节流可以读取任何文件
- 当需要解析文件文件的内容时候,选择字符流(比如:解析文件内容txt,按行读取)
- 读取二进制文件时候可以选择字节流(视频、音频、图片…)
要提高效率可以选择带缓冲区的比如BufferedInputStream…
18. serialVersionUID的作用是什么
当执行序列化时,我们写对象到磁盘中,会根据当前这个类的结构生成一个版本号ID
当反序列化时,程序会比较磁盘中的序列化版本号ID跟当前的类结构生成的版本号ID是否一致,如果一致则反序列化成功,否则,反序列化失败
加上版本号,有助于当我们的类结构发生了变化,依然可以之前已经序列化的对象反序列化成功
比如:一个类序列化之后存入了磁盘,然后新增了属性,如果不自己制定版本号那么两个版本号会不同,那么原来的对象取出来进行强转会报错
19. java异常体系
保证程序健壮性
健壮性:保证程序的运行
19.1 Error
jvm的异常
栈内存溢出错误:StackOverflowError(递归,递归层次太多或递归没有结束)
堆内存溢出错误:OutOfMemoryError(堆创建了很多对象)
不能被捕捉
19.2 运行时异常(逻辑异常)
算数异常,
空指针,
类型转换异常,
数组越界,
NumberFormateException(数字格式异常,转换失败,比如“a12”就会转换失败)
19.3 非运行时异常
比如:文件读取,位置不对会造成异常 所以会提示你try catch
由于第三方操作造成
IOException,
SQLException,
FileNotFoundException,
NoSuchFileException,
NoSuchMethodException
20. throw跟throws区别
throw用于方法内 抛出异常
throws用于方法上 声明异常
项目一般会往上抛出,最后由统一异常处理
21. 创建线程的方式
- 继承Thread
- 实现Runable
- 实现Callable接口(可以拿到线程执行后返回值)
注意!!!
实际后两种,更准确的理解是创建了一个可执行的任务,依然要采用线程的方式执行,
需要通过创建Thread对象来执行,比如 new Thread(new Runnable(){}).start();这样的方式来执行。
21.1 如何正确启动一个线程
class MyThread extends Thread{
@Override
public void run() {
System.out.println(Thread.currentThread().getName()+":running.....");
}
}
public static void main(String[] args){
MyThread thread = new MyThread();
//thread.run();//调用方法不会开启新线程
thread.start(); //正确启动线程的方式
}
21.2 实现Runnable只是创建了一个任务,并不是一个线程
class MyTask implements Runnable{
@Override
public void run() {
System.out.println(Thread.currentThread().getName()+":running....");
}
}
public static void main(String[] args){
MyTask task = new MyTask();
//task.start(); //并不能直接以线程的方式来启动
//它表达的是一个任务,需要启动一个线程来执行
new Thread(task).start();
}
21.2 runnable 与 callable的区别 线程返回值
class MyTask2 implements Callable<Boolean>{
@Override
public Boolean call() throws Exception {
return null;
}
}
所以不管怎么说,创建线程,执行线程最终都是使用 继承Thread,线程池就是内部已经创建了多个Thread对象等待执行任务
22. 一个main方法是个单线程吗?
当然不是
java有个重要的特性,叫垃圾自动回收机制,所以答案是多线程,这里面有两部分,主线程(用户线程),垃圾回收线程GC(守护线程、后台线程)同时存在。
23.线程的生命周期
new(新建),runnable(运行),blocked(等待锁),waiting(等待wait),timed waiting(sleep或有时间的wait),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 {
//目的是让线程进入TIMED_WAITING状态
Thread.sleep(1000);
//进入waiting状态
wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
注意:
blocked,waiting,timed waiting 我们都称为阻塞状态
上述的就绪状态和运行状态,都表现为runnable状态
24. 线程安全(理解、实现)
24.1 什么是线程安全?
在多线程的情况下,可以不通过额外的同步操作或者协调操作,调用这个对象都可以获取到正确的结果,这就是~
24.2 如何做到
最简单的:synchronized(代码块或方法上锁)
涉及到分布式:乐观锁
24.3 什么时候用考虑
- 在多线程访问共同资源的情况下需要进行考虑
25. sleep 和 wait的区别
- 所属的类不同
- sleep是在Thread上
- wait是在Object上
- 对于锁资源的处理方式
- sleep不会被释放
- wait会释放锁
- 唤醒方式不同
- sleep到时间后会自动恢复
- wait需要外部的唤醒
- void notify() 随机唤醒一个waiting状态的线程
- void notifyAll 唤醒所有的waiting状态的线程
- 线程的状态不同
- 当线程调用wait(),线程都会进入到waiting状态
- 当线程调用sleep(time),或者wait(time)时,进入timed waiting状态
为什么wait在Object中,sleep在Thread中?
因为java的锁是对象级别不是线程级别
为什么wait要使用在同步代码块中?
因为同步代码块中cpu不会切换执行别的线程,防止,在notify方法在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。
27. 类加载
请问,我现在编写一个类,类全名如下:java.lang.String,
我们知道JDK也给我们听过了一个java.lang.String,
那么,我们编写的这个String类能否替换到JDK默认提供,也就是说程序实际运行的时候,会加载我们的String还是JDK的String?为什么?
加载JDK的,java提供了3个类加载器 BootstrapClassLoader ExtClassLoader AppClassLoader 分别加载的是jre提供的类库、jre提供的扩展库、我们自己项目中依赖的外部jar以及自己编写的class ,,,加载的顺序也是从上到下,如果父级有,那么就直接加载了。
27.1首先,什么是类的加载机制?
JVM使用Java类的流程如下:
1,Java源文件----编译---->class文件
2,类加载器ClassLoader会读取这个.class文件,并将其转化为java.lang.Class的实例。有了该实例,JVM就可以使用他来创建对象,调用方法等操作了。
那么ClassLoader是以一种什么机制来加载Class的?
这就是我们要谈的类的加载机制!
27.2搞清楚这个问题,首先要知道,我们用到的Class文件都有哪些来源?
1,Java内部自带的核心类,位于 J A V A H O M E / j r e / l i b , 其 中 最 著 名 的 莫 过 于 r t . j a r 2 , J a v a 的 扩 展 类 , 位 于 JAVA_HOME/jre/lib,其中最著名的莫过于rt.jar 2,Java的扩展类,位于 JAVAHOME/jre/lib,其中最著名的莫过于rt.jar2,Java的扩展类,位于JAVA_HOME/jre/lib/ext目录下
3,我们自己开发的类或项目开发用到的第三方jar包,位于我们项目的目录下,比如WEB-INF/lib目录
27.3那么,针对这些Class,JDK是怎么分工的?谁来加载这些Class?
针对不同的来源,Java分了不同的ClassLoader来加载
1,Java核心类,这些Java运行的基础类,由一个名为BootstrapClassLoader加载器负责加载。这个类加载器被称为“根加载器或引导加载器”
注意:BootstrapClassLoader不继承ClassLoader,是由JVM内部实现。法力无边,所以你通过java程序访问不到,得到的是null。
2,Java扩展类,是由ExtClassLoader负责加载,被称为“扩展类加载器”。
3,项目中编写的类,是由AppClassLoader来负责加载,被称为“系统类加载器”。
27.4 那凭什么,我就知道这个类应该由老大BootStrapClassLoader来加载?
这里面就要基于双亲委托机制?
所谓双亲委托机制,就是加载一个类,会先获取到一个系统类加载器AppClassLoader的实例,然后往上层层请求,先由BootstarpClassLoader去加载,
如果BootStrapClassLoader发现没有,再下发给ExtClassLoader去加载,还是没有,才由AppClassLoader去加载。
如果还是没有,则报错
28. Ajax工作原理
客户端多了一个ajax引擎,只需要刷新部分内容即可
异步刷新
29. 浅谈JavaScript的原型机制
JavaScript的原型有一个关键的作用就是来扩展其原有类的特性,比如下面这段代码,给String扩展了hello方法。很多的框架就是采用这种方式来进行扩展,从而让框架更易用。
30. jsp 和 servlet的区别技术的角度:
技术的角度:
JSP本质就是一个Servlet
JSP的工作原理:JSP->翻译->Servlet(java)->编译->Class(最终跑的文件)
应用的角度:
JSP=HTML+Java
Servlet=Java+HTML
各取所长,JSP的特点在于实现视图,Servlet的特点在于实现控制逻辑
与mvc公谈
m:model v:view c: controller 模型 视图 控制器
31. Servlet的生命周期
首先,要明确一点,Servlet是单实例的!
需要考虑线程安全,因为单例,资源共享
生命周期的流程:
创建对象–>初始化–>service()–>doXXX()–>销毁
创建对象的时机:
1,默认是第一次访问该Servlet的时候创建
2,也可以通过配置web.xml,来改变创建时机,比如在容器启动的时候去创建,DispatcherServlet(SpringMVC前端控制器)就是一个例子
1
执行的次数
对象的创建只有一次,单例
初始化一次
销毁一次
关于线程安全
构成线程不安全三个因素:
1,多线程的环境(有多个客户端,同时访问Servlet)
2,多个线程共享资源,比如一个单例对象(Servlet是单例的)
3,这个单例对象是有状态的(比如在Servlet方法中采用全局变量,并且以该变量的运算结果作为下一步操作的判断依据)
32. cookie 和 session
存储位置:
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的域名
33. 转发和重定向
33.1 转发
服务器内部的跳转,对于客户端来说,自始至终就是一次请求,所以在这期间,传递request对象就可以进行传值
33.2 重定向
发生在客户端之间的跳转,所以是多个请求,这时候请求之间传递的数据不能被传递 需要的话可以用session
问题:如果想跳转百度,使用转发还是重定向?
使用重定向,因为转发是服务器内部服务之间的转发。
34. 分层架构和mvc
三层架构:
-
web层:负责给用户展示数据和提供对外接口。(UI层)
springmvc、status2、status1
-
业务逻辑层:实现业务逻辑
Spring
-
持久层:将业务逻辑持久化,操作数据库
Hibernate、Mybatis、SpringDateJPA。。。。。
mvc:
Model(模型)代表一个存取数据的对象 JAVA POJO
View(视图)模型包含的数据可视化
Controller(控制器)作用于模型和视图上。控制数据流向对象,并在数据变化时候更新视图,是视图和模型分开Servlet、Controller
DispatchServlet:前端控制器,由它接收,然后根据请求路径URL分发到对应的业务控制器。
35. IO 和 NIO的区别
35.1 IO 是面向流的 NIO是面向缓冲区的
-
在Java IO中读取数据和写入数据是面向流(Stream)的,就如同河流一样。所有的数据不停地向前的流淌,我们只能触碰到当前的流水。
如果需要获取某个数据的前一项或后一项数据那就必须自己缓存数据(将水从河流中打出来),而不能直接从流中获取(因为面向流就意味着我们只有一个数据流的切面)
-
Java NIO中数据的读写是面向缓冲区(Buffer)的,读取时可以将整块的数据读取到缓冲区中,在写入时则可以将整个缓冲区中的数据一起写入。
这就好像是在河流上建立水坝,面向流的数据读写只提供了一个数据流切面,而面向缓冲区的IO则使我们能够看到所有的水(数据的上下文),也就是说在缓冲区中获取某项数据的前一项数据或者是后一项数据十分方便。这种便利是有代价的,因为我们必须管理好缓冲区,这包括不能让新的数据覆盖了缓冲区中还没有被处理的有用数据;将缓冲区中的数据正确的分块,分清哪些被处理过哪些还没有等等。
35.2 IO是阻塞的 NIO 是非阻塞的
- Java IO是阻塞的,如果在一次读写数据调用时数据还没有准备好,或者目前不可写,那么读写操作就会被阻塞直到数据准备好或目标可写为止。
- Java NIO则是非阻塞的,每一次数据读写调用都会立即返回,并将目前可读(或可写)的内容写入缓冲区或者从缓冲区中输出,即使当前没有可用数据,调用仍然会立即返回并且不对缓冲区做任何操作。
36. static 关键字
static是Java中的一个关键字,单词本身是静态的含义。一个类的成员包括变量、方法、构造方法、代码块和内部类,static可以修饰除了构造方法以外的所有成员。
使用static修饰的成员成为静态成员,是属于某个类的;而不使用static修饰的成员成为实例成员,是属于类的每个对象的
1,实例成员变量的值存放在堆内存上面 静态成员变量的值存储在方法区中
2, 静态成员可以通过类名.属性名的方式来访问,成员变量只能通过对象名.属性名访问
3, static修饰的方法叫做静态成员方法,静态方法只能直接调用静态方法,方法内不能使用this关键字
4, 修饰代码块 在类装载进jvm时候会执行,只会执行一次
37. java动态类加载
什么是动态类加载:
动态类加载就是通过Class.forName(类去全路径名) 的方式来获取一个Class类型的实例,然后通过这个Class实例的newInstance来初始化,在程序运行时会才会构建一个类并执行对应的方法
什么静态加载:
Student student = new Student();
38. jdk8的新特性
38.1 lambda表达式
引入了函数式编程
38.2 函数式接口
@FunctionalInterface 修饰的接口表示是一个函数是结构,内部只有一个方法就比如
Runnable run = () -> {} 一个函数式接口的实现可以直接实现为这样
38.3 方法引用
语法 : ( 先把笔记记下来 )
类名 :: 静态方法名
类名 :: 非静态方法名
类名 :: new ( 类的构造方法引用 )
实列对象 :: 非静态方法名 ( 指定某个实列的某个非静态方法引用)
38.4 接口中可以有静态方法和默认方法
默认方法是在1.8之后新增的
38.5 Stream流
38.6 Date --> localdate
39. java集合类继承关系
蓝色实线为类的继承关系
绿色实线为接口的继承关系
绿色虚线为接口的实现关系
40. mysql
ACID
1)、 原子性(Atomicity)
原子性是指事务包含的所有操作要么全部成功,要么全部失败进行回滚,因此事务的操作如果成功就必须要完全应用到数据库,如果操作失败则不能对数据库有任何影响。
2)、一致性(Consistency)
一致性是指事务必须使数据库从一个一致性状态变换到另一个一致性状态,也就是说一个事务执行之前和执行之后都必须处于一致性状态。
3)、 隔离性(Isolation)
隔离性是当多个用户并发访问数据库时,比如操作同一张表时,数据库为每一个用户开启的事务,不能被其他事务的操作所干扰,多个并发事务之间要相互隔离。
4)、 持久性(Durability)
持久性是指一个事务一旦被提交了,那么对数据库中的数据的改变就是永久性的,即便是在数据库系统遇到故障的情况下也不会丢失提交事务的操作。
并发事务,事务隔离级别
在关系型数据库中,事务的隔离性分为四个隔离级别
脏读:事务A已经更新了一份数据,在这个过程中,事务B去执行了同一份数据,但是由于某些原因,被修改的数据rollback了,然后一个事务所读取的数据就不一样了(没有提交,进行了回滚)
不可重复读(一个事务中不允许多次读取数据):事务a多次读取同一个数据,事务B在事务A多次读取的过程中对数据做了更改,导致最终事务A读的数据不一致(提交成功了)
幻读:管理员A已经把学生的信息全部统计完毕了,在统计过程中,管理员B添加了一条数据,但是管理员A不知道,等管理员A执行完之后,发现有一条数据没有被统计进来,这个时候就发生了幻读(提交成功了)
不可重复读和幻读的区别:不可重复读是侧重于修改,幻读侧重于新增或删除,解决不可重复读就锁住满足条件的,解决幻读是需要锁全表
解决:
#悲观锁
悲观锁就是锁定要更新的这一行,然后在事务提交之前不让其他事务对该行数据做任何操作
#乐观锁
乐观锁是在并发的表上加一个version字段,更新的时候只有版本号大于当前版本号才能更新成功
数据库的隔离级别
- Read uncommitted (读未提交):最低级别,任何情况都无法保证(脏读、不可重复读、幻读)。
- Read committed (读已提交):可避免脏读的发生。
- Repeatable read (可重复读):可避免脏读、不可重复读的发生。
- Serializable (串行化):可避免脏读、不可重复读、幻读的发生。
mysql底层
(1)链接层:
主要提供与客户端的链接服务,判断权限,进行链接限制等等。
创建好链接后下一步把请求发给服务层。
(2)服务层:
解析SQL语句,使用SQL优化器对SQL语句进行重排序,再进行优化。
对热点数据和已经查询过的数据有进行部分缓存。
提供其他各种各样的接口供用户使用。
(3)引擎层:
innodb和myisam的两种不同的表引擎,分别对应不同的存储数据方式和查找数据的方式。
(4) 存储层:
对处理后的数据和传入的数据进行保存操作
es为什么比mysql查询快
答: 因为ES的倒排索引还做了 Term Index。
40.6 mysql锁
40.6.1 对数据操作的锁粒度来分:行级锁、表级锁、页级锁、间隙锁
行级锁
行级锁是mysql中锁定粒度最细的一种锁。表示只针对当前操作的行进行加锁。行级锁能大大减少数据库操作的冲突,其加锁粒度最小,但加锁的开销也最大。行级锁分为共享锁和排他锁
**特点:**开销大,加锁慢,会出现死锁。发生锁冲突的概率最低,并发度也最高。
InnoDB有三种行锁的算法:
1,Record Lock(记录锁):单个行记录上的锁。这个也是我们日常认为的行锁。
2,Gap Lock(间隙锁):间隙锁,锁定一个范围,但不包括记录本身(只不过它的锁粒度比记录锁的锁整行更大一些,他是锁住了某个范围内的多个行,包括根本不存在的数据)。GAP锁的目的,是为了防止同一事务的两次当前读,出现幻读的情况。该锁只会在隔离级别是RR或者以上的级别内存在。间隙锁的目的是为了让其他事务无法在间隙中新增数据。
3,Next-Key Lock(临键锁):它是记录锁和间隙锁的结合,锁定一个范围,并且锁定记录本身。对于行的查询,都是采用该方法,主要目的是解决幻读的问题。next-key锁是InnoDB默认的锁
表级锁
表级锁是mysql中锁定粒度最大的一种锁,表示对当前操作的整张表加锁,它实现简单,资源消耗较少,被大部分mysql引擎支持。最常使用的MyISAM与InnoDB都支持表级锁定。表级锁定分为表共享读锁(共享锁)与表独占写锁(排他锁)
特点:
开销小,加锁快,不会出现死锁。发生锁冲突的概率最高,并发度也最低。
LOCK TABLE my_table_name READ; 用读锁锁表,会阻塞其他事务修改表数据。
LOCK TABLE my_table_name WRITE; 用写锁锁表,会阻塞其他事务读和写。
MyISAM在执行查询语句(SELECT)前,会自动给涉及的所有表加读锁,在执行更新操作(UPDATE、DELETE、INSERT等)前,会自动给涉及的表加写锁,这个过程并不需要用户干预,因此,用户一般不需要直接用LOCK TABLE命令给MyISAM表显式加锁。
但是在InnoDB中如果需要表锁就需要显式地声明了。
页级锁
页级锁是 MySQL 中锁定粒度介于行级锁和表级锁中间的一种锁。表级锁速度快,但冲突多,行级冲突少,但速度慢。因此,采取了折中的页级锁,一次锁定相邻的一组记录。BDB 支持页级锁。
**特点:**开销和加锁时间界于表锁和行锁之间;会出现死锁;锁定粒度界于表锁和行锁之间,并发度一般。
40.6.2 按照锁的共享策略来分:共享锁、排他锁、意向共享锁、意向排他锁
共享锁和排他锁在MySQL中具体的实现就是读锁和写锁:
读锁(共享锁):Shared Locks(S锁),针对同一份数据,多个读操作可以同时进行而不会互相影响
写锁(排它锁):Exclusive Locks(X锁),当前写操作没有完成前,它会阻断其他写锁和读锁
IS锁:意向共享锁、Intention Shared Lock。当事务准备在某条记录上加S锁时,需要先在表级别加一个IS锁。
IX锁:意向排他锁、Intention Exclusive Lock。当事务准备在某条记录上加X锁时,需要先在表级别加一个IX锁。
41. CAP理论
C:数据一致性
即,同时访问所有节点的得到的数据应该是一样的,即使在刚操作完成。
A:服务可用性
即,所有节点必须保持高可用性,注意:如果某个节点等待数据同步而阻塞请求,响应时间出现延迟,就不满足A。
也就是说没有发生故障的节点必须在有限的时间返回合理的结果
P:分区容错性
系统中任意信息的丢失或失败不会影响系统的继续运作。
就是说系统中部分服务挂掉而返回失败,或者因为网络是不可靠的,发生丢包少包,发生数据不一致,不影响系统正常运行。
zookeeper:CP
eureka:AP
nacos:AP
42. 设计模式
六大原则:
单一职责
每个类只负责自己领域中的职责,就一个类而言,应该只有一个引起它变化的原因。
里氏替换
所有引用基类(父类)的地方必须能透明地使用其子类的对象。
开闭原则
对扩展开放、对修改关闭。即软件实体应尽量在不修改原有代码的情况下进行扩展
迪米特法则(最少知识原则)
一个软件实体应当尽可能少地与其他实体发生相互作用。(减少耦合)
接口隔离
使用多个专门的接口,而不使用单一的总接口,即客户端不应该依赖那些它不需要的接口。
当一个接口太大时,我们需要将它分割成一些更细小的接口。
依赖倒置
高层模块不应该依赖低层模块,两者都应该依赖其抽象;抽象不应该依赖细节,细节应该依赖抽象,其核心思想是:要面向接口编程,不要面向实现编程。
42.1 单例模式
保证一个类仅有1个实例对象,并提供一个访问他的接口
分为懒汉式、饿汉式,一个是在类加载时候实例化对象,一个是在第一次调用时创建
缺点:线程不安全,在多线程中很容易出现不同步的情况,如在数据库对象进行的频繁读写操作时。
使用双重线程检查模式
通过volatile保证内存可见性,通过synchronized保证线程的安全性
内存可见性
内存可见性是指当一个线程修改了某个变量的值,其它线程总是能知道这个变量变化。也就是说,如果线程 A 修改了共享变量 V 的值,那么线程 B 在使用 V 的值时,能立即读到 V 的最新值。
42.2. 工厂模式
为创建对象提供过渡接口,以便将创建对象的具体过程屏蔽隔离起来,达到提高灵活性的目的。
分为三类:
- 简单工厂模式
Simple Factory
:不利于产生系列产品; - 工厂方法模式
Factory Method
:又称为多形性工厂; - 抽象工厂模式
Abstract Factory
:又称为工具箱,产生产品族,但不利于产生新的产品;
42.3 建造(Builder)模式
是一种对象构建的设计模式,它可以将复杂对象的建造过程抽象出来(抽象类别),使这个抽象过程的不同实现方法可以构造出不同表现(属性)的对象。
42.4 观察者模式
观察者模式定义了一种一对多的依赖关系,让多个观察者对象同时监听某一主题对象。这个主题对象在状态发生变化时,会通知所有观察者对象,使它们能够自动更新自己。观察者模式又叫发布-订阅(Publish/Subscribe)模式。
42.5 适配器(Adapter)模式
适配器模式把一个类的接口变换成客户端所期待的另一种接口,从而使原本因接口不匹配而无法在一起工作的两个类能够在一起工作。
适配器模式的结构
适配器模式有类的适配器模式
和对象的适配器模式
两种不同的形式。
42.6 代理模式
为其他对象提供一种代理以控制对这个对象的访问。也可以说,在出发点到目的地之间有一道中间层,意为代理。
42.7 装饰模式
装饰模式(Decorator),动态地给一个对象添加一些额外的职责,就增加功能来说,装饰模式比生成子类更为灵活。
43. 周期性线程池 ScheduledThreadPoolExecutor
ScheduledThreadPoolExecutor scheduledThreadPoolExecutor = new ScheduledThreadPoolExecutor(5);
scheduledThreadPoolExecutor.scheduleAtFixedRate(()->{
System.out.println(System.currentTimeMillis()); // 要执行的任务
},0,5, TimeUnit.SECONDS); // 执行到这之后隔多长时间开始执行,执行任务的时间为,单位
44. 锁
偏向锁、轻量级锁、重量级锁其实分别是针对几种场景所做的优化机制,当线程获取锁的时候不存储竞争的时候,这时使用的是偏向锁,当线程之前有少量的竞争时,我们采用轻量级锁等一等的方式来获取锁,当锁竞争激烈等的时间太长那就没办法只能使用Monitor 基于操作系统的锁达到效果了。
Synchronyzed 重量级锁monitor 的实现
每个synchronized修饰的代码块前后都会有加上一个monitorenter 和monitorexit指令, 这其实就对应了我们上面那种加锁逻辑图里的lock 和unlock操作
垃圾回收机制是守护线程的最佳实例,因为它始终是在后台运行。它主要目标是通过销毁无法访问的对象从而来释放堆内存。
**无法访问的对象的:**简单来说,也就是没有被引用的的对象,同时在隔离岛的对象也无法访问。
45. 垃圾回收机制
45.1 主动请求jvm垃圾回收器的方法
- 使用System.gc() 方法:系统类包含静态方法gc() 用于请求 JVM 运行垃圾收集器。
- 使用Runtime.getRuntime().gc() 方法:运行时类允许应用程序与运行应用程序的 JVM 交互。因此,通过使用其 gc() 方法,我们可以请求 JVM 运行垃圾收集器。
45.2 判断对象可以回收
引用计数法:
如果一个对象被其他变量所引用,那么该对象的引用计数器会 +1 ,引用一次 +1
如果某变量引用完毕,那么会 -1
在垃圾收集器执行的时候,会回收计数器为0的对象
弊端:如果出现循环引用,那么两个对象的引用计数器都是1 ,无法进行回收,最终可能会导致内存泄漏
可达性分析法:
GC Root(根对象,就是肯定不会被当成垃圾回收的对象)。
在垃圾回收之前,JVM会先对堆中的所有对象进行扫描,判断每一个对象是否能被GC Root直接或者间接的引用,如果能被根对象直接或间接引用则表示该对象不能被垃圾回收,反之则表示该对象可以被回收
可以作为GC Root的对象:
- 虚拟机栈(栈帧中的本地变量表)中引用的对象。
- 方法区中类静态属性引用的对象。
- 方法区中常量引用的对象
- 本地方法栈中JNI(即一般说的Native方法)引用的对象。
45.3 垃圾回收算法
标记-清除(老年代)
在jvm执行垃圾回收的过程中,先采用标记算法确定可回收对象,然后垃圾收集器根据标识,清除对应的内容,给堆内存腾出相应的空间。
这里的腾出内存空间并不是将内存空间的字节清 0,而是记录下这段内存的起始结束地址,下次分配内存的时候,会直接覆盖这段内存。
**缺点:**容易造成大量的内存碎片,无法满足大对象的内存分配,一旦导致无法分配对象,那么会导致jvm启动gc,程序就可能会暂停
标记-整理
会将不被GC Root引用的对象回收,清除其占用的内存空间。然后整理剩余的对象,可以有效避免因内存碎片而导致的问题,但是牵扯到对象的整理移动,需要消耗一定的时间,所以效率较低。
复制
当需要回收对象时,先将GC Root直接引用的的对象(不需要回收)放入TO中:
然后清除FROM中的需要回收的对象:
最后 交换 FROM 和 TO 的位置:(FROM换成TO,TO换成FROM)
复制算法:将内存分为等大小的两个区域,FROM和TO(TO中为空)。先将被GC Root引用的对象从FROM放入TO中,再回收不被GC Root引用的对象。然后交换FROM和TO。这样也可以避免内存碎片的问题,但是会占用双倍的内存空间。
分代垃圾回收
- 新创建的对象首先会被分配在伊甸园区域。
- 新生代空间不足时,触发Minor GC,伊甸园和 FROM幸存区需要存活的对象会被COPY到TO幸存区中,存活的对象寿命+1,并且交换FROM和TO。
- Minor GC会引发 Stop The World:暂停其他用户的线程,等待垃圾回收结束后,用户线程才可以恢复执行。
- 当对象寿命超过阈值15时,会晋升至老年代。
- 如果新生代、老年代中的内存都满了,就会先触发Minor GC,再触发Full GC,扫描新生代和老年代中所有不再使用的对象并回收。
长时间使用的对象放在老年代中(长时间回收一次,回收花费时间久),用完即可丢弃的对象放在新生代中(频繁需要回收,回收速度相对较快):
回收流程
新创建的对象都被放在了新生代的伊甸园中:
当伊甸园中的内存不足时,就会进行一次垃圾回收,这时的回收叫做 Minor GC:
Minor GC 会将伊甸园和幸存区FROM仍需要存活的对象先复制到 幸存区 TO中, 并让其寿命加1,再交换FROM和TO。
伊甸园中不需要存活的对象清除:
交换FROM和TO:
同理,继续向伊甸园新增对象,如果满了,则进行第二次Minor GC:
流程相同,仍需要存活的对象寿命+1
:(下图中FROM中寿命为1的对象是新从伊甸园复制过来的,而不是原来幸存区FROM中的寿命为1的对象,这里只是静态图片不好展示,只能用文字描述了)
再次创建对象,若新生代的伊甸园又满了,则会再次触发 Minor GC(会触发 stop the world, 暂停其他用户线程,只让垃圾回收线程工作),这时不仅会回收伊甸园中的垃圾,还会回收幸存区中的垃圾,再将活跃对象复制到幸存区TO中。回收以后会交换两个幸存区,并让幸存区中的对象寿命加1!
如果幸存区中的对象的寿命超过某个阈值(最大为15,4bit),就会被放入老年代中:
如果新生代老年代中的内存都满了,就会先触发Minor Gc,再触发Full GC,扫描新生代和老年代中所有不再使用的对象并回收:
46. java传值、传引用(对象)的区别
- 传值:传递的是值的副本。方法中对副本的修改,不会影响到调用方。
- 传引用:传递的是引用的副本,共用一个内存,会影响到调用方。此时,形参和实参指向同一个内存地址。对引用副本本身(对象地址)的修改,如设置为null,重新指向其他对象,不会影响到调用方。(不包括String、以及包装类)
47. 并发并行的区别
并发(Concurrency)
早期计算机的 CPU 都是单核的,一个 CPU 在同一时间只能执行一个进程/线程,当系统中有多个进程/线程等待执行时,CPU 只能执行完一个再执行下一个。
计算机在运行过程中,有很多指令会涉及 I/O 操作,而 I/O 操作又是相当耗时的,速度远远低于 CPU,这导致 CPU 经常处于空闲状态,只能等待 I/O 操作完成后才能继续执行后面的指令。
为了提高 CPU 利用率,减少等待时间,人们提出了一种 CPU 并发工作的理论。
所谓并发,就是通过一种算法将 CPU 资源合理地分配给多个任务,当一个任务执行 I/O 操作时,CPU 可以转而执行其它的任务,等到 I/O 操作完成以后,或者新的任务遇到 I/O 操作时,CPU 再回到原来的任务继续执行。
并行(Parallelism)
并发是针对单核 CPU 提出的,而并行则是针对多核 CPU 提出的。和单核 CPU 不同,多核 CPU 真正实现了“同时执行多个任务”。
多核 CPU 内部集成了多个计算核心(Core),每个核心相当于一个简单的 CPU,如果不计较细节,你可以认为给计算机安装了多个独立的 CPU。
多核 CPU 的每个核心都可以独立地执行一个任务,而且多个核心之间不会相互干扰。在不同核心上执行的多个任务,是真正地同时运行,这种状态就叫做并行。
48. synchronized和lock的区别
层次:
synchronized是java的关键字,是jvm层面的
lock是java中的一个接口
释放:
synchronized:当获取锁的线程执行完毕之后,会释放锁;或者当线程执行是发生异常会释放锁。
lock:需要手动释放,所以在使用时候最好使用try catch 在finally中释放
获取
synchronized:假设线程A获得锁,B线程等待。如果A线程阻塞,B线程会一直等待
Lock: 分情况而定,Lock有多个锁获取的方式,大致就是可以尝试获得锁,线程可以不用一直等待(可以通过tryLock判断有没有锁)
死锁产生
synchronized: 在发生异常时候会自动释放占有的锁,因此不会出现死锁
Lock: 发生异常时候,不会主动释放占有的锁,必须手动unlock来释放锁,可能引起死锁的发生
锁的状态
synchronized:无法判断
lock:可以判断
锁的类型
synchronized: 可重入 不可中断 非公平
Lock: 可重入 可判断 可公平(两者皆可),公平锁是通过入队列排队
性能
synchronized: 少量同步
Lock: 大量同步
- Lock可以提高多个线程进行读操作的效率。(可以通过readwritelock实现读写分离)
- 在资源竞争不是很激烈的情况下,Synchronized的性能要优于ReetrantLock,但是在资源竞争很激烈的情况下,Synchronized的性能会下降几十倍,但是ReetrantLock的性能能维持常态;
- ReentrantLock提供了多样化的同步,比如有时间限制的同步,可以被Interrupt的同步(synchronized的同步是不能Interrupt的)等。在资源竞争不激烈的情形下,性能稍微比synchronized差点点。但是当同步非常激烈的时候,synchronized的性能一下子能下降好几十倍。而ReentrantLock确还能维持常态。
用法
synchronized: 在需要同步的对象中加入此控制,synchronized可以加在方法上,也可以加在特定代码块中,括号中表示需要锁的对象。
Lock: 一般使用ReentrantLock类做为锁。在加锁和解锁处需要通过lock()和unlock()显示指出。所以一般会在finally块中写unlock()以防死锁。
底层实现
synchronized: 底层使用指令码方式来控制锁的,映射成字节码指令就是增加来两个指令:monitorenter和monitorexit。当线程执行遇到monitorenter指令时会尝试获取内置锁,如果获取锁则锁计数器+1,如果没有获取锁则阻塞;当遇到monitorexit指令时锁计数器-1,如果计数器为0则释放锁。
Lock: 底层是CAS乐观锁,依赖AbstractQueuedSynchronizer类,把所有的请求线程构成一个CLH队列。而对该队列的操作均通过Lock-Free(CAS)操作。