前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >【BATJ面试必会】JAVA面试到底需要掌握什么?【上】

【BATJ面试必会】JAVA面试到底需要掌握什么?【上】

作者头像
乔戈里
发布2019-01-09 11:02:55
9210
发布2019-01-09 11:02:55
举报
文章被收录于专栏:Java那些事Java那些事

秋招几个月累积的知识点,东西太多,分两篇发,尽量用(*)和加粗标注出高频知识点, 都是面试问过的或笔试考过的

Java基础知识(*)

  • https://blog.csdn.net/qq_16633405/article/details/79211002

Spring Boot 启动 流程(*)

  • https://juejin.im/post/5b679fbc5188251aad213110#heading-0

Spring 一些面试题(*)

  • https://www.ctolib.com/topics-35589.html

匿名内部类编译class(*)

  • https://blog.csdn.net/lazyer_dog/article/details/50669473

为什么集合类没有实现Cloneable和Serializable接口?

  • https://www.nowcoder.com/questionTerminal/2a4902f67d5b49b6b4c05f9d7e422caf

自动装箱原理

  • https://www.jianshu.com/p/0ce2279c5691

final关键字

  • http://www.importnew.com/7553.html

基于Redis的分布式锁

  • https://segmentfault.com/a/1190000012919740

数据库分布式锁

  • http://www.hollischuang.com/archives/1716

防备DDOS攻击(*)

  • https://www.zhihu.com/question/19581905

什么时候Mysql调用行锁?(*)

  • https://blog.csdn.net/songwei128/article/details/43418343

CMS,G1(*)

  • https://crowhawk.github.io/2017/08/15/jvm_3/

内部类,外部类互访(*)

  • https://blog.csdn.net/jueblog/article/details/13434551
  • https://blog.csdn.net/Van_L_/article/details/54667365

设计模式(*) 熟背单例模式和工厂模式,会写适配器和建造者也行

  • https://www.jianshu.com/p/8a293e4a888e
  • https://segmentfault.com/a/1190000004255439

深拷贝,浅拷贝(*)

  • https://segmentfault.com/a/1190000010648514

泛型擦除

  • https://blog.csdn.net/briblue/article/details/76736356

Java 8 Stream, 函数编程

  • https://www.jianshu.com/p/0c07597d8311
  • https://www.jianshu.com/p/9bd647bcf1e3

中断线程

  • https://www.jianshu.com/p/264d4e1b76af

Lock,tryLock,lockInterruptibly区别

  • https://blog.csdn.net/u013851082/article/details/70140223

JUC

  • http://www.cnblogs.com/chenpi/p/5358579.html#_label2
  • http://www.cnblogs.com/chenpi/p/5614290.html

NIO

  • https://blog.csdn.net/u013063153/article/details/76473578
  • https://www.jianshu.com/p/052035037297
  • https://segmentfault.com/a/1190000006824196

Start和run区别(*)

  • https://blog.csdn.net/qq_36544760/article/details/79380963

jvm内存屏障

  • https://www.jianshu.com/p/2ab5e3d7e510

Java构造器能被重载,但是不能被重写(*)

  • https://blog.csdn.net/weixin_36513603/article/details/54968094

HttpSession

  • https://blog.csdn.net/zy2317878/article/details/80275463

Thread类的方法

  • https://blog.csdn.net/gxx_csdn/article/details/79210192

String是值类型,还是引用类型(*)

  • https://blog.csdn.net/a220315410/article/details/27743607

Redis 实现消息队列

  • 消息/订阅+List
  • https://segmentfault.com/a/1190000012244418

minor gc full gc 区别(*)

  • https://blog.csdn.net/u010796790/article/details/52213708

Java如何查看死锁

  • https://blog.csdn.net/u014039577/article/details/52351626
  • https://juejin.im/post/5aaf6ee76fb9a028d3753534

c3p0,dbcp与druid

  • https://blog.csdn.net/qq_34359363/article/details/72763491

Spring Bean 生命周期(*)

  • https://www.jianshu.com/p/3944792a5fff

Spring的BeanFactory和ApplicationContext的区别?

  • ApplicationContext是实现类,继承ListableBeanFactory(继承BeanFactory),功能更多
  • ApplicationContext默认立即加载,BeanFactory懒加载
  • https://my.oschina.net/yao00jun/blog/215642
  • https://blog.csdn.net/qq_36748278/article/details/78264764

Java 如何有效地避免OOM:善于利用软引用和弱引用

  • https://www.cnblogs.com/dolphin0520/p/3784171.html

分布式数据库主键生成策略(*)

  • https://www.jianshu.com/p/a0a3aa888a49
  • https://tech.meituan.com/MT_Leaf.html

String底层(*)

  • https://blog.csdn.net/yadicoco49/article/details/77627302

count(1)、count(*)与count(列名)的执行区别

  • https://blog.csdn.net/iFuMI/article/details/77920767

主键,唯一索引区别

  • 1)主键一定会创建一个唯一索引,但是有唯一索引的列不一定是主键;
  • 2)主键不允许为空值,唯一索引列允许空值;
  • 3)一个表只能有一个主键,但是可以有多个唯一索引;
  • 4)主键可以被其他表引用为外键,唯一索引列不可以;
  • 5)主键是一种约束,而唯一索引是一种索引,是表的冗余数据结构,两者有本质的差别

死锁 产生死锁的四个必要条件:

  • 互斥条件:一个资源每次只能被一个进程使用。
  • 占有且等待:一个进程因请求资源而阻塞时,对已获得的资源保持不放。
  • 不可强行占有:进程已获得的资源,在末使用完之前,不能强行剥夺。
  • 循环等待条件:若干进程之间形成一种头尾相接的循环等待资源关系。

避免死锁: https://segmentfault.com/a/1190000000378725

  • 确保所有的线程都是按照相同的顺序获得锁,那么死锁就不会发生.
  • 另外一个可以避免死锁的方法是在尝试获取锁的时候加一个超时时间,这也就意味着在尝试获取锁的过程中若超过了这个时限该线程则放弃对该锁请求。若一个线程没有在给定的时限内成功获得所有需要的锁,则会进行回退并释放所有已经获得的锁,然后等待一段随机的时间再重试。
  • 死锁检测是一个更好的死锁预防机制,它主要是针对那些不可能实现按序加锁并且锁超时也不可行的场景。

乐观锁,悲观锁(*)

  • https://blog.csdn.net/lovejj1994/article/details/79116272

公平锁、非公平锁

  • 公平锁(Fair):加锁前检查是否有排队等待的线程,优先排队等待的线程,先来先得 非公平锁(Nonfair):加锁时不考虑排队等待问题,直接尝试获取锁,获取不到自动到队尾等待 非公平锁性能比公平锁高5~10倍,因为公平锁需要在多核的情况下维护一个队列 Java中的ReentrantLock 默认的lock()方法采用的是非公平锁。

OOM分析

  • https://blog.csdn.net/zheng12tian/article/details/40617369

JVM调优参数 知道-Xms,-Xmx,-XX:NewRatio=n,会算就行,笔试题考过

  • https://www.jianshu.com/p/a2a6a0995fee

堆设置

  • -Xms:初始堆大小 -Xmx:最大堆大小 -XX:NewSize=n:设置年轻代大小 -XX:NewRatio=n:设置年轻代和年老代的比值。如:为3,表示年轻代与年老代比值为1:3,年轻代占整个年轻代年老代和的1/4 -XX:SurvivorRatio=n:年轻代中Eden区与两个Survivor区的比值。注意Survivor区有两个。如:3,表示Eden:Survivor=3:2,一个Survivor区占整个年轻代的1/5 -XX:MaxPermSize=n:设置持久代大小

收集器设置

  • -XX:+UseSerialGC:设置串行收集器 -XX:+UseParallelGC:设置并行收集器 -XX:+UseParalledlOldGC:设置并行年老代收集器 -XX:+UseConcMarkSweepGC:设置并发收集器

垃圾回收统计信息

  • -XX:+PrintGC -XX:+PrintGCDetails -XX:+PrintGCTimeStamps -Xloggc:filename

并行收集器设置

  • -XX:ParallelGCThreads=n:设置并行收集器收集时使用的CPU数。并行收集线程数。 -XX:MaxGCPauseMillis=n:设置并行收集最大暂停时间 -XX:GCTimeRatio=n:设置垃圾回收时间占程序运行时间的百分比。公式为1/(1+n)

并发收集器设置

  • -XX:+CMSIncrementalMode:设置为增量模式。适用于单CPU情况。 -XX:ParallelGCThreads=n:设置并发收集器年轻代收集方式为并行收集时,使用的CPU数。并行收集线程数。

调优总结 年轻代大小选择

  • 响应时间优先的应用:尽可能设大,直到接近系统的最低响应时间限制(根据实际情况选择)。在此种情况下,年轻代收集发生的频率也是最小的。同时,减少到达年老代的对象。 吞吐量优先的应用:尽可能的设置大,可能到达Gbit的程度。因为对响应时间没有要求,垃圾收集可以并行进行,一般适合8CPU以上的应用。

