本文已参与「新人创作礼」活动,一起开启掘金创作之路。
跟一位同事,聊起来平时学习技巧的问题,他谈到一个点,我觉得非常有道理,要有自己的知识体系, 从自己的知识体系出发,不断的由浅入深去扩充丰富自己的体系结构。
诚然,一语惊醒了我,回想一下自己确实在这方便做的比较差,平时学习也是各种找资料,想看什么就看什么,这样就导致学习的东西不具有连贯性,很杂,容易忘记,于是梳理了一下自己当前掌握的以及需要拓展的知识体系,后续会根据这个体系去回顾,去拓展自己的专业知识。
当然,当前列出的知识点知识一部分,后期想到之后会持续补充。
Java基础知识
问题积累
问题:变量存储位置
- 函数中定义的基本类型变量和对象的引用变量(即局部变量)存储再虚拟机栈对应的函数栈内存中,栈中数据课共享
- 堆内存中测试几乎全部new出来的对象,数组,和对象的实例变量
数据类型
java八大基本类型
- byte / 8
- char / 16
- short / 16
- int / 32
- float / 32
- long / 64
- double / 64
- boolean / ~
每一个基本类型都涉及到装箱拆箱
- 每一个基本数据类型的包装类都不能被继承
缓存池
- 在Java8中,Integer缓存池的大小默认为-128~127
- boolean 缓冲池: true & false
- byte: 所有byte值
- short: -128 ~127
- int:-128 ~127
- char: \u0000 ~ \u007F
JDK1.8中,Integer得缓冲池IntegerCache上界127可以通过jvm参数指定
String
- String被声明为final,因此它不可被继承
- 在Java8中,String内部使用char数组存储数据
- 在Java9之后,String类的实现改用byte数组存储字符串,同时使用coder来标识使用了哪种编码
- 重要:value(即存储字符串数据的全局变量)被声明为final,并且 String内部也没有 能修改value数组的方法,因此可以保证String不可变
String 不可变的意义
- 保证hash相同
不可变可以使hash值同样不可变,因此只需要进行一次计算,并保存到String对象中
- String Pool的需要
String对象已经创建过后,就会从String Pool中获取引用,如果String可变,会导致引用错误。
- 安全性
String经常作为 参数,String不可变可以保证参数不可变。
- 线程安全
不可变即具备了线程安全性
String,StringBuffer,StringBuilder
- 可变性
- String 不可变
- StringBuffer 和StringBuilder可变
- 线程安全
- String线程安全
- StringBuilder非线程安全
- StringBuffer线程安全,内部使用synchronized进行同步
String Pool
- 字符串常量池(StringPool)保存着所有字符串字面量(literalstrings),这些字面量在编译时期就确定
- 可以使用String的intern()方法在运行过程将字符串添加到StringPool中
- 当一个字符串调用intern()方法时,如果StringPool中已经存在一个字符串和该字符串值相等(使用equals()方法进行确定),那么就会返回StringPool中字符串的引用;否则,就会在StringPool中添加一个新的字符串,并返回这个新字符串的引用
运算
隐式类型转换
精度高的类型无法直接转换成精度低的类型
- 使用+=或者++运算符会执行隐式类型转换
switch
从Java7开始,可以在switch条件判断语句中使用String对象
java特性
重写
子类实现了一个与父类在方法声明上完全相同的一个方法 为了满足里式替换原则,重写有以下三个限制:
- 子类方法的访问权限必须大于等于父类方法;
- 子类方法的返回类型必须是父类方法返回类型或为其子类型。
- 子类方法抛出的异常类型必须是父类抛出异常类型或为其子类型
内部类
详细总结,参考海子大佬的博客Java内部类详解
分类
- 静态内部类
- 与静态方法,静态变量类似,不属于对象,即不依赖与外部类
- 不能使用外部类的非静态成员变量和方法
- 创建时无需创建外部类,直接创建该类即可
public class InnerClass {
static int sInt = 1;
String mString = "";
/**
* 静态内部类
* 调用方式:
* InnerClass.StaticInner inner = new InnerClass.StaticInner();
* inner.get();
*/
static class StaticInner {
public void get() {
System.out.println("static get");
}
}
}
复制代码
- 成员内部类
- 成员内部类默认持有外部类对象
- 内部类可以无条件访问外部类的所有成员属性和方法(包括私有和静态的),外部类访问内部类则必须创建对象
- 当成员内部类中的属性和方法与外部类同名时,会发生隐藏现象,即默认会访问成员内部类中的成员。
直接引用即是内部类的成员,Outter.this.name,这种就是调用外部类的成员
- 局部内部类
局部内部类的作用域在定义它的作用域内 - 类似与局部变量,不能被访问修饰符,static修饰符修饰
- 匿名内部类
即没有名字,没有构造方法的内部类 - 必须继承某些类,或者实现某些接口 - 不能被访问修饰符和static修饰符修饰
相关问题
为什么成员内部类可以无条件访问外部类的成员
基本原理就是因为成员内部类会持有外部类的对象,也就可以访问外部类的成员 在编译过程中,编译器会将内部类单独编译成独立的字节码文件,名字为Outter$Inner.class,字节码文件中会有只想外部类对象的指针,该指针是在构造器初始化时赋值,编译器默认会添加一个外部类参数来创建内部类
为什么局部内部类和匿名内部类只能访问局部final变量
访问变量时可能存在出现内部类和内部类中使用的变量生命周期不一致问题,常见场景为创建匿名Thread场景,编译器通过进行拷贝或者通过构造器传参的方式对拷贝进行赋值
局部变量值如果在编译器内可以确定,则创建一个等值的拷贝,如果值无法确定,则通过构造器传参的方式对拷贝进行初始化赋值
通过上述方法可以解决生命周期不同问题,但是如果拷贝之后在匿名内部类中修改了该值就会出现数据不一致问题,所以编译器限制必须使用final变量
java内部类的使用场景
- 每个内部类都能独立的继承一个接口的实现,所以无论外部类是否已经继承了某个(接口的)实现,对于内部类都没有影响。内部类使得多继承的解决方案变得完整,
- 方便将存在一定逻辑关系的类组织在一起,又可以对外界隐藏。
- 方便编写事件驱动程序
- 方便编写线程代码
泛型
- 编译期会对类型进行检查,在使用时才会确认具体类型
- JDK1.5后加入
- 分类
- 泛型类,泛型接口
- 泛型方法
泛型类
实例化时可以指定泛型,也可以不指定泛型,如果不指定,泛型会被擦除,默认为Object,指定时要遵循指定对象类型
- 泛型类中的静态方法无法使用泛型(指在泛型类中定义的泛型)
? extends Person
表示泛型受限,限制的是泛型的上限,表示当前泛型中的类必须继承于Person类
? super Person
表示泛型受限,限制的是泛型的下限,表示当前泛型中的类必须是Person的父类
Math类常用方法
不知道你们啥样,我是总忘啊,开根号,绝对值总是傻傻分不清楚,每次使用都是找api或者在网上找,今天突然想到,方法名一定跟英语关联,于是今天整理一下加深自己记忆
方法名 | 中文含义 | 英文含义 | 备注 |
---|---|---|---|
Math.sqrt() | 平方根 | square root | |
Math.cbrt() | 立方根 | Cube root | |
Math.pow(4, 3) | 次方 | Power | 4的三次方 |
Math.max(10, 9) | - | - | |
Math.min( , ) | - | - | |
Math.abs() | 绝对值 | absolute value | |
Math.ceil(a) | - | - | 比a大的最近的整数 |
Math.floor(a) | - | - | 比a小的最近的整数 |
Math.rint() | - | - | 四舍五入 返回double 遇到0.5取偶数 e.m: 10.5-> 10 |
Math.round() | 四舍五入 float返回int double返回long |
集合
各个集合对比
ArrayList和Vector
- Vector是线程安全的,而ArrayList线程不安全
- Vector实现了同步,所以开销要比ArrayList大,访问速度慢
- Vector扩容默认为2倍,ArrayList为1.5倍
- 实现ArrayList同步可以通过Collections.sychronizedList(arrayList)得到一个线程安全得ArrayList;或者使用CopyOnWriteArrayList
ArrayList和LinkedList
- ArrayList基于动态数组实现,LinkedList基于双向链表实现
ArrayList
- 底层数组实现
- 默认长度为10
- 只会序列化当前有元素的数组位置,并不会将数组整体序列化
1.7与1.8中的区别
- 调用无参构造器时,1.7会创建长度为10的数组,而1.8会创建一个空数组
扩容
private void grow(int minCapacity) {
// overflow-conscious code
int oldCapacity = elementData.length;
// 重点:扩容为原来的1.5倍
int newCapacity = oldCapacity + (oldCapacity >> 1);
if (newCapacity - minCapacity < 0)
newCapacity = minCapacity;
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);
}
复制代码
添加元素
- 在数组最后添加元素
public boolean add(E e) {
// 先去检测是否需要扩容,如果需要调用grow()方法进行扩容
ensureCapacityInternal(size + 1); // Increments modCount!!
elementData[size++] = e;
return true;
}
复制代码
- 在数组指定index处添加元素
public void add(int index, E element) {
// 判断index合法性
rangeCheckForAdd(index);
// 检测是否需要扩容
ensureCapacityInternal(size + 1); // Increments modCount!!
// 将数组中的index以后的元素全部向后移动一位
System.arraycopy(elementData, index, elementData, index + 1, size - index);
// 将要添加的元素添加到index处
elementData[index] = element;
size++;
}
复制代码
删除元素
- 按照index删除元素
public E remove(int index) {
// 判断index合法性
rangeCheck(index);
modCount++;
E oldValue = elementData(index);
int numMoved = size - index - 1;
// 将index+1以后的数据向前移动一位到index
if (numMoved > 0)
System.arraycopy(elementData, index+1, elementData, index, numMoved);
// 将最后一位元素赋空
elementData[--size] = null; // clear to let GC do its work
return oldValue;
}
``
- 按照存储元素进行删除
```java
public boolean remove(Object o) {
// 检测是否当前要删除的元素是空的
// 如果元素为null,遍历数组将第一个null元素删除
// 如果元素不为null,遍历数组将对应元素删除
if (o == null) {
for (int index = 0; index < size; index++)
if (elementData[index] == null) {
fastRemove(index);
return true;
}
} else {
for (int index = 0; index < size; index++)
if (o.equals(elementData[index])) {
fastRemove(index);
return true;
}
}
return false;
}
private void fastRemove(int index) {
modCount++;
int numMoved = size - index - 1;
if (numMoved > 0)
System.arraycopy(elementData, index+1, elementData, index,
numMoved);
elementData[--size] = null; // clear to let GC do its work
}
复制代码
Fail-fast机制
modCount用来记录ArrayList结构变化次数,添加或者删除元素,或者调整内部数组的大小都会导致结构变化,在进行序列化或者迭代过程需要比较操作前后modCount是否变化,如果变化会抛出ConcurrentModificationException
Vector
与ArrayList类似,区别在于使用synchronized实现同步
扩容
private void grow(int minCapacity) {
// overflow-conscious code
int oldCapacity = elementData.length;
// capacityIncrement 是构造函数中传入的扩容值,如果不传入默认为0,则将之前容量扩容2倍
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);
}
复制代码
CopyOnWriteArrayList
- 写操作是通过在一个复制的数组上进行,读操作在原数组中进行
- 写操作需要加锁,防止并发写入出现数据丢失情况
- 写操作结束之后将原始数据指向新的复制数组
适用场景
- 写的同时可以进行读取操作,大大提高了读取的性能,使用读多写少场景
缺陷
- 内存占用过高
写操作需要复制新数组,是内存占用为原来的两倍
- 数据不一致
读操作不能实时读取数据,存在写入操作未同步到数组中的情况
get
private E get(Object[] a, int index) {
return (E) a[index];
}
复制代码
remove
public E remove(int index) {
final ReentrantLock lock = this.lock;
lock.lock();
try {
Object[] elements = getArray();
int len = elements.length;
E oldValue = get(elements, index);
int numMoved = len - index - 1;
if (numMoved == 0)
setArray(Arrays.copyOf(elements, len - 1));
else {
Object[] newElements = new Object[len - 1];
System.arraycopy(elements, 0, newElements, 0, index);
System.arraycopy(elements, index + 1, newElements, index,
numMoved);
setArray(newElements);
}
return oldValue;
} finally {
lock.unlock();
}
}
复制代码
add
public boolean add(E e) {
final ReentrantLock lock = this.lock;
lock.lock();
try {
Object[] elements = getArray();
int len = elements.length;
Object[] newElements = Arrays.copyOf(elements, len + 1);
newElements[len] = e;
setArray(newElements);
return true;
} finally {
lock.unlock();
}
}
复制代码
LinkedList
LinkedList实际上就是一个双向链表,可以实现队列,栈等功能,具体操作都是基于双向链表节点操作,相对简单,API分析可以查看 LinkedList源码分析
HashMap
详细解读,请参照 烟雨星空大神的HashMap底层原理
HashMap 数据结构
- 1.7中是以数组 + 链表形式存储
- 1.8中是以数组 + 链表(或者红黑树)存储
重要参数
- 容量 capacity
- 必须是2的n次幂,如果传入了不是2的n次幂的容量,则会通过tableSizeFor()方法转换成大于容量的最小2的n次幂,如传入14,容量会被设置成16
- 最大容量: 1<<30,即2^32
- 实际存放元素个数 size
- 数组中元素个数
- 数组扩容阈值 threshold
- 数组元素size达到阈值,进行resize(),将容量扩大
- 默认负载因子 loadFactor
- 0.75
负载因子选取0.75,是对空间和时间的权衡,如果太小,空间利用率低,导致频繁扩容;如果太大,hash冲突概率增高,影响效率,增加操作元素时间
- 链表与红黑树转换阈值
- 只有当数组容量达到64时,才会出现红黑树结构
- 当链表长度达到8,链表会转换成红黑树
- 当红黑树元素减少到6个时,会退化成链表
- fail-fast机制保障 modCount
hash()
(key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16)
- 通过key的hash值与hash值右移16位异或,保证了高16位的特性,同时将高16位也添加到计算当中,降低hash冲突概率
- 异或运算预期值(0或1)的概率比为1:1
tableSizeFor()
该方法确保了map的容量必须是2的n次幂,实现算法是将n-1,然后将n-1 分别无符号右移1,2,4,8,16 位并与n的每一位相或确保,所有位数都能变成1,然后再 + 1就能得到大于当前值的最小2的n次幂
put() 添加元素
- 判断数组是否为空,如果是空则进行resize()扩容
- 通过 (n-1)&hash确认元素在数组中bucket的位置
- 如果当前数组中为null,表示没有元素,则直接将值添加到该位置
- 如果当前数组中元素不是null
- 如果当前hash值与当前元素相同,则将当前元素覆盖
- 如果当前hash值不同,并且是红黑树结构,则将值添加到红黑树中
- 如果当前hash值不同,并且非红黑树结构,即是链表,则便利链表,采用尾插法(JDK 1.8)将值添加到链表尾部
- 在插入链表过程中,如果链表长度超过8,则将当前链表转换成红黑树
- 如果发生了碰撞,需要替换新值,则将旧值返回。方法结束
- fail-fast机制,++modCount
- 检测当前元素个数是否超过阈值,需要扩容
resize() 扩容
- 扩容将之前的容量左移一位,达到2倍之前容量的数组,
- 判断之前的数组是否为null,如果非null,说明之前存在元素,需要进行重新分配元素
- 遍历旧数组
- 如果当前数组元素非null,且元素的next节点为null,说明只有一个元素,则重新hash()和新数组容量取模,得到新下标位置即可
- 否则如果当前是红黑树结构,则拆分红黑树,必要时可能会退化成链表
- 否则说明当前元素是链表,将链表拆分,通过 (e.hash & oldCap) == 0 算法将链表元素分配到当前下标或者(当前下标+老数组容量)的位置
(e.hash & oldCap) == 0 算法的核心就是,扩容之后,确定下标的hash值不变,但是由于扩容2倍原因,导致,参与计算的位数增加一位,为老数组容量的位置,所以,只需要判断老数组容量的位置如果是0,保持原位,为1将元素移动到(当前下标+老数组容量)位置
get() 获取元素
- 检测数组不为null,并且当前下标元素不为null
- 检测hash值是否与第一个元素相同
- 如果不同,检测是否是红黑树,遍历红黑树
- 如果是链表,遍历找到链表结尾
LinkedHashMap
- 实质上与HashMap原理相同,额外的在HashMap的基础上添加了一个双链表来维护有序性
- 节点除了next指向链表中的下一个节点外,额外添加了before,after节点,用于维护双链表中顺序
- 有两种排序方法:HashMap中有accessOrder来标记排序顺序
- 默认为false,表示维护插入顺序
- true表示按照最近使用情况排序,即LRU算法,最近使用的放在链表尾部,最久未使用在链表头部
重点方法
实现思路与HashMap基本相同,我们重点看维护双向链表的方法,无外乎就是如何添加,删除,更新节点在数组中的位置
put,添加方法
LinkedHashMap中并未实现put方法,也就是说他使用的是他父类HashMap中的put(), 但是此时又分成两种情况,map中是否已经存在该节点,即要添加的节点,是需要新增还是更新即可,新增指的是bucket桶中并无元素或者bucket桶中有元素,但是当前桶中链表不存在该元素。
- 新增节点情况
// HashMap#put()
if ((p = tab[i = (n - 1) & hash]) == null)
// 我们来看一下在添加一个map中没有的数据时会创建新节点,LinkedHashMap重写了newNode()方法
tab[i] = newNode(hash, key, value, null);
// LinkedHashMap#newNode(yuansu)
Node<K,V> newNode(int hash, K key, V value, Node<K,V> e) {
// 创建新节点
LinkedHashMap.Entry<K,V> p = new LinkedHashMap.Entry<K,V>(hash, key, value, e);
// 这一步应该就是操作双向链表
linkNodeLast(p);
return p;
}
// LinkedHashMap#linkNodeLast()
// 我们可以很清晰的看出来他就是在操作双向链表,将添加的yuan'su
private void linkNodeLast(LinkedHashMap.Entry<K,V> p) {
LinkedHashMap.Entry<K,V> last = tail;
tail = p;
if (last == null)
head = p;
else {
p.before = last;
last.after = p;
}
}
复制代码
- 更新节点情况
我们依旧从put方法下手,
final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
boolean evict) {
...
if ((p = tab[i = (n - 1) & hash]) == null)
tab[i] = newNode(hash, key, value, null);
else {
Node<K,V> e; K k;
...
// e正常来讲已经添加到map中, 所以一定会不为null
if (e != null) { // existing mapping for key
V oldValue = e.value;
if (!onlyIfAbsent || oldValue == null)
e.value = value;
afterNodeAccess(e);
return oldValue;
}
}
++modCount;
if (++size > threshold)
resize();
afterNodeInsertion(evict);
return null;
}
复制代码
这里面我们看到有两个方法,第一个是 afterNodeAccess() 一个是afterNodeInsertion(), 我们先一个一个看
- afterNodeAccess
HashMap中默认为空方法,LinkedHashMap中实现了该方法
void afterNodeAccess(Node<K,V> e) { // move node to last
LinkedHashMap.Entry<K,V> last;
if (accessOrder && (last = tail) != e) {
LinkedHashMap.Entry<K,V> p = (LinkedHashMap.Entry<K,V>)e, b = p.before, a = p.after;
p.after = null;
if (b == null)
head = a;
else
b.after = a;
if (a != null)
a.before = b;
else
last = b;
if (last == null)
head = p;
else {
p.before = last;
last.after = p;
}
tail = p;
++modCount;
}
}
复制代码
- 简单总结来说就是
- 如果使用的是LRU算法,则需要更新双向链表中的节点位置,即将当前节点的前后节点相连(相当于删除当前节点),并将当前节点添加到尾部;
- 如果使用的是默认插入算法,则什么也不需要操作
- afterNodeInsertion()
// 同样, 该方法也是LinkedHashMap中实现的HashMap的空方法, 但是removeEldestEntry() 方法默认为空,不会执行
// 可以看到该方法的作用就是将双向链表第一个元素,即最久没有使用的元素删除
void afterNodeInsertion(boolean evict) { // possibly remove eldest
LinkedHashMap.Entry<K,V> first;
if (evict && (first = head) != null && removeEldestEntry(first)) {
K key = first.key;
removeNode(hash(key), key, null, false, true);
}
}
复制代码
get()
public V get(Object key) {
Node<K,V> e;
// getNode方法就是HashMap中获取节点方法,不赘述
if ((e = getNode(hash(key), key)) == null)
return null;
// 如果是LRU排序,则调用上述说的afterNodeAccess方法, 进行更新双向链表;如果是默认插入顺序,则什么也不做
if (accessOrder)
afterNodeAccess(e);
return e.value;
}
复制代码
remove()
final Node<K,V> removeNode(int hash, Object key, Object value, boolean matchValue, boolean movable) {
Node<K,V>[] tab; Node<K,V> p; int n, index;
if ((tab = table) != null && (n = tab.length) > 0 && (p = tab[index = (n - 1) & hash]) != null) {
Node<K,V> node = null, e; K k; V v;
//查找要删除的节点
...
if (node != null && (!matchValue || (v = node.value) == value || (value != null && value.equals(v)))) {
if (node instanceof TreeNode)
((TreeNode<K,V>)node).removeTreeNode(this, tab, movable);
else if (node == p)
tab[index] = node.next;
else
p.next = node.next;
++modCount;
--size;
// 重点, 看这个方法
afterNodeRemoval(node);
return node;
}
}
return null;
}
// afterNodeRemoval() 就是将当前节点在双向链表中的前后节点相连,即删除当前节点。
void afterNodeRemoval(Node<K,V> e) { // unlink
LinkedHashMap.Entry<K,V> p =
(LinkedHashMap.Entry<K,V>)e, b = p.before, a = p.after;
p.before = p.after = null;
if (b == null)
head = a;
else
b.after = a;
if (a == null)
tail = b;
else
a.before = b;
}
复制代码
多线程
join 方法 (Thread中方法)
aThread.join();
复制代码
- join的作用是:使aThread线程强制获取CPU资源,强制运行,直到aThread线程结束,其他线程才能继续执行;但是在调用join()之前,多个线程还是通过抢占式不一定哪一个线程执行
synchronized
详细分析见胖虎大神博文synchronized
java中的锁基于对象,并且保存在对象头中,称为mark word空间
- 当对象状态为偏向锁时,空间中存储偏向的线程ID
- 当对象状态为轻量级锁时,空间中存储只想线程栈中Lock Record的指针
- 当对象状态为重量级锁时,空间存储指向堆中monitor对象的指针
synchronized实现原理
- 同步方法和同步块synchronized实现有略微不同,本质都是通过monitor实现
- 同步方法属于隐式方法实现,无需通过字节码实现。通过ACC_SYNCHRONIZED标识符实现,当JVM检测到方法带有ACC_SYNCHRONIZED标志时,执行线程会先获取monitor,获取成功后才会执行方法体,方法执行之后释放monitor
- 基于java对象监视器(monitor)实现,通过使用monitorenter和monitorexit两个指令实现
- monitorenter指令是插入在同步的开始位置
- monitorexit指令是插入在同步的结束位置和异常位置
- monitorenter和monitorexit通常来说会成对出现,但是存在多个monitorexit多的情况
多个monitorexit指令的原因:编译器需要确保每一个monitorenter执行之后都要执行monitorexit指令,为了保证方法异常时也满足该条件,编译器会自动产生一个异常处理器来执行异常的monitorexit。
- monitorenter获取monitor所有权过程:
- 如果monitor进入数为0,该线程进入monitor,并将进入数设置为1该线程即是monitor的所有者
- 如果当前线程已经占用该monitor,只是重新进入,则进入数加1
- 如果其他线程占用monitor,线程进入阻塞状态,知道monitor进入数为0,再重新尝试获取
- monitorexit放弃monitor所有权过程:
- monitor进入数减1
- 如果进入数为0,则线程退出monitor,不再持有,其他被该monitor阻塞的线程可以尝试去获取monitor所有权
锁升级与锁优化
- 锁的状态一共分为四种
- 无锁状态
- 偏向锁
- 轻量级锁
- 重量级锁
- 随着锁的竞争,锁可以从偏向锁升级为轻量级锁,再升级为重量级锁
- 锁升级是单项的,只能从低到高升级,不能出现锁降级,锁升级单项的目的是为了提高获取锁和释放锁的效率
锁的比对
锁 | 优点 | 缺点 | 使用场景 |
---|---|---|---|
偏向锁 | 加锁和解锁不需要额外的消耗 | 线程间存在锁竞争,会带来额外的锁撤销的消耗 | 只有一个线程访问同步块场景 |
轻量级锁 | 竞争的线程不会阻塞,提高了程序的响应速度 | 得不到锁竞争的线程自旋消耗CPU | 追求响应时间,锁占用时间短 |
重量级锁 | 线程竞争不使用自旋,不会消耗CPU | 线程阻塞,响应时间缓慢 | 追求吞吐量,锁占用时间较长 |
#### 偏向锁 |
- 偏向锁的核心思想
- 如果一个线程首次获得了对象,虚拟机将会把对象头标志位设为“01”,那么就进入偏向模式,此时Mark Word 的结构也变为偏向锁结构,同时使用CAS操作把当前对象头Mark Word里存储了当前偏向线程的线程ID
- 当这个线程再次请求同步块时,无需再做任何同步操作,即获取锁的过程,这样就省去了大量有关锁申请的操作,从而也就提供程序的性能
- 如果当有线程出现竞争时,哪怕就来一个竞争者,锁就会不认可这种偏向模式了,也就是偏向锁就失效了
- 偏向锁比较脆弱,如果偏向锁线程不再活动,则会将对象头设置成无所状态
- 如果有另外的线程去尝试获取这个锁,偏向锁模式结束,锁会升级为轻量级锁
- 偏向锁插销流程
- 在一个安全点停止拥有锁的流程
- 遍历线程栈,如果存在锁记录的话,需要修复锁记录和Mark Word,使其变成无锁状态
- 唤醒被停⽌的线程,将当前锁升级成轻量级锁
轻量级锁
- 轻量级锁加锁流程
- 虚拟机会在无所或者偏向锁状态下,将当前对象mark work拷贝到当前线程栈帧中。
- 当前线程尝试使用CAS操作将对象中的mark work更新指向Lock Record的指针
- 如果成功,当前线程获得锁,当前对象处于轻量级锁状态
- 如果失败,表示当前锁存在其他线程竞争,线程会尝试自旋获取资源,如果自选达到最大次数都没有获取锁,线程阻塞,锁升级为重量级锁
java 虚拟机中采用得自适应自旋,如果当前自旋成功,则下次自旋次数会更多,如果自旋失败,则自旋次数会减少
- 轻量级锁解锁流程
- 采用CAS将拷贝到栈帧得mark work内容复制会锁的mark work里面
- 如果没有发生竞争,CAS复制操作成功
- 如果锁因为其他线程自旋多次导致锁升级为重量级锁,则CAS操作失败,此时释放锁并唤醒被阻塞的线程。
- 采用CAS将拷贝到栈帧得mark work内容复制会锁的mark work里面
锁粗化
同步范围理论上要越小越好,这样其他等待线程可以尽快获取锁,但是某些场景需要将同步范围扩大,即锁粗化,如最典型的在循环体内的同步可以放到循环体外进行,这样就避免了频繁的加锁,解锁
锁消除
虚拟机在运行期会对代码的同步操作进行检测,消除一些不必要的同步操作,如不可能存在共享数据的竞争,增加无用锁等。
volatile
作用
保证可见性和防止指令重排序
流程
- 如果对声明了volatile的变量进行写操作,JVM就会向处理器发送一条Lock前缀的指令,讲这个变量所在缓存行的数据写回到系统内存。
- 在多处理器下,为了保证各个处理器的缓存是一致的,实现缓存一致性协议,每个处理器通过嗅探在总线上传播的数据来检查自己缓存的值是否过期,
- 如果发现缓存行对应的内存地址被修改,会将当前缓存行设置为无效状态,
- 当处理器对数据进行操作时,会重新从系统内存中把数据读到处理器缓存中
如何保证可见性和防止指令重排
- 可见性
- 代码转换成汇编代码时,会在指令前添加Lock前缀,Lock前缀在多核处理器下会引发两个事情
- 当前处理器缓存行的数据写回到系统内存
- 这个写回内存的操作会使在其他CPU里缓存了该内存地址的数据无效。
- 代码转换成汇编代码时,会在指令前添加Lock前缀,Lock前缀在多核处理器下会引发两个事情
- 防止指令重排
- 在转化为字节码时,会在指令前后添加内存屏障,保证对读写的顺序控制
CAS
- 乐观锁
- 每次去尝试获取值,如果内存地址的值与预期 原值相同,则将值更新成新值,否则不断去尝试
实现原理
- CompareAndSwap,比较交换,CPU原子指令,作用是让CPU先进行比较两个值是否相等,然后原子得更新某个位置的值
- 实现方式是基于硬件平台的汇编指令,在intel的CPU中,使用的是cmpxchg指令
- 当多个线程同时使用CAS 操作一个变量时,只有一个会胜出,并成功更新,其余均会失败。失败的线程不会挂起,仅是被告知失败,并且允许再次尝试(自旋),当然也允许实现的线程放弃操作。基于这样的原理,CAS 操作即使没有锁,也可以发现其他线程对当前线程的干扰
- 通过UnSafe的ComapreAndSwapInt等方法实现
CAS存在的问题
- ABA问题
如果内存地址的值修改了两次,如预期原值为A,内存中的值显示修改为B,再修改为A,则CAS会认为他没有修改过,直接更新目标值 - java1.5中提供了AtomicStampedReference来解决ABA问题,通过
- 循环时间长,开销大
- 只能保证一个共享变量的原子操作
同时操作多个共享变量时,CAS无法保证原子性 - AtomicReference类来保证引用对象之间的原子性,你可以把多个变量放在一个对象里来进行CAS操作
Lock
Lock接口的实现通过聚合了一个同步器的子类来完成线程访问控制
API
主要需要调用Lock接口的加锁,解锁方法
- void lock()
获取锁
- void lockInterruptibly() throws InterruptedException
- 如果没有其他线程持有锁,或者当前线程已持有锁,获取锁并立即返回
- 如果其他线程持有锁,当前线程休眠直到获取到锁,或者其他线程中断当前线程
- 如果当前线程获取到锁,锁的进入数设置为1
- boolean tryLock()
- 成功获取锁会返回true,否则返回false
- boolean tryLock(long time, TimeUnit unit) throws InterruptedException
- 在给定时间内获取锁,超时返回false,可被中断
- void unlock()
- 释放锁
推荐用法模板
lock.lock();
try {
// manipulate protected state
} finally {
lock.unlock();
}
复制代码
在java.util.concurrent包内
共有三个实现: ReentrantLock
ReentrantReadWriteLock.ReadLock ReentrantReadWriteLock.WriteLock
- 特点:
- lock更灵活,可以自由定义多把锁的加锁解锁顺序(synchronized要按照先加的后解顺序)
- 提供多种加锁方案,lock 阻塞式, trylock 无阻塞式, lockInterruptily 可打断式, 还有trylock的带超时时间版本
- 本质上和监视器锁(即synchronized是一样的)
- 能力越大,责任越大,必须控制好加锁和解锁,否则会导致灾难
- 和Condition类的结合
- 性能更高
Lock接口提供得synchronized关键字不具备得主要特征
- 尝试非阻塞地获取锁
- 能被中断地获取锁
- 超时获取锁
队列同步器
AbstractQueuedSynchronizer是队列同步器,使用一个int成员变量表示同步状态,通过内置的FIFO队列来完成资源获取线程的排队工作
- 主要方法
需要重写的模板方法来实现独占式,共享式获取同步状态 - getState() 重写方法中调用此方法获取当前同步状态 - setState(int newState) 重写方法中调用此方法设置当前同步状态 - compareAndSetState(int expect,int update) 使用CAS设置当前状态,能够保证设置的原子性 - tryAcquire(int arg) 独占式获取同步状态,实现该方法需要查询当前状态并判断同步状态是否符合预期,然后再CAS地设置同步状态 - tryRelease(int arg) 独占式释放同步状态,等待获取同步状态的线程将有机会获取同步状态 - tryAcquireShared(int arg) 共享式获取同步状态,返回大于等于0的值表示获取成功,反之失败 - tryReleaseShared(int arg) 共享式释放同步状态
- 实现原理
- 独占式锁获取
- 独占式锁释放
- 唤醒头节点的后继节点线程
- 使用unparkSuccessor方法使用LockSupport来唤醒处于等待状态的线程
ReentrantLock
-
可重入锁,持有锁的线程可以继续持有,并要释放同等次数之后才能完全释放锁
-
支持获取锁时的公平和非公平选择
-
获取锁
- 判断当前是否可以获取锁,如果可以直接获取
- 判断是否当前线程为当前获取锁的线程,如果是可重入数+1,获取成功
-
释放锁
- 锁被获取了n次,只有在第n次调用tryRelease才会返回true,之前都会返回null,当同步状态为0时才会最终释放锁,并将占有线程设置为null,返回true,表示释放成功
公平锁和非公平锁
- 公平锁
绝对时间上,先对锁进行获取的请求一定先满足
- 非公平锁
先对锁进行获取的请求不一定先满足
- 公平锁和非公平锁的区别
- 非公平锁
- 公平锁
- 优缺点
当一个线程请求锁时,只要获取了同步状态即成功获取锁。在此前提下,刚释放锁的线程再次获取同步状态几率会非常大。 - 非公平锁可能会使线程饥饿,但是减少了线程切换,保证了更大的吞吐量 - 公平锁严格按照锁的请求顺序获取,代价是大量的线程切换
读写锁
特性
- 公平性选择
- 重进入
- 锁降级
遵循获取写锁,获取读锁再释放写锁的次序,写锁能够降级成为读锁
- 读锁是共享锁,写锁是排他锁
读写状态保存
- 通过一个整形变量维护读写状态,高16位表示读,低16位表示写
- 通过位运算获取状态
写锁的获取和释放
- 流程
- 如果当前线程已经获取了写锁,则增加写状态
- 如果当前线程再获取写锁时,读锁已经被获取或者该线程不是已经获取写锁得线程,则当前线程进入等待状态
- 特点
- 只有等待其他读线程都释放了读锁,写锁才能被当前线程获取
- 写锁一旦获取,其他读写线程的后续访问均被阻塞
读锁的获取和释放
- 流程
- 如果当前已经获取了读锁,则增加读状态
- 如果当前线程再获取读锁时,写锁已被其他线程获取,则进入等待状态
- 特点
- 如果其他线程已经获取了写锁,则当前线程获取读锁失败,进入等待状态
- 如果当前线程获取了写锁或者写锁未被获取,则当前线程通过CAS增加读状态,成功获取读锁
锁降级
锁降级指把持住(当前拥有的)写锁,再获取到读锁,随后释放(先前拥有的)写锁的过程 目的是为了保持可见性
LockSupport 工具
使用LockSupport可以阻塞或唤醒线程
ThreadLocal
原子类(AtomicInteger、AtomicBoolean……)
AtomicReference
Android基础知识
架构
Android系统开机启动流程
Android 10.0 系统启动之SystemServer进程
五大组件
四大组件+Fragment
Activity
启动流程
详细博客 app以及activity启动流程分析
onSaveInstanceState和onRestoreInstanceState
即使onSaveInstanceState触发,也不一定onRestoreInstanceState就会触发。onSaveInstanceState在activity可能会被杀死时调用,而onRestoreInstanceState则是activity确确实实已经非正常情况正常关闭的场景时,会触发。
只有在xml中定义了id的view才会默认将内容缓存
onSaveInstanceState调用时机
onSaveInstanceState在onPause之后,在onStop之前
在之前android版本中,onSaveInstanceState会在onStop之前调用,而在android较新版本中,如9.0中,则Activity跳转等场景下,一定会触发onSaveInstanceState,并且在onStop之后()
- 用户按下HOME键
- 应用中运行其他应用
- 锁屏
- 当前activity启动新的activity
- 屏幕旋转
onRestoreInstanceState
onRestoreInstanceState在onStart之后,onResume之前。
Fragment
Fragment特点
- 模块化
- 可重用
- 可适配
控件的使用
常见的用法,使用技巧,常见问题,容易遇到的坑
基础Layout(FrameLayout, RelativeLayout,LinearLayout, ConstraintLayout)
ViewPager, ListView, ScrollView, RecyclerView等
RecyclerView
真正带你搞懂 RecyclerView 的缓存机制,再也不怕面试被虐了 www.jianshu.com/p/443d741c7… www.jianshu.com/p/1d2213f30… RecyclerView刷新机制
动画
view的绘制流程
Android显示底层机制
View与Window的逻辑关系
- ViewRootImpl是连接WindowManager和DecorView的纽带
- View的三大绘制流程都是通过ViewRoot完成
- ViewRootImpl在attachActivity,即Activity的onCreate之前创建
- 当Activity创建完成,会将DecorView添加到Window中
View的大致流程
从ViewRootImpl的performTraversals开始,依次调用performMeasure,performLayout和performDraw,分别又调用measure,layout,draw方法。
measure过程
- View通过measure完成测量,通过measure实现自定义测量
- ViewGroup除了自己的测量还需要遍历所有子节点的measure去递归执行测量流程
View的measure过程
- measure是final方法
- onMeasure默认无法区分AT_MOST和EXACTLY的SpecMode,都会将size设置位specSize
ViewGroup的measure过程
- ViewGroup中的measureChildren方法中会遍历的触发子view的measure,
通过取出子view的layoutParams构建子view的MeasureSpec,然后将MeasureSpec传入到子View中的measure完成子view的测量
- ViewGroup并没有实现默认的测量过程,需要各个子类去实现
如何获取View的宽高
- Activity/Viw#onWindowFocusChanged
- view.post(runnable)
- ViewTreeObserver
- view.measure 只适用于view的Layoutparams为具体数值或者wrap_content
layout过程
- layout方法确认View本身位置
- onLayout确认所有子元素的位置(具体ViewGroup会实现,View中为空实现)
layout的大致流程
- 通过setFrame()设置View的四个顶点位置,即初始化mLeft,mRight, mTop,mbottom
- 调用onLayout确认子元素位置
在View的默认实现中,View的测量宽高和最终宽高是相等的,区别在于赋值时机不同,测量宽高在measure过程,最终狂高形成于View的layout过程,即确认了四个顶点之后。
draw过程
draw流程
- 绘制背景 background.draw(canvas)
- 绘制自己 onDraw
- 绘制children dispatchDraw
- 绘制装饰 onDrawForeground onDrawScrollBars等
自定义 View
- 让view支持wrap_content onMeasure实现
- 让view支持padding draw中处理padding
- 尽量不要在view中使用handler
- view中如果有动画或线程,要及时停止,在View#onDetachedFromWindow处理
- 处理好滑动冲突
自定义属性
- 创建自定义属性配置
<declare-styleable name="CircleView">
<attr name="circle_color" format="color" />
</declare-styleable>
复制代码
- view的构造方法中解析自定义属性的值,并处理
TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.CirclrView);
mColor = a.getColor(R.styleable.CircleView_circle_color, Color.RED);
a.recycle();
复制代码
- 布局文件中使用自定义属性,并添加schemas声明
事件分发
点击事件的分发过程由三个很重要的方法来共同完成
- dispatchTouchEvent
- 用来进行事件分发,事件传递到当前view,此方法一定会被调用
- 返回结果受当前onTouchEvent方法和下级View的dispatchTouchEvent方法影响
- 表示是否消耗当前事件
- onInterceptTouchEvent
- 用来判断是否拦截某个事件
- 如果拦截,同一事件序列中,此方法不会再次调用
- 表示是否拦截当前事件
- onTouchEvent
- 用来处理点击事件
- 表示是否消耗当前事件
- 如果不消耗,同一序列中,当前View无法再次接受到事件
大致流程
public boolean dispatchTouchEvent(MotionEvent ev) {
boolean isConsume = false;
if(onInterceptTouchEvent(ev)) {
isConsume = onTouchEvent(ev);
} else {
isConsume = child.dispatchTouchEvent(ev);
}
return isConsume;
}
复制代码
多进程
- 多进程优点
- 安全
- 解决内存不够
Binder
- 只拷贝一次 通过MMAP 内存映射
- 数据接收方 和内核空间的虚拟地址映射的物理地址相同。
JNI与NDK
框架
组件化
插件化
热修复
mvc,mvp,mvvm
MVC
工作原理
用户触发事件 - view层传递指令到controller - controller去通知model层更新数据 - model层更新数据之后直接显示在view层
缺点
- activity既是view层用于动态更新布局,又用于controller层去更新model,导致activity过于繁重
- view和model层需要通信,导致无法完全解耦,增加维护成本
MVP
工作原理
- 基于MVC,将model和view完全解耦,中间通过presenter桥接
- activity或者fragment实现接口,presenter通过接口调用方法
- 用户触发事件 - view层将事件传递到presenter - presenter操作model - model将数据返回presenter - presenter将数据返回给view展示
特点
- model和view完全解耦,通过presenter对接,但是presenter层持有view层引用
如何在Presenter中实现线程切换
view中调用presenter,presenter调用model,去请求网络(切换到子线程),通过回调(需要处理ui,当前还在子线程)返回到presenter层,需要处理ui应该如何操作 如果使用开源框架Rxjava等,可以通过Rxjava实现动态的切换,如果自行实现可以通过Handler在presenter层实现,但是具体的切换线程时机在哪里要更好一些,是在Presenter中还是在网络 请求结束,触发回调的时候就已经切换回来?
优秀框架
MVPArms
demo地址
很简单的demo,意在体验MVP的搭建过程,可以作为参考,如果有问题或建议,欢迎留言讨论
MVVM
工作原理
- 基于MVC, view 和viewmodel相互绑定,更新viewmodel时,view自动变动
- viewmodel层不再持有view层引用,进一步降低耦合,view层代码的修改不影响viewmodel
databinding,viewbinding
设计模式
单例模式
单例双重检测
先展示一下代码
public class SingleTon {
private volatile static Instance instance;
public static Instance getInstance(){
if(instance ==null) {
synchronized (SingleTon.class) {
if (instance == null)
instance = new Instance();
}
}
return instance;
}
}
复制代码
双重检测主要涉及到几个问题:
- 为什么会有两个判空
- 第一个判空是为了防止每次调用getInstance()都要去走同步块,影响性能
- 第二个判空是为了防止创建多个对象
如现在两个线程A,B同时调用getInstance(),同时走到了第一个判空位置,此时为空,两个线程均走入到if代码块内,此时由于synchronized关键字限制,第一个抢占资源的线程会创建对象,创建对象结束之后会结束同步块,此时第二个线程进入同步块,同样会创建一个单例对象,导致单例模式出现了多个对象
- volatile关键字的作用
此处的作用是防止指令重排,关于volatile关键字的解释可以参照大神海子的博客 volatile关键字解析 - 创建对象(new )的过程实际上需要三步 - 1.申请内存 - 2.初始化对象 - 3.将引用指向内存 如果没有volatile关键字,由于虚拟机会进行编译优化进行指令重排可能执行顺便变更为1-3-2,如果线程A按照此顺序执行到步骤3后,此时线程B执行第一个判空检测,发现并不为null,而直接返回,但是此时对象并没有初始化,所以返回的对象是有问题的
静态内部类实现单例
public class SingleTon{
private SingleTon(){}
private static class SingleTonHolder {
private static final SingleTon INSTANCE = new SingleTon();
}
public static SingleTon getInstance() {
return SingleTonHolder.INSTANCE;
}
}
复制代码
- 原理
只有在调用getInstance()第一次调用时,才会进行初始化SingleTonHolder,去加载INSTANCE,虚拟机保证了类的()的线程安全性。
- 缺点
没办法传递参数,如Context
生产者消费者
package com.learning.lib;
import java.util.LinkedList;
import java.util.Queue;
public class KnowledgePointTest {
private static final int MAX_LENGTH = 10;
private Queue<Integer> que = new LinkedList<>();
private int productId = 0;
public static void main(String[] args) {
KnowledgePointTest obj = new KnowledgePointTest();
Producer producer = obj.new Producer();
Costomer costomer = obj.new Costomer();
producer.start();
costomer.start();
}
// 生产者
class Producer extends Thread {
@Override
public void run() {
while (true) {
synchronized (que) {
// 生产足够, 将生产线程阻塞,等待消费者消费
if (que.size() == MAX_LENGTH) {
try {
System.out.println("仓库已满");
que.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
} else {
// 生产商品, 同事通知所有持有que monitor的线程
que.add(productId++);
System.out.println("生产 :" + productId + " 商品");
que.notifyAll();
}
}
}
}
}
// 消费者
class Costomer extends Thread {
@Override
public void run() {
while (true) {
synchronized (que) {
// 无商品, 阻塞消费线程,等待生产者生产
if(que.size() == 0) {
try {
System.out.println("没货了");
que.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
} else {
// 消费商品, 同时通知生产者生产
int cur = que.poll();
System.out.println("消费了:" + cur + ", 还剩下:" + que.size());
que.notifyAll();
}
}
}
}
}
}
复制代码
开源框架
OKhttp
图片加载
Lottie动画
工具
其他语言
kotlin
flutter
算法
动态规划
- 动态规划问题的一般类型就是求最值
- 核心问题是穷举找最值
- 动态规划存在重复子问题和最优子结构
- 难点为写出状态转移方程
- 通常解决动态规划问题核心框架为:
- 明确状态
- 定义dp数组/函数的含义
- 明确选择
- 明确base case
回溯算法
解决回溯问题,实际上就是对决策树的遍历过程,需要注意三个问题
- 路径
已经做出的选择
- 选择列表
当前可以做的选择
- 结束条件
到达决策树底层,无法再做选择的条件
代码框架
result = []
def backtrack(路径,选择列表):
if 满足结束条件:
result.add(路径)
return
for 选择 in 选择列表:
做选择
backtrack(路径, 选择列表)
撤销选择
复制代码
BFS算法
- 本质就是找到最短距离
代码框架
// 计算从起点start到终点target的最近距离
int BFS(Node start, Node target) {
Queue<Node> q; // 核⼼数据结构
Set<Node> visited; // 避免⾛回头路
q.offer(start); // 将起点加⼊队列
visited.add(start);
int step = 0; // 记录扩散的步数
while (q not empty) {
int sz = q.size();
/* 将当前队列中的所有节点向四周扩散 */
for (int i = 0; i < sz; i++) {
Node cur = q.poll();
/* 划重点:这⾥判断是否到达终点 */
if (cur is target)
return step;
/* 将 cur 的相邻节点加⼊队列 */
for (Node x : cur.adj())
if (x not in visited) {
q.offer(x);
visited.add(x);
}
}
/* 划重点:更新步数在这⾥ */
step++;
}
}
复制代码
股票买卖问题框架
主要思想
每天都有三种「选择」:买⼊、卖出、⽆操作,我们⽤buy, sell, rest 表⽰这三种选择。但问题是,并不是每天都可以任意选择这三种选择的,因为 sell 必须在 buy 之后,buy 必须在 sell 之后。那么 rest 操作还应该分两种状态,⼀种是 buy 之后的 rest(持有了股票),⼀种是 sell 之后的 rest(没有持有股票)。⽽且别忘了,我们还有交易次数 k 的限制,就是说你 buy 还只能在 k > 0 的前提下操作
然后我们⽤⼀个三维数组就可以装下这⼏种状态的全部组合:
dp[i][k][0 or 1] 0 <= i <= n-1, 1 <= k <= K n 为天数,⼤ K 为最多交易数 此问题共 n × K × 2 种状态,全部穷举就能搞定。 for 0 <= i < n: for 1 <= k <= K: for s in {0, 1}: dp[i][k][s] = max(buy, sell, rest)
状态转移方程
dp[i][k][0] = max(dp[i-1][k][0], dp[i-1][k][1] + prices[i]) max(选择 rest, 选择 sell) 解释:今天我没有持有股票,有两种可能: 要么是我昨天就没有持有,然后今天选择 rest,所以我今天还是没有持有; 要么是我昨天持有股票,但是今天我 sell 了,所以我今天没有持有股票了。 dp[i][k][1] = max(dp[i-1][k][1], dp[i-1][k-1][0] - prices[i]) max(选择 rest , 选择 buy) 解释:今天我持有着股票,有两种可能: 要么我昨天就持有着股票,然后今天选择 rest,所以我今天还持有着股票; 要么我昨天本没有持有,但今天我选择 buy,所以今天我就持有股票了。
base case
dp[-1][k][0] = 0 解释:因为 i 是从 0 开始的,所以 i = -1 意味着还没有开始,这时候的利润当然是 0 。 dp[-1][k][1] = -infinity 解释:还没开始的时候,是不可能持有股票的,⽤负⽆穷表⽰这种不可能。 dp[i][0][0] = 0 解释:因为 k 是从 1 开始的,所以 k = 0 意味着根本不允许交易,这时候利润当然是 0 。 dp[i][0][1] = -infinity 解释:不允许交易的情况下,是不可能持有股票的,⽤负⽆穷表⽰这种不可能。
框架
base case:
dp[-1][k][0] = dp[i][0][0] = 0
dp[-1][k][1] = dp[i][0][1] = -infinity(表示不可能)
状态转移⽅程:
dp[i][k][0] = max(dp[i-1][k][0], dp[i-1][k][1] + prices[i])
dp[i][k][1] = max(dp[i-1][k][1], dp[i-1][k-1][0] - prices[i])
复制代码