java面试题目前为止全内容收集

10 篇文章 0 订阅

目录

java异常处理

异常是程序中的一些错误,但并不是所有的错误都是异常,并且错误有时候是可以避免的
比如说,你的代码少了一个分号,那么运行出来结果是提示是错误java.lang.Error;如果你用System.out.println(11/0),那么由于你将0用做了除数,会抛出java.lang.ArithmeticException的异常。

异常发生的原因有很多,通常包含了以下几大类:
1.用户输入了非法数据。
2.要打开的文件不存在。
3.网络通信时连接中断,或者JVM内存溢出。
这些异常有的是因为用户错误引起,有的是程序错误引起的,还有其他一些是因为物理错误引起的。
要理解Java异常处理时如何工作的,你需要掌握以下三种类型的异常:
检查性异常:最具代表的检查性异常是用户错误或问题引起的异常,这是程序员无法预见的。例如要打开一个不存在文件时,一个异常就发生了,这些异常在编译时不能被简单地忽略。
运行时异常:运行时异常是可以被程序员避免的异常。与检查性异常相反,运行时异常可以在编译时被忽略。
错误:错误不是异常,而是脱离程序员控制的问题。错误在代码中通常被忽略。例如,当栈溢出时,一个错误就发生了,它们在编译时也是检查不到的。

Exception类的层次

所有的异常类是从java.lang.Exception类继承的子类。
Exception类是Throwable类的子类。除了Exception类外,Throwable还有一个子类Error。
java程序通常不捕获错误。错误一般发生在严重故障时,它们在java程序处理的范畴之外。
Error用来指示运行时环境发生的错误。
例如,JVM内存溢出,一般地,程序不会从错误中恢复。
异常类有两个主要的子类:IOException类和RuntimeException类。

Java内置异常类

java语言定义了一些异常类在java.lang标准包中。
标准运行时异常类的子类是最常见的异常类。由于java.lang包是默认加载到所有的java程序的,所以大部分从运行时异常类继承而来的异常都可以直接使用。
java根据各个类库也定义了一些其他的异常,下面的表中列出了java的非检查性异常。

异常方法

捕获异常

使用try 和catch关键字可以捕获异常。try/catch代码块放在异常可能发生的地方。
try/catch代码块中的代码称为保护代码。

try
{
//程序代码
}catch(ExceptionName e1)
{
//Catch块
}

Catch语句包含要捕获异常类型的声明。当保护代码块中发生一个异常时,try后面的catch块就会被检查。如果发生的异常包含在catch块中,异常会被传递到该catch,这和传递一个参数到方法是一样的。

实例

ExcepTest.java

import java.io.*;
public class ExcepTest{
 
   public static void main(String args[]){
      try{
         int a[] = new int[2];
         System.out.println("Access element three :" + a[3]);
      }catch(ArrayIndexOutOfBoundsException e){
         System.out.println("Exception thrown  :" + e);
      }
      System.out.println("Out of the block");
   }
}

以上代码编译运行输出结果如下:

Exception thrown :java.lang.ArrayIndexOutOfBoundsException: 3
Out of the block

https://blog.csdn.net/weixin_45927841/article/details/123217462?spm=1001.2101.3001.6650.1&utm_medium=distribute.pc_relevant.none-task-blog-2%7Edefault%7ECTRLIST%7ERate-1-123217462-blog-124825404.pc_relevant_antiscanv3&depth_1-utm_source=distribute.pc_relevant.none-task-blog-2%7Edefault%7ECTRLIST%7ERate-1-123217462-blog-124825404.pc_relevant_antiscanv3&utm_relevant_index=2

作者:晗江雪
链接:https://www.nowcoder.com/discuss/836019
来源:牛客网

JAVA基础

JAVA反射

什么是反射?

反射是JAVA的特征之一,它允许运行中的JAVA程序对自身的检查
反射非常强大,它甚至能直接操作程序的私有属性。我们前面学习都有一个概念,被,被private封装的资源只能类内部访问,外部是不行的,但这个规定被反射赤裸裸的打破了。
反射就像一面镜子,它可以在运行时获取一个类的所有信息,可以获取到任何定义的信息(包括成员变量,成员方法,构造器等),并且可以操纵类的字段、方法、构造器等部分。

为什么需要反射?

如果向创建对象,我们可以直接new User();为何要通过反射去创建对象呢?

那我要先问你个问题了,你为什么要去餐馆吃饭呢?
例如:我们要吃个牛排大餐,如果我们自己创建,就什么都得管理。
好处是,每一步做什么我都很清晰,坏处是什么都得自己实现,那不是累死了。牛接生你管,吃什么你管,屠宰你管,运输你管,冷藏你管,烹饪你管,上桌你管。就拿做菜来说,你能有特级厨师做的好?
那怎么办呢?有句话说的好,专业的事情交给专业的人做,饲养交给农场主,屠宰交给刽子手,烹饪交给特级厨师。那我们干嘛呢?
我们翘起二郎腿直接拿过来吃就好了。
再者,饭店把东西做好,不能扔到地上,我们去捡着吃吧,那不是都成原始人了。那怎么办呢?很简单,把做好的东西放在一个容器中吧,如把牛排放在盘子里。

我们在后面的学习中,会学习框架,有一个框架Spring就是一个非常专业且功能强大的产品,它可以帮我们创建对象,管理对象。以后我无需手动new对象,直接从Spring提供的容器中的Beans获取即可。Beans底层其实就是一个Map<String,Object>,最终通过getBean(“user”)来获取。而这其中最核心的实现就是利用反射技术。

总结一句,类不是你创建的,是你同事或者直接是第三方公司,此时你要或得这个类的底层功能调用,就需要反射技术实现。有点抽象,别着急,我们做个案例,你就立马清晰。

package cn.tedu.reflection;

import java.lang.reflect.Constructor;

import org.junit.Test;

/**本类用来测试反射*/
public class TestReflect {
//7.通过单元测试方法,创建Student目标类的对象
    @Test
    public void getObject() throws Exception {
        //1.获取字节码对象
        Class<?> clazz = Student.class;
        //2.通过反射技术创建目标类的对象,注意抛出异常
        /*反射创建对象方案1:通过触发目标类的无参构造创建对象*/
        Object o = clazz.newInstance();
        System.out.println(o);//这一步已经获取到了对象Student{name='null', age=0}

        /*反射创建对象方案2:通过触发目标类的全参构造创建对象
        * 思路:
        * 1.先获取指定的构造函数对象,注意需要指定构造函数的参数,传入的是.class字节码对象
        * 2.通过刚刚获取到的构造函数对象创建Student目标类的对象,并且给对象的属性赋值
        * */
        //3.获取目标类中指定的全参构造
        Constructor<?> c = clazz.getConstructor(String.class, int.class);
        //System.out.println(c);
        //4.通过获取到的构造函数:创建对象+给对象的属性赋值
        Object o2 = c.newInstance("赵六", 6);
        System.out.println(o2);
    }
}

JAVA接口

为什么要用接口

接口被用来描述一种抽象。因为JAVA不像C++一样支持多继承,所以JAVA可以通过实现接口来弥补这个局限
接口也可以用来实现解耦。
接口被用来实现抽象,而抽象类也被用来实现抽象,为什么一定要用接口呢?接口和抽象类之间又有什么区别呢?原因是抽象类内部可能包含非final的变量,接口的静态成员变量要用static final public 来修饰
\接口中的方法都是抽象的,是没有方法体的,可以使用接口类型的引用指向一个实现了该接口的对象,并且可以调用这个接口中的方法。

可以直接把接口理解为100%的抽象类,既接口中的方法必须全部是抽象方法。(JDK1.8之前可以这样理解)

和抽象类区别:

● 抽象类实例化是变量指向实现抽象方法的子类对象,接口变量必须实现所有接口方法的类对象
● 抽象类要被子类继承,接口被类实现
● 接口只能做方法申明,抽象类可以做方法实现
● 接口定义的变量只能是公共的静态的常量,抽象类中是普通变量
● 接口可以通过匿名内部类实例化

● 一个抽象类可以是public、private、protected、default,接口只有public;
● 一个抽象类中的方法可以是public、private、protected、default,接口中的方法只能是public和default
● abstract不能与final并列修饰同一个类;abstract 不能与private、static、final或native并列修饰同一个方法
● 抽象方法不能有方法体,抽象方法不能使用private修饰符,也不宜使用默认修饰符(default)接口 不可以实例化 。 通过接口实现类创建对象

JAVA构造方法

构造方法的声明:
修饰符 class_name(类名)  (参数列表){
 逻辑代码
}
  1. 构造⽅法的⽅法名和类名⼀致(包括⼤⼩写)
  2. 构造⽅法没有返回值类型(连void都没有)
  3. 构造⽅法可以重载
  4. 构造⽅法不可以⼿动调⽤,只能在创建对象的时,jvm⾃动调⽤
  5. 构造⽅法在创建对象时只能调⽤⼀次
  6. 构造⽅法的⽅法名和类名⼀致(包括⼤⼩写)
  7. 构造⽅法没有返回值类型(连void都没有)
  8. 构造⽅法可以重载
  9. 构造⽅法不可以⼿动调⽤,只能在创建对象的时,jvm⾃动调⽤
  10. 构造⽅法在创建对象时只能调⽤⼀次

当⼀个类中,没有定义构造⽅法 系统会⾃动提供⼀个公开的 ⽆参的构造⽅法 当类中已经定义了构 造⽅法,系统不再提供⽆参公开构造,如果需要使⽤⽆参的构造 那么必须⾃⼰定义出来 ⼀般开发如果 定义了有参的构造 都会再定义⼀个⽆参的构造

构造方法不能被 static、final、synchronized、abstract 和 native(类似于 abstract)修饰。构造方法用于初始化一个新对象,所以用 static 修饰没有意义。构造方法不能被子类继承,所以用 final 和 abstract 修饰没有意义。

构造函数的作用是创建一个类的实例。用来创建一个对象,同时可以给属性做初始化。当程序执行到new操作符时, 首先去看new操作符后面的类型,因为知道了类型,才能知道要分配多大的内存空间。分配完内存之后,再调用构造函数,填充对象的各个域,这一步叫做对象的初始化。

JAVA集合

哪些集合类是线程安全的?

Vector、Hashtable、Stack 都是线程安全的,而像 HashMap 则是非线程安全的,不过在 jdk 1.5 之后随着 java.util.concurrent 并发包的出现,它们也有了自己对应的线程安全类,比如 HashMap 对应的线程安全类就是 ConcurrentHashMap。

list :LinkedList,ArrayList 和vector

LinkedList 接口实现类, 链表, 插入删除, 没有同步, 线程不安全
ArrayList 接口实现类, 数组, 随机访问, 没有同步, 线程不安全
Vector 接口实现类 数组, 同步, 线程安全

set:HashSet和TreeSet

HashSet 使用哈希表存储元素,元素可以是null
LinkedHashSet 链表维护元素的插入次序
TreeSet 底层实现为红黑树,元素排好序,元素不可以是null

map:HashMap、TreeMap和HashTable

线程安全

HshaMap线程不安全
TreeMap线程不安全
HashTable线程安全

空值

HashMap一个null key,多个null value
TreeMap不能null key,多个null value
HashTable都不能有null

继承和接口

HashMap继承AbstractMap,实现接口Map
TreeMap继承AbstractMap,实现接口NavigableMap(SortMap的一种)
HashTable继承Dictionary,实现接口Map

顺序

HashMap中key是无序的
TreeMap是有序的
HashTable是无序的

构造函数

HashMap有调优初始容量和负载因子
TreeMap没有
HashTable有

数据结构

HashMap是链表+数组+红黑树
TreeMap是红黑树
HashTable是链表+数组

list、set和map的区别

list:元素按进入先后有序保存,可重复
set:不可重复,并做内部排序
map:代表具有映射关系的集合,其所有的key是一个Set集合,即key无序且不能重复。

Collection:单列集合的根接口

Map:双列集合的根接口,用于存储具有键(key)、值(value)映射关系的元素。

List:元素有序 可重复

● ArrayList:类似一个长度可变的数组 。适合查询,不适合增删
● LinkedList:底层是双向循环链表。适合增删,不适合查询。

Set:元素无序,不可重复

● HashSet:根据对象的哈希值确定元素在集合中的位置
● TreeSet: 以二叉树的方式存储元素,实现了对集合中的元素排序

LinkedHashSet继承于HashSet、又基于 LinkedHashMap 来实现

TreeSet使用二叉树的原理对新 add()的对象按照指定的顺序排序(升序、降序),每增加一个对象都会进行排序,将对象插入的二叉树指定的位置。

HashSet存储元素的顺序并不是按照存入时的顺序(和 List 显然不同) 而是按照哈希值来存的所以取数据也是按照哈希值取得

Set集合的底层是如何保证数据的不重复?

在往Set集合中添加对象的时候,首先会通过该对象的hashCode方法计算该对象的hash值。

如果该对象的hash值不存在,表示集合中不存在新的对象,将元素存入set集合中,如果hash值已经存在,则进一步的比较值是否相等,调用equals方法进行比较,如果值不相等,说明不是同一个对象,会将这个对象添加到已有对象的末尾。

总结:

新插入元素后,计算元素的hash值,如果计算的hash值在hash字典表中不存在,则为新插入的元素,插入 Set集合中,但是如果数据的hash值已经存在,在进行equal进行值的比较,如果值相等,插入失败,否则将对象插入源对象的链表尾部。

ArrayList与LinkedList的区别

● arrayList使用的是数组数据结构,可以以O(1)时间复杂度对元素进行随机访问;LinkedList使用的是(双)链表结构查找某个元素的时间复杂度是O(n)
● arrayList更适合于随机查找操作,linkedList更适合增删改查,时间复杂度根据数据浮动
● 两者都实现了List接口,但LinkedList额外实现了Deque接口,因此Linked还可以当作队列使用
● LinkedList比ArrayList更占内存,因为LinkedList为每一个节点存储了两个引用,一个指向前一个元素,一个指向下一个元素

 //遍历ArrayList
    public static void main(String args[]){
        List<String> list = new ArrayList<String>();
        list.add("luojiahui");
        list.add("luojiafeng");

        //方法1
        Iterator it1 = list.iterator();
        while(it1.hasNext()){
            System.out.println(it1.next());
        }

        //方法2
        for(Iterator it2 = list.iterator();it2.hasNext();){
             System.out.println(it2.next());
        }

        //方法3
        for(String tmp:list){
            System.out.println(tmp);
        }

        //方法4
        for(int i = 0;i < list.size(); i ++){
            System.out.println(list.get(i));
        }

    }

java面向对象

即封装继承多态
概论:面向对象是把一组数据结构和处理他们的方法组成对象;
把具有相同行为的对象归纳成类;通过封装隐藏类的内部细节;通过继承使类得到泛化;
通过多态实现基于对象类型的动态分派

吃饭:
面向过程:买菜,洗菜,做饭,洗手吃饭,洗碗
面向对象:点外卖,吃饭,扔外卖盒

封装:内部细节对外部调用透明
体现:1.JavaBean的属性私有,提供get set 对外访问,因为属性的赋值或者获取

Java的基础数据类型有哪些?(5分)

有布尔型 true false ,整数型long int byte short,字符类型char,浮点型float double

int 和 Integer 有什么区别?(5分)

Int 是基本数据类型,Integer是引用数据类型,Interger更多用于面向对象过程,即更多用于创建类过程中。

ArrayList和LinkedList有什么区别

ArrayList和LinkedList都实现了List接口,他们有以下的不通电
ArrayList是基于索引的数据接口,底层是数组,它可以以O(1)时间复杂度对元素进行随机访问。
对应的是LinkedList是以元素列表的形式存储它的数据,每一个元素都和它的前一个和后一个元素链接在一起,在这种情况下,

总结:
ArrayList基于数组,LinkedList基于链表

JAVA中的集合类

第一代线程安全集合类

Vector,Hashtable
是怎么保证线程安排的;使用synchronized修饰方法
缺点:效率低下

第二代线程非安全集合类

ArrayList ,HashMap
线程不安全,但是性能好,用来代替vector和Hashtable
使用ArrayList ,HashMap,需要线程安全怎么办呢?
使用Collections.synchronizedList(list);Collections.synchronizedMap(m);
底层使用synchronized代码块锁,虽然也是锁住了所有代码,

第三代线程安全集合类

在大量并发情况下如何提高集合的效率和安全呢

jdk1.8的新特性有哪些

接口的默认方法

java8允许我们给接口添加一个非抽象的方法实现,只需要使用default关键字即可,这个特征又叫做扩展方法,
示例如下:
代码如下:
interface Formula{
double calculate(int a)
default double sqrt(int a) {
return Math.sqrt(a);
}
}

JAVA中抽象类和接口有什么区别

不同
抽象类:
1.抽象类中可以定义构造器
2.可以有抽象方法和具体方法
3.接口中的成员全都是public的
4.抽象类中可以定义成员变量
5.有抽象方法的类必须声明为抽象类,而抽象类未必要有抽象方法
6.抽象类中可以包含静态方法
7.一个类只能继承一个抽象类

接口:
1.接口中不能定义构造器
2.方法全都是抽象方法
3.抽象类中的成员可以是private,默认,protected,public
4.接口中定义的成员变量实际上是常量
5.接口中不能有静态方法
6.一个类可以实现多个接口

Java中==和equals有哪些区别

equals和==最大的区别是一个是方法一个是运算符

==:如果比较的对象时基本数据类型,则比较的是数值是否相等;如果比较的是引用数据类型,则比较的是对象的地址值是否相等
equals(): 用来比较两个方法的内容是否相等。
注意:equals方法不能用于基本数据类型的变量,如果没有对equals方法进行重写,则比较的是引用类型的变量所指向二代对象的地址。

● == : 对比的是栈中的值,基本数据类型是变量值,引用数据类型是堆中内存对象的地址;
直接比较的两个对象的堆内存地址,如果相等,则说明这两个引用实际是指向同一个对象地址的
● equals:object中默认也是采用==比较,通常会重写

== 是java提供的等于比较运算符,用来比较两个变量指向的内存地址是否相同.而equals()是Object提供的一个方法.Object中equals()方法的默认实现就是返回两个对象==的比较结果.但是equals()可以被重写,所以我们在具体使用的时候需要关注equals()方法有没有被重写.

Java中重写和重载有哪些区别

方法的重载和重写都是实现多态的方式,区别在于前者实现的是编译时的多态性,而后者实现的是运行时的多态性,重载发生在一个类中,同名的方法如果有不同的参数列表(参数类型不同,参数个数不同或者二者都不同)则视为重载;重写发生在子类与父类之间,重写要求子类被重写方法与父类被重写方法有相同的返回类型,比父类被重写方法更好访问,不能比父类被重写方法生命更多的

String ,StringBuffer,StringBuilder区别及使用场景

Java平台提供了两种类型的字符串:String和StringBuilder,它们都可以存储和操作字符串,区别如下。

1.String是只读字符串,也就意味着String引用的字符串内容是不能被改变的,初学者可能会有这样的误解:

String str ="abc";
str = "bad"

如上,字符串str命名是可以改变的吖!其实不然,str仅仅是一个是一个引用对象,它指向一个字符串对象“abc”,
第二行代码的含义是让str重新指向一个新的字符串"bcd"对象,而“abc”对象并没有任何改变,只不过该对象已经成为了一个不可及对象罢了

2.StringBuffer/StringBuffer表示的字符串对象可以直接修改
3.StringBuffer是java5中引入的,它和StringBuffer的方法完全相同,区别在于它是在单线程环境下使用的,因为它的所有方法都没有被synchronized修饰,因此它的效率理论上也比StringBuffer要高。

Redis

Redis线程模型有哪些,单线程为什么快

JAVA中的数据结构

Java哈希表

hashCode() 和equals()方法的重要性体现在上面地方?

Java中的HashMap使用hashCode()和equals()方法来确定键值对的索引,当根据键获取值的时候也会用到这两个方法。如果没有正确的实现这两个方法,两个不同的键可能会有相同的hash值,因此,可能会被集合认为是相等的。而且,这两个方法也用来发现重复元素。所以这两个方法的实现对HashMap的精确性和正确性是至关重要的。

请说一说,Java中的HashMap的工作原理是什么?

HashMap是以键值对的形式存储元素的,需要一个哈希函数,使用hashcode和eaquels方法,从集合中添加和检索元素,调用put方法时,会计算key 的哈希值,然后把键值对存在集合中合适的索引上,如果键key已经存在,value会被更新为新值。另外HashMap的初始容量是16,在jdk1.7的时候底层是基于数组和链表实现的,插入方式是头插法。jdk1.8的时候底层是基于数组和链表/红黑树实现的,插入方式为尾插法。

介绍一下,什么是hashmap?

HashMap 是一个散列表,它存储的内容是键值对(key-value)。
HashMap 继承于AbstractMap,实现了Map、Cloneable、java.io.Serializable接口。
HashMap 的实现不是同步的,所以它不是线程安全的。它的key、value都可以为null。此外,HashMap中的映射不是有序的。
HashMap 的实例有两个参数影响其性能:“初始容量” 和 “负载因子”。容量 是哈希表中桶的数量,初始容量 只是哈希表在创建时的容量。负载因子 是哈希表在其容量自动增加之前可以达到多满的一种尺度。当哈希表中的条目数超出了加载因子与当前容量的乘积时,则要对该哈希表进行 rehash 操作(即重建内部数据结构),从而哈希表将具有大约两倍的桶数。
通常,默认加载因子是 0.75, 这是在时间和空间成本上寻求一种折衷。加载因子过高虽然减少了空间开销,但同时也增加了查询成本(在大多数 HashMap 类的操作中,包括 get 和 put 操作,都反映了这一点)。在设置初始容量时应该考虑到映射中所需的条目数及其加载因子,以便最大限度地减少 rehash 操作次数。如果初始容量大于最大条目数除以加载因子,则不会发生 rehash 操作。
HashMap在扩容的时候是2的n次幂。

HashMap和LinkedHashMap的区别?

HashMap,LinkedHashMap,TreeMap都属于Map

Map 主要用于存储键(key)值(value)对,根据键得到值,因此键不允许键重复,但允许值重复。

HashMap
是一个最常用的Map,它根据键的HashCode 值存储数据,根据键可以直接获取它的值,具有很快的访问速度。HashMap最多只允许一条记录的键为Null;允许多条记录的值为 Null;HashMap不支持线程的同步,即任一时刻可以有多个线程同时写HashMap;可能会导致数据的不一致。如果需要同步,可以用 Collections的synchronizedMap方法使HashMap具有同步的能力。

LinkedHashMap
LinkedHashMap也是一个HashMap,但是内部维持了一个双向链表,可以保持顺序

TreeMap 可以用于排序

HashMap的例子

public static void main(String[] args) {

  Map<String, String> map = new HashMap<String, String>(); 
 
  map.put("a3", "aa");
 
  map.put("a2", "bb"); 
 
  map.put("b1", "cc");
 
  for (Iterator iterator = map.values().iterator(); iterator.hasNext();)     {
 
        String name = (String) iterator.next(); 
 
        System.out.println(name);   
 
 }  
}

输出:bbccaa

LinkedHashMap例子:

public static void main(String[] args) {

 Map<String, String> map = new LinkedHashMap<String, String>();
 
 map.put("a3", "aa");       
 
 map.put("a2", "bb"); 
 
 map.put("b1", "cc"); 
 
 for (Iterator iterator = map.values().iterator(); iterator.hasNext();) {           
 
         String name = (String) iterator.next(); 
 
         System.out.println(name);     
 
 }
}

输出:
aa
bb
cc

总结归纳为:linkedMap在于存储数据你想保持进入的顺序与被取出的顺序一致的话,优先考虑LinkedMap,hashMap键只能允许为一条为空,value可以允许为多条为空,键唯一,但值可以多个。

经本人测试linkedMap键和值都不可以为空

HashMap和HashTable有什么区别?

1、HashMap是非线程安全的,HashTable是线程安全的。
2、HashMap的键和值都允许有null值存在,而HashTable则不行。
3、因为线程安全的问题,HashMap效率比HashTable的要高。
4、Hashtable是同步的,而HashMap不是。因此,HashMap更适合于单线程环境,而Hashtable适合于多线程环境。
一般现在不建议用HashTable:
①是HashTable是遗留类,内部实现很多没优化和冗余。
②即使在多线程环境下,现在也有同步的ConcurrentHashMap替代,没有必要因为是多线程而用HashTable

final关键字在Java中有什么作用?

可用来修饰类,方法,变量

1.final修饰变量,则等同于常量 2.final修饰方法中的参数,称为最终参数。

3.final修饰类,则类不能被继承 (太监类)

4.final修饰方法,则方法不能被重写。5.final 不能修饰抽象类

6.final修饰的方法可以被重载 但不能被重写 (不能继承)

  1. 当用final修饰一个类时,表明这个类不能被继承。也就是说,如果一个类你永远不会让他被继承,就可以用final进行修饰。final类中的成员变量可以根据需要设为final,但是要注意final类中的所有成员方法都会被隐式地指定为final方法。在使用final修饰类的时候,要注意谨慎选择,除非这个类真的在以后不会用来继承或者出于安全的考虑,尽量不要将类设计为final类
  2. 使用final方法的原因有两个:第一个原因是把方法锁定,以防任何继承类修改它的含义;第二个原因是效率。在早期的Java实现版本中,会将final方法转为内嵌调用。但是如果方法过于庞大,可能看不到内嵌调用带来的任何性能提升。在最近的Java版本中,不需要使用final方法进行这些优化了。
    只有在想明确禁止 该方法在子类中被覆盖的情况下才将方法设置为final的。 注:类的private方法会隐式地被指定为final方法
  3. 对于一个final变量,如果是基本数据类型的变量,则其数值一旦在初始化之后便不能更改;如果是引用类型的变量,则在对其初始化之后便不能再让其指向另一个对象。

static作用于成员变量用来表示只保存一份副本,而final的作用是用来保证变量不可变

即使没有final修饰的情况下,在方法内部改变了变量i的值也不会影响方法外的i;java采用的是值传递,对于引用变量,传递的是引用的值,也就是说让实参和形参同时指向了同一个对象,因此让形参重新指向另一个对象对实参并没有任何影响

static的作用

static修饰的变量没有发生变化是因为static作用于成员变量只是用来表示保存一份副本,其不会发生变化。怎么理解这个副本呢?其实static修饰的在类加载的时候就加载完成了(初始化),而且只会加载一次也就是说初始化一次,所以不会发生变化!

static是不允许用来修饰局部变量 静态变量只能在类主体中定义,不能在方法中定义

● 静态变量:
静态变量由于不属于任何实例对象,属于类的,所以在内存中只会有一份,在类的加载过程中JVM只为静态变量分配一次内存空间。
● 实例变量:
每次创建对象都会为每个对象分配成员变量内存空间,实例变量是属于实例对象的,在内存中,创建几次对象,就有几份成员变量。

super的作用

● 调用父类被子类重写的方法;
● 调用父类被子类重定义的字段(被隐藏的成员变量);
● 调用父类的构造方法;

如果子类没有重写父类的方法,调用父类的方法用不用super关键字结果都一样。 如果子类重写父类的方法,调用父类的方法必须用super关键字

String的底层存储是什么?

在JDK9之前,String的底层存储结构是char[],一个char需要占用两个字节的存储单位

