文章目录

  • Java基础
    • 数据类型
      • 不可变对象
      • String、StringBuffer和StringBuilder
      • ==、equals和hashcode
      • 值传递和引用传递
      • 集合
      • List
      • Map
      • Set
      • 关键字
      • static
      • final
      • 面向对象
      • 反射机制
      • 代理模式
      • 面经
    • 异常
    • 多线程
      • synchronized
      • 线程的实现方式
      • volatile
      • IO
      • 面经
    • jvm
      • 组成
      • 线程共享
      • 线程私有
      • 常量池
      • 垃圾回收
      • 回收算法
      • 类的加载
        • 加载
      • 面经
  • Struct
  • Hibernate
  • MyBatis
  • Spring
    • IOC控制反转(依赖注入DI)
    • AOP面向切片编程
    • 面经
  • SpringBoot
  • redis
  • Nginx

Java基础

数据类型

有八种基本的数据类型

  • int:8 Byte,-231~231-1
  • char:2 Byte
  • byte:1 Byte,-27~27-1
  • short:2 Byte,-215~215-1
  • long:8 Byte,-263~263-1
  • float:4 Byte
  • double:8 Byte
  • boolean:1、4 Byte

不可变对象

类似于python的可变和不可变

String、StringBuffer和StringBuilder

String

是不可变的,每次进行更改的时候都会创建一个新的String对象,然后把指针指向新的对象,频繁修改的话不要用这个,每次生成新的对象都会造成性能损耗,而且没用的对象多了以后会触发GC,性能降低。

对于一个字符串变量s,执行s+=“hello”,原有的s变量对象不会清除,会重新创建一个对象然后修改s的引用。

为什么对于一些敏感数据字符数组存储比字符串更安全?

因为字符串是不可变类,创建的对象会存储在内存池中,相同值的引用可以共享,直到被垃圾回收,但是即便不被使用还是要存留一段时间以后才能被回收,因此如果此时有程序可以访问到内存的一些区域就可以查看到这些信息,然后把敏感信息暴露出去。所以不安全。

StringBuffer
可变的,而且是线程安全的。修改值的时候不会创建新的对象,而是直接修改引用。
需要频繁修改字符串的时候可以使用这个,调用toString()转为字符串。
可以用append()和insert()方法将变量自动转为字符串插入到目标字符串里。

当多个String变量相加的时候,会自动将每一个转为StringBuffer然后进行操作,速度会慢很多,然后再把最后的Buffer转为String。如果一个字符串变量单纯是几个字符串加在一起,不是变量,java编译器会自动将其转为buffer进行相加,速度就会快不少。
StringBuilder
字符串变量,是线程不安全的。
适用于单线程

为了尽可能的会的更好的性能,在使用StringBuffer和StringBuilder的时候,最好指定长度,不指定的话默认为16

String和常量的内存划分

以下两种

String s="abc";
String ss=new String("abc");

前者会在堆中创建一个"abc"对象,将其引用存储在字符串常量池的StringTable里面,然后将这个引用赋值给s

而后者会创建两个"abc"对象,第一个和上面一样,在字符串常量池里,第二个对象则是因为调用了String的构造方法,初始化了一个新的对象。

因为在类编译的时候"abc"已经保存在常量池里了,再次new一下的话,在堆里就会有一个引用

==、equals和hashcode

等于号用于判断两个变量的值是否相等,对于基本变量类型就是比较值,如果是比如字符串,应该就是比较引用,如果指向同一个内存空间,就是相等。如果想比较具体的值那就不行了。

equals默认的其实和等号一样,就是直接调用等号进行判断,不一样的就是可以覆盖,就是通过重写让他判断具体的值。

哈希就是返回对象在内存中地址转换成的一个int值,如果没有重写,所有对象的值都不一样。
为什么重写equals也要重写hashcode
因为哈希冲突的存在,所以不同对象的哈希值可能会一致。
为了提高效率,再重写了hashcode方法以后,比较的时候会默认判断哈希值,如果不一样就不需要equals了。
如果哈希相等,不一定一样,需要继续equals。否则不一样,大大提高判断效率。
如果单纯重写equals判断值相同,那么可能还是会返回false,因为会先判断哈希,哈希不相等所以是false

值传递和引用传递

值传递是传递一个拷贝,引用就是传递一个变量地址。

但是其实本质上还是一个值传递,对于基本变量类型传递的是具体的值(传入一个拷贝),对于不可变对象比如stringbuffer传递的是引用的地址作为值(传入一个地址的拷贝)。

集合

主要有三种:List、Map和Set
主要分为两大类:

  • Collection接口:单一元素,List、Set和Queue
  • Map接口:键值对

List

ArrayList

存储的元素是有序的,可重复的,线程不安全的

底层实现是Object数组,因此支持随机搜索,插入删除的话会有一些效率影响,毕竟是数组。

无参的时候是一个空数组,添加数据的时候才会进行扩容操作,长度是10.(JDK6无参的时候默认长度是10)。接下来的扩容一般是当前长度的1.5倍

  • 插入和删除元素的效率根据元素位置不同,默认插入到尾部O(1),如果指定插入到第i个就是O(n-i),因为要移动前后i个元素
  • 可以随机访问,get(int index)

LinkedList
存储的元素是有序的,可重复的,线程不安全的
底层是双向链表,JDK1.6之前是双向循环,之后是双向,不支持随机索引
每一个元素都需要消耗比ArrayList更多的空间存储前驱和后继指针

  • 链式存储,头尾节点的插入删除较为方便O(1),其余位置需要移动O(n)
  • 不能够随机访问,只能顺序访问
  • 但是一般不会用,基本都用ArrayList

Vector
存储的元素是有序的,可重复的,线程安全的
底层是Object数组

list的迭代

  • 直接使用下标,for里面范围用list.size()获取,list.get(i)获取元素
  • 增强for:for (String str : data)
  • 使用迭代器:Iterator it = list.iterator(); while (it.hasNext()) {}
  • list迭代器(可向后也可向前迭代):ListIterator listIt = list.listIterator(); while (listIt.hasNext()) {}或者while (listIt.hasPrevious()) {}

Map

重要的hashmap

HashMap

一个HashMap跟面试官扯了半个小时

存储的元素是无序的,不可重复的,线程不安全的,因此单线程里效率非常高。

存储的时候,先调用hashcode()判断元素应该存储的位置,然后调用equals()判断当前位置是否有元素或者是否相同,相同就更新,不相同的话插入到链表里

多线程需要考虑HashTable和currentHashMap

底层是数组+链表+红黑树

  • 链表是为了解决哈希冲突,使用链地址法
  • 红黑树:当同一个key的value太多的时候会保存到链表里,但是当链表太长的话查找效率会很低,将链表转为红黑树查找效率会很高。当链表长度达到阈值(默认8)的时候,会转为红黑树;红黑树元素小于6个的时候转为链表

为什么是8和6:8的话是用概率算的,碰撞8次发生的概率是万分之六很小了;6的话是因为如果阈值是8,在8附近会出现链表和红黑树的不停转换影响效率

哈希表的初始大小是16,如果指定构造函数的参数k,则大小为大于k的2的整数次方(如k为10,则大小为16),扩容按照n^2扩容。

数组长度为2的倍数的原因

  • 要保证h & (length-1)和h & length等效,前提就是length是2的倍数。这一步的目的是判断元素存放的位置,按位运算速度快
  • 防止哈希冲突,尽可能均匀分布

负载因子是0.75
是1的话空间利用率很高,但是很容易碰撞,使得链表变长查询效率降低
0.5的话碰撞概率低,产生链表的概率也低,但是空间利用率不高,造成浪费
折中选择0.75

HashMap函数设计
计算得到key的哈希值,是一个32位int,然后高16位和低16位异或操作。是为了避免冲突,尽量分散;使用位运算是为了提高计算效率。

1.8的HashMap主要做了以下几点优化

  • 数组+链表改为了数组+红黑树:为了防止哈希冲突后链表太长,时间复杂度从O(n)将为O(logn)
  • 插入数据从头插法改为尾插:头插法可能会导致反转,多线程可能会出现环
  • 插入时1.7先判断是否需要扩容再插入,1.8则直接插入然后再判断是否需要扩容

HashMap线程不安全
A线程判断index为空后挂起,然后B也判断为空,就开始写入数据,之后A继续写入数据,导致数据覆盖。多线程里面扩容可能也会出问题。

LinkedHashMap和TreeMap
HashMap是无序的,有序的是这两个。
前者内部维护了一个单链表,有头尾节点,Map节点由两个变量描述前后节点。底层还是拉链式的数组+链表或者红黑树,在HashMap基础上增加了一条双向链表,使得可以保持顺序
后者按照key的顺序自然排列,内部是红黑树(自平衡二叉排序树)。

HashTable
基本被淘汰了,代码里面不好用了
存储的元素是无序的,key不可以重复,线程安全的。
是在操作方法上添加关键词synchronized,锁住整个数组,粒度较大。
底层是数组加链表,默认大小是11,扩容原先2n+1大小,可以指定大小,会按照实际设置。链表主要为了解决哈希冲突
HashTable不允许null作为键,效率也不如HashMap,一般也不用了
Collections.synchronizedMap
通过传入Map封装一个SynchronizedMap,内部定义一个对象锁,使用了分段锁降低粒度提高并发。
分段锁的原理:成员变量使用volatile修饰,还是用了CAS和synchronized实现赋值操作,多线程操作的时候只会锁住当前操作索引的节点。

ConcurrentHashMap
元素无序的,线程安全的
之前是采用分段锁,后来改成CAS+syncronized
使用分段数组+链表实现。将整个数组分为多个不同的桶(segment),每次操作的时候只给对应桶上锁,不影响其他的桶。

  • 1.7版本:分段数组和链表
  • 1.8版本:摒弃了segment桶的概念,用了node数组+链表/红黑树。当是链表的时候是Node,如果是红黑树,是TreeNode

并发控制使用sunchronized和CAS实现

Set

HashSet
存储的元素是无序的,不可重复,线程不安全的
底层的数据结构是HashMap,用于不需要保证数据的存入和取出的场景

  • 自身只实现了几个方法其余都是HashMap的
  • 仅存储对象,使用add()添加元素
  • 对于两个对象来说哈希值可能会相等冲突,用equals()判断对象是否相等

TreeSet
存储的元素是有序的,不可重复,线程不安全的
底层结构是红黑树(自平衡二叉树)

  • 除了和HashMap一样继承了AbstractMap,还实现了NavigableMap和SortedMap接口,前者可以对集合中的元素进行搜索,后者排序,默认升序key
  • 可以在构造函数里使用new Comparator重写compare方法设置排序条件
  • 判断去重,先判断hashcode是否相等,等于的话再调用equals判断,如果还是相等就不插入了

关键字

static

为某些特定类型或者变量对象提供单一的存储空间;在不创建对象的情况下可以使用类的变量和方法。
static静态代码块可以是随着类的加载加载的,只会执行一次,在main之前执行。内存中static修饰的变量存在于方法区中。

final

声明属性、方法和类。分别表示不可变、不可覆盖和不可继承。
变量不可变包括引用不可变和对象不可变,如下,上面会输出ssszzz,下面则会报错。因为上面赋值实质上只是改变了内存里的值,而s的值是内存地址没有改变,因此下面就报错了,因为保存的地址是不能改变的。

