JMM
java内存模型试图屏蔽不同硬件与操作系统的内存访问差异,以实现java在不同平台都能达到一致的内存访问效果(跨平台)
处理器上的寄存器的读写速度比内存快几个数量级,为了解决这种速度矛盾,在之间加了高速缓存,这时会带来另一个问题:缓存一致性,如果多个缓存共享同一块 主内存区域,那么多个缓存的数据可能会不一致,需要一个协议来解决这个问题。
所有变量都存储在主内存中,每个线程还有自己的工作内存,工作内存存储在高速缓存或寄存器中,保存了该线程使用的变量的主内存副本拷贝。
内存间交互操作:
- read:把一个变量从主内存拷贝到工作内存中
- load:在read之后,把read得到的值放入工作内存的变量副本中
- use:把工作内存的一个变量的值传递给执行引擎
- assign:把一个从执行引擎得到的值赋给工作内存的变量
- store:把工作内存的一个变量的值传递给主内存
- write:store之后,把store得到的值放入主内存的变量中
- lock:加锁,作用于主内存的变量
- unlock:释放锁
三大特性:
- 原子性
- 可见性:volatile,synchronized,final
- 有序性
Java虚拟机
程序计数器
本地方法栈
虚拟机栈:栈帧(局部变量表、操作数栈、常量池引用)
堆
元空间(1.8)
垃圾收集器
Serial收集器:单线程,新生代(复制算法,STW),老年代(标记整理算法,STW)
ParNew收集器:多线程,新生代(复制算法,STW),老年代(标记整理算法,STW)
CMS收集器:初始标记(STW),并发标记,重新标记(STW),并发清除
G1收集器:初始标记,并发标记,最终标记,筛选回收
常用工具
- jps:列出正在运行的虚拟机进程
- jstat:虚拟机状态
jstat -gc 1505 250 20
- jinfo
- jmap
- jhat
- jstack
- hsdis
基础
Integer缓冲池的最大值可调:-XX:AutoBoxCacheMax
String
value不可变,final
字符串常量池(String Pool)保存着所有字符串字面量(literal strings),这些字面量在编译时期就确定。不仅如
此,还可以使用 String 的 intern() 方法在运行过程中将字符串添加到 String Pool 中。
反射
java.lang.reflect:Field,Method,Constructor
并发
Java内存模型规定,将所有的变量都存放在主内存中,当线程使用变量时,会把主内存里面的变量复制一份到工作空间或者叫工作内存,线程读写变量时操作的是自己工作内存里的变量。
synchronized关键字
synchronized是一种原子性内置锁,同时java中的线程与操作系统的原生线程是一一对应的,所以当阻塞一个线程时,需要从用户态切换到内核态执行阻塞操作,很耗时,synchronized使用时会导致上下文切换。synchronized会把其块内使用的变量从工作内存中清除,直接从主内存中获取,同时退出时会把对共享变量的修改刷新到主内存中。
JDK1.6的优化:对锁的实现引入了大量的优化,如偏向锁、轻量级锁、自旋锁、适应性自旋锁、锁消除、锁粗化等技术来减 少锁操作的开销。
锁主要存在四种状态,依次是:无锁状态、偏向锁状态、轻量级锁状态、重量级锁状态,他们会随着竞争的激烈而逐渐升级。注意锁可以升级不可降级,这种策略是为了提高获得锁和释放锁的效率。
volatile关键字
当一个变量被声明为volatile时,线程在写入变量时不会把值缓存到寄存器或其他地方,而是直接刷新到主内存中,当其他线程读取该变量时,不会从工作内存中读,而是直接从主内存读
写入volatile变量 相当于 线程退出synchronized块
读取volatile变量 相当于 线程进入synchronized块
volatile只保证可见性,无法保证原子性
使用场景:
- 写入变量不依赖变量当前值时
- 读写变量值时没有加锁
CAS操作
Compare And Set
解决线程上下文切换和重新调度开销
Unsafe类:对于var1中偏移量为var2的变量,如果其值时var4,那么将其更新为var6
1 | // var1:对象内存位置,var2:变量的偏移量,var4:变量预期值,var6:变量新值 |
存在ABA问题:线程1尝试将X的值从A更新到B,但是线程2将X的值从A更新到B之后,又更新到A,此时的A已不是最开始的A,但是线程1无法感知。(原因:存在环形状态,解决方案:状态只能朝一个方向转换-AtomicStampedReference)
Java指令重排序
多线程下会有问题,用volatile声明变量解决
写volatile变量时,不会把写之前的操作重排到写之后;读volatile变量时,不会把读之后的操作重排到读之前
伪共享
CPU与主内存之间存在一级或者多级高速缓冲存储器(Cache),集成到CPU内部
Cache内部按行存储,每一行叫做Cache行,大小一般为2的指数次字节
存放到Cache行的是内存块而不是单个变量,所以一行会有很多变量(目标变量地址相近的变量会一同加载到一行)
问题:多个线程操作一行内的多个变量时,会导致其他线程的缓存失效,频繁访问主内存
解决方案:利用冗余变量填充一行
1 | //jdk7以上使用此方法(jdk7的某个版本oracle对伪共享做了优化) |
可重入锁
自己可以再次获取自己的内部锁。比如一个线程获得了某个对象的锁,此时 这个对象锁还没有释放,当其再次想要获取这个对象的锁的时候还是可以获取的,如果不可锁重入的话,就会造成死 锁。同一个线程每次获取锁,锁的计数器都自增1,所以要等到锁的计数器下降为0时才能释放锁。
ReentrantLock
可重入锁,Lock的实现类
1 | // 非公平锁 |
锁优化
自旋锁:互斥同步进入阻塞状态的开销都很大,应该尽量避免。在许多应用中,共享数据的锁定状态只会持续很短的一段时
间。自旋锁的思想是让一个线程在请求一个共享数据的锁时执行忙循环(自旋)一段时间,如果在这段时间内能获得
锁,就可以避免进入阻塞状态。
锁消除:锁消除是指对于被检测出不可能存在竞争的共享数据的锁进行消除。(逃逸分析)
锁粗化:如果一系列的连续操作都对同一个对象反复加锁和解锁,频繁的加锁操作就会导致性能损耗。
轻量级锁
偏向锁:偏向锁的思想是偏向于让第一个获取锁对象的线程,这个线程在之后获取该锁就不再需要进行同步操作,甚至连 CAS 操作也不再需要。
集合
HashMap
put流程
- 若table未初始化,则初始化
- 若定位到的槽
i=(n - 1) & hash
为空,则直接写入 - 若与当前key相同,则直接覆盖旧值
若当前节点为红黑树,则调用putTreeVal设值
若当前节点为链表,遍历是否存在要插入的key,遍历到尾部后,决定是否要转为红黑树(长度大于8) - 如果达到阈值(threshold),则进行扩容
扩容流程
- 设置新容量与新阈值
- 若原来容量大于0,并且达到MAXIMUM_CAPACITY(1 << 30),则设值阈值为Integer.MAX_VALUE,并取消扩容;否则新容量为原来的2倍
- 若原来的threshold大于0,则新容量直接设置为threshold
- 否则新容量设置为DEFAULT_INITIAL_CAPACITY(16),新threshold设置为DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY
- 创建新数组
- 遍历旧数组,将当前node赋给e,清空当前节点,利用4个指针完成扩容(无需重新计算hash,利用e.hash & oldCap是否为0来判断新数组的下标):loHead存储为0的头节点,loTail不断延伸为0的链表;hiHead存储为1的头节点,hiTail不断延伸为1的链表
- 最后loHead放入newTab[j],hiHead放入newTab[j + oldCap]
Hash:(key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16) , (n - 1) & hash
1 | int threshold; // 所能容纳的key-value对极限 |
解决哈希表的冲突-开放地址法和链地址法
并发问题—resize时会出现环形链表,1.8后大大降低了概率
ConcurrentHashMap
put流程
- 判空
- 计算hash,调用spread计算
hash=(h ^(h >>>16))& HASH_BITS
; - 遍历table:
- 若table为空,则初始化(判断sizeCtl小于0,代表其他线程正在操作,让出CPU,否则CAS更新sizectl为-1)
- 若定位(volatile)到的槽
i=(n - 1) & hash
为空,则CAS写入数据 - 若当前槽hash为-1,就是为ForwardingNode,代表其他线程正在扩容,帮助扩容
- 否则出现哈希碰撞,利用synchronized同步寻找value,分为链表和红黑树两种情况
- 若链表长度为8,则treeifyBin转树(Note:若length<64,直接tryPresize,两倍table.length;不转树)
- addCount增加数量
addCount:
- CAS更新baseCount
- 检验是否需要扩容,若sizeCtl小于0,代表正在扩容,则sizeCtl+1同时辅助扩容transfer(tab, nt),否则开始扩容transfer(tab, null),完毕后计算数量
1 | // 存储节点数据,扩容时总是2的指数次方 |
线程池
Executors:工厂类
ThreadPoolExecutor:
- corePoolSize:核心线程数
- maximumPoolSize:最大线程数
- keepAliveTime:线程空闲时的存活时间
- unit:keepAliveTime单位
- workQueue:保存等待被执行任务的阻塞队列:
- ArrayBlockingQueue:基于数组的有界阻塞队列,FIFO
- LinkedBlockingQueue:基于链表的阻塞队列,FIFO,吞吐量高于ArrayBlockingQueue
- SynchornousQueue:不存储元素的阻塞队列,每个插入操作必须等到另一个线程调用移除操作,否则插入操作一直处于阻塞状态,吞吐量高于LinkedBlockingQueue
- PriorityBlockingQueue:具有优先级的无界阻塞队列
四种线程池
newFixedThreadPool:corePoolSize=maximumPoolSize,利用LinkedBlockingQueue做阻塞队列,当线程没有可执行任务时,也不会释放线程
newCachedThreadPool:存活时间60s,最大线程数可达Integer.MAX_VALUE,使用SynchornousQueue作为阻塞队列,注意控制并发数
newSingleThreadExecutor:初始化只有一个线程,如果该线程异常结束后,会重新创建一个新线程继续执行任务,可以保证任务的顺序执行,使用LinkedBlockingQueue作为阻塞队列
newScheduledThreadPool:在指定的时间内周期性的执行所提交的任务
除newScheduledThreadPool,其他都基于ThreadPoolExecutor实现
线程状态
1 | private final AtomicInteger ctl = new AtomicInteger(ctlOf(RUNNING, 0)); |
Spring
IOC
控制反转,一种设计思想,是将对象创建及生命周期的管理移交给Spring容器来做。