熟练掌握 JAVASE 基础语法,熟悉集合框架底层、熟悉并发编程,熟悉理解锁 熟练掌握 Redis 的使用,熟悉 Redis 的持久化机制、哨兵原理、集群脑裂及双写一致性问题 、缓存穿透、缓存击穿、缓存雪崩、主从复制 熟练使用 Dubbo、Zookeeper,以及了解底层 RPC 原理,Zookeeper 分布式锁,Leader 选举,集群同步等 熟练使用各种框架,Spring、Mybatis,Mybatis-plus、SpringMvc、SpringBoot 等 熟练使用微服务框架 SpringCloud,了解各个组件 了解 JVM、垃圾回收机制 熟练使用 MySQL,Oracle 等关系数据库,掌握基本的 sql 调优及原理 熟悉消息中间件,Rabbitmq、Rocketmq、Kafka 熟悉消息丢失、消息重复消费、消息乱序、消息堆积解决方案 熟练掌握 Linux 系统的基本操作,熟悉 Nginx、Docker、ElasticSearch、FastDFS 等 熟练使用 java 开发工具,Ecplise 、IDEA,及代码管理管理 Git、SVN,及 MAVEN
一 JAVASE 基础语法
Java 集合体系有什么?
集合类存放于 Java.util 包中,主要有 3 种:set(集)、list(列表包含 Queue) 和 map(映射)
1. Collection:Collection 是集合 List、Set、Queue 的最基本的接口。
2. Iterator:迭代器,可以通过迭代器遍历集合中的数据。
3. Map:是映射表的基础接口。
ArrayList 底层结构是数组,底层查询快,增删慢
LinkedList 底层结构是链表型的,增删快,查询慢
Voctor 底层结构是数组 线程安全的,增删慢,查询慢
List
ArrayList 线程不安全,查询速度快
l Vector 线程安全,但速度 慢,已被 ArrayList 替代
l LinkedList 链表结果,增删速度快
l TreeList 树型结构,保证增删复杂度都是O(log n),增删性能远高于 ArrayList和LinkedList,但是稍微占用内存
Set
Set:元素是无序(存入和取出的顺序不一定一致),元素不可以重复。
u HashSet:底层数据结构是哈希表, 是线程不安全的, 数据不同步。
HashSet 是如何保证元素唯一性的呢?
是通过元素的两个方法,hashCode 和 equals 来完成。 如果元素的 HashCode 值相同,才会判断 equals 是否为 true。 如果元素的 hashcode 值不同,不会调用 equals。 注意,对于判断元素是否存在,以及删除等操作,依赖的方法是元素的 hashcode 和 equals 方法
l TreeSet:底层数据结构是二叉树,存放有序:TreeSet 线程不安全可以对 Set 集合中的元素进行排序。通过 compareTo 或者 compare 方法来保证 元素的唯一性。
Map
Correction、Set、List 接口都属于单值的操作,而 Map 中的每个元素都使 用 key——>value 的形式存储在集合中。 Map 集合:该集合存储键值对, 是 key:value 一对一对往里存, 而且要保证 键的唯一性
Map 接口的常用子类
l HashMap:底层数据结构是哈希表,允许使用 null 值和 null 键,该集合 是数据不同步的,将 hashtable 替代,jdk1.2.效率高。
l TreeMap:底层数据结构是二叉树,线程不同步,可以用于给 map 集合中 的键进行排序
hashmap
结构:
1.7 数组+链表
1.8 数组+链表+红黑树
1.7 采用头插法
新的值会取代原有的值,原有的值就会顺推到链表中取,目的是为了提升查询的效率
1.8 采用尾插法
为了安全,防止环化
扩容:
1.7的扩容
将数组的长度变为原来的两倍,然后将原来的元素放到新数组上,即将旧数组先遍历数组,在遍历链表,将数据全部取出,然后通过hash算法,就能得到该数据在新数组的索引值
1.8的扩容
看hash和oldcap的&值是否为0,如果为0,则新旧数组不变,如果不是0,则新数组的索引应该是原索引+oldcap长度的索引
hash
将任意长度的输入,转变成固定长度的输出,该输出的值就是散列值,当前存储数据的结构就称为哈希表,所对应得关系得方法,称为散列函数
hash算法及hash冲突
算法:高低16位取异或的值^2的n次幂-1
高低16位取异或:
1.让整合的hash能够更多位参与到运算
2.采用异或,异或可以让0,1尽可能更平均
2 的n次幂-1 = 数组的长度-1
1.能够让数据能够更好的落在数组的奇数或者偶数位上===》更加均匀的排列
2 能够让数据都落在数组索引上
3 让hash值有意义,因为底层采用的是2进制,所以只有是1,才能保证hash值有作用,如果是0,不管hash值为多少,并过的值永远是0
HashMap发生hash冲突之后,他是如何解决的
如何解决 数学的角度上:链式地址法 hashMap的解决方案 : :线性探测法 hash冲突有什么后果:效率会变低,会形成链表,查询速度就会变慢 hash冲突能够解决吗? :不能,只能尽可能去避免
Hashtable
跟HashMap相比Hashtable是线程安全的,适合在多线程的情况下使用,但是效率可不太乐观。
Hashtable 是不允许键或值为 null 的,HashMap 的键值则都可以为 null。
Hashtable使用的是安全失败机制(fail-safe),这种机制会使你此次读到的数据不一定是最新的数据。
快速失败(fail—fast)是java集合中的一种机制, 在用迭代器遍历一个集合对象时,如果遍历过程中对集合对象的内容进行了修改(增加、删除、修改),则会抛出Concurrent Modification Exception。
如果你使用null值,就会使得其无法判断对应的key是不存在还是为空,因为你无法再调用一次contain(key)来对key是否存在进行判断,ConcurrentHashMap同理。
初始化容量不同:HashMap 的初始容量为:16,Hashtable 初始容量为:11,两者的负载因子默认都是:0.75。
扩容机制不同:当现有容量大于总容量 * 负载因子时,HashMap 扩容规则为当前容量翻倍,Hashtable 扩容规则为当前容量翻倍 + 1。
在源码中,他在对数据操作的时候都会上锁,所以效率比较低下。
reentrantlock
ReentrantLock是可重入的互斥锁
在ReentrantLock中,它对AbstractQueuedSynchronizer(aqs)的state状态值定义为线程获取该锁的重入次数,state状态值为0表示当前没有被任何线程持有,state状态值为1表示被其他线程持有,因为支持可重入,如果是持有锁的线程,再次获取同一把锁,直接成功,并且state状态值+1,线程释放锁state状态值-1,同理重入多次锁的线程,需要释放相应的次数。
AQS使用一个volatile修饰的私有变量来表示同步状态,当state=0表示释放了锁,当sta-te>0表示获得锁。
currentHashMap
1.7 采用的是分段式锁
数据结构:reentrantlock+segment+hashEntry
每个段就是一个segment,这个segment就是对应的锁,在操作过程中,如果没有去操作同一个节点,就不存在阻塞问题
1.8
采用的是volatile+cas+syn
线程安全的map
1 初始化:
在源码中有一个while循环,所有的线程都会进入到while循环中,然后进行cas,如果进入到cas的线程,会进行map的初始化,而其他没有进入到cas的线程,会进行线程的礼让,释放cpu的执行权,这就保证在多个线程的情况下,只有一个线程在进行初始化
2 put
根据key计算出hash值
判断是否需要初始化
即为当前key定位出的Node,如果表示为空,则表示当前位置可以写入数据,利用cas尝试写入。失败则自旋保证成功
如果当前位置的hash值==moced==-1,则需要进行扩容
如果都不满足,则利用syn锁写入数据
(之所以这个要用 syn,而不单纯采用cas,是因为这个地方除了 插入逻辑以外,你还要遍历的逻辑也是线程安全)
如果数量大于TREEIFY_THRESHOLD 则要转换为红黑树。
gets
根据计算出来的 hashcode 寻址,如果就在桶上那么直接返回值。
如果是红黑树那就按照树的方式获取值。
就不满足那就按照链表的方式遍历获取值。
volatile
可见性 原子性 有序性
如果一个变量被volatile关键字修饰,那么所有线程都是可见的。
volatile是一个变量修饰符,只能用来修饰变量。
底层原理
当对非volatile变量进行读写的时候,每个线程先从主内存拷贝变量到CPU缓存中,如果计算机有多个CPU,每个线程可能在不同的CPU上被处理,这意味着每个线程可以拷贝到不同的CPU cache中。
volatile变量不会被缓存在寄存器或者对其他处理器不可见的地方,保证了每次读写变量都从主内存中读,跳过CPU cache这一步。当一个线程修改了这个变量的值,新值对于其他线程是立即得知的。
可见性
对于volatile关键字修饰的变量:多线程下jvm会为每个线程分配一个独立的缓存来提高效率,当一个线程修改了该变量后,另外一个线程立即可以看到
禁止指令重排序
指令重排序是JVM为了优化指令、提高程序运行效率,在不影响单线程程序执行结果的前提下,尽可能地提高并行度。
指令重排序可能会带来的问题
如果一个操作不是原子的,就会给JVM留下重排的机会。
volatile关键字提供内存屏障的方式来防止指令被重排,编译器在生成字节码文件时,会在指令序列中插入内存屏障来禁止特定类型的处理器重排序。
CAS
内存值V、预期值A、要修改的新值B
CAS 是乐观锁的一种实现方式,是一种轻量级锁,JUC 中很多工具类的实现就是基于 CAS 的。
线程在读取数据时不进行加锁,在准备写回数据时,比较原值是否修改,若未被其他线程修改则写回,若已被修改,则重新执行读取流程。
ABA问题?
就是说来了一个线程把值改回了B,又来了一个线程把值又改回了A,对于这个时候判断的线程,就发现他的值还是A,所以他就不知道这个值到底有没有被人改过,其实很多场景如果只追求最后结果正确,这是没关系的。
但是实际过程中还是需要记录修改过程的,比如资金修改什么的,你每次修改的都应该有记录,方便回溯。 解决方法:
用版本号去保证就好了,就比如说,我在修改前去查询他原来的值的时候再带一个版本号,每次判断就连值和版本号一起判断,判断成功就给版本号加1。
时间戳也可以,查询的时候把时间戳一起查出来,对的上才修改并且更新值的时候一起修改更新时间,这样也能保证
面向过程与面向对象的区别
面向过程主要是针对功能,而面向对象主要是针对能够实现该功能的背后的实体
面向对象实质上就是面向实体,所以当我们使用面向对象进行编程时,一定要建立这样一个观念:万物皆对象!
都可以实现代码重用和模块化编程,但是面对对象的模块化更深,数据更封闭,也更安全!因为面向对象的封装性更强!面对对象的思维方式更加贴近于现实生活,更容易解决大型的复杂的业务逻辑从前期开发角度上来看,面对对象远比面向过程要复杂,但是从维护和扩展功能的角度上来看,面对对象远比面向过程要简单!
二 集合框架底层 (多线程、线程池)
线程的实现方式
1.继承 thread 2.实现 runanble 3.实现 callAble 3.1 可以抛出异常 3.2 可以接受返回值 3.3 阻塞 callable 需要和FutureTask 配合使用 4.使用线程池
线程的生命周期
新建状态:指的就是线程被new 出来 就绪状态:你去调用start方法,但是他还没有得到cpu的分配 运行状态: 已经跑起来了,已经得到cpu的分配权利 阻塞状态: 就是遇见了wait,sleep 之类的东西我,就会被阻塞 ,sleep会自动变为就绪状态(而且sleep 不会释放锁),而wait(会释放锁资源)必要要等待notify唤醒 死亡状态:运行结束后,就进入到死亡状态
线程池的7大参数
长足线程数 corePoolSize
最大线程数 maximumPoolSize
阻塞队列 workQueue
拒绝策略 defaultHandler
默认工厂 Executors.defaultThreadFactory()
多余的空线程的存活时间 keepAliveTime
时间单位 unit
线程池在创建之后,并不会立刻创建线程,而是当任务过来后,我们线程池会去判断当前线程池中的线程是否小于常驻线程,如果小于则创建线程,直到线程池中的线程不再小于常驻线程,任务交给线程去处理,如果常驻线程处理不过来之后,把这个任务交给阻塞队列,如果阻塞队列都满了之后,此时会开辟最大线程,如果最大线程和阻塞队列都满了的话,此时就会拒绝策略。
拒绝策略
1.丢弃 2.抛出异常 3.交给方法调用者 4.丢弃队列中等待时间最久的那一个
工作中使用哪种线程池
在工作中咱们不会使用executors创建线程池,因为如果使用可伸缩的线程池,整体线程数无法控制,也不使用单个线程线程池,也不能使用 固定长度大小线程池 因为他的底层是一个 linkedBlockingQueue,而这个queue,他会开辟一个Integer.maxValue大小的阻塞队列,这样的太占用内容 不使用executors 线程池他是 阿里规范上边明确指出的 我们怎么使用: 1.使用第三方线程池 2.自定义线程池
工作中如何配置线程池
注意:这个地方有一个理论依据,但是你不要说是你配的 cpu型:进行了大量计算这种操作 cpu+1 io 型:除了cpu就是io 2 * cpu+1 具体地配置应该是由 压力测试工具 ,比如jmeter 这样的压测工具,在一定强度下压测出来的最终结果
这个是由 老大来配置的,只是去了解了一下
CountDownLatch
public void createMappingAndIndex() {
//创建索引
elasticsearchTemplate.createIndex(SkuInfo.class);
//创建映射
elasticsearchTemplate.putMapping(SkuInfo.class);
}
ExecutorService threadPool = Executors.newFixedThreadPool(5); //不建议使用
//导入全部sku集合进入到索引库
@Override
public void importAll() {
// pageHelper.startPage(1,1000) 第一页 , 1000条
// pageHelper.startPage(2,1000) 第二页 1001~ 2000条
// 首先确定他有多少页 总记录数 10001 / 每页显示条数 1000== 0? 10+1 countDolnlatch
int pageSize = 1000;
int allCount = skuFeign.selectCount();
int allPage = allCount % pageSize == 0 ? allCount / pageSize :allCount / pageSize + 1;
CountDownLatch cdl = new CountDownLatch(allPage);
for (int page= 1; page <= allPage; page++) {
threadPool.submit(new TaskThread(page,pageSize,cdl));
}
// 当所有分线程走完之后,他就应该不再阻塞
// 什么时候 countdown 完成一个线程 countdown 一次
// 变量是多少 一共有多少个线程要执行
try {
cdl.await();
} catch (IsnterruptedException e) {
e.printStackTrace();
}
三 锁
为什么要使用分布式锁
在采用分布式架构后,程序会有多个实例,则就会有多个锁对象,但不管是lock或者syn都不能保证锁住的是同一个对象
redis的分布式锁
利用redis的setnx和setex
具体实现: 设定一个字符串,将该字符串写入到数据库中,如果redis中没有该条数据记录,则写入成功,返回1.当client拿到的返回值为1,则表明拿到了锁,那就会继续执行自己的业务逻辑,使用完后会将redis中的记录删除,如果写入失败,则返回0,当client拿到的值为0,则表明获取锁失败,那么将会进行自旋抢锁。
缺陷:不能防止锁 会删除其他的锁 无法进行续约逻辑 不能保证公平性
解决办法:使用redission
redission
是redis提供的对于分布式支持的lock锁,底层原理是看门狗原理
redission有两种模式
1 如果你没有传入参数,则lock以-1接受(底层源码执行的),如果传入了参数,则使用该参数
2当获得当前锁的线程,则会去判断leasTime是否!=-1,
3 如果不等于-1,则表明传入了过期时间
3.1 不会续期
3.2 锁以传入的过期时间作为锁失效的时间
3.3 到期后直接返回
4 如果等于-1,则表明没有传入过期时间,则会使用系统默认的过期时间,即看门狗原理
5 底层以一个taskTimer进行续约逻辑,即看门狗的默认时间为30s,那么每过1/3看门狗时间就进行预约,即每过10秒执行续约逻辑
zk的分布式锁
核心理论:当客户端获取锁,则创建临时节点,使用完锁,删除节点
具体实现:
1. 客户端获取锁时,在lock节点下创建临时顺序节点
2. 然后会获取到lock下的所有子节点,当客户端获取到节点后,会去验证该节点是否为最小节点
3. 如果发现是最小的节点,那么说明自己获取到锁,然后执行自己的业务逻辑,使用完锁后会删除该节点
4. 如果发现不是最小节点,说明未获取到锁,那么会去找到那个最小的节点,并给该节点绑定一个监听事件器,监听删除事件
5. 如果发现该节点被删除后,客户端的watcher会收到通知,然后客户端会再次判断自己的节点是否为最小节点
6. 如果为最小节点,则表明获取到锁,执行自己的业务逻辑,使用完后删除即可
7. 如果依旧不是最小节点,那么表明仍为获取到锁,继续执行之前的获取那个最小节点的步骤
syn
synchronized
jvm层面的锁
sny其实锁住的是一个对象,对象布局为
1 实例数据
2填充数据
3 object-header
里面包含了: mark—word :锁信息 hashcode gc标志位 gc的年龄
kclass—pointer :即一个指针压缩,它最终会指向处于方法类的模板信息
锁的升级
为什么会产生锁升级?
在jdk1.5以前,只要经过了syn,那么不管处于什么状态,都会进行用户态和内核态的切换,导致性能的极限下降
偏向锁
在锁对象的对象头中会记录当前获取到该锁的线程id,该线程下次如果又来获取该锁就可以直接获取
轻量级锁
由偏向锁升级而来,当一个线程获取到锁以后,此时这把锁是偏向锁,如果有第二个线程来竞争锁,偏向锁就会升级为轻量级锁,之所以叫轻量级锁,是为了和重量级锁区分开来,轻量级锁底层是通过自旋来实现的,并不会阻塞线程
自旋锁
自旋锁就是线程在获取锁 的过程中,不会去阻塞线程,也就无所谓唤醒线程,阻塞和唤醒这两个步骤都是需要操作系统去进行的,比较消耗时间,自旋锁是线程通过cas获取预期的一个标记,如果没有获取到,则继续循环获取,如果获取到了则表示获取到锁,这个过程线程一直运行中,相对而言没有使用太多的操作系统资源,比较轻量。
重量级锁
如果自旋次数过多仍然没有获取到锁,则会升级为重量级锁,重量级锁会导致线程阻塞
lock
lock是java层面的锁
他分为公平锁和非公平锁
Lock锁可重入、等待时可中断、可判断、可公平可非公平
原理
如果是一把非公平锁:先进行一次cas ,如果cas 失败,就会去执行自旋1.获得state 变量,判断state 是否是个0 ,如果不是0 表示有人正在持有锁,如果是0 ,则判断队列是否有元素,如果队列没有元素,则cas 抢锁,如果当前锁 被人持有,他就判断是不是自己持有这把锁,如果是,则表明现在是可重入状态-> 将state 加1 ,如果需要排队,则 lock 锁,将线程放置到 双向链表中进行排队,他会让我们线程中的元素 进行LockSupport.park(),阻塞,如果我们持有锁的线程此时执行完之后,他会将队列中的第一个元素唤醒 -> 他就和其他线程进行抢锁,如果抢成功,被唤醒的线程就会执行,抢锁失败,则继续阻塞
synchronized和lock的区别(8点)
1.来源及用法: lock是一个接口,是java写的控制锁的代码,而synchronized是java的一个内置关键字,synchronized是托管给JVM执行的; synchronized:在需要同步的对象中加入此控制,synchronized可以加在方法上,也可以加在特定代码块中,括号中表示需要锁的对象。 lock:一般使用ReentrantLock类做为锁。在加锁和解锁处需要通过lock()和unlock()显示指出。所以一般会在finally块中写unlock()以防死锁。
2.异常是否释放锁: synchronized在发生异常时候会自动释放占有的锁,因此不会出现死锁;而lock发生异常时候,不会主动释放占有的锁,必须手动unlock来释放锁,可能引起死锁的发生。(所以最好将同步代码块用try catch包起来,finally中写入unlock,避免死锁的发生。)
3.是否响应中断 lock等待锁过程中可以用interrupt来中断等待,而synchronized只能等待锁的释放,不能响应中断;
4.是否阻塞 Lock可以尝试获取锁,synchronized获取不到锁只能一直阻塞 synchronized关键字的两个线程1和线程2,如