整理面试题

一、基础篇

Java 基础

1.面向对象的特征

可以说是三大特征,也可以说是四大特征

①第一个是抽象:就是把有共同特征的一类事物,构造成类,只关注它的属性和行为,而不关注这些行为的细节

②第二个是封装:就是把数据和操作数据的方法绑定起来,私有化,对外只提供一个最简单的方法

③第三个是继承:子类继承父类的属性,比如说有多个类定义了共性的内容时,为了提高代码的复用性,就把这些类中的共性内容抽取出来,定义在一个独立的类中,然后再使用其他类去继承这个类中的共性部分,实现了复用,这就是继承

④第四个是多态:顾名思义就是一个对象具有多种形态,也可以理解为事务存在的多种形态,具体表现就是一个父类引用指向子类实现,多态实现的前提是,必须存在继承或者实现关系。

2.final, finally, finalize 的区别

①首先是final修饰符,被final修饰的类,是最终类,不能作为父类被子类继承,所以说一个类不能同时被final和abstract同时修饰; 被final修饰的变量,它的属性不能改变,所以在创建的时候必须给出初始值,此后只能被使用; 被final修饰的方法也同样只能被使用,不能被重写。

②然后是finally,finally是异常处理语句结构的一部分,不管有没有异常发生,finally里的代码块总是会被执行,除非是虚拟机停止才不被执行,所以在有需要无论发生什么都必须执行的代码,就可以放在finally块里,比如IO流,JDBC连接的资源释放,就适合写在这里。

③finalize ,finalize()是Object中的方法,当垃圾回收器将要回收对象所占内存之前被调用。

但在java中,如果内存充足,垃圾回收可能永不执行

3.int 和 Integer 有什么区别

JAVA数据类型有基本数据类型和引用数据类型,为了方便把基本数据类型当成对象处理,JAVA引入了基本数据类型对应的封装类,int的封装类就是Integer

所以说他们的区别,第一点就是数据类型不同,int是基本数据类型,Integer是引用数据类型;第二点是默认值不同,int默认值是0,integer默认值是null;第三点是int可以直接存储数值,而Integer必须要实例化对象,指向对象的地址。还有值得提的一点就是如果在-128到正127之间的数值,保存在JAVA的常量池中,他们被装箱成Integer对象后,会在内存中被重用,始终只存在一个对象。

parseInt()是把String转换成int,是基本类型。

valueOf()还可以接受int类型参数,返回的封装类Integer。

4.重载和重写的区别

方法重写和方法重载都是实现多态的方式,区别在于重载是编译时的多态,重写是运行时的多态;

重载发生在一个类中,同名的方法如果有不同的参数列表,参数类型不同,参数个数不同或者都不相同,就是方法重载,和返回值没有关系;

重写是发生在父类和子类之间,重写要求子类重写的方法和父类被重新写方法有相同的参数列表,有兼容的返回类型(子类<父类),比父类被重新写方法更好访问,不能比父类被重写方法声明更多的异常。

5.说说反射的用途及实现

反射的核心是JVM在运行的时候,才动态加载类或者调用方法或属性,事先不需要知道运行的对象是谁。

主要用途,就是开发各种通用的框架,比如Spring,通过xml文件去配置JavaBean;

实现比如可以获取class对象,创建实例,获取方法,调用方法,获取类的成员变量信息,获取构造器信息等。

①类名.class

②对象名.getClass

③class.forName(全限名)

6.说说自定义注解的场景及实现

java中有四种元注解:@Retention、@Inherited、@Documented、@Targe

使用场景有

①类属性自动赋值

②验证对象属性完整性

③代替配置文件功能,像spring基于注解的配置

④可以生成文档,像java代码注释中的@see,@param等

7.HTTP 请求的 GET 与 POST 方式的区别

一般我们在浏览器输入一个网址访问网站都是GET请求;

在FORM表单中,可以通过设置Method指定提交方式为GET或者POST提交方式,默认为GET提交方式

1.请求方式上

GET请求,请求的数据会附加在URL后面,用问号?分割URL和传输的数据,多个参数直接用&号连接,采用的编码格式是ASCII。

Post请求,会把请求的数据放在HTTP的请求体中

2.传输数据大小上

GET请求受到URL长度的限制,因此传输数据较小。(URL不存在参数上限的问题,这个限制是特定浏览器和服务器的限制)

POST理论上不会受到限制,除非是服务器规定限制提交数据的大小。

3.安全性上

POST的安全性更高,GET请求会把数据暴露在地址栏中,如果是用户名和密码明文出现在URL上,很容易被别人查看浏览器的历史记录而获取到账号密码

4.GET是从服务器上获取数据,而POST是用来向服务器上传递数据

8.如何解决ajax跨域

有三种解决方案

第一是①在响应头中添加Header允许访问,因为JavaScript无法控制HTTP头,需要通过目标域返回的HTTP头来授权是否允许跨域访问,在Controller中加@CrossOrigin注解

