Java面试题

本文涉及的产品
RDS MySQL Serverless 基础系列,0.5-2RCU 50GB
RDS MySQL Serverless 高可用系列,价值2615元额度,1个月
简介: Java面试题

Java面试题


Java面试题

什么是面向对象,谈谈你对面向对象的理解

面向过程

对比面向过程,面向对象是两种不同的处理问题的角度

面向过程更注重事情的每一个步骤及顺序,面向对象更注重事情有哪些参与者(对象)、及各自需要做什么

面向过程比较直接高效,而面向对象更易于复用、扩展和维护

面向对象

封装

封装的意义,在于明确标识出允许外部使用的所有成员函数和数据项

内部细节对外调用透明,外部则无需修改或者关心内部实现

1、Java bean的属性私有,提供Get/Set对外访问,因为属性的赋值或者获取逻辑只能由Java本身决定,而不能由外部胡乱修改

private String name;
public void setName(String name){
    this.name = "tuling_" + name;
}
// 该name有自己的命名规则,明显不能由外部直接赋值

2、ORM框架


操作数据库,我们不需要关心链接是如何建立的、Sql是如何执行的,只需要引入MyBatis,调用即可


继承

继承基类的方法,并做出自己的改变和/或扩展


子类共性的方法或者属性直接使用父类的,而不需要自己再定义,只需扩展自己个性化的


多态

基于对象所属类的不同,外部对同一个方法的调用,实际执行的逻辑不同


多态的条件:继承,方法重写,父类引用指向子类对象

父类类型 变量名 = new 子类对象;
变量名.方法名();

无法调用子类特有的功能

Java虚拟机(JVM)

JDK、JRE、JVM三者区别和联系

JDK

Java Development Kit Java工具开发包

JRE

Java Runtime Environment Java运行环境

JVM

Java Virtual Machine Java虚拟机

==和equals的区别

==对比的是栈中的值,基本数据类型是变量值,引用类型是堆中内存对象的地址

equals: object中默认也是采用==比较,通常会重写

Object

public boolean equals(Object obj){
    return (this == obj);
}

String

public boolean equals(Object anObject) {
    if (this == anObject) {
        return true;
    }
    if (anObject instanceof String) {
        String anotherString = (String)anObject;
        int n = value.length;
        if (n == anotherString.value.length) {
            char v1[] = value;
            char v2[] = anotherString.value;
            int i = 0;
            while (n-- != 0) {
                if (v1[i] != v2[i])
                    return false;
                i++;
            }
            return true;
        }
    }
    return false;
}

上述代码可以看出,String类中被复写的equals()方法其实是比较两个字符串的内容

public class StringDemo{
    public static void main(Sting[] args){
        String str1 = "Hello";
        String str2 = new String("Hello");
        String str3 = str2; // 引用传递
        System.out.println(str1 == str2); // false
        System.out.println(str1 == str3); // false
        System.out.println(str2 == str3); // true
        System.out.println(str1.equals(str2)); // true
        System.out.println(str1.equals(str3)); // true
        System.out.println(str2.equals(str3)); // true
    }
}

final面试

简述final作用

最终的


修饰类:表示类不可被继承

修饰方法:表示方法不可被子类覆盖,但是可以重载

修饰变量:表示本类一旦被赋值就不可以更改它的值

(1)修饰成员变量


如果final修饰的是类变量,只能在静态初始化块中指定初始值或者声明该类变量时指定初始值

如果final修饰的是成员变量,可以在非静态初始化块、声明该变量或者在构造器中赋值

(2)修饰局部变量


系统不会为局部变量初始化,局部变量必须由程序员显示参数,因此使用final修饰局部变量时,即可以在定义时自动默认值(后面代码不能对变量再赋值),也可以不指定默认值,而在后面的代码中对final变量赋初始值(仅一次)

public class FinalVal{
    final static int a = 0; // 再声明的时候就需要赋值,或者静态代码块赋值
    /*
    static{
        a = 0;
    }
    */
    final int b = 0; // 再声明的时候就需要赋值,或者代码块中赋值,或者在构造器中赋值
    /*
    static{
        b = 0;
    }
    */
    public static void main(String[] args){
        final int localA; // 局部变量只声明没有初始化,不会报错,与final无关
        localA = 0; // 在使用之前一定要赋值  
        // localA = 1; 但是不允许第二次赋值
    }
}

(3)修饰基本类型数据和引用类型数据

  • 如果是基本数据类型的变量,则器数值一旦在初始化之后便不能更改
  • 如果是引用类型的变量,则在其初始化之后便不能再让其指向另一个对象,但是引用的值是可变的
public class FinalReferenceTest{
    public static void main(String[] args){
        final int[] iArr = {1, 2, 3, 4};
        iArr[2] = -3; // 合法
        iArr = null; // 非法
        final Person p = new Person(25);
        p.setAge(24); // 合法
        p = null; // 非法
    }
}

为什么局部内部类和匿名内部类只能访问局部final变量

编译之后会生成两个class文件:Test.class +Test1.class

