java基础
一、前置知识
1.原码反码补码
1.正数的原码、反码、补码都一样
2.负数的反码 = 它的原码符号位不变,其他位取反(取反的意思:0 换成 1 、 1 换成 0 )
3.负数的补码 = 它的反码 +1
4.0的反码、补码都是0
2.进制转换
任意进制到十进制的转换
十进制到任意进制转换
十进制到二进制的转换
十进制到十六进制的转换
3.位运算
二、基础语法
1.重载和重写的区别
在编译阶段根据静态类型可以确定重载方法
重写方法是在运行期间动态分派的
2.面向对象的三大特性
封装
继承
继承是面向对象三大特征之一,可以使得子类具有父类的属性和方法,还可以在子类中重新定义,以及追加属性和方法。
Java中类只支持单继承,不支持多继承
Java中类支持多层继承
多态
3.创建字符串对象的区别对比
4.== 和 equals 区别是什么、String 中的 equals 方法是如何重写的、为什么要重写 equals 方法、为什么要重写 hashCode 方法
== 和 equals 区别是什么
String中的equals方法是如何重写的
public boolean equals(Object anObject) {
if (this == anObject) {
return true;
}
if (anObject instanceof String) {
String anotherString = (String)anObject;
int n = value.length;
if (n == anotherString.value.length) {
char v1[] = value;
char v2[] = anotherString.value;
int i = 0;
while (n-- != 0) {
if (v1[i] != v2[i])
return false;
i++;
}
return true;
}
}
return false;
}
内部实现分为三个步骤:先比较引用是否相同(是否为同一对象),再判断类型是否一致(是否为同一类型),最后比较内容是否一致。
为什么要重写equals方法
1.举例说明不重写会出现什么问题
public static void main(String[] args) {
Student stu1 = new Student("张三", 20);
Student stu2 = new Student("张三", 20);
System.out.println(stu1.equals(stu2));
}
不重写结果会返回false
2.说明出现问题的原因
public boolean equals(Object obj) {
return (this == obj);
}
equals方法默认继承的Object类,默认比较两个对象的内存地址
3.说明重写后解决了问题
public class Student {
private String name;
private Integer stuId;
public Student(String name, Integer stuId) {
this.name = name;
this.stuId = stuId;
}
@Override
public boolean equals(Object o) {
if (this == o) {
return true;
}
if (!(o instanceof Student)) {
return false;
}
Student student = (Student) o;
return Objects.equals(name, student.name) &&
Objects.equals(stuId, student.stuId);
}
}
重写后返回true
为什么要重写hashCode方法
1.举例说明不重写会出现什么问题
public static void main(String[] args) {
Student stu1 = new Student("张三", 20);
Student stu2 = new Student("张三", 20);
Map<Student, Integer> map = new HashMap<>();
map.put(stu1, 1);
map.put(stu2, 2);
System.out.println(map);
}
利用hashmap举例,上面已经重写了equals方法,理论上key相同的话,应该数据会被覆盖,但实际上打印了两个值,如下
2.说明出现问题的原因
因为在HashMap中,是根据key的hashcode值确定数组下标位置的,如果数组下标位置相同,先比较key的hashcode值,如果相同,再用equals方法对比,如果相等,则覆盖。
而如果不重写hashcode方法,默认比较的是物理地址,则stu1和stu2的hashcode值不同
3.说明重写后解决了问题
@Override
public int hashCode() {
return Objects.hash(name, stuId);
}
重写后两个对象的hashcode值相同,可以覆盖,结果打印一个值
5.String s1 = new String(“abc”)在内存中创建了几个对象。
1.说出结果
创建 1 个或者 2 个对象
2.解释结果
new String 会先去常量池中判断有没有此字符串,如果有则只在堆上创建一个字符串并且指向常量池中的字符串,如果常量池中没有此字符串,则会创建 2 个对象,先在常量池中新建此字符串,然后把此引用返回给堆上的对象
7.描述一下值传递和引用传递的区别
1.解释概念
值传递(pass by value)是指在调用函数时将实际参数复制一份传递到函数中,在函数内对参数进行修改,不会影响到实际参数。
引用传递(pass by reference)是指在调用函数时将实际参数的地址直接传递到函数内,在函数内对参数所进行的修改,将影响到实际参数。
2.java参数传递特点
如果是基础类型,那么在方法传递的时候复制的是(栈中)基础类型的引用和值,如果是引用类型复制的是(栈中)引用地址。
3.结论
在Java中本质上只有值传递,也就说Java的传参只会传递它的副本,并不会传递参数本身
详解
9.请描述一下static关键字和final关键字的用法
static关键字
1.总体说明static关键字的应用范围
static 关键字可用于变量、方法、代码块和内部类,表示某个特定的成员只属于某个类本身,而不是该类的某个对象。
2.分条说明用法
2.1静态变量
1)由于静态变量属于一个类,所以不要通过对象引用来访问,而应该直接通过类名来访问。
2)不需要初始化类就可以访问静态变量。
2.2静态方法
1)Java 中的静态方法在编译时解析,因为静态方法不能被重写(方法重写发生在运行时阶段,为了多态)。
2)抽象方法不能是静态的。
3)静态方法不能使用 this 和 super 关键字。
4)成员方法可以直接访问其他成员方法和成员变量。
5)成员方法也可以直接方法静态方法和静态变量。
6)静态方法可以访问所有其他静态方法和静态变量。
7)静态方法无法直接访问成员方法和成员变量。
2.3静态代码块
1)一个类可以有多个静态代码块。
2)静态代码块的解析和执行顺序和它在类中的位置保持一致。
2.4静态内部类
1)静态内部类不能访问外部类的所有成员变量。
2)静态内部类可以访问外部类的所有静态变量,包括私有静态变量。
3)外部类不能声明为 static。
final关键字
1.从使用上说
1.1final变量
final变量有成员变量或者是本地变量(方法内的局部变量),在类成员中final经常和static一起使用,作为类常量使用。其中类常量必须在声明时初始化,final成员常量可以在构造函数初始化。
1.2final方法
final方法表示该方法不能被子类的方法重写
1.3final类
final类不能被继承,final类中的方法默认也会是final类型的,java中的String类和Integer类都是final类型的。
2.使用细节
1)final成员变量必须在声明的时候初始化或者在构造器中初始化,否则就会报编译错误。final变量一旦被初始化后不能再次赋值。
2)本地变量必须在声明时赋值。 因为没有初始化的过程
3)在匿名类中所有变量都必须是final变量。
4)final方法不能被重写, final类不能被继承
5)接口中声明的所有变量本身是final的。类似于匿名类
6)final和abstract这两个关键字是反相关的,final类就不可能是abstract的。
7)final方法在编译阶段绑定,称为静态绑定(static binding)。
8)将类、方法、变量声明为final能够提高性能,这样JVM就有机会进行估计,然后优化。
3.final好处(为什么使用final)
1)提高了性能,JVM在常量池中会缓存final变量
2)final变量在多线程中并发安全,无需额外的同步开销
3)final方法是静态编译的,提高了调用速度
4)final类创建的对象是只可读的,在多线程可以安全共享
10.接口和抽象类的区别是什么
1.语法层面上的区别
1)抽象类可以提供成员方法的实现细节,而接口中只能存在public abstract 方法;
2)抽象类中的成员变量可以是各种类型的,而接口中的成员变量只能是public static final类型的;
3)接口中不能含有静态代码块以及静态方法,而抽象类可以有静态代码块和静态方法;
4)一个类只能继承一个抽象类,而一个类却可以实现多个接口。
2.设计层面上的区别
2.1抽象类是对整个类整体进行抽象,包括属性、行为,但是接口却是对类局部(行为)进行抽象。
举个简单的例子,飞机和鸟是不同类的事物,但是它们都有一个共性,就是都会飞。那么在设计的时候,可以将飞机设计为一个类Airplane,将鸟设计为一个类Bird,但是不能将 飞行 这个特性也设计为类,因此它只是一个行为特性,并不是对一类事物的抽象描述。此时可以将 飞行 设计为一个接口Fly,包含方法fly( ),然后Airplane和Bird分别根据自己的需要实现Fly这个接口。
2.2抽象类作为很多子类的父类,它是一种模板式设计。而接口是一种行为规范,它是一种辐射式设计。
模板式设计:
大家都用过ppt里面的模板,如果用模板A设计了ppt B和ppt C,ppt B和ppt C公共的部分就是模板A了,如果它们的公共部分需要改动,则只需要改动模板A就可以了,不需要重新对ppt B和ppt C进行改动。
辐射式设计:
比如某个电梯都装了某种报警器,一旦要更新报警器,就必须全部更新
11.Integer缓存池
1.缓冲范围-128-127
2.面试题
1.Integer i1 = new Integer(127);
Integer i2 = new Integer(127);
System.out.println(i1 == i2); //False,i1和i2都是通过new分配的内存空间,所以指向两个不同的内存空间
System.out.println(i1.equals(i2));//True
2.Integer i3 = new Integer(128);
Integer i4 = new Integer(128);
System.out.println(i3 == i4); //False,i3和i4都是通过new分配的内存空间,所以指向两个不同的内存空间
System.out.println(i3.equals(i4));//True
3.Integer i5 = 127;
Integer i6 = 127;
System.out.println(i5 == i6); //True
System.out.println(i5.equals(i6));//True
4.Integer i7 = 128;
Integer i8 = 128;
System.out.println(i7 == i8); //False
System.out.println(i7.equals(i8));//True
3.底层源码和原理
private static class IntegerCache {
static final int low = -128;
static final int high;
static final Integer cache[];
static {
// high value may be configured by property
int h = 127;
String integerCacheHighPropValue =
VM.getSavedProperty("java.lang.Integer.IntegerCache.high");
if (integerCacheHighPropValue != null) {
try {
int i = parseInt(integerCacheHighPropValue);
i = Math.max(i, 127);
// Maximum array size is Integer.MAX_VALUE
h = Math.min(i, Integer.MAX_VALUE - (-low) -1);
} catch( NumberFormatException nfe) {
// If the property cannot be parsed into an int, ignore it.
}
}
high = h;
cache = new Integer[(high - low) + 1];
int j = low;
for(int k = 0; k < cache.length; k++)
cache[k] = new Integer(j++);
// range [-128, 127] must be interned (JLS7 5.1.7)
assert IntegerCache.high >= 127;
}
private IntegerCache() {}
}
1)内部使用的cache数组作为缓存数据的机制;
2)使用了low、high分别作为缓存的数值的最小值和最大值;
3)low实际上是一个固定的值-128;
4)high是可以设置的:
12.为什么Java是解释性语言
1.编译性语言和解释性语言概念
编译型语言:把做好的源程序全部编译成二进制代码的可运行程序。然后,可直接运行这个程序。
解释型语言:把做好的源程序翻译一句,然后执行一句,直至结束!
2.java为什么是解释性语言
虽然java也需要编译,编译成.class文件,但是并不是机器可以识别的语言,而是字节码,最终还是需要 jvm的解释,才能在各个平台执行,这同时也是java跨平台的原因。
13.权限修饰符
14.代码块
概述
在Java中,使用 { } 括起来的代码被称为代码块
分类
局部代码块
public class Test {
/*
局部代码块
位置:方法中定义
作用:限定变量的生命周期,及早释放,提高内存利用率
*/
public static void main(String[] args) {
{
int a = 10;
System.out.println(a);
}
// System.out.println(a);
}
}
构造代码块
public class Test {
/*
构造代码块:
位置:类中方法外定义
特点:每次构造方法执行的时,都会执行该代码块中的代码,并且在构造方法执行前执行
作用:将多个构造方法中相同的代码,抽取到构造代码块中,提高代码的复用性
*/
public static void main(String[] args) {
Student stu1 = new Student();
Student stu2 = new Student(10);
}
}
class Student {
{
System.out.println("好好学习");
}
public Student(){
System.out.println("空参数构造方法");
}
public Student(int a){
System.out.println("带参数构造方法...........");
}
}
静态代码块
public class Test {
/*
静态代码块:
位置:类中方法外定义
特点:需要通过static关键字修饰,随着类的加载而加载,并且只执行一次
作用:在类加载的时候做一些数据初始化的操作
*/
public static void main(String[] args) {
Person p1 = new Person();
Person p2 = new Person(10);
}
}
class Person {
static {
System.out.println("我是静态代码块, 我执行了");
}
public Person(){
System.out.println("我是Person类的空参数构造方法");
}
public Person(int a){
System.out.println("我是Person类的带...........参数构造方法");
}
}
15.内部类
概念
在一个类中定义一个类。举例:在一个类A的内部定义一个类B,类B就被称为内部类
定义格式
/*
格式:
class 外部类名{
修饰符 class 内部类名{
}
}
*/
class Outer {
public class Inner {
}
}
访问特点
/*
内部类访问特点:
内部类可以直接访问外部类的成员,包括私有
外部类要访问内部类的成员,必须创建对象
*/
public class Outer {
private int num = 10;
public class Inner {
public void show() {
System.out.println(num);
}
}
public void method() {
Inner i = new Inner();
i.show();
}
}
成员内部类
class Outer {
private int num = 10;
private class Inner {
public void show() {
System.out.println(num);
}
}
public void method() {
Inner i = new Inner();
i.show();
}
}
public class InnerDemo {
public static void main(String[] args) {
//Outer.Inner oi = new Outer().new Inner();
//oi.show();
Outer o = new Outer();
o.method();
}
}
class Outer {
static class Inner {
public void show(){
System.out.println("inner..show");
}
public static void method(){
System.out.println("inner..method");
}
}
}
public class Test3Innerclass {
/*
静态成员内部类演示
*/
public static void main(String[] args) {
// 外部类名.内部类名 对象名 = new 外部类名.内部类名();
Outer.Inner oi = new Outer.Inner();
oi.show();
Outer.Inner.method();
}
}
局部内部类
class Outer {
private int num = 10;
public void method() {
int num2 = 20;
class Inner {
public void show() {
System.out.println(num);
System.out.println(num2);
}
}
Inner i = new Inner();
i.show();
}
}
public class OuterDemo {
public static void main(String[] args) {
Outer o = new Outer();
o.method();
}
}
匿名内部类
概述
匿名内部类在开发中的使用
当发现某个方法需要,接口或抽象类的子类对象,我们就可以传递一个匿名内部类过去,来简化传统的代码
/*
游泳接口
*/
interface Swimming {
void swim();
}
public class TestSwimming {
public static void main(String[] args) {
goSwimming(new Swimming() {
@Override
public void swim() {
System.out.println("铁汁, 我们去游泳吧");
}
});
}
/**
* 使用接口的方法
*/
public static void goSwimming(Swimming swimming){
/*
Swimming swim = new Swimming() {
@Override
public void swim() {
System.out.println("铁汁, 我们去游泳吧");
}
}
*/
swimming.swim();
}
}
16.Lambda表达式
函数式编程思想
Lambda表达式的使用前提
Lambda表达式和匿名内部类的区别
17.java中的异常
概述
编译时异常和运行时异常的区别
18.对象序列化和反序列化
19.反射
概述
获取Class类对象的三种方式
20.xml
概述
作用
作为配置文件的优势
常见的解析工具
DOM4J: 开源组织提供了一套XML的解析的API-dom4j,全称:Dom For Java
xml解析
xml解析就是从xml中获取到数据
21.注解
元注解
元注解就是描述注解的注解
二、String相关
1.String,String Builder,String Buffer 的区别
2.为什么String的是不可变的?
因为存储数据的char数组是使用final进行修饰的,所以不可变。
3.为什么String Buffer是线程安全的?
这是因为在StringBuffer类内,常用的方法都使用了synchronized 进行同步所以是线程安全的,然而StringBuilder并没有。这也就是运行速度StringBuilder > StringBuffer的原因了。
三、反射
1.什么是反射
Java 的反射机制是指在运行状态中,对于任意一个类都能够知道这个类所有的属性和方法; 并且对于任意一个对象,都能够调用它的任意一个方法;这种动态获取信息以及动态调用对象方法的功能成为Java语言的反射机制。
三、java中的集合类
1.HashMap相关、HashMap和 Hashtable的区别、HashMap和HashSet 区别、HashMap底层实现、HashMap的长度为什么是 2 的幂次方、HashMap多线程操作导致死循环问题、HashMap的线程安全实现有哪些、ConcurrentHashMap 的底层实现。
HashMap简介
HashMap采用key/value存储结构,每个key对应唯一的value,查询和修改的速度都很快,能达到O(1)的平均时间复杂度。它是非线程安全的,且不保证元素存储的顺序。key值和vaule都可以为空。
不保证元素存储的顺序
Map<String, String> mp = new HashMap<String, String>();
for (int i=0; i<10; i++) {
mp.put("key" + i, "value" + i);
}
for (Map.Entry<String, String> entry : mp.entrySet()) {
System.out.println(entry.getKey() + "-" + entry.getValue());
}
结果
key1-value1
key2-value2
key0-value0
key5-value5
key6-value6
key3-value3
key4-value4
key9-value9
key7-value7
key8-value8
如果想要有序,采用LinkedHashMap
HashMap存储结构
jdk1.7中采用数组+链表
1.7中hashmap有一个内部类Entry,每添加一个新的键值对就将它封装到一个Entry对象中,然后放进容器中。
jdk1.8采用数组+链表+红黑树
数组元素和链表节点采用内部类Node类实现,与jdk1.7的Entry类对比,仅仅只是换了名字。而红黑树采用TreeNode类实现
1)引入红黑树的原因
加快查询速度
数组的查询效率为O(1),链表的查询效率是O(k),红黑树的查询效率是O(log k),k为桶(数组的一个元素又称作桶)中的元素个数,所以当元素数量非常多的时候,转化为红黑树能极大地提高效率。
2)转换阈值
数组长度大于等于64,且链表长度大于等于8,将链表转换成红黑树。
当红黑树中节点数量小于等于6,则将红黑树还原回链表
3)为什么转换阈值是8和6
根据泊松分布,hash碰撞发生8次的概率非常低,此时链表性能已经非常差,后序可能还会继续发生hash碰撞。转换成红黑树主要出于性能考虑。
而红黑树转链表的阈值为6,主要是因为,如果也将该阈值设置于8,那么当hash碰撞在8时,会反生链表和红黑树的不停相互激荡转换,白白浪费资源。
4)为什么采用红黑树而不是平衡二叉树
对于插入删除等操作,红黑树效率更高。原因是平衡二叉树对于平衡性的要求更高,在调整平衡的过程中消耗更大。
详解
5)补充:红黑树的介绍
是一种特殊的二叉查找树
HashMap解决hash冲突的方法
1)hashmap采用的方法
链地址法
hashmap中的hash算法本质上分为三步:取key的hashCode值、高位运算、取模运算
方法一:
//jdk1.8 & jdk1.7
static final int hash(Object key) {
int h;
// h = key.hashCode() 为第一步 取hashCode值
// h ^ (h >>> 16) 为第二步 高位参与运算
return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}
方法二:
//jdk1.7的源码,jdk1.8没有这个方法,但是实现原理一样的
static int indexFor(int h, int length) {
//第三步 取模运算
return h & (length-1);
}
jdk1.8中优化了高位运算的算法,(h = key.hashCode()) ^ (h >>> 16)主要考虑性能优化,使得数组较小的时候,也能保证考虑到高低位都参与到Hash的计算中,同时不会有太大的开销。
第三步取模运算采用h & (table.length -1)。因为HashMap底层数组的长度总是2的n次方。当length总是2的n次方时,h& (length-1)运算等价于对length取模,也就是h%length,但是&比%具有更高的效率。
2)解决hash冲突的主要方法
(1)开放定址法(2)链地址法(3)再哈希法(4)公共溢出区域法
HashMap主要参数
//默认的初始容量为16
static final int DEFAULT_INITIAL_CAPACITY = 1 << 4;
//最大的容量为2的30次方
static final int MAXIMUM_CAPACITY = 1 << 30;
//默认的装载因子
static final float DEFAULT_LOAD_FACTOR = 0.75f;
1)为什么初始化容量是2的n次幂?
可以保证数据均匀插入,如果是其他值,可能数组的一些位置永远不会插入数据,浪费数组空间,加大hash冲突。
如果创建hashmap时,给的参数不是2的n次幂,则hashmap会根据运行,得到离2的n次幂最近的数
2)为什么加载因子设置为0.75,初始化临界值是12?
答题角度从过大过小都不太好来说明
HashMap扩容机制
- 概述
2)jdk1.7需要重新计算hash,而jdk1.8只需要看看原来的hash值新增的那个bit是1还是0就好了,是0的话索引没变,是1的话索引变成“原索引+oldCap”。 - 扩容后的HashMap容量是之前容量的两倍
HashMap的put方法
HashMap的get方法
HashMap为什么是线程不安全的
1)在jdk1.7中,在多线程环境下,扩容时会造成环形链或数据丢失
2)在jdk1.8中,在多线程环境下,会发生数据覆盖的情况
HashMap1.7和1.8区别
HashMap和HashTable区别
1)底层结构不一样
HashMap采用了数组+链表+红黑树,而Hashtable采用数组+链表
2)容量(capacity)默认值不一样
HashMap的容量默认值为16,而Hashtable的默认值是11
3)哈希表扩容算法不一样
HashMap的容量扩容按照原来的容量2,而Hashtable的容量扩容按照原来的容量2+1
4)键值对规则不一样
HashMap允许键值为null,而Hashtable不允许键值为null
5)应对多线程处理方式不一样
HashMap是非线程安全的,Hashtable是线程安全的,所以Hashtable效率比较低
详解
HashMap和HashSet区别
2.Arrays.asList获得的List使用时需要注意什么
3.Collection和Collections区别
4.你知道fail-fast和fail-safe吗
1.fail-fast 快速失败
1.1解释现象
在用迭代器遍历一个集合对象时,如果遍历过程中对集合对象的内容进行了增加、删除、修改操作,则会抛出ConcurrentModificationException。
1.2原理
迭代器在遍历时直接访问集合中的内容,并且在遍历过程中使用一个 modCount 变量。集合在被遍历期间如果内容发生变化,就会改变modCount的值。每当迭代器使用hashNext()/next()遍历下一个元素之前,都会检测modCount变量是否为expectedmodCount值,是的话就返回遍历;否则抛出ConcurrentModificationException异常,终止遍历。
1.3注意事项
这里异常的抛出条件是检测到 modCount!=expectedmodCount 这个条件。如果集合发生变化时修改modCount值刚好又设置为了expectedmodCount值,则异常不会抛出。因此,不能依赖于这个异常是否抛出而进行并发操作的编程,这个异常只建议用于检测并发修改的bug。
1.4场景
java.util包下的集合类都是快速失败的,不能在多线程下发生并发修改(迭代过程中被修改)。
详解
2.Java集合-失败安全
2.1解释现象
采用失败安全机制的集合容器,在遍历时不是直接在集合内容上访问的,而是先复制原有集合内容,在拷贝的集合上进行遍历。
2.2原理
由于迭代时是对原集合的拷贝进行遍历,所以在遍历过程中对原集合所作的修改并不能被迭代器检测到,所以不会触发ConcurrentModificationException。
2.3注意事项
基于拷贝内容的优点是避免了ConcurrentModificationException,但同样地,迭代器并不能访问到修改后的内容,即:迭代器遍历的是开始遍历那一刻拿到的集合拷贝,在遍历期间原集合发生的修改迭代器是不知道的。这也就是他的缺点,同时,由于是需要拷贝的,所以比较吃内存。
2.4场景
java.util.concurrent包下的容器都是安全失败,可以在多线程下并发使用,并发修改。
5.ArrayList和LinkedList 和Vector 的区别
6.Set和List区别、Set如何保证元素不重复
7.ConcurrentHashMap 1.7和1.8的区别:整体结构;put()方法、get()方法、rehash()方法、size()方法
多线程下 HashMap 有什么问题(ConcurrentHashMap出现背景)
1)JDK1.7中HashMap存在死循环问题
JDK1.8中HashMap存在元素覆盖问题
2)Hashtable可以解决线程安全问题,但是里面使用过多synchronized关键字,效率低下
3)Collections工具类可以生成SynchronizedMap解决线程安全问题,但也是锁住整个表,效率低下
4)在JDK1.7中ConcurrentHashMap采用锁分段技术,把整张表分成 N 个部分,并使元素尽量均匀的分布到每个部分中,分别给他们加锁,互相之间并不影响。每个部分就是一个 Segment(段)。
5)在jdk1.8中,进一步升级,采用Synchronized + CAS,把锁的粒度进一步降低,而放弃了 Segment 分段。
ConcurrentHashMap1.7原理
1.底层数据结构
1)数组+链表
2)每个Segment里边是由HashEntry组成的数组
3)可以把每个Segment看成是一个小的HashMap,其内部结构和HashMap是一模一样的
2.常用方法
2.1put方法
2.2get方法
1)计算hash值
2)先定位到 key 所在的Segment ,然后从主内存中取出最新的节点
2.3size方法
采用乐观锁的思想,认为在统计 size 的过程中,并没有发生 put, remove 等会改变 Segment 结构的操作。
如果发生了,就需要重试。如果重试2次都不成功(执行三次,第一次不能叫做重试),就只能强制把所有 Segment 都加锁之后,再统计了,以此来得到准确的结果。
2.4rehash方法
1)当put方法时,发现元素个数超过了阈值,则会扩容。
2)每个Segment只管它自己的扩容,互相之间并不影响。
ConcurrentHashMap1.8原理
1.底层数据结构
数组+链表+红黑树,加上一些并发处理
2.常用方法
2.1put方法
1)若当前桶为空,则通过 CAS 原子操作,把新节点插入到此位置
2)用synchronized锁的方式,来保证线程安全,给桶中第一个节点对象加锁
三、常用开发组件
1.版本控制工具
版本控制出现背景
SVN版本控制
概念
SVN是集中式版本控制系统,版本库是集中放在中央服务器的,而开发人员工作的时候,用的都是自己的电脑,
所以首先要从中央服务器下载最新的版本,然后开发,开发完后,需要把自己开发的代码提交到中央服务器。
SVN存在的问题
Git版本控制
概念
远程仓库工作流程
2.maven
出现背景
jar包不统一,jar不兼容
工程升级维护过程操作繁琐
概念
Maven的本质是一个项目管理工具,将项目开发和管理过程抽象成一个项目对象模型(POM)
Maven是用Java语言编写的。他管理的东西统统以面向对象的形式进行设计,最终他把一个项目看成一个对象,而这个对象叫做POM(project object model),即项目对象模型
Maven的作用
项目构建:提供标准的,跨平台的自动化构建项目的方式
依赖管理:方便快捷的管理项目依赖的资源(jar包),避免资源间的版本冲突等问题
统一开发结构:提供标准的,统一的项目开发结构
聚合和继承
聚合
继承
其他备选
24.讲一下CMS垃圾回收器
概述
CMS(Concurrent Mark Sweep)收集器是一种以获取最短回收停顿时间为目标的收集器,它是基于“标记-清除”算法实现的,并且常见的应用场景是互联网站或者B/S系统的服务端上的Java应用。
工作流程
1)初始标记:仅仅只是标记一下GC Roots能直接关联到的对象,速度很快,需要“Stop The World”。
2)并发标记:进行GC Roots Tracing的过程,在整个过程中耗时最长。
3)重新标记:为了修正并发标记期间因用户程序继续运作而导致标记产生变动的那一部分对象的标记记录,这个阶段的停顿时间一般会比初始标记阶段稍长一些,但远比并发标记的时间短。此阶段也需要“Stop The World”。
4)并发清除
CMS缺点
1)垃圾碎片的问题,我们都知道CMS是使用的是标记-清除算法的,所以不可避免的就是会出现垃圾碎片的问题。
2)一般CMS的GC耗时80%都在remark阶段,remark阶段停顿时间会很长
3)concurrent mode failure异常
执行CMS GC的过程中,同时业务线程也在运行,当年轻带空间满了,执行ygc时,需要将存活的对象放入到老年代,而此时老年代空间不足,这时CMS还没有机会回收老年带产生的,或者在做Minor GC的时候,新生代救助空间放不下,需要放入老年代,而老年代也放不下而产生的。
4)promotion failed
在进行Minor GC时,Survivor空间不足,对象只能放入老年代,而此时老年代也放不下造成的,多数是由于老年代有足够的空闲空间,但是由于碎片较多,新生代要转移到老年带的对象比较大,找不到一段连续区域存放这个对象导致的。
解决CMS缺点
1)垃圾碎片的问题
针对这个问题,这时候我们需要用到这个参数:-XX:CMSFullGCsBeforeCompaction=n 意思是说在上一次CMS并发GC执行过后,到底还要再执行多少次full GC才会做压缩。默认是0,也就是在默认配置下每次CMS GC顶不住了而要转入full GC的时候都会做压缩。
2)concurrent mode failure
3)remark阶段停顿时间会很长的问题
解决这个问题巨简单,加入-XX:+CMSScavengeBeforeRemark。在执行remark操作之前先做一次Young GC,目的在于减少年轻代对老年代的无效引用,降低remark时的开销。
25.JDK 动态代理和 GClib 动态代理、JDK 动态代理具体实现原理、CGLib 动态代理、两者对比
JDK动态代理
1.概念
代理类实例在程序运行时,由JVM根据反射机制动态的生成。也就是说代理类不是用户自己定义的,而是由JVM生成的。
2.核心类
JDK动态代理有两大核心类,它们都在Java的反射包下(java.lang.reflect),分别为InvocationHandler接口和Proxy类。
2.1InvocationHandler接口
我们创建的每一个代理实例都要有一个关联的InvocationHandler,并且在调用代理实例的方法时,会被转到InvocationHandler的invoke方法上。
2.2Proxy类
Proxy类提供了创建动态代理类及其实例的静态方法,该类也是动态代理类的超类。
getProxyClass
newProxyInstance
Proxy类主要用来获取动态代理对象,InvocationHandler接口主要用于方法调用的约束与增强。
3.使用方法
首先创建目标接口及其实现类
创建一个InvocationHandler接口的实现类
通过getProxyClass方法或者newProxyInstance方法获取代理实例
4.代理类特点
5.创建代理类原理
CGLib动态代理
26.Threadlocal内存泄漏问题
出现背景
ThreadLocal类用来提供线程内部的局部变量。这些变量在多线程环境下访问(通过get或set方法访问)时能保证各个线程里的变量相对独立于其他线程内的变量,ThreadLocal实例通常来说都是private static类型。
应用场景
ThreadLocal的主要应用场景为按线程多实例(每个线程对应一个实例)的对象的访问,并且这个对象很多地方都要用到。例如:同一个网站登录用户,每个用户服务器会为其开一个线程,每个线程中创建一个ThreadLocal,里面存用户基本信息等,在很多页面跳转时,会显示用户信息或者得到用户的一些信息等频繁操作,这样多线程之间并没有联系而且当前线程也可以及时获取想要的数据。
原理
ThreadLocal可以看做是一个容器,容器里面存放着属于当前线程的变量。ThreadLocal类提供了四个对外开放的接口方法,这也是用户操作ThreadLocal类的基本方法:
内存泄漏问题
最要命的是线程对象不被回收的情况,这就发生了真正意义上的内存泄露。比如使用线程池的时候,线程结束是不会销毁的,会再次使用的就可能出现内存泄露 。(在web应用中,每次http请求都是一个线程,tomcat容器配置使用线程池时会出现内存泄漏问题)
使用建议
1)使用ThreadLocal,建议用static修饰 static ThreadLocal headerLocal = new ThreadLocal();
2)使用完ThreadLocal后,执行remove操作,避免出现内存溢出情况。
详解
28.BIO、NIO、AIO区别
前置知识
时钟中断、区分操作系统的NIO和java中的NIO
Java中的IO原理
首先Java中的IO都是依赖操作系统内核进行的,我们程序中的IO读写其实调用的是操作系统内核中的read&write两大系统调用。
内核如何进行IO交互
同步与异步
同步指的是调用一旦开始,调用者必须等到方法调用返回后,才能继续后续的行为。即方法二一定要等到方法一执行完成后才可以执行。
异步指的是调用立刻返回,调用者不必等待方法内的代码执行结束,就可以继续后续的行为。(具体方法内的代码交由另外的线程执行完成后,可能会进行回调)。即执行方法一的时候,直接交给其他线程执行,不由主线程执行,也就不会阻塞主线程,所以方法二不必等到方法一完成即可开始执行。
阻塞与非阻塞
阻塞指的是遇到同步等待后,一直在原地等待同步方法处理完成。
非阻塞指的是遇到同步等待,不在原地等待,先去做其他的操作,隔断时间再来观察同步方法是否完成。
BIO、NIO、AIO通俗理解
A顾客去吃海底捞,就这样干坐着等了一小时,然后才开始吃火锅。(BIO)
B顾客去吃海底捞,他一看要等挺久,于是去逛商场,每次逛一会就跑回来看有没有排到他。于是他最后既购了物,又吃上海底捞了。(NIO)
C顾客去吃海底捞,由于他是高级会员,所以店长说,你去商场随便玩吧,等下有位置,我立马打电话给你。于是C顾客不用干坐着等,也不用每过一会儿就跑回来看有没有等到,最后也吃上了海底捞(AIO)