第二是②使用jsonp,只支持get请求,不支持post请求,使用时要把dataType改成jsonp,jsonp写jsonpCallback,后端获取get请求中的jsonpCallback,构造回调结构

第三是③使用接口网关,nginx、springcloud zuul

实际上就是通过"同源"的域名,不同的项目名进行区分,通过nginx拦截匹配,转发到对应的网址。整个过程,两次请求,第一次请求nginx服务器,第二次nginx服务器通过拦截匹配分发到对应的网址。

9.session 与 cookie 区别

①存储位置:Cookie存储在浏览器或者本地,Session只能存在服务器上

②存储对象:Cookie只能存储String类型的对象,Session能够存储任意的java对象

③安全性:Cookie有安全隐患,拦截或者在本地文件中能找到cookie,进而攻击,Session相对安全

④性能上:Session占用服务器的性能,Session过多,会增加服务器的压力

⑤存储大小上:单个Cookie保存的数据不能超过4K,很多浏览器都限制一个站点最多保存20个Cookie

​ Session没有大小的限制,和服务器的内存大小有关系。

session的工作原理:

​ 当浏览器请求服务器后,服务器会读取浏览器的cookie中有无sessionid,如果没有读到,那么容器将创建一个session对象,同时会为该session分配一个sessionid,当服务器响应时,这个sessionid会随着响应报文写入客户端的cookie中,那么下次再访问时,这个sessionid随着cookie传入服务器,服务器获取cookie中的sessionid,获取到后在容器中获取和sessionid对应的session对象。

10.JDBC 流程

执行一次JDBC连接,一般需要6个步骤。

第一步,导入jar包。

第二步,注册JDBC的驱动程序,需要初始化驱动程序,这样就可以打开和数据库的通信。

(可以不注册驱动)

第三步,创建一个连接,用DriverManager.getConnection()方法来创建一个Connection连接对象,它代表一个数据库的物理连接。

第四步,执行一个查询,需要使用一个Statement或PreparedStatement类型的对象,并且提交一个SQL语句到数据库中执行。

(关系:PreparedStatement继承自Statement,两者都是接口)

(区别:PreparedStatement可以使用占位符,防止SQL注入,而且是预编译的,批处理比Statement效率高)

第五步,是执行完查询获取到一个结果集,从结果集中获取检索到的结果。

最后一步就是释放资源,减少资源的浪费。

11.MVC 设计思想

Model-View-Controller
把一个应用的输入,处理,输出按照Model,Controller,View的方式分离,这样应用就被分成三层也就是模型层, 控制层,视图层
视图层代表用户交互界面,对于web应用来说,就是HTML界面
模型层就是业务流程、状态的处理以及业务规则的制定
控制层就是从客户接收请求,将模型与视图匹配在一起,就是一个分发器

12.equals 与 == 的区别

①"=="(比较的是地址值)
在编译String s1=“a"的时候.

其实是jvm在常量池中创建了一个内容为"a"的地址值,然后让s1去指向"a”,而不是把"a"直接赋值给s1;
在编译String s2=“a"的时候常量池中已经有了"a"的地址值,所以让s2直接指向常量池中的"a”,这样s1和s2的地址值都是常量池中"a"的地址值,所以通过双等号的运算结果是true。

然而在第二种情况下执行String s2=new String(“a”)的时候,每new一次就会出现一个新的对象,所以这种情况是直接在堆内存中开辟了一块新的空间去储存"a",所以此时s1和s2的地址值是不一样的,自然==的结果就为false

②equals 比较的是地址指向的内容是否相等

先比较地址是否相同,如果地址相同,值就一定相等,如果地址不同,再判断是不是String类型,然后比较字符串的长度,如果不同直接false,相同再转成字符数组,来循环比较,如果都相同就是true,否则就是false

13.java有以下四种创建多线程的方式
  • 1:继承Thread类创建线程
  • 2:实现Runnable接口创建线程
  • 3:使用Callable和FutureTask创建线程
  • 4:使用线程池,例如用Executor框架创建线程
callable和Runable的区别

Callable与Runnable的功能大致相似,Callable中有一个call()函数,但是call()函数有返回值

而Runnable的run()函数不能将结果返回给客户程序。

实现Runnable 与 继承Thread相比有什么优势?

实现Runnable接口避免了单继承的局限性,所以较为常用。

实现Runnable接口的方式,更加的符合面向对象,线程分为两部分,一部分线程对象,一部分线程任务。

继承Thread类,线程对象和线程任务耦合在一起。

一旦创建Thread类的子类对象,既是线程对象,有又有线程任务。

实现runnable接口,将线程任务单独分离出来封装成对象,类型就是Runnable接口类型。

Runnable接口对线程对象和线程任务进行解耦。

什么是线程安全

​ 当多个线程访问某个方法时,不管你通过怎样的调用方式或者说这些线程如何交替的执行,我们在主程序中不需要去做任何的同步,这