public void Test{
    public static void main(String[] args){
    }
    // 局部final变量a,b 
    public void test(final int b){
        final int = 20;
        // 匿名内部类
        new Thread(){
            public void run(){
                System.out.println(a);
                System.out.println(b);
            }
        }.start();
    }
}
class OutClass{
    private int age = 12;
    public void outPrint(final int x){
        class InClass{
            public void inPrint{
                System.out.println(x);
                System.out.println(age);
            }
        }
        new InClass().inPrint();
    }
}

首先需要指定的一点是:内部类和外部类是处于同一级别的,内部类不会因为定义在方法中就会跟着方法的执行完毕就被销毁

这里就会产生问题:当外部类的方法结束时,局部变量就会被销毁了,但是内部类对象可能还存在(只有没有人再引用它时,才会死亡)。这里就出现了一个矛盾:内部类对象访问了一个不存在的变量。为了解决这个问题,就将局部变量复制了一份作为内部类的成员变量,这样当局部变量死亡后,内部类仍可以访问它,实际访问的是局部变量的"“copy”。这样就好像廷延长了局部变量的生命周期

将局部变量复制为内部类的成员变量时,必须保证这两个变量是一样的,也就是如果我们在内部类中修改了成员变量,方法中的局部变量也得跟着改变,怎么解决问题呢?

就将局部变量没置为final,对它初始化后,我就不让你再去修改这个变量,就保证了内部类的成员变量和方法的局部变量的一致性。这实际上也是一种妥协。使得局部变量与内部类内建立的拷贝保持一致。

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

String是final修饰的,不可变,每次操作都会产生新的String对象


StringBufffer和StringBuilder都是在原对象上操作


StringBuffer是线程安全的,StringBuilder是线程不安全的


StringBuffer方法都是synchronized修饰的


性能:StringBuilder > StringBuffer > String


场景:经常需要改变字符串内容时使用后面两个


优先使用StringBuilder,多线程使用共享变量时使用StringBuffer


重载和重写的区别

重载:发生在同一个类中,方法名必须相同,参数类型不同,个数不同,顺序不同,方法返回值和访问修饰符可以不同,发生在编译时


重写:发生在父子类中,方法名、参数列表必须相同,返回值范围小于等于父类,抛出的异常范围小于等于父类,访问修饰符大于等于父类;如果父类方法访问修饰符为private,则子类不能重写该方法

public int add(int a, String b);
public String add(int a, String b);
// 编译报错

接口和抽象类

抽象类可以存在普通成员函数,而接口中只能存在public abstract方法

抽象类中的成员变量可以是各种类型的,而接口中的成员变量只能是public static final类型的

抽象类只能继承一个,接口可以实现多个

接口的设计目的是对类的行为进行约束,而抽象类的设计目的是代码复用

抽象类是对类本质的抽象(is a),而接口是对行为的抽象(like a)

使用场景:

关注一个事物的本质的时候,用抽象类

关注一个操作的时候,用接口

List和Set的区别

List:有序,按对象进入的顺序保存对象,可重复,允许多个null元素对象,可以使用iterator取出所有元素,再逐一遍历,还可以使用get(int index)获取指定下标的元素

Set:无序,不可重复,最多允许一个null元素对象,取元素时只能用iterator接口取得所有元素,再逐一遍历各个元素

hashCode与equals

hashCode

hashCode()的作用是获取哈希码,也称为散列码;它实际上是返回一个int整数。这个哈希码的作用是确定该对象在哈希表中的索引位置。hashCode()定义在JDK的Object.java中,Java中的任何类都包含有hashCode()函数。散列表存储的是键值对(key-value),它的特点是:能根据"键"快速的检索出对应的“值”。这其中就利用到了散列码!(可以快速找到所需要的对象)

为什么要有hashCode

以"HashSet如何检查重复"为例子来说明为什么要有hashCode:

对象加入HashSet时,HashSet会先计算对象的hashCode值来判断对象加入的位置,看该位置是否有值,如果没有、HashSet会假设对象没有重复出现。但是如果发现有值,这时会调用equals ()方法来检查两个对象是否真的相同。如果两者相同,HashSet就不会让其加入操作成功。如果不同的话,就会重新散列到其他位置。这样就大大减少了equals的次数,相应就大大提高了执行速度。

如果两个对象相等,则hashCode一定也是相同的

两个对象相等,对两个对象分别调用equals方法都返回true

两个对象有相同的hashCode值,它们也不一定是相等的

因此,equals方法被覆盖过,则hashCode方法也必须被覆盖

hashCode()的默认行为是对堆上的对象产生独特值。如果没有重写hashCode(),则该class的两个对象无论如何都不会相等(即使这两个对象指向相同的数据)

ArrayList和LinkedList区别

ArrayList:基于动态数组,连续内存存储,适合下标访问(随机访问),扩容机制,因为数组长度固定,超出长度存数据时需要新建数组,调用System.arrayCopy()方法将老数组拷贝到新数组,如果不是尾部插入数据还会涉及到元素的一道,使用尾插法并自动初识容量可以极大提升性能,甚至超过LinkedList(需要创建大量的node对象)