sleep()与wait()区别?

区别1:使用限制
使用 sleep 方法可以让让当前线程休眠,时间一到当前线程继续往下执行,在任何地方都能使用,但需要捕获 InterruptedException 异常。

try {
Thread.sleep(3000L);
} catch (InterruptedException e) {
e.printStackTrace();
}
而使用 wait 方法则必须放在 synchronized 块里面,同样需要捕获 InterruptedException 异常,并且需要获取对象的锁。

synchronized (lock){
try {
lock.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
而且 wait 还需要额外的方法 notify/ notifyAll 进行唤醒,它们同样需要放在 synchronized 块里面,且获取对象的锁。。

synchronized (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.Object#wait
public final 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 后可能还是有机会重新竞争到锁继续执行的。

MySQL索引有哪几种类型?

平时讲的索引类型一般是指在应用层次的划分。
(1)普通索引:即一个索引只包含单个列,一个表可以有多个单列索引
(2)复合索引:多列值组成一个索引,专门用于组合搜索,其效率大于索引合并
(3)唯一索引:索引列的值必须唯一,但允许有空值

MySQL之事务的四大特性?

原子性,一致性,隔离性,持久性

怎么避免并发?

MySQL中count(id)、count(1)、count(字段)的区别?

你知道哪些数据库优化事项?

1.在表中建立索引
2.尽量避免用select *,返回无用的字段会降低查询的效率
3.尽量避免使用 in和notin,会导致数据库放弃索引进行全表扫描
4.尽量避免使用or,会导致数据库放弃索引进行全表扫描
5.尽量避免进行null值的判断,会导致数据库放弃索引进行全表扫描
6.使用explain查看sql的执行情况,看看那些表查询时间比较长
7.在java中,减少与数据库的交互次数,比如可以使用在存储过程中。

抽象类和普通类有哪些区别?(5分)

抽象类只能用public 和protected来修饰,
抽象类不可以用来创建对象

java反射获取类的三种方式

1.调用对象的.getClass()方法
2.调用某个类的.class属性
3.使用Class.forName+类的全路径

HashSet 判断两个元素相等性的方法

HashSet首先判断两个元素的哈希值,如果哈希值一样,接着会比较equals方法,如果equls结果为true,HashSet就是为一个元素,如果equals为false,就不是一个元素

Java多线程

为什么要使用java线程池

1.降低资源消耗。通过重复利用已创建的线程降低线程创建,销毁线程造成的消耗。
2.提高响应速度。当任务到达时,任务可以不需要等到线程创建就立即执行。
3.提高线程的可管理性。线程是稀缺资源,如果无限制的创建,不仅会消耗系统资源,还会降低系统的稳定性,使用线程池可以进行统一的分配,调优和监控
4.提供更多更强大的功能:线程池具备可拓展性,允许开发人员向其中增加更多的功能。比如延时定时线程池ScheduledThreadPoolExecutor,就允许任务延期执行或定期执行。

Redis

Redis可以用来做什么

1.Redis最长用来做缓存,是实现分布式缓存的首先中间件
2.Redis可以作为数据库,实现诸如点赞,关注,排行等对性能要求极高的互联网需求
3.Redis可以作为计算工具,能用很小的代价,统计诸如PV/UV,用户在线天数等数据
4.Redis还有很多其他的使用场景,例如:可以实现分布式锁,可以作为消息队列使用。

Redis和传统的关系型数据库有什么不同?

Redis是一种基于键值对的NoSQL数据库,而键值对的值是由多种数据结构和算法组成的。Redis的数据都存储与内存中,因此它的速度惊人,读写性能可达10万/秒,远超关系型数据库

关系型数据库是基于二维数据表来存储数据的,它的数据格式更为严谨,并支持关系查询。关系型数据库的数据存储于磁盘上,可以存放海量的数据,但性能远不如Redis

区别:
NoSQL存放在内存中,关系型数据库存放在磁盘中

Redis有哪些数据类型

1.Redis支持5中核心的数据类型,分别是字符串,哈希,列表,集合,有序集合
2.Redis还提供了Bitmap,HyperLogLog,Geo类型,但是这些都是基于上述的核心数据类型实现的
3.Redis在5.0新增加了Streams数据类型,它是一个功能强大的、支持多播的、可持久化的消息队列。

Redis为什么是单线程的却这么快

1.单线程避免了线程切换和竞争所产生的消耗
2.Redis大部分操作在内存上完成
3.Redis采用了IO多路复用机制,使其在网络IO操作中能并发处理大量的客户端请求,实现高吞吐率。

set和zset有什么区别呢

set:即集合

集合中的元素是无序、不可重复的,一个集合最多能存储232-1个元素;

集合除了支持对元素的增删改查之外,还支持对多个集合取交集、并集、差集。

zset:即有序集合

有序集合保留了集合元素不能重复的特点;

有序集合会给每个元素设置一个分数,并以此作为排序的依据;

有序集合不能包含相同的元素,但是不同元素的分数可以相同。

1.7 说一下Redis中的watch命令

参考答案

很多时候,要确保事务中的数据没有被其他客户端修改才执行该事务。Redis提供了watch命令来解决这类问题,这是一种乐观锁的机制。客户端通过watch命令,要求服务器对一个或多个key进行监视,如果在客户端执行事务之前,这些key发生了变化,则服务器将拒绝执行客户端提交的事务,并向它返回一个空值。

1.8 说说Redis中List结构的相关操作

参考答案

列表是线性有序的数据结构,它内部的元素是可以重复的,并且一个列表最多能存储2^32-1个元素。列表包含如下的常用命令:

lpush/rpush:从列表的左侧/右侧添加数据;

lrange:指定索引范围,并返回这个范围内的数据;

lindex:返回指定索引处的数据;

lpop/rpop:从列表的左侧/右侧弹出一个数据;

blpop/brpop:从列表的左侧/右侧弹出一个数据,若列表为空则进入阻塞状态。

1.9 你要如何设计Redis的过期时间?

参考答案

热点数据不设置过期时间,使其达到“物理”上的永不过期,可以避免缓存击穿问题;

在设置过期时间时,可以附加一个随机数,避免大量的key同时过期,导致缓存雪崩。

MySQL部分

存储引擎的区别

InnoDB拥有事务和外键,而MyISAM则没有,InnoDB使用聚集索引,MyISAM是非聚集索引标,InnoDB所用的是行锁 表锁,MyISAM用的是表锁,InnoDB操作中Insert与Update效率更高,MyISAMSelect效率更高
DELETE FROM table时,InnoDB不会重新建立表,而是一行一行的删除,在innodb上如果要清空保存有大量数据的表,最好使用truncate table这个命令 参考链接

1.InnoDB 支持事务,MyISAM 不支持事务。这是 MySQL 将默认存储引擎从 MyISAM 变成 InnoDB 的重要原因之一;

\2. InnoDB 支持事务,而 MyISAM 不支持。对一个包含外键的 InnoDB 表转为 MYISAM 会失败;

\3. InnoDB 是聚集索引,MyISAM 是非聚集索引。聚簇索引的文件存放在主键索引的叶子节点上,因此 InnoDB 必须要有主键,通过主键索引效率很高。但是辅助索引需要两次查询,先查询到主键,然后再通过主键查询到数据。因此,主键不应该过大,因为主键太大,其他索引也都会很大。而 MyISAM 是非聚集索引,数据文件是分离的,索引保存的是数据文件的指针。主键索引和辅助索引是独立的。

\4. InnoDB 不保存表的具体行数,执行 select count(*) from table 时需要全表扫描。而MyISAM 用一个变量保存了整个表的行数,执行上述语句时只需要读出该变量即可,速度很快;

\5. InnoDB 最小的锁粒度是行锁,MyISAM 最小的锁粒度是表锁。一个更新语句会锁住整张表,导致其他查询和更新都会被阻塞,因此并发访问受限。这也是 MySQL 将默认存储引擎从 MyISAM 变成 InnoDB 的重要原因之一;

关系数据模型的三个组成部分 完整性规则、 数据结构、 数据操作。

● 数据结构:描述数据库的组成对象以及对象之间的联系。
● 数据操作:指对数据库中各种对象(型)的实例(值)允许执行的操作的集合,包括操作及有关的操作规则。
● 数据的完整性约束规则:一组完整性规则。

说一说你对数据库优化的理解

数据库的优化是多方面的,原则就是减少系统的瓶颈,减少资源的占用,增加系统的反应速度。
例如,通过优化文件系统,提高磁盘I\O的读写速度;通过优化操作系统调度策略,提高MySQL在高负荷情况下的负载能力;优化表结构、索引、查询语句等使查询响应更快。

针对查询,我们可以通过使用索引、使用连接代替子查询的方式来提高查询速度。

针对慢查询,我们可以通过分析慢查询日志,来发现引起慢查询的原因,从而有针对性的进行优化。

针对插入,我们可以通过禁用索引、禁用检查等方式来提高插入速度,在插入之后再启用索引和检查。

针对数据库结构,我们可以通过将字段很多的表拆分成多张表、增加中间表、增加冗余字段等方式进行优化。

该如何优化Mysql的查询

使用索引:

如果查询时没有使用索引,查询语句将扫描表中的所有记录。在数据量大的情况下,这样查询的速度会很慢。如果使用索引进行查询,查询语句可以根据索引快速定位到待查询记录,从而减少查询的记录数,达到提高查询速度的目的。

索引可以提高查询的速度,但并不是使用带有索引的字段查询时索引都会起作用。有几种特殊情况,在这些情况下有可能使用带有索引的字段查询时索引并没有起作用。

使用LIKE关键字的查询语句

在使用LIKE关键字进行查询的查询语句中,如果匹配字符串的第一个字符为“%”,索引不会起作用。只有“%”不在第一个位置,索引才会起作用。

使用多列索引的查询语句

MySQL可以为多个字段创建索引。一个索引可以包括16个字段。对于多列索引,只有查询条件中使用了这些字段中的第1个字段时索引才会被使用。

使用OR关键字的查询语句

查询语句的查询条件中只有OR关键字,且OR前后的两个条件中的列都是索引时,查询中才使用索引。否则,查询将不使用索引。

优化子查询:

使用子查询可以进行SELECT语句的嵌套查询,即一个SELECT查询的结果作为另一个SELECT语句的条件。子查询可以一次性完成很多逻辑上需要多个步骤才能完成的SQL操作。

子查询虽然可以使查询语句很灵活,但执行效率不高。执行子查询时,MySQL需要为内层查询语句的查询结果建立一个临时表。然后外层查询语句从临时表中查询记录。查询完毕后,再撤销这些临时表。因此,子查询的速度会受到一定的影响。如果查询的数据量比较大,这种影响就会随之增大。

在MySQL中,可以使用连接(JOIN)查询来替代子查询。连接查询不需要建立临时表,其速度比子查询要快,如果查询中使用索引,性能会更好。

了解数据库的锁吗?

锁是数据库系统区别于文件系统的一个关键特性,锁机制用于管理对共享资源的并发访问。
下面以Mysql数据库的InnoDB引擎为例:来说明锁的一些特点。

锁:
InnoDB存储引擎实现了如下两种标准的行级锁:
共享锁:允许事务读取一行数据
排他锁:允许事务删除或更新一行数据
如果一个事务T1已经获得了行r的共享锁,那么另外的事务T2可以立即获得行r的共享锁,因为读取并没有改变行r的数据,称这种情况为锁兼容。但若有其他的事务T3想获得行r的排他锁,则其必须等待事务T1、T2释放行r上的共享锁,这种情况称为锁不兼容。下图显示了共享锁和排他锁的兼容性,可以发现X锁与任何的锁都不兼容,而S锁仅和S锁兼容。需要特别注意的是,S和X锁都是行锁,兼容是指对同一记录(row)锁的兼容性情况。
锁的粒度:

InnoDB存储引擎支持多粒度锁定,这种锁定允许事务在行级上的锁和表级上的锁同时存在。为了支持在不同粒度上进行加锁操作,InnoDB存储引擎支持一种额外的锁方式,称之为意向锁。意向锁是将锁定的对象分为多个层次,意向锁意味着事务希望在更细粒度上进行加锁。

InnoDB存储引擎支持意向锁设计比较简练,其意向锁即为表级别的锁。设计目的主要是为了在一个事务中揭示下一行将被请求的锁类型。其支持两种意向锁:

意向共享锁(IS Lock),事务想要获得一张表中某几行的共享锁。

意向排他锁(IX Lock),事务想要获得一张表中某几行的排他锁。

由于InnoDB存储引擎支持的是行级别的锁,因此意向锁其实不会阻塞除全表扫以外的任何请求。故表级意向锁与行级锁的兼容性如下图所示。

锁的算法:

InnoDB存储引擎有3种行锁的算法,其分别是:

Record Lock:单个行记录上的锁。

Gap Lock:间隙锁,锁定一个范围,但不包含记录本身。

Next-Key Lock∶Gap Lock+Record Lock,锁定一个范围,并且锁定记录本身。

Record Lock总是会去锁住索引记录,如果InnoDB存储引擎表在建立的时候没有设置任何一个索引,那么这时InnoDB存储引擎会使用隐式的主键来进行锁定。Next-Key Lock是结合了Gap Lock和Record Lock的一种锁定算法,在Next-Key Lock算法下,InnoDB对于行的查询都是采用这种锁定算法。采用Next-Key Lock的锁定技术称为Next-Key Locking,其设计的目的是为了解决Phantom Problem(幻读)。而利用这种锁定技术,锁定的不是单个值,而是一个范围,是谓词锁(predict lock)的一种改进。

关于死锁:

死锁是指两个或两个以上的事务在执行过程中,因争夺锁资源而造成的一种互相等待的现象。若无外力作用,事务都将无法推进下去。

解决死锁问题最简单的一种方法是超时,即当两个事务互相等待时,当一个等待时间超过设置的某一阈值时,其中一个事务进行回滚,另一个等待的事务就能继续进行。

除了超时机制,当前数据库还都普遍采用wait-for graph(等待图)的方式来进行死锁检测。较之超时的解决方案,这是一种更为主动的死锁检测方式。InnoDB存储引擎也采用的这种方式。wait-for graph要求数据库保存以下两种信息:

锁的信息链表;

事务等待链表;

通过上述链表可以构造出一张图,而在这个图中若存在回路,就代表存在死锁,因此资源间相互发生等待。这是一种较为主动的死锁检测机制,在每个事务请求锁并发生等待时都会判断是否存在回路,若存在则有死锁,通常来说InnoDB存储引擎选择回滚undo量最小的事务。

锁的升级:

锁升级(Lock Escalation)是指将当前锁的粒度降低。举例来说,数据库可以把一个表的1000个行锁升级为一个页锁,或者将页锁升级为表锁。

InnoDB存储引擎不存在锁升级的问题。因为其不是根据每个记录来产生行锁的,相反,其根据每个事务访问的每个页对锁进行管理的,采用的是位图的方式。因此不管一个事务锁住页中一个记录还是多个记录,其开销通常都是一致的。

怎么样插入数据才能高效?

影响插入速度的主要是索引、唯一性校验、一次插入记录条数等。针对这些情况,可以分别进行优化。

对于MyISAM引擎的表,常见的优化方法如下:

禁用索引

对于非空表,插入记录时,MySQL会根据表的索引对插入的记录建立索引。如果插入大量数据,建立索引会降低插入记录的速度。为了解决这种情况,可以在插入记录之前禁用索引,数据插入完毕后再开启索引。对于空表批量导入数据,则不需要进行此操作,因为MyISAM引擎的表是在导入数据之后才建立索引的。

禁用唯一性检查

插入数据时,MySQL会对插入的记录进行唯一性校验。这种唯一性校验也会降低插入记录的速度。为了降低这种情况对查询速度的影响,可以在插入记录之前禁用唯一性检查,等到记录插入完毕后再开启。

使用批量插入

插入多条记录时,可以使用一条INSERT语句插入一条记录,也可以使用一条INSERT语句插入多条记录。使用一条INSERT语句插入多条记录的情形如下,而这种方式的插入速度更快。

INSERT INTO fruits VALUES (‘x1’, ‘101’, ‘mongo2’, ‘5.7’), (‘x2’, ‘101’, ‘mongo3’, ‘5.7’), (‘x3’, ‘101’, ‘mongo4’, ‘5.7’);
使用LOAD DATA INFILE批量导入

当需要批量导入数据时,如果能用LOAD DATA INFILE语句,就尽量使用。因为LOAD DATA INFILE语句导入数据的速度比INSERT语句快。

对于InnoDB引擎的表,常见的优化方法如下:

禁用唯一性检查

插入数据之前执行set unique_checks=0来禁止对唯一索引的检查,数据导入完成之后再运行set unique_checks=1。这个和MyISAM引擎的使用方法一样。

禁用外键检查

插入数据之前执行禁止对外键的检查,数据插入完成之后再恢复对外键的检查。

禁用自动提交

插入数据之前禁止事务的自动提交,数据导入完成之后,执行恢复自动提交操作。

数据库在什么情况下会发生死锁?

死锁是指两个或两个以上的事务在执行过程中,因争夺锁资源而造成的一种互相等待的现象。若无外力作用,事务都将无法推进下去。下图演示了死锁的一种经典的情况,即A等待B、B等待A,这种死锁问题被称为AB-BA死锁。

说说数据库死锁的解决办法

解决死锁问题最简单的一种方法是超时,即当两个事务互相等待时,当一个等待时间超过设置的某一阈值时,其中一个事务进行回滚,另一个等待的事务就能继续进行。

除了超时机制,当前数据库还都普遍采用wait-for graph(等待图)的方式来进行死锁检测。较之超时的解决方案,这是一种更为主动的死锁检测方式。InnoDB存储引擎也采用的这种方式。wait-for graph要求数据库保存以下两种信息:

锁的信息链表;

事务等待链表;

通过上述链表可以构造出一张图,而在这个图中若存在回路,就代表存在死锁,因此资源间相互发生等待。这是一种较为主动的死锁检测机制,在每个事务请求锁并发生等待时都会判断是否存在回路,若存在则有死锁,通常来说InnoDB存储引擎选择回滚undo量最小的事务。

介绍一下数据库分页

mysql的分页语法:
在mysql中,select语句默认返回所有匹配的行,它们可能是指定表中的每个行,为了返回第一行或者前几行,可使用LIMIT子句,以实现分页查询.LIMIT子句的语法如下:、

-- 在所有的查询结果中,返回前5行记录。 
SELECT prod_name FROM products LIMIT 5; 
-- 在所有的查询结果中,从第5行开始,返回5行记录。 
SELECT prod_name FROM products LIMIT 5,5;

介绍一下SQL中的聚合函数

常用的聚合函数有COUNT()、AVG()、SUM()、MAX()、MIN(),下面以MySQL为例,说明这些函数的作用。
COUNT()函数:

COUNT()函数统计数据表中包含的记录行的总数,或者根据查询结果返回列中包含的数据行数,它有两种用法:

COUNT(*)计算表中总的行数,不管某列是否有数值或者为空值。

COUNT(字段名)计算指定列下总的行数,计算时将忽略空值的行。

COUNT()函数可以与GROUP BY一起使用来计算每个分组的总和。

AVG()函数():

AVG()函数通过计算返回的行数和每一行数据的和,求得指定列数据的平均值。

AVG()函数可以与GROUP BY一起使用,来计算每个分组的平均值。

SUM()函数:

SUM()是一个求总和的函数,返回指定列值的总和。

SUM()可以与GROUP BY一起使用,来计算每个分组的总和。

MAX()函数:

MAX()返回指定列中的最大值。

MAX()也可以和GROUP BY关键字一起使用,求每个分组中的最大值。

MAX()函数不仅适用于查找数值类型,也可应用于字符类型。

MIN()函数:

MIN()返回查询列中的最小值。

MIN()也可以和GROUP BY关键字一起使用,求出每个分组中的最小值。

MIN()函数与MAX()函数类似,不仅适用于查找数值类型,也可应用于字符类型。

表跟表之间是怎么关联的?

表与表之间常用的关联方式有两种:内连接、外连接,下面以MySQL为例来说明这两种连接方式。

内连接:
内连接通过INNER JOIN来实现,它将返回两张表中满足连接条件的数据,不满足条件的数据不会查询出来。

外连接:
外连接通过OUTER JOIN来实现,它会返回两张表中满足连接条件的数据,同时返回不满足连接条件的数据。外连接有两种形式:左外连接(LEFT OUTER JOIN)、右外连接(RIGHT OUTER JOIN)。

左外连接:可以简称为左连接(LEFT JOIN),它会返回左表中的所有记录和右表中满足连接条件的记录。

右外连接:可以简称为右连接(RIGHT JOIN),它会返回右表中的所有记录和左表中满足连接条件的记录。

除此之外,还有一种常见的连接方式:等值连接。这种连接是通过WHERE子句中的条件,将两张表连接在一起,它的实际效果等同于内连接。出于语义清晰的考虑,一般更建议使用内连接,而不是等值连接。

以上是从语法上来说明表与表之间关联的实现方式,而从表的关系上来说,比较常见的关联关系有:一对多关联、多对多关联、自关联。

一对多关联:这种关联形式最为常见,一般是两张表具有主从关系,并且以主表的主键关联从表的外键来实现这种关联关系。另外,以从表的角度来看,它们是具有多对一关系的,所以不再赘述多对一关联了。

多对多关联:这种关联关系比较复杂,如果两张表具有多对多的关系,那么它们之间需要有一张中间表来作为衔接,以实现这种关联关系。这个中间表要设计两列,分别存储那两张表的主键。因此,这两张表中的任何一方,都与中间表形成了一对多关系,从而在这个中间表上建立起了多对多关系。

自关联:自关联就是一张表自己与自己相关联,为了避免表名的冲突,需要在关联时通过别名将它们当做两张表来看待。一般在表中数据具有层级(树状)时,可以采用自关联一次性查询出多层级的数据。

说一说你对外连接的了解

外连接通过OUTER JOIN来实现,它会返回两张表中满足连接条件的数据,同时返回不满足连接条件的数据。常见的外连接有两种形式:左外连接(LEFT OUTER JOIN)、右外连接(RIGHT OUTER JOIN)。

左外连接:可以简称为左连接(LEFT JOIN),它会返回左表中的所有记录和右表中满足连接条件的记录。

右外连接:可以简称为右连接(RIGHT JOIN),它会返回右表中的所有记录和左表中满足连接条件的记录。

实际上,外连接还有一种形式:完全外连接(FULL OUTER JOIN),但MySQL不支持这种形式。

说一说数据库的左连接和右连接

外连接通过OUTER JOIN来实现,它会返回两张表中满足连接条件的数据,同时返回不满足连接条件的数据。常见的外连接有两种形式:左外连接(LEFT OUTER JOIN)、右外连接(RIGHT OUTER JOIN)。

左外连接:可以简称为左连接(LEFT JOIN),它会返回左表中的所有记录和右表中满足连接条件的记录。

右外连接:可以简称为右连接(RIGHT JOIN),它会返回右表中的所有记录和左表中满足连接条件的记录。

Where和HAVING有什么区别

WHERE是一个约束声明,使用WHERE约束来自数据库的数据,WHERE是在结果返回之前起作用的,WHERE中不能使用聚合函数。

HAVING是一个过滤声明,是在查询返回结果集以后对查询结果进行的过滤操作,在HAVING中可以使用聚合函数。另一方面,HAVING子句中不能使用除了分组字段和聚合函数之外的其他字段。

从性能的角度来说,HAVING子句中如果使用了分组字段作为过滤条件,应该替换成WHERE子句。因为WHERE可以在执行分组操作和计算聚合函数之前过滤掉不需要的数据,性能会更好。

索引

说一说你对Mysql索引的理解

索引是一个单
独的,存储在磁盘上的数据库结构,包含着对数据库表里所有记录的引用指针。使用索引可以加快找出在某个或多个列中有一特定值的行,所有MySQL列类型都可以被索引,对相关列使用索引是提高查询和操作速度的最佳途径

索引是在存储引擎中实现的,因此,每种存储引擎的索引都不一定完全相同,并且每种存储引擎也不一定支持所有的索引类型。MySQL中索引的存储类型有两种,即Btree和HASH,具体和表的存储引擎相关

索引的有点主要有以下几条:
1.通过创建唯一索引,可以保证数据库表中每一行数据的唯一性
2.可以大大加快数据的查询速度。
3.有助于实现数据的参考完整性,可以加速表和表之间的链接
4.使用分组和排序子句进行数据查询时,可以显著减少查询中分组和排序的时间

增加索引也有许多不利的方面,主要表现在如下几个方面:
1.创建索引和维护索引要耗费时间,并且随着数据量的增加所耗费的时间也会增加
2.索引需要占据磁盘空间,除了数据表占数据空间之外,每一个索引还需要占一定的物理空间,如果有大量的索引,索引文件可能比数据文件更大
3.当对表中的数据进行增删改的时候,索引也要动态地维护,这样降低了数据的维护速度

mysql有哪些索引

可以分为以下几类:
1.普通索引和唯一索引
普通索引是MySQL中的基本索引类型,允许在定义索引的列中插入重复值和空值。
唯一索引要求索引列的值必须唯一,但允许有空值,如果是组合索引,则组合的值必须唯一。
主键索引是一种特殊的唯一索引,不允许有空值。

2.单列索引和组合索引
单列索引即一个索引只包含单个列,一个表可以有多个单列索引。
组合索引是指在表的多个字段组合上创建的索引,只有在查询条件中使用了这些字段的左边字段时,索引才会被使用。使用组合索引时遵循最左前缀集合

3.全文索引
4.空间索引

MySQL怎么判断要不要加索引

建议按照如下的原则来创建索引:

当唯一性是某种数据本身的特征时,指定唯一索引。使用唯一索引需能确保定义的列的数据完整性,以提高查询速度。

在频繁进行排序或分组(即进行group by或order by操作)的列上建立索引,如果待排序的列有多个,可以在这些列上建立组合索引。

那些情况不适合创建索引

1.频繁更新的字段
2.where条件中用不到的字段
3.数据比较少的表
4.数据重复且分布比较均匀的字段,比如性别,真价值
5.参与列计算的列不适合建索引

索引的实现原理

在MySQL中,索引是在存储引擎层实现的,不同存储引擎对索引的实现方式是不同的,下面我们探讨一下MyISAM和InnoDB两个存储引擎的索引实现方式。

MyISAM索引实现:

MyISAM引擎使用B+Tree作为索引结构,叶节点的data域存放的是数据记录的地址,MyISAM索引的原理图如下。这里假设表一共有三列,假设我们以Col1为主键,则上图是一个MyISAM表的主索引(Primary key)示意。可以看出MyISAM的索引文件仅仅保存数据记录的地址。在MyISAM中,主索引和辅助索引(Secondary key)在结构上没有任何区别,只是主索引要求key是唯一的,而辅助索引的key可以重复。
如果我们在Col2上建立一个辅助索引,则此索引的结构如下图所示。同样也是一颗B+Tree,data域保存数据记录的地址。因此,MyISAM中索引检索的算法为首先按照B+Tree搜索算法搜索索引,如果指定的Key存在,则取出其data域的值,然后以data域的值为地址,读取相应数据记录。

InnoDB索引实现:

虽然InnoDB也使用B+Tree作为索引结构,但具体实现方式却与MyISAM截然不同。

第一个重大区别是InnoDB的数据文件本身就是索引文件。从上文知道,MyISAM索引文件和数据文件是分离的,索引文件仅保存数据记录的地址。而在InnoDB中,表数据文件本身就是按B+Tree组织的一个索引结构,这棵树的叶节点data域保存了完整的数据记录。这个索引的key是数据表的主键,因此InnoDB表数据文件本身就是主索引。

下图是InnoDB主索引(同时也是数据文件)的示意图,可以看到叶节点包含了完整的数据记录。这种索引叫做聚集索引。因为InnoDB的数据文件本身要按主键聚集,所以InnoDB要求表必须有主键(MyISAM可以没有),如果没有显式指定,则MySQL系统会自动选择一个可以唯一标识数据记录的列作为主键,如果不存在这种列,则MySQL自动为InnoDB表生成一个隐含字段作为主键,这个字段长度为6个字节,类型为长整形。

第二个与MyISAM索引的不同是InnoDB的辅助索引data域存储相应记录主键的值而不是地址。换句话说,InnoDB的所有辅助索引都引用主键作为data域。下图为定义在Col3上的一个辅助索引。这里以英文字符的ASCII码作为比较准则。聚集索引这种实现方式使得按主键的搜索十分高效,但是辅助索引搜索需要检索两遍索引:首先检索辅助索引获得主键,然后用主键到主索引中检索获得记录。

了解不同存储引擎的索引实现方式对于正确使用和优化索引都非常有帮助,例如知道了InnoDB的索引实现后,就很容易明白为什么不建议使用过长的字段作为主键,因为所有辅助索引都引用主索引,过长的主索引会令辅助索引变得过大。再例如,用非单调的字段作为主键在InnoDB中不是个好主意,因为InnoDB数据文件本身是一颗B+Tree,非单调的主键会造成在插入新记录时数据文件为了维持B+Tree的特性而频繁的分裂调整,十分低效,而使用自增字段作为主键则是一个很好的选择。

聚簇索引有哪些类别

聚簇索引和非聚簇索引有什么区别

在InnoDB存储引擎中,可以将B+树索引分为聚簇索引和辅助索引(非聚簇索引)。无论是何种索引,每个页的大小都为16KB,且不能更改。

聚簇索引是根据主键创建的一棵B+树,聚簇索引的叶子节点存放了表中的所有记录。辅助索引是根据索引键创建的一棵B+树,与聚簇索引不同的是,其叶子节点仅存放索引键值,以及该索引键值指向的主键。也就是说,如果通过辅助索引来查找数据,那么当找到辅助索引的叶子节点后,很有可能还需要根据主键值查找聚簇索引来得到数据,这种查找方式又被称为书签查找。因为辅助索引不包含行记录的所有数据,这就意味着每页可以存放更多的键值,因此其高度一般都要小于聚簇索引。

mysql的存储引擎有哪些

MySQL的存储引擎有MyISAM和InnoDB,MEMORY/HEAP
MyISAM和InnoDB存储引擎只支持BTREE索引
MEMORY/HEAP存储引擎可以支持HASH和BTREE索引。

Mysql的隔离级别有哪些?

Mysql定义了四种隔离级别,包括一些具体规则,用于限定事物内外哪些改变是可见的,哪些改变时不可见的。
低级别的隔离一般支持更高的并发处理,并且拥有更低的系统开销

READ UNCOMMITTED 读取未提交内容

在这个隔离级别,所有事物都可以“看到”未提交事物的执行结果。在这种级别上,可能会产生很多问题,除非用户真的知道自己在干什么,并有很好的理由选择这样做。本隔离级别很少用于实际应用,因为它的性能也不比其他性能好多少,而别的级别还有其他更多的优点,读取未提交数据,也被称为“脏读”

READ COMMITTED 读取提交内容
大多数数据库的默认隔离级别(但是不是MySQL的默认隔离级别),满足了隔离的早先简单定义:一个事务开始时,只能“看见”已经提交事务所做的改变,一个事务从开始到提交钱,所做的任何数据改变都是不可见的,除非已经提交,这种隔离级别也支持所谓的“不可重复读”,这意味着用户运行同一个语句两次,看到的结果是不同的。

repeatble read 可重复读
MySQL 数据库默认的隔离界别。该级别解决了read uncommitted隔离级别导致的问题。它保证同一事务的多个实例在并发读取事务时,会“看到同样的”数据行。

serializable 可串行化
强制给事务排序,使之不可能冲突

1.脏读
脏读是指一个事务读取了未提交事务执行过程中的数据。
当一个事务的操作正在多次修改数据,而在事务还未提交的时候,另外一个并发事务
2.不可重复读
3.虚读(幻读)

单例模式

优点

● 单例类只有一个实例对象,节省了系统资源(省去了对象的频繁创建 与销毁);
● 该单例对象必须由单例类自行创建;
● 单例类对外提供一个访问该单例的全局访问点。

缺点

● 单例模式一般没有接口,扩展困难。如果要扩展,则除了修改原来的代码,没有第二种途径,违背开闭原则。
● 在并发测试中,单例模式不利于代码调试。在调试过程中,如果单例中的代码没有执行完,也不能模拟生成一个新的对象。
● 单例模式的功能代码通常写在一个类中,如果功能设计不合理,则很容易违背单一职责原则。

使用场景

● 需要频繁的进行创建和销毁的对象;
● 创建对象时耗时过多或耗费资源过多,但又经常用到的对象;
● 工具类对象;
● 频繁访问数据库或文件的对象。

手写一个单例模式

饿汉式单例模式

public class Singleton {     
private static Singleton instance = new Singleton(); 
    // 私有构造方法,保证外界无法直接实例化。
         private Singleton() {}     
         // 通过公有的静态方法获取对象实例     
         public static Singleton getInstace() {         
         	return instance;     
         } 
 }
public class Singleton {
     private static Singleton instance = null; 
         // 私有构造方法,保证外界无法直接实例化。    
          private Singleton() {}     
          // 通过公有的静态方法获取对象实例    
           public static Singleton getInstace() {        
            if (instance == null) {             
            instance = new Singleton();         
            }        
             return instance;     
             }
    }

手写一个线程安全的单例模式

public class Singleton {     
private static Singleton instance = null; 
    // 私有构造方法,保证外界无法直接实例化。
         private Singleton() {
         }     
         // 通过公有的静态方法获取对象实例     synchronized public static Singleton getInstace() {         if (instance == null) {             instance = new Singleton();        
          }
                   return instance;
                        } 
             }

高并发

高并发有哪些实践方案

要实现高并发,一般针对高性能,高可用,高扩展3个方面,总结一下可落地的实践方案。

高性能的时间方案

1.集群部署,通过负载均衡减轻单机压力
2.多级缓存,包括静态数据使用CDN,本地缓存,分布式缓存等,以及处理对缓存场景中的热点key,缓存穿透,缓存并发,数据一致性等问题的处理。
3.分库分表和索引优化,以及借助搜索引擎解决复杂查询问题。
4.考虑NoSQL数据库的使用,比如HBase,TiDB等,但是团队必须熟悉这些组件,且有较强的运维能力
5.异步化,将次要流程通过多线程、MQ、甚至延时任务进行异步处理。
限流,需要先考虑业务是否允许限流(比如秒杀场景是允许的),包括前端限流、Nginx接入层的限流、服务端的限流。

对流量进行削峰填谷,通过MQ承接流量。

并发处理,通过多线程将串行逻辑并行化。

预计算,比如抢红包场景,可以提前计算好红包金额缓存起来,发红包时直接使用即可。

缓存预热,通过异步任务提前预热数据到本地缓存或者分布式缓存中。

减少IO次数,比如数据库和缓存的批量读写、RPC的批量接口支持、或者通过冗余数据的方式干掉RPC调用。

减少IO时的数据包大小,包括采用轻量级的通信协议、合适的数据结构、去掉接口中的多余字段、减少缓存key的大小、压缩缓存value等。

程序逻辑优化,比如将大概率阻断执行流程的判断逻辑前置、For循环的计算逻辑优化,或者采用更高效的算法。

各种池化技术的使用和池大小的设置,包括HTTP请求池、线程池(考虑CPU密集型还是IO密集型设置核心参数)、数据库和Redis连接池等。

JVM优化,包括新生代和老年代的大小、GC算法的选择等,尽可能减少GC频率和耗时。

锁选择,读多写少的场景用乐观锁,或者考虑通过分段锁的方式减少锁冲突。

上述方案无外乎从计算和 IO 两个维度考虑所有可能的优化点,需要有配套的监控系统实时了解当前的性能表现,并支撑你进行性能瓶颈分析,然后再遵循二八原则,抓主要矛盾进行优化。

3.2.2 高可用的实践方案

对等节点的故障转移,Nginx和服务治理框架均支持一个节点失败后访问另一个节点。

非对等节点的故障转移,通过心跳检测并实施主备切换(比如redis的哨兵模式或者集群模式、MySQL的主从切换等)。

接口层面的超时设置、重试策略和幂等设计。

降级处理:保证核心服务,牺牲非核心服务,必要时进行熔断;或者核心链路出问题时,有备选链路。

限流处理:对超过系统处理能力的请求直接拒绝或者返回错误码。

MQ场景的消息可靠性保证,包括producer端的重试机制、broker侧的持久化、consumer端的ack机制等。

灰度发布,能支持按机器维度进行小流量部署,观察系统日志和业务指标,等运行平稳后再推全量。

监控报警:全方位的监控体系,包括最基础的CPU、内存、磁盘、网络的监控,以及Web服务器、JVM、数据库、各类中间件的监控和业务指标的监控。

灾备演练:类似当前的“混沌工程”,对系统进行一些破坏性手段,观察局部故障是否会引起可用性问题。

高可用的方案主要从冗余、取舍、系统运维3个方向考虑,同时需要有配套的值班机制和故障处理流程,当出现线上问题时,可及时跟进处理。

3.2.3 高扩展的实践方案

合理的分层架构:比如上面谈到的互联网最常见的分层架构,另外还能进一步按照数据访问层、业务逻辑层对微服务做更细粒度的分层(但是需要评估性能,会存在网络多一跳的情况)。

存储层的拆分:按照业务维度做垂直拆分、按照数据特征维度进一步做水平拆分(分库分表)。

业务层的拆分:最常见的是按照业务维度拆(比如电商场景的商品服务、订单服务等),也可以按照核心接口和非核心接口拆,还可以按照请求源拆(比如To C和To B,APP和H5)。

get请求与post请求有什么区别?

16)GET方法和POST方法的区别是什么?