final StringBuilder s=new StringBuffer("sss");
s.append("zzz");s=new StringBuffer("kkk");

面向对象

不允许多继承,但是可以通过实现多个接口实现多继承。

多继承还可以通过内部类实现。

反射机制

Java反射(超详细!)
通过反射机制可以实现对于字节码的修改操作
应用场景

  • Spring、SpringBoot和MyBatis都大量应用了反射机制
  • 框架中用到的动态代理其实也是依赖于反射实现的
  • 注解也使用了反射机制

优缺点:

  • 可以是的代码更加灵活、提供开箱即用的便利
  • 增加了安全问题,如无视泛型的安全检查,性能也会差一些,但是问题不大

有几个比较重要的类:

  • java.lang.Class:整个字节码,就是一个类
  • java.lang.reflact.Method:代表字节码中的方法字节码,就是类中的方法
  • java.lang.reflect.Constructor:字节码中的构造方法字节码,类中的构造方法
  • java.lang.reflect.Field:字节码中的属性字节码,代表类中的成员变量

要先获取Class然后才可以得到Method、Constructor和Field
如何获取一个类:

  • Class xxx=Class.forName(‘xxx’);
  • 对象.getClass():TargetObject o=new TargetObject();Class a=o.getClass();
  • 任何类型.class:Class xxx=TargetObject.class;
  • 还有一种用类加载器实现:ClassLoader.getSystemClassLoader().loalClass(“com.xxx.yyy”);这种不会进行初始化,静态代码块和静态对象都会执行

通过反射实例化对象,要保证这个类有一个无参构造函数,因为会默认调用
TargetObject object=(TargetObject)TestClass.newInstance();

如果只需要执行一个类的静态代码块
Class.forName(“balabala”)
Class一些常用的方法

  • newInstacne():创建对象
  • getName():返回完整类名带包名
  • getSimpleName():只返回类名
  • getFields():返回public修饰的成员变量
  • getDeclaredFields():返回类中的所有属性
  • getDeclaredFields(String name):返回类中的指定属性
  • getDecalredMethods():返回类中的所有方法
  • getDecalredMethod(String name,Class<?> params…):根据方法名和指定参数返回方法
  • getDeclaredConstructors():返回类的所有构造方法
  • getDeclaredConstructor(Class<?> params):根据参数类型获取对应构造方法
  • getIntefaces():返回类实现的所有接口名

Field常用方法

  • String getName():返回属性名
  • int getModifiers():获取属性的修饰符列表,是一个数字,每个数字是一个修饰符的代号,配合Modifier类的toString(int)一起用
  • Class<?> getType():以Class为类型,例如String属性返回:class java.lang.String
  • void set(Object object,Object value):给对象设置属性值,如果设置私有变量,需要先setAccessible(true)
  • Object get(Object object):读取对象属性值

Methods常用方法

  • String getName():返回方法名
  • int getModifiers():获取方法修饰符列表,是一个整型,使用Modifier.toString(int)转换为字符串
  • Class<?> getReturnType():返回方法的类型,配合getSimpleName()使用
  • Class<?> getParameterTypes():返回方法修饰符列表,配合上面的getSimpleName()使用
  • Object invoke(Object obj,Object… args):调用方法

小例子
假设我现在有一个类
第一种:调用public方法并且传入参数

import java.lang.reflect.Method;
public class prac{public static void main(String[] args) throws Exception{//通过类名获得ClassClass<?> test=Class.forName("javap.main.java.TargetObject");//获得这个类的所有声明方法,包括public和privateMethod[] methods=test.getDeclaredMethods();for (Method temp:methods){System.out.println(temp.getName());}//获取指定方法Method,参数是方法名和参数类Method publicMethod=test.getDeclaredMethod("publicMethod", String.class);//调用invode调用刚才获取到的Method,同时传参publicMethod.invoke((TargetObject)test.newInstance(), "babaaa");}
}

第二个,获取私有方法,并且直接修改对应对象里面的变量值(private)
这里涉及到一个传入object,这里的field和invoke里面其实都需要传入类的实例,可以直接实例化也可以newInstance(),但是如果用后者的话,每次都是新的实例,比如第二部分代码,两次都是new最终输出的结果其实不是我们预期的,因为第一次虽然修改了,但是第二次用的不是第一次的而是new。
所以直接实例化以后传入tobject就修改好了。
这里使用了Field修改了类里面的成员变量,

public class prac{public static void main(String[] args) throws Exception{Class<?> test=Class.forName("javap.main.java.TargetObject");//获取成员变量,设置可以访问取消安全检查Field field=test.getDeclaredField("value");field.setAccessible(true);//声明一个对象然后把这个Field设置给类里面,然后再赋值TargetObject tobject=(TargetObject)test.newInstance();field.set(tobject,"这是用字段设置的value");Method privateMethod=test.getDeclaredMethod("privateMethod");privateMethod.invoke(tobject);//如果两次都new,最终的结果不是我们需要的,没有修改成功field.set((TargetObject)test.newInstance();,"这是用字段设置的value");Method privateMethod=test.getDeclaredMethod("privateMethod");privateMethod.invoke((TargetObject)test.newInstance(););}
}

使用Field操作的一点代码

Field[] fields=test.getDeclaredFields();
for (Field temp:fields){System.out.println(temp.getName());System.out.println(temp.getModifiers());System.out.println(Modifier.toString(temp.getModifiers()));System.out.println(temp.getType()+"\t"+temp.getType().getSimpleName());
}

代理模式

是一种设计模式,使用代理对象代替真实对象的访问,在不修改目标对象的前提下提供额外的功能操作,扩展目标对象的功能,比如在某个方法执行前后增加一些自定义操作。
其中包括静态代理和动态代理。
静态代理
对目标对象的方法增强都是手动完成的,不是很灵活,如果接口一代增加新方法,目标对象和代理对象都需要修改,每个目标类都有一个单独的代理类,实际使用场景非常少。
静态代理在表一的时候就将接口、实现类和代理类变成一个个实际的字节码文件,提前做好了。
实现步骤:

  • 定义一个接口及其实现类
  • 创建一个代理类
  • 将目标对象注入代理类,在代理类的对应方法调用目标类中的对应方法,这样就可以通过代理类屏蔽对目标对象的访问,可以在目标方法执行前后做一些自己的事情
    代码示例
public class prac{public static void main(String[] args) throws Exception{SmsService Service=new SmsServiceImplement();SmsProxy Proxy=new SmsProxy(Service);Proxy.send("巴拉巴拉");}
}interface SmsService{String send(String Messange);
}
class SmsServiceImplement implements SmsService{public String send(String Message){System.out.println("发送"+Message);return Message;}
}
class SmsProxy implements SmsService{private final SmsService Service;public SmsProxy(SmsService Service){this.Service=Service;}@Overridepublic String send(String Message){System.out.println("发送之前");Service.send(Message);System.out.println("发送之后");return null;}
}

输出如下,可以看到已经添加了功能:
发送之前
发送巴拉巴拉
发送之后
动态代理
动态代理比静态代理更加灵活,不需要针对每一个目标类单独创建代理类,也不需要直线所有接口,可以直接代理实现类(CGLIB动态代理机制)
JVM角度看,动态代理是运行时动态生成类的字节码并且加载到JVM里面。
AOP和RPC框架的实现都依赖于动态代理。
分为两种,JDK动态代理和CGLIB动态代理。
JDK动态代理
核心是InvocationHandler接口和Proxy类。

public static Object newProxyInstance(ClassLoader loader,Class<?>[] interfaces,InvocationHandler h)throws IllegalArgumentException</code>

三个参数:loader(类加载器,加载代理对象)、interfaces(被代理类的一些接口)、h实现InvocationHandler接口的对象
使用InvocationHandler自定义处理逻辑,动态代理对象调用方法的时候最终就会通过这个里面的invoke实现
invoke()有三个参数:proxy(动态生成的代理类)、method(代理类对象调用方法相对应)、args(方法参数)
即通过Proxy类的newPeoxyInstance()创建的代理对象调用方法的时候会实现InvocationHandler接口类的invoke()方法,在这里面进行比如前后执行的操作
大致步骤:

  • 定义接口和实现类
  • 自定义InvocationHandler并重写invoke方法,调用原生方法和自定义逻辑
  • 通过Proxy.newProxyInstance()方法创建代理对象
    代码示例
public class prac{public static void main(String[] args) throws Exception{SmsService smsService = (SmsService) JDKProxyFactory.getProxy(new SmsServiceImplement());smsService.send("java");}
}interface SmsService{String send(String Messange);
}
class SmsServiceImplement implements SmsService{public String send(String Message){System.out.println("发送"+Message);return Message;}
}
class DebugInvocationHandler implements InvocationHandler{private final Object target;public DebugInvocationHandler(Object target){this.target=target;}public Object invoke(Object proxy,Method method,Object[] args)throws Exception{System.out.println("之前");Object result=method.invoke(target,args);System.out.println("之后");return result;}
}
class JDKProxyFactory{public static Object getProxy(Object target){return Proxy.newProxyInstance(target.getClass().getClassLoader(), target.getClass().getInterfaces(),new DebugInvocationHandler(target));}
}

CGLIB动态代理
是一个基于ASM的字节码生成库,允许在运行时对字节码进行修改和动态生成,通过继承方式实现代理,很多开源框架都使用了CGLIB,比如Spring的AOP,如果目标对象实现了接口,默认是JDK,否则就是CGLIB。
核心是MethodInterceptor接口和Enhancer类,需要自定义MethodInterceptor并重写intercept方法,用于拦截增强被代理类的方法。

public interface MethodInterceptor
extends Callback{// 拦截被代理类中的方法public Object intercept(Object obj, java.lang.reflect.Method method, Object[] args,MethodProxy proxy) throws Throwable;
}
  • obj:动态生成的代理对象
  • method:被拦截的方法(需要增强的)
  • args:方法参数
  • proxy:用于调用原始方法
    通过Enhancer类动态获取被代理类,当调用方法的时候,实际调用的是MethodInterceptor的intercept方法。
    使用步骤:
  • 定义一个类
  • 自定义MethodInterceptor,重写intercept,类似于JDK里面的invoke
  • 通过Enhancer类的create()创建代理类

需要引入依赖
区别
JDK代理只实现了接口类或者直接代理接口,CGLIB可以代理为实现任何借口的类,是通过生成第一个被代理类的子类来拦截方法调用的,所以不能代理final类和方法,因为没法生成子类
大部分是JDK更优秀,效率高

面经

short s1=1;s1+=1和s1=s1+1;哪个对哪个错
前者正确,会自动把1转为short相加,后者会报错int和short无法运算
重载和重写
重载是方法名相同,参数的个数类型以及返回值都可以不同;重写必须是都一致才行
数组实例化有几种方式
动态初始化:

int[] array=new int[5];

静态初始化

int[] array=new int[]{1,2,3,4,5};
int[] array={1,2,3,4,5};

Object 类常用方法有哪些