LinkedList:基于链表,可以存储在分散的内存中,适合做数据插入及删除操作,不适合查询:需要逐一遍历

遍历LinkedList必须使用iterator不能使用for循环,因为每次for循环体通过get(i)取得某一元素时都需要对list重新进行遍历,性能消耗极大

另外不要试图使用indexOf等返回元素索引,并利用其进行遍历,使用indexOf对list进行遍历,当结果为空时会遍历整个链表

HashMap和HashTable的区别?底层实现?

区别

HashMap方法没有synchronized修饰,线程非安全,HashTable线程安全,但HashTable现在基本不用,用ConcurrentHashMap

HashMap允许key和value为null,而HashTable不允许

底层实现

数组+链表实现

jdk8开始链表高度达到8,数组长度超过64,链表转为红黑树,元素以内部类Node节点存在

jdk7采用头插法插入链表,jdk8则采用尾插法插入链表

计算key的hash值,二次hash然后对数组长度取模,对应到数组下标

如果没有产生hash冲突(下标位置没有元素),则直接创建Node存入数组

如果产生hash冲突,先进行equals比较,相同则取代该元素,不同,则判断链表高度插入链表,链表高度达到8,并且数组长度到64则转变为红黑树,长度低于6则将红黑树转回链表

key为null,存放在下标为0的位置

数组扩容

ConcurrentHashMap原理简述,jdk7和jdk8版本的区别

jdk7:

数据结构:ReentrantLock+Segment+HashEntry,一个Segment中包含一个HashEntry数组,每个HashEntry又是一个链表结构

元素查询:二次hash,第一次hash定位到Segment,第二次hash定位到元素所在的链表的头部

锁:Segment分段锁,Segment继承了ReentrantLock,锁定操作的Segment,其它的Segment不受影响,并发度为segment个数,可以通过构造函数指定,数组扩容不会影响其它的segment

get方法无需加锁,volatile保证

jdk8:

数据结构:synchronized+CAS(乐观锁)+Node+红黑树,Node的val和next都用volatile修饰,保证可见性

查找、替换、赋值操作都使用CAS

锁:锁链表的head节点,不影响其它元素的读写,锁粒度更细,效率更高,扩容时,阻塞所有的读写操作、并发扩容

读操作无锁:

Node的val和next使用volatile修饰,读写程序对该变量互相可见

数组用volatile修饰,保证扩容时读线程感知

如何实现一个IOC容器

配置文件配置包扫描路径

递归包扫描获取.class文件

反射、确定需要交给IOC管理的类

对需要注入的类进行依赖注入

配置文件中指定需要扫描的包路径

定义一些注解,分别表示访问控制层、业务服务层、数据持久层、依赖注入注解、获取配置文件注解

从配置文件中获取需要扫描的包路径,获取到当前路径下的文件信息及文件夹信息,我们将当前路径想下所有以.class结尾的文件添加到一个Set集合中进行存储

遍历这个Set集合,获取在类上有指定注解的类,并将其交给IOC容器,定义一个安全的Map用来存储这些对象

遍历这个IOC容器,获取到每一个类的实例,判断里面是有有依赖其他的类的实例,然后进行递归注入

什么是字节码?好处是什么

Java中的编译器和解释器:

Java中引入了虚拟机的概念,即在机器和编译直接加入一层程序的虚拟的机器,这台虚拟的机器在任何平台上都提供给程序一个共同的接口

编译程序只需要面向虚拟机,生成虚拟机能够理解的代码,然后由解释器来将虚拟机代码转换为特定系统的机器码执行。在Java中,这种虚拟机理解的代码叫做字节码(即扩展名为.class的文件),它不面向任何特定的处理器,只面向虚拟机

每一种平台的解释器是不同的,但是实现的虚拟机是相通的。Java源程序结果编译器编译后变成字节码,字节码有虚拟机解释执行,虚拟机将每一条要执行的字节码送给解释器,解释器将其翻译成特定机器上的机器码,然后在特定的机器上运行,这也就是解释了Java的编译与解释并存的特点

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-Hq4yp5RP-1621048281561)(e:/typora/class.png)]

好处:

Java语言通过字节码的方式,在一定程度上解决了传统解释型语言效率低的问题,同时又保留解释型语言可移植的特点。所以Java程序运行时比较高效,而且,由于字节码并不专对一种特定的机器,因此Java程序无需重新编译便可在多种不同的计算机上运行

Java类加载器有哪些

JDK自带有三个类加载器:BootstrapClassLoader、ExtClassLoader、AppClassLoader

BootstrapClassLoader:是ExtClassLoader的父类加载器,默认负责加载%JAVA_HOME%/lib下的jar包和class文件

ExtClassLoader:是AppClassLoader的父类加载器,负责加载%JAVA_HOME%/lib/ext文件夹下的jar包和class类

