有八种基本的数据类型
类似于python的可变和不可变
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默认的其实和等号一样,就是直接调用等号进行判断,不一样的就是可以覆盖,就是通过重写让他判断具体的值。
哈希就是返回对象在内存中地址转换成的一个int值,如果没有重写,所有对象的值都不一样。
为什么重写equals也要重写hashcode
因为哈希冲突的存在,所以不同对象的哈希值可能会一致。
为了提高效率,再重写了hashcode方法以后,比较的时候会默认判断哈希值,如果不一样就不需要equals了。
如果哈希相等,不一定一样,需要继续equals。否则不一样,大大提高判断效率。
如果单纯重写equals判断值相同,那么可能还是会返回false,因为会先判断哈希,哈希不相等所以是false
值传递是传递一个拷贝,引用就是传递一个变量地址。
但是其实本质上还是一个值传递,对于基本变量类型传递的是具体的值(传入一个拷贝),对于不可变对象比如stringbuffer传递的是引用的地址作为值(传入一个地址的拷贝)。
主要有三种:List、Map和Set
主要分为两大类:
ArrayList
存储的元素是有序的,可重复的,线程不安全的
底层实现是Object数组,因此支持随机搜索,插入删除的话会有一些效率影响,毕竟是数组。
无参的时候是一个空数组,添加数据的时候才会进行扩容操作,长度是10.(JDK6无参的时候默认长度是10)。接下来的扩容一般是当前长度的1.5倍
LinkedList
存储的元素是有序的,可重复的,线程不安全的
底层是双向链表,JDK1.6之前是双向循环,之后是双向,不支持随机索引
每一个元素都需要消耗比ArrayList更多的空间存储前驱和后继指针
Vector
存储的元素是有序的,可重复的,线程安全的
底层是Object数组
list的迭代
Iterator it = list.iterator(); while (it.hasNext()) {}
ListIterator listIt = list.listIterator(); while (listIt.hasNext()) {}或者while (listIt.hasPrevious()) {}
重要的hashmap
HashMap
一个HashMap跟面试官扯了半个小时
存储的元素是无序的,不可重复的,线程不安全的,因此单线程里效率非常高。
存储的时候,先调用hashcode()
判断元素应该存储的位置,然后调用equals()
判断当前位置是否有元素或者是否相同,相同就更新,不相同的话插入到链表里
多线程需要考虑HashTable和currentHashMap
底层是数组+链表+红黑树
为什么是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主要做了以下几点优化
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),每次操作的时候只给对应桶上锁,不影响其他的桶。
并发控制使用sunchronized和CAS实现
HashSet
存储的元素是无序的,不可重复,线程不安全的
底层的数据结构是HashMap,用于不需要保证数据的存入和取出的场景
TreeSet
存储的元素是有序的,不可重复,线程不安全的
底层结构是红黑树(自平衡二叉树)
为某些特定类型或者变量对象提供单一的存储空间;在不创建对象的情况下可以使用类的变量和方法。
static静态代码块可以是随着类的加载加载的,只会执行一次,在main之前执行。内存中static修饰的变量存在于方法区中。
声明属性、方法和类。分别表示不可变、不可覆盖和不可继承。
变量不可变包括引用不可变和对象不可变,如下,上面会输出ssszzz,下面则会报错。因为上面赋值实质上只是改变了内存里的值,而s的值是内存地址没有改变,因此下面就报错了,因为保存的地址是不能改变的。
final StringBuilder s=new StringBuffer("sss");
s.append("zzz");
s=new StringBuffer("kkk");
不允许多继承,但是可以通过实现多个接口实现多继承。
多继承还可以通过内部类实现。
Java反射(超详细!)
通过反射机制可以实现对于字节码的修改操作
应用场景
优缺点:
有几个比较重要的类:
要先获取Class然后才可以得到Method、Constructor和Field
如何获取一个类:
通过反射实例化对象,要保证这个类有一个无参构造函数,因为会默认调用
TargetObject object=(TargetObject)TestClass.newInstance();
如果只需要执行一个类的静态代码块
Class.forName(“balabala”)
Class一些常用的方法
Field常用方法
Methods常用方法
小例子
假设我现在有一个类
第一种:调用public方法并且传入参数
import java.lang.reflect.Method;
public class prac{
public static void main(String[] args) throws Exception{
//通过类名获得Class
Class<?> test=Class.forName("javap.main.java.TargetObject");
//获得这个类的所有声明方法,包括public和private
Method[] 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;
}
@Override
public 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()方法,在这里面进行比如前后执行的操作
大致步骤:
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;
}
需要引入依赖
区别
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 类常用方法有哪些
java 中是值传递引用传递
java是值传递,但是对于基本数据类型,传递的是值,而对于引用类型,传递的则是地址值的拷贝
构造方法能否被重写,重载呢
可以重载,但是不能重写,构造方法名是和类名一样的,如果重写了说明子类和父类类名一致,矛盾了,所以不能重写
自动装箱和自动拆箱
自动装箱就是java可以将基本数据类型自动转为对应的对象,比如将int基本类型转为Integer类的对象,调用Integer.valueOf()或者String.valueOf()等
将Integer类对象转为int基本数据类型,就是拆箱,比如调用object.intValue()
然后这个对应的类就叫包装类,使得基本数据类型能够具有类中对象的特征。
在泛型里面会使用到,如List,这里面只能放类的对象,不能放基本数据类型
包装类也包括有八个
java中的修饰符
接口和抽象类的特点和区别
接口:使用interface修饰;不可实例化;一个类可以实现多个接口;实现类一定要实现接口里的所有方法,在接口类里面只有方法的声明;接口里的变量通常是静态变量
抽象类:用abstrate修饰;一个类只能够继承一个抽象类;抽象类允许只实现部分方法;抽象类里除了有方法的声明还可以有方法的实现。
普通类:可以直接实例化;抽象类的子类必须重写父类的抽象方法,普通类的子类可以重写也可以直接调用;不用abstrate修饰;
有哪些集合?哪些是线程安全的不安全的,为什么呢
线程安全:
线程不安全:
hashMap的jdk1.7和jdk1.8有什么区别?为什么要用红黑树
区别主要是底层数据结构的变化,1.7中使用数组+链表的形式,1.8则采用了数组+链表+红黑树的结构,当链表长度为8且数组长度大于64的时候链表会转为红黑树,长度低于6的时候会转为链表。
红黑树是一种平衡二叉搜索树,能够通过左旋和右旋变色保持树的平衡。链表复杂度O(n),红黑树复杂度O(logn)。
为什么长度大于8以及大于64的时候才转换,因为节点太少没必要转换,转换的时候需要浪费给时间和空间;俄日什么一开始不转换,因为树的结构浪费空间,只有节点多的时候这个结构才有优势,节点少的时候会转为链表占用空间少。
HashMap的迭代方式
Iterator<Map.Entry<Integer,Integer>> iter=map.entrySet().iterator();
while (iter.hasNext()){
Map.Entry<Integer,Integer> item=iter.next();
System.out.println(item.getKey());
}
for (Map.Entry<Integer,Integer> item:map.entrySet()){
System.out.println(item.getValue());
}
For Entry KeySet:和上面差不多,用key访问
map.forEach((key,value)->{
System.out.println(key+"\t"+value);
});
map.entrySet().stream().forEach((entry) -> {
System.out.println(entry.getKey());
System.out.println(entry.getValue());
});
map.entrySet().parallelStream().forEach((entry) -> {
System.out.println(entry.getKey());
System.out.println(entry.getValue());
});
final和finally
final是一个关键词,用于修饰变量、方法和类等,被修饰的不能够可变、不能继承、不能被重写
finally是异常处理机制里的一个关键词,和try、catch一起连用,不管有没有异常,都会执行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的初始容量、加载因子、扩容增量
有序的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主要包括如RuntimeException
,ArrayIndexOutOfBoundsException
,NullPointerException
,ClassNotFoundException
异常处理一般有两种方法
Exception分为CheckedException和UncheckedException
前者受检查异常如果没有被catch或者throws,那么不能通过编译,只有RuntimeException及其子类不是是这一类,其余异常都是
RuntimeException
线程安全有三要素:
在多线程访问互斥资源的时候需要用到锁。
可以锁住一个代码块,比如在线程里,用这个锁住这个循环,在执行这个代码块的时候就会被锁住其他线程无法执行
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
java对象由三部分组成
然后每个对象都有与之关联的monitor
对象,里面有一些属性信息,这个对象有一个线程内部竞争锁的机制。
JDK6以前的实现
A和B需要修改数据的时候,发现方法是被修饰过的,此时A被调度,A就抢先得到锁,步骤为:设置MonitorObject
的_owner
为A线程、mark word
设置Monitor地址,锁标志位改为10、B线程放在ContentionList阻塞。
每次会从ContentionList里面选择有资格成为候选线程的放在EntryList里面
是一种非公平锁
底层调用的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{
@Override
public 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差不多只不过可以有返回值。
大致步骤是:
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{
@Override
public void run() {
for (int i = 0; i <= 100; i++) {
if (i % 2 == 0) {
System.out.println("线程:"+Thread.currentThread().getName() + ",输出偶数" + i);
}
}
}
}
//输出奇数
class NumberThread2 implements Runnable{
@Override
public 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()方法适合Runnable
executorService.execute(new NumberThread1());
executorService.execute(new NumberThread2());
//submit()方法适合Callable
//关闭线程池
executorService.shutdown();
}
}
Java内存模型有三个特性
volatile可以保证可见性和有序性。
可见性
对于非volatile变量,读写,每次都要从线程将变量拷贝到CPU缓存中,如果有多个CPU线程可能会将其拷贝到多个Cache中。
而如果是volatile,会保证每次都从内存中读取变量,而不会去找CPU Cache。
进行写操作的时候,会在后面加一条store屏障指令,将工作内存中的共享变量写入到主内存中
进行读操作的时候,会在后面加一条load屏障指令,从主内存中读取
用于修饰会被多线程访问的对象,保持修改对所有线程可见。
不保证第三个要素,因此不严格线程安全。
在对一个字段进行修改的时候,jvm会执行Write-Barrier操作,将当前缓存的数据写入到系统内存,使得其他核心引用了改地址的数据变成了脏数据。
读取的时候,会再执行一个Read-Barrier执行,如果是脏数据,就从内存重新获取。
IO、NIO和BIO、AIO
BIO:同步阻塞IO,一个连接一个线程,客户端每有一个连接就需要启动一个线程处理请求,在不做什么事的时候对应线程会有不必要的资源开销,适合连接较少而且比较固定的场景
NIO:同步非阻塞IO(Non-blocking),一个请求一个线程,客户端发来的请求会被注册到多路复用器,多路复用轮询到有一个请求到来的时候,会创建一个线程进行处理,适用于连接数多而且连接时间较短的场景,比如聊天服务器
AIO:异步非阻塞IO:一个有效请求一个线程,IO请求一般由系统先完成以后再通知服务器启动线程处理,适用于连接数目多而且连接时间长的场景。用户发送一个请求,不管数据准没准备好都会收到一个返回结果,然后用户可以发送其他请求,之后如果有数据准备好了会通知用户
同步和异步
同步是发送一个请求需要等待结果返回,之后再发送下一个请求,避免死锁、脏读等问题
异步指的是发送一个请求不等待结果返回,可以直接发送下一个请求,提高效率保证并发
二者主要区别在于是否等待结果返回
多路复用
多路复用其实就是异步阻塞IO模型,select和epoll都是多路复用的实现。
redis单线程快的原因就是多路复用IO和缓存
用户发出请求,当数据准备好以后,会通知用户,然后用户会对再线程进行相应操作
select:一个进程监听所有的文件描述符fd,通过不断轮询所有的fd判断哪个活跃
poll:提高了可以打开的fd的个数,1G内存可以打开10w左右,使用了链表的结构,以链表的方式存储在内核中,所以没有个数限制。将用户传入的数组拷贝到内核,进行遍历,查询每个fd状态,时间复杂度还是O(n)
epoll:实质上是事件驱动的,当fd活跃的时候,会把哪个流发生什么IO事件通知用户进行处理,时间复杂度O(1)
synchronized的使用方式、底层实现以及JDK1.6的优化?
谈谈 synchronized和ReentrantLock 的区别?
CAS
这个其实就是MySQL里面的那个机制。就是会记录三个值:修改值的地址、旧的值、期望的值。当获取数据的时候会记录这几个值,在操作完进行修改的时候,会先判断当前的值是不是旧值,如果是就修改为新的,否则会认为被其他线程修改了,就不会执行当前修改操作,而是会不断等待,等的时间长了对系统负担影响较大。
会出现ABA的问题,就是原先的旧值可能是A,被其他线程修改为了B,但是在该线程修改之前又改回了A,这样该线程看到的是没有修改数据,就会执行自己的修改,实际上是修改过了的。因为实际中值相同的代表的可能不相同。
解决方案是使用版本号机制,同时记录下版本号,每次执行完操作判断版本号是否有修改,如果没有旧修改数据同时版本号+1,否则不进行修改
多线程的实现方式,start()是立刻启动吗?
线程不是马上执行的,首先线程会从new状态变成READY就绪状态,之后需要等待cpu的调度运行才能变成RUNNING状态,取决于CPU调度机制。
java中线程的状态:
ThreadPoolExecutor的重要参数?执行顺序?如何设置参数?
参数有
执行顺序:
参数设置:
什么是死锁,死锁的四个必要条件?
必要条件:
java虚拟机内存空间有五个部分:方法区、堆、虚拟机栈、本地方法栈和程序计数器。
其中方法区和堆是线程共享的,其他是私有的。
方法区
存储已经被虚拟机加载的类信息、常量、静态变量和即时编译器编译后的代码等。
当方法区无法满足内存分配的需求时,抛出OOM(Out of Memory)
异常。
有一个叫运行常量池的东西,里面保存了编译时生成的字面量和符号引用
堆
存放对象实例,几乎所有对象实例和数组都在这里分配内存。
这里也是垃圾回收主要的管理区域,所以也叫作GC堆
程序计数器
当线程数超过CPU核数的时候,就需要根据某些策略强夺CPU时间片,为了使得线程切换以后还能够恢复到正常执行位置,使用独立的程序计数器记录正在执行的字节码指令地址。
程序计数器是唯一一个没有规定任何OutOfMemory的区域
虚拟机栈
描述Java方法执行的内存模型,每个方法在执行的时候都会创建一个栈帧,存储局部变量表、操作数栈、动态链接、方法出口等信息。每个方法的调用到完成的过程,对应一个栈桢在虚拟机栈的入栈和出栈的过程。
虚拟机栈有两种异常StackOverflow和OutOfMemory
栈的大小决定了线程可以递归的深度或者嵌套调用多少层方法。
栈大小固定的话,递归太深会报错前者;如果可以动态扩展,没有内存的话会报错后者
本地方法栈
和虚拟机栈差不多,区别就是虚拟机栈调用Java方法,这个本地方法栈为虚拟机执行Native(本地)方法。
大概有三种:类常量池、运行时常量池和字符串常量池
类常量池
java文件被编译成class字节码文件的时候会生成class类常量池。
除了包含有类的版本、字段等信息以外,还有一个就是常量池这个东西,常量池里面存放的就是编译器生成的各个字面量和符号引用。在类加载后进入方法区,会将一些常量放在运行时常量池里。
运行时常量池
类被加载的时候,会将Class文件常量池的内容转入运行时常量池中,将字面量和符号引用解析为直接引用存储在运行时常量池里。
字面量包括文本字符串、final修饰的常量值以及静态变量值等。
字符串常量池
在类加载完成以后,在堆中会生成对于字符串对象的实例,将其引用值保存到String Pool里面。这里保存的是引用,实际的值在具体内存地址里面。
HotSpot VM里面实现是通过StringTable,是一个哈希表,保存的是字符串的引用,在每个VM实例只有一份,所有类共享。
垃圾回收
好像和python一样啊,也是引用计数的方式标记这个对象是否被使用,同样的无法解决循环引用的问题。
可达性分析
从一个GC Root
对象出发,沿着遍历,搜所走过的路径叫做引用链,把可以访问到的对象标记下来。没有被标记的就是不可达的,会被判定为可回收对象。
可以被当作root的对象:
触发垃圾回收
system.gc()
标记清除
分为两个阶段,标记和清除。
首先会标记出所有需要回收的对象(不可达的对象就是需要回收的),然后统一清除。
有两个缺点:
复制算法
把可用的内存按照容量划分为相等的两块,每次只用一块,一块用完以后,把还存活的对象复制到另一块去,清理已经使用过的。
优点:实现简单,运行高效,不会太出现内存碎片问题
缺点:内存利用率不高,只能用一半,如果对象存活较多的时候,复制影响效率
标记整理
和标记清除差不多,但是标记以后,不是对不可达对象清除,而是将所有存活对象移动到一端,清理掉边界外的内存
优点:避免了内存碎片,也不需要内存一分为二,性价比高
缺点:仍然需要对对象进行移动操作,存活对象多的时候就影响效率
分代回收
根据对象存活周期的不同将内存分为几块,一般是将堆分为新生代和老年代。
新生代里面每次回收有大量对象死亡,只存留少数,就选择复制算法。
老年代里对象存活率高,最好使用标记清除和标记整理回收。
其中年轻代又分为Eden、From和To三个区域。
当第一次Eden满了以后,会触发回收,存活的复制到From,Eden清空;第二次Eden满了以后,会回收Eden和From,将存活的复制到To,清空前两个;第三次回收Eden和To,存活的复制到From里面
存活的对象会一直在From和To里面复制移动,当一定次数以后,如果还存留,就放在老年代里面去。
如果老年代也满了,会触发全局垃圾回收,清理所有区域。
因此要合理设置新生代和老年代的大小,避免Full GC的执行。
java类的加载过程
当程序使用一个没有加载到内存中的一个类的时候,如下三步对类进行加载初始化
将类的文件信息加载到内存中,作为程序方法的入口
如果加载的是一个数组类型,是不直接通过类加载器加载,而是由虚拟机完成,但是其引用需要使用加载器加载,加载器会加载完数组的数据类型后将数组绑定到相应的加载器上,然后和类加载器一起绑定标识唯一性。
链接
链接分为三步
符号引用:一个类中如果引用了其他类,但是JVM并不会知道其他类的具体地址在哪,先用符号引用表示,等解析以后再根据唯一的符号找到具体的类地址。字段变量也可以用符号引用
直接引用:和虚拟机布局有关,如果是直接引用,引用的目标对象一定已经加载到内存中了
初始化
类在什么时候会被加载
类的加载机制
有三种
类加载器
主要分为四种
启动类加载器:加载lib目录的类,java.xxx(如java.lang.Object)
扩展类加载器:加载lib/ext目录的类,包名是javax.xxx(如javax.swing.xxx)
应用程序扩展器:ClassLoader的getSystemClassLoader的返回值,是默认类加载器
双亲委派的意义在一不同的类加载器之间分别负责搜索范围内的类的加载工作,保证一个类在使用中不会出现不平等的类。(保证一个类不会被不同的类加载器加载,主要的考虑是安全方面,杜绝通过使用和JRE相同的类名冒充现有GRE的类达到替换的攻击方式)
JAVA内存模型和JAVA运行时数据区域,常量放在哪
什么是内存泄漏和如何处理
如何判断对象是否死亡?(两种方法)
一个线程OOM后,其他线程还能正常运行吗?
可以,一个线程OOM以后,所占用的内存资源会全部被释放,不会影响其他线程的正常运行。
使用
在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()获取数据
优势
五个核心接口
使用Integer和int的映射区别
Integer是实体类对象,可以为null;int是基本数据类型不能为null
工作流程
SessionFactory是什么
是一个单例,储存数据的,是线程安全的,可以在多线程中使用。
在启动的时候只能建立一次,应该包装各种单例以至于能简单存在一个代码中。
Session的清理缓存和清空缓存
前者是调用flush()将缓存中的数据刷新写入到数据库中
后者是调用clear()方法,清空缓存,不会写入数据库
缓存机制
有两种缓存
支持的缓存策略
hibernate 对象有哪些状态
save、get、load
等方法后,就是持久化状态三种状态的转换
当在Session中存储数据的时候,会先保存在Session Map中,然后给数据库保存一份,缓存中的数据就叫持久对象(Persist),Session关闭后,这个仍然存在,但是变为游离态了。
游离态update()的时候会变为持久态。
持久态delete()时会变成瞬时状态,数据库中也没有与之对应的数据了。
getCurrentSession 和 openSession区别是什么
前者是和线程绑定的,事务由Spring控制,不需要手动管理
后者不和线程绑定,需要自己手动开启事务
实体类必须要无参构造函数吗
必须要有无参构造函数,因为要用refaction api,通过CalssnewInstance()应该是反射构造实体类的实例,没有构造函数会报错。
三种检索策略
sorted collection 和ordered collection
前者是在内存中使用java进行排序,后者直接在数据库中排序。
对于大数据集最开始用后者,避免出现OOM的情况
查询很慢如何优化
persist()和save()的区别
前者不会立刻更新数据库,可能调用flush才会更新,也不会更新缓存数据,没有返回
后者把一个瞬时状态的对象变成持久化状态,返回一个主键值
update()和saveOrUpdate()的区别
前者的调用对象必须处于持久化状态,如果数据库中不存在,就不能使用update()
后者的对象可以是持久化也可以是非持久化的,也就是说持久化的会调用update(),非持久化会调用save()
get()和load()的区别
前者是饿加载,不支持延迟加载,执行后立刻获得数据,如果没有数据对象存在返回null
后者是懒加载,延迟加载,使用的时候才获取数据,没有对象的时候返回一个代理对象;不会立即触发SQL语句,而是返回一个代理对象,只保存了实例的ID值之类的,当需要使用的时候的到期对象,然后执行相应SQL语句;会先查询一句缓存,没有找到的时候返回一个对象,在使用的时候才去检查二级缓存和数据库
Hibernate 和 Mybatis 的区别
相同点:
Mybatis:
hIBERNATE:
Hibernate和JDBC的区别
相同:
不同:
HiberNate优化
实现原理
如何配置sql语句
怎么将目标参数绑定到sql语句中?
一级缓存和二级缓存
分页怎么实现
#{}和¥符号有什么区别
是一个轻量级的java ee框架 ,有以下几个特点:
有两个核心部分:
Bean
Bean其实就是一个对象,由IOC负责管理,应用程序就是一个一个Bean
Bean主要管理对象和依赖以及依赖注入的问题,bean其实是一个java对象,主要有以下几个规范
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的方式或者非构造函数传入的方式注入依赖。
提供了三种管理方式和注入方式
管理方式
注入方式
指将与业务无关的,但是被多个模块共同调用的代码封装起来,减少重复代码,降低模块之间的耦合。
关键在于代理模式。
OOP面向对象的编程是如果多个子类有相同的方法的话避免复用可以将相应方法写在父类里面,提取共有属性直接在子类里面用。但是如果父类里面有这种需求,比如父类多个方法里面有复用的方法,就需要使用AOP了,常用于事务控制、权限校验和日志打印等场景
Spring的AOP是动态代理模式,不会修改字节码文件,而是运行时每次在内存中临时生成一个AOP对象。
Aspect AOP就是静态代理模式,会在编译的时候生成AOP代理类,将AspectJ(切面)注入java字节码中,运行以后就是增强的AOP对象。
比如说实现Cloneable接口就可以使得类可以使用clone方法进行对象的复制
spring有哪些模块
有七大模块
spring特性
就是IOC、AOP和DI
spring mvc有哪些组件
有五大组件:
spring mvc执行流程
如何理解Spring的自动装配
Spring会通过上下文自动找出对应的依赖项的类,并且自动给Bean装配相关的属性
有两种方式:使用xml文件和使用注解
IOC和AOP原理
IOC的实现
AOP实现(静态和动态)
循环依赖怎么解决
多个bean之间相互依赖,形成了一个环。一般是指默认单例bean中,出现了属性相互依赖的场景。
通过单例Bean的三级缓存实现:
使用的哪种动态代理
Spring的优点,用到了哪些设计模式,IOC和AOP的理解
Bean的生命周期
解释Spring支持的几种bean的作用域
BeanFactory、FactoryBean的区别
SpringMVC、Mybatis 执行过程
Spring事务
Boot是在Spring基础上消除了设置Spring应用所需要的XML配置,更加高效。
SpringBoot和Spring区别
Spring是一个框架,为web开发提供了全面的基础架构支持,有很多好的功能,依赖注入和开箱即用的模块等,包括:Spring JDBC、MVC、Security、AOP、ORM等
这些模块可以极大缩短应用开发时间。
SpringBoot则是Spring的一个扩展,消除了设置Spring应用程序所需要的复杂的配置环节。
同样致力于更快速更高效开发web程序,主要功能包括:使用starter简化程序的配置、可以直接main函数启动有嵌入tomcat服务器直接使用、尽可能自动化配置Spring
springboot一些特点
注解@RestController和@ResponseBody的区别
前者等于Controller和ResponseBody
两个合起来,当类中所有方法都需要返回json或者xml的时候,用这个注解,是针对类的。
后者一般置于方法前面,不会走视图控制器,而是直接返回文本信息给前端,比如json或者xml,是针对某个方法专门编写的。
redis知识点必备
Redis数据结构及各结构的内部实现
是键值形式的,其中值的数据类型包括:String(字符串)、List(列表)、Hash(哈希)、Set(集合)和Zset(有序集合),也就是数据的保存形式,其底层实现的方式用到了数据结构
Redis为什么快
主要原因是基于内存的,所有操作都是在内存上完成的,所以较快;
数据结构较为简单操作较快,数据结构能够高效进行增删改查;
采用单线程模型,避免线程切换带来的额外开销(上下文切换寄存器切换等),也不用考虑锁带来的开销或者死锁;
单线程使用了多路复用IO,非阻塞式IO
缓存穿透
查询一个一定不存在数据的时候,缓存中查询不到,那么就需要去数据库中进行查询,数据库还是查询不到,那么就不会写入缓存,导致每次查询这种数据的时候都会取数据库中进行查询,性能下降。
解决方法:
带来的问题:
缓存雪崩
如果缓存集中在同一时间失效,那么所有压力都落在数据库上,造成了缓存雪崩。
解决方法
如何保证数据库和缓存的一致性
理论上,过期时间较为合理的话,就能够保证。当缓存过期,就回去数据库中读取,然后会更新到缓存中,从而保证一致性。
增删改查数据库的时候同步更新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文件中,然后根据条件将文件内容写入到磁盘上。
区别:
内存淘汰策略
Redis通信协议
Redis事务
Redis 的过期策略以及内存淘汰机制
Redis主从复制机制redis 集群模式的工作原理能说一下么?在集群模式下,redis 的 key 是如何寻址的?分布式寻址都有哪些算法?了解一致性 hash 算法吗?
Redis 和 DB 一致性?缓存穿透?缓存击穿?缓存雪崩?缓存清洗?并发竞争Key?
Nginx是一个反向代理服务器,占用内存绩效,启动极快,高并发能力强。
正向代理就是浏览器主动请求代理服务器,由代理服务器将请求转发到目标服务器上。
反向代理是部署在Web服务器上,代理所有外部网络对内部网络的请求,对用户是不可见的,被动,由代理服务器选择将请求转发到合适的服务器上。
因为WSGI server并发量很低,所以需要一个专门的服务器软件进行缓冲。
作用:
Nginx优势
应用场景
如何处理请求
如何实现高并发
异步、非阻塞、epoll的应用。
web服务器正好是IO密集型,使得Nginx将等待网络传输之类的时间,空出来。基于事件模型
是一master多worker模型。
master负责接收转发请求,有请求到来的时候,拉起一个worker进程处理请求。同时需要监控worker的状态,保证HA。
worker数一般和cpu核心数一样,worker和apaache不一样,Nginx的只要内存够大,一个worker就可以处理多个请求。而Apache一个进程一个时间只能处理一个,因此需要几百上千进程。
每次进来一个request,会有一个worker处理,但是不是全程处理,当处理到需要阻塞的时候,比如将请求转发了等待结果,他就不会继续等了,而是会注册一个事件:如果又返回了通知我。也就是事件驱动。这个时候就可以继续接受其他请求了。
Nginx目录结构
配置文件nginx.conf有哪些属性模块
为什么Nginx不用多线程
Apache是通过创建多个进程或者线程单独处理一个请求的,如果太多的话,cpu和内存的消耗会较大,并大多的时候会榨干系统资源
而Nginx用的是单线程的异步非阻塞式(epoll),不会给每个请求单独分配进程或者线程,节省了大量的cpu和内存资源,同时也避免了切换造成的上下文切换
Nginx如何负载均衡
主要有五种策略
如何配置虚拟主机
/data/www、/data/bbs
,在hosts添加域名和虚拟机IP的解析,对应域名网站目录下新增index.htmllocation的作用
就是根据用户请求的URI执行不同的应用,就是匹配URL。里面可以指定访问的路径和默认主页;或者跳转到对应转发URL的指定主页
可以使用一些正则进行匹配
Nginx如何限流
主要有三种方式:正常限制访问频率(正常流量)、突发限制访问频率(突发流量)、限制并发连接数。
都是基于漏桶流算法实现的。
正常限制访问流量:通过模块ngx_http_limit_req_module
限制访问频率,如果还有请求没有处理完,会拒绝该用户请求
突发限制访问流量:上面的一个缺点就是对于突发流量无法进行处理,通过参数burst和nodelay
可以设置能够处理超过最大请求的请求个数,如果设置为5,说明可以对于一个用户额外处理五个请求,超过就会慢慢来。
限制并发连接数:通过模块可以设置单个IP最大连接数是十个连接,以及整个虚拟服务器最大并发是100个连接,只有header被服务器处理才算一个连接数
漏桶算法和令牌算法
Nginx的高可用
当一个服务器出现故障无法及时响应的时候,就会轮询下一台服务器。