GET和POST本质上就是TCP链接,并无差别。但是由于HTTP的规定和浏览器/服务器的限制,导致他们在应用过程中体现出一些不同。

● Get产生一个TCP数据包;Post产生两个TCP数据包。
● GET请求会被浏览器主动缓存,而POST不会,除非手动设置。
● GET请求只能进行url编码,而POST支持多种编码方式。
● GET请求参数会被完整保留在浏览器历史记录里,而POST中的参数不会被保留。

  1. GET与POST都有自己的语义,不能随便混用。
  2. 据研究,在网络环境好的情况下,发一次包的时间和发两次包的时间差别基本可以无视。而在网络环境差的情况下,两次包的TCP在验证数据包完整性上,有非常大的优点。
  3. 并不是所有浏览器都会在POST中发送两次包,Firefox就只发送一次。

GET在浏览器回退时是无害的,而POST会再次提交请求。

GET产生的URL地址可以被Bookmark,而POST不可以。

GET请求会被浏览器主动cache,而POST不会,除非手动设置。

GET请求只能进行url编码,而POST支持多种编码方式。

GET请求参数会被完整保留在浏览器历史记录里,而POST中的参数不会被保留。

GET请求在URL中传送的参数是有长度限制的,而POST没有。

对参数的数据类型,GET只接受ASCII字符,而POST没有限制。

GET比POST更不安全,因为参数直接暴露在URL上,所以不能用来传递敏感信息。

GET参数通过URL传递,POST放在Request body中。

  1. GET与POST都有自己的语义,不能随便混用。
  2. 据研究,在网络环境好的情况下,发一次包的时间和发两次包的时间差别基本可以无视。而在网络环境差的情况下,两次包的TCP在验证数据包完整性上,有非常大的优点。
  3. 并不是所有浏览器都会在POST中发送两次包,Firefox就只发送一次。

Linux简单命令

查看空间,内存

1.free

free命令用于显示内存状态。

free指令会显示内存的使用情况,包括实体内存,虚拟的交换文件内存,共享内存区段,以及系统核心使用的缓冲区等。

查询日志

cat 由第一行开始显示文件内容
tac 从最后一行开始显示,可以看出 tac 是 cat 的倒着写!
nl 显示的时候,顺道输出行号!
more 一页一页的显示文件内容
less 与 more 类似,但是比 more 更好的是,他可以往前翻页!
head 只看头几行
tail 只看尾巴几行

#根据文件名查文件路径 -i表示不区分大小写
find -iname “文件名”
#根据关键字查询在文件哪一行(注意关键字与文件名大小写区分)
grep -n 【关键字】 文件名

查端口号

Linux 查看端口占用情况可以使用 lsof 和 netstat 命令。

lsof(list open files)是一个列出当前系统打开文件的工具。

lsof 查看端口占用语法格式:
lsof -i:端口号

netstat -tunlp 用于显示 tcp,udp 的端口和进程等相关情况。

netstat 查看端口占用语法格式:

netstat -tunlp | grep 端口号

mikdir——Make Directory

执行mkdir命令创建相应的文件夹

pwd — Print Working Directory

显示当前工作目录

cd — Change Directory

切换文件路径,cd 将给定的文件夹(或目录)设置成当前工作目录。
切换路径到桌面 desktop

.rmdir— Remove Directory

删除给定的目录

rm— Remove

删除某个文件

cp— Copy

文件的复制

mv— Move

mv 命令对文件或文件夹进行移动,如果文件或文件夹存在于当前工作目录,还可以对文件或文件夹进行重命名。

grep

在给定的文件中搜寻指定的字符串。grep -i “” 在搜寻时会忽略字符串的大小写,而grep -r “” 则会在当前工作目录的文件中递归搜寻指定的字符串。

find

这个命令会在给定位置搜寻与条件匹配的文件。你可以使用find -name 的-name选项来进行区分大小写的搜寻,find -iname 来进行不区分大小写的搜寻。

tar

tar命令能创建、查看和提取tar压缩文件。tar -cvf 是创建对应压缩文件,tar -tvf 来查看对应压缩文件,tar -xvf 来提取对应压缩文件。
查看压缩文件

gzip

gzip 命令创建和提取gzip压缩文件,还可以用gzip -d 来提取压缩文件。
生成gzip文件

2、八股文题目
Java基础
1)为什么Java代码可以实现一次编写、到处运行?
2)一个Java文件里可以有多个类吗(不含内部类)?
3)说一说你对Java访问权限的了解
4)介绍一下Java的数据类型
5)int类型的数据范围是多少?
6)请介绍全局变量和局部变量的区别
7)请介绍一下实例变量的默认值
8)为啥要有包装类?
10)如何对Integer和Double类型判断相等?
11)int和Integer有什么区别,二者在做==运算时会得到什么结果?

14)封装的目的是什么,为什么要有封装?
15)说一说你对多态的理解
16)Java中的多态是怎么实现的?
17)Java为什么是单继承,为什么不能多继承?
18)说一说重写与重载的区别
19)构造方法能不能重写?
20)介绍一下Object类中的方法
集合类
1)Java中有哪些容器(集合类)?
2)Java中的容器,线程安全和线程不安全的分别有哪些?
3)Map接口有哪些实现类?
4)描述一下Map put的过程
5)如何得到一个线程安全的Map?
6)HashMap有什么特点?
7)JDK7和JDK8中的HashMap有什么区别?
8)介绍一下HashMap底层的实现原理
9)介绍一下HashMap的扩容机制
10)HashMap中的循环 链表是如何产生的?
11)HashMap为什么用 红黑树而不用B树?
12)HashMap为什么线程不安全?
13)HashMap如何实现线程安全?
14)HashMap是如何解决哈希冲突的?
15)说一说HashMap和HashTable的区别
16)HashMap与ConcurrentHashMap有什么区别?
17)介绍一下ConcurrentHashMap是怎么实现的?
18)ConcurrentHashMap是怎么分段分组的?
19)说一说你对LinkedHashMap的理解
20)请介绍LinkedHashMap的底层原理
IO
1)介绍一下Java中的IO流
2)怎么用流打开一个大文件?
3)说说NIO的实现原理
4)介绍一下Java的序列化与反序列化
5)Serializable接口为什么需要定义serialVersionUID变量?
6)除了Java自带的序列化之外,你还了解哪些序列化工具?
7)如果不用JSON工具,该如何实现对实体类的序列化?
多线程
1)说一说线程的生命周期
2)说一说线程同步的方式
3)说一说你对volatile关键字的理解
4)说一说synchronized的实现原理
5)创建线程有哪几种方式
JVM
1)说一说你对双亲委派模型的理解
2)介绍一下分代回收机制
3)内存泄漏问题该如何解决
4)内存溢出问题该如何解决
5)JVM包含哪几部分?
框架
1)请描述Spring Boot自动装配的过程
2)说一说你对Spring IoC的理解
3)说一说你对Spring AOP的理解
4)什么是MVC
5)说说你对Spring Boot的理解
Redis
1)说一说Redis的单线程模型
2)缓存穿透、缓存击穿、缓存雪崩有什么区别,该如何解决?
3)说一说你对布隆过滤器的理解
4)说一说hash类型的底层数据结构
5)说一说zset类型的底层数据结构
6)Redis有哪些数据类型
操作系统

3)请你说说 Linux 中 fork() 函数的作用
4)说一说进程调度算法有哪些
5)说一说 Linux 如何管理内存

8)简述一下操作系统中的缺页中断
9)说一说虚拟地址空间有哪些部分
10)什么是孤儿进程,什么是僵尸进程,如何解决僵尸进程
11)说一说进程通信的方式有哪些?
12)说说常见信号有哪些,表示什么含义
13)说一说进程有多少种状态,如何转换
14)介绍一下信号量
15)说一说 select 的原理以及缺点
16)说一说 epoll 的原理
17)介绍一下几种 IO 模型
18)说一说软链接和硬链接的区别
计算机网络
网络模型
OSI七层模型

物理层:网卡,网线,集线器,中继器,调制解调器

数据链路层:网桥,交换机

网络层:路由器

1)TCP/IP四层模型
2)结合OSI模型和TCP/IP模型的五层协议体系结构
3)IP协议的首部结构
4)DNS(域名系统)是什么?

TCP/UDP

TCP 是面向连接,能保证数据的可靠性交付,因此经常用于:

● FTP文件传输
● HTTP / HTTPS

UDP 面向无连接,它可以随时发送数据,再加上UDP本身的处理既简单又高效,因此经常用于:

● 包总量较少的通信,如 DNS 、SNMP等
● 视频、音频等多媒体通信
● 广播通信

1)TCP(传输控制协议)是什么?
2)TCP协议的特点有哪些?
3)TCP协议的首部结构
4)TCP协议三次握手和四次挥手的过程
5)TCP协议是怎么保证有效传输的?
6)TCP协议的流量控制和拥塞控制
7)UDP(用户数据报协议)是什么?
8)UDP协议的特点有哪些?
9)UDP协议的首部结构
10)如何让UDP协议变得可靠?
11)TCP协议和UDP协议的区别是什么?
12)HTTP(超文本传输协议)是什么?
14)常见的HTTP协议状态码有哪些?
状态代码有三位数字组成,第一个数字定义了响应的类别,共分五种类别:
● 1xx:指示信息–表示请求已接收,继续处理
● 2xx:成功–表示请求已被成功接收、理解、接受
● 3xx:重定向–要完成请求必须进行更进一步的操作
● 4xx:客户端错误–请求有语法错误或请求无法实现
● 5xx:服务器端错误–服务器未能实现合法的请求

15)HTTP协议的请求方式有哪些?

17)常见的HTTP协议请求头有哪些?
18)常见的HTTP协议响应头有哪些?
HTTP/HTTPS
● HTTP 明文传输,数据都是未加密的,安全性较差,HTTPS(SSL+HTTP) 数据传输过程是加密的,安全性较好。
● 使用 HTTPS 协议需要到 CA(Certificate Authority,数字证书认证机构) 申请证书,一般免费证书较少,因而需要一定费用。证书颁发机构如:Symantec、Comodo、GoDaddy 和 GlobalSign 等。
● HTTP 页面响应速度比 HTTPS 快,主要是因为 HTTP 使用 TCP 三次握手建立连接,客户端和服务器需要交换 3 个包,而 HTTPS除了 TCP 的三个包,还要加上 ssl 握手需要的 9 个包,所以一共是 12 个包。
● http 和 https 使用的是完全不同的连接方式,用的端口也不一样,前者是 80,后者是 443。
● HTTPS 其实就是建构在 SSL/TLS 之上的 HTTP 协议,SSL运行在TCP协议之上;所以,要比较 HTTPS 比 HTTP 要更耗费服务器资源。

HTTP原理HTTP是一个基于TCP/IP通信协议来传递数据的协议,传输的数据类型为HTML 文件,、图片文件, 查询结果等。

HTTP协议一般用于B/S架构()。浏览器作为HTTP客户端通过URL向HTTP服务端即WEB服务器发送所有请求。

存在的问题:请求信息明文传输,容易被窃听截取;数据的完整性未校验,容易被篡改;没有验证对方身份,存在冒充危险

HTTP结构
request 请求行(request line)、请求头部(header)、空行和请求数据四个部分组成。
response 状态行、消息报头、空行和响应正文

HTTP 5大特点

  1. 支持客户/服务器模式。
  2. 简单快速:客户向服务器请求服务时,只需传送请求方法和路径。请求方法常用的有GET、HEAD、POST。每种方法规定了客户与服务器联系的类型不同。由于HTTP协议简单,使得HTTP服务器的程序规模小,因而通信速度很快。
  3. 灵活:HTTP允许传输任意类型的数据对象。正在传输的类型由Content-Type加以标记。
  4. 无连接:无连接的含义是限制每次连接只处理一个请求。服务器处理完客户的请求,并收到客户的应答后,即断开连接。采用这种方式可以节省传输时间。早期这么做的原因是请求资源少,追求快。后来通过Connection: Keep-Alive实现长连接
  5. 无状态:HTTP协议是无状态协议。无状态是指协议对于事务处理没有记忆能力。缺少状态意味着如果后续处理需要前面的信息,则它必须重传,这样可能导致每次连接传送的数据量增大。另一方面,在服务器不需要先前信息时它的应答就较快。

HTTPS 协议(HyperText Transfer Protocol over Secure Socket Layer):一般理解为HTTP+SSL/TLS,通过 SSL证书来验证服务器的身份,并为浏览器和服务器之间的通信进行加密。

HTTPS存在的问题:

● HTTPS协议多次握手,导致页面的加载时间延长近50%;
● HTTPS连接缓存不如HTTP高效,会增加数据开销和功耗;
● 申请SSL证书需要钱,功能越强大的证书费用越高。
● SSL涉及到的安全算法会消耗 CPU 资源,对服务器资源消耗较大。

1)HTTP协议的缓存策略有哪些?
2)HTTPS(安全超文本传输协议)是什么?
3)HTTPS协议如何保证整个传输过程安全?
4)HTTPS协议对称加密的过程?
5)HTTPS协议非对称加密的过程?
6)HTTPS协议中间人攻击是什么?
7)哪些问题是HTTPS无法解决的?
数据库

5)如何判断MySQL中的索引有没有生效
6)说一说InnoDB引擎中索引的实现原理
7)MySQL的ACID特性分别是怎么实现的
8)谈谈MySQL的事务隔离级别
9)谈谈InnoDB引擎中的锁
10)InnoDB中的行级锁是怎么实现的
11)说一说你对MySQL引擎的了解
12)说一说你对redo log、undo log、bin log的了解
14)MySQL主从同步是如何实现的
15)你对MySQL的慢查询优化有了解吗

1.项目中使用的redis客户端,版本
2.springboot版本
3.redis中list 和set区别,list命令
4.redis的淘汰策略
5.线程池参数,核心与队列都满的情况下,新进任务如何处理
6.char和varchar区别
7.spring事务如何回滚
8.spring事务在各种隔离级别情况下,内层发生异常后,外层事务运行情况
9.stringbuffer与stringbuilder区别
10.es与Mysql如何同步

1.两个栈实现一个队列
2.单列模式(懒汉与饿汉)
3.redis持久化机制
4.MySQL慢日志查询方式
5.explian各个参数
6.spring boot自动装配原理
7.MySQL事务级别

1.jvm说一下相关区域
2.说一下**回收算法
3.线程池说一下
4.AQS说一下
5.如何排查线上问题
6.sync与lock区别
7.sync具体实现,锁升级

1.项目中使用的redis客户端,版本
2.springboot版本
3.redis中list 和set区别,list命令
4.redis的淘汰策略
5.线程池参数,核心与队列都满的情况下,新进任务如何处理
6.char和varchar区别
7.spring事务如何回滚
8.spring事务在各种隔离级别情况下,内层发生异常后,外层事务运行情况
9.stringbuffer与stringbuilder区别
10.es与Mysql如何同步

1.两个栈实现一个队列
2.单列模式(懒汉与饿汉)
3.redis持久化机制
4.MySQL慢日志查询方式
5.explian各个参数
6.spring boot自动装配原理
7.MySQL事务级别

1.jvm说一下相关区域
2.说一下**回收算法
3.线程池说一下
4.AQS说一下
5.如何排查线上问题
6.sync与lock区别
7.sync具体实现,锁升级

179.redis 是什么?都有哪些使用场景?

redis 是一个使用 C 语言开发的高速缓存数据库。

redis 使用场景:

● 记录帖子点赞数、点击数、评论数;
● 缓存近期热帖;
● 缓存文章详情信息;
● 记录用户会话信息。

180.redis 有哪些功能?

● 数据缓存功能
● 分布式锁的功能
● 支持数据持久化
● 支持事务
● 支持消息队列

181.redis 和 memcache 有什么区别?

● 存储方式不同:memcache 把数据全部存在内存之中,断电后会挂掉,数据不能超过内存大小;redis 有部份存在硬盘上,这样能保证数据的持久性。
● 数据支持类型:memcache 对数据类型支持相对简单;redis 有复杂的数据类型。
● 使用底层模型不同:它们之间底层实现方式,以及与客户端之间通信的应用协议不一样,redis 自己构建了 vm 机制,因为一般的系统调用系统函数的话,会浪费一定的时间去移动和请求。
● value 值大小不同:redis 最大可以达到 512mb;memcache 只有 1mb。

182.redis 为什么是单线程的?

因为 cpu 不是 redis 的瓶颈,redis 的瓶颈最有可能是机器内存或者网络带宽。既然单线程容易实现,而且 cpu 又不会成为瓶颈,那就顺理成章地采用单线程的方案了。

关于 redis 的性能,官方网站也有,普通笔记本轻松处理每秒几十万的请求。

而且单线程并不代表就慢 nginx 和 nodejs 也都是高性能单线程的代表。

183.什么是缓存穿透?怎么解决?

缓存穿透:指查询一个一定不存在的数据,由于缓存是不命中时需要从数据库查询,查不到数据则不写入缓存,这将导致这个不存在的数据每次请求都要到数据库去查询,造成缓存穿透。