AppClassLoader:是自定义类加载器的父类,负责加载classpath下的类文件,系统类加载器,线程上下文加载器

继承ClassLoader实现自定义类加载器

双亲委托模型

好处

  • 主要是为了安全性,避免用户自己编写的类动态替代Java的一些核心类,比如String
  • 同时也避免了类的重复加载,因为JVM中区分不同类,不仅仅是根据类名,相同的class文件被不同的ClassLoader加载就是两个不同的类

Java异常体系

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-cH1MeExo-1621048281565)(E:/typora/exception.png)]


Java中的所有异常都来自顶级父类Throwable


Throwable下有两个子类Error和Exception


Error是抽象无法处理的错误,一旦出现这个错误,则程序将被迫停止运行


Exception不会导致程序停止,又分为两个部分RuntimeException运行时异常和CheckedException检查异常


RuntimeException常常发生在程序运行过程中,会导致程序当前线程执行失败。


CheckedException常常发生在程序编译过程中,会导致程序编译不通过


GC如何判断对象可以被回收

引用计数算法:为每一个对象创建一个引用计数器,每当一个地方引用改对象,计数器加一,当对象引用失效时,计数器减一,当计数器为0时可以被回收,这个算法的缺点是对象相互循环引用,计数器永远不为0

可达性分析算法:一个GC Roots对象为起点,从这些节点向下搜索,这个路径被称为引用链,当对象没有被引用链相连时,该对象是不可用的,那么该虚拟机就判断是可回收对象

GC Roots的对象有:


虚拟机栈(栈帧中的本地变量表)中引用的对象

方法区中类静态属性引用的对象

方法区中常量引用的对象

本地方法栈中JNI(即一般为Native方法)引用的对象

可达性算法中的不可达对象并不是立即死亡的,对象拥有一次自我拯教的机会。对象被系统宣告死亡至少要经历两次标记过程:第一次是经过可达性分析发现没有与GC Roots相连接的引用链,第二次是在由虚拟机自动建立的finalize队列中判断是否需要执行finalize()方法。


当对象变成(GC Roots)不可达时,GC会判断该对象是否覆盖了finalize方法,若未覆盖,则直接将其回收。否则,若对象未执行过finalize方法,将其放入F-Queue队列,由一低优先级线程执行该队列中对象的finalize方法。执行finalize方法完毕后,GC会再次判断该对象是否可达,若不可达,则进行回收,否则,对象"复活“


每个对象只能触发一次finalize()方法


由于finalize()方法运行代价高昂,不确定性大,无法保证各个对象的调用顺序,不推荐大家使用,建议遗忘它。


回收对象有无用的类(①.这个类的实现对象已被回收,②.这个类的Class对象不能反射调用获取其方法属性,③.这个类ClassLoader对象已被回收)、废弃的字符串


线程的生命周期及状态

线程通常有五种状态:创建、就绪、运行、阻塞和死亡状态


阻塞又分三种情况


(1). 等待阻塞:运行时线程执行wait()方法,该线程会释放占用的所有资源,JVM会把该线程放入“等待池”中,进入这个状态后,是不能自动唤醒的,必须依靠其他线程调用notify()或notifyAll()方法才能被唤醒,wait()是Object类的方法


(2). 同步阻塞:运行的线程在获取对象的同步锁时,若该同步锁被别的线程占用,则JVM会把该线程放入“锁池”中


(3). 其他阻塞:运行的线程执行sleep()或join()方法,或者发出了I/O请求时,JVM会把该线程置为阻塞状态。当sleep()状态超时、join()等待线程终止或者超时、或者I/O处理完毕是,线程重新转入就绪状态。sleep()是Thread类的方法


新建状态(New):新创建了一个线程对象


就绪状态(Runnable):线程对象创建后,其他线程调用了该对象的start方法。该状态的线程位于可运行线程池中,变得可运行,等待获取CPU的所有权


运行状态(Running):就绪状态的线程获取了CPU,执行程序代码


阻塞状态(Blocked):阻塞状态是线程因为某种原因放弃CPU所有权,暂时停止运行,直到线程进入就绪状态,才有机会转到运行状态


死亡状态(Dead):线程执行完了或者因异常退出了run方法,该线程结束生命周期


sleep()、wait()、join()、yield()的区别

锁池


所有需要竞争同步同步锁都会放在锁池当中,比如当前对象的语句被其中一个线程得到,则其他线程需要在这个锁池进行得到,当前面是线程释放同步锁后,锁池中的线程去竞争同步锁,当某个线程得到后会进入就绪队列等待CPU资源分配


等待池


当我们调用wait()方法后,线程会放到等待池当中,等待池的线程是不会去竞争同步锁,只有调用了notify()或notifyAll()后等待池的线程才会去竞争锁。notify()是随机从等待池选出一个线程放到锁池,而notifyAll()是将等待池的所有线程放到锁池当中


sleep()是Thread类的静态本地方法,wait()则是Object类的本地方法


sleep()方法不会释放lock,但是wait()会释放,而且会加入到等待队列中


