类型
存储需求
取值范围
byte
1B
-128~127
short
2B
-32768~32767
int
4B
-20亿~20亿
long
8B
float
4B
小数点后6~7位
double
8B
小数点后15位
char
2B
boolean
true/false
被static修饰的成员变量和成员方法独立于该类的任何对象。也就是说,它不依赖类特定的实例,被类的所有实例共享。只要这个类被加载了,Java虚拟机就能根据类名在运行时数据区的方法区内定找到他们。
**静态变量(**类变量):静态变量被所有的对象所共享,也就是说我们创建了一个类的多个对象,多个对象共享着一个静态变量,如果我们修改了静态变量的值,那么其他对象的静态变量也会随之修改。
非静态变量(实例变量):如果我们创建了一个类的多个对象,那么每个对象都有它自己该有的非静态变量。当你修改其中一个对象中的非静态变量时,不会引起其他对象非静态变量值得改变。
static修饰的成员方法称作静态方法,这样我们就可以通过“类名**.**方法名”进行调用。由于静态方法在类加载的时候就存在了,所以它不依赖于任何对象的实例就可以进行调用,因此对于静态方法而言,是木有当前对象的概念,即没有this、super关键字的。因为static方法独立于任何实例,因此static方法必须被实现,而不能是抽象的abstract。
被static修饰的代码块也叫静态代码块,会随着JVM加载类的时候而加载这些静态代码块,并且会自动执行。它们可以有多个,可以存在于该类的任何地方。JVM会按照它们的先后顺序依次执行它们,而且每个静态代码块只会被初始化一次,不会进行多次初始化。
普通类是不允许声明为静态的,只有内部类才可以,被static修饰的内部类可以直接作为一个普通类来使用,而不需先实例一个外部类。
不能,因为static方法独立于任何实例,因此static方法必须被实现,而不能是抽象的abstract。
可以。静态内部类只能访问外部类的静态成员,否则编译会报错。
**重载:**发生在同一个类中,方法名必须相同,参数类型不同、个数不同、顺序不同,方法返回值和访问修饰符可以不同,发生在编译时。
**重写:**发生在父子类中,**方法名、参数列表必须相同,返回值范围小于等于父类,抛出的异常范围小于等于父类,访问修饰符范围大于等于父类;**如果父类方法访问修饰符为 private 则子类就不能重写该方法。
不可以。在java语言中,要重载(overload)一个方法,除了要与原方法具有相同的简单名称之外,还要求必须拥有一个与原方法不同的特征签名,特征签名就是一个方法中各个参数在常量池中的字段符号引用的集合,也就是因为返回值不会包含在特征签名中,因此java语言里面无法仅仅依靠返回值不同来对一个已有的方法进行重载。
==: 它的作用是判断两个对象的地址是不是相等。即,判断两个对象是不是同一个对象(基本数据类型比较的是值,引用数据类型比较的是内存地址)。
equals(): 它的作用也是判断两个对象是否相等。但它一般有两种使用情况:
情况1:类没有覆盖写equals() 方法。则通过 equals() 比较该类的两个对象时,等价于通过“==”比较这两个对象。
情况2:类override了 equals() 方法。一般,我们都覆盖 equals() 方法来比较两个对象的内容是否相等;若它们的内容相等,则返回 true (即,认为这两个对象相等)。
举个例子:
public class test1 {
public static void main(String[] args) {
String a = new String("ab"); // a 为一个引用
String b = new String("ab"); // b为另一个引用,对象的内容一样
String aa = "ab"; // 放在常量池中
String bb = "ab"; // 从常量池中查找
if (aa == bb) // true
System.out.println("aa==bb");
if (a == b) // false,非同一对象
System.out.println("a==b");
if (a.equals(b)) // true
System.out.println("aEQb");
if (42 == 42.0) { // true
System.out.println("true");
}
}
}
说明:
String 中的 equals 方法是被重写过的,因为 object 的 equals 方法是比较的对象的内存地址,而 String 的 equals 方法比较的是对象的值。
当创建 String 类型的对象时,虚拟机会在常量池中查找有没有已经存在的值和要创建的值相同的对象,如果有就把它赋给当前引用。如果没有就在常量池中重新创建一个 String 对象。
private final char value[]
,所以 String 对象是不可变的。而StringBuilder 与 StringBuffer 都继承自 AbstractStringBuilder 类,在 AbstractStringBuilder 中也是使用字符数组保存字符串char[]value
但是没有用 final 关键字修饰,所以这两种对象都是可变的。equals()
方法来检查 hashcode 相等的对象。如果equals方法返回为true,HashSet 就不会让其加入操作成功。如果返回false,就会重新散列到其他位置。class Person {
int age;
String name;
public Person(String name, int age) {
this.name = name;
this.age = age;
}
public String toString() {
return name + " - " +age;
}
/**
* @desc重写hashCode
*/
@Override
public int hashCode(){
int nameHash = name.toUpperCase().hashCode();
return nameHash ^ age;
}
/**
* @desc 覆盖equals方法
*/
@Override
public boolean equals(Object obj){
if(obj == null){
return false;
}
//如果是同一个对象返回true,反之返回false
if(this == obj){
return true;
}
//判断是否类型相同
if(this.getClass() != obj.getClass()){
return false;
}
Person person = (Person)obj;
return name.equals(person.name) && age==person.age;
}
}
java 中的length 属性是针对数组说的,比如说你声明了一个数组,想知道这个数组的长度则用到了 length 这个属性.
java 中的length()方法是针对字符串String说的,如果想看这个字符串的长度则用到 length()这个方法.
.java 中的size()方法是针对泛型集合说的,如果想看这个泛型有多少个元素,就调用此方法来查看!
ArrayList 的底层是数组队列,相当于动态数组。与 Java 中的数组相比,它的容量能动态增长。**在添加大量元素前,应用程序可以使用ensureCapacity
操作来增加 ArrayList 实例的容量。**这可以减少递增式再分配的数量。
它继承于AbstractList,实现了List,RandomAccess,Cloneable,java.io.Serializable这些接口。
在我们学数据结构的时候就知道了线性表的顺序存储,插入删除元素的时间复杂度为O(n),求表长以及增加元素,取第 i 元素的时间复杂度为O(1)
LinkedList是一个实现了List接口和Deque接口的双端链表。 LinkedList底层的链表结构使它支持高效的插入和删除操作,另外它实现了Deque接口,使得LinkedList类也具有队列的特性; LinkedList不是线程安全的,如果想使LinkedList变成线程安全的,可以调用静态类Collections类中的synchronizedList方法:
List list=Collections.synchronizedList(new LinkedList(...));
看LinkedList类中的一个内部私有类Node就很好理解了:
private static class Node {
Node prev;//前驱节点
E item;//节点值
Node next;//后继节点
Node(Node prev, E element, Node next) {
this.item = element;
this.next = next;
this.prev = prev;
}
}
这个类就代表双端链表的节点Node。这个类有三个属性,分别是前驱节点,本节点的值,后继结点。
获取尾节点(index=-1)数据方法:getLast()方法在链表为空时,会抛出NoSuchElementException,而peekLast()则不会,只是会返回null
**删除方法:removeLast()在链表为空时将抛出NoSuchElementException,而pollLast()**方法返回null
**是否保证线程安全:**ArrayList 和 LinkedList 都是不同步的,也就是不保证线程安全;
**底层数据结构:**Arraylist 底层使用的是Object数组;LinkedList 底层使用的是双向链表数据结构;
插入和删除是否受元素位置的影响:①ArrayList 采用数组存储,所以插入和删除元素的时间复杂度受元素位置的影响。比如:执行add(E e)?
方法的时候, ArrayList 会默认在将指定的元素追加到此列表的末尾,这种情况时间复杂度就是O(1)。但是如果要在指定位置 i 插入和删除元素的话(add(int index, E element)?
)时间复杂度就为 O(n-i)。因为在进行上述操作的时候集合中第 i 和第 i 个元素之后的(n-i)个元素都要执行向后位/向前移一位的操作。 ②LinkedList 采用链表存储,所以插入,删除元素时间复杂度不受元素位置的影响,都是近似 O(1)而数组为近似 O(n)。
**是否支持快速随机访问:**LinkedList 不支持高效的随机元素访问,而 ArrayList 支持。快速随机访问就是通过元素的序号快速获取元素对象(对应于get(int index)?
方法)。
**内存空间占用:**ArrayList的空间浪费主要体现在在list列表的结尾会预留一定的容量空间,而LinkedList的空间花费则体现在它的每一个元素都需要消耗比ArrayList更多的空间(因为要存放直接后继和直接前驱以及数据)。
/**
* 如有必要,增加此ArrayList实例的容量,以确保它至少能容纳元素的数量
* @param minCapacity 所需的最小容量
*/
public void ensureCapacity(int minCapacity) {
int minExpand = (elementData != DEFAULTCAPACITY_EMPTY_ELEMENTDATA)
// any size if not default element table
? 0
// larger than default for default empty table. It's already
// supposed to be at default size.
: DEFAULT_CAPACITY;
if (minCapacity > minExpand) {
ensureExplicitCapacity(minCapacity);
}
}
//得到最小扩容量
private void ensureCapacityInternal(int minCapacity) {
if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA) {
// 获取默认的容量和传入参数的较大值
minCapacity = Math.max(DEFAULT_CAPACITY, minCapacity);
}
ensureExplicitCapacity(minCapacity);
}
//判断是否需要扩容,上面两个方法都要调用
private void ensureExplicitCapacity(int minCapacity) {
modCount++;
// 如果说minCapacity也就是所需的最小容量大于保存ArrayList数据的数组的长度的话,就需要调用grow(minCapacity)方法扩容。
//这个minCapacity到底为多少呢?举个例子在添加元素(add)方法中这个minCapacity的大小就为现在数组的长度加1
if (minCapacity - elementData.length > 0)
//调用grow方法进行扩容,调用此方法代表已经开始扩容了
grow(minCapacity);
}
/**
* 允许分配的数组最大容量
*/
private static final int MAX_ARRAY_SIZE = Integer.MAX_VALUE - 8;
/**
* ArrayList扩容的核心方法
* @param minCapacity:最小扩容量
*/
private void grow(int minCapacity) {
// oldCapacity为旧容量=数组当前长度
int oldCapacity = elementData.length;
// newCapacity为新容量=oldCapacity的1.5倍
int newCapacity = oldCapacity + (oldCapacity >> 1);
// 然后检查新容量是否大于最小需要容量,若还是小于最小扩容量,那么就把最小扩容量当作数组的新容量
if (newCapacity - minCapacity < 0)
newCapacity = minCapacity;
// 再检查新容量是否超出了ArrayList所定义的最大容量,
// 若超出了,则调用hugeCapacity()来比较minCapacity和 MAX_ARRAY_SIZE,
// 如果minCapacity大于MAX_ARRAY_SIZE,则新容量则为Interger.MAX_VALUE,否则,新容量大小则为 MAX_ARRAY_SIZE。
if (newCapacity - MAX_ARRAY_SIZE > 0)
newCapacity = hugeCapacity(minCapacity);
// minCapacity is usually close to size, so this is a win:
elementData = Arrays.copyOf(elementData, newCapacity);
}
private static int hugeCapacity(int minCapacity) {
if (minCapacity < 0) // overflow
throw new OutOfMemoryError();
return (minCapacity > MAX_ARRAY_SIZE) ?
Integer.MAX_VALUE :
MAX_ARRAY_SIZE;
}
Vector与ArrayList一样,也是通过数组实现的,Vector类的所有方法都是同步的。它也是线程安全的,而Arraylist是线程异步(ASynchronized)的,是不安全的。如果不考虑到线程的安全因素,一般用Arraylist效率比较高。
使用ArrayList时,如果不指定大小,会生成一个空的数组;
使用Vector时,如果不指定大小,会默认生成一个10个元素大小的数组
Vector 实现类中有一个变量 capacityIncrement 用来表示每次容量自增时应该增加多少,如果不指定,默认为0
在扩容时,会判断,如果指定了capacityIncrement,会先把数组容量扩大到oldCapacity + capacityIncrement,如果没有指定capacityIncrement,会先把数组容量扩大到2倍的oldCapacity, 然后再进行判断扩充后的容量是否满足要求,如果不满足要求,直接将容量扩大到指定大小,源码如下:
private void grow(int minCapacity) {
// overflow-conscious code
int oldCapacity = elementData.length;
int newCapacity = oldCapacity + ((capacityIncrement > 0) ?
capacityIncrement : oldCapacity);
if (newCapacity - minCapacity < 0)
newCapacity = minCapacity;
if (newCapacity - MAX_ARRAY_SIZE > 0)
newCapacity = hugeCapacity(minCapacity);
elementData = Arrays.copyOf(elementData, newCapacity);
}
clone()?
、writeObject()
、readObject()
是 HashSet 自己不得不实现之外,其他方法都是直接调用 HashMap 中的方法。JDK1.8 之前 HashMap 底层是数组和链表结合在一起使用也就是链表散列。HashMap 通过 key 的 hashCode 经过扰动函数处理过后得到 hash 值,然后通过(n - 1) & hash
判断当前元素存放的位置(这里的 n 指的是数组的长度),如果当前位置存在元素的话,就判断该元素与要存入的元素的 hash 值以及 key 是否相同,如果相同的话,直接覆盖,不相同就通过拉链法解决冲突。所谓扰动函数指的就是 HashMap 的 hash 方法。使用 hash 方法也就是扰动函数是为了防止一些实现比较差的 hashCode() 方法 换句话说使用扰动函数之后可以减少碰撞。
JDK 1.8 HashMap 的 hash 方法源码:
static final int hash(Object key) {
int h;
// key.hashCode():返回散列值也就是hashcode
// ^ :按位异或
// >>>:无符号右移,忽略符号位,空位都以0补齐
return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);//hashcode的高16位与低16位异或
}
所谓**“拉链法”**就是:将链表和数组相结合。也就是说创建一个链表数组,数组中每一格就是一个链表。若遇到哈希冲突,则将冲突的值加到链表中即可:
jdk1.8之前的内部结构
因为元素在底层数组中的索引位置是经过hash算法(key的哈希值+扰动)计算出来的,与原来的顺序没有关系。线程不安全,多线程访问的情况下,会出现问题。
jdk1.8之后的内部结构
loadFactor负载因子/装填因子
loadFactor加载因子是控制数组存放数据的疏密程度,loadFactor越趋近于1,那么 数组中存放的数据(entry)也就越多,也就越密,也就是会让链表的长度增加,loadFactor越小,也就是趋近于0,数组中存放的数据(entry)也就越少,也就越稀疏。
loadFactor太大导致查找元素效率低,太小导致数组的利用率低,存放的数据会很分散。loadFactor的默认值为0.75f是官方给出的一个比较好的临界值。
给定的默认容量为 16,负载因子为 0.75。Map 在使用过程中不断的往里面存放数据,当数量达到了 16 * 0.75 = 12 就需要将当前 16 的容量进行扩容,而扩容这个过程涉及到 rehash、复制数据等操作,所以非常消耗性能。
threshold
threshold = capacity * loadFactor,当Size>=threshold的时候,那么就要考虑对数组的扩增了,也就是说,这个的意思就是衡量数组是否需要扩增的一个标准。
进行扩容,会伴随着一次重新hash分配,并且会遍历hash表中所有的元素,是非常耗时的。在编写程序中,要尽量避免resize。
下面我们讲解下JDK1.8做了哪些优化。经过观测可以发现,我们使用的是2次幂的扩展(指长度扩为原来2倍),所以,**元素的位置要么是在原位置,要么是在原位置再移动2次幂的位置。**看下图可以明白这句话的意思,n为table的长度,图(a)表示扩容前的key1和key2两种key确定索引位置的示例,图(b)表示扩容后key1和key2两种key确定索引位置的示例,其中hash1是key1对应的哈希与高位运算结果。
元素在重新计算hash之后,因为n变为2倍,那么n-1的mask范围在高位多1bit(红色),因此新的index就会发生这样的变化:
因此,我们在扩充HashMap的时候,不需要像JDK1.7的实现那样重新计算hash,只需要看看原来的hash值新增的那个bit是1还是0就好了,是0的话索引没变,是1的话索引变成“原索引+oldCap”,可以看看下图为16扩充为32的resize示意图:
这个设计确实非常的巧妙,既省去了重新计算hash值的时间,而且同时,由于新增的1bit是0还是1可以认为是随机的,因此resize的过程,均匀的把之前的冲突的节点分散到新的bucket了。这一块就是JDK1.8新增的优化点。有一点注意区别,JDK1.7中rehash的时候,旧链表迁移新链表的时候,如果在新表的数组索引位置相同,则链表元素会倒置,但是从上图可以看出,JDK1.8不会倒置。
final Node[] resize() {
Node[] oldTab = table;
// oldCap旧容量:当前table的长度
int oldCap = (oldTab == null) ? 0 : oldTab.length;
int oldThr = threshold;
int newCap, newThr = 0;
//
if (oldCap > 0) {
// 如果旧容量超过最大值就不再扩容了,就只好随你碰撞去吧
if (oldCap >= MAXIMUM_CAPACITY) {
threshold = Integer.MAX_VALUE;
return oldTab;
}
// 没超过最大值,newCap新容量为旧容量的2倍,newThr新阈值也扩容为旧阈值的2倍
else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY && oldCap >= DEFAULT_INITIAL_CAPACITY)
newThr = oldThr << 1; // double threshold
}
else if (oldThr > 0) // initial capacity was placed in threshold
newCap = oldThr;
else {
// signifies using defaults
newCap = DEFAULT_INITIAL_CAPACITY;
newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY);
}
// 计算新的resize上限
if (newThr == 0) {
float ft = (float)newCap * loadFactor;
newThr = (newCap < MAXIMUM_CAPACITY && ft < (float)MAXIMUM_CAPACITY ? (int)ft : Integer.MAX_VALUE);
}
threshold = newThr;
@SuppressWarnings({"rawtypes","unchecked"})
Node[] newTab = (Node[])new Node[newCap];
table = newTab;
if (oldTab != null) {
// 把oldTab中每个bucket都移动到新的buckets中
for (int j = 0; j < oldCap; ++j) {
Node e;
// oldTable该索引位置有key
if ((e = oldTab[j]) != null) {
oldTab[j] = null;
// 该位置无冲突Node
if (e.next == null)
// 计算该key在新table中索引位置:e.hash&(newCap-1),存放在此
newTab[e.hash & (newCap - 1)] = e;
// 有冲突,是红黑树Node
else if (e instanceof TreeNode)
// 插入红黑树
((TreeNode)e).split(this, newTab, j, oldCap);
// 有冲突,是链表Node
else {
Node loHead = null, loTail = null;
Node hiHead = null, hiTail = null;
Node next;
do {// 遍历插入
next = e.next;
// 原索引位置
if ((e.hash & oldCap) == 0) {
if (loTail == null)
loHead = e;
else
loTail.next = e;
loTail = e;
}
// 原索引位置+oldCap
else {
if (hiTail == null)
hiHead = e;
else
hiTail.next = e;
hiTail = e;
}
} while ((e = next) != null);
// 原索引放到bucket里
if (loTail != null) {
loTail.next = null;
newTab[j] = loHead;
}
// 原索引+oldCap放到bucket里
if (hiTail != null) {
hiTail.next = null;
newTab[j + oldCap] = hiHead;
}
}
}
}
}
return newTab;
}
散列值是不能直接拿来用的。用之前还要先做对数组的长度取模运算,得到的余数才能用来要存放的位置也就是对应的数组下标。我们首先可能会想到采用%取余的操作来实现。但是,重点来了:“取余(%)操作中如果除数是2的幂次则等价于与其除数减一的与(&)操作(也就是说 hash%length==hash&(length-1)的前提是 length 是2的 n 次方;)。”并且采用二进制位操作 &,相对于%能够提高运算效率,这就解释了 HashMap 的长度为什么是2的幂次方。
HashMap只提供了put用于添加元素,putVal方法只是给put方法调用的一个方法,并没有提供给用户使用。
对putVal方法添加元素的分析如下:
①如果定位到的数组位置没有元素 就直接插入。
②如果定位到的数组位置有元素就和要插入的key比较,如果key相同就直接覆盖,如果key不相同,就判断p是否是一个树节点,如果是就调用e = ((TreeNode
将元素添加进入。如果不是就遍历链表插入(插入的是链表尾部)。
①.判断键值对数组table[i]是否为空或为null,否则执行resize()进行扩容;
②.根据键值key计算hash值得到插入的数组索引i,如果table[i]==null,直接新建节点添加,转向⑥,如果table[i]不为空,转向③;
③.判断table[i]的首个元素是否和key一样,如果相同直接覆盖value,否则转向④,这里的相同指的是hashCode以及equals;
④.判断table[i] 是否为treeNode,即table[i] 是否是红黑树,如果是红黑树,则直接在树中插入键值对,否则转向⑤;
⑤.遍历table[i],判断链表长度是否大于8,大于8的话把链表转换为红黑树,在红黑树中执行插入操作,否则进行链表的插入操作;遍历过程中若发现key已经存在直接覆盖value即可;
⑥.插入成功后,判断实际存在的键值对数量size是否超多了最大容量threshold,如果超过,进行扩容。
public V put(K key, V value) {
// 对key的hashCode()做hash
return putVal(hash(key), key, value, false, true);
}
final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
boolean evict) {
Node[] tab; Node p; int n, i;
// 步骤①:tab为空则创建
if ((tab = table) == null || (n = tab.length) == 0)
n = (tab = resize()).length;
// 步骤②:计算index,并对null做处理
if ((p = tab[i = (n - 1) & hash]) == null)
tab[i] = newNode(hash, key, value, null);
else {
Node e; K k;
// 步骤③:节点key存在,直接覆盖value
if (p.hash == hash &&
((k = p.key) == key || (key != null && key.equals(k))))
e = p;
// 步骤④:判断该链为红黑树
else if (p instanceof TreeNode)
e = ((TreeNode)p).putTreeVal(this, tab, hash, key, value);
// 步骤⑤:该链为链表
else {
for (int binCount = 0; ; ++binCount) {
if ((e = p.next) == null) {
p.next = newNode(hash, key,value,null);
//链表长度大于8转换为红黑树进行处理
if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
treeifyBin(tab, hash);
break;
}
// key已经存在直接覆盖value
if (e.hash == hash &&
((k = e.key) == key || (key != null && key.equals(k)))) break;
p = e;
}
}
// 表示在桶中找到key值、hash值与插入元素相等的结点
if (e != null) {
V oldValue = e.value;// 记录e的value
if (!onlyIfAbsent || oldValue == null)
e.value = value;// 用新值替换旧值
afterNodeAccess(e);// 访问后回调
return oldValue;// 返回旧值
}
}
++modCount;
// 步骤⑥:超过最大容量 就扩容
if (++size > threshold)
resize();
afterNodeInsertion(evict);
return null;
}
当一个链表太长的时候,HashMap会动态的将它替换成一个红黑树,这话的话会将时间复杂度从O(n)降为O(logn)。
synchronized
修饰。(如果你要保证线程安全的话就使用 ConcurrentHashMap 吧!);tableSizeFor()
方法保证,下面给出了源代码)。也就是说 HashMap 总是使用2的幂作为哈希表的大小,后面会介绍到为什么是2的幂次方。**Error:**程序无法处理的系统问题,一般与JVM相关,编译器不做检查,如:系统奔溃,虚拟机错误,系统内存不足,调用栈溢出。
**Exception:**程序可以处理的异常,可以被程序捕获、处理和恢复。
单线程收集器(一个收集线程)
多线程并行收集器(多个收集线程)
并发收集器(收集线程可与工作线程同时执行)
在进行Minor GC回收新生代中的内存时,若存活下来的对象的大小总和超过To Survivor空间的大小,则需要转移到老年代(HandlePromotionFailure开启:可以冒险地执行Minor GC)
加载机制的过程:
若两个操作满足以下的规则之一,则这两个操作无需进行任何同步就能保证顺序性
线程数量过多会把内存容量耗尽:OutOfMemoryError
StringBuffer是线程安全的,而StringBuilder是非线程安全的,在单线程的情况下,StringBuffer由于同步机制性能降低,建议使用StringBuilder。
一个ConcurrentHashMap维护多个Segment数组,一个Segment维护多个HashEntry数组。
对于同一个Segment的操作才需考虑线程同步,不同的Segment则无需考虑。
当插入或删除节点使得红黑树的规则被打破时,需要对其进行调整:
1.变色:为了重新符合红黑树的规则,尝试把红色节点变为黑色,或者把黑色节点变为红色(会发生连锁效应)
2.左旋:逆时针旋转红黑树的两个节点,使得父节点被自己的右孩子取代,而自己成为自己的左孩子。
3.右旋:顺时针旋转红黑树的两个节点,使得父节点被自己的左孩子取代,而自己成为自己的右孩子。
与平衡树不同的是,红黑树在插入、删除等操作,不会像平衡树那样,频繁着破坏红黑树的规则,所以不需要频繁着调整,这也是我们为什么大多数情况下使用红黑树的原因。
表面上,进程、线程、协程都存在上下文切换的问题,但是三者上下文切换又有明显不同,见下表:
协程适合I/O 阻塞型。
I/O本身就是阻塞型的(相较于CPU的时间世界而言)。就目前而言,无论I/O的速度多快,也比不上CPU的速度,所以一个I/O相关的程序,当其在进行I/O操作时候,CPU实际上是空闲的。
我们假设这样的场景,如下图:1个线程有5个I/O的事情(子程序)要处理。如果我们绝对的串行化,那么当其中一个I/O阻塞时,其他4个I/O并不能得到执行,因为程序是绝对串行的,5个I/O必须一个一个排队等待处理,当一个I/O阻塞时,其它4个也得等着。
而协程能比较好地处理这个问题,当一个协程(特殊子进程)阻塞时,它可以切换到其他没有阻塞的协程上去继续执行,这样就能得到比较高的效率,如下图所示:
在短进程优先的调度算法下,长进程可能会长时间得不到CPU的调度,即产生饥饿现象
不会影响上下文切换
为每个页面设置一个访问字段,来记录页面自上次被访问以来所经历的时间,淘汰页面时选择现有页面中值最大的予以淘汰
防止两次握手的情况下,客户端发出的已失效的连接请求报文段突然又传送到了服务器端,产生了错误,因为三次握手时,客户端收到服务器端对失效连接报文段的确认时,可以不予理睬,则连接不会建立。
发送窗口的上限值=Min(rwnd,cwnd)
HTTP协议无状态的解决方案
1.1相较1.0最明显的区别是引入了长连接技术
为网络通信提供安全及数据完整性的一种安全协议
位于TCP与各个应用层协议之间,是操作系统对外的API
采用身份认证和数据加密保证网络通信的安全及数据完整性
HTTP:客户端和服务器没有任何身份确认的过程,数据全部明文传输,容易被黑客截获,黑客可冒充服务器,返回任意信息给客户端,而不被客户端察觉
HTTPS(超文本传输安全协议):以计算机网络安全通信为目的的传输协议,在HTTP下加入了SSL层,从而具有了保护数据隐私、完整性和提供对网站服务器身份认证的功能。
数据加密(给数据裹上一层外套)
数字签名:在信息的后面加上一段内容(经过哈希后的值),可以证明信息没有被修改过
哈希算法:将任意长度的信息转化为固定长度的值,算法不可逆,常见:MD5算法
非对称加密:加密时用的秘钥(公钥)和解密使用的秘钥(私钥)是不相同的,私钥是保密的
对称加密:加密和解密使用同一个秘钥
HTTPS数据传输流程(数据传输之前,web浏览器会与服务器进行一次握手,已确定双方的加密密码信息)
在域名服务器中广泛使用高速缓存:DNS服务器可以将自己查询到的结果信息缓存到高速缓存中。当下次有另一个星通的域名查询到达该服务器时,该服务器能够直接提供所需要的IP地址,而不需要去其他的DNS服务器上查询了。
浏览器缓存==>系统缓存==>路由器缓存==>本地域名服务器缓存==>根域名服务器缓存==>顶级域名服务器缓存
public class BIOPlainEchoServer {
//=====服务器端=====问题:每收到一个请求就创建一个线程开销比较大
public void serve(int port) throws IOException {
//1.将ServerSocket绑定到指定的端口里
final ServerSocket socket = new ServerSocket(port);
while (true) {
//2.阻塞直到收到新的客户端连接
final Socket clientSocket = socket.accept();
System.out.println("Accepted connection from " + clientSocket);
//3.创建一个子线程去处理客户端的请求
new Thread(new Runnable() {
@Override
public void run() {
//4.从客户端socket的输入流中读取数据
try (BufferedReader reader = new BufferedReader(new InputStreamReader(clientSocket.getInputStream()))) {
//5.将客户端的请求原封不动回写回去
PrintWriter writer = new PrintWriter(clientSocket.getOutputStream(), true);
while (true) {
writer.println(reader.readLine());
writer.flush();
}
} catch (IOException e) {
e.printStackTrace();
}
}
}).start();
}
}
//=====服务器端(改进版:伪异步):引入线程池,将客户端请求提交给线程池去执行=====
public void improvedServe(int port) throws IOException {
//1.将ServerSocket绑定到指定的端口里
final ServerSocket socket = new ServerSocket(port);
//2.=======创建一个固定的线程池=======
ExecutorService executorService = Executors.newFixedThreadPool(6);
while (true) {
//3.阻塞直到收到新的客户端连接
final Socket clientSocket = socket.accept();
System.out.println("Accepted connection from " + clientSocket);
//4.将请求提交给线程池去执行
executorService.execute(() -> {
//4.从客户端socket的输入流中读取数据
try (BufferedReader reader = new BufferedReader(new InputStreamReader(clientSocket.getInputStream()))) {
PrintWriter writer = new PrintWriter(clientSocket.getOutputStream(), true);
//5.将客户端的请求原封不动回写回去
while (true) {
writer.println(reader.readLine());
writer.flush();
}
} catch (IOException e) {
e.printStackTrace();
}
});
}
}
}
BIO(Blocking IO):同步阻塞(服务器端需要阻塞等待来自客户端的连接请求)
每接收到一个来自客户端的连接就创建一个子线程去处理客户端的请求
**缺点:**客户端并发访问量急剧膨胀可能会导致线程堆栈溢出、创建新线程失败等问题,最终导致进程宕机或者僵死,不能对外提供服务
**改进(引入线程池):**后端通过一个线程池来处理多个客户端的请求接入,形成客户端个数M:线程池最大线程数N的比例关系,其中M可以远远大于N.通过线程池可以灵活地调配线程资源,设置线程的最大值,防止由于海量并发接入导致线程耗尽。
NIO(Non-Blocking IO):同步非阻塞(服务器端在从Channel读取数据到Buffer期间可以执行其他操作,在将Buffer中的数据写入Channel期间也可以执行其他操作**)**
它支持面向缓冲(Buffer)的,基于通道(Channel)的I/O操作方法。数据可以从Channel读到Buffer中,也可以从Buffer写到Channel中.
Buffer:在读取数据时,它是直接读到缓冲区中的; 在写入数据时,写入到缓冲区中。
Channel:通道是双向的,可读也可写。无论读写,通道只能和Buffer交互。因为 Buffer,通道可以异步地读写。
Selectors(选择器):NIO的底层使用了操作系统的IO多路复用技术:单线程可以同时处理多个网络IO,调用系统级别的select/poll/epoll模型,由系统进行监控IO状态,例如select轮训可以监控许多个io请求,当有一个socket数据被准备好的时候,就可以返回了。
当监测到一个新连接,不需要创建一个线程,而是直接注册到clientSelector
AIO(Asynchronous IO):异步非阻塞(一旦一个新的客户端请求被接受时,会调用回调函数,通知服务器端执行响应操作**)**
第一范式:当关系模式R的所有属性都不能在分解为更基本的数据单位时,称R是满足第一范式的,简记为1NF。满足第一范式是关系模式规范化的最低要求,否则,将有很多基本操作在这样的关系模式中实现不了。
第二范式:如果关系模式R满足第一范式,并且R得所有非主属性都完全依赖于R的每一个候选关键属性,称R满足第二范式,简记为2NF。
第三范式:设R是一个满足第一范式条件的关系模式,X是R的任意属性集,如果X非传递依赖于R的任意一个候选关键字,称R满足第三范式,简记为3NF.
第一范式:
每一列属性都是不可再分的属性值,确保每一列的原子性
两列的属性相近或相似或一样,尽量合并属性一样的列,确保不产生冗余数据。
第二范式:
第三范式:
首先Mysql的基本存储结构是页(记录都存在页里面)
各个数据页可以组成一个双向链表
而每个数据页中的记录又可以组成一个单向链表
所以说,如果我们写select * from user where username = 'Java3y'
这样没有进行任何优化的sql语句,默认会这样做:
将无序数据变成有序数据(相对)
要找到id为8的记录简要步骤:
很明显的是:没有用索引我们是需要遍历双向链表来定位对应的页,现在通过**“目录”**就可以很快地定位到对应的页上了!其实底层结构就是B+树,B+树作为树的一种实现,能够让我们很快地查找出对应的记录。
最左匹配原则:
假设我们有两列a,b,a和b是联合索引,他的顺序是a,b,我们在where语句中调用a= and b=的时候就会走联合索引,如果调用where a = 的时候也会走索引,但是当我们使用where b = 的时候就不会走这个联合索引
成因:
mysql创建复合索引的规则是首先会对复合索引的最左边,也就是索引中的第一个字段进行排序,在第一个字段排序的基础上,在对索引上第二个字段进行排序,其实就像是实现类似order by 字段1,字段2这样的排序规则,那么第一个字段是绝对有序的,而第二个字段就是无序的了,因此一般情况下直接只用第二个字段判断是用不到索引的,这就是为什么mysql要强调联合索引最左匹配原则的原因。
**B树:**B树中每个节点包含了键值和键值对于的数据对象存放地址指针,所以成功搜索一个对象可以不用到达树的叶节点。
关键字最左边节点中的关键字都小于本关键字;关键字最右边节点中的关键字都大于本关键字;其他孩子节点中的关键都介于该节点两侧关键字大小之间。
**B+树:**B+树非叶节点中存放的关键码并不指示数据对象的地址指针,非叶节点只是索引部分。所有的叶节点在同一层上,包含了全部关键码和相应数据对象的存放地址指针,且叶节点按关键码从小到大顺序链接。
哈希索引:哈希索引就是采用一定的哈希算法,把键值换算成新的哈希值,检索时不需要类似B+树那样从根节点到叶子节点逐级查找,只需一次哈希算法即可立刻定位到相应的位置,速度非常快。
缺点
不支持范围查询,因为经过Hash算法得到的Hash值顺序并不能保证和之前的顺序完全一样
数据库无法进行排序
不能利用部分索引键进行查询,因为对于组合键,Hash索引先拼接组合键之后计算Hash值
不能避免表扫描:对于Hash值冲突的数据,还是要扫描bucket
当有大量Hash值发生冲突时,查询效率比较低
关系型数据库一般使用B+树作为存储引擎原因
共享锁(读锁/S锁)**:**上了共享锁之后,依然支持上共享锁,但不支持上排它锁
**排他锁(写锁/X锁):**上了排它锁之后,就不支持再上其他的锁
增删改查时的默认加锁行为
表锁:开销小,加锁快;不会出现死锁;锁定力度大,发生锁冲突概率高,并发度最低
行锁:开销大,加锁慢;会出现死锁;锁定粒度小,发生锁冲突的概率低,并发度高
InnoDB只有通过索引条件检索数据才使用行级锁,否则,InnoDB将使用表锁
**悲观锁:**对系统被外界(本系统当前其他事务+来自外部系统事务)修改持悲观态度,因此在数据处理过程中将数据处于锁定状态,依赖于数据库层提供的锁机制。
**乐观锁:认为数据一般情况下不会造成冲突,所以在数据进行提交更新时,才会对数据冲突与否进行检测,如果发生冲突,则返回错误信息,让用户决定如何去做。**不会使用数据库提供的锁机制。**一般就是记录数据版本,使用版本号或者时间戳。**处理完业务逻辑开始更新的时候,需要再次查看该字段的值是否和第一次的一样。如果一样更新,反之拒绝
update A set Name=lisi,version=version+1 where ID=#{id} and version=#{version}
GAP锁(间隙锁):当我们用范围条件检索数据而不是相等条件检索数据,并请求共享或排他锁时,InnoDB会给符合范围条件的已有数据记录的索引项加锁;对于键值在条件范围内但并不存在的记录,叫做“间隙(GAP)”。InnoDB也会对这个“间隙”加锁,这种锁机制就是所谓的间隙锁。
Select * from emp where empid > 100 for update;
上面是一个范围查询,InnoDB不仅会对符合条件的empid值为101的记录加锁,也会对empid大于101(这些记录并不存在)的“间隙”加锁。
Automic(原子性):事务包含的一组操作要么全执行,要么全不执行
Consistency(一致性):数据从一个一致性状态到达另一个一致性状态,数据满足完整性约束
Isolation(隔离性):事务完成前数据的中间状态对其他事务是不可见的
Durabliity(持久性):保证提交的事务对数据库影响的永久性,需要REDO日志重启时恢复
LU(Lost Update丢失更新):一个事物的更新覆盖了另一个事物的更新----现在主流数据库都为为我们自动加锁,mysql所有事务隔离级别在数据库上均可避免此类问题。
DR(DirtyRead读取脏数据):一个事务读取了另一个事务更新却没有提交的数据----在RC(Read Commmited)隔离级别以上均可避免
set session transaction isolation level read commited
原理:把释放锁的位置调整到事务提交之后,此时在事务提交前,其他进程是无法对该行数据进行读取的,包括任何操作
NRR(Non-Repeatable Reads不可重复读):一个事务对同一个数据项的多次读取操作可能得到不同的结果,事务A多次读取同一数据,事务B在事务A读取数据的过程中,对数据进行了更新和提交,使得事务A多次读取时,得到的数据不一致----在RR(Repeatable Read)隔离级别以上均可避免,确保在同一事务中,对同一个数据的先后读取的结果是一样的。
set session transaction isolation level repeatable read
原理:事务级别的快照!每次读取的都是当前事务的版本,即使被修改了,也只会读取当前事务版本的数据。
PR(Phantom Reads幻读):事务A读取与搜索条件相匹配的若干行,事务B以插入或删除行等方式来修改事务A的结果集,导致事务A看起来像出现幻觉一样-—S(Serializable串行化)隔离级别可以避免
set session transaction isolation level serializable
原理:事务的执行是串行的
简单来说 redis 就是一个数据库,不过与传统数据库不同的是 redis 的数据是存在内存中的,所以读写速度非常快,因此 redis 被广泛应用于缓存方向。另外,redis 也经常用来做分布式锁。redis 提供了多种数据类型来支持不同的业务场景。除此之外,redis 支持事务 、持久化、LUA脚本、LRU驱动事件、多种集群方案。
单个 Redis 命令的执行是原子性的,但 Redis 没有在事务上增加任何维持原子性的机制,所以 Redis 事务的执行并不是原子性的。
事务可以理解为一个打包的批量执行脚本,但批量指令并非原子化的操作,中间某条指令的失败不会导致前面已做指令的回滚,也不会造成后续的指令不做。
string(字符串)
hash(哈希)–map
list(双向列表)
set(集合)
zset(sorted set:有序集合)
1.ps:探查进程,只能显示某个特定时间点的信息
C列:进程生命周期中的CPU利用率
2.top:实时监测进程,实时显示进程信息
第1行:当前时间,系统运行时间,登录用户数量,系统的平均负载(最近1分钟,5分钟,15分钟)
第2行:进程总数,running状态的进程数,sleeping状态下的进程数,stopped状态下的进程数,zombie(僵化:完成了,但父进程没有响应)
第3行:CPU的利用率(详细:计算linux服务器CPU利用率,性能分析Linux服务器CPU利用率)
CPU时间包含User time、System time、Nice time、Idle time、Waiting time、Hardirq time、Softirq time、Steal time
空闲态时间==idle time
用户态时间==user time+ Nice time。
内核态时间==system time+ Hardirq time+ Softirq time。
user time。指CPU在用户态执行进程的时间。
system time。指CPU在内核运行的时间。
nice time。指系统花费在调整进程优先级上的时间。
idle time。系统处于空闲期,等待进程运行。
waiting time。指CPU花费在等待I/O操作上的总时间,与ed相似。
steal time。指当前CPU被强制(involuntary wait )等待另外虚拟的CPU处理完毕时花费的时间,此时 hypervisor 在为另一个虚拟处理器服务。
Softirq time 、Hardirq time。分别对应系统在处理软硬中断时候所花费的CPU时间。
第4行:系统的物理内存:内存总容量,已使用大小,空闲大小,缓冲区大小
第5行:系统的交换空间:总容量,已使用大小,空闲大小,缓存大小
PR列:进程的优先级
VIRT列:进程占用的虚拟内存总量
RES列:进程占用的物理内存总量
%CPU:进程是用的CPU时间比例
%MEM:进程是用的内存占可用内存的比例
键入f:选择对输出进行排序的字段
键入d:修改轮训间隔
减去q:退出
Swap分区在系统的物理内存(这里应该是运行内存)不够用的时候,把物理内存中的一部分空间释放出来,以供当前运行的程序使用。那些被释放的空间可能来自一些很长时间没有什么操作的程序,这些被释放的空间被临时保存到Swap分区中,等到那些程序要运行时,再从Swap分区中恢复保存的数据到内存中。