解决方案:最简单粗暴的方法如果一个查询返回的数据为空(不管是数据不存在,还是系统故障),我们就把这个空结果进行缓存,但它的过期时间会很短,最长不超过五分钟。

184.redis 支持的数据类型有哪些?

redis 支持的数据类型:string(字符串)、list(列表)、hash(字典)、set(集合)、zset(有序集合)。

185.redis 支持的 java 客户端都有哪些?

支持的 java 客户端有 redisson、jedis、lettuce 等。

186.jedis 和 redisson 有哪些区别?

● jedis:提供了比较全面的 redis 命令的支持。
● redisson:实现了分布式和可扩展的 java 数据结构,与 jedis 相比 redisson 的功能相对简单,不支持排序、事务、管道、分区等 redis 特性。

187.怎么保证缓存和数据库数据的一致性?

● 合理设置缓存的过期时间。
● 新增、更改、删除数据库操作时同步更新 redis,可以使用事物机制来保证数据的一致性。

188.redis 持久化有几种方式?

redis 的持久化有两种方式,或者说有两种策略:

● RDB(Redis Database):指定的时间间隔能对你的数据进行快照存储。
● AOF(Append Only File):每一个收到的写命令都通过write函数追加到文件中。

189.redis 怎么实现分布式锁?

redis 分布式锁其实就是在系统里面占一个“坑”,其他程序也要占“坑”的时候,占用成功了就可以继续执行,失败了就只能放弃或稍后重试。

占坑一般使用 setnx(set if not exists)指令,只允许被一个程序占有,使用完调用 del 释放锁。

190.redis 分布式锁有什么缺陷?

redis 分布式锁不能解决超时的问题,分布式锁有一个超时时间,程序的执行如果超出了锁的超时时间就会出现问题。

191.redis 如何做内存优化?

尽量使用 redis 的散列表,把相关的信息放到散列表里面存储,而不是把每个字段单独存储,这样可以有效的减少内存使用。比如将 web 系统的用户对象,应该放到散列表里面再整体存储到 redis,而不是把用户的姓名、年龄、密码、邮箱等字段分别设置 key 进行存储。

192.redis 淘汰策略有哪些?

● volatile-lru:从已设置过期时间的数据集(server.db[i].expires)中挑选最近最少使用的数据淘汰。
● volatile-ttl:从已设置过期时间的数据集(server.db[i].expires)中挑选将要过期的数据淘汰。
● volatile-random:从已设置过期时间的数据集(server.db[i].expires)中任意选择数据淘汰。
● allkeys-lru:从数据集(server.db[i].dict)中挑选最近最少使用的数据淘汰。
● allkeys-random:从数据集(server.db[i].dict)中任意选择数据淘汰。
● no-enviction(驱逐):禁止驱逐数据。

193.redis 常见的性能问题有哪些?该如何解决?

● 主服务器写内存快照,会阻塞主线程的工作,当快照比较大时对性能影响是非常大的,会间断性暂停服务,所以主服务器最好不要写内存快照。
● redis 主从复制的性能问题,为了主从复制的速度和连接的稳定性,主从库最好在同一个局域网内。

1.给定如下java代码,编译运行之后,将会输出( )
public class Test {
public static void main(String[] args) {
int a = 5;
System.out.println(a % 2 == 1 ? (a + 1) / 2 : a / 2);
}
}
A. 1
B. 2.5
C. 3
D. 2

2.用“newFileOutputStream(“data.txt”,true)”创建一个FileOutputStream实例对象,则下面哪种说法是正确的( )
A. 如果文件“data.txt”存在,则将覆盖掉文件中已有的内容
B. 如果文件“data.txt”存在,则将在文件的末尾开始添加新内容
C. 如果文件“data.txt”不存在,则将抛出IOException异常
D. 如果文件“data.txt”存在,则将抛出IOException异常

3.以下json格式数据,错误的是( )
A. {company:“1234”}
B. {“company”:[1234,1234,1234]}
C. {“company”:{“name”:[1234,1234,1234]}}
D. {[1234,1234,1234]}

4.公司中有多个部门和多名员工,每个员工只能属于一个部门,一个部门能有多名员工,从员工到部门的联系类型是( )
A. 一对一
B. 多对一
C. 一对多
D. 多对多

5.以下能够删除一列的是( )
A.alter table emp delete column name
B.alter table emp drop column name
C.alter table emp delete name
D.alter table emp remove name

6.以下对TCP和UDP描述正确的是( )
A. TCP数据传输效率高于UDP
B. UDP能够保证数据的可靠性
C. TCP不能提供数据的可靠性
D. UDP数据传输效率高于TCP

7.SQL语句中,条件“between 20 and 30”表示在20到30之间,且( )
A.不包括20,包括30
B.包括20,不包括30
C.不包括20和30
D.包括20和30

8.下列HTTP响应状态码与含义对应错误的是( )
A. 404-请求的资源不存在
B. 503-网关接收到无效响应
C. 500-请求服务无响应
D. 403-请求被拒绝

9.以下关于break,continue说法正确的是( )
A. continue仅结束当前循环
B. continue结束当次循环而进行下一次循环
C. break结束当次循环而进行下一次循环
D. break仅结束当前循环

10.查询student表中的所有非空email信息,以下语句正确的是( )
A. select email from student where email <> null
B. select email from student where email is not null
C. select email from student where email != null
D. select email from student where email not is null

11.下列关于Tomcat的目录说法错误的是( )
A. bin目录——包含启动/关闭脚本
B. work目录——包含web项目示例,当发布web应用时,默认把war包放在此目录下
C. lib目录——包含使用的jar文件
D. conf目录——包含不同的配置文件

12.下列选项中哪个不是基础类型( )
A. byte
B. boolean
C. Long
D. float

13.UNIQUE唯一索引的作用是( )
A. 保证各行在该索引上的值不得重复
B. 保证参加唯一索引的各列,不得再参与其他的索引
C. 保证各行在该索引上的值不能为NULL
D. 保证唯一索引不能被删除

4.假定有x和y为整型,其值分别为20和3,则(double)x/y的值是( )
A. 6.666666666666667
B. 6
C. 20
D. 6.0

15.以下关于final关键字说法错误的是( )
A. final修饰的方法不能被重载
B. final是java中的修饰符,可以修饰类、接口、方法和属性
C. final修饰的变量不允许被再次赋值
D. final修饰的类肯定不能被继承

16.存在以下三个字符串:String x = “string”; String y = “string”; String z = new String(“string”); ,则下面哪个语句返回false( )
A. System.out.println(x.equals(z));
B. System.out.println(xz);
C. System.out.println(x
y);
D. System.out.println(x.equals(y));

17.下面对session对象的说法,错误的是( )
A. session可以用来存储访问者的一些特定信息
B. 当用户在应用程序间的页之间跳转时,存储在session对象中的变量会被清除
C. session可以创建访问者信息容器
D. session对象提供HTTP服务器和HTTP客户端之间的会话

18.在异常处理中,如释放资源、关闭文件、关闭数据库等由( )来完成。
A. finally子句
B. try子句
C. throw子句
D. catch子句

19.下列选项中哪个不是Collection接口的实现( )
A. LinkedList
B. ArrayList
C. HashMap
D. HashSet

20.以下对继承的描述错误的是( )
A. Java中的继承允许一个子类继承多个父类
B. 父类更具有通用性,子类更具体
C. Java中的继承存在着传递性
D. 当实例化子类时会递归调用父类中的构造方法

21.给定如下java代码,编译运行之后,将会输出( )
public class Animal {
void eat() {
System.out.print(“吃;”);
}
}
public class Cat extends Animal {
public void eat() {
System.out.print(“吃鱼;”);
}
public void work() {
System.out.print(“抓老鼠;”);
}
}
public class Dog extends Animal {
public void eat() {
System.out.print(“吃骨头;”);
}
public void work() {
System.out.print(“看家;”);
}
}
public class Test {
public static void main(String[] args) {
Animal animalOne = new Animal();
animalOne.eat();
Animal animalTwo = new Cat();
animalTwo.eat();
Dog dog = new Dog();
dog.eat();
dog.work();
}
}
A. 吃;吃鱼;吃骨头;抛出异常
B. 吃;吃;吃;看家;
C. 吃;吃鱼;吃骨头;看家;
D. 吃;吃;吃骨头;看家;

22.以下对接口描述错误的有( )
A.接口中的属性默认使用public、static、final修饰
B.接口不允许多继承
C.接口中的方法默认使用public、abstract修饰
D.接口没有提供构造方法

23.有关数据冗余说法错误的是( )
A. 通过分类存储,可以有效减少数据冗余,但是会增加数据查找的复杂性
B. 数据库中,数据存在副本的现象,就是数据冗余
C. 数据冗余通常是由于数据库设计引起的
D. 在数据库设计阶段,一定要尽最大可能避免数据冗余,最好做到无数据冗余

24.给定如下java代码,编译运行之后,将会输出( )
public class Test {
public static void main(String[] args) {
int a = 10;
int b = a++;
int c = --b;
System.out.print(“a=” + a + “;”);
System.out.print(“b=” + b + “;”);
System.out.print(“c=” + c + “;”);
}
}
A. a=10;b=11;c=10;
B. a=10;b=10;c=10;
C. a=11;b=11;c=9;
D. a=11;b=9;c=9;

25.为AB类的一个无形式参数无返回值的方法method书写方法头,使得使用类名AB作为前缀就可以调用它,该方法头的形式为( )
A. static void method()
B. final void method()
C. public void mothed()
D. abstract void method()

26.给定如下java代码,编译运行之后,将会输出( )
public class Test {
public static void main(String[] args) {
int a = 10;
int b = 20;
int c = 0;
if (++a > 10 || b++ > 20) {
c = a + b;
}
System.out.print(“a=” + a + “;”);
System.out.print(“b=” + b + “;”);
System.out.print(“c=” + c + “;”);
}
}
A. a=11;b=20;c=31;
B. a=10;b=20;c=30;
C. a=11;b=21;c=32;
D. a=11;b=20;c=0;

27.在一个线程中Sleep(1000)方法,将使得该线程在多少时间后获得对CPU的控制(假设睡眠过程中不会有其他事件唤醒该线程)( )
A. 1000毫秒不到
B. 不一定
C. 正好1000毫秒
D. =>1000毫秒

28.以下代码执行后的输出结果为( )
int x = -3;
int y = -10;
System.out.println(y%x);
A. 1
B. 3
C. -1
D. 2

29.下列选项中不符合变量命名规则的是( )
A. userName
B. HelloWorld
C. class
D. SCHOOL_NAME

30.设有数组的定义int[ ] a = new int[3],则下面对数组元素的引用错误的是( )
A. a[0]
B. a[【A】length-1]
C. int i = 1; a[i];
D. a[3]

https://nyimac.gitee.io/2020/07/03/JVM%E5%AD%A6%E4%B9%A0/#1%E3%80%81%E7%A8%8B%E5%BA%8F%E8%AE%A1%E6%95%B0%E5%99%A8

什么是JVM

翻译过来即jvm虚拟机

好处:
1.一次编写,到处运行
2.自动内存管理,垃圾回收机制
3.数组下标越界检查

比较
JVM JRE JDK的区别

JRE=JVM+基础库类
JDK=JVM+基础库类+编译工具

程序计数器

作用:
用于保存JVM中下一条所要执行的指令的之地

特点:
1:.线程私有
CPU会为每个线程分配时间片,当前线程使用的时间片完成后,CPU就会去执行另一个线程的代码
程序计数器是每个线程所稀有的,当另一个线程的时间片用完,又返回来执行当前线程的代码时,通过程序计数器可以知道应该执行哪一句指令
2.不会存在内存溢出

虚拟机栈

定义:
1.每个线程运行需要的内存空间,称为虚拟机栈
2.每个栈由多个栈帧组成,对应着每次调用方法时所占用的内存
3.每个线程只能有一个活动栈帧,对应着当前正在执行的方法

演示:

public class Main{
	public static void main(String[] args){
		method1();
	}
	
	private static void method1(){
		method2(1,2);
	}
	private static method2(int a,int b){
		int c = a + b;
		return c;
	} 
	
}

在这里插入图片描述
在控制台中可以看到,主类中的方法在进入虚拟机栈的时候,符合栈的特点
(即先进先出)

问题辨析
垃圾回收是否设计栈内存?
不需要。因为虚拟机栈中是由一个个栈帧组成的,在方法执行完毕后,对应的栈帧就会被弹出栈。所以无需通过垃圾回收机制去回收内存。
栈内存的分配越大越好吗?
不是。因为物理内存是一定的,栈内存越大,可以支持更多的递归调用,但是可执行的线程数就会越少。
方法内的局部变量是否是线程安全的?
如果方法内局部变量没有逃离方法的作用范围,则是线程安全的
如果局部变量引用了对象,并逃离了方法的作用范围,则需要考虑线程安全问题

内存溢出

Java.lang.stackOverflowError 栈内存溢出
发生原因
虚拟机栈中,栈帧过多(无限递归)
每个栈帧所占用过大

线程运行诊断

CPU占用过高
Linux环境下运行某些程序的时候,可能导致CPU的占用过高,这时需要定位占用CPU过高的线程
1.top命令,查看是哪个进程占用CPU过高
2.ps H -eo pid, tid(线程id), %cpu | grep 通过刚才top命令查到的进程号,通过ps命令进一步查看是哪个线程占用CPU过高
4.jstack 进程id 通过查看进程中的线程的nid,刚才通过ps命令看到的tid来对比定位,注意jstack查找出的线程id是16进制的,需要转换

本地方法栈

一些带有native关键字的方法就是需要JAVA去调用本地的C或者C++方法,因为JAVA有时候没法直接和操作系统底层交互,所以需要用到本地方法

定义 :
通过new关键字创建的对象都会被放在堆内存

特点:
所有线程共享,堆内存中的对象都需要考虑线程安全问题
有垃圾回收机制

堆内存溢出
java.lang.OutofMemoryError:java heap space 堆内存溢出

堆内存诊断
jps

jmap

jconsole

jvirsalvm

5.方法区

在这里插入图片描述

内存溢出

1.8以前会导致永久代内存溢出
1.8以后会导致元空间内存溢出

常量池

二进制字节码的组成:类的基本信息,常量池,类的方法定义(包含了虚拟机指令)

通过反编译来查看类的信息
  1. 获得对应类的.class文件
    在JDK对应的bin目录下运行cmd,也可以在IDEA控制台输入
    或者输入javac对应类的绝对路径
    输入完成后,对应的目录下就会出现类的.class文件

  2. 在控制台输入javap -v类的绝对路径

javap -v F:\Thread_study\src\com\nyima\JVM\day01\Main.class
  1. 然后能在控制台看到反编译以后类的信息了
    类的基本信息
    在这里插入图片描述

常量池

在这里插入图片描述
在这里插入图片描述
虚拟机中执行编译的方法(框内的是真正编译执行的内容,#号的内容需要在常量池中查找)
在这里插入图片描述

虚拟机中执行编译的方法(框内的是真正编译执行的内容,#号的内容需要在常量池中查找)

常量池
常量池实际上就是一张表,如上图中的constant pool,虚拟机指令根据这张常量表找到要执行的雷鸣,方法名,参数类型,字面量信息

运行时常量池
常量池是.class文件中的,当该*类被加载以后,它的常量池信息就会放入运行时常量池,并把里面的富豪地址变为真实地址**

常量池与串池的关系

串池StringTable

特征

1.常量池中的字符串仅是富豪,只有在被用到时才会转化为对象
2.利用串池的机制,来避免重复创建字符串对象
3.字符串变量拼接的原理是StringBuilder
4.字符串常量拼接的原理是编译器优化
5.可以使用intern方法,主动将串池中还没有的字符串放入串池中
6.注意:无论是串池还是堆里面的字符串,都是对象

用来放字符串对象且里面的元素不重复

public class StringTableStudy {
	public static void main(String[] args) {
		String a = "a"; 
		String b = "b";
		String ab = "ab";
	}
}

常量池中的信息,都会被加载到运行时常量池中,但这是a b ab 仅是常量池中的符号,还没有成为java字符串

0: ldc           #2                  // String a
2: astore_1
3: ldc           #3                  // String b
5: astore_2
6: ldc           #4                  // String ab
8: astore_3
9: return

当执行到 ldc #2 时,会把符号 a 变为 “a” 字符串对象,并放入串池中(hashtable结构 不可扩容)

当执行到 ldc #3 时,会把符号 b 变为 “b” 字符串对象,并放入串池中

当执行到 ldc #4 时,会把符号 ab 变为 “ab” 字符串对象,并放入串池中

最终StringTable [“a”, “b”, “ab”]

注意:字符串对象的创建都是懒惰的,只有当运行到那一行字符串且在串池中不存在的时候(如 ldc #2)时,该字符串才会被创建并放入串池中。

使用拼接字符串变量对象创建字符串的过程

public class StringTableStudy {
	public static void main(String[] args) {
		String a = "a";
		String b = "b";
		String ab = "ab";
		//拼接字符串对象来创建新的字符串
		String ab2 = a+b; 
	}
}

反编译后的结果

	 Code:
      stack=2, locals=5, args_size=1
         0: ldc           #2                  // String a
         2: astore_1
         3: ldc           #3                  // String b
         5: astore_2
         6: ldc           #4                  // String ab
         8: astore_3
         9: new           #5                  // class java/lang/StringBuilder
        12: dup
        13: invokespecial #6                  // Method java/lang/StringBuilder."<init>":()V
        16: aload_1
        17: invokevirtual #7                  // Method java/lang/StringBuilder.append:(Ljava/lang/String
;)Ljava/lang/StringBuilder;
        20: aload_2
        21: invokevirtual #7                  // Method java/lang/StringBuilder.append:(Ljava/lang/String
;)Ljava/lang/StringBuilder;
        24: invokevirtual #8                  // Method java/lang/StringBuilder.toString:()Ljava/lang/Str
ing;
        27: astore        4
        29: return

通过拼接的方式来创建字符串的过程是:StringBuilder().append(“a”).append(“b”).toString()

最后的toString方法的返回值是一个新的字符串,但字符串的值和拼接的字符串一致,但是两个不同的字符串,一个存在于串池之中,一个存在于堆内存之中

String ab = "ab";
String ab2 = a+b;
//结果为false,因为ab是存在于串池之中,ab2是由StringBuffer的toString方法所返回的一个对象,存在于堆内存之中
System.out.println(ab == ab2);

使用拼接字符串常量对象的方法创建字符串

public class StringTableStudy {
	public static void main(String[] args) {
		String a = "a";
		String b = "b";
		String ab = "ab";
		String ab2 = a+b;
		//使用拼接字符串的方法创建字符串
		String ab3 = "a" + "b";
	}
}

反编译后的结果

 	  Code:
      stack=2, locals=6, args_size=1
         0: ldc           #2                  // String a
         2: astore_1
         3: ldc           #3                  // String b
         5: astore_2
         6: ldc           #4                  // String ab
         8: astore_3
         9: new           #5                  // class java/lang/StringBuilder
        12: dup
        13: invokespecial #6                  // Method java/lang/StringBuilder."<init>":()V
        16: aload_1
        17: invokevirtual #7                  // Method java/lang/StringBuilder.append:(Ljava/lang/String
;)Ljava/lang/StringBuilder;
        20: aload_2
        21: invokevirtual #7                  // Method java/lang/StringBuilder.append:(Ljava/lang/String
;)Ljava/lang/StringBuilder;
        24: invokevirtual #8                  // Method java/lang/StringBuilder.toString:()Ljava/lang/Str
ing;
        27: astore        4
        //ab3初始化时直接从串池中获取字符串
        29: ldc           #4                  // String ab
        31: astore        5
        33: return

使用拼接字符串常量的方法来创建新的字符串时,因为内容是常量,javac在编译期会进行优化,结果已在编译期确定为ab,而创建ab的时候已经在串池中放入了“ab”,所以ab3直接从串池中获取值,所以进行的操作和 ab = “ab” 一致。
使用拼接字符串变量的方法来创建新的字符串时,因为内容是变量,只能在运行期确定它的值,所以需要使用StringBuilder来创建

intern方法 1.8

调用字符串对象的intern方法,会将该字符串对象尝试放入到串池中

如果串池中没有该字符串对象,则放入成功
如果有该字符串对象,则放入失败
无论放入是否成功,都会返回串池中的字符串对象

注意:此时如果调用intern方法成功,堆内存与串池中的字符串对象是同一个对象;如果失败,则不是同一个对象

例1

public class Main {
	public static void main(String[] args) {
		//"a" "b" 被放入串池中,str则存在于堆内存之中
		String str = new String("a") + new String("b");
		//调用str的intern方法,这时串池中没有"ab",则会将该字符串对象放入到串池中,此时堆内存与串池中的"ab"是同一个对象
		String st2 = str.intern();
		//给str3赋值,因为此时串池中已有"ab",则直接将串池中的内容返回
		String str3 = "ab";
		//因为堆内存与串池中的"ab"是同一个对象,所以以下两条语句打印的都为true
		System.out.println(str == st2);
		System.out.println(str == str3);
	}
}

例2

public class Main {
	public static void main(String[] args) {
        //此处创建字符串对象"ab",因为串池中还没有"ab",所以将其放入串池中
		String str3 = "ab";
        //"a" "b" 被放入串池中,str则存在于堆内存之中
		String str = new String("a") + new String("b");
        //此时因为在创建str3时,"ab"已存在与串池中,所以放入失败,但是会返回串池中的"ab"
		String str2 = str.intern();
        //false
		System.out.println(str == str2);
        //false
		System.out.println(str == str3);
        //true
		System.out.println(str2 == str3);
	}
}
intern方法 1.6

调用字符串对象的intern方法,会将该字符串对象尝试放入到串池中

如果串池中没有该字符串对象,会将该字符串对象复制一份,再放入到串池中
如果有该字符串对象,则放入失败
无论放入是否成功,都会返回串池中的字符串对象

注意:此时无论调用intern方法成功与否,串池中的字符串对象和堆内存中的字符串对象都不是同一个对象

StringTable 垃圾回收
StringTable在内存紧张时,会发生垃圾回收

StringTable调优

因为StringTable是由HashTable实现的,所以可以适当增加HashTable桶的个数,来减少字符串放入串池所需要的时间

-XX:StringTableSize=xxxx

考虑是否需要将字符串对象入池

可以通过intern方法减少重复入池

6、直接内存

属于操作系统,常见于NIO操作时,用于数据缓冲区
分配回收成本较高,但读写性能高
不受JVM内存回收管理

文件读写流程

在这里插入图片描述

使用了DirectBuffer

在这里插入图片描述

直接内存是操作系统和Java代码都可以访问的一块区域,无需将代码从系统内存复制到Java堆内存,从而提高了效率

释放原理

直接内存的回收不是通过JVM的垃圾回收来释放的,而是通过unsafe.freeMemory来手动释放

通过

//通过ByteBuffer申请1M的直接内存
ByteBuffer byteBuffer = ByteBuffer.allocateDirect(_1M);

申请直接内存,但JVM并不能回收直接内存中的内容,它是如何实现回收的呢?

allocateDirect的实现

public static ByteBuffer allocateDirect(int capacity) {
    return new DirectByteBuffer(capacity);
}

DirectByteBuffer类

DirectByteBuffer(int cap) {   // package-private
   
    super(-1, 0, cap, cap);
    boolean pa = VM.isDirectMemoryPageAligned();
    int ps = Bits.pageSize();
    long size = Math.max(1L, (long)cap + (pa ? ps : 0));
    Bits.reserveMemory(size, cap);

    long base = 0;
    try {
        base = unsafe.allocateMemory(size); //申请内存
    } catch (OutOfMemoryError x) {
        Bits.unreserveMemory(size, cap);
        throw x;
    }
    unsafe.setMemory(base, size, (byte) 0);
    if (pa && (base % ps != 0)) {
        // Round up to page boundary
        address = base + ps - (base & (ps - 1));
    } else {
        address = base;
    }
    cleaner = Cleaner.create(this, new Deallocator(base, size, cap)); //通过虚引用,来实现直接内存的释放,this为虚引用的实际对象
    att = null;
}

这里调用了一个Cleaner的create方法,且后台线程还会对虚引用的对象监测,如果虚引用的实际对象(这里是DirectByteBuffer)被回收以后,就会调用Cleaner的clean方法,来清除直接内存中占用的内存