年老代大小选择

  • 响应时间优先的应用:年老代使用并发收集器,所以其大小需要小心设置,一般要考虑并发会话率和会话持续时间等一些参数。如果堆设置小了,可以会造成内存碎片、高回收频率以及应用暂停而使用传统的标记清除方式;如果堆大了,则需要较长的收集时间。最优化的方案,一般需要参考以下数据获得: 并发垃圾收集信息 持久代并发收集次数 传统GC信息 花在年轻代和年老代回收上的时间比例 减少年轻代和年老代花费的时间,一般会提高应用的效率 吞吐量优先的应用:一般吞吐量优先的应用都有一个很大的年轻代和一个较小的年老代。原因是,这样可以尽可能回收掉大部分短期对象,减少中期的对象,而年老代尽存放长期存活对象。

较小堆引起的碎片问题

  • 因为年老代的并发收集器使用标记、清除算法,所以不会对堆进行压缩。当收集器回收时,他会把相邻的空间进行合并,这样可以分配给较大的对象。但是,当堆空间较小时,运行一段时间以后,就会出现“碎片”,如果并发收集器找不到足够的空间,那么并发收集器将会停止,然后使用传统的标记、清除方式进行回收。如果出现“碎片”,可能需要进行如下配置: -XX:+UseCMSCompactAtFullCollection:使用并发收集器时,开启对年老代的压缩。 -XX:CMSFullGCsBeforeCompaction=0:上面配置开启的情况下,这里设置多少次Full GC后,对年老代进行压缩

synchronized实现原理(*)

  • https://blog.csdn.net/javazejian/article/details/72828483
  • 内存对象头, Mark Word保存锁信息
  • JVM层:Monitor对象,字节码中的monitorenter 和 monitorexit 指令
  • 无锁,偏向锁,轻量级锁(自选),重量级锁
  • 可重入
  • notify/notifyAll和wait方法,在使用这3个方法时,必须处于synchronized代码块或者synchronized方法中,否则就会抛出IllegalMonitorStateException异常,这是因为调用这几个方法前必须拿到当前对象的监视器monitor对象,也就是说notify/notifyAll和wait方法依赖于monitor对象,在前面的分析中,我们知道monitor 存在于对象头的Mark Word 中(存储monitor引用指针),而synchronized关键字可以获取 monitor ,这也就是为什么notify/notifyAll和wait方法必须在synchronized代码块或者synchronized方法调用的原因.

synchronized, lock区别(*)

  • https://blog.csdn.net/u012403290/article/details/64910926

Spring容器中Bean的作用域(*) 当通过Spring容器创建一个Bean实例时,不仅可以完成Bean实例的实例化,还可以为Bean指定特定的作用域。Spring支持如下5种作用域:

  • singleton:单例模式,在整个Spring IoC容器中,使用singleton定义的Bean将只有一个实例
  • prototype:原型模式,每次通过容器的getBean方法获取prototype定义的Bean时,都将产生一个新的Bean实例
  • request:对于每次HTTP请求,使用request定义的Bean都将产生一个新实例,即每次HTTP请求将会产生不同的Bean实例。只有在Web应用中使用Spring时,该作用域才有效
  • session:对于每次HTTP Session,使用session定义的Bean产生一个新实例。同样只有在Web应用中使用Spring时,该作用域才有效
  • globalsession:每个全局的HTTP Session,使用session定义的Bean都将产生一个新实例。典型情况下,仅在使用portlet context的时候有效。同样只有在Web应用中使用Spring时,该作用域才有效   其中比较常用的是singleton和prototype两种作用域。对于singleton作用域的Bean,每次请求该Bean都将获得相同的实例。容器负责跟踪Bean实例的状态,负责维护Bean实例的生命周期行为;如果一个Bean被设置成prototype作用域,程序每次请求该id的Bean,Spring都会新建一个Bean实例,然后返回给程序。在这种情况下,Spring容器仅仅使用new 关键字创建Bean实例,一旦创建成功,容器不在跟踪实例,也不会维护Bean实例的状态。   如果不指定Bean的作用域,Spring默认使用singleton作用域。Java在创建Java实例时,需要进行内存申请;销毁实例时,需要完成垃圾回收,这些工作都会导致系统开销的增加。因此,prototype作用域Bean的创建、销毁代价比较大。而singleton作用域的Bean实例一旦创建成功,可以重复使用。因此,除非必要,否则尽量避免将Bean被设置成prototype作用域。

Spring IOC实现原理, 相关知识(*)

Spring 启动时读取应用程序提供的Bean配置信息,并在Spring容器中生成一份相应的Bean配置注册表,然后根据这张注册表实例化Bean,装配好Bean之间的依赖关系,为上层应用提供准备就绪的运行环境。

enter image description here

Bean缓存池:HashMap实现

Spring 通过一个配置文件描述 Bean 及 Bean 之间的依赖关系,利用 Java 语言的反射功能实例化 Bean 并建立 Bean 之间的依赖关系。 Spring 的 IoC 容器在完成这些底层工作的基础上,还提供了 Bean 实例缓存、生命周期管理、 Bean 实例代理、事件发布、资源装载等高级服务。

BeanFactory 是 Spring 框架的基础设施,面向 Spring 本身;

ApplicationContext 面向使用 Spring 框架的开发者,几乎所有的应用场合我们都直接使用 ApplicationContext 而非底层的 BeanFactory。

enter image description here

BeanDefinitionRegistry: Spring 配置文件中每一个节点元素在 Spring 容器里都通过一个 BeanDefinition 对象表示,它描述了 Bean 的配置信息。而 BeanDefinitionRegistry 接口提供了向容器手工注册 BeanDefinition 对象的方法。

BeanFactory 接口位于类结构树的顶端 ,它最主要的方法就是 getBean(String beanName),该方法从容器中返回特定名称的 Bean,BeanFactory 的功能通过其他的接口得到不断扩展:

ListableBeanFactory:该接口定义了访问容器中 Bean 基本信息的若干方法,如查看Bean 的个数、获取某一类型 Bean 的配置名、查看容器中是否包括某一 Bean 等方法;

HierarchicalBeanFactory:父子级联 IoC 容器的接口,子容器可以通过接口方法访问父容器; 通过 HierarchicalBeanFactory 接口, Spring 的 IoC 容器可以建立父子层级关联的容器体系,子容器可以访问父容器中的 Bean,但父容器不能访问子容器的 Bean。Spring 使用父子容器实现了很多功能,比如在 Spring MVC 中,展现层 Bean 位于一个子容器中,而业务层和持久层的 Bean 位于父容器中。这样,展现层 Bean 就可以引用业务层和持久层的 Bean,而业务层和持久层的 Bean 则看不到展现层的 Bean。

ConfigurableBeanFactory:是一个重要的接口,增强了 IoC 容器的可定制性,它定义了设置类装载器、属性编辑器、容器初始化后置处理器等方法;

AutowireCapableBeanFactory:定义了将容器中的 Bean 按某种规则(如按名字匹配、按类型匹配等)进行自动装配的方法;

SingletonBeanRegistry:定义了允许在运行期间向容器注册单实例 Bean 的方法;

@Bean, @Component 区别

  • Componet 一般放在类上面,Bean放在方法上面,自己可控制是否生成bean. bean 一般会放在classpath scanning路径下面,会自动生成bean. 有Componet /bean生成的bean都提供给autowire使用.
  • 在@Component中(@Component标注的类,包括@Service,@Repository, @Controller)使用@Bean注解和在@Configuration中使用是不同的。在@Component类中使用方法或字段时不会使用CGLIB增强(及不使用代理类:调用任何方法,使用任何变量,拿到的是原始对象,后面会有例子解释)。而在@Configuration类中使用方法或字段时则使用CGLIB创造协作对象(及使用代理:拿到的是代理对象);当调用@Bean注解的方法时它不是普通的Java语义,而是从容器中拿到的由Spring生命周期管理、被Spring代理甚至依赖于其他Bean的对象引用。在@Component中调用@Bean注解的方法和字段则是普通的Java语义,不经过CGLIB处理。

如何停止线程?

  • 主线程提供volatile boolean flag, 线程内while判断flag
  • 线程内while(!this.isInterrupted), 主线程里调用interrupt
  • if(this.isInterrupted) throw new InterruptedException() 或return,主线程里调用interrupt
  • 将一个线程设置为守护线程后,当进程中没有非守护线程后,守护线程自动结束

多线程实现方式?(*)

  • extends Thread
  • implements Runnable
  • implements Callable, 重写call, 返回future (主线程可以用线程池submit)

线程池(*) 线程池处理过程:

  • 如果当前运行的线程少于corePoolSize,则创建新线程来执行任务(注意,执行这一步骤需要获取全局锁)。
  • 如果运行的线程等于或多于corePoolSize,则将任务加入BlockingQueue。
  • 如果无法将任务加入BlockingQueue(队列已满),则创建新的线程来处理任务(注意,执行这一步骤需要获取全局锁)。
  • 如果创建新线程将使当前运行的线程超出maximumPoolSize,任务将被拒绝,并调用RejectedExecutionHandler.rejectedExecution()方法。

四种线程池:

  • CachedThreadPool
  • FixedThreadPool
  • ScheduledThreadPool
  • SingleThreadExecutor