sleep()就是把CPU的执行资格和执行权释放出去,不再运行此线程,当定时时间结束再取回CPU资源,参与CPU的调度,获取到CPU资源后就可以继续运行了。而如果sleep时该线程有锁,那么sleep不会释放这个锁,而是把锁带着进入了冻结状态,也就是说其他需要这个锁的线程根本不可能获取到这个锁。也就是说无法执行程序。如果在睡眠期间其他线程调用了这个线程的interrupt方法,那么这个线程也会抛出InterruptException异常返回,这点和wait是一样的。


sleep()方法不依赖于同步器synchronized,但是wait()需要依赖synchronized关键字


sleep()不需要唤醒(休眠之后推出阻塞),但wait()需要(不指定时间需要被别人中断)


sleep()一般用于当前线程休眠,或者轮询暂停操作,wait()则多用于多线程之间的通信


sleep()会让出CPU执行时间且强制上下文切换,而wait()则不一定,wait()后可能还有机会重新竞争到锁继续执行的


yield()执行后线程直接加入就绪状态,马上释放了CPU的执行权,但是依然保留了CPU的执行资格,所以有可能CPU下次进行线程调度还会让这个线程获取到执行权继续执行


join()执行线程后进入阻塞状态,例如在线程B中调用线程A的join(),则线程B会进入到阻塞队列,直到线程A结束或中断线程

public static void main(String[] args) throws InterruptException{
    Thread t1 = new Thread(new Runnable(){
        public void run(){
            try{
                Thread.sleep(3000);
            } catch(InterruptException e){
                e.printStackTrance();
            }
            System.out.println("222222222222");
        }
    });
    t1.start();
    t1.join();
    System.out.println("1111111111111");
}

对线程安全的理解

不是线程安全,应该是内存安全,堆是共享内存,可以被所有线程访问


当多个线程访问一个对象是,如果不用进行例外的同步控制的协调操作,调用这个对象的行为都可以获得正确的结果,就说这个对象是线程安全的


堆是进程和线程共有的空间,分全局堆和局部堆。全局堆就是所有没有分配的全间,局部堆就是用户分配的全间。堆在操作系统对进程初始化的时候分配,运行过程中也可以向系统要额外的堆,但是用完了要还给操作系统,要不然就是内存泄漏。


在Java中,堆是Java虚拟机所管理的内存中最大的一块,是所有线程共享的一块内存区域,在虚拟机启动时创建。堆所存在的内存区域的唯一目的就是存放对象实例,几乎所有的对象实例以及数组都在这里分配内存。


栈是每个线程独有的,保存其运行状态和局部自动变量的。栈在线程开始的时候初始化,每个线程的栈互相独立,因此,栈是线程安全的。操作系统在切换线程的时候会自动切换栈。栈空间不需要在高级语言里面显式的分配和释放。


目前主流操作系统都是多任务的,即多个进程同时运行。为了保证安全,每个进程只能访问分配给自己的内存空间,而不能访问别的进程的,这是由操作系统保障的。


在每个进程的内存空间中都会有一块特殊的公共区域,通常称为堆(内存)。进程内的所有线程都可以访问到该区域。这就是造成问题的潜在原因。


Thread和Runable的区别

Thread和Runable的实际是继承关系,没有可比性。无论使用Runable还是Thread,都会new Thread,然后执行run()方法,用法上,如果有复杂的线程操作需求,那就选中继承Thread,如果只是简单的执行一个任务,那就实现Runable


对守护线程的理解

守护线程:为所有非守护线程提供服务的线程;任何一个守护线程都是整个JVM中所有非守护线程的保姆;


守护线程类似于整个进程的一个默默无闻的小喽喽;它的生死无关重要,它却依赖整个进程而运行;哪天其他线程结束了,没有要执行的了,程序就结束了,理都没理守护线程,就把它中断了;


注意:由于守护线程的终止是自身无法控制的,因此千万不要把IO、File等重要操作逻辑分配给它;因为它不靠谱;


守护线程的作用是什么?


举例,GC垃圾回收线程:就是一个经典的守护线程,当我们的程序中不再有任何运行的Thread,程序就不会再产生垃圾,垃圾回收器也就无事可做,所以当垃圾回收线程是JVM上仅剩的线程时,垃圾回收线程会自动离开。它始终在低级别的状态中运行,用于实时监控和管理系统中的可回收资源。


应用场景:(1)来为其它线程提供服务支持的情况;(2)或者在任何情况下,程序结束时,这个线程必须正常且立刻关闭,就可以作为守护线程来使用;反之,如果一个正在执行某个操作的线程必须要正确地关闭掉否则就会出现不好的后果的话,那么这个钱程就不能是守护线程,而是用户线程。通常都是些关键的事务,比方说,数据库录入或者更新,这些操作都是不能中断的。


thread.setDaemon(true)必须在thread.start()之前设置,否则会跑出一个llegalThreadStateException异常。你不能把正在运行的常规线程设置为守护线程。


在Daemon线程中产生的新线程也是Daemon的。