public void clean() {
       if (remove(this)) {
           try {
               this.thunk.run(); //调用run方法
           } catch (final Throwable var2) {
               AccessController.doPrivileged(new PrivilegedAction<Void>() {
                   public Void run() {
                       if (System.err != null) {
                           (new Error("Cleaner terminated abnormally", var2)).printStackTrace();
                       }

                       System.exit(1);
                       return null;
                   }
               });
           }

对应对象的run方法

public void run() {
    if (address == 0) {
        // Paranoia
        return;
    }
    unsafe.freeMemory(address); //释放直接内存中占用的内存
    address = 0;
    Bits.unreserveMemory(size, capacity);
}

直接内存的回收机制总结

使用了Unsafe类来完成直接内存的分配回收,回收需要主动调用freeMemory方法
ByteBuffer的实现内部使用了Cleaner(虚引用)来检测ByteBuffer。一旦ByteBuffer被垃圾回收,那么会由ReferenceHandler来调用Cleaner的clean方法调用freeMemory来释放内存

三 垃圾回收

1.如何判断对象可以回收
引用计数法

弊端:循环引用时,两个对象的计数都为1,导致两个对象都无法被释放

可达性分析算法

JVM中的垃圾回收期通过可达性分析来探索所有存货的对象
扫描堆中的对象,看能否沿着GC Root对象为起点的引用链找到该对象,如果找不到,则表示可以回收
可以作为GC Root的对象
虚拟机栈(栈帧中的本地变量表)中引用的对象。 
方法区中类静态属性引用的对象
方法区中常量引用的对象
本地方法栈中JNI(即一般说的Native方法)引用的对象

物种引用

在这里插入图片描述
强引用
只有GC Root都不引用该对象时,才会回收强引用对象

如上图B、C对象都不引用A1对象时,A1对象才会被回收
软引用
当GC Root指向软引用对象时,在内存不足时,会回收软引用所引用的对象

如上图如果B对象不再引用A2对象且内存不足时,软引用所引用的A2对象就会被回收
软引用的使用

public class Demo1 {
	public static void main(String[] args) {
		final int _4M = 4*1024*1024;
		//使用软引用对象 list和SoftReference是强引用,而SoftReference和byte数组则是软引用
		List<SoftReference<byte[]>> list = new ArrayList<>();
		SoftReference<byte[]> ref= new SoftReference<>(new byte[_4M]);
	}
}

如果在垃圾回收时发现内存不足,在回收软引用所指向的对象时,软引用本身不会被清理

如果想要清理软引用,需要使用引用队列

public class Demo1 {
	public static void main(String[] args) {
		final int _4M = 4*1024*1024;
		//使用引用队列,用于移除引用为空的软引用对象
		ReferenceQueue<byte[]> queue = new ReferenceQueue<>();
		//使用软引用对象 list和SoftReference是强引用,而SoftReference和byte数组则是软引用
		List<SoftReference<byte[]>> list = new ArrayList<>();
		SoftReference<byte[]> ref= new SoftReference<>(new byte[_4M]);

		//遍历引用队列,如果有元素,则移除
		Reference<? extends byte[]> poll = queue.poll();
		while(poll != null) {
			//引用队列不为空,则从集合中移除该元素
			list.remove(poll);
			//移动到引用队列中的下一个元素
			poll = queue.poll();
		}
	}
}

大概思路为:查看引用队列中有无软引用,如果有,则将该软引用从存放它的集合中移除(这里为一个list集合)

弱引用
只有弱引用引用该对象时,在垃圾回收时,无论内存是否充足,都会回收弱引用所引用的对象

如上图如果B对象不再引用A3对象,则A3对象会被回收
弱引用的使用和软引用类似,只是将 SoftReference 换为了 WeakReference

虚引用
当虚引用对象所引用的对象被回收以后,虚引用对象就会被放入引用队列中,调用虚引用的方法

虚引用的一个体现是释放直接内存所分配的内存,当引用的对象ByteBuffer被垃圾回收以后,虚引用对象Cleaner就会被放入引用队列中,然后调用Cleaner的clean方法来释放直接内存
如上图,B对象不再引用ByteBuffer对象,ByteBuffer就会被回收。但是直接内存中的内存还未被回收。这时需要将虚引用对象Cleaner放入引用队列中,然后调用它的clean方法来释放直接内存

终结器引用
所有的类都继承自Object类,Object类有一个finalize方法。当某个对象不再被其他的对象所引用时,会先将终结器引用对象放入引用队列中,然后根据终结器引用对象找到它所引用的对象,然后调用该对象的finalize方法。调用以后,该对象就可以被垃圾回收了

如上图,B对象不再引用A4对象。这是终结器对象就会被放入引用队列中,引用队列会根据它,找到它所引用的对象。然后调用被引用对象的finalize方法。调用以后,该对象就可以被垃圾回收了

引用队列
软引用和弱引用可以配合引用队列

在弱引用和虚引用所引用的对象被回收以后,会将这些引用放入引用队列中,方便一起回收这些软/弱引用对象
虚引用和终结器引用必须配合引用队列

虚引用和终结器引用在使用时会关联一个引用队列

2、垃圾回收算法

标记-清除
定义:
标记清除算法顾名思义,是指在虚拟机执行垃圾回收的过程中,先采用标记算法确定可回收对象,然后垃圾收集器根据标识清除相应的内容,给堆内存腾出相应的空间
这里的腾出内存空间并不是将内存空间的字节清0,而是记录下这段内存的起始结束地址,下次分配内存的时候,会直接覆盖这段内存

缺点:容易产生大量的内存碎片,可能无法满足大对象的内存分配,一旦导致无法分配对象,那就会导致jvm启动gc,一旦启动gc,我们的应用程序就会暂停,这就导致应用的响应速度变慢

标记-整理
标记整理算法会将不被GC Root引用的对象回收,清除其占用的内存空间。然后整理剩余的对象,可以有效避免因内存碎片而导致的问题,但是因为整体需要消耗一定的时间,所以效率较低

复制
将内存分为等大小的两个取余,FROM和TO(TO中为空)。先将被GC Root引用的对象从FROM放入TO中,再回首不被GC Root引用的对象。然后交换FROM和TO。这样也可以避免内存碎片的问题,但是会占用双倍的内存空间。

3、分代回收

回收流程
新创建的对象都被放在了新生代的伊甸园中

当伊甸园中的内存不足时,就会进行一次垃圾回收,这时的回收叫做 Minor GC

Minor GC 会将伊甸园和幸存区FROM存活的对象先复制到 幸存区 TO中, 并让其寿命加1,再交换两个幸存区
再次创建对象,若新生代的伊甸园又满了,则会再次触发 Minor GC(会触发 stop the world, 暂停其他用户线程,只让垃圾回收线程工作),这时不仅会回收伊甸园中的垃圾,还会回收幸存区中的垃圾,再将活跃对象复制到幸存区TO中。回收以后会交换两个幸存区,并让幸存区中的对象寿命加1

如果幸存区中的对象的寿命超过某个阈值(最大为15,4bit),就会被放入老年代中
如果新生代老年代中的内存都满了,就会先触发Minor GC,再触发Full GC,扫描新生代和老年代中所有不再使用的对象并回收

GC 分析
大对象处理策略
当遇到一个较大的对象时,就算新生代的伊甸园为空,也无法容纳该对象时,会将该对象直接晋升为老年代

线程内存溢出
某个线程的内存溢出了而抛异常(out of memory),不会让其他的线程结束运行

这是因为当一个线程抛出OOM异常后,它所占据的内存资源会全部被释放掉,从而不会影响其他线程的运行,进程依然正常

4、垃圾回收器

相关概念
并行收集:指多条垃圾收集线程并行国祚,但此时用户线程仍处于等待状态。

并发收集:指用户线程与垃圾回收线程同时工作(不一定是并行的,可能会交替执行)。用户程序在继续运行,而垃圾收集程序运行在另外一个CPU上

吞吐量:即CPU用于运行用户代码的时间与CPU总消耗时间的比值(吞吐量 = 运行用户代码时间 / ( 运行用户代码时间 + 垃圾收集时间 )),也就是。例如:虚拟机共运行100分钟,垃圾收集器花掉1分钟,那么吞吐量就是99%

串行
单线程
内存较小,个人电脑(CPU核数较少)
安全点:让其他线程都在这个点停下来,以免垃圾回收时移动对象地址,使得其他线程找不到被移动的对象

因为是串行的,所以只有一个垃圾回收线程。且在该线程执行回收工作时,其他线程进入阻塞状态

Serial 收集器
Serial收集器是最基本的、发展历史最悠久的收集器

特点:单线程、简单高效(与其他收集器的单线程相比),采用复制算法。对于限定单个CPU的环境来说,Serial收集器由于没有线程交互的开销,专心做垃圾收集自然可以获得最高的单线程手机效率。收集器进行垃圾回收时,必须暂停其他所有的工作线程,直到它结束(Stop The World)

ParNew 收集器
ParNew收集器其实就是Serial收集器的多线程版本

特点:多线程、ParNew收集器默认开启的收集线程数与CPU的数量相同,在CPU非常多的环境中,可以使用-XX:ParallelGCThreads参数来限制垃圾收集的线程数。和Serial收集器一样存在Stop The World问题

Serial Old 收集器
Serial Old是Serial收集器的老年代版本

特点:同样是单线程收集器,采用标记-整理算法

吞吐量优先
多线程
堆内存较大,多核CPU
单位时间内,STW(stop the world,停掉其他所有工作线程)时间最短
JDK1.8默认使用的垃圾回收器
Parallel Scavenge 收集器
与吞吐量关系密切,故也称为吞吐量优先收集器

特点:属于新生代收集器也是采用复制算法的收集器(用到了新生代的幸存区),又是并行的多线程收集器(与ParNew收集器类似)

该收集器的目标是达到一个可控制的吞吐量。还有一个值得关注的点是:GC自适应调节策略(与ParNew收集器最重要的一个区别)

GC自适应调节策略:Parallel Scavenge收集器可设置-XX:+UseAdptiveSizePolicy参数。当开关打开时不需要手动指定新生代的大小(-Xmn)、Eden与Survivor区的比例(-XX:SurvivorRation)、晋升老年代的对象年龄(-XX:PretenureSizeThreshold)等,虚拟机会根据系统的运行状况收集性能监控信息,动态设置这些参数以提供最优的停顿时间和最高的吞吐量,这种调节方式称为GC的自适应调节策略。

Parallel Scavenge收集器使用两个参数控制吞吐量:

XX:MaxGCPauseMillis 控制最大的垃圾收集停顿时间
XX:GCRatio 直接设置吞吐量的大小
Parallel Old 收集器
是Parallel Scavenge收集器的老年代版本

特点:多线程,采用标记-整理算法(老年代没有幸存区)

响应时间优先
多线程

堆内存较大,多核CPU

尽可能让单次STW时间变短(尽量不影响其他线程运行)

https://blog.csdn.net/weixin_45269353/article/details/123795614?spm=1001.2014.3001.5502

什么是JVM内存结构

JVM将虚拟机分为5大取余,程序计数器,虚拟机栈,本地方法栈,java堆,方法区

程序计数器:线程私有的,是一块很小的内存空间,作为当前线程的行号指示器,用于记录当前虚拟机正在执行的线程指令地址;
虚拟机栈:线程私有的,每个方法执行的时候都会创建一个栈帧,用于存储局部变量表、操作数、动态链接和方法返回等信息,当线程请求的栈深度超过了虚拟机允许的最大深度时,就会抛出StackOverFlowError;
本地方法栈:线程私有的,保存的是native方法的信息,当一个jvm创建的线程调用native方法后,jvm不会在虚拟机栈中为该线程创建栈帧,而是简单的动态链接并直接调用该方法;
java堆:java堆是所有线程共享的一块内存,几乎所有对象的实例和数组都要在堆上分配内存,因此该区域经常发生垃圾回收的操作;
方法区:存放已被加载的类信息、常量、静态变量、即时编译器编译后的代码数据。即永久代,在jdk1.8中不存在方法区了,被元数据区替代了,原方法区被分成两部分;1:加载的类信息,2:运行时常量池;加载的类信息被保存在元数据区中,运行时常量池保存在堆中;

什么是JVM内存模型

Java 内存模型(下文简称 JMM)就是在底层处理器内存模型的基础上,定义自己的多线程语义。它明确指定了一组排序规则,来保证线程间的可见性。

这一组规则被称为 Happens-Before, JMM 规定,要想保证 B 操作能够看到 A 操作的结果(无论它们是否在同一个线程),那么 A 和 B 之间必须满足 Happens-Before 关系:

单线程规则: 一个线程中的每个动作都 happens-before 该线程中后续的每个动作
监视器锁定规则: 监听器的解锁动作 happens-before 后续对这个监听器的锁定动作
volatile 变量规则: 对 volatile 字段的写入动作 happens-before 后续对这个字段的每个读取动作
线程 start 规则: 线程 start() 方法的执行 happens-before 一个启动线程内的任意动作
线程 join 规则: 一个线程内的所有动作 happens-before 任意其他线程在该线程 join() 成功返回之前
传递性: 如果 A happens-before B, 且 B happens-before C, 那么 A happens-before C
怎么理解 happens-before 呢?如果按字面意思,比如第二个规则,线程(不管是不是同一个)的解锁动作发生在锁定之前?这明显不对。happens-before 也是为了保证可见性,比如那个解锁和加锁的动作,可以这样理解,线程1释放锁退出同步块,线程2加锁进入同步块,那么线程2就能看见线程1对共享对象修改的结果。

Java 提供了几种语言结构,包括 volatile, final 和 synchronized, 它们旨在帮助程序员向编译器描述程序的并发要求,其中:

volatile - 保证可见性和有序性
synchronized - 保证可见性和有序性; 通过管程(Monitor)\保证一组动作的原子性
final - 通过禁止在构造函数初始化和给 final 字段赋值这两个动作的重排序,保证可见性(如果 this 引用逃逸就不好说可见性了)
编译器在遇到这些关键字时,会插入相应的内存屏障,保证语义的正确性。

有一点需要注意的是,synchronized 不保证同步块内的代码禁止重排序,因为它通过锁保证同一时刻只有一个线程访问同步块(或临界区),也就是说同步块的代码只需满足 as-if-serial 语义 - 只要单线程的执行结果不改变,可以进行重排序。

所以说,Java 内存模型描述的是多线程对共享内存修改后彼此之间的可见性,另外,还确保正确同步的 Java 代码可以在不同体系结构的处理器上正确运行。

堆和栈有什么区别

(1)申请方式

stack:由系统自动分配。例如,声明在函数中一个局部变量 int b; 系统自动在栈中为 b 开辟空间

heap:需要程序员自己申请,并指明大小,在 c 中 malloc 函数,对于Java 需要手动 new Object()的形式开辟

(2)申请后系统的响应

stack:只要栈的剩余空间大于所申请空间,系统将为程序提供内存,否则将报异常提示栈溢出。

heap:首先应该知道操作系统有一个记录空闲内存地址的链表,当系统收到程序的申请时,会遍历该链表,寻找第一个空间大于所申请空间的堆结点,然后将该结点从空闲结点链表中删除,并将该结点的空间分配给程序。另外,由于找到的堆结点的大小不一定正好等于申请的大小,系统会自动的将多余的那部分重新放入空闲链表中。

(3)申请大小的限制

stack:栈是向低地址扩展的数据结构,是一块连续的内存的区域。这句话的意思是栈顶的地址和栈的最大容量是系统预先规定好的,在 WINDOWS 下,栈的大小是 2M(默认值也取决于虚拟内存的大小),如果申请的空间超过栈的剩余空间时,将提示 overflow。因此,能从栈获得的空间较小。

heap:堆是向高地址扩展的数据结构,是不连续的内存区域。这是由于系统是用链表来存储的空闲内存地址的, 自然是不连续的,而链表的遍历方向是由低地址向高地址。堆的大小受限于计算机系统中有效的虚拟内存。由此可见, 堆获得的空间比较灵活,也比较大。

(4)申请效率的比较

stack:由系统自动分配,速度较快。但程序员是无法控制的。

heap:由 new 分配的内存,一般速度比较慢,而且容易产生内存碎片,不过用起来最方便。

(5)heap和stack中的存储内容

stack:在函数调用时,第一个进栈的是主函数中后的下一条指令(函数调用语句的下一条可执行语句)的地址, 然后是函数的各个参数,在大多数的 C 编译器中,参数是由右往左入栈的,然后是函数中的局部变量。注意静态变量是不入栈的。

当本次函数调用结束后,局部变量先出栈,然后是参数,最后栈顶指针指向最开始存的地址,也就是主函数中的下一条指令,程序由该点继续运行。

heap:一般是在堆的头部用一个字节存放堆的大小。堆中的具体内容有程序员安排。

说一说对 OOM 的认识?以及如何排查 OOM 的问题?

除了程序计数器,其他内存区域都有 OOM 的风险。

栈一般经常会发生 StackOverflowError,比如 32 位的 windows 系统单进程限制 2G 内存,无限创建线程就会发生栈的 OOM
Java 8 常量池移到堆中,溢出会出 java.lang.OutOfMemoryError: Java heap space,设置最大元空间大小参数无效;
堆内存溢出,报错同上,这种比较好理解,GC 之后无法在堆中申请内存创建对象就会报错;
方法区 OOM,经常会遇到的是动态生成大量的类、jsp 等;
直接内存 OOM,涉及到 -XX:MaxDirectMemorySize 参数和 Unsafe 对象对内存的申请。
排查 OOM 的方法:

增加两个参数 -XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=/tmp/heapdump.hprof,当 OOM 发生时自动 dump 堆内存信息到指定目录;
同时 jstat 查看监控 JVM 的内存和 GC 情况,先观察问题大概出在什么区域;
使用 MAT 工具载入到 dump 文件,分析大对象的占用情况,比如 HashMap 做缓存未清理,时间长了就会内存溢出,可以把改为弱引用 。

谈一谈 JVM 中的常量池?

JVM常量池主要分为Class文件常量池、运行时常量池,全局字符串常量池,以及基本类型包装类对象常量池。

Class文件常量池: class文件是一组以字节为单位的二进制数据流,在java代码的编译期间,我们编写的java文件就被编译为.class文件格式的二进制数据存放在磁盘中,其中就包括class文件常量池。
运行时常量池: 运行时常量池相对于class常量池一大特征就是具有动态性,java规范并不要求常量只能在运行时才产生,也就是说运行时常量池的内容并不全部来自class常量池,在运行时可以通过代码生成常量并将其放入运行时常量池中,这种特性被用的最多的就是String.intern()。
全局字符串常量池: 字符串常量池是JVM所维护的一个字符串实例的引用表,在HotSpot VM中,它是一个叫做StringTable的全局表。在字符串常量池中维护的是字符串实例的引用,底层C++实现就是一个Hashtable。这些被维护的引用所指的字符串实例,被称作”被驻留的字符串”或”interned string”或通常所说的”进入了字符串常量池的字符串”。
基本类型包装类对象常量池: java中基本类型的包装类的大部分都实现了常量池技术,这些类是Byte,Short,Integer,Long,Character,Boolean,另外两种浮点数类型的包装类则没有实现。另外上面这5种整型的包装类也只是在对应值小于等于127时才可使用对象池,也即对象不负责创建和管理大于127的这些类的对象。

https://www.yuque.com/xuhaotian-nsxhn/bwzknf/yykx1y

java容器(集合)

java容器都有哪些

java容器分为Collection 和 Map 两大类,其下又有很多子类,如下所示:
Collection
list
ArrayList
LinkedList
vector
stack
Set
HashSet
linkedHashSet
TreeSet
Map
HashMap
LinkedHashMap
TreeMap
ConcurrentHashMap
Hashtable

Collection 和Collections有什么区别

● Collection 是一个集合接口,它提供了对集合对象进行基本操作的通用接口方法,所有集合都是它的子类,比如 List、Set 等。
● Collections 是一个包装类,包含了很多静态方法,不能被实例化,就像一个工具类,比如提供的排序方法: Collections.sort(list)。

List、Set、Map 之间的区别是什么?

List、Set、Map 的区别主要体现在两个方面:元素是否有序、是否允许元素重复。

三者之间的区别,如下表

在这里插入图片描述

如何决定使用 HashMap 还是 TreeMap?

对于在 Map 中插入、删除、定位一个元素这类操作,HashMap 是最好的选择,因为相对而言 HashMap 的插入会更快,但如果你要对一个 key 集合进行有序的遍历,那 TreeMap 是更好的选择。

说一下 HashMap 的实现原理?

HashMap 基于 Hash 算法实现的,我们通过 put(key,value)存储,get(key)来获取。当传入 key 时,HashMap 会根据 key.hashCode() 计算出 hash 值,根据 hash 值将 value 保存在 bucket 里。当计算出的 hash 值相同时,我们称之为 hash 冲突,HashMap 的做法是用链表和红黑树存储相同 hash 值的 value。当 hash 冲突的个数比较少时,使用链表否则使用红黑树。

说一下 HashSet 的实现原理?

HashSet 是基于 HashMap 实现的,HashSet 底层使用 HashMap 来保存所有元素,因此 HashSet 的实现比较简单,相关 HashSet 的操作,基本上都是直接调用底层 HashMap 的相关方法来完成,HashSet 不允许重复的值。

ArrayList 和 LinkedList 的区别是什么?

● 数据结构实现:ArrayList 是动态数组的数据结构实现,而 LinkedList 是双向链表的数据结构实现。
● 随机访问效率:ArrayList 比 LinkedList 在随机访问的时候效率要高,因为 LinkedList 是线性的数据存储方式,所以需要移动指针从前往后依次查找。
● 增加和删除效率:在非首尾的增加和删除操作,LinkedList 要比 ArrayList 效率要高,因为 ArrayList 增删操作要影响数组内的其他数据的下标。

综合来说,在需要频繁读取集合中的元素时,更推荐使用 ArrayList,而在插入和删除操作较多时,更推荐使用 LinkedList

如何实现数组和 List 之间的转换?

● 数组转 List:使用 Arrays.asList(array) 进行转换。
● List 转数组:使用 List 自带的 toArray() 方法。

// list to array
List<String> list = new ArrayList<String>();
list.add("王磊");
list.add("的博客");
list.toArray();
// array to list
String[] array = new String[]{"王磊","的博客"};
Arrays.asList(array);

ArrayList 和 Vector 的区别是什么?

● 线程安全:Vector 使用了 Synchronized 来实现线程同步,是线程安全的,而 ArrayList 是非线程安全的。
● 性能:ArrayList 在性能方面要优于 Vector。
● 扩容:ArrayList 和 Vector 都会根据实际的需要动态的调整容量,只不过在 Vector 扩容每次会增加 1 倍,而 ArrayList 只会增加 50%。

Array 和 ArrayList 有何区别?

● Array 可以存储基本数据类型和对象,ArrayList 只能存储对象。
● Array 是指定固定大小的,而 ArrayList 大小是自动扩展的。
● Array 内置方法没有 ArrayList 多,比如 addAll、removeAll、iteration 等方法只有 ArrayList 有

在 Queue 中 poll()和 remove()有什么区别?

● 相同点:都是返回第一个元素,并在队列中删除返回的对象。
● 不同点:如果没有元素 poll()会返回 null,而 remove()会直接抛出 NoSuchElementException 异常。

代码示例:

Queue<String> queue = new LinkedList<String>();
queue.offer("string"); // add
System.out.println(queue.poll());
System.out.println(queue.remove());
System.out.println(queue.size());

哪些集合类是线程安全的?

Vector、Hashtable、Stack 都是线程安全的,而像 HashMap 则是非线程安全的,不过在 jdk 1.5 之后随着 java.util.concurrent 并发包的出现,它们也有了自己对应的线程安全类,比如 HashMap 对应的线程安全类就是 ConcurrentHashMap。

迭代器 Iterator 是什么?

Iterator 接口提供遍历任何 Collection 的接口。我们可以从一个 Collection 中使用迭代器方法来获取迭代器实例。迭代器取代了 java 集合框架中的 Enumeration,迭代器允许调用者在迭代过程中移除元素。

Iterator 使用代码如下:

List<String> list = new ArrayList<>();
Iterator<String> it = list.iterator();
while(it.hasNext()){
  String obj = it.next();
  System.out.println(obj);
}

Iterator 的特点是更加安全,因为它可以确保,在当前遍历的集合元素被更改的时候,就会抛出 ConcurrentModificationException 异常。

.Iterator 和 ListIterator 有什么区别?

● Iterator 可以遍历 Set 和 List 集合,而 ListIterator 只能遍历 List。
● Iterator 只能单向遍历,而 ListIterator 可以双向遍历(向前/后遍历)。
● ListIterator 从 Iterator 接口继承,然后添加了一些额外的功能,比如添加一个元素、替换一个元素、获取前面或后面元素的索引位置。

怎么确保一个集合不能被修改?

可以使用 Collections.unmodifiableCollection(Collection c) 方法来创建一个只读集合,这样改变集合的任何操作都会抛出 java.lang.UnsupportedOperationException 异常。

List<String> list = new ArrayList<>();
list.add("x");
Collection<String> clist = Collections.unmodifiableCollection(list);
clist.add("y"); // 运行时此行报错
System.out.println(list.size());

面向过程:
强调的是功能行为,以函数为最小单位,考虑怎么做

面向对象:
强调具备功能的对象,以类/对象为最小单位,考虑谁来做

面向对象的两个要素
类:对一类事物的描述,是抽象的,概念上的定义
对象:是实际存在的该类事务的每个个体,因而也称为实例

可以理解为
类代指人这一类事务
实例代指具体的人,例如乔布斯,马云等
面向对象程序设计的重点是类的设计
类的设计,其实就是类的成员的设计

一,设计类其实就是设计类的成员

属性=成员变量=field=域,字段
方法= 成员方法=函数=method

创建类的对象=类的实例化=实例化类

二,类和对象的使用(面对对象思想落地的实现)
1.创建类,设计类的成员(包括属性和方法)
2.创建类的对象(即new一个对象,例如p1)
3.通过“对象.属性”或“对象.方法”调用对象的结构

class Person{
	String name;
	int age;
	boolean is Married;
	//以上三行称为属性,或者叫成员变量
	
	public void walk(){
		System.out.println("人走路")
	}
	public String display(){
		return "名字是:"name+",年龄是"+age+",married"+isMarried
	}
	//以上两者称为方法或者函数

	{
		name ="HanMeiMei";
		age = 17;
		isMarried =true;
	}
		//以上称为代码块
	class pet{
		String name;
		float weight;
	}
	//以上称为内部类
}
//测试类
public class PersonTest{
	public static void main(String[] args){
		//创建Person类的对象
		Person p1 = new Person();
		//Scanner scanner =new Scanner(System.in);

		//调用对象的结构:属性,方法
		//调用属性:"对象.属性"
		p1.name ="Tom";
		p1.isMale =true;
		System.out.println(p1.name);
		//调用p1对象的name属性,了打印结果为Tom,这就是调用属性的用法
		
		Person p2 =new Person();
		System.out.println(p2.name);//null		
		//在这里我们再次输出p2.name,结果为null,是因为我们没有定义p2的name属性
		//故可以总结,如果创建了一个类的多个对象,则每个对象都独立的拥有一套属性
		Person p3 =p1;
		System.out.println(p3.name);
		//输出Tom,证明对象可以赋予 

		//调用方法:“对象.方法”
		p1.eat();
		p1.sleep();
		p1.talk("Chinese");
	}
}

class Person{

	//属性
	String name;
	int age =1;
	boolean isMale;

	//方法
	public void eat(){
		System.out.printf("人可以吃饭");
	}

	public void sleep(){
		System.out.println("人可以睡觉");
	}
	public void talk(String language){
		System.out.println("人可以说话,使用的是:"+language);
	}

}

重载的概念
在同一个类中,允许存在一个以上的同名方法,只要他们的参数个数或者参数类型不同即可。
例如
sort(char[] a)
sort(double[] a)
sort(float[] a)
sort(int[] a)
但需要注意的是,跟方法的权限修饰符,返回值类型,形参变量名,方法体都没有关系

为什么需要重载

如下的两个同名方法构成了重载
//反转数组

public void reverse(int[] arr){
	for(int i = 0;i<arr.length/2;i++){
		int temp =arr[i];
		arr[i] =arr[arr.length - i -1];
		arr[arr.length- i -1]= temp;
	}
}

public void reverse(String[] arr){

}

同样的还有

public class OverLoadTest{
	
	public void getSum(int i,int j){
		System.out.println(i+j);
	}

	public void getSum(double d1,double d2){
		
	}
	public void getSum(String s ,int i){
		
	}
}

类的结构之三:构造器(或构造方法,constructor)的使用

创建对象

public class PersonTest{
	public static void main(String[] args){
		//创建类的对象:new+构造器
		Person p = new Person();
		p.eat();
	}
}

JavaBean

javaBean是一种Java语言写成的可重用组件

所谓javaBean,是指符合如下标准的Java类
1.类是公共的
2.有一个无参的公共的构造器
3.有属性,且有对应的get,set方法

用户可以使用javaBean将功能,处理,值,数据库访问和其他任何可以用Java代码创造的对象进行打包,并且其他的开发者可以通过内部的JSP页面,Servlet,其他JavaBean,applet程序或者应用来使用这些对象。用户可以认为JavaBean提供了一种随时随地的赋值和粘贴的功能,而不用关心任何改变。

方法的重写

定义:在子类中可以根据需要对从父类中继承来的方法进行改造,即为重写
在程序执行时,子类的方法将覆盖父类的方法

要求:
1.子类重写的方法必须和父类被重写的方法具有相同的方法名称,参数列表
2.子类重写的方法的返回值不能大于父类被重写的方法的返回值类型
3.子类重写的方法使用的访问权限不能小于父类被重写的方法的访问权限
子类不能重写父类中声明为private权限的方法
4.子类方法抛出的异常不能大于父类被重写方法的异常

注意:
子类与父类中同名同参数的方法必须同时声明为非static的(即为重写)。或者同时声明为static的(不是重写),因为static方法是属于类的,子类无法覆盖父类的方法。

IO流

输入输出(IO)是指计算机同任何外部设备之间的数据传递。常见的输入输出设备有文件,键盘,打印机,屏幕等。数据可以按记录(或称数据库)的方式传递,也可以流的方式传递。

* IO流:
*          流向
*                  输入流 读数据  FileReader  Reader
*                  输出流 写数据  FileWriter  Writer
*          数据类型
*                  字节流
*                          字节输入流   读数据  InputStream
*                          字节输出流   写数据  OutputStream
*                  字符流
*                          字符输入流   读数据  Reader
*                          字符输出流   写数据  Writer

字节流传输需要刷新缓冲区,字符流无需刷新缓冲区,直接转换

● 二进制文件只能使用字节流进行复制
● 文本文件可以用字节流也可以用字符流进行复制

字节流

InputStream
|-- FileInputStream (基本文件流)
|-- BufferedInputStream
|-- DataInputStream

|-- ObjectInputStream

字符流

Reader
|-- InputStreamReader (byte->char 桥梁)
|-- BufferedReader (常用)
Writer
|-- OutputStreamWriter (char->byte 桥梁)
|-- BufferedWriter
|-- PrintWriter (常用)

基础八股知识

Collection
List-----有序可重复列表
Set------不可重复链表
Queue/Deque
BlockQueue

Map
HashMap
ConcrrrntHashMap
TreeMap,LinkedHashMap,HashTable
Iterator迭代器

面试八股真题

1、简述ArrayList和LinkedList的区别

2、HashMap和HashTable的区别

3、Collection包结构,与Collections的区别

4、有没有可能两个不相等的对象有相同的hashcode

5、说说List,Set,Map三者的区别?

6、HashMap 中的 key 我们可以使用任何类作为 key 吗?

7、HashMap 的长度为什么是 2 的 N 次方呢?

8、HashMap 与 ConcurrentHashMap 的异同

9、红黑树有哪几个特征?

Collection

1.1 List——有序可重复列表

Arraylist

1.底层是由数组实现的,重要的成员有元素数组/数组中元素数size/modeCount修改子树
2.进行add添加时,若没有指定添加位置,就会根据size确定添加的新元素在数组中的下标进行添加,若size>=数组长度,则需要扩容
3.在进行扩容时,会将 modCount++,并且会将原数组中的元素,拷贝至新数组中,新数组的大小是原数组的 1.5 倍,默认的初始容量是 0,且在第一次添加元素时,设置数组大小为 10
4. 若指定 i 为添加元素的位置,会调用 System.ArrayCopy 方法对 i 下标以后的元素进行拷贝,拷贝完成后再把添加的元素放在 i 位置
5.调用 get 方法时,由于ArrayList 底层基于数组,因此实现了一个 RandomAccess 标识接口,表示按下标读取数据速度很快,主要是由于数组在内存中是连续存储,可以通过第一个元素的存储地址和偏移量offset直接计算得到访问的元素的存储地址
5. 若 contains 方法是通过元素的值进行查找,则需要遍历数组

LinkedList

LinkedList是基于链表存储的,链表中结点Node主要包含存储的元素的引用E,指向前驱节点指针prev和指向后继结点的指针next,LinkedList中重要成员主要有size/头结点,first/尾结点last
LinkedList 由于 Node 在内存中是分散存储的,因此没有对 size 限制,在执行 add 方法时,默认添加在链表的尾部
若指定添加元素在链表中的位置,LinkedList 需要调用 node 方法,传入参数 Index 找到对应的节点,在查找对应的节点时,若 Index < size / 2,从头节点向后遍历,若 Index > size / 2,从尾节点向前遍历,因此即使添加元素只需要改变节点的指针,但查找添加的位置的耗时仍然不小,开销不比 ArrayList 少
调用 get 方法时,若按下标查找,也需要进行同样的遍历,性能比 ArrayList 差很多,按 contains 进行查找时,也需要进行遍历,与 ArrayList 相比半斤八两
 链表节点除了元素值,还需要维护两个指针,导致 LinkedList 存储的内存开销远大于 ArrayList

Vector & Stack

Vector 也是基于数组进行存储,其原理与 ArrayList 类似,区别在于,Vector 中会维护一个 CapacityIncrement变量,每一次进行扩容时,只增加一个 CapacityIncrement 变量的大小,若没有指定 CapacityInc,默认为 0,且在扩容时,若 CapacityInc 为 0,则将数组大小翻倍;

Vector 中的所有增删改查方法,包括 get 方法,都通过在方法上加 Sychronized 锁方式锁住 Vector 对象,实现线程安全,性能较差。

1.2 Set——不可重复列表
HashSet

内部有一个成员变量的 HashMap,实际上就是使用的 HashMap 的方法

TreeSet

内部有一个成员变量的 TreeMap,底层就是 TreeMap

LinkedHashSet

底层调用的 HashSet 的方法,HashSet 中的 HashMap 也可以是 LinkedHashMap,因此实际是由 LinkedHashMap 实现

1.3 Queue / Dequeue
LinkedList 实现 Queue 和 Deque

LinkedList 中使用 first 和 last 两个节点的指针用来标记链表的头/尾节点,根据源码,pop应该返回first节点,pop和poll对返回节点为空处理不同,poll直接返回null,pop抛出异常

ArrayDeque

数组的方式实现了双端队列,主要是使用 head 和 tail 两个指针来标记 队列头/栈底 和 队列尾/栈顶

PriorityQueue

① 使用数组的方式实现了优先队列,内部其实是一个二叉堆结构(小顶堆),原理逻辑与堆排序一致

② 重要属性有 size,用来标记数组中元素的个数,即新添加的元素的下标

③ 执行 offer 方法时,先确认 size 是否大于等于数组长度,若大于等于则进行扩容,执行 siftup 方法

④ siftup 方法中,若制定了 Comparator,按照 Comparator 的 compare 方法与父节点比较,也就是 (size - 1) >>> 1 下标对应的元素进行堆的插入,若没有指定,按元素实现的 Comparable 接口的 compareTo 方法进行比较

⑤ 执行 poll() 方法时,若 size 为 0,返回空值,否则返回数组中下标为 0 的堆顶元素,将数组下标 0 位置替换为 size - 1 位置的元素,并减小 size,调用 siftdown 方法整理堆

⑥ siftdown 方法同样也分为根据 Comparator 和 Comparable 两种比较,主要是对 0 位置新换上来的元素与子节点进行比较,将其与最大的并大于 0 位置的子节点进行交换,并循环直到将其放到合适的位置

BlockQueue——阻塞队列,详见并发

2 MAP
2.1 HashMap
重要成员变量

① 负载因子:默认为 0.75,表示 HashMap 存储的元素数到达 HashMap 散列数组长度的 0.75 时就进行扩容

负载因子越大,散列数组的内存利用率越高,但哈希冲突概率也越高,查询效率相对降低;

负载因子越小,散列数组的内存利用率越低,但哈希冲突概率越低,查询效率相对较高

② 散列数组:HashMap 通过分离链表法解决哈希冲突的问题,在散列数组中存储的就是链表中的一个个头节点

③ K-V 键值对:K-V 键值对以 Node 节点的方式进行存储,Node 继承自 Map.Entry,包含 Next 指针,Key,Value 和 hash

④ threshold 门限,就是 负载因子 * 数组容量

⑤ size:HashMap 中当前存储的节点数

put方法流程:

① 再哈希:首先通过key获取到hashCode值,再对hashCode值右移16位,最后再与原hashCode值做异或运算。

#目的:在对散列数组长度取余时,高位通常不参与运算,再哈希让高位参与哈希运算,让低位尽量不同,使哈希分布更均匀

② 检查散列数组是否为空,若为空,初始化散列数组,数组容量将会初始化为16

③ 若 Key 的 HashCode 对应的散列数组位置上节点为空,则直接将新节点加入

④ 若不为空,比较链表的头节点 Key 与 put Key 是否相等,先比较 hashcode,再比较(== || equals),若相等,进行更新,若不相等,顺着链表按同样的逻辑进行比较

⑤ 若是 1.8 的HashMap,会先判断链表的头节点是否是 TreeNode,若是,则按照红黑树的逻辑进行插入

⑥ 1.7 的 HashMap 中,若遍历完链表仍未找到 Key 相同的节点,则将新节点加入到链表头,会造成并发扩容的死链问题,1.8 中,将头插法改为了尾插法,解决了死链问题,但并发环境下的其他基本问题,如更新丢失等,仍然没有解决

1.8 以前一直采用头插法的是由于计算机的局部性原理,主要是时间局部性原理,即一个最近被访问过的对象,很有可能很快会被再访问到,基于该假设,在头节点插入,可以有效的提高查询的效率,在发生并发问题时,完全可以使用 concurrent map来解决

HashMap的线程不安全带来了循环链表问题 可以使用Collections中的SynchronizedMap,concurrentHashMap,HashTable解决,推荐使用ConcurrentHashMap

⑦ 若添加后 size >= threshold,就会进行扩容

扩容流程

① 首先 HashMap 的初始容量是 16,并且每次对原数组长度 * 2 进行扩容,当 size > threshold 时就会进行扩容

HashMap 每次 * 2 的原因:1)2 的幂次可以用 & 的方式进行取余运算,效率更高;2)在扩容移动链表节点时,节点在新数组中的位置只可能是原位置 i 或 i + oldCap 旧数组长度,扩容时效率更高

