类的加载过程:Java虚拟机把描述类的数据把.class字节码文件加载到内存,并对数据进行校验、转换解析和初始化,最终形成可以被虚拟机直接使用的Class对象。类的生命周期包括:加载,验证,准备,解析,初始化,使用,卸载。按顺序开始,而不是按顺序进行或完成,因为这些阶段通常都是互相交叉地混合进行的,通常在一个阶段执行的过程中调用或激活另一个阶段。
保证类加载流程的安全性,才使用的双亲委派?
当一个类加载器收到了类加载的请求的时候,他不会直接去加载指定的类,而是把这个请求委托给自己的父加载器去加载。 只有父加载器无法加载这个类的时候,才会由当前这个加载器来负责类的加载。自定义Class Loader ->Appilication Class loader -> Extension Classloader ->Bootstrap ClassLoader ;
违例:JDBC–Context Classloader
tomcat 为每个 App 创建一个 Loader,里面保存着此 WebApp 的 ClassLoader。需要加载 WebApp 下的类时,就取出 ClassLoader 来使用
时机:1.new对象 2.反射调用对象 3.调用类的静态方法4.读写类的静态变量5.初始化某类是他的直接父类还未初始化则会初始化其父类6.启动时会初始化main()方法所在启动类。 准备阶段,虚拟机会将类变量(static 修饰的变量)分配内存并设置零值。
在类初始化阶段,执行类构造器 () 方法。 类初始化方法有如下特点:
编译器会在将 .java 文件编译成 .class 文件时,收集所有类初始化代码和 static {} 域的代码,收集在一起成为 () 方法;
子类初始化时会首先调用父类的 () 方法;
JVM 会保证 () 方法的线程安全,保证同一时间只有一个线程执行。
反射是一种运行时获取和修改对象数据的能力。1.Class.forname(“全限定名”) 2.xx.getClass() 3.String.class 原理:Method类的invoke方法,获取一个MethodAccessor对象ma【此时会check是否已有现成的ma,有就返回,没有就用ReflectionFactory 对象的 newMethodAccessor 方法生成一个,此处会返回一个代理了NativeMethodAccessorImpl 对象(delegate熟属性)的DelegatingMethodAccessorImpl对象。】,用ma.invoke()来返回反射的对象,此时便调用了NativeMethodAccessorImpl 的 invoke 方法,而在 NativeMethodAccessorImpl 的 invoke 方法里,其会判断调用次数是否超过阀值(numInvocations)。如果超过该阀值,那么就会生成另一个MethodAccessor 对象,并将原来 DelegatingMethodAccessorImpl 对象中的 delegate 属性指向最新的 MethodAccessor 对象。 实际的 MethodAccessor 实现有两个版本,一个是 Native 版本,一个是 Java 版本。
String类利用了final修饰的char类型数组存储字符,源码如下:
private final char value[];
StringBuilder与StringBuffer都继承自AbstractStringBuilder类,在AbstractStringBuilder中也是使用字符数组保存字符串,这两种对象都是可变的。源码如下:
char[] value;
在JDK1.7 和JDK1.8 中有所差别:在JDK1.7 中,由“数组+链表”组成。数组是 HashMap 的主体,链表则是主要为了解决哈希冲突而存在的。
在JDK1.8 中,由“数组+链表+红黑树”组成。当链表过长,则会严重影响 HashMap 的性能,红黑树搜索时间复杂度是 O(logn),而链表是糟糕的 O(n)。因此,JDK1.8 对数据结构做了进一步的优化,引入了红黑树,链表和红黑树在达到一定条件会进行转换:
JDK1.7中的ConcurrentHashMap 是由 Segment 数组结构和 HashEntry 数组结构组成,即ConcurrentHashMap 把哈希桶切分成小数组(Segment ),每个小数组有 n 个 HashEntry 组成。
其中,Segment 继承了 ReentrantLock,所以 Segment 是一种可重入锁,扮演锁的角色;HashEntry 用于存储键值对数据。
首先将数据分为一段一段的存储,然后给每一段数据配一把锁,当一个线程占用锁访问其中一个段数据时,其他段的数据也能被其他线程访问,能够实现真正的并发访问。
再来看下JDK1.8
在数据结构上, JDK1.8 中的ConcurrentHashMap 选择了与 HashMap 相同的数组+链表+红黑树结构;在锁的实现上,抛弃了原有的 Segment 分段锁,采用CAS + synchronized实现更加低粒度的锁。
将锁的级别控制在了更细粒度的哈希桶元素级别,也就是说只需要锁住这个链表头结点(红黑树的根节点),就不会影响其他的哈希桶元素的读写,大大提高了并发度。
先来看JDK1.7
首先,会尝试获取锁,如果获取失败,利用自旋获取锁;如果自旋重试的次数超过 64 次,则改为阻塞获取锁。
获取到锁后:
将当前 Segment 中的 table 通过 key 的 hashcode 定位到 HashEntry。
遍历该 HashEntry,如果不为空则判断传入的 key 和当前遍历的 key 是否相等,相等则覆盖旧的 value。
不为空则需要新建一个 HashEntry 并加入到 Segment 中,同时会先判断是否需要扩容。
释放 Segment 的锁。
再来看JDK1.8
大致可以分为以下步骤:
相同点:都是接口,都可以编写多线程程序,都采用Thread.Start()启动线程。
不同点:Runnable接口run方法无返回值,Callable接口Call方法有返回值,是个泛型,和Future、FutureTask配合来获取异步执行的结果。Runnable接口run方法只能抛出运行时的异常,并且无法捕获处理;Callable接口的call方法允许抛出异常,可以获取异常信息。Callable接口支持返回执行结果,需要调用FutureTask.get()得到,此方法会阻塞主进程的继续往下执行,如果不调用就不会阻塞。"
sleep()是Thread线程类的静态方法,wait()是object类的方法。
sleep()不释放锁;wait()释放锁。
wait通常被用于线程间交互/通信,sleep通常被用于暂停执行。
wait()方法被调用后,线程不会自动苏醒,需要别的线程调用同一个对象上的notify()或者notifyAll()方法。Sleep()方法执行完成后,线程会自动苏醒。或者可以调用wait(long timeOut)超时后线程或自动苏醒
在锁对象的对象头里面有一个threadid字段,在第一次访问的时候threadid为空,jvm让其持偏向锁,并将threadid设置为线程的id,再次进入的时候会先判断threadid是否与其它线程id一致,如果一致的话可以直接使用此对象,如果不一致,则升级偏向锁成为轻量级锁,通过自旋循环一定次数来获取锁,执行一定次数之后,如果还没有正常获取到要使用的对象,此时就会把锁从轻量级升级为重量级锁,这个过程就构成了 synchronized 锁的升级。
死锁是指2个或2个以上的进程在执行过程中,由于竞争资源或者由于彼此通信而照成的一种堵塞现象。 死锁就是2个线程要相互等待对方释放对象锁。
产生原因,如何避免死锁?
只要其中的一个条件不成立的话, 就不会产生死锁。
互斥条件是无法破坏的。
比如:将系统中的所有资源标识设置标志位、排序,规定所有的线程申请资源必须按照一定的顺序来进行操作, 去避免循环等待。
破坏不剥夺条件: 占用部分资源的线程如果在去申请其他资源的时候,如果申请不到,可以主动师傅它只有的资源 。
并行流就是把一个内容分成多个数据块,并用不同的线程分成多个数据块,并用不同的线程分别处理每个数据块的流。
采用了Fock/Join框架来进行对数据的并行处理,简单说就是将一个大任务进行拆分(Fock)成若干个小任务,再将一个个小任务运算的记过进行Join汇总。
过滤:
filter:按条件过滤集合中的数据
distinct: distinct操作类似于我们在写SQL语句时,添加的DISTINCT关键字,用于去重处理,distinct基于Object.equals(Object)实现
limit:类似于SQL语句中的LIMIT关键字,不过相对功能较弱,limit返回包含前n个元素的流,当集合大小小于n时,则返回实际长度。
sorted:该操作用于对流中元素进行排序,sorted要求待比较的元素必须实现Comparable接口,如果没有实现也不要紧,我们可以将比较器作为参数传递给sorted(Comparator super T> comparator)。
skip:skip操作与limit操作相反,如同其字面意思一样,是跳过前n个元素
映射:
map
flatMap
查找:
allMatch: 用于检测是否全部都满足指定的参数行为,如果全部满足则返回true
anyMatch: 则是检测是否存在一个或多个满足指定的参数行为,如果满足则返回true
noneMathch: 用于检测是否不存在满足指定行为的元素,如果不存在则返回true
findFirst: 用于返回满足条件的第一个元素
findAny: 相对于findFirst的区别在于,findAny不一定返回第一个,而是返回任意一个
stream 的分组操作:
1、Collectors.groupingBy来操作集合
Map
2、多级分组:
Map
Collectors.groupingBy(Student::getSchool, // 一级分组,按学校
Collectors.groupingBy(Student::getMajor))); // 二级分组,按专业
3、Collector.counting,分组后统计个数:
SpringBoot应用程序的启动流程主要包括初始化SpringApplication和运行SpringApplication两个过程。1.初始化SpringApplication:配置基本的环境变量、资源、构造器和监听器,为运行SpringApplciation实例对象作准备;2.SpringApplication.run():SpringApplicationRunListeners 引用启动监控模块、ConfigrableEnvironment配置环境模块和监听及ConfigrableApplicationContext配置应用上下文。当完成刷新应用的上下文和调用SpringApplicationRunListener#contextPrepared方法后表示SpringBoot应用程序已经启动完成。
主从复制原理:
主节点(master)负责读写,从节点(slave)负责读。这个系统的运行依靠三个主要的机制:
a. 当一个 master 实例和一个 slave 实例连接正常时, master 会发送一连串的命令流来保持对 slave 的更新,以便于将自身数据集的改变复制给 slave ,包括客户端的写入、key 的过期或被逐出等等。
b. 当 master 和 slave 之间的连接断开之后,因为网络问题、或者是主从意识到连接超时, slave 重新连接上 master 并会尝试进行部分重同步:这意味着它会尝试只获取在断开连接期间内丢失的命令流。
c. 当无法进行部分重同步时, slave 会请求进行全量重同步。这会涉及到一个更复杂的过程,例如 master 需要创建所有数据的快照,将之发送给 slave ,之后在数据集更改时持续发送命令流到 slave 。
主从复制优缺点:
优点:
a. 高可靠性,采用双机主备架构,能够在主库出现故障时自动进行主备切换,从库提升为主库提供服务,保证服务平稳运行。另一方面,开启数据持久化功能和配置合理的备份策略,能有效的解决数据误操作和数据异常丢失的问题
b. 读写分离策略,从节点可以扩展主库节点的读能力,能有效应对大并发量的读操作。
弊端:
a. 故障恢复复杂,主节点挂了之后,需要手动将一个从节点晋升为主节点,同事需要通知业务方变更配置,并且需要让其他从节点去复制新的主节点,整个过程需要人为干预,比较繁琐。
b. 主库的写能力受到单机的限制,可以考虑分片
c. 主库的存储能力受到单机的限制,可以考虑Pika
d. 丛节点复制数据有弊端。
哨兵模式:
原理:Redis Sentinel是社区版本推出的原生高可用解决方案,其部署架构主要包括两部分:Redis Sentinel集群和Redis数据集群。
其中Redis Sentinel集群是由若干Sentinel节点组成的分布式集群,可以实现故障发现、故障自动转移、配置中心和客户端通知。
Redis Sentinel的节点数量要满足2n+1(n>=1)的奇数个
哨兵模式优缺点:
优点:
a. redis sentinel 集群部署简单
b. 能够解决主从模式下的高可用切换问题
c. 很方便实现Redis数据节点的线性扩展,轻松突破Redis自身单线程瓶颈,可极大满足Redis大容量或高性能的业务需求
d. 可以实现一套Sentinel监控一组Redis数据节点或多组数据节点
缺点:
a. 资源浪费,Redis数据节点中slave节点作为备份节点不提供服务;
b. Redis Sentinel主要是针对Redis数据节点中的主节点的高可用切换,对Redis的数据节点做失败判定分为主观下线和客观下线两种,对于Redis的从节点有对节点做主观下线操作,并不执行故障转移。
c. 不能解决读写分离问题,实现起来相对复杂。
集群的原理:
Redis Cluster是社区版推出的Redis分布式集群解决方案,主要解决Redis分布式方面的需求,比如,当遇到单机内存,并发和流量等瓶颈的时候,Redis Cluster能起到很好的负载均衡的目的。
Redis Cluster集群节点最小配置6个节点以上(3主3从),其中主节点提供读写操作,从节点作为备用节点,不提供请求,只作为故障转移使用。
Redis Cluster采用虚拟槽分区,所有的键根据哈希函数映射到0~16383个整数槽内,每个节点负责维护一部分槽以及槽所印映射的键值数据。
集群模式下多个master节点,是怎么解决数据分片问题的? 希望数据平均分配的话,用的什么算法?
1、 哈希取模算法:
当有n个节点得时候,此时这多个节点都是正常得,这些节点都是固定有序得。
当存储一个数据,会对这个数据得key获取哈希值然后进行取模操作,取模得到得值肯定不会大于节点数量得。
通过得到的值去操作对应的节点。
如果此时,有一个节点挂机了,等于之前节点的顺序改变了。除了失去了这个节点上面得数据外,
若此时对key进行哈希取模,就会发现得到的值很有可能找不到对应的节点去拿了,
这就可能丢掉得不只是一个节点得数据。
2、 一致性哈希算法
一致性哈希的原理: 把所有的哈希值空间组织成一个虚拟的圆环(哈希环),整个空间按顺时针方向组织。
因为是环形空间,0 和 2^32-1 是重叠的。 假设我们有四台机器要哈希环来实现映射(分布数据),
我们先根据机器的名称或者 IP 计算哈希值,然后分布到哈希环中(红色圆圈)。
集群的优缺点
优点:
a. 无中心架构;
b. 数据按照slot存储分布在多个节点,节点间数据共享,可动态调整数据分布;
c. 可扩展性:可线性扩展到1000多个节点,节点可动态添加或删除;
d. 高可用性:部分节点不可用时,集群仍可用。通过增加Slave做standby数据副本,能够实现故障自动failover,节点之间通过gossip协议交换状态信息,用投票机制完成Slave到Master的角色提升;
e. 降低运维成本,提高系统的扩展性和可用性
缺点:
a. 数据通过异步复制,不保证数据的强一致性。
b. 节点会因为某些原因发生阻塞(阻塞时间大于clutser-node-timeout),被判断下线,这种failover是没有必要的。
c. 多个业务使用同一套集群时,无法根据统计区分冷热数据,资源隔离性较差,容易出现相互影响的情况。
d. 不支持多数据库空间,单机下的redis可以支持到16个数据库,集群模式下只能使用1个数据库空间,即db0。
缓存穿透,缓存雪崩问题,如何避免?
缓存穿透说简单点就是大量请求的 key 根本不存在于缓存中,导致请求直接到了数据库上,根本没有经过缓存这一层
缓存雪崩就是缓存在同一时间大面积的失效,后面的请求都直接落到了数据库上,造成数据库短时间内承受大量请求
缓存穿透解决方案:
缓存雪崩解决方案:
缓存淘汰策略:
LRU(Least Recently Used):淘汰最近最少使用的key。在缓存写满的时候,会根据所有数据的访问记录,淘汰掉未来被访问几率最低的数据
LFU(Least Frequently Used):优先淘汰最不常用的、使用最少的key,LFU的侧重点是缓存的使用频率,系统有大量热点缓存数据可能更适合
FIFO(First In First Out): 先进先出。即先缓存进来的数据会优先被淘汰。
RANDOM: 随机。随机淘汰,适用于缓存数据被访问的概率差不多时
Redis的淘汰策略:
allkeys-lru:从所有 key 中使用 LRU 算法,选出最近使用最少的数据进行淘汰;
volatile-lru:从设置了过期时间的 key 中使用 LRU 算法,选出最近使用最少的数据进行淘汰;
allkeys-lfu:从所有 key 中使用 LFU 算法,选出使用频率最低的数据,进行淘汰;
volatile-lfu:从设置了过期时间的 key 中使用 LFU 算法,选出使用频率最低的数据进行淘汰;
allkeys-random:从所有的 key 中,随机选出数据进行淘汰;
volatile-random:从设置了过期时间的 key 中,随机选出数据进行淘汰;
volatile-ttl:从设置了过期时间的 key 中,选出即将过期的数据(按照过期时间的先后,选出最先过期的数据)进行淘汰;
noeviction:禁止淘汰数据,写入操作报错。这是 Redis 默认的内存淘汰策略。