守护线程不能用于去访问固有资源,比如读写操作或者计算逻辑。因为它会在任何时候甚至在一个操作的中间发生中断。


Java自带的多线程框架,比如ExecutorService,会将守护线程转换为用户线程,所以如果要使用后台线程就不能使用Java的线程池


ThreadLocal的原理和使用场景

每一个Thread对象均含义一个ThreadLocalMap类型的成员变量threadLocals,它存储本线程中所有ThreadLocal对象及其对应的值


ThreadLocalMap由一个个Entry对象构成


Entry继承自WeakReference<ThreadLocal<>>,一个Entry由ThreadLocal对象和Object构成,由此可见,Entry的key是ThreadLocal对象,并且是一个弱引用。当没指向key的强引用后,该key就会被垃圾收集器回收


当执行set()方法时,ThreadLocal首先会获得当前线程的ThreadLocalMap对象,再以当前ThreadLocal对象为key,将值存储到ThreadLocalMap对象中


get()方法执行过程类似,ThreadLocal首先会获取当前线程的ThreadLocalMap对象再以当前 ThreadLocal对象作为key,获取对象的value


由于每一条线程均含有各自私有的ThreadLocalMap容器,这些容器相互独立互不影响,因此不会存在线程安全性问题,从而也无需使用同步机制来保证多条线程访问容器的互斥性


使用场景:


在进行对象跨层传递的时候,使用ThreadLocal可以避免多次传递,打破层次间的约束

线程间数据隔离

进行事务操作,用于存储线程事务信息

数据库连接,Session会话管理

spring框架在事务开始i时会给当前线程绑定一个Jdbc connection ,在整个事务过程都是使用该线程绑定的connection来执行数据库操作,实现了事务的隔离性。Spring板架里面就是用的ThreadLocal来实现这种隔离

Redis面试题集

Redis


Spring是什么

轻量级的开源的J2EE框架。它是一个容器框架,用来装javabean (Java对象),中间层框架(万能胶)可以起一个连接作用,比如说把struts和hibernate粘合在一起运用,可以让我们的企业开发更快、更简洁


Spring是一个轻量级的控制反转( loC)和面向切面(AOP)的容器框架


从大小与开销两方面而吉Spring都是轻量级的。

通过控制反转(IoC)的技术达到松耦合的目的

提供了面向切面编程的丰富支持,允许通过分离应用的业务逻辑与系统级服务进行内聚性的开发–包含并管理应用对象(Bean)的配置和生命周期,这个意义上是一个容器。

将简单的组件配置、组合成为复杂的应用,这个意义上是一个框架。

谈谈对AOP的理解

系统是由许多不同的组件所组成的,每一个组件各负责一块特定功能。除了实现自身核心功能之外,这些组件还经常承担着额外的职责。例如日志、事务管理和安全这样的核心服务经常融入到自身具有核心业务逻辑的组件中去。这些系统服务经常被称为横切关注点,因为它们会跨越系统的多个组件。


当我们需要为分散的对象引入公共行为的时候,OOP则显得无能为力。也就是说,OOP允许你定义从上到下的关系,但并不适合定义从左到右的关系。例如日志功能。


日志代码往往水平地散布在所有对象层次中,而与它所散布到的对象的核心功能毫无关系。


在OOP设计中,它导致了大量代码的重复,而不利于各个模块的重用。


AOP:将程序中的交叉业务逻辑(比如安全,日志,事务等),封装成一个切面,然后注入到目标对象(具体业务逻辑)中去。AOP可以对某个对象或某些对象的功能进行增强,比如对象中的方法进行增强,可以在执行某个方法之前额外的做一些事情,在某个方法执行之后额外的做一些事情


对IOC的理解

IOC容器

就是一个map(key,value),里面存的是各种对象(在xml里配置的 bean节点、@Repository、@Service、@Controller、@Component),在项目启动的时候会读取配置文件里面的bean节点,根据全限定类名使用反射创建对象放到map里、扫描到打上上述注解的类还是通过反射创建对象放到map里


这个时候map里就有各种对象了,接下来我们在代码里需要用到里面的对象时,再通过DI注入(@Autowired、@Resource等注解,xml里bean节点内的ref属性,项目启动的时候会读取xml节点ref属性根据id注入,也会扫描这些注解,根据类型绘制id注入;id就是对象名)


控制反转

没有引入IOC容器之前,对象A依赖于对象B,那么对象A在初始化或者运行到某一点的时候,自己必须主动去创建对象B或者使用已经创建的对象B。无论是创建还是使用对象B,控制权都在自己手上。


引入IOC容器之后,对象A与对象B之间失去了直接联系,当对象A运行到需要对象B的时候,IOC容器会主动创建一个对象B注入到对象A需要的地方。


通过前后的对比,不难看出来:对象A获得依赖对象B的过程,由主动行为变为了被动行为,控制权颠倒过来了,这就是"控制反转"这个名称的由来。