个类的结果行为都是我们设想的正确行为,那么我们就可以说这个类时线程安全的。

​ 如果一段代码可以保证多个线程访问的时候正确操作共享数据,那么它是线程安全的。

sleep()
sleep() 方法是线程类(Thread)的静态方法,让调用线程进入睡眠状态,让出执行机会给其他线程,等到休眠时间结束后,线程进入就绪状态和其他线程一起竞争cpu的执行时间,没有释放锁。

wait()

​ wait()是Object类的方法,当一个线程执行到wait方法时,它就进入到一个和该对象相关的等待池,同时释放对象的锁,使得其他线程能够访问,可以通过notify,notifyAll方法来唤醒等待的线程。

14.线程池参数

corePoolSize——线程池核心线程大小数

线程池中会维护一个最小的线程数量,即使这些线程处理空闲状态,他们也不会被销毁;

maximumPoolSize——线程池最大线程数量

一个任务被提交到线程池以后,首先会找有没有空闲存活线程,如果有则直接将任务交给这个空闲线程来执行,

如果没有则会缓存到工作队列(后面会介绍)中,如果工作队列满了,才会创建一个新线程。

线程池不会无限制的去创建新线程,它会有一个最大线程数量的限制。

keepAliveTime——空闲线程存活时间

一个线程如果处于空闲状态,并且当前的线程数量大于corePoolSize,那么在指定时间后,这个空闲线程会被销毁。

unit——空闲线程存活时间单位

workQueue——工作队列(阻塞队列)

新任务被提交后,会先进入到此工作队列中,任务调度时再从队列中取出任务。

threadFactory——线程工厂

创建一个新线程时使用的工厂,可以用来设定线程名等等。

handler——拒绝策略

当工作队列中的任务已到达最大限制,并且线程池中的线程数量也达到最大限制,这时如果有新任务提交进来,这里就拒绝。

AbortPolicy:丢弃任务并抛出RejectedExecutionException异常。

DiscardPolicy:丢弃任务,但是不抛出异常。如果线程队列已满,则后续提交的任务都会被丢弃,且是静默丢弃。

DiscardOldestPolicy:丢弃队列最前面的任务,然后重新提交被拒绝的任务。

CallerRunsPolicy:由调用线程处理该任务

*创建线程池的4种方式

①newCachedThreadPool创建一个可缓存线程池,如果线程池长度超过处理需要,可灵活回收空闲线程,若无可回收,则新建线程。

②newFixedThreadPool 创建一个定长线程池,可控制线程最大并发数,超出的线程会在队列中等待。

③newScheduledThreadPool 创建一个定长线程池,支持定时及周期性任务执行。

④newSingleThreadExecutor 创建一个单线程化的线程池,它只会用唯一的工作线程来执行任务,保证所有任务按照指定顺序(FIFO, LIFO, 优先级)执行

15.String常用方法

一、判断

①equals equalsIgnoreCase——相等

②contains——包含

③startsWith endsWith——以…开头或结尾

二、改变内容

①toUpperCase toLowerCase——大写、小写

②replace——替换

③subString——截取

④trim——去除字符串前后空格

三、长度

①length()

②indexof

③charAt

*String StringBuffer StringBuilder

String底层是final修饰的是字符数组(不可变)

StringBuffer安全 同步锁 慢 append在原来的基础上拼接

StringBuilder不安全 无锁 快 append在原来的基础上拼接

16.深拷贝和浅拷贝

对于基本类型,深拷贝和浅拷贝都是一样的,都是对原始数据的复制,修改原始数据,不会对复制数据产生影响。
两者的区别,在于对引用属性的复制。
浅拷贝
浅拷贝复制引用属性时,仅仅复制指针值,没有复制指向的对象。
深拷贝
深拷贝完整复制一份该属性指向的对象,两个对象修改时,互不影响。

Object.clone()方法属于浅拷贝。

如果想使用深拷贝,必须在类里面重写clone()方法。
要想调用类的clone()方法,类必须实现Cloneable接口,

该接口是个空接口,如果不实现该接口,调用clone()方法会抛出异常CloneNotSupportedException。

17.java8新特性
①Lambda表达式

函数式接口

Lambda表达式允许把函数作为一个方法的参数(函数作为参数传递进方法中),或者把代码看成数据在最简单的形式中,一个lambda可以由用逗号分隔的参数列表、–>符号与函数体三部分表示。

②**流(Stream) **

流就是数据渠道,用于操作数据源(集合、数组等)所生成的元素序列,生成一个新集合。

1.创建 Stream

一个数据源(如: 集合、数组), 获取一个流。

2.中间操作

一个中间操作链,对数据源的数据进行处理。

3.终止操作(终端操作)

一个终止操作,执行中间操作链,并产生结果 。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-7M9bzrtB-1639059913921)(C:\Users\Administrator\Desktop\cz\20200525225333187.jpg)]

二、Java 集合

1.List 和 Set 和Map区别

List

