编译型 :编译型语言 会通过编译器 将源代码一次性翻译成可被该平台执行的机器码。一般情况下,编译语言的执行速度比较快,开发效率比较低。常见的编译性语言有 C、C++、Go、Rust 等等。
解释型 :解释型语言 会通过解释器 一句一句的将代码解释为机器代码后再执行。解释型语言开发效率比较快,执行速度比较慢。常见的解释性语言有 Python、JavaScript、PHP 等等。
Java:既是编译性语言(需要由编译器编译为.class字节码文件),又是解释性语言(需要由JVM读一行执行一行,由解释器解释为操作系统能执行的命令)
Java的编译器是javac.exe,解释器是java.exe
引⽤数据类型:数组、类、接口
基本数据类型存放在 Java 虚拟机栈
中的局部变量表中,而包装类型属于对象类型,存在于堆
中。
所有整型包装类对象之间值的比较,全部使用 equals
方法比较。
对象相等:比的是内存中存放的内容是否相等。
引用相等:比较的是他们指向的内存地址是否相等。
==:
基本数据类型(八种基本数据类型)比较的是值,引用数据类型(数组、类、接口)比较的是内存地址。
equals() :
只能判断引⽤数据类型,若没有覆盖equals,则等价于 ==
;若覆盖了,大多数情况比如string,integer,则是比较内容
string:
虚拟机在常量池中查找有没有已经存在的值和要创建的值相同的对象,如果有就把它赋给当前引用。如果没有就在常量池中重新创建一个 String 对象
String x = "string";
String y = "string";
String z = new String("string");
System.out.println(x==y); // true,引用相同
System.out.println(x==z); // false,==:string比较引用,开辟了新的堆内存空间,所以false
System.out.println(x.equals(y)); // true,equals:string:比较值,相同
System.out.println(x.equals(z)); // true,equals:string比较值,相同
hashcode
与equals:
获取哈希码(散列码),返回int,确定对象在哈希表中的索引位置,任何类都有hashcode()函数。
如果hashcode相同,再调用equals()检查两个对象是否真的相同,否则hashset不会让其加入成功,这样减少了equals使用次数,提高就执行速度。
两个对象相同,则有相同的hashcode;反之hashcode相同,对象不一定相等。所以要重写equals时必须重写hashcode()函数,如果没有重写 hashCode(),则该 class的两个对象⽆论如何都不会相等。
StringBuilder
与 StringBuffer
都继承自 AbstractStringBuilder 类,没有用 final 关键字修饰,所以这两种对象都是可变的。String 类中使用 final 关键字修饰,不可变。
StringBuffer 是线程安全的(加了同步锁)。 StringBuilder 是非线程安全的。
每次对 String 类型进行改变的时候,都会生成一个新的 String 对象,然后将指针指向新的 String对象。
StringBuffer 每次都会对 StringBuffer 对象本身进行操作,而不是生成新的对象并改变对象引用。相同情况下使用 StringBuilder 相比使用 StringBuffer获得 10%~15% 左右的性能提升,但却要冒多线程不安全的风险。
重写:
1.重写发生在父类与子类之间,是运行时的多态性
2.方法名,参数列表,返回类型(除过子类中方法的返回类型是父类中返回类型的子类)必须相同
3.访问修饰符的限制一定要大于被重写方法的访问修饰符(public>protected>default>private)
4.不可被重写:final,static,private
重载:
1.重载Overload发生在一个类中,是编译时的多态性
2.重载要求同名方法的参数列表不同(参数类型,参数个数甚至是参数顺序),返回值类型可以相同也可以不相同。
static修饰的方法:
1、父类中的静态方法可以被继承、但不能被子类重写。
2、如果在子类中写一个和父类中一样的静态方法,那么该静态方法由该子类特有,两者不构成重写关系。
3、static修饰的属性的初始化在编译期(类加载的时候),初始化后能改变。跟该类的具体对象无关
final修饰:
1、修饰类表示不允许被继承。
2、修饰方法表示不允许被子类重写,但是可以被子类继承,不能修饰构造方法。一个类中的private方法会隐式地被指定为final方法。
3、修饰变量表示不允许被修改
a)方法内部的局部变量,使用前被赋值即可(只能赋值一次),没有必要非得初始化。
b)类中的成员变量(如果没有在定义时候初始化,那么只能在构造代码块中或者构造方法中赋值)
c)基本数据类型的变量(初始化赋值之后不能更改)
d)引用数据类型的变量(初始化之后不能再指向另外一个对象,但对象的内容是可以变的)
Throwable
类有两个重要的子类 Exception
(异常)和 Error
(错误)。
Exception
:程序本身可以处理的异常,可以通过 try-catch 来进行捕获。Exception 又可以分为 受检查异常(必须处理否则无法通过编译 ) 和 不受检查异常(可以不处理)。Error
属于程序无法处理的错误 ,我们没办法通过 catch 来进行捕获 。例如,Java 虚拟机运行错误(Virtual MachineError)、虚拟机内存不够错误(OutOfMemoryError)、类定义错误(NoClassDefFoundError)等 。这些异常发生时,Java 虚拟机(JVM)一般会选择线程终止。try-catch-finally:
无论是否捕获或处理异常,finally 块里的语句都会被执行。当在 try 块或 catch 块中遇到 return 语句时,finally 语句块将在方法返回之前被执行,且会覆盖之前的返回值。
除非: 在异常语句之前System.exit(int);线程死亡;关闭CPU
I/O
I/O
流对于不想进行序列化的变量,使用 transient
关键字修饰。 当对象被反序列化时,被 transient 修饰的变量值不会被持久化和恢复。
I/O
模型:我们的应用程序对操作系统的内核发起 IO 调用(系统调用),操作系统负责的内核执行具体的 IO 操作。会经历两个步骤:
Java 中 3 种常见 IO 模型:BIO,NIO,AIO
BIO
(Blocking I/O): 同步阻塞应用程序发起 read 调用后,会一直阻塞,直到内核把数据拷贝到用户空间。当面对十万甚至百万级连接的时候,传统的 BIO 模型是无能为力的。
NIO
(Non-blocking/New I/O):同步非阻塞NIO 基于通道(Channel)和缓存区(Buffer),对于高负载、高并发的(网络)应用,应使用 NIO 。可以直接分配堆外内存,通过存储在Java堆中的DirectByteBuffer对象作为这块内存的引用进行操作,显著提升性能。
NIO提供了与传统 BIO 模型中的 Socket 和 ServerSocket 相对应的 SocketChannel 和ServerSocketChannel 两种不同的套接字通道实现,两种通道都支持阻塞和非阻塞两种模式。
阻塞模式使用就像传统中的支持一样,比较简单,但是性能和可靠性都不好;非阻塞模式正好与之相反。对于低负载、低并发的应用程序,可以使用同步阻塞 I/O 来提升开发速率和更好的维护性;对于高负载、高并发的(网络)应用,应使用 NIO 的非阻塞模式来开发。
NIO 通过轮询操作,避免了一直阻塞。应用程序不断进行 I/O 系统调用轮询数据是否已经准备好的过程是十分消耗 CPU 资源的。
IO 多路复用模型中:线程首先发起 select 调用,询问内核数据是否准备就绪,等内核把数据准备好了,用户线程再发起 read 调用。
在非阻塞 IO 中,不断地询问 socket 状态时通过用户线程去进行的,而在多路复用 IO 中,轮询每个 socket 状态是内核在进行的,这个效率要比用户线程要高的多。
AIO
(Asynchronous I/O):异步AIO 也就是 NIO 2。在 Java 7 中引入了 NIO 的改进版 NIO 2,它是异步非阻塞的 IO 模型。
当用户线程发起 read 操作之后,立刻就可以开始去做其它的事。而另一方面,从内核的角度,当它受到一个 asynchronous read 之后,内核会等待数据准备完成,然后将数据拷贝到用户线程,当这一切都完成之后,内核会给用户线程发送一个信号,告诉它 read 操作完成了。
用户线程只需要先发起一个请求,当接收内核返回的成功信号时表示 IO 操作已经完成,可以直接去使用数据了
同步IO操作会引起进程阻塞直到IO操作完成。
异步IO操作不引起进程阻塞。
指在运行状态中,动态获取信息以及动态调用对象方法的功能
对象在运行是都会出现两种类型:编译时类型和运行时类型。编译时类型由声明对象时使用的类型来决定,运行时的类型由实际赋值给对象的类型决定 。比如编译时根本无法预知该对象和类属于哪些类,程序只能靠运行时信息来发现该对象和类的信息,那就要用到反射了。
反射最大的用途就是框架,比如:Spring中的Di/IoC、Hibernate中的find(Class clazz)、Jdbc中的classForName()、SpringBoot的Service注解等,很多开发框架都用到了反射机制
优点:
1)能够运行时动态获取类的实例,提高灵活性;
2)与动态编译结合
缺点:
1)使用反射性能较低,需要解析字节码,将内存中的对象进行解析。
获取class对象的三种方式:
// 这一new 产生一个Student对象,一个Class对象。*
Student student = new Student();
// 调用某个类的 class 属性来获取该类对应的 Class 对象
Class studentClass2 = Student.class;
// 使用 Class 类中的 forName() 静态方法 ( 最安全 / 性能最好 )
Class studentClass3 = Class.forName("com.reflect.Student")
泛型的原理就是“类型的参数化”,即把类型看作参数。也就是说把所有要操作的数据类型看作参数,就像方法的形式参数是运行时传递的值的占位符一样。
好处:
①类型安全。泛型的主要目标是实现java的类型安全。 泛型可以使编译器知道一个对象的限定类型是什么,这样编译器就可以在一个高的程度上验证这个类型
②消除了强制类型转换。使得代码可读性好,减少了很多出错的机会
③安全简单。泛型的好处是在编译的时候检查类型安全,并且所有的强制转换都是自动和隐式的,提高代码的重用率。
原理:
泛型的实现是靠类型擦除技术。类型擦除是在编译期完成的,在编译期,编译器会将泛型的类型参数都擦除成它的限定类型,如果没有,则擦除为object类型,之后在获取的时候再强制类型转换为对应的类型。 在运行期间并没有泛型的任何信息,因此也没有优化。
Collection
: List、Set
和 Queue
Map
: hashmap、hashtable、hashtree
List
(有序):存储的元素是有序的、可重复的。(可以有多个元素引⽤相同的对象)
Arraylist
: Object[] 数组Vector
:Object[] 数组(线程安全)LinkedList
: 双向链表Arraylist
线程不安全解决:
Vector
(ArrayList
所有方法加synchronized
,太重)。java.concurrent.CopyOnWriteArrayList
(推荐)。是JUC的类,通过写时复制来实现读写分离。比如其add()
方法,就是先复制一个新数组,长度为原数组长度+1,然后将新数组最后一个元素设为添加的元素。Arraylist
扩容:
当添加元素时,如果元素个数+1> 当前数组长度 【size + 1 > elementData.length】时,进行扩容, int newCapacity = oldCapacity + (oldCapacity >> 1)
,所以 ArrayList 每次扩容之后容量都会变为原来的 1.5 倍,将旧数组内容通过Array.copyOf全部复制到新数组(如果5,就扩成5+2=7)
Arraylist
与 LinkedList
Arraylist
底层是 object 动态数组,支持快速随机访问,适合频繁查找 get(int index)
LinkedList
底层是双向链表,插入、添加、删除快,更占内存vector
是同步的,线程安全,但效率太低,尽量用 ArraylistSet
(无重复):不允许重复的集合。不会有多个元素引用相同的对象。无序,hashcode决定
HashSet
(无序,唯一): 基于 HashMap 实现的,底层采用 HashMap 来保存元素 。只不过Value被写死了,是一个private static final Object
对象。用于不需要保证元素插入和取出顺序的场景LinkedHashSet
: LinkedHashSet 是 HashSet 的子类,并且其内部是通过 LinkedHashMap 来实现的。 底层数据结构是链表和哈希表,元素的插入和取出顺序满足 FIFO。TreeSet
(有序,唯一): 红黑树(自平衡的排序二叉树) ,用于支持对元素自定义排序规则的场景Queue
(实现排队功能的叫号机):按特定的排队规则来确定先后顺序,存储的元素是有序的、可重复的
Queue
:是单端队列,只能从一端插入元素,另一端删除元素,实现上一般遵循 先进先出(FIFO) 规则Deque
:是双端队列,在队列的两端均可以插入或删除元素PriorityQueue
: Object[] 数组来实现二叉堆,优先级最高的元素先出队,默认是小顶堆,通过堆元素的上浮和下沉,实现了在 O(logn) 的时间复杂度内插入元素和删除堆顶元素。非线程安全的,不支持存储nullArrayQueue
: Object[] 数组 + 双指针阻塞队列的应用:
①生产者消费者模式,不用加锁
②线程池
Map
(键值对):使用键值对存储。key 是无序的、不可重复的,value 是无序的、可重复的,两个Key可以引用相同的对象,但Key不能重复,Key可以是任何对象
HashMap
: JDK1.8 之前 HashMap 由数组+链表组成的,数组是 HashMap 的主体,链表则是主要为了解决哈希冲突而存在的(“拉链法”解决冲突)。JDK1.8 以后,当链表长度大于阈值(默认为 8)(将链表转换成红黑树前会判断,如果当前数组的长度小于 64,那么会选择先进行数组扩容,而不是转换为红黑树)时,将链表转化为红黑树,以减少搜索时间(红黑树就是为了解决二叉查找树的缺陷,因为二叉查找树在某些情况下会退化成一个线性结构。)
LinkedHashMap
: LinkedHashMap 继承自 HashMap,它的底层仍然是基于拉链式散列结构即由数组和链表或红黑树组成。另外,LinkedHashMap 在上面结构的基础上,增加了一条双向链表,使得上面的结构可以保持键值对的插入顺序。同时通过对链表进行相应的操作,实现了访问顺序相关逻辑。Hashtable
: 数组+链表组成的,数组是 Hashtable 的主体,链表则是主要为了解决哈希冲突而存在的TreeMap
: 红黑树(自平衡的排序二叉树)HashMap
, Hashtable
, HashSet
,TreeMap
, ConcurrentHashMap
(2的幂次方:Hash 值的范围值非常大,先做对数组的长度取模运算,得到的余数才是对应的数组下标。当length 是 2 的 幂次方,hash%length
等价于hash&(length-1)
, &
相比%
效率较高)
容量(初始16),加载因子(0.75),阈值(容量x加载因子)
如果加载因子比较大,扩容发生的频率比较低,浪费的空间比较小,发生hash冲突的几率比较大,底层的红黑树变得异常复杂。对于查询效率极其不利;
&
),然后赋给新的HashMapjdk1.7数据丢失与链表成环:
hashmap的get操作:
HashSet
底层就是基于 HashMap 实现的,仅存储对象,使用成员对象计算hashcode,(hashmap存储键值对,使用key计算hashcode)
HashMap来说, TreeMap
主要多了对集合中的元素根据键排序(SortedMap 接口)的能力,以及对集合内元素的搜索(NavigableMap 接口)的能力。
ConcurrentHashMap 解决多线程并发环境下 HashMap死循环问题,线程安全的,比Hashtable效率高。底层为数组+链表/红黑树。
synchronized
只锁定当前链表或红黑二叉树的首节点。先 CAS(Unsafe.compareAndSwapObject方法)尝试插入,如果有其它线程在该位置提前插入了节点,就自旋再次尝试,失败了再加锁。(Hashtable使用synchronized,没有分段,效率低)
产生原因:
由于哈希算法被计算的数据是无限的,而计算后的结果范围有限,因此总会存在不同的数据经过计算后得到的值相同,这就是哈希冲突。(两个不同的数据计算后的结果一样)
解决方法:
①开放地址方法
(1)线性探测:按顺序决定值时,如果某数据的值已经存在,则在原来值的基础上往后加一个单位,直至不发生哈希冲突。
(2)再平方探测:按顺序决定值时,如果某数据的值已经存在,则在原来值的基础上先加1的平方个单位,若仍然存在则减1的平方个单位。随之是2的平方,3的平方等等。直至不发生哈希冲突。
(3)伪随机探测:按顺序决定值时,如果某数据已经存在,通过随机函数随机生成一个数,在原来值的基础上加上随机数,直至不发生哈希冲突。
②链式地址法(HashMap的哈希冲突解决方法)
创建一个链表数组,数组中每一格就是一个链表。若遇到哈希冲突,则将冲突的值加到链表中即可。
优点:
(1)拉链法处理冲突简单,且无堆积现象,即非同义词决不会发生冲突,因此平均查找长度较短;
(2)由于拉链法中各链表上的结点空间是动态申请的,故它更适合于造表前无法确定表长的情况;
(3)开放定址法为减少冲突,要求装填因子α较小,故当结点规模较大时会浪费很多空间。而拉链法中可取α≥1,且结点较大时,拉链法中增加的指针域可忽略不计,因此节省空间;
(4)在用拉链法构造的散列表中,删除结点的操作易于实现。只要简单地删去链表上相应的结点即可。
缺点:
指针占用较大空间时,会造成空间浪费,若空间用于增大散列表规模进而提高开放地址法的效率。
③建立公共溢出区
建立公共溢出区存储所有哈希冲突的数据。
④再哈希法
对于冲突的哈希值再次进行哈希处理,直至没有哈希冲突。
进程与线程:
比如:启动 main 函数时其实就是启动了一个 JVM 的进程,而 main 函数所在的线程就是这个进程中的一个线程,也称主线程。
多进程与多线程:
并行与并发:
通信:
多线程的实现:
协程:
start()
和run()
:
start()
:会启动一个线程并使线程进入了就绪状态,当分配到时间片后就可以开始运行了,会自动执行 run()。run()
:main 线程下的普通方法去执行,不是多线程。上下文切换:
如果线程的个数都大于 CPU 核心的个数,而一个 CPU 核心在任意时刻只能被一个线程使用,CPU 为每个线程分配时间片并轮转的形式。当一个线程的时间片用完的时候就会重新处于就绪状态让给其他线程使用。
发生线程切换,保存当前线程的上下文,留待线程下次占用 CPU 的时候恢复现场。并加载下一个将要占用 CPU 的线程上下文。
发生原因:
进程控制块PCB(process control block),程序段、相关数据段,三部分构成了进程
进程通信是一种手段,而进程同步是一种目的。也可以说,为了能够达到进程同步的目的,需要让进程进行通信,传输一些进程同步所需要的信息
临界区:对临界资源进行访问的那段代码称为临界区。需要互斥访问临界资源
进程通信:管道(匿名/有名)、消息队列、信号、信号量、共享内存、套接字
共享内存:
为了确定首先执行哪个进程以及最后执行哪个进程以实现最大 CPU 利用率
线程同步是两个或多个共享关键资源的线程的并发执行。应该同步线程以避免关键的资源使用冲突。有下面三种线程同步的方式:互斥量、信号量、事件
synchronized
关键词和各种 Lock 都是这种机制。线程切换,第1步是不需要做的,第2是进程和线程切换都要做的。
线程的切换虚拟内存空间依然是相同的,但是进程切换是不同的。这两种上下文切换的处理都是通过操作系统内核来完成的。内核的这种切换过程伴随的最显著的性能损耗是将寄存器中的内容切换出。
僵尸进程: 一个父进程利用fork创建子进程,如果子进程退出,而父进程没有利用wait 或者 waitpid 来获取子进程的状态信息,那么子进程的状态描述符依然保存在系统中。
孤儿进程:一个父进程退出, 而它的一个或几个子进程仍然还在运行,那么这些子进程就会变成孤儿进程,孤儿进程将被init进程(进程号为1)所收养,并由init进程对它们完成状态收集的工作
死锁恢复:
死锁避免:银行家算法。银行家算法的主要思想是避免系统进入不安全状态。在每次进行资源分配时,它首先检查系统是否有足够的资源满足要求,如果有,则先进行分配,并对分配后的新状态进行安全性检查。如果新状态安全,则正式分配上述资源,否则就拒绝分配上述资源。这样,它保证系统始终处于安全状态,从而避免死锁现象的发生。
找死锁的步骤:
jps
确定当前执行任务的进程号jstack
命令查看当前进程堆栈信息Found a total of 1 deadlock
synchronized
可以保证代码片段的原子性。volatile
关键字可以保证共享变量的可见性。volatile
关键字可以禁止指令进行重排序优化。缓存一致性(Cache Coherence),解决是多个缓存副本之间的数据的一致性问题。
内存一致性(Memory Consistency),保证的是多线程程序访问内存时可以读到什么值。
程序运行的时候我们把外存的数据复制到内存,解决硬盘访问速度过慢的问题。
先复制一份数据到 CPU Cache 中,当 CPU 需要用到的时候就可以直接从 CPU Cache 中读取数据,当运算完成后,再将运算得到的数据写回 Main Memory 中。
问题:内存缓存不一致
两个线程同时从Cache中读取i=1
进行i++
,返回Main Memory之后 i=2,而正确结果应该是 i=3
解决:
①加锁
②缓存一致性协议,保证每个缓存中使用的共享变量的副本是一致的。
线程可以把变量保存本地内存中,而不是直接在主存中进行读写。这就可能造成一个线程在主存中修改了一个变量的值,而另外一个线程还继续使用它在寄存器中的变量值的拷贝,造成数据的不一致。
要解决这个问题,就需要把变量声明为 volatile
,这就指示 JVM,这个变量是共享且不稳定的,每次使用它都到主存中进行读取,即可见性。
load:主内存 read 后,把 read 的值放入工作内存的变量副本中
assign:把线程执行后的值赋给工作内存的变量
store:把工作内存的一个值送到主内存
synchronized
、ReentrantLock
、volatile
synchronized
可以保证被它修饰的普通方法(锁实例对象)、静态方法(锁class对象)、代码块在任意时刻只能有一个线程执行。通过对 对象监视器 monitor
的获取来实现。
JDK1.6 对锁的实现引入了大量的优化,无锁、偏向锁、轻量级锁、重量级锁,他们会随着竞争的激烈而逐渐升级(可以升级不可降级),偏向锁,轻量级锁都是乐观锁,重量级锁是悲观锁。
MarkWord
:
自旋:
就是尝试获取锁的线程不会立即阻塞,而是采用循环的方式去尝试获取,好处是减少线程切换的上下文开销,缺点是会消耗CPU
synchronized
和Lock
synchronized
和Lock
synchronized
依赖于 JVM ,优化都是在虚拟机层面实现的,并没有直接暴露给我们。而 Lock
是一个接口,依赖于API,是 JDK 层面实现的,需要 lock() 和 unlock() 方法配合try/finally语句块来完成,所以我们可以通过查看它的源代码,来看它是如何实现的。
synchronized在发生异常时,会自动释放线程占有的锁,因此不会导致死锁现象发生;而Lock在发生异常时,如果没有主动通过unLock()
去释放锁,则很可能造成死锁现象,因此使用Lock时需要在finally块中释放锁;
ReentrantLock
和 synchronized
都是可重入锁,自己可以再次获取自己的内部锁。同一个线程每次获取锁,锁的计数器都子增1,所以要等到锁的计数器下降为0时才能释放锁。
ReentrantLock
比 synchronized
增加了⼀些⾼级功能:
①通过Lock可以知道有没有成功获取锁,而synchronized却无法办到。
②ReentrantLock提供了一种能够中断等待锁的线程的机制。 sync
不可中断,除非抛出异常或者正常运行完成。Lock
是可中断的,通过调用interrupt()
方法。
③ReentrantLock可以指定是公平锁还是非公平锁。而synchronized只能是非公平锁。公平锁就是先等待的线程先获得锁。
④可以实现“选择性通知”(锁可以绑定多个条件),可以通过一个或多个Condition来处理等待唤醒,一个Condition就是AQS中的一条等待队列。synchronized 在使用notify()/notifyAll()方法进行通知时,被通知的线程是由 JVM 选择的,
公平锁与非公平锁:
选择性通知condition:
condition.await
方法的线程会加入到等待队列中,并且线程状态转换为等待状态。signal
或者signalAll
方法,如果该线程能够从await()方法返回的话,一定是该线程获取了与condition相关联的lock。如果在等待状态中被中断会抛出被中断异常。synchronized
和volatile
:volatile
只能用于变量,而synchronized
可以修饰⽅法以及代码块。volatile
关键字是线程同步的轻量级实现,所以volatile
性能比synchronized
要好。但synchronized
引入的偏向锁和轻量级锁优化之后,执行效率有了显著提升, 实际开发中使用synchronized
的场景还是更多一些。volatile
能保证数据的可见性,但不能保证数据的原子性。synchronized
两者都能保证。volatile
主要用于解决变量在多个线程之间的可见性、有序性,而synchronized
解决的是多个线程之间访问资源的同步性,可以保证被它修饰的方法或者代码块在任意时刻只能有一个线程执行。Threadlocal
Threadlocal在线程内提供局部变量,让每个线程绑定自己的值,创建了一个ThreadLocal变量,那么访问这个变量的每个线程都会有这个变量的本地副本,避免线程竞争
底层是通过 ThreadLocalMap
进行存储键值,每个 Thread 都有一个 ThreadLocal.ThreadLocalMap
对象。每个ThreadLocal类创建一个Map,ThreadLocal(的弱引用)作为key, 实例对象作为value,通过Thread.currentThread()
获取到当前线程对象后,通过getMap(Thread t)
访问到该线程的ThreadLocalMap ,这样就能达到各个线程的值隔离的效果。
让每个线程可以关联多个 ThreadLocal变量。会使用 Thread内部都是使用仅有那个ThreadLocalMap 存放多个 ThreadLocal变量, key 就是 ThreadLocal对象,value 就是 ThreadLocal 对象调用set方法设置的值
ThreadLocal key 为弱引用,而 value 是强引用。在垃圾回收时,一旦发现了只具有弱引用的对象(不一定会立即发现),都会回收它的内存。
池化技术:线程池、数据库连接池、Http 连接池,为了减少每次获取资源的消耗,提高对资源的利用率。
线程池提供了⼀种限制和管理资源。每个线程池还维护一些基本统计信息,例如已完成任务的数量。
使用线程池的好处:
实现 Runnable
接口和 Callable
接口:
call()
方法,而不是run()
方法。执行 execute()
方法和 submit()
方法:
sleep
和wait
:
《阿里巴巴Java开发⼿册》中不允许使用 Executors 去创建,而是通过ThreadPoolExecutor
的方式。规避资源耗尽的风险
/**
* ⽤给定的初始参数创建⼀个新的ThreadPoolExecutor。
* 构造函数有四种,workQueue及以前必须都有,
* threadFactory和rejectHandler,全没有、有一个、全有,共四种
*/
public ThreadPoolExecutor(int corePoolSize,
int maximumPoolSize,
long keepAliveTime,
TimeUnit unit,
BlockingQueue<Runnable> workQueue,
ThreadFactory threadFactory,
RejectedExecutionHandler handler) {
if (corePoolSize < 0 ||
maximumPoolSize < 0 ||
maximumPoolSize < corePoolSize ||
keepAliveTime < 0)
throw new IllegalArgumentException();
if (workQueue == null || threadFactory == null || handler ==
null)
throw new NullPointerException();
this.corePoolSize = corePoolSize;
this.maximumPoolSize = maximumPoolSize;
this.workQueue = workQueue;
this.keepAliveTime = unit.toNanos(keepAliveTime);
this.threadFactory = threadFactory;
this.handler = handler;
}
import java.util.concurrent.*;
public class Test {
private static ExecutorService pool;
public static void main( String[] args )
{
//maximumPoolSize设置为2 ,拒绝策略为AbortPolic策略,直接抛出异常
pool = new ThreadPoolExecutor(1, 2, 1000, TimeUnit.MILLISECONDS,
new SynchronousQueue<Runnable>(),
Executors.defaultThreadFactory(),
new ThreadPoolExecutor.AbortPolicy());
for(int i=0;i<3;i++) {
pool.execute(new ThreadTask());
}
//获取返回值
//Future> future=threadPoolExecutor.submit(futureTask);
//Object value=future.get();
}
}
class ThreadTask implements Runnable{
public void run() {
System.out.println(Thread.currentThread().getName());
}
}
就像一个银行。corePoolSize
就像银行的“当值窗口“,比如今天有2位柜员在受理客户请求(任务)。如果超过2个客户,那么新的客户就会在等候区(等待队列workQueue
)等待。当等候区也满了,这个时候就要开启“加班窗口”,让其它3位柜员来加班,此时达到最大窗口maximumPoolSize
,为5个。如果开启了所有窗口,等候区依然满员,此时就应该启动“拒绝策略” handler,告诉不断涌入的客户,叫他们不要进入,已经爆满了。由于不再涌入新客户,办完事的客户增多,窗口开始空闲,这个时候就通过
keepAlivetTime`将多余的3个“加班窗口”取消,恢复到2个“当值窗口”。
ThreadPoolExecutor 3 个最重要的参数:
ThreadPoolExecutor 其他常见参数:
workQueue
的类型为BlockingQueue:
ArrayBlockingQueue
:基于数组的先进先出队列,此队列创建时必须指定大小;LinkedBlockingQueue
:基于链表的先进先出队列,如果创建时没有指定此队列大小,则默认为Integer.MAX_VALUE;吞吐量高,但会出现队列中的任务由于无法及时处理导致一直增长,直到最后资源耗尽的问题。synchronousQueue
:这个队列比较特殊,它不会保存提交的任务,而是将直接新建一个线程来执行新来的任务。常见线程池:
LinkedBlockingQueue
实现。即线程池中没有可运行任务时,它也不会释放工作线程,需要shutdownLinkedBlockingQueue
实现。在线程池中没有任务时可执行,也不会释放系统资源的,所以需要shutdownSynchronousQueue
实现。corePoolSize 被设置为空(0),maximumPoolSize被设置为 Integer.MAX.VALUE。总会迫使线程池增加新的线程去执行新的任务。在没有任务执行时,当线程的空闲时间超过keepAliveTime(60秒),则工作线程将会终止被回收,不需要shutdownimport java.util.concurrent.*;
public class Test {
private static Runnable getThread(final int i) {
return new Runnable() {
@Override
public void run() {
try {
Thread.sleep(500);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(i);
}
};
}
public static void main(String[] args) {
// 1、newSingleThreadExecutor
ExecutorService singPool = Executors.newSingleThreadExecutor();
for (int i=0;i<10;i++){
singPool.execute(getThread(i));
}
singPool.shutdown();
// 2、newFixedThreadPool
// ExecutorService fixPool = Executors.newFixedThreadPool(5);
// for (int i = 0; i < 10; i++) {
// fixPool.execute(getThread(i));
// }
// fixPool.shutdown();
// 3、newCachedThreadPool
// ExecutorService cachePool = Executors.newCachedThreadPool();
// for (int i=0;i<10;i++){
// cachePool.execute(getThread(i));//不需要shutdown(),keepAliveTime后自动释放
// }
}
}
基本类型( AtomicInteger、AtomicLong、 AtomicBoolean)、数组类型、引用类型、对象的属性修改类型
AtomicInteger 类主要利用 CAS
(compare and swap) + volatile
和 native 方法来保证原子操作,从而避免 synchronized 的高开销,不用加锁也可以保证线程安全。执行效率大为提升。
CAS
的原理是拿期望的值和原本的一个值作比较,如果相等,则证明共享数据没有被修改,替换成操作后的新值,然后继续往下运行;如果不相等,说明共享数据已经被修改,放弃已经所做的操作,用新的数重新执行刚才的操作。因此 JVM 可以保证任何时刻任何线程总能拿到该变量的最新值。
“我认为位置 V 应该包含值 A;如果包含该值,则将 B 放到这个位置;否则,不要更改该位置,只告诉我这个位置现在的值即可。”
CAS实际上是一种自旋锁,
AtomicStampedReference
类可以解决ABA问题。这个类维护了一个“版本号”Stamp,在进行CAS操作的时候,不仅要比较当前值,还要比较版本号。只有两者都相等,才执行更新操作。主要提供了显示锁,如重入锁(ReentrantLock)和读写锁(ReadWriteLock)。核心是AQS这个抽象队列同步器框架。
是⼀个⽤来构建锁和同步器的框架,比如ReentrantLock
AQS 通过内置的虚拟的 双向队列来完成获取资源线程的排队工作;AQS 使用一个 private volatile int state
成员变量来表示同步状态;使用 CAS
对该同步状态进行原子操作实现对其值的修改。
以 ReentrantLock 为例,state 初始化为 0,表示未锁定状态。A 线程 lock() 时,会调用 tryAcquire() 独占该锁并将 state+1 。此后,其他线程再 tryAcquire() 时就会失败,直到 A 线程 unlock() 到 state=0(即释放锁)为止,其它线程才有机会获取该锁。
ReentrantLock
。又可分为公平锁和非公平锁。公平锁:按照线程在队列中的排队顺序,先到者先拿到锁。 非公平锁:当线程要获取锁时,无视队列顺序,谁抢到就是谁的Semaphore
(信号量)-允许多个线程同时访问: synchronized 和 ReentrantLock 都是一次只允许一个线程访问某个资源,Semaphore(信号量)可以指定多个线程同时访问某个资源。
CountDownLatch
(倒计时器): CountDownLatch 是一个同步工具类,用来协调多个线程之间的同步。这个工具通常用来控制线程等待,它可以让某一个线程等待直到倒计时结束,再开始执行。 state
初始化为 N, N 个子线程是并行执行的,每个子线程执行完后countDown() 一次,state 会 CAS
减 1。等到所有子线程都执行完后(即 state=0 ),会 unpark() 主调用线程,然后主调用线程就会从 await() 函数返回,继续后余动作。
CyclicBarrier
(循环栅栏): CyclicBarrier 和 CountDownLatch 非常类似,它也可以实现线程间的技术等待,让一组线程到达一个屏障(也可以叫同步点)时被阻塞,每个线程调用 await() 方法告诉 CyclicBarrier 我已经到达了屏障,然后当前线程被阻塞。直到最后一个线程到达屏障时,屏障才会开门,所有被屏障拦截的线程才会继续干活。
同步器的设计是基于模板方法模式的(这和我们以往通过实现接口的方式有很大区别)
如果需要自定义同步器一般的方式是这样:
自定义同步器时需要重写下面几个 AQS 提供的钩子方法:
protected boolean tryAcquire(int)//独占方式。尝试获取资源,成功则返回true,失败则返回false。
protected boolean tryRelease(int)//独占方式。尝试释放资源,成功则返回true,失败则返回false。
protected int tryAcquireShared(int)//共享方式。尝试获取资源。负数表示失败;0表示成功,但没有剩余可用资源;正数表示成功,且有剩余资源。
protected boolean tryReleaseShared(int)//共享方式。尝试释放资源,成功则返回true,失败则返回false。
protected boolean isHeldExclusively()//该线程是否正在独占资源。只有用到condition才需要去实现它。
synchronized
和ReentrantLock
等独占锁就是悲观锁思想的实现。CAS
算法实现。乐观锁适用于多读的应用类型,这样可以提高吞吐量,在Java中java.util.concurrent.atomic包下面的原子变量类就是使用了乐观锁的一种实现方式CAS
实现的。MySQL
乐观锁通过MVCC
版本实现。程序计数器私有主要是为了线程切换后能恢复到正确的执行位置。如果线程正在执行的是一个Java方法,这个计数器记录的是正在执行的虚拟机字节码指令的地址;如果执行的是本地(Native)方法,计数器的值为空(Undefined)。
虚拟机栈存储的都是局部变量,每个方法在执行时会创建一个栈帧,用于存储局部变量表、操作数栈、常量池引用等信息。变量一旦离开作用域就会被释放。栈内存的更新速度很快。
本地方法栈类似虚拟机栈,为虚拟机使用到的 Native 方法服务
堆:主要⽤于存放新创建的数组和对象,堆里的实体虽然不会被随时释放,但是会被当成垃圾,Java有垃圾回收机制不定时的收取。
方法区:主要⽤于存放已被加载的类信息、常量、静态变量、即时编译器编译后的代码等数据(方法区也称永久代,JVM设置其大小固定,而元空间使用直接内存,只受系统内存限制,也可根据运行时需求动态调整大小。)
直接内存:这不是虚拟机规范中定义的内存区域,但是这部分内存也被频繁地使用。新加入的 NIO(New Input/Output) 类,它可以直接使用 Native 函数库直接分配堆外内存,然后通过一个存储在 Java 堆中的 DirectByteBuffer 对象作为这块内存的引用进行操作。提高了性能
1)栈由系统自动分配,速度较快;而堆是人为申请开辟;一般速度比较慢
2)栈获得的空间较小,而堆获得的空间较大;
3)栈是连续的空间,而堆是不连续的空间。
public class Model {
public static int a = 1;
public int b;
public Model(int b) {
this.b = b;
}
}
public static void main(String[] args) {
int c = 10;
Model modela = new Model(2);
Model modelb = new Model(3);
}
Mark Word
:偏向锁的标识;通过cas操作竞争锁,如果竞争成功则操作Mark Word中线程ID设置为当前线程ID
metadata
:指向方法区中的类。JVM 默认开启指针压缩,一个指针用 4 字节表示
array length
:若当前对象不为数组,则对象头中不存在此项信息
填充字段:为满足 Java 对象所占内存必须为 8 字节的倍数
计算一个对象的大小:
参考
①java.lang.instrument.Instrumentation.getObjectSize()
此方法求出的值是一个近似值,并不准确
②java 中的 sun.misc.Unsafe类,有一个 objectFieldOffset(Field f) 方法,表示获取指定字段在所在实例中的起始地址偏移量。计算开头和结尾字段,就能知道该对象整体的大小。但只能是对象本身的大小,并没有计算对象中的引用类型所引用的对象的大小
程序计数器是唯一一个不会发生OutOfMemoryError的区域。
栈中异常:
堆中异常:
内存泄漏:
jps(jps -l)
可以查看运行的Java进程;jstack pid
查看JVM中运行线程的状态,可以定位CPU占用过高位置,定位死锁位置。jstat
”(jstat -gcutil 20954 1000)查看已使用空间站总空间的百分比,可以看到FGC的频次。--XX:+HeapDumpOnOutOfMemoryError,-XX:HeapDumpPath=/path/heap/dump
这样就是说OOM的时候自动导出一份内存快照,然后使用MAT打开刚刚导出的hprof文件,查看堆栈情况
补充: 线上出现OOM排查方法
String str1 = "str";
String str2 = "ing";
String str3 = "str" + "ing";//常量池中的对象
String str4 = str1 + str2; //在堆上创建的新的对象
String str5 = "string";//常量池中的对象
System.out.println(str3 == str4);//false
System.out.println(str3 == str5);//true
System.out.println(str4 == str5);//false
对于编译期可以确定值的字符串,也就是常量字符串 ,jvm 会将其存入字符串常量池(在堆中),避免字符串的重复创建。
String str3 = "str" + "ing";
编译器会给你优化成 String str3 = "string"
,创建str3
时会先在常量池中查找“string”
,所有两个实际时一样的String str4 = str1 + str2;
,实际上是通过 StringBuilder 调用 append() 方法,之后toString() 得到的,所以str4
并不是字符串常量池中存在的对象,属于堆上的新对象,所以不相等。str1
和str2
都被final
修饰,会被当做常量处理, 则str4 = str3;
String str2 = new String("abcd");
这种也是在堆中创建了新对象,不在常量池中Java 虚拟机的堆内存设置不够。
比如:可能存在内存泄漏问题;也很有可能就是堆的大小不合理,比如我们要处理比较可观的数据量,但是没有显式指定 JVM 堆大小或者指定数值偏小。我们可以通过参数 -Xms 、-Xmx 来调整。
代码中创建了大量大对象,并且长时间不能被垃圾收集器收集(存在被引用)。
在抛出 OutOfMemoryError 之前,通常垃圾收集器会被触发,尽其所能去清理出空间。比如,我们去分配一个超大对象,类似一个超大数组超过堆的最大值,JVM 可以判断出垃圾收集并不能解决这个问题,所以直接抛出 OutOfMemoryError。
内存泄漏
可达性分析一些引用链没有断开。比如,单例的生命周期和应用程序是一样长的,所以单例程序中,如果持有对外部对象的引用的话,那么这个外部对象是不能被回收的,则会导致内存泄漏的产生。
还有一些提供 close 的资源未关闭导致内存泄漏。数据库连接,网络连接(Socket)和 IO 连接必须手动 close,否则是不能被回收的。
Java堆:eden + survivor(from、to)+老年代
垃圾收集也主要在这个区域
minor GC
,清空Eden 区和"From"区,"From"和"To"会交换他们的角色,保证名为 To 的 Survivor 区域是空的。full GC
,会触发整个堆的回收。发生Full GC的原因:
①调用 System.gc()
只是建议虚拟机执行 Full GC,但是虚拟机不一定真正去执行。不建议使用这种方式,而是让虚拟机管理内存。
②老年代空间不足
老年代空间不足的常见场景为前文所讲的大对象直接进入老年代、长期存活的对象进入老年代等。
可以调大新生代的大小,让对象尽量在新生代被回收掉,不进入老年代。还可以调大对象进入老年代的年龄,让对象在新生代多存活一段时间。
③空间分配担保失败
GC Roots
的对象作为起点,从这些节点开始向下搜索,节点所走过的路径称为引用链,当一个对象到 GC Roots没有任何引用链相连的话,则证明此对象是不可用的。在可达性分析法中不可达的对象,也并非是“非死不可”的,要真正回收,至少要经历两次标记过程;
GC roots
强引用、软引用、弱引用、虚引用
软引用使用最多,可以加速 JVM 对垃圾内存的回收速度,可以维护系统的运行安全,防止内存溢出(OutOfMemory)等问题的产生。一般很少使用弱引用与虚引用
为一个对象设置虚引用的唯一目的是能在这个对象被回收时收到一个系统通知。
垃圾回收主要发生在堆中,方法区主要回收的是废弃常量和无用的类:
①废弃常量
②无用的类
(python使用引用计数
进行垃圾回收,使用标记清除
解决循环引用问题,用分代回收
提高效率)
CMS:老年代
CMS(Concurrent Mark Sweep)收集器是一种以获取最短回收停顿时间为目标的收集器。非常注重用户体验。它第一次实现了让垃圾收集线程与用户线程(基本上)同时工作。
是⼀种 “标记-清除”算法实现的,整个过程分为四个步骤:
缺点:
G1:新生代+老年代
把堆划分成多个大小相等的独立区域(Region),新生代和老年代不再物理隔离,每个小空间可以单独进行垃圾回收。通过预测每个 Region 垃圾回收时间
以及回收所获得的空间,并维护一个优先列表,优先回收价值最大的 Region。
每个 Region 都有一个 Remembered Set,用来记录该 Region 对象的引用对象所在的 Region。通过使用 Remembered Set,在做可达性分析的时候就可以避免全堆扫描。
具备如下特点:
G1
收集器会尽量满足。分类:
-XX:+某个属性
、-XX:-某个属性
,开启或关闭某个功能。比如-XX:+PrintGCDetails
,开启GC详细信息。-XX:属性key=值value
。比如-XX:Metaspace=128m
、-XX:MaxTenuringThreshold=15
。①使用jps -l
配合jinfo -flags pid
可以查看所有参数。
② -Xms
和-Xmx
-Xms
等价于-XX:InitialHeapSize
,用于设置初始堆大小,初始默认为物理内存的1/64。
-Xmx
等价于-XX:MaxHeapSize
。用于设置最大堆大小,最大默认为物理内存的1/4
③-Xss
等价于-XX:ThresholdStackSize
。用于设置单个栈的大小,系统默认值是0,不代表栈大小为0。而是根据操作系统的不同,有不同的值。
④-Xmn
,新生代大小,一般不调。
⑤-XX:MetaspaceSize
,设置元空间大小。
⑥-XX:+PrintGCDetails
,输出GC收集信息,包含GC
和Full GC
信息。
⑦-XX:SurvivorRatio
,新生代中,Eden
区和两个Survivor
区的比例,默认是8:1:1
。通过-XX:SurvivorRatio=4
改成4:1:1
⑧-XX:NewRatio,老生代和新年代的比列,默认是2,即老年代占2,新生代占1。如果改成-XX:NewRatio=4
,则老年代占4,新生代占1。
⑨-XX:MaxTenuringThreshold
,新生代设置进入老年代的时间,默认是新生代逃过15次GC后,进入老年代。如果改成0,那么对象不会在新生代分配,直接进入老年代。
Java文件经过编译后变成 .class 字节码文件,字节码文件通过类加载器被搬运到 JVM 虚拟机中
对象实例初始化时会去方法区中找类信息,在方法区中【类的方法表】中找相应方法。然后再到栈那里去运行方法。
clinit
方法的过程。是所有类变量的赋值动作和静态语句块(static块)中的语句合并产生类加载器ClassLoader
:
BootstrapClassLoader(启动类加载器) ,
ExtensionClassLoader(扩展类加载器) ,
AppClassLoader(应用程序类加载器)
ClassLoader 在协同工作的时候会默认使用 双亲委派模型 。即在类加载的时候:
双亲委派模型的好处:
缺点:
loadClass
方法;findClass
() 方法即可,无法被父类加载器加载的类最终会通过这个方法被加载。OSI
七层物理(比特流)、数据链路(封装成“帧”)、网络(IP,ARP/RARP,“包”)、传输(TCP\UDP,“段”)、会话、表示、应用(HTTP、FTP、DNS、SMTP)
端到端:传输层
点到点:数据链路层、网络层
交换机工作在数据链路层,将MAC地址和端口对应,MAC地址的数据包将仅送往其对应的端口,而不是所有的端口
路由器工作在网络层。
TCP
三次握手+四次挥手
三次握手的目的:
建立可靠的通信信道,双方确认自己与对方的发送与接收都是正常的。
四次挥手的目的:
任何一方都可以在数据传送结束后发出连接释放的通知,待对方确认后(ACK)进⼊半关闭状态。当另一方也没有数据再发送的时候,则发出连接释放通知(FIN),对方确认后就完全关闭了TCP连接。
CLOSE_WAIT
,再发出 FIN 信号TIME_WAIT
(2MSL)时间,约4分钟。主要是防止最后一个ACK丢失。TCP
TCP
:面向连接;可靠;字节流;效率慢;所需资源多;用于文件传输、发送和接收邮件、远程登录等
UDP
:无连接;不可靠;数据报文段;效率快(没有拥塞控制);所需资源少(首部开销小);用于即时通信、视频通话、直播等
TCP
如何保证传输可靠:流量控制(滑动窗口):
拥塞控制:
确认迟到:A给B发消息,B的确认消息丢失了,A超时重传,B丢弃重复消息,再给A发送确认
确认丢失:A给B发消息,限时内没收到确认,重传,A丢弃重复的确认,B丢弃重复的消息
TCP粘包:
TCP报文长度大于缓冲区–>拆包
TCP报文长度小于缓冲区–>粘包
解决粘包:
①特定分隔符 ②特殊的头 ③划分为等长
处理大量TIME_WAIT状态的连接
参考
ACK
:发送的时候 ACK 为 0,一旦接收端接收数据之后,就将 ACK 置为 1,发送端就知道了接收端已经接收了数据。SYN
:表示「同步序列号」,是 TCP 握手的发送的第一个数据包。当连接请求的时候,SYN=1,ACK=0;连接被响应的时候,SYN=1,ACK=1;可用于端口扫描,扫描者发送一个只有 SYN 的数据包,如果对方主机响应了一个数据包回来 ,就表明这台主机存在这个端口。FIN
:表示发送端已经达到数据末尾,也就是说双方的数据传送完成,连接将被断开。url
地址显示主页的过程DNS(获取IP)、TCP、IP(发送数据)、OSPF(IP 数据路由选择)、ARP(IP地址与MAC地址转换)、HTTP (访问网页)
一条域名的DNS记录会在本地有两种缓存:浏览器缓存和操作系统(OS)缓存。在浏览器中访问的时候,会优先访问浏览器缓存,如果未命中则访问OS缓存,最后再访问DNS服务器(一般是ISP提供),然后DNS服务器会递归式的查找域名记录,然后返回。
在修改hosts文件后,所有OS中DNS缓存会被清空,而浏览器缓存则不发生变化,并且浏览器缓存有过期时间,所以修改hosts文件之后,不一定会立刻生效。
递归查询:将待转换的域名放在 DNS 请求中,以 UDP 报文方式发给本地域名服务器。首先在本地域名服务器中查询 IP 地址,如果没有找到,本地域名服务器会向根域名服务器发送一个请求,如果根域名服务器也没找到,本地域名会向 com 顶级域名服务器发送一个请求,依次类推下去。直到最后本地域名服务器得到目标 IP 地址并把它缓存到本地,供下次查询使用。
URI(Uniform Resource Identifier)
是统一资源标志符,可以唯一标识一个资源。URL(Uniform Resource Locator)
是统一资源定位符,可以提供该资源的路径。它是一种具体的 URI,即 URL 可以用来标识一个资源,而且还指明了如何 locate 这个资源。URI 的作用像身份证号一样,URL 的作用更像家庭住址一样。URL 是一种具体的 URI,它不仅唯一标识资源,而且还提供了定位该资源的信息。
ping命令基于网络层的命令,是基于ICMP协议工作的。
DNS
,将ping后接的域名转换为ip地址。(DNS使用的传输层协议是UDP
)ARP
解析服务,由ip地址解析出MAC地址,以在数据链路层传输。ICMP
回显请求给目标主机,并等待ICMP回显应答。(ICMP用于在ip主机、路由器间传递网络是否通畅、主机是否可达等控制信息)3xx (5种)
301 Moved Permanently:永久性重定向,表示请求的资源被分配了新的URL,之后应使用更改的URL;
302 Found:临时性重定向,表示请求的资源被分配了新的URL,希望本次访问使用新的URL;
(301与302的区别:前者是永久移动,后者是临时移动(之后可能还会更改URL))
303 See Other:表示请求的资源被分配了新的URL,应使用GET方法定向获取请求的资源;
(302与303的区别:后者明确表示客户端应当采用GET方式获取资源)
304 Not Modified:表示客户端发送附带条件(是指采用GET方法的请求报文中包含if-Match、If-Modified-Since、If-None-Match、If-Range、If-Unmodified-Since中任一首部)的请求时,服务器端允许访问资源,但是请求为满足条件的情况下返回改状态码;
307 Temporary Redirect:临时重定向,与303有着相同的含义,307会遵照浏览器标准不会从POST变成GET;(不同浏览器可能会出现不同的情况);
4xx (4种)
5xx (2种)
HTTP
Connection:keep-alive
,在一定的时间内TCP不会关闭。一个 TCP 连接是可以发送多个 HTTP 请求的,但两个请求的生命周期不能重叠QUIC (Quick UDP Internet Connections), 快速 UDP 互联网连接。
QUIC是基于UDP
协议的。
加密:首次连接时客户端和服务端的会密钥协商,服务端传递了config包,客户端会将config存储下来,后续再连接时可以直接使用。
前向安全:指的是密钥泄漏也不会让之前加密的数据被泄漏,影响的只有当前,对之前的数据无影响。
前向纠错:QUIC每发送一组数据就对这组数据进行异或运算,并将结果作为一个FEC包发送出去,接收方收到这一组数据后根据数据包和FEC包即可进行校验和纠错。
80
,而HTTPS的URL由“https://”起始且默认使用端口443
。TLS/SSL
握手),在握手过程中将确立双方加密传输数据的密码信息。在HTTP报文进入TCP报文之前,先使用 TLS/SSL 对HTTP报文进行加密。数据加密采用对称加密,但对称加密的密钥用服务器方的证书进行了非对称加密。
对称加密:密钥只有一个,加密解密为同一个密码,且加解密速度快,典型的对称加密算法有 DES、AES 等;
非对称加密:密钥成对出现(且根据公钥无法推知私钥,根据私钥也无法推知公钥),加密解密使用不同密钥(公钥加密需要私钥解密,私钥加密需要公钥解密),相对对称加密速度较慢,典型的非对称加密算法有 RSA、DSA、MD5(签名算法) 等。
https:DES+RSA
HTTPS 经由 HTTP 进行通信,利用 TLS 来保证安全
加密过程:
服务器证书:
cookie
、session
、token
HTTP 是一种不保存状态,不对请求和响应之间的通信状态进行保存。Session 的主要作用就是通过服务端记录用户的状态,过时销毁。
通常 session 保存在服务端数据库或内存中,但服务器保存session开销过大,如果服务器做了负载均衡,下一个请求到了另外一台服务器的话,session会丢失。
通常通过在 Cookie 中附加一个 Session ID 来方式来跟踪用户。如果 cookie 被禁用,就加载 url 后面
Cookie 和 Session 都用来跟踪浏览器用户身份
Token
在 Cookie 中,下次登录的时候只需要根据 Token 值来查找用户即可(为了安全考虑,重新登录一般要将 Token 重写)cookie、session区别:
Token身份验证是无状态的,不需要存在服务端,一般浏览器本地存储。
服务器生成并发给用户一个token,包含了用户的数据,用密钥对数据做一个签名,把签名和数据一起作为token发给用户(这个token服务器不保存)。下次用户再访问服务端时,客户端每次请求都在HTTP header里面带上这个Token,服务器负责验证这个Token是不是合法的,有没有过期等;服务器用同样的密钥对数据计算签名,对比是否与token里的一致,如果相同就代表合法。(避免了攻击者通过 cookie 拿到用户的 session,进而伪造一些数据)
优点:
为什么用JWT?
项目里用到:
完成登录—>生成包含用户信息的字符串—>放到header请求头、或cookie、或url里传递—>设置有效时间(30min)
每次请求操作,判断如果登录—>检查是否包含token—>校验—>确定登陆状态
PUT与PATCH:
CPU 从逻辑上可以划分成3个模块,分别是控制单元、运算单元和存储单元
一些与硬件关联交紧密的模块,诸如时钟管理程序、中断处理程序、设备驱动程序等处于最底层。其次是运行频率较高的程序,诸如进程管理、存储器管理和设备管理等。这两部分内容构成了操作系统的内核。
大多数操作系统内核包括四个方面的内容。
中断(会发生用户态与内核态切换)
系统调用与函数调用
发生中断或系统调用可能会引起用户态转向核心态,所使用的堆栈也可能需要由用户堆栈切换为系统堆栈。
每个进程会有两个栈,一个用户栈,存在于用户空间,一个内核栈,存 在于内核空间。当进程在用户空间运行时,cpu堆栈指针寄存器里面的内容是用户堆栈地址,使用用户栈。
创建进程首先要将程序和数据装入内存。将用户原程序变成可在内存中执行的程序,通常需要三个步骤。
链接:
比如,打印的库函数,可能很多程序都要用到,静态链接开销太大,要用动态链接,运行的时候再链接
装入:
分页机制和分段机制
内存管理单元(MMU)管理着地址空间和物理内存的转换,其中的页表(Page table)存储着页(程序地址空间)和页框(物理内存空间)的映射表。
一个虚拟地址分成两个部分,一部分存储页面号,一部分存储偏移量。
页表:
系统为每个进程建立一张页表,记录页面在内存中对应的物理块号,页表一般存放在内存中。
地址变换:
将逻辑地址中的页号,转换为内存中物理块号。地址分为页号和页内偏移量两部分,用页号去检索页表。在系统中通常设置一个页表寄存器PTR,存放页表在内存的初值和页表长度。
快表:
若页表全部放在内存中,则要存取一个数据至少要访问两次内存,一次是访问页表得到物理地址,第二次才根据该地址存取数据或指令。
地址变换机构中增设了一个具有并行查找能力的高速缓冲存储器——快表,又称联想寄存器TLB,用以存放当前访问的若干页表项。与此对应,主存中的页表也常称为慢表。
将虚拟地址转化后,送入块表查找页号,如果存在,直接找到对应地址;如果不存在,再去内存中查找页表并存在快表里
虚拟内存的基本单位是页。当访问虚拟内存时,会通过MMU(内存管理单元)去匹配对应的物理地址,而如果虚拟内存的页并不存在于物理内存中,会产生缺页中断,从磁盘中取出缺的页放入内存,如果内存已满,还会根据某种置换算法将磁盘中淘汰一页,腾出空间。
当用户程序要访问的部分尚未调入内存,则产生中断。
页面置换算法:
FIFO先进先出算法、LRU最近最少使用算法、LFU最少使用次数算法、OPT最佳置换算法(保证置换出去的是未来不再或最晚被使用的页)
普通文件(-)、目录文件(d,directory file)、 符号链接文件(l,symbolic link,软链接,类似快捷方式)、 字符设备(c,char,访问字符设备比如键盘) 、 设备文件(b,block,访问块设备比如硬盘、软盘)、 管道文件(p,pipe)、 套接字(s,socket)
cd usr
: 切换到该目录下 usr 目录
cd ..
: 切换到上一层目录
cd /
: 切换到系统根目录
cd ~
: 切换到用户主目录
cd -
: 切换到上一个操作所在目录
mkdir 目录名称
: 增加目录。
ls/ll
:查看目录信息。
(ll 命令可以看到该目录下的所有目录和文件的详细信息):
find 目录 参数
: 寻找目录(查)。
示例:① 列出当前目录及子目录下所有文件和文件夹: find .;② 在/home目录下查找以.txt 结尾的文件名:find /home -name “*.txt” ,
mv 目录名称 新目录名称
: 修改目录的名称(改)。
mv 目录名称 目录的新位置
: 移动目录的位置—剪切(改),cp 是复制
cp -r 目录名称 目录拷贝的目标位置
: 拷贝目录(改),-r 代表递归拷贝 。
rm [-rf] 目录
: 删除目录(删)。
touch 文件名称
: 文件的创建(增)。
find 目录 -name 文件名
:文件查找,find /etc -name zhangsan,还可以-size,-group
head/cat/more/less/tail 文件名称
:文件的查看(查) 。head -n 文件名,显示文件的前n行内容。cat 显示文件全部内容。 tail -f 文件 可以对某个文件进行动态监控,tail -f 文件名。
vim 文件
: vim 文件—>进入文件—>命令模式—>按 i 进入编辑模式—>编辑文件 —>按Esc进入底行模式—>输入:wq/q! (输入 wq 代表写入内容并退出,即保存;输入 q!代表强制退出不保存)。
rm -rf 文件
: 删除文件(删)
tar -zcvf 打包压缩后的文件名 要打包压缩的文件或目录名
:打包并压缩文件,tar -zcvf test.tar.gz aaa.txt bbb.txt ccc.txt 或 tar -zcvf test.tar.gz /test/
tar [-xvf] 压缩文件
:解压压缩包,tar -xvf test.tar.gz -C /usr
每个用户必须属于一个组,每个文件有所有者(u)、所在组(g)、其它组(o)的概念
修改文件/目录的权限的命令:chmod
chmod u=rwx,g=rw,o=r aaa.txt
或者 chmod 764 aaa.txt
Linux 系统是一个多用户多任务的分时操作系统,任何一个要使用系统资源的用户,都必须首先向系统管理员申请一个账号,然后以这个账号的身份进入系统。每个用户都有一个用户组
useradd 选项 用户名
:添加用户账号
userdel 选项 用户名
:删除用户帐号
usermod 选项 用户名
:修改帐号
passwd 用户名
:更改或创建用户的密码
passwd -S 用户名
:显示用户账号密码信息
passwd -d 用户名
: 清除用户密码
groupadd 选项 用户组
:增加一个新的用户组
groupdel 用户组
:要删除一个已有的用户组
groupmod 选项 用户组
: 修改用户组的属性
pwd
: 显示当前所在位置
sudo + 其他命令
:以系统管理者的身份执行指令
网络通信命令
: ifconfig 查看当前系统的网卡信息:ping查看与某台机器的连接情况: netstat -an查看当前系统的端口使用:
reboot
: 重开机
grep 要搜索的字符串 要搜索的文件 --color
: 搜索命令,–color 代表高亮显示
ps -ef/ps -aux
: 这两个命令都是查看当前系统正在运行进程。如果想要查看特定的进程:ps -ef|grep redis
。查看进程并排序:ps auxw --sort=%cpu
,%cpu按占用cpu排序,%mem按内存占用率,后面可以ps auxw --sort=%cpu | java
,然后 top 所显示的进程名
kill -9 进程的pid
: 杀死进程(-9 表示强制终止。)
CTRL+Z
:当前程序挂起进程并放入后台(暂停)。bg %N 使第N个任务在后台运行,fg %N 使第N个任务在前台运行,没有 N 默认最后一个
ctrl-c
:终止程序的执行;等于kill -2
,在结束之前,能够保存相关数据,然后再退出。
top
:命令主要用于查看进程的相关信息,cpu 信息和内存信息等。虚拟内存(预计用量)、常驻内存(实际用量)、共享内存、物理内存(常驻内存减去共享内存)
free
:命令显示系统内存的使用情况,包括物理内存、交换内存(swap)和内核缓冲区内存。(swap页面置换的时候才用,一般都是0)
tail -f
:在控制台上查看日志,他可以将新增的日志实时的打印出来
tail -n 行数
:这个展示的是文件最后一行倒数的行数,
cat -n
:根据条件筛选出行号
sed -n '行1,行2p'
:常常需要查询这条记录的完整信息,这个时候可以使用
第一范式(1NF):(属性不可分)
第二范式(2NF):
第三范式(3NF):
数据类型:int
bigint
float
double
char
varchar
日期
默认存储引擎:InnoDB
(之前为MyISAM
)
支持事务、支持外键,支持行级锁,支持MVCC,有崩溃修复(redo log)
行级锁:
悲观锁,对当前操作加锁,能减少数据冲突,但开销大,会出现死锁。锁的是索引,分为共享锁与排他锁
InnoDB 存储引擎的锁的算法:
因为要是分成多个表之后,数据遍布在不同服务器上的数据库,我们需要一个全局唯一的 id来支持。生成全局 id 有下面这几种方式:
开启 MySQL 慢查询日志功能,并设置时间,可以定位具体sql:
mysql> SET GLOBAL slow_query_log=ON;
Query OK, 0 rows affected (0.05 sec)
mysql> SET GLOBAL long_query_time=0.001;
Query OK, 0 rows affected (0.00 sec)
利用explain
关键字可以模拟优化器执行SQL查询语句
+----+-------------+------------+-------+---------------------------------+-----------------------+---------+-------------------+-------+--------------------------------+
| id | select_type | table | type | possible_keys | key | key_len | ref | rows | Extra |
+----+-------------+------------+-------+---------------------------------+-----------------------+---------+-------------------+-------+--------------------------------+
| 1 | PRIMARY | cl | range | cm_log_cls_id,idx_last_upd_date | idx_last_upd_date | 8 | NULL | 379 | Using where; Using temporary |
| 1 | PRIMARY | <derived2> | ALL | NULL | NULL | NULL | NULL | 63727 | Using where; Using join buffer |
| 2 | DERIVED | emp | ALL | NULL | NULL | NULL | NULL | 13317 | Using where |
| 2 | DERIVED | emp_cert | ref | emp_certificate_empid | emp_certificate_empid | 4 | meituanorg.emp.id | 1 | Using index |
+----+-------------+------------+-------+---------------------------------+-----------------------+---------+-------------------+-------+--------------------------------+
table | type | possible_keys | key |key_len | ref | rows | Extra :
优化:
select id,title from collect limit 90000,10;
select id,title from collect where id>=(select id from collect order by id limit 90000,1) limit 10;
inner join
。让MySQL扫描尽可能少的页面,获取需要的记录后,再根据关联列,回原表查询需要的所有列。如果这个表非常大,那么这个查询可以改写成如下的方式:Select news.id, news.description from news inner join (select id from news order by title limit 50000,5) as myNew using(id);
MySQL
事务的ACIDACID
:ACID
实现原理:bin log
(归档日志)、redo log
和undo log
:redolog
保证InnoDB 的持久性,有数据崩溃恢复的能力;redo log 它是物理日志,记录内容是“在某个数据页上做了什么修改”。如果只修改了页面上一点内容,但把整个页面刷盘,是不合理的,所以用redo log。redo log体积小,只记录了哪一页修改了啥,因此体积小,刷盘快。并且 redo log是一直往末尾进行追加,属于顺序IO。效率显然比刷盘随机IO来的快。redolog作为异常宕机或者介质故障后的数据恢复使用。
binlog
保证了MySQL集群架构的数据一致性;binlog 是逻辑日志,记录是对应的SQL语句,属于MySQL Server 层
。 不管用什么存储引擎,只要发生了表数据更新,都会产生 binlog 日志。 MySQL数据库的数据备份、主备、主主、主从都离不开binlog,需要依靠binlog来同步数据,保证数据一致性。
bin log 和 redo log 都保证了数据的持久化;redo log在事务执行过程中可以不断写入,属于InnoDB引擎层;而binlog只有在提交事务时才写入,属于server 层。
为了解决两份日志之间的一致问题,InnoDB存储引擎使用两阶段提交方案。原本的 redo --> bin , 变成redo(prepare) --> bin -->redo(commit)
binlog不是循环使用,在写满或者重启之后,会生成新的binlog文件,redolog是循环使用。
undo
回滚行记录到特定版本,保证事务的原子性。回滚日志会先于数据持久化到磁盘上
MySQL InnoDB
存储引擎的默认⽀持的隔离级别是 REPEATABLE-READ
(可重读)
如果执行的是下列语句,就是 锁定读:
select ... lock in share mode
(加 s 锁,其它事务也可以加 s 锁,如果加 x 锁则会被阻塞)
select ... for update
insert
、update
、delete
(加 x 锁,其它事务不能加锁)
每次读取的是数据的最新版本。会对当前读取的数据进行加锁,防止其他事务修改数据。是悲观锁的一种操作。如果两次查询中间有其它事务插入数据,就会产生幻读
(Multi-Version Concurrency Control
,多版本并发控制)
含义:为事务分配单向增长的时间戳。为每个数据修改保存一个版本。读操作只读取该事务开始前的数据库快照,使读写操作没有冲突
并发读-写时,可以做到读操作不阻塞写操作,同时写操作也不会阻塞读操作。解决脏读、幻读、不可重复读等事务隔离问题,但不能解决上面的写-写 更新丢失问题,还需添加乐观锁/悲观锁。
实现:隐藏字段、Read View
、undo log
隐藏字段:
①DB_TRX_ID
:表示最后一次插入或更新该行的事务 id。
②DB_ROLL_PTR
: 回滚指针,指向该行的 undo log
③DB_ROW_ID
:如果没有设置主键且该表没有唯一非空索引时,会使用该 id 来生成聚簇索引
Undo log
主要用于记录数据被修改之前的日志,在表信息修改之前先会把数据拷贝到undo log里。
①当事务回滚时用于将数据恢复到修改前的样子,保证原子性和一致性
②MVCC ,当读取记录时,若该记录被其他事务占用或当前版本对该事务不可见,则可以通过 undo log 读取之前的版本数据,以此实现非锁定读
Read View
主要是用来做可见性判断的, 创建一个新事务时,copy一份当前系统中的活跃事务列表。意思是,当前不应该被本事务看到的其他活跃未提交事务id列表。即当我们某个事务执行快照读的时候,对该记录创建一个Read View读视图,需要判断当前事务能够看到哪个版本的数据,既可能是当前最新的数据,也有可能是该行记录的undo log里面的某个版本的数据。
查询语句:权限校验—>分析器—>优化器—>权限校验—>执行器—>引擎
更新语句:查询—>执行器—>引擎—>redo log(prepare 状态)—>binlog—>redo log(commit状态)
在innodb层,prepare redo log中会记录一个trxid,宕机重新起来恢复时
建立索引的目的是:希望通过索引进行数据查找,减少随机 IO,增加查询性能 ,索引能过滤出越少的数据,则从磁盘中读入的数据也就越少。
-哈希索引底层的数据结构就是哈希表,因此在绝⼤多数需求为单条记录查询的时候,可以选择哈希索引,查询性能最快,但不可排序,只能精确查找,无法部分查找;
InnoDB 和 MyISAM 都不支持 hash 索引
⼤部分场景选择B+Tree索引,是B Tree(多路平衡查找树)的变种。对于主要的两种存储引擎的实现⽅式是不同的。
聚簇索引:
非聚簇索引:
比如:
log n
。 B+树只存索引,一页能存更多的索引,IO次数更少b+树一般只有1~3层:
1、要在where条件中(要有条件查询)
2、如果where条件中是OR
关系,加索引不起作用
3、like
“%aaa%” 不会使用索引;而like “aaa%”可以使用索引;
4、NOT IN
不会使用索引;NOT IN可以NOT EXISTS代替
5、索引列参与计算,使用了函数,会导致失效
6、符合最左原则。对于索引 (a,b,c),(b,c) 不起作用,但 (a,c)(a,c)(b,a,c)都可以走联合索引
7、当一个表有多条索引可走时, Mysql 根据查询语句的成本来选择走哪条索引, 可以用explain
预估,联合索引的话, 它往往计算的是最左边字段, 这样往往会走错索引。比如(b,a)索引和 a 索引,b 比 a 费时,很可能就走 a 索引了而不走联合索引了,就会降低效率,应当把(b,a)改为(a,b)
联合索引多种情况参考
a = 1 and b = 2 and c > 3 and d = 4
如果建立(a,b,c,d)顺序的索引,d是用不到索引的,如果建立(a,b,d,c)的索引则都可以用到,a,b,d的顺序可以任意调整。A>5
;A=5 AND B>6
;A=5 AND B=6 AND C=7
;A=5 AND B IN (2,3) AND C>5
B>5
;B=6 AND C=7
——查询条件不包含组合索引首列字段A=5 AND B>6 AND C=2
——遇到大于停止,C没有用到索引ORDER BY A
——首列排序A=5 ORDER BY B
——第一列过滤后第二列排序ORDER BY A DESC, B DESC
——注意,此时两列以相同顺序排序A>5 ORDER BY A
——数据检索和排序都在第一列ORDER BY B
——排序在索引的第二列A>5 ORDER BY B
——范围查询在第一列,排序在第二列A IN(1,2) ORDER BY B
——理由同上ORDER BY A ASC, B DESC
——注意,此时两列以不同顺序排序添加 PRIMARY KEY(主键索引)、UNIQUE(唯一索引)、INDEX(普通索引) 、
FULLTEXT(全文索引)
ALTER TABLE `table_name` ADD PRIMARY KEY ( `column` )
多列索引
ALTER TABLE `table_name` ADD INDEX index_name ( `column1`, `column2`, `column3` )
联合索引:(a,b.c)只要有a,不管(c,b,a)(b,c,a)(a,c)都会自动优化,可以使用到联合索引!b、bc、c、cb不能用到
为什么有了平衡树还需要红黑树?
虽然平衡树解决了二叉查找树退化为近似链表的缺点,能够把查找时间控制在 O(logn),不过却不是最佳的,因为平衡树要求每个节点的左子树和右子树的高度差至多等于1,这个要求实在是太严了,导致每次进行插入/删除节点的时候,几乎都会破坏平衡树的第二个规则,进而我们都需要通过左旋和右旋来进行调整,使之再次成为一颗符合要求的平衡树,这会使平衡树的性能大打折扣,
是一种自平衡的二叉查找树,在O(log n)时间内做查找,插入和删除。
为解决二叉查找树(BST)多次插入新节点导致不平衡,可能会退化成一个线性结构。
为什么有了平衡树还需要红黑树?
虽然平衡树解决了二叉查找树退化为近似链表的缺点,能够把查找时间控制在 O(logn),不过却不是最佳的,因为平衡树要求每个节点的左子树和右子树的高度差至多等于1,这个要求实在是太严了,导致每次进行插入/删除节点的时候,几乎都会破坏平衡树的第二个规则,进而我们都需要通过左旋和右旋来进行调整,使之再次成为一颗符合要求的平衡树,这会使平衡树的性能大打折扣
特点:
2-3树与红黑树:我们将3-结点表示为由一条左斜的红色链接相连的两个2-结点。红链接的头看作红节点,尾看作黑节点。
调整:变色+旋转(左旋/右旋)
变色:
左旋:
右旋:
AVL树:严格平衡的二叉树,要求每个节点左右子树高度不超过1,(红黑树只说路径长度不超过2倍),频繁查找使用AVL,频繁插入删除使用红黑树。
log n
。而B 树查询时间复杂度不固定,与 key 在树中的位置有关,最好为O(1)。MySQL
与Redis
:MySQL
是关系型数据库,主要用于存放持久化数据,将数据存储在硬盘中,读取速度较慢,用于持久化的存储数据到硬盘Redis
是NOSQL,即非关系型数据库,也是内存数据库,即将数据存储在内存中,读写速度非常快,被广泛应用于缓存方向,也经常用来做分布式锁,甚至是消息队列。Redis
的好处:高性能+高并发
消息队列
:Redis 自带的 list 或 Stream 数据结构可以作为一个简单的队列使用。分布式锁:
条件:
①在数据库中创建一个表,表中包含方法名等字段,并在方法名字段上创建唯一索引,想要执行某个方法,就使用这个方法名向表中插入数据,成功插入则获取锁,执行完成后删除对应的行数据释放锁。
②redis,使用SETNX key val加锁,value值为一个随机生成的UUID,并使用expire命令超时自动释放锁,释放锁的时候通过UUID判断是不是自己的锁,并执行delete进行锁释放。
③Redisson框架。客户端A要加锁,在redis集群中,用hash算法选择一台机器,检查锁是否存在,不存在就加锁,hset myLock
,会出现如下数据结构,表示“AAA”
这个客户端已经对“myLock”
这个锁Key完成了加锁,然后设置过期时间。(watch dog机制每隔一段时间查看,如果A还持有锁,就自动延长key的生存时间)
myLock
{
“AAA”:1
}
如果锁已经存在,就检查可重入,把“AAA”:1
变为“AAA”:2
,unlock的时候,锁次数减一,最后delete。
Redisson缺点:主从架构时,锁在主从之间复制是异步的,如果主突然宕机,可能会有别的客户端在新的主机上加锁成功
解决:利用RedLock——超过半数的Redis节点加锁成功才算成功
常用命令:
set、get、mset、getm(批量)、strlen(长度)、
exists、decr、incr、expire(过期)、ttl(查看数据还有多久过期)
常用命令: rpush、rpop、lpush、lpop、lrange(对应下标范围的列表元素)、llen(链表长度) 等。
应用场景: 发布与订阅或者说消息队列、慢查询
类似HashMap,内部实现也差不多(数组 + 链表),适合用于存储对象。
常用命令:
hset、hmset、hexists、hget、hgetall、hkeys、hvals 等。
应用场景: 系统中对象数据的存储。
压缩列表实际上类似于一个数组,压缩列表在表头有三个字段 zlbytes、zltail 和 zllen,分别表示列表长度、列表尾的偏移量和列表中的 entry 个数;压缩列表在表尾还有一个 zlend,表示列表结束。所以查找头尾很快。时间复杂度 o(N)
跳表在链表的基础上,增加了多级索引,通过索引位置一次性多级跳转,实现数据的快速定位。时间复杂度 o(logN)
常用命令: setbit 、getbit 、bitcount(被被设置为 1 的位的数量)、bitop
BITOP
:BITOP operation destkey key [key ...]
,对保存二进制位的字符串 key 进行位元操作,并将结果保存到 destkey 上。支持 AND 、 OR 、 NOT 、 XOR
应用场景: 适合需要保存状态信息(比如是否签到、是否登录…)并需要进一步对这些信息进行分析的场景。比如用户签到情况、活跃用户情况、用户行为统计(比如是否点赞过某个视频)
eg:统计活跃用户
使用时间作为 key,然后用户 ID 为 offset,如果当日活跃过就设置为 1
初始化数据:
统计 20210308~20210309 两天都在线的用户:
统计 20210308~20210309 至少一天在线的用户:
Redis为什么快:
为什么单线程:
select
/ poll
/ epoll
主要是为了故障备份
RDB
)默认方式:save 900 1
:在900秒后,如果至少有1个key发生变化,则创建快照。AOF
)实时性更好:appendonly yes
:开启AOFappendfsync always/everysec/no
:每次有数据发生修改,先写入缓存,然后根据配置【每次有数据修改发生时 / 每秒 / 让操作系统决定】,写入AOF文件。Redis采用的是定期删除和惰性删除策略
MULTI
。EXEC
)。DISCARD
命令取消一个事务,它会清空事务队列中保存的所有命令。WATCH
,监听指定的键,当调用 EXEC 执行事务时,如果一个被 WATCH 命令监视的键被修改的话,整个事务都不会执行,直接返回失败。问题:针对缓存中没有但数据库有的数据。当Key失效后,假如瞬间突然涌入大量的请求,来请求同一个Key,这些请求不会命中Redis,都会请求到DB,导致数据库压力过大。
解决:
问题:针对数据库和缓存中都没有的数据,如果从数据库查不到则不写入缓存,这将导致这个不存在的数据每次请求都要到存储层去查询,失去了缓存的意义,数据库容易崩溃
解决:
布隆过滤器:
bitmap
缓存在同一时间大面积的失效,后面的请求都直接落到了数据库上,造成数据库短时间内承受大量请求。比如系统缓存模块崩溃;热点数据大面积失效
解决:
旁路缓存协议:先更新完数据库,然后直接删除缓存
问题:仍会有时间差导致不一致
解决:①更新数据库和删除缓存操作加锁 ②给缓存设置一个比较短的过期时间
集群
Redis Sentinel
着眼于高可用,在master宕机时会自动将slave提升为master,继续提供服务。Redis Cluster
着眼于扩展性,在单个redis内存不足时,使用Cluster进行分片存储。重复购买:数据库加唯一索引防止用户重复购买
超卖:
from >> on>> join >> where >> group by >>聚合函数(min, max)>> having >> select >> distinct >> order by >> limit
select d.name 'Department',e.name 'Employee',e.salary 'Salary'
from Employee as e,Department as d
where e.DepartmentId=d.id
and (e.Salary,e.DepartmentId) in
(select max(Salary),DepartmentId from Employee group by DepartmentId)
select d.Name as Department,e1.Name as Employee,e1.Salary as Salary
from Employee as e1
left join Department as d
on e1.DepartmentId=d.id
where 3>(
select count(distinct e2.salary)
from Employee e2
where e1.DepartmentId = e2.DepartmentId
and e1.salary<e2.salary
)
es提供了基于HTTP协议的RESTful APIS
,也就是说我们可以通过向es服务器发送curl HTTP请求来操作es服务器,如对文档读写、查询文档API、搜索API、索引的创建与删除,es默认使用9200端口接收HTTP请求
# 使用curl调用es,创建一个文档
curl http://localhost:9200/my_test/1 -H "Content-Type:application/json" \
-X POST -d '{"uid":1,"username":"test"}'
es是面向文档的,文档是es中可搜索的最小单位,每个文档都有唯一的id
,这个id可以由我们自己指定,也可以由es自动生成。
es的文档由一个或多个字段组成,类似于关系型数据库中的一行记录,但es的文档是以JSON
进行序列化并保存的,每个JSON对象由一个或多个字段组成,字段类型可以是布尔,数值,字符串、二进制、日期等数据类型。
PUT /megacorp/employee/1
{
"name" : "John",
"sex" : "Male",
"age" : 25,
"about" : "I love to go rock climbing",
"interests": [ "sports", "music" ]
}
往Elasticsearch里插入一条记录,其实就是直接PUT
一个json的对象,这个对象有多个fields,比如上面例子中的name, sex, age, about, interests,那么在插入的同时,Elasticsearch还为这些字段建立索引–倒排索引
,(为了提高搜索的性能,难免会牺牲某些其他方面,比如插入/更新,)
ES索引原理
【文档】:
ID是Elasticsearch自建的文档id
【Posting List】:
Kate, John, 24, Female这些叫term
,而[1,2]就是Posting List
,就是一个int的数组,存储了所有符合某个term的文档id
。
【Term Dictionary】:
Elasticsearch为了能快速找到某个term,将所有的term排个序,二分法查找term,logN的查找效率,就像通过字典查找一样,这就是Term Dictionary。
【Term Index】:
B-Tree通过减少磁盘寻道次数来提高查询性能,Elasticsearch也是采用同样的思路,于是有了Term Index,就像字典里的索引页一样,A开头的有哪些term,分别在哪页,这棵树不会包含所有的term,它包含的是term的一些前缀。通过term index
可以快速地定位到term dictionary
的某个offset
,然后从这个位置再往后顺序查找。
term index不需要存下所有的term,而仅仅是他们的一些【前缀】与【Term Dictionary的block】之间的映射关系,再结合FST(Finite State Transducers)的压缩技术,可以使term index
缓存到内存中。从term index查到对应的term dictionary的block位置之后,再去磁盘上找term,大大减少了磁盘随机读的次数。
IoC(Inverse of Control:控制反转) 和 AOP(Aspect-Oriented Programming:面向切面编程)
IoC 的思想就是将原本在程序中手动创建对象的控制权,交由 Spring 框架来管理,IoC的实现方式是依赖注入(Dependency Injection, DI)。当我们需要创建一个对象的时候,只需要配置好配置文件/注解,在需要的时候引入即可,完全不用考虑对象是如何被创建出来的,不需要考虑底层复杂的构造函数。
IoC 容器实际上就是个 Map(key,value),Map 中存放的是各种对象。
Spring 框架的核心是 Spring 容器。容器创建对象,将它们装配在一起,配置它们并管理它们的完整生命周期。Spring 容器使用依赖注入来管理组成应用程序的组件。容器通过读取提供的配置元数据来接收对象进行实例化,配置和组装的指令。该元数据可以通过 XML,Java 注解或 Java 代码提供。
依赖注入:
在依赖注入中,不必创建对象,但必须描述如何创建它们。不是直接在代码中将组件和服务连接在一起,而是描述配置文件中哪些组件需要哪些服务。由 IoC 容器将它们装配在一起。
IOC核心容器的两个接口:BeanFactory
和ApplicationContext
ApplicationContext
创建容器,容器创建对象采取的策略是立即加载,也就是说只要读完配置文件,马上创建配置文件中的配置的对象(适用于单例对象)BeanFactory
:在创建核心容器时,创建的对象采取的策略是延迟加载的方式。什么时候用到对象什么时候再创建对象。不支持基于依赖的注解。AOP:面向切面编程。作为面向对象的一种补充,能够将那些与业务无关,却为业务模块所共同调用的逻辑或责任(例如事务处理、日志管理、权限控制等)封装起来,便于减少系统的重复代码,降低模块间的耦合度,并有利于未来的可拓展性和可维护性。依赖代理。通过代理原始类增加额外功能,我们可以将额外功能一次定义,然后配合切点达到多次使用的效果
如果要代理的对象,实现了某个接口,那么 Spring AOP 会使用 JDK Proxy,去创建代理对象。通过反射机制调用目标类的代码,动态将横切逻辑和业务逻辑编织在一起。 Proxy 类利用 InvocationHandler 接口,动态创建一个符合某一接口的实例,生成目标类的代理对象。
而对于没有实现接口的对象, 会使用 Cglib 生成一个被代理对象的子类来作为代理。CGLIB 是通过继承的方式做的动态代理,因此如果某个类被标记为 final ,那么它是无法使用 CGLIB 做动态代理的。
简单写一个aop切面:
参考
基于AspectJ注解配置AOP
@Controller
@Aspect//标注当前类为切面
public class MyLoggerAspect {
//@Before 将方法指定为前置通知
//必须设置value,其值为切入点表达式
@Before(value = "execution(public int com.atguigu.spring.day03_Spring.aop.MathImpl.add(int,int))")
public void beforeMethod(){
System.out.println("方法执行之前!");
}
}
bean 代指的被 IoC 容器所管理的对象。
创建Bean对象的方式:
1、使用默认构造函数创建
2、使用普通工厂方法创建(类似@bean注解那样的形式)
3、使用工厂中的静态方法创建
Spring 启动时:
我们一般使用 @Autowired
注解自动装配 bean,要想把类标识成可用于 @Autowired 注解自动装配的 bean 的类,采用以下注解可实现:
@Component
:通用的注解,可标注任意类为 Spring 组件。如果一个 Bean 不知道属于哪个层,可以使用@Component 注解标注。@Repository
: 对应持久层即 Dao 层,主要用于数据库相关操作。@Service
: 对应服务层,主要涉及一些复杂的逻辑,需要用到 Dao 层。@Controller
: 对应 Spring MVC 控制层,主要用户接受用户请求并调用 Service 层返回数据给前端页面。@Component和@bean的区别是什么
Spring中出现同名bean怎么办:
实例化–>属性赋值–>初始化–>销毁
当通过Spring容器创建一个Bean实例时,不仅可以完成Bean实例的实例化,还可以为Bean指定特定的作用域
当 bean 在 Spring 容器中组合在一起时, Spring 容器需要知道需要什么 bean 以及容器,应该如何使用依赖注入,来将 bean 绑定在一起,同时装配bean。
Spring 容器能够自动装配 bean。也就是说,可以通过检查 BeanFactory 的内容让 Spring 自动解析 bean 的协作者。
自动装配的不同模式:
自动装配有如下局限性:
当多个用户同时请求一个服务时,容器会给每一个请求分配一个线程,这时多个线程会并发执行该方法,如果该方法中有对单例状态的修改(体现为该单例的成员属性),则必须考虑线程同步问题。
若每个线程中对全局变量、静态变量只有读操作,而无写操作,一般来说,这个全局变量是线程安全的;若有多个线程同时执行写操作,一般都需要考虑线程同步,否则就可能影响线程安全。
@component,@repository @controller @service @configration @bean 注册类
@configuration 作为配置类,替代xml配置文件
@autowired默认是按照类型装配注入的,默认情况下要求依赖对象必须存在。@resource 默认按照名称进行装配,名称 可以通过name属性进行指定。
声明bean的注解
@Component 组件,没有明确的角色
@Service 在业务逻辑层使用(service层)
@Repository 在数据访问层使用(dao层)
@Controller 在展现层使用,控制器的声明(C)
注入bean的注解
@Autowired:由Spring提供
@Configuration 声明当前类为配置类,相当于xml形式的Spring配置(类上)
@Bean 注解在方法上,声明当前方法的返回值为一个bean,替代xml中的方式(方法上)
@Configuration 声明当前类为配置类,其中内部组合了@Component注解,表明这个类是一个bean(类上)
@ComponentScan 用于对Component进行扫描,相当于xml中的(类上)
SpringMVC部分
@EnableWebMvc 在配置类中开启Web MVC的配置支持,如一些ViewResolver或者MessageConverter等,若无此句,重写WebMvcConfigurerAdapter方法(用于对SpringMVC的配置)。
@Controller 声明该类为SpringMVC中的Controller
@RequestMapping 用于映射Web请求,包括访问路径和参数(类或方法上)
@ResponseBody 支持将返回值放在response内,而不是一个页面,通常用户返回json数据(返回值旁或方法上)
@RequestBody 允许request的参数在request体中,而不是在直接连接在地址后面。(放在参数前)
@PathVariable 用于接收路径参数,比如@RequestMapping(“/hello/{name}”)申明的路径,将注解放在参数中前,即可获取该值,通常作为Restful的接口实现方法。
@RestController 该注解为一个组合注解,相当于@Controller和@ResponseBody的组合,注解在类上,意味着,该Controller的所有方法都默认加上了@ResponseBody。
构造器和非单例的循环依赖:无法解决
单例模式下的setter循环依赖解决方法:三级缓存
1、singletonObjects(一级缓存):存放完全初始化好的bean,可以直接使用
2、earlySingletonObjects(二级缓存):存放尚未填充属性的bean
3、singletonFactories(三级缓存):存放bean工厂对象
三级缓存
中一级缓存
中寻找,没有找到(在这个步骤还有个判断对象是否正在创建,若是正在创建,基本是循环依赖),②在二级缓存
中找如果没有找到,③在三级缓存
中找,没有就实例化一个 B 对象,存放在三级缓存
中三级缓存
中发现有 A 对象,就把其提升到二级缓存
中,删除三级缓存
中的 A 对象,一级缓存
中,其余的缓存中删除该对象,由于 B 对象创建完成,返回注入到 A 对象中,将 A 对象保存在一级缓存
中,删除二级缓存中的对象管理事务的方式:
@Transactional
),实际是通过 AOP 实现。其本质是对方法前后进行拦截,然后在目标方法开始之前创建或者加入一个事务,执行完目标方法之后根据执行的情况提交或者回滚。唯一不足的地方就是声明式事务管理的粒度是方法级别,而编程式事务管理是可以到代码块的@Transactional注解可以帮助我们把事务开启、提交或者回滚的操作,通过aop的方式进行管理
作用:
①根据配置,设置是否自动开启事务
②自动提交事务或者遇到异常自动回滚
原理:
TransactionInterceptor
类。事务传播行为:
事务传播行为是为了解决业务层方法之间互相调用的事务问题。 当事务方法被另一个事务方法调用时,必须指定事务应该如何传播。例如:方法可能继续在现有事务中运行,也可能开启一个新事务,并在自己的事务中运行。
TransactionDefinition.PROPAGATION_REQUIRED
使用的最多的一个事务传播行为,我们平时经常使用的@Transactional
注解默认使用就是这个事务传播行为。如果外层有事务,则当前事务加入到外层事务,一块提交,一块回滚。如果外层没有事务,新建一个事务执行事务中的隔离级别:
Spring 也相应地定义了一个枚举类:Isolation
和MYSQL一样,READ_UNCOMMITTED
、READ_COMMITTED
、 REPEATABLE_READ
、 SERIALIZABLE
,默认使用后端数据库默认的隔离级别
回滚:
Exception 分为运行时异常 RuntimeException(数学错误、数组越界,空指针等),和非运行时异常 IOException(找不到文件等)。事务管理保证即使出现异常情况,它也可以保证数据的一致性。
@Transactional可以加在类或者方法上,只能应用到 public 方法才有效,(当 @Transactional 注解作用于类上时,该类的所有 public 方法将都具有该类型的事务属性)。这个类里面的方法抛出异常,就会回滚,数据库里面的数据也会回滚。
@Transactional(rollbackFor = Exception.class)
注解:
事务失效
starter
即可,Spring Boot将为你管理依赖关系。Spring Boot 在启动的时候,按照约定去读取 Spring Boot Starter 的配置信息,再根据配置信息对资源进行初始化,并注入到 Spring 容器中。这样 Spring Boot 启动完毕后,就已经准备好了一切资源,使用过程中直接注入对应 Bean 资源即可Spring Boot 通过@EnableAutoConfiguration
开启自动装配,在启动类 SpringApplication.run(…) 的内部就会执行,@Import 注解的 selectImports() 方法,在启动类所在包立找到所有 JavaConfig 形式的自动配置类(以AutoConfiguration结尾来命名),从META-INF/spring.factories中获取资源,筛选出以 EnableAutoConfiguration 为 key 的数据,加载到 IOC 容器中,
@SpringBootApplication
看作是以下三个的集合:
@Resource和@Autowired:都是做bean的注入时使用
public class TestServiceImpl {
// 下面两种@Autowired只要使用一种即可
@Autowired
private UserDao userDao; // 用于字段上
@Autowired
public void setUserDao(UserDao userDao) { // 用于属性的方法上
this.userDao = userDao;
}
// 下面两种@Resource只要使用一种即可
@Resource(name="userDao")
private UserDao userDao; // 用于字段上
@Resource(name="userDao")
public void setUserDao(UserDao userDao) { // 用于属性的setter方法上
this.userDao = userDao;
}
}
Mybatis底层封装了JDBC,使用了动态代理模式。
组件:
工作流程:
Mybatis 中有一级缓存和二级缓存,默认情况下一级缓存是开启的
#{}
和 ${}
:
${}
是字符串替换。#{}
替换为?
号,调用PreparedStatement的set方法来赋值;使用#{}可以有效的防止SQL注入${}
时,就是把 ${}
替换成变量的值。#{}
预编译防止SQL注入:
// 安全,预编译,执行sql前会预编译号该条语句,替换为?
String sql = "select id, username, password, role from user where id=?"; // 用set方法给?赋值
PreparedStatement pstmt = conn.prepareStatement(sql);
pstmt.setString(1, id);
// 不安全,没有预编译,直接替换为id值
String sql = "select id,username,password,role from user where id=" + id;
//当id参数为"3;drop table user;"时,执行的sql语句如下:
//select id,username,password,role from user where id=3; drop table user;
PreparedStatement pstmt = conn.prepareStatement(sql);
Spring Cloud组件运行:
微服务优点:
Nacos如果服务突然挂掉
CAP
一个分布式系统不可能同时满足一致性(C:Consistency)、可用性(A:Availability)和分区容错性(P:Partition tolerance)这三个基本需求,最多只能同时满足其中两项。
分区容错性是一个分布式系统必然需要面对和解决的问题(否则就成了单机分布)。因此需要在C(一致性)和A(可用性)之间寻求平衡。
BASE
Basically Available(基本可用)、Soft state(软状态)和Eventually consistent(最终一致性)
通过牺牲强一致性(ACID)来获得可用性,并允许数据在一段时间内是不一致的,但最终达到一致状态
Nginx是集中式负载均衡,请求是先进入负载均衡器,再发给客户端;
而在 Ribbon 中是先在客户端进行负载均衡,然后才进行请求的。
Nginx 使用的是 轮询和加权轮询算法。而在 Ribbon 中有更多的负载均衡调度算法,其默认是使用的 RoundRobinRule 轮询策略:若经过一轮轮询没有找到可用的 provider,其最多轮询 10 轮。若最终还没有找到,则返回 null。
OpenFeign 直接内置了 Ribbon 进行负载均衡
如果不使用rpc框架,那么调用服务走http需要配置请求head、body,然后才能发起请求。获得响应体后,还需解析等操作,十分繁琐。Feign是一个http请求调用的轻量级框架,以Java接口注解的方式调用Http请求。Feign通过处理注解,将请求模板化,当实际调用的时候,传入参数,根据参数再应用到请求上,进而转化成真正的请求,封装了http调用流程。
RPC(Remote Procedure Call Protocol)——远程过程调用协议,在OSI网络通信模型中,RPC跨域了传输层和应用层。
RPC框架:
RPC框架原理到选型:gRPC、Thrift、Dubbo、Spring Cloud
Thrift:跨语言,协议层、传输层有多种控制要求,socket通信
Dubbo是rpc,不能很好跨语言,cloud基于http rest api,跨语言
序列化NIO netty
将自身token和appkey签名,加在请求协议里,发送给服务端,签名相同或在白名单内,鉴权通过,连接粒度/接口粒度
Octo:服务注册、服务自动发现、负载均衡、容错、灰度发布、数据可视化、监控告警等功能,类似
MTthrift :分布式服务通讯框架,致力于提供高性能和透明化的RPC远程服务调用方案,是 OCTO 服务治理方案的核心框架,支持服务注册、服务自动发现、分布式服务调用跟踪等,屏蔽了底层高性能网络通信的实现细节, 从而实现简单高效的服务开发,支持不同语言版本的代码实现, 保持通信协议的一致性,支持 Thrift/HTTP/pigeon 等协议
MNS 是 Meituan Naming Service 的缩写。 MNS 是服务注册路由中心,基于 ZooKeeper 构建,为公司各类分布式服务提供稳健可靠的命名服务管理组件, 快速实现服务注册、路由、服务自动发现。
MCC 是 Meituan Config Center 的缩写。 统一配置中心,提供统一配置管理服务, 实现配置与代码分离、配置信息实时更新、高可用性、版本控制, 提高服务开发效率,降低运维成本。 其原理是将 JSON 格式的配置文件存储在 ZooKeeper 目录下,当用户有需要时,将zk中的配置数据落地到本机的指定目录中。
使用thrift 提供的@ThriftService、@ThriftMethod、@ThriftStruct、@ThriftField等注解,注解于普通的Java类,使其成为thrift的数据模型(model)和服务接口(service)。其使用模式与 Dubbo 非常相似:服务的提供者和消费者基于共同的一套接口定义。
好处:
问题:
Kafka、RocketMQ、RabbitMQ
优点:
缺点:
Kafka 单机超过 64 个队列/分区, Load 会发生明显的飙高现象,队列越多, load 越高,发送消息响应时间变长;
消费失败不支持重试;支持消息顺序,但是一台代理宕机后,就会产生消息乱序,
Kafka如何保证消息的有序性:
【前提条件:
生产者生产的消息是有序的,
kafka默认保证同一个partition分区内的消息是有序的】
用 Java 语言实现,在设计时参考了 Kafka,并做出了自己的一些改进。被阿里巴巴广泛应用在订单,交易,充值,流计算,消息推送,日志流式处理, binglog 分发等场景。
优点:
分布式
架构,消息可以做到 0 丢失由于 erlang 语言的高并发特性
,性能较好; 吞吐量到万级, MQ 功能比较完备,健壮、稳定、易用、跨平台、
AMQP,Advanced Message Queuing Protocol(高级消息队列协议),一个提供统一消息服务的应用层标准,是应用层协议的一个开放标准。
基于此协议的客户端与消息中间件可传递消息,并不受客户端/中间件不同产品,不同开发语言等条件的限制。 RabbitMQ是该协议的典型实现。
Exchange: 消息交换机,它指定消息按什么规则,路由到哪个队列
Queue: 消息队列载体,每个消息都会被投入到一个或多个队列
Routing Key: 路由关键字,exchange根据这个关键字进行消息投递
原因:正常情况下,消费者在消费消息的时候,消费完毕后,会发送一个确认消息给消息队列,就会将该消息从消息队列中删除;但是因为网络传输等等故障,确认信息没有传送到消息队列,导致消息队列不知道自己已经消费过该消息了,再次将消息分发给其他的消费者。
解决:
原因:消息不可靠的情况可能是消息丢失,劫持等原因;
丢失又分为:生产者丢失消息、消息列表丢失消息、消费者丢失消息;
生产者丢失消息:RabbitMQ提供 transaction 和 confirm 模式来确保生产者不丢消息;
transaction机制:发送消息前,开启事务,然后发送消息,如果发送过程中出现什么异常,事务就会回滚,如果发送成功则提交事务。缺点:吞吐量下降;
confirm模式:一旦消息被投递到所有匹配的队列之后,rabbitMQ就会发送一个ACK
给生产者(包含消息的唯一ID),如果rabbitMQ没能处理该消息,则会发送一个Nack消息给你,你可以进行重试操作。
消息队列丢数据:消息持久化。
开启持久化磁盘的配置(将queue的持久化标识durable设置为true),可以和 confirm 机制配合使用,你可以在消息持久化磁盘后,再给生产者发送一个Ack信号。
消费者丢失消息:消费者在收到消息之后,处理消息之前,会自动回复RabbitMQ已收到消息;如果这时处理消息失败,就会丢失该消息;
改为处理消息成功后,手动回复确认消息
①简单模式 / work模式
一个生产者 –> 一个队列 –> 一个或多个消费者。一条消息只能给一个消费者消费。
例子:发送一次短信
②发布订阅模式(广播)
每个消费者监听自己的队列;由交换机将消息转发到绑定此交换机的每个队列。不处理路由键routingkey。
例子:发送多种通知,当用户充值成功后,需要发送多种通知,比如短信、邮件。
③路由模式
每个消费者监听自己的队列,并且设置routingkey。
生产者将消息发给交换机,由交换机根据 routingkey 来转发消息到指定的队列。需要将一个队列绑定到交换机上,
④topic 主题模式
每个消费者监听自己的队列,并且设置带统配符的routing key。
发送消息时,由交换机根据routing key来转发消息到指定的队列。routing key使用通配符,和队列绑定的key匹配,该队列就行接收到消息。
设计模式分为三大类:
第一次调用时才初始化
public class Singleton {
private static Singleton instance;
private Singleton (){}
public static Singleton getInstance() {
if (instance == null) {
instance = new Singleton();
}
return instance;
}
//用来测试
public void showMessage(){
System.out.println("Hello World!");
}
}
测试:
public class SingletonPatternDemo {
public static void main(String[] args) {
//编译时错误:构造函数 SingleObject() 是不可见的
//SingleObject object = new SingleObject();
SingleObject object = SingleObject.getInstance();
object.showMessage();
}
}
加synchronized
锁
public class Singleton {
private static Singleton instance;
private Singleton (){}
public static synchronized Singleton getInstance() {
if (instance == null) {
instance = new Singleton();
}
return instance;
}
}
类加载时就初始化,浪费内存,容易产生垃圾对象。
没有加锁,执行效率会提高。
public class Singleton {
private static Singleton instance = new Singleton();
private Singleton (){}
public static Singleton getInstance() {
return instance;
}
}
懒加载,双锁机制,安全且在多线程情况下能保持高性能。
class Singleton {
private volatile static Singleton singleton;
private Singleton (){}
public static Singleton getSingleton() {
if (singleton == null) {
synchronized (Singleton.class) {
if (singleton == null) singleton = new Singleton();
}
}
return singleton;
}
}
定义一个创建对象的接口,让其子类自己决定实例化哪一个工厂类,工厂模式使其创建过程延迟到子类进行。
何时使用:计划不同条件下创建不同实例时。
优点: 1、一个调用者想创建一个对象,只要知道其名称就可以了。 2、扩展性高,如果想增加一个产品,只要扩展一个工厂类就可以。 3、屏蔽产品的具体实现,调用者只关心产品的接口。
缺点:每次增加一个产品时,都需要增加一个具体类和对象实现工厂,使得系统中类的个数成倍增加,在一定程度上增加了系统的复杂度,同时也增加了系统具体类的依赖。
//面条的抽象类
abstract class INoodles {
public abstract void desc();
}
//面条1
class Noodles1 extends INoodles {
@Override
public void desc() {
System.out.println("面条1");
}
}
//面条2
class Noodles2 extends INoodles {
@Override
public void desc() {
System.out.println("面条2");
}
}
//工厂(面馆)
class SimpleNoodlesFactory {
public static final int TYPE_1 = 1;
public static final int TYPE_2 = 2;
// 提供静态方法
public static INoodles createNoodles(int type) {
switch (type) {
case TYPE_1:return new Noodles1();
case TYPE_2:return new Noodles2();
default:return new Noodles1();//应该是Noodles3
}
}
}
//测试
public class FactoryMode {
public static void main(String[] args) {
INoodles noodles = SimpleNoodlesFactory.createNoodles(SimpleNoodlesFactory.TYPE_1);
noodles.desc();
}
}
原型模式(Prototype Pattern)是用于创建重复的对象,这种模式是实现了一个原型接口,该接口用于创建当前对象的克隆。当直接创建对象的代价比较大时,则采用这种模式。即利用已有的一个原型对象,快速地生成和原型对象一样的实例。
例如,一个对象需要在一个高代价的数据库操作之后被创建。我们可以缓存该对象,在下一个请求时返回它的克隆,在需要的时候更新数据库,以此来减少数据库调用。
装饰器模式(Decorator Pattern)这种模式创建了一个装饰类,用来包装原有的类,并在保持类方法签名完整性的前提下,添加新的功能。
动态地给一个对象添加一些额外的职责。就增加功能来说,装饰器模式相比生成子类更为灵活。
在代理模式(Proxy Pattern)中,用一个类代表另一个类的功能。我们创建具有现有对象的对象,以便向外界提供功能接口。为其他对象提供一种代理以控制对这个对象的访问。
主要解决:在直接访问对象时带来的问题,比如说:要访问的对象在远程的机器上。在面向对象系统中,有些对象由于某些原因,直接访问会给使用者或者系统结构带来很多麻烦,我们可以在访问此对象时,加上一个对此对象的访问层。
import java.lang.reflect.Proxy;
//1、静态代理
//接口Subject
interface Subject {
void visit();
}
//实现Subject的目标对象类
class RealSubject implements Subject {
private String name = "dreamcat";
@Override
public void visit() {
System.out.println(name);
}
}
//目标代理对象
class ProxySubject implements Subject {
private Subject subject;
public ProxySubject(Subject subject) {
this.subject = subject;
}
@Override
public void visit() {
System.out.println("我是静态代理");
subject.visit();
System.out.println("静态代理结束了");
}
}
//2、动态代理
class DynamicProxy {
private Object target;
DynamicProxy (Object target) {
this.target = target;
}
public Object getProxyInstance() {
return Proxy.newProxyInstance(
target.getClass().getClassLoader(),
target.getClass().getInterfaces(),
(proxy, method, args) -> {
System.out.println("我是动态代理");
Object value = method.invoke(target, args);
System.out.println("代理结束了");
return value;
});
}
}
//测试
public class ProxyMode {
public static void main(String[] args) {
// 静态代理
ProxySubject proxySubject = new ProxySubject(new RealSubject());
proxySubject.visit();
// 动态代理
Subject proxyInstance = (Subject)new DynamicProxy(new RealSubject()).getProxyInstance();
proxyInstance.visit();
}
}
适配器模式(Adapter Pattern)涉及到一个单一的类,该类负责加入独立的或不兼容的接口功能。将一个类的接口转换成客户希望的另外一个接口。适配器模式使得原本由于接口不兼容
而不能一起工作的那些类可以一起工作。
比如读卡器是作为内存卡和笔记本之间的适配器。
适配器继承或依赖已有的对象,实现想要的目标接口。
spring MVC 中也是用到了适配器模式适配Controller。
在模板模式(Template Pattern)中,一个抽象类公开定义了执行它的方法的方式/模板。它的子类可以按需要重写方法实现,但调用将以抽象类中定义的方式进行。
封装不变部分,扩展可变部分;行为由父类控制,子类实现。
一些智力题1
一些智力题2
9颗糖分给10个人,有几种分法
答:把糖看作1,人看作0,相当于这些 1 和 0 排列组合,永远把 1 分给后面紧接着的 0,由于最后一位必须是 0,否则最后一个糖没人给,所以就18个位置选出9个作为 1.