② 创建新的散列数组,并将旧的散列数组中的链表的头节点拷贝至新数组中,在移动时,节点的 hash 与 oldCap进行 & 运算,若结果为 0,表示在新数组中位置不变,若不为 0,表示在新数组中位置为 i + oldCap

③ 在移动节点时,会使用一个 loHead/loTail 和 hiHead/hiTail 分别指向新数组中位置 i 和 i + oldCap 的链表的头尾节点,用一个 next 指针指向当前正在移动节点的下一个节点,在 1.8 中,由于使用尾插法,每一次移动节点都会添加至 loTail/hiTail之后,不会发生死链问题,而在 1.7 中,由于使用头插法,每一次移动节点都会添加至 head 之前,会发生扩容死链问题

④ 1.7 扩容死链问题:t1 线程正在移动 A 节点,next 节点指向 A 节点的下一个节点 B,即 A -> B,此时 t2 线程完成了 A 和 B 的移动,此时 B.next = A,在 t1 移动 A 到新数组中后,当前正在移动节点 e 指向 B,next 指向 B.next = A,此时就形成了链表中的环,最终导致程序崩溃

1.8 红黑树

① 与AVL树比较

增删性能,红黑树较好; AVL 树通过节点的左右子树高度差是否大于 1 判断是否平衡,若不平衡,需要通过单/双旋转恢复平衡,而红黑树主要根据节点颜色和节点到子孙节点路径上黑节点数量判断平衡,可以通过改变节点颜色或者旋转恢复平衡,开销较小

查询性能,AVL 树较好;AVL 树的最大高度为 1.44 * log(N + 2),一般情况只比 log(N) 稍大,红黑树由于平衡判断更加宽松,由着色法则得到的最大高度为 2 * log(N + 1),因此性能会稍微差于 AVL 树

内存空间消耗,红黑树稍大;

为什么不用跳表; ① 跳表的实现更加灵活简单,并且在范围查找上非常方便,Redis 中 ZSet 就使用跳表 + 散列的结构 ② 红黑树在空间复杂度上更胜一筹,通常发生了红黑树化的情况,都是数据量非常非常大时,此时红黑树空间占用小的优点将更加明显,并且 HashMap 的元素本身是无序的,即用不到跳表范围查找的优势

② 特点

每个节点不是黑色就是红色,根节点是黑色

红色节点的子节点必须是黑色

从一个节点到每一个子孙节点的路径上的黑色节点数必须相同

③ HashMap 中什么时候树化/树退化

当链表长度达到 8 时进行树化;在 HashMap 源码注解中有解释为什么是 8,大概意思是,红黑树的节点大小大约是正常节点大小的两倍,并且当节点数较少时链表与红黑树的查找效率差距可以忽略不记,在理想情况下,使用 0.75 作为负载因子,采用随机哈希算法,树化阈值与树化概率遵循泊松分布,选择 8 的概率是千万分之 6,7 的概率约是十万分之 1

当扩容后链表长度小于等于 6 进行树的退化;红黑树的节点大小大约是正常节点大小的两倍,并且当节点数较少时链表与红黑树的查找效率差距可以忽略不记,并且在插入和删除时维护红黑树的平衡性需要额外的开销

2.2 ConcurrentHashMap
1.7之前

① 通过分段锁 Segment 保证线程安全,Segment 继承自 ReentrantLock,每一个 Segment 中都包含一个 volatile 的 HashEntry 数组,一个 volatile 的 count 表示当前 Segment 中存储的 K-V 节点数,一个 Threshold 表示扩容阈值

② HashEntry 是基本存储单位,与 HashMap 中的Entry 基本一样,包含 Key,Value,next,hash,区别在于 HashEntry 的 Value 是 volatile 的,因此对 value 的修改对所有线程可见,并且 next 指针是 final 修饰,防止扩容时发生死链

③ ConcurrentHashMap 默认有一个长度为 16 的 Segment 数组,在进行增删改查时,会根据 Key 对 16 取余得到具体处于哪个 Segment,对于 get 操作不加锁执行,对于 put 等修改操作,调用 Segment 的 lock 方法锁住当前段进行修改,不影响其他段的并发操作,在进行扩容时,也仅是对单个 Segment 进行扩容

1.8

④ 1.8 的 ConcurrentHashMap 摒弃了 Segment,采用 CAS 和类似于分段锁的方式保证线程安全;

⑤ 大体结构与 HashMap 一样,在 put 操作时,若 Node 数组对应下标处为空,使用 CAS 的方式不加锁添加节点,若数组中当前位置链表头节点不为空,则对链表头节点加锁,在 Sychronized 同步代码块中执行与 HashMap 同样的逻辑;

⑥ ConcurrentHashMap 在扩容时的移动原数组节点到新数组的操作,可以由多个线程并发完成,从而大大提高效率,在进行移动时,会将当前线程正在移动的头节点包装为一个 ForwardNode,用来标识当前位置正在移动,当其他线程遍历到数组中的 ForwardNode节点时,就会忽略并继续遍历,从而保证线程安全,并且在扩容时,仍然可以执行增删改查操作,这些操作通过头节点的 hash 判断是否是一个 ForwardNode 节点,若是,则调用 helpTransfer 方法,并发的帮助完成扩容动作,完成后再回到循环中执行 put;

⑦ get 方法也是通过 Unsafe 的 getObjectVolatile 进行读取,不加锁

2.3 TreeMap
底层是红黑树,不展开讲解

2.4 LinkedHashMap
采用 HashMap 来存储节点,用一个双向链表存储 HashMap 的 Node 节点使其有序

2.5 HashTable
使用 Sychronized 锁住方法来保证线程安全,并且默认初始容量为 11,每一次扩容为 oldCap * 2 + 1

  1. Iterator迭代器
    ① 用来进行集合的遍历,不同的集合有不同的实现,例如 ArrayList 通过一个 cursor 指向下一个遍历的元素的下标,用一个 lastRet 表示上一个遍历的元素的下标;

② Iterator 接口中主要包含三个基本方法,hasNext 判断是否还有下一个元素,next 表示遍历获取下一个元素,remove 表示删除元素;

③ 常见的三种遍历方式

fori,foreach,Iterator

其中 fori 只能用于知道集合中具体元素数量时,fori 和 foreach 只能用于知道集合中元素具体类型时,foreach 底层就是 Iterator 的方式遍历

④ 三种遍历方式的删除

fori:在 fori 进行删除时,问题在于删除当前下标会导致之后的元素前移,比如 “A,B,B,C,C”,在 fori 中判断若元素等于 B 就删除,则会导致 i = 1 时,进行删除后,i = 2 位置的 B 到达 1 的位置,即 “A,B,C,C”,而此时 i 已经到达 2 的位置,即 i 指向 C,从而造成漏删,可以通过删除后让 i - 1,或者反向遍历删除解决

foreach:对于 ArrayList 等迭代器是 fail-fast 的集合,在遍历时底层是使用迭代器进行遍历,但在删除元素时,由于没有显式的获取迭代器,因此调用的是 List 的 remove 方***触发迭代器的 fail-fast 机制,抛出异常中断程序

Iterator:没有任何问题

⑤ fail-fast:

迭代器实现了 fail-fast 机制的集合,在集合中都会维护一个 modCount 成员变量,用来表示对集合的修改次数,在获取迭代器时,在迭代器中会为一个 ExpectedModCount 变量赋值为当前的 modCount,在执行 next,remove,hasnext方法之前,会先对迭代器的 ExpectedModCount 与集合的 modCount 进行比较,若不相等直接抛出异常

⑥ fail-safe:

Java 中实现 fail-safe 迭代器的集合主要都是通过 Copy-On-Write 方法实现的,例如 CopyOnWriteList,其内部有一个 ReentrantLock 锁对象,在进行增删改操作时,先加锁,将原有的数组拷贝一份,在新的数组上进行修改,而获取迭代器时,会用一个 final 变量指向当前的数组,在旧的数组上进行遍历

二、面试八股真题🎈🎈🎈
1、简述ArrayList和LinkedList的区别
① Array(数组)是基于索引(index)的数据结构,它使用索引在数组中搜索和读取数据是很快的。

Array获取数据的时间复杂度是O(1),但是要删除数据却是开销很大,因为这需要重排数组中的所有数据, (因为删除数据以后, 需要把后面所有的数据前移)

缺点: 数组初始化必须指定初始化的长度, 否则报错

例如
1
2
3
int[] a = new int[4]; //推荐使用int[] 这种方式进行初始化

int c[] = {12,34,56,78}; //长度:4,索引范围:[0,3]
② List—是一个有序的集合,可以包含重复的元素,提供了按索引访问的方式,它继承Collection。List有两个重要的实现类:ArrayList 和 LinkedList

ArrayList: 可以看作是能够自动增长容量的数组

ArrayList的toArray方法返回一个数组

ArrayList的asList方法返回一个列表

ArrayList底层的实现是Array, 数组扩容实现

LinkedList 是一个双链表,在添加和删除元素时具有比ArrayList更好的性能.但在get与set方面弱于 ArrayList.当然,这些对比都是指数据量很大或者操作很频繁。

③ 区别

ArrayList

优点:ArrayList 是实现了基于动态数组的数据结构,因为地址连续,一旦数据存储好了,查询 操作效率会比较高(在内存里是连着放的)。

缺点:因为地址连续,ArrayList 要移动数据,所以插入和删除操作效率比较低。

LinkedList

优点:LinkedList 基于链表的数据结构,地址是任意的,所以在开辟内存空间的时候不需要等 一个连续的地址。对于新增和删除操作,LinkedList 比较占优势。LinkedList 适用于要头尾操 作或插入指定位置的场景。

缺点:因为 LinkedList 要移动指针,所以查询操作性能比较低。

④ 适用场景分析:

当需要对数据进行对随机访问的时候,选用 ArrayList。

当需要对数据进行多次增加删除修改时,采用 LinkedList。

2、HashMap和HashTable的区别
① 两者父类不同

HashMap是继承自AbstractMap类,

Hashtable是继承自Dictionary类。

不过它们都实现了同时 实现了map、Cloneable(可复制)、Serializable(可序列化)这三个接口。

② 对外提供的接口不同

Hashtable比HashMap多提供了elments() 和contains() 两个方法。

elments() 方法继承自ashtable的父类Dictionnary。elements() 方法用于返回此Hashtable中的value的枚举。

contains()方法判断该Hashtable是否包含传入的value。它的作用与containsValue()一致。事实上,contansValue() 就只是调用了一下contains() 方法。

③ 对null的支持不同

Hashtable:key和value都不能为null。

HashMap:key可以为null,但是这样的key只能有一个,因为必须保证key的唯一性;可以有多个key值对应的value为null。

④ 安全性不同

HashMap是线程不安全的,在多线程并发的环境下,可能会产生死锁等问题,因此需要开发人员自 己处理多线程的安全问题。

Hashtable是线程安全的,它的每个方法上都有synchronized 关键字,因此可直接用于多线程中。

虽然HashMap是线程不安全的,但是它的效率远远高于Hashtable,这样设计是合理的,因为大部 分的使用场景都是单线程。当需要多线程操作的时候可以使用线程安全的ConcurrentHashMap。

ConcurrentHashMap虽然也是线程安全的,但是它的效率比Hashtable要高好多倍。因为ConcurrentHashMap使用了分段锁,并不对整个数据进行锁定。

⑤ 初始容量大小和每次扩充容量大小不同

⑥ 计算hash值的方法不同

3、Collection包结构,与Collections的区别
Collection是集合类的上级接口,子接口有 Set、List、LinkedList、ArrayList、Vector、Stack、 Set;

Collections是集合类的一个帮助类, 它包含有各种有关集合操作的静态多态方法,用于实现对各种 集合的搜索、排序、线程安全化等操作。此类不能实例化,就像一个工具类,服务于Java的 Collection框架。

4、有没有可能两个不相等的对象有相同的hashcode
有可能

在产生hash冲突时,两个不相等的对象就会有相同的 hashcode 值.当hash冲突产生时,一般 有以下几种方式来处理:

拉链法:每个哈希表节点都有一个next指针,多个哈希表节点可以用next指针构成一个单向链

表,被分配到同一个索引上的多个节点可以用这个单向链表进行存储。

开放定址法:一旦发生了冲突,就去寻找下一个空的散列地址,只要散列表足够大,空的散列地址总 能找到,并将记录存入 List iniData = new ArrayList<>()

再哈希:又叫双哈希法,有多个不同的Hash函数.当发生冲突时,使用第二个,第三个….等哈希函数 计算地址,直到无冲突。

5、说说List,Set,Map三者的区别?
List(对付顺序的好帮手): List接口存储一组不唯一(可以有多个元素引用相同的对象),有序 的对象

Set(注重独一无二的性质): 不允许重复的集合。不会有多个元素引用相同的对象。

Map(用Key来搜索的专家): 使用键值对存储。Map会维护与Key有关联的值。两个Key可以引 用相同的对象,但Key不能重复,典型的Key是String类型,但也可以是任何对象。

6、HashMap 中的 key 我们可以使用任何类作为 key 吗?
平时可能大家使用的最多的就是使用 String 作为 HashMap 的 key,但是现在我们想使用某个自定 义类作为 HashMap 的 key,那就需要注意以下几点:

如果类重写了 equals 方法,它也应该重写 hashCode 方法。

类的所有实例需要遵循与 equals 和 hashCode 相关的规则。

如果一个类没有使用 equals,你不应该在 hashCode 中使用它。

咱们自定义 key 类的最佳实践是使之为不可变的,这样,hashCode 值可以被缓存起来,拥有更好的性能。不可变的类也可以确保 hashCode 和 equals 在未来不会改变,这样就会解决与 可变相关的问题了。

7、HashMap 的长度为什么是 2 的 N 次方呢?
为了能让 HashMap 存数据和取数据的效率高,尽可能地减少 hash 值的碰撞,也就是说尽量把数据能均匀的分配,每个链表或者红黑树长度尽量相等。我们首先可能会想到 % 取模的操作来实现

取余(%)操作中如果除数是 2 的幂次,则等价于与其除数减一的与(&)操作(也就是说 hash % length == hash &(length - 1) 的前提是 length 是 2 的 n 次方)。并且,采用二进 制位操作 & ,相对于 % 能够提高运算效率。

8、HashMap 与 ConcurrentHashMap 的异同
① 都是 key-value 形式的存储数据;

② HashMap 是线程不安全的,ConcurrentHashMap 是 JUC 下的线程安全的;

③ HashMap 底层数据结构是数组 + 链表(JDK 1.8 之前)。JDK 1.8 之后是数组 + 链表 + 红黑 树。当链表中元素个数达到 8 的时候,链表的查询速度不如红黑树快,链表会转为红黑树,红 黑树查询速度快;

④ HashMap 初始数组大小为 16(默认),当出现扩容的时候,以 0.75 * 数组大小的方式进行扩 容;

⑤ ConcurrentHashMap 在 JDK 1.8 之前是采用分段锁来现实的 Segment + HashEntry, Segment 数组大小默认是 16,2 的 n 次方;JDK 1.8 之后,采用 Node + CAS + Synchronized 来保证并发安全进行实现。

9、红黑树有哪几个特征?
每个节点都是黑色或者红色

根节点是黑色

每个叶子节点都是黑色(指向空的叶子节点)

如果一个叶子节点是红色,那么其子节点必须都是黑色的

从一个节点到该节点的子孙节点的所有路径上包含相同数目的黑节点

https://blog.csdn.net/weixin_43884234/article/details/115056812?ops_request_misc=%257B%2522request%255Fid%2522%253A%2522162907817716780262531897%2522%252C%2522scm%2522%253A%252220140713.130102334…%2522%257D&request_id=162907817716780262531897&biz_id=0&utm_medium=distribute.pc_search_result.none-task-blog-2blogtop_positive~default-2-115056812.pc_v2_rank_blog_default&utm_term=%E5%8F%8D%E5%B0%84&spm=1018.2226.3001.4450

什么是反射

反射是java的特征之一,它允许运行中的Java程序对自身进行检查
反射十分强大,它甚至能直接操作程序的私有属性。
我们在之前学习过,被private封装的资源只能内部访问,外部是不行的,但是这个规则会被反射打破
反射可以在运行时获取一个类的所有信息,可以获取到任何定义的信息(包括成员变量,成员方法,构造器等),并且可以提供操作类的字段,方法,构造器等部分。

为什么需要反射

如果向创建对象,我们直接new User(),不是很方便么,为什么要通过反射去创建对象呢?

假设我们直接new User()的话,其中的细节步骤我们都需要自己把控,就像我们自己做菜,需要做到运输屠宰烹饪上菜,这是很麻烦的
而我们使用反射,就可以做到直接调用,省略了很多步骤,让其中的内部细节交给更专业的人去做

我们在后面的学习中,会学习框架,有一个框架叫做spring,就是一个功能非常强大的产品,可以帮助我们创建对象,管理对象。以后我无需手动new对象,直接从Spring提供的容器中的Beans获取即可。Beans底层就是一个Map<String,Object>,最终通过getBean(“user”)来获取。而这其中最核心的实现就是利用反射技术。

总结一句,类不是你创建的,是你同事或者直接是第三方公司,此时你要或得这个类的底层功能调用,就需要反射技术实现。

反射需要用到的API

class.forName(“类的全路径”)
类名.class
对象.getClass();

常用方法

获取包名 类名
clazz.getPackage().getName()//包名
clazz.getSimpleName()//类名
clazz.getName()//完整类名

获取成员变量定义信息
getFields()//获取所有公开的成员变量,包括继承变量
getDeclaredFields()//获取本类定义的成员变量,包括私有,但不包括继承的变量
getField(变量名)
getDeclaredField(变量名)

获取构造方法定义信息
getConstructor(参数类型列表)//获取公开的构造方法
getConstructors()//获取所有的公开的构造方法
getDeclaredConstructors()//获取所有的构造方法,包括私有
getDeclaredConstructor(int.class,String.class)

获取方法定义信息
getMethods()//获取所有可见的方法,包括继承的方法
getMethod(方法名,参数类型列表)
getDeclaredMethods()//获取本类定义的的方法,包括私有,不包括继承的方法
getDeclaredMethod(方法名,int.class,String.class)

反射新建实例
clazz.newInstance();//执行无参构造创建对象
clazz.newInstance(666,”海绵宝宝”);//执行含参构造创建对象
clazz.getConstructor(int.class,String.class)//获取构造方法

