题目来源于网络
后面是笔者自己提供的答案,仅供参考,如有错误,欢迎指正
一、基础篇 1.1、Java基础
-
面向对象的特征:继承、封装和多态
-
final, finally, finalize 的区别
-
Exception、Error、运行时异常与一般异常有何异同
-
请写出5种常见到的runtime exception
-
int 和 Integer 有什么区别,Integer的值缓存范围
-
包装类,装箱和拆箱
-
String、StringBuilder、StringBuffer
-
重载和重写的区别
-
抽象类和接口有什么区别
-
说说反射的用途及实现
-
说说自定义注解的场景及实现
-
HTTP请求的GET与POST方式的区别
-
Session与Cookie区别
-
列出自己常用的JDK包
-
MVC设计思想
-
equals与==的区别
-
hashCode和equals方法的区别与联系
-
什么是Java序列化和反序列化,如何实现Java序列化?或者请解释Serializable 接口的作用
-
Object类中常见的方法,为什么wait notify会放在Object里边?
-
Java的平台无关性如何体现出来的
-
JDK和JRE的区别
Java 8有哪些新特性
1.2、Java常见集合-
List 和 Set 区别
-
Set和hashCode以及equals方法的联系
-
List 和 Map 区别
-
Arraylist 与 LinkedList 区别
-
ArrayList 与 Vector 区别
-
HashMap 和 Hashtable 的区别
-
HashSet 和 HashMap 区别
-
HashMap 和 ConcurrentHashMap 的区别
-
HashMap 的工作原理及代码实现,什么时候用到红黑树
-
多线程情况下HashMap死循环的问题
-
HashMap出现Hash DOS攻击的问题
-
ConcurrentHashMap 的工作原理及代码实现,如何统计所有的元素个数
-
手写简单的HashMap
-
看过那些Java集合类的源码
1.3、进程和线程
-
线程和进程的概念、并行和并发的概念
-
创建线程的方式及实现
-
进程间通信的方式
-
说说 CountDownLatch、CyclicBarrier 原理和区别
-
说说 Semaphore 原理
-
说说 Exchanger 原理
-
ThreadLocal 原理分析,ThreadLocal为什么会出现OOM,出现的深层次原理
-
讲讲线程池的实现原理
-
线程池的几种实现方式
-
线程的生命周期,状态是如何转移的
可参考:《Java多线程编程核心技术》
1.4、锁机制-
说说线程安全问题,什么是线程安全,如何保证线程安全
-
重入锁的概念,重入锁为什么可以防止死锁
-
产生死锁的四个条件(互斥、请求与保持、不剥夺、循环等待)
-
如何检查死锁(通过jConsole检查死锁)
-
volatile 实现原理(禁止指令重排、刷新内存)
-
synchronized 实现原理(对象监视器)
-
synchronized 与 lock 的区别
-
AQS同步队列
-
CAS无锁的概念、乐观锁和悲观锁
-
常见的原子操作类
-
什么是ABA问题,出现ABA问题JDK是如何解决的
-
乐观锁的业务场景及实现方式
-
Java 8并法包下常见的并发类
-
偏向锁、轻量级锁、重量级锁、自旋锁的概念
可参考:《Java多线程编程核心技术》
1.5、JVM-
JVM运行时内存区域划分
-
内存溢出OOM和堆栈溢出SOE的示例及原因、如何排查与解决
-
如何判断对象是否可以回收或存活
-
常见的GC回收算法及其含义
-
常见的JVM性能监控和故障处理工具类:jps、jstat、jmap、jinfo、jconsole等
-
JVM如何设置参数
-
JVM性能调优
-
类加载器、双亲委派模型、一个类的生命周期、类是如何加载到JVM中的
-
类加载的过程:加载、验证、准备、解析、初始化
-
强引用、软引用、弱引用、虚引用
个人分享答案:
Java基础
1.面向对象的特征:继承、封装和多态
* 继承:A extends B,A则继承了B所有的属性(非private)和方法,A可重写方法,也可扩展自己的属性和方法
* 封装:将数据和基于数据的操作封装在一起,使其构成一个不可分割的对象,数据的操作隐藏在对象内部,只保留一些接口使其与外部发生联系。
封装的优点:良好的封装减少耦合;类内部的结构可以自由修改;隐藏内部信息,实现细节。
* 多态:分为编译时多态和运行时多态。
编译时多态:主要指方法的重载,它根据参数列表的不同来区分不同的函数;
运行时多态:当实现多态的三个条件(继承、重写、向上转型)都具备时,指向子类的父类类型引用在真正执行时会执行子类的方法,这样对外表现为使用相同的类当时表现出不同的结果
2.final, finally, finalize 的区别
* final用来作为类、方法、成员变量的修饰符,表明类是不可继承的、方法和成员变量是不可修改的
* finally用在try...catch..finally里,作为异常捕获里最后执行的代码块,一般用于释放资源、关闭连接
* finalize是定义在Object中的方法,子类可以重写这个方法以整理系统资源或者清理工作,方法在垃圾回收器回收这个对象之前调用
3.Exception、Error、运行时异常与一般异常有何异同
* Exception和Error都继承自Throwable
* Exception是所有异常的父类,运行时异常和一般异常都继承自Exception
* 运行时异常:编译期间不会强制要求捕获,通常是可以通过编码避免的异常
* 可检查异常:在源代码中必须进行显示的捕获处理,这是编译器检查的一部分
* Error表现了java运行时系统的内部错误和资源耗尽错误。大部分错误与编写者执行的操作无关,而表示JVM运行时出现的问题。应用程序不应该抛出这种错误
4.请写出5种常见到的runtime exception
ArrayIndexOutOfBoundsException
NullPointException
ClassNotFoundException
ClassCastException
NoSuchMethodException
5.int 和 Integer 有什么区别,Integer的值缓存范围
* int是基本数据类型
* Integer是对象类型,有一系列的对象方法
int和Integer可互相转换,称为拆箱封箱操作
* Integer缓存值范围为-128~127
6.包装类,装箱和拆箱
* 包装类是相对于基本类型而言的,八中基本类型都有相对应的包装类
* 装箱和拆箱操作是在基本类型和其对象类型之间发生的,是一种自动的行为
Integer i = 10; //装箱操作,自动调用 Integer.valueOf(int val) 方法
i = 10; //装箱操作,自动调用 Integer.valueOf(int val) 方法
int n = i; // 拆箱操作,自动调用Integer.intValue()方法
n = i; // 拆箱操作,自动调用Integer.intValue()方法
7.String、StringBuilder、StringBuffer
* String作为一个final类型的对象,可用来表示字符串值,字符串的拼接实际是一个不断产生新的String对象的过程
* StringBuilder在String的拼接过程中,只产生一个对象
* StringBuffer的方法都是synchronized的,相对于StringBuilder而言是线程安全的
在执行速度方面:StringBuilder > StringBuffer > String
在线程安全方面:StringBuilder非线程安全;StringBuffer线程安全
8.重载和重写的区别
* 重载:一个类的多个方法,方法名相同,而参数不同(参数类型、参数个数不同,返回类型可相同也可不同,参数类型不可作为重载函数的判断标准)
* 重写:发生在父子类之间,子类定义的方法与父类拥有相同的名称和参数,这说明该方法被重写
9.抽象类和接口有什么区别
* 抽象类:以abstract描述的类,一般存在未实现的抽象方法,子类继承抽象类之后,需要实现该抽象方法,如果没有实现,则可以设置子类也为abstract类
* 接口:接口是一种特殊的抽象类,接口里的方法全部为抽象方法,子类实现接口后,需要实现这些方法
应用场景:
* 抽象类不能被实例化,只能用作子类的父类,模板模式一般就使用抽象类来实现
* 如果想实现多重继承,这需要使用接口来实现
10.说说反射的用途及实现
反射作为JDK的一个优秀特性,可作为一种特殊的实现方式来实现对类的操作。
在运行态中,针对于任何一个类,我们能获取这个类的属性和方法,并且能调用对象的方法。
我们常用的Spring框架就使用了反射,通过使用反射来获取类的实例;
实现:获取类的Class对象,可通过Class来获取类的属性、方法,并可通过Class对象来创建实例
11.说说自定义注解的场景及实现
我们在使用框架如Spring、Mybatis时都会用到很多注解,注解可以在类上、方法上、成员变量上。
一般来说,使用注解可以为类添加许多附加功能。
Spring的Controller、Service注解,主要就是让Spring在扫描包下的类的时候,针对于添加了这些注解的类这注册到Spring容器中
12.HTTP请求的GET与POST方式的区别
HTTP的请求类型有很多种方式,常用的就是GET和POST,其他还有PUT/DELETE/HEAD等
GET:一般用于查询操作,参数拼接在URL上
POST:一般用于添加或更新操作,参数可作为http请求的body来放入,
可以从安全性、参数传输长度等方面来分析其区别
更多可参考: https://www.cnblogs.com/logsharing/p/8448446.html
13.Session与Cookie区别
在常规的Web开发中,经常会使用到Session和Cookie
* Session可被看做是一次会话,当session过期或session关闭的时候会话结束,我们在开发中可以使用session传递一些参数
* Cookie是使用在客户端的,相对而言,session是使用在服务器端的。最经典的Cookie使用方式是:在Cookie中存放session_id,将Cookie和Session的联合使用,实现http请求的连续性交易
14.列出自己常用的JDK包
java.lang
java.io
java.nio
java.util
java.util.concurrent
15.MVC设计思想
在Web开发中的一个经典开发思想
model-view-controller
将web项目分成不同的层次,每个层次之间对外提供接口访问,实现一种高内聚低耦合。
主要要体会到MVC的分层思想,在实际的代码开发中需要注意到
16.equals与==的区别
* equals比较的是两个对象的值是否相同
* == 比较的是两个对象的对象内存地址是否相同
17.hashCode和equals方法的区别与联系
hashCode和equals是Object的两个方法,hashCode主要是为了获取对象的哈希值;equals这比较两个对象是否相等(内存地址意义上的相同)
equals方法相等这hashCode一定相等;但是hashCode相等equals方法不一定相等
注意:hashCode和equals方法都是为了比较对象是否相等的,那么为什么需要两个方法了?
equals的比较相对复杂点,而hashCode的比较相对简单,但是equals的比较更严谨,hashCode就不那么严谨了。所以,针对大量的对象比较,如果都用equals比较的话,显然效率比较低,所以,一般先用hashCode进行比较,如果hashCode不同则两个对象肯定不同;如果hashCode相同,则再进行equals比较。
用户一般不需要重写hashCode和equals方法,如果要重写的话,这两个都要重写
更多可参考https://www.cnblogs.com/keyi/p/7119825.html
18.什么是Java序列化和反序列化,如何实现Java序列化?或者请解释Serializable 接口的作用
java序列化:将对象转换为字节流的过程
反序列化:将字节流转换为对象的过程
序列化的用途:将对象的字节序列保存到硬盘上;在网络上传输对象的字节序列。
java实现序列化:对象实现Serializable接口,然后使用流处理操作ObjectInputSteam和ObjectOutputStream来进行序列化操作
19.Object类中常见的方法,为什么wait notify会放在Object里边?
toString()/hashCode()/equals()/clone()
wait方法暂停的是持有对象的锁,所以要调用wait方法必须为object.wait()
同理:notify方法唤醒的是等待锁的对象,
20.Java的平台无关性如何体现出来的
最主要的是JDK的部署,JDK扮演了java程序与所在硬件平台和操作系统之间的缓冲角色。这样java程序只需要与JDK打交道而不用管具体的操作系统
体现在:我们只需要编写java程序,如果需要在不同的平台运行,那么首先在对应平台安装JDK,然后将打包后的应用运行在JDK上即可,与操作系统无关
21.JDK和JRE的区别
JDK包含JRE,JRE是java运行时环境,包含了JVM
JDK不仅仅有JRE,同时还有java源码、编译器javac、java程序调试工具jconsole等,可以进行开发调试工作
22.Java 8有哪些新特性
* lambda
* java.util.Stream提供对元素流进行函数式操作
明细见:https://www.oracle.com/technetwork/cn/java/javase/8-whats-new-2157071-zhs.html
Java常见集合
1.List 和 Set 区别
List和Set同为集合类型,父接口都为Collection
List为有序可重复的
Set为无序不可重复的
2.Set和hashCode以及equals方法的联系
Set实际就是用Map来实现的,使用的是Map的key,value用一个static final类型的Object来代替
Set的结构为数组结构,准确点说是哈希数组,以值的hashCode来确定所在数组的index
Set所存的值是不可重复的,为什么呢?因为Set会使用对象的equals方法进行比较,如果相同,则使用新值替换掉老值
3.List 和 Map 区别
List和Map都是集合类型,List继承了Collection接口,Map没有继承该接口
数据结构不同:List一般是数组或者链表结构;而Map一般是数组+链表的结构或者树结构
4.Arraylist 与 LinkedList 区别
ArrayList底层是数组
LinkedList底层是链表(双端链表)
5.ArrayList 与 Vector 区别
ArrayList和Vector实现的功能基本保持一致
ArrayList非线程安全
Vector的方法都是synchronized,所以线程安全
6.HashMap 和 Hashtable 的区别
HashMap和HashTable实现的功能保持一致
HashMap非线程安全
HashTable线程安全
7.HashSet 和 HashMap 区别
HashSet的实现实际上是基于HashMap的
HashSet使用的就是HashMap的哈希表来实现的,相对于HashMap而言就是value是固定的为一个static final的Object
8.HashMap 和 ConcurrentHashMap 的区别
HashMap是线程非安全的
ConcurrentHashMap是线程安全的,相对于HashTable而言,ConcurrentHashMap实现的方式更优雅,采取的是分段锁的措施
9.HashMap 的工作原理及代码实现,什么时候用到红黑树
HashMap的数据结构是哈希表+链表的格式,使用key.hashCode来确定所在哈希表的位置,然后将value存放在链表中
TreeMap是基于红黑树实现的,可以实现快速查询
10.多线程情况下HashMap死循环的问题
主要发生在HashMap.put()方法时,如果添加当前元素后超过了阈值,则需要扩容,扩容的时候需要将原来的map所有元素都存放到新的map中
如果在多线程操作情况下,则有可能引发环形链接的情况
详细请参考:https://coolshell.cn/articles/9606.html
11.HashMap出现Hash DOS攻击的问题
主要是将json转换为map时候,当json过大且map的key被不停的碰撞(hash collision),导致CPU爆满
具体可参考: https://yq.aliyun.com/articles/92194?utm_campaign=wenzhang&utm_medium=article&utm_source=QQ-qun&201762&utm_content=m_22308
12.ConcurrentHashMap 的工作原理及代码实现,如何统计所有的元素个数
进程和线程
1.线程和进程的概念、并行和并发的概念
* 进程:每一个单独的应用都可以看做一个进程,拥有一个程序运行的入口、顺序执行序列和程序的出口
* 线程:属于进程里的,一个进程可以有多个线程,线程无法独立执行,必须已存在应用程序中
线程开销小,但不利于资源的管理和保护;进程正相反。
进程在执行过程中拥有独立的内存单元;而多个线程共享内存
* 并行:多个任务同时执行
* 并发:也是多任务执行,从CPU的角度来看,实际上只有一个任务在执行
2.创建线程的方式及实现
* new Thread()
* implements Runnable
3.进程间通信的方式
* 无名管道通信(半双工的通信方式,数据只能单向流动,而且只能在具有亲缘关系[如父子进程]的进程间使用)
* 高级管道通信(将另一个程序当做一个新进程在当前进程中启动,则他算是当前进程的子进程)
* 有名管道通信(半双工的通信方式,允许无亲缘关系的进程通信)
* 消息队列通信(消息队列是由消息的链表,存放在内核中并有消息队列标识符标识。消息队列克服了信号传递信息少、管道只能承载无格式字节流以及缓冲区大小受限等缺点)
* 信号量通信(信号量是一个计数器,可以用来控制多个进程对共享资源的访问,它常作为一种锁机制,防止某进程正在访问共享资源时,其他进程也访问该资源。因此主要作为进程间以及同一进程内不同线程之间的同步手段)
* 信号(信号是一种比较复杂的通信方式,用于通知接收进程某个事件已经发生)
* 共享内存通信(共享内存就是映射一段能够被其他进程访问的内存,这段共享内存有一个进程创建,但多个进程都可以访问。共享内存是最快的IPC方式,它是针对其他进程通信方式运行效率低而专门设计的。它往往与其它方式如信号量等配合使用,来实现进程间的同步和通信)
* 套接字通信(套接字也是一种进程通信方式,与其它方式不同的是:它还可以实现不同服务器之间的通信)
4.说说 CountDownLatch、CyclicBarrier 原理和区别
* CountDownLatch闭锁,相当于一扇门,在门打开之前,没有线程能够通过,在达到结束状态时,这扇门会允许所有的线程通过
* CyclicBarrier类似于闭锁,它能阻止一组线程直到某个时间发生
不同点:闭锁做减法,栅栏做加法,闭锁只能使用一次,而栅栏可以使用多次
5.说说 Semaphore 原理
计数信号量,Semaphore管理一系列许可证,每个acquire方法阻塞,直到有一个许可证可以获得然后拿走一个许可证;每个release方法增加一个许可证
Semaphore经常用于限制获取某种资源的线程数量。
Semaphore有两种模式:公平模式和非公平模式;公平模式下调用acquire的顺序就是获取许可证的顺序,遵循FIFO;非公平模式是抢占式的
6.说说 Exchanger 原理
Exchanger是一个用于线程间协作的工具类。用于线程间的数据交换,它提供一个同步点,在这个同步点,两个线程可以交换彼此的数据。他提供一个同步点,在这个同步点,两个线程可以交换彼此的数据。这两个线程通过exchange方法来交换数据,如果第一个线程先执行exchange方法,它会一直等待第二个线程也执行exchange方法,当两个线程达到同步点时,这两个线程就可以交换数据
7.ThreadLocal 原理分析,ThreadLocal为什么会出现OOM,出现的深层次原理
ThreadLocal为每个线程创建一个副本,那么每个线程可以访问自己内部的副本变量。
注意ThreadLocalMap是每个线程所独有的
OOM:ThreadLocal到Entry对象key的引用断裂,而不及时的清理Entry对象,可能造成OOM内存溢出
8.讲讲线程池的实现原理
线程池中的核心线程数,当提交一个任务时,线程池创建一个线程执行任务,直到当前线程数等于corePoolSize;
如果当前线程数为corePoolSize,那么继续提交的任务则保存到阻塞队列中,等待被执行;
如果阻塞队列也满了,那就创建新的线程执行任务,直到当前线程数等于maxPoolSize;
如果这时再有任务,则执行reject处理任务
9.线程池的几种实现方式
* newFixedThreadPool()
* newCachedThreadPool()
* newSingleThreadExecutor()
* newScheduledThreadPool()
拒绝策略:
* AbortPolicy 丢弃任务并抛出RejectException
* DiscardPolicy 丢弃任务,但是不抛出异常
* DiscardOldestPolicy 丢弃最前面的任务,然后尝试执行任务(重复此过程)
* CallerRunsPolicy 由调用线程处理该任务
线程池的状态:
* RUNNING 接受新任务,并处理阻塞队列中的任务
* SHUTDOWN 不会接受新任务,但会处理阻塞队列中的任务
* STOP 不接受新任务,也不会处理阻塞队列中的任务,而且会中断正在运行的任务
* TIDYING 线程池对线程进行整理优化
* TERMINATED 线程池停止工作
向线程池提交任务的两种方式:
Executor.execute(Runnable command);
ExecutorService.submit(Callable task);
* execute()内部实现1.首次通过workCountof()获知当前线程池中的线程数,
如果小于corePoolSize, 就通过addWorker()创建线程并执行该任务;
否则,将该任务放入阻塞队列;
2. 如果能成功将任务放入阻塞队列中,
如果当前线程池是非RUNNING状态,则将该任务从阻塞队列中移除,然后执行reject()处理该任务;
如果当前线程池处于RUNNING状态,则需要再次检查线程池(因为可能在上次检查后,有线程资源被释放),是否有空闲的线程;如果有则执行该任务;
3、如果不能将任务放入阻塞队列中,说明阻塞队列已满;那么将通过addWoker()尝试创建一个新的线程去执行这个任务;如果addWoker()执行失败,说明线程池中线程数达到maxPoolSize,则执行reject()处理任务;
* sumbit()内部实现会将提交的Callable任务会被封装成了一个FutureTask对象
FutureTask类实现了Runnable接口,这样就可以通过Executor.execute()提交FutureTask到线程池中等待被执行,最终执行的是FutureTask的run方法;
比较:
两个方法都可以向线程池提交任务,execute()方法的返回类型是void,它定义在Executor接口中, 而submit()方法可以返回持有计算结果的Future对象,它定义在ExecutorService接口中,它扩展了Executor接口,其它线程池类像ThreadPoolExecutor和ScheduledThreadPoolExecutor都有这些方法。
线程池关闭的两种方式:
* shutdown() 不会立即终止线程池,不接受新的任务,等缓冲池中的任务都执行完毕之后 终止 ,线程池的状态变为SHUT_DOWN
* shutdownNow() 立即终止线程池,不接受新的任务,打断正在执行的任务,并且清空任务缓存队列,返回尚未执行的任务,线程池的状态变为STOP
来源: https://www.cnblogs.com/zhaojinxin/p/6668247.html
10.线程的生命周期,状态是如何转移的
生命周期:new/runnable/running/blocked/dead
1.说说线程安全问题,什么是线程安全,如何保证线程安全
什么是线程安全:当多个线程操作一个对象时,这个对象始终表现出正确的行为,那么称这个类线程安全
线程安全问题:多个线程同时操作一个可变对象,就有可能发生线程安全问题
如何保证线程安全:对象设置为final类型;多线程操作对象时加锁
2.重入锁的概念,重入锁为什么可以防止死锁
重入锁:如果某个线程试图获取一个已经有他自己持有的锁,那么这个请求就会成功。因为内置锁是可重入的
重入的实现方式:为每个锁关联一个计数器和所有者线程。当计数器为0时,说明这个锁没有被任何线程持有;当线程请求一个未被持有的锁时,JVM将记下锁的持有者,并将计数器设置为1,如果同一个线程再次获取这个锁,计数器将递增;而当线程退出同步代码块时,计数器相应的递减,当计数器为0时,这个锁将被释放
防止死锁:内置锁可重入,就避免了同一个线程再次获取该锁的时候,无法获取的问题
3.产生死锁的四个条件
* 互斥:某个资源一次只允许一个线程访问
* 占有且等待:一个线程本身占有资源,同时还等待资源未得到满足,正在等待其他线程释放该资源
* 不可抢占:别人已经占有了某项资源,你不能因为自己的需求,就去把别人的资源抢占过来
* 循环等待:存在一个线程链,使得每个线程都占有下一个线程所需的至少一种资源
4.如何检查死锁
jconsole可以关联相关应用,并查看死锁线程
jstack pid可以打印栈信息
如何详细使用请参考:https://www.cnblogs.com/flyingeagle/articles/6853167.html
5.volatile 实现原理
volatile提示线程每次从共享内存中读取变量,而不是从私有内存中读取。这样就保证了同步数据的可见性
volatile本身并不处理数据的原子性,而是强制对数据的读写及时影响到主内存中。
1)过程分析
* read和load阶段:从主内存复制变量到线程内存
* use和assign阶段:执行代码,改变共享变量值
* store和write阶段:用工作内存数据刷新主内存对应变量的值
2)多线程数据脏读问题分析
在多线程环境中,use和assign是多次出现的,但这一操作并不是原子性,也就是在read和load之后,如果主线程的数据发生修改,线程工作内存中的值由于已经加载不会产生对应的变化,也就是私有内存和公共内存中的变量不同步,所以计算出的结果和预期值不一致,产生脏读问题。
3)如何理解volatile只保证可见性,不保证原子性?
简单来说,volatile在多CPU环境下不能保证其他CPU的缓存同步刷新,因此无法保证原子性。以最常用的i++来说吧,包含3个步骤
1:从内存中读取i当前的值
2:加1
3:把修改后的值刷新到主内存
对于普通变量来说,多线程下1、2之间被中断,其他线程修改了i的值,那原来在1、2之间被中断的i的值就已经无效了,所以多线程是不安全的
另外对于普通变量来说,步骤1也不是每次都从内存中读取,步骤3也不会每次都保证立即刷新到主内存。所以这里有两个问题:可见性和原子性
volatile只保证了可见性,即步骤1每次都重新读,步骤3每次都立即刷新到主内存。但1、2之间仍然会被中断,多个线程交叉修改,所以仍然不安全。
4)如何保证内存可见性?
volatile的内存可见性是基于内存屏障(Memory Barrier)实现的
内存屏障又称内存栅栏,是一个CPU指令,在程序运行时,为了提高程序运行性能,编译器和处理器会对指令进行重排序,JVM为了保证在不同的编译器和CPU上有相同的执行结果,通过插入特定类型的内存屏障来禁止特定类型的编译器重排序和处理器重排序,插入一条内存屏障会告诉编译器和CPU:不管什么指令都不能和这条Memory Barrier指令重排序。
CPU为了提高处理性能,并不直接和内存进行通信,而是将内存的数据读取到内存缓存(L1、L2)再进行操作,但操作并不能确定何时写回到内存。如果对volatile变量进行写操作,当CPU执行到lock前缀指令时,会将这个变量所在缓存行的数据写回到内存。
不过还是有一个问题,就算内存数据是最新的,其他缓存数据还是旧的,所以为了保证各个CPU缓存一致性,各个CPU通过嗅探在总线上传播的数据来检查自己缓存的数据有效性,当发现自己缓存行对应的内存地址的数据被修改,就会将该缓存行设置为无效状态,当CPU读取该变量时,发现所在的缓存行数据被设置为无效,就会重新从内存中读取数据到缓存中。
6.synchronized 实现原理
我们通过对synchronized修饰的代码进行class文件分析,可以看到,同步代码块是使用monitorenter和monitorexit指令实现的(也就是监视器的进入和监视器的退出)
任何对象都有一个monitor与之相关联,当且一个monitor被持有之后,他将处于锁定状态。线程执行到monitorenter指令时,将会尝试获取对象所对应的monitor所有权,即尝试获取对象的锁;
具体可见:http://www.importnew.com/23511.html
7.synchronized 与 lock 的区别
synchronized和lock都可以实现锁的功能;
只是lock比synchronized功能更多一些,synchronized的代码不可中断,如果无法获取锁的话也会一直阻塞等待。而lock我们可以使用tryLock来实现规定时间内阻塞的锁,使用lockIntercupt来实现响应中断的锁
8.AQS同步队列
AQS是抽象的队列式同步器,AQS定义了一套多线程访问共享资源的同步器框架。许多并发类都基于此实现。如ReentrantLock、Semaphore、CountDownLatch等
AQS使用int成员变量来表示同步状态,通过内置的FIFO队列来完成获取资源线程的排队工作
以两种方式方式获取资源:独占式、共享式
独占式:tryAcquire()/tryRelease()
共享式:tryAcquireShared()/tryReleaseShared()
具体可见:https://www.cnblogs.com/waterystone/p/4920797.html
9.CAS无锁的概念、乐观锁和悲观锁
CAS(compare and swap)
对于并发控制而言,锁是一种悲观策略,会阻塞线程执行。而无锁是一种乐观策略,它会假设对资源的访问时没有冲突的,线程也不需要阻塞。
那么当多个线程共同访问临界区的资源时,无锁策略则采用一种比较交换技术来鉴别线程冲突
一个CAS包括三个参数CAS(V,E,N)V表示要更新的变量、E表示预期的值、N表示新值。只有当V的值等于E时,才会将V的值修改为N。如果不相等,说明已经被其他线程修改了,当前线程可以放弃该次操作,也可继续尝试直至修改成功。
相对而言,synchronized就是悲观锁,CAS锁就是乐观锁
10.常见的原子操作类
AtomicInteger
AtomicLong
AtomicBoolean
AtomicIntegerArray
11.什么是ABA问题,出现ABA问题JDK是如何解决的
具体可见https://www.jianshu.com/p/72d02353dc7e
12.乐观锁的业务场景及实现方式
乐观锁:比较适合读取比较频繁的场景
悲观锁:比较适合写入比较频繁的场景
JDK中的CAS就是乐观锁的一种实现方式;
数据库中使用version来进行update操作也是一个乐观锁的实现方式
13.Java 8并法包下常见的并发类
CopyonwriteArrayList
CopyOnWriteArraySet
ConcurrentHashMap
ConcurrentLinkedQueue
ConcurrentLinkedDeque
14.偏向锁、轻量级锁、重量级锁、自旋锁的概念
* 偏向锁:目的是消除数据在无竞争情况下的同步源语,进一步提高程序的运行性能。锁会偏向于第一个获取她的线程,如果在接下来的执行中,该锁没有被其他线程获取,那么持有偏向锁的线程则永远不需要再进行同步
适用场景:始终只有一个线程在执行同步块,在他没有执行完释放锁之前,没有其他线程去执行同步块,在锁无竞争的情况下使用,如果一旦有竞争就升级为轻量级锁;升级为轻量级锁的时候需要撤销偏向锁,撤销偏向锁会导致stop the World操作
* 轻量级锁:轻量级锁并不是用来替代重量级锁的,它的本意是在没有多线程竞争的前提下,减少传统的重量级锁使用操作系统互斥量产生的性能消耗。轻量级锁由偏向锁升级来的,偏向锁运行在一个线程进入同步块的情况下,当第二个线程加入所竞争的时候,偏向锁就会升级为轻量级锁
* 重量级锁:轻量级和重量级是相对而言的,使用操作系统互斥量来实现的传统锁就是重量级锁。synchronized锁。当轻量级锁由两条以上的线程竞争时,那轻量级锁就不再有效,要膨胀为重量级锁
* 自旋锁:如果共享数据的锁定状态只会持续很短一段时间,为了这段时间去挂起和恢复线程并不值得。那么等待锁的线程可以先空转一会等待锁的释放而不是直接挂起,这样的锁称为自旋锁
更多请参考:https://blog.csdn.net/zqz_zqz/article/details/70233767
JVM
1.JVM运行时内存区域划分
jdk6的内存区域划分:程序计数器、方法区、堆、栈、本地方法栈
jdk8中,方法区(也就是永久代)已经不存在了,编译后的代码数据等已经移动到了元空间(metaspace),元空间并没有处于堆内存上,而是直接占用的本地内存(NativeMemory)
2.内存溢出OOM和堆栈溢出SOE的示例及原因、如何排查与解决
* 堆内存溢出OOM:持有对象过多而无法释放,再分配内存空间的时候就会发生堆内存溢出
* 栈溢出:与栈相关的两个异常
StackOverflowERROR(方法调用过深,内存不够新建栈帧)
OutofMemoryError(线程太多,内存不够新建线程)
3.如何判断对象是否可以回收或存活
对象的回收是需要达到一定条件的
当一个对象没有任何GCRoot可以达到的时候,说明该对象已经没有引用可被回收
可作为GCRoot的对象有:虚拟机栈中引用的对象;方法去中静态变量引用的对象;方法区中常量引用的对象;本地方法栈中JNI引用的对象
4.常见的GC回收算法及其含义
* 标记清除算法
* 复制算法
* 标记整理算法
* 分代收集算法
5.常见的JVM性能监控和故障处理工具类
jps、jstat、jmap、jinfo、jconsole
6.JVM如何设置参数
-Xmx -Xmn设置堆内存大小参数,类似于这种设置方式,可以在应用启动的时候,添加上这些JVM参数,以限定应用的内存使用
更多参数请参考:https://www.cnblogs.com/jianyungsun/p/6911380.html
7.JVM性能调优
可以从以下几个方法来回答
* 堆内存大小、比例设置
* 回收器的选择
* 年轻代的设置
更多请参考:http://www.jb51.net/article/36964.htm
8.类加载器、双亲委派模型、一个类的生命周期、类是如何加载到JVM中的
* 类加载器:通过一个类的权限定名来获取描述此类的二进制字节流,这个动作由类加载器实现。这个动作在JVM之外,以便让应用程序自己来决定如何获取所需要的类
* 双亲委派模型:从开发人员的角度来看,类加载器可分为:启动类加载器(BootStrap ClassLoader,加载JAVA_HOME/lib下的jar)、扩展类加载器(Extension ClassLoader,加载JAVA_HOME/lib/ext下的jar)、应用程序类加载器(Application ClassLoader,加载用户类路径上指定的类库)
工作原理:一个类加载器收到类加载的请求,他首先不会自己尝试加载这个类,而是把这个请求委托给父类加载器去执行,每一个层次的类加载器都是如此,只有当父加载器反馈自己无法完成这个加载请求时,子加载器才会尝试自己去加载
* 类生命周期:加载(Loading)、验证(Verification)、准备(Preparation)、解析(Resolution)、初始化(Initialization)、使用(Using)、卸载(Unloading)7个阶段。其中 验证 准备 解析3个部分统称为连接
9.强引用、软引用、弱引用、虚引用
具体可参考:https://www.cnblogs.com/gudi/p/6403953.html