创建线程池的参数:

  • corePoolSize(线程池的基本大小):当提交一个任务到线程池时,线程池会创建一个线程来执行任务,即使其他空闲的基本线程能够执行新任务也会创建线程,等到需要执行的任务数大于线程池基本大小时就不再创建。如果调用了线程池的prestartAllCoreThreads()方法,线程池会提前创建并启动所有基本线程。
  • runnableTaskQueue(任务队列):用于保存等待执行的任务的阻塞队列。可以选择以下几个阻塞队列。 ArrayBlockingQueue:是一个基于数组结构的有界阻塞队列,此队列按FIFO(先进先出)原则对元素进行排序。 LinkedBlockingQueue:一个基于链表结构的阻塞队列,此队列按FIFO排序元素,吞吐量通常要高于ArrayBlockingQueue。静态工厂方法Executors.newFixedThreadPool()使用了这个队列。 SynchronousQueue:一个不存储元素的阻塞队列。每个插入操作必须等到另一个线程调用移除操作,否则插入操作一直处于阻塞状态,吞吐量通常要高于Linked-BlockingQueue,静态工厂方法Executors.newCachedThreadPool使用了这个队列。 PriorityBlockingQueue:一个具有优先级的无限阻塞队列。
  • maximumPoolSize(线程池最大数量):线程池允许创建的最大线程数。如果队列满了,并且已创建的线程数小于最大线程数,则线程池会再创建新的线程执行任务。值得注意的是,如果使用了无界的任务队列这个参数就没什么效果。
  • ThreadFactory:用于设置创建线程的工厂
  • RejectedExecutionHandler(饱和策略):当队列和线程池都满了,说明线程池处于饱和状态,那么必须采取一种策略处理提交的新任务。这个策略默认情况下是AbortPolicy,表示无法处理新任务时抛出异常。在JDK 1.5中Java线程池框架提供了以下4种策略。 AbortPolicy:直接抛出异常。 CallerRunsPolicy:只用调用者所在线程来运行任务。 DiscardOldestPolicy:丢弃队列里最近的一个任务,并执行当前任务。 DiscardPolicy:不处理,丢弃掉。

**ArrayList,LinkedList **

  • ArrayList初始化可以指定大小,知道大小的建议指定 arraylist添加元素的时候,需要判断存放元素的数组是否需要扩容(扩容大小是原来大小的1/2+1)
  • LinkedList添加、删除元素通过移动指针 LinkedList遍历比arraylist慢,建议用迭代器
  • ArrayList是实现了基于动态数组的数据结构,LinkedList基于链表的数据结构。 对于随机访问get和set,ArrayList优于LinkedList,因为ArrayList可以随机定位,而LinkedList要移动指针一步一步的移动到节点处。(参考数组与链表来思考)
  • 对于新增和删除操作add和remove,LinedList比较占优势,只需要对指针进行修改即可,而ArrayList要移动数据来填补被删除的对象的空间。

HashMap原理(*)

  • HashMap最多只允许一条Entry的键为Null(多条会覆盖),但允许多条Entry的值为Null
  • HashSet 本身就是在 HashMap 的基础上实现的.
  • 若负载因子越大,那么对空间的利用更充分,但查找效率的也就越低;若负载因子越小,那么哈希表的数据将越稀疏,对空间造成的浪费也就越严重。系统默认负载因子0.75
  • 调用put方法存值时,HashMap首先会调用Key的hashCode方法,然后基于此获取Key哈希码,通过哈希码快速找到某个桶,这个位置可以被称之为bucketIndex.如果两个对象的hashCode不同,那么equals一定为false;否则,如果其hashCode相同,equals也不一定为 true。所以,理论上,hashCode可能存在碰撞的情况,当碰撞发生时,这时会取出bucketIndex桶内已存储的元素,并通过hashCode() 和 equals()来逐个比较以判断Key是否已存在。如果已存在,则使用新Value值替换旧Value值,并返回旧Value值;如果不存在,则存放新的键值对 到桶中。因此,在 HashMap中,equals() 方法只有在哈希码碰撞时才会被用到。
  • 首先,判断key是否为null,若为null,则直接调用putForNullKey方法;若不为空,则先计算key的hash值,然后根据hash值搜索在table数组中的索引位置,如果table数组在该位置处有元素,则查找是否存在相同的key,若存在则覆盖原来key的value,否则将该元素保存在链头(最先保存的元素放在链尾)。此外,若table在该处没有元素,则直接保存。
  • HashMap 永远都是在链表的表头添加新元素。

hash()和indexFor()

static int hash(int h) {
    // This function ensures that hashCodes that differ only by
    // constant multiples at each bit position have a bounded
    // number of collisions (approximately 8 at default load factor).
    h ^= (h >>> 20) ^ (h >>> 12);
    return h ^ (h >>> 7) ^ (h >>> 4);
}
static int indexFor(int h, int length) {
    return h & (length-1);  // 作用等价于取模运算,但这种方式效率更高
}
  • hash() 方法用于对Key的hashCode进行重新计算,而 indexFor()方法用于生成这个Entry对象的插入位置。当计算出来的hash值与hashMap的(length-1)做了&运算后,会得到位于区间[0,length-1]的一个值。特别地,这个值分布的越均匀,HashMap 的空间利用率也就越高,存取效率也就越好,保证元素均匀分布到table的每个桶中以便充分利用空间。
  • hash():使用hash()方法对一个对象的hashCode进行重新计算是为了防止质量低下的hashCode()函数实现。由于hashMap的支撑数组长度总是2 的幂次,通过右移可以使低位的数据尽量的不同,从而使hash值的分布尽量均匀。
  • indexFor():保证元素均匀分布到table的每个桶中; 当length为2的n次方时,h&(length -1)就相当于对length取模,而且速度比直接取模要快得多,这是HashMap在速度上的一个优化.

扩容resize()和重哈希transfer()

void resize(int newCapacity) {
    Entry[] oldTable = table;
    int oldCapacity = oldTable.length;

    // 若 oldCapacity 已达到最大值,直接将 threshold 设为 Integer.MAX_VALUE
    if (oldCapacity == MAXIMUM_CAPACITY) {
        threshold = Integer.MAX_VALUE;
        return;             // 直接返回
    }

    // 否则,创建一个更大的数组
    Entry[] newTable = new Entry[newCapacity];

    //将每条Entry重新哈希到新的数组中
    transfer(newTable);

    table = newTable;
    threshold = (int) (newCapacity * loadFactor);  // 重新设定 threshold
}

void transfer(Entry[] newTable) {

    // 将原数组 table 赋给数组 src
    Entry[] src = table;
    int newCapacity = newTable.length;

    // 将数组 src 中的每条链重新添加到 newTable 中
    for (int j = 0; j < src.length; j++) {
        Entry<K,V> e = src[j];
        if (e != null) {
            src[j] = null;   // src 回收

            // 将每条链的每个元素依次添加到 newTable 中相应的桶中
            do {
                Entry<K,V> next = e.next;

                // e.hash指的是 hash(key.hashCode())的返回值;
                // 计算在newTable中的位置,注意原来在同一条子链上的元素可能被分配到不同的子链
                int i = indexFor(e.hash, newCapacity);   
                e.next = newTable[i];
                newTable[i] = e;
                e = next;
            } while (e != null);
        }
    }
}
  • 为了保证HashMap的效率,系统必须要在某个临界点进行扩容处理,该临界点就是HashMap中元素的数量在数值上等于threshold(table数组长度*加载因子)
  • 重哈希的主要是一个重新计算原HashMap中的元素在新table数组中的位置并进行复制处理的过程

HashMap 的底层数组长度为何总是2的n次方

  • 当底层数组的length为2的n次方时, h&(length - 1) 就相当于对length取模,而且速度比直接取模得多,这是HashMap在速度上的一个优化
  • 不同的hash值发生碰撞的概率比较小,这样就会使得数据在table数组中分布较均匀,空间利用率较高,查询速度也较快

ConcurrenytHashMap原理(*)

  • https://blog.csdn.net/justloveyou_/article/details/72783008

enter image description here

  • 通过锁分段技术保证并发环境下的写操作; 通过 HashEntry的不变性、Volatile变量的内存可见性和加锁重读机制保证高效、安全的读操作; 通过不加锁和加锁两种方案控制跨段操作的的安全性。

HashMap线程不安全

  • https://blog.csdn.net/justloveyou_/article/details/72783008
  • 在HashMap进行扩容重哈希时导致Entry链形成环。一旦Entry链中有环,势必会导致在同一个桶中进行插入、查询、删除等操作时陷入死循环

Segment数组

static final class Segment<K,V> extends ReentrantLock 
implements Serializable {
    transient volatile int count;    // Segment中元素的数量,可见的
    transient int modCount;  //对count的大小造成影响的操作的次数(比如put或者remove操作)
    transient int threshold;      // 阈值,段中元素的数量超过这个值就会对Segment进行扩容
    transient volatile HashEntry<K,V>[] table;  // 链表数组
    final float loadFactor;  // 段的负载因子,其值等同于ConcurrentHashMap的负载因子
    ...
}
  • Segment 类继承于 ReentrantLock 类,从而使得 Segment 对象能充当锁的角色
  • 在Segment类中,count 变量是一个计数器,它表示每个 Segment 对象管理的 table 数组包含的 HashEntry 对象的个数,也就是 Segment 中包含的 HashEntry 对象的总数。特别需要注意的是,之所以在每个 Segment 对象中包含一个计数器,而不是在 ConcurrentHashMap 中使用全局的计数器,是对 ConcurrentHashMap 并发性的考虑:因为这样当需要更新计数器时,不用锁定整个ConcurrentHashMap。事实上,每次对段进行结构上的改变,如在段中进行增加/删除节点(修改节点的值不算结构上的改变),都要更新count的值,此外,在JDK的实现中每次读取操作开始都要先读取count的值。特别需要注意的是,count是volatile的,这使得对count的任何更新对其它线程都是立即可见的。modCount用于统计段结构改变的次数,主要是为了检测对多个段进行遍历过程中某个段是否发生改变.table是一个典型的链表数组,而且也是volatile的,这使得对table的任何更新对其它线程也都是立即可见的。