全部对象的控制权全部上缴给"第三方"IOC容器,所以,IOC容器成了整个系统的关键核心,它起到了一种类似粘合剂"的作用,把系统中的所有对象粘合在一起发挥作用,如果没有这个粘合剂”,对象与对象之间会彼此失去联系,这就是有人把lOC容器比喻成"粘合剂"的由来。


依赖注入

“获得依赖对象的过程被反转了”,控制被反转之后,获得依赖对象的过程有自身管理变为了由IOC容器主动注入,依赖注入是实现IOC的方法,就是由IOC容器在运行期间,动态地将某种依赖关系注入到对象之中


BeanFactory和ApplicationContext有什么区别

ApplicationContext是BeanFactory的子接口ApplicationContext提供了更完整的功能:


①继承MessageSource,因此支持国际化。


②统一的资源文件访问方式。


③提供在监听器中注册bean的事件。


④同时加载多个配置文件。


⑤载入多个(有继承关系)上下文,使得每一个上下文都专注于一个特定的层次,比如应用的web层。


BeanFactory采用的是延迟加载形式来注入Bean的,即只有在使用到某个Bean时(调用getBean()),才对该Bean进行加载实例化。这样,我们就不能发现一些存在的Spring的配置问题。如果Bean的某一个属性没有注入,BeanFactory加载后,直至第一次使用调用getBean方法才会抛出异常。

ApplicationContext,它是在容器启动时,一次性创建了所有的Bean。这样,在容器启动时,我们就可以发现Spring中存在的配置错误,这样有利于检查所依赖属性是否注入。ApplicationContext启动后预载入所有的单实例Bean,通过预载入单实例bean ,确保当你需要的时候,你就不用等待,因为它们已经创建好了。

相对于基本的BeanFactory,ApplicationContext唯一的不足是占用内存空间。当应用程序配置Bean较多时,程序启动较慢。

BeanFactory通常以编程的方式被创建,ApplicationContext还能以声明的方式创建,如使用ContextLoader

BeanFactory和ApplicationContext都支持BeanPostProcessor,BeanFactoryPostProcessor的使用,但两者的区别是:BeanFactory需要手动注册,ApplicationContext则是自动在注册

SpringMVC工作流程

用户发送请求至前端控制器DispatcherServlet。

DispatcherServlet 收到请求调用HandlerMapping处理器映射器。

处理器映射器找到具体的处理器(可以根据xml 配置、注解进行查找),生成处理器及处理器拦截器(如果有则生成)一并返回给DispatcherServlet。

DispatcherServlet调用HandlerAdapter处理器适配器。

HandlerAdapter经过适配调用具体的处理器(Controller,也叫后端控制器)

Controller执行完成返回ModelAndView。

HandlerAdapter将controller 执行结果ModelAndView返回给DispatcherServlet。

DispatcherServlet将ModelAndView传给ViewReslover视图解析器。

ViewReslover解析后返回具体View。

DispatcherServlet根据View进行渲染视图(即将模型数据填充至视图中)。

DispatcherServlet响应用户。


MySQL执行计划怎么看

执行计划就是SQL的执行查询的顺序,以及如何使用索引查询,返回的结果集的行数

explain select * from A where X = ? and Y = ?;


id:是一个有顺序的编号,是查询的顺序号,有几个select就显示几行。id的顺序是按select出现的顺序增长的。id列的值越大执行优先级越高越先执行,id列的值则从上往下执行,id列的值为null最后执行

select_type:表示查询中每个select子句的类型

SIMPLE:表示此查询不包含union查询或者子查询

PRIMARY:表示此查询是最外层的查询(包含子查询)

SUBQUERY:子查询中的第一个select

UNION:表示此查询是union的第二或随后的查询

DEPENDENT UNION:union中的第二个或后面的查询语句,取决于外面的查询

UNION RESULT:union的结果

DEPENDENT SUBQUERY:子查询中的第一个select,取决于外面的查询,即子查询游览与外层查询的结果

DERIVED:衍生,表示导出表的select(from子句的子查询)

table:表示该语句查询的表

type:优化SQL的重要字段,也是我们判断SQL性能和优化程度的重要指标。取值范围:

const:通过索引一次命中,匹配一行数据

system:表中只有一行记录,相当于系统表

eq_ref:唯一性索引扫描,对于每个索引键,表中只有一条记录与之匹配

ref:非唯一性索引扫描,返回匹配某个值的所有

range:只检索给定范围的行,使用一个索引来选择行,一般用于between、<、>

index:只遍历索引树

ALL:表示全表扫描,这个类型的查询是性能最差的查询之一。那么基本就是随着表的数量增多,执行效率越慢

执行效率:ALL < index < range < ref < eq_ref < const < system。最好避免ALL和index

possible_keys:它表示MySQL在执行该SQL语句的时候,可能用到的索引信息,仅仅是可能,实际不一定会用到

key:此字段是MySQL在当前查询时所真正使用到的索引。它是possible_keys的子集

key_len:表示查询优化器使用了索引的字节数,这个字段可以评估组合索引是否完全被使用,这也是我们优化SQL时,评估索引的重要指标

