1.G1为什么高吞吐量
G1多线程并行并发
2.B+树解决什么问题
解决查询遍历太深的问题
3.硬连接和软连接
硬连接:新建的文件是已经存在的文件的一个别名,当原文件删除时,新建的文件仍然可以使用. 软连接:也称为 符号链接,新建的文件以“路径”的形式来表示另一个文件,和Windows的快捷方式十分相似,新建的软链接可以指向不存在的文件.
4.Concurrent HashMap了解吗,1.7和1.8的区别?
jdk1.7:数组(Segment) + 数组(HashEntry) + 链表(HashEntry节点)
底层一个Segments数组,存储一个Segments对象,一个Segments中储存一个Entry数组,存储的每个Entry对象又是一个链表头结点。
jdk1.8:数组(Node) + 链表 + 红黑树
jdk1.7
ConcurrentHashMap(分段锁) 对整个桶数组进行了分割分段(Segment),每一把锁只锁容器其中一部分数据,多线程访问容器里不同数据段的数据,就不会存在锁竞争,提高并发访问率。jdk1.8
使用的是优化的synchronized 关键字 和 cas操作了维护并发。jdk1.7
ConcurrentHashMap 使用的分段锁,如果一个线程占用一段,别的线程可以操作别的部分,
jdk1.8简化结构,put和get不用二次哈希,一把锁只锁住一个链表或者一棵树,并发效率更加提升。
5.Stringbuffer和StringBuilder的区别?
StringBuffer是线程安全,可以不需要额外的同步用于多线程中;
StringBuilder是非同步,运行于多线程中就需要使用着单独同步处理,
但是速度就比StringBuffer快多了;
StringBuilder:适用于单线程下在字符缓冲区进行大量操作的情况
StringBuffer:适用多线程下在字符缓冲区进行大量操作的情况
6.字符串操作,什么时候用+,什么时候用append?
常量多的时候用+,变量多的时候用append
"a"和"b"这些字符串是常量,"a"+"b"在编译期间会被优化为"ab",只需要一次append,
Sting s="c"; s是变量
在 String str = "a" + "b" + s; 时,先会优化成 "ab" 再与s 进行处理,这时 StringBuilder 仅调用了两次 append 方法。
如果是 String str = "a" + s + "b"; 这种就没办法优化了,StringBuilder 得调用三次 append 方法。
如果只有一句 String str = "a" + s; 这样子的,其效率与
String str = new StringBuilder().append("a").append(s).toString()
;是一样的。一般所说的 String 采用连接运算符(+)效率低下主要产生在以下的情况中:
public class Test { public static void main(String args[]) { String s = null; for(int i = 0; i < 100; i++) { s += "a"; } } }
每做一次 + 就产生个 StringBuilder 对象,然后 append 后就扔掉。下次循环再到达时重新产生个 StringBuilder 对象,然后 append 字符串,如此循环直至结束。
如果我们直接采用 StringBuilder 对象进行 append 的话,我们可以节省 N - 1 次创建和销毁对象的时间。
7.说一下Object类的几个方法?
1.Object() 构造方法
2.registerNatives() 通过使用registerNatives,可以命名任何你想要你的C函数。
3.clone()
用来另存一个当前存在的对象,只有实现了Cloneable接口才可以调用该方法,否则抛出CloneNotSupportedException异常。
4.getClass()
该方法返回的是此Object对象的类对象/运行时类对象Class。效果与Object.class相同。
5.equals()
用来比较两个对象的内容是否相等。默认情况下(继承自Object类),equals和==是一样的,除非被覆写(override)了。
6.hashCode()
该方法用来返回其所在对象的物理地址(哈希码值),常会和equals方法同时重写,确保相等的两个对象拥有相等的hashCode。
7.toString() 返回该对象的字符串表示
8.wait() 导致当前的线程等待,直到其他线程调用此对象的 notify() 方法或 notifyAll() 方法。
9.wait(long timeout) 导致当前的线程等待,.....,或者超过指定的时间量。
10.wait(long timeout, int nanos)
导致当前的线程等待,......,......,或者其他某个线程中断当前线程
11.notify()
唤醒在此对象监视器上等待的单个线程
12. notifyAll()
唤醒在此对象监视器上等待的所有线程
13.finalize()
当垃圾回收器确定不存在对该对象的更多引用时,由对象的垃圾回收器调用此方法。
8.InnoDB中 B+树的优化?
分裂操作的优化,在记录6插入完毕,在插入7时,不移动原有页面的任何记录,只是将新插入的记录7写到新页面之中
9.列举几个常用的linux命令以及说明?
LS 显示目录内容
- -l:使用较长的格式列出信息
- -r:按照文件名的逆序打印输出
cat 显示文件内容
- -n:显示文件内容的行号。
- -b:类似-n,但是不对空白行进行编号。
mv 更改文件或者目录的名字
- -f:强制模式,覆盖文件不提示。
- -i:交互模式,当要覆盖文件的时候给提示。
rm 删除文件
- -f:强制模式,不给提示。
- -r,-R:删除目录,recursive
mkdir 创建目录
- -p:创建目录和它的父目录。
- -m:指定模式,类似chmod。
grep 在文件中搜索特定的字符串
- -i:不区分大小写
- -n:显示序号
- -v:显示不匹配的内容
chmod 改变文件存取权限
- +r:增加读权限
- -W:删除写权限
- -x:增加执行权限
10.为什么说B+树比B树更适合数据库索引?
1.B+树的磁盘读写代价更低
B+树的内部节点并没有指向关键字具体信息的指针,因此其内部节点相对B树更小,
盘块所能容纳的关键字数量也越多,一次性读入内存的需要查找的关键字也就越多,
相对IO读写次数就降低了。
2.B+树的查询效率更加稳定
由于非终结点并不是最终指向文件内容的结点,而只是叶子结点中关键字的索引。所以任何关键字的查找必须走一条从根结点到叶子结点的路。所有关键字查询的路径长度相同,导致每一个数据的查询效率相当。
11.WAITING和BLOCKED的区别?
WAITING:一个线程进入了锁,但是需要等待其他线程执行某些操作。时间不确定
当wait,join,park方法调用时,进入waiting状态。前提是这个线程已经拥有锁了。BLOCKED:一个线程因为等待临界区的锁被阻塞产生的状态
Lock 或者synchronize 关键字产生的状态
12.符号引用与直接引用的区别?
符号引用以一组符号来描述所引用的目标,符号可以是任何形式的字面量,只要使用时能够无歧义的定位到目标即可,使用符号引用时,被引用的目标不一定已经加载到内存中。
直接引用可以是直接指向目标的指针,相对偏移量,一个能间接定位到目标的句柄,使用直接引用时,引用的目标必定已经存在于虚拟机的内存中了。
13.==和equals的区别?
对于基本类型和引用类型 == 的作用效果是不同的,如下所示:
基本类型:比较的是值是否相同;
引用类型:比较的是引用是否相同;equals 本质上就是 ==,只不过 String 和 Integer 等重写了 equals 方法,把它变成了值比较。
总结:
==对于基本类型来说就是值比较,对于引用类型来说比较的就是引用;
而equals默认情况下是引用比较,只是很多类重写了equals方法,
比如String,Integer等把它变成了值比较,所以一般情况下比较的是指是否相等。
14.为什么重写equals方法,一定要重写HashCode方法?
判断的时候先根据hashcode进行的判断,相同的情况下再根据equals()方法进行判断。如果只重写了equals方法,而不重写hashcode的方法,会造成hashcode的值不同,而equals()方法判断出来的结果为true。
在Java中的一些容器中,不允许有两个完全相同的对象,插入的时候,如果判断相同则会进行覆盖。这时候如果只重写了equals()的方法,而不重写hashcode的方法,Object中hashcode是根据对象的存储地址转换而形成的一个哈希值。这时候就有可能因为没有重写hashcode方法,造成相同的对象散列到不同的位置而造成对象的不能覆盖的问题。
15.OSI七层模型?
16. Redis除了做缓存,还能做什么?
- 队列
- 签到统计
- 原子扣减库存
- 分布式锁
17.SQL查询速度慢的原因?
1、没有索引或者没有用到索引(这是查询慢最常见的问题,是程序设计的缺陷)
2、I/O吞吐量小,形成了瓶颈效应。
3、没有创建计算列导致查询不优化。
4、内存不足
5、网络速度慢
6、查询出的数据量过大(可以采用多次查询,其他的方法降低数据量)
7、锁或者死锁(这也是查询慢最常见的问题,是程序设计的缺陷)
8、sp_lock,sp_who,活动的用户查看,原因是读写竞争资源。
9、返回了不必要的行和列
10、查询语句不好,没有优化
18.如何避免消息队列的重复消费?
1.利用数据库唯一约束实现幂等
可以通过给消息的某一些属性设置唯一约束,比如增加唯一uuid,添加的时候查询是否存对应的uuid
2.设置前置条件
可以通过设置版本号version,没修改一次则版本号+1,在更新时则通过判断两个数据的版本号是否一致。
3. 通过全局ID实现
通过设置全局Id去实现。实现的思路是,在发送消息时,给每条消息指定一个全局唯一的 ID(可以通过雪花算法去实现),消费时,先根据这个 ID 检查这条消息是否有被消费过,如果没有消费过,才更新数据。
19.Redis的五种数据结构以及三种高级数据结构?
string、list、hash、set、sorted set
HyperLogLog、Geo、BloomFilter
20.RPC?
远程过程调用,它是一种通过网络从远程计算机程序上请求服务,而不需要了解底层网络技术的协议。RPC协议假定某些传输协议的存在,如TCP或UDP,为通信程序之间携带信息数据。在OSI网络通信模型中,RPC跨越了传输层和应用层。RPC使得开发包括网络分布式多程序在内的应用程序更加容易。
21.Restful API设计规范?四种方法怎么用?
1、每一个URI代表1种资源;
2、客户端使用GET、POST、PUT、DELETE4个表示操作方式的动词对服务端资源进行操作:GET用来获取资源,POST用来新建资源(也可以用于更新资源),PUT用来更新资源,DELETE用来删除资源;
3、通过操作资源的表现形式来操作资源;
4、资源的表现形式是XML或者HTML;
5、客户端与服务端之间的交互在请求之间是无状态的,从客户端到服务端的每个请求都必须包含理解请求所必需的信息。
通过接口查询相应的数据时一般是采用GET请求
post方法一般用于创建订单或者创建的某个动作
put请求专注于update操作
delete执行相应的删除操作
22.乐观锁有什么缺点?
1.乐观锁只能保证一个共享变量的原子操作。
如果多一个或几个变量,乐观锁将变得力不从心,但互斥锁能轻易解决,不管对象数量多少及对象颗粒度大小。
2.长时间自旋可能导致开销大。
假如CAS长时间不成功而一直自旋,会给CPU带来很大的开销。
3.ABA问题。
CAS的核心思想是通过比对内存值与预期值是否一样而判断内存值是否被改过,但这个判断逻辑不严谨,假如内存值原来是A,后来被一条线程改为B,最后又被改成了A,则CAS认为此内存值并没有发生改变,但实际上是有被其他线程改过的,这种情况对依赖过程值的情景的运算结果影响很大。解决的思路是引入版本号,每次变量更新都把版本号加一。
23.如何优化SQL查询?
- 对查询进行优化,应尽量避免全表扫描,首先应考虑在 where 及 order by 涉及的列上建立索引
- 用索引可以提高查询
- SELECT子句中避免使用*号,尽量全部大写SQL
- 应尽量避免在 where 子句中对字段进行 is null 值判断,否则将导致引擎放弃使用索引而进行全表扫描,使用 IS NOT NULL
- where 子句中使用 or 来连接条件,也会导致引擎放弃使用索引而进行全表扫描
- in 和 not in 也要慎用,否则会导致全表扫描
24.简述PageHelper分页原理?
PageHelper首先将前端传递的参数保存到page这个对象中,
接着将page的副本存放入ThreadLocal中,这样可以保证分页的时候,参数互不影响,
接着利用了mybatis提供的拦截器,取得ThreadLocal的值,重新拼装分页SQL,完成分页。
25.简述几条JDK1.8特性
1.Switch支持String类型
2.Catch多个异常
3.泛型实例创建可以通过类型推断简化
4.HashMap性能优化:
- jdk1.8 当每个链表长度 >8 ,并且数组元素个数 ≥64时,会调整成红黑树,目的是提高效率
- jdk1.8 当链表长度 <6 时 调整成链表
- jdk1.8 以前,链表时头插入,之后为尾插入
- 原因:头插法在并发情况下会出现链表成环的问题,当然我们知道它不是线程安全的类,不会用它,并发编程时用的是Concurrent包下的ConcurrentHashMap
5.永久代移除
6.Lambda表达式
26.线程池有哪几部分组成?线程池有什么优点?
1、线程池管理器(ThreadPool):用于创建并管理线程池,包括 创建线程池,
销毁线程池,添加新任务;
2、工作线程(PoolWorker):线程池中线程,在没有任务时处于等待状态,可以循环的执行任务;
3、任务接口(Task):每个任务必须实现的接口,以供工作线程调度任务的执行,它主要规定了任务的入口,任务执行完后的收尾工作,任务的执行状态等;
4、任务队列(taskQueue):用于存放没有处理的任务。提供一种缓冲机制。
27.ThreadPoolExecutor线程池的构造参数?
corePoolSize //核心线程数量
maximumPoolSize //最大线程数量
keepAliveTime //线程保持时间,N个时间单位 unit 时间单位(比如秒,分)
workQueue //阻塞队列
threadFactory //线程工厂
handler //线程池拒绝策略
28.什么是内存泄漏?什么是内存溢出?如何解决?
内存泄漏:程序在申请内存后,由于某种原因无法释放已申请的内存空间,
导致这块内存无法被利用,造成内存的浪费。
内存溢出:程序在申请内存时,空间不够,出现OOM
- 栈内存溢出:栈深度过大导致,可以写一个死递归程序触发。
- 堆内存溢出:可以不断往堆中新增StringBuffer对象,堆满溢出。
- 持久代溢出:用String.intern()触发常量池溢出
如果是内存溢出,可以调大-xms,-xmx参数。
如果是内存泄漏,则看对象如何被GCRoot引用。
29.让你读取一个大文件10G,然后排序写入另外一个文件中,但你只有1G内存,如何实现?
分治
30.Tomcat是如何实现多线程的?
31.死锁知道伐?说一个死锁的场景?死锁如何避免?
进程死锁:指两个或两个以上的进程在执行过程中,由于竞争资源或者由于彼此通信而造成的一种阻塞的现象
线程死锁:当两个或两个以上的线程在执行过程中,因为争夺资源而造成的一种相互等待的状态
区别:只不过是死锁的基本单元不同,一个是进程之间,一个是线程之间,仅此而已
通俗点说就是互相持有对方所需要的的锁,而发生的阻塞现象,我们称为死锁
死锁场景
1.
public Class DeadLock implements Runnable{ public int flag=1; //静态对象是类的所有对象共享的 public static o1=new Object(); public static o2=new Object(); @Override public void run(){ System.out.println("flag:"+flag); if(flag==1)//先锁1再锁2 { synchronized(o1){ try { Thread.sleep(500); } catch (Exception e) { e.printStackTrace(); } synchronized (o2) { System.out.println("1"); } } } if(flag==0)//先锁2再锁1 { Synchronized(o2){ try{ Thread.sleep(500); }catch(Exception e){ e.printStackTrace(); } synchronized(o1){ System.out.println("2"); } } } } public static void main(String []args){ DeadLock d1=new DeadLock(); DeadLock d2=new DeadLock(); d1.flag=1; d2.flag=0; //td1,td2都处于可执行状态,但JVM线程调度先执行哪个线程是不确定的。 //td2的run()可能在td1的run()之前运行 new Thread(d1.start()); new Thread(d2.start()); } }
1、当DeadLock 类的对象flag=1时(td1),先锁定o1,睡眠500毫秒
2、而td1在睡眠的时候另一个flag==0的对象(td2)线程启动,先锁定o2,睡眠500毫秒
3、td1睡眠结束后需要锁定o2才能继续执行,而此时o2已被td2锁定;
4、td2睡眠结束后需要锁定o1才能继续执行,而此时o1已被td1锁定;
5、td1、td2相互等待,都需要得到对方锁定的资源才能继续执行,从而死锁。
2.动态锁顺序死锁:
// 资金转账到账号 public static void transferMoney(Account fromAccount, Account toAccount, DollarAmount amount) throws InsufficientFundsException { // 锁定汇款者的账户 synchronized (fromAccount) { // 锁定到账者的账户 synchronized (toAccount) { // 判断账户的余额不能为负数 if (fromAccount.getBalance().compareTo(amount) < 0) { throw new InsufficientFundsException(); } else { // 汇款者的账户减钱 fromAccount.debit(amount); // 到账者的账户增钱 toAccount.credit(amount); } } } }
上面的代码看起来都是按照相同的顺序来获得锁的,按道理来说是没有问题,但是上述代码中上锁的顺序取决于传递给transferMoney()的参数顺序,而这些参数顺序又取决于外部的输入
- 如果两个线程(A和B)同时调用transferMoney()
- 其中一个线程(A),从X向Y转账:transferMoney(myAccount,yourAccount,10);
- 另一个线程(B),从Y向X转账 :transferMoney(yourAccount,myAccount,20);
- 此时 A线程 可能获得 myAccount 的锁并等待 yourAccount的锁,然而 B线程 此时已经持有 yourAccount 的锁,并且正在等待 myAccount 的锁,这种情况下就会发生死锁。
如何避免死锁?
- 尽量使用 tryLock(long timeout, TimeUnit unit)的方法(ReentrantLock、ReentrantReadWriteLock),设置超时时间,超时可以退出防止死锁。
- 尽量使用 Java. util. concurrent 并发类代替自己手写锁。
- 尽量降低锁的使用粒度,尽量不要几个功能用同一把锁。
- 尽量减少同步的代码块。
32.定位搜索,显示附近有多少商家的那种,和地图有关怎么实现的?
33.商家的点位信息怎么保存?如果数据量大了怎么优化?
34.SpringMVC运行流程?
- SpringMVC先将请求发送给DispacherServlet;
- DispatcherServlet查询一个多个HandlerMapping,找到处理请求的Controller;
- Controller进行业务逻辑处理后,会返回一个ModelAndView;
- DispatcherServlet查询一个或ViewResolver视图解析器,找到ModelAndView对象指定的视图对象;
- 视图对象负责渲染返回给前端;
35.说一下HashMap,HashTable,Arraylist,LinkedIist?Tree Map和HashMap的区别?
Collection
List
Arraylist: Object数组
Vector: Object数组
LinkedList: 双向循环链表
Set
- HashSet(无序,唯一):基于 HashMap 实现的,底层采用 HashMap 来保存元素
- LinkedHashSet: LinkedHashSet 继承与 HashSet,并且其内部是通过 LinkedHashMap 来实现的。有点类似于我们之前说的LinkedHashMap 其内部是基于 Hashmap 实现一样,不过还是有一点点区别的。
- TreeSet(有序,唯一): 红黑树(自平衡的排序二叉树。)
Map
- HashMap: JDK1.8之前HashMap由数组+链表组成的,数组是HashMap的主体,链表则是主要为了解决哈希冲突而存在的(“拉链法”解决冲突).JDK1.8以后在解决哈希冲突时有了较大的变化,当链表长度大于阈值(默认为8)时,将链表转化为红黑树,以减少搜索时间
- LinkedHashMap:LinkedHashMap 继承自 HashMap,所以它的底层仍然是基于拉链式散列结构即由数组和链表或红黑树组成。另外,LinkedHashMap 在上面结构的基础上,增加了一条双向链表,使得上面的结构可以保持键值对的插入顺序。同时通过对链表进行相应的操作,实现了访问顺序相关逻辑。
- HashTable: 数组+链表组成的,数组是 HashMap 的主体,链表则是主要为了解决哈希冲突而存在的
- TreeMap: 红黑树(自平衡的排序二叉树)
什么是TreeMap
- TreeMap 是一个有序的key-value集合,它是通过红黑树实现的。
- TreeMap基于红黑树(Red-Black tree)实现。该映射根据其键的自然顺序进行排序,或者根据创建映射时提供的 Comparator 进行排序,具体取决于使用的构造方法。
- TreeMap是线程非同步的。
36.集合存放的是对象还是对象的引用?
集合类存放的都是对象的引用,而不是对象的本身
37.说一下Java集合的快速失败机制"fail-fast"?
是java集合的一种错误检测机制,当多个线程对集合进行结构上的改变的操作时,有可能会产生 fail-fast 机制。
例如:假设存在两个线程(线程1、线程2),线程1通过Iterator在遍历集合A中的元素,在某个时候线程2修改了集合A的结构(是结构上面的修改,而不是简单的修改集合元素的内容),那么这个时候程序就会抛出 ConcurrentModificationException 异常,从而产生fail-fast机制。
原因:迭代器在遍历时直接访问集合中的内容,并且在遍历过程中使用一个 modCount 变量。集合在被遍历期间如果内容发生变化,就会改变modCount的值。每当迭代器使用hashNext()/next()遍历下一个元素之前,都会检测modCount变量是否为expectedmodCount值,是的话就返回遍历;否则抛出异常,终止遍历。
解决办法:
在遍历过程中,所有涉及到改变modCount值得地方全部加上synchronized。
使用CopyOnWriteArrayList来替换ArrayList
38.list集合什么时候使用迭代器比较快?什么时候使用for循环比较快?
ArrayList用for循环遍历比iterator迭代器遍历快
LinkedList用iterator迭代器遍历比for循环遍历快
39.RandomAccess接口
Java Collections 框架中提供了一个 RandomAccess 接口,用来标记 List 实现是否支持 Random Access。
如果一个数据集合实现了该接口,就意味着它支持 Random Access,按位置读取元素的平均时间复杂度为 O(1),如ArrayList。
如果没有实现该接口,表示不支持 Random Access,如LinkedList。
推荐的做法就是,支持 Random Access 的列表可用 for 循环遍历,否则建议用 Iterator 或 foreach 遍历。
总结:实现RandomAccess接口的的List可以通过简单的for循环来访问数据比使用iterator访问来的高效快速。
40.HashMap 与 HashTable 有什么区别?
- 线程安全: HashMap 是非线程安全的,HashTable 是线程安全的;HashTable 内部的方法基本都经过
synchronized
修饰。(如果你要保证线程安全的话就使用 ConcurrentHashMap );- 效率: 因为线程安全的问题,HashMap 要比 HashTable 效率高一点。另外,HashTable 基本被淘汰,不要在代码中使用它;(如果你要保证线程安全的话就使用 ConcurrentHashMap );
- 对Null key 和Null value的支持: HashMap 中,null 可以作为键,这样的键只有一个,可以有一个或多个键所对应的值为 null。但是在 HashTable 中 put 进的键值只要有一个 null,直接抛NullPointerException。
- 初始容量大小和每次扩充容量大小的不同 :
- 创建时如果不指定容量初始值,Hashtable 默认的初始大小为11,之后每次扩充,容量变为原来的2n+1。HashMap 默认的初始化大小为16。之后每次扩充,容量变为原来的2倍。
- 创建时如果给定了容量初始值,那么 Hashtable 会直接使用你给定的大小,而 HashMap 会将其扩充为2的幂次方大小。也就是说 HashMap 总是使用2的幂作为哈希表的大小,后面会介绍到为什么是2的幂次方。
- 底层数据结构: JDK1.8 以后的 HashMap 在解决哈希冲突时有了较大的变化,当链表长度大于阈值(默认为8)时,将链表转化为红黑树,以减少搜索时间。Hashtable 没有这样的机制。
推荐使用:在 Hashtable 的类注释可以看到,Hashtable 是保留类不建议使用,推荐在单线程环境下使用 HashMap 替代,如果需要多线程使用则用 ConcurrentHashMap 替代。
41.ConcurrentHashMap 和 Hashtable 的区别?
ConcurrentHashMap 和 Hashtable 的区别主要体现在实现线程安全的方式上不同。
- 底层数据结构: JDK1.7的 ConcurrentHashMap 底层采用 分段的数组+链表 实现,JDK1.8 采用的数据结构跟HashMap1.8的结构一样,数组+链表/红黑二叉树。Hashtable 和 JDK1.8 之前的 HashMap 的底层数据结构类似都是采用 数组+链表 的形式,数组是 HashMap 的主体,链表则是主要为了解决哈希冲突而存在的;
- 实现线程安全的方式:
- 在JDK1.7的时候,ConcurrentHashMap(分段锁) 对整个桶数组进行了分割分段(Segment),每一把锁只锁容器其中一部分数据,多线程访问容器里不同数据段的数据,就不会存在锁竞争,提高并发访问率。(默认分配16个Segment,比Hashtable效率提高16倍。) 到了 JDK1.8 的时候已经摒弃了Segment的概念,而是直接用 Node 数组+链表+红黑树的数据结构来实现,并发控制使用 synchronized 和 CAS 来操作。(JDK1.6以后 对 synchronized锁做了很多优化) 整个看起来就像是优化过且线程安全的 HashMap,虽然在JDK1.8中还能看到 Segment 的数据结构,但是已经简化了属性,只是为了兼容旧版本;
- ② Hashtable(同一把锁) :使用 synchronized 来保证线程安全,效率非常低下。当一个线程访问同步方法时,其他线程也访问同步方法,可能会进入阻塞或轮询状态,如使用 put 添加元素,另一个线程不能使用 put 添加元素,也不能使用 get,竞争会越来越激烈效率越低。
两者的对比图:
1、HashTable:
2、 JDK1.7的ConcurrentHashMap:
3、JDK1.8的ConcurrentHashMap(TreeBin: 红黑二叉树节点 Node: 链表节点):
- 答:ConcurrentHashMap 结合了 HashMap 和 HashTable 二者的优势。HashMap 没有考虑同步,HashTable 考虑了同步的问题使用了synchronized 关键字,所以 HashTable 在每次同步执行时都要锁住整个结构。 ConcurrentHashMap 锁的方式是稍微细粒度的。
42.ARP协议?为什么有了ip地址还要mac地址?
ARP
ARP协议是“Address Resolution Protocol”(地址解析协议)的缩写
其作用是在以太网环境中,数据的传输所依懒的是MAC地址而非IP地址,而将已知IP地址转换为MAC地址的工作是由ARP协议来完成的。
在局域网中,网络中实际传输的是“帧”,帧里面是有目标主机的MAC地址的。在以太网中,一个主机和另一个主机进行直接通信,必须要知道目标主机的MAC地址。
静态映射:手动创建一张ARP表,把逻辑(IP)地址和物理地址关联起来,
需要定期维护更新,因为物理地址可能会变化,比如更换适配器,移动电脑;
动态映射:每次只要机器知道另一台机器的逻辑(IP)地址,
就可以使用协议找出相对应的物理地址。
43.tcp怎么保证可靠传输的?
- 校验和:发送的数据包的二进制相加然后取反,目的是检测数据在传输过程中的任何变化。如果收到段的检验和有差错,TCP将丢弃这个报文段和不确认收到此报文段。
- 确认应答+序列号(累计确认+seq):接收方收到报文就会确认(累积确认:对所有按序接收的数据的确认),TCP给发送的每一个包进行编号,接收方对数据包进行排序,把有序数据传送给应用层。
- 超时重传:当TCP发出一个段后,它启动一个定时器,等待目的端确认收到这个报文段。如果不能及时收到一个确认,将重发这个报文段。
- 流量控制:TCP连接的每一方都有固定大小的缓冲空间,TCP的接收端只允许发送端发送接收端缓冲区能接纳的数据。当接收方来不及处理发送方的数据,能提示发送方降低发送的速率,防止包丢失。TCP使用的流量控制协议是可变大小的滑动窗口协议。
- 拥塞控制:当网络拥塞时,减少数据的发送。发送方有拥塞窗口,发送数据前比对接收方发过来的即使窗口,取小慢启动、拥塞避免、拥塞发送、快速恢复
44.进程和线程的区别?
- 调度:线程作为调度和分配的基本单位,进程作为拥有资源的基本单位
- 并发性:不仅进程之间可以并发执行,同一个进程的多个线程之间也可并发执行
- 拥有资源:进程是拥有资源的一个独立单位,线程不拥有系统资源,但可以访问隶属于进程的资源.
- 系统开销:在创建或撤消进程时,由于系统都要为之分配和回收资源,导致系统的开销明显大于创建或撤消线程时的开销。
进程,在一定的环境下,把静态的程序代码运行起来,通过使用不同的资源,来完成一定的任务。
线程,作为进程的一部分,扮演的角色就是怎么利用中央处理器去运行代码。这其中牵扯到的最重要资源的是中央处理器和其中的寄存器,和线程的栈(stack)。
简言之:线程关注的是中央处理器的运行,进程关注的是内存等资源的管理。
45.进程间通信的方式?
管道,信号量,消息队列,共享内存,套接字
46.linux查看cpu使用情况
top -i
47.一个服务器并发200个线程和200个服务器并发200个线程线程并发的优缺点?
48.tcp和udp的区别?
1、 TCP面向连接 (如打电话要先拨号建立连接); UDP是无连接 的,即发送数据之前不需要建立连接
2、TCP提供可靠的服务。也就是说,通过TCP连接传送的数据,无差错,不丢失,不重复,且按序到达;UDP尽最大努力交付,即不保证可靠交付
Tcp通过校验和,重传控制,序号标识,滑动窗口、确认应答实现可靠传输。如丢包时的重发控制,还可以对次序乱掉的分包进行顺序控制。
3、UDP具有较好的实时性,工作效率比TCP高,适用于对高速传输和实时性有较高的通信或广播通信。
4.每一条TCP连接只能是点到点的;UDP支持一对一,一对多,多对一和多对多的交互通信
5、TCP对系统资源要求较多,UDP对系统资源要求较少。
49.JMM(Java内存模型)
JMM定义了线程和主内存之间的抽象关系:
线程之间的共享变量存储在主内存(Main Memory)中,
每个线程都有一个私有的本地内存(Local Memory),
本地内存中存储了该线程以读/写共享变量的副本。
本地内存是JMM的一个抽象概念,并不真实存在。
它涵盖了缓存、写缓冲区、寄存器以及其他的硬件和编译器优化。
所有原始类型(boolean,byte,short,char,int,long,float,double)的局部变量都直接保存在线程栈当中,对于它们的值各个线程之间都是独立的。
堆区包含了Java应用创建的所有对象信息,不管对象是哪个线程创建的,其中的对象包括原始类型的封装类(如Byte、Integer、Long等等)。不管对象是属于一个成员变量还是方法中的局部变量,它都会被存储在堆区。
50.说一下Volatile关键字
作用:
1.保证变量对所有线程的可见性
当一个线程修改了这个变量的值,Volatile能保证新值立即同步到主内存,
以及每次使用前立即刷新。
2.禁止指令重排序
有Volatile修饰的变量,赋值后多执行一个"load add |$0x0,(%esp)"操作,
这个 操作相当于一个内存屏障(指令重排序时不能把后边的指令重排序到内存屏障前边的 位置);
性能:读性能与普通变量几乎相同,但是写操作慢,因为插入了内存屏障
实现原理: 1.进行写操作时Lock前缀指令引起处理器缓存写会内存;
2.一个处理器的缓存写回内存将会导致其他处理器的缓存失效;
3.当处理器发现本地缓存失效后,就会从内存中重读该变量数据,
即可以获取当前最新值,这样针对Volatile变量通过这样的机制就使得每个线程 都能获取到变量的最新值。
在多处理器下,为了保证各个处理器的缓存是一致的,就会实现缓存缓存一致性协议(MESI),每个处理器通过嗅探在总线上传播的数据来检查自己的缓存值是不是过期了,如果处理器发现自己缓存行对应的内存地址呗修改,就会将当前处理器的缓存行设置无效状态,当处理器对这个数据进行修改操作的时候,会重新从系统内存中把数据库读到处理器缓存中。
编译器不会对volatile读后面的任意内存操作重排序;
编译器不会对volatile写前面的任意内存操作重排序;
51.说一下缓存一致性协议(MESI)
缓存一致性协议有MSI,MESI,MOSI,Synapse,Firefly及DragonProtocol等等,接下来我们主要介绍MESI协议。
MESI分别代表缓存行数据所处的四种状态,通过对这四种状态的切换,来达到对缓存数据进行管理的目的。
状态 描述 监听任务 M 修改(Modify) 该缓存行有效,数据被修改了,和内存中的数据不一致,数据只存在于本缓存行中 缓存行必须时刻监听所有试图读该缓存行相对应的内存的操作,其他缓存须在本缓存行写回内存并将状态置为E之后才能操作该缓存行对应的内存数据 E 独享、互斥(Exclusive) 该缓存行有效,数据和内存中的数据一致,数据只存在于本缓存行中 缓存行必须监听其他缓存读主内存中该缓存行相对应的内存的操作,一旦有这种操作,该缓存行需要变成S状态 S 共享(Shared) 该缓存行有效,数据和内存中的数据一致,数据同时存在于其他缓存中 缓存行必须监听其他缓存是该缓存行无效或者独享该缓存行的请求,并将该缓存行置为I状态 I 无效(Invalid) 该缓存行数据无效 无 备注:
1.MESI协议只对汇编指令中执行加锁操作的变量有效,表现到java中为使用voliate关键字定义变量或使用加锁操作
2.对于汇编指令中执行加锁操作的变量,MESI协议在以下两种情况中也会失效:
一、CPU不支持缓存一致性协议。
二、该变量超过一个缓存行的大小,缓存一致性协议是针对单个缓存行进行加锁,此时,缓存一致性协议无法再对该变量进行加锁,只能改用总线加锁的方式。
MESI工作原理:(此处统一默认CPU为单核CPU,在多核CPU内部执行过程和一下流程一致)
1、CPU1从内存中将变量a加载到缓存中,并将变量a的状态改为E(独享),并通过总线嗅探机制对内存中变量a的操作进行嗅探
2、此时,CPU2读取变量a,总线嗅探机制会将CPU1中的变量a的状态置为S(共享),并将变量a加载到CPU2的缓存中,状态为S
3、CPU1对变量a进行修改操作,此时CPU1中的变量a会被置为M(修改)状态,而CPU2中的变量a会被通知,改为I(无效)状态,此时CPU2中的变量a做的任何修改都不会被写回内存中(高并发情况下可能出现两个CPU同时修改变量a,并同时向总线发出将各自的缓存行更改为M状态的情况,此时总线会采用相应的裁决机制进行裁决,将其中一个置为M状态,另一个置为I状态,且I状态的缓存行修改无效)
4、CPU1将修改后的数据写回内存,并将变量a置为E(独占)状态
5、此时,CPU2通过总线嗅探机制得知变量a已被修改,会重新去内存中加载变量a,同时CPU1和CPU2中的变量a都改为S状态
在上述过程第3步中,CPU2的变量a被置为I(无效)状态后,只是保证变量a的修改不会被写回内存,但CPU2有可能会在CPU1将变量a置为E(独占)状态之前重新读取内存中的变量a,这个取决于汇编指令是否要求CPU2重新加载内存。
总结
以上就是MESI的执行原理,MESI协议只能保证并发编程中的可见性,并未解决原子性和有序性的问题,所以只靠MESI协议是无法完全解决多线程中的所有问题。
52.MySQL左连接,右连接,分组求和
LEFT JOIN 左连接:以左表为主,显示左表所有的数据,右表中没有的显示null值。
RIGHT JOIN 右连接:以右表为主,显示右表所有的数据,左表中没有的显示null值。
先准备两个表CREATE TABLE A( id INT NOT NULL AUTO_INCREMENT, 作者 VARCHAR(10), 出自 VARCHAR(10), PRIMARY KEY (id) ); INSERT INTO A (作者,出自) VALUES('荀子','劝学'); INSERT INTO A (作者,出自) VALUES('屈原','离骚'); INSERT INTO A (作者,出自) VALUES('老子','老子'); CREATE TABLE B( id INT, verse VARCHAR(30) ); INSERT INTO B(id,verse) VALUES(1,'不积跬步无,以至千里'); INSERT INTO B(id,verse) VALUES (2,'路漫漫其修远兮,吾将上下而求索'); INSERT INTO B(id,verse) VALUES (4,'不积小流,无以成江海');
1. 左链接
#左链接 SELECT A.* ,B.* FROM A LEFT OUTER JOIN B ON(A.`id`=B.`id`);
结果显示
2.右连接
#右连接 SELECT A.* ,B.* FROM A RIGHT OUTER JOIN B ON(A.`id`=B.`id`);
结果显示:
从图中可以看出,JOIN谁,谁只显示符合条件的
3.分组查询
1.单独使用GROUP BY关键字进行分组
如果单独使用GROUP BY关键字,查询结果只显示一个分组的一条记录。
SELECT * FROM employee GROUP BY sex;
GROUP BY关键字单独使用时,只能查询出每个分组的一条记录,这样做的意义不大。
2.GROUP BY关键字与GROUP_CONCAT()函数一起使用
实例:将employee表按照sex字段进行分组查询。使用GROUP_CONCAT()函数将每个分组的name字段的值显示出来。
SELECT sex,GROUP_CONCAT(name) FROM employee GROUP BY sex;
3.GROUP BY关键字与集合函数一起使用
实例:将employee表的sex字段进行分组查询。sex字段取值相同的为一组。然后对每一组使用集合函数COUNT()函数进行计算,求出每一组的记录数。
SELECT sex,COUNT(sex) FROM employee GROUP BY sex;
实例:查询的是男生和女生的各自的总年龄select sex, sum(age) age from users group by sex
4.GROUP BY关键字与HAVING一起使用
使用GROUP BY关键字时,如果加上“HAVING 条件表达式”,则可以限制输出的结果。只有符合条件表达式的结果才会显示。
实例:将employee表的sex字段进行分组查询。然后显示记录数大于等于3的分组。
SELECT sex,COUNT(sex) FROM employee GROUP BY sex HAVING COUNT(sex)>=3;
5.按照多个字段进行分组
实例:将employee表按照d_id字段和sex字段进行分组。
SELECT * FROM employee GROUP BY d_id,sex;
6.GROUP BY关键字与WITH ROLLUP一起使用
使用WITH ROLLUP时,将会在所有记录的最后加上一条记录。这条记录是上面所有记录的总和。
实例:将employee表的sex字段进行分组查询。使用COUNT()函数计算每组的记录数,并且加上WITH ROLLUP。
SELECT sex,COUNT(sex) FROM employee GROUP BY sex WITH ROLLUP;
53.查询的语句模板以及先后顺序
模板:
select 字段,聚合函数,... from 表名 [where 条件] group by 分组表达式 having 分组过滤条件 order by [排序条件] limit [offset,] count;
在写分组查询的时候,最好按照标准的规范来写,select后面出现的列必须在group by中或必须使用聚合函数;
select语法顺序:select、from、where、group by、having、order by、limit,顺序不能搞错了,否则报错。
select语法执行顺序:from > where > group by > having > select > order by > limit
54.集合小细节
- Hashmap无序,key不可重复,只允许有一个null值,value可重复,并且允许
(任意多的)values值为null
TreeMap有序,不允许null值
- Arraylist有序,值可重复,允许null值
- LinkedList有序,值可重复,允许null值
- HashSet无序,值不可重复,可以有1个为null的元素
- TreeSet有序,值不可重复,不能有key为null的元素
- HashTable 无序,Key不可重复,不允许key为null,Value也不可以为null
55.java中静态变量,静态代码块,静态方法,实例变量,普通代码块的初始化顺序
父类静态变量、静态代码块 --> 子类静态变量、静态代码块 --> 父类成员变量、普通代码块--> 父类构造方法-- --> 子类成员变量、普通代码块--> 子类构造方法
1、先执行父类的静态代码块和静态变量初始化,并且静态代码块和静态变量的执行顺序只跟代码中出现的顺序有关。
2、执行子类的静态代码块和静态变量初始化。
3、执行父类的实例变量、普通代码块的初始化
4、执行父类的构造方法
5、执行子类的实例变量、普通代码块的初始化
6、执行子类的构造方法
- 静态代码块(只加载一次)
- 构造方法(创建一个实例就加载一次)
- 静态方法需要调用才会执行
56.XSS攻击,如何避免?SQL注入是什么?如何避免?CSRF是什么?如何避免?
XSS
XSS 攻击,即跨站脚本攻击(Cross Site Scripting),它是 web 程序中常见的漏洞。
原理
攻击者往 web 页面里插入恶意的 HTML 代码(Javascript、css、html 标签等),当用户浏览该页面时,嵌入其中的 HTML 代码会被执行,从而达到恶意攻击用户的目的。如盗取用户 cookie 执行一系列操作,破坏页面结构、重定向到其他网站等。
预防
- web 页面中可由用户输入的地方,如果对输入的数据转义、过滤处理
- 后台输出页面的时候,也需要对输出内容进行转义、过滤处理(因为攻击者可能通过其他方式把恶意脚本写入数据库)
- 前端对 html 标签属性、css 属性赋值的地方进行校验
CSRF
CSRF,跨站请求伪造,是一种劫持受信任用户向服务器发送非预期请求的攻击方式。
通常情况下,CSRF 攻击是攻击者借助受害者的 Cookie 骗取服务器的信任,在受害者毫不知情的情况下以受害者名义伪造请求发送给受攻击服务器,从而在并未授权的情况下执行在权限保护之下的操作。
预防:
1.验证请求来源地址
2.关键操作添加验证码
3.在请求地址添加token验证
SQL注入
SQL注入,用户提交带有恶意的数据与SQL语句进行字符串方式的拼接,从而影响了SQL语句的语义,最终产生数据泄露的现象
预防:
1. 代码层防止sql注入攻击的最佳方案就是sql预编译
2. 确认每种数据的类型,比如是数字,数据库则必须使用int类型来存储
3. 规定数据长度,能在一定程度上防止sql注入
4. 严格限制数据库权限,能最大程度减少sql注入的危害
5. 避免直接响应一些sql异常信息,sql发生异常后,自定义异常进行响应
6. 过滤参数中含有的一些数据库关键词
57.CAS,优缺点?ABA问题,如何处理?
CAS(Compare And Swap)是一种有名的无锁算法。CAS算法是乐观锁的一种实现。CAS有3个操作数,内存值V,旧的预期值A,要修改的新值B。当且仅当预期值A和内存值V相同时,将内存值V修改为B并返回true,否则返回false。
乐观锁避免了悲观锁独占对象的现象,同时也提高了并发性能,乐观锁是对悲观锁的改进,虽然它也有缺点,但它确实已经成为提高并发性能的主要手段,而且jdk中的并发包也大量使用基于CAS的乐观锁。
缺点:
1.CPU可能开销较大
在并发量比较高的情况下,如果许多线程反复尝试更新某一个变量,却又一直更新不成功,循环往复,会给CPU带来很大的压力。2.不能保证代码块的原子性
CAS机制所保证的只是一个变量的原子性操作,而不能保证整个代码块的原子性。比如需要保证3个变量共同进行原子性的更新,就不得不使用悲观锁了。3.ABA问题。
CAS的核心思想是通过比对内存值与预期值是否一样而判断内存值是否被改过,但这个判断逻辑不严谨,假如内存值原来是A,后来被一条线程改为B,最后又被改成了A,则CAS认为此内存值并没有发生改变,但实际上是有被其他线程改过的,这种情况对依赖过程值的情景的运算结果影响很大。解决的思路是引入版本号,每次变量更新都把版本号加一。
ABA问题:
线程1准备用CAS修改变量值A,在此之前,其它线程将变量的值由A替换为B,又由B替换为A,然后线程1执行CAS时发现变量的值仍然为A,所以CAS成功。但实际上这时的现场已经和最初不同了。
ABA问题处理:
思路:解决ABA最简单的方案就是给值加一个修改版本号,每次值变化,都会修改它版本号,CAS操作时都对比此版本号。
58.Springboot的@SpringBootApplication注解包含哪些注解,都用来做什么?
每个SpringBoot程序都有一个主入口,也就是main方法,main里面调用SpringApplication.run()启动整个spring-boot程序,该方法所在类需要使用@SpringBootApplication注解,以及@ImportResource注解(if need),@SpringBootApplication包括三个注解,功能如下:
@EnableAutoConfiguration:SpringBoot根据应用所声明的依赖来对Spring框架进行自动配置
@SpringBootConfiguration(内部为@Configuration):被标注的类等于在spring的XML配置文件中(applicationContext.xml),装配所有bean事务,提供了一个spring的上下文环境@ComponentScan:组件扫描,可自动发现和装配Bean,默认扫描SpringApplication的run方法里的Booter.class所在的包路径下文件,所以最好将该启动类放到根包路径下|
59.Spring源码看过吧?说一下
60.什么情况下put了key和value会替换掉链表上的元素,什么情况下是插入到链表里?
我们首先要了解HashMap的put方法
HashMap在插入一个键值对的时候,会首先把key的hashcode再次进行hash算法处理,得到的Hash,然后(Node数组长度-1)&Hash得到我们要存放这个键值对的位置,然后插入到这个位置。
同样,如果我们查找HashMap上的某个元素,我们首先会比较他们的Hash(hashcode再次hash算法处理后的hash值),找到Hash相等的键值对,因为hash冲突的原因,所以这个键值对可能不唯一,它们一般在一个链表上,我们会继续比较他们的equals方法找到真正一模一样的那个键值对(元素);
所以put的键值对替换掉链表上元素的情况:
查找时发现比较Hash值和equals方法都相等,需要替换;
插入到链表里的情况:
如果查找时发现比较Hash值相等,equals没有一个与之相等的,就插入到链表里;
接着我们来详细讲一下这里的hash算法:
hash
方法实际是让key.hashCode()
与key.hashCode()>>>16
进行异或操作,高16bit补0,一个数和0异或不变,所以 hash 函数大概的作用就是:高16bit不变,低16bit和高16bit做了一个异或,目的是减少碰撞。因为bucket数组大小是2的幂,计算下标
index = (table.length - 1) & hash
,如果不做 hash 处理,相当于散列生效的只有几个低 bit 位,为了减少散列的碰撞,设计者综合考虑了速度、作用、质量之后,使用高16bit和低16bit异或来简单处理减少碰撞,而且JDK8中用了复杂度 O(logn)的树结构来提升碰撞下的性能。hash()函数
上面提到的问题,主要是因为如果使用hashCode取余,那么相当于参与运算的只有hashCode的低位,高位是没有起到任何作用的,所以我们的思路就是让hashCode取值出的高位也参与运算,进一步降低hash碰撞的概率,使得数据分布更平均,我们把这样的操作称为扰动,在JDK 1.8中的hash()函数如下:
static final int hash(Object key) { int h; return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);// 与自己右移16位进行异或运算(高低位异或) }
这比在JDK 1.7中,更为简洁,相比在1.7中的4次位运算,5次异或运算(9次扰动),在1.8中,只进行了1次位运算和1次异或运算(2次扰动);
61.为什么Java8中HashMap使用红黑树而不是AVL树?
首先我们要了解AVL树和红黑树适用的场景:
(1)AVL树是更加严格的平衡,因此可以提供更快的查找速度,一般读取查找密集型任务,适用AVL树。
(2)红黑树更适合于插入修改密集型任务。
(3)通常,AVL树的旋转比红黑树的旋转更加难以平衡和调试。细说区别:
(1)添加/删除操作时完成的旋转操作次数。
(2)两种实现都缩放为a O(lg N),其中N是叶子的数量,但实际上AVL树在查找密集型任务上更快:利用更好的平衡,树遍历平均更短。另一方面,插入和删除方面,AVL树速度较慢:需要更高的旋转次数才能在修改时正确地重新平衡数据结构。
(3)在AVL树中,从根到任何叶子的最短路径和最长路径之间的差异最多为1。在红黑树中,差异可以是2倍。
(4)两个都给O(log n)查找,但平衡AVL树可能需要O(log n)旋转,而红黑树将需要最多两次旋转使其达到平衡(尽管可能需要检查O(log n)节点以确定旋转的位置)。旋转本身是O(1)操作,因为你只是移动指针。
62.MySql的事务隔离级别以及与锁的关系
未提交读,提交读,可重复读,序列化;
- 未提交读:最低隔离级别,事务提交前,就可以被其他事务读取(会出现幻读,脏读,不可重复读)
- 提交读:一个事务提交后,才能被其他事务读取到(会造成幻读,不可重复读)
- 可重复读:默认级别,保证多次读取同一数据时,其值都和事务开始时的内容一致,禁止读取到别的事务未提交的数据(会造成幻读)
- 序列化:代价最高最可靠的隔离级别,该隔离级别能防止脏读,不可重复读,幻读。
隔离级别与锁的关系:
- read-uncommitted级别下,读取数据不需要加共享锁,不会跟被修改数据的排它锁冲突
- read-committed级别下,读取数据的时候加上共享锁,语句执行完以后释放锁
- repeatable-read级别下,读取操作要加上共享锁,事务完成以后才能释放锁
- serializable级别下,锁定整个范围的键,一直持有锁,直到事务完成
共享锁(读锁):其他事务可以读,但不能写。允许一个事务去读一行,阻止其他事务获得相同数据集的排他锁。
排他锁(写锁) :其他事务不能读取,也不能写。允许获得排他锁的事务更新数据,阻止其他事务取得相同数据集的共享读锁和排他写锁。
63.可重复读和已提交读的区别
1.当数据库系统使用 提交读(READ COMMITTED)隔离级别时,一个事务在执行过程中可以看到其他事务已经提交的新插入的记录,而且还能看到其他事务已经提交的对已有记录的更新。会出现不可重复读的问题。
2.当数据库系统使用可重复读(REPEATABLE READ)隔离级别时,一个事务在执行过程中可以看到其他事务已经提交的新插入的记录,但是不能看到其他事务对已有记录的更新。会出现幻读的情况。
顺便说一下幻读和不可重复读的区别:
幻读:同一事务内多次查询返回的结果不一样
不可重复读:在一个事务内,多次读取同一数据,结果不一样。
1.不可重复读重点在于update和delete,而幻读的重点在于insert。
2.不可重复读和幻读最大的区别,就在于如何通过锁机制来解决他们产生的问题。
如何解决幻读和不可重复读?
幻读
解决:在 MySQL InnoDB 中,Repeatable Read 隔离级别不存在幻读问题,
对于快照读,InnoDB 使用 MVCC 解决幻读,
对于当前读,InnoDB 通过 gap locks 或 next-key locks 解决幻读。
不可重复读
解决:在 MySQL InnoDB 中,Repeatable Read 隔离级别使用 MVCC 来解决不可重复读问题。
64.什么是事务?事务传播机制?
事务是指作为单个逻辑工作单元执行的一系列操作,要么都成功要么都失败
事务是最小的执行单位,不能分割。
事务的传播机制:
- REQUIRED:如果有事务则加入事务;如果没有事务则创建新的事务(默认值)注:当两个方法传播机制都是REQUIRED时,一旦发生回滚,两个方法都会回滚
- NOT_SUPPORTED:Spring不为当前方法开启事务,相当于没有事务
@Transactional(propagation=Propagation.NOT_SUPPORTED)- REQUIRED_NEW : 不管是否存在事务,都创建一个新的事务,原来的方法挂起来,新的方法执行完毕以后,继续执行老的方法
@Transactional(propagation=Propagation.REQUIRED_NEW)
注:当delete方法传播机制为REQUIRED_NEW,会开启一个新的事务,并且单独提交方法,所以save方法回滚不影响delete方法事务的提交- MANDATORY:必须在一个已有的事务中执行,否则报错
@Transactional(propagation=Propagation.MANDATORY)- NEVER;必须在一个没有的事务中执行,否则报错
@Transactional(propagation=Propagation.NEVER)- SUPPORTS:如果其他bean调用了这个方法时,其他bean声明了事务,就用这个事务,没有声明,那就不用事务
- NESTED:如果当前存在事务,则在嵌套事务内执行。如果当前没有事务,则执行REQUIRED类似的操作注:当save方法为required时,delete方法为nested时,delete方法开启一个嵌套事务;当save方法回滚时,delete方法也会回滚;delete回滚不影响save方法的提交
65.分页查询原理,MYSQL分页以及如何优化?
分页查询原理:分页查询原理
一般会传CurrentPage(当前页),PageSize两个参数
(CurrentPage-1)*PageSize,PageSize传给mapper
MYSQL分页:
一般的分页查询使用简单的 limit 子句就可以实现。
IMIT 子句可以被用于指定 SELECT 语句返回的记录数。需注意以下几点:
- 第一个参数指定第一个返回记录行的偏移量,注意从
0
开始- 第二个参数指定返回记录行的最大数目
- 如果只给定一个参数:它表示返回最大的记录行数目
- 第二个参数为 -1 表示检索从某一个偏移量到记录集的结束所有的记录行
- 初始记录行的偏移量是 0(而不是 1)
select * from orders_history where type=8 order by id limit 10000,10;
随着查询偏移的增大,尤其查询偏移大于10万以后,查询时间急剧增加。
这种分页查询方式会从数据库第一条记录开始扫描,所以越往后,查询速度越慢,而且查询的数据越多,也会拖慢总查询速度。
如何优化?
1.子查询优化
带偏移量的话实际上是把开始的前边都扫描,然后就不要了,子查询优化就是可以不查前边的,从需要的地方开始,取一个pageSize大小的数量
select * from orders_history where type=8 and id>=(select id from orders_history where type=8 limit 100000,1) limit 100;
那我要是输入页码呢?子查询优化存在的逻辑删除数据怎么处理?
限制随意跳转,只能点击下一页按钮,
然后在点击下一页的时候把最后一条数据的主键值拿到,用子查询优化,走索引
(点击下一页的时候,把本页最后一条的数据拿到,去表里边查,取到这条数据后边的,把那个偏移量给省掉了)
select* from table where id > (select id from table where ...) limit pageSize;
2.使用 id 限定优化
这种方式假设数据表的id是连续递增的,则我们根据查询的页数和查询的记录数可以算出查询的id的范围,可以使用 id between and 来查询:
select * from orders_history where type=2 and id between 1000000 and 1000100 limit 100;
还可以有另外一种写法:
select * from orders_history where id >= 1000001 limit 100;
当然还可以使用 in 的方式来进行查询,这种方式经常用在多表关联的时候进行查询,使用其他表查询的id集合,来进行查询:
select * from orders_history where id in (select order_id from trade_2 where goods = 'pen') limit 100;
这种 in 查询的方式要注意:某些 mysql 版本不支持在 in 子句中使用 limit。
一般情况下,在数据库中建立表的时候,强制为每一张表添加 id 递增字段,这样方便查询。
如果像是订单库等数据量非常庞大,一般会进行分库分表。这个时候不建议使用数据库的 id 作为唯一标识,而应该使用分布式的高并发唯一 id 生成器来生成,并在数据表中使用另外的字段来存储这个唯一标识。
使用先使用范围查询定位 id (或者索引),然后再使用索引进行定位数据,能够提高好几倍查询速度。
66.如果保证接口调用的安全性
1.签名(token)
根据用户名或者用户id,结合用户的ip或者设备号,生成一个token。在请求后台,后台获取http的head中的token,校验是否合法(和数据库或者redis中记录的是否一致,在登录或者初始化的时候,存入数据库/redis)
在使用Base64方式的编码后,Token字符串还是有20多位,有的时候还是嫌它长了。由于GUID本身就有128bit,在要求有良好的可读性的前提下,很难进一步改进了。那我们如何产生更短的字符串呢?还有一种方式就是较少Token的长度,不用GUID,而采用一定长度的随机数,例如64bit,再用Base64编码表示:
var rnd = new Random();
var tokenData = userIp+userId;
rnd.NextBytes(tokenData);
var token = Convert.ToBase64String(tokenData).TrimEnd('=');由于这里只用了64bit,此时得到的字符串为Onh0h95n7nw的形式,长度要短一半。这样就方便携带多了。但是这种方式是没有唯一性保证的。不过用来作为身份认证的方式还是可以的(如网盘的提取码)。
客户端和服务器都保存一个秘钥,每次传输都加密,服务端根据秘钥解密。
客户端:
1、设置一个key(和服务器端相同)
2、根据上述key对请求进行某种加密(加密必须是可逆的,以便服务器端解密)
3、发送请求给服务器
服务器端:
1、设置一个key
2、根据上述的key对请求进行解密(校验成功就是「信任」的客户端发来的数据,否则拒绝响应)
3、处理业务逻辑并产生结果
4、将结果反馈给客户端
比如Spring security-oauth
67.通常app 的 一键登录怎么实现的
在应用客户端中嵌入认证SDK,用户请求登录时,通过该SDK与运营商的网络通信来采集用户手机号码,在获得用户同意授权后,应用客户端获得接口调用token,传递给应用服务端,请求认证服务端获取手机号码接口,最终实现获取当前授权用户的手机号码等信息。
68.TCP粘包问题
粘包问题分析与对策
TCP粘包是指发送方发送的若干包数据到接收方接收时粘成一包,从接收缓冲区看,后一包数据的头紧接着前一包数据的尾。
出现粘包现象的原因是多方面的,它既可能由发送方造成,也可能由接收方造成。
什么时候需要考虑粘包问题
如果双方建立连接,需要在连接后一段时间内发送不同结构数据,如连接后,有好几种结构:
1)"hellogive me sth abour yourself"
2)"Don'tgive me sth abour yourself"
那这样的话,如果发送方连续发送这个两个包出去,接收方一次接收可能会是"hellogive me sth abour yourselfDon't give me sth abour yourself"这样接收方就傻了,到底是要干嘛?不知道,因为协议没有规定这么诡异的字符串,所以要处理把它分包,怎么分也需要双方组织一个比较好的包结构,所以一般可能会在头加一个数据长度之类的包,以确保接收。
粘包出现原因
简单得说,在流传输中出现,UDP不会出现粘包,因为它有消息边界(参考Windows网络编程)
1发送端需要等缓冲区满才发送出去,造成粘包
2接收方不及时接收缓冲区的包,造成多个包接收
为了避免粘包现象,可采取以下几种措施:
(1)对于发送方引起的粘包现象,用户可通过编程设置来避免,TCP提供了强制数据立即传送的操作指令push,TCP软件收到该操作指令后,就立即将本段数据发送出去,而不必等待发送缓冲区满;
(2)对于接收方引起的粘包,则可通过优化程序设计、精简接收进程工作量、提高接收进程优先级等措施,使其及时接收数据,从而尽量避免出现粘包现象;
(3)由接收方控制,将一包数据按结构字段,人为控制分多次接收,然后合并,通过这种手段来避免粘包。
69.wait()、notify()和notifyAll()方法为什么要在synchronized代码块中?
使用wait和notify的时候一定要配合synchronized关键字去使用
在Object的wait()方法上面有这样一行注释:The current thread must own this object's monitor,意思是调用实例对象的wait()方法时,该线程必须拥有当前对象的monitor对象锁,而要拥有monitor对象锁就需要在synchronized修饰的方法或代码块中竞争并生成占用monitor对象锁。而不使用synchronized修饰的方法或代码块就不会占有monitor对象锁,所以在synchronized代码块之外调用会出现错误,错误提示为:
Exception in thread "main" java.lang.IllegalMonitorStateException at java.lang.Object.wait(Native Method) at java.lang.Object.wait(Object.java:502) at it.cast.basic.thread.SynchronizedTest.main(SynchronizedTest.java:27)
因为只有拥有了monitor对象的线程才能调用wait()方法进入_WaitSet 等待集合中等待被唤醒,而且对于monitor对象来说,同一时刻只能被一个线程锁持有,在不加synchronized修饰的时候(也就是不是有锁的时候),monitor对象就不会被使用到,这里突然使用wait()方法就是出现逻辑错误,所以必须在synchronized代码块中。
70.TreeMap是如何排序的,按什么排序的?是对什么进行排序的?
TreeMap 默认排序规则:按照key的字典顺序来排序(升序)
当然,也可以自定义排序规则:要实现Comparator接口。
1、排序规则
两个字符串 s1, s2比较
(1)、如果s1和s2是父子串关系,则 子串 < 父串
(2)、如果不是父子串关系, 则从第一个非相同字符来比较。
例子 s1 = "ab", s2 = "ac" 这种情况算法规则是从第二个字符开始比较,由于'b' < 'c' 所以 "ab" < "ac"
(3)、字符间的比较,是按照字符的字节码(ascii)来比较
2、 compareTo 实现机制:对于字符串来说,字典排序规则;对于数字来说,直接按照大小排序
下面, 是我在项目中,遇到的一个坑,也不能算坑吧,只能说基础掌握得不扎实,导致老不断犯错。先说下场景,有个需求要对Map排序,当时想当然就用了自定义的TreeMap(new
Comparator )
key 为 String, value 也会String类型, 然后很不幸的是,我的Key 是 数字 字符串 ,如 Map.put("2","1"),Map.put("12","1"),Map.put("13","1")
正常思维排序结果是 "2" < "12" < "13" ,仔细一想,compareTo 底层算法是 "字典排序",正确的排序结果 : "12" < "13" < "2"
但是我的需求又是想要"2" < "12" < "13"这种效果,如何实现呢?很简单,把Key改为Long类型,这样,就会按照大小来排序。
71.为什么红黑树搜索比链表快?
因为红黑树会旋转,加上二分查找比线性扫描快
72.偏向锁、轻量级锁是解决什么问题的?
偏向锁
减少无竞争且只有一个线程使用锁的情况下,使用轻量级锁产生的性能消耗。
如果不仅仅没有实际竞争,自始至终,使用锁的线程都只有一个,那么,维护轻量级锁都是浪费的。
轻量级锁每次申请、释放锁都至少需要一次CAS,但偏向锁只有初始化时需要一次CAS。
“偏向”的意思是,偏向锁假定将来只有第一个申请锁的线程会使用锁(不会有任何线程再来申请锁),因此,只需要在Mark Word中CAS记录owner(本质上也是更新,但初始值为空),如果记录成功,则偏向锁获取成功,记录锁状态为偏向锁,以后当前线程等于owner就可以零成本的直接获得锁;否则,说明有其他线程竞争,膨胀为轻量级锁。
轻量级锁
轻量级锁的目标是,减少无实际竞争情况下,使用重量级锁产生的性能消耗,包括系统调用引起的内核态与用户态切换、线程阻塞造成的线程切换等。
使用轻量级锁时,不需要申请互斥量,仅仅将Mark Word中的部分字节CAS更新指向线程栈中的Lock Record,如果更新成功,则轻量级锁获取成功,记录锁状态为轻量级锁;否则,说明已经有线程获得了轻量级锁,目前发生了锁竞争(不适合继续使用轻量级锁),接下来膨胀为重量级锁。
73.ThreadLocal 是什么?有哪些使用场景?
ThreadLocal 是线程本地存储,在每个线程中都创建了一个 ThreadLocalMap 对象,每个线程可以访问自己内部 ThreadLocalMap 对象内的 value。
ThreadLocal 为每个使用该变量的线程提供独立的变量副本,所以每一个线程都可以独立地改变自己的副本,而不会影响其它线程所对应的副本。
使用场景:数据库连接、Session管理
74.强引用,软引用,弱引用,虚引用?
强引用:使用最普遍的引用,发生GC的时候不会被回收
软引用:在发生内存溢出前会被回收。
弱引用:在下一次GC时会被回收。
虚引用:无法通过虚引用获取对象,其用途就是在gc时返回一个通知。
75.自定义线程池?
/** * 自定义简单线程池 */ public class MyThreadPool{ /**存放线程的集合*/ private ArrayList<MyThead> threads; /**任务队列*/ private ArrayBlockingQueue<Runnable> taskQueue; /**线程池初始限定大小*/ private int threadNum; /**已经工作的线程数目*/ private int workThreadNum; private final ReentrantLock mainLock = new ReentrantLock(); public MyThreadPool(int initPoolNum) { threadNum = initPoolNum; threads = new ArrayList<>(initPoolNum); //任务队列初始化为线程池线程数的四倍 taskQueue = new ArrayBlockingQueue<>(initPoolNum*4); threadNum = initPoolNum; workThreadNum = 0; } public void execute(Runnable runnable) { try { mainLock.lock(); //线程池未满,每加入一个任务则开启一个线程 if(workThreadNum < threadNum) { MyThead myThead = new MyThead(runnable); myThead.start(); threads.add(myThead); workThreadNum++; } //线程池已满,放入任务队列,等待有空闲线程时执行 else { //队列已满,无法添加时,拒绝任务 if(!taskQueue.offer(runnable)) { rejectTask(); } } } finally { mainLock.unlock(); } } private void rejectTask() { System.out.println("任务队列已满,无法继续添加,请扩大您的初始化线程池!"); } public static void main(String[] args) { MyThreadPool myThreadPool = new MyThreadPool(5); Runnable task = new Runnable() { @Override public void run() { System.out.println(Thread.currentThread().getName()+"执行中"); } }; for (int i = 0; i < 20; i++) { myThreadPool.execute(task); } } class MyThead extends Thread{ private Runnable task; public MyThead(Runnable runnable) { this.task = runnable; } @Override public void run() { //该线程一直启动着,不断从任务队列取出任务执行 while (true) { //如果初始化任务不为空,则执行初始化任务 if(task != null) { task.run(); task = null; } //否则去任务队列取任务并执行 else { Runnable queueTask = taskQueue.poll(); if(queueTask != null) queueTask.run(); } } } } }
过程:
- 初始化线程池,指定线程池的大小。
- 向线程池中放入任务执行。
- 如果线程池中创建的线程数目未到指定大小,则创建我们自定义的线程类放入线程池集合,并执行任务。执行完了后该线程会一直监听队列
- 如果线程池中创建的线程数目已满,则将任务放入缓冲任务队列
- 线程池中所有创建的线程,都会一直从缓存任务队列中取任务,取到任务马上执行
76.Spring容器的启动过程?
如果让你去设计一个 IOC 容器,你会怎么做?
- 首先我肯定会提供一个入口(AnnotationConfigApplicationContext )给用户使用,然后需要去初始化一系列的工具组件;
- 如果我想生成 bean 对象,那么就需要一个 beanFactory 工厂(DefaultListableBeanFactory);
- 如果我想对加了特定注解(如 @Service、@Repository)的类进行读取转化成 BeanDefinition 对象(BeanDefinition 是 Spring 中极其重要的一个概念,它存储了 bean 对象的所有特征信息,如是否单例,是否懒加载,factoryBeanName 等),那么就需要一个注解配置读取器(AnnotatedBeanDefinitionReader);
- 如果我想对用户指定的包目录进行扫描查找 bean 对象,那么还需要一个路径扫描器(ClassPathBeanDefinitionScanner)。
启动的时候要准备各种bean,包括spring 内部用到的bean以及读取BeanDefinition ,准备一系列后置bean.....
77.如何使用注解定义Bean?
使用注解定义Bean
@Component("user") public class UserImpl implements User{}
@Component(“user”) 相当于除了@Component(),
Spring还提供了三个特殊注解
- @Repository 用于标注Dao类
- @Service 用于标注业务类
- @Controller 用于标注控制器类
Spring提供了@Autowired注解实现Bean的装配
@Service("userService") public class UserServiceImpl(){ @Autowired private User u; }
如果容器中有一个以上相同类型的Bean时 @Autowired就不可以用了 ,所以要用**@Qualifier(“user”)** 这里的user表示上面注解定义的bean
@Service("userService") public class UserServiceImpl(){ @Qualifier("user") private User u; }
加载注解定义的Bean(ApplicationContext.xml文件中 两个注解间用逗号隔开)
<context:component-scan base-package="包名1,包名2"/>
使用java标准注解完成装配
为属性注入bean
@Resource(name=“bean的id”)
@Resource
不写后面括号就是匹配字段名或setter方法
78.maven依赖冲突问题应该如何解决?
首先查看产生依赖冲突的类jar,其次找出我们不想要的依赖类jar,手工将其排除在外就可以了。
具体步骤:
1、查看依赖冲突
通过dependency:tree是命令来检查版本冲突
mvn -Dverbose dependency:tree
如果是idea,可以安装maven helper插件来检查依赖冲突
2、解决冲突
- 使用第一声明者优先原则,谁先定义的就用谁的传递依赖,即在pom.xml文件自上而下,先声明的jar坐标,就先引用该jar的传递依赖。
- 使用路径近者优先原则,即直接依赖级别高于传递依赖。
- 排除依赖,可以使用maven helper插件进行排除。点开pom.xml,切换到Dependency Analyzer视图,选择All Dependencies as Tree,点击要排除的jar,右键会出现Execlude选项
- 版本锁定,使用dependencyManagement 进行版本锁定,dependencyManagement可以统一管理项目的版本号,确保应用的各个项目的依赖和版本一致。
原文链接:Maven依赖冲突以及解决办法
79.Maven依赖scope标签的作用
1、以junit依赖为例,如下依赖代码:
<dependency> <groupId>junit</groupId> <artifactId>junit</artifactId> <version>4.12</version> <scope>test</scope> </dependency>
2、scope标签参数如下:
参考链接:maven依赖scope标签作用
80.git中如何合并分支?
1.确认已经获取了最新的主分支
git fetch master
2.合并
git rebase master
3.把合并后的分支发送到服务器
git push --force
4.加force可强制pust,查看服务器上是否已经获取最新内容
5.git log查看日志,找到commit的hash值
git show <tag>
如:git show 0a4613be12891aaaa6ace7ec071c35cf12e83af7如果是IDEA的话
首先,切换到dev分支下,所以选中local分支中的项目选中它里面的merge,合并完之后,提交到远程仓库即可。
参考链接:Git如何合并分支
81.原子类的底层实现
CAS
CAS(Compare And Swap)是一种有名的无锁算法。CAS算法是乐观锁的一种实现。CAS有3个操作数,内存值V,旧的预期值A,要修改的新值B。当且仅当预期值A和内存值V相同时,将内存值V修改为B并返回true,否则返回false。
核心方法 compareAndSet
82.Spring,SpringMVC,Springboot之间的关系
Spring 是一个“引擎”;
Spring MVC 是基于Spring的一个 MVC 框架;
Spring Boot 是基于Spring4的条件注册的一套快速开发整合包。
83.SpringBoot中condition注解的使用
84.spring-boot-starter原理
spring-boot 在配置上相比spring要简单许多, 其核心在于spring-boot-starter, 在使用spring-boot来搭建一个项目时, 只需要引入官方提供的starter, 就可以直接使用, 免去了各种配置, 原因在于spring-boot的自动发现,比如当classpath下面有tomcat-embedded.jar 时,对应的bean就会被加载.下面会介绍如何自己写一个简单的starter,并在自己的工程中使用这个starter
新建一个maven project结构如下
其中GreetorService
是我们要提供给外部使用的bean,
GreetorProperties
包含了这个bean需要的信息,GreetorAutoConfiguration
负责提供这个bean@SpringBootApplication 包含了@EnableAutoConfiguration, @EnableAutoConfiguration包含了@Import(AutoConfigurationImportSelector.class)
@Import可以接受一个ImportSelector
而AutoAutoConfigurationImportSelector
是ImportSelector
的实现决定了哪些configuration会被import
SpringFactoriesLoader
会从META-INF/spring.factories中读取对应的factory, 所以当我们启动项目时,会检查META-INF/spring.factories key为
org.springframework.boot.autoconfigure.EnableAutoConfiguration
的值.这些autoconfiguration使我们只需引入各个starter就能够快速搭建一个spring-boot app
作者:kokokokokoishi
参考链接:spring-boot-starter简介 - 简书
来源:简书
85.java异常体系
86.说一下非对称加密,公钥密码的传输效率比较低,那种方式可以保证既可以使用到非对称密码也可以使用到对称密码?
SSL/TLS
87.索引失效的几种情况(MySQL)
1、最好全值匹配;
2、最左前缀法则:如果索引了多列,查询从索引的最左前列开始,且不能跳过索引中的列;
3、不在索引列上做任何操作(计算,函数,类型转换),会导致索引时校而转向全表扫描;
4、存储引擎不能使用索引中范围条件右边的列,即范围之后全失效;
5、尽量使用覆盖索引,只访问索引的查询(索引列和查询列一致),减少selec *;
6、MySQL在使用不等于的时候无法使用索引会导致全表扫描;
7、is null,is not null 也无法使用索引;
8、like 以通配符开头(’%aa‘)索引会失效,变成全表扫描;
9、字符串不加单引号,索引失效;
10、少用 or,用它来连接时候会索引失效
88.索引优化
1.索引列上不能使用表达式或函数
2.前缀索引和索引列的选择性
3.联合索引
如何选择索引列的顺序
- 经常会被使用到的列优先
- 选择性高的列优先
4.使用索引优化查询
89.Spring的AOP,IOC实现原理
AOP实现原理:聊聊Spring的AOP实现原理
IOC实现原理: Spring的IOC实现原理
90.JVM垃圾回收机制
JVM垃圾回收机制:JVM垃圾回收机制
91.Java类加载过程
总共分三步:装载(Load),链接(Link),初始化(Initialize)
装载:查找并加载类的二进制数据(查找和导入Class文件)
链接:
- 验证:确保被加载的类的正确性(文件格式验证,元数据验证,字节码验证,符号引用验证)
- 准备:为类的静态变量分配内存,并将其初始化为默认值
- 解析:把类中的符号引用转换为直接引用
初始化:对类的静态变量,静态代码块执行初始化操作
类什么时候才被初始化:
1)创建类的实例,也就是new一个对象
2)访问某个类或接口的静态变量,或者对该静态变量赋值
3)调用类的静态方法
4)反射(Class.forName("com.lyj.load"))
5)初始化一个类的子类(会首先初始化子类的父类)
6)JVM启动时标明的启动类,即文件名和类名相同的那个类 只有这6中情况才会导致类的类的初始化。
类的初始化步骤 / JVM初始化步骤:
1)如果这个类还没有被加载和链接,那先进行加载和链接
2)假如这个类存在直接父类,并且这个类还没有被初始化(注意:在一个类加载器中,类只能初始化一次),那就初始化直接的父类(不适用于接口)
3 ) 假如类中存在初始化语句(如static变量和static块),那就依次执行这些初始化语句。
父类静态变量、静态代码块 --> 子类静态变量、静态代码块 --> 父类成员变量、普通代码块--> 父类构造方法-- --> 子类成员变量、普通代码块--> 子类构造方法
92.说一下Mysql的锁
乐观锁
乐观锁, 顾名思义,就是很乐观,每次去拿数据的时候都认为别人不会修改,所以不会上锁,但是在更新的时候会判断一下在此期间别人有没有去更新这个数据,可以使用版本号等机制。乐观锁适用于多读的应用类型,这样可以提高吞吐量,但不能解决脏读的问题。
悲观锁
悲观锁,顾名思义,就是很悲观,每次去拿数据的时候都认为别人会修改,所以每次在拿数据的时候都会上锁,这样别人想拿这个数据就会block直到它拿到锁。传统的关系型数据库里边就用到了很多这种锁机制,比如行锁,表锁等,读锁,写锁等,都是在做操作之前先上锁。
行级锁
行级锁是Mysql中锁定粒度最细的一种锁,表示只针对当前操作的行进行加锁。行级锁能大大减少数据库操作的冲突。其加锁粒度最小,但加锁的开销也最大。行级锁分为共享锁 和 排他锁。
特点:开销大,加锁慢;会出现死锁;锁定粒度最小,发生锁冲突的概率最低,并发度也最高。
表级锁
表级锁是MySQL中锁定粒度最大的一种锁,表示对当前操作的整张表加锁,它实现简单,资源消耗较少,被大部分MySQL引擎支持。最常使用的MYISAM与INNODB都支持表级锁定。表级锁定分为表共享读锁(共享锁)与表独占写锁(排他锁)。
特点:开销小,加锁快;不会出现死锁;锁定粒度大,发出锁冲突的概率最高,并发度最低。
InnoDB的间隙锁
当我们用范围条件而不是相等条件检索数据,并请求共享或排他锁时,InnoDB会给符合条件的已有数据记录的索引项加锁;对于键值在条件范围内但并不存在的记录,叫做“间隙(GAP)”,InnoDB也会对这个“间隙”加锁,这种锁机制就是所谓的间隙锁(Next-Key锁)。
很显然,在使用范围条件检索并锁定记录时,InnoDB这种加锁机制会阻塞符合条件范围内键值的并发插入,这往往会造成严重的锁等待。因此,在实际应用开发中,尤其是并发插入比较多的应用,我们要尽量优化业务逻辑,尽量使用相等条件来访问更新数据,避免使用范围条件。
InnoDB使用间隙锁的目的:
- 防止幻读,以满足相关隔离级别的要求;
- 满足恢复和复制的需要
共享锁(读锁):其他事务可以读,但不能写。允许一个事务去读一行,阻止其他事务获得相同数据集的排他锁。
排他锁(写锁) :其他事务不能读取,也不能写。允许获得排他锁的事务更新数据,阻止其他事务取得相同数据集的共享读锁和排他写锁。
意向共享锁(IS):事务打算给数据行加行共享锁,事务在给一个数据行加共享锁前必须先取得该表的 IS 锁。
意向排他锁(IX):事务打算给数据行加行排他锁,事务在给一个数据行加排他锁前必须先取得该表的 IX 锁。
93.Mysql的索引
B-Tree 索引:最常见的索引类型,大部分引擎都支持B树索引。
普通索引
这是最基本的索引类型,而且它没有唯一性之类的限制。普通索引可以通过以下几种方式创建:
(1)创建索引: CREATE INDEX 索引名 ON 表名(列名1,列名2,...);
(2)修改表: ALTER TABLE 表名ADD INDEX 索引名 (列名1,列名2,...);
(3)创建表时指定索引:CREATE TABLE 表名 ( [...], INDEX 索引名 (列名1,列名 2,...) );UNIQUE索引
表示唯一的,不允许重复的索引,如果该字段信息保证不会重复例如身份证号用作索引时,可设置为unique:
(1)创建索引:CREATE UNIQUE INDEX 索引名 ON 表名(列的列表);
(2)修改表:ALTER TABLE 表名ADD UNIQUE 索引名 (列的列表);
(3)创建表时指定索引:CREATE TABLE 表名( [...], UNIQUE 索引名 (列的列表) );主键:PRIMARY KEY索引
主键是一种唯一性索引,但它必须指定为“PRIMARY KEY”。
(1)主键一般在创建表的时候指定:“CREATE TABLE 表名( [...], PRIMARY KEY (列的列表) ); ”。
(2)但是,我们也可以通过修改表的方式加入主键:“ALTER TABLE 表名ADD PRIMARY KEY (列的列表); ”。
每个表只能有一个主键。 (主键相当于聚合索引,是查找最快的索引)注:不能用CREATE INDEX语句创建PRIMARY KEY索引
HASH 索引:只有Memory引擎支持,使用场景简单。
R-Tree 索引(空间索引):空间索引是MyISAM的一种特殊索引类型,主要用于地理空间数据类型。
Full-text (全文索引):全文索引也是MyISAM的一种特殊索引类型,主要用于全文索引,InnoDB从MYSQL5.6版本提供对全文索引的支持。
BTree索引和哈希索引的区别
Hash索引的查询效率要远高于B-Tree索引,但是Hash索引本身由于其特殊性也带来了很多限制和弊端
- Hash索引仅仅能满足"=","IN"和"<=>"查询,不能使用范围查询。哈希索引只支持等值比较查询,包括=、 IN 、<=> (注意<>和<=>是不同的操作)。 也不支持任何范围查询,例如WHERE price > 100。
由于Hash索引比较的是进行Hash运算之后的Hash值,所以它只能用于等值的过滤,不能用于基于范围的过滤,因为经过相应的Hash算法处理之后的Hash值的大小关系,并不能保证和Hash运算前完全一样。- Hash索引无法被用来避免数据的排序操作。
由于Hash索引中存放的是经过Hash计算之后的Hash值,而且Hash值的大小关系并不一定和Hash运算前的键值完全一样,所以数据库无法利用索引的数据来避免任何排序运算;- Hash索引不能利用部分索引键查询。
对于组合索引,Hash索引在计算Hash值的时候是组合索引键合并后再一起计算Hash值,而不是单独计算Hash值,所以通过组合索引的前面一个或几个索引键进行查询的时候,Hash索引也无法被利用。- Hash索引在任何时候都不能避免表扫描。
前面已经知道,Hash索引是将索引键通过Hash运算之后,将 Hash运算结果的Hash值和所对应的行指针信息存放于一个Hash表中,由于不同索引键存在相同Hash值,所以即使取满足某个Hash键值的数据的记录条数,也无法从Hash索引中直接完成查询,还是要通过访问表中的实际数据进行相应的比较,并得到相应的结果。- Hash索引遇到大量Hash值相等的情况后性能并不一定就会比BTree索引高。
对于选择性比较低的索引键,如果创建Hash索引,那么将会存在大量记录指针信息存于同一个Hash值相关联。这样要定位某一条记录时就会非常麻烦,会浪费多次表数据的访问,而造成整体性能低下MySql添加索引的五种方法
1.添加primary key(主键索引)
alter table 表名称 add primary key(列名);
2.添加unique(唯一索引)
alter table 表名称 add unique(列名);
3.添加index(普通索引)
alter table 表名称 add index 索引名(index_name) (列名);
4.添加fulltext(全文索引)alter table 表名称 add fulltext (列名);
5.添加多列索引alter table 表名称 add index 索引名(index_name) (列名1,列名2.......);
详解:MYSQL-索引
94.synchronized修饰在静态方法和非静态方法上面的区别
static的方法属于类方法,它属于这个Class(注意:这里的Class不是指Class的某个具体对象),那么static获取到的锁,是属于类的锁。
而非static方法获取到的锁,是属于当前对象的锁。所以,他们之间不会产生互斥。
当我们想让所有这个类下面的方法都同步的时候,也就是让所有这个类下面的静态方法和非静态方法共用同一把锁的时候,我们如何办呢? 此时我们可以使用Lock
95.Executors工厂类可以创建的四种类型的线程池
1. FixedThreadPool
public static ExecutorService newFixedThreadPool(int nThreads){ return new ThreadPoolExecutor(nThreads,nThreads,0L,TimeUnit.MILLISECONDS,new LinkedBlockingQueue<Runnable>()); }
- 它是一种固定大小的线程池;
- corePoolSize和maximunPoolSize都为用户设定的线程数量nThreads;
- keepAliveTime为0,意味着一旦有多余的空闲线程,就会被立即停止掉;但这里keepAliveTime无效;
- 阻塞队列采用了LinkedBlockingQueue,它是一个无界队列;
- 由于阻塞队列是一个无界队列,因此永远不可能拒绝任务;
- 由于采用了无界队列,实际线程数量将永远维持在nThreads,因此maximumPoolSize和keepAliveTime将无效。
2. CachedThreadPool
public static ExecutorService newCachedThreadPool(){ return new ThreadPoolExecutor(0,Integer.MAX_VALUE,60L,TimeUnit.MILLISECONDS,new SynchronousQueue<Runnable>()); }
- 它是一个可以无限扩大的线程池;
- 它比较适合处理执行时间比较小的任务;
- corePoolSize为0,maximumPoolSize为无限大,意味着线程数量可以无限大;
- keepAliveTime为60S,意味着线程空闲时间超过60S就会被杀死;
- 采用SynchronousQueue装等待的任务,这个阻塞队列没有存储空间,这意味着只要有请求到来,就必须要找到一条工作线程处理他,如果当前没有空闲的线程,那么就会再创建一条新的线程。
3. SingleThreadExecutor
public static ExecutorService newSingleThreadExecutor(){ return new ThreadPoolExecutor(1,1,0L,TimeUnit.MILLISECONDS,new LinkedBlockingQueue<Runnable>()); }
- 它只会创建一条工作线程处理任务;
- 采用的阻塞队列为LinkedBlockingQueue;
4. ScheduledThreadPool
它用来处理延时任务或定时任务。
- 它接收SchduledFutureTask类型的任务,有两种提交任务的方式:
- scheduledAtFixedRate
- scheduledWithFixedDelay
- SchduledFutureTask接收的参数:
- time:任务开始的时间
- sequenceNumber:任务的序号
- period:任务执行的时间间隔
- 它采用DelayQueue存储等待的任务
- DelayQueue内部封装了一个PriorityQueue,它会根据time的先后时间排序,若time相同则根据sequenceNumber排序;
- DelayQueue也是一个无界队列;
- 工作线程的执行过程:
- 工作线程会从DelayQueue取已经到期的任务去执行;
- 执行结束后重新设置任务的到期时间,再次放回DelayQueue
参考链接:四种线程池
96.MySQL整个查询执行过程
1、客户端同数据库服务层建立TCP连接。
2、客户端向MySQL服务器发送一条查询请求。
3、连接线程接收到SQL语句之后,将语句交给SQL语句解析模块进行语法分析和语义分析。
4、先看查询缓存中是否有结果,如果有结果可以直接返回给客户端。
5、如果查询缓存中没有结果,就需要真的查询数据库引擎层了,于是发给SQL优化器,进行查询的优化,生成相应的执行计划。
6、MySQL根据执行计划,调用存储引擎的API来执行查询
7、使用存储引擎查询时,先打开表,如果需要的话获取相应的锁。 查询缓存页中有没有相应的数据,如果有则可以直接返回,如果没有就要从磁盘上去读取。
8、当在磁盘中找到相应的数据之后,则会加载到缓存中来,从而使得后面的查询更加高效,由于内存有限,多采用变通的LRU表来管理缓存页,保证缓存的都是经常访问的数据。
9、最后,获取数据后返回给客户端,关闭连接,释放连接线程。
97.LRU算法的实现
LRU(Least Recently Used)是一种常见的页面置换算法,是一种缓存淘汰机制策略。在计算中,所有的文件操作都要放在内存中进行,然而计算机内存大小是固定的,所以我们不可能把所有的文件都加载到内存,因此我们需要制定一种策略对加入到内存中的文件进项选择。
LRU 最近最久未使用算法
LRU原理
LRU的设计原理就是,当数据在最近一段时间经常被访问,那么它在以后也会经常被访问。这就意味着,如果经常访问的数据,我们需要然其能够快速命中,而不常访问的数据,我们在容量超出限制内,要将其淘汰。
每次访问的数据都会放在栈顶,当访问的数据不在内存中,且栈内数据存储满了,我们就要选择移除栈底的元素,因为在栈底部的数据访问的频率是比较低的。所以要将其淘汰。
LRU的实现
我们可以选择双向链表+HashMap
可以用一个特殊的栈来保存当前正在使用的各个页面的页面号。当一个新的进程访问某页面时,便将该页面号压入栈顶,其他的页面号往栈底移,如果内存不够,则将栈底的页面号移除。这样,栈顶始终是最新被访问的页面的编号,而栈底则是最近最久未访问的页面的页面号。
Java中的LinkedHashmap对哈希链表已经有了很好实现了,需要注意的是,这段不是线程安全的,要想做到线程安全,需要加上synchronized修饰符。
整体的设计思路是,可以使用 HashMap 存储 key,这样可以做到 save 和 get key的时间都是 O(1),而 HashMap 的 Value 指向双向链表实现的 LRU 的 Node 节点,如图所示。
首先预先设置 LRU 的容量,如果存储满了,可以通过 O(1) 的时间淘汰掉双向链表的尾部,每次新增和访问数据,都可以通过 O(1)的效率把新的节点增加到对头,或者把已经存在的节点移动到队头。
总结一下核心操作的步骤:
- save(key, value),首先在 HashMap 找到 Key 对应的节点,如果节点存在,更新节点的值,并把这个节点移动队头。如果不存在,需要构造新的节点,并且尝试把节点塞到队头,如果LRU空间不足,则通过 tail 淘汰掉队尾的节点,同时在 HashMap 中移除 Key。
- get(key),通过 HashMap 找到 LRU 链表节点,把节点插入到队头,返回缓存的值。
参考链接:全面讲解LRU算法
98.LFU算法的实现
LFU算法:least frequently used,最近最不经常使用算法
对于每个条目,维护其使用次数cnt、最近使用时间time。
cache容量为n,即最多存储n个条目。
那么当我需要插入新条目并且cache已经满了的时候,需要删除一个之前的条目。删除的策略是:优先删除使用次数cnt最小的那个条目,因为它最近最不经常使用,所以删除它。如果使用次数cnt最小值为min_cnt,这个min_cnt对应的条目有多个,那么在这些条目中删除最近使用时间time最早的那个条目(举个栗子:a资源和b资源都使用了两次,但a资源在5s的时候最后一次使用,b资源在7s的时候最后一次使用,那么删除a,因为b资源更晚被使用,所以b资源相比a资源来说,更有理由继续被使用,即时间局部性原理)。
类似lru算法的想法,利用哈希表+链表。
链表是负责按时间先后排序的。哈希表是负责O(1)时间查找key对应节点的。
引用力扣的一张图片
lfu算法是按照两个维度:引用计数、最近使用时间来排序的。所以一个链表肯定不够用了。解决办法就是按照下图这样,使用第二个哈希表,key是引用计数,value是一个链表,存储使用次数为当前key的所有节点。该链表中的所有节点按照最近使用时间排序,最近使用的在链表头部,最晚使用的在尾部。这样我们可以完成O(1)时间查找key对应节点(通过第一个哈希表);O(1)时间删除、更改某节点(通过第二个哈希表)。
注意:get(查询)操作和put(插入)操作都算“使用”,都会增加引用计数。
所以get(key)操作实现思路:如果第一个哈希表中能查到key,那么取得相应链表节点。接下来在第二个哈希表中,把它移到其引用计数+1位置的链表头部,并删除之前的节点。
put(key,value)操作实现思路:如果第一个哈希表中能查找key,那么操作和get(key)一样,只是把新节点的value置为新value。
如果查不到key,那么我们有可能需要删除cache中的某一项(容量已经达到限制):直接找到第二个哈希表中最小引用计数的链表,删除其末尾节点(最晚使用),之后再添加新节点即可。
注意点:
1.容量超限需要删除节点时,删除了第二个哈希表中的项的同时,第一个哈希表中对应的映射也应该删掉。
2.需要保持一个min_cnt整型变量用来保存当前的最小引用计数。因为容量超限需要删除节点时,我们需要O(1)时间找到需要删除的节点。
实现代码:
import java.util.HashMap; import java.util.Map; public class LFUCache { /** * key 就是题目中的 key * value 是结点类 */ private Map<Integer, ListNode> map; /** * 访问次数哈希表,使用 ListNode[] 也可以,不过要占用很多空间 */ private Map<Integer, DoubleLinkedList> frequentMap; /** * 外部传入的容量大小 */ private Integer capacity; /** * 全局最高访问次数,删除最少使用访问次数的结点时会用到 */ private Integer minFrequent = 1; public LFUCache(int capacity) { // 显式设置哈希表的长度 = capacity 和加载因子 = 1 是为了防止哈希表扩容带来的性能消耗 // 这一步操作在理论上的可行之处待讨论,实验下来效果是比较好的 map = new HashMap<>(capacity, 1); frequentMap = new HashMap<>(); this.capacity = capacity; } /** * get 一次操作,访问次数就增加 1; * 从原来的链表调整到访问次数更高的链表的表头 * * @param key * @return */ public int get(int key) { // 测试测出来的,capacity 可能传 0 if (capacity == 0) { return -1; } if (map.containsKey(key)) { // 获得结点类 ListNode listNode = removeListNode(key); // 挂接到新的访问次数的双向链表的头部 int frequent = listNode.frequent; addListNode2Head(frequent, listNode); return listNode.value; } else { return -1; } } /** * @param key * @param value */ public void put(int key, int value) { if (capacity == 0) { return; } // 如果 key 存在,就更新访问次数 + 1,更新值 if (map.containsKey(key)) { ListNode listNode = removeListNode(key); // 更新 value listNode.value = value; int frequent = listNode.frequent; addListNode2Head(frequent, listNode); return; } // 如果 key 不存在 // 1、如果满了,先删除访问次数最小的的末尾结点,再删除 map 里对应的 key if (map.size() == capacity) { // 1、从双链表里删除结点 DoubleLinkedList doubleLinkedList = frequentMap.get(minFrequent); ListNode removeNode = doubleLinkedList.removeTail(); // 2、删除 map 里对应的 key map.remove(removeNode.key); } // 2、再创建新结点放在访问次数为 1 的双向链表的前面 ListNode newListNode = new ListNode(key, value); addListNode2Head(1, newListNode); map.put(key, newListNode); // 【注意】因为这个结点是刚刚创建的,最少访问次数一定为 1 this.minFrequent = 1; } // 以下部分主要是结点类和双向链表的操作 /** * 结点类,是双向链表的组成部分 */ private class ListNode { private int key; private int value; private int frequent = 1; private ListNode pre; private ListNode next; public ListNode() { } public ListNode(int key, int value) { this.key = key; this.value = value; } } /** * 双向链表 */ private class DoubleLinkedList { /** * 虚拟头结点,它无前驱结点 */ private ListNode dummyHead; /** * 虚拟尾结点,它无后继结点 */ private ListNode dummyTail; /** * 当前双向链表的有效结点数 */ private int count; public DoubleLinkedList() { // 虚拟头尾结点赋值多少无所谓 this.dummyHead = new ListNode(-1, -1); this.dummyTail = new ListNode(-2, -2); dummyHead.next = dummyTail; dummyTail.pre = dummyHead; count = 0; } /** * 把一个结点类添加到双向链表的开头(头部是最新使用数据) * * @param addNode */ public void addNode2Head(ListNode addNode) { ListNode oldHead = dummyHead.next; // 两侧结点指向它 dummyHead.next = addNode; oldHead.pre = addNode; // 它的前驱和后继指向两侧结点 addNode.pre = dummyHead; addNode.next = oldHead; count++; } /** * 把双向链表的末尾结点删除(尾部是最旧的数据,在缓存满的时候淘汰) * * @return */ public ListNode removeTail() { ListNode oldTail = dummyTail.pre; ListNode newTail = oldTail.pre; // 两侧结点建立连接 newTail.next = dummyTail; dummyTail.pre = newTail; // 它的两个属性切断连接 oldTail.pre = null; oldTail.next = null; // 重要:删除一个结点,当前双向链表的结点个数少 1 count--; // 维护 return oldTail; } } /** * 将原来访问次数的结点,从双向链表里脱离出来 * * @param key * @return */ private ListNode removeListNode(int key) { // 获得结点类 ListNode deleteNode = map.get(key); ListNode preNode = deleteNode.pre; ListNode nextNode = deleteNode.next; // 两侧结点建立连接 preNode.next = nextNode; nextNode.pre = preNode; // 删除去原来两侧结点的连接 deleteNode.pre = null; deleteNode.next = null; // 维护双链表结点数 frequentMap.get(deleteNode.frequent).count--; // 【注意】维护 minFrequent // 如果当前结点正好在最小访问次数的链表上,并且移除以后结点数为 0,最小访问次数需要加 1 if (deleteNode.frequent == minFrequent && frequentMap.get(deleteNode.frequent).count == 0) { // 这一步需要仔细想一下,经过测试是正确的 minFrequent++; } // 访问次数加 1 deleteNode.frequent++; return deleteNode; } /** * 把结点放在对应访问次数的双向链表的头部 * * @param frequent * @param addNode */ private void addListNode2Head(int frequent, ListNode addNode) { DoubleLinkedList doubleLinkedList; // 如果不存在,就初始化 if (frequentMap.containsKey(frequent)) { doubleLinkedList = frequentMap.get(frequent); } else { doubleLinkedList = new DoubleLinkedList(); } // 添加到 DoubleLinkedList 的表头 doubleLinkedList.addNode2Head(addNode); frequentMap.put(frequent, doubleLinkedList); } public static void main(String[] args) { LFUCache cache = new LFUCache(2); cache.put(1, 1); cache.put(2, 2); System.out.println(cache.map.keySet()); int res1 = cache.get(1); System.out.println(res1); cache.put(3, 3); System.out.println(cache.map.keySet()); int res2 = cache.get(2); System.out.println(res2); int res3 = cache.get(3); System.out.println(res3); cache.put(4, 4); System.out.println(cache.map.keySet()); int res4 = cache.get(1); System.out.println(res4); int res5 = cache.get(3); System.out.println(res5); int res6 = cache.get(4); System.out.println(res6); } } 作者:liweiwei1419 链接:https://leetcode-cn.com/problems/lfu-cache/solution/ha-xi-biao-shuang-xiang-lian-biao-java-by-liweiwei/ 来源:力扣(LeetCode)
99.什么是缓存污染?如何解决缓存污染问题
在一些场景下,有些数据被访问的次数非常少,甚至只会被访问一次。当这些数据服务完访问请求后,如果还继续留存在缓存中的话,就只会白白占用缓存空间。这种情况,就是缓存污染。
在明确知道数据被再次访问的情况下,volatile-ttl 可以有效避免缓存污染。
例如,业务部门知道数据被访问的时长就是一个小时,并把数据的过期时间设置为一个小时后。这样一来,被淘汰的数据的确是不会再被访问了。
使用 LRU 策略在处理扫描式单次查询操作(指应用对大量的数据进行一次全体读取,每个数据都会被读取,而且只会被读取一次。)时,无法解决缓存污染。
LFU可以解决缓存污染问题
LFU 缓存策略是在 LRU 策略基础上,为每个数据增加了一个计数器,来统计这个数据的访问次数。当使用 LFU 策略筛选淘汰数据时,首先会根据数据的访问次数进行筛选,把访问次数最低的数据淘汰出缓存。如果两个数据的访问次数相同,LFU 策略再比较这两个数据的访问时效性,把距离上一次访问时间更久的数据淘汰出缓存。
100.LRU和LFU的关系
LRU不能解决缓存污染,LFU可以解决缓存污染
LRU策略更加关注数据的 时效性,
LFU策略更加关注数据的 访问频次。
通常情况下,实际应用的负载具有较好的时间局部性,所以LRU策略的应用会更加广泛。
在扫描式查询的应用场景中,LFU策略可以很好地应对缓存污染问题。
101.HashMap为什么会出现死循环
因为JDK1.7HashMap采用的是头插法,在并发的情况,发生扩容时,可能会产生循环链表,在执行get的时候,会触发死循环,引起CPU的100%问题,所以一定要避免在并发环境下使用HashMap,并发环境下要使用ConcurrentHashmap。
102.HTTP是有状态的吗?如何让HTTP有状态?
HTTP是一种无状态协议,即服务器不保留与客户交易时的任何状态。
如何让它有状态?
使用回话跟踪技术,Cookie,Session。
Cookie 和 Session 的区别
- Cookie 数据存放在客户的浏览器上,Session 数据放在服务器上;
- Cookie 不是很安全,别人可以分析存放在本地的COOKIE并进行COOKIE欺骗,考虑到安全应当使用 Session ;
- Session 会在一定时间内保存在服务器上。当访问增多,会比较占用你服务器的性能。考虑到减轻服务器性能方面,应当使用COOKIE;
- 单个Cookie 在客户端的限制是3K,就是说一个站点在客户端存放的COOKIE不能超过3K;
- Cookie 机制是通过检查客户身上的“通行证”来确定客户身份
- Session机制就是通过检查服务器上的“客户明细表”来确认客户身份
- Session 相当于程序在服务器上建立的一份客户档案,客户来访的时候只需要查询客户档案表就可以了。
Cookie 和 Session 的方案虽然分别属于客户端和服务端,但是服务端的 Session 的实现对客户端的 Cookie 有依赖关系的,上面我讲到服务端执行 Session 机制时候会生成 Session 的 id 值,这个 id 值会发送给客户端,客户端每次请求都会把这个 id 值放到 http 请求的头部发送给服务端,而这个 id 值在客户端会保存下来,保存的容器就是 Cookie,因此当我们完全禁掉浏览器的Cookie的时候,服务端的Session也会不能正常使用。
当我们关闭浏览器的时候Session其实并没有挂掉,而是关闭浏览器会清除回话Cookie,
回话Cookie里保存的有Sessionid,回话Cookie没了,相当于Sessionid没了,
找不到对应的session了。
Cookie 和 Session 应用场景:
(1)登录网站,今输入用户名密码登录了,第二天再打开很多情况下就直接打开了。这个时候用到的一个机制就是cookie。
(2)session一个场景是购物车,添加了商品之后客户端处可以知道添加了哪些商品,而服务器端如何判别呢,所以也需要存储一些信息就用到了session。
参考链接:HTTP协议为什么是无状态的?如何让HTTP“有状态”?_H_Expect的博客-CSDN博客_http为什么是无状态的
103.MySQL索引失效的情况
1.如果条件中有or,即使其中有条件带索引也不会使用(这也是为什么尽量少用or的原因)
要想使用or,又想让索引生效,只能将or条件中的每个列都加上索引
2.对于多列索引,不是使用的第一部分,则不会使用索引
3.like查询以%开头
4.如果列类型是字符串,那一定要在条件中将数据使用引号引用起来,否则不使用索引
5.如果mysql估计使用全表扫描要比使用索引快,则不使用索引
104.Mysql回表是怎么回事?
下面我们来假设一种情况,一个表有三个字段 ID ,name ,age,我将ID设置成主键索引,name设成辅助索引。然后来看一下下面的sql:
1.select * from t where id='5';
2.select * from t where name='张三';
第一个sql不用说,直接通过主键索引,从树上直接可以得到结果,
那第二个sql:首先name,mysql并不能得到所有列的信息(也就是*),他只能得到主键ID,然后他会根据ID在进行二次查询,这就引发了--回表问题。这就是为啥不能使用*的原因。
如何解决?
第一不要写*,第二利用组合索引,根据业务实际需要,将需要的字段形成组合索引。
参考链接:mysql索引回表怎么回事_Mysql之索引引发的回表问题_weixin_39618597的博客-CSDN博客
105.分库分表是如何做的,分库的目的是什么,分表你们用的什么方法?
1.垂直分表
把主键和一些列放在一个表,然后把主键和另外的列放在另一个表中
2.水平分表
就是把一个表的数据给弄到的一个数据库的多个表里去,只不过每个表放的数据是不同的,所有表的数据加起来就是全部数据。
需要注意的一点是:分表仅仅是解决了单一表数据过大的问题,但由于表的数据还是在同一台机器上,其实对于提升MySQL并发能力没有什么意义,所以 水平拆分最好分库 。
3.水平分库
就是把一个表的数据给弄到多个库里去,但是每个库的表结构都一样,只不过每个库表放的数据是不同的,所有库表的数据加起来就是全部数据。将数据均匀放更多的库里,然后用多个库来抗更高的并发,还有就是用多个库的存储容量来进行扩容。
分库分表使用的中间件
sharding-jdbc和mycat
垂直拆分,你可以在表层面来做,对一些字段特别多的表做一下拆分;
水平拆分,你可以说是并发承载不了,或者是数据量太大,容量承载不了,你给拆了,按什么字段来拆,你自己想好;
分表,你考虑一下,你如果哪怕是拆到每个库里去,并发和容量都ok了,但是每个库的表还是太大了,那么你就分表,将这个表分开,保证每个表的数据量并不是很大。
两种分库分表的方式
一种是按照range来分,就是每个库一段连续的数据,这个一般是按比如时间范围来的,但是这种一般较少用,因为很容易产生热点问题,大量的流量都打在最新的数据上了;或者是按照某个字段hash一下均匀分散,这个较为常用。
- range来分,好处在于说,后面扩容的时候,就很容易,因为你只要预备好,给每个月都准备一个库就可以了,到了一个新的月份的时候,自然而然,就会写新的库了;缺点,但是大部分的请求,都是访问最新的数据。实际生产用range,要看场景,你的用户不是仅仅访问最新的数据,而是均匀的访问现在的数据以及历史的数据
- hash分法,好处在于说,可以平均分配没给库的数据量和请求压力;坏处在于说扩容起来比较麻烦,会有一个数据迁移的这么一个过程。
参考链接:如何进行分库分表 - 简书
106.Mysql的RR级别下真的可以解决幻读吗?
其实,传统的rr隔离级别下,是存在幻读问题的,但mysql 下真的会有幻读问题吗?先说明一下我的结论:
mysql在rr隔离级别下一定程度上解决了幻读问题。 由于innodb引擎下存在当前读和快照读的概念。
在当前读的情况下,mysql通过配置可以采用记录锁(Record Lock)+间隙锁(Gap Lock),让其他插入或删除事务死锁,达到解决幻读问题。
在快照读的情况下,mysql如果不更新插入记录,那么由于是读取的旧版本记录,对于其他事务插入数据不可见,从而达到了解决幻读问题。但是如果当前事务更新记录(包括不可见的),会去读取最新版本内容,从而看到了其他事务插入的数据,产生幻读。
什么是当前读
以下操作就是采用当前读,会读取当前数据的最新记录,同时加锁
select ... lock in share mode select ... for update insert update delete
什么是快照读
最普通的查询操作,查询的时候不包括lock in share mode跟 for updateselect * from test
107.HTTP请求包含哪几部分
1.请求报文(请求行/请求头/请求数据/空行)
请求行
求方法字段、URL字段和HTTP协议版本
例如:GET /index.html HTTP/1.1
get方法将数据拼接在url后面,传递参数受限
请求方法:
GET、POST、HEAD、PUT、DELETE、OPTIONS、TRACE、CONNECT
请求头(key value形式)
User-Agent:产生请求的浏览器类型。
Accept:客户端可识别的内容类型列表。
Accept-Charset:能接收的字符集。
Accept-Encoding:申明自己接收的编码方法,是否支持压缩,支持什么压缩方法
Accept-Language:申明自己接收的语言。
Host:主机地址
请求数据
post方法中,会把数据以key value形式发送请求
空行
发送回车符和换行符,通知服务器以下不再有请求头
2.响应报文(状态行、消息报头、响应正文)
状态行
消息报头
响应正文
108.volatile,static,final底层实现原理
volatile
禁止指令重排序,保证可见性,用到内存屏障
详细的看博客文章
static
Java的内存包含了栈、堆和静态存储区,栈主要是存放对象的应用,堆是放对象,而静态存储区主要是放一些方法和常量。
静态变量是存储在静态存储区上面,不论建立多少个对象,都只指向这个一个地方。在类加载的时候,静态存储区中的内容已经初始化完成,所以在内存当中就只有这一份。
final
final关键字,实际的含义就一句话,不可改变。什么是不可改变?就是初始化完成之后就不能再做任何的修改,修饰成员变量的时候,成员变量变成一个常数;修饰方法的时候,方法不允许被重写;修饰类的时候,类不允许被继承;修饰参数列表的时候,入参的对象也是不可以改变。这个就是不可变,无论是引用新的对象,重写还是继承,都是改变的方法,而final就是把这个变更的路给堵死。
参考链接:01.Java关键字与底层原理-static关键字 - 简书
修饰的final变量会被放到常量池中。
109.AQS获取锁和释放锁的过程
获取锁
1、首先线程尝试获取锁,如果成功则直接返回,不成功则新建一个Node节点并添加到CLH队列中。tryAcquire尝试获取锁,addWaiter则新建节点并添加到CLH队列中。
2、acquireQueued主要功能是根据该节点寻找CLH队列的头结点,并且尝试获取锁,判断是否需要挂起,并且返回挂起标识。在acquireQueued()内部仍然调用tryAcquire()来获取锁。
释放锁
1.尝试释放锁
2.寻找继任节点
3.唤醒继任节点
110.Mysql通过什么方式实现主从复制
依靠二进制 日志
111.CMS垃圾回收器工作过程
112.MySQL的联合索引(a,b,c,)ac能用到索引吗?
可以用到
联合索引在innodb底层的存储和排序方式:数据首先在a字段的维度进行排序,然后在b字段维度排序,最后是c字段,从左向右依次类推,最终存放在b+树里。如果搜索条件是a,c的话,虽然c在结果区间是无序的(中间有一层b),但是单单是用a字段就可以把结果区间圈定在一个很小的范围内,肯定是比全表扫描需要遍历的字段要少对吧。
网上去查资料很多都是a,c不支持的回答,应该是早期的innodb版本没有把优化做到这一步
参考文章:有mysql联合索引(A, B, C),那么AC查询会用到索引吗? - Cgj20060102030405 - 博客园
113.线程互斥四种方式
1. 临界区(Critical Section):适合一个进程内的多线程访问公共区域或代码段时使用
2. 互斥量 (Mutex):适合不同进程内多线程访问公共区域或代码段时使用,与临界区相似。
3. 事件(Event):通过线程间触发事件实现同步互斥
4. 信号量(Semaphore):与临界区和互斥量不同,可以实现多个线程同时访问公共区域数据,原理与操作系统中PV操作类似,先设置一个访问公共区域的线程最大连接数,每有一个线程访问共享区资源数就减一,直到资源数小于等于零。
114.ClassNotFoundException异常排查
(1)配置文件中根本就不存在该类的配置或者配置的路径写错;(比较常见)
(2)配置文件中存在,但是项目中类写错了名字;
(3)类放错了文件夹;
115.依赖注入的优势
1.查找定位操作与应用代码完全无关
2.不依赖于容器的API,可以很容易的在任何容器以外使用对象
3.不需要特殊的接口,绝大多数对象可以做到完全不必依赖容器。
让容器全权负责依赖查询,受管组件只需要暴露JavaBean的setter方法或者带参数的构造器
或者接口,使容器可以在初始化时组装对象的依赖关系。
让使用者不需要自己去创建或获取自己的依赖,既然创建或获取的过程不是使用者控制的,这也就意味着,当我需要切换依赖时,不需要改变使用者的代码。
116.Redis为什么这么快
1、完全基于内存,绝大部分请求是纯粹的内存操作,非常快速。数据存在内存中,类似于HashMap,HashMap的优势就是查找和操作的时间复杂度都是O(1);
2、数据结构简单,对数据操作也简单,Redis中的数据结构是专门进行设计的;
3、采用单线程,避免了不必要的上下文切换和竞争条件,也不存在多进程或者多线程导致的切换而消耗 CPU,不用去考虑各种锁的问题,不存在加锁释放锁操作,没有因为可能出现死锁而导致的性能消耗;
4、使用多路I/O复用模型,非阻塞IO;
5、使用底层模型不同,它们之间底层实现方式以及与客户端之间通信的应用协议不一样,Redis直接自己构建了VM 机制 ,因为一般的系统调用系统函数的话,会浪费一定的时间去移动和请求;
117.@RestController和@Controller的区别?详细说一下@ResponseBody
1.使用@Controller 注解,在对应的方法上,视图解析器可以解析return 的jsp,html页面,并且跳转到相应页面,@Controller配合视图解析器InternalResourceViewResolver才可以返回到指定页面。
若返回json等内容到页面,则需要加@ResponseBody注解
2.@RestController注解,相当于@Controller+@ResponseBody两个注解的结合,返回json数据不需要在方法前面加@ResponseBody注解了,但使用@RestController这个注解,配置的视图解析器 InternalResourceViewResolver不起作用,就不能返回jsp,html页面,视图解析器无法解析jsp,html页面,返回的内容就是Return 里的内容。
@ResponseBody的作用其实是将java对象转为json格式的数据
@responseBody注解的作用是将controller的方法返回的对象通过适当的转换器转换为指定的格式之后,写入到response对象的body区,通常用来返回JSON数据或者是XML数据。
注意:在使用此注解之后不会再走视图处理器,而是直接将数据写入到输入流中,他的效果等同于通过response对象输出指定格式的数据。@ResponseBody是作用在方法上的,@ResponseBody 表示该方法的返回结果直接写入 HTTP response body 中,一般在异步获取数据时使用【也就是AJAX】。
注意:在使用 @RequestMapping后,返回值通常解析为跳转路径,但是加上 @ResponseBody 后返回结果不会被解析为跳转路径,而是直接写入 HTTP response body 中。 比如异步获取 json 数据,加上 @ResponseBody 后,会直接返回 json 数据。@RequestBody 将 HTTP 请求正文插入方法中,使用适合的 HttpMessageConverter 将请求体写入某个对象。后台 Controller类中对应的方法: @RequestMapping("/login.do") @ResponseBody public Object login(String name, String password, HttpSession session) { user = userService.checkLogin(name, password); session.setAttribute("user", user); return new JsonResult(user); } @RequestBody是作用在形参列表上,用于将前台发送过来固定格式的数据【xml格式 或者 json等】封装为对应的 JavaBean 对象, 封装时使用到的一个对象是系统默认配置的 HttpMessageConverter进行解析,然后封装到形参上。 如上面的登录后台代码可以改为: @RequestMapping("/login.do") @ResponseBody public Object login(@RequestBody User loginUuser, HttpSession session) { user = userService.checkLogin(loginUser); session.setAttribute("user", user); return new JsonResult(user); }
=========================================================================================
@RequestBody@RequestBody 注解则是将 HTTP 请求正文插入方法中,使用适合的 HttpMessageConverter 将请求体写入某个对象。
作用:1) 该注解用于读取Request请求的body部分数据,使用系统默认配置的HttpMessageConverter进行解析,然后把相应的数据绑定 到要返回的对象上; 2) 再把HttpMessageConverter返回的对象数据绑定到 controller中方法的参数上。
使用时机:
A) GET、POST方式提时, 根据request header Content-Type的值来判断:
application/x-www-form-urlencoded, 可选(即非必须,因为这种情况的数据@RequestParam, @ModelAttribute 也可以处理,当然@RequestBody也能处理); multipart/form-data, 不能处理(即使用@RequestBody不能处理这种格式的数据); 其他格式, 必须(其他格式包括application/json, application/xml等。这些格式的数据,必须使用@RequestBody来处理);
B) PUT方式提交时, 根据request header Content-Type的值来判断:
application/x-www-form-urlencoded, 必须;multipart/form-data, 不能处理;其他格式, 必须;
说明:request的body部分的数据编码格式由header部分的Content-Type指定;
例如:
@RequestMapping(value = "user/login") @ResponseBody // 将ajax(datas)发出的请求写入 User 对象中 public User login(@RequestBody User user) { // 这样就不会再被解析为跳转路径,而是直接将user对象写入 HTTP 响应正文中 return user; }
118.什么是缓存穿透,缓存雪崩,解决缓存穿透的方法,解决缓存雪崩的方法
119.AOP,以及AOP使用到的注解
120.说一下自动装配过程和原理
121.Mybatis的工作流程及原理
工作流程:
1.通过SqlSessionFactoryBuilder创建SqlSessionFactory对象
2.通过SqlSessionFactory创建SqlSession对象
3.通过SqlSession拿到Mapper代理对象
4.通过MapperProxy调用Mapper中增删改查的方法
原理:
Mybatis底层封装了JDBC,使用了动态代理模式。
1.SqlSessionFactoryBuilder (构造器):使用Builder模式根据mybatis-config.xml配置或者代码来生成SqISessionFactory。
2.SqlSessionFactory (工厂接口):使用工厂模式生成SqlSession。
3.SqlSession (会话): 一个既可以发送 SQL 执行返回结果,也可以获取Mapper的接口。
4.SQL Mapper (映射器): 它由一个Java接口和XML文件(或注解)构成,需要给出对应的SQL和映射规则,它负责发送SQL去执行,并返回结果。
5.Executor(执行器)
122.Synchronized的锁升级原理
synchronized 锁升级原理:在锁对象的对象头里面有一个 threadid 字段,在第一次访问的时候 threadid 为空,jvm 让其持有偏向锁,并将 threadid 设置为其线程 id,再次进入的时候会先判断 threadid 是否与其线程 id 一致,如果一致则可以直接使用此对象,如果不一致,则升级偏向锁为轻量级锁,通过自旋循环一定次数来获取锁,执行一定次数之后,如果还没有正常获取到要使用的对象,此时就会把锁从轻量级升级为重量级锁,此过程就构成了 synchronized 锁的升级。
123.Synchronized底层实现原理
Synchronized的语义底层是通过一个monitor(监视器锁)的对象来完成,
每个对象有一个监视器锁(monitor)。每个Synchronized修饰过的代码当它的monitor被占用时就会处于锁定状态并且尝试获取monitor的所有权 ,过程:
1、如果monitor的进入数为0,则该线程进入monitor,然后将进入数设置为1,该线程即为monitor的所有者。
2、如果线程已经占有该monitor,只是重新进入,则进入monitor的进入数加1.
3、如果其他线程已经占用了monitor,则该线程进入阻塞状态,直到monitor的进入数为0,再重新尝试获取monitor的所有权。
synchronized是可以通过 反汇编指令 javap命令,查看相应的字节码文件。
124.在监视器(Monitor)内部,是如何做线程同步的?程序应该做哪种级别的同步?
在 java 虚拟机中,监视器和锁在Java虚拟机中是一块使用的。监视器监视一块同步代码块,确保一次只有一个线程执行同步代码块。每一个监视器都和一个对象引用相关联。线程在获取锁之前不允许执行同步代码。
一旦方法或者代码块被 synchronized 修饰,那么这个部分就放入了监视器的监视区域,确保一次只能有一个线程执行该部分的代码,线程在获取锁之前不允许执行该部分的代码
另外 java 还提供了显式监视器( Lock )和隐式监视器( synchronized )两种锁方案
125.synchronized关键字最主要的三种使用方式:
- 修饰实例方法: 作用于当前对象实例加锁,进入同步代码前要获得当前对象实例的锁
- 修饰静态方法: 也就是给当前类加锁,会作用于类的所有对象实例,因为静态成员不属于任何一个实例对象,是类成员( static 表明这是该类的一个静态资源,不管new了多少个对象,只有一份)。所以如果一个线程A调用一个实例对象的非静态 synchronized 方法,而线程B需要调用这个实例对象所属类的静态 synchronized 方法,是允许的,不会发生互斥现象,因为访问静态 synchronized 方法占用的锁是当前类的锁,而访问非静态 synchronized 方法占用的锁是当前实例对象锁。
- 修饰代码块: 指定加锁对象,对给定对象加锁,进入同步代码库前要获得给定对象的锁。
总结: synchronized 关键字加到 static 静态方法和 synchronized(class)代码块上都是是给 Class 类上锁。synchronized 关键字加到实例方法上是给对象实例上锁。尽量不要使用 synchronized(String a) 因为JVM中,字符串常量池具有缓存功能!
126.JVM空间分配担保机制
在发生Minor GC之前,虚拟机会先检查老年代最大可用的连续空间是否大于新生代所有对象总空间。如果这个条件成立,那么Minor GC可以确保是安全的。如果不成立,则虚拟机会查看HandlerPromotionFailure设置是否允许担保失败。如果允许,那么会继续检查老年代最大可用的连续空间是否大于历次晋升到老年代对象的平均大小。如果大于,将尝试着进行一次Monitor GC,尽管这次GC是有风险的。如果小于,或者HandlerPromotionFailure设置不允许冒险,进行一次Full GC。
127.Spring Boot 中的 starter 到底是什么 ?
这个 Starter 并非什么新的技术点,基本上还是基于 Spring 已有功能来实现的。首先它提供了一个自动化配置类,一般命名为
XXXAutoConfiguration
,在这个配置类中通过条件注解来决定一个配置是否生效(条件注解就是 Spring 中原本就有的),然后它还会提供一系列的默认配置,也允许开发者根据实际情况自定义相关配置,然后通过类型安全的属性(spring.factories)注入将这些配置属性注入进来,新注入的属性会代替掉默认属性。正因为如此,很多第三方框架,我们只需要引入依赖就可以直接使用了。当然,开发者也可以自定义 Starter参考链接:掘金
128.Spring框架中的单例Beans是线程安全的么?
Spring框架并没有对单例bean进⾏任何多线程的封装处理。关于单例bean的线程安全和并发问题需要 开发者⾃⾏去搞定。但实际上,⼤部分的Spring bean并没有可变的状态(⽐如Serview类和DAO类),所 以在某种程度上说Spring的单例bean是线程安全的。如果你的bean有多种状态的话(⽐如 View Model 对象),就需要⾃⾏保证线程安全。
最浅显的解决办法就是将多态bean的作⽤域由“singleton”变更为“prototype”
129.垃圾回收的STW是什么?
Stop一the一World,简称STW,指的是GC事件发生过程中,会产生应用程序的停顿。停顿产生时整个应用程序线程都会被暂停,没有任何响应,有点像卡死的感觉,这个停顿称为STW。
举例:
可达性分析算法中枚举根节点(GC Roots)会导致所有Java执行线程停顿。停顿的原因:
分析工作必须在一个能确保一致性的快照 中进行
一致性指整个分析期间整个执行系统看起来像被冻结在某个时间点上V
如果出现分析过程中对象引用关系还在不断变化,则分析结果的准确性无法保证
特点描述:
- 被STW中断的应用程序线程会在完成GC之后恢复,频繁中断会让用户感觉像是网速不快造成电影卡带一样, 所以我们需要减少STW的发生。
- STW事件和采用哪款GC无关,所有的GC都有这个事件。
- 哪怕是G1也不能完全避免Stop一the一world情况发生,只能说垃圾回收器越来越优秀,回收效率越来越高,尽可能地缩短了暂停时间。
- STW是JVM在后台自动发起和自动完成的。在用户不可见的情况下,把用户正常的工作线程全部停掉。
- 开发中不要采用System.gc();会导致Stop一the一world的发生。
130.LinkedHashMap和HashMap和TreeMap的区别,TreeMap是key有序还是value有序
131.谈谈你对公平锁和非公平锁的理解
非公平锁与公平锁的主要区别是看在加锁时会不会去抢锁;
公平锁:
- 当线程到来时,首先会判断当前线程是否有资格获取锁。如果没有其他线程在等待队列中,则该线程有资格获取锁。
- 如果当前有其他线程在等待队列中,则线程会进入等待队列并等待轮到自己时再获取锁资源。
非公平锁:
- 当线程到来时,先会尝试立即获取锁资源,而不管是否有其他线程在等待队列中。
- 如果获取锁成功,则线程可以继续执行。
- 如果获取锁失败,则线程会进入等待队列并等待轮到自己时再次尝试获取锁。
公平锁就是很公平,争抢锁的几率一样,每个线程会先看等待队列是否为空,若为空,直接获取锁,若不为空,自动排队等候获取锁;非公平锁就是所有的线程都会优先去尝试争抢锁,不会按顺序等待,若抢不到锁,再用类似公平锁的方式获取锁。
只要进入等待队列就没有公平和非公平之分了,之后获取锁的顺序就是队列的出队顺序了。(synchronized是倒序唤醒——唤醒的时候是按阻塞顺序倒序唤醒的,ReentrantLock是顺序唤醒——唤醒的时候是按阻塞顺序唤醒的)
132.ReentrantLock的公平锁和非公平锁实现
公平锁的获取和释放:
可以看到这个过程跟AQS独占锁的获取和释放是一样的,实现公平锁的关键是tryRelease方法的实现:
公平锁实现关键:当有资源时也会判断当前有没有线程在等待,只要有线程在等待不管有没有资源都要排队等待,这就保证了线程完全是按先后顺序执行。
可重入实现细节:同一个线程可以在没有资源情况下无限次获取锁,通过上图可以看到,在state不是0的情况下会判断当前线程是否是持锁线程,是的情况下会改变state的值并return true。此时要注意,同一个线程lock几次,也要unlock几次,不然会死锁。
非公平锁的获取和释放:
非公平锁有两次非公平的方式:1.在刚调lock时会直接CAS尝试获取锁。
2.在nonfairTryAcquire方法中发现有资源的情况下会直接CAS尝试获取锁,而不会管等待队列中有没有其他线程在排队。
公平锁和非公平锁实现细节差异:公平锁在有资源的情况下也会判断等待队列中是否有线程在等待,而非公平锁会有两次非公平获锁的机会,但如果失去这两次机会则也要乖乖的排队等候按时间顺序执行。
133.CMS为什么要重新标记,说一下场景
134.Tomcat和Springboot的关系,Tomcat都做了哪些工作,SpringMVC都做了哪些工作。
135.三个线程顺序打印"123"
使用lock的condition方法
136.mysql都用到了那些数据结构
137.orderby排序,没用用到索引的情况下,是把数据在内存排,还是在磁盘排
138.脏读,幻读,不可重复读
139.数据库是如何保证数据一致性的
140.mysql主从复制
141.实现一个线程安全的链表
142.mysql死锁场景
143.什么时候需要破坏双亲委派机制
144.对象都在哪些地方分配内存
堆,元空间,栈上分配
145.类可以被回收吗?
146.枚举类的==的和equals一样吗?
147.线程安全的单例创建的对象唯一,我现在想创建一个另一个对象,怎么做?
可以,不过需要通过反射
148.Integer a=new Integer(1);Integer b=new Integer(1);a==b返回true还是false?如果是Integer a=128;Integer b=128;a==b返回true还是false?
149.在一个方法里拼接动态sql,使用StringBuilder还是StringBuffer更好?
150.ThreadLocal关键字详细说一下
151.Mybatis的工作流程,SQLSessionFactory的作用
152.Redis的持久化方式,默认持久化方式,这两种方式的优缺点比较
153.多态,向上转型,向下转型
154.Synchronized锁静态代码块和非静态代码块的区别?
155.异常体系,编译时异常和运行时异常举几个例子
156.Java反射
157.Java泛型的理解,泛型属不属于多态的一种实现
158.new string(abc)创建了几个对象
下面代码中创建了几个对象?
new String("abc");
答案众说纷纭,有说创建了1个对象,也有说创建了2个对象。答案对,也不对,关键是要学到问题底层的原理。
底层原理分析在上篇文章《面试题系列第1篇:说说==和equals的区别?你的回答可能是错误的》中我们已经提到,String的两种初始化形式是有本质区别的。
String str1 = "abc"; // 在常量池中 String str2 = new String("abc"); // 在堆上
当直接赋值时,字符串“abc”会被存储在常量池中,只有1份,此时的赋值操作等于是创建0个或1个对象。如果常量池中已经存在了“abc”,那么不会再创建对象,直接将引用赋值给str1;如果常量池中没有“abc”,那么创建一个对象,并将引用赋值给str1。
那么,通过new String("abc");的形式又是如何呢?答案是1个或2个。
当JVM遇到上述代码时,会先检索常量池中是否存在“abc”,如果不存在“abc”这个字符串,则会先在常量池中创建这个一个字符串。然后再执行new操作,会在堆内存中创建一个存储“abc”的String对象,对象的引用赋值给str2。此过程创建了2个对象。
当然,如果检索常量池时发现已经存在了对应的字符串,那么只会在堆内创建一个新的String对象,此过程只创建了1个对象。
在上述过程中检查常量池是否有相同Unicode的字符串常量时,使用的方法便是String中的intern()方法。
public native String intern();
下面通过一个简单的示意图看一下String在内存中的两种存储模式。
上面的示意图我们可以看到在堆内创建的String对象的char value[]属性指向了常量池中的char value[]。
还是上面的示例,如果我们通过debug模式也能够看到String的char value[]的引用地址。
图中两个String对象的value值的引用均为{char[3]@1355},也就是说,虽然是两个对象,但它们的value值均指向常量池中的同一个地址。当然,大家还可以拿一个复杂对象(Person)的字符串属性(name)相同时的debug结果进行比对,结果是一样的。
深入问法如果面试官说程序的代码只有下面一行,那么会创建几个对象?
new String("abc");
答案是2个?
还真不一定。之所以单独列出这个问题是想提醒大家一点:没有直接的赋值操作(str="abc"),并不代表常量池中没有“abc”这个字符串。也就是说衡量创建几个对象、常量池中是否有对应的字符串,不仅仅由你是否创建决定,还要看程序启动时其他类中是否包含该字符串。
升级加码以下实例我们暂且不考虑常量池中是否已经存在对应字符串的问题,假设都不存在对应的字符串。
以下代码会创建几个对象:
String str = "abc" + "def";
上面的问题涉及到字符串常量重载“+”的问题,当一个字符串由多个字符串常量拼接成一个字符串时,它自己也肯定是字符串常量。字符串常量的“+”号连接Java虚拟机会在程序编译期将其优化为连接后的值。
就上面的示例而言,在编译时已经被合并成“abcdef”字符串,因此,只会创建1个对象。并没有创建临时字符串对象abc和def,这样减轻了垃圾收集器的压力。
我们通过javap查看class文件可以看到如下内容。
很明显,字节码中只有拼接好的abcdef。
针对上面的问题,我们再次升级一下,下面的代码会创建几个对象?
String str = "abc" + new String("def");
创建了4个,5个,还是6个对象?
4个对象的说法:常量池中分别有“abc”和“def”,堆中对象new String("def")和“abcdef”。
这种说法对吗?不完全对,如果说上述代码创建了几个字符串对象,那么可以说是正确的。但上述的代码Java虚拟机在编译的时候同样会优化,会创建一个StringBuilder来进行字符串的拼接,实际效果类似:
String s = new String("def"); new StringBuilder().append("abc").append(s).toString();
很显然,多出了一个StringBuilder对象,那就应该是5个对象。
那么创建6个对象是怎么回事呢?有同学可能会想了,StringBuilder最后toString()之后的“abcdef”难道不在常量池存一份吗?
这个还真没有存,我们来看一下这段代码:
@Test public void testString3() { String s1 = "abc"; String s2 = new String("def"); String s3 = s1 + s2; String s4 = "abcdef"; System.out.println(s3==s4); // false }
按照上面的分析,如果s1+s2的结果在常量池中存了一份,那么s3中的value引用应该和s4中value的引用是一样的才对。下面我们看一下debug的效果。
很明显,s3和s4的值相同,但value值的地址并不相同。即便是将s3和s4的位置调整一下,效果也一样。s4很明确是存在于常量池中,那么s3对应的值存储在哪里呢?很显然是在堆对象中。
我们来看一下StringBuilder的toString()方法是如何将拼接的结果转化为字符串的:
很显然,在toString方法中又新创建了一个String对象,而该String对象传递数组的构造方法来创建的:
public String(char value[], int offset, int count)
也就是说,String对象的value值直接指向了一个已经存在的数组,而并没有指向常量池中的字符串。
因此,上面的准确回答应该是创建了4个字符串对象和1个StringBuilder对象。
小结我们通过一行创建字符串的代码逐步分析String对象的整个构建及拼接过程,了解了底层实现原理。是不是很有意思?当你掌握了这些底层基本知识,即便面试题的形式如何变化,你必定能一眼识破真相。
本文首发来自微信公众号:程序新视界。一个软实力、硬技术同步学习的平台。
原文链接:new string(abc)创建了几个对象_面试题系列第2篇:new String()创建几个对象?有你不知道的..._田小圣的博客-CSDN博客
159.父类的构造方法是否可以被子类重写?请说明原因
不能,因为父类的构造方法和子类的构造方法名字不可能一样,
不存在重写,只能用super()调用父类的构造方法
160.如果有两个类A、B(注意不是接口),你想同时使用这两个类的功能,那么你会如何编写这个C类呢?
我不想,哈哈哈
1.因为类A、B不是接口,所以是不可以直接实现的,但可以将A、B类定义成父子类,那么C类就能实现A、B类的功能了。假如A为B的父类,B为C的父类,此时C就能使用A、B的功能。
2.写一个内部类,C继承A,内部类继承B
161.谈谈你对抽象类和接口的理解
定义抽象类的目的是提供可由其子类共享的一般形式、子类可以根据自身需要扩展抽象类、抽象类不能实例化、抽象方法没有函数体、抽象方法必须在子类中给出具体实现。他使用extends来继承。
接口:一个接口允许一个类从几个接口继承而来,Java 程序一次只能继承一个类但可以实现几个接口,接口不能有任何具体的方法,接口也可用来定义可由类使用的一组常量。其实现方式是interface来实现。
162.对象的分配策略,栈上分配与TLAB
Java自动内存管理概述
Java所支持的自动内存管理针对的是对象内存的自动分配和回收,原因如下:
1、在Java的内存区域中,本地方法栈、虚拟机栈、程序计数器这三块内存区域的分配和回收具有确定性,他们在编译阶段就能确定需要分配的空间大小。此外,这些内存区域属于线程私有,随线程生而生,随线程灭而灭。综上,虚拟机不需要在这部分内存区域花费太多精力用于垃圾回收。
2、方法区存储的是类信息、静态变量、常量、即时编译器编译过的代码,这部分数据的回收条件较为苛刻,垃圾回收的“成绩”并不是那么令人满意,因此不是垃圾收集器需要重点关注的区域。
3、Java堆存储所有线程的对象,这些对象内存空间的分配是在程序运行期间才进行的,因此具有不确定性。此外,对象的生命周期长短不一,为了提高垃圾收集的效率,需要针对不同生命周期的对象设置不同的垃圾收集算法,这也就增加了内存管理的复杂度。
对象分配策略
对象优先在 Eden 区分配大多数情况下,对象在新生代 Eden 区中分配。当 Eden 区没有足够空间分配时,虚拟机将发起一次 Minor GC。
大对象直接进入老年代大对象就是指需要大量连续内存空间的 Java 对象,最典型的大对象便是那种很长的字符串,或者元素数量很庞大的数组。
大对象对虚拟机的内存分配来说就是一个不折不扣的坏消息,比遇到一个大对象更加坏的消息就是遇到“朝生夕灭”的“短命大对象”,我们写程序的时候应注意避免。
在 Java 虚拟机中要避免大对象的原因是,在分配空间时,它容易导致内存明明还有不少空间时就提前触发垃圾收集,以获取足够的连续空间才能安置好它们。而当复制对象时,大对象就意味着高额的内存复制开销。
HotSpot 虚拟机提供了-XX:PretenureSizeThreshold 参数,指定大于该设置值的对象直接在老年代分配,这样做的目的就是避免在 Eden 区及两个 Survivor区之间来回复制,产生大量的内存复制操作。
这样做的目的:1.避免大量内存复制,
2.避免提前进行垃圾回收,明明内存有空间进行分配。
PretenureSizeThreshold 参数只对 Serial 和 ParNew 两款收集器有效。-XX:PretenureSizeThreshold=4m
长期存活对象进入老年区HotSpot 虚拟机中多数收集器都采用了分代收集来管理堆内存,那内存回收时就必须能决策哪些存活对象应当放在新生代,哪些存活对象放在老年代中。为做到这点,虚拟机给每个对象定义了一个对象年龄(Age)计数器,存储在对象头中。如果对象在 Eden 出生并经过第一次 Minor GC 后仍然存活,并且能被 Survivor 容纳的话,将被移动到 Survivor 空间中,并将对象年龄设为 1,对象在 Survivor区中每熬过一次 Minor GC,年龄就增加 1,当它的年龄增加到一定程度(并发的垃圾回收器默认为 15),CMS 是 6 时,就会被晋升到老年代中。
-XX:MaxTenuringThreshold 调整。
空间分配担保在发生 MinorGC 之前,虚拟机会先检查老年代最大可用的连续空间是否大于新生代所有对象总空间,如果这个条件成立,那么 MinorGC 可以确保是安全 的。如果不成立,则虚拟机会查看 HandlePromotionFailure 设置值是否允许担保失败。
如果允许,那么会继续检查老年代最大可用的连续空间是否大于历次晋升到老年代对象的平均大小,如果大于,将尝试着进行一次 MinorGC,尽管这次 MinorGC 是有风险的(因为判断的是平均大小,有可能这次的晋升对象比平均值大很多),如果担保失败则会进行一次 FullGC;如果小于,或者 HandlePromotionFailure 设置不允许冒险,那这时也要改为进行一次 FullGC。
BUT
在《深入理解Java虚拟机》书中,有这么一句话:“对于大多数应用来说,Java堆是Java虚拟机所管理的内存中最大的一块Java堆是被所有线程共享的一块内存区域,在虚拟机启动时创建。此内存区域的唯一目的就是存放对象实例,几乎所有的对象实例都在堆上分配”。这里没有说所有的对象都在堆上进行分配,而是使用了“几乎所有”一词进行描述,那么今天就来简单聊一聊,除了堆以外的对象分配。
对Java对象分配的过程进行了分析,分析后可知为了解决线程安全问题并且提高效率,有另外两个地方也是可以存放对象的这两个地方分别是栈和TLAB。
栈上分配再问一个问题:
如果确定一个对象的作用域不会逃逸出方法之外,那可不可以将这个对象分配在栈上?这样的话,对象所占用的内存空间就可以随着栈帧的出栈而销毁。而且,在一般应用中,不会逃逸的局部对象所占的比例很大,所以如果能使用栈上分配,那大量的对象就会随着方法的结束而自动销毁了,无须通过垃圾收集器回收,可以还可以减小垃圾收集器的负载。
分析完以后给出栈上分配官方定义:JVM允许将线程私有的对象打散分配在栈上,而不是分配在堆上。分配在栈上的好处是可以在函数调用结束后自行销毁,而不需要垃圾回收器的介入,从而提高系统性能。栈上分配只是JVM虚拟机提供的一种优化技术,对象主要还是分配在堆上的
逃逸分析栈上分配也是有前提的,并不是所有的对象都可以栈上分配,首先需要进行逃逸分析的,所以逃逸分析是栈上分配的技术基础那什么是逃逸分析呢?逃逸分析是指判断对象的作用域是否有可能逃逸出函数体,关于具体的逃逸分析算法和技术此篇不讨论Java SE 6u23版本之后,HotSpot中默认就开启了逃逸分析,可以通过选项-XX:+PrintEscapeAnalysis查看逃逸分析的筛选结果。
如果是逃逸分析出来的对象可以在栈上分配的话,那么该对象的生命周期就跟随线程了,就不需要垃圾回收,如果是频繁的调用此方法则可以得到很大的性能提高。
逃逸分析的几种情况:
public class EscapeAnalysisTest { static V global_v; public void a_method() { V v = b_method(); c_method(); } public V b_method() { V v = new V(); return v; } public void c_method() { global_v = new V(); } }
采用了逃逸分析后,满足逃逸的对象在栈上分配
/** * 逃逸分析-栈上分配 * -XX:-DoEscapeAnalysis */ public class EscapeAnalysisTest { private static class Stu { String a; int b; public Stu(String a, int b) { this.a = a; this.b = b; } } public static void alloc() { Stu stu = new Stu("小明", 22); } public static void main(String[] args) { long b = System.currentTimeMillis(); for (int i = 0; i < 100000000; i++) { alloc(); } long e = System.currentTimeMillis(); System.out.println(e - b); } }
运行结果:没有开启逃逸分析,对象都在堆上分配,会频繁触发垃圾回收(垃圾回收会影响系统性能),导致代码运行慢。
// 1、参数为:-server -Xmx10m -Xms10m -XX:+DoEscapeAnalysis -XX:+PrintGC [GC (Allocation Failure) 2048K->728K(9728K), 0.0017996 secs] [GC (Allocation Failure) 2776K->696K(9728K), 0.0013323 secs] 10 // 2、参数为:-server -Xmx10m -Xms10m -XX:-DoEscapeAnalysis -XX:+PrintGC [GC (Allocation Failure) 2760K->712K(9728K), 0.0004889 secs] ...疯狂GC [GC (Allocation Failure) 2760K->712K(9728K), 0.0004889 secs] [GC (Allocation Failure) 2760K->712K(9728K), 0.0003785 secs] [GC (Allocation Failure) 2760K->712K(9728K), 0.0008545 secs] 1955
总结:
- 栈上分配可以提升代码性能,降低在多线程情况下的锁使用,但是会受限于其空间的大小。
- 进行逃逸分析之后,产生的后果是所有的对象都将由栈上分配,而非从JVM内存模型中的堆来分配。
- 栈上分配可以提升代码性能,降低在多线程情况下的锁使用,但是会受限于其空间的大小。
- 分析找到未逃逸的变量,将变量类的实例化内存直接在栈里分配(无需进入堆),分配完成后,继续在调用栈内执行,最后线程结束,栈空间被回收,局部变量对象也被回收。
- 能在方法内创建对象,就不要再方法外创建对象。
TLAB(线程本地分配缓冲)TLAB的全称是Thread Local Allocation Buffer,即线程本地分配缓存区。
为什么需要TLAB?
创建对象时,需要在堆上为新生的对象申请指定大小的内存,如果同时有大量线程申请内存的话,可以通过锁机制确保不会申请到同一块内存,在JVM运行中,内存分配是一个极其频繁的动作,使用锁这种方式势必会降低性能。
所以就出现了TLAB,JVM通过使用TLAB来避免多线程冲突,每个线程使用自己的TLAB,这样就保证了不使用同步,也不会出现线程安全问题,提高了对象分配的效率。(为新对象分配内存空间时,让每个 Java 应用线程能在使用自己专属的分配指针来分配空间,减少同步开销。)TLAB是什么?
- TLAB本身占用eden区空间,在开启TLAB的情况下,虚拟机会为每个Java线程分配一块TLAB空间。参数-XX:+UseTLAB开启TLAB,默认是开启的。
- TLAB空间的内存非常小,缺省情况下仅占有整个Eden空间的1%,当然可以通过选项-XX:TLABWasteTargetPercent设置TLAB空间所占用Eden空间的百分比大小。
- TLAB只是让每个线程有私有的分配指针,但底下存对象的内存空间还是给所有线程访问的,只是其它线程无法在这个区域分配而已。当一个TLAB用满,就新申请一个TLAB,而在老TLAB里的对象还留在原地什么都不用管——它们无法感知自己是否是曾经从TLAB分配出来的,而只关心自己是在eden里分配的。
- TLAB空间由于比较小,因此很容易装满。比如,一个100K的空间,已经使用了80KB,当需要再分配一个30KB的对象时,肯定就无能为力了。这时虚拟机会有两种选择,第一,废弃当前TLAB,这样就会浪费20KB空间;第二,将这30KB的对象直接分配在堆上,保留当前的TLAB,这样可以希望将来有小于20KB的对象分配请求可以直接使用这块空间。实际上虚拟机内部会维护一个叫作refill_waste的值,通俗一点来说就是可允许浪费空间的值,当TLAB剩余的空间小于新申请对象的大小,且这个剩余的空间大于refill_waste(可允许浪费空间的值)时,会选择在堆中分配(保留当前的TLAB);若剩余的空间小于refill_waste(可允许浪费空间的值)时,则会废弃当前TLAB,新建TLAB来分配对象。这个阈值可以使用TLABRefillWasteFraction来调整,它表示TLAB中允许产生这种浪费的比例。默认值为64,即表示使用约为1/64的TLAB空间作为refill_waste。默认情况下,TLAB和refill_waste都会在运行时不断调整的,使系统的运行状态达到最优。
再举两个通俗易懂的例子帮助理解:大家可以花两分钟时间跟着下边的例子算一下,算完后,对refill_waste会有更到位的理解
假设TLAB大小为100KB,refill_waste(可允许浪费空间的值)为5KB
1、假如当前TLAB已经分配96KB,还剩下4KB,但是现在new了一个对象需要6KB的空间,显然TLAB的内存不够了,这时可以简单的重新申请一个TLAB,原先的TLAB交给Eden管理,这时只浪费4KB的空间,在refill_waste 之内。
2、假如当前TLAB已经分配90KB,还剩下10KB,现在new了一个对象需要11KB,显然TLAB的内存不够了,这时就不能简单的抛弃当前TLAB,因为此时抛弃的话,就会浪费10KB的空间,10KB是大于咱们设置的refill_waste(可允许浪费空间的值)5KB的,所以此时会保留当前的TLAB不动,会把这11KB会被安排到Eden区进行申请。
总结
名称 针对点 处于对象分配流程的位置 栈上分配 避免gc无谓负担 1 TLAB 加速堆上对象的分配 2
对象分配流程图注意:内容仅供学习使用
原文链接:面试官:你了解对象的分配吗?对象的分配策略,栈上分配与TLAB_FMC_WBL的博客-CSDN博客
163.如果让你设计一个缓存,你怎么设计?
1.数据结构
首要考虑的就是数据该如何存储,用什么数据结构存储,最简单的就直接用Map来存储数据;或者复杂的如redis一样提供了多种数据类型哈希,列表,集合,有序集合等,底层使用了双端链表,压缩列表,集合,跳跃表等数据结构;
2.对象上限
因为是本地缓存,内存有上限,所以一般都会指定缓存对象的数量比如1024,当达到某个上限后需要有某种策略去删除多余的数据;
3.清除策略上面说到当达到对象上限之后需要有清除策略,常见的比如有LRU(最近最少使用)、FIFO(先进先出)、LFU(最近最不常用)、SOFT(软引用)、WEAK(弱引用)等策略;
4.过期时间除了使用清除策略,一般本地缓存也会有一个过期时间设置,比如redis可以给每个key设置一个过期时间,这样当达到过期时间之后直接删除,采用清除策略+过期时间双重保证;
5.线程安全像redis是直接使用单线程处理,所以就不存在线程安全问题;而我们现在提供的本地缓存往往是可以多个线程同时访问的,所以线程安全是不容忽视的问题;并且线程安全问题是不应该抛给使用者去保证;
提供一个傻瓜式的对外接口是很有必要的,对使用者来说使用此缓存不是一种负担而是一种享受;提供常用的get,put,remove,clear,getSize方法即可;
这个其实不是必须的,是否需要将缓存数据持久化看需求;本地缓存如ehcache是支持持久化的,而guava是没有持久化功能的;分布式缓存如redis是有持久化功能的,memcached是没有持久化功能的;
8.阻塞机制在看Mybatis源码的时候,二级缓存提供了一个blocking标识,表示当在缓存中找不到元素时,它设置对缓存键的锁定;这样其他线程将等待此元素被填充,而不是命中数据库。
164.生产者消费者生产和消费速率不同,你会怎么解决?除了消息队列呢,还有什么方法呢?
165.5M的内存,从大量数据中找TopK,你怎么找?
XXXXX中找出最大的一个,最小的一个,最大的几个,最小的几个
。这类的就可以使用分治法+最小堆/最大堆
秒之。
166.udp怎么设计才能变成可靠的呢?tcp为什么是可靠吗
确认机制
UDP要想可靠,就要接收方收到UDP之后回复个确认包,
超时重传
发送方有个机制,收不到确认包就要重新发送,每个包有递增的序号,接收方发现中间丢了包就要发重传请求,。
滑动窗口
当网络太差时候频繁丢包,防止越丢包越重传的恶性循环,要有个发送窗口的限制,发送窗口的大小根据网络传输情况调整,调整算法要有一定自适应性
tcp为什么可靠
- 校验和:发送的数据包的二进制相加然后取反,目的是检测数据在传输过程中的任何变化。如果收到段的检验和有差错,TCP将丢弃这个报文段和不确认收到此报文段。
- 确认应答+序列号(累计确认+seq):接收方收到报文就会确认(累积确认:对所有按序接收的数据的确认),TCP给发送的每一个包进行编号,接收方对数据包进行排序,把有序数据传送给应用层。
- 超时重传:当TCP发出一个段后,它启动一个定时器,等待目的端确认收到这个报文段。如果不能及时收到一个确认,将重发这个报文段。
- 流量控制:TCP连接的每一方都有固定大小的缓冲空间,TCP的接收端只允许发送端发送接收端缓冲区能接纳的数据。当接收方来不及处理发送方的数据,能提示发送方降低发送的速率,防止包丢失。TCP使用的流量控制协议是可变大小的滑动窗口协议。
- 拥塞控制:当网络拥塞时,减少数据的发送。发送方有拥塞窗口,发送数据前比对接收方发过来的即使窗口,取小慢启动、拥塞避免、拥塞发送、快速恢复
167.开闭原则
软件中的对象(类,模块,函数等)应该对于扩展是开放的,对于修改是关闭的。英文全称(Open Close Principle),简称:OCP
168.新new的对象放在哪里
->堆内存是用来存放由new创建的对象和数组,即动态申请的内存都存放在堆内存
-->栈内存是用来存放在函数中定义的一些基本类型的变量和对象的引用变量
169.哪些东西放在栈区
寄存器:最快的存储区, 由编译器根据需求进行分配,我们在程序中无法控制;
栈:存放基本类型 的变量数据和对象的引用,但对象本身不存放在栈中,而是存放在堆(new出来的对象)或者常量池中(字符串常量对象存放的常量池中),局部变量【注意:(方法中的局部变量使用final修饰后,放在堆中,而不是栈中)】
堆:存放使用new创建的对象,全局变量
静态域:存放静态成员(static定义的);
常量池:字符串常量和基本类型常量(public static final)。有时,在嵌入式系统中,常量本身会和其他部分分割离开(由于版权等其他原因),所以在这种情况下,可以选择将其放在ROM中 ;
非RAM存储:硬盘等永久存储空间
170.重载和重写的区别?
重载:发生在一个类里面,方法名相同,参数列表(参数类型和个数)不同,返回值类型可以不同(混淆点:跟返回类型没关系)
//不构成重载 public double add(int a,int b) public int add(int a,int b)
重写:发生在父类子类之间的,方法名相同,参数列表相同,返回值类型相同
171.JDK动态代理和cglib动态代理的区别
原理区别:
java动态代理是利用反射机制生成一个实现代理接口的匿名类,在调用具体方法前调用InvokeHandler来处理。
而cglib动态代理是利用asm开源包,对代理对象类的class文件加载进来,通过修改其字节码生成子类来处理。
1、如果目标对象实现了接口,默认情况下会采用JDK的动态代理实现AOP
2、如果目标对象实现了接口,可以强制使用CGLIB实现AOP3、如果目标对象没有实现了接口,必须采用CGLIB库,spring会自动在JDK动态代理和CGLIB之间转换
如何强制使用CGLIB实现AOP?
(1)添加CGLIB库,SPRING_HOME/cglib/*.jar
(2)在spring配置文件中加入<aop:aspectj-autoproxy proxy-target-class="true"/>
JDK动态代理和CGLIB字节码生成的区别?1.JDK动态代理是面向接口的。
2.CGLib动态代理是通过字节码底层继承要代理类来实现,因此如果被代理类被final关键字所修饰,会失败。
使用注意:
如果要被代理的对象是个实现类,那么Spring会使用JDK动态代理来完成操作(Spirng默认采用JDK动态代理实现机制);
如果要被代理的对象不是个实现类那么,Spring会强制使用CGLib来实现动态代理。
在1.6和1.7的时候,JDK动态代理的速度要比CGLib动态代理的速度要慢,但是并没有教科书上的10倍差距,在JDK1.8的时候,JDK动态代理的速度已经比CGLib动态代理的速度快很多了,但是JDK动态代理和CGLIB动态代理的适用场景还是不一样的
172.装箱和拆箱
1 //自动装箱 2 Integer total = 99; 3 4 //自动拆箱 5 int totalprim = total;
装箱就是自动将基本数据类型转换为包装器类型;
拆箱就是自动将包装器类型转换为基本数据类型。
Integer 在-127~128范围,是直接从int数组里边去取,如果超出范围,new一个Integer对象。
173.Java 异步实现的几种方式
174.Mysql悲观锁,select for update是锁表还是锁行?
看情况
要看是不是用了索引/主键。
没用索引/主键的话就是表锁,否则就是是行锁。
175.AOF和RDB的优缺点
176.说一下Spring的事务
177.Spring解析URL的过程
178.mysql的binlog,redolog,undolog
179.非递归方法解决二叉树后续遍历
180.幻读和不可重复读的区别
181.可重入锁的作用,可重入锁的使用场景
防止递归调用时形成死锁
场景:add操作将会获取锁,若一个事务当中多次add,就应该允许该线程多次进入该临界区。synchronized锁也是个可重入锁,比如一个类当中的两个非静态方法都被synchronized修饰,则线程在获取synchronized锁访问一个方法时是可以进入另一个synchronized方法的(PS:应该也能进入static方法的synchronized修饰临界区的,因为是两把不同的锁,表现的不是可重入的特性)
用户名和密码保存在本地txt文件中,则登录验证方法和更新密码方法都应该被加synchronized,那么当更新密码的时候需要验证密码的合法性,所以需要调用验证方法,此时是可以调用的。
参考链接:java的可重入锁用在哪些场合? - 知乎
182.如何破坏双亲委派模型
重写ClassLoader类的loadClass()方法,加载(Object)交由我们自定义的类加载器加载。
183.Redis是单线程的吗?
Redis并不是单线程的,它实际上是一种多线程的实现方式。Redis内部采用了多个线程来处理不同的任务,例如接受连接、处理命令、持久化数据等。但是,在处理命令时,Redis是单线程的。这是因为Redis的主要瓶颈在于CPU的计算能力,而不是I/O的处理能力。因此,通过单线程处理命令可以避免线程切换带来的开销,同时也可以避免多线程之间的竞争和同步问题,从而提高了Redis的性能和吞吐量。 需要注意的是,虽然Redis是单线程的,但是它是通过事件驱动来处理命令的。当有新的命令请求到达时,Redis会将其封装成事件,并将事件放入事件队列中。然后,Redis会从事件队列中取出事件,并处理该事件。因此,虽然Redis是单线程的,但是它可以处理多个命令请求,并且可以同时处理多个客户端的请求。同时,Redis还采用了一些技术来优化性能,例如使用多个IO线程来处理网络I/O操作,使用异步IO来进行数据持久化等。
184.如何解决分表时针对特定场景出现的数据倾斜问题
1.哈希取模法
可以在查询某个表时,通过哈希函数计算出该表的物理位置,从而实现对该表的访问。哈希函数的设计需要能够保证数据均匀分布到各个表中,从而避免数据倾斜问题。
2.分桶法
可以将数据分成多个桶,每个桶对应一个表。在查询时,先根据一定的规则将数据分到对应的桶中,再对桶中的数据进行查询。通过合理的分桶规则,可以将数据均匀地分布在各个表中,从而避免数据倾斜问题。
3.动态调整
如果出现了数据倾斜问题,可以通过动态调整分表策略来解决。例如,可以将某个表中的数据分到多个表中,从而减轻该表的负载。或者可以将某个表中的数据合并到其他表中,从而降低表的数量,提高查询效率。
4.使用分布式数据库
如果数据量非常大,单机无法处理,可以考虑使用分布式数据库。分布式数据库可以将数据分散在多个节点上,从而提高数据处理能力和可用性。在分布式数据库中,数据倾斜问题可以通过数据分片和负载均衡等技术来解决。
185.Kafka是什么?
Kafka是一种高吞吐量、分布式、基于发布/订阅的消息系统,最初由LinkedIn公司开发,使用Scala语言编写,目前是Apache的开源项目。
- broker: Kafka服务器,负责消息存储和转发
- topic:消息类别,Kafka按照topic来分类消息
- partition: topic的分区,一个topic可以包含多个partition, topic 消息保存在各个partition上
- offset:消息在日志中的位置,可以理解是消息在partition上的偏移量,也是代表该消息的唯一序号
- Producer:消息生产者
- Consumer:消息消费者
- Consumer Group:消费者分组,每个Consumer必须属于一个group
- Zookeeper:保存着集群 broker、 topic、 partition等meta 数据;另外,还负责broker故障发现, partition leader选举,负载均衡等功能
186.RocketMQ是什么?
RocketMQ是一款分布式消息中间件,它的工作原理是基于发布-订阅模型或点对点模型,消息生产者将消息发送到消息中间件中,消息中间件将消息存储起来,并将消息发送给消息消费者进行处理。 RocketMQ的架构由四个主要组件组成:NameServer、Broker、Producer和Consumer。
- NameServer组件
- RocketMQ集群中的NameServer组件负责维护Broker的路由信息,即记录Broker的名称、IP地址及端口等信息,并将这些信息提供给Producer和Consumer使用。一个RocketMQ集群中可以有多个NameServer实例,它们之间相互独立,没有任何关系,不同的Producer和Consumer也可以连接到不同的NameServer。
- Broker组件
- RocketMQ中的消息存储和传输都由Broker组件来完成。一个Broker可以存储多个Topic的消息,一个Topic可以由多个Broker来存储。每个Broker之间通过主从复制的方式来保证数据的可靠性和高可用性。
- Producer组件
- Producer组件负责创建和发送消息,将消息发送到Broker中存储。在发送消息之前,Producer需要先连接到NameServer,获取到Broker的路由信息,然后才能向Broker发送消息。
- Consumer组件
- Consumer组件负责订阅和消费消息,从Broker中拉取消息进行处理。Consumer需要先连接到NameServer,获取到Broker的路由信息,然后才能从Broker中拉取消息。
RocketMQ的消息传递过程分为三个阶段:
- Producer将消息发送到Broker
- Producer首先连接到NameServer,获取到Broker的路由信息,然后将消息发送到对应的Broker。
- Broker存储消息并将消息发送给Consumer
- Broker接收到消息后,先将消息存储起来,并将消息发送给订阅该Topic的所有Consumer。
- Consumer消费消息
- Consumer从Broker中拉取消息进行处理,处理完毕后向Broker发送确认消息,告诉Broker该消息已经被成功消费。
总的来说,RocketMQ的工作原理就是通过NameServer、Broker、Producer和Consumer这四个组件来实现消息的传递和存储,其中NameServer用于记录Broker的路由信息,Broker用于存储和传输消息,Producer用于创建和发送消息,Consumer用于订阅和消费消息。通过这些组件的协同工作,RocketMQ可以实现高可靠、高吞吐、低延迟的消息传递。
187.Kafka和RocketMQ比较
Kafka和RocketMQ都是常用的分布式消息中间件,它们都具有高吞吐量、可靠性强、可扩展性好等优点,但在实际应用中,它们有以下不同点:
- 消息传递模型不同
- Kafka采用的是发布-订阅模型,即消息生产者将消息发布到一个或多个主题(Topic)中,消息消费者订阅这些主题,并从中接收消息。RocketMQ采用的是点对点模型,即消息生产者将消息发送到一个或多个队列(Queue)中,消息消费者从这些队列中接收消息。
- 消息存储方式不同
- Kafka使用的是基于文件的存储方式,即消息以追加的方式写入磁盘文件中,消费者从文件中读取数据。RocketMQ则使用的是基于内存映射的存储方式,即消息存储在内存中,当内存不足时会将部分数据写入磁盘中。
- 性能和扩展性不同
- Kafka的性能和扩展性都非常强,它可以处理每秒数百万条消息的高并发场景,并且可以通过增加节点来实现横向扩展。RocketMQ的性能和扩展性也比较好,但相对于Kafka来说稍逊一些。
- 社区和生态系统不同
- Kafka拥有广泛的社区和完善的生态系统,可以方便地与其他开源项目集成,例如Spark、Flume等。RocketMQ的社区和生态系统相对来说比较小,但也在不断发展壮大。 综上所述,Kafka和RocketMQ都是优秀的分布式消息中间件,选择哪一个取决于具体的应用场景和需求。如果需要高吞吐量和可靠性,同时需要与其他开源项目集成,可以选择Kafka;如果需要点对点模型和更易于部署的方案,可以选择RocketMQ。
整理不易,如果大家感觉有用的话,还请三连一波,多谢!!