HashEntry

static final class HashEntry<K,V> {
   final K key;                       // 声明 key 为 final 的
   final int hash;                   // 声明 hash 值为 final 的
   volatile V value;                // 声明 value 被volatile所修饰
   final HashEntry<K,V> next;      // 声明 next 为 final 的

    HashEntry(K key, int hash, HashEntry<K,V> next, V value) {
        this.key = key;
        this.hash = hash;
        this.next = next;
        this.value = value;
    }

    @SuppressWarnings("unchecked")
    static final <K,V> HashEntry<K,V>[] newArray(int i) {
    return new HashEntry[i];
    }
}
  • 在HashEntry类中,key,hash和next域都被声明为final的,value域被volatile所修饰,因此HashEntry对象几乎是不可变的,这是ConcurrentHashmap读操作并不需要加锁的一个重要原因
  • 由于value域被volatile修饰,所以其可以确保被读线程读到最新的值,这是ConcurrentHashmap读操作并不需要加锁的另一个重要原因

put(), get()

  • 不允许key值为null,也不允许value值为null
  • HashTable 和由同步包装器包装的HashMap每次只能有一个线程执行读或写操作,ConcurrentHashMap 在并发访问性能上有了质的提高。在理想状态下,ConcurrentHashMap 可以支持 16 个线程执行并发写操作(如果并发级别设置为 16),及任意数量线程的读操作。 重哈希rehash()
  • ConcurrentHashMap的重哈希实际上是对ConcurrentHashMap的某个段的重哈希,因此ConcurrentHashMap的每个段所包含的桶位自然也就不尽相同

存在key-value为null的特殊情况

V get(Object key, int hash) {
        if (count != 0) {            // read-volatile,首先读 count 变量
            HashEntry<K,V> e = getFirst(hash);   // 获取桶中链表头结点
            while (e != null) {
                if (e.hash == hash && key.equals(e.key)) {    // 查找链中是否存在指定Key的键值对
                    V v = e.value;
                    if (v != null)  // 如果读到value域不为 null,直接返回
                        return v;   
                    // 如果读到value域为null,说明发生了重排序,加锁后重新读取
                    return readValueUnderLock(e); // recheck
                }
                e = e.next;
            }
        }
        return null;  // 如果不存在,直接返回null
    }
  • 初始化HashEntry时发生的指令重排序导致的,也就是在HashEntry初始化完成之前便返回了它的引用
  • 加锁重读

读操作不需要加锁

  • 用HashEntery对象的不变性来降低读操作对加锁的需求;
  • 用Volatile变量协调读写线程间的内存可见性;
  • 若读时发生指令重排序现象,则加锁重读;

结构性操作的并发安全

 remove(Object key, int hash, Object value) {
        lock();     // 加锁
        try {
            int c = count - 1;      
            HashEntry<K,V>[] tab = table;
            int index = hash & (tab.length - 1);        // 定位桶
            HashEntry<K,V> first = tab[index];
            HashEntry<K,V> e = first;
            while (e != null && (e.hash != hash || !key.equals(e.key)))  // 查找待删除的键值对
                e = e.next;

            V oldValue = null;
            if (e != null) {    // 找到
                V v = e.value;
                if (value == null || value.equals(v)) {
                    oldValue = v;
                    // All entries following removed node can stay
                    // in list, but all preceding ones need to be
                    // cloned.
                    ++modCount;
                    // 所有处于待删除节点之后的节点原样保留在链表中
                    HashEntry<K,V> newFirst = e.next;
                    // 所有处于待删除节点之前的节点被克隆到新链表中
                    for (HashEntry<K,V> p = first; p != e; p = p.next)
                        newFirst = new HashEntry<K,V>(p.key, p.hash,newFirst, p.value); 

                    tab[index] = newFirst;   // 将删除指定节点并重组后的链重新放到桶中
                    count = c;      // write-volatile,更新Volatile变量count
                }
            }
            return oldValue;
        } finally {
            unlock();          // finally子句解锁
        }
    }
  • clear操作只是把ConcurrentHashMap中所有的桶置空,每个桶之前引用的链表依然存在,只是桶不再引用这些链表而已,而链表本身的结构并没有发生任何修改。
  • put操作如果需要插入一个新节点到链表中时会在链表头部插入这个新节点,此时链表中的原有节点的链接并没有被修改
  • 在执行remove操作时,原始链表并没有被修改
  • 只要之前对链表做结构性修改操作的写线程M在退出写方法前写volatile变量count(segment中的,segment中元素的个数),读线程N就能读取到这个volatile变量count的最新值

跨segment操作

  • size(): JDK只需要在统计size前后比较modCount(Segment中的)是否发生变化就可以得知容器的大小是否发生变化
  • size方法主要思路是先在没有锁的情况下对所有段大小求和,这种求和策略最多执行RETRIES_BEFORE_LOCK次(默认是两次):在没有达到RETRIES_BEFORE_LOCK之前,求和操作会不断尝试执行(这是因为遍历过程中可能有其它线程正在对已经遍历过的段进行结构性更新);在超过RETRIES_BEFORE_LOCK之后,如果还不成功就在持有所有段锁的情况下再对所有段大小求和。

JVM内存模型(*) 必考,熟背

enter image description here

  • 线程私有的数据区 包括 程序计数器、 虚拟机栈 和 本地方法栈
  • 线程共享的数据区 具体包括 Java堆 和 方法区

线程计数器

  • 在多线程情况下,当线程数超过CPU数量或CPU内核数量时,线程之间就要根据 时间片轮询抢夺CPU时间资源。也就是说,在任何一个确定的时刻,一个处理器都只会执行一条线程中的指令。因此,为了线程切换后能够恢复到正确的执行位置,每条线程都需要一个独立的程序计数器去记录其正在执行的字节码指令地址。

虚拟机栈

  • 每个方法从调用直至完成的过程,对应一个栈帧在虚拟机栈中入栈到出栈的过程

本地方法栈

  • 本地方法栈与Java虚拟机栈非常相似,也是线程私有的,区别是虚拟机栈为虚拟机执行 Java 方法服务,而本地方法栈为虚拟机执行 Native 方法服务。与虚拟机栈一样,本地方法栈区域也会抛出 StackOverflowError 和 OutOfMemoryError 异常

Java堆

  • Java 堆的唯一目的就是存放对象实例,几乎所有的对象实例(和数组)都在这里分配内存
  • Java堆可以处于物理上不连续的内存空间中,只要逻辑上是连续的即可。而且,Java堆在实现时,既可以是固定大小的,也可以是可拓展的,并且主流虚拟机都是按可扩展来实现的(通过-Xmx(最大堆容量) 和 -Xms(最小堆容量)控制)。如果在堆中没有内存完成实例分配,并且堆也无法再拓展时,将会抛出 OutOfMemoryError 异常。
  • TLAB (线程私有分配缓冲区) : 虚拟机为新生对象分配内存时,需要考虑修改指针 (该指针用于划分内存使用空间和空闲空间) 时的线程安全问题,因为存在可能出现正在给对象A分配内存,指针还未修改,对象B又同时使用原来的指针分配内存的情况。TLAB 的存在就是为了解决这个问题:每个线程在Java堆中预先分配一小块内存 TLAB,哪个线程需要分配内存就在自己的TLAB上进行分配,若TLAB用完并分配新的TLAB时,再加同步锁定,这样就大大提升了对象内存分配的效率。

方法区

  • 方法区与Java堆一样,也是线程共享的并且不需要连续的内存,其用于存储已被虚拟机加载的 类信息、常量、静态变量、即时编译器编译后的代码等数据
  • 运行时常量池:是方法区的一部分,用于存放编译期生成的各种 字面量 和 符号引用. 字面量比较接近Java语言层次的常量概念,如文本字符串、被声明为final的常量值. 符号引用:包括以下三类常量:类和接口的全限定名、字段的名称和描述符 和 方法的名称和描述符.

方法区的回收

  • 主要是针对 常量池的回收 (判断引用) 和 对类型的卸载
  • 回收类: 1) 该类所有的实例都已经被回收,也就是Java堆中不存在该类的任何实例加载 2) 该类的ClassLoader已经被回收 3) 该类对应的 java.lang.Class 对象没有在任何地方被引用,无法在任何地方通过反射访问该类的方法。

垃圾回收机制(*) 必考,熟背

引用计数法

  • 循环引用