rows:MySQL查询优化器根据统计信息,估算该SQL返回结果集需要扫描读取的行数,这个值至关重要,索引优化之后,扫描读取的行数越多,说明索引设置不对,或者字段传入的类型之类的问题,说明要优化空间越大

filtered:返回结果的行占需要读到的行(rows列的值)的百分比,就是百分比越高,说明需要查询到数据越准确,百分比越小,说明查询到的数据量大,而结果集很小

extra

using filesort:表示MySQL对结果集进行外部排序,不能通过索引顺序达到排序效果。一般有using filesort都建议优化去掉,因为这样的查询CPU资源消耗大,延时大

using index:覆盖索引扫描,表示查询在索引树中就可查找所需数据,不用扫描表数据文件,往往扫描性能不错

using temporary:查询有使用临时表,一般出现于排序,分组和多表join的情况,查询效率不高,建议优化

using where:SQL使用了where过滤,效率较高

事务的基本特性和隔离级别

事务基本特性ACID

原子性指的是一个事务中的操作要么全部成功,要么全部失败

一致性指的是数据库总是从一个一致性的状态转换到另一个一致性的状态。比如A转账给B100块钱,假设A只有90块,支付之前我们数据库里的数据都是符合约束的,但是如果事务事务执行成功了,我们的数据库数据就破坏约束了,因此事务不能成功,这里我们说事务提供看一致性的保证

隔离性指的是一个事务的修改在最终提交前,对其它事务是不可见的

持久性指的是一旦事务提交,所做的修改就会永久保存到数据库中

隔离级别

read uncommitted:读未提交,可能会读到其它事务未提交的数据,也叫做脏读

用户本来应该读取到id=1的用户age应该是10,结果读取到了其它事务还没有提交的事务,结果读取结果age=2-,这就是脏读

read committed:读已提交,这是Oracle的默认级别, 两次读取结果不一致,叫做不可重复读

不可重复读解决了脏读的问题 ,它只会读取已经提交的事务

用户开启事务读取id=1的用户,查询到age=10,再次读取发现结果age=20,在同一个事务里同一个查询读取到不同的结果叫做不可重复读

repeatable read:可重复读,这是MySQL的默认级别,就是每次读取结果都应用,但是有可能产生幻读

serializable:可串行化,一般是不会使用的,它会给每一行读取的数据加锁,会导致大量超时和锁竞争的问题

脏读(Drity Read):某个事务已更新一份数据,另一个事务在此时读取了同一份数据,由于某些原因,前一个RollBack了操作,则后一个事务所读取的数据就会是不正确的。

不可重复读(Non-repeatable read):在一个事务的两次查询之中数据不一致,这可能是两次查询过程中间插入了一个事务更新的原有的数据。

幻读(Phantom Read):在一个事务的两次查询中数据笔数不一致,例如有一个事务查询了几列(Row)数据,而另一个事务却在此时插入了新的几列数据,先前的事务在接下来的查询中,就会发现有几列数据是它先前所没有的。


目录
相关文章
|
6天前
|
Java 数据库连接 数据库
spring--为web(1),富士康java面试题整理
spring--为web(1),富士康java面试题整理
|
1天前
|
消息中间件 存储 Java
Java分布式技术面试总结(全面,实时更新)
Java分布式技术面试总结(全面,实时更新)
|
1天前
|
监控 Java Nacos
Java微服务框架面试总结(全面,实时更新)
Java微服务框架面试总结(全面,实时更新)
|
1天前
|
缓存 NoSQL Redis
Java技术栈Redis面试总结(全面,实时更新)
Java技术栈Redis面试总结(全面,实时更新)
|
1天前
|
存储 算法 安全
Java高级进阶面试总结(全面,实时更新)
Java高级进阶面试总结(全面,实时更新)
|
3天前
|
数据采集 Java 数据挖掘
最新Python+OpenCV+dlib汽车驾驶员疲劳驾驶检测!,2024年最新网易云java面试
最新Python+OpenCV+dlib汽车驾驶员疲劳驾驶检测!,2024年最新网易云java面试
最新Python+OpenCV+dlib汽车驾驶员疲劳驾驶检测!,2024年最新网易云java面试
|
3天前
|
机器学习/深度学习 数据采集 Java
2024年9(2),2024年最新大厂面试题java
2024年9(2),2024年最新大厂面试题java
2024年9(2),2024年最新大厂面试题java
|
6天前
|
移动开发 前端开发 JavaScript
Java和web前端,IT新人该如何选择?,2024年最新Web前端内存优化面试
Java和web前端,IT新人该如何选择?,2024年最新Web前端内存优化面试
|
6天前
|
安全 Java 数据库
Spring boot 入门教程-Oauth2,java面试基础题核心
Spring boot 入门教程-Oauth2,java面试基础题核心
|
6天前
|
Java
Java中int[]与Integer[]相互转化的方法,java基础知识面试重点总结
Java中int[]与Integer[]相互转化的方法,java基础知识面试重点总结