反射调用成员变量
clazz.getDeclaredField(变量名);//获取变量
clazz.setAccessible(true);//使私有成员允许访问
f.set(实例,值);//为指定实例的变量赋值,静态变量,第一参数给null
f.get(实例);//访问指定实例变量的值,静态变量,第一参数给null

反射调用成员方法
Method m = Clazz.getDeclaredMethod(方法名,参数类型列表);
m.setAccessible(true);//使私有方法允许被调用
m.invoke(实例,参数数据);//让指定实例来执行该方法

反射的应用距离

物料类

创建包:cn.tedu.reflection
创建类:Student.java*

package cn.tedu.review;
public class Student{
	//1.定义成员变量
	private String name;
	public int age;

	2.给被封装属性提供get与set方法
	public String getName(){
		return name;
	}
	public void setName(String name){
		this.name =name;
	}

	//3.生成本类的无参构造与全参构造
	public Student(){
	public Student(String name,int age){
			this.name =name;
			this.age =age;
		}
	}

	//4.提供本类的普通方法
	public void play(){
		System.out.println("今天大结局,放学后我要写1W行代码玩玩~")
	}	
 	public void sunDay(int n){
        System.out.println("国庆一共放"+n+"天");
    }
    //5.为了查看学生对象的具体属性与属性值,重写toString()
    @Override
    public String toString() {
        return "Student{" +
                "name='" + name + '\'' +
                ", age=" + age +
                '}';
    		}
}
	

练习:获取类对象

创建包:cn.tedu.reflection
创建类:TestReflect.java

package cn.tedu.reflection;

import org.junit.Test;

import java.lang.reflect.Method;
import java.util.Arrays;

/*本类用于反射的测试*/
public class TestReflect {
    //1.创建程序的入口函数main()--不用
    /*单元测试方法:是Java中最小的测试单位,使用灵活,推荐指数:5颗星
    * 语法要求:@Test + public + void + 没有参数
    * 注意:使用时需要导包:Add JUnit 4 library to the build path
    * 导包后的效果:import org.junit.Test
    * 执行方式:选中方法名前绿色的小三角,成功运行会有绿色的小对勾
    * */
    //2.通过单元测试方法,获取目标类Student对应的字节码对象
    @Test
    public void getClazz() throws ClassNotFoundException {
        //练习获取字节码对象的3种方式
        Class<?> clazz1 = Class.forName("cn.tedu.review.Student");
        Class<?> clazz2 = Student.class;
        Class<?> clazz3 = new Student().getClass();

        //打印的是Student类对应的字节码对象
        System.out.println(clazz1);//class cn.tedu.reflection.Student
        //获取当前字节码对象clazz1的名字
        System.out.println(clazz1.getName());//cn.tedu.reflection.Student
        //通过字节码对象,获取Student类的类名
        System.out.println(clazz2.getSimpleName());
        //通过字节码对象,获取Student类对应的包对象
        System.out.println(clazz3.getPackage());
        //通过字节码对象,先获取Student类对应的包对象,再获取这个包对象的名字
        System.out.println(clazz3.getPackage().getName());
    }

练习:获取成员变量

package cn.tedu.reflection;

import java.lang.reflect.Field;

import org.junit.Test;

/**本类用来测试反射*/
public class TestReflect {
	//3.通过单元测试方法练习引用类型数组的定义与遍历
    @Test
    public void getStu() {
        //1.创建Student类的3个对象
        Student s1 = new Student("张三", 3);
        Student s2 = new Student("李四", 4);
        Student s3 = new Student("王五", 5);
        //2.创建数组将刚刚的3个对象存入数组中
        Student[] s = {s1, s2, s3};
        //3.直接打印数组,查看数组中的元素
        System.out.println(Arrays.toString(s));
        //4.遍历学生数组,拿到每一个学生对象,做进一步的操作
        for (Student stu : s) {
            //System.out.println(stu);
            stu.play();//通过遍历到的对象,执行play()
            System.out.println(stu.age);//通过遍历到的对象,打印age属性
        }
    }

	//4.通过单元测试方法,获取Student类中的成员变量
    @Test
    public void getFie() throws ClassNotFoundException {
        //1.获取字节码对象
        Class<?> clazz = Class.forName("cn.tedu.review.Student");
        //2.通过字节码对象获取成员变量们
        Field[] fs = clazz.getFields();
        //3.遍历数组,获取每个成员变量的具体信息
        /*注意!目前成员变量的修饰符必须是public的才能获取到,不然,像默认修饰符也是获取不到的*/
        for(Field f : fs){
            System.out.println(f.getName());//通过本轮循环到的字段对象获取字段名
            System.out.println(f.getType());//通过本轮循环到的字段对象获取字段的类型
        }

    }
}

练习:通过字节码对象获取类的成员方法

package cn.tedu.reflection;

import java.lang.reflect.Method;
import java.util.Arrays;

import org.junit.Test;

/**本类用来测试反射*/
public class TestReflect {
    //5.通过单元测试方法,获取Student类中的成员方法
    @Test
    public void getFunction() {
        //1.获取字节码对象
        Class<?> clazz = Student.class;
        //2.通过字节码对象获取目标类中的成员方法们
        Method[] ms = clazz.getMethods();
        //3.通过高效for循环遍历数组,拿到每一个方法对象
        for (Method m : ms) {
            System.out.println(m);//直接打印遍历到的方法对象
            System.out.println(m.getName());//通过方法对象获取方法名
            Class<?>[] pt = m.getParameterTypes();//通过方法对象获取方法所有参数的数组
            System.out.println(Arrays.toString(pt));//打印方法参数的数组
        }
    }

4.5 练习 : 通过字节码对象获取类的构造方法

package cn.tedu.reflection;

import java.lang.reflect.Constructor;
import java.util.Arrays;

import org.junit.Test;

/**本类用来测试反射*/
public class TestReflect {
    //6.通过单元测试方法,获取Student类中的构造方法
    @Test
    public void getCons() {
        //1.获取字节码对象
        Class<?> clazz = new Student().getClass();
        //2.通过字节码对象获取目标类Student的构造方法们
        Constructor<?>[] cs = clazz.getConstructors();
        //3.通过高效for循环遍历数组
        for(Constructor c : cs){
            System.out.println(c.getName());//打印本轮遍历到的构造方法的名字
            Class[] pt = c.getParameterTypes();//通过本轮遍历到的构造函数对象获取构造函数的参数类型
            System.out.println(Arrays.toString(pt));//打印参数类型
        }
    }

练习 : 创建对象

package cn.tedu.reflection;

import java.lang.reflect.Constructor;

import org.junit.Test;

/**本类用来测试反射*/
public class TestReflect {
//7.通过单元测试方法,创建Student目标类的对象
    @Test
    public void getObject() throws Exception {
        //1.获取字节码对象
        Class<?> clazz = Student.class;
        //2.通过反射技术创建目标类的对象,注意抛出异常
        /*反射创建对象方案1:通过触发目标类的无参构造创建对象*/
        Object o = clazz.newInstance();
        System.out.println(o);//这一步已经获取到了对象Student{name='null', age=0}

        /*反射创建对象方案2:通过触发目标类的全参构造创建对象
        * 思路:
        * 1.先获取指定的构造函数对象,注意需要指定构造函数的参数,传入的是.class字节码对象
        * 2.通过刚刚获取到的构造函数对象创建Student目标类的对象,并且给对象的属性赋值
        * */
        //3.获取目标类中指定的全参构造
        Constructor<?> c = clazz.getConstructor(String.class, int.class);
        //System.out.println(c);
        //4.通过获取到的构造函数:创建对象+给对象的属性赋值
        Object o2 = c.newInstance("赵六", 6);
        System.out.println(o2);
    }
}

5.1 创建 : 测试物料类

创建包: cn.tedu. reflection
创建类: Person.java*

package cn.tedu.review;
/*本类用作暴力反射测试的物料类*/
public class Person {
    //1.提供私有属性
    private String name;
    private int age;

    //2.提供私有方法
    private void save(int n,String s){
        System.out.println("save()..."+n+s);
    }
    private void update(){
        System.out.println("update()...");
    }
}


练习 : 创建测试类

创建包: cn.tedu. reflection
创建类: TestReflect2.java

package tedu.reflection;

import org.junit.Test;

import java.lang.reflect.Field;
import java.lang.reflect.Method;

/*本类用于测试暴力反射*/
public class TestReflect2 {
   /*1.通过暴力反射获取与操作属性*/
    @Test
    public void getFie2() throws Exception {
        //1.获取字节码对象
        Class<?> clazz = Person.class;
        //2.获取指定的私有属性,传入的是属性名,注意抛出异常
        Field field = clazz.getDeclaredField("name");
        //3.根据刚刚获取到的属性对象,查看属性的信息
        System.out.println(field);//直接打印获取到的字段对象
        System.out.println(field.getType().getName());//java.lang.String
        System.out.println(field.getType());//class java.lang.String

        //4.设置属性的值
        //4.1 需要指定到底是给哪个对象的name属性设置值,没有对象就创建对象
        Object obj = clazz.newInstance();//触发无参构造利用反射创建对象

        //4.2暴力反射,需要设置私有可见权限!!!
        field.setAccessible(true);

        //4.3通过字段对象给刚刚创建好的对象obj设置属性值为海绵宝宝
        //field就是我们刚刚获取的name属性
        //set(m,n)--m是给哪个对象的name属性设置值,n是设置的值是什么
        field.set(obj,"海绵宝宝");
        //4.4 打印查看刚刚设置的属性值
        //field.get(m)--field代表的就是Person类的name属性,m是查看哪个对象的这个属性值
        System.out.println(field.get(obj));
    }

    //2.定义单元测试方法,利用暴力反射操作Person类中的私有属性age【巩固练习】
    @Test
    public void getFie3() throws Exception {
        //1.获取字节码对象
        Class<?> clazz = Person.class;
        //2.获取指定的私有属性对象
        Field f = clazz.getDeclaredField("age");
        //3.根据获取到的属性对象,查看相关信息,比如属性的类型
        System.out.println(f.getType().getName());
        //4.操作:设置属性的值:一共需要三个元素:给哪个对象【1】的哪个属性【2】设置一个什么值【3】
        //4.1 需要先指定给哪个对象的这个age属性设置值
        Object obj = clazz.newInstance();
        //4.2 在给属性设置值之前,需要设置权限私有可见,否则报错!
        f.setAccessible(true);
        //4.3通过刚刚获取到的age属性对象,给obj对象设置值
        f.set(obj,17);
        //4.4打印查看刚刚的属性值是否设置成功
        System.out.println(f.get(obj));
    }
    /*3.单元测试2:暴力反射获取和设置私有方法*/
    @Test
    public void getFunction() throws Exception {
        //1.获取Class字节码对象
        Class<?> clazz = Person.class;
        //2.通过暴力反射获取私有方法
        /*getDeclaredMethod(m,x,y,z...)
        * m:要获取的方法名
        * x,y,z...可变参数,是这个方法的参数类型,但注意要加“.class”
        * */
        Method method = clazz.getDeclaredMethod("save",int.class,String.class);
        //3.1没有对象就通过反射的方式创建对象
        Object obj = clazz.newInstance();
        //3.2 想要执行私有方法,也需要先设置私有可见
        method.setAccessible(true);
        /*invoke(o,x,y,z...),表示通过反射技术执行方法
        * o :要执行的是哪个对象的方法
        * x,y,z...:执行这个方法【method对象代表的之前获取到的save()】时需要传入的参数
        * */
        //3.3 通过反射技术invoke(),执行目标对象obj的目标方法method【save()】
        //save()被调用时传入的参数是100,"海绵宝宝"
        method.invoke(obj,100,"海绵宝宝");
    }
}

https://www.xn2001.com/archives/668.html

面试中最常问的设计模式:单例模式

考察的点:单例模式的五种实现方式,jdk中使用单例模式的场景,为何DCL(双重检查锁)实现时要使用volatile修饰静态变量

1.饿汉式

饿汉式即在类初始化时就创建了对象,(单例的,使用static修饰)

public class Singleton1 implements Serializable{
	private static final Singleton1 INSTANCE =  new Singleton1();
	private Singleton1(){
		//构造私有
	}
	public static Singleton1 getInstance(){
		return INSTANCE;
	}
}

但是上面的代码不够健壮,可以通过反射或者反序列化来破坏单例,我们需要进一步修改

public class Singleton1 implements Serializable{
	private static final SINGLETON1 INSTANCE = new Singleton1();
	private Singleton1(){
		//防止反射破坏单例
		if(INSTANCE != null){
			throw new RuntimeException("单例对象无法创建实例")
			//RuntimeException 运行时异常类,下方有一个实例
		}
	}
}

88.说一下你熟悉的设计模式?

● 单例模式:保证被创建一次,节省系统开销。
● 工厂模式(简单工厂、抽象工厂):解耦代码。
● 观察者模式:定义了对象之间的一对多的依赖,这样一来,当一个对象改变时,它的所有的依赖者都会收到通知并自动更新。
● 外观模式:提供一个统一的接口,用来访问子系统中的一群接口,外观定义了一个高层的接口,让子系统更容易使用。
● 模版方法模式:定义了一个算法的骨架,而将一些步骤延迟到子类中,模版方法使得子类可以在不改变算法结构的情况下,重新定义算法的步骤。
● 状态模式:允许对象在内部状态改变时改变它的行为,对象看起来好像修改了它的类。

89.简单工厂和抽象工厂有什么区别?

● 简单工厂:用来生产同一等级结构中的任意产品,对于增加新的产品,无能为力。
● 工厂方法:用来生产同一等级结构中的固定产品,支持增加任意产品。
● 抽象工厂:用来生产不同产品族的全部产品,对于增加新的产品,无能为力;支持增加产品族。

mysql创建数据库
创建数据库为create命令,语法如下

create database 数据库名

删除数据库为drop,语法为

drop database 数据库名

创建数据库表
create table table_name(column_name column_type);

删除数据表
DROP TABLE table_name;

插入数据表
insert into table_name(field1,field2,field3)
values
(value1,value2,value3);

查询数据
select column_name,column_name
from table_name

查询员工表中有多少个经历

select distinct manager_id from t_employees;

按照员工工资进行升序排序

select employee_id,salary from t_employees order by salary asc;

查询符合条件的数据

select employee id, first_name,salary
from t_employees
where salary=11000;

查询符合两个条件的数据
select employee_id,first_name,salary
from t_employees
where salary =11000 and commission_pct =0.30;

不等于 用 != 或者 <> 表示

查询出经理编号为NULL的员工信息
select employee_id ,first_name,manager_id
from t_employees
where manager_id is NULL;

这里要注意查询NULL时,要用 is NULL, 而不是等于NULL。

枚举查询
查询部门编号为70,80,90的员工信息

select employee_id ,first_name,salary,department_id
from t_employees
where department_id in(70,80,90)
注意,in的查询效率较低,可通过多条件拼接

模糊查询
模糊查询只能和LIKE关键字结合使用

查询名字以’L’开头的员工信息(编号,名字,薪资,部门编号)
select employee_id,first_name,salary,department_id
from t_employees
where first_name like ‘L%’;

查询名字以’L’开头并且长度为4的员工信息(编号,名字,薪资,部门编号)
select employee_id,first_name,salary,department_id
from t_employees
where first_name like ‘L—’;

分支结构查询

CASE
WHEN 条件1 THEN 结果1
WHEN 条件2 THEN 结果2
WHEN 挑拣3 THEN 结果3
ELSE
END

查询员工信息(编号,名字,薪资,薪资级别<对应条件表达式生成>)
select employee_id,first_name,salary,department_id
case
WHEN salary>=10000 THEN ‘A’
WHEN salary>=8000 AND salary<10000 THEN ‘B’
WHEN salary>=6000 AND salary<8000 THEN ‘C’
ELSE ‘D’
END as “LEVEL”
FROM t_employees;

通过case end 进行条件判断,每条数据对应生成一个值,类似于java中的switch

聚合函数
对多条数据的单列进行统计,返回统计后的一行结果。
select 聚合函数(列名) FROM 表名;
经验:对多条数据的单列进行统计,返回统计后的一行结果。

SUM() 求和
AVG() 平均值
MAX() 最大值
MIN() 最小值
COUNT() 求总行数

求单列所有数据的和
select sum(salary) from t_employees;

求单列所有数据的平均值
select avg(salary) from t_employees;

求单列最大值
select max(salary) from t_employees;

求单列最小值
select min(salary) from t_employees;

求总行数 员工总数
select count(employee_id) from t_employees;

统计有提成的人数
select commission_pct from t_employees;

分组查询

语法:
select 列名 from 表名 where 条件 group by 分组依据 (列)

关键字 说明
group by 分组依据,必须在where之后生效

查询各部门的总人数

思路:
#1.按照部门编号进行分组(分组依据是department_id)
#2.在针对各部门的人数进行统计(count)
select department_id,count(employee_id)
from t_employees
group by department_id;

查询各部门的平均工资

思路:
#1.按照部门编号进行分组(分组依据是department_id)
#2.针对每个部门进行平均工资统计(avg)
select department_id,AVG(salary)
from t_employees
group by department_id

查询各个部门,各个岗位的人数
#1.按照部门编号进行分组(分组依据是department_id)
#2.按照岗位名称进行分组 (分组依据job_id)
#3.针对每个部门中的各个岗位进行人数统计(count)
select department_id,job_id,count(employee_id)
from t_employees
group by department_id,job_id;

分组过滤查询

select 列名 from 表名 where 条件 group by 分组列 having 过滤规则

having 过滤规则 过滤规则定义对分组后的数据进行过滤

统计部门的最高工资

#统计60,70,90号部门的最高工资
#思路
1) 确定分组依据(department_id)
2) 对分组后的数据,过滤出部门编号是60,70,90的信息
3) max()函数处理

select department_id,max(salary)
from t_employees
group by department_id
having department_id in (60,70,90)

#group 确定分组依据department_id
#having过滤出60 70 90 部门
#select 查看部门编号和max函数。

限定查询

语法: select 列名 from 表名 limit 起始行,查询行数

关键字 说明
limit offset_start ,row_count 限定查询结果的起始行和总行数

查询前5行记录

#查询表中前五名员工的所有信息
select * from t_employees limit 0,5;

注意,起始行是从0开始,代表了第一行,第二个参数代表的是从指定行开始查询几行

经典应用

分页查询:一页显示10条,一共查询三页

#思路:第一页是从0开始,显示10条
select * from limit 0,10;

#思路:第二页是从10开始,显示10条
select * from limit 10,10;

#思路:第三页是从20开始,显示10条
select * from limit 20,10;

查询总结

sql语句编写顺序

select 列名 from 表名 where 条件 group by 分组 having 过滤条件 order by 排序方式 limit 起始行,总条数

sql语句执行顺序

1.from 指定数据来源表
2.where:对查询数据做第一次过滤
3.group by :分组
4.having :对分组后的数据进行第二次过滤
5.select: 查询各字段的值
6.group by:排序
7.limit :限定查询结果

子查询(作为条件判断)

select 列名 from 表名 where 条件(子查询)

查询工资大于bruce 的员工信息

#1.先查询到bruce的工资(一行一列)
select salary from t_employees where first_name =‘bruce’;#工资是6000

#2.查询工资大于bruce的员工信息
select * from t_employees where salary > 6000;

#3.将1,2两条语句整合
select * from t_employees where salary >(select salary from t_employees where first_name=‘bruce’);

注意:
将子查询“一行一列”的结果做为外部查询的条件,做第二次查询
子查询得到一行一列的结果才能做为外部查询的等值判断条件或不等值条件判断

子查询(做为枚举查询条件)

查询与名为’King’同一部门的员工信息

#思路:
#1.先查询’King’所在的部门编号(多行单列)
select department_id
from t_employees
where last_name = ‘king’; //部门编号80,90

#2.在查询80,90号部门的员工信息
select employee_id,first_name,salary,department_id
from t_employees
where department_id in(80,90);

#3.SQL,合并
select employee_id,first_name,salary,department_id
from t_employees
where department_id in (select department_id from t_employees where last_name =‘King’);
#N行一列

将子查询“多行一列的结果做为外部查询的枚举查询条件,做二次查询”

工资高于60部门所有人的人的信息

#1.查询60部门所有人的工资
select salary from t_employees where department_id=60;

#2.查询高于60部门所有人的工资的员工信息(高于所有)
select * from t_employees where salary >ALL(select salary from t_employees where department_id=60);

#3.查询高于60部门所有人的工资的员工信息(高于部分)
select * from t_employees where salary >ANY(select salary from t_employees where department_id=60);

https://blog.csdn.net/weixin_45269353/article/details/124020482?spm=1001.2014.3001.5502

https://blog.csdn.net/weixin_45269353/article/details/121641850?spm=1001.2014.3001.5501

164.数据库的三范式是什么?

● 第一范式:强调的是列的原子性,即数据库表的每一列都是不可分割的原子数据项。
● 第二范式:要求实体的属性完全依赖于主关键字。所谓完全依赖是指不能存在仅依赖主关键字一部分的属性。
● 第三范式:任何非主属性不依赖于其它非主属性。

165.一张自增表里面总共有 7 条数据,删除了最后 2 条数据,重启 mysql 数据库,又插入了一条数据,此时 id 是几?

● 表类型如果是 MyISAM ,那 id 就是 8。
● 表类型如果是 InnoDB,那 id 就是 6。

InnoDB 表只会把自增主键的最大 id 记录在内存中,所以重启之后会导致最大 id 丢失。

166.如何获取当前数据库版本?

使用 select version() 获取当前 mysql 数据库版本。

167.说一下 ACID 是什么?

● Atomicity(原子性):一个事务(transaction)中的所有操作,或者全部完成,或者全部不完成,不会结束在中间某个环节。事务在执行过程中发生错误,会被恢复(Rollback)到事务开始前的状态,就像这个事务从来没有执行过一样。即,事务不可分割、不可约简。
● Consistency(一致性):在事务开始之前和事务结束以后,数据库的完整性没有被破坏。这表示写入的资料必须完全符合所有的预设约束、触发器、级联回滚等。
● Isolation(隔离性):数据库允许多个并发事务同时对其数据进行读写和修改的能力,隔离性可以防止多个事务并发执行时由于交叉执行而导致数据的不一致。事务隔离分为不同级别,包括读未提交(Read uncommitted)、读提交(read committed)、可重复读(repeatable read)和串行化(Serializable)。
● Durability(持久性):事务处理结束后,对数据的修改就是永久的,即便系统故障也不会丢失。

168.char 和 varchar 的区别是什么?

● char(n) :固定长度类型,比如订阅 char(10),当你输入"abc"三个字符的时候,它们占的空间还是 10 个字节,其他 7 个是空字节。

chat 优点:效率高;缺点:占用空间;适用场景:存储密码的 md5 值,固定长度的,使用 char 非常合适。

● varchar(n) :可变长度,存储的值是每个值占用的字节再加上一个用来记录其长度的字节的长度。

所以,从空间上考虑 varcahr 比较合适;从效率上考虑 char 比较合适,二者使用需要权衡。

169.float 和 double 的区别是什么?

● float 最多可以存储 8 位的十进制数,并在内存中占 4 字节。
● double 最可可以存储 16 位的十进制数,并在内存中占 8 字节。

170.mysql 的内连接、左连接、右连接有什么区别?

内连接关键字:inner join;左连接:left join;右连接:right join。

内连接是把匹配的关联数据显示出来;左连接是左边的表全部显示出来,右边的表显示出符合条件的数据;右连接正好相反。

171.mysql 索引是怎么实现的?

索引是满足某种特定查找算法的数据结构,而这些数据结构会以某种方式指向数据,从而实现高效查找数据。

具体来说 mysql 中的索引,不同的数据引擎实现有所不同,但目前主流的数据库引擎的索引都是 B+ 树实现的,B+ 树的搜索效率,可以到达二分法的性能,找到数据区域之后就找到了完整的数据结构了,所有索引的性能也是更好的。

172.怎么验证 mysql 的索引是否满足需求?

使用 explain 查看 sql 是如何执行查询语句的,从而分析你的索引是否满足需求。

explain 语法:explain select * from table where type=1。

173.说一下数据库的事务隔离?

mysql 的事务隔离是在 mysql.ini 配置文件里添加的,在文件的最后添加:

transaction-isolation = REPEATABLE-READ

可用的配置值:READ-UNCOMMITTED、READ-COMMITTED、REPEATABLE-READ、SERIALIZABLE。

● READ-UNCOMMITTED:未提交读,最低隔离级别、事务未提交前,就可被其他事务读取(会出现幻读、脏读、不可重复读)。
● READ-COMMITTED:提交读,一个事务提交后才能被其他事务读取到(会造成幻读、不可重复读)。
● REPEATABLE-READ:可重复读,默认级别,保证多次读取同一个数据时,其值都和事务开始时候的内容是一致,禁止读取到别的事务未提交的数据(会造成幻读)。
● SERIALIZABLE:序列化,代价最高最可靠的隔离级别,该隔离级别能防止脏读、不可重复读、幻读。

脏读 :表示一个事务能够读取另一个事务中还未提交的数据。比如,某个事务尝试插入记录 A,此时该事务还未提交,然后另一个事务尝试读取到了记录 A。

不可重复读 :是指在一个事务内,多次读同一数据。

幻读 :指同一个事务内多次查询返回的结果集不一样。比如同一个事务 A 第一次查询时候有 n 条记录,但是第二次同等条件下查询却有 n+1 条记录,这就好像产生了幻觉。发生幻读的原因也是另外一个事务新增或者删除或者修改了第一个事务结果集里面的数据,同一个记录的数据内容被修改了,所有数据行的记录就变多或者变少了。

174.说一下 mysql 常用的引擎?

● InnoDB 引擎:mysql 5.1 之后默认引擎,提供了对数据库 acid 事务的支持,并且还提供了行级锁和外键的约束,它的设计的目标就是处理大数据容量的数据库系统。mysql 运行的时候,InnoDB 会在内存中建立缓冲池,用于缓冲数据和索引。但是该引擎是不支持全文搜索,同时启动也比较的慢,它是不会保存表的行数的,所以当进行 select count() from table 指令的时候,需要进行扫描全表。由于锁的粒度小,写操作是不会锁定全表的,所以在并发度较高的场景下使用会提升效率的。
● MyIASM 引擎:不提供事务的支持,也不支持行级锁和外键。因此当执行插入和更新语句时,即执行写操作的时候需要锁定这个表,所以会导致效率会降低。不过和 InnoDB 不同的是,MyIASM 引擎是保存了表的行数,于是当进行 select count(
) from table 语句时,可以直接的读取已经保存的值而不需要进行扫描全表。所以,如果表的读操作远远多于写操作时,并且不需要事务的支持的,可以将 MyIASM 作为数据库引擎的首选。

175.说一下 mysql 的行锁和表锁?

MyISAM 只支持表锁,InnoDB 支持表锁和行锁,默认为行锁。

● 表级锁:开销小,加锁快,不会出现死锁。锁定粒度大,发生锁冲突的概率最高,并发量最低。
● 行级锁:开销大,加锁慢,会出现死锁。锁力度小,发生锁冲突的概率小,并发度最高。

176.说一下乐观锁和悲观锁?