可达性分析算法

  • 通过一系列的名为 “GC Roots” 的对象作为起始点,从这些节点开始向下搜索,搜索所走过的路径称为引用链(Reference Chain)。当一个对象到 GC Roots 没有任何引用链相连(用图论的话来说就是从 GC Roots 到这个对象不可达)时,则证明此对象是不可用的
  • 虚拟机栈(栈帧中的局部变量表)中引用的对象
  • 方法区中类静态属性引用的对象
  • 方法区中常量引用的对象
  • 本地方法栈中Native方法引用的对象

标记清除算法

  • 标记-清除算法分为标记和清除两个阶段。该算法首先从根集合进行扫描,对存活的对象对象标记,标记完毕后,再扫描整个空间中未被标记的对象并进行回收
  • 效率问题:标记和清除两个过程的效率都不高;
  • 空间问题:标记-清除算法不需要进行对象的移动,并且仅对不存活的对象进行处理,因此标记清除之后会产生大量不连续的内存碎片,空间碎片太多可能会导致以后在程序运行过程中需要分配较大对象时,无法找到足够的连续内存而不得不提前触发另一次垃圾收集动作

复制算法

  •  复制算法将可用内存按容量划分为大小相等的两块,每次只使用其中的一块。当这一块的内存用完了,就将还存活着的对象复制到另外一块上面,然后再把已使用过的内存空间一次清理掉。这种算法适用于对象存活率低的场景,比如新生代。这样使得每次都是对整个半区进行内存回收,内存分配时也就不用考虑内存碎片等复杂情况,只要移动堆顶指针,按顺序分配内存即可,实现简单,运行高效。
  • 实践中会将新生代内存分为一块较大的Eden空间和两块较小的Survivor空间 (如下图所示),每次使用Eden和其中一块Survivor。当回收时,将Eden和Survivor中还存活着的对象一次地复制到另外一块Survivor空间上,最后清理掉Eden和刚才用过的Survivor空间。HotSpot虚拟机默认Eden和Survivor的大小比例是 8:1,也就是每次新生代中可用内存空间为整个新生代容量的90% ( 80%+10% ),只有10% 的内存会被“浪费”。

enter image description here

  • 现在商用的虚拟机都采用这种算法来回收新生代

为什么分代收集

  • 不同的对象的生命周期(存活情况)是不一样的,而不同生命周期的对象位于堆中不同的区域,因此对堆内存不同区域采用不同的策略进行回收可以提高 JVM 的执行效率.

新生代进入老生代的情况

  • 对象优先在Eden分配,当Eden区没有足够空间进行分配时,虚拟机将发起一次MinorGC。现在的商业虚拟机一般都采用复制算法来回收新生代,将内存分为一块较大的Eden空间和两块较小的Survivor空间,每次使用Eden和其中一块Survivor。 当进行垃圾回收时,将Eden和Survivor中还存活的对象一次性地复制到另外一块Survivor空间上,最后处理掉Eden和刚才的Survivor空间。(HotSpot虚拟机默认Eden和Survivor的大小比例是8:1)当Survivor空间不够用时,需要依赖老年代进行分配担保。
  • 大对象直接进入老年代。所谓的大对象是指,需要大量连续内存空间的Java对象,最典型的大对象就是那种很长的字符串以及数组。
  • 长期存活的对象(-XX:MaxTenuringThreshold)将进入老年代。当对象在新生代中经历过一定次数(默认为15)的Minor GC后,就会被晋升到老年代中。
  • 动态对象年龄判定。为了更好地适应不同程序的内存状况,虚拟机并不是永远地要求对象年龄必须达到了MaxTenuringThreshold才能晋升老年代,如果在Survivor空间中相同年龄所有对象大小的总和大于Survivor空间的一半,年龄大于或等于该年龄的对象就可以直接进入老年代,无须等到MaxTenuringThreshold中要求的年龄。

内存分配担保机制

  • 我们知道如果对象在复制到Survivor区时若Survivor空间不足,则会出发担保机制,将对象转入老年代;但老年代的能力也不是无限的,因此需要在minor GC时做一个是否需要Major GC 的判断:
  • 如果老年代的剩余空间 < 之前转入老年代的对象的平均大小,则触发Major GC
  • 如果老年代的剩余空间 > 之前转入老年代的对象的平均大小,并且允许担保失败,则直接Minor GC,不需要做Full GC
  • 如果老年代的剩余空间 > 之前转入老年代的对象的平均大小,并且不允许担保失败,则触发Major GC 出发点还是尽量为对象分配内存。但是一般会配置允许担保失败,避免频繁的去做Full GC。

标记整理算法

  • 标记整理算法的标记过程类似标记清除算法,但后续步骤不是直接对可回收对象进行清理,而是让所有存活的对象都向一端移动,然后直接清理掉端边界以外的内存,类似于磁盘整理的过程,该垃圾回收算法适用于对象存活率高的场景(老年代)
  • 无内存碎片

新生代、老年代、永久代

  • 新生代的目标就是尽可能快速的收集掉那些生命周期短的对象,一般情况下,所有新生成的对象首先都是放在新生代的. 如果老年代也满了,就会触发一次FullGC,也就是新生代、老年代都进行回收。注意,新生代发生的GC也叫做MinorGC,MinorGC发生频率比较高,不一定等 Eden区满了才触发。
  • 老年代存放的都是一些生命周期较长的对象,就像上面所叙述的那样,在新生代中经历了N次垃圾回收后仍然存活的对象就会被放到老年代中
  • 永久代主要用于存放静态文件,如Java类、方法等

垃圾收集器

enter image description here

  • Serial收集器(复制算法): 新生代单线程收集器,标记和清理都是单线程,优点是简单高效;
  • Serial Old收集器 (标记-整理算法): 老年代单线程收集器,Serial收集器的老年代版本;
  • ParNew收集器 (复制算法):新生代收并行集器,实际上是Serial收集器的多线程版本,在多核CPU环境下有着比Serial更好的表现;
  • Parallel Scavenge收集器 (复制算法): 新生代并行收集器,追求高吞吐量,高效利用 CPU。吞吐量 =用户线程时间/(用户线程时间+GC线程时间),高吞吐量可以高效率的利用CPU时间,尽快完成程序的运算任务,适合后台应用等对交互相应要求不高的场景;
  • Parallel Old收集器 (标记-整理算法): 老年代并行收集器,吞吐量优先,Parallel Scavenge收集器的老年代版本;
  • CMS(Concurrent Mark Sweep)收集器(标记-清除算法):老年代并行收集器,以获取最短回收停顿时间为目标的收集器,具有高并发、低停顿的特点,追求最短GC回收停顿时间。
  • G1(Garbage First)收集器 (标记-整理算法):Java堆并行收集器,G1收集器是JDK1.7提供的一个新收集器,G1收集器基于“标记-整理”算法实现,也就是说不会产生内存碎片。此外,G1收集器不同于之前的收集器的一个重要特点是:G1回收的范围是整个Java堆(包括新生代,老年代),而前六种收集器回收的范围仅限于新生代或老年代。

CMS,G1

  • https://blog.csdn.net/huanbia/article/details/75581423

内存泄露问题

  • 静态集合类: 如 HashMap、Vector 等集合类的静态使用最容易出现内存泄露,因为这些静态变量的生命周期和应用程序一致,所有的对象Object也不能被释放
  • 各种资源连接包括数据库连接、网络连接、IO连接等没有显式调用close关闭
  • 监听器的使用,在释放对象的同时没有相应删除监听器的时候也可能导致内存泄露。

MYSQL索引(*)

建立索引

  • 表的主键、外键必须有索引;
  • 数据量超过300的表应该有索引;
  • 经常与其他表进行连接的表,在连接字段上应该建立索引;
  • 经常出现在Where子句中的字段,特别是大表的字段,应该建立索引;
  • 索引应该建在选择性高的字段上;
  • 索引应该建在小字段上,对于大的文本字段甚至超长字段,不要建索引;
  • 频繁进行数据操作的表,不要建立太多的索引;

索引失效

  • 字符串不加单引号
  • 将要使用的索引列不是复合索引列表中的第一部分,则不会使用索引
  • 应尽量避免在 where 子句中对字段进行 null 值判断,否则将导致引擎放弃使用索引而进行全表扫描,如: select id from t where num is null
  • 可以在num上设置默认值0,确保表中num列没有null值,然后这样查询: select id from t where num=0
  • 应尽量避免在 where 子句中使用!=或<>操作符,否则将引擎放弃使用索引而进行全表扫描。优化器将无法通过索引来确定将要命中的行数,因此需要搜索该表的所有行。
  • 应尽量避免在 where 子句中使用 or 来连接条件 (用or分割开的条件,如果or前的条件中的列有索引,而后面的列中没有索引,那么涉及的索引都不会被用到),否则将导致引擎放弃使用索引而进行全表扫描,如: select id from t where num=10 or num=20
  • 可以这样查询: select id from t where num=10 union all select id from t where num=20
  • in 和 not in 也要慎用,因为IN会使系统无法使用索引,而只能直接搜索表中的数据。如: select id from t where num in(1,2,3)
  • 对于连续的数值,能用 between 就不要用 in 了: select id from t where num between 1 and 3
  • 尽量避免在索引过的字符数据中,使用非打头字母%搜索。这也使得引擎无法利用索引。 见如下例子: SELECT * FROM T1 WHERE NAME LIKE ‘%L%’ SELECT * FROM T1 WHERE SUBSTING(NAME,2,1)=’L’ SELECT * FROM T1 WHERE NAME LIKE ‘L%’
  • 即使NAME字段建有索引,前两个查询依然无法利用索引完成加快操作,引擎不得不对全表所有数据逐条操作来完成任务。而第三个查询能够使用索引来加快操作
  • 应尽量避免在 where 子句中对字段进行表达式操作,这将导致引擎放弃使用索引而进行全表扫描
  • 应尽量避免在where子句中对字段进行函数操作,这将导致引擎放弃使用索引而进行全表扫描
  • 不要在 where 子句中的“=”左边进行函数、算术运算或其他表达式运算,否则系统将可能无法正确使用索引