一个有序(元素存入集合的顺序和取出的顺序一致)容器,元素可以重复,可以插入多个null元素,元素都有索引。

常用的实现类有 ArrayList、LinkedList 和 Vector。

Arraylist: Object数组

Vector: Object数组

LinkedList: 双向循环链表

Set

一个无序(存入和取出顺序有可能不一致)容器,不可以存储重复元素,只允许存入一个null元素,必须保证元素唯一性。

常用实现类是 HashSet、LinkedHashSet 以及 TreeSet

HashSet(无序,唯一)

基于 HashMap 实现的,底层采用 HashMap 来保存元素

LinkedHashSet

LinkedHashSet 继承与 HashSet,并且其内部是通过 LinkedHashMap 来实现的

TreeSet(有序,唯一)

红黑树(自平衡的排序二叉树)

Map

是一个键值对集合,存储键、值和之间的映射。 Key无序,唯一;value 不要求有序,允许重复。

Map没有继承于Collection接口,从Map集合中检索元素时,只要给出键对象,就会返回对应的值对象。

常用实现类:HashMap、TreeMap、HashTable、LinkedHashMap、ConcurrentHashMap

3.ArrayList 与 LinkedList 区别

主要差别在于数据结构不同,ArrayList是基于动态数组实现的,内存空间连续,而LinkedList是基于双向链表实现的。

因为结构的不同,在查询比较多的情况下,ArrayList要优于LinkedList,因为LinkedList要移动指针

在新增和删除操作比较多的情况下,LinkedList要优于ArrayList,因为ArrayList要移动数据进行排序

LinkedList需要更多的内存,因为ArrayList的每个索引的位置都是实际数据,而LinkedList中的每个节点存储的是实际数据和前后节点的位置。

4.ArrayList 与 Vector 区别

ArrayList和Vector都实现了List接口,都是有序、可重复的集合

区别主要在两个方面。

一方面是线程安全上,Vector的线程安全,因此效率会低一些,而ArrayList是非线程安全,效率会高一些。

另一方面在扩容上,ArrayList增长为原来的1.5倍,Vector增加为原来的一倍,都可以设置初始的空间大小。

(另外Vector可以设置增长的空间大小,而ArrayList没有这样的方法。)

5.HashMap 和 HashTable 的区别

① Hashtable是线程安全,而HashMap则非线程安全,Hashtable的实现方法里面大部分都添加了synchronized关键字来确保线程同步,因此相对而言HashMap性能会高一些,在多线程环境下若使用HashMap需要使用Collections.synchronizedMap()方法来获取一个线程安全的集合。

②HashMap的键和值都可以为null,而Hashtable的键值都不能为null。

③ HashMap的初始容量为16,Hashtable初始容量为11,两者的填充因子默认都是0.75。HashMap扩展容量是当前容量翻倍,Hashtable扩展容量是容量翻倍+1。

④两者的哈希算法不同

HashMap是先对key(键)求hashCode码,然后把这个码和数组初始长度减一做&(与运算)

就可以计算出此键值对应该保存到数组的那个位置上(hash&(n-1))。

hashtable直接计算key的哈希码,然后与2的31次方做&(与运算),然后对数组长度取余数计算位置。

⑤HashMap是对Map接口的实现,HashTable实现了Map接口和Dictionary抽象类。

6.HashSet 和 HashMap 区别
HashMap HashSet
实现了Map接口 实现Set接口
存储键值对 仅存储对象
调用put()向map中添加元素 调用add()方法向Set中添加元素
HashMap使用键(Key)计算Hashcode HashSet使用成员对象来计算hashcode值,对于两个对象来说hashcode可能相同,所以equals()方法用来判断对象的相等性,如果两个对象不同的话,那么返回false
HashMap相对于HashSet较快,因为它是使用唯一的键获取对象 HashSet较HashMap来说比较慢

HashMap

线程不安全,底层是数组加链表或者红黑树实现,初始容量16,扩容为原来的2倍,扩充因子是0.75

key和value都可以为null值,key不可以重复,但是value可以重复,如果key重复,value值会覆盖,

HashMap是先对key(键)求hashCode码,然后把这个码和数组初始长度减一做&(与运算)

就可以计算出此键值对应该保存到数组的那个位置上,如果重复,就用倒插法,先来的放后边。

如果链表的长度大于等于8时,链表就会转为树结构。

链表长度超过8转换成树结构

如果桶中的链表元素个数小于等于6时,树结构会还原成链表。

因为红黑树的平均查找长度是log(n),长度为8的时候,平常查找长度为3,

如果继续使用链表,平均查找长度是8/2=4,因此才有转换为树的必要。

如果链表长度小于等于6,查找长度是6/2=3,虽然速度也很快,但是转换为树结构,和生成树也需要时间,因为不用转成树

而且选择6和8,中间有一个差值7,可以有效的防止链表和树结构频繁转换。

假如链表个数超过8就转换成树结构,链表个数小于8就转换成链表结构,如果有一个HashMap不停的插入、删除元素。链表个数在8左右