● 乐观锁:每次去拿数据的时候都认为别人不会修改,所以不会上锁,但是在提交更新的时候会判断一下在此期间别人有没有去更新这个数据。
● 悲观锁:每次去拿数据的时候都认为别人会修改,所以每次在拿数据的时候都会上锁,这样别人想拿这个数据就会阻止,直到这个锁被释放。

数据库的乐观锁需要自己实现,在表里面添加一个 version 字段,每次修改成功值加 1,这样每次修改的时候先对比一下,自己拥有的 version 和数据库现在的 version 是否一致,如果不一致就不修改,这样就实现了乐观锁。

177.mysql 问题排查都有哪些手段?

● 使用 show processlist 命令查看当前所有连接信息。
● 使用 explain 命令查询 sql 语句执行计划。
● 开启慢查询日志,查看慢查询的 sql。

178.如何做 mysql 的性能优化?

● 为搜索字段创建索引。
● 避免使用 select *,列出需要查询的字段。
● 垂直分割分表。

MySQL部分
存储引擎的区别
InnoDB拥有事务和外键,而MyISAM则没有,InnoDB使用聚集索引,MyISAM是非聚集索引标,InnoDB所用的是行锁 表锁,MyISAM用的是表锁,InnoDB操作中Insert与Update效率更高,MyISAMSelect效率更高
DELETE FROM table时,InnoDB不会重新建立表,而是一行一行的删除,在innodb上如果要清空保存有大量数据的表,最好使用truncate table这个命令 参考链接

1.InnoDB 支持事务,MyISAM 不支持事务。这是 MySQL 将默认存储引擎从 MyISAM 变成 InnoDB 的重要原因之一;

\2. InnoDB 支持事务,而 MyISAM 不支持。对一个包含外键的 InnoDB 表转为 MYISAM 会失败;

\3. InnoDB 是聚集索引,MyISAM 是非聚集索引。聚簇索引的文件存放在主键索引的叶子节点上,因此 InnoDB 必须要有主键,通过主键索引效率很高。但是辅助索引需要两次查询,先查询到主键,然后再通过主键查询到数据。因此,主键不应该过大,因为主键太大,其他索引也都会很大。而 MyISAM 是非聚集索引,数据文件是分离的,索引保存的是数据文件的指针。主键索引和辅助索引是独立的。

\4. InnoDB 不保存表的具体行数,执行 select count(*) from table 时需要全表扫描。而MyISAM 用一个变量保存了整个表的行数,执行上述语句时只需要读出该变量即可,速度很快;

\5. InnoDB 最小的锁粒度是行锁,MyISAM 最小的锁粒度是表锁。一个更新语句会锁住整张表,导致其他查询和更新都会被阻塞,因此并发访问受限。这也是 MySQL 将默认存储引擎从 MyISAM 变成 InnoDB 的重要原因之一;

关系数据模型的三个组成部分 完整性规则、 数据结构、 数据操作。

● 数据结构:描述数据库的组成对象以及对象之间的联系。
● 数据操作:指对数据库中各种对象(型)的实例(值)允许执行的操作的集合,包括操作及有关的操作规则。
● 数据的完整性约束规则:一组完整性规则。

说一说你对数据库优化的理解
数据库的优化是多方面的,原则就是减少系统的瓶颈,减少资源的占用,增加系统的反应速度。
例如,通过优化文件系统,提高磁盘I\O的读写速度;通过优化操作系统调度策略,提高MySQL在高负荷情况下的负载能力;优化表结构、索引、查询语句等使查询响应更快。

针对查询,我们可以通过使用索引、使用连接代替子查询的方式来提高查询速度。

针对慢查询,我们可以通过分析慢查询日志,来发现引起慢查询的原因,从而有针对性的进行优化。

针对插入,我们可以通过禁用索引、禁用检查等方式来提高插入速度,在插入之后再启用索引和检查。

针对数据库结构,我们可以通过将字段很多的表拆分成多张表、增加中间表、增加冗余字段等方式进行优化。

该如何优化Mysql的查询
使用索引:

如果查询时没有使用索引,查询语句将扫描表中的所有记录。在数据量大的情况下,这样查询的速度会很慢。如果使用索引进行查询,查询语句可以根据索引快速定位到待查询记录,从而减少查询的记录数,达到提高查询速度的目的。

索引可以提高查询的速度,但并不是使用带有索引的字段查询时索引都会起作用。有几种特殊情况,在这些情况下有可能使用带有索引的字段查询时索引并没有起作用。

使用LIKE关键字的查询语句

在使用LIKE关键字进行查询的查询语句中,如果匹配字符串的第一个字符为“%”,索引不会起作用。只有“%”不在第一个位置,索引才会起作用。

使用多列索引的查询语句

MySQL可以为多个字段创建索引。一个索引可以包括16个字段。对于多列索引,只有查询条件中使用了这些字段中的第1个字段时索引才会被使用。

使用OR关键字的查询语句

查询语句的查询条件中只有OR关键字,且OR前后的两个条件中的列都是索引时,查询中才使用索引。否则,查询将不使用索引。

优化子查询:

使用子查询可以进行SELECT语句的嵌套查询,即一个SELECT查询的结果作为另一个SELECT语句的条件。子查询可以一次性完成很多逻辑上需要多个步骤才能完成的SQL操作。

子查询虽然可以使查询语句很灵活,但执行效率不高。执行子查询时,MySQL需要为内层查询语句的查询结果建立一个临时表。然后外层查询语句从临时表中查询记录。查询完毕后,再撤销这些临时表。因此,子查询的速度会受到一定的影响。如果查询的数据量比较大,这种影响就会随之增大。

在MySQL中,可以使用连接(JOIN)查询来替代子查询。连接查询不需要建立临时表,其速度比子查询要快,如果查询中使用索引,性能会更好。

了解数据库的锁吗?
锁是数据库系统区别于文件系统的一个关键特性,锁机制用于管理对共享资源的并发访问。
下面以Mysql数据库的InnoDB引擎为例:来说明锁的一些特点。

锁:
InnoDB存储引擎实现了如下两种标准的行级锁:
共享锁:允许事务读取一行数据
排他锁:允许事务删除或更新一行数据
如果一个事务T1已经获得了行r的共享锁,那么另外的事务T2可以立即获得行r的共享锁,因为读取并没有改变行r的数据,称这种情况为锁兼容。但若有其他的事务T3想获得行r的排他锁,则其必须等待事务T1、T2释放行r上的共享锁,这种情况称为锁不兼容。下图显示了共享锁和排他锁的兼容性,可以发现X锁与任何的锁都不兼容,而S锁仅和S锁兼容。需要特别注意的是,S和X锁都是行锁,兼容是指对同一记录(row)锁的兼容性情况。
锁的粒度:

InnoDB存储引擎支持多粒度锁定,这种锁定允许事务在行级上的锁和表级上的锁同时存在。为了支持在不同粒度上进行加锁操作,InnoDB存储引擎支持一种额外的锁方式,称之为意向锁。意向锁是将锁定的对象分为多个层次,意向锁意味着事务希望在更细粒度上进行加锁。

InnoDB存储引擎支持意向锁设计比较简练,其意向锁即为表级别的锁。设计目的主要是为了在一个事务中揭示下一行将被请求的锁类型。其支持两种意向锁:

意向共享锁(IS Lock),事务想要获得一张表中某几行的共享锁。

意向排他锁(IX Lock),事务想要获得一张表中某几行的排他锁。

由于InnoDB存储引擎支持的是行级别的锁,因此意向锁其实不会阻塞除全表扫以外的任何请求。故表级意向锁与行级锁的兼容性如下图所示。

锁的算法:

InnoDB存储引擎有3种行锁的算法,其分别是:

Record Lock:单个行记录上的锁。

Gap Lock:间隙锁,锁定一个范围,但不包含记录本身。

Next-Key Lock∶Gap Lock+Record Lock,锁定一个范围,并且锁定记录本身。

Record Lock总是会去锁住索引记录,如果InnoDB存储引擎表在建立的时候没有设置任何一个索引,那么这时InnoDB存储引擎会使用隐式的主键来进行锁定。Next-Key Lock是结合了Gap Lock和Record Lock的一种锁定算法,在Next-Key Lock算法下,InnoDB对于行的查询都是采用这种锁定算法。采用Next-Key Lock的锁定技术称为Next-Key Locking,其设计的目的是为了解决Phantom Problem(幻读)。而利用这种锁定技术,锁定的不是单个值,而是一个范围,是谓词锁(predict lock)的一种改进。

关于死锁:

死锁是指两个或两个以上的事务在执行过程中,因争夺锁资源而造成的一种互相等待的现象。若无外力作用,事务都将无法推进下去。

解决死锁问题最简单的一种方法是超时,即当两个事务互相等待时,当一个等待时间超过设置的某一阈值时,其中一个事务进行回滚,另一个等待的事务就能继续进行。

除了超时机制,当前数据库还都普遍采用wait-for graph(等待图)的方式来进行死锁检测。较之超时的解决方案,这是一种更为主动的死锁检测方式。InnoDB存储引擎也采用的这种方式。wait-for graph要求数据库保存以下两种信息:

锁的信息链表;

事务等待链表;

通过上述链表可以构造出一张图,而在这个图中若存在回路,就代表存在死锁,因此资源间相互发生等待。这是一种较为主动的死锁检测机制,在每个事务请求锁并发生等待时都会判断是否存在回路,若存在则有死锁,通常来说InnoDB存储引擎选择回滚undo量最小的事务。

锁的升级:

锁升级(Lock Escalation)是指将当前锁的粒度降低。举例来说,数据库可以把一个表的1000个行锁升级为一个页锁,或者将页锁升级为表锁。

InnoDB存储引擎不存在锁升级的问题。因为其不是根据每个记录来产生行锁的,相反,其根据每个事务访问的每个页对锁进行管理的,采用的是位图的方式。因此不管一个事务锁住页中一个记录还是多个记录,其开销通常都是一致的。

怎么样插入数据才能高效?
影响插入速度的主要是索引、唯一性校验、一次插入记录条数等。针对这些情况,可以分别进行优化。

对于MyISAM引擎的表,常见的优化方法如下:

禁用索引

对于非空表,插入记录时,MySQL会根据表的索引对插入的记录建立索引。如果插入大量数据,建立索引会降低插入记录的速度。为了解决这种情况,可以在插入记录之前禁用索引,数据插入完毕后再开启索引。对于空表批量导入数据,则不需要进行此操作,因为MyISAM引擎的表是在导入数据之后才建立索引的。

禁用唯一性检查

插入数据时,MySQL会对插入的记录进行唯一性校验。这种唯一性校验也会降低插入记录的速度。为了降低这种情况对查询速度的影响,可以在插入记录之前禁用唯一性检查,等到记录插入完毕后再开启。

使用批量插入

插入多条记录时,可以使用一条INSERT语句插入一条记录,也可以使用一条INSERT语句插入多条记录。使用一条INSERT语句插入多条记录的情形如下,而这种方式的插入速度更快。

INSERT INTO fruits VALUES (‘x1’, ‘101’, ‘mongo2’, ‘5.7’), (‘x2’, ‘101’, ‘mongo3’, ‘5.7’), (‘x3’, ‘101’, ‘mongo4’, ‘5.7’);
使用LOAD DATA INFILE批量导入

当需要批量导入数据时,如果能用LOAD DATA INFILE语句,就尽量使用。因为LOAD DATA INFILE语句导入数据的速度比INSERT语句快。

对于InnoDB引擎的表,常见的优化方法如下:

禁用唯一性检查

插入数据之前执行set unique_checks=0来禁止对唯一索引的检查,数据导入完成之后再运行set unique_checks=1。这个和MyISAM引擎的使用方法一样。

禁用外键检查

插入数据之前执行禁止对外键的检查,数据插入完成之后再恢复对外键的检查。

禁用自动提交

插入数据之前禁止事务的自动提交,数据导入完成之后,执行恢复自动提交操作。

数据库在什么情况下会发生死锁?
死锁是指两个或两个以上的事务在执行过程中,因争夺锁资源而造成的一种互相等待的现象。若无外力作用,事务都将无法推进下去。下图演示了死锁的一种经典的情况,即A等待B、B等待A,这种死锁问题被称为AB-BA死锁。

说说数据库死锁的解决办法
解决死锁问题最简单的一种方法是超时,即当两个事务互相等待时,当一个等待时间超过设置的某一阈值时,其中一个事务进行回滚,另一个等待的事务就能继续进行。

除了超时机制,当前数据库还都普遍采用wait-for graph(等待图)的方式来进行死锁检测。较之超时的解决方案,这是一种更为主动的死锁检测方式。InnoDB存储引擎也采用的这种方式。wait-for graph要求数据库保存以下两种信息:

锁的信息链表;

事务等待链表;

通过上述链表可以构造出一张图,而在这个图中若存在回路,就代表存在死锁,因此资源间相互发生等待。这是一种较为主动的死锁检测机制,在每个事务请求锁并发生等待时都会判断是否存在回路,若存在则有死锁,通常来说InnoDB存储引擎选择回滚undo量最小的事务。

介绍一下数据库分页
mysql的分页语法:
在mysql中,select语句默认返回所有匹配的行,它们可能是指定表中的每个行,为了返回第一行或者前几行,可使用LIMIT子句,以实现分页查询.LIMIT子句的语法如下:、

– 在所有的查询结果中,返回前5行记录。
SELECT prod_name FROM products LIMIT 5;
– 在所有的查询结果中,从第5行开始,返回5行记录。
SELECT prod_name FROM products LIMIT 5,5;
1
2
3
4
介绍一下SQL中的聚合函数
常用的聚合函数有COUNT()、AVG()、SUM()、MAX()、MIN(),下面以MySQL为例,说明这些函数的作用。
COUNT()函数:

COUNT()函数统计数据表中包含的记录行的总数,或者根据查询结果返回列中包含的数据行数,它有两种用法:

COUNT(*)计算表中总的行数,不管某列是否有数值或者为空值。

COUNT(字段名)计算指定列下总的行数,计算时将忽略空值的行。

COUNT()函数可以与GROUP BY一起使用来计算每个分组的总和。

AVG()函数():

AVG()函数通过计算返回的行数和每一行数据的和,求得指定列数据的平均值。

AVG()函数可以与GROUP BY一起使用,来计算每个分组的平均值。

SUM()函数:

SUM()是一个求总和的函数,返回指定列值的总和。

SUM()可以与GROUP BY一起使用,来计算每个分组的总和。

MAX()函数:

MAX()返回指定列中的最大值。

MAX()也可以和GROUP BY关键字一起使用,求每个分组中的最大值。

MAX()函数不仅适用于查找数值类型,也可应用于字符类型。

MIN()函数:

MIN()返回查询列中的最小值。

MIN()也可以和GROUP BY关键字一起使用,求出每个分组中的最小值。

MIN()函数与MAX()函数类似,不仅适用于查找数值类型,也可应用于字符类型。

表跟表之间是怎么关联的?
表与表之间常用的关联方式有两种:内连接、外连接,下面以MySQL为例来说明这两种连接方式。

内连接:
内连接通过INNER JOIN来实现,它将返回两张表中满足连接条件的数据,不满足条件的数据不会查询出来。

外连接:
外连接通过OUTER JOIN来实现,它会返回两张表中满足连接条件的数据,同时返回不满足连接条件的数据。外连接有两种形式:左外连接(LEFT OUTER JOIN)、右外连接(RIGHT OUTER JOIN)。

左外连接:可以简称为左连接(LEFT JOIN),它会返回左表中的所有记录和右表中满足连接条件的记录。

右外连接:可以简称为右连接(RIGHT JOIN),它会返回右表中的所有记录和左表中满足连接条件的记录。

除此之外,还有一种常见的连接方式:等值连接。这种连接是通过WHERE子句中的条件,将两张表连接在一起,它的实际效果等同于内连接。出于语义清晰的考虑,一般更建议使用内连接,而不是等值连接。

以上是从语法上来说明表与表之间关联的实现方式,而从表的关系上来说,比较常见的关联关系有:一对多关联、多对多关联、自关联。

一对多关联:这种关联形式最为常见,一般是两张表具有主从关系,并且以主表的主键关联从表的外键来实现这种关联关系。另外,以从表的角度来看,它们是具有多对一关系的,所以不再赘述多对一关联了。

多对多关联:这种关联关系比较复杂,如果两张表具有多对多的关系,那么它们之间需要有一张中间表来作为衔接,以实现这种关联关系。这个中间表要设计两列,分别存储那两张表的主键。因此,这两张表中的任何一方,都与中间表形成了一对多关系,从而在这个中间表上建立起了多对多关系。

自关联:自关联就是一张表自己与自己相关联,为了避免表名的冲突,需要在关联时通过别名将它们当做两张表来看待。一般在表中数据具有层级(树状)时,可以采用自关联一次性查询出多层级的数据。

说一说你对外连接的了解
外连接通过OUTER JOIN来实现,它会返回两张表中满足连接条件的数据,同时返回不满足连接条件的数据。常见的外连接有两种形式:左外连接(LEFT OUTER JOIN)、右外连接(RIGHT OUTER JOIN)。

左外连接:可以简称为左连接(LEFT JOIN),它会返回左表中的所有记录和右表中满足连接条件的记录。

右外连接:可以简称为右连接(RIGHT JOIN),它会返回右表中的所有记录和左表中满足连接条件的记录。

实际上,外连接还有一种形式:完全外连接(FULL OUTER JOIN),但MySQL不支持这种形式。

说一说数据库的左连接和右连接
外连接通过OUTER JOIN来实现,它会返回两张表中满足连接条件的数据,同时返回不满足连接条件的数据。常见的外连接有两种形式:左外连接(LEFT OUTER JOIN)、右外连接(RIGHT OUTER JOIN)。

左外连接:可以简称为左连接(LEFT JOIN),它会返回左表中的所有记录和右表中满足连接条件的记录。

右外连接:可以简称为右连接(RIGHT JOIN),它会返回右表中的所有记录和左表中满足连接条件的记录。

Where和HAVING有什么区别
WHERE是一个约束声明,使用WHERE约束来自数据库的数据,WHERE是在结果返回之前起作用的,WHERE中不能使用聚合函数。

HAVING是一个过滤声明,是在查询返回结果集以后对查询结果进行的过滤操作,在HAVING中可以使用聚合函数。另一方面,HAVING子句中不能使用除了分组字段和聚合函数之外的其他字段。

从性能的角度来说,HAVING子句中如果使用了分组字段作为过滤条件,应该替换成WHERE子句。因为WHERE可以在执行分组操作和计算聚合函数之前过滤掉不需要的数据,性能会更好。

索引
说一说你对Mysql索引的理解
索引是一个单
独的,存储在磁盘上的数据库结构,包含着对数据库表里所有记录的引用指针。使用索引可以加快找出在某个或多个列中有一特定值的行,所有MySQL列类型都可以被索引,对相关列使用索引是提高查询和操作速度的最佳途径

索引是在存储引擎中实现的,因此,每种存储引擎的索引都不一定完全相同,并且每种存储引擎也不一定支持所有的索引类型。MySQL中索引的存储类型有两种,即Btree和HASH,具体和表的存储引擎相关

索引的有点主要有以下几条:
1.通过创建唯一索引,可以保证数据库表中每一行数据的唯一性
2.可以大大加快数据的查询速度。
3.有助于实现数据的参考完整性,可以加速表和表之间的链接
4.使用分组和排序子句进行数据查询时,可以显著减少查询中分组和排序的时间

增加索引也有许多不利的方面,主要表现在如下几个方面:
1.创建索引和维护索引要耗费时间,并且随着数据量的增加所耗费的时间也会增加
2.索引需要占据磁盘空间,除了数据表占数据空间之外,每一个索引还需要占一定的物理空间,如果有大量的索引,索引文件可能比数据文件更大
3.当对表中的数据进行增删改的时候,索引也要动态地维护,这样降低了数据的维护速度

mysql有哪些索引
可以分为以下几类:
1.普通索引和唯一索引
普通索引是MySQL中的基本索引类型,允许在定义索引的列中插入重复值和空值。
唯一索引要求索引列的值必须唯一,但允许有空值,如果是组合索引,则组合的值必须唯一。
主键索引是一种特殊的唯一索引,不允许有空值。

2.单列索引和组合索引
单列索引即一个索引只包含单个列,一个表可以有多个单列索引。
组合索引是指在表的多个字段组合上创建的索引,只有在查询条件中使用了这些字段的左边字段时,索引才会被使用。使用组合索引时遵循最左前缀集合

3.全文索引
4.空间索引

MySQL怎么判断要不要加索引
建议按照如下的原则来创建索引:

当唯一性是某种数据本身的特征时,指定唯一索引。使用唯一索引需能确保定义的列的数据完整性,以提高查询速度。

在频繁进行排序或分组(即进行group by或order by操作)的列上建立索引,如果待排序的列有多个,可以在这些列上建立组合索引。

那些情况不适合创建索引
1.频繁更新的字段
2.where条件中用不到的字段
3.数据比较少的表
4.数据重复且分布比较均匀的字段,比如性别,真价值
5.参与列计算的列不适合建索引

索引的实现原理
在MySQL中,索引是在存储引擎层实现的,不同存储引擎对索引的实现方式是不同的,下面我们探讨一下MyISAM和InnoDB两个存储引擎的索引实现方式。

MyISAM索引实现:

MyISAM引擎使用B+Tree作为索引结构,叶节点的data域存放的是数据记录的地址,MyISAM索引的原理图如下。这里假设表一共有三列,假设我们以Col1为主键,则上图是一个MyISAM表的主索引(Primary key)示意。可以看出MyISAM的索引文件仅仅保存数据记录的地址。在MyISAM中,主索引和辅助索引(Secondary key)在结构上没有任何区别,只是主索引要求key是唯一的,而辅助索引的key可以重复。
如果我们在Col2上建立一个辅助索引,则此索引的结构如下图所示。同样也是一颗B+Tree,data域保存数据记录的地址。因此,MyISAM中索引检索的算法为首先按照B+Tree搜索算法搜索索引,如果指定的Key存在,则取出其data域的值,然后以data域的值为地址,读取相应数据记录。

InnoDB索引实现:

虽然InnoDB也使用B+Tree作为索引结构,但具体实现方式却与MyISAM截然不同。

第一个重大区别是InnoDB的数据文件本身就是索引文件。从上文知道,MyISAM索引文件和数据文件是分离的,索引文件仅保存数据记录的地址。而在InnoDB中,表数据文件本身就是按B+Tree组织的一个索引结构,这棵树的叶节点data域保存了完整的数据记录。这个索引的key是数据表的主键,因此InnoDB表数据文件本身就是主索引。

下图是InnoDB主索引(同时也是数据文件)的示意图,可以看到叶节点包含了完整的数据记录。这种索引叫做聚集索引。因为InnoDB的数据文件本身要按主键聚集,所以InnoDB要求表必须有主键(MyISAM可以没有),如果没有显式指定,则MySQL系统会自动选择一个可以唯一标识数据记录的列作为主键,如果不存在这种列,则MySQL自动为InnoDB表生成一个隐含字段作为主键,这个字段长度为6个字节,类型为长整形。

第二个与MyISAM索引的不同是InnoDB的辅助索引data域存储相应记录主键的值而不是地址。换句话说,InnoDB的所有辅助索引都引用主键作为data域。下图为定义在Col3上的一个辅助索引。这里以英文字符的ASCII码作为比较准则。聚集索引这种实现方式使得按主键的搜索十分高效,但是辅助索引搜索需要检索两遍索引:首先检索辅助索引获得主键,然后用主键到主索引中检索获得记录。

了解不同存储引擎的索引实现方式对于正确使用和优化索引都非常有帮助,例如知道了InnoDB的索引实现后,就很容易明白为什么不建议使用过长的字段作为主键,因为所有辅助索引都引用主索引,过长的主索引会令辅助索引变得过大。再例如,用非单调的字段作为主键在InnoDB中不是个好主意,因为InnoDB数据文件本身是一颗B+Tree,非单调的主键会造成在插入新记录时数据文件为了维持B+Tree的特性而频繁的分裂调整,十分低效,而使用自增字段作为主键则是一个很好的选择。

聚簇索引有哪些类别
聚簇索引和非聚簇索引有什么区别
在InnoDB存储引擎中,可以将B+树索引分为聚簇索引和辅助索引(非聚簇索引)。无论是何种索引,每个页的大小都为16KB,且不能更改。

聚簇索引是根据主键创建的一棵B+树,聚簇索引的叶子节点存放了表中的所有记录。辅助索引是根据索引键创建的一棵B+树,与聚簇索引不同的是,其叶子节点仅存放索引键值,以及该索引键值指向的主键。也就是说,如果通过辅助索引来查找数据,那么当找到辅助索引的叶子节点后,很有可能还需要根据主键值查找聚簇索引来得到数据,这种查找方式又被称为书签查找。因为辅助索引不包含行记录的所有数据,这就意味着每页可以存放更多的键值,因此其高度一般都要小于聚簇索引。

mysql的存储引擎有哪些
MySQL的存储引擎有MyISAM和InnoDB,MEMORY/HEAP
MyISAM和InnoDB存储引擎只支持BTREE索引
MEMORY/HEAP存储引擎可以支持HASH和BTREE索引。

Mysql的隔离级别有哪些?
Mysql定义了四种隔离级别,包括一些具体规则,用于限定事物内外哪些改变是可见的,哪些改变时不可见的。
低级别的隔离一般支持更高的并发处理,并且拥有更低的系统开销

READ UNCOMMITTED 读取未提交内容

在这个隔离级别,所有事物都可以“看到”未提交事物的执行结果。在这种级别上,可能会产生很多问题,除非用户真的知道自己在干什么,并有很好的理由选择这样做。本隔离级别很少用于实际应用,因为它的性能也不比其他性能好多少,而别的级别还有其他更多的优点,读取未提交数据,也被称为“脏读”

READ COMMITTED 读取提交内容
大多数数据库的默认隔离级别(但是不是MySQL的默认隔离级别),满足了隔离的早先简单定义:一个事务开始时,只能“看见”已经提交事务所做的改变,一个事务从开始到提交钱,所做的任何数据改变都是不可见的,除非已经提交,这种隔离级别也支持所谓的“不可重复读”,这意味着用户运行同一个语句两次,看到的结果是不同的。

repeatble read 可重复读
MySQL 数据库默认的隔离界别。该级别解决了read uncommitted隔离级别导致的问题。它保证同一事务的多个实例在并发读取事务时,会“看到同样的”数据行。

serializable 可串行化
强制给事务排序,使之不可能冲突

1.脏读
脏读是指一个事务读取了未提交事务执行过程中的数据。
当一个事务的操作正在多次修改数据,而在事务还未提交的时候,另外一个并发事务
2.不可重复读
3.虚读(幻读)

1.在表中建立索引
2.尽量避免用select *,返回无用的字段会降低查询的效率
3.尽量避免使用 in和notin,会导致数据库放弃索引进行全表扫描
4.尽量避免使用or,会导致数据库放弃索引进行全表扫描
5.尽量避免进行null值的判断,会导致数据库放弃索引进行全表扫描
6.使用explain查看sql的执行情况,看看那些表查询时间比较长
7.在java中,减少与数据库的交互次数,比如可以使用在存储过程中。

  • 2
    点赞
  • 13
    收藏
    觉得还不错? 一键收藏
  • 2
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值