共享锁,排他锁

  • InnoDB普通 select 语句默认不加锁(快照读,MYISAM会加锁),而CUD操作默认加排他锁
  • MySQL InnoDB存储引擎,实现的是基于多版本的并发控制协议——MVCC (Multi-Version Concurrency Control) (注:与MVCC相对的,是基于锁的并发控制,Lock-Based Concurrency Control)。MVCC最大的好处,相信也是耳熟能详:读不加锁读写不冲突。在读多写少的OLTP应用中,读写不冲突是非常重要的,极大的增加了系统的并发性能,这也是为什么现阶段,几乎所有的RDBMS,都支持了MVCC。
  • 多版本并发控制(MVCC)是一种用来解决读-写冲突的无锁并发控制,也就是为事务分配单向增长的时间戳,为每个修改保存一个版本,版本与事务时间戳关联,读操作只读该事务开始前的数据库的快照。 这样在读操作不用阻塞写操作,写操作不用阻塞读操作的同时,避免了脏读和不可重复读.MVCC 在语境中倾向于 “对多行数据打快照造平行宇宙”,然而 CAS 一般只是保护单行数据而已
  • 在MVCC并发控制中,读操作可以分成两类:快照读 (snapshot read)与当前读 (current read)。快照读,读取的是记录的可见版本 (有可能是历史版本),不用加锁。当前读,读取的是记录的最新版本,并且,当前读返回的记录,都会加上锁,保证其他事务不会再并发修改这条记录。
  • SELECT … LOCK IN SHARE MODE :共享锁(S锁, share locks)。其他事务可以读取数据,但不能对该数据进行修改,直到所有的共享锁被释放。
  • SELECT … FOR UPDATE:排他锁(X锁, exclusive locks)。如果事务对数据加上排他锁之后,则其他事务不能对该数据加任何的锁。获取排他锁的事务既能读取数据,也能修改数据。
  • InnoDB默认隔离级别 可重复读(Repeated Read)
  • 查询字段未加索引(主键索引、普通索引等)时,使用表锁
  • InnoDB行级锁基于索引实现
  • 索引数据重复率太高会导致全表扫描:当表中索引字段数据重复率太高,则MySQL可能会忽略索引,进行全表扫描,此时使用表锁。可使用 force index 强制使用索引。

隔离级别

  • Read Uncommitted(读取未提交内容): 在该隔离级别,所有事务都可以看到其他未提交事务的执行结果。本隔离级别很少用于实际应用,因为它的性能也不比其他级别好多少。读取未提交的数据,也被称之为脏读(Dirty Read)。
  • Read Committed(读取提交内容): 这是大多数数据库系统的默认隔离级别(但不是MySQL默认的)。它满足了隔离的简单定义:一个事务只能看见已经提交事务所做的改变。这种隔离级别 也支持所谓的不可重复读(Nonrepeatable Read),因为同一事务的其他实例在该实例处理其间可能会有新的commit,所以同一select可能返回不同结果。
  • Repeatable Read(可重读): 这是MySQL的默认事务隔离级别,它确保同一事务的多个实例在并发读取数据时,会看到同样的数据行。不过理论上,这会导致另一个棘手的问题:幻读 (Phantom Read)。简单的说,幻读指当用户读取某一范围的数据行时,另一个事务又在该范围内插入了新行,当用户再读取该范围的数据行时,会发现有新的“幻影” 行。InnoDB和Falcon存储引擎通过多版本并发控制(MVCC,Multiversion Concurrency Control)机制解决了该问题。
  • Serializable(可串行化): 这是最高的隔离级别,它通过强制事务排序,使之不可能相互冲突,从而解决幻读问题。简言之,它是在每个读的数据行上加上共享锁。在这个级别,可能导致大量的超时现象和锁竞争.

Spring IOC 怎么注入类,怎么实例化对象 实例化

  • Spring IoC容器则需要根据Bean定义里的配置元数据使用反射机制来创建Bean
  • 使用构造器实例化Bean 有参/无参;使用静态工厂实例化Bean;使用实例工厂实例化Bean.
  • 使用@Autowire注解注入的时机则是容器刚启动的时候就开始注入;注入之前要先初始化bean;ApplicationContext 的初始化和BeanFactory 有一个重大的区别:BeanFactory在初始化容器时,并未实例化Bean,直到第一次访问某个Bean 时才实例目标Bean;而ApplicationContext 则在初始化应用上下文时就实例化所有单实例的Bean。

注入

  • 接口、setter、构造器

AOP(*) 动态代理

@Aspect
public class Audience
{
    @Before("execution(** concert.Performance.perform(..))")        // 表演之前
    public void silenceCellPhones()
    {
        System.out.println("Silencing cell phones");
    }
    @Before("execution(** concert.Performance.perform(..))")        // 表演之前
    public void takeSeats()
    {
        System.out.println("Taking seats");
    }        
    @AfterReturning("execution(** concert.Performance.perform(..))")        // 表演之后
    public void applause()
    {
        System.out.println("CLAP CLAP CLAP!!!");
    }
    @AfterThrowing("execution(** concert.Performance.perform(..))")        // 表演失败之后
    public void demandRefound()
    {
        System.out.println("Demanding a refund");
    }
}   
  • JDK动态代理,接口,用Proxy.newProxyInstance生成代理对象,InvocationHandler
  • CGLIB,类,用enhancer生成代理对象,MethodInteceptor
  • 如果目标对象实现了接口,默认情况下会采用JDK的动态代理实现AOP ; 如果目标对象实现了接口,可以强制使用CGLIB实现AOP ; 如果目标对象没有实现了接口,必须采用CGLIB库,spring会自动在JDK动态代理和CGLIB之间转换;
  • 切点+注解
  • AspectJ是一个比较牛逼的AOP框架,他可以对类的成员变量,方法进行拦截。由于 AspectJ 是 Java 语言语法和语义的扩展,所以它提供了自己的一套处理方面的关键字。除了包含字段和方法之外,AspectJ 的方面声明还包含切入点和通知成员。 Spring AOP依赖的是 Spring 框架方便的、最小化的运行时配置,所以不需要独立的启动器。但是,使用这个技术,只能通知从 Spring 框架检索出的对象。Spring的AOP技术只能是对方法进行拦截。 在spring AOP中我们同样也可以使用类似AspectJ的注解来实现AOP功能,但是这里要注意一下,使AspectJ的注解时,AOP的实现方式还是Spring AOP。Spring缺省使用J2SE动态代理来作为AOP的代理,这样任何接口都可以被代理,Spring也可以使用CGLIB代理,对于需要代理类而不是代理接口的时候CGLIB是很有必要的。如果一个业务对象没有实现接口,默认就会使用CGLIB代理。 Spring AOP和AscpectJ之间的关系:Spring使用了和aspectj一样的注解,并使用Aspectj来做切入点解析和匹配。但是spring AOP运行时仍旧是纯的spring AOP,并不依赖于Aspectj的编译器或者织入器

volatile和内存模型(*)

happens-before

  • 什么是happens-before 令A和B表示两组操作,如果A happens-before B,那么由A操作引起的内存变化,在B开始执行之前,都应该是可见的。 A happens-before B,不代表A在B之前执行.
  • 如何确保happen-before 锁(互斥锁、读写锁等)、内存屏障

内存屏障

  • 内存屏障是一个指令,这个指令可以保证屏障前后的指令遵守一定的顺序,并且保证一定的可见性
  • 为了实现volatile的内存语义,编译器在生成字节码时,会在指令序列中插入内存屏障来禁止特定类型的处理器重排序。

Java内存模型

  • 屏蔽各个硬件平台和操作系统的内存访问差异,以实现让 Java 程序在各种平台下都能达到一致的内存访问效果
  • Java内存模型 规定所有的变量都是存在主存当中(类似于前面说的物理内存),每个线程都有自己的工作内存(类似于前面的高速缓存)。线程对变量的所有操作都必须在工作内存中进行,而不能直接对主存进行操作,并且每个线程不能访问其他线程的工作内存。

原子性

  • 只有简单的读取、赋值(而且必须是将数字赋值给某个变量,变量之间的相互赋值不是原子操作)才是原子操作
  • Java内存模型只保证了基本读取和赋值是原子性操作,如果要实现更大范围操作的原子性,可以通过 synchronized 和 Lock 来实现

可见性

  • 当一个共享变量被 volatile 修饰时,它会保证修改的值会立即被更新到主存,当有其他线程需要读取时,它会去内存中读取新值. 通过 synchronized 和 Lock 也能够保证可见性,synchronized 和 Lock 能保证同一时刻只有一个线程获取锁然后执行同步代码,并且 在释放锁之前会将对变量的修改刷新到主存当中,因此可以保证可见性