徘徊,就会频繁的发生树转链表,链表转树,效率会很低。

ConcurrentHashMap

底层是数组加链表或者红黑树实现,

jdk1.7中采用Segment分段数组的方式进行实现,采取分段锁来保证安全性,对每一个段加锁,这样就比hashtable给整个数组加锁要效率高,理论上效率要高16倍。

JDK1.8 做了优化,直接用Node 数组+链表+红黑树的数据结构来实现,并发控制使用Synchronized 和 CAS来操作,在头结点加锁,整个看起来就像是优化过且线程安全的 HashMap。

HashMap多线程条件下会出现死循环

线程1扩容完成,还没来得及把原数据重新排序赋值,切换到线程2,线程2完成这个顺序,把原数据重新排序完成,线程1再来排序,元素A移动节点头的位置,指向元素B的entry,在这之前元素B的next也指向元素A的entry,出现死循环。也有key相同,出现元素丢失的情况。

三、Java 锁机制

1.说说线程安全问题

比如说有一个arraylist数组,有两个线程往数组里赋值,正常顺序是A线程赋值,然后让size加1,随后B线程赋值,然后size+1,但在实际运行中,可能A线程赋完值,还没Size+1,这个时候cpu正好轮转给B线程,这个时候B线程赋值的位置还没有size+1,所以和A线程操作了同一个位置上的值,这样的线程就是不安全的。

当多个线程访问某个方法时,不管你通过怎样的调用方式或者说这些线程如何交替的执行,我们在主程序中不需要去做任何的同步,这

个类的结果行为都是我们设想的正确行为,那么我们就可以说这个类时线程安全的。

如果一段代码可以保证多个线程访问的时候正确操作共享数据,那么它是线程安全的。

为了解决这种线程安全问题,可以加锁

synchronized关键字,就是用来控制线程同步的,保证我们的线程在多线程环境下,不被多个线程同时执行,确保我们数据的完整性,使用方法一般是加在方法上。

虽然加synchronized关键字,可以让我们的线程变得安全,但是我们在用的时候,也要注意缩小synchronized的使用范围,如果随意使用时很影响程序的性能,别的对象想拿到锁,结果你没用锁还一直把锁占用,这样就有点浪费资源。

或者lock锁

2.volatile 实现原理

volatile是一个变量修饰符,只能用来修饰变量。无法修饰方法及代码块等。

volatile的用法比较简单,只需要在声明一个可能被多线程同时访问的变量时,使用volatile修饰就可以了。

原理:

为了提高处理器的执行速度,在处理器和内存之间增加了多级缓存来提升。但是由于引入了多级缓存,就存在缓存数据不一致问题。

但是,对于volatile变量,当对volatile变量进行写操作的时候,JVM会向处理器发送一条lock前缀的指令,将这个缓存中的变量回写到系统主存中。

但是就算写回到内存,如果其他处理器缓存的值还是旧的,再执行计算操作就会有问题,所以在多处理器下,为了保证各个处理器的缓存是一致的,就会实现缓存一致性协议

缓存一致性协议:每个处理器通过嗅探在总线上传播的数据来检查自己缓存的值是不是过期了,当处理器发现自己缓存行对应的内存地址被修改,就会将当前处理器的缓存行设置成无效状态,当处理器要对这个数据进行修改操作的时候,会强制重新从系统内存里把数据读到处理器缓存里。

因此volatile保证了可见性

①volatile与可见性

Java中的volatile关键字提供了一个功能,那就是被其修饰的变量在被修改后可以立即同步到主内存,被其修饰的变量在每次是用之前都从主内存刷新。因此,可以使用volatile来保证多线程操作时变量的可见性。

②volatile与有序性

volatile除了可以保证数据的可见性之外,还有一个强大的功能,那就是他可以禁止指令重排优化,使程序执行的顺序按照代码的先后顺序执行

③volatile与原子性

为了保证原子性,需要通过字节码指令monitorenter和monitorexit,但是volatile和这两个指令之间是没有任何关系的。

所以,volatile是不能保证原子性的。

3.synchronized 实现原理

显式同步:

查看带有Synchronized语句块的class文件可以看到在同步代码块的起始位置插入了moniterenter指令,在同步代码块结束的位置插入了monitorexit指令。(JVM需要保证每一个monitorenter都有一个monitorexit与之相对应,但每个monitorexit不一定都有一个monitorenter)

隐式同步:

​ 但是查看同步方法的class文件时,同步方法并没有通过指令monitorenter和monitorexit来完成,而被翻译成普通的方法调用和返回指令,只是在其常量池中多了ACC_SYNCHRONIZED标示符。JVM就是根据该标示符来实现方法的同步的:当方法调用时,调用指令将会检查方法的 ACC_SYNCHRONIZED 访问标志是否被设置,如果设置了,执行线程将先获取monitor,获取成功之后才能执行方法体,方法执行完后再释放monitor。在方法执行期间,其他任何线程都无法再获得同一个monitor对象。 其实本质上没有区别,只是方法的同步是一种隐式的方式来实现,无需通过字节码来完成。