  • euqals():判断该对象与形参对象所指的内存空间是否相同,如果是同一块内存空间返回true,不同返回false,相同内容的不同内存空间也是false;而String重写了equals()方法
  • toString():返回该对象的字符串形式,结果应该是一个简明易懂的字符串,建议所有子类都重写该方法,Object类的返回值是【类名+@+无符号十六进制哈希】:getClass().getName() + ‘@’ + Integer.toHexString(hashCode())
  • finalize():是protected修饰的,只有一个方法体没有内容,由程序员手动重写,但是是jvm自动调用的。当一个垃圾对象即将被jvm回收的时候,如需要在销毁的时候执行一些代码,写在这里jvm会自动调用该方法。而static静态代码块则是在类加载的时候执行一次,和这个类似
  • hashcode():返回一个根据java对象内存地址哈希运算后得到的哈希值,这个结果其实可以等同看待为是对象的地址
  • clone():由protected修饰的,实现对象的浅复制,需要实现Cloneable接口才可以,否则会报错CloneNotSupportedException
  • getClass():和反射使用的,用于获取对象运行时的java对象
  • notify()和notifyAll():final修饰,用于唤醒该对象上的某个等待的线程或者全部线程

java 中是值传递引用传递
java是值传递,但是对于基本数据类型,传递的是值,而对于引用类型,传递的则是地址值的拷贝
构造方法能否被重写,重载呢
可以重载,但是不能重写,构造方法名是和类名一样的,如果重写了说明子类和父类类名一致,矛盾了,所以不能重写
自动装箱和自动拆箱
自动装箱就是java可以将基本数据类型自动转为对应的对象,比如将int基本类型转为Integer类的对象,调用Integer.valueOf()或者String.valueOf()等
将Integer类对象转为int基本数据类型,就是拆箱,比如调用object.intValue()
然后这个对应的类就叫包装类,使得基本数据类型能够具有类中对象的特征。
在泛型里面会使用到,如List,这里面只能放类的对象,不能放基本数据类型
包装类也包括有八个

  • byte - Byte
  • short - Short
  • int - Integer
  • long - Long
  • char - Character
  • double - Double
  • float- Float
  • boolean - Boolean

java中的修饰符

  • private:私有的,只能够被类中的对象所访问
  • default:默认的,同一个包内可以进行访问
  • protected:受保护的,能够被本类、本包以及不同包继承后的子类访问
  • public:所以访问

接口和抽象类的特点和区别
接口:使用interface修饰;不可实例化;一个类可以实现多个接口;实现类一定要实现接口里的所有方法,在接口类里面只有方法的声明;接口里的变量通常是静态变量
抽象类:用abstrate修饰;一个类只能够继承一个抽象类;抽象类允许只实现部分方法;抽象类里除了有方法的声明还可以有方法的实现。
普通类:可以直接实例化;抽象类的子类必须重写父类的抽象方法,普通类的子类可以重写也可以直接调用;不用abstrate修饰;

有哪些集合?哪些是线程安全的不安全的,为什么呢
线程安全:

  • Vector、HashTable

线程不安全:

  • ArrayList、LinkedList、HashSet、TreeSet、HashMap、TreeMap

hashMap的jdk1.7和jdk1.8有什么区别?为什么要用红黑树
区别主要是底层数据结构的变化,1.7中使用数组+链表的形式,1.8则采用了数组+链表+红黑树的结构,当链表长度为8且数组长度大于64的时候链表会转为红黑树,长度低于6的时候会转为链表。
红黑树是一种平衡二叉搜索树,能够通过左旋和右旋变色保持树的平衡。链表复杂度O(n),红黑树复杂度O(logn)。
为什么长度大于8以及大于64的时候才转换,因为节点太少没必要转换,转换的时候需要浪费给时间和空间;俄日什么一开始不转换,因为树的结构浪费空间,只有节点多的时候这个结构才有优势,节点少的时候会转为链表占用空间少。
HashMap的迭代方式

  • 迭代器(Iterator)EntrySet:
Iterator<Map.Entry<Integer,Integer>> iter=map.entrySet().iterator();
while (iter.hasNext()){Map.Entry<Integer,Integer> item=iter.next();System.out.println(item.getKey());
}
  • 迭代器(Iterator)KeySet:和上面其实一样,只不过遍历不是entry,而是key的集合,然后再用key去得到对应的value
    For Each EntrySet:
for (Map.Entry<Integer,Integer> item:map.entrySet()){System.out.println(item.getValue());}

For Entry KeySet:和上面差不多,用key访问

  • Lambda:
map.forEach((key,value)->{System.out.println(key+"\t"+value);
});
  • Streams API单线程
map.entrySet().stream().forEach((entry) -> {System.out.println(entry.getKey());System.out.println(entry.getValue());
});
  • Streams API多线程
map.entrySet().parallelStream().forEach((entry) -> {System.out.println(entry.getKey());System.out.println(entry.getValue());
});

final和finally
final是一个关键词,用于修饰变量、方法和类等,被修饰的不能够可变、不能继承、不能被重写
finally是异常处理机制里的一个关键词,和try、catch一起连用,不管有没有异常,都会执行finally的代码。

  • 如果try和finally都有return,那么执行了finally的return以后就不执行try的了
  • 如果try返回了一些变量值,但是在finally中修改了这个变量的值,实际return的值不会发生改变,因为在进入finally之前已经临时保存了

注解配置的原理
为什么重写equals()方法就必须重写hashCode()方法
==和equals其实默认都是比较变量的地址,判断地址所指向的值是否一致,只不过String这种类重写了equals,从比较地址改为了直接比较值。
再重写equals()的时候比需要重写hashcode()方法,如果不修改hashcode,那么在hash查找的时候就需要一个一个进行遍历判断值是否一致,
不同对象的hashcode可能一致,但是hashcode不一致的一定不一样;equals相同的hashcode一定相同,hashcode相同equals不一定相同
重点:如果不重写的话在set或者hashmap里面使用哈希的时候会默认调用Object类的hashcode(),会比较对象的地址,两个不同地址值一样结果还是false不一致;如果是重写了,就会判断两个对象的哈希值,而不是地址,那么就一致了。
String s1 = new String(“abc”) 和 String s2 = “abc” 的区别
后者会在对象内存池中存储一个abc的缓存,s2创建一个对象,其指向的值是abc
前者会先创建一个String对象,s1的值是String对象的地址,而这个地址又指向上面的abc的地址
为什么String设计成不可变?String 和 StringBuilder、StringBuffer 的区别
String类使用final修饰,是不可变是常量,线程安全的,当对String进行修改的时候实质上是创建了一个新的对象,修改了引用,回收旧对象
StringBuffer加了同步锁,所以线程安全,多个String相加的时候其实会自动转为StringBuffer,如果不断创建新对象又慢又浪费,多线程适合
StringBuilder线程不安全,单线程适合
Arraylist、HashMap的初始容量、加载因子、扩容增量

  • ArrayList:初始10,加载因子1(元素个数超过长度的时候扩容),原先的0.5倍(Vector扩容一倍)
  • HashMap:初始16,加载因子0.75(元素个数超过长度的0.75倍就扩容),扩容1倍
  • HashTable:线程安全,初始11,加载因子0.75,扩容原有的一倍+1

有序的Map有哪些?为什么TreeMap是有序的?哪些集合是线程安全的?
有序的:LinkedHashMap(用单链表记录添加的顺序)和TreeMap(默认升序,二叉树中序遍历保证有序)

HashMap的底层数据结构,是如何插入的?哈希冲突解决方案?为什么是非线程安全的?
JDK1.7中是数组+链表,1.8中数组+链表+红黑树
链地址法解决冲突;1.8中当数组长度大于8的时候转为红黑树,小于6的时候转为链表。但是在转换之前会判断数组长度是否到达64,如果没到先扩容数组
在1.7中会出现死循环和数据丢失的维妮塔,1.8修复了但是会出现覆盖的问题,线程安全的话需要使用ConcurrentHashMap

HashMap为什么初始容量总是2的n次方
主要和hashmap的寻址有关系,在插入数据的时候通过(n - 1) & hash计算散列地址,这其实是一个取余的操作,就是%,但是这个运算不如&与运算速度快,当a%b=a&(b-1)想用与运算成立,就必须要满足b为2的次方才行
而如何保证扩容是2的次方呢,扩容函数用了位运算,(n-1)对1,2,4,8,16分别进行位运算然后或|操作,这样可以使得当前最高位的和后面全部变为1.比如9的话,n=8,也就是1000,后面全部变为1就是1111,,即16,2的5次方

ConcurrentHashMap 和 Hashtable 的区别?
Con基于HashTable改进,后者线程安全但是需要锁定整个表才能修改,而前者将整个表分为了16个(或更多)桶(segmentation),每次只锁定对应的桶,相当于可以并发16线程修改,同时,读取并发更高效,几乎不受限制。

异常

程序错误分为三种

  • 编译错误:语法出问题,在编译的时候报错
  • 运行时错误:运行的时候发现一些错误,比如下标越界
  • 逻辑错误:就是程序没有按照预先的逻辑顺序运行

Throwable有两个子类,分别是Error和Exception,主要区别是异常是可以进行相应处理的,错误无法进行处理。
常用方法
Error是程序无法处理的错误,一般是比较严重的问题,比如当运行程序的时候jvm内存不足的时候,会报错OutOfMemoryError,这些错误是不可控的,也是程序本身无法处理的,一般会选择线程终止
Exception主要包括如RuntimeExceptionArrayIndexOutOfBoundsExceptionNullPointerExceptionClassNotFoundException

  • getMessage():返回异常发生时的简要描述
  • toString():返回异常发生时的详细信息
  • printStackTrace():控制台打印对象封装的异常信息

异常处理一般有两种方法

  • 抛出异常:方法通过throws向上级抛出异常,上级可以选择捕获处理或者继续抛出给上一级,一直不处理会抛给jvm
  • 捕捉异常:使用try-catch-finally捕捉异常,对异常进行处理

Exception分为CheckedException和UncheckedException
前者受检查异常如果没有被catch或者throws,那么不能通过编译,只有RuntimeException及其子类不是是这一类,其余异常都是
RuntimeException

  • NullPointer
  • IllegalArgument
  • ArrayIndexOutOfBounds
  • ClassCast:类型转换错误

多线程

线程安全有三要素:

  • 可见性
  • 有序性
  • 原子性

synchronized

在多线程访问互斥资源的时候需要用到锁。
可以锁住一个代码块,比如在线程里,用这个锁住这个循环,在执行这个代码块的时候就会被锁住其他线程无法执行

private static final Object monitor=new Object();
Thread A=new Thread(()->{synchronized (monitor){for (int i=0;i<1--;i++){System.out.println(i);}}
});

其实主要有三种作用范围

  • 静态方法加锁
  • 非静态方法加锁
  • 代码块加锁

锁是加在对象上面的。三者的区别实质上就是上锁的对象的不同

  • 类对象:SynchronizedSample.class
  • 当前对象:this
  • 指定对象,比如上面的monitor

java对象由三部分组成

  • 对象头:Mard Word(保存HashCode、锁信息、分代年龄和GC标志等)和Klass Word(存储指向对象所属类的指针)组成
  • 实例数据:就是类里面的变量
  • 对其填充:java要求对象占用的空间是8的整数倍,如果不够,在这里补齐

然后每个对象都有与之关联的monitor对象,里面有一些属性信息,这个对象有一个线程内部竞争锁的机制。

JDK6以前的实现
A和B需要修改数据的时候,发现方法是被修饰过的,此时A被调度,A就抢先得到锁,步骤为:设置MonitorObject_owner为A线程、mark word设置Monitor地址,锁标志位改为10、B线程放在ContentionList阻塞。
每次会从ContentionList里面选择有资格成为候选线程的放在EntryList里面
是一种非公平锁
底层调用的mutex锁,内核提供的。因为他不是按照申请锁的先后进行锁的分配,而是在每次对象释放锁的时候,所有等待线程都有机会获得锁,好处是可以提高性能,坏处就是会造成某些线程饥饿
缺点:

  • 底层依赖于内核的mutex,加锁和解锁需要频繁切换内核和用户态之间,性能损耗比较明显
  • 测试发现大量的加锁和解锁操作都出现在特定线程里面,出现竞争的情况挺低的

线程的实现方式

有四种
继承Thread类
重写run方法。
之后调用start()方法启动线程即可。如果直接调用run方法,那就相当于单纯的调用方法,不会创建新的线程,分配单独的栈和内存空间等,是一个单独的。

public class ThreadTest01 {public static void main(String[] args) {MyThead myThead = new MyThead();myThead.start();for (int i = 0; i < 1000; i++) {System.out.println("主线程-->" + i);}}
}
class MyThead extends Thread{@Overridepublic void run() {   //必须写for (int i = 0; i < 1000; i++) {System.out.println("分支线程-->" + i);}}
}

继承Runnable接口
同样实现run方法,这个比较灵活常用,因为继承的话只能继承一个类,接口可以实现多个。
实现了接口的自定义的线程类不能直接调用,这其实是一个可运行的对象,需要将其封装为一个线程对象Threa t=new Thread(xxx),然后调用start方法

public class myjava{public static void main(String[] args){MyThread t1=new MyThread();MyThread t2=new MyThread();new Thread(t1).start();new Thread(t2).start();}
class MyThread implements Runnable{public void run(){for (int i=0;i<100;i++){System.out.println(i);}}
}

匿名内部类
就是直接在括号里创建一个可执行的Runnable对象然后执行start方法

public class myjava{public static void main(String[] args){new Thread(new Runnable() {public void run(){for(int i=0;i<100;i++)System.out.println(i);}}).start();}
}

Callable接口
通过实现这个接口,可以获取到线程执行完成以后的结果,但是获取结果的时候是阻塞的,需要等待。
创建FutureTask类,使用匿名内部类的方式创建一个Callable对象传入,重写call方法,和run差不多只不过可以有返回值。
大致步骤是:

  • 创建FurureTask对象,构造函数的参数是一个Callable的内部类
  • 将上述对象作为参数创建一个Thread,调用start方法
  • 调用FutureTask的get方法获取结果
import java.io.*;
import java.net.*;
import java.util.*;
import java.util.concurrent.FutureTask;
import java.util.concurrent.Callable;public class myjava{public static void main(String[] args) throws Exception{FutureTask t=new FutureTask(new Callable() {public Object call() throws Exception{return 6;}});Thread tt=new Thread(t);tt.start();Object result=t.get();System.out.println(result);}
}

从线程池中使用线程
需要实现Runnable接口,重写run方法,作为可执行对象,然后调用execute方法
或者实现Callable接口,重写call方法,作为可执行对象调用submit方法

class NumberThread1 implements Runnable{@Overridepublic void run() {for (int i = 0; i <= 100; i++) {if (i % 2 == 0) {System.out.println("线程:"+Thread.currentThread().getName() + ",输出偶数" + i);}}}
}
//输出奇数
class NumberThread2 implements Runnable{@Overridepublic void run() {for (int i = 0; i <= 100; i++) {if (i % 2 != 0) {System.out.println("线程:"+Thread.currentThread().getName() + ",输出奇数" + i);}}}
}public class ThreadPool {public static void main(String[] args) {//创建指定线程数的线程池ExecutorService executorService = Executors.newFixedThreadPool(5);//执行指定操作的线程,需要提供实现Runnable接口实现类或Callable接口实现类的对象//execute()方法适合RunnableexecutorService.execute(new NumberThread1());executorService.execute(new NumberThread2());//submit()方法适合Callable//关闭线程池executorService.shutdown();}
}

volatile

Java内存模型有三个特性

  • 可见性:就是一个线程进行的修改对其他线程都是可见的。volatile、synchronized和final都可以实现可见性
  • 原子性:再执行某个操作的时候不可再分,整体成功和失败。比如a=0就是原子的,a++本质上是a+=1不是原子的,可以分成三个步骤
  • 有序性:为了提高性能,处理器和编译器会对代码指令进行重排以便高效率,针对单线程没问题,但是多线程下不同变量或者操作指令重排后不好保证顺序和一致性。为了保证持有同一个对象锁的两个同步块是串行的。

volatile可以保证可见性和有序性。

可见性
对于非volatile变量,读写,每次都要从线程将变量拷贝到CPU缓存中,如果有多个CPU线程可能会将其拷贝到多个Cache中。
而如果是volatile,会保证每次都从内存中读取变量,而不会去找CPU Cache。
进行写操作的时候,会在后面加一条store屏障指令,将工作内存中的共享变量写入到主内存中
进行读操作的时候,会在后面加一条load屏障指令,从主内存中读取

用于修饰会被多线程访问的对象,保持修改对所有线程可见。
不保证第三个要素,因此不严格线程安全。
在对一个字段进行修改的时候,jvm会执行Write-Barrier操作,将当前缓存的数据写入到系统内存,使得其他核心引用了改地址的数据变成了脏数据。
读取的时候,会再执行一个Read-Barrier执行,如果是脏数据,就从内存重新获取。

IO

IO、NIO和BIO、AIO
BIO:同步阻塞IO,一个连接一个线程,客户端每有一个连接就需要启动一个线程处理请求,在不做什么事的时候对应线程会有不必要的资源开销,适合连接较少而且比较固定的场景
NIO:同步非阻塞IO(Non-blocking),一个请求一个线程,客户端发来的请求会被注册到多路复用器,多路复用轮询到有一个请求到来的时候,会创建一个线程进行处理,适用于连接数多而且连接时间较短的场景,比如聊天服务器
AIO:异步非阻塞IO:一个有效请求一个线程,IO请求一般由系统先完成以后再通知服务器启动线程处理,适用于连接数目多而且连接时间长的场景。用户发送一个请求,不管数据准没准备好都会收到一个返回结果,然后用户可以发送其他请求,之后如果有数据准备好了会通知用户
同步和异步
同步是发送一个请求需要等待结果返回,之后再发送下一个请求,避免死锁、脏读等问题
异步指的是发送一个请求不等待结果返回,可以直接发送下一个请求,提高效率保证并发
二者主要区别在于是否等待结果返回

多路复用
多路复用其实就是异步阻塞IO模型,select和epoll都是多路复用的实现。
redis单线程快的原因就是多路复用IO和缓存
用户发出请求,当数据准备好以后,会通知用户,然后用户会对再线程进行相应操作
select:一个进程监听所有的文件描述符fd,通过不断轮询所有的fd判断哪个活跃

  • 受限于系统,一个进程最多打开1024个fd,因此并发最大就是1024
  • 每次需要轮询所有的fd,时间复杂度O(n)
  • 用户内核空间拷贝问题,应该是资源?

poll:提高了可以打开的fd的个数,1G内存可以打开10w左右,使用了链表的结构,以链表的方式存储在内核中,所以没有个数限制。将用户传入的数组拷贝到内核,进行遍历,查询每个fd状态,时间复杂度还是O(n)
epoll:实质上是事件驱动的,当fd活跃的时候,会把哪个流发生什么IO事件通知用户进行处理,时间复杂度O(1)

面经

synchronized的使用方式、底层实现以及JDK1.6的优化?

  • 修饰代码块:通过monitorenter和monitorexit实现。当执行enter方法的时候,试图获取锁对象,即获取monitor所有权,当锁计数器为0的时候才可以获取到,获取到以后进行自增+1;exit的时候减去1表明释放锁
  • 修饰方法:使用ACC_SYCHRONIZED表明是一个同步方法,jvm使用这个判断是不是一个同步方法,然后进行同步调用。
  • JDK16进行了优化:使用偏向锁、轻量级锁、自旋锁、自适应自旋锁,锁消除和锁粗化等减少锁的开销
    • 偏向锁:偏向锁倾向于第一个获取的线程,同一线程有时候会重复获取一个锁,在无竞争的时候,线程可以直接获得锁,避免释放带来的开销,当有竞争的时候,会膨胀为轻量级锁
    • 轻量级锁::当偏向锁失败的时候,不会直接变成重量级锁,会是轻量级锁,适用于线程交替执行同步代码块的场景,同一时间存在多个线程访问同一把锁,就会变成重量级锁。轻量级锁没有竞争的时候使用CAS机制,不用互斥量。提升性能的依据就是大部分锁在同步周期内不存在竞争
    • 重量级锁:轻量级锁自旋一次失败后会变成重量级锁,是基于Monitor机制的。使用系统互斥量实现会有一定性能损耗。
    • 自旋锁:一个线程拥有锁的时间不会太长,然后如果另一个线程需要获得锁的话,挂起然后再恢复消耗会比较大,就执行忙循环,自己尝试获得10次锁,如果还没得到就挂起线程等到锁
    • 自适应自旋:jd1.6以后引入的,自旋的时间不固定,根据上一个自旋的情况进行动态改变

谈谈 synchronized和ReentrantLock 的区别?

  • 前者:隐式获得锁;是JVM级别的;不可以响应中断;同步阻塞,悲观并发策略;是一个关键词;发生异常会自动释放锁
  • 后者:显示获得锁;是API级别的;可以响应中断;可以实现公平锁;同步非阻塞,乐观并发策略;是一个接口;发生异常如果没有手动释放锁,会死锁,所以需要在finally里面写上释放锁

CAS
这个其实就是MySQL里面的那个机制。就是会记录三个值:修改值的地址、旧的值、期望的值。当获取数据的时候会记录这几个值,在操作完进行修改的时候,会先判断当前的值是不是旧值,如果是就修改为新的,否则会认为被其他线程修改了,就不会执行当前修改操作,而是会不断等待,等的时间长了对系统负担影响较大。
会出现ABA的问题,就是原先的旧值可能是A,被其他线程修改为了B,但是在该线程修改之前又改回了A,这样该线程看到的是没有修改数据,就会执行自己的修改,实际上是修改过了的。因为实际中值相同的代表的可能不相同。
解决方案是使用版本号机制,同时记录下版本号,每次执行完操作判断版本号是否有修改,如果没有旧修改数据同时版本号+1,否则不进行修改
多线程的实现方式,start()是立刻启动吗?
线程不是马上执行的,首先线程会从new状态变成READY就绪状态,之后需要等待cpu的调度运行才能变成RUNNING状态,取决于CPU调度机制。
java中线程的状态:

  • 新建状态(New):新建以后是new状态
  • 就绪状态(Runnable):调用start()方法启动以后变为就绪状态,随时等待cpu调度
  • 运行状态(Running):线程获取cpu开始运行,只能从就绪态进来
  • 阻塞状态(Blocked):因为某些原因进入就绪状态,等待重新调度
    • 调用wait()等待线程某项工作完成
    • 同步阻塞,synchronized获取锁同步失败会阻塞
    • 其他阻塞,比如sleep()、IO操作等
  • 死亡状态(DEAD):线程执行完成或异常退出

ThreadPoolExecutor的重要参数?执行顺序?如何设置参数?
参数有

  • 核心线程数(corePoolSize):会一直存活,即便没有任务执行。
  • 任务队列容量(阻塞队列,queueCapacity):核心线程满了以后新的任务会在这里等待
  • 最大线程数(maxPoolSize):核心线程满了以后,而且阻塞队列也满了,会创建新的线程执行到来的任务,当线程总数达到最大线程数的时候,且阻塞队列也满了,就拒绝接受任务抛出异常
  • 线程空闲时间(keepAliveTime):当线程空闲时间达到一定的时候,会结束线程,直到数量等于核心线程数

执行顺序:

  • 线程数小于核心线程数,创建线程执行新任务
  • 线程数大于核心线程数,将新任务放在阻塞队列里
  • 线程数大于核心线程数且阻塞队列满了
    • 达到了最大线程数,拒绝新任务,抛出异常
    • 没有达到最大线程数,创建普通线程执行

参数设置:

  • 盲区

什么是死锁,死锁的四个必要条件?
必要条件:

  • 互斥:资源只能同时被一个进程使用
  • 循环等待:若干个进程形成一个循环等待资源关系
  • 请求和保持:一个进程一直请求资源,一个进程一直使用资源不释放
  • 不可剥夺:资源一旦获取除非用完,不然不能剥夺

jvm

java虚拟机内存空间有五个部分:方法区、堆、虚拟机栈、本地方法栈和程序计数器。
其中方法区和堆是线程共享的,其他是私有的。

组成

线程共享

方法区
存储已经被虚拟机加载的类信息、常量、静态变量和即时编译器编译后的代码等。
当方法区无法满足内存分配的需求时,抛出OOM(Out of Memory)异常。
有一个叫运行常量池的东西,里面保存了编译时生成的字面量和符号引用

  • 字面量:文本字符串以及final常量值
  • 符号引用:不懂


存放对象实例,几乎所有对象实例和数组都在这里分配内存。
这里也是垃圾回收主要的管理区域,所以也叫作GC堆

线程私有

程序计数器
当线程数超过CPU核数的时候,就需要根据某些策略强夺CPU时间片,为了使得线程切换以后还能够恢复到正常执行位置,使用独立的程序计数器记录正在执行的字节码指令地址。
程序计数器是唯一一个没有规定任何OutOfMemory的区域
虚拟机栈
描述Java方法执行的内存模型,每个方法在执行的时候都会创建一个栈帧,存储局部变量表、操作数栈、动态链接、方法出口等信息。每个方法的调用到完成的过程,对应一个栈桢在虚拟机栈的入栈和出栈的过程。
虚拟机栈有两种异常StackOverflow和OutOfMemory
栈的大小决定了线程可以递归的深度或者嵌套调用多少层方法。
栈大小固定的话,递归太深会报错前者;如果可以动态扩展,没有内存的话会报错后者
本地方法栈

和虚拟机栈差不多,区别就是虚拟机栈调用Java方法,这个本地方法栈为虚拟机执行Native(本地)方法。

常量池

大概有三种:类常量池、运行时常量池和字符串常量池
类常量池
java文件被编译成class字节码文件的时候会生成class类常量池。
除了包含有类的版本、字段等信息以外,还有一个就是常量池这个东西,常量池里面存放的就是编译器生成的各个字面量和符号引用。在类加载后进入方法区,会将一些常量放在运行时常量池里。
运行时常量池
类被加载的时候,会将Class文件常量池的内容转入运行时常量池中,将字面量和符号引用解析为直接引用存储在运行时常量池里。
字面量包括文本字符串、final修饰的常量值以及静态变量值等。
字符串常量池
在类加载完成以后,在堆中会生成对于字符串对象的实例,将其引用值保存到String Pool里面。这里保存的是引用,实际的值在具体内存地址里面。
HotSpot VM里面实现是通过StringTable,是一个哈希表,保存的是字符串的引用,在每个VM实例只有一份,所有类共享。

垃圾回收

垃圾回收
好像和python一样啊,也是引用计数的方式标记这个对象是否被使用,同样的无法解决循环引用的问题。
可达性分析
从一个GC Root对象出发,沿着遍历,搜所走过的路径叫做引用链,把可以访问到的对象标记下来。没有被标记的就是不可达的,会被判定为可回收对象。
可以被当作root的对象:

  • Java虚拟机栈(局部变量表)中引用的对象
  • 处于存活状态中的线程对象
  • 方法区中静态引用指向的对象
  • Native方法中JNI引用的对象

触发垃圾回收

  • 当在堆中分配内存出现剩余可用内存不够的时候会
  • 主动调用system.gc()

回收算法

标记清除
分为两个阶段,标记和清除。
首先会标记出所有需要回收的对象(不可达的对象就是需要回收的),然后统一清除。
有两个缺点:

  • 效率不高,需要遍历所有的对象
  • 清除以后会产生大量的内存碎片,导致申请较大内存的时候无法满足需求,再次触发GC

复制算法
把可用的内存按照容量划分为相等的两块,每次只用一块,一块用完以后,把还存活的对象复制到另一块去,清理已经使用过的。
优点:实现简单,运行高效,不会太出现内存碎片问题
缺点:内存利用率不高,只能用一半,如果对象存活较多的时候,复制影响效率

标记整理
和标记清除差不多,但是标记以后,不是对不可达对象清除,而是将所有存活对象移动到一端,清理掉边界外的内存
优点:避免了内存碎片,也不需要内存一分为二,性价比高
缺点:仍然需要对对象进行移动操作,存活对象多的时候就影响效率

分代回收
根据对象存活周期的不同将内存分为几块,一般是将堆分为新生代和老年代。
新生代里面每次回收有大量对象死亡,只存留少数,就选择复制算法。
老年代里对象存活率高,最好使用标记清除和标记整理回收。

其中年轻代又分为Eden、From和To三个区域。
当第一次Eden满了以后,会触发回收,存活的复制到From,Eden清空;第二次Eden满了以后,会回收Eden和From,将存活的复制到To,清空前两个;第三次回收Eden和To,存活的复制到From里面
存活的对象会一直在From和To里面复制移动,当一定次数以后,如果还存留,就放在老年代里面去。
如果老年代也满了,会触发全局垃圾回收,清理所有区域。
因此要合理设置新生代和老年代的大小,避免Full GC的执行。

类的加载

java类的加载过程
当程序使用一个没有加载到内存中的一个类的时候,如下三步对类进行加载初始化

  • 类的加载:将类的class文件读入内存,并且创建一个java.lang.Class对象到方法区,由类加载器实现
  • 类的链接:将类的二进制数据合并到JRE中
  • 类的初始化:JVM负责对类进行初始化
加载

将类的文件信息加载到内存中,作为程序方法的入口

  • 通过类的全限定名获得类的二进制字节流
  • 将字节流的静态存储结构转化为方法区的运行时结构
  • 内存中生成Class对象,作为方法区入口

如果加载的是一个数组类型,是不直接通过类加载器加载,而是由虚拟机完成,但是其引用需要使用加载器加载,加载器会加载完数组的数据类型后将数组绑定到相应的加载器上,然后和类加载器一起绑定标识唯一性。

链接
链接分为三步

  • 验证:确保加载的类符合JVM的规范
  • 准备:为类变量(static)分配内存设置初始值,都是在方法区进行分配的,实例变量会被分配到堆中
  • 解析:虚拟机常量池内的符号引用(常量名)替换为直接引用的过程

符号引用:一个类中如果引用了其他类,但是JVM并不会知道其他类的具体地址在哪,先用符号引用表示,等解析以后再根据唯一的符号找到具体的类地址。字段变量也可以用符号引用
直接引用:和虚拟机布局有关,如果是直接引用,引用的目标对象一定已经加载到内存中了

初始化

  • 执行构造器方法,编译器自动收集所有类变量的赋值动作和静态代码块的语句
  • 初始化类的时候判断父类有没有被初始化,没有的话先初始化父类
  • 保证一个类的方法在多线程下正确加锁和同步

类在什么时候会被加载

  • 创建一个类的实例,new的时候
  • 通过反射获得类的信息
  • 调用一个类的静态方法
  • 访问类或者接口的静态变量或者对其修改赋值
  • 初始化一个子类的时候,会先初始化父类

类的加载机制
有三种

  • 全盘负责:但有一个类加载器负责加在某个Class的时候,其以来和引用其他Class也将被加载
  • 双亲委派:先让父类加载器试图加载Class,当父类无法加载该类的时候试图从自己的类路径中加载这个类。就是让父类先进行加载,依次递归,父类加载器可以加载就成功返回,否则自己加载
  • 缓存机制:保证所有加载过的类都有相应缓存,当程序需要使用到某个Class的时候,类加载器会先从缓存区中找,不存在的话才回去读取类文件二进制转换为Class,存入缓冲区。这也是为什么修改类Class以后需要重启JVM才能生效,因为有缓存的存在

类加载器
主要分为四种

  • 启动类加载器(Bootstrap ClassLoader)
  • 扩展类加载器(Extension ClassLoader)
  • 应用类加载器(Application ClassLoader)
  • 自定义类加载器

启动类加载器:加载lib目录的类,java.xxx(如java.lang.Object)
扩展类加载器:加载lib/ext目录的类,包名是javax.xxx(如javax.swing.xxx)
应用程序扩展器:ClassLoader的getSystemClassLoader的返回值,是默认类加载器
双亲委派的意义在一不同的类加载器之间分别负责搜索范围内的类的加载工作,保证一个类在使用中不会出现不平等的类。(保证一个类不会被不同的类加载器加载,主要的考虑是安全方面,杜绝通过使用和JRE相同的类名冒充现有GRE的类达到替换的攻击方式)

面经

JAVA内存模型和JAVA运行时数据区域,常量放在哪

什么是内存泄漏和如何处理

如何判断对象是否死亡?(两种方法)

  • 引用计数:通过引用计数进行判断,当一个变量引用了该对象,引用计数会加一,当引用断开时会减一,当计数为0的时候会进行回收,但是无法解决循环引用的问题。
  • 可达性分析:从GC Root出发,遍历搜索,把所有可达的对象进行标记,在遍历完以哦胡,没有被搜索标记的对象就是不可达对象,即是不可用的需要回收的对象。通常可以作为GC Root的对象有以下几种:虚拟机栈中的引用对象、方法区中类静态属性引用的对象、方法区中常量引用的对象、本地方法栈中引用的对象

一个线程OOM后,其他线程还能正常运行吗?
可以,一个线程OOM以后,所占用的内存资源会全部被释放,不会影响其他线程的正常运行。

Struct

Hibernate

使用
hibernate.cfg.xml中配置数据库的信息,如driver、用户名密码、连接等
根据数据库的表创建一个实体类
编写相应的xml文件Emp.hbm.xml,里面可以写一些SQL语句
需要创建一个session,然后开启事务beginTransaction进行使用
主要有三种使用
第一种是query查询
分为饿加载和懒加载。使用session.get(xxx)或者session.load(xxx),前者执行后会直接返回结果,后者执行完后,在使用的时候才会进行数据获取

第二种是hql方式
session.createQuery({sq})
通过创建SQL语句,然后使用setParameter()提供参数,获得结果

  • 属性查询
  • 参数查询、命名参数查询
  • 关联查询
  • 分页查询
  • 统计函数

第三种是creteria
先创建一个session.createCreTeria,可以添加一些限定如createria.add(Restrictions.lt(“xxx”,5);之类的然后调用list()获取数据

优势

  • hibernate 是对 jdbc 的封装,大大简化了数据访问层的繁琐的重复性代码。
  • hibernate 是一个优秀的 ORM 实现,很多程度上简化了 DAO 层的编码功能。
  • 可以很方便的进行数据库的移植工作。
  • 提供了缓存机制,是程序执行更改的高效。
    缺点
  • 没法对SQL进行优化
  • 效率比原生JDBC低
  • 不支持批量修改和删除。大批量更新还是直接用SQL好

五个核心接口

  • Cobfiguration:配置,比如启动文件之类的
  • SessionFactory:初始化,创建Session对象,是线程安全的,一个实例可以被多个线程共享,可以作为二级缓存
  • Session接口:负责更新、添加、删除、修改和查询对象,线程不安全,要避免多个线程共享一个Session,可作为一级缓存
  • Transcation接口:负责事务管理
  • Query和Creteia接口:负责数据库的查询执行

使用Integer和int的映射区别
Integer是实体类对象,可以为null;int是基本数据类型不能为null
工作流程

  • 读取并解析配置
  • 读取并解析映射文件,创建SessionFactory
  • 打开Session
  • 创建Transcation
  • 进行持久化操作,就是上面的三种查询
  • 提交事务
  • 关闭Session和SessionFactory

SessionFactory是什么
是一个单例,储存数据的,是线程安全的,可以在多线程中使用。
在启动的时候只能建立一次,应该包装各种单例以至于能简单存在一个代码中。
Session的清理缓存和清空缓存
前者是调用flush()将缓存中的数据刷新写入到数据库中
后者是调用clear()方法,清空缓存,不会写入数据库

缓存机制
有两种缓存

  • 一级缓存:也叫Session缓存,在Session内有效不需要用户干预,减少对数据库的访问,可以选择清除或者刷新缓存。
  • 二级缓存:应用级别的缓存,使用第三方提供缓存服务。其实又叫“SessionFactory缓存”,因为他是个单例,所以它的生命周期和整个程序相对应,因此二级缓存是一个进程范围或者集群范围的,可能有并发的需求,需要使用适当的并发策略。默认情况下不会启用这个。

支持的缓存策略

  • Read-Only只读:适合频繁读取不会更新的,最简单和最有效的策略
  • Read/Write:适合需要被更新的数据,比Read-Only更消耗资源
  • Nonstrict Read/Write:不保障两个同时进行的事务修改同一块数据,适合经常读取极少修改
  • Transcational:完全事务化缓存

hibernate 对象有哪些状态

  • 瞬时/瞬间状态:刚new出来的对象还没有被持久化,不被Session管理
  • 持久化状态:调用Session的save、get、load等方法后,就是持久化状态
  • 游离状态:Session关闭后对象是游离状态

三种状态的转换
当在Session中存储数据的时候,会先保存在Session Map中,然后给数据库保存一份,缓存中的数据就叫持久对象(Persist),Session关闭后,这个仍然存在,但是变为游离态了。
游离态update()的时候会变为持久态。
持久态delete()时会变成瞬时状态,数据库中也没有与之对应的数据了。

getCurrentSession 和 openSession区别是什么
前者是和线程绑定的,事务由Spring控制,不需要手动管理
后者不和线程绑定,需要自己手动开启事务

实体类必须要无参构造函数吗
必须要有无参构造函数,因为要用refaction api,通过CalssnewInstance()应该是反射构造实体类的实例,没有构造函数会报错。
三种检索策略

  • 立即检索:就是饿加载,不管调用什么持久化的方法,都会立刻从一个对象导航关联到另一个对象。缺点是select语句太多可能会加载到不需要的对象浪费资源。lazy=false
  • 延迟检索:就是懒加载,有应用程序决定加载哪些,可以避免过多的select语句和不必要的对象加载,提高检索性能和空间。缺点是如果需要访问游离状态代理类的实例,在持久化阶段就要实例化。lazy=true。通过代理(Proxy)机制来实现延迟加载。Hibernate 从数据库获取某一个对象数据时、获取某一个对象的集合属性值时,或获取某一个对象所关联的另一个对象时,由于没有使用该对象的数据(除标识符外),并不从数据库加载真正的数据,而只是为该对象创建一个代理对象来代表这个对象,这个对象上的所有属性都为默认值;只有在真正需要使用该对象的数据时才创建这个真正的对象,真正从数据库中加载它的数据。使用CGLIB生成代理类
  • 迫切左外连接检索:结合了上面两个优点,完全透明,不论什么状态都可以方便进行关联对象,使用外连接可以减少select语句。缺点是也会有不必要对象的加载,浪费内存空间,复杂数据表连接会消耗性能。

sorted collection 和ordered collection
前者是在内存中使用java进行排序,后者直接在数据库中排序。
对于大数据集最开始用后者,避免出现OOM的情况

查询很慢如何优化

  • 建立索引
  • 减少表之间的关联
  • 减少不必要的字段访问
  • 优化sql语句,不要全表搜索,走索引

persist()和save()的区别
前者不会立刻更新数据库,可能调用flush才会更新,也不会更新缓存数据,没有返回
后者把一个瞬时状态的对象变成持久化状态,返回一个主键值
update()和saveOrUpdate()的区别
前者的调用对象必须处于持久化状态,如果数据库中不存在,就不能使用update()
后者的对象可以是持久化也可以是非持久化的,也就是说持久化的会调用update(),非持久化会调用save()
get()和load()的区别
前者是饿加载,不支持延迟加载,执行后立刻获得数据,如果没有数据对象存在返回null
后者是懒加载,延迟加载,使用的时候才获取数据,没有对象的时候返回一个代理对象;不会立即触发SQL语句,而是返回一个代理对象,只保存了实例的ID值之类的,当需要使用的时候的到期对象,然后执行相应SQL语句;会先查询一句缓存,没有找到的时候返回一个对象,在使用的时候才去检查二级缓存和数据库

Hibernate 和 Mybatis 的区别
相同点:

  • 都可以使用配置文件是生成SessionFactory,然后生成Session,在开启事务执行SQL语句
  • 都支持JDBC的封装

Mybatis:

  • 可以更细致对SQL进行优化,减少查询字段
  • 更容易掌握,使用方便

hIBERNATE:

  • DAO层比MyBatis简单,后者需要维护SQL和结果映射
  • 对象维护和缓存较好,在增删改查方便
  • 一致性较好,MyBatis换一个数据库需要写不同的SQL
  • 有二级缓存支持第三方缓存,MyBatis自身缓存不行

Hibernate和JDBC的区别
相同:

  • 二者都是java对于数据库操作的中间件
  • 两者对数据库的直接操作都是线程不安全的,需要及时关闭和相应同步
  • 两者都可以进行事务的处理

不同:

  • JDBC是sun开发的一套操作数据库的规范,使用的是SQL语言;操作的是数据,通过数据直接在数据库中进行修改
  • Hibernate则是一套框架,对JDBC进行了封装,使用的是HQL语言;操作的对象时持久化对象,底层持久化对象将数据库更新到数据库

HiberNate优化

  • 数据库设计调整
  • HQL优化
  • API的配置使用,不同场景需求使用不同的API
  • 一级缓存管理
  • 事务控制策略
  • 映射文件优化

MyBatis

实现原理
如何配置sql语句
怎么将目标参数绑定到sql语句中?
一级缓存和二级缓存
分页怎么实现
#{}和¥符号有什么区别

Spring

是一个轻量级的java ee框架 ,有以下几个特点:

  • 方便解耦,简化开发
  • 方便程序测试
  • 方便和其他框架整合
  • 方便事务
  • 降低API开发难度

有两个核心部分:

  • IOC:控制反转,创建对象的过程移交给Spring管理。降低代码的耦合度,
  • AOP:面向切面,不改变源代码进行功能增强

IOC控制反转(依赖注入DI)

Bean
Bean其实就是一个对象,由IOC负责管理,应用程序就是一个一个Bean
Bean主要管理对象和依赖以及依赖注入的问题,bean其实是一个java对象,主要有以下几个规范

  • 所有属性是private
  • 有getter和setter
  • 实现serializable接口
  • 提供默认构造函数

IOC实现对象之间的松耦合,最常用的方法是依赖注入和依赖查找。目的是降低耦合
底层原理主要是:xml解析,工厂模式,反射
主要有两点
使用私有属性保存依赖对象,只在构造函数中通过参数传入
比如有一个Person依赖Computer类,符合IOC的方式如下

public class Person {private Computer computer;public Person(Computer computer) {this.computer = computer;}
}

不符合IOC的如下

// 直接在Person里实例化Computer类
public class Person {private Computer computer = new Computer("AMD", 3);
}// 通过【非构造函数】传入依赖
public class Person {private Computer computer;public void init(Computer computer) {this.computer = computer;}

让Spring控制类的构建过程
不手动去new,让Spring来做,在启动的时候会把需要的类进行实例化,如果需要依赖,会先将依赖实例化,然后实例化当前类,依赖就是通过构造函数的参数传入的,就是依赖注入。
综上,类的实例化、依赖的实例化和依赖注入都是Spring Bean实现的,而不是通过new的方式或者非构造函数传入的方式注入依赖。
提供了三种管理方式和注入方式
管理方式

  • XML显式管理
  • Java中显式配置
  • 注解的方式

注入方式

  • 构造函数注入
  • setter注入
  • 注解的方式

AOP面向切片编程

指将与业务无关的,但是被多个模块共同调用的代码封装起来,减少重复代码,降低模块之间的耦合。
关键在于代理模式。
OOP面向对象的编程是如果多个子类有相同的方法的话避免复用可以将相应方法写在父类里面,提取共有属性直接在子类里面用。但是如果父类里面有这种需求,比如父类多个方法里面有复用的方法,就需要使用AOP了,常用于事务控制、权限校验和日志打印等场景
Spring的AOP是动态代理模式,不会修改字节码文件,而是运行时每次在内存中临时生成一个AOP对象。
Aspect AOP就是静态代理模式,会在编译的时候生成AOP代理类,将AspectJ(切面)注入java字节码中,运行以后就是增强的AOP对象。
比如说实现Cloneable接口就可以使得类可以使用clone方法进行对象的复制

面经

spring有哪些模块

有七大模块

  • Spring Core:是核心模块,主要实现了IOC
  • Spring Context:基于bean提供上下文管理,扩展出国际化、校验等功能
  • AOP:使用了拦截器机制,并且提供了常用的拦截器
  • ORM:本身不实现ORM,只是对常用的ORM框架提供了支持
  • DAO:提供了对于jdbc的支持,允许jdbc使用Spring资源,统一管理事务,但是也是不会具体实现jdbc
  • WEB:对于常用框架比如Struct等的支持,将Spring的资源注入到框架里,并且给框架配置拦截器实现相应的功能
  • MVC:可以使用Struct也可以使用自己的MVC框架

spring特性
就是IOC、AOP和DI
spring mvc有哪些组件
有五大组件:

  • DispatcherServlet(前端控制器):所有请求在这里进行转发,通过HandlerMapping判断使用什么模型处理请求,会确定到具体的Controller
  • HandlerMapping(映射处理器):请求路径和模型的对应关系,可以实现用户请求到Controller的映射
  • Controller(控制器):负责最终的业务逻辑的处理,需要对并发用户实现请求的处理,因此需要线程安全并且可重用。业务逻辑处理完后,会返回一个ModelAndView对象给前端控制器,包括Model和View
  • ModelAndView:封装业务逻辑的处理结果
  • ViewResolver(视图解析器):在Web中查找View视图并且进行渲染展示给用户,比如说jsp、甚至pdf、excel

spring mvc执行流程

  1. 用户发送请求到前端控制器DispatcherServlet
  2. 前端控制器说到请求以后调用HandlerMapping进行映射处理
  3. HandlerMapping找到请求所对应的处理器,生成处理器对象以及处理器拦截器(如果有的话),传给前端控制器
  4. 前端控制器调用HandlerAdapter具体的处理器进行执行(如Controller)
  5. Controller执行完业务逻辑以后,会返回一个ModelAndView对象
  6. HandlerAdapter将执行结果ModelAndView返回给前端控制器
  7. 前端控制器将ModelAndView传给ViewResolver视图解析器
  8. ViewResolver返回解析后的View视图
  9. 前端控制器根据View进行页面的渲染(如将模型数据填充到视图李)
  10. DispatcherServlet相应给用户

如何理解Spring的自动装配
Spring会通过上下文自动找出对应的依赖项的类,并且自动给Bean装配相关的属性
有两种方式:使用xml文件和使用注解
IOC和AOP原理
IOC的实现
AOP实现(静态和动态)
循环依赖怎么解决
多个bean之间相互依赖,形成了一个环。一般是指默认单例bean中,出现了属性相互依赖的场景。
通过单例Bean的三级缓存实现:

  • 第一级缓存:存放已经经历了完整生命周期的Bean对象
  • 第二级缓存:存放早期暴露出来的Bean对象,生命周期没有结束
  • 第三级缓存:Map<String, ObiectFactory<?>> singletonFactories,存放可以生成Bean的工厂

使用的哪种动态代理
Spring的优点,用到了哪些设计模式,IOC和AOP的理解
Bean的生命周期
解释Spring支持的几种bean的作用域
BeanFactory、FactoryBean的区别
SpringMVC、Mybatis 执行过程
Spring事务

SpringBoot

Boot是在Spring基础上消除了设置Spring应用所需要的XML配置,更加高效。

SpringBoot和Spring区别
Spring是一个框架,为web开发提供了全面的基础架构支持,有很多好的功能,依赖注入和开箱即用的模块等,包括:Spring JDBC、MVC、Security、AOP、ORM等
这些模块可以极大缩短应用开发时间。
SpringBoot则是Spring的一个扩展,消除了设置Spring应用程序所需要的复杂的配置环节。
同样致力于更快速更高效开发web程序,主要功能包括:使用starter简化程序的配置、可以直接main函数启动有嵌入tomcat服务器直接使用、尽可能自动化配置Spring
springboot一些特点

  • 可以自动检查项目里的依赖项等,然后自动配置
  • 通过thymeleaf依赖自动配置对于thymelead的支持,而不需要在视图解析器里面配置依赖项
  • 通过security依赖可以实现相应的安全防护,

注解@RestController和@ResponseBody的区别
前者等于Controller和ResponseBody两个合起来,当类中所有方法都需要返回json或者xml的时候,用这个注解,是针对类的。
后者一般置于方法前面,不会走视图控制器,而是直接返回文本信息给前端,比如json或者xml,是针对某个方法专门编写的。

redis

redis知识点必备
Redis数据结构及各结构的内部实现
是键值形式的,其中值的数据类型包括:String(字符串)、List(列表)、Hash(哈希)、Set(集合)和Zset(有序集合),也就是数据的保存形式,其底层实现的方式用到了数据结构

  • String:是二进制安全的,用字节的形式保存起来,Redis是C语言编写的,没有直接采用char*存储而是自己封装了一个简单动态字符串(SDS)表示字符串。可以保存任何数据,比如jpg图片或者序列化的对象数据,最大可以储存512MB

    • C语言中的bug:获取字符串长度时间复杂度O(n);\0结尾,不好保存二进制数据;字符串操作不高效也不安全,存在缓冲区溢出风险
    • SDS包含:
      • len(字符串长度):直接获取长度时间复杂度O(1)
      • alloc(分配的空间长度):修改字符串的时候alloc-len直接判断是否需要扩展
      • flags(SDS类型):一共有五种,sdshdr5,sdshdr8,sdshdr16、sdshdr32、sdshdr64
      • buf(字节数组):实际存放数据的,字符和二进制都可以保存
  • List:底层实现是链表,封装了一套链表操作,最多可以有2 ^ 32 – 1 个元素。是双向链表,获取前后数据的时间复杂度O(1);通过头尾指针可以直接获取到头尾节点;获取长度len也是O(1)。不足之处:节点不连续无法很好利用cpu缓存,只有数组连续地址才好用缓存。在redis3.0里面使用ziplist压缩列表,节约空间内存紧凑但是有性能问题(无法保存过多数据否则查询效率低、新增修改元素的时候占用的内存空间可能会修改);5.0使用listpack结构
  • Hash:能够O(1)查找到数据,很快,哈希本质上是数组。风险是哈希表大小固定以后数据越多冲突越大,用了链地址法解决哈希冲突。
  • Set:是string类型元素的集合,且不允许重复的成员。一堆不重复的值,无顺序的,底层是整数集合,无法使用下标获取元素。除了支持增删改查之外,还支持多个集合取交集、并集和差集等等
  • Zset:是string类型元素的集合,且不允许重复的成员。。底层实现是跳表,也是唯一一个使用两个数据结构实现的。一个是跳表一个是哈希表又可以高校范围查找也可以高效单点查询。每个元素都会关联一个double类型的分数。redis正是通过分数来为集合中的成员进行从小到大的排序。zset的成员是唯一的,但分数(score)却可以重复。
    • 跳表:通过对链表加多级索引的机制就是跳表。时间复杂度可以降低为O(logn),索引节点树是前一层的一半,类似二分查找。为了保持严格的2:1关系,插入和删除节点以后,需要调整整个跳表结构,效率较低。

Redis为什么快
主要原因是基于内存的,所有操作都是在内存上完成的,所以较快;
数据结构较为简单操作较快,数据结构能够高效进行增删改查;
采用单线程模型,避免线程切换带来的额外开销(上下文切换寄存器切换等),也不用考虑锁带来的开销或者死锁;
单线程使用了多路复用IO,非阻塞式IO
缓存穿透
查询一个一定不存在数据的时候,缓存中查询不到,那么就需要去数据库中进行查询,数据库还是查询不到,那么就不会写入缓存,导致每次查询这种数据的时候都会取数据库中进行查询,性能下降。
解决方法:

  • 在查询一个数据为空的时候(不管是不存在还是系统异常),把这个空结果也保存在缓存中,但是过期时间会比较短,一般不超过五分钟
  • 布隆过滤器:将所有数据的哈希保存在bitmap里,利用bitmap进行过滤判断

带来的问题:

  • 这样可能会导致有很多的键出现,占用更多空间,所以需要尽快让其剔除
  • 缓存和数据更新之间可能会存在一个偏差,比如有一个缓存空数据,但是这时候这个数据添加进来了,就会导致结果不一致,利用消息系统等清除缓存中的空数据

缓存雪崩
如果缓存集中在同一时间失效,那么所有压力都落在数据库上,造成了缓存雪崩。
解决方法

  • 加锁排队:失效以后,使用锁机制控制数据库的读写线程数,比如针对一个key只允许一个线程读写,其他线程等待
  • 数据预热:通过缓存reload机制,预先更新缓存时间,在即将大并发之前,手动触发加载缓存的不同key,使得过期时间分布均匀一些
  • 二级缓存:Cache1(原始缓存)短期缓存失效以后,查询Cache2(拷贝缓存)长期缓存
  • 随机值:设置过期时间的时候添加一个随机值,避免都落在同一时间

如何保证数据库和缓存的一致性
理论上,过期时间较为合理的话,就能够保证。当缓存过期,就回去数据库中读取,然后会更新到缓存中,从而保证一致性。
增删改查数据库的时候同步更新redis,可以使用事务机制保证一致性。
大致有四种方案:

  • 更新数据库、更新缓存
  • 更新缓存、更新数据库
  • 删除缓存、更新数据库
  • 更新数据库、删除缓存

前两种不会用,第一种在高并发饿时候可能会出现缓存读取到了数据库的脏数据,不是最新数据;第二种会出现数据库更新失败的情况,从而导致不一致。
方案3会出现,当A修改数据的时候,先删除缓存,B查询数据发现没有缓存,读取数据库的旧值并写入缓存,A将新值写入数据库,这样导致永远都是旧值。解决方案是:

  • 延时清除:在更新完数据库后,延时再删除一次缓存,这样就可以强制更新缓存了
  • 异步串行化:一个数据一个队列,当数据更新的时候,先清除缓存,然后更新数据库,如果此时数据库没有更新完成但是有读操作到来,那么将这个读的操作放在更新数据库操作的后面,串行化,等数据库更新完以后,再更新缓存;如果有多个读操作,如果队列里面已经有了,就不重复放进去了,等到更新完操作就可以读取了

方案4会出现如果缓存没有删除成功,每次读取到的都是旧数据了。是用消息队列进行补偿。对数据进行数据库的更新成功以后,删除redis缓存,删除失败以后发送一个消息给消息队列,然后收到消息以后取出进行重新删除缓存的操作。
Redis持久化
持久化就是将数据保存在磁盘上永久保存下来,避免宕机等原因导致数据丢失。
主要有两种:RDB(默认)和AOF。
RDB
就是Redis Database,会按照一定周期策略将内存数据以快照的形式保存到磁盘上的二进制文件里。
产生的数据文件是dump.rdb,有两个主要函数:rdbSave(生成rdb文件)和rdbLoad(文件加载到内存)
Append-only file
就是Redis收到的每一条命令,都会以追加的形式写入到文件中去,相当于MySQL的binlog,重启Redis后会重新执行这个文件中的命令回复内存中的数据。
当服务器执行定时任务或者函数的时候会调用flushAppendOnlyFile函数,执行两个操作:先根据条件将aof_ buf缓存的数据写入到AOF文件中,然后根据条件将文件内容写入到磁盘上。
区别:

  • AOF比RDB更新的频率高,优先使用AOF恢复数据
  • AOF更安全也更大
  • RDB性能更好
  • 如果都配置了优先使用AOF

内存淘汰策略

  • volatile-lru:
  • volatile-ttl:

Redis通信协议
Redis事务
Redis 的过期策略以及内存淘汰机制
Redis主从复制机制redis 集群模式的工作原理能说一下么?在集群模式下,redis 的 key 是如何寻址的?分布式寻址都有哪些算法?了解一致性 hash 算法吗?
Redis 和 DB 一致性?缓存穿透?缓存击穿?缓存雪崩?缓存清洗?并发竞争Key?

Nginx

Nginx是一个反向代理服务器,占用内存绩效,启动极快,高并发能力强。
正向代理就是浏览器主动请求代理服务器,由代理服务器将请求转发到目标服务器上。
反向代理是部署在Web服务器上,代理所有外部网络对内部网络的请求,对用户是不可见的,被动,由代理服务器选择将请求转发到合适的服务器上。
因为WSGI server并发量很低,所以需要一个专门的服务器软件进行缓冲。
作用:

  • 安全:客户端对web服务器访问需要先经过反向代理服务器,可以阻止外部直接对内部服务器的访问攻击等
  • 负载均衡:反向代理服务器可以根据Web服务器的负载情况,动态把HTTP请求分发给不同的内部服务器处理
  • 提高性能:比如如果直接用WSGI server,单进程一次只能处理一个请求,过来会阻塞一下很影响性能。反向代理可以先让代理服务器接收完整请求以后转发给服务器,会提高性能

Nginx优势

  • 跨平台、配置简单,还开源
  • 非阻塞式、高并发:可以处理两三万的连接,官方可以处理5w
  • 还有健康检查功能,一个服务器宕机了,就不会给他发送请求

应用场景

  • http服务器:可以独立提供http服务,访问静态资源
  • 虚拟主机:在一个服务器上虚拟出多个网站,比如个人建站使用
  • 反向代理:用于负载均衡,当网站访问量大的时候,单机不能够满足需求,可以配置多台服务器,使用Nginx做反向代理,将请求分发给多台服务器,同时可以检测故障,自动过滤出问题的服务器
  • 安全管理:可以搭建API网关,对接口的服务进行拦截

如何处理请求

  • 启动的时候先检查配置文件,根据IP和端口初始化对应的Socket
  • fork出多个进程,子进程会竞争新的连接。当客户端和Nginx三次握手成功后,就会有一个子进程accept成功
  • 设置读写事件处理函数,添加读写事件进行数据交换
  • Nginx或客户端主动关闭连接

如何实现高并发
异步、非阻塞、epoll的应用。
web服务器正好是IO密集型,使得Nginx将等待网络传输之类的时间,空出来。基于事件模型
是一master多worker模型。
master负责接收转发请求,有请求到来的时候,拉起一个worker进程处理请求。同时需要监控worker的状态,保证HA。
worker数一般和cpu核心数一样,worker和apaache不一样,Nginx的只要内存够大,一个worker就可以处理多个请求。而Apache一个进程一个时间只能处理一个,因此需要几百上千进程。
每次进来一个request,会有一个worker处理,但是不是全程处理,当处理到需要阻塞的时候,比如将请求转发了等待结果,他就不会继续等了,而是会注册一个事件:如果又返回了通知我。也就是事件驱动。这个时候就可以继续接受其他请求了。
Nginx目录结构

  • conf:所有的配置文件的目录
  • html:存放站点的默认目录,包括一些错误页面50x.html,还有默认首页index.html等
  • logs:日志文件目录,包括access.log(访问日志文件)、error.log(错误日志文件)
  • proxy_temp
  • sbin:Nginx命令目录,其中包括nginx(Nginx启动命令)
  • scgi_temp
  • uwsgi_temp

配置文件nginx.conf有哪些属性模块

  • events事件区块里面的work_connections指定每个worker的最大连接数
  • http里面包含有超时时间、默认媒体类型(octet-stream)
    • http里面的server区块,每个server区块表示一个独立的虚拟主机站点。包括监听的端口、错误页配置、站点跟目录默认首页文件

为什么Nginx不用多线程
Apache是通过创建多个进程或者线程单独处理一个请求的,如果太多的话,cpu和内存的消耗会较大,并大多的时候会榨干系统资源
而Nginx用的是单线程的异步非阻塞式(epoll),不会给每个请求单独分配进程或者线程,节省了大量的cpu和内存资源,同时也避免了切换造成的上下文切换
Nginx如何负载均衡
主要有五种策略

  • 轮询:根据时间逐一分配到不同的后端服务器
  • 权重:每个服务器在配置文件中赋予一个权重,权重越大分配到的概率越高,主要用于每个服务器性能不均衡的情况下,肯定优先高性能服务器
  • ip_hash:每个请求按照IP进行哈希,根据hash分配对应服务器,可以解决动态网页session的共享问题
  • fair(第三方插件):更加智能,可以根据页面加载事件和页面大小只能进行负载均衡,优先分配响应时间短的
  • url_hash(第三方插件):根据url的hash进行服务器分配,可以进一步提高后端缓存服务器的效率

如何配置虚拟主机

  • 基于域名的虚拟主机:需要创建一个/data/www、/data/bbs,在hosts添加域名和虚拟机IP的解析,对应域名网站目录下新增index.html
  • 基于端口的虚拟主机:使用端口号区分,每一个server区块可以配置监听端口和location,访问监听端口就会跳转到location指定的url(含端口号)

location的作用
就是根据用户请求的URI执行不同的应用,就是匹配URL。里面可以指定访问的路径和默认主页;或者跳转到对应转发URL的指定主页
可以使用一些正则进行匹配
Nginx如何限流
主要有三种方式:正常限制访问频率(正常流量)、突发限制访问频率(突发流量)、限制并发连接数。
都是基于漏桶流算法实现的。
正常限制访问流量:通过模块ngx_http_limit_req_module限制访问频率,如果还有请求没有处理完,会拒绝该用户请求
突发限制访问流量:上面的一个缺点就是对于突发流量无法进行处理,通过参数burst和nodelay可以设置能够处理超过最大请求的请求个数,如果设置为5,说明可以对于一个用户额外处理五个请求,超过就会慢慢来。
限制并发连接数:通过模块可以设置单个IP最大连接数是十个连接,以及整个虚拟服务器最大并发是100个连接,只有header被服务器处理才算一个连接数
漏桶算法和令牌算法

  • 漏桶算法:其实就相当于是一个缓冲区,缓冲区每次比如处理一个请求,如果非空,新请求都可以进来等待处理,如果满了就拒绝新的请求
  • 令牌桶算法:相当于挂号,系统维护一个令牌桶,一定的速度生成令牌,如果有请求到来需要先获取令牌以后再处理,如果没有令牌,请求无法被处理。通过控制令牌桶的大小和令牌生成速率进行限流

Nginx的高可用
当一个服务器出现故障无法及时响应的时候,就会轮询下一台服务器。

面经 - JAVA知识点相关推荐

  1. Java知识点总结(JavaIO-合并流类)

    Java知识点总结(JavaIO- 合并流类 ) @(Java知识点总结)[Java, JavaIO] [toc] 合并流的主要功能是将两文件的内容合并成一个文件 public class Demo1 ...

  2. 给Java新手的一些建议——Java知识点归纳(Java基础部分)

    写这篇文章的目的是想总结一下自己这么多年来使用java的一些心得体会,主要是和一些java基础知识点相关的,所以也希望能分享给刚刚入门的Java程序员和打算入Java开发这个行当的准新手们,希望可以给 ...

  3. Java知识点总结(JavaIO- System类对IO的支持与Scanner类 )

    Java知识点总结(JavaIO- System类对IO的支持与Scanner类 ) @(Java知识点总结)[Java, JavaIO] [toc] System类 public class Dem ...

  4. Java知识点总结(JDBC-封装JDBC)

    Java知识点总结(JDBC-封装JDBC) @(Java知识点总结)[Java, JDBC] 封装JDBC src目录下新建一个db.properties文件,用于封装数据库连接信息 把获取数据库连 ...

  5. Java知识点总结(Java容器-EnumSet)

    Java知识点总结(Java容器-EnumSet) @(Java知识点总结)[Java, Java容器, JavaCollection, JavaSet] EnumSet EnumSet是一个专为枚举 ...

  6. java webservice接口开发_给Java新手的一些建议----Java知识点归纳(J2EE and Web 部分)

    J2EE(Java2 Enterprise Edition) 刚出现时一般会用于开发企业内部的应用系统,特别是web应用,所以渐渐,有些人就会把J2EE和web模式画上了等号.但是其实 J2EE 里面 ...

  7. Java知识点总结(注解-内置注解)

    Java知识点总结(注解-内置注解) @(Java知识点总结)[Java, 注解] @Override 定义在java.lang.Override 中,此注释只适用于修饰方法,表示一个方法声明打算重写 ...

  8. Java知识点总结(Java容器-ArrayList)

    Java知识点总结(Java容器-ArrayList) @(Java知识点总结)[Java, Java容器, JavaCollection, JavaList] ArrayList 底层实现是数组,访 ...

  9. Java知识点总结(反射-获取类的信息)

    Java知识点总结(反射-获取类的信息) @(Java知识点总结)[Java, 反射] 应用反射的API,获取类的信息(类的名字.属性.方法.构造器等) import java.lang.reflec ...

  10. Java知识点汇总以及常见面试题

    Java知识点汇总以及常见面试题 1. "=="和equals()的区别 2. 构造方法能不能重写或者重载 3. 基本数据类型 4. 匿名内部类能被继承? 5. Integer和i ...

最新文章

  1. 修改Tomcat默认端口号,避免与IDEA冲突
  2. 【AI不惑境】移动端高效网络,卷积拆分和分组的精髓
  3. linux安装截图讲解01
  4. Java-Map从入门到性能分析3【LinkedHashMap(性能耗时对比、模拟LRU缓存)、TreeMap(排序、性能对比)】
  5. Apollo核心概念
  6. VS2012 中 c++项目中的各个选项介绍
  7. 60TB 数据量的作业从 Hive 迁移到 Spark 在 Facebook 的实践
  8. idea mysql错误提示_idea提示错误:java.lang.ClassNotFoundException: com.mysql.jdbc.Driver
  9. 面试题:为什么局部变量不赋初始值报错
  10. 《深入浅出DPDK》读书笔记(十一):DPDK虚拟化技术篇(I/O虚拟化、CPU虚拟化、内存虚拟化、VT-d、I/O透传)
  11. 使用内存数据库的.NET Core 3.0中基于身份(Identity-Based)的身份验证
  12. 口腔取模过程及注意事项_为什么牙齿矫正前要拍片取模,没有拍片取模就设计不了详细方案!...
  13. jquery cookie 插件 (支持json对象) 可以跟jquery 集成 也可以单独使用
  14. 四川服务器磁盘阵列卡电池性能,内置磁盘阵列卡的不足之处
  15. PyTorch多进程子进程瘫痪,解决办法
  16. 五阶魔方公式java_五阶魔方降阶法公式是什么?
  17. 3、Nginx系列之: location和alias的区别
  18. Matlab(Simulink)+ANSYS Simplorer+Maxwell联合仿真(一)——软件选取问题
  19. NASBench101-安装及简单样例使用指南
  20. 郑莉老师c++第五版 复习笔记

热门文章

  1. 【考试总结贴】工程测量学
  2. w10投影全屏设置_win10投影仪怎么铺满全屏|win10投影器全屏的设置方法
  3. seekbar 的用法
  4. 如何将多个.TXT文本文件合并到一个excel表中
  5. vultarget-a红日靶场全面解析(完整版)
  6. 上传文件,路径为C:\fakepath\的问题(待跟进)
  7. 由于应用程序配置不正确,应用程序未能启动。重新安装应用程序可能会纠正这个问题
  8. MP3比特率编码模式
  9. 修改BT种子的tracker服务器list
  10. ps4手柄驱动linux,GeForce 344.11正式版驱动:支持GTX 980/970,集成DSR选项