本文已参与「新人创作礼」活动,一起开启掘金创作之路。
跟一位同事,聊起来平时学习技巧的问题,他谈到一个点,我觉得非常有道理,要有自己的知识体系, 从自己的知识体系出发,不断的由浅入深去扩充丰富自己的体系结构。
诚然,一语惊醒了我,回想一下自己确实在这方面做的比较差,平时学习也是各种找资料,想看什么就看什么,这样就导致学习的东西不具有连贯性,很杂,容易忘记,于是梳理了一下自己当前掌握的以及需要拓展的知识体系,后续会根据这个体系去回顾,去拓展自己的专业知识。
当然,当前列出的知识点只是一部分,后期想到之后会持续补充。
本文可以当作是一本词典,忘了什么知识来回顾就可以了.文章较长,后期知识点整理差不多之后会根据类型分出几篇文章
Java基础知识
Java语言
Java定义
- Java是一种静态 面向对象编程语言
- 具有功能强大和简单易用两个特征
Java SE, Java EE, Java ME 区别
Android 中的java开发环境是Java ME嘛?
Android 中的java环境并不是Java ME, 而是SE. 准确来说, Android 中的java环境是通过部分JavaSE规范设计的SDK, 并且允许使用java系统api.
JVM, JRE, JDK 之间的关系
JVM
- 全称: Java Virtual Machine, java虚拟机
- Java程序必须运行在JVM上
- 通过JVM实现跨平台
JRE
- 全称: Java Runtime Environment
- 包含JVM和java 程序所需的核心类库
- 核心类库主要是java.lang包
- 程序默认回加载该包
- 包含很多系统类
- 基础数据类型
- 基本数学函数
- 字符串处理
- 线程
- 异常处理类
- ...
- 核心类库主要是java.lang包
- 运行已经开发完成的Java程序, 只需要安装JRE
JDK
- 全称: Java Development Kit
- 包含了JRE和Java的开发工具
- 开发工具
- 编译工具(javac.exe)
- 打包工具(jar.exe)
- 开发工具
Java 跨平台性
- 含义
- java程序 一次编译, 可以在多个系统平台运行
- 原理
- Java程序通过JVM在系统平台运行
- 只要系统安装对应的JVM, 就可以运行Java程序
Java语言特点
- 简单易学
- 与C和C++ 语言相近
- 面向对象
- 继承
- 封装
- 多态
- 平台无关性
- 通过JVM实现平台无关性
- 支持网络编程, 且很方便
- Java诞生就是为了简化网络编程设计
- 支持多线程
- 应用程序可以同一时间并行执行多项任务
- 健壮性
- 强类型机制
- 异常处理
- 垃圾自动回收机制
- ...
- 安全性
字节码
- 定义
- Java代码经过JVM编译器编译后产生的文件(即.class文件)
- 只面向JVM, 不面向任何特定的处理器
- 字节码优势
- 一定程度上解决了传统解释性语言执行效率低的问题
- 保留了解释性语言可移植的特点
Java代码运行流程
Java和C++区别
OracleJDK 和 OpenJDK 对比
OracleJDK | OpenJDK | |
---|---|---|
版本发布周期 | 三年 | 三个月 |
是否完全开源 | 是OpenJDK的一个实现,非完全开源 | 是一个参考模型,完全开源 |
稳定性 | 有更多类和错误修复, 更稳定 | - |
响应性和JVM性能 | 性能更好 | - |
是否长期支持 | 不对即将发布的版本提供长期支持, 必须更新最新版本 | 是 |
许可协议 | 二进制代码许可协议 | GPL v2 |
如何确认我们使用的JDK是哪个
-
通过java --version 查看当前JDK版本
-
输出信息OpenJDK会以“OpenJDK”开头, 而OracleJDK会以“Java” 开头
- OpenJDK输出结果
openjdk version "1.8.0_66-internal" OpenJDK Runtime Environment (build 1.8.0_66-internal-b17) OpenJDK 64-Bit Server VM (build 25.66-b17, mixed mode)
- OracleJDK 输出结果
java 11.0.18 2023-01-17 LTS Java(TM) SE Runtime Environment 18.9 (build 11.0.18+9-LTS-195) Java HotSpot(TM) 64-Bit Server VM 18.9 (build 11.0.18+9-LTS-195, mixed mode)
问题积累
问题:变量存储位置
- 函数中定义的基本类型变量和对象的引用变量(即局部变量)存储在虚拟机栈对应的函数栈内存中,栈中数据可共享
- 堆内存中则是几乎全部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中添加一个新的字符串,并返回这个新字符串的引用
趣味问题 Java中的String类型到底能有多长
解析
- 这个问题是半开放性质的,首先我们要识别出我们要用什么知识点来解决这个问题
- 这个问题简单剖析一下,String到底有多长这个具体是指的字符还是字节,创建String类型的方法有多少,会有区别吗,字符和字节的对应关系时什么,java中限制String的因素又是啥
- 如果我们能通过这道题联想出这些问题,相信我们的整体思路或者说我们解决问题的思路非常好,那么这道题的答案是什么呢?
- String的最大长度其实受几个因素影响
- 我们创建String是通过字面量创建的还是通过io等其他无法在编译器就确定String到底是什么的方法
- java中, 字符和字节对应关系是怎样的,其实这个才是我想聊的事情
引出问题
java中的字符和字节的转换关系主要是通过字符集实现,并且受编码方式(utf-8,utf-16等)影响,详细讲解可以参考旭锐大神的[juejin.cn/post/712639… Unicode 和 UTF-8 说清楚) 讲的很好,看完之后对于字符和字符集的认识又提高了
运算
隐式类型转换
精度高的类型无法直接转换成精度低的类型
- 使用+=或者++运算符会执行隐式类型转换
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 |
集合
集合已经整合到单独博文 「Android」 自建知识体系 - 集合篇
多线程
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) 共享式释放同步状态
- 实现原理
- 独占式锁获取
首先调用自定义同步器实现得tryAcquire(),该方法保证线程安全得获取同步状态,如果同步状态获取失败,则构造同步节点(独占式Node.EXCLUSIVE,同一时刻只能有一个线程成功获取同步状态)并通过addWaiter方法将该节点加入到同步队列的尾部,最后调用acquireQueued方法,使得该节点以"死循环"的方式获取同步状态。如果获取不到则阻塞节点中的线程,而被阻塞线程的唤醒主要依靠前驱节点的出队或阻塞线程被中断来实现。
- 尝试获取同步状态 tryAcquire()
- 如果成功了,则直接返回,表示已经获取了锁
- 如果失败了:
- 构造节点
- 通过CAS将节点设置为尾节点
- 开启自旋,不断去检测前驱节点是否为头节点,然后获取同步状态
- 如果获取失败,线程进入等待状态,线程阻塞,直到线程被中断或前驱节点被释放。
- 独占式锁释放
- 唤醒头节点的后继节点线程
- 使用unparkSuccessor方法使用LockSupport来唤醒处于等待状态的线程
- 独占式锁获取
首先调用自定义同步器实现得tryAcquire(),该方法保证线程安全得获取同步状态,如果同步状态获取失败,则构造同步节点(独占式Node.EXCLUSIVE,同一时刻只能有一个线程成功获取同步状态)并通过addWaiter方法将该节点加入到同步队列的尾部,最后调用acquireQueued方法,使得该节点以"死循环"的方式获取同步状态。如果获取不到则阻塞节点中的线程,而被阻塞线程的唤醒主要依靠前驱节点的出队或阻塞线程被中断来实现。
ReentrantLock
-
可重入锁,持有锁的线程可以继续持有,并要释放同等次数之后才能完全释放锁
-
支持获取锁时的公平和非公平选择
-
获取锁
- 判断当前是否可以获取锁,如果可以直接获取
- 判断是否当前线程为当前获取锁的线程,如果是可重入数+1,获取成功
-
释放锁
- 锁被获取了n次,只有在第n次调用tryRelease才会返回true,之前都会返回null,当同步状态为0时才会最终释放锁,并将占有线程设置为null,返回true,表示释放成功
公平锁和非公平锁
- 公平锁 绝对时间上,先对锁进行获取的请求一定先满足
- 非公平锁 先对锁进行获取的请求不一定先满足
- 公平锁和非公平锁的区别
- 非公平锁 只要CAS设置同步状态成功,则表示当前线程获取了锁
- 公平锁 在当前锁没有被占用时(即同步状态为0),会先去检测当前队列中是否有前驱节点,如果有,则需要等待前驱节点获取并释放锁后才能获取锁
- 优缺点
当一个线程请求锁时,只要获取了同步状态即成功获取锁。在此前提下,刚释放锁的线程再次获取同步状态几率会非常大。
- 非公平锁可能会使线程饥饿,但是减少了线程切换,保证了更大的吞吐量
- 公平锁严格按照锁的请求顺序获取,代价是大量的线程切换
读写锁
特性
- 公平性选择
- 重进入
- 锁降级 遵循获取写锁,获取读锁再释放写锁的次序,写锁能够降级成为读锁
- 读锁是共享锁,写锁是排他锁
读写状态保存
- 通过一个整形变量维护读写状态,高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刷新机制
CoordinatorLayout
CoordinatorLayout(协调布局, 依赖布局) 从名字上就能看出来它的作用应该就是协调子view的一个布局, 再直白点,就是如果我有两个view想绑定在一起实现一些动画效果就可以采用这个.
它的使用还是非常简单的, 主要元素一共有三个dependency(CoordinatorLayout 子view), childView(CoordinatorLayout 子view, 需要设置behavior), behavior(确认dependency和childView的关系,并且实现childView动画效果)
基础的原理就是dependency实现动画效果,然后通过对childView设置behavior实现动画联动(只需要记住这句话就能分清协调布局的结构了), 详细可参考CoordinatorLayout使用和源码分析
先看一下效果
简单实例: 实现随手动的view
- 布局
<androidx.coordinatorlayout.widget.CoordinatorLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
xmlns:app="http://schemas.android.com/apk/res-auto">
<Button
android:id="@+id/dependency"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="我是被依赖的view"
android:layout_gravity="center"
/>
<TextView
android:id="@+id/text"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="我是依赖的view"
app:layout_behavior=".TestBehavior"/>
</androidx.coordinatorlayout.widget.CoordinatorLayout>
- 自定义behavior
class TestBehavior(val context: Context, attrs: AttributeSet)
: CoordinatorLayout.Behavior<TextView>() {
override fun layoutDependsOn(
parent: CoordinatorLayout,
child: TextView,
dependency: View
): Boolean {
// 判断设置该behavior的childView是否依赖dependency
return dependency is Button
}
override fun onDependentViewChanged(
parent: CoordinatorLayout,
child: TextView,
dependency: View
): Boolean {
// 当dependency发生变化时触发
child.x = dependency.x;
child.y = dependency.y + 150
// true: childView发生了变化
return true;
}
}
- 实现被依赖布局滑动效果
// 被依赖布局
dependency.setOnTouchListener { v, event ->
when (event.action) {
MotionEvent.ACTION_DOWN -> {
lastX = event.x
lastY = event.y
}
// 实现被依赖布局随手滑动
MotionEvent.ACTION_MOVE -> {
v.translationX = v.translationX + event.x - lastX
v.translationY = v.translationY + event.y - lastY
}
}
true;
}
动画
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
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,而直接返回,但是此时对象并没有初始化,所以返回的对象是有问题的
- 创建对象(new )的过程实际上需要三步
静态内部类实现单例
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动画
JSBridge
android与js调用方式
安卓原生和H5交互的方式主要通过WebView容器实现,其中js和android的交互流程相对简单,具体例子可以在网上查阅
android调用js
- webview.loadUrl()
- 无法执行回调方法
- webview.evaluateJavascript()
- 可以传入回调方法
js调用android
- 注入API,通过webview.addJavascriptInterface()
- 响应速度快
- android版本小于4.2可能存在安全漏洞,可以通过注解 @JavascriptInterface解决,但是存在版本兼容问题, 由于市面上目前基本已经没有4.2以下设备所以可以认为当前漏洞已经修复
- url拦截,通过webview的shouldOverrideUrlLoading通过url进行拦截
- 响应速度慢
- 通过webview的回调onConsoleMessage,onJsPrompt,onJsAlert,onJsConfirm等方法进行拦截
对于jsBridge框架很多,原理也是离不开上面的几个根本,但是要想彻底理解还是需要花费功夫的,鄙人不睬目前理解的还是不够透彻无法输出分析,在这里提供几个人为讲解的比较到位全面的文章项目必备功能之JsBridge源码解析, webview之JSB通信原理, 简化Android与JS交互,JsBridge框架全面解析
工具
其他语言
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])