Java基础
关键点:
String(immutable、性能、intern、StringBuilder、StringBuffer)
HashMap(散列表-哈希算法、ArrayMap、SparseMap、HashTable、HashSet)
哈希算法
* JVM(intrinsic、String特殊优化的本地代码)
对象创建
分配内存
地址空间初始化
设置对象头
初始 => 场景和父类顺序,子类对象的成员变量初始化在父类后执行,父类init中调用了某方法,子类实现该方法并且使用了成员变量,会导致使用在初始化之前。 => 实战空指针
引用入栈,指向该对象
值传递
Java方法调用无论参数是基本数据类型还是引用,都是值传递。
Person a = new Person();
change(a);
public void change(Person p){
p = new Person(); // 不会改变a的内存地址
}
String
特点、性能、优点、intern、StringBuilder、StringBuffer
1、String的不变性
- 线程安全
- 常量池
1. final char[] value -> final byte[] value (拉丁语不需要这么char宽)
2. str = new String(“TODAY”) "TODAY"会加入到常量池,str在堆中
2、性能
- 增删频繁的情况下需要使用StringBuffer
- StringBuffer
1. 线程安全
1. synchronized实现
2. 适合场景
1. Http请求参数拼接
2. xml解析
3、优点
- 可读性好
4、StringBuilder => 内存抖动
- 拼接,在编译器里面会变成StringBuilder
- 线程不安全
- 性能高
- 性能优化:for循环中做String的+=操作,会新建StringBuilder对象,再toString。导致内存抖动
5、intern
- 常量池中找到直接返回,不存在就存储在常量池并且返回其引用
- JDK随着版本变化,字符串常量池从JDK6永久代(方法区) -> JDK7堆 -> JDK8元空间(方法区的新实现)
- 字符串排重?JDK8。相同的String只会在字符串常量池有一个
- intern大量使用,可能导致OOM
HashMap
HashMap(特点、容量、负载因子、扩容、散列函数)=> ArrayMap、SparseMap
1、HashMap
- 特点:O(1)
- 实现自Map => 和Collections无关(Queue、Set、List)
- 容量:16
- 负载因子:0.75
- 扩容:2n
- Entry:key、value、next、hash 四个字段
- Composable:性能提升
- 实现:get、put(key无,插入新值;key存在,新值替换旧值,返回旧值)
- 哈希算法:>= 8 采用红黑树,核心本质是抵抗散列碰撞攻击,导致性能衰退。即使退化,性能也是Ologn <8 链表,避免树转换的开销
- hashCode(): 将内部内存地址转为整数(地址唯一)
2、HashMap哈希函数的技巧
- 高位和低位,异或,保证敏感性,随机性,均匀
- 除留余数法,可以理解为低位掩码,也就是达到取模的效果。
3、HashMap-fast-fail机制
- 内部有modCount
- 非线程安全
- hashNext()遍历时,检查modCount是否和期望值一样,不一样就报错
4、LinkedHashMap
- 双向链表:保证有序性
- 散列:高效无序
- 方便实现LruCache和DiskLruCache
5、WeakHashMap
- 内部集合了WeakReference和引用队列
- 可以在连续两次GC后,检查是否还存在,存在则有内存泄漏
6、扩容规则,超过负载因子,且发生哈希冲突。
7、转为红黑树条件:达到8个节点,且数组大小>64,<=64会扩容。
散列表
1、如何获取到高质量的散列函数
- 不可推导
- 敏感
- 冲突少
- 高效
2、Hash算法的场景
- 安全加密 => AES、DES、SHA => 字典攻击(加盐) => 区块链(头、体)SHA256
- 唯一标识 => MD5找图
- 数据校验 => BT算法
- 散列函数
- 负载均衡 => 同一个客户的所有请求路由到同一个Server上
- 数据分片 => (1)统计搜索关键字出现的次数 1TB 数据 => MapReduce (2)快速判断图片是否在图库中(一亿图片)
- 分布式存储 => 数据用多设备存储 => 扩容 => 雪崩
1. 一致性哈希函数 => 环、环偏斜、虚拟节点 => 场景:网络CPC、git commit id
一致性哈希算法
1、是什么?
- hash上引入环形
- 虚拟节点
- 核心:对2^32取模
2、解决:分布式hash表中动态伸缩的问题 - 所有缓存都失效->雪崩,都去后端请求
- 优化后:服务器增加减少(少部分缓存失效)
3、虚拟节点是为了解决什么问题? - 环倾斜/环偏移
布隆过滤器
1、是什么?
- 概率数据结构
- 只判断key是否存在,不存储具体数据,占用空间小
- 有误差
- 不支持删除
2、场景:巨大文件、巨大数据库、缓存系统、爬虫,寻找key是否存在
3、适合条件:(1)判定一定不在集合中 (2)判定可能在集合中
4、元素
K:hash函数个数
m:布隆过滤器长度
n:插入元素个数
p:误报率
5、复杂度:
插入、查询O(k)
空间O(m)
6、如何选择k和m?
m=-(n*lnp)/(ln2)^2
k=m/n*ln2
==>Google Guava提供BloomFilter
ConcurrentHashMap
1.7:segment数据结构+HashEntry数组组成,分段锁,ReentrantLock
1.8:粒度变大,不再分段,数据结构简单,但是实现更复杂。基于synchronized
ArrayMap
key和value都是对象,稀疏数组
SparseArray
SparseArray
- 节约了27%内存
- key = int, value = Object
- 稀疏数组 => 插入,二分法。删除,标记。
- 不是原型模式:原型模式更多的侧重于通过克隆来创建独立的对象,并且可以灵活地修改对象的属性。
性能优化
场景:profile火焰图,GC 5s,MemoryAllocate定位,内存抖动
解决:byte[]很多->Glide内部的LruMap->Integer很多->SparseArray优化
HashTable
@Depricate,性能很差,synchronized实现
HashSet
HashSet
- 只存储元素(对象)
- 非线程安全
- 底层用HashMap实现
ConcurrentHashMap
final关键字
- 方法内联
- 实现不可变类和集合
- finally => try-with-resources
- finalize => 性能有问题 => 守护线程Finalizer处理
- Cleaner机制,也不推荐,无法及时执行
动态代理
- 语言类型 => 动态、静态、强、弱
- 反射 => setAccessible
- 场景:AOP、框架
1. Retrofit:用于创建网络请求的代理类,用来发送请求
2. Dagger2:动态代理来生成依赖注入的代码
3. EventBus:生成订阅者的代理类,事件发生时,通过代理类调用其方法 - 实现:cglib和JDK,JDK在重构后不再采用反射实现,ASM实现,性能一致
接口
- 设计原则:接口隔离、依赖倒置
- 存储区域
- 标记接口Marker Interface => Annotation
- 函数式接口:一个方法
- default method:接口也允许有默认实现的方法,方便扩展不修改所有实现者
抽象类
- 类的访问权限修饰符
- 特点
- 抽象方法
- 抽象
原始数据类型
优点:
- 性能高
- 数组,地址连续
隐式转换
包装类
- 数组,地址不连续
- 线程安全:
1. AtomicLongFieldUpdater 保护long字段安全
2. AtomicXXX - 货币:BigDicimal
Integer
- 自动装箱/拆箱 => 反射性能
- Integer.valueOf int转为包装类
优点:
缺点:
缓存机制
- IntegerCache 缓存-128~127
- Byte
- Character
- Boolean
- Short
- Integer
String缓存机制:常量池,不变性,安全
引用类型
可达性:强、软、弱、虚、不可达
引用队列
虚引用
可达性栅栏
// 在可达性栅栏之前的操作对垃圾回收器可见
Reference.reachabilityFence(sharedObject);
// 在可达性栅栏之后的操作
- 避免对象实例方法在执行完成前,对象已经被GC。某些属性还需要使用 => 没有强引用,也先不GC
- Reference.reachabilityFence(excutor) 线程池等经常需要异步调用的,需要可达性栅栏
IO
BIO
NIO
NIO2/AIO
BIO
特点
- 流
- 带缓冲区IO
- JDK 1.4后底层用NIO 重构
BIO方式 - 字符流 I/O
- 字节流 R/W-有缓冲区flush/close
- RandomAccessFile-随机文件访问
File:本质是文件路径,叫FilePath更准确
BIO服务器结构:
1个Thread -> 1个Socket -> 1个Channel
NIO
1、NIO组成部分
- Channel-OS底层机制-性能优化-DMA(Direct Memory Access)
- Buffer-NIO操作数据的基本工具
- Selector-多路复用(一个线程处理多个连接)(单线程轮询,不适合大量耗时操作)
- Scatter/Gather-分散/聚集,将消息拆分为消息头和消息体
2、NIO服务器结构
keys -> Selector -> N个SockectChannel(N个客户端)
3、NIO和BIO的区别
- NIO,多个请求顺序处理,耗时操作会阻塞。
- BIO,适合大量耗时操作
4、NIO节省了线程切换的开销
5、DougLeo推荐多个Slector,在多个线程,并发监听Socket
ByteBuffer
- HeapByteBuffer
- flip 翻转
- DirectByteBuffer
DirectByteBuffer
1、关键词
- 堆外内存
- Unsafe API提供
- 不受堆大小限制,受到实际内存大小限制
- 底层unsafe_allocatememory
- 性能高:避免了用户空间和内核空间,data传输的消耗
2、如何创建堆外内存
allocateDirect()
=>COW 写时拷贝技术
3、DirectBuffer优点
- 适合长期使用
- 适合数据量大
4、HeapBuffer优点
- 短期使用
- 数据量小
DirectBuffer的GC
- GC时机无法预测
- 一般在full gc
- 基于Cleaner机制和虚引用
文件拷贝
BIO => FileSystemProvider
NIO => 零拷贝技术 srcFileChannel.transferTo(dstFileChannel)
零拷贝技术:4次 copy 下降到 2次Copy
4次copy:
- 磁盘A->内核
- 内核->B用户空间
- B用户空间->内核
- 内核->磁盘B
2次copy:
- 磁盘A->内核缓存
- 内核缓存->磁盘B1
异常
1、Throwable-Exception-Error
2、Error-JVM错误,无法恢复
3、Exception-RuntimeException
4、throws异常声明
5、throw抛出异常
6、ClassNotFoundException(异常) => ARouter => 插件优化查找类的开销(ASM)
- 类加载阶段,找不到Class
- 例如:一个类被某个ClassLoader加载到内存中,另一个ClassLoader也尝试加载,会报错
class.forName
classLoader.findSystemClass
classLoader.loadClass
7、NoClassDefFoundError(LinkageError)
- 类的链接阶段,找不到Class(运行时,内存中找不到)
- Android的编译环境和运行时环境不一样
1. 插件化、使用第三方SDK、动态加载或实例化类,失败
2. 【so中找不到,armabi、v7、v8 中缺少了so容易出现】=> NDK
3. 手机系统版本低,class在低版本系统中不存在
4. 分dex,dex中删除了该类(同一)
5. 系统资源紧张,需要大量加载class,需要竞争,加载失败
6. 【类初始化失败,静态变量顺序要保证,初始化了才能使用】静态代码块抛出ExceptionInitialError后,继续引用该变量 => 类初始化,静态代码块顺序
7. 类依赖的class.jar不存在 => 什么情况下会出现?
Java并发
synchronized
- 关键字
- 释放锁(自动)
- 方法和代码块
- 公平
关键点:
- 实现:在同步代码块前后生成字节码指令monitorEnter和monitorExit(该指令需要引用reference类型参数,用于lock和unlock)
- 指明对象:对象加锁
- 不指明对象:实例方法(实例对象)、类方法(类对象)
- 可重入锁 => 避免死锁
- 无法中断等待(因synchronized等待,其他线程执行了interupt也不能中断)、无法超时退出、无法强制有锁线程释放锁
- 重量级操作(锁升级到重量级锁后)
1. 阻塞和唤醒由操作系统完成,涉及用户态和内核态 切换
2. 简单方法,会出现切换消耗比代码执行还多 - 锁的升级降级:不支持
Lock
-
isHeldByCurrentThread
-
intercept可中断
-
hasQueuedThread 获取等待的线程
-
tryLock 尝试获得锁/非阻塞
-
读写锁
-
释放锁(手动)
-
任何地方,不可以加给方法
-
可重入 => 文件锁
-
公平/非公平 => CLH => AQS
-
可重入、可中断、非阻塞、公平锁
-
ReentrantLock可以绑定多个对象 ==> Condition配合使用
-
要确保finally中释放锁 ==> try-with-resources LockHelper()自动释放,不需要手动释放
-
性能:优化后性能不是考虑因素
ReentrantLock实现原理
- 基于AQS = LockSupport + CAS
- AQS作用:竞争锁,等待锁基于AQS
- 公平、非公平:AQS
- 重入:AQS中state,并且isHeldByCurrentThread判断谁独占
1、自己如何实现ReentrantLock
- 实现AQS
- xxx ==> 忘了,后面敲代码,试下
2、CLH思想、AQS思想,实现公平锁和非公平锁
- 阻塞的线程LockSupoort.park()
- 运行完的线程,发现
3、公平锁加锁:tryAcquire()实现公平锁和非公平锁 ==> 源码再看一遍
- state = 0,没有线程获得锁:1. CAS操作成功 2.setExclusiveThread()自己独占锁
- state > 0,发现是自己占有锁,state++
- CAS失败,发现有队列。
1. 用while-CAS,加入到队列尾部。并且将前一个节点waitStatus设置为-1
2. LockSupport.park() 休眠
4、公平锁解锁
- 线程执行完后,检查自己的状态
- 0:无需要unpark的线程
- -1:需要唤醒下一个节点
5、非公平锁加锁
- 先CAS竞争,竞争失败了再加入队列
- 队列中线程会按顺序唤醒,可能会饿死
6、非公平锁解锁
- 唤醒队列的下一个线程:伪非公平
7、ReentrantReadWriteLock => 锁降级(写将为读)
AQS
AQS是CLH的变体:虚拟双向队列FIFO
Condition
await
signal/signalAll
CyclicBarrier
- 所有线程都执行完成后,才继续执行
- 基于ReentrantLock
CountDownLatch
门栓:
- => DAG启动框架
- ARouter => Interceptor拦截器的处理
- 基于AQS实现
JUC架构
操作系统:Mutex、Condition
基础工具:synchronized、CAS、LockSupport
AQS: CAS(资源竞争) + LockSupport(阻塞) + 条件队列(虚拟双向队列,CLH变体)
ReentrantLock:AQS
BlockingQueue:ReentrantLock
ThreadPoolExecutor:BlockingQueue + ReentrantLock + CAS
CopyOnWriteArrayList:
ConcurrentHashMap:synchronized + xxx
CountDownLatch: AQS
CyclicBarrier: AQS
AtomicInteger、AtomicRefrence、LongAddr
LongAddr
分段CAS
线程
1、多线程上下文切换中,上下文是指什么?切换是指什么?
- 上下文:某一时间点CPU寄存器和PC的内容
- 切换:线程通过【时间片轮转】算法执行任务,切换上下文 => 20000个时钟周期,约0.01ms
2、绿色线程是什么?用于JVM调度,JDK1.3后废弃
3、Thread start做了什么?
一言以蔽之,JVM层面JavaThread->OS层面的OSThread->pthread->JavaCalls->Thread.run
start0() // native
->thread.c#JVM_startThread()
->jvm.c#JavaThread(&thread_entry, xxx) // JVM层面的Thread对象,传入创建后需要执行的方法
thread.cpp#
->属性保存
->os::create_thread ===> JVM跨平台核心,看JVM在OS目录下,有windows、linux等目录
os_linux.cpp#
->创建OSThread对象
->(JavaThread)thread->set_osThread(osThread) 建立联系
->pthread_create // ========> mmkv
->父线程while()等待子线程初始化完成
//子线程
-> 将创建的内核线程 和 OSThread(父线程) 关联
-> 初始化操作
-> while()中wait等待父线程 // wait ======> mmkv、Linux
->帮助子线程prepare,
->将JVM的JavaThread和上层线程对象(我们的)互相关联
->设置优先级
->父线程OSThread执行start() // 将状态改为Runnable
->将状态改为Runnable
->notify()子线程 // notify =====>linux 、mmkv
// 子线程
thread.cpp#
->JavaThread::run()
->取出属性的方法并且执行
->JavaCalls() // 访问Java方法的大门
->执行到Thread.run()
死锁
1、什么是死锁?
一组线程竞争资源,并且相互等待,导致永久阻塞的情况 => JVMTI => 有向无环图 => 深度遍历
2、死锁的原因
- 互斥条件:共享资源xy只能一个线程占有
- 占有且等待:占有资源,且等待时不会释放
- 不可抢占:不能强行获取线程的资源
- 循环等待:t1 t2 互相等待占有的 x和y
3、解决方案
- 等待资源时,释放自己的资源
- 一次性请求所有资源
- 按照顺序申请
4、解决死锁相关算法
=>有效资源分配算法
=>银行家算法
两次start
1、只能调用一次,调用两次会出现异常illegalThreadStateException
2、Java线程的六种状态
- new
- running
- blocked
- waiting
- time-waiting
- terminated
3、posix线程库,线程有11种状态,从-1~9
=> KOOM dump => fork => suspendAllThread => suspend状态
4、无论是安全角度还是底层逻辑都不应该start两次
=> Java Thread 和 Native Thread 源码剖析
安全
互斥同步
- 互斥是手段,通不是目的
- 互斥是实现方法,同步是并发时共享数据只能被一个线程使用
- 方法:
1. 临界区
2. 互斥量Mutex => LockSupport => mmkv多线程安全
3. 信号量Semaphore
非阻塞同步
1、核心思想:先处理,有冲突再补偿
2、实现基础:依靠硬件指令集发展,保证多个操作的行为可以在一个CPU指令完成
3、相关指令
- 比较并交换CAS
- 加载链接LL/条件存储SC == CAS
4、CAS特点
- 适合写少读多,吞吐量高 => AtomicInteger(while(CAS))实现
- JDK 1.6后自适应
5、CAS = V A B
- 内存地址,旧值,新值
- xxx
6、加载链接LL/条件存储SC == CAS
- 一对原子指令,用于实现乐观并发控制
- 加载链接(Load-Link)指令用于将指定内存位置的值加载到寄存器中,并在加载过程中创建一个链接标记(Link)。
- 条件存储(Store-Conditional)指令用于将寄存器中的值存储回指定内存位置,但仅当加载链接指令之后,没有其他线程对该内存位置进行修改的情况下才会成功存储,即链接标记没有被破坏。
- 用于实现无锁数据结构和并发算法,如无锁队列、无锁哈希表等。
7、问题
- ABA:加版本号,但没实际意义。
- 自旋时间过长:自适应自旋转 or 锁升级 =>JVM
无同步方案
1、ThreadLocal
- 每个线程都有ThreadLocalMap对象,以key=ThreadLocal(会算出哈希值),value=变量,存储
- 获取当前的ThreadLocalMap后进行存储、读写,获得线程独占变量的效果
2、ThreadLocalMap的Entry继承自WeakReference<ThreadLocal<?>>
3、ThredLocalMap中为什么ThreadLocal使用弱引用?
- 外部使用ThreadLocal已经释放了强引用
- 但是Thread的ThreadLocalMap中,ThreadLocal还是强引用,必然导致内存泄漏
4、ThreadLocal中value是强引用会存在内存泄漏
- Entry数组中会存储,key=null,value=强引用的Entry。
5、ThreadLocal的清理机制
- get、put、remove,会对key=null的Entry进行清理
- 但是这种清理不及时(如果一直不调用)
6、线程池结合ThreadLocal容易出现内存泄漏
==> 哈希冲突,开放寻址法,线性探测(+1)
线程池
1、线程组ThreadGroup:构成树形结构,方便管理(如统一中断)
没有指明线程组,就都是main线程组
2、重要元素
- corePoolSize 核心线程数
- maximumPoolSize 最大线程数
- keepAliveTime:
- 队列
- 工厂
- 拒绝策略
3、为什么一定是阻塞队列?
- 让核心线程在取任务处,阻塞等待。(空闲时)
4、核心线程,在没任务时干什么?
- 保活:任务执行完后,while循环会去取阻塞队列的下一个任务,无任务阻塞
- 回收:超过核心线程数的线程执行完任务后,回收
- 实现:当线程数 > core, 队列中取任务会用workQueue.poll(keepAliveTime, Unit)
5、线程池调度线程执行的例子
- 11个线程都空闲,要取任务,core = 10, 因为 11 > 10,都会超时等待poll
- 超时后,11个线程都会退出while(),调用processWorkerExit() // 没有该方法会导致11个线程都停止
- 会和核心线程数比较,多的return(消失),核心的调用addWorker(),会换一个新的Thread对象执行
6、CTL什么意思?Control,控制
Executor
1、Executor的优点和缺点
- 性能:减少创建和销毁的开销
- 解耦:将任务的提交和任务处理想分离,方便管理
2、Executor是顶级接口
3、Executors是工具类
五种状态
1、线程池的五种状态
- Running (new出来就是)
- ShutDown(shutdown)剩余任务还会执行,
- Stop(shutdownNow)剩余的也不执行
- Tidying 清理中
- Terminated terminated()
2、线程池如何回收阻塞中的线程池?
- 中断
- 中断后,还会getTask-判断状态去return,不拿任务就stop,还拿任务就shutdown
3、onShutDown()和terminated()空方法给子类去实现
4、中断只是信号,不一定要停止。
- Thread.interrupted()返回值决定要做什么 // 会恢复标志位
JVM
锁优化
锁粗化
零碎操作反复加锁、解锁,将锁的范围扩展至整个操作之外,如循环体
锁消除
JIT将不存在数据竞争的锁去除 ==> 逃逸分析(不逃逸出线程)
轻量级锁
1、性能
- 无锁竞争情况下,性能>重量级锁
- 锁激烈竞争,多了CAS操作,性能<重量级锁
2、加锁流程
- 进入同步代码块,检查对象头
- 未获得锁:在栈帧中创建LockRecord。用CAS将Markword字段更新为指向LockRecord
- 成功:无竞争,进入轻量级锁状态,执行代码
- 失败:
1. Markword指向了当前线程的栈帧中LockRecord,继续执行(可重入特性)
2. MarkWord指向其他,代表有竞争,进入【重量级锁】
3、解锁流程 - CAS操作MarkWord,失败代表有其他线程在竞争锁
- 释放并唤醒其他挂起的线程
自选锁
1、自选锁和自适应自旋
2、自适应自旋的时间要怎么选择? ==> 自旋失败一次,且不是自己获得锁,升级
- 上一次同一个锁的自旋转时间和调用者状态决定
偏向锁
- 无竞争时,整个同步都消除,锁对象第一次获得锁的时候,进入偏向模式
- 有其他线程请求锁,立马退出偏向模式
- 偏向模式:1 锁状态: ===> 多少?