面试技巧基础

熟练掌握 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,如

  • 2
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值