有序性

  • 指令重排序
  • 不能由于 synchronized 和 Lock 可以让线程串行执行同步代码,就说它们可以保证指令不会发生重排序

volatile

  • 保证了不同线程对共享变量进行操作时的可见性,即一个线程修改了某个变量的值,这个新值对其他线程来说是 立即可见
  • 禁止进行指令重排序 (双重检查锁单例模式)
  • synchronized 也可以保证可见性,因为每次运行synchronized块 或者 synchronized方法都会导致线程工作内存与主存的同步,使得其他线程可以取得共享变量的最新值。也就是说,synchronized 语义范围不但包括 volatile 具有的可见性,也包括原子性,但不能禁止指令重排序,这是二者一个功能上的差异

i被volatile修饰,如果多线程来运行i++,那么是否可以达到理想的效果?

  • 不能,volatile不能保证操作的原子性

Sleep()和wait()的区别,使用wait()方法后,怎么唤醒线程(*)

笔试题经常考

  • sleep方法只让出了CPU,而并不会释放同步资源锁
  • wait()方法则是指当前线程让自己暂时退让出同步资源锁,以便其他正在等待该资源的线程得到该资源进而运行
  • sleep()方法可以在任何地方使用;wait()方法则只能在同步方法或同步块中使用
  • sleep()是线程线程类(Thread)的方法,调用会暂停此线程指定的时间,但监控依然保持,不会释放对象锁,到时间自动恢复;wait()是Object的方法,调用会放弃对象锁,进入等待队列,待调用notify()/notifyAll()唤醒指定的线程或者所有线程,才会进入锁池,不再次获得对象锁才会进入运行状态
  • notify让之前调用wait的线程有权利重新参与线程的调度

Mybatis缓存(*)

  • 一级缓存的作用域是同一个SqlSession,在同一个sqlSession中两次执行相同的sql语句,第一次执行完毕会将数据库中查询的数据写到缓存(内存),第二次会从缓存中获取数据将不再从数据库查询,从而提高查询效率。当一个sqlSession结束后该sqlSession中的一级缓存也就不存在了。Mybatis默认开启一级缓存
  • 二级缓存是mapper级别的缓存,多个SqlSession去操作同一个Mapper的sql语句,多个SqlSession去操作数据库得到数据会存在二级缓存区域,多个SqlSession可以共用二级缓存,二级缓存是跨SqlSession的。不同的sqlSession两次执行相同namespace下的sql语句且向sql中传递参数也相同即最终执行相同的sql语句,第一次执行完毕会将数据库中查询的数据写到缓存(内存),第二次会从缓存中获取数据将不再从数据库查询,从而提高查询效率。Mybatis默认没有开启二级缓存需要在setting全局参数中配置开启二级缓存
  • https://segmentfault.com/a/1190000013678579

Redis的数据结构(*)

  • String, Hash, List, Set, ZSet

Hash底层结构

  • redis的哈希对象的底层存储可以使用ziplist(压缩列表)和hashtable

Redis缓存怎么运行的?

  • 使用ANSI C编写的开源、支持网络、基于内存、可选持久性的键值对存储数据库
  • 主从复制
  • 哨兵模式

持久化

  • 快照文件
  • AOF语句追加

过期策略

  • https://blog.csdn.net/xiangnan129/article/details/54928672

反向代理是什么?

  • 反向代理(Reverse Proxy)方式是指以代理服务器来接受internet上的连接请求,然后将请求转发给内部网络上的服务器,并将从服务器上得到的结果返回给internet上请求连接的客户端,此时代理服务器对外就表现为一个反向代理服务器。客户端只会得知反向代理的IP地址,而不知道在代理服务器后面的服务器簇的存在.

负载均衡是什么?

  • 负载平衡(Load balancing)是一种计算机技术,用来在多个计算机(计算机集群)、网络连接、CPU、磁盘驱动器或其他资源中分配负载,以达到最优化资源使用、最大化吞吐率、最小化响应时间、同时避免过载的目的。 使用带有负载平衡的多个服务器组件,取代单一的组件,可以通过冗余提高可靠性。负载平衡服务通常是由专用软件和硬件来完成。 主要作用是将大量作业合理地分摊到多个操作单元上进行执行,用于解决互联网架构中的高并发和高可用的问题。

单例模式(*)

必考,静态内部类,双重检查锁至少会写一个

  • 私有的构造方法; 指向自己实例的私有静态引用; 以自己实例为返回值的静态的公有方法。

双重检查锁

public class Singleton2 {

private volatile static Singleton2 singleton2;

private Singleton2() {
}

public static Singleton2 getSingleton2() {

    if (singleton2 == null) {
        synchronized (Singleton2.class) {
            if (singleton2 == null) {
                singleton2 = new Singleton2();
            }
        }
    }
    return singleton2;
}
}
  • 第一个if (instance == null),只有instance为null的时候,才进入synchronized. 第二个if (instance == null),是为了防止可能出现多个实例的情况。
  • volatile: 主要在于singleton = new Singleton()这句,这并非是一个原子操作,事实上在 JVM 中这句话大概做了下面 3 件事情。   1. 给 singleton 分配内存   2. 调用 Singleton 的构造函数来初始化成员变量,形成实例   3. 将singleton对象指向分配的内存空间(执行完这步 singleton才是非 null 了)但是在 JVM 的即时编译器中存在指令重排序的优化。   也就是说上面的第二步和第三步的顺序是不能保证的,最终的执行顺序可能是 1-2-3 也可能是 1-3-2。如果是后者,则在 3 执行完毕、2 未执行之前,被线程二抢占了 ,这时 instance 已经是非 null 了(但却没有初始化),所以线程二会直接返回 instance,然后使用,然后顺理成章地报错。

静态内部类

public class Singleton1 {

private Singleton1() {
}

public static final Singleton1 getSingleton1() {
    return Singleton1Holder.singleton1;
}

private static class Singleton1Holder {
    private static final Singleton1 singleton1 = new Singleton1();
}
}

ThreadLocal内存泄露?

static class ThreadLocalMap {
    /**
     * The entries in this hash map extend WeakReference, using
     * its main ref field as the key (which is always a
     * ThreadLocal object).  Note that null keys (i.e. entry.get()
     * == null) mean that the key is no longer referenced, so the
     * entry can be expunged from table.  Such entries are referred to
     * as "stale entries" in the code that follows.
     */
    static class Entry extends WeakReference<ThreadLocal> {
        /** The value associated with this ThreadLocal. */
        Object value;

        Entry(ThreadLocal k, Object v) {
            super(k);
            value = v;
        }
    }
    ...
}
  • ThreadLocalMap里面对Key的引用是弱引用。那么,就存在这样的情况:当释放掉对threadlocal对象的强引用后,map里面的value没有被回收,但却永远不会被访问到了,因此ThreadLocal存在着内存泄露问题
  • Java为了最小化减少内存泄露的可能性和影响,在ThreadLocal进行get、set操作时会清除线程Map里所有key为null的value。所以最怕的情况就是,ThreadLocal对象设null了,开始发生“内存泄露”,然后使用线程池,线程结束后被放回线程池中而不销毁,那么如果这个线程一直不被使用或者分配使用了又不再调用get/set方法,那么这个期间就会发生真正的内存泄露。因此,最好的做法是:在不使用该ThreadLocal对象时,及时调用该对象的remove方法去移除ThreadLocal.ThreadLocalMap中的对应Entry.

线程死锁检测工具?

  • Jconsole, Jstack, visualVM

线程池调优?

  • 设置最大线程数,防止线程资源耗尽;
  • 使用有界队列,从而增加系统的稳定性和预警能力(饱和策略);
  • 根据任务的性质设置线程池大小:CPU密集型任务(CPU个数个线程),IO密集型任务(CPU个数两倍的线程),混合型任务(拆分)。

几种锁?

  • 无锁状态,偏向锁状态,轻量级锁状态和重量级锁状态,它会随着竞争情况逐渐升级。锁可以升级但不能降级,意味着偏向锁升级成轻量级锁后不能降级成偏向锁。这种锁升级却不能降级的策略,目的是为了提高获得锁和释放锁的效率

偏向锁

  • 偏向锁的目的是在某个线程获得锁之后,消除这个线程锁重入(CAS)的开销,看起来让这个线程得到了偏护
  • 偏向锁使用了一种等到竞争出现才释放锁的机制,所以当其他线程尝试竞争偏向锁时,持有偏向锁的线程才会释放锁

自旋锁

  • 线程的阻塞和唤醒需要CPU从用户态转为核心态,频繁的阻塞和唤醒对CPU来说是一件负担很重的工作. 所谓“自旋”,就是让线程去执行一个无意义的循环,循环结束后再去重新竞争锁,如果竞争不到继续循环,循环过程中线程会一直处于running状态,但是基于JVM的线程调度,会出让时间片,所以其他线程依旧有申请锁和释放锁的机会。
  • 自旋锁省去了阻塞锁的时间空间(队列的维护等)开销,但是长时间自旋就变成了“忙式等待”,忙式等待显然还不如阻塞锁。所以自旋的次数一般控制在一个范围内,例如10,100等,在超出这个范围后,自旋锁会升级为阻塞锁。

