1. 接口和抽象类的区别
2. 数据库中的搜索引擎介绍一下
3. 说说你对spring的理解
4. 输入一个url,,具体到 spring中是怎样处理的呢,返回呢
解:springmvc的执行流程
5. hashmap concurrentHashMap hashtable hashset
6. ioc aop的理解
7. 给你一个表,,然后,,sql有点慢,,,请你进行优化。
解: 建索引
反问,,,后面还有 两面 视频面试 + hr面试
应该就这么多,,,,我具体记不得了。
JMM
volatile
单例模式
JVM运行时数据区
B+树和索引原则
AOP 原理和源码(源码不太了解)
链表
缓存机制和原理(不太了解)
红黑树
HashMap
Linux 命令(不太熟悉linux)
get和post区别,这个问得比较深,后面有些没答上来; 解:1、GET和POST本质上就是TCP链接,并无差别。但是由于HTTP的规定和浏览器/服务器的限制,导致他们在应用过程中体现出一些不同。
2、GET产生一个TCP数据包;POST产生两个TCP数据包。(视浏览器)
3、url可见性:get,参数url可见,post,url参数不可见
4、传输数据的大小:get一般传输数据大小不超过2k-4k,post请求传输数据的大小根据php.ini 配置文件设定,也可以无限大
5、数据传输上:get,通过拼接url进行传递参数,post,通过body体传输参数
6、后退页面的反应:get请求页面后退时,不产生影响,post请求页面后退时,会重新提交请求
7、缓存性:get请求是可以缓存的,post请求不可以缓存
8、GET请求只能进行url编码,而POST支持多种编码方式
9、GET只接受ASCII字符的参数的数据类型,而POST没有限制
解:索引的概念:简单来说,索引就是一个指针,指向表里的数据。索引通常与相应的表时分开保存的,目的是提高检索的性能。索引的创建与删除不会影响数据本身,但会影响数据检索的速度。索引也会占据物理存储空间,可能比表本身还大,因此创建索引也要考虑存储空间。
简单的说:索引相当于字典的音序表,如果要查某个字,如果不使用音序表,则需要从几百页中逐页去查。
强调:一旦为表创建了索引,以后的查询最好先查索引,再根据索引定位的结果去找数据
联合索引时指对表上的多个列合起来做一个索引,省的你查询的时候,where后面的条件字段一直再变,你就想给每个字段加索引的尴尬问题。联合索引的创建方法与单个索引的创建方法一样,不同之处在仅在于有多个索引列.
注意建立联合索引的一个原则:索引是有个最左匹配的原则的,所以建联合索引的时候,将区分度高的放在最左边,依次排下来,范围查询的条件尽可能的往后边放。
联合索引的第二个好处是在第一个键相同的情况下,已经对第二个键进行了排序处理,例如在很多情况下应用程序都需要查询某个用户的购物情况,并按照时间进行排序,最后取出最近三次的购买记录,这时使用联合索引可以帮我们避免多一次的排序操作,因为索引本身在叶子节点已经排序了
彩蛋:覆盖索引:InnoDB存储引擎支持覆盖索引(covering index,或称索引覆盖),即从辅助索引中就可以得到查询记录,而不需要查询聚集索引中的记录。
https://www.cnblogs.com/fengqiang626/p/11459434.html#1介绍
线程池 线程的创建集成Thread,实现Runnable ,使用Callable和Future创建线程,使用线程池。
了解spring吗了解servlet吗java I/O相关的简单介绍一下关于maven说一下Hashmap了解吗 那你说说关于红黑树Java 反射机制解:概念:首先大家应该先了解两个概念,编译期和运行期,编译期就是编译器帮你把源代码翻译成机器能识别的代码,比如编译器把java代码编译成jvm识别的字节码文件,而运行期指的是将可执行文件交给操作系统去执行,JAVA反射机制是在运行状态中,对于任意一个类,都能够知道这个类的所有属性和方法;对于任意一个对象,都能够调用它的任意方法和属性;这种动态获取信息以及动态调用对象方法的功能称为java语言的反射机制 简单说,反射机制值得是程序在运行时能够获取自身的信息。在java中,只要给定类的名字,那么就可以通过反射机制来获得类的所有信息
使用反射的前提:
必须先要获取到该类的字节码文件对象,即Class类型对象。关于Class描述字节码文件如下图所示:
说明:
1)Java中使用Class类表示某个class文件.
2)任何一个class文件都是Class这个类的一个实例对象.
网络层次 HTTP TCP对称加密 非对称加密(可能我是安全专业?)乐观锁 悲观锁设计模式 说几种
1. 为什么Java中要有两种异常,哪种异常更容易处理,实践中该怎么做?
解:检查型异常:除了运行异常外,其他异常都属于受检查异常,这种异常的特点是要么用try...catch捕获处理,要么用throws语句声明抛出,否则编译不会通过。
运行时异:运行异常的特点是Java编译器不去检查它,也就是说,当程序中可能出现这类异常时,即使没有用try...catch语句捕获它,也没有用throws字句声明抛出它,还是会编译通过。出现运行时异常后,如果没有捕获处理这个异常(即没有catch),系统会把异常一直往上层抛,一直到最上层,如果是多线程就由Thread.run()抛出,如果是单线程就被main()抛出。抛出之后,如果是线程,这个线程也就退出了
对于受检查的异常,在编译的时候不能通过,就只能老老实实的用throw或者try..catch...来处理,那么用什么情况下用那种方式呢?看下面的例子:
public static Connection getConn(){ try { Connection conn = DriverManager.getConnection(url,user,pwd); return conn; } catch (SQLException e) { return null; } }明显的出现一个问题就是,出现了受检查的异常直接用try...catch...捕获,可是这种捕获有作用么,基本上没有什么有用的意义,还不如抛出去的好直接告诉调用者,这个方法会出现SQLException,不然调用者用这个方法结果返回了一个null,就完全不知道发生了什么事。
其实在try...catch...语句块中,一般的受检查异常是很难在自己方法内处理的,那么如果出现了异常怎么办,一般的做法是在catch语句块中庸log4j记录异常,给后台工作人员。
异常处理的原则上是,能自己处理的不往上抛,尽量把异常细化不要什么异常都直接用Exception来代,在一个try语句块中尽量少的异常。
2. springboot用过没?(用过一点)看过哪些框架的源码?(springmvc看过一点)注解的实现机制是什么?springmvc的过滤器使用了什么设计模式(责任链模式)?
解:
过滤器:
过滤器使用filter实现,拦截的是request请求,基于回调,基于servlect规范
依赖容器,有初始化方法和销毁方法,拦截的是地址,粒度很大
过滤器Filter:过滤器通过实现Filter接口,实现了过滤器的三个方法,分别是初始化方法,dofilter方法和销毁方法,随着容器的启动和销毁而初始化和销毁,依赖于servlet容器,过滤器拦截的是地址栏请求,过滤器是在进入容器后执行的servlet之前后执行,针对的在处理业务之前的操作。
拦截器:
是基于Java的jdk动态代实现的,实现HandlerInterceptor接口。不依赖于servlet容器,
拦截器针对于contraller方法,并且能获取到所有的类,对类里面所有的方法实现拦截,粒度更小,拦截器中可以注入service,也可以调用业务逻辑
拦截器于过滤器对比:
两者都是AOP编程思想的实现,都能够实现权限控制和日志记录等问题的处理,但是两者粒度不同拦截对象不一样
适用范围不同:Filter是servlet的规范,只能用于web程序,但是拦截器可以用于application等程序。
规范不同:Filter是servlet的规范。但是Interceptor是spring容器支撑,有spring框架支持。
使用资源不一样:spring的拦截器由于依赖spring,也是spring的一个组件,因此能够在拦截器中使用spring的任何资源和对象。例如service对象,数据源,事务管理等,通过ioc注入拦截器即可,而filter不能
粒度不同:Filter只能在servlet的前后起作用,而拦截器能在方法前后异常前后执行,更加灵活,粒度更小,spring框架程序优先使用拦截器。
3. 了解过多线程吗?ArrayList的线程安全版本java中有吗?如果让你设计一个线程安全的ArrayList,你会怎么设计?ConcurrentHashMap的键可以存储超大对象吗?(重点是超大)
解:可以但是不推荐吧。
4. ThreadLocal内存溢出了怎么办?
解:
先来讲解一下ThreadLocal的原理:
ThreadLocal并不是一个Thread,而是Thread的一个局部变量,也许把它命名为ThreadLocalVariable更容易让人理解一些。
当使用ThreadLocal维护变量时,ThreadLocal为每个使用该变量的线程提供独立的变量副本,所以每一个线程都可以独立地改变自己的副本,而不会影响其它线程所对应的副本。
ThreadLocal的的组成:
从上图可以看出:ThreadLocal公有的方法就四个,分别为:get、set、remove、intiValue:
ThreadLocal是如何做到为每一个线程维护变量的副本的呢?(重点)
其实实现的思路很简单:在ThreadLocal类中有一个static声明的Map即ThreadLocalMap,用于存储每一个线程的变量副本,Map中元素的键为线程对象,而值对应线程的变量副本。我们自己就可以提供一个简单的实现版本:
public class SimpleThreadLocal<T> { /** * Key为线程对象,Value为传入的值对象 */ private static Map<Thread, T> valueMap = Collections.synchronizedMap(new HashMap<Thread, T>()); /** * 设值 * @param value Map键值对的value */ public void set(T value) { valueMap.put(Thread.currentThread(), value); } /** * 取值 * @return */ public T get() { Thread currentThread = Thread.currentThread(); //返回当前线程对应的变量 T t = valueMap.get(currentThread); //如果当前线程在Map中不存在,则将当前线程存储到Map中 if (t == null && !valueMap.containsKey(currentThread)) { t = initialValue(); valueMap.put(currentThread, t); } return t; } public void remove() { valueMap.remove(Thread.currentThread()); } public T initialValue() { return null; } public static void main(String[] args) { SimpleThreadLocal<List<String>> threadLocal = new SimpleThreadLocal<>(); new Thread(() -> { List<String> params = new ArrayList<>(3); params.add("张三"); params.add("李四"); params.add("王五"); threadLocal.set(params); System.out.println(Thread.currentThread().getName()); threadLocal.get().forEach(param -> System.out.println(param)); }).start(); new Thread(() -> { try { Thread.sleep(1000); List<String> params = new ArrayList<>(2); params.add("Chinese"); params.add("English"); threadLocal.set(params); System.out.println(Thread.currentThread().getName()); threadLocal.get().forEach(param -> System.out.println(param)); } catch (InterruptedException e) { e.printStackTrace(); } }).start(); } }ThreadLocal源码分析:
上述自己实现的功能比较粗略,下面看看源码
1、线程局部变量在Thread中的位置
既然是线程局部变量,那么理所当然就应该存储在自己的线程对象中,我们可以从 Thread 的源码中找到线程局部变量存储的地方,我们可以看到线程局部变量是存储在Thread对象的 threadLocals 属性中,而 threadLocals 属性是一个 ThreadLocal.ThreadLocalMap 对象。ThreadLocalMap为ThreadLocal的静态内部类。
2、Thread和ThreadLocalMap的关系
Thread和ThreadLocalMap的关系,先看下边这个简单的图,可以看出Thread中的threadLocals就是ThreadLocal中的ThreadLocalMap:
到这里应该大致能够感受到上述三者之间微妙的关系,再看一个复杂点的图:
可以看出每个thread实例都有一个ThreadLocalMap。在上图中的一个Thread的这个ThreadLocalMap中分别存放了3个Entry,默认一个ThreadLocalMap初始化了16个Entry,每一个Entry对象存放的是一个ThreadLocal变量对象。
再简单一点的说就是:一个Thread中只有一个ThreadLocalMap,一个ThreadLocalMap中可以有多个ThreadLocal对象,其中一个ThreadLocal对象对应一个ThreadLocalMap中的一个Entry(也就是说:一个Thread可以依附有多个ThreadLocal对象)。 再看一张网络上的图片,应该可以更好的理解,如下图:
这里的Map其实是ThreadLocalMap。
3、ThreadLocalMap与WeakReference
ThreadLocalMap从字面上就可以看出这是一个保存ThreadLocal对象的map(其实是以它为Key),不过是经过了两层包装的ThreadLocal对象:
(1)第一层包装是使用 WeakReference<ThreadLocal<?>> 将ThreadLocal对象变成一个弱引用的对象;
(2)第二层包装是定义了一个专门的类 Entry 来扩展 WeakReference<ThreadLocal<?>>:
类 Entry 很显然是一个保存map键值对的实体,ThreadLocal<?>为key, 要保存的线程局部变量的值为value。super(k)调用的WeakReference的构造函数,表示将ThreadLocal<?>对象转换成弱引用对象,用做key。
4、ThreadLocalMap 的构造函数
可以看出,ThreadLocalMap这个map的实现是使用一个数组 private Entry[] table 来保存键值对的实体,初始大小为16,
总的来说,ThreadLocalMap是一个类似HashMap的集合,只不过自己实现了寻址,也没有HashMap中的put方法,而是set方法等区别。
由于每个thread实例都有一个ThreadLocalMap,所以在进行set的时候,首先根据Thread.currentThread()获取当前线程,然后根据当前线程t,调用getMap(t)获取ThreadLocalMap对象, 如果是第一次设置值,ThreadLocalMap对象是空值,所以会进行初始化操作,即调用createMap(t,value)方法:
即是调用上述的构造方法进行构造,这里仅仅是初始化了16个元素的引用数组,并没有初始化16个 Entry 对象。而是一个线程中有多少个线程局部对象要保存,那么就初始化多少个 Entry 对象来保存它们。
为什么要用 ThreadLocalMap 来保存线程局部对象呢?
原因是一个线程拥有的的局部对象可能有很多,这样实现的话,那么不管你一个线程拥有多少个局部变量,都是使用同一个 ThreadLocalMap 来保存的,ThreadLocalMap 中 private Entry[] table 的初始大小是16。超过容量的2/3时,会扩容。
然后在回到如果map不为空的情况,会调用map.set(this, value);方法,我们看到是以当前 thread 的引用为 key, 获得 ThreadLocalMap ,然后调用 map.set(this, value); 保存进 private Entry[] table :
从set(T value)方法一路跟下来,可以看到,set(T value)方法为每个Thread对象都创建了一个ThreadLocalMap,并且将value放入ThreadLocalMap中,ThreadLocalMap作为Thread对象的成员变量保存。那么可以用下图来表示ThreadLocal在存储value时的关系。
了解了set方法的大致原理之后,我们在研究一段程序如下:
/** * 三个ThreadLocal */ private static ThreadLocal<String> threadLocal1 = new ThreadLocal<>(); private static ThreadLocal<String> threadLocal2 = new ThreadLocal<>(); private static ThreadLocal<String> threadLocal3 = new ThreadLocal<>(); //线程池变量指定一个线程 ExecutorService executorService = Executors.newFixedThreadPool(1); executorService.execute(() -> { threadLocal1.set("123"); threadLocal2.set("234"); threadLocal3.set("345"); Thread t = Thread.currentThread(); System.out.println(Thread.currentThread().getName()); });这样的话就相当于一个线程依附了三个ThreadLocal对象,执行完最后一个set方法之后,调试过程如下:
可以看到table(Entry集合)中有三个对象,对象的值就是我们设置的三个threadLocal的对象值;
如果在修改一下代码,修改为两个线程:
/** * 三个ThreadLocal */ private static ThreadLocal<String> threadLocal1 = new ThreadLocal<>(); private static ThreadLocal<String> threadLocal2 = new ThreadLocal<>(); private static ThreadLocal<String> threadLocal3 = new ThreadLocal<>(); //线程池变量指定一个线程 ExecutorService executorService = Executors.newFixedThreadPool(THREAD_LOOP_SIZE); for (int i = 0; i < THREAD_LOOP_SIZE; i++) { executorService.execute(() -> { threadLocal1.set("123"); threadLocal2.set("234"); threadLocal3.set("345"); }); }这样的话,可以看到运行调试图如下:
然后更改到Thread2,查看,由于多线程,线程1运行到上图情况,线程2运行到下图情况,也可以看出他们是不同的ThreadLocalMap:
到这里应该可以清楚了的了解Thread、ThreadLocal和ThreadLocalMap之间的关系了!
经过上述set方法的分析,对于get方法应该理解起来轻松了许多,首先获取ThreadLocalMap对象,由于ThreadLocalMap使用的当前的ThreadLocal作为key,所以传入的参数为this,然后调用getEntry()方法,通过这个key构造索引,根据索引去table(Entry数组)中去查找线程本地变量,根据下边找到Entry对象,然后判断Entry对象e不为空并且e的引用与传入的key一样则直接返回,如果找不到则调用getEntryAfterMiss()方法。调用getEntryAfterMiss表示直接散列到的位置没找到,那么顺着hash表递增(循环)地往下找,从i开始,一直往下找,直到出现空的槽为止。
ThreadLocal 涉及到的两个层面的内存自动回收:
1)在 ThreadLocal 层面的内存回收:
当线程死亡时,那么所有的保存在的线程局部变量就会被回收,其实这里是指线程Thread对象中的 ThreadLocal.ThreadLocalMap threadLocals 会被回收,这是显然的。
2)ThreadLocalMap 层面的内存回收:
如果线程可以活很长的时间,并且该线程保存的线程局部变量有很多(也就是 Entry 对象很多),那么就涉及到在线程的生命期内如何回收 ThreadLocalMap 的内存了,不然的话,Entry对象越多,那么ThreadLocalMap 就会越来越大,占用的内存就会越来越多,所以对于已经不需要了的线程局部变量,就应该清理掉其对应的Entry对象。
使用的方式是,Entry对象的key是WeakReference 的包装,当ThreadLocalMap 的 private Entry[] table,已经被占用达到了三分之二时 threshold = 2/3(也就是线程拥有的局部变量超过了10个) ,就会尝试回收 Entry 对象,我们可以看到 ThreadLocalMap.set()方法中有下面的代码:
cleanSomeSlots 就是进行回收内存:
我们知道ThreadLocal变量是维护在Thread内部的,这样的话只要我们的线程不退出,对象的引用就会一直存在。当线程退出时,Thread类会进行一些清理工作,其中就包含ThreadLocalMap,Thread调用exit方法如下:
但是,当我们使用线程池的时候,就意味着当前线程未必会退出(比如固定大小的线程池,线程总是存在的)。如果这样的话,将一些很大的对象设置到ThreadLocal中(这个很大的对象实际保存在Thread的threadLocals属性中),这样的话就可能会出现内存溢出的情况。
一种场景就是说如果使用了线程池并且设置了固定的线程,处理一次业务的时候存放到ThreadLocalMap中一个大对象,处理另一个业务的时候,又一个线程存放到ThreadLocalMap中一个大对象,但是这个线程由于是线程池创建的他会一直存在,不会被销毁,这样的话,以前执行业务的时候存放到ThreadLocalMap中的对象可能不会被再次使用,但是由于线程不会被关闭,因此无法释放Thread 中的ThreadLocalMap对象,造成内存溢出。
也就是说,ThreadLocal在没有线程池使用的情况下,正常情况下不会存在内存泄露,但是如果使用了线程池的话,就依赖于线程池的实现,如果线程池不销毁线程的话,那么就会存在内存泄露。所以我们在使用线程池的时候,使用ThreadLocal要格外小心!
通过源代码可以看到每个线程都可以独立修改属于自己的副本而不会互相影响,从而隔离了线程和线程.避免了线程访问实例变量发生安全问题. 同时我们也能得出下面的结论:
(1)ThreadLocal只是操作Thread中的ThreadLocalMap对象的集合;
(2)ThreadLocalMap变量属于线程的内部属性,不同的线程拥有完全不同的ThreadLocalMap变量;
(3)线程中的ThreadLocalMap变量的值是在ThreadLocal对象进行set或者get操作时创建的;
(4)使用当前线程的ThreadLocalMap的关键在于使用当前的ThreadLocal的实例作为key来存储value值;
(5) ThreadLocal模式至少从两个方面完成了数据访问隔离,即纵向隔离(线程与线程之间的ThreadLocalMap不同)和横向隔离(不同的ThreadLocal实例之间的互相隔离);
(6)一个线程中的所有的局部变量其实存储在该线程自己的同一个map属性中;
(7)线程死亡时,线程局部变量会自动回收内存;
(8)线程局部变量时通过一个 Entry 保存在map中,该Entry 的key是一个 WeakReference包装的ThreadLocal, value为线程局部变量,key 到 value 的映射是通过:ThreadLocal.threadLocalHashCode & (INITIAL_CAPACITY - 1) 来完成的;
(9)当线程拥有的局部变量超过了容量的2/3(没有扩大容量时是10个),会涉及到ThreadLocalMap中Entry的回收;
对于多线程资源共享的问题,同步机制采用了“以时间换空间”的方式,而ThreadLocal采用了“以空间换时间”的方式。前者仅提供一份变量,让不同的线程排队访问,而后者为每一个线程都提供了一份变量,因此可以同时访问而互不影响。
1、先上模拟代码:
import java.util.ArrayList; import java.util.List; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; public class ThreadLocalOOMDemo { private static final int THREAD_LOOP_SIZE = 500; private static final int MOCK_DIB_DATA_LOOP_SIZE = 10000; private static ThreadLocal<List<User>> threadLocal = new ThreadLocal<>(); public static void main(String[] args) throws InterruptedException { ExecutorService executorService = Executors.newFixedThreadPool(THREAD_LOOP_SIZE); for (int i = 0; i < THREAD_LOOP_SIZE; i++) { executorService.execute(() -> { threadLocal.set(new ThreadLocalOOMDemo().addBigList()); Thread t = Thread.currentThread(); System.out.println(Thread.currentThread().getName()); //threadLocal.remove(); //不取消注释的话就可能出现OOM }); try { Thread.sleep(1000L); } catch (InterruptedException e) { e.printStackTrace(); } } //executorService.shutdown(); } private List<User> addBigList() { List<User> params = new ArrayList<>(MOCK_DIB_DATA_LOOP_SIZE); for (int i = 0; i < MOCK_DIB_DATA_LOOP_SIZE; i++) { params.add(new User("xuliugen", "password" + i, "男", i)); } return params; } class User { private String userName; private String password; private String sex; private int age; public User(String userName, String password, String sex, int age) { this.userName = userName; this.password = password; this.sex = sex; this.age = age; } } }2、设置JVM参数设置最大内存为256M,以便模拟出OOM
3、运行代码,输出结果:
1、首先看一下ThreadLocal的原理图:
Thread、ThreadLocal、ThreadLocalMap、Entry之间的关系:
上图中描述了:一个Thread中只有一个ThreadLocalMap,一个ThreadLocalMap中可以有多个ThreadLocal对象,其中一个ThreadLocal对象对应一个ThreadLocalMap中一个的Entry(也就是说:一个Thread可以依附有多个ThreadLocal对象)。
在ThreadLocal的生命周期中,都存在这些引用。看下图: 实线代表强引用,虚线代表弱引用。
2、ThreadLocal的实现是这样的:每个Thread 维护一个 ThreadLocalMap 映射表,这个映射表的 key 是 ThreadLocal实例本身,value 是真正需要存储的 Object。
3、也就是说 ThreadLocal 本身并不存储值,它只是作为一个 key 来让线程从 ThreadLocalMap 获取 value。值得注意的是图中的虚线,表示 ThreadLocalMap 是使用 ThreadLocal 的弱引用作为 Key 的,弱引用的对象在 GC 时会被回收。 4、ThreadLocalMap使用ThreadLocal的弱引用作为key,如果一个ThreadLocal没有外部强引用来引用它,那么系统 GC 的时候,这个ThreadLocal势必会被回收,这样一来,ThreadLocalMap中就会出现key为null的Entry,就没有办法访问这些key为null的Entry的value,如果当前线程再迟迟不结束的话,这些key为null的Entry的value就会一直存在一条强引用链:Thread Ref -> Thread -> ThreaLocalMap -> Entry -> value永远无法回收,造成内存泄漏。 5、其实,ThreadLocalMap的设计中已经考虑到这种情况,也加上了一些防护措施:在ThreadLocal的get(),set(),remove()的时候都会清除线程ThreadLocalMap里所有key为null的value。这一点在上一节中也讲到过!
6、但是这些被动的预防措施并不能保证不会内存泄漏
(1)使用static的ThreadLocal,延长了ThreadLocal的生命周期,可能导致内存泄漏。 (2)分配使用了ThreadLocal又不再调用get(),set(),remove()方法,那么就会导致内存泄漏,因为这块内存一直存在。
下面我们分两种情况讨论:
(1)key 使用强引用:引用的ThreadLocal的对象被回收了,但是ThreadLocalMap还持有ThreadLocal的强引用,如果没有手动删除,ThreadLocal不会被回收,导致Entry内存泄漏。
(2)key 使用弱引用:引用的ThreadLocal的对象被回收了,由于ThreadLocalMap持有ThreadLocal的弱引用,即使没有手动删除,ThreadLocal也会被回收。value在下一次ThreadLocalMap调用set、get、remove的时候会被清除。
比较两种情况,我们可以发现:由于ThreadLocalMap的生命周期跟Thread一样长,如果都没有手动删除对应key,都会导致内存泄漏,但是使用弱引用可以多一层保障:弱引用ThreadLocal不会内存泄漏,对应的value在下一次ThreadLocalMap调用set、get、remove的时候会被清除。
1、每次使用完ThreadLocal,都调用它的remove()方法,清除数据。
5. 数据库的联合索引用过没?如果索引列有a,b两列,a+b可以查到东西吗?
6. 常见的设计模式除了单例,工厂、代理还了解过哪些?(装饰者模式)java中哪些类使用了装饰者模式?如果要扩展一个类的功能除了装饰者还可以使用什么方法?
解:在io流中,比如 BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(new FileInputStream(new File("d:/text.txt"))));
其实无论是代理模式还是装饰模式。本质上我认为就是对原有对象增强的方式
一般来说,实现对象增强有三种方式:
继承 继承父类,子类扩展装饰器模式 使用“包装”的方式来增强对象代理模式 给女朋友讲解么是代理模式7. 了解过微服务吗?
8. 其实我想问你jvm的内存方面调优的问题,但你没了解过,那就算了吧...
9. 做到题吧,5分钟时间。题目是:给一个数,输出他的所有质数因子,例如180,输出2,2,3,3,5
static List<Integer> list=new ArrayList<>(); public static void main(String[] args) { Scanner sc=new Scanner(System.in); int anInt = sc.nextInt(); int i=1; while (i<=anInt) { if (anInt%i==0 && isZS(i) ) { list.add(i); } i++; } System.out.println(Arrays.toString(list.toArray())); } public static boolean isZS(int num){ double n=Math.sqrt(num); if (num==1 || num==2)return true; for (int i = 2; i <=n; i++) { if (num%i==0) { return false; } } return true; }这些基本都只是题目,关于答案大家自行查找
整体感受:全程被吊打😂。有些东西还是要实际操作过才行,光看理论是远远不够的。
此次面试也更加激发了自己的斗志。最后希望大家共同进步!
二面
1.自我介绍
2.spring aop 和ioc
3.aop中用了两种动态代理,区别是什么?
解:
spring 中 AOP是基于 “动态代理” 实现,其采用了两种方式:
java代理:采用java内置的代理API实现cglib代理:采用第三方API实现jdk动态代理要求被代理对象要实现接口。
cglib代理要求被代理类可以被继承。
彩蛋:静态代理和动态代理的区别?
静态代理:编译时将增强代码植入class文件,因为是编译期进行的增强,所以代码运行时效率比动态代理高。
动态代理:运行时生成代理类并加载,效率比静态代理要低
https://www.jianshu.com/p/9bcac608c714
4.请求到达spring MVC中是怎么处理的?
解:SpringMvc的执行流程。
5.mysql中事务隔离级别?
6.讲一下聚集索引,非聚集索引?
7.讲一下联合主键 QAQ数据库这些根本没看 orz
解复:合主键,一张表只能有一个主键,但根据需要,我们可以设置多个字段同时为主键,这就叫做复合主键。
CREATE TABLE IF NOT EXIST student( Name VARCHAR (30 ) COMMENT '姓名', Age INT(30) COMMENT '年龄', PRIMARY KEY(Name,Age) );
什么是联合主键? 说一下联合主键,联合主键其实就是中间表。在多对多模型里,需要两个表中的主键组成联合主键,这样就可以查到两个表中的每个数据。
(主键原则上是唯一的,别被唯一值所困扰。) 顾名思义就是多个主键联合形成一个主键组合 一个简单的例子 主键A跟主键B组成联合主键 主键A跟主键B的数据可以完全相同(困扰吧,没关系),联合就在于主键A跟主键B形成的联合主键是唯一的。 下例主键A数据是1,主键B数据也是1,联合主键其实是11,这个11是唯一值,绝对不充许再出现11这个唯一值。(这就是多对多关系) 主键A数据 主键B数据 1 1 2 2 3 3 主键A与主键B的联合主键值最多也就是 11 12 13 21 22 23 31 32 33
8.讲一下redis,说一下它的工作原理和架构
https://blog.csdn.net/weixin_42022924/article/details/105206574
https://blog.csdn.net/mysteryflower/article/details/94551679
9.讲一下rabbitmq,说一下它的工作原理和架构 这。。。。
10.数据库联表查询
11.不用循环思想,找数组中最大值。
如果有一亿条用户信息(各种字段)的数据,如何建表存到数据库中
解;走你
. String 和 char[] 数组谁更适合存密码
1、Nginx缓存了解吗?
解:
概念:利用请求的局部性原理,将请求过的内容在本地建立一个副本,下次访问时不再连接到后端服务器,直接响应本地内容。
Nginx服务器启动后,会对本地磁盘上的缓存文件进行扫描,在内存中建立缓存索引,并有专门的进程对缓存文件进行过期判断、更新等进行管理。
对于缓存,我们大概会有以下问题: (1)缓存文件放在哪儿? (2)缓存的空间大小是否可以限定? (3)如何指定哪些请求被缓存? (4)缓存的有效期是多久? (5)对于某些请求,是否可以不走缓存?
开启缓存:要使用缓存,首先要使用 proxy_cache_path 这个指令(必须放在 http 上下文的顶层位置),然后在目标上下文中使用 proxy_cache 指令
http { ... proxy_cache_path /data/nginx/cache keys_zone=one:10m; server { proxy_cache one; location / { proxy_pass http://localhost:8000; } } }proxy_cache_path 有两个必填参数,第一个参数为 缓存目录,第二个参数keys_zone指定缓存名称和占用内存空间的大小(注:示例中的10m是对内存中缓存内容元数据信息大小的限制,如果想限制缓存总量大小,需要用 max_size 参数)
proxy_cache 的参数为之前指定的缓存名称
缓存管理的相关进程:
(1)缓存管理器 定期检查缓存状态,看缓存总量是否超出限制,如果超出,就移除其中最少使用的部分 (2)缓存加载器 加载器只在nginx启动后运行一次,把缓存内容的元数据信息加载到内存空间,如果一次性加载全部缓存信息,会大量消耗资源,使nginx在启动后的几分钟里变慢,为避免此问题,有3种加载策略: loader_threshold – 指定每次加载执行的时间 loader_files – 每次最多加载的数量 loader_sleeps – 每次加载的延时
例如:proxy_cache_path /data/nginx/cache keys_zone=one:10m loader_threshold=300 loader_files=200;
更具体的
限流降级,有什么办法 Hystrix?常见的限流算法?
解:漏算桶法,令牌桶算法,计数法。
计数法的缺点:计数器方式的缺点是不能处理临界问题,或者说限流策略不够平滑。
springboot相比spring,有什么改进 spring jar包运行的原理(怎么找到main类,MANIFEST.MF)
解:先看一下:打完Jar包里的信息如下:
我们先来重点关注两个地方:META-INF下面的Jar包描述文件和BOOT-INF这个目录。
Manifest-Version: 1.0 Archiver-Version: Plexus Archiver Built-By: xxxx Start-Class: com.xxxx.AppServer Spring-Boot-Classes: BOOT-INF/classes/ Spring-Boot-Lib: BOOT-INF/lib/ Spring-Boot-Version: 2.1.6.RELEASE Created-By: Apache Maven 3.3.9 Build-Jdk: 1.8.0_73 Main-Class: org.springframework.boot.loader.JarLauncher在上面我们看到一个熟悉的配置Main-Class: org.springframework.boot.loader.JarLauncher。我们大概能猜到这个类是整个系统的入口。
再看下BOOT-INF这个目录下面,我们会发现里面是我们项目打出来的class文件和项目依赖的Jar包。看到这里,你可能已经猜到Spring Boot是怎么启动项目的了。
我们找到这个Main-Class的类,可以看到下面代码
public class JarLauncher extends ExecutableArchiveLauncher { static final String BOOT_INF_CLASSES = "BOOT-INF/classes/"; static final String BOOT_INF_LIB = "BOOT-INF/lib/"; public JarLauncher() { } protected JarLauncher(Archive archive) { super(archive); } @Override protected boolean isNestedArchive(Archive.Entry entry) { if (entry.isDirectory()) { return entry.getName().equals(BOOT_INF_CLASSES); } return entry.getName().startsWith(BOOT_INF_LIB); } public static void main(String[] args) throws Exception { //项目入口,重点在launch这个方法中 new JarLauncher().launch(args); } } //launch方法 protected void launch(String[] args) throws Exception { JarFile.registerUrlProtocolHandler(); //创建LaunchedURLClassLoader。如果根类加载器和扩展类加载器没有加载到某个类的话,就会通过LaunchedURLClassLoader这个加载器来加载类。这个加载器会从Boot-INF下面的class目录和lib目录下加载类。 ClassLoader classLoader = createClassLoader(getClassPathArchives()); //这个方法会读取jar描述文件中的Start-Class属性,然后通过反射调用到这个类的main方法。 launch(args, getMainClass(), classLoader); }总结:
Spring Boot可执行Jar包的入口点是JarLauncher的main方法;这个方法的执行逻辑是先创建一个LaunchedURLClassLoader,这个加载器加载类的逻辑是:先判断根类加载器和扩展类加载器能否加载到某个类,如果都加载不到就从Boot-INF下面的class和lib目录下去加载;读取Start-Class属性,通过反射机制调用启动类的main方法,这样就顺利调用到我们开发的Spring Boot主启动类的main方法了。spring定时任务?
目前Java的定时任务有三种:
Java自带的java.util.Timer类,这个类允许你调度一个java.util.TimerTask任务。使用这种方式可以让你的程序按照某一个频度执行,但不能在指定时间运行;而且作业类需要集成java.util.TimerTask,一般用的较少。 Quartz,这是一个功能比较强大的的调度器,可以让你的程序在指定时间执行,也可以按照某一个频度执行;使用起来需要继承org.springframework.scheduling.quartz.QuartzJobBean,配置稍显复杂,所以,一般会使用spring集成quartz,稍后会详细介绍; Spring3.0以后自带的task,即:spring schedule,可以将它看成一个轻量级的Quartz,而且使用起来比Quartz简单许多。
先看怎么使用:
spring schedule方式
xml配值的方式:
1)application.xml:
首先在application.xml中引入task的命名空间,以及通过task标签定义任务。
<?xml version="1.0" encoding="UTF-8"?> <beans xmlns="http://www.springframework.org/schema/beans" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:task="http://www.springframework.org/schema/task" xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-4.0.xsd http://www.springframework.org/schema/task http://www.springframework.org/schema/task/spring-task-4.0.xsd"> <task:scheduler id="myScheduler"/> <task:scheduled-tasks scheduler="myScheduler"> <task:scheduled ref="doSomethingTask" method="doSomething" cron="0 * * * * *"/> </task:scheduled-tasks> </beans>2)任务类:
import org.springframework.scheduling.annotation.Scheduled; import org.springframework.stereotype.Component; @Component public class DoSomethingTask { public void doSomething() { System.out.println("do something"); } }
@schedule 注解方式:
1)application.xml
同样在application.xml中引入task的命名空间,以及启用注解驱动的定时任务。
<?xml version="1.0" encoding="UTF-8"?> <beans xmlns="http://www.springframework.org/schema/beans" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:task="http://www.springframework.org/schema/task" xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-4.0.xsd http://www.springframework.org/schema/task http://www.springframework.org/schema/task/spring-task-4.0.xsd"> <task:annotation-driven/> </beans>2)任务类:
import org.springframework.scheduling.annotation.Scheduled; import org.springframework.stereotype.Component; @Component public class DoSomethingTask { @Scheduled(cron="0 * * * * *") public void doSomething() { System.out.println("do something"); } }
Cron表达式:
由6~7项组成,中间用空格分开。从左到右依次是:秒、分、时、日、月、周几、年(可省略)。值可以是数字,也可以是以下符号: *:所有值都匹配 ?:无所谓,不关心,通常放在“周几”里 ,:或者 /:增量值 -:区间
0 * * * * *:每分钟(当秒为0的时候) 0 0 * * * *:每小时(当秒和分都为0的时候) */10 * * * * *:每10秒 0 5/15 * * * *:每小时的5分、20分、35分、50分 0 0 9,13 * * *:每天的9点和13点@Scheduled注解的另外两个属性:fixedRate和fixedDelay
1)fixedDelay设置的是:上一个任务结束后多久执行下一个任务;
2)fixedRate设置的是:上一个任务的开始到下一个任务开始时间的间隔;
注:如果是强调任务间隔的定时任务,建议使用fixedRate和fixedDelay,如果是强调任务在某时某分某刻执行的定时任务,建议使用cron表达式。
并发执行:
默认是单线程执行的即上一个任务不结束,下一个任务不开始。如何让其并发执行,看下面:
让任务分别运行在不同的scheduler里。
<task:scheduler id="myScheduler1"/> <task:scheduler id="myScheduler2"/> <task:scheduled-tasks scheduler="myScheduler1"> <task:scheduled ref="doSomethingTask" method="doSomething" cron="${0 * * * * *}"/> </task:scheduled-tasks> <task:scheduled-tasks scheduler="myScheduler2"> <task:scheduled ref="doOtherThingTask" method="doOtherThing" cron="${0 * * * * *}"/> </task:scheduled-tasks>springboot自动配置原理
在面经书签里面 spring循环依赖
在面经书签里面
redis的bitmap,hyper-loglog用过吗?
解:bitmap:位图不是一个真实的数据类型,而是定义在字符串类型上的面向位的操作的集合。由于字符串类型是二进制安全的二进制大对象,并且最大长度是 512MB,适合于设置 2^32个不同的位。
操作:
setbit key offset value 设定某一位的值,如果key不存在就创建一个新的key并设定值,offset是偏移量,可以看做是下标
getbit key offset 获取某一位的值
bitcount key start end 统计start到end之间的1的数量
使用场景:bitmap适合的场景:对连续增长的整形id进行相应的数据统计,且活跃量较高。例如 统计某用户在当天是否登录过游戏,本来使用set就可以完成的需求,但是想到了刚学习的bitmap,于是使用了bitmap来处理。
HyperLogLog 是用来做基数统计的算法,HyperLogLog 的优点是,在输入元素的数量或者体积非常非常大时,计算基数所需的空间总是固定 的、并且是很小的。
127.0.0.1:6379> pfadd m1 1 2 3 4 1 2 3 2 2 2 2 pfcount m1 pfmerge mergeDes m1 m2
使用场景假:设一个淘宝网店在其店铺首页放置了10个宝贝链接,分别从Item01到Item10为这十个链接编号。店主希望可以在一天中随时查看从今天零点开始到目前这十个宝贝链接分别被多少个独立访客点击过。所谓独立访客(Unique Visitor,简称UV)是指有多少个自然人,例如,即使我今天点了五次Item01,我对Item01的UV贡献也是1,而不是5。
还可以统计在线用户数
redis过期删除策略?
解:删除键有三种策略。
1)定时删除:在设置键的过期时间的同时,创建一个定时器(timer),让定时器在键的过期时间来临时,立即执行对键的删除操作。
2)惰性删除(被动删除):放任键过期不管,但是每次从键空间中获取键时,都检查取得的键是否过期,如果过期的话,就删除该键;如果没有过期,就返回该键。
3)定期删除:每隔一段时间,程序就对数据库进行一次检查,删除里面的过期键。至于要删除多少过期键,以及要检查多少个数据库,则由算法决定。
彩蛋:redis的6中数据淘汰策略(内存溢出控制策略)
1)volatile-lru:从已设置过期时间的数据集(server.db[i].expires)中挑选最近最少使用的数据淘汰 2)volatile-ttl:从已设置过期时间的数据集(server.db[i].expires)中挑选将要过期的数据淘汰 3)volatile-random:从已设置过期时间的数据集(server.db[i].expires)中任意选择数据淘汰 4)allkeys-lru:当内存不足以容纳新写入数据时,在键空间中,移除最近最少使用的key(这个是最常用的) 5)allkeys-random:从数据集(server.db[i].dict)中任意选择数据淘汰 6)no-eviction:禁止驱逐数据,也就是说当内存不足以容纳新写入数据时,新写入操作会报错。这个应该没人使用吧! 4.0版本新增
volatile-lfu:从已设置过期时间的数据集(server.db[i].expires)中挑选最不经常使用的数据淘汰
redis的zset有什么应用场景?
解:排名
mysql隔离级别?
解:前面面经提到过。 kafka消费者的offset是自动提交吗,如果后续秒杀失败了怎么办?
分布式事务有哪些解决方案?
走你然后问了问基础 synchronized和ReentrantLock的区别?
解:
1)API层面:synchronized 是jvm层面的,可修饰方法和代码块,不需要手动释放锁。ReentrantLock是API级别的,需要加lock上锁,手动放锁unlock。
2)等待可中断:具体说来,假如业务代码中有两个线程,Thread1 Thread2.假设Thread1获取了对象object的锁,Thread2将等待Thread1释放object的锁。
使用synchronized。如果Thread1不释放,Thread2将一直等待,不能被中断。
使用ReentrantLock。如果Thread1不释放,Thread2等待了很长时间以后,可以中断等待,转而去做别的事情。
3)是否公平锁:synchronized非公平,ReentrantLock默认非公平,也可公平
4)绑定多个条件:ReentrantLock可以同时绑定多个Condition对象,只需多次调用newCondition方法即可。
synchronized中,锁对象的wait和notify() 或notifyAll()方法可以实现一个隐含的条件。
5)底层实现不一样: synchronized是同步阻塞,使用的是悲观并发策略,lock是同步非阻塞,采用的是乐观并发策略
彩蛋:
①Java中每个对象都有一个锁(lock)或者叫做监视器(monitor).
②ReentrantLock和synchronized持有的对象监视器不同。
③如果synchronized方法是static的,那么当线程方法改方法时,它锁的并不是synchronized方法所在的对象,而是synchronized方法所在对象所对应的class对象,因为java中不管一个类有多少对象,这些对象会应对唯一一个Class对象。因此当线程分别访问同一个类的两个对象的两个static,synchronized方法时,是顺序执行的,即一个线程先执行,另一个才开始。
④synchronized方法是一种粗粒度的并发控制,某一时刻只能有一个线程执行synchronized方法,synchronized快则是一种细粒度的并发控制。只会将 块中代码同步,位于方法内,synchronized块之外的是可以被多个线程同时访问的。
⑤synchronized关键字经过编译之后,会在同步块的前后分别形成monitorenter和monitorexit两个字节码指令,操作对象均为锁的计数器。
⑥相同点:都是可重入的。可重入值的是同一个线程多次试图获取它所占的锁,请求会成功。当释放的时候,直到冲入次数清零,锁才释放。
1.6以上,对synchronized的优化 CountDownLatch,Semaphore ThreadLocal原理(上面详细介绍了原理) 子线程,怎么获取父线程的ThreadLocal 解:
使用inheritableThreadLocalJVM垃圾回收算法介绍 g1 怎么设置预期的停顿时间,参数设置 -XX:MaxGCPauseMillis 怎么启用G1? JVM有哪些参数设置? 程序运行的时候,怎么查看GC情况。用什么命令 jstat -gc 类加载,双亲委派
解:
双亲委派模式的工作原理的是;如果一个类加载器收到了类加载请求,它并不会自己先去加载,而是把这个请求委托给父类的加载器去执行,如果父类加载器还存在其父类加载器,则进一步向上委托,依次递归,请求最终将到达顶层的启动类加载器,如果父类加载器可以完成类加载任务,就成功返回,倘若父类加载器无法完成此加载任务,子加载器才会尝试自己去加载,这就是双亲委派模式,即每个儿子都不愿意干活,每次有活就丢给父亲去干,直到父亲说这件事我也干不了时,儿子自己想办法去完成,这不就是传说中的双亲委派模式.那么这种模式有什么作用呢?
采用双亲委派模式的是好处是Java类随着它的类加载器一起具备了一种带有优先级的层次关系,通过这种层级关可以避免类的重复加载,当父亲已经加载了该类时,就没有必要子ClassLoader再加载一次。其次是考虑到安全因素,java核心api中定义类型不会被随意替换,假设通过网络传递一个名为java.lang.Integer的类,通过双亲委托模式传递到启动类加载器,而启动类加载器在核心Java API发现这个名字的类,发现该类已被加载,并不会重新加载网络传递的过来的java.lang.Integer,而直接返回已加载过的Integer.class,这样便可以防止核心API库被随意篡改。
彩蛋:
BootstrapClassLoader(启动类加载器):加载java核心库 java.* 构造ExtClassLoader和AppClassLoader。由于引导类加载器涉及到虚拟机本地实现细节,开发者无法直接获取到启动类加载器的引用,所以不允许直接通过引用进行操作
ExtClassLoader (标准扩展类加载器):java编写,加载扩展库,如classpath中的jre ,javax.*或者java.ext.dir 指定位置中的类,开发者可以直接使用标准扩展类加载器。
AppClassLoader(系统类加载器):java编写,加载程序所在的目录,如user.dir所在的位置的class。
CustomClassLoader(用户自定义类加载器)
tomcat类加载器结构?
https://www.jianshu.com/p/d4f519ed5bfb 内存溢出问题,怎么排查?
https://blog.csdn.net/u010430495/article/details/87283064?utm_medium=distribute.pc_relevant_t0.none-task-blog-BlogCommendFromMachineLearnPai2-1.nonecase&depth_1-utm_source=distribute.pc_relevant_t0.none-task-blog-BlogCommendFromMachineLearnPai2-1.nonecase CPU100%怎么排查、