Java面试题收录含答案(整理版)持续中....
本文分为17个模块,分别是:Java基础、容器、多线程、反射、对象拷贝、Java web、异常、网络、设计模式、算法、Spring/Spring MVC、Spring Boot/Spring Cloud、Hibernate、MyBatis、MySQL、Redis、JVM。
1. Java基础
1. JDK和JRE有什么区别?
答:
JDK:Java Development Kit的简称,Java开发工具包,提供了Java的开发环境和运行环境。
JRE:Java Runtime Environment的简称,Java运行环境,为Java的运行提供了所需环境。
2. == 和 equals的区别是什么?
答:
对于基本类型和引用类型 == 的作用效果是不同的,如下所示:
基本类型:比较的是值是否相同;
引用类型:比较的是引用是否相同;
==对于基本类型来说是值比较,对于引用类型来说是比较的是引用;而equals默认情况下是引用比较,只是很多类重写了equals方法,比如String、Integer等把它变成了值比较,所以一般情况下equals比较的是值是否相等。
3. 普通类和抽象类有哪些区别?
答:
①普通类不能包含抽象方法,抽象类可以包含抽象方法。
②抽象类是不能被实例化的,就是不能用new调出构造方法创建对象,普通类可以直接实例化。
③如果一个类继承于抽象类,则该子类必须实现父类的抽象方法。如果子类没有实现父类的抽象方法,则必须将子类也定义为abstract类。
4. 抽象类能使用final修饰吗?
答:
不能,定义抽象类就是让其他类继承的,如果定义为final该类就不能被继承,这样彼此就会产生矛盾,所以final不能修饰抽象类。
5. 接口和抽象类有什么区别?类可以继承多个类么,接口可以继承多个接口么,类可以实现多个接口么
答:
接口和抽象类的区别:
①实现:抽象类的子类使用extend来继承;接口必须使用implements来实现接口。
②构造函数:抽象类可以有构造函数;接口不能有。
③实现数量:类可以实现很多个接口;但只能继承一个抽象类。
④访问修饰符:接口中的方法默认使用public修饰;抽象类中的抽象方法可以使用Public和Protected修饰,如果抽象方法修饰符为Private,则报错:The abstract method 方法名 in type Test can only set a visibility modifier,one of public or protected。
类不可以继承多个类,接口可以继承多个接口,类可以实现多个接口。
6. 两个Integer的引用对象传给一个swap方法在方法内部交换引用,返回后,两个引用的值是否会发现变化?
答:
线程对变量的所有操作(读取、赋值)都必须在工作内存中进行,而不能直接读写主内存中的变量。在swap方法内部交换引用,只会交换线程的工作内存中持有的方法参数,而工作内存中的方法参数是主内存中变量的副本,因此执行这样的swap方法不会改变主内存中变量的指向。
public class ExchangeInteger{ public static void main(String[] args){ int A = 2; int B = 3; swap(A,B); System.out.println("A:" + A +"\nB:" + B); } public static void swap(int A,int B){ int C = A; B = C; A = B; } } 结果为: A:2 B:3
6. String属于基础的数据类型吗?Java中的几种基本数据类型是什么,各自占用多少字节?
答:
String不属于基础类型,基础类型有8种:byte、short、int、long、float、double、char、boolean,而String属于对象。
int 32bit / short 16bit
long 64bit / byte 8bit
char 16bit / float 32bit
double 64bit / boolean 1bit
7. String,StringBuffer,StringBuilder的区别是什么?String为什么是不可变的?
答:
①String是字符串常量,StringBuffer和StringBuilder都是字符串变量。后两者的字符内容可变,而前者创建后内容不可变。
②String不可变是因为在JDK中String类被声明为一个final类。
③StringBuffer是线程安全的,而StringBuilder是非线程安全的。
④执行速度:StringBuilder > StringBuffer > String
注意:线程安全会带来额外的系统开销,所以StringBuilder的效率比StringBuffer高。所以单线程环境下推荐使用StringBuilder,多线程环境下推荐使用StringBuffer。
8. String str = “i” 与 String str = new String(“i”)一样吗?
答:
不一样,因为内存的分配方式不一样。String str = “i”的方式,Java虚拟机会将其分配到常量池中,如果常量池中有”i”,就返回”i”的地址,如果没有就创建”i”,然后返回”i”的地址;而String str = new String(“i”)则会被分到堆内存中新开辟一块空间。
9. sting s = new string(“abc”)分别在堆栈上新建了哪些对象?
答:
栈:string s。
堆:new String(“abc”)
字符串池(方法区):”abc”
JVM中存在着一个字符串池,使用引号 创建文本的方式的String对象都会放入字符串池,可以提高效率。
String a = “abc”;String b = “abc”;//这两句在字符串池,只创建一个实例对象。
String a = “ab” + “cd”;//这一句在字符串池创建三个实例对象。
new 方式新建String对象则不会放入字符串池,放入堆。
9. 如何将字符串反转?
答:
使用StringBuilder或者stringBuffer的reverse()方法。
// StringBuffer reverse StringBuffer stringBuffer = new StringBuffer(); stringBuffer. append("abcdefg"); System. out. println(stringBuffer. reverse()); // gfedcba // StringBuilder reverse StringBuilder stringBuilder = new StringBuilder(); stringBuilder. append("abcdefg"); System. out. println(stringBuilder. reverse()); // gfedcba
10. String类的常用方法都有哪些?
答:
①indexOf():返回指定字符的索引;
②charAt():返回指定索引处的字符;
③replace():字符串替换;
④trim():去除字符串两端空白;
⑤split():分割字符串,返回一个分割后的字符串数组;
⑥getBytes():返回字符串的byte类型数组;
⑦length():返回字符串长度;
⑧toLowerCase():将字符串转成小写字符;
⑨toUpperCase():将字符串转成大写字符;
⑩subString():截取字符串;
⑪equals():字符串比较;
12. 在自己的代码中,如果创建一个java.lang.String对象,这个对象是否可以被类加载器加载?为什么?
答:
不可以,双亲委派模式会保证父类加载器先加载类,就是BootStrap(启动类)加载器加载jdk里面的java.lang.String类,而自定义的java.lang.String类永远不会被加载到。
11. final在Java中有什么作用?
答:
①final修饰的类叫最终类,该类不能被继承。
②final修饰的方法不能被重写。
③final修饰的变量叫常量,常量必须初始化,初始化之后值就不能被修改。
12. Java中IO流分为几种?
答:
①按功能来分:输入流(input)和输出流(output)。
②按类型来分:字节流和字符流。
③字节流和字符流的区别是:字节流按8为传输以字节为单位输入输出数据,字符流按16位传输以字符为单位输入输出数据。
13. BIO、NIO、AIO有什么区别?谈谈reactor模型。
答:
①BIO:Block IO同步阻塞IO,就是我们平常使用的传统IO,它的特点是模式简单使用方便,并发处理能力低。服务器实现模式为一个连接一个线程,即客户端有连接请求时服务器端就需要启动一个线程进行处理,如果这个连接不做任何事情会造成不必要的线程开销,当然可以通过线程池机制改善。
②NIO:New IO同步非阻塞IO,就是传统IO的升级,客户端和服务器通过Channel(通道)通讯,实现了多路复用。服务器实现模式为一个请求一个线程,即客户端发送的连接请求都会注册到多路复用器上,多路复用器轮询到连接有I/O请求时才启动一个线程进行处理。
③AIO:Asynchronous IO是NIO的升级,也叫NIO2,实现了异步非堵塞IO,异步IO的操作基于事件和回调机制。
reactor模型:反应器模式(事件驱动模式):当一个主体发生改变时,所有的属体都得到通知,类似于观察者模式。
14. Files的常用方法都有哪些?
答:
①Files.exists():检测文件路径是否存在。
②Files.createFile():创建文件。
③Files.createDirectory():创建文件夹。
④Files.delete():删除一个文件或目录。
⑤Files.copy():复制文件。
⑥Files.move():移动文件。
⑦Files.size():查看文件个数。
⑧Files.read():读取文件。
⑨Files.write():写入文件。
15. 讲讲类的实例化顺序,比如父类静态数据,构造函数,字段,子类静态数据,构造函数,字段,当new的时候,他们的执行顺序。
答:
类的实例化顺序:先静态再父子
父类静态数据 -> 子类静态数据 -> 父类字段 -> 子类字段 -> 父类构造函数 -> 子类构造函数
16. 请结合OO设计理念,谈谈访问修饰符public、private、protected、default在应用设计中的作用。
答:
OO设计理念:封装、继承、多态
封装,也就是把客观事物封装成抽象的类,并且类可以把自己的数据和方法只让可信的类或者对象操作,对不可信的进行信息隐藏。所以我们可以通过public、private、protected、default来进行访问控制。
17. 在jdk1.5中,引入了泛型,泛型的存在是用来解决什么问题。
答:
泛型的好处是在编译的时候检查类型安全,并且所有的强制转换都是自动和隐式的,提高代码的重用率。
18. Java8的新特性。
答:
Java8有十大新特性,具体参考:https://blog.csdn.net/yitian_66/article/details/81010434
19. 简单说说你了解的类加载器。
答:
类加载器主要分为:引导类加载器(Bootstrap ClassLoader)、扩展类加载器(Extension ClassLoader)、系统类加载器(AppClassLoader)和自定义加载器(Custom ClassLoader)
20. int的取值范围。
答:
-2^31 ~ 2^31-1,即-2147483648 ~ 2147483647。
2. Java集合容器
1. Java容器都有哪些?
答:
Java容器分为Collection和Map两大类,其下又有很多子类,如下所示:
Collection
List
ArrayList
LinkedList
Vector
Stack
Set
HashSet
LinkedHashSet
TreeSet
Map
HashMap
LinkedHashMap
TreeMap
ConcurrentHashMap
HashTable
2. Collection和Collections有什么区别?
答:
Collection是一个集合接口,它提供了对集合对象进行基本操作的通用接口方法,所有集合都是他的子类,比如List、Set等。
Collections是一个包装类,包含了很多静态方法,不能被实例化,就像一个工具类,比如提供的排序方法:Collections.sort(list)。
3. List、Set、Map之间的区别是什么?
答:
List、Set、Map的区别主要体现在两个方面:元素是否有序、是否允许元素重复。
4. Array和ArrayList有何区别?
答:
①Array可以存储基本数据类型和对象,ArrayList只能存储对象。
②Array是指固定大小的,而ArrayList大小是自动扩展的。
③Array内置方法没有ArrayList多,比如addAll,removeAll,iteration等方法只有ArrayList有。
4. Vector,ArrayList,LinkedList的区别是什么?
答:
①Vector、ArrayList都是以类似数组的形式存储在内存中,LinkedList则以链表的形式进行存储。
②List中的元素有序、允许有重复的元素,Set中的元素无序,不允许有重复元素。
③Vector线程安全,ArrayList、LinkedList线程不安全。
④LinkedList适合指定位置插入、删除操作,不适合查找,因为LinkedList是线性的数据存储方式,所以需要移动指针从前往后依次查找;ArrayList、Vector适合查找,在随机访问的时候效率要高,不适合指定位置的插入、删除操作,因为ArrayList增删操作要影响数组内的其他数据的下标。
⑤ArrayList在元素填满容器使会自动扩充容器大小的50%,而Vector则是100%,因此ArrayList更节省空间。
5. 如何实现数组和List之间的转换?
答:
数组转List:使用Arrays.asList(array)进行转换。
List转数组:使用List自带的toArray()方法。
6. HashTable,HashMap,TreeMap区别?
答:
①HashTable线程同步,HashMap非线程同步。
②HashTable和HashMap有几个主要不同:线程安全以及速度;HashMap速度比HashTable快。
③HashTable不允许<键,值>有空值,HashMap允许<键,值>有空值。
④HashTable使用Enumeration,HashMap使用Iterator。
⑤HashTable中hash数组的默认大小是11,增加方式的n*2+1,HashMap中hash数组的默认大小是16,之后每次扩充,容量变为原来的2倍。
⑥TreeMap能够把它保存的记录根据键排序,默认是按升序排序。
7. 如何决定使用HashMap还是TreeMap?
答:
对于在Map中插入、删除、定位一个元素这类操作,HashMap是最好的操作,因为相对而言HashMap的插入会更快,但如果你要对一个key集合进行有序的遍历,那TreeMap是更好的选择。
8. hashMap的底层实现原理?
答:
①hashMap基于hashing原理,我们通过put()和get()方法存储和获取对象。当我们将键值对传递给put()方法时,它调用键对象的hashCode()方法来计算hash值,然后根据hash值找到bucket位置来存储value值对象。当获取对象时,通过键对象的equals()方法找到正确的键值对,然后返回值对象。HashMap使用链表来解决碰撞问题,当发生碰撞了,对象将会存储在链表的下一个节点中。HashMap在每个链表节点中存储键值对对象。
当两个不同的键对象的hashCode相同时,我们称之为hash冲突,HashMap的做法是用链表和红黑树存储相同hash值的value。当hash冲突的个数比较少时,使用链表否则使用红黑树。键对象的equals()方法用来找到键值对。
②HashMap的存储结构:数组+链表+红黑树(jdk1.8)
HashMap是一种可以快速存储以及快速查找的键值容器,那么jdk是如何实现HashMap的快速存储和快速查找呢?
从数组和链表以及二叉查找树这三种数据结构说起:
1)数组:数组结构是连续的内存地址,数组的部分元素被连续存放在cpu缓存中,利用二分查找法,数组的时间复杂度位低到O(1),可见数组的查询效率是非常高的。但是由于数组的内存占用严重,空间复杂度很高,所以数组的增删操作效率将非常低下。
2)链表:链表内存地址比较分散,空间复杂度较低,在插入和删除上效率较高。但是内存地址过于分散,导致查询效率大大降低。
3)二叉树在查询效率上和排序后数组的二分查找效率完全相同,从根节点开始,到下面分支节点左边的永远比父节点的要小,右边的比父节点大。二叉树的元素过于分散,导致空间复杂度过大,在插入和删除上会非常低效。为了解决这个问题,jdk使用了红黑树这种数据结构,而红黑树在时间复杂度上可以做到O(log n)的高效率。
综合以上三种数据结构的特点,HashMap有效的利用了各个数据结构的长处。
实现快速存储
快速存储是链表和红黑树以及无移动添加数组元素的优势。
HashMap中数组的索引是通过hashCode的无符号右移16位后异或然后取余获得,hash公式如下
index = [ (hashCode) ^ (HashCode >>> 16) ] / 数组的长度
通过这样的计算可以保证数组索引的分散。但是分散并不代表不会出现相同的index,也就是索引冲突(hash冲突)。在遇到索引冲突的时候,HashMap会在该索引的位置生成一个单向链表,将元素放置到next。但是我们知道链表这种数据结构在存储方面高效,但是在查询上会非常低效。所以HashMap在链表元素大于8个的时候,会自动将链表转成红黑树,以达到查询高效,插入也高效的目的。当然,在红黑树中元素个数小于一定数量,也会变回原来的链表结构,jdk设置这个数量为6个。
这样不管在外围”数组”上还是在”链表”上以及变成”红黑树”这种数据结构,HashMap都能做到快速存储。
HashMap对数组的扩容触发条件是数组元素达到长度的0.75(75%),使用这样的触发条件jdk是从时间和空间角度上思考的,为了这个条件更加容易被触发,也要考虑到暂用过多内存浪费资源,75%非常理想化的触发条件。
实现快速查找
HashMap的外围数组这点毋庸置疑,查找效率绝对不会存在问题。索引冲突变成链表,元素数量仅仅只有8个的链表,查询效率不需要考虑。大于8个元素后变成的红黑树,二叉查找树的查询效率和数组相当,这点也不需要质疑。综合考虑在查询方面HashMap,也做到了快速查找的特性。
9. HashMap、HashTable和ConcurrentHashMap的区别,HashTable和ConcurrentHashMap是如何实现线程安全的?
答:
①因为多线程环境下,使用HashMap进行put操作会引起死循环,导致CPU利用率接近100%,所以在并发情况下不能使用HashMap。
②HashTable容器使用synchronized来保证线程安全,但在线程竞争激烈的情况下,HashTable的效率非常低下。因为当一个线程访问HashTable的同步方法时,其他线程访问HashTable的同步方法时,可能会进入阻塞或者轮询状态。如线程1使用put进行添加元素,线程2不但不能使用put方法添加元素,并且也不能使用get方法来获取元素,所以竞争越激烈效率越低。
总结:线程安全,效率和容器的大小成正比。容器数据量越大,效率越慢。
③由于HashTable效率低下,JDK1.5提出了ConcurrentHashMap替代HashTable。其实ConcurrentHashMap实现线程安全也是通过synchronized关键字来控制代码同步来实现的,不同于HashTable的是ConcurrentHashMap在线程同步上更加细分化,它不会像HashTable那样一把包揽的将所有数据都锁住。ConcurrentHashMap采用分段锁。底层数据结构实现原理和HashMap没什么两样,都是数组+链表+红黑树。
总结:线程安全,效率相对于不如HashMap,但是和HashTable相比,效率得到很大的提升。
综合考虑,如果使用线程安全容器,推荐使用ConcurrentHashMap。
10. 用hashmap实现redis有什么问题
答:
①容量问题:HashMap是有最大容量的。
②时效问题:redis可以持久化,也可以定时时间;hashmap不可以持久化。
③线程并发问题:hashmap不是线程安全的,可能会出现死锁,死循环。
④可用ConcurrentHashMap。
11. 遍历HashMap的三种方式
答:
①通过HashMap.entrySet键值对集合,再通过迭代器Iterator遍历键值对集合得到key值和value值;
②通过HashMap.keySet()获得键的Set集合,遍历键的Set集合iterator()获取值;
③通过HashMap.values()得到“值”的集合iterator(),遍历“值”的集合;
12. HashMap如果只有一个写其他全读会出什么问题
答:
会出现死锁,死循环等问题,因为hashmap不是线程安全的,建议使用ConcurrentHashMap。
13. ConcurrenHashMap求size是如何加锁的,如果刚求完一段后这段发生了变化该如何处理
答:
Put等操作都是在单个Segment中进行的,但是ConcurrentHashMap有一些操作是在多个Segment中进行的,比如size操作,ConcurrentHashMap的size操作也采用了一种比较巧的方式,来尽量避免对所有的Segment都加锁。
Segment中的有一个modCount变量,代表的是对Segment中元素的数量造成影响的操作的次数,这个值只增不减。
size操作就是遍历了两次Segment,每次记录Segment的modCount值,然后将两次的modCount进行比较,如果相同,则表示期间没有发生过写入操作,就将原先遍历的结果返回,如果不相同,则把这个过程再重复做一次,如果再不相同,则就需要将所有的Segment都锁住,然后一个一个遍历了。
14. ConcurrentHashmap的分段锁是如何加的?是不是分段越多越好
答:
ConcurrentHashMap是由Segment数组结构和HashEntry数组结构组成。Segment是一种可重入锁ReentrantLock,在ConcurrentHashMap里扮演锁的角色,HashEntry则用于存储键值对数据。一个ConcurrentHashMap里包含一个Segment数组,Segment的结构和HashMap类似,是一种数组和链表结构,一个Segment里包含一个HashEntry数组,每个HashEntry是一个链表结构的元素,每个Segment守护着一个HashEntry数组里的元素,当对HashEntry数组的数据进行修改时,必须首先获得它对应的Segment锁。
Java7的segment数量推荐是hardware thread数。ConcurrentHashMap的segment分段数并不是越多越好,根据你并发的线程数量决定,太多会导致性能降低。
14. Java8的ConcurrentHashmap为什么放弃了分段锁,有什么问题吗,如果你来设计,你如何设计。
答:
jdk8放弃了分段锁而是用了Node锁,减低锁的粒度,提高性能,并使用CAS操作来确保Node的一些操作的原子性,取代了锁。但是ConcurrentHashMap的一些操作使用了synchronized锁,而不是ReentrantLock,虽然说jdk8的synchronized的性能进行了优化,但是我觉得还是使用ReentrantLock锁能有更多的提高性能。
15. 有没顺序的Map实现类,如果有,他们是怎么保证有序的。顺序的Map实现类:LinkedHashMap,TreeMap。
答:
LinkedHashMap是基于元素进入集合的顺序或者被访问的先后顺序排序,TreeMap则是基于元素的固有顺序(由Comparator或者Comparable确定)。
15. 说一下HashSet的实现原理?
答:
HashSet是基于HashMap实现的,HashSet底层使用HashMap来保存所有元素,因此HashSet的实现比较简单,相关HashSet的操作,基本上都是直接调用底层HashMap的相关方法来完成,HashSet不允许重复的值。
17. 在Queue中poll()和remove()有什么区别?
答:
相同点:都是返回第一个元素,并在队列中删除返回的对象。
不同点:如果没有元素remove()会直接抛出NoSuchElementException异常,而poll()会返回null。
18. 迭代器Iterator是什么?
答:
Iterator接口提供遍历任何Collection的接口。我们可以从一个Collection中使用迭代器方法来获取迭代器实例。迭代器取代了Java集合框架中的Enumeration,迭代器允许调用者在迭代过程中移除元素。
Iterator的特点是更加安全,因为它可以确保,在当前遍历的集合元素被更改的时候,就会抛出ConcurrentModificationException异常。
19. 怎么确保一个集合不能被修改?
答:
可以使用Collections.unmodifiableCollection(Collection c)方法来创建一个只读集合,这样改变集合的任何操作都会抛出Java.lang.UnsupportedOperationException异常。
20. 并发包里了解哪些?
答:
- ConcurrentHashMap 线程安全的HashMap的实现;
- CopyOnWriteArrayList 线程安全且在读操作时无锁的ArrayList;
- CopyOnWriteArraySet 基于CopyOnWriteArrayList,不添加重复元素;
- ArrayBlockingQueue 基于数组、先进先出、线程安全,可实现指定时间的阻塞读写,并且容量可以限制;
- LinkedBlockingQueue 基于链表实现,读写各用一把锁,在高并发读写操作都多的情况下,性能优于ArrayBlockingQueue。组成一个链表+两把锁+两个条件。
3. Java多线程
1. 并行和并发有什么区别?
答:
并行:多个处理器或多核处理器同时处理多个任务。
并发:多个任务在同一个CPU核上,按细分的时间片轮流(交替)执行,从逻辑上来看那些任务是同时执行的。
2. 线程和进程的区别?
答:
一个程序下至少有一个进程,一个进程下至少有一个线程,一个进程下也可以有多个线程来增加程序的执行速度。
3. 守护线程是什么?
答:
守护线程是运行在后台的一种特殊进程。它独立于控制终端并且周期性地执行某种任务或等待处理某些发生的事件。在Java中垃圾回收线程就是特殊的守护线程。
4. 多线程有几种实现方式?
答:
有4种,分别是:
①继承Thread类;
②实现Runnable接口;
③实现Callable接口通过FutureTask包装器来创建Thread线程;
④通过线程池创建线程,使用线程池接口ExecutorService结合Callable、Future实现有返回结果的多线程。
5. 说一下Runnable和Callable有什么区别?
答:
Runnable没有返回值,Callable可以拿到有返回值,Callable可以看作是Runnable的补充。
6. 线程有哪些状态?
答:
线程的6种状态:
①初始(NEW):新创建了一个线程对象,但还没有调用start()方法;
②运行(RUNNABLE):Java线程中将就绪(ready)和运行中(running)两种状态笼统的称为”运行”。线程对象创建后,其他线程(比如main线程)调用了该对象的start()方法。该状态的线程位于可运行线程池中,等待被线程调度选中,获取CPU的使用权,此时处于就绪状态(ready)。就绪状态的线程在获得CPU时间片后变为运行中状态(running)。
③阻塞(BLOCKED):表示线程阻塞于锁。
④等待(WAITING):进入该状态的线程需要等待其他线程做出一些特定动作(通知或中断)。
⑤超时等待(TIMED_WAITING):该状态不同于WAITING,它可以在指定的时间后自行返回。
⑥终止(TERMINATED):表示该线程已经执行完毕。
7. sleep()和wait()有什么区别?
答:
类的不同:sleep()来自Thread,wait()来自Object。
释放锁:sleep()不释放锁;wait()释放锁。
用法不同:sleep()时间会自动恢复;wait可以使用notify()/notifyAll()直接唤醒。
8. notify()和notifyAll()有什么区别?
答:
notifyAll()会唤醒所有的线程,notify()之后唤醒一个线程。notifyAll()调用后,会将全部线程由等待池移到锁池,然后参与锁的竞争,竞争成功则继续执行,如果不成功则留在锁池被释放后再次参与竞争。而notify()只会唤醒一个线程,具体唤醒哪一个线程由虚拟机控制。
9. 线程的run()和start()有什么区别?
答:
start()方法用于启动线程,run()方法用于执行线程的运行时代码。run()可以重复调用,而start()只能调用一次。
10. 创建线程池有哪几种方式?
答:
①newSingleThreadExecutor():它的特点在于工作线程数目被限制为1,操作一个无界的工作队列,所以它保证了所有任务的都是被顺序执行,最多会有一个任务处于活动状态,并且不允许使用者改动线程池实例,因此可以避免其改变线程数目;
②newCachedThreadPool():它是一种用来处理大量短时间工作任务的线程池,具有几个鲜明特点:它会试图缓存线程并重用,当无缓存线程可用时,就会创建新的工作线程;如果线程闲置的时间超过60秒,则被终止并移出缓存;长时间闲置时,这种线程池,不会消耗什么资源。其内部使用SynchronousQueue作为工作队列;
③newFixedThreadPool(int nThreads):重用指定数目(nThreads)的线程,其背后使用的是无界的工作队列,任何时候最多有nThreads个工作线程是活动的。这意味着,如果任务数据超过了活动队列数目,将在工作队列中等待空闲线程出现;如果有工作线程退出,将会有新的工作线程被创建,以补足指定的数目nThreads;
④newSingleThreadScheduledExecutor():创建单线程池,返回ScheduledExecutorService,可以进行定时或周期性的工作调度;
⑤newScheduledThreadPool(int corePoolSize):和newSingleThreadScheduledExecutor()类似,创建的是个ScheduledExecutorService,可以进行定时或周期性的工作调度,区别在于单一工作线程还是多个工作线程;
⑥newWorkStrealingPool(int parallelism):这是一个经常被人忽略的线程池,Java8才加入这个创建方法,其内部会构建ForkJoinPool,利用Work-Stealing算法,并行地处理任务,不保证处理顺序;
11. 线程池都有哪些状态?
答:
①RUNNING;
②SHUTDOWN;
③STOP;
④TIDYING;
⑤TERMINATED;
12. 线程池中submit()和execute()方法有什么区别?
答:
①execute():只能执行Runnable类型的任务。
②submit():可以执行Runnable和Callable类型的任务。
Callable类型的任务可以获取执行的返回值,而Runnable执行无返回值。
13. 在Java程序中怎么保证多线程的运行安全?
答:
方法一:使用安全类,比如Java.util.concurrent下的类。
方法二:使用自动锁synchronized。
方法三:使用手动锁Lock。
14. 什么是死锁以及如何避免死锁?
答:
死锁产生:当线程A持有独占锁a,并尝试去获取独占锁b的同时,线程B持有独占锁b,并尝试获取独占锁a的情况下,就会发生AB两个线程由于互相持有对方需要的锁,而发生的阻塞现象,我们称为死锁。
线程死锁是指由于两个或者多个线程互相持有对方所需要的资源,导致这些线程处于等待状态,无法前往执行。当线程进入对象的synchronized代码块时,便占有了资源,直到它退出该代码块或者调用wait方法,才释放资源,在此期间,其他线程将不能进入该代码块。当线程互相持有对方所需要的资源时,会互相等待对方释放资源,如果线程都不主动释放所占有的资源,将产生死锁。
避免死锁:
①尽量使用tryLock(long timeout, TimeUnit unit)的方法(ReentrantLock、ReentrantReadWriteLock),设置超时时间,超时可以退出,防止死锁。
②尽量使用Java.util.concurrent并发类代替自己手写锁。
③尽量降低锁的使用粒度,尽量不要几个功能用同一把锁。
④尽量减少同步的代码块。
15. volatile关键字的作用?
答:
volatile字段值在所有的线程和CPU缓存中必须保持同步。简单讲,你读取的volatile关键字修饰的变量看到的随时是自己的最新值,而无关乎哪个线程写入的,线程1中对变量v的最新修改,对线程2是可见的。volatile字段本身保证了可见性,所有线程都能看到共享内存的最新状态。
16. 可重入的读写锁,可重入是如何实现的?
答:
可重入锁又叫做递归锁。
reentrant锁意味着什么呢?简单来说,它有一个与锁相关的获取计数器,如果拥有锁的某个线程再次得到锁,那么获取计数器就加1,然后锁需要被释放两次才能获得真正释放。
这相当于是模仿了synchronized中又可以嵌套一个synchronized这样的场景。
17. threadLocal使用时注意的问题
答:
(ThreadLocal和Synchronized都用于解决多线程并发访问。但是ThreadLocal与synchronized有本质的区别。
synchronized是利用锁的机制,使变量或代码块在某一时刻只能被一个线程访问。
而ThreadLocal为每一个线程都提供了变量的副本,使得每个线程在某一时间访问到的并不是同一个对象,这样就隔离了多个线程对数据的数据共享。而Synchronized却正好相反,它用于在多个线程间通信时能够获得数据共享。)
ThreadLocal的内存泄漏问题:
当使用线程池来复用线程时,一个线程使用完并不会销毁线程,那么分发的那个实例会一直绑定在这个线程上。由于WeakReference封装了ThreadLocal,并作为了ThreadLocalMap的Entry的Key。如果在某些时候ThreadLocal对象被赋Null的话,弱应用会被GC收集,这样就会导致Entry的Value对象找不到,线程被复用后如果有调用ThreadLocal.get/set方法的话,方法里面会去做遍历清除,以[ThreadLocal=Null]为Key的Entry;但如果一直没调用ThreadLocal.get/set方法的话就会导致内存泄漏了。
ThreadLocal的经典使用场景是数据库连接和session管理等。
18. 说一下synchronized底层实现原理?
答:
synchronized是由一对monitorenter/monitorexit指令实现的,monitor对象是同步的基本实现单元。在Java6之前,monitor的实现完全是依靠操作系统内部的互斥锁,因为需要进行用户态到内核态的切换,所以同步操作是一个无差别的重量级操作,性能也很低。但在Java6的时候,Java虚拟机对此进行了大刀阔斧地改进,提供了三种不同的monitor实现,也就是常说的三种不同的锁:偏向锁(Biased Locking)、轻量级和重量级锁,大大改进了其性能。
19. synchronized 和 volatile 的区别是什么?
答:
①volatile是变量修饰符;synchronized是修饰类、方法、代码段。
②volatile仅能实现变量的修改可见性,不能保证原子性;而synchronized则可以保证变量的修改可见性和原子性。
③volatile不会造成线程的阻塞;synchronized可能会造成线程的阻塞。
20. synchronized 和 Lock 有什么区别?
答:
①synchronized可以给类、方法、代码块加锁;而lock只能给代码块加锁。
②synchronized不需要手动获取锁和释放锁,使用简单,发生异常会自动释放锁,不会造成死锁;而lock需要自己加锁和释放锁,如果使用不当没有unLock()去释放锁就会造成死锁。
③通过Lock可以知道有没有成功获取锁,而synchronized却无法办到。
21. synchronized 和 ReentrantLock 区别是什么?
答:
synchronized 早期的实现比较低效,对比ReentrantLock,大多数场景性能都相差较大,但是在Java6中对synchronized进行了非常多的改进。
主要区别如下:
①ReentrantLock使用起来比较灵活,但是必须有释放锁的配合动作;
②ReentrantLock必须手动获取与释放锁,而synchronized可用于修饰方法、代码块等。
③ReentrantLock只适用于代码块锁,而synchronized可用于修饰方法、代码块等。
④ReentrantLock标记的变量不会被编译器优化;synchronized标记的边两个可以被编译器优化。
22. 说一下atomic的原理?
答:
atomic主要利用CAS(Compare And Swap)和 volatile 和 native() 方法来保证原子操作,从而避免synchronized的高开销,执行效率大为提升。
23. 怎么解决项目中的超卖现象?
答:
可以有条件有选择的在读操作上加锁,比如可以对库存做一个判断,当库存小于一个量时开始加锁,让购买者排队,这样一来就解决了超卖现象。或者加乐观锁,即更新库存时,必须更新versionId字段,若两个用户同时使得versionId=3并提交,那么有一个用户一定被回滚。
24. 乐观锁vs悲观锁
答:
悲观锁:总是假设最坏的情况,每次去拿数据的时候都认为别人会修改,所以每次在拿数据的时候都会上锁,这样别人想拿这个数据就会阻塞直到它拿到锁。传统的关系型数据库里边就用到了很多这种锁机制,比如行锁、表锁等,读锁、写锁等,都是在做操作之前先上锁。再比如Java里面的同步原语synchronized关键字的实现和Lock的实现类也是悲观锁。悲观锁适合写操作多的场景,先加锁可以保证写操作时数据正确。
乐观锁:顾名思义,就是很乐观,每次去拿数据的时候都认为别人不会修改,所以不会上锁,但是在更新的时候会判断一下在此期间别人有没有去更新这个数据,可以使用版本号等机制。乐观锁适用于多读的应用类型,这样可以提高吞吐量,像数据库提供的类似于write_condition机制,其实都是提供的乐观锁。乐观锁在java中是通过无锁编程来实现的。在Java中java.util.concurrent.atomic包下面的原子变量类就是使用了乐观锁的一种实现方式CAS实现的。乐观锁适合读操作多的场景,不加锁的特点能够使其读操作的性能大幅提升。
4. 反射
1. 什么是反射?反射的原理,反射创建类实例的三种方式是什么?
答:
反射是在运行状态中,对于任意一个类,都能知道这个类的所有属性和方法;对于任意一个对象,都能够调用它的任意一个方法和属性;这种动态获取的信息以及动态调用对象的方法的功能称为Java语言的发射机制。
反射的原理:如果知道一个类的名称/或者它的一个实例对象,就能把这个类的所有方法和变量的信息(方法名,变量名,方法,修饰符,类型,方法参数等等所有信息)找出来。
发射创建类实例的三种方式:
①Class.forName(“com.A”);
②new A().getClass();
③A.class;
2. 反射中,Class.forName和ClassLoader区别。
答:
class.forName()除了将类的.class文件加载到jvm中之外,还会对类进行解释,执行类中的static块。
classLoader只干一件事情,就是将.class文件加载到jvm中,不会执行static中的内容,只有在newInstance才会去执行static块。
2. 什么是Java序列化?什么情况下需要序列化?
答:
Java序列化是为了保存各种对象在内存中的状态,并且可以把保存的对象状态再读出来。
以下情况需要使用Java序列化:
①想把内存中的对象状态保存到一个文件中或者数据库中的时候;
②想用套接字在网络上传送对象的时候;
③想通过RMI(远程方法调用)传输对象的时候。
3. Java如何实现序列化和反序列化的,底层原理是怎样的?
答:
① JDK类库中序列化和反序列化API:
(1) java.io.ObjectOutputStream:表示对象输出流,它的writeObject(Object obj)方法可以对参数指定的obj对象进行序列化,把得到的字节序列写到一个目标输出流中;
(2) java.io.ObjectInputStream:表示对象输入流,它的readObject()方法源输入流中读取字节列,再把它们反序列化成为一个对象,并将其返回;
② 实现序列化的要求:只有实现了Serializable或Externalizable接口的类的对象才能被序列化,否则抛出异常!
③ 实现Java对象序列化与反序列化的方法
假定一个User类,它的对象需要序列化,可以有如下三种方法:
(1) 若User类仅仅实现了Serializable接口,则可以按照如下方式进行序列化和反序列化。ObjectOutputStream采用默认的序列化方式,对User对象的非transient的实例变量进行序列化;ObjectInputStream采用默认的反序列化方式,对User对象的非transient的实例变量进行反序列化。
(2) 若User类实现了Serializable接口,并且还定义了readObject(ObjectInputStream in)和writeObject(ObjectOutputStream out),则采用以下方式进行序列化和反序列化。ObjectOutputStream调用User对象的writeObject(ObjectOutputStream out)的方法进行序列化;ObjectInputStream会调用User对象的readObject(ObjectInputStream in)的方法进行反序列化。
(3) 若User类实现了Externalnalizable接口,且User类必须实现readExternal(ObjectInput in)和writeExternal(ObjectOutput out)方法,则按照以下方式进行序列化与反序列化。ObjectOutputStream调用User对象的writeExternal(ObjectOutput out)的方法进行序列化;ObjectInputStream调用User对象的readExternal(ObjectInput in)的方法进行反序列化。
④ JDK类库中序列化的步骤
一,创建一个对象输出流,它可以包装一个其它类型的目标输出流,如文件输出流:
ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("D:\\object.out"));
二,通过对象输出流的writeObject()方法写对象:
oos.writeObject(new User("xuliugen", "123456", "male"));
⑤ JDK类库中反序列化的步骤
一,创建一个对象输入流,它可以包装一个其它类型输入流,如文件输入流:
ObjectInputStream ois= new ObjectInputStream(new FileInputStream("object.out"));
二,通过对象输出流的readObject()方法读取对象:
User user = (User) ois.readObject();
4. 动态代理是什么?有哪些应用?
答:
动态代理是运行时动态生成代理类。
动态代理的应用有spring aop、hibernate数据查询、测试框架的后端mock、rpc,Java 注解对象获取等。
5. 描述动态代理的几种实现方式,分别说出相应的优缺点?它们的底层分别是怎么实现的(jdk动态代理与cglib动态代理的区别)
答:
动态代理有两种实现方式,分别是:jdk动态代理和cglib动态代理。
jdk动态代理的前提是目标类必须实现一个接口,代理对象跟目标类实现一个接口,从而避过虚拟机的校验。
cglib动态代理是继承并重写目标类,所以目标类和方法不能被声明成final。
7. 如何在父类中为子类自动完成所有的hashcode和equals实现?这么做有何优劣。(说一说你对java.lang.Object对象中hashCode和equals方法的理解。在什么场景下需要重新实现这两个方法。)
答:
父类的equals不一定满足子类的equals需求。比如所有的对象都继承Object,默认使用的是Object的equals方法,在比较两个对象的时候,是看他们是否指向同一个地址。
但是我们的需求是对象的某个属性相同,就相等了,而默认的equals方法满足不了当前的需求,所以我们要重写equals方法。
如果重写了equals方法就必须重写hashcode方法,否则就会降低map等集合的索引速度。
8. 有没有可能,2个不相等的对象有同hashcode。
答:
有可能,最简单的方法,百分百实现的方式就是重写hashcode()。
5. Java对象拷贝模块
1. 为什么要使用克隆?
答:
克隆的对象可能包含一些已经修改过的属性,而new出来的对象的属性都还是初始化时候的值,所以当需要一个新的对象来保存当前对象的”状态“就靠克隆方法了。
2. 深拷贝和浅拷贝区别。
答:
浅拷贝只拷贝指针,深拷贝就是拷贝他的值,重新生成的对象。
6. JavaWeb与tomcat模块
1. tomcat集群怎么保证同步
答:
① 同步方式:关于集群的具体同步机制,tomcat提供了两种,一种是集群增量会话管理器,另一种是集群备份会话管理器。
② 集群增量会话管理器:这是一种全节点复制模式,全节点复制指的是集群中一个节点发生改变后会同步到其余全部节点。而非全节点复制,指的是集群中一个节点发生改变后,只同步到其余一个或部分节点。除了这一特点,集群增量会话管理器还具有只同步会话增量的特点,增量是以一个完整请求为周期,也就是说在一个请求被响应之前同步到其余节点上。
③ 集群备份会话管理器:全节点复制模式存在的一个很大的问题就是用于备份的网络流量会随着节点数的增加而急速增加,这也就是无法构建较大规模集群的原因。为了解决这个问题,tomcat提出了集群备份会话管理器。每个会话只有一个备份。这样就可构建大规模的集群。
④ 同步组件:在上述无论是发送还是接收信息的过程中,使用到的组件主要有三个:Manager,Cluster,tribes。简单来说,Manager的作用是将操作的信息记录下来,然后序列化交给Cluster,接着Cluster是依赖于tribes将信息发送出去的。其余节点收到信息后,按照相反的流程一步步传到Manager,经过反序列化之后使该节点同步传递过来的操作信息。
2. Spring IoC有什么好处
答:
参考资料:https://blog.csdn.net/xuefeiliuyuxiu/article/details/79181540
要了解控制反转,需要先了解软件设计的一个重要思想:依赖倒置原则。
什么事依赖倒置原则?假设我们设计一辆汽车:先设计轮子,然后根据轮子大小设计底盘,接着根据底盘设计车身,最后根据车身设计好整个汽车。这里就出现了一个“依赖”关系:汽车依赖车身,车身依赖底盘,底盘依赖轮子。但这种设计维护性很低。
换一种思路:我们先设计汽车的大概样子,然后根据汽车的样子来设计车身,根据车身来设计底盘,最后根据底盘来设计轮子。这时候,依赖关系就倒置过来了:轮子依赖底盘,底盘依赖车身,车身依赖汽车。
这时候,上司再说要改动轮子的设计,我们就只需要改动轮子的设计,而不需要动底盘、车身、汽车的设计了。
这就是依赖倒置原则——把原本的高层建筑依赖底层建筑“倒置”过来,变成底层建筑依赖高层建筑。高层建筑决定需要什么,底层去实现这样的需求,但是高层并不用管底层是怎么实现的。
控制反转就是依赖倒置原则的一种代码设计的思路。具体采用的方法就是所谓的依赖注入。这几种概念的关系大概如下:
为了理解这几个概念,我们还是用上面汽车的例子。只不过这次换成代码,我们先定义四个Class,车、车身、底盘、轮胎。然后初始化这辆车,最后跑这辆车。代码结构如下:
这样,就相当于上面第一个例子,上层建筑依赖下层建筑——每一个类的构造函数都直接调用了底层代码的构造函数。假设我们需要改动一下轮胎(Tire)类,把它的尺寸变成动态的,而不是一直都是30。我们需要像上面这样改。
由于我们修改了轮胎的定义,为了让整个程序正常运行,我们需要做以下改动:
由此我们可以看到,仅仅是为了修改轮胎的构造函数,这种设计却需要修改整个上层所有类的构造函数!在软件工程中,这样的设计几乎是不可维护的——在实际工程项目中,有的类可能会是几千个类的底层,如果每次修改这个类,我们都要修改所有以它作为依赖的类,那软件的维护成本就太高了。
所以我们需要进行控制反转(IoC),即上层控制下层,而不是下层控制着上层。我们用依赖注入(Dependency Injection)这种方式来实现控制反转。所谓依赖注入,就是把底层类作为参数传入上层类,实现上层类对下层类的“控制”。这里我们用构造方法传递的依赖注入方式重新写车类的定义:
这里我只需要修改轮胎类就行了,不用修改其他任何上层类。这显然是更容易维护的代码。
这里我们采用的构造函数传入的方式进行的依赖注入。其实还有另外两种方法:Setter传递和接口传递,核心思路都是一样的,都是为了实现控制反转。
那什么是控制反转容器(IoC Container)呢?其实上面的例子中,对车类进行初始化的那段代码发生的地方,就是控制反转容器。
因为采用了依赖注入,在初始化的过程中就不可避免的会写大量的new。这里IoC容器就解决了这个问题。这个容器可以自动对你的代码进行初始化,你只需要维护一个Configuration(可以是xml,也可以是一段代码),而不用每次初始化一辆车都要亲手去写那一大段初始化的代码。这是引入IoC Container的第一个好处。
IoC Container的第二个好处是:我们在创建实例的时候不需要了解其中的细节。在上面的例子中,我们自己手动创建一个车instance时候,是从底层往上层new的:
这个过程中,我们需要了解整个Car/Framework/Bottom/Tire类构造函数是怎么定义的,才能一步一步new注入。
而IoC Container在进行这个工作的时候是反过来的,它先从最上层开始往下找依赖关系,到达最底层之后再往上一步一步new。
实际项目中,有Service Class可能是十年前写的,有几百个类作为它的底层。假设我们新写一个API需要实例化这个Service,我们总不可能回去搞清楚这几百个类的构造函数吧。IoC Container的这个特性就很完美的解决了这类问题——因为这个架构要求你在写class的时候需要写相应的Config文件,所以你要初始化很久以前的Service类的时候,前人都已经写好了Config文件,你直接在需要用的地方注入这个Service就可以了。这大大增加了项目的可维护性且降低了开发难度。
6. 异常模块
1. error和exception的区别,CheckedException,RuntimeException的区别。
答:
①Error(错误)表示系统级的错误和程序不必处理的异常,是java运行环境中的内部错误或者硬件问题。比如:内存资源不足等。对于这种错误,程序基本无能为力,除了退出运行外别无选择,它是由Java虚拟机抛出的。
②Exception(违例)表示需要捕捉或者需要程序进行处理的异常,它处理的是因为程序设计的瑕疵而引起的问题或者在外的输入等引起的一般性问题,是程序必须处理的。
Exception又分为运行时异常,受检查异常。
1) RuntimeException(运行时异常),表示无法让程序恢复的异常,导致的原因通常是因为执行了错误的操作,建议终止程序,因此,编译器不检查这些异常。
2) CheckedException(受检查异常),是表示程序可以处理的异常,也即表示程序可以修复(由程序自己接受异常并且做出处理),所以称之为受检查异常。
2. 请列出5个运行时异常。
答:
NullPointerException、IndexOutOfBoundsException、ClassCastException、ArrayStoreException、BufferOverflowException
6. JVM
1. 什么情况下会发生栈内存溢出?
答:
栈(JVM Stack)存放主要是栈帧(局部变量表,操作数栈,动态链接,方法出口信息)的地方。
与线程栈相关的内存异常有两个:
1)、StackOverflowError(方法调用层次太深,内存不够新建栈帧)
2)、OutOfMemoryError(线程太多,内存不够新建线程)
发生栈内存溢出的情况可能是:
①方法创建了一个很大的对象,如List,Array
②是否产生了循环调用方法、死循环,不停的产生栈帧,栈帧充满了整个栈后溢出
③是否引用了较大的全局变量
2. JVM的内存结构,Eden和Survivor比例。
答:
JVM分为堆内存、方法区,栈内存、本地方法栈、程序计数器
具体参考:https://www.cnblogs.com/swordfall/p/10723938.html
3. JVM中一次完整的GC流程是怎样的,对象如何晋升到老年代,说说你知道的几种主要的JVM参数。
答:
对象诞生即新生代 -> eden,在进行minor gc过程中,如果依旧存活,移动到from,变成Survivor,进行标记代数,如此检查一定次数后,晋升为老年代。
对象晋升老年代一共有三个可能:
①当对象达到成年,经历过15次GC(默认15次,可配置),对象就晋升为老年代;
②大的对象会直接在老年代创建;
③新生代跟幸存区内存不足时,对象可能晋升到老生代;
jvm参数:
-Xms:初始堆大小
-Xmx:堆最大内存
-Xss:栈内存
-XX:PermSize 初始永久带内存
-XX:MaxPermSize最大永久带内存
4. 你们线上应用的JVM参数有哪些。
答:
-XX:PermSize=128M
-XX:MaxPermSize=512m
-Xms512m
-Xmx1024m
-XX:NewSize=64m
-XX:MaxNewSize=256m
-verbose:gc
-XX:+PrintGCDetails
-XX:+PrintGCTimeStamps
4. 你知道哪几种垃圾收集器,各自的优缺点,重点讲下cms,包括原理,流程,优缺点
答:
https://www.cnblogs.com/swordfall/p/10734403.html
5. 垃圾回收算法的实现原理
答:
常用的垃圾回收算法有两种:引用计数和可达性分析
引用计数:是增加一个字段来标识当前的引用次数,引用计数为0的就是可以GC的。但是引用计数不能解决循环引用的问题
可达性分析:就是通过一系列GC ROOT的对象作为起点,向下搜索,搜索所有没有与当前对象GC ROOT有引用关系的对象。这些对象就是可以GC的
6. 当出现了内存溢出,你怎么排错
答:
①首先控制台查看错误日志
②然后使用jdk自带的jvisualvm工具查看系统的堆栈日志
③定位出内存溢出的空间:堆,栈还是永久代(jdk8以后不会出现永久代的内存溢出)
④如果是堆内存溢出,看是否创建了超大的对象
⑤如果是栈内存溢出,看是否创建了超大的对象,或者产生了死循环
7. JVM内存模型的相关知识了解多少,比如重排序,内存屏障,happen-before,主内存,工作内存等。
答:
重排序:jvm虚拟机允许在不影响代码最终结果的情况下,可以乱序执行
内存屏障:可以阻挡编译器的优化,也可以阻挡处理器的优化
happens-before原则:
①一个线程的A操作总是在B之前,那多线程的A操作肯定在B之前
②monitor再加锁的情况下,持有锁的肯定先执行
③volatile修饰的情况下,写先于读发生
④线程启动在一起之前start
⑤线程死亡在一切之后end
⑥线程操作在一切线程中断之前
⑦一个对象构造函数的结束都在该对象的finalizer的开始之前
⑧传递性,如果A肯定在B之前,B肯定在C之前,那A肯定是在C之前
主内存:所有线程共享的内存空间
工作内存:每个线程特有的内存空间
8. g1和cms区别,吞吐量优先和响应优先的垃圾收集器选择。
答:
CMS:并发标记清除。它的主要步骤有:初始收集,并发标记,重新标记,并发清除(删除),重置
G1:主要步骤:初始标记,并发标记,重新标记,复制清除(整理)
CMS的缺点是对CPU的要求比较高。G1是将内存化成了多块,所有对内段的大小有很大的要求。
CMS是清除,所以会存在很多的内存碎片。G1是整理,所以碎片空间较小。
吞吐量优先:G1
响应优先:CMS
9. gc
7. 网络编程
1. http协议格式中get和post的区别?
答:
Http协议定义了很多与服务器交互的方法,最基本的有4种,分别是GET, POST, PUT, DELETE。一个URL地址用于描述一个网络上的资源,而HTTP中的GET, POST, PUT, DELETE就对应着这个资源的查,改,增,删4个操作。我们最常见的就是GET和POST了。GET一般用于获取/查询资源信息,而POST一般用于更新资源信息。
GET和POST的区别:
①GET提交的数据会放在URL之后,以”?”分割URL和传输数据,参数之间以&相连,如EditPosts.aspx?name=test1&id=123456.(注意对于用户登录来说,get是不安全的,网页直接显示你的用户名和密码);POST方法是把提交的数据放在HTTP包的Body中;
②GET提交的数据大小有限制(因为浏览器对URL的长度有限制),而POST方法提交的数据没有限制;
③GET方式需要使用Request.QueryString来取得变量的值,而POST方式通过Request.Form来获取变量的值;
④GET方式提交数据,会带来安全问题,比如一个登录页面,通过GET方式提交数据时,用户名和密码将出现在URL上,如果页面可以被缓存或者其他人可以访问这台机器,就可以从历史记录获得该用户的账号和密码。
2.
8. 算法
1. 二分查找算法
答:
用二分查找在已排序的数组中查看该数组是否含有一个特定的值。速度是非常快速的。
迭代方式:
public int BinarySearchIteration(int[] array, int key) { int begin = 0; int end = array.Length - 1; while (begin <= end) { int mid = begin + (end - begin) / 2; if (array[mid] > key) { end = mid - 1; } else if (array[mid] < key) { begin = mid + 1; } else { return mid; } } return -1; }
2. 快速排序算法
9. 数据库
1. Mysql的三大引擎是啥?
答:
mysql经常使用的引擎有InnoDB,MyISAM,Memory,默认是InnoDB。
①InnoDB:磁盘表,支持事务,支持行级锁,B+ Tree索引
ps:
优点:具有良好的ACID特性。适用于高并发,更新操作比较多的表。需要使用事务的表。对自动灾难恢复有要求的表。
缺点:读写效率相对MYISAM比较差。占用的磁盘空间比较大。
使用场景:InnoDB用于事务处理应用程序,具有众多特性,包括ACID事务支持。如果应用中需要执行大量的INSERT或UPDATE操作,则应该使用InnoDB,这样可以提高多用户并发操作的性能。
②MyISAM:磁盘表,不支持事务,支持表级锁,B+ Tree索引
ps:
优点:占用空间小,措置速度快(相对InnoDB来说)。
缺点:不支持事务的完整性和并发性。
使用场景:MyISAM管理非事务表。它提供高速存储和检索,以及全文搜索能力。如果应用中需要执行大量的SELECT查询,做很多count的计算,那么MyISAM是更好的选择。
③MEMORY(Heap):内存表,不支持事务,表级锁,Hash索引,不支持Blob、Text大类型
ps:
优点:速度要求快的,临时数据
缺点:丢失以后,对项目整体没有或者负面影响不大的时候。
2. Mysql中,读多写少可用什么引擎?
答:
MyISAM。
3. 假如要统计多个表应该用什么引擎?
答:
MyISAM
4. 了解nosql吗?redis?
答:
nosql是非关系型的数据库。redis为键值对的内存存储数据库。支持的键值数据类型为字符串类型,list类型,map类型,set类型,unset类型。
5.怎么保证redis和db中的数据一致
答:
如果仅仅查询的话,缓存的数据和数据库的数据是没问题的。但是,当我们要更新时候呢?各种情况很可能就造成数据库的数据和缓存的数据不一致了。
从理论上说,只要我们设置了键的过期时间,我们就能保证缓存和数据库的数据最终是一致的。因为只要缓存数据过期了,就会被删除。随后读的时候,因为缓存里没有,就可以查数据库的数据,然后将数据库查出来的数据写入到缓存中。除了设置过期时间,我们还需要做更多的措施来尽量避免数据库与缓存处于不一致的情况发生。
一般来说,执行更新操作时,我们会有两种选择:
- 先操作数据库,再删除缓存
- 先删除缓存,再操作数据库
首先,要明确的是,无论我们选择哪个,我们都希望这两个操作要么同时成功,要么同时失败。所以,这会演变成一个分布式事务的问题。所以,如果原子性被破环了,可能会有以下的情况:①操作数据库成功了,删除缓存失败了;②删除缓存成功了,操作数据库失败了。所以如果第一步已经失败了,我们直接返回Exception出去就好了,第二部根本不会执行。
缓存为什么采取删除而不是更新呢,原因如下:
① 高并发环境下,无论是先操作数据库还是后操作数据库而言,如果加上更新缓存,那就更加容易导致数据库与缓存数据不一致问题。(删除缓存直接和简单很多);
② 如果每次更新了数据库,都要更新缓存(这里指的是频繁更新的场景,这会耗费一定的性能),倒不如直接删除掉。等再次读取时,缓存里没有,再到数据库里找,在数据库找到之后再写到缓存里边去(体现懒加载)。
1) 先更新数据库,再删除缓存
如果第一步成功(操作数据库),第二步失败(删除缓存),会导致数据库里是新数据,而缓存是旧数据;如果第一步(操作数据库)就失败了,我们可以直接返回错误(Exception),不会出现数据不一致。
删除缓存失败的解决思路:
- 先删除缓存,成功;
- 自己消费消息,获得需要删除的key
- 不断重试删除操作,直到成功
2) 先删除缓存,再更新数据库
如果第一步成功(删除缓存),第二步失败(更新数据库),数据库和缓存的数据还是一致的;如果第一步(删除缓存)就失败了,我们可以直接返回错误(Exception),数据库和缓存的数据还是一致的。
但是在并发场景下,还是有问题的,比如线程A删除了缓存;线程B查询,发现缓存已不存在;线程B去数据库查询得到旧值,并把旧值写入缓存;线程A将新值写入数据库。所以也会导致数据库和缓存不一致的问题。
并发下解决数据库与缓存不一致的思路:将删除缓存、修改数据库、读取缓存等的操作积压到队列里边,实现串行化。
3) 其他保障数据一致的方案与资料
可以用databus或者阿里的canal监听binlog进行更新。
10. 操作系统
1. Linux系统下你关注过哪些内核参数,说说你知道的
答:
2. 用一行命令查看文件的最后五行,前五行。
答:
tail -n 5 filename / head -n 5 filename
3. 用一行命令输出正在运行的java进程
答:
ps -ef | grep java
4.
总结
【参考资料】
https://blog.csdn.net/youanyyou/article/details/82142014
https://blog.csdn.net/zhanglei082319/article/details/87872156
https://juejin.im/post/5b97486cf265da0ac669347f#heading-0
https://www.cnblogs.com/kkdn/p/9039601.html
https://www.cnblogs.com/xyfer1018/p/10434827.html
http://blog.sina.com.cn/s/blog_73b4b91f0102xlkm.html
http://www.vcchar.com/thread-36870-1-1.html
https://www.cnblogs.com/kevingrace/p/5685355.html
https://www.cnblogs.com/lxli/p/8205854.html
https://blog.csdn.net/yitian_66/article/details/81010434 JAVA8新特性
https://learnku.com/articles/22363