如果一个同步方法执行期间抛 出了异常,并且在方法内部无法处理此异常,那这个同步方法所持有的monitor将在异常抛到同步方法之外时自动释放。

monitorenter和monitorexit。当一条线程进行执行的遇到monitorenter指令的时候,它会去尝试获得锁,如果获得锁那么锁计数+1(为什么会加一呢,因为它是一个可重入锁,所以需要用这个锁计数判断锁的情况),如果没有获得锁,那么阻塞。当它遇到monitorexit的时候,锁计数器-1,当计数器为0,那么就释放锁

4.synchronized 与 lock 的区别

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-VYxvl1vY-1639059913922)(C:\Users\Administrator\Desktop\cz\微信截图_20211019085725.png)]

两者都是锁,用来控制并发冲突,

①区别在于synchronized 是一个关键字,是jvm层面上的,Lock是个接口,提供的功能更加丰富
②synchronized获取锁的线程执行完同步代码会自动释放锁,线程执行过程中发生异常,JVM会让线程释放锁。

而Lock必须手动释放,并且代码中出现异常会导致unlock代码不执行,所以Lock一般在Finally中释放。

③synchronized假设A线程获得锁,B线程等待,如果A线程阻塞,B线程会一直等待下去。

​ Lock可以尝试获得锁,线程不用一直等待

④synchronized无法判断锁的状态,lock可以提供trylock()判断锁的状态

⑤synchronized不可中断,是非公平锁,而lock锁可以通过 lock.lockInterruptibly()方法中断锁

⑥性能上synchronized少量同步,lock适合大量同步

5.CAS 乐观锁

CAS是一种有名的无锁算法

synchronized关键字保证同步,加锁,这种锁机制会产生一些问题

多线程时,加锁释放锁会耗费很多时间,引发性能问题
独占锁是悲观锁,synchronized是独占锁

而更有效的是乐观锁,CAS就是乐观锁

CAS有三个参数,内存位置的值—V,预期原值—A,新值—B

如果内存位置的值V与预期原值A相匹配,那么处理器会自动将该位置更新为新值;否则,处理器不做任何操作

总结如下:

  • CAS(Compare And Swap)比较并替换,是线程并发运行时用到的一种技术
  • CAS是原子操作,保证并发安全,而不能保证并发同步
  • CAS是CPU的一个指令(需要JNI调用Native方法,才能调用CPU的指令)
  • CAS是非阻塞的、轻量级的乐观锁

CAS优点

非阻塞的轻量级的乐观锁,通过CPU指令实现,在资源竞争不激烈的情况下性能高,

相比synchronized重量锁,synchronized会进行比较复杂的加锁、解锁和唤醒操作

CAS缺点
①会产生ABA问题
②自旋时间过长,消耗CPU资源,如果资源竞争激烈,多线程自旋长时间消耗资源

何时使用
**乐观锁适用于写比较少的情况下(多读场景),**即冲突真的很少发生的时候,这样可以省去了锁的开销,加大了系统的吞吐量。

但如果是多写的情况,一般会经常发生冲突,这就会导致CAS算法会不断的进行retry,这样反倒是降低了性能,所以一般多写的场景下用

悲观锁就比较合适

6.ABA 问题

ABA问题: 线程C、D;线程D将A修改为B后又修改为A,此时C线程以为A没有改变过,java的原子类AtomicStampedReference,通过控制变量值的版本号来保证CAS的正确性。具体解决思路就是在变量前追加上版本号,每次变量更新的时候把版本号加一,那么A - B - A就会变成1A - 2B - 3A

7.乐观锁的业务场景及实现方式
8.锁优化/锁升级

锁可以升级不可以降级,但是偏向锁状态可以被重置为无锁状态。

锁的四种状态:无锁状态、偏向锁状态、轻量级锁状态、重量级锁状态(级别从低到高)

(1)偏向锁:

大多数时候是不存在锁竞争的,常常是一个线程多次获得同一个锁,因此如果每次都要竞争锁会增大很多没有必要付出的代价,为了降低获取锁的代价,才引入的偏向锁。

偏向锁的升级

当线程1访问代码块并获取锁对象时,会在java对象头和栈帧中记录偏向的锁的threadID,因为偏向锁不会主动释放锁,因此以后线程1再次获取锁的时候,需要比较当前线程的threadID和Java对象头中的threadID是否一致,如果一致还是线程1获取锁对象,则无需使用CAS来加锁、解锁;

如果不一致,如线程2要竞争锁对象,而偏向锁不会主动释放因此还是存储的线程1的threadID,那么需要查看Java对象头中记录的线程1是否存活,如果没有存活,那么锁对象被重置为无锁状态,线程2可以竞争将其设置为偏向锁;

