可以说,学姐给我的这份文档真的把我的知识查漏补缺,面试问到了好多,值得收藏。
并发编程
一.Executor
为什么使用线程池:手动创建线程耗费性能,不利于管理。
首先创建线程池有两种方式:使用Executors工厂来创建ThreadPoolExecutor这类自定义线程池。
1. 使用Executors工厂来创建
Executors是一个类,用来创建线程池,常用的有四种线程池
1.newFixedThreadPool 创建一个可重复固定的线程数的线程池
2.newCachedThreadPool 创建一个可缓存的线程池,调用execute将重复用以前构造的线程(如果当前线程可用)。如果没有可用线程则创建新的线程并加入到池中。终止并从缓存中移除那些已有60s未被使用的线程。
3.newSingleThreadExecutor创建一个单线程化的线程池,它只会用唯一的工作线程来执行任务,保证所有任务按照指定顺序(FIFO, LIFO, 优先级)执行
4. newScheduledThreadPool 创建一个支持定时及周期性的任务执行的线程池,多数情况下可用来替代Timer类。
使用示例:
2. 使用ThreadPoolExecutor这个类自定义创建线程池
二.ThreadLocal
三.中断线程
1.线程执行完毕会自动结束
2.在线程处于阻塞,期限等待火无期限等待时,调用线程的interrupt()会抛出interruptedException异常从而提前终止线程
3.若没有处于阻塞状态,调用interrupte()将线程标记为中断,此时在调用interrupted()判断线程是否处于中断状态,来提前终止线程。
4.线程池调用shutdown()等待所有线程执行完毕之后关闭
三.Volatile关键字的理解
两层语义:1.保证不同线程对该变量的内存可见线
2.禁止进行指令重排序
内存语义:
读这个变量的时候,JMM会把本地内存中的变量设置为无效,从主内存中取;
写这个变量的时候,JMM会把本地内存变量刷新到主内存当中
实现:添加Volatile关键字会在汇编代码中多处一个kock前指令,也就是内存屏障
四.Synchronized关键字
(1)synchronized是Java中的关键字,是一种同步锁。它修饰的对象有以下几种:
1. 修饰一个代码块,被修饰的代码块称为同步语句块,其作用的范围是大括号{}括起来的代码,作用的对象是调用这个代码块的对象;
2. 修饰一个方法,被修饰的方法称为同步方法,其作用的范围是整个方法,作用的对象是调用这个方法的对象;
3. 修饰一个静态的方法,其作用的范围是整个静态方法,作用的对象是这个类的所有对象;
4. 修饰一个类,其作用的范围是synchronized后面括号括起来的部分,作用主的对象是这个类的所有对象。
(2)Synchronized的作用主要有三个:(1)确保线程互斥的访问同步代码(2)保证共享变量的修改能够及时可见(3)有效解决重排序问题。
(3)Synchronized 原理
在编译的字节码中加入了两条指令来进行代码的同步。
monitorenter :
每个对象有一个监视器锁(monitor)。当monitor被占用时就会处于锁定状态,线程执行monitorenter指令时尝试获取monitor的所有权,过程如下:
1.如果monitor的进入数为0,则该线程进入monitor,然后将进入数设置为1,该线程即为monitor的所有者。
2.如果线程已经占有该monitor,只是重新进入,则进入monitor的进入数加1.
3.如果其他线程已经占用了monitor,则该线程进入阻塞状态,直到monitor的进入数为0,再重新尝试获取monitor的所有权。
monitorexit:
执行monitorexit的线程必须是objectref所对应的monitor的所有者。
指令执行时,monitor的进入数减1,如果减1后进入数为0,那线程退出monitor,不再是这个monitor的所有者。其他被这个monitor阻塞的线程可以尝试去获取这个 monitor 的所有权。
通过这两段描述,我们应该能很清楚的看出Synchronized的实现原理,Synchronized的语义底层是通过一个monitor的对象来完成,其实wait/notify等方法也依赖于monitor对象,这就是为什么只有在同步的块或者方法中才能调用wait/notify等方法,否则会抛出java.lang.IllegalMonitorStateException的异常的原因。
(4)Synchronized的优化
(4)synchronized与Lock的区别
lock是一个类,主要有以下几个方法:
lock():获取锁,如果锁被暂用则一直等待
unlock():释放锁
tryLock(): 注意返回类型是boolean,如果获取锁的时候锁被占用就返回false,否则返回true
tryLock(long time, TimeUnit unit):比起tryLock()就是给了一个时间期限,保证等待参数时间
(1)Lock的加锁和解锁都是由java代码实现的,而synchronize的加锁和解锁的过程是由JVM管理的。
(2)synchronized能锁住类、方法和代码块,而Lock是块范围内的。
(3)Lock能提高多个线程读操作的效率;
(4)Lock:Lock实现和synchronized不一样,后者是一种悲观锁,它胆子很小,它很怕有人和它抢吃的,所以它每次吃东西前都把自己关起来。而Lock底层其实是CAS乐观锁的体现,它无所谓,别人抢了它吃的,它重新去拿吃的就好啦,所以它很乐观。如果面试问起,你就说底层主要靠volatile和CAS操作实现的。
五.CAS机制
Compare And Swap
CAS操作是一个原子操作,由一条CPU指令完成,使用了3个基本操作数,内存地址V,旧的预期值A ,要修改的新值B
需要更新一个变量的时候,只有当内存地址V中的值和预期值A相等的时候才会修改为新值B ,如果失败则自旋,重新尝试
问题:
1.ABA 解决办法,加一个版本号,只有当版本号和预期值都符合的时候才修改
2.不能保证代码块的原子性,CAS机制知识保证一个变量的原子操作
五.JMM的理解
JMM的核心是找到一个平衡点,在保证内存可见性的前提下,尽量的放松对编译器和处理器重排序的限制。
可能会产生缓存一致性的问题:
1.总线锁定
2.缓存一致性协议
三大特性:原子性,可见性,有序性
六.JVM的锁优化
1.自旋锁和自适应自旋锁
线程挂起和恢复都需要很大的性能开销,很多共享数据的锁定状态只持续很短的时间,为这段时间挂起恢复不值得,所以可以让现场进入一个自旋状态,但仍占用cpu的时间,默认是10次,超过10次则采用传统的方式挂起线程。自适应自旋锁会根据上次在这个锁上自旋的时间调整自选次数
2.锁消除
检测到对局部变量加锁,会自动将锁消除,如在一个方法里面有sb = s1 + s2 + s3以前会转化为StringBuffer(线程安全,有加锁)的append操作,优化之后会转换成StringBuilder的sppend操作
3.锁粗化
对一个对象反复加锁解锁,如上述的append方法,编译器会优化,只在最外层加一个锁
4.轻量级锁
对象有一个对象头,对象头分两部分区域,一部分存储子树的hashcode,GC分代年龄,锁标志位等,官方成为mark word,另一部分用于存储指向方法区对象类型的指针;
轻量级锁的执行过程是代码进入同步块的时候,如果当前对象没有被锁定(锁标志为是01)则先在线程的栈帧中建立一个锁记录空间(lock record)将对象的mark word拷贝过来,然后利用cas操作尝试将对象的markword更新为指向lock record,如果成功则当前线程拥有该对象锁,并且将锁标志位从01改为00,如果更新失败则会检查当前对象markword是否指向当前线程的栈帧,如果是则直接进入同步块执行否则说明当前的对象锁已经被其他线程占有。如果有两条以上的线程竞争这个锁,那就会膨胀成重量级锁,锁标志为变成10;
5.偏向锁
锁对象第一次被线程获取的时候,会将当前的锁标志设置为偏向锁01状态,并使用cas操作将当前线程的id记录在markword当中,如果成功那么在以后的操作当中不需进行任何操作就可以进入同步代码块,当有另外一个线程获取当前锁的时候,偏向模式就宣告结束,撤销偏向锁之后恢复到未锁定或者轻量级锁状态
JAVA基础
String
1.string s1 = “aaa” 与 String s1 = new String(aaa);
前者会在Stringpool中创建一个对象,如果Stringpool中已经有了,则直接引用
后者会在Stringpool和堆中分别创建一个对象,如果StringPool中已经有了则只创建一个对象
2.调用s1.integer()方法,可以将字符串放入StringPool中,如果已经存在则直接返回这个对象
继承
1.几种修饰符的访问权限范围
2. == equals hashcode的关系
1.==是比较两个对象的地址是否相等
2.equals默认和 == 一样也是比较地址,也可以根据自己的需求来重写什么叫做相等
3.hashcode 是根据对象的地址来返回一串int型数字,如果对象不一样的话返回的值也不一样
3.为什么重写equals的同时也要重写hashcode
首先必须满足如果equals相同,那么hashcode也必须相同,如果hashcode不相同,那么equals必须不相同的原则,否则会导致该类无法与基于散列值的集合类(hashmap , hashset ,hashtable)结合正常运行
因为对于这些集合在进行重复性判断时,是先用hashcode判断,如果相同在用equals判断
4.hash冲突的解决方法
1.开放地址法(线性探测法,二次探测法)
2.在散列函数法
3.链地址法
5.hashtable与hashmap的区别
1.hashmap线程不安全,hashtable线程安全
2.hashmap最多允许一个键为null,而hashtable不允许
3.hashtable中默认的hash数组大小为11,增加的方式是old*2+1,而hashmap默认是16,肯定是2的指数
4.计算hash的方法不一样,hashtable是直接用hashcode,而hashmap使用了key的高16位和低16位做异或运算
Linkedhashmap继承于hashmap,可以按顺序读取,底层使用的是双向链表
TreeMap实现了sortmap接口,可以对元素进行排序,底层使用的红黑树
7.java BIO NIO AIO 序列化
字节操作:使用inputStream和outPutSream实现,字节是传输和存储的最小单位
字符操作:inputStreamReader:将字节流解码成字符流
OutPutStramWriter:将字符流编码成字节流
对象操作:序列化就是将对象转换成字节序列,方便存储和运输,两个用途:
1.把对象的字节序列永久地保存到硬盘中,通常存放在一个文件里
2.在网络上传送对象的字节序列
在很多应用中,需要对某些对象进行序列化,让他们离开内存空间,入住到物理磁盘方便长期保存,比如session对象,当有大量用户并发访问的时候可能会出现10万个session对象,内存吃不消,此时就需要将这些session先序列化到硬盘中,等要用的时候在把对象还原到内存中。
当两个进程在进行远程通信的时候,彼此可以发送各种类型的数据,无论是何种类型的数据,都会以二进制的序列在网络上传送。发送方需要把这个java对象转换成字节序列,才能在网络上传送;接收方则需要把字节序列恢复成java对象。
Io与NIO的区别:
1.NIO是非阻塞的
2.NIO是面向缓冲区的,IO是面向流的
AIO:异步非阻塞
8.static关键字的作用
一.修饰变量:
1.静态变量在类加载的时候被创建并初始化,只被创建一次(类加载只进行一次),可以修改
2.静态变量属于整个类而不属于某个对象。
二.修饰方法
1.可以通过类名来访问,不需要创建对象来访问,可以用来实现单例模式
2.静态方法只能调用静态方法和静态变量
三.修饰静态代码块
在类加载的时候执行,且只执行一次
9.单例模式
实现:
1.为什么要用判断双重:
因为可能有两个线程都执行完了第一个if语句,如果没有第二重判断,那么当其中有个线程执行完synchronized里面的语句之后,另外一个线程跟着也会执行,这样就达不到单例模式的效果
2.第一重判断去掉也可以实现,为什么不去掉
这个设计性能问题,因为
参考:https://qinjiangbo.com/mechanism-of-double-locking-check.html
10.this与super关键字
9.java中的多态
分为两种:
1.编译时多态:体现在重载(方法名相同而参数不同),在编译时期就根据传入的参数确定好调用哪个方法;
2.运行时多态:体现在方法的重写。在运行时期判断引用类型的实际类型根据实际的类型调用其相应的方法;
当父类对象引用变量引用子类对象时,被引用对象的类型决定了调用谁的成员方法,引用变量类型决定可调用的方法。如果子类中没有覆盖该方法,那么会去父类中寻找。
参考链接:https://www.runoob.com/w3cnote/java-polymorphism.html
10.接口类和抽象类的异同
区别:
1.抽象类可以有非抽象方法,但接口只能有抽象方法
2.接口中的成员变量默认修饰为public static final(高度抽象的模版,所以这些都是提取出来的不变特征),方法默认修饰为public abstract。抽象类中的成员变量可以被不同的修饰符来修饰
3.类可以实现多个接口但只能继承一个抽象类
相同:
1.不能被实例化
2.派生类都必须实现未实现的方法
11.instanceof
用来判断一个对象是否是一个类的实例
12.各种排序算法复杂度及稳定性
1.java底层如何实现排序的
Java中Arrays.sort使用了两种排序方法,快速排序和优化归并排序。
快速排序主要针对基本的数据类型(int short long)排序,而归并排序用于对象类型的排序
如果数据量小于60会使用插入排序,插入排序是稳定的
13.java中的堆和栈
栈:主要用于存储局部变量和对象的引用变量,每个线程都有一个独立的栈空间,所以线程之间是不共享数据的;
栈的特点是存取速度快,但所存数据的大小和生存期必须是确定的(编译后就已经确定大小)
堆:堆中主要存储实例化的对象和数组。线程共享。堆的优势是可以动态的分配内存大小,生存期也不必事先告诉编译器,但缺点是存取速度较慢
Linux基本面试题:
Ls:用于显示指定工作目录下的类容,不会列出详细信息
Ll:会列出当前文件目录的详细信息,含有时间,权限,大小等
Cd:用于切换到目标目录
Mkdir:用于简历当前目录的子目录
rm-r 删除的文件或者目录,需确认
rm –rf同上但无需确认
cp:复制文件 若复制目录则必须加上-r
数据库MySQL
一.索引
1.B树,B+树,以及两者的区别
B树是一种多路平衡查找树,其每一个节点都存储Key和data
B+树是B树的一个变种,叶子节点存储data,非叶子节点只存储key,B+树的叶子节点增加了顺序访问指针,每一个叶子节点都可以访问到他的下一个叶子节点
区别:
1.B+树种只有叶子节点会带有全部信息,非叶子节点只起到索引的作用,二B树的所有节点都带有全部信息,B+树的每一层节点都会再次出现在下一层节点上
2.B+树种所有叶子节点都是通过指针连在一起,B树则没有
2.索引的优点和缺点
优点:可以加大检索速度
缺点:创建和维护索引需要耗费时间
3.Mysql为什么选择B+树
Mysql数据本质上是放在外部存储的,B+树是为了加快读取速度二设计的一种数据结构
1.可以减少i/o次数,只有叶子节点才存储数据,非叶子节点存储索引,这样一次读取到内存的关键字增多,相对i/o次数也就减少(根据区别一)
2.能够提供稳定高效的范围扫描,因为所有的叶子节点都互相连接(根据区别二)
4.索引越多越好吗?
索引可以提高select的效率,但是也降低了insert和updata的效率,因为插入和更新的时候可能会重建索引,索引怎么建索引要慎重考虑。
5.索引分类
1.B+树索引:以b+树作为数据结构的索引
2.hash索引:能以O(1)的时间复杂度查找,但失去了有序性,innodb有一个自适应哈希索引,当这个索引值被频繁使用时会在b+树上创建一个哈希索引
3.全文索引:用于查找文本的关键词,中文需要由中文分词插件
二. MySQL优化
一.MySQL的优化,主要分为索引的的优化,sql语句的优化,表的优化。同时可以使用缓存增加效率
1.索引的优化
只要列中含有null,最好不要再此列设置索引
对于经常在where语句中使用的列,最好设置一个索引
对于like语句,以%或者-开头的不会使用索引,以%结尾会使用索引
二.sql语句的优化
查询优化要尽量避免全表扫描
查询时能不用*就不用*,尽量写字段名
三. MySQL常问问题
1.数据库如何应对大规模的写入和读取
(1)使用NoSQL,通过降低数据的安全性,减少对事物的支持,减少复杂查询的支持来获取性能的提升;但有些场合NoSQL无法满足要求
(2)分库分表:
水平切分:不修改数据库的表结构,通过对表中的数据拆分而达到分片的目的,一般水平切分在查询的时候可能会用到union操作(多个结果并)
可以根据hash或者日期来进行分表
垂直切分:修改表结构,按照访问的差异将某些列拆分出去,一般查询数据的时候可能会用到join操作;把常用的字段放在一个表中,不常用的放在一个表中;把字段比较大的比如text字段拆出来放在一个表中。
分库:分表能够解决数据量过大带来的查询效率下降问题,但是却无法给数据库的并发处理能力带来质的提升;分库可以对关键字取模的方式来对数据访问进行路由;
(3)读写分离:
读写分离是在主服务器上修改数据,数据也会同步到从服务器上,从服务器只能提供读取,不能写入,实现备份的同时也实现了数据库的性能优化
如何保证数据一致性:
(1)主节点
保证事务每次提交之后,要确保binlog都能刷新到磁盘中,只要有了binlog,innoDB就有方法恢复数据,不至于导致主从复制的数据丢失
(2)从节点
开启 relay log 自动修复机制,发生 crash 时,会自动判断哪些 relay log 需要重新从master 上抓取回来再次应用,以此避免部分数据丢失的可能性。
2.数据库事务及其隔离级别
事务的特性:ACID
事务在并发的时候,隔离性很难保证主要可能出现下面这些问题:
脏读:一个事务读了另外一个事务未提交的数据,如果另一个事务回滚则会发生脏读
不可重复读:一个事务前后读取同一行数据,如果在这个过程中有其他事务修改了此数据则会发生不可重复读
幻读:一个事务前后读取范围的时候
事务隔离级别:
MySQL实现事务是基于undo/redo日志实现的:
undo日志记录修改前的状态,ROLLBACK基于UNDO日志实现;
REDO日志记录修改后的状态,事务的持久性基于REDO日志实现
两种解决脏读、不可重复读、幻读的方案:
MVCC(性能较高,但读的可能是历史版本)
1.版本链:对于每一行的数据,在undo日志中,总会记录每个版本记录以及对应的事务id,
2.readView:
核心问题:当前版本链中哪个版本对当前事务可见
Readview包含的内容:{当前活跃的事务id,下一个应该分配的事务id,当前自己的事务id},根据当前读的版本事务id和这个readview对比,如果是活跃的或者大于下一个应该分配的事务id则说明当前版本对此事务不可见,应该前读一个版本,依次类推直到找到可见的版本
提交读:每次读取数据前都会生成一个readview
可重复读:在第一次读取时句时生成一个readview
锁(性能不高,但读的是最新版本):
MyISAM和innoDB的区别
1.innodb支持行锁,myisam不支持行锁
2.innodb支持事务,myisam不支持事务
3.innodb支持回滚和安全回复,myisam不支持
4.innodb的索引就是数据,myisam的索引只存储了主键和行号,还需要根据行号去查找相应的记录
5.innodb更适合写密集的表,myisam更适合读密集的表
计算机网络
1.tcp和udp的区别:
Udp:无连接,尽最大可能交付,没有拥塞控制流量控制
Tcp:面向连接,可靠交付,有拥塞控制和流量控制
2.输入一条url,整个过程:
1.DNS解析,获取ip地址(本机,本地域名服务器,根域名服务器,顶级域名服务器,权限域名服务器)
2.建立TCP连接
3.浏览器发出http请求
4.服务器进行响应
5.TCP连接释放
6.浏览器渲染
3.为什么是三次握手,四次挥手
三次握手:防止之前滞留的连接请求再次到达服务端
四次挥手:因为tcp是全双工模式,客户端停止发送请求之后,服务端也要停止发送请求
4.time_wait存在的原因,时间是多少(两倍的报文最大存活时间)
1.确保客户端发送的最后一个报文能被收到,服务端可以正常关闭。
2.让所有报文都在网络中消失,时间是两倍的最大报文存活时间。
5.tcp的可靠传输靠什么:
超时重传:如果已经发送的报文在超过时间内没有被确认,那么就重新发送这个报文
6.Tcp的滑动窗口
发送方和接收方都有一个滑动窗口
7.TCP流量控制
流量控制是为了控制发送方的发送速率,保证接收方来得及接收
通过滑动窗口来控制,根据报文知道对方窗口的大小,然后根据窗口大小来控制发送速率
8.TCP拥塞控制
如报文过多,会导致超时重传,又会导致网络更加阻塞
1.慢开始和拥塞避免:
慢开始:设置初始的报文数量为1;
拥塞避免:设置一个阈值,当报文数超过这个阈值的时候每次,报文每次加一,如果出现超时领阈值等于当前报文的一半,重新执行慢开始
快重传:如果收到3个确认报文,则重传丢失的那个报文
快恢复:这种情况令阈值等于当前报文的一半,并令当前发送的报文数等于阈值,因为没有出现网络阻塞
http各个版本的区别
http0.9 : 仅支持GET请求,仅能访问html资源
http 1.0 :增加了post和head请求
http1.1 : 增加了长连接,一个tcp连接可以发送多个http请求,新增了put,patch,delete请求
http2.0 : 增加了双工模式,不仅客户端能发送多个请求,服务端也能处理多个请求
Redis
1.redis的数据淘汰策略
当redis内存数据大小达到一定的大小时,就会施行数据淘汰策略,主要有六种策略
2.数据库和缓存的数据一致性
2.1 mySQL里有2000w数据,redis中只存20w的数据,如何保证redis中的数据都是热点数据
根据数据淘汰策略,先算一下这20W的数据大概占多少内存,然后设置redis的内存,启用从所有数据集中挑选最近最少使用的淘汰策略
2.2 redis缓存和mysql数据库同步
3.Redis持久化
1.RDB持久化(redis默认方式)
将某个时间点的所有数据都存在硬盘中,如果发生故障将丢失最后一次创建快照的数据
触发RDB快照的条件:在指定的时间间隔内,执行指定次数的写操作
2.AOF持久化
所执行的每一条指令,都会记录到appendonly.aof文件中,redis会按照策略将指令存入硬盘中。当redis重启的时候会根据日志文件的内容将写指令从前到后执行一次完成数据恢复的功能
Java异常体系
Error:主要是虚拟机产生,与代码编写者无关,例如:outOfMemoryError
Exception:主要有运行时异常和io异常
运行时异常:数组越界,空指针异常
Io异常:找不到文件
发生oom的可能区域:
除了程序计数器都有可能发生
虚拟机栈:当jvm尝试去扩展栈空间失败时可能会抛出oom异常
堆:堆中对象堆积起来无法释放
方法区
四种引用类型:
主要体现在对象不可达性和对垃圾回收的影响:
强引用:只要有强引用指向一个对象,就表面对象还活着,垃圾回收期不会碰这种对象
软引用:只有当jvm认为内存不足的时候才会去试图回收这些对象
弱引用:不能使对象豁免垃圾回收,只是提供一种对象的访问途径
虚引用:不能提供他访问对象,仅仅提供一种确保对象呗finalize之后做某些事情的机制
JAVA虚拟机是如果加载类的
1.加载:主要是用类加载器(启动类加载器,扩展类加载器,应用类加载器)来查找对应类的字节流
双亲委派的好处:由于类随着类加载器一起有一种优先级的层级关系,从而使基础类得到统一
2.链接
验证:确保字节流的安全性,不会对虚拟机造成危害
准备:为类的静态字段分配内存
解析:将符号引用解析成实际引用
3.初始化:调用<clinit>方法,为静态变量赋与实际的值,执行静态代码块
运行时数据区域:
运行时内存区域:
1.程序计数器:记录正在执行虚拟机字节码的指令地址
2.java虚拟机栈:主要存储局部变量表
3.本地方法栈
4.堆:存储对象的地方,有新生代和老年代
5.方法区:存储类信息,常量,静态变量等信息
内存分配策略:
1.对象优先在eden区域分配
2.大对象直接进入老年代
3.长期存活的对象进入老年代:会有一个年龄计数器,达到指定的阈值就会进入老年代
FullGC触发条件:
1.调用system.gc()
2.老年代空间不足
3.minor GC时老年代空间分配担保失败