轻量级锁

  • 线程尝试使用CAS将对象头中的Mark Word替换为指向锁记录的指针。如果成功,当前线程获得锁,如果失败,则自旋获取锁,当自旋获取锁仍然失败时,表示存在其他线程竞争锁(两条或两条以上的线程竞争同一个锁),则轻量级锁会膨胀成重量级锁。

重量级锁

  • 重量锁在JVM中又叫对象监视器(Monitor),它很像C中的Mutex,除了具备Mutex(0|1)互斥的功能,它还负责实现了Semaphore(信号量)的功能,也就是说它至少包含一个竞争锁的队列,和一个信号阻塞队列(wait队列),前者负责做互斥,后一个用于做线程同步。

enter image description here

innoDB和MyISAM的区别? (*)

  • https://www.jianshu.com/p/a957b18ba40d
  • InnoDB支持事务,MyISAM不支持,对于InnoDB每一条SQL语言都默认封装成事务,自动提交,这样会影响速度,所以最好把多条SQL语言放在begin和commit之间,组成一个事务;
  • InnoDB支持外键,而MyISAM不支持。对一个包含外键的InnoDB表转为MYISAM会失败;
  • InnoDB是聚集索引,数据文件是和索引绑在一起的,必须要有主键,通过主键索引效率很高。但是辅助索引需要两次查询,先查询到主键,然后再通过主键查询到数据。因此,主键不应该过大,因为主键太大,其他索引也都会很大。而MyISAM是非聚集索引,数据文件是分离的,索引保存的是数据文件的指针。主键索引和辅助索引是独立的。
  • InnoDB不保存表的具体行数,执行select count(*) from table时需要全表扫描。而MyISAM用一个变量保存了整个表的行数,执行上述语句时只需要读出该变量即可,速度很快;
  • Innodb不支持全文索引,而MyISAM支持全文索引,查询效率上MyISAM要高;

索引失效 (*)

  • https://blog.csdn.net/qq_32331073/article/details/79041232

MySQL 索引实现原理+几种索引 (*)

普通索引

  • B+ree
  • MyISAM的B+Tree的叶子节点上的data,并不是数据本身,而是数据存放的地址。主索引和辅助索引没啥区别,只是主索引中的key一定得是唯一的。这里的索引都是非聚簇索引.

InnoDB

  • InnoDB 的数据文件本身就是索引文件,B+Tree的叶子节点上的data就是数据本身,key为主键,这是聚簇索引。
  • 因为InnoDB的数据文件本身要按主键聚集,所以InnoDB要求表必须有主键(MyISAM可以没有),如果没有显式指定,则MySQL系统会自动选择一个可以 唯一 标识数据记录的列作为主键,如果不存在这种列,则MySQL自动为InnoDB表生成一个隐含字段作为主键,这个字段长度为6个字节,类型为长整形。
  • 聚集索引这种实现方式使得按主键的搜索十分高效,但是辅助索引(普通索引)搜索需要 检索两遍索引:首先检索辅助索引获得主键,然后用主键到主索引中检索获得记录.

几种索引

  • 主键索引;
  • 唯一索引;
  • 普通索引;
  • 联合索引;
  • 全文索引。

辅助索引

  • https://www.cnblogs.com/xiangyangzhu/p/index.html

为什么用B+树

  • https://blog.csdn.net/xlgen157387/article/details/79450295
  • 在MySQL中的数据一般是放在磁盘中的,读取数据的时候肯定会有访问磁盘的操作,磁盘中有两个机械运动的部分,分别是盘片旋转和磁臂移动。盘片旋转就是我们市面上所提到的多少转每分钟,而磁盘移动则是在盘片旋转到指定位置以后,移动磁臂后开始进行数据的读写。那么这就存在一个定位到磁盘中的块的过程,而定位是磁盘的存取中花费时间比较大的一块,毕竟机械运动花费的时候要远远大于电子运动的时间。当大规模数据存储到磁盘中的时候,显然定位是一个非常花费时间的过程,但是我们可以通过B树进行优化,提高磁盘读取时定位的效率。
  • 为什么B类树可以进行优化呢?我们可以根据B类树的特点,构造一个多阶的B类树,然后在尽量多的在结点上存储相关的信息,保证层数尽量的少,以便后面我们可以更快的找到信息,磁盘的I/O操作也少一些,而且B类树是平衡树,每个结点到叶子结点的高度都是相同,这也保证了每个查询是稳定的。
  • 总的来说,B/B+树是为了磁盘或其它存储设备而设计的一种平衡多路查找树(相对于二叉,B树每个内节点有多个分支),与红黑树相比,在相同的的节点的情况下,一颗B/B+树的高度远远小于红黑树的高度(在下面B/B+树的性能分析中会提到)。B/B+树上操作的时间通常由存取磁盘的时间和CPU计算时间这两部分构成,而CPU的速度非常快,所以B树的操作效率取决于访问磁盘的次数,关键字总数相同的情况下B树的高度越小,磁盘I/O所花的时间越少。

B+树的插入删除

  • https://www.cnblogs.com/nullzx/p/8729425.html

为什么说B+树比B树更适合数据库索引

  • B+树的磁盘读写代价更低:B+树的内部节点并没有指向关键字具体信息的指针,因此其内部节点相对B树更小,如果把所有同一内部节点的关键字存放在同一盘块中,那么盘块所能容纳的关键字数量也越多,一次性读入内存的需要查找的关键字也就越多,相对IO读写次数就降低了。
  • B+树的查询效率更加稳定:由于非终结点并不是最终指向文件内容的结点,而只是叶子结点中关键字的索引。所以任何关键字的查找必须走一条从根结点到叶子结点的路。所有关键字查询的路径长度相同,导致每一个数据的查询效率相当。
  • 由于B+树的数据都存储在叶子结点中,分支结点均为索引,方便扫库,只需要扫一遍叶子结点即可,但是B树因为其分支结点同样存储着数据,我们要找到具体的数据,需要进行一次中序遍历按序来扫,所以B+树更加适合在区间查询的情况,所以通常B+树用于数据库索引。

JVM内存配置参数

  • -Xmx Java Heap最大值,默认值为物理内存的1/4,最佳设值应该视物理内存大小及计算机内其他内存开销而定;
  • -Xms Java Heap初始值,Server端JVM最好将-Xms和-Xmx设为相同值,开发测试机JVM可以保留默认值;
  • -Xmn Java Heap Young区大小,不熟悉最好保留默认值;
  • -Xss 每个线程的Stack大小,不熟悉最好保留默认值;

extends 抽象类和 interface 区别 (*)

  • 接口(interface)可以说成是抽象类的一种特例,接口中的所有方法都必须是抽象的。
  • 接口中的方法定义默认为 public abstract 类型,接口中的成员变量类型默认为 public static final
  • 抽象类可以有构造方法,接口中不能有构造方法。
  • 抽象类中可以有普通成员变量,接口中没有普通成员变量。
  • 抽象类中可以包含非抽象的普通方法,接口中的所有方法必须都是抽象的,不能有非抽象的普通方法。
  • 抽象类中的抽象方法的访问类型可以是public,protected,但接口中的抽象方法只能是public类型的,并且默认即为public abstract类型。
  • 抽象类中可以包含静态(static)方法,接口中不能包含静态(static)方法。
  • 抽象类和接口中都可以包含静态成员变量(static),抽象类中的静态成员变量的访问类型可以任意,但接口中定义的变量只能是public static final类型,并且默认即为public static final类型。
  • 一个类只能继承一个抽象类,但是可以实现多个接口。
  • 一个接口可以继承多个接口。
  • 抽象类所体现的是一种继承关系,要想使得继承关系合理,父类和派生类之间必须存在”is-a”关系关系,即父类和派生类在概念本质上应该是相同的。对于接口则不然,并不要求接口的实现者和接口定义在概念本质上是一致的,仅仅是实现了接口定义的契约而已,是”like-a”的关系。

Servlet生命周期

  • 调用 init() 方法初始化
  • 调用 service() 方法来处理客户端的请求
  • 调用 destroy() 方法释放资源,标记自身为可回收
  • 被垃圾回收器回收

Cookie, Session区别

  • cookie数据存放在客户的浏览器上,session数据放在服务器上
  • cookie不是很安全,别人可以分析存放在本地的COOKIE并进行COOKIE欺骗,考虑到安全应当使用session
  • session会在一定时间内保存在服务器上。当访问增多,会比较占用你服务器的性能,考虑到减轻服务器性能方面,应当使用COOKIE
  • 单个cookie在客户端的限制是3K,就是说一个站点在客户端存放的COOKIE不能3K。

END

本文参与 腾讯云自媒体分享计划,分享自微信公众号。
原始发表:2018-12-30,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 程序员乔戈里 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体分享计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
相关产品与服务
容器服务
腾讯云容器服务(Tencent Kubernetes Engine, TKE)基于原生 kubernetes 提供以容器为核心的、高度可扩展的高性能容器管理服务,覆盖 Serverless、边缘计算、分布式云等多种业务部署场景,业内首创单个集群兼容多种计算节点的容器资源管理模式。同时产品作为云原生 Finops 领先布道者,主导开源项目Crane,全面助力客户实现资源优化、成本控制。
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档