如果存活,那么立刻查找该线程的栈帧信息,如果还是需要继续持有这个锁对象,那么暂停当前线程1,撤销偏向锁,升级为轻量级锁,如果线程1 不再使用该锁对象,那么将锁对象状态设为无锁状态,重新偏向新的线程。

(2)轻量级锁

轻量级锁考虑的是竞争锁对象的线程不多,而且线程持有锁的时间也不长的情景。因为阻塞线程需要CPU从用户态转到内核态,代价较大,如果刚刚阻塞不久这个锁就被释放了,那这个代价就有点得不偿失了,因此这个时候就干脆不阻塞这个线程,让它自旋这等待锁释放。

轻量级锁升级为重量级

CAS乐观锁如果自旋时间太长也不行,因为自旋是要消耗CPU的,因此自旋的次数是有限制的,比如10次或者100次,如果自旋次数到了线程1还没有释放锁,或者线程1还在执行,线程2还在自旋等待,这时又有一个线程3过来竞争这个锁对象,那么这个时候轻量级锁就会膨胀为重量级锁。

重量级锁把除了拥有锁的线程都阻塞,防止CPU空转。

9.单例模式

单例模式是一种对象创建模式,用于生产一个对象的实例,它可以确保系统中一个类只产生一个实例,这样做有两个好处:

1.对于频繁使用的对象,可以省略创建对象所花费的时间,这对于那些重量级对象而言,是非常可观的一笔系统开销。

2.由于new操作的次数减少,所以系统内存的使用评率也会降低,这将减少GC压力,缩短GC停顿时间。

①饿汉模式(天生线程安全)

public final class EagerSingleton {
   
    
    private static EagerSingleton singObj = new EagerSingleton();
 
    private EagerSingleton() {
   
    }
 
    public static EagerSingleton getSingleInstance() {
   
        return singObj;
    }
}

这种写法就是所谓的饥饿模式,每个对象在没有使用之前就已经初始化了。这就可能带来潜在的性能问题:如果这个对象很大呢?没有使用这个对象之前,就把它加载到了内存中去是一种巨大的浪费。

针对这种情况,我们可以对以上的代码进行改进,使用一种新的设计思想——延迟加载(Lazy-load Singleton)

②懒汉模式(非线程安全)

public final class LazySingleton {
   
 
    private static LazySingleton singObj = null;
 
    private LazySingleton() {
   
    }
 
    public static LazySingleton getSingleInstance() {
   
        if (null == singObj ) {
   
            singObj = new LazySingleton();
        }
        return singObj;
    }
}

③懒汉模式(线程安全)

public final class ThreadSafeSingleton {
   
    
    private static ThreadSafeSingleton singObj = null;
 
    private ThreadSafeSingleton() {
   
    }
 
    public static Synchronized ThreadSafeSingleton getSingleInstance() {
   
        if (null == singObj ) {
   
            singObj = new ThreadSafeSingleton();
        }
        return singObj;
    }
}

④懒汉模式(双重锁校验)

public final class DoubleCheckedSingleton {
   
 
    private volatile static DoubleCheckedSingletonsingObj = null;
 
    private DoubleCheckedSingleton() {
   
    }
 
    public static DoubleCheckedSingleton getSingleInstance() {
   
        if (null == singObj ) {
   
            Synchronized(DoubleCheckedSingleton.class) {
   
                if (null == singObj) {
   
                    singObj = new DoubleCheckedSingleton();
                }
            }
        }
        return singObj;
    }
}

四、数据库

Mybatis执行sql语句的批量操作

①foreach标签

②循环调用方法

③执行器切换为batch 对相同的sql进行一次预编译,然后设置参数,最后统一执行操作

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-vw6kO30X-1639059913923)(C:\Users\Administrator\Desktop\cz\微信图片_20211028084936.png)]

SQL语句数据库层面的解析过程

①先查询高速缓存(library cache)

②语句合法性检查(data dict cache)

③语言含义检查(data dict cache)

④获得对象解析锁(control structer)

⑤数据访问权限的核对(data dict cache)

⑥确定最佳执行计划。

1.数据库设计的三大范式

第一范式

在关系模式R中,所有属性只包含原子值,原子性: 每一列不可拆分

第二范式

当且仅当关系模式R满足第一范式,必须有主键,每张表只描述一件事

第三范式

目的: 消除传递依赖 关联性: 从表的外键必须使用主表的主键

2.如何优化数据库 MySQL

①Sql语句及索引的优化(索引优化 1.Join语句优化 2.避免索引失效)

②数据库表结构的优化:符合三大范式

③系统配置的优化

④硬件的优化(扩大虚拟内存等)

3.MySQL索引使用的注意事项

什么是索引?

索引是一种特殊的文件(InnoDB数据表上的索引是表空间的一个组成部分),它们包含着对数据表里所有记录的引用指针。

索引是一种数据结构。数据库索引,是数据库管理系统中一个排序的数据结构,以协助快速查询、更新数据库表中数据。索引的实现通常使用B树及其变种B+树。

更通俗的说,索引就相当于目录。为了方便查找书中的内容,通过对内容建立索引形成目录。索引是一个文件,它是要占据物理空间的。

索引有哪些优缺点?

索引的优点

  • 可以大大加快数据的检索速度,这也是创建索引的最主要的原因。
  • 通过使用索引,可以在查询的过程中,使用优化隐藏器,提高系统的性能。

索引的缺点

  • 时间方面:创建索引和维护索引要耗费时间,具体地,当对表中的数据进行增加、删除和修改的时候,索引也要动态的维护,会降低增/改/删的执行效率;
  • 空间方面:索引需要占物理空间。
索引设计的原则

1.适合索引的列是出现在where子句中的列,或者连接子句中指定的列

2.基数较小的类,索引效果较差,没有必要在此列建立索引

3.使用短索引,如果对长字符串列进行索引,应该指定一个前缀长度,这样能够节省大量索引空间

4.不要过度索引。索引需要额外的磁盘空间,并降低写操作的性能。在修改表内容的时候,索引会进行更新甚至重构,索引列越多,这个时间就会越长。所以只保持需要的索引有利于查询即可。

创建索引的原则

索引虽好,但也不是无限制的使用,最好符合一下几个原则

1) 最左前缀匹配原则,组合索引非常重要的原则,mysql会一直向右匹配直到遇到范围查询(>、<、between、like)就停止匹配,比如a = 1 and b = 2 and c > 3 and d = 4 如果建立(a,b,c,d)顺序的索引,d是用不到索引的,如果建立(a,b,d,c)的索引则都可以用到,a,b,d的顺序可以任意调整。

​ 适合索引的列是出现在where子句中的列

2)较频繁作为查询条件的字段才去创建索引

3)更新频繁字段不适合创建索引

​ 基数较小的类,不适合创建索引

4)若是不能有效区分数据的列不适合做索引列(如性别,男女未知,最多也就三种,区分度实在太低)

5)不要过度索引,能使用扩展索引,尽量不要新建索引。比如表中已经有a的索引,现在要加(a,b)的索引,那么只需要修改原来的索引即可, 索引需要额外的磁盘空间,并降低写操作的性能

​ 使用短索引,如果对长字符串列进行索引,应该指定一个前缀长度,这样能够节省大量索引空间

6)定义有外键的数据列一定要建立索引。

7)对于那些查询中很少涉及的列,重复值比较多的列不要建立索引。

8)对于定义为text、image和bit的数据类型的列不要建立索引。

4.数据库索引的原理

索引用来快速地寻找那些具有特定值的记录。如果没有索引,一般来说执行查询时遍历整张表。

索引的原理很简单,就是把无序的数据变成有序的查询

①把创建了索引的列的内容进行排序

②对排序结果生成倒排表

③在倒排表内容上拼上数据地址链

④在查询的时候,先拿到倒排表内容,再取出数据地址链,从而拿到具体数据

5.什么情况下索引会失效

1、索引列中使用了运算或者函数

2、or语句中前后字段没有同时都为索引

3、数据类型出现隐式转化, 例如字符串比较没有使用单引号

4、like语句以%开头

5、索引字段中使用is null 、is not null 、!=、<>

6、复合索引中没有遵循最佳左前缀原则:最佳左前缀原则即要使复合索引生效,必须要先有左边的字段,再使用右边的字段时才会生效,直接使用右边的字段索引不会生效

7、查询结果大于全表的30%

6.为什么要用 B-Tree
使用B树的好处

B树可以在内部节点同时存储键和值,因此,把频繁访问的数据放在靠近根节点的地方将会大大提高热点数据的查询效率。这种特性使得B树在特定数据重复多次查询的场景中更加高效。

使用B+树的好处

由于B+树的内部节点只存放键,不存放值,因此,一次读取,可以在内存页中获取更多的键,有利于更快地缩小查找范围。 B+树的叶节点由一条链相连,因此,当需要进行一次全数据遍历的时候,B+树只需要使用O(logN)时间找到最小的一个节点,然后通过链进行O(N)的顺序遍历即可。而B树则需要对树的每一层进行遍历,这会需要更多的内存置换次数,因此也就需要花费更多的时间

7.聚集索引与非聚集索引的区别
  • 聚簇索引:将数据存储与索引放到了一块,找到索引也就找到了数据
  • 非聚簇索引:将数据存储于索引分开结构,索引结构的叶子节点指向了数据的对应行,myisam通过key_buffer把索引先缓存到内存中,当需要访问数据时(通过索引访问数据),在内存中直接搜索索引,然后通过索引找到磁盘相应数据,这也就是为什么索引不在key buffer命中时,速度慢的原因

澄清一个概念:innodb中,在聚簇索引之上创建的索引称之为辅助索引,辅助索引访问数据总是需要二次查找,非聚簇索引都是辅助索引

  • 0
    点赞
  • 10
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

隋zy

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值