Java 基础
类的初始化过程
- 一个类要创建实例需要先加载并初始化该类
- main方法所在的类需要先加载和初始化
- 子类初始化要先初始化父类
- 一个类初始化就是执行
<clinit>()
方法<clinit>()
方法由静态类变量显示赋值代码和静态代码块组成- 类变量显示赋值代码和静态代码块从上向下顺序执行
<clinit>()
方法只执行一次
类的初始化顺序
- 父类的静态变量和静态代码块(按声明顺序)
- 自身的静态变量和静态代码块(按声明顺序)
- 父类的成员变量和非静态代码块(按声明顺序)
- 父类的构造器。如果父类中包含有构造器,却没有无参构造器,则在子类中一定要使用
supper(参数)
指定调用父类的有参构造,不然会报错 - 自身的成员变量和非静态代码块(按声明顺序)
- 自身的构造器
类的实例化过程
IO流
-
分类
-
按数据的流向:输入流、输出流
输入流:InputStream、Reader 是所有输入流的基类
输出流:OutputSteam、Writer 是所有输出流的基类
-
按数据的类型:字节流、字符流
字节流:InputStream、OutputStream 是所有字节流的基类
字符流:Reader、Writer 是所有字符流的基类
-
按流的角色分:节点流、处理流
-
-
缓冲流
- 字节缓冲流:BufferedInputStream、BufferedOutputStream
- 字符缓冲流:BufferedReader、BufferedWriter
缓冲流的基本原理,是在创建流对象时,会创建一个内置的默认大小的缓冲区数组,通过缓冲区读写,减少系统IO次数,从而提高读写的效率
-
转换流
InputStreamReader
是字节流通向字符流的桥梁OutputStreamWriter
是字符流通向字节流的桥梁
Java 集合框架
1. 集合概述
在 Java 中除了以 Map
结尾的类之外, 其他类都实现了 Collection
接口。并且,以 Map
结尾的类都实现了 Map
接口。
2. List、Set、Map的区别
- List:存储的元素是有序的,可重复的
- Set:存储的元素是无序的,不可重复的
- Map:以 key-value 键值对存储元素,key是无序的不可重复的,value是无序的,可重复的,每个key最多映射到一个值
3. 集合框架的数据结构
实现的了Collection
的集合
3.1 List
ArrayList
:Object[]
数组Vector
:Object[]
数组LinkedLiat
:双端链表,jdk1.6 之前是循环链表,1.7之后取消了循环
3.2 Set
HashSet
: (无序,唯一)基于HashMap实现,底层使用HashMap
保存元素,无序的,唯一的LinkedHashSet
: 是HashSet
的子类,底层通过LinkedHashMap
实现TreeSet
: (有序,唯一)红黑树
实现了Map
接口的集合
3.3 Map
-
HashMap
: JDK1.8 之前HashMap
由数组+链表组成的,数组是HashMap
的主体,链表则是主要为了解决哈希冲突而存在的(“拉链法”解决冲突)。JDK1.8 以后在解决哈希冲突时有了较大的变化,当链表长度大于阈值(默认为 8)(将链表转换成红黑树前会判断,如果当前数组的长度小于 64,那么会选择先进行数组扩容,而不是转换为红黑树)时,将链表转化为红黑树,以减少搜索时间 -
LinkedHashMap
:LinkedHashMap
继承自HashMap
,所以它的底层仍然是基于拉链式散列结构即由数组和链表或红黑树组成。另外,LinkedHashMap
在上面结构的基础上,增加了一条双向链表,使得上面的结构可以保持键值对的插入顺序。同时通过对链表进行相应的操作,实现了访问顺序相关逻辑。 -
Hashtable
: 数组+链表组成的,数组是HashMap
的主体,链表则是主要为了解决哈希冲突而存在的 -
TreeMap
: 红黑树
4. 集合的选择
主要根据集合的特点来选用
当我们需要根据键值获取到元素值时就选用 Map
接口下的集合,需要排序时选择 TreeMap
,不需要排序时就选择 HashMap
,需要保证线程安全就选用 ConcurrentHashMap
。
当我们只需要存放元素值时,就选择实现Collection
接口的集合,需要保证元素唯一时选择实现 Set
接口的集合比如 TreeSet
或 HashSet
,不需要就选择实现 List
接口的比如 ArrayList
或 LinkedList
,然后再根据实现这些接口的集合的特点来选用。
5. List
5.1 ArrayList
和 Vector
的区别
ArrayList
是Object[] 存储元素的,适用于 频繁的查找工作,线程不安全Vector
:底层通过Object[] 存储元素,线程安全,效率低下
5.2 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
采用链表存储,所以对于add(E e)
方法的插入,删除元素时间复杂度不受元素位置的影响,近似 O(1),如果是要在指定位置i
插入和删除元素的话((add(int index, E element)
) 时间复杂度近似为o(n))
因为需要先移动到指定位置再插入。 -
是否支持快速随机访问:LinkedList 不支持,ArrayList支持。快速随机访问是指通过元素的序号来获取元素对象
-
内存占用:ArrayList 的空 间浪费主要体现在在 list 列表的结尾会预留一定的容量空间,而 LinkedList 的空间花费则体现在它的每一个元素都需要消耗比 ArrayList 更多的空间(因为要存放直接后继和直接前驱以及数据)
5.3 CopyOnWriteArrayList
写时复制是一种读写分离的思想,在并发读的时候不需要加锁,因为它能够保证并发读的情况下不会添加任何元素。而在并发写的情况下,需要先加锁,但是并不直接对当前容器进行写操作。而是先将当前容器进行复制获取一个新的容器,进行完并发写操作之后,当之前指向原容器的引用更改指向当前新容器。也就是说,并发读和并发写是针对不同集合,因此不会产生并发异常
6. Set
6.1 比较 HashSet、LinkedHashSet 和 TreeSet 三者的异同
HashSet
是 Set
接口的主要实现类 ,HashSet
的底层是 HashMap
,线程不安全的,可以存储 null 值;
LinkedHashSet
是 HashSet
的子类,能够按照添加的顺序遍历;
TreeSet
底层使用红黑树,能够按照添加元素的顺序进行遍历,排序的方式有自然排序和定制排序。
6.2 无序性和不可重复性的含义是什么
1、什么是无序性?无序性不等于随机性 ,无序性是指存储的数据在底层数组中并非按照数组索引的顺序添加 ,而是根据数据的哈希值决定的。
2、什么是不可重复性?不可重复性是指添加的元素按照 equals()判断时 ,返回 false,需要同时重写 equals()方法和 HashCode()方法。
6.3 CopyOnWriteArraySet
也是写时复制思想,但是内部还是使用CopyOnWriteArrayList实现
7. Map
7.1 HashMap 和 Hashtable 的区别
- 线程是否安全:
HashMap
是非线程安全的,Hashtable
是线程安全的,因为Hashtable
内部的方法基本都经过synchronized
修饰。(如果你要保证线程安全的话就使用ConcurrentHashMap
吧!); - 效率: 因为线程安全的问题,
HashMap
要比HashTable
效率高一点。另外,HashTable
基本被淘汰,不要在代码中使用它; - 对 Null key 和 Null value 的支持:
HashMap
可以存储 null 的 key 和 value,但 null 作为键只能有一个,null 作为值可以有多个;HashTable 不允许有 null 键和 null 值,否则会抛出NullPointerException
。 - 初始容量大小和每次扩充容量大小的不同 : ① 创建时如果不指定容量初始值,
Hashtable
默认的初始大小为 11,之后每次扩充,容量变为原来的 2n+1。HashMap
默认的初始化大小为 16。之后每次扩充,容量变为原来的 2 倍。② 创建时如果给定了容量初始值,那么 Hashtable 会直接使用你给定的大小,而HashMap
会将其扩充为 2 的幂次方大小(HashMap
中的tableSizeFor()
方法保证,下面给出了源代码)。也就是说HashMap
总是使用 2 的幂作为哈希表的大小,后面会介绍到为什么是 2 的幂次方。 - 底层数据结构: JDK1.8 以后的
HashMap
在解决哈希冲突时有了较大的变化,当链表长度大于阈值(默认为 8)(将链表转换成红黑树前会判断,如果当前数组的长度小于 64,那么会选择先进行数组扩容,而不是转换为红黑树)时,将链表转化为红黑树,以减少搜索时间。Hashtable 没有这样的机制。
7.2 HashMap 和 ConcurrentHashMap 的区别
- HashMap 是线程不安全的,ConcurrentHashMap 是线程安全的
- ConcurrentHashMap
7.3 HashMap 的底层结构
JDK1.8 之前 HashMap
底层是 数组和链表 结合在一起使用也就是 链表散列。HashMap 通过 key 的 hashCode 经过扰动函数处理过后得到 hash 值,然后通过 (n - 1) & hash 判断当前元素存放的位置(这里的 n 指的是数组的长度),如果当前位置存在元素的话,就判断该元素与要存入的元素的 hash 值以及 key 是否相同,如果相同的话,直接覆盖,不相同就通过拉链法解决冲突。
所谓扰动函数指的就是 HashMap 的 hash 方法。使用 hash 方法也就是扰动函数是为了防止一些实现比较差的 hashCode() 方法 换句话说使用扰动函数之后可以减少碰撞。
所谓 “拉链法” 就是:将链表和数组相结合。也就是说创建一个链表数组,数组中每一格就是一个链表。若遇到哈希冲突,则将冲突的值加到链表中即可。
JDK1.8 之后在解决哈希冲突时有了较大的变化,当链表长度大于阈值(默认为 8)(将链表转换成红黑树前会判断,如果当前数组的长度小于 64,那么会选择先进行数组扩容,而不是转换为红黑树)时,将链表转化为红黑树,以减少搜索时间。
7.4 ConcurrentHashMap 和 Hashtable 的区别
ConcurrentHashMap
和 Hashtable
的区别主要体现在实现线程安全的方式上不同。
-
底层数据结构: JDK1.7 的
ConcurrentHashMap
底层采用 分段的数组+链表 实现,JDK1.8 采用的数据结构跟HashMap1.8
的结构一样,数组+链表/红黑二叉树。Hashtable
和 JDK1.8 之前的HashMap
的底层数据结构类似都是采用 数组+链表 的形式,数组是 HashMap 的主体,链表则是主要为了解决哈希冲突而存在的; -
实现线程安全的方式(重要):
① 在 JDK1.7 的时候,
ConcurrentHashMap
(分段锁) 对整个桶数组进行了分割分段(Segment
),每一把锁只锁容器其中一部分数据,多线程访问容器里不同数据段的数据,就不会存在锁竞争,提高并发访问率。 到了 JDK1.8 的时候已经摒弃了Segment
的概念,而是直接用Node
数组+链表+红黑树的数据结构来实现,并发控制使用synchronized
和CAS
来操作。(JDK1.6 以后 对synchronized
锁做了很多优化) 整个看起来就像是优化过且线程安全的HashMap
,虽然在 JDK1.8 中还能看到Segment
的数据结构,但是已经简化了属性,只是为了兼容旧版本;②
Hashtable
(同一把锁) :使用synchronized
来保证线程安全,效率非常低下。当一个线程访问同步方法时,其他线程也访问同步方法,可能会进入阻塞或轮询状态,如使用 put 添加元素,另一个线程不能使用 put 添加元素,也不能使用 get,竞争会越来越激烈效率越低。
8 Collections 工具类
Collections 工具类常用方法:
- 排序
- 查找,替换操作
- 同步控制(不推荐,需要线程安全的集合类型时请考虑使用 JUC 包下的并发集合)
8.1 排序操作
void reverse(List list)//反转
void shuffle(List list)//随机排序
void sort(List list)//按自然排序的升序排序
void sort(List list, Comparator c)//定制排序,由Comparator控制排序逻辑
void swap(List list, int i , int j)//交换两个索引位置的元素
void rotate(List list, int distance)//旋转。当distance为正数时,将list后distance个元素整体移到前面。当distance为负数时,将 list的前distance个元素整体移到后面
8.2 查找,替换操作
int binarySearch(List list, Object key)//对List进行二分查找,返回索引,注意List必须是有序的
int max(Collection coll)//根据元素的自然顺序,返回最大的元素。 类比int min(Collection coll)
int max(Collection coll, Comparator c)//根据定制排序,返回最大元素,排序规则由Comparatator类控制。类比int min(Collection coll, Comparator c)
void fill(List list, Object obj)//用指定的元素代替指定list中的所有元素。
int frequency(Collection c, Object o)//统计元素出现次数
int indexOfSubList(List list, List target)//统计target在list中第一次出现的索引,找不到则返回-1,类比int lastIndexOfSubList(List source, list target).
boolean replaceAll(List list, Object oldVal, Object newVal), 用新元素替换旧元素
8.3 同步控制
Collections
提供了多个synchronizedXxx()
方法,该方法可以将指定集合包装成线程同步的集合,从而解决多线程并发访问集合时的线程安全问题。
我们知道 HashSet
,TreeSet
,ArrayList
,LinkedList
,HashMap
,TreeMap
都是线程不安全的。Collections
提供了多个静态方法可以把他们包装成线程同步的集合。
最好不要用下面这些方法,效率非常低,需要线程安全的集合类型时请考虑使用 JUC 包下的并发集合。
方法如下:
synchronizedCollection(Collection<T> c) //返回指定 collection 支持的同步(线程安全的)collection。
synchronizedList(List<T> list)//返回指定列表支持的同步(线程安全的)List。
synchronizedMap(Map<K,V> m) //返回由指定映射支持的同步(线程安全的)Map。
synchronizedSet(Set<T> s) //返回指定 set 支持的同步(线程安全的)set。
Java 8新特性
1. Lambda 表达式
2. 函数式接口
Supplier<T>
:生产,返回一个值
() -> T
Predicate<T>
:返回一个Boolean类型的值
T -> boolean
Consumer<T>
:消费,
T -> void
3. 默认方法
在 接口中定义 以 default
修饰的方法
4. Stream
4.1 什么是 Stream
Stream(流)是一个来自数据源的元素队列并支持聚合操作
4.2 创建流
stream() // 为集合创建串行流。
parallelStream() //为集合创建并行流。
4.3 forEach
Stream 提供了新的方法 forEach
来迭代流中的每个数据
4.4 map
高并发多线程
线程的状态
-
new
:初始化状态 -
RUNNABLE
:可运行/运行状态 -
BLOCKED
:阻塞状态 -
WAITING
:无限时等待 -
TIMED_WAITING
:有限时等待 -
TERMINATED
:终止状态
在操作系统层面,Java 线程中的 BLOCKED、WAITING、TIMED_WAITING 是一种状态(休眠状态)。即只要 Java 线程处于这三种状态之一,就永远没有 CPU 的使用权。
类锁与对象锁
- 对象锁
类声明后,通过 new 出来很多的实例对象。这时候,每个实例在 JVM 中都有自己的引用地址和堆内存空间,这时候,这些实例都是独立的个体,很显然,在实例上加的锁和其他的实例就没有关系,互不影响了。
- 类锁
类锁是加载类上的,而类信息是存在 JVM 方法区的,并且整个 JVM 只有一份,方法区又是所有线程共享的,所以类锁是所有线程共享的。
类锁是所有线程共享的锁,所以同一时刻,只能有一个线程使用加了锁的方法或方法体,不管是不是同一个实例
synchronized
synchronized是Java中的关键字,是一种同步锁。它修饰的对象有以下几种
- 修饰一个代码块,被修饰的代码块称为同步语句块,其作用的范围是大括号{}括起来的代码,作用的对象是调用这个代码块的对象;
- 修饰一个方法,被修饰的方法被称为 同步方法,其作用范围是整个方法,作用的对象时调用这个方法的对象
- 修饰一个静态方法,其作用范围是整个静态方法,作用的对象时这个类的所有对象
- 修饰一个类,其作用范围是被 synchronized 后面括号括起来的部分,作用的对象是这个类的所有对象
加了synchronized 且有static 的方法称为类锁,没有static 的方法称为对象锁
(1)多线程使用同一个对象,只允许同时使用一个对象锁,一个类锁,其他操作搭配都互斥,只能等前一个线程解锁才能让下一个线程使用;
(2)多线程分别 new 一个对象,允许同时使用任意的对象锁,也允许对象锁和一个类锁同时使用,但是类锁不能够同时使用,会互斥,只能等前一个线程解锁才能让下一个线程使用;
Lock
Lock
是J.U.C
下的一个接口,Lock
和synchronized
有一点非常大的不同,采用synchronized
不需要用户去手动释放锁,当synchronized
方法或者synchronized
代码块执行完之后,系统会自动让线程释放对锁的占用;而Lock
则必须要用户去手动释放锁,如果没有主动释放锁,就有可能导致出现死锁现象。主要声明了以下几个方法:
lock()
:无返回值,用来获取锁。如果锁已被其他线程获取,则进行等待tryLock()
:有返回值的,它表示用来尝试获取锁,如果获取成功,则返回true,如果获取失败(即锁已被其他线程获取),则返回false,也就说这个方法无论如何都会立即返回。在拿不到锁时不会一直在那等待。tryLock(long time, TimeUnit unit)
:有返回值,和tryLock()
方法是类似的,只不过区别在于这个方法在拿不到锁时会等待一定的时间,在时间期限之内如果还拿不到锁,就返回false。如果如果一开始拿到锁或者在等待期间内拿到了锁,则返回true。lockInterruptibly()
:无返回值,比较特殊,当通过这个方法去获取锁时,如果线程正在等待获取锁,则这个线程能够响应中断,即中断线程的等待状态。也就使说,当两个线程同时通过lock.lockInterruptibly()
想获取某个锁时,假若此时线程A获取到了锁,而线程B只有在等待,那么对线程B调用threadB.interrupt()
方法能够中断线程B的等待过程。unLock()
:释放锁newCondition()
:有返回值,
Lock 与 synchronized 区别
**构成方面 ** :
-
synchronized
是Java关键字是jvm层面的, -
Lock
是JUC提供的具体类,是API层面的东西;
用法方面
synchronized
不需要用户手动释放锁,当synchronized
代码执行完毕之后会自动让线程释放持有的锁;lock
需要一般使用try-finally
模式去手动释放锁,并且加锁-解锁数量需要一直,否则容易出现死锁或者程序不终止现象;
等待是否可中断:
synchronized
是不可中断的,除非抛出异常或者程序正常退出lock
可中断:- 设置超时方法
tryLock(time, unit)
; - 使用
lockInterruptibly
,调用iterrupt
方法可中断
- 设置超时方法
是否公平锁
synchronized
是非公平锁lock
默认是非公平锁,可以通过 构造函数传入boolean
类型的值更改是否公平锁
锁是否能绑定多个条件(condition)
synchronized
没有condition
的说法,要么唤醒所有线程,要么随机唤醒一个线程lock
可以使用condition
实现分组唤醒需要唤醒的线程,实现精准唤醒
Lock 和 synchronized 的选择
Lock
是一个接口,而synchronized
是Java中的关键字,synchronized
是内置的语言实现;synchronized
在发生异常时,会自动释放线程占有的锁,因此不会导致死锁现象发生;而Lock
在发生异常时,如果没有主动通过unLock()
去释放锁,则很可能造成死锁现象,因此使用Lock
时需要在finally
块中释放锁;Lock
可以让等待锁的线程响应中断,而synchronized
却不行,使用synchronized
时,等待的线程会一直等待下去,不能够响应中断;- 通过
Lock
可以知道有没有成功获取锁,而synchronized
却无法办到。 Lock
可以提高多个线程进行读操作的效率。
在性能上来说,如果竞争资源不激烈,两者的性能是差不多的,而当竞争资源非常激烈时(即有大量线程同时竞争),此时Lock的性能要远远优于synchronized。所以说,在具体使用时要根据适当情况选择
简述java的锁
- 可重入锁:
如果锁具备可重入性,则称作为可重入锁。像
synchronized
和ReentrantLock
都是可重入锁,可重入性在我看来实际上表明了锁的分配机制:基于线程的分配,而不是基于方法调用的分配。举个简单的例子,当一个线程执行到某个synchronized方法时,比如说method A,而在method A中会调用另外一个synchronized方法method B,此时线程不必重新去申请锁,而是可以直接执行方法method B。
- 可中断锁
在Java中,synchronized就不是可中断锁,而Lock是可中断锁。
如果某一线程A正在执行锁中的代码,另一线程B正在等待获取该锁,可能由于等待时间过长,线程B不想等待了,想先处理其他事情,我们可以让它中断自己或者在别的线程中中断它,这种就是可中断锁。
lockInterruptibly()
的用法时已经体现了Lock的可中断性。
- 公平锁/非公平锁
尽量以请求锁的顺序来获取锁。比如同是有多个线程在等待一个锁,当这个锁被释放时,等待时间最久的线程(最先请求的线程)会获得该所,这种就是公平锁。
非公平锁即无法保证锁的获取是按照请求锁的顺序进行的。这样就可能导致某个或者一些线程永远获取不到锁。
在Java中,synchronized就是非公平锁,它无法保证等待的线程获取锁的顺序。
而对于
ReentrantLock
和ReentrantReadWriteLock
,它默认情况下是非公平锁,但是可以通过构造函数设置为公平锁。另外在
ReentrantLock
类中定义了很多方法,比如:
isFair()
//判断锁是否是公平锁
isLocked()
//判断锁是否被任何线程获取了
isHeldByCurrentThread()
//判断锁是否被当前线程获取了
hasQueuedThreads()
//判断是否有线程在等待该锁在
ReentrantReadWriteLock
中也有类似的方法,同样也可以设置为公平锁和非公平锁。不过要记住,ReentrantReadWriteLoc
k并未实现Lock接口,它实现的是ReadWriteLock
接口。
- 读写锁
读写锁将对一个资源(比如文件)的访问分成了2个锁,一个读锁和一个写锁。正因为有了读写锁,才使得多个线程之间的读操作不会发生冲突。
ReadWriteLock
就是读写锁,它是一个接口,ReentrantReadWriteLock
实现了这个接口。可以通过readLock()
获取读锁,通过writeLock()
获取写锁。
- 写锁(独占锁):指该锁一次只能被一个线程所持有,
ReentrantLock
和synchronized
都是独占锁- 读锁(共享锁):指该锁可以被多个线程所持有
- 读锁的共享锁可保证并发读是非常高效的,读写、写读、写写的过程都是互斥的
- 自旋锁
自旋锁尝试获取锁的线程不会立即阻塞,而是采用循环的方式去尝试获取锁,这样的好处是避免线程上下文切换的消耗,缺点是
如果一直自旋会消耗CPU
cas(v,a,b) 变量v, 期待值a,修改值b
java.util.concurrent.locks
包下常用的类
ReentrantLock
: 是Lock的实现类ReadWriteLock
:是一个接口,提供了两个方法,读写锁
public interface ReadWriteLock {
/**
* Returns the lock used for reading.
*
* @return the lock used for reading.
*/
Lock readLock();
/**
* Returns the lock used for writing.
*
* @return the lock used for writing.
*/
Lock writeLock();
}
一个用来获取读锁,一个用来获取写锁。也就是说将文件的读写操作分开,分成2个锁来分配给线程,从而使得多个线程可以同时进行读操作。
ReentrantReadWriteLock
实现了ReadWriteLock
接口。
ReentrantReadWriteLock
:是ReadWriteLock
的实现类
volitle
volatile是Java提供的轻量级的同步机制,主要有三个特性:
保证内存可见性
不保证原子性
禁止指令重排
使用 场景:
- 高并发环境下DCL单例模式使用volatile
- JUC包下AtomicXxx类:原子类AtomicXxx中都有一个成员变量value,该value变量被声明为volatile,保证
AtomicXxx类的内存可见性,而原子性由CAS算法&Unsafe类保证,结合这两点才能让AtomicXxx类很好地替代synchronized
关键字。
CAS
算法
CAS(Compare And Swap)算法是一条原子的CPU指令(Atomic::cmpxchg(x, addr, e) == e;),需要三个操作数:
变量的内存地址(或者是偏移量valueOffset) V ,预期值 A 和更新值 B,CAS指令执行时:
当且仅当对象偏移量V上的值和预期值A相等时,才会用更新值B更新V内存上的值,否则不执行更新。但是无论是否更新
了V内存上的值,最终都会返回V内存上的旧值。
优缺点
- do-while循环,如果CAS失败就会一直进行尝试,即一直在自旋,导致CPU开销,这也是自旋锁的缺点;
- 只能保证一个共享变量的原子操作,如果操作多个共享变量则需要加锁实现;
- ⭐️ABA问题:如果一个线程在初次读取时的值为A,并且在准备赋值的时候检查该值仍然是A,但是可能在这两次操作
之间,有另外一个线程现将变量的值改成了B,然后又将该值改回为A,那么CAS会误认为该变量没有变化过。
ABA问题
-
AtomicStampedReference
解决方案:每次修改都会让stamp值加1,类似于版本控制号 -
AtomicMarkableReference
:如果不关心引用变量中途被修改了多少次,而只关心是否被修改过,可以使用AtomicMarkableReference
阻塞队列
阻塞是一个队列,当阻塞队列是空的时候,从队列中获取元素的操作将会被阻塞;当队列满时,往队列中添加元素的操作将会被阻塞。
BlockingQueue
BlockingQueue
让我们不需要关心什么时候需要阻塞线程,什么时候需要唤醒线程
继承树:
ArrayBlockingQueue
⭐️:由数组结构组成的有界阻塞队列LinkedBlockingQueue
⭐️:由链表结构组成的有界阻塞(默认大小Integer.MAX_VALUE()
)队列PriorityBlockingQueue
:支持优先级排序的无界阻塞队列DelayQueue
:使用优先队列实现延迟无界阻塞队列SynchronizedQueue
⭐️:不存储元素的阻塞队列,也即单个元素的队列LinkedTransferQueue
:由链表结构组成的无界阻塞队列LinkedBlockingDeque
⭐️:由链表结构组成的双向阻塞队列
四组api
- 抛出异常组:
add()
: 当阻塞队列满时,抛出异常remove()
:当队列为空时,抛出异常element()
:取出队列开头元素
- null&false返回值:
offer()
:当阻塞队列满时,返回falsepoll()
:当阻塞队列为空时,返回nullpeek()
:返回阻塞队列开头元素,如果队列为空,返回null
- 一直阻塞:
put()
:当阻塞队列满时,队列会一直阻塞直到队列有数据被拿走或者响应中断take()
:当阻塞队列为空时,队列会一直阻塞直到队列中有新数据被放入或者响应中断
- 超时退出:
offer(e, time, unit)
:当阻塞队列满时,会阻塞生产者线程一段时间,超出时间限制后生产者线程退出poll(time, unit)
:当阻塞队列为空时,会阻塞消费者线程一段时间,超出时间限制后消费者线程退出
线程池
线程池的创建方式
使用 Executors
Executors.newCacheThreadPool()
:可缓存线程池,先查 看池中有没有以前建立的线程,如果有,就直接使用。如果没有,就建一个新的线程加入池中,缓存型池子通常用于执行一些生存期很短的异步型任务,执行很多短期异步的小程序或者负载较轻的服务Executors.newFixedThreadPool(int n)
:创建一个可重用固定个数的线程池,以共享的无界队列方式来运行这些线程,执行长期任务,性能好Executors.newScheduledThreadPool(int n)
:创建一个定长线程池,支持定时及周期性任务执行Executors.newSingleThreadExecutor()
:创建一个单线程化的线程池,它只会用唯一的工作线程来执行任务,保证所有任务按照指定顺序(FIFO, LIFO, 优先级)执行,一个任务一个任务的执行Executors.newWorkStealingPool(int n)
:Java 8 新特性
阻塞队列BlockingQueue
和自定义线程池ThreadPoolExecutor
构造函数
自定义线程池,可以用ThreadPoolExecutor
类创建,它有多个构造方法来创建线程池。
常见的构造函数:
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.acc = System.getSecurityManager() == null ?
null :
AccessController.getContext();
this.corePoolSize = corePoolSize;
this.maximumPoolSize = maximumPoolSize;
this.workQueue = workQueue;
this.keepAliveTime = unit.toNanos(keepAliveTime);
this.threadFactory = threadFactory;
this.handler = handler;
}
参数
corePoolSize
:线程池保有的最小线程数,常驻核心线程数maximumPoolSize
:线程池创建的最大线程数,必须大于等于1keepAliveTime
:如果一个线程空闲了keepAliveTime
这么久,而且线程池的线程数大于corePoolSize
,那么这个空闲的线程就要被回收unit
:keepAliveTime 的时间单位workQueue
:任务队列,被提交但未执行的任务等待区(候车厅)threadFactory
:表示生成线程池中工作线程的线程工厂,用于创建线程一般用默认的即可handler
:拒绝策略
拒绝策略
AbortPolicy
(默认):直接抛出RejectedExecutionException
异常阻止系统正常运行CallerRunsPolicy
:调用者运行的一种机制,该策略既不会抛弃任务,也不会抛出异常,而是将某些任务回退到调用者DiscardOldestPolicy
:抛弃队列中等待最久的任务,然后把当前任务加入到队列中尝试再次提交当前任务DiscardPolicy
:直接丢弃任务,不予任何处理也不抛出异常。如果任务允许丢失,那么该策略是最好的方案
实现多线程的几种方式
- 继承 Thread 类
- 实现 Runnable 接口
- 实现 Callable 接口,使用
FutureTask
类 - 使用线程池
以上三种方式中,前两种方式线程执行完之后没有返回值,第三种有。一般推荐实现Runnable接口的方式,原因如下:Thread类定义了多种方法可以被派生类使用或重写,但是
只有run方法是必须被重写
的,在run方法中实现这个线程的主要功能。所以没有必要继承Thread,去修改其他方法。
ThreadLocal
ThreadLocal
提供了**「线程局部变量」**,一个线程局部变量在多个线程中,分别有独立的值(副本)。
基本API
- 构造函数:
ThreadLocal()
- 初始化:
initialValue()
- 访问器:
get/set
- 回收:
remove()
构造函数是一个泛型的,传入的类型是你要使用的局部变量变量的类型。初始化
initialValue()
用于如果你没有调用set()
方法的时候,调用get()
方法返回的默认值。如果不重载初始化方法,会返回null
。如果调用了set()方法,再调用get()方法,就不会调用
initialValue()
方法。
如果调用了set(),再调用remove(),再调用get(),是会调用initialValue()
的。JDK 8提供了静态方法
withInitial()
来进行更好的初始化。
核心场景
资源持有
比如我们有三个不同的类。在一次Web请求中,会在不同的地方,不同的时候,调用这三个类的实例。但用户是同一个,用户数据可以保存在**「一个线程」**里。
线程一致
sleep() 和 wait() 方法的区别和共同点
-
两者的区别在于
sleep()
方法没有释放锁,wait()
方法释放了锁 -
两者都暂停线程的执行
-
wait()
方法通常使用与线程间的通信,sleep()
一般用于暂停执行 -
wait()
方法使用后,线程不会自动苏醒,需要别的线程调用同一个对象上的notify()
或者notifyAll()
方法来进行唤醒 -
sleep()
方法执行完成后,线程会自动苏醒。或者可以使用wait(long timeout)
超时后线程会自动苏醒。
Java实现高并发的方案
Spring
Spring bean 的生命周期
实例化 -> 属性赋值 -> 初始化 -> 销毁
简单来说,Spring Bean的生命周期只有四个阶段:实例化 Instantiation --> 属性赋值 Populate --> 初始化 Initialization --> 销毁 Destruction
- 实例化 Bean
对于
BeanFactory
容器,当客户向容器请求一个尚未初始化的bean时,或初始化bean的时候需要注入另一个尚未初始化的依赖时,容器就会调用createBean
进行实例化。
对于ApplicationContext
容器,当容器启动结束后,通过获取BeanDefinition
对象中的信息,实例化所有的bean。
-
设置对象属性(依赖注入):实例化后的对象被封装在
BeanWrapper
对象中,紧接着,Spring根据BeanDefinition
中的信息 以及 通过BeanWrapper
提供的设置属性的接口完成属性设置与依赖注入。 -
处理
Aware
接口: -
BeanPostProcessor
前置处理 -
InitializingBean
:如果Bean实现了InitializingBean
接口,执行afeterPropertiesSet()
方法 -
init-method
:如果Bean在Spring配置文件中配置了init-method
属性,则会自动调用其配置的初始化方法。 -
BeanPostProcessor
后置处理:如果这个Bean实现了BeanPostProcessor
接口,将会调用postProcessAfterInitialization(Object obj, String s)
方法;由于这个方法是在Bean初始化结束时调用的,所以可以被应用于内存或缓存技术;以上几个步骤完成后,Bean就已经被正确创建了,之后就可以使用这个Bean了。
-
DisposableBean
:当Bean不再需要时,会经过清理阶段,如果Bean实现了DisposableBean
这个接口,会调用其实现的destroy()方法; -
destroy-method
:最后,如果这个Bean的Spring配置中配置了destroy-method属性,会自动调用其配置的销毁方法。
Spring Bean 的作用域
(1)singleton:默认作用域,单例bean,每个容器中只有一个bean的实例。
(2)prototype:为每一个bean请求创建一个实例。
(3)request:为每一个request请求创建一个实例,在请求完成以后,bean会失效并被垃圾回收器回收。
(4)session:与request范围类似,同一个session会话共享一个实例,不同会话使用不同的实例。
(5)global-session:全局作用域,所有会话共享一个实例。如果想要声明让所有会话共享的存储变量的话,那么这全局变量需要存储在global-session中。
AOP
一般称为面向切面,作为面向对象的一种补充,用于将那些与业务无关,但却对多个对象产生影响的公共行为和逻辑,抽取并封装为一个可重用的模块,这个模块被命名为“切面”(Aspect),减少系统中的重复代码,降低了模块间的耦合度,提高系统的可维护性。可用于权限认证、日志、事务处理。
AOP实现的关键在于 代理模式,AOP代理主要分为静态代理和动态代理。静态代理的代表为
AspectJ
;动态代理则以Spring AOP为代表。
AspectJ
是静态代理,也称为编译时增强,AOP框架会在编译阶段生成AOP代理类,并将AspectJ(切面)织入到Java字节码中,运行的时候就是增强之后的AOP对象。- Spring AOP使用的动态代理,所谓的动态代理就是说AOP框架不会去修改字节码,而是每次运行时在内存中临时为方法生成一个AOP对象,这个AOP对象包含了目标对象的全部方法,并且在特定的切点做了增强处理,并回调原对象的方法。
IOC
IOC
就是控制反转,指创建对象的控制权转移给Spring框架进行管理,并由Spring根据配置文件去创建实例和管理各个实例之间的依赖关系,对象与对象之间松散耦合,也利于功能的复用。DI依赖注入,和控制反转是同一个概念的不同角度的描述,即 应用程序在运行时依赖IoC容器来动态注入对象需要的外部依赖。- 最直观的表达就是,以前创建对象的主动权和时机都是由自己把控的,IOC让对象的创建不用去new了,可以由spring自动生产,使用java的反射机制,根据配置文件在运行时动态的去创建对象以及管理对象,并调用对象的方法的。
- Spring的IOC有三种注入方式 :构造器注入、setter方法注入、根据注解注入。
Spring 声明式事务的传播性
事务的属性
-
propagation
:设置事务的传播性事务的传播行为,一个方法运行在了一个开启了事务的方法中时,当前方法是使用原来的事务还是开启以新的事务
保证在同一事务中:
Propagation.REQUTRED
:默认值,使用原来的事务Propagation.SUPPORTS
:支持当前事务,如果不存在,就不使用事务Propagation.MANDATORY
:支持当前事务,如果不存在,抛出异常
保证不在同一事务中:
Propagation.REQUIRES_NEW
:如果有事务存在,挂起当前事务,创建一个新的事务Propagation.NOT_SUPPORTED
:以非事务方式运行,如果有事务存在,挂起当前事务Propagation.NEVER
:以非事务方式运行,如果有事务存在,抛出异常Propagation.NESTED
:如果当前事务存在,则嵌套事务执行
-
isolation:设置事务的隔离级别
Isolation.REPEATABLE_READ
:可重复读,MySQL默认的隔离级别Isolation.READ_COMMITTED
:读已提交,Oracle默认的隔离级别,开发时通常使用的隔离级别
Spring boot
Spring boot 的自动装配原理
- 通过主启动类的注解
@SpringBootApplication
,这个注解是个复合注解,在它的声明注解中有一个@EnableAutoConfiguration
注解 @EnableAutoCongfiguration
注解声明了一个@Import(AutoConfigurationImportSelector.class)
注解,@Import
注解的参数可以是静态类(用作直接导入)也可以是实现了ImportSelector
接口的类,当是实现了ImportSelector
会根据实现的selectImports
方法来对类进行导入- 在
AutoConfigurationImportSelector
类中,getAutoConfigurationEntry
方法会加载项目配置信息,最后会获取到EnableAutoConfiguration.class
类所在包下的MATE-INF/spring.factories
配置文件
配置文件加载顺序
Spring boot启动会扫描一下位置的application.properties或者application.yml作为默认的配置文件
- 工程根目录: ./config/
- 工程根目录:./
- classpath: /config/
- classpath: /
加载的优先级顺序是从上向下加载,并且所有的文件都会被加载,高优先级的内容会覆盖底优先级的内容,形成互补配置
也可以通过指定配置
spring.config.location
来改变默认配置,一般在项目已经打包后,我们可以通过指令
java -jar xxxx.jar --spring.config.location=/project/config/application.yml
来加载外部的配置
什么是 Spring Boot Stater
启动器是一套方便的依赖没描述符,它可以放在自己的程序中。你可以一站式的获取你所需要的 Spring 和相关技术,而不需要依赖描述符的通过示例代码搜索和复制黏贴的负载。
MySQL
事务的特性
- 原子性
- 隔离性
- 持久性
- 一致性
MySQL的事务隔离级别
READ UNCOMMITTED
:读未提交,允许线程1读取线程2未提交的数据
READ COMMITTED
:读已提交,线程1只能读取线程2 已提交的数据
REPEATABLE READ
:可重复读,确保线程1可以多次从一个字段中读取到相同的值,即当线程1 更新某个字段时,禁止其他线程更新该字段
SERIALIZABLE
:串行化
脏读 | 不可重复读 | 幻读 | MySQL | Oracle | |
---|---|---|---|---|---|
READ UNCOMMITTED | 有 | 有 | 有 | 支持 | 不支持 |
READ COMMITTED | 无 | 有 | 有 | 支持 | 支持(默认) |
REPEATABLE READ | 无 | 无 | 有 | 支持(默认) | 不支持 |
SERIALIZABLE | 无 | 无 | 无 | 支持 | 支持 |
MySQL的索引
索引是为了加速对表中数据行的检索而创建的一种分散存储的数据结构
索引是表的目录,是数据库中专门用于帮助用户快速查询数据的一种数据结构。类似于字典中的目录,查找字典内容时可以根据目录查找到数据的存放位置,以此快速定位查询数据。对于索引,会保存在额外的文件中。
MySQL的索引机制
什么是聚集索引
聚集(clustered)索引:也叫聚簇索引,是指数据行的物理顺序与列值(一般是主键的那一列)的逻辑顺序相同,一个表中只能拥有一个聚集索引。MySQL中一般默认主键为聚集索引。
非聚集(unclustered)索引:该索引中索引的逻辑顺序与磁盘上行的物理存储顺序不同,一个表中可以拥有多个非聚集索引。其实按照定义,除了聚集索引以外的索引都是非聚集索引,只是人们想细分一下非聚集索引,分成普通索引,唯一索引,全文索引。
哈希索引的优势
**等值查询,**哈希索引具有绝对优势(前提是:没有大量重复键值,如果大量重复键值时,哈希索引的效率很低,因为存在所谓的哈希碰撞问题。)
MySQL 的查询优化器
B Tree 和 B+ Tree
B 树:
B树,每个节点都存储key和data,所有节点组成这棵树,并且叶子节点指针为null,叶子结点不包含任何关键字信息。
B+ 树:
- B+Tree的特点
- 每个父节点的元素都会出现在叶子节点中,是叶子结点中的最大(或最小)元素
- 根节点的最大元素也就是整个B+Tree 的最大元素,无论以后插入删除多少个元素,始终都要保证最大元素在根节点中
- 每个父节点都出现在叶子节点中,所以所有叶子节点都包含了全量的元素信息
- 每个叶子节点都带有指向下一个节点的指针,形成一个有序链表
- 卫星数据
卫星数据是指索引元素指向的数据记录,比如数据库中的某一行,在B+Tree中只有叶子节点带有卫星数据,其余节点仅仅是索引,没有数据关联
注意:在数据库的聚集索引中,叶子节点直接包含卫星数据,在非聚集索引中,叶子节点带有指向卫星数据的指针
- B+ Tree的优势
- 单一节点存储更多的元素,查询的IO次数更少
- 所有的查询都要查找叶子节点,查询性能稳定
- 所有叶子节点形成有序链表,便于范围查询
- B+树的特征:
- 有k个子树的中间节点包含有k个元素(B树中是k-1个元素),每个元素不保存数据,只用来索引,所有数据都保存在叶子节点。
- 所有的叶子结点中包含了全部元素的信息,及指向含这些元素记录的指针,且叶子结点本身依关键字的大小自小而大顺序链接。
- 所有的中间节点元素都同时存在于子节点,在子节点元素中是最大(或最小)元素。
数据库引擎
查询计划
Redis
Redis 的基本数据类型
- string
- 常规命令
set key value
get key
mset key1 value1 key2 value2
mget key1 key2
- 递增/递减
incr key
decr key
incrby key
decrby key
- 使用场景
商品编号,订单号采用
incr
命令生成是否喜欢文章,点赞
- hash
对应的 Java:-> Map<String,Map<Object,Object>>
- 一次设置一个字段值
hset key filed value
- 一次获取一个字段值
hget key filed
- 一次设置多个字段值
hmset key filed value [filed value ....]
- 一次获取多个字段值
hmget key filed [filed ...]
- 获取所有字段值
hgetall key
- 获取某个key内的全部数量
hlen
- 删除一个key
hdel
- 应用场景
购物车,对象属性
- list
- 向列表的左边添加元素
lpush key value [value .....]
- 向列表的右边添加元素
rpush key value [value ....]
- 查看列表
lrange key start stop
- 获取列表中的元素个数
llen key
- 应用场景
微信订阅号
- set:
- 添加元素
sadd key member [member ...]
- 删除元素
srem key member [member ...]
- 获取集合中的所有元素
smembers key
- 判断元素是否在集合中
sismembers key member
- 获取集合中的元素个数
scard key
- 从集合中随机弹出一个元素,元素不删除
srandmember key [数字]
- 从集合中随机弹出一个元素,弹出一个删除一个
spop key [数字]
- 集合运算
- 应用场景
抽奖
点赞功能
共同好友推送:
- zset:
简述一下分布式锁 及分布式锁带来的问题
- 使用string 类型的
setnx key value
命令 或者set key value [EX seconds] [PX milliseconds] [NX|XX]
setnx key value
:当key不存在时才创建key
set key value [EX seconds] [PX milliseconds] [NX|XX]
:
EX
:在多少秒之后失效
PX
:在多少毫秒之后失效
NX
:当key不存在时,才才创建key,效果等同于setnx
XX
:当key存在时,覆盖key
Redis 的持久化方案
- 分类
- AOF: 日志方式,存储操作过程,存储格式复杂,关注点在数据的操作过程,记录每一个操作,将Redis有写操作的命令全部记录,只许追加文件但不改写文件,redis启动之初就会读取该文件内容重新构建数据。优点:备份机制完善,丢失数据概率低,可读的日志文本,可以处理误操作。缺点:占用磁盘空间大,恢复速度慢,读写同步话,有一定性能压力
- RDB: 快照,存储数据结果,存储格式简单,关注点在数据,优点:节省磁盘空间,恢复速度快,比AOF高效,缺点:最后一次持久化数据可能会丢失,对数据要求不高可以使用
-
区别
-
启动方式
RDB:使用save
命令执行,使用bgsave
命令执行,使用save
配置
配置相关:
dbfilename dump.rdb # 说明:设置本地数据库文件名,默认值为 dump.rdb,通常设置为dump-端口号.rdb
dir # 设置存储.rdb文件的路径,通常设置成存储空间较大的目录中,目录名称data
rdbcompression yes # 设置存储至本地数据库时是否压缩数据,默认为 yes,采用LZF压缩,通常默认为开启状态,如果设置为no,可以节省 CPU 运行时间,但会使存储的文件变大(巨大)
rdbchecksum yes # 设置是否进行RDB文件格式校验,该校验过程在写文件和读文件过程均进行,通常默认为开启状态,如果设置为no,可以节约读写性过程约10%时间消耗,但是存储一定的数据损坏风险
stop-writes-on-bgsave-error yes # 后台存储过程中如果出现错误现象,是否停止保存操作,通常默认为开启状态
save second changes # 满足限定时间范围内key的变化数量达到指定数量即进行持久化,second:监控时间范围,changes:监控key的变化量
redis数据的淘汰策略?
volatile-lru
:从已经设置过期时间的数据集中,挑选最近最少使用的数据淘汰。volatile-ttl
:从已经设置过期时间的数据集中,挑选即将要过期的数据淘汰。volatile-random
:从已经设置过期时间的数据集中,随机挑选数据淘汰。allkeys-lru
:从所有的数据集中,挑选最近最少使用的数据淘汰。allkeys-random
:从所有的数据集中,随机挑选数据淘汰。no-enviction
:禁止淘汰数据。
什么是缓存雪崩
当缓存服务器重启或者大量缓存集中在某一个时间段失效,这样在失效的时候,会给后端系统带来很大压力。导致系统崩溃
解决方法:
1:在缓存失效后,通过加锁或者队列来控制读数据库写缓存的线程数量。比如对某个 key 只允许一个线程查询数据和写缓存,其他线程等待。
2:做二级缓存,A1 为原始缓存,A2 为拷贝缓存,A1 失效时,可以访问 A2,A1 缓存失效时间设置为短期,A2 设置为长期
3:不同的 key,设置不同的过期时间,让缓存失效的时间点尽量均匀
什么是缓存穿透
一般的缓存系统,都是按照 key 去缓存查询,如果不存在对应的 value,就应该去后端系统查找(比如DB)。一些恶意的请求会故意查询不存在的 key,请求量很大,就会对后端系统造成很大的压力。这就叫做缓存穿透
解决方法:
1:对查询结果为空的情况也进行缓存,缓存时间设置短一点,或者该 key 对应的数据 insert 了之后清理缓存。
2:对一定不存在的 key 进行过滤。可以把所有的可能存在的 key 放到一个大的 Bitmap 中,查询时通过该 bitmap 过滤。
什么是缓存击穿
缓存击穿,是指一个key非常热点,在不停的扛着大并发,大并发集中对这一个点进行访问,当这个key在失效的瞬间,持续的大并发就穿破缓存,直接请求数据库,就像在一个屏障上凿开了一个洞。
解决方案:缓存击穿的话,设置热点数据永远不过期。或者加上互斥锁就能搞定了
Spring Cloud
CAP 理论
C:一致性
A:可用性
P:分区容错性
服务网关 GateWay
什么是网关
网关是整个微服务API请求的入口,负责拦截所有请求,分发到服务上去。可以实现日志拦截、权限控制、解决跨域问题、限流、熔断、负载均衡,隐藏服务端的ip,黑名单与白名单拦截、授权等。
gateway是一个全新的项目,其基于Spring 5.0 以及Spring Boot 2.0和项目Reactor等技术开发的网关,其主要的目的是为微服务架构提供一种简单有效的API路由管理方式.
gateway 的组成
-
路由 : 网关的基本模块,由一个ID,目标URI,一组断言和一组过滤器组成,就是根据某些规则,将请求发送到指定服务上
-
断言:就是访问该路由的访问规则,可以用来匹配来自Http请求的任何内容,例如headers或者参数
-
过滤器:这个就是我们平时说的过滤器,用来过滤一些请求的,gateway有自己默认的过滤器,具体请参考官网,我们也可以自定义过滤器,但是要实现两个接口,ordered和globalfilter,路由前后,过滤请求
断言的类型
Path
:外部访问路径是指定路径,就路由到指定微服务上After
:可以指定,只有在指定时间后,才可以路由到指定微服务Before
:与after类似,他说在指定时间之前的才可以访问Between
:需要指定两个时间,在他们之间的时间才可以访问Cookie
:只有包含某些指定cookie(key,value),的请求才可以路由Header
:只有包含指定请求头的请求,才可以路由Host
:只有指定主机的才可以访问Method
:只有指定请求才可以路由,比如get请求…Query
:必须带有请求参数才可以访问
示例:
spring:
cloud:
gateway:
routes:
- id: after_route
uri: http://ityouknow.com
predicates:
- Path=/api/.../**
- After=2018-01-20T06:06:06+08:00[Asia/Shanghai]
- Before=2018-01-20T06:06:06+08:00[Asia/Shanghai]
- Between=2018-01-20T06:06:06+08:00[Asia/Shanghai], 2019-01-20T06:06:06+08:00[Asia/Shanghai]
- Cookie=ityouknow, kee.e
- Header=X-Request-Id, \d+
- Host=**.**.com
- Method=GET
- Query=xxxx
服务注册发现 Eureka
保证 cap中的 ap,可用性和分区容错性
eureka 的自我保护机制
当Eureka Server 节点在短时间内丢失了过多实例的连接时(比如网络故障或频繁启动关闭客户端)节点会进入自我保护模式,保护注册信息,不再删除注册数据,故障恢复时,自动退出自我保护模式
服务注册发现 zookeeper
服务调用 Ribbon
服务调用 OpenFigen
Spring Cloud Alibaba
Nacos
Sentinel
什么是 sentinel
Sentinel是一个面试分布式架构的轻量级服务保护框架,主要以流量控制、熔断降级、系统负载保护等多个维度
服务接口保护有哪些方案
- 使用服务保护框架Sentinel,hytrix,进行服务限流、熔断、降级
- 黑白名单限制访问
什么是服务限流、服务熔断、服务降级、服务雪崩
服务限流:在接口访问超过设置的阈值,走服务降级
fallback
方法
服务熔断:接口出现异常或者处理时间过长,直接熔断,走服务降级fallback
方法
服务降级:在服务限流或者服务熔断的情况下,走服务降级fallback方法,一段时间内不再走业务逻辑方法
服务雪崩:默认的情况下,一个服务器只有一个线程池,在高并发情况下,一个接口访问次数过多,把线程池线程全部占用了,导致其他接口不可用,造成服务雪崩。黑客攻击同一个接口
解决方法:线程池隔离或者信号量隔离
线程池隔离就是每个接口设置一个线程池,这样占用内存非常大
信号量隔离就是每个接口设置一个阈值,超过阈值直接走服务降级
服务降级有哪三种策略
- rt(平均响应时间):一秒内接口的访问响应时间超过指定阈值,则触发服务熔断,调用服务降级方法,指定时间(时间窗口 秒)内,不能够再次访问接口,一秒内访问五次,五次的平均响应时间超过阈值,最大阈值为4.9秒,要改最大阈值要去改配置
- 异常比例:一秒内请求出现异常的比例超过指定阈值,服务降级,时间窗口(秒
- 异常次数:一分钟内请求出现异常的次数超过指定阈值,服务降级,时间窗口(分钟)
限流配置两种方案
- 手动使用代码配置
- Sentinel控制台形式配置
sentinel规则数据持久化的四种方案
默认情况下Sentinel不对数据持久化,需要自己独立持久化
- Nacos分布式配置中心:有界面,也不用重启服务器,推荐
- 携程阿波罗:有界面,也不用重启服务器,推荐
- ZK,没有界面,不推荐
- 存放在本地文件:不容易修改配置,不推荐
QPS和线程数的区别
QPS是每秒访问次数,线程数就是信号量隔离,一个接口的最大访问线程数量