我自己的java大全
2024-06-11 17:02:36 32 举报
AI智能生成
我自己的java大全
作者其他创作
大纲/内容
数据结构
算法
子主题
设计模式
boolean
线程安全
线程安全:StringBuffer:线程安全,StringBuilder:线程不安全。因为 StringBuffer 的所有公开方法都是 synchronized 修饰的,而 StringBuilder 并没有 synchronized 修饰。
缓冲区:StringBuffer 每次获取 toString 都会直接使用缓存区的 toStringCache 值来构造一个字符串。StringBuilder 则每次都需要复制一次字符数组,再构造一个字符串。所以, StringBuffer 对缓存区优化,不过 StringBuffer 的这个toString 方法仍然是同步的。
但是一行加起来,比新建两行再加快
Java中对String对象进行的操作实际上是一个不断创建并回收对象的过程,因此在运行速度上很慢
而StringBuilder和StringBuffer的对象是变量,对变量的操作是直接对该对象就行更改,因此不会进行反复的创建和回收。所以在运行速度上比较快。
性能:(通常情况下):StringBuilder > StringBuffer > String
font color=\"#000000\
代码
解释:上述代码先创建一个String对象str,并赋值abc给str,然后运行到第三行,JVM会再创建一个新的str对象,并将原有str的值和de加起来再赋值给新的str。而第一个创建的str对象被JVM的垃圾回收机制(GC)回收掉。所以str实际上并没有被更改,即String对象一旦创建就不可更改。所以Java中对String对象进行的操作实际上是一个不断创建并回收对象的过程,因此在运行速度上很慢。而StringBuilder和StringBuffer的对象是变量,对变量的操作是直接对该对象就行更改,因此不会进行反复的创建和回收。所以在运行速度上比较快。
拼写为什么不用+,而是用StringBuffer
2个对象。s首先会在常量池创建“abc”字符串常量,当new的时候就会在堆内存中创建一个对象,此时会把常量池中的字符串常量拷贝一份副本到给到堆内存中的对象,堆内存中的这个对象就会把地址值赋给s。常量池中对象的地址值和堆内存中对象的地址值是不一样的,s指向的是堆内存中的对象,不是常量池中的对象。此时堆内存中有一个对象,常量池中有一个对象,所以创建了2个对象。查看API,String的有参构造。
String s = new String(\"abc\");创建几个对象
Java中有常量优化机制,“a”、“b”、“c”本身就是字符串常量,所以在编译时,\"a\"+\"b\"+\"c\"就是“abc”字符串,所以就在常量池创建了“abc”字符串,当执行s2的时候,此时常量池中已经存在了“abc”,所以==号比较返回true。equals方法比较毫无疑问是true。
“a”、“b”、“c”和“abc”相等判断
s1+\"c\"中s1不是常量,所以不能用常量优化机制来分析。equals方法比较毫无疑问是true。s1和s2会分别在常量池中创创建\"ab\"、\"abc\"两个对象s3的时候是字符串串联,API解释如下图。所以s3的时候会在对内存中创建StringBuilder(或者StringBuffer)对象,通过append方法拼接成“abc”对象,此时的“abc”是StringBuilder(或者StringBuffer)类型的,通过调用toString方法转成String对象,此时s3指向的是堆内存中这个String对象s2指向的是常量池中的对象,s3指向的是堆内存中的对象,所以==号比较返回false
判断定义为String类型的s3和s2是否相等
因为只有当字符串是不可变的,字符串池才有可能实现。字符串池的实现可以在运行时节约很多heap空间,因为不同的字符串变量都指向池中的同一个字符串。但如果字符串是可变的,那么String interning将不能实现,因为这样的话,如果变量改变了它的值,那么其它指向这个值的变量的值也会一起改变。如果字符串是可变的,那么会引起很严重的安全问题。譬如,数据库的用户名、密码都是以字符串的形式传入来获得数据库的连接,或者在socket编程中,主机名和端口都是以字符串的形式传入。因为字符串是不可变的,所以它的值是不可改变的,否则黑客们可以钻到空子,改变字符串指向的对象的值,造成安全漏洞。因为字符串是不可变的,所以是多线程安全的,同一个字符串实例可以被多个线程共享。这样便不用因为线程安全问题而使用同步。字符串自己便是线程安全的。因为字符串是不可变的,所以在它创建的时候HashCode就被缓存了,不需要重新计算。这就使得字符串很适合作为Map中的键,字符串的处理速度要快过其它的键对象。这就是HashMap中的键往往都使用字符串。
为什么Java语言的开发者,把String类定义为final的呢?
相关问题
String
包装类作为引用类型,可以存在 NULL 值
所有的包装类,都是 final 不可变类,这点与 String 是一致的,任何的修改都是新对象的创建
final 不可变,赋值与声明同步,此后不可修改。如 Integer、String 这些不可变类,是借由 final 完成的修饰。所以,若需要修改其中的内容,必须开辟新的内存空间,这会造成不必要的浪费,也是各种缓存机制存在的必然
缓存机制的来源
基本概念
有:Byte、Short、Integer、Long、Character
无:Float、Double 与 Boolean
有和无
缓存池
包装类
在对象的内存地址基础上经过特定算法返回一个hash码
hashCode方法
【强制】所有的相同类型的包装类对象之间值的比较,全部使用 equals 方法比较。说明:对于 Integer var = ? 在-128 至 127 范围内的赋值,Integer 对象是在IntegerCache.cache 产生,会复用已有对象,这个区间内的 Integer 值可以直接使用==进行判断,但是这个区间之外的所有数据,都会在堆上产生,并不会复用已有对象,这是一个大坑,推荐使用 equals 方法进行判断。
特殊情况
1、 ==是判断两个变量或实例是不是指向同一个内存空间 equals是判断两个变量或实例所指向的内存空间的值是不是相同2、 ==是指对内存地址进行比较 equals()是对字符串的内容进行比较3、==指引用是否相同 equals()指的是值是否相同
==判断是不是同一个equal判断值是不是一样
总结
==和equal区别
因为对象里的是对象的地址对比,现在变成了属性对比,那属性里的HashSet/HashMap/Hashtable类通过hashcode找数据,则就要对比属性的哈希
为什么重写equal后要重写hashcode
(1)当obj1.equals(obj2)为true时,obj1.hashCode() == obj2.hashCode()必须为true(2)当obj1.hashCode() == obj2.hashCode()为false时,obj1.equals(obj2)必须为false
因为如果不重写equals方法,当将自定义对象放到 map或者 set中时;如果这时两个对象的 hashCode相同,就会调用 equals方法进行比较,这个时候会调用 Object中默认的 equals方法,而默认的 equals方法只是比较了两个对象的引用是否指向了同一个对象(return (this == obj);),显然大多数时候都不会指向,这样就会将重复对象存入 map或者 set中。这就破坏了 map与 set不能存储重复对象的特性,会造成内存溢出。
obj instanceoof Person
记得先判断是不是一个类
想要判断两个对象是不是一样? 不能业务代码一个一个属性值判断吧? 重写对象的equal将属性值判断放在对象内部完成
什么时候重写equal
为什么重写equal
hashCode() 与 equals() 之间有什么关系?
序列化
重写clone
怎么实现深拷贝
浅拷贝:对基本数据类型进行值传递,对引用数据类型进行引用传递般的拷贝,此为浅拷贝。深拷贝:对基本数据类型进行值传递,对引用数据类型,创建一个新的对象,并复制其内容,此为深拷贝。
问题的产生:拷贝时如果一个对象内部引用了另一个对象,则拷贝时只传过去引用对象的地址,导致是拷贝后引用和之前的内部引用的对象是同一个对象.即浅拷贝只是拷贝了“一层”,深拷贝是真的全部拷贝
clone方法(深拷贝浅拷贝)
Object
String也属于引用类型,String在进行传递的时候也是传递的地址,这个地址是堆中的运行时常量池中的地址,但是因为String具有不可变性,在对对象进行赋值的时候,实际上是新创建了一个对象,而不是在原有地址上进行修改,但是观感就像没有变。
基本类型是值传递
引用类型是引用传递
Java对象作为参数传递是传值还是传引用
对象
数据类型
异常
泛型
红黑树
链表遍历更新红黑树查询更新
总结:1、无则插入,有则分情况;2、如果是1.7就是扩容3、1.8因为有两种结构,会判断一下是哪种: 是链表节点就插链表是红黑树节点就插红黑树 插完再判断需不需要扩容、
hashmap的put方法
元素的位置要么是在原位置,要么是在原位置再移动2次幂的位置。
(1)重新建立一个新的数组,长度为原数组的两倍;(2)遍历旧数组的每个数据,重新计算每个元素在新数组中的存储位置。使用节点的hash值与旧数组长度进行位与运算,如果运算结果为0,表示元素在新数组中的位置不变;否则,则在新数组中的位置下标=原位置+原数组长度。(3)将旧数组上的每个数据使用尾插法逐个转移到新数组中,并重新设置扩容阈值。
hashmap扩容
大于8进入树化函数,在判断数组是不是大于64
链表数据长度大于8且数组超过64就变树
链表长度达到8,b style=\
链表转红黑树treeifyBin
6个
红黑树转回链表
转换时机
hashmap变树
前提:位运算取余必须保证table数组的size为二的幂次方
导致:所以初始时、扩容时要是的2次幂
算地址时为什么用位运算(提高效率)
异或会更散一点,与和或会归0和归1
hash值32位,数组初始化size为16,直接拿hashcode取余16操作hashCode & 1111会导致很多高位不同,低位相同的数算出来的索引都是一样的,引发冲突
hashcode为int类型,4个字节32位,为了确保散列性,肯定是32位都能进行散列算法计算是最好的。
为什么要高十六位参与hash计算(增加散列程度)
关于哈希算法
二倍保证二次幂
位运算取余必须保证table数组的size为二的幂次方
扩容时为什么是二倍
面试题
hashmap
1.7表现为在多线程环境下进行扩容,由于采用头插法,位于同一索引位置的节点顺序会反掉,导致可能出现死循环的情况
1.8表现为在多线程环境下添加元素,可能会出现数据丢失的情况
表现
底层通过一个 modCount 值记录修改的次数,对 HashMap 的修改操作都会增加这个值。迭代器在初始过程中会将这个值赋给 exceptedModCount ,在迭代的过程中,如果发现 modCount 和 exceptedModCount 的值不一致,代表有其他线程修改了Map,就立刻抛出异常。
Fail-Fast 机制
怎么保证数据正确
底层的每个方法都使用了 synchronized 保证线程同步,所以每次都锁住整张表,在性能方面会相对比较低
Hashtable
获得一个synchronized同步的map
Collections.synchronizedMap()
ConcurrentHashMap
哪些线程安全的map可以使用
HashMap线程不安全解析
segment默认16个
1.CAS,发现内存位置值和原值相等,则代表没动过,可以插入
2.内存位置数据和原值不相等,需要往链表或者红黑树里插入数据则锁住当前桶
JDK1.8的--基于CAS和synchronized的同步代码块实现线程安全
ConcurrentHashMap线程安全机制
hashmap的没有final修饰,可以改
数组长度达到扩容的阈值0.75
链表达到8,会进函数,在数组长度没有到64扩容
在执行putAll操作的时候,会直接优先判断是否需要扩容
会计算扩容戳看是否需要帮助扩容
每次put
相关结论:有的会先执行tryPresize;有的会自行判断,计算扩容戳,执行transfer方法进行扩容
触发条件
在第一次 put时才会初始化数组
sizeCtl = -1 :代表当前map的数组正在初始化
sizeCtl < -1 :代表当前map正在扩容,font color=\"#f44336\
sizeCtl = 0 :还未初始化
sizeCtl > 0 :如果没有数组初始化代表数组长度,如果数组已经初始化,代表扩容阈值。
相同则帮助其扩容不同则回头自己扩
1、扩容前先计算扩容戳,算出的扩容戳和原数组的长度是绑定的,比如同是长度16,扩容戳相同
都是32->64或者64->128,多线程扩容时,要得是同比例扩大(2倍)。
扩容戳是个负数10000000 00011010 00000000 00000010高十六位:扩容的标识(32->64)低十六位:正在扩容的线程个数(此例是-2,代表有一个线程在扩容)
刚扩容时候先根据原长度算出扩容后的大小,再算出正在扩容的线程数+2
使用
sizeCtl扩容戳
1、开始扩容,线程只有一个,会把新数组new出来2、其他线程会从transfer\\helpTransfer方法进来,进来时候会对扩容戳进行CAS操作加一3、一个桶一个桶的迁移,迁移完会把ForwardingNode设置到老数据,哈希设置成-1,代表已经迁移了,可用find函数找4、链表迁移是使用lastRun机制
流程
扩容
,那么就可以通过 FWD 节点的 find() 方法重新定向到新散链表中去查询目标元素!
如果 Node.hash = -1,表示当前节点是 **FWD(ForWardingNode) **节点(表示已经被迁移的节点)。如果 Node.hash = -2,表示当前节点已经树化,且当前节点为 TreeBin 对象,TreeBin 对象代理操作红黑树。如果 Node.hash > 0,表示当前节点是正常的 Node 节点,可能是链表,或者单个 Node。
哈希值的含义,一定是个正数
ConcurrentHashMap扩容(比hashmap快)
首先,先判断散链表是否已经初始化,如果没初始化则先初始化散链表,再进行写入操作。② 当向桶位中写数据时,先判断桶中是否为空,如果是空桶,则直接通过 CAS 的方式将新增数据节点写入桶中。并且计算扩容戳看看是不是需要帮助扩容如果当前桶中不为空,就需要判断当前桶中头结点的类型(哈希是不是-2,哈希-2代表树节点)③ 如果桶中头结点的 hash 值 为 -1,表示当前桶位的头结点为 FWD 结点,目前散链表正处于扩容过程中。这时候当前线程需要去协助扩容。④ 如果 ②、③ 条件不满足,则表示当前桶位的存放的可能是一条链表,也可能是红黑树的代理对象 TreeBin。这种情况下会使用 synchronized 锁住桶中的头结点,来保证桶内的写操作是线程安全的。
put
写线程会被阻塞,因为红黑树比较特殊,新写入数据,可能会触发红黑树的自平衡,这就会导致树的结构发生变化,会影响读线程的读取结果!在红黑树上读取数据和写入数据是互斥的,具体原理分析如下:我们知道 ConcurrentHashMap 中的红黑树由 TreeBin 来代理,TreeBin 内部有一个 Int 类型的 state 字段。当读线程在读取数据时,会使用 CAS 的方式将 state 值 +4(表示加了读锁),读取数据完毕后,再使用CAS 的方式将 state 值 -4。如果写线程去向红黑树中写入数据时,会先检查 state 值是否等于 0,如果是 0,则说明没有读线程在检索数据,这时候可以直接写入数据,写线程也会通过 CAS 的方式将 state 字段值设置为 1(表示加了写锁)。如果写线程检查 state 值不是 0,这时候就会park()挂起当前线程,使其等待被唤醒。挂起写线程时,写线程会先将 state 值的第 2 个 bit 位设置为 1(二进制为 10),转换成十进制就是 2,表示有写线程等待被唤醒。反过来,当红黑树上有写线程正在执行写入操作,那么如果有新的读线程请求该怎么处理?
当桶位中链表升级为红黑树,且当前红黑树上有读线程正在访问,那么如果再来新的写线程请求该怎么处理?
HashMap和ConcurrentHashMap
Collection
容器
JAVA基础知识
为什么学习线程?为了解决CPU利用率问题,提高CPU利用率。=》 什么是进程?什么是线程? =》 怎么创建线程?有哪几种方式?有什么特点? =》 分别怎么启动线程? =》 多线程带来了数据安全问题,该怎么解决? =》 怎么使用synchronized(同步)决解? =》使用同步可能会产生死锁,该怎么决解? =》 线程之间是如何通信的? =》 线程有返回值吗?该如何拿到? =》 怎么才能一次性启动几百上千个的线程?
思路
而进程由内存空间(代码,数据,进程空间,打开的文件)和一个或多个线程组成。
进程是一个具有一定独立功能的程序在一个数据集上的一次动态执行的过程,是操作系统进行资源分配和调度的一个独立单位,是应用程序运行的载体。进程是一种抽象的概念,从来没有统一的标准定义。
一个标准的线程由线程ID,当前指令指针PC,寄存器和堆栈组成。
线程是程序执行中一个单一的顺序控制流程,是程序执行流的最小单元,是处理器调度和分派的基本单位。一个进程可以有一个或多个线程,各个线程之间共享程序的内存空间(也就是所在进程的内存空间)。
进程与线程
CPU数:独立的中央处理单元,体现在主板上就是有多少个CPU槽位
每一个物理核可以模拟出多个逻辑核,\"超线程\"技术就是通过采用特殊的指令,把逻辑内核模拟为物理超线程,这样的核就是processor.是一个处理数据的通道,流水线。可以理解为逻辑核(比如我们常说的4核8线程中的线程)
一种逻辑上的概念,并非真实存在的物体,只是为了更好地描述CPU的运作能力。简单地说,就是模拟出的CPU核心数。
CPU线程数(processor逻辑核)
CPU核心数,线程数
CPU在执行多个线程的时候,会不断的切换执行的任务。这个任务之间的切换动作我们就叫做上下文切换。
CPU时间片轮转机制——上下文切换
什么是并行和并发
计算机基础概念
JMM结构规范
由Java内存模型来直接保证的原子性变量操作包括read、load、assign、use、store和write。大致可以认为1、基本数据类型的操作是原子性的。2、同时lock和unlock可以保证更大范围操作的原子性。3、 synchronize同步块操作的原子性是用更高层次的字节码指令monitorenter和monitorexit来隐式操作的。
1、基本类型的读取和赋值操作,且赋值必须是值赋给变量,变量之间的相互赋值不是原子性操作;2、所有引用reference的赋值操作;3、java.concurrent.Atomic.* 包中所有类的一切操作。
一个操作或者多个操作,要么全部执行并且执行的过程不会被任何因素打断,要么就都不执行。
CAS和锁来保证
原子性(Atomicity)
1、volatile来保证可见性,当一个变量被volatile修饰后,表示着线程本地内存无效,当一个线程修改共享变量后他会立即被更新到主内存中,其他线程读取共享变量时,会直接从主内存中读取。2、synchronize和Lock都可以保证可见性。synchronized和Lock能保证同一时刻只有一个线程获取锁然后执行同步代码,并且在释放锁之前会将对变量的修改刷新到主存当中。因此可以保证可见性。
指当多个线程访问同一个变量时,一个线程修改了这个变量的值,其他线程能够立即看得到修改的值。
JAVA内存模型来保证
可见性(Visibility)
如果在本线程内观察,所有操作都是有序的;如果在一个线程中观察另一个线程,所有操作都是无序的。
即程序执行的顺序按照代码的先后顺序执行。
as-if-searil、happens-before来保证
有序性(Ordering)
三个特征
原子性变量操作包括read、load、assign、use、store和write
从抽象的角度来看,JMM定义了线程和主内存之间的抽象关系:线程之间的共享变量存储在主内存(main memory)中,每个线程都有一个私有的本地内存(local memory),本地内存中存储了该线程以读/写共享变量的副本。本地内存是JMM的一个抽象概念,并不真实存在。本地内存它涵盖了缓存,写缓冲区,寄存器以及其他的硬件和编译器优化之后的一个数据存放位置
概念
本地内存A和B有主内存中共享变量x的副本。假设初始时,这三个内存中的x值都为0。线程A在执行时,把更新后的x值(假设值为1)临时存放在自己的本地内存A中。当线程A和线程B需要通信时,线程A首先会把自己本地内存中修改后的x值刷新到主内存中,此时主内存中的x值变为了1。随后,线程B到主内存中去读取线程A更新后的x值,此时线程B的本地内存的x值也变为了1。
JMM通过控制主内存与每个线程的本地内存之间的交互,来为java程序员提供内存可见性保证。或者说内存模型JMM控制多线程对共享变量的可见性!!!
作用
主内存和本地内存结构
内存模型JMM
重排序是指编译器和处理器为了优化程序性能而对指令序列进行排序的一种手段
JAVA源代码--->编译器优化的重排序--->指令级并行的重排序--->内存系统的重排序--->最终执行指令
编译器在不改变单线程语义下,可以重新安排语句的执行顺序
编译器优化的重排序
处理器将多条指令重叠执行
指令级并行的重排序
处理器使用缓存和读写缓冲区,是的加载和存储操作看上去可能是在乱序执行
内存系统的重排序
三种重排序
可见性问题
有序性问题
引发的问题
JMM的编译器重排序规则会禁止特定类型的编译器重排序(不是所有的编译器重排序都要禁止)。
编译器的重排序
JMM的处理器重排序规则会要求Java编译器在生成指令序列时,插入特定类型的内存屏障指令,来禁止特定类型的处理器重排序。
JMM根据代码中的关键字(如:synchronized、volatile)和J.U.C包下的一些具体类来插入内存屏障。
处理器的重排序
解决方法
① 写后读:a = 1;b = a; 写一个变量之后,再读这个位置
② 写后写:a = 1;a = 2; 写一个变量之后,再写这个变量。
③ 读后写:a = b;b = 1;读一个变量之后,再写这个变量。
如果两个操作同时操作同一个变量且其中一个操作是写操作,则这两个操作之间存在数据依赖
存在数据依赖关系的两个语句,不可以重排序
数据依赖性
遵守数据依赖性和as-if-serial 语义实质上是一回事。为了遵守 as-if-serial 语义,编译器和处理器不会对存在数据依赖关系的操作做重排序,因为这种重排序会改变执行结果。
不管怎么重排序,单线程语义下程序的执行结果不能变
看起来好像是串行的
as-if-serial原则
1、如果a操作happends-before 另一个b操作,那么a操作需要对b操作是可见的,并且a操作发生在b操作之前2、JMM只是保证了happends-before语义的正确性,即使内部发生了指令重排序,那么我们也不用管,只要满足第一条,我们就认为他是符合happends-before语义的。换句话说,JMM这种优化就是合法的。
在同一个线程或者多个线程之间,如果A线程的a操作 happends-before B线程的b操作,那么JMM向程序员保证a操作对b操作是可见的。
happens-before原则
as-if-serial语义保证单线程内程序的执行结果不被改变,happens-before关系保证正确同步的多线程程序的执行结果不被改变。
as-if-serial语义给编写单线程程序的程序员创造了一个幻境:单线程程序是按程序的顺序来执行的。happens-before关系给编写正确同步的多线程程序的程序员创造了一个幻境:正确同步的多线程程序是按happens-before指定的顺序来执行的。
as-if-serial语义和happens-before这么做的目的,都是为了在不改变程序执行结果的前提下,尽可能地提高程序执行的并行度。
一句话:都是为了保证重排序对最终结果不会产生影响
as-if-serial原则和happens-before原则的区别
规则
重排序
为了保证内存可见性,可以通过volatile、final等修饰变量,java编译器在生成指令序列的适当位置会插入内存屏障指令来禁止特定类型的处理器重排序。
阻止屏障两侧的指令重排序;
它会强制将对缓存的修改操作立即写入主存
如果是写操作,它会导致其他CPU中对应的缓存行无效缓存一致性(窥探技术 + MESI协议 ))
功能
对于Load Barrier来说,在指令前插入Load Barrier,可以让高速缓存中的数据失效,强制重新从主内存加载数据;
对于Store Barrier来说,在指令后插入Store Barrier,能让写入缓存中的最新数据更新写入主内存,让其他线程可见
Load Barrier 和 Store Barrier即读屏障和写屏障。
硬件层面的内存屏障
LoadLoad
StoreStore
LoadStore
StoreLoad
java内存屏障
种类
在每个volatile写操作前插入StoreStore屏障,这样就能让其他线程修改A变量后,把修改的值对当前线程可见,在写操作后插入StoreLoad屏障,这样就能让其他线程获取A变量的时候,能够获取到已经被当前线程修改的值
在每个volatile读操作前插入LoadLoad屏障,这样就能让当前线程获取A变量的时候,保证其他线程也都能获取到相同的值,这样所有的线程读取的数据就一样了,在读操作后插入LoadStore屏障;这样就能让当前线程在其他线程修改A变量的值之前,获取到主内存里面A变量的的值。
内存屏障在Volatile关键字里面的作用
内存屏障
并发编程基础概念
线程基本状态
创建线程方式
JAVA多线程基础概念
继承Thread类
实现Runnable接口
实现Callable接口
使用线程池创建
创建线程的方法
基础知识
AQS
CAS
可重入性
不可中断性
并发实现基础知识
as-if-serial
happens-before
有序性
因为某一个线程进入synchronized代码块前后,线程会获得锁,清空工作内存,从主内存拷贝共享变量最新的值到工作内存成为副本,执行代码,将修改后的副本的值刷新回主内存中,线程释放锁。而获取不到锁的线程会阻塞等待,所以变量的值肯定一直都是最新的。
可见性
原子性
特性保证
基本信息
其实wait/notify等方法也依赖于monitor对象,这就是为什么只有在同步的块或者方法中才能调用wait/notify等方法,否则会抛出java.lang.IllegalMonitorStateException的异常的原因。
当我们进入一个人方法的时候,执行monitorenter,就会获取当前对象的一个所有权,这个时候monitor进入数为1,当前的这个线程就是这个monitor的owner。如果你已经是这个monitor的owner了,你再次进入,就会把进入数+1.同理,当他执行完monitorexit,对应的进入数就-1,直到为0,才可以被其他线程持有。
同步代码块时
一个特殊标志位,ACC_SYNCHRONIZED。同步方法的时候,一旦执行到这个方法,就会先判断是否有标志位,然后,ACC_SYNCHRONIZED会去隐式调用刚才的两个指令:monitorenter和monitorexit。所以归根究底,还是monitor对象的争夺。
同步方法时
monitor的对象来完成
原理
每个对象都有一个对象锁,不同的对象,他们的锁不会互相影响。new两个对象就不行了,存在着两个不同的实例对象锁
一个类,开两个线程可以同步
修饰实例方法(不包括静态方法) 锁实例对象
synchronized(this) 锁实例对象
所有对象可同步
synchronized(类.class) 锁类对象
修饰实例方法代码块
修饰静态方法 以及 静态方法中的代码块 锁类对象
我们应该意识到这种情况下可能font color=\"#f44336\
当前类class锁被获取,不影响对象锁的获取,两者互不影响。
注意:在java中,实际上同步加锁的是一个对象或者一个类,而不是代码。
使用场景
monitorenter
monitorexit
加减
程序计数器count
实现机制
默认存储对象的HashCode,分代年龄和锁标志位信息。它会根据对象的状态复用自己的存储空间,也就是说在运行期间Mark Word里存储的数据会随着标志位的变化而变化。
MARK WORD(标记字段)
对象指向它的类元数据的指针,虚拟机通过这个指针来确定这个对象是哪个类的实例。
CLASS POINT (类型指针)
EntryList
Owner(会指向持有Monitor对象的线程)
WaitSet
Monitor
对象头(Header)
存放类的数据信息,父类的信息
实例数据
一个空对象占多少个字节?就是8个字节,是因为对齐填充的关系哈,不到8个字节对其填充会帮我们自动补齐。
由于虚拟机要求对象起始地址必须是8字节的整数倍,填充数据不是必须存在的,仅仅是为了字节对齐。
对齐填充
JVM对象
从JDK5引入了现代操作系统新增加的CAS原子操作( JDK5中并没有对synchronized关键字做优化,而是体现在J.U.C中,所以在该版本concurrent包有更好的性能 ),从JDK6开始,就对synchronized的实现机制进行了较大调整,包括使用JDK5引进的CAS自旋之外,还增加了自适应的CAS自旋、锁消除、锁粗化、偏向锁、轻量级锁这些优化策略。由于此关键字的优化使得性能极大提高.
锁优化
用户态把一些数据放到寄存器,或者创建对应的堆栈,表明需要操作系统提供的服务。用户态执行系统调用(系统调用是操作系统的最小功能单位)。CPU切换到内核态,跳到对应的内存指定的位置执行指令。系统调用处理器去读取我们先前放到内存的数据参数,执行程序的请求。调用完成,操作系统重置CPU为用户态返回结果,并执行下个指令。
大概流程
内核态和用户态之间的转换
1.6之后优化了
1.6之前是重量级锁
重锁
synchronized
ReentrantLock是基于AQS框架实现的锁,它类似于Synchronized互斥锁,可以保证线程安全。ReentrantLock相比Synchronized,拥有更多的特性,比如支持手动加锁、解锁,支持公平锁等。
ReentrantLock锁是一个轻量级锁,底层其实就是用自旋锁也叫无锁(CAS)实现的,lock锁不依赖操作系统,而是使用java实现的锁,当我们调用lock方法的时候,在内部其实调用了Sync.lock()方法,而Sync继承了AbstractQueuedSynchronizer,简称AQS,所以在底层调用的其实是AQS的 lock() 方法;
AQS定义了一套多线程访问共享资源的同步器框架,是一个依赖状态的同步器。AQS定义了很多并发中的行为,比如:阻塞等待队列、共享/独占、公平/非公平、可重入、允许中断
unsafe下面的cas方法
缺点
ReentrantLock
455
线程同步
根据对同步资源处理策略不同,锁在宏观上分为乐观锁与悲观锁,这只是概念上的一种称呼,Java中并没有具体的实现类叫做乐观锁或者悲观锁。
概述
乐观锁回滚重试
认为别人不会修改,所以不会上锁,但是在执行更新时会问一下(判断):没有更新,则当前线程将自己的值才成功写入;已经被更新,则根据不同的实现方式执行不同的操作,例如报错或者重试。
乐观锁
悲观锁阻塞事务
总是假设最坏的情况,每次去拿数据的时候都认为别人会修改,所以每次在拿数据的时候都会上锁
悲观锁
在Java中是通过使用无锁编程来实现,最常采用的是CAS算法,Java原子类中的递增操作就通过CAS自旋实现的。
Java的synchronized和ReentrantLock等锁去实现
实现方式
适合用于写操作比较少的场景,因为冲突很少发生,这样可以省去锁的开销,加大了系统的整个吞吐量。
适用于写操作比较多的场景。如果经常产生冲突,上层应用会不断的进行重试,乐观锁反而会降低性能,悲观锁则更加合适。
适用场景
CAS,即Compare And Swap(比较与交换),是一种无锁算法。在不使用锁(没有线程被阻塞)的情况下实现多线程之间的变量同步。java.util.concurrent包中的原子类就是通过CAS来实现了乐观锁。
比较:读取到一个值 A,在将其更新为 B 之前,检查原值是否为 A(未被其它线程修改过,这里忽略 ABA 问题)。替换:如果是,更新 A 为 B,结束。如果不是,则不会更新。
理解
JDK从1.5开始提供了AtomicStampedReference类来解决ABA问题,具体操作封装在compareAndSet()中。compareAndSet()首先检查当前引用和当前标志与预期引用和预期标志是否相等,如果都相等,则以原子方式将引用值和标志的值设置为给定的更新值。
ABA问题。CAS需要在操作值的时候检查内存值是否发生变化,没有发生变化才会更新内存值。但是如果内存值原来是A,后来变成了B,然后又变成了A,那么CAS进行检查时会发现值没有发生变化,但是实际上是有变化的。ABA问题的解决思路就是在变量前面添加版本号,每次变量更新的时候都把版本号加一,这样变化过程就从“A-B-A”变成了“1A-2B-3A”。
ABA问题
CAS操作如果长时间不成功,do while会导致其一直自旋,给CPU带来非常大的开销。
循环时间长开销大
对一个共享变量执行操作时,CAS能够保证原子操作,但是对多个共享变量操作时,CAS是无法保证操作的原子性的。Java从1.5开始JDK提供了AtomicReference类来保证引用对象之间的原子性,可以把多个变量放在一个对象里来进行CAS操作。
只能保证一个共享变量的原子操作
存在的问题
乐观锁之CAS
乐观锁&悲观锁
线程状态切换会使CPU运行状态从用户态转换到内核态。每个线程在运行时的指令是被放在CPU的寄存器中的,如果切换内存状态,需要先把本线程的代码和变量写入内存,这样经常切换会耗费时间。
线程状态切换的开销是非常大的
来历
线程因为无法获取到锁而进行挂起以及后续的恢复操作,这个时间消耗很可能大于同步资源的锁定时间,这种情况对系统而言是得不偿失的。那么,能不能让获取锁失败的线程先不挂起,而是“稍等一下”,如果锁被释放,这个线程便可以直接获取锁,从而避免线程切换。这个“稍等一下”依赖于自旋实现,所谓自旋,即在一个循环中不停判断是否可以获取锁。获取锁的操作,就是通过 CAS 操作修改对象头里的锁标志位。先比较当前锁标志位是否为释放状态,如果是,将其设置为锁定状态,比较并设置是原子性操作,这个是 JVM 层面保证的。当前线程就算持有了锁,然后线程将当前锁的持有者信息改为自己。这是一种折中的思想,用短时间的忙等来换取线程切换的开销。它并不会放弃 CPU 时间片,而是通过自旋等待锁的释放,也就是说,它会不停地再次地尝试获取锁,如果失败就再次尝试,直到成功为止
解释
自旋锁用循环去不停地尝试获取锁,让线程始终处于 Runnable 状态,节省了线程状态切换带来的开销
好处
长时间的自旋带来的开销可能会大于线程的切换
自旋获取锁过程完全看运气
是一种非公平锁,等待时间最长的线程并不能优先获取锁,可能会产生“线程饥饿”问题。
坏处
自旋锁
b style=\
由自旋锁的坏处可知,自旋应该是有限度的。
上一个自选获得锁成功,且成功运行,则下一次可能很快就成功,后续自旋等待时间会更长些
自旋很少成功获得过,那在以后尝试获取这个锁时将可能省略掉自旋过程,直接阻塞线程,避免浪费处理器资源。
自适应意味着自旋的时间(次数)不再固定,而是由前一次在同一个锁上的自旋时间及锁的拥有者的状态来决定。
适应性自旋锁
getAndIncrementgetAndIncrement 也是直接调用 nsafe 的 getAndAddInt 方法,从下面源码可以看出这个方法直接就是做了一个 do-while 的循环。「这个循环就是一个自旋操作,如果在修改过程中遇到了其他线程竞争导致没修改成功的情况,就会 while 循环里进行死循环,直到修改成功为止」。
在 Java 1.5 版本及以上的并发包中,也就是 java.util.concurrent 的包中,里面的原子类基本都是自旋锁的实现。外加AQS。
自旋锁适用于并发度不是特别高的场景,以及临界区比较短小的情况,这样我们可以利用避免线程切换来提高效率可是如果临界区很大,线程一旦拿到锁,很久才会释放的话,那就不合适用自旋锁,因为自旋会一直占用 CPU 却无法拿到锁,白白消耗资源
并发不高,避免一直自旋不成功代码简单,不能太复杂,耗时比较短
使用 AtomicReference 类提供一个可以原子读写的对象引用变量。
定义一个加锁方法,如果有其他线程已经获取锁,当前线程将进入自旋,如果还是已经持有锁的线程获取锁,那就是重入。
定义一个解锁方法,解锁的话,只有持有锁的线程才能解锁,解锁的逻辑思维将 count-1,如果 count == 0,则是把当前持有锁线程设置为 null,彻底释放锁。
手写自旋锁
自旋锁 & 适应性自旋锁
公平锁是指,如果多个线程争夺一把公平锁,这些线程会进入一个按申请顺序排序的队列(队列中的线程都处于阻塞状态,等待唤醒),当锁释放的时候,队列中第一个线程获取锁。等待的线程挨个获得锁。
公平锁
非公平锁机制下,试图获取锁的线程们会尝试直接去获取锁,获取不到再进入等待队列。
非公平锁
ReentrantLock类无参构造函数指定了使用非公平锁 NonfairSync,另外又提供了有参构造方法,允许调用者自己指定使用公平锁 FairSync 还是非公平锁 NonfairSync(FairSync和NonfairSync是ReentrantLock 内部类 Sync 的两个子类,而添加锁和释放锁的大部分操作是 Sync 实现的)。
实现
平锁相对于非公平锁多了一个限制条件:hasQueuedPredecessors(),这个方法是判断当前线程是否位于队列的首位
hasQueuedPredecessors()
对比
ReentrantLock 公平锁和非公平锁的实现机制
非公平锁通过插队直接获取锁,减少了一次线程阻塞与唤醒过程,系统整体吞吐量提升。
公平锁不会线程饥饿,非公平锁不能保证等待时间最长的线程优先获取锁。
公平锁与非公平锁的优劣势比较
线程饥饿
synchronized 锁只能是一种非公平锁; ReentrantLock 锁则可以通过构造函数指定使用公平锁还是非公平锁(默认是非公平锁)。
具体
公平锁 & 非公平锁
可重入锁又名递归锁,是指在同一个线程在外层方法获取锁的时候,再进入该线程的内层方法会自动获取锁(前提锁对象得是同一个对象或者class),不会因为之前已经获取过还没释放而阻塞。
可重入锁
反之
不可重入锁
可重入锁先查询当前 state 值,如果status 是 0,表示没有其他线程在占有锁,则该线程获取锁并将 state 执行+1操作。如果status != 0,则判断当前线程是否是获取到这个锁的线程,如果是的话执行state+1操作,如此循环,当前线程便可以重复获得锁。非可重入锁是直接去判断当前 state 的值是否是 0 ,如果是则将其置为1,并返回 true,从而获取锁,如果不是 0 ,则返回 false,获取锁失败,当前线程阻塞。
获取锁时
可重入锁先获取当前 state 的值,在当前线程是持有锁的线程的前提下。如果status-1 == 0,则表示当前线程所有重复获取锁的操作都已经执行完毕,然后该线程才会真正释放锁。非可重入锁则是在确定当前线程是持有锁的线程之后,直接将status置为0,将锁释放。
释放锁时
ReentrantLock和NonReentrantLock都继承AQS,AQS中维护了一个同步状态 state 来计数,state 初始值为0,随着占有锁的线程的子流程占据和释放锁,state进行相应增减操作。getState() 方法能获取最新的 state 值。即通过state的累加和最终状态是否是0来实现可重入。
代码递归时持有同一个对象锁,但是第一次递归的代码持有锁了,第二次递归就拿不到,则死锁。
非重入锁容易导致死锁问题
Java 中以 Reentrant 开头命名的锁都是可重入锁,而且 JDK 提供的所有现成 Lock 的实现类,包括 synchronized 关键字锁都是可重入的。不可重入锁需要自己去实现。不可重入锁的使用场景非常非常少。
可重入锁 & 不可重入锁
Synchronized和ReentrantLock就是一种排它锁,CountDownLatch是一种共享锁,它们都是纯粹的共享锁或独享锁,不能转换形态。
共享锁是指该锁可被多个线程所持有。独享锁,也叫排他锁,是指该锁一次只能被一个线程所持有。共享锁与独享锁互斥,独享锁与独享锁互斥。
共享锁 & 独享锁
ReentrantReadWriteLock(读写锁)就是一种特殊的锁,它既是互斥锁,又是共享锁,read模式是共享,write是互斥(排它锁)的
写锁
可以看到在tryAcquireShared(int unused)方法中,如果其他线程已经获取了写锁,则当前线程获取读锁失败,进入等待状态。如果当前线程获取了写锁或者写锁未被获取,则当前线程(线程安全,依靠CAS保证)增加读状态,成功获取读锁。读锁的每次释放(线程安全的,可能有多个读线程同时释放读锁)均减少读状态,减少的值是“1<<16”。所以读写锁才能实现读读的过程共享,而读写、写读、写写的过程互斥。
读锁
读写锁
共享锁 & 独享锁 & 读写锁
锁的分类
线程会判断此时持有锁的线程是否就是自己(持有锁的线程ID也在对象头里),线程只有一个,很明显偏向锁几乎没有额外开销,性能极高。
向于第一个获得它的线程”的锁。执行完同步代码块后,线程并不会主动释放偏向锁
偏向锁
轻量级锁状态下继续锁竞争,没有抢到锁的线程将自旋,即不停地循环判断锁是否能够被成功获取。获取锁的操作,其实就是通过CAS修改对象头里的锁标志位。不会进入阻塞队列中
轻量级锁是指当锁是偏向锁的时候,却被另外的线程所访问,此时偏向锁就会升级为轻量级锁,其他线程会通过自旋(关于自旋的介绍见文末)的形式尝试获取锁,线程不会阻塞,从而提高性能。
长时间的自旋操作是非常消耗资源的,一个线程持有锁,其他线程就只能在原地空耗CPU,执行不了任何有效的任务,这种现象叫做忙等(busy-waiting)。如果多个线程用一个锁,但是没有发生锁竞争,或者发生了很轻微的锁竞争,那么synchronized就用轻量级锁,允许短时间的忙等现象。这是一种折衷的想法,短时间的忙等,换取线程在用户态和内核态之间切换的开销。
此忙等是有限度的(有个计数器记录自旋次数,默认允许循环10次,可以通过虚拟机参数更改)
何时升级
轻量级锁
重量级锁是指当有一个线程获取锁之后,其余所有等待获取该锁的线程都会处于阻塞状态。
重量级锁
因为已经到编译了
当锁膨胀(inflate)为重量锁时,就不能再退回到轻量级锁。
锁的升级Synchronized
虽然StringBuffer的append是一个同步方法,但是这段程序中的StringBuffer属于一个局部变量,并且不会从该方法中逃逸出去(即StringBuffer sb的引用没有传递到该方法外,不可能被其他线程拿到该引用),所以其实这过程是线程安全的,可以将锁消除。
锁消除即删除不必要的加锁操作。虚拟机即时编辑器在运行时,对一些“代码上要求同步,但是被检测到不可能存在共享数据竞争”的锁进行消除。根据代码逃逸技术,如果判断到一段代码中,堆上的数据不会逃逸出当前线程,那么可以认为这段代码是线程安全的,不必要加锁。
锁消除
这里每次调用stringBuffer.append方法都需要加锁和解锁,如果虚拟机检测到有一系列连串的对同一个对象加锁和解锁操作,就会将其合并成一次范围更大的加锁和解锁操作,即在第一次append方法时进行加锁,最后一次append方法结束后进行解锁。
如果一系列的连续操作都对同一个对象反复加锁和解锁,甚至加锁操作是出现在循环体中的,那即使没有出现线程竞争,频繁地进行互斥同步操作也会导致不必要的性能损耗。如果虚拟机检测到有一串零碎的操作都是对同一对象的加锁,将会把加锁同步的范围扩展(粗化)到整个操作序列的外部。
锁粗化
其他锁的优化
锁相关
线程池通常会维护一些线程(数量为 corePoolSize),这些线程被重复使用来执行不同的任务,任务完成后不会销毁。
降低资源消耗
由于线程池维护了一批 alive 状态的线程,当任务到达时,不需要再创建线程,而是直接去执行任务,从而减少了任务的等待时间。
提高响应速度
使用线程池可以对线程进行统一的分配,调优和监控。
提高线程的可管理性
优势
工厂中有固定的一批工人,称为正式工人,工厂接收的订单由这些工人去完成。当订单增加,正式工人已经忙不过来了,工厂会将生产原料暂时堆积在仓库中,等有空闲的工人时再处理(因为工人空闲了也不会主动处理仓库中的生产任务,所以需要调度员实时调度)。仓库堆积满了后,订单还在增加怎么办?工厂只能临时扩招一批工人来应对生产高峰,而这批工人高峰结束后是要清退的,所以称为临时工。当时临时工也招满后(受限于工位限制,临时工数量有上限),后面的订单只能忍痛拒绝了。我们做如下一番映射:工厂——线程池订单——任务(Runnable)正式工人——核心线程临时工——普通线程仓库——任务队列调度员——getTask()
设计思路
ThreadPoolExecutor:从Java线程池Executor框架体系可以看出:线程池的真正实现类是ThreadPoolExecutor,因此我们接下来重点研究这个类。
ThreadPoolExecutor的构造方法
CPU密集型:核心线程数 = CPU核数 + 1IO密集型:核心线程数 = CPU核数 * 2
corePoolSize(必需):核心线程数。即池中一直保持存活的线程数,即使这些线程处于空闲。但是将allowCoreThreadTimeOut参数设置为true后,核心线程处于空闲一段时间以上,也会被回收。maximumPoolSize(必需):池中允许的最大线程数。当核心线程全部繁忙且任务队列打满之后,线程池会临时追加线程,直到总线程数达到maximumPoolSize这个上限。keepAliveTime(必需):线程空闲超时时间。当非核心线程处于空闲状态的时间超过这个时间后,该线程将被回收。将allowCoreThreadTimeOut参数设置为true后,核心线程也会被回收。unit(必需):keepAliveTime参数的时间单位。有:TimeUnit.DAYS(天)、TimeUnit.HOURS(小时)、TimeUnit.MINUTES(分钟)、TimeUnit.SECONDS(秒)、TimeUnit.MILLISECONDS(毫秒)、TimeUnit.MICROSECONDS(微秒)、TimeUnit.NANOSECONDS(纳秒)workQueue(必需):任务队列,采用阻塞队列实现。当核心线程全部繁忙时,后续由execute方法提交的Runnable将存放在任务队列中,等待被线程处理。threadFactory(可选):线程工厂。指定线程池创建线程的方式。handler(可选):拒绝策略。当线程池中线程数达到maximumPoolSize且workQueue打满时,后续提交的任务将被拒绝,handler可以指定用什么方式拒绝任务。
同步队列。这是一个内部没有任何容量的阻塞队列,任何一次插入操作的元素都要等待相对的删除/读取操作,否则进行插入操作的线程就要一直等待,反之亦然。
SynchronousQueue:同步队列
无界队列(严格来说并非无界,上限是Integer.MAX_VALUE),基于链表结构。使用无界队列后,当核心线程都繁忙时,后续任务可以无限加入队列,因此线程池中线程数不会超过核心线程数。这种队列可以提高线程池吞吐量,但代价是牺牲内存空间,甚至会导致内存溢出。另外,使用它时可以指定容量,这样它也就是一种有界队列了。
LinkedBlockingQueue:无界队列
有界队列,基于数组实现。在线程池初始化时,指定队列的容量,后续无法再调整。这种有界队列有利于防止资源耗尽,但可能更难调整和控制。
ArrayBlockingQueue:有界队列
存放在PriorityBlockingQueue中的元素必须实现Comparable接口,这样才能通过实现compareTo()方法进行排序。优先级最高的元素将始终排在队列的头部;只能保证第一个的顺序是正确的,后面的无法保证。
PriorityBlockingQueue:支持优先级排序的无界阻塞队列
基于二叉堆实现,同时具备:无界队列、阻塞队列、优先队列的特征。DelayQueue延迟队列中存放的对象,必须是实现Delayed接口的类对象。通过执行时延从队列中提取任务,时间没到任务取不出来。更多内容请见DelayQueue。
DelayQueue:延迟队列
基于链表实现,既可以从尾部插入/取出元素,还可以从头部插入元素/取出元素。
LinkedBlockingDeque:双端队列
这次不空就拿数据,空了就挖个坑,下次生产者就填坑
由链表结构组成的无界阻塞队列。这个队列比较特别的时,采用一种预占模式,意思就是消费者线程取元素时,如果队列不为空,则直接取走数据,若队列为空,那就生成一个节点(节点元素为null)入队,然后消费者线程被等待在这个节点上,后面生产者线程入队时发现有一个元素为null的节点,生产者线程就不入队了,直接就将元素填充到该节点,并唤醒该节点等待的线程,被唤醒的消费者线程取走元素。
LinkedTransferQueue
额外
任务队列workQueue
当线程池workQueue已满且无法再创建新线程池时,就要拒绝后续任务了。拒绝策略需要实现RejectedExecutionHandler接口,不过Executors框架已经为我们实现了4种拒绝策略
AbortPolicy(默认) 丢弃并抛出RejectedExecutionException异常。
但并非是由线程池的线程处理,而是交由任务的调用线程处理。因为线程池已经拒绝了。
CallerRunsPolicy:直接运行这个任务的run方法
DiscardPolicy:直接丢弃任务
DiscardOldestPolicy:将当前处于等待队列列头的等待任务强行取出,然后再试图将当前被拒绝的任务提交到线程池执行。
策略
拒绝策略handler
这个参数不是必选项,Executors类已经为我们非常贴心地提供了一个默认的线程工厂
线程工厂threadFactory
参数解释
runState表示当前线程池的状态,它是一个 volatile 变量用来保证线程之间的可见性。
线程池有5种状态
RUNNING:当创建线程池后,初始时,线程池处于RUNNING状态;
状态切换:调用线程池的shutdown()接口时,线程池由RUNNING -> SHUTDOWN。
SHUTDOWN:如果调用了shutdown()方法,则线程池处于SHUTDOWN状态,此时线程池不能够接受新的任务,它会等待所有任务执行完毕;
状态切换:调用线程池的shutdownNow()接口时,线程池由(RUNNING or SHUTDOWN ) -> STOP。
STOP:如果调用了shutdownNow()方法,则线程池处于STOP状态,此时线程池不能接受新的任务,并且会去尝试终止正在执行的任务;
状态切换:当线程池在SHUTDOWN状态下,阻塞队列为空并且线程池中执行的任务也为空时,就会由 SHUTDOWN -> TIDYING。当线程池在STOP状态下,线程池中执行的任务为空时,就会由STOP -> TIDYING。
TIDYING:当所有的任务已终止,ctl记录的”任务数量”为0,线程池会变为TIDYING状态。
状态切换:线程池处在TIDYING状态时,执行完terminated()之后,就会由 TIDYING -> TERMINATED。
TERMINATED:当线程池处于SHUTDOWN或STOP状态,并且所有工作线程已经销毁,任务缓存队列已经清空或执行结束后,线程池被设置为TERMINATED状态。
状态解释
线程池状态
默认情况下,创建线程池之后,线程池中是没有线程的,需要提交任务之后才会创建线程。
prestartCoreThread():boolean prestartCoreThread(),初始化一个核心线程
prestartAllCoreThreads():int prestartAllCoreThreads(),
立即初始化线程
线程初始化
执行完任务后终止。而是要等所有任务缓存队列中的任务都执行完后才终止,但再也不会接受新的任务
shutdown()
立即终止线程池,并尝试打断正在执行的任务,并且清空任务缓存队列,返回尚未执行的任务
shutdownNow()
线程池关闭
设置核心池大小
setCorePoolSize
设置线程池最大能创建的线程数目大小 当上述参数从小变大时,ThreadPoolExecutor进行线程赋值,还可能立即创建新的线程来执行任务。
setMaximumPoolSize
线程池容量调整
初始化&容量调整&关闭
深入线程池
通过构造方法使用ThreadPoolExecutor是线程池最直接的使用方式
固定容量线程池。其特点是最大线程数就是核心线程数,意味着线程池只能创建核心线程,keepAliveTime为0,即线程执行完任务立即回收。任务队列未指定容量,代表使用默认值Integer.MAX_VALUE。适用于需要控制并发线程的场景。
LinkedBlockingQueue无界队列
构造函数的keepAliveTime=0,然后核心线程个数和最大线程个数都限制死了,所以虽然用的是LinkedBlockingQueue无界队列,但是其实不会用到无界的特性,其实是当有界来用。
固定数量的线程池 newFixedThreadPool
缓存线程池。没有核心线程,普通线程数量为Integer.MAX_VALUE(可以理解为无限),线程闲置60s后回收,任务队列使用SynchronousQueue这种无容量的同步队列。适用于任务量大但耗时低的场景。
SynchronousQueue同步队列
缓存线程池 newCachedThreadPool
单线程线程池。特点是线程池中只有一个线程(核心线程),线程执行完任务立即回收,使用有界阻塞队列(容量未指定,使用默认值Integer.MAX_VALUE)
核心线程和最大线程都是1,keepAliveTime=0,这个也是拿LinkedBlockingQueue无界队列当有界队列使用。
只有一个线程的线程池 newSingleThreadPool
定时线程池。指定核心线程数量,普通线程数量无限,线程执行完任务立即回收,任务队列为延时阻塞队列。这是一个比较特别的线程池,适用于执行定时或周期性的任务。
DelayQueue延迟队列
因为需要延时,这个使用DelayQueue延迟队列就没有任何毛病
延时线程池 newScheduleThreadPool
并行线程池 newWorkStealingPool
线程池使用及种类
父节点链接
尽量使用较小的线程池,一般为CPU核心数+1。 因为CPU密集型任务使得CPU使用率很高,若开过多的线程数,只能增加上下文切换的次数,因此会带来额外的开销。
CPU密集型,则线程池大小设置为N+1;
可以使用稍大的线程池,一般为2*CPU核心数。 IO密集型任务CPU使用率并不高,因此可以让CPU在等待IO的时候去处理别的任务,充分利用CPU时间。
IO密集型,则线程池大小设置为2N+1。
具体情况具体分析,可通过压测分析
混合型任务
线程池线程数的确定
线程池任务的执行
线程池
栅栏锁
CyclicBarrier
实现最大的并行性:有时我们想同时启动多个线程,实现最大程度的并行性。例如,我们想测试一个单例类。如果我们创建一个初始计数器为1的CountDownLatch,并让其他所有线程都在这个锁上等待,只需要调用一次countDown()方法就可以让其他所有等待的线程同时恢复执行。
开始执行前等待N个线程完成各自任务:例如应用程序启动类要确保在处理用户请求前,所有N个外部系统都已经启动和运行了
死锁检测:一个非常方便的使用场景是你用N个线程去访问共享资源,在每个测试阶段线程数量不同,并尝试产生死锁。
用途
多等一
CountDownLatch闭锁
多线程访问有限资源的并发访问控制
限制最大线程数
Semaphpre信号量
Exchanger
并发工具类
同步集合
ConcuerrentHashMap
ConcuerrentLinkedQueue
ConcuerrentSkipListMap
ConcuerrentSkipListSet
CopyOnWriteList
并发集合
多线程集合
阻塞队列
java.util.concurrent
引用原子类
java.util.concurrent.atomic
java.util.concurrent.locks
包concurrent
Threadlocal
Fork/Join
其他
并发编程
Java Virtual Machine是Java虚拟机,Java程序需要运行在虚拟机上,不同的平台有自己的虚拟机,因此Java语言可以实现跨平台。
JVM
Java Runtime Environment包括Java虚拟机和Java程序所需的核心类库等。核心类库主要是java.lang包:包含了运行Java程序必不可少的系统类,如基本数据类型、基本数学函数、字符串处理、线程、异常处理类等,系统缺省加载这个包如果想要运行一个开发好的Java程序,计算机中只需要安装JRE即可。
JRE
JDK
一个是运行环境,一个是开发大礼包。部署只要JRE就行了。
Java Development Kit是提供给Java开发人员使用的,其中包含了Java的开发工具,也包括了JRE。所以安装了JDK,就无需再单独安装JRE了。其中的开发工具:编译工具(javac.exe),打包工具(jar.exe)等
为什么JRE和JDK分开
JVM、JRE和JDK的关系
这个跨平台是中间语言(JVM)实现的跨平台java有JVM从软件层面屏蔽了底层硬件、指令层面的细节让他兼容各种系统
为什么说java是跨平台语言
基础概念
方法区(Method Area)----线程共享的内存区域,它用于存储已被Java虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码等数据。
从内存回收角度来看java堆可分为:新生代和老生代
从内存分配的角度看,线程共享的Java堆中可能划分出多个线程私有的分配缓冲区
Java堆(Java Heap)----线程共享的内存区域,存放对象实例,所有的对象实例以及数组都要在堆上分配
也叫局部变量表,它是Java方法执行的内存模型。里面会对局部变量,动态链表,方法出口,栈的操作(入栈和出栈)进行存储,且线程独享。同时如果我们听到局部变量表,那也是在说虚拟机栈。
如果线程请求的栈的深度大于虚拟机栈的最大深度,就会报堆栈溢出错误 **StackOverflowError** (这种错误经常出现在递归中)。Java虚拟机也可以动态扩展,但随着扩展会不断地申请内存,当无法申请足够内存时就会报错 **OutOfMemoryError**。
虚拟机栈存在的异常
对于栈来说,不存在垃圾回收。只要程序运行结束,栈的空间自然就会释放了。栈的生命周期和所处的线程是一致的。这里补充一句:8种基本类型的变量+对象的引用变量(地址值、也叫指针)都是在栈里面分配内存。
虚拟机栈的生命周期
虚拟机栈的执行
局部变量的复用
Java虚拟机栈(Java Virtual Machine Stacks)生命周期和线程相同
本地方法栈(Native Method Stack)本地方法栈很好理解,他很栈很像,只不过方法上带了 native 关键字的栈字即本地方法栈中就是C和C++的代码
程序计数器----其实就是一个指针它指向了我们程序中下一句需要执行的指令,它也是内存区域中唯一一个不会出现OutOfMemoryError的区域,而且占用内存空间小到基本可以忽略不计。这个内存仅代表当前线程所执行的字节码的行号指示器,字节码解析器通过改变这个计数器的值选取下一条需要执行的字节码指令。
堆的物理地址分配对对象是不连续的。因此性能慢些。在GC的时候也要考虑到不连续的分配,所以有各种算法。比如,标记-消除,复制,标记-压缩,分代(即新生代使用复制算法,老年代使用标记——压缩)栈使用的是数据结构中的栈,先进后出的原则,物理地址分配是连续的。所以性能快。内存分别堆因为是不连续的,所以分配的内存是在运行期确认的,因此大小不固定。一般堆大小远远大于栈。栈是连续的,所以分配的内存大小要在编译期就确认,大小是固定的。存放的内容堆存放的是对象的实例和数组。因此该区更关注的是数据的存储栈存放:局部变量,操作数栈,返回结果。该区更关注的是程序方法的执行。PS:静态变量放在方法区静态的对象还是放在堆。程序的可见度堆对于整个应用程序都是共享、可见的。栈只对于线程是可见的。所以也是线程私有。他的生命周期和线程相同。
说一下堆栈的区别
成员变量在堆上局部变量在栈上静态变量在方法区中
JVM运行时数据区
JAVA内存结构
程序在运行过程中,会产生大量的内存垃圾(一些没有引用指向的内存对象都属于内存垃圾,因为这些对象已经无法访问,程序用不了它们了,对程序而言它们已经死亡),为了确保程序运行时的性能,java虚拟机在程序运行的过程中不断地进行自动的垃圾回收(GC)
System.gc();建议执行垃圾收集器,但是他是否执行,什么时候执行却都是不可知的
手动回收垃圾
属于Object的方法在类中重写此方法,则在垃圾收集器删除对象之前会执行,可以借此留下信息或者重新“复活”
finalize方法
两个生僻的东西
通过参数 –XX:SurvivorRatio 来设定
JVM 每次只会使用 Eden 和其中的一块 Survivor 区域来为对象服务,所以无论什么时候,总是有一块 Survivor 区域是空闲着的
新生代 ( Young ) 又被划分为三个区域:Eden、From Survivor、To Survivor。Edem : From Survivor : To Survivor = 8 : 1 : 1
通过参数 –XX:NewRatio 来指定
新生代 ( Young ) 与老年代 ( Old ) 的比例的值为 1:2
永久代就是JVM的方法区。在这里都是放着一些被虚拟机加载的类信息,静态变量,常量等数据。这个区中的东西比老年代和新生代更不容易回收。
新生代中,每次垃圾收集时都发现大批对象死去,只有少量对象存活,便采用了复制算法,只需要付出少量存活对象的复制成本就可以完成收集。
老年代中因为对象存活率高、没有额外空间对它进行分配担保,就必须采用“标记-清理”或者“标记-整理”算法。
更便于回收,采用最适当的收集算法
分代的原因
初始:新生代分为Eden和Survivor (From与To,这里简称一个区)两个区。加上老年代就这三个区。数据会首先分配到Eden区当中(当然也有特殊情况,如果是大对象那么会直接放入到老年代(大对象是指需要大量连续内存空间的java对象)
晋升老年代的年龄是可以设置的。
垃圾回收的过程
触发条件1、eden区满时,触发MinorGC。即申请一个对象时,发现eden区不够用,则触发一次MinorGC。2、新创建的对象大小 > Eden所剩空间
Minor GC是新生代GC,指的是发生在新生代的垃圾收集动作。由于java对象大都是朝生夕死的,所以Minor GC非常频繁,一般回收速度也比较快。
Minor GC
触发条件:1、长期存活的对象将进入老年代2、MinorGC后存活的对象超过了老年代剩余空间3、永久代空间不足4、执行System.gc()5、CMS GC异常6、堆内存分配很大的对象
Major GC是老年代GC,指的是发生在老年代的GC,通常执行Major GC会连着Minor GC一起执行。Major GC的速度要比Minor GC慢的多。
Major GC
1)调用System.gc()时,系统建议执行Full GC,但是不必然执行。(2)老年代空间不足。(3)方法区空间不足。(4)通过Minor GC后进入老年代的平均大小大于老年代的可用内存。(5)由Eden区、survivor space0(From Space)区向survivor space1(To Space)区复制时,对象大小大于To Space可用内存,则把该对象转存到老年代,且老年代的可用内存小于该对象大小。说明:Full GC 是开发或调优中尽量要避免的。这样暂时时间会短一些出现OOM一定会出现Full GC
触发条件和Major GC一样
Full GC是清理整个堆空间,包括年轻代和老年代
Full GC
垃圾回收器的种类
新生代、老年代和永久代
每个对象在创建的时候,就给这个对象绑定一个计数器。每当有一个引用指向该对象时,计数器加一;每当有一个指向它的引用被删除时,计数器减一。这样,当没有引用指向该对象时,计数器为0就代表该对象死亡
引用计数算法的实现简单,判定效率也很高,在大部分情况下它都是一个不错的算法,
优点
当代码执行完line4时,两个对象的引用计数均为2。此时将myObject1和myObject2分别置为null,以前一个对象为例,它的引用计数将减1。若要满足垃圾回收的条件,需要清除myObject2中的ref这个引用,而要清除掉这个引用的前提条件是myObject2引用的对象被回收,可是该对象的引用计数也为1,因为myObject1.ref指向了它。以此类推,也就进入一种死循环的状态。myObject1和myObject2将不能被回收,因为他们的引用计数无法为零。
MyObject myObject1 = new MyObject();MyObject myObject2 = new MyObject();myObject1.ref = myObject2;myObject2.ref = myObject1;myObject1 = null;myObject2 = null;
很难解决对象之间相互循环引用的问题。
引用计数法(主流JAVA虚拟机都不用)
对象的 finalize 方法可以起死回生。在此方法里将当前对象与 GC Roots 关联,这样执行 finalize 方法之后,GC 会再次判断对象是否可达
该种方法是从GC Roots开始向下搜索,搜索所走过的路径为引用链。当一个对象到GC Roots没用任何引用链时,则证明此对象是不可用的,表示可以回收。
方法中的栈,栈中有他的栈成员 Integer a = XXX,当方法没有被释放,没有出栈的时候,方法没有被弹出的时候,那Integer a 所引用的对象也是不会被回收的,在什么情况下回收呢,就是这个对象没有挂在根上,就会被回收。
1、虚拟机栈(栈帧中的本地变量表)中引用的对象;(局部变量)
2、本地方法栈中JNI(即一般说的Native方法)引用的对象;
3、方法区中常量引用的对象;
4、方法区中类静态属性引用的对象;
可以作为GC Roots 的对象(两栈两方法)
可达性分析法
如何判断对象是否存活
引用计数法
该算法将内存平均分成两部分,然后每次只使用其中的一部分,当这部分内存满的时候,将内存中所有存活的对象复制到另一个内存中,然后将之前的内存清空,只使用这部分内存,循环下去。这个算法与标记-整理算法的区别在于,该算法不是在同一个区域复制,而是将所有存活的对象复制到另一个区域内。
所以适合新生代
在存活对象不多的情况下,性能高,能解决内存碎片和java垃圾回收算法之-标记清除 中导致的引用更新问题。
会造成一部分的内存浪费。不过可以根据实际情况,将内存块大小比例适当调整;如果存活对象的数量比较大,复制算法的性能会变得很差。
1、复制算法一般是使用在新生代中,因为新生代中的对象一般都是朝生夕死的,存活对象的数量并不多(98%熬不过第一轮收集),这样使用复制算法进行拷贝时效率比较高。 2、每次分配内存只使用Eden和其中一块Survivor。 发生垃圾搜集时, 将font color=\"#f44336\
应用场景
复制算法
为每个对象存储一个标记位,记录对象的状态(活着或是死亡)。分为两个阶段,一个是标记阶段,这个阶段内,为每个对象更新标记位,检查对象是否死亡;第二个阶段是清除阶段,该阶段对死亡的对象进行清除,执行 GC 操作。
是可以解决循环引用的问题必要时才回收(内存不足时)
回收时,应用需要挂起,也就是stop the world。尤其是要扫描的对象比较多的时候标记和清除的效率不高,会造成font color=\"#f44336\
标记–清除算法(Mark-Sweep)
标记清除算法和标记整理算法非常相同,但是标记整理算法在标记清除算法之上解决内存碎片化(有些人叫\"标记整理算法\"为\"标记压缩算法\")1、在标记阶段,该算法也将所有对象标记为存活和死亡两种状态;2、不同的是,在第二个阶段,该算法并没有直接对死亡的对象进行清理,而是将所有存活的对象整理一下,放到另一处空间,(则可以整理出连续的大存储空间)3、然后把剩下的所有对象全部清除。这样就达到了标记-整理的目的。
解决标记清除算法出现的内存碎片问题,
压缩阶段,由于移动了可用对象,需要去更新引用。
标记–整理算法
在新生代,每次垃圾收集器都发现有大批对象死去,只有少量存活,采用复制算法,只需要付出少量存活对象的复制成本就可以完成收集。
新生代
而老年代中因为对象存活率高、没有额外空间对它进行分配担保,就必须“标记清除法或者标记整理算法进行回收。
老年代
分代算法(主要的算法就是上面四种,这个是附加的)
垃圾回收机制策略(也称为GC的算法)
垃圾收集器是垃圾回收算法(引用计数法、标记清除法、标记整理法、复制算法)的具体实现,不同垃圾收集器、不同版本的JVM所提供的垃圾收集器可能会有很在差别。
新生代收集器,使用复制算法收集新生代垃圾。单线程的收集器,GC工作时,其它所有线程都将停止工作。简单高效,适合单 CPU 环境。单线程没有线程交互的开销,因此拥有最高的单线程收集效率。
设置垃圾收集器:\"-XX:+UseSerialGC\" --添加该参数来显式的使用改垃圾收集器;
Serial 收集器:新生代。发展历史最悠久的收集器。它是一个单线程收集器,它只会使用一个 CPU 或者线程去完成垃圾收集工作,而且在它进行垃圾收集时,必须暂停其他所有的工作线程,直到它收集结束。
Serial复制算法
新生代收集器。ParNew垃圾收集器是Serial收集器的多线程版本,采用复制算法。除了多线程外,其余的行为、特点和Serial收集器一样。只有它能与 CMS 收集器配合使用。但在单个CPU环境中,不比Serail收集器好,多线程使用它比较好。
设置垃圾收集器:\"-XX:+UseParNewGC\" --强制指定使用ParNew; 设置垃圾收集器: \"-XX:+UseConcMarkSweepGC\" --指定使用CMS后,会默认使用ParNew作为新生代收集器;设置垃圾收集器参数:\"-XX:ParallelGCThreads\" --指定垃圾收集的线程数量,ParNew默认开启的收集线程与CPU的数量相同;
ParNew 收集器:新生代。Serial 的多线程版本,即同时启动多个线程去进行垃圾收集。
ParNew复制算法
新生代收集器。采用复制算法。多线程收集。与ParNew 不同的是:高吞吐量为目标,(减少垃圾收集时间,让用户代码获得更长的运行时间)
设置垃圾收集器:\"-XX:+UseParallelGC\" --添加该参数来显式的使用改垃圾收集器;设置垃圾收集器参数:\"-XX:MaxGCPauseMillis\" --控制垃圾回收时最大的停顿时间(单位ms)设置垃圾收集器参数:\"-XX:GCTimeRatio\" --控制程序运行的吞吐量大小吞吐量大小=代码执行时间/(代码执行时间+gc回收的时间)设置垃圾收集器参数:\"-XX:UseAdaptiveSizePolicy\" --内存调优交给虚拟机管理
Parallel Scavenge 收集器:新生代。和 ParNew 的关注点不一样,该收集器更关注吞吐量,尽快地完成计算任务。也是多线程的。
Parallel Scavenge复制算法
前者偏重效率:会有stop the world后者关注吞吐量: 减少垃圾收集时间,让用户代码获得更长的运行时间
Serial/ParNew和Parallel Scavenge对比
新生代回收器
老年代收集器, 采用\"标记-整理\"算法。单线程收集。
在JDK1.5及之前,与Parallel Scavenge收集器搭配使用,在JDK1.6后有Parallel Old收集器可搭配。现在的作为CMS收集器的后备预案,在并发收集发生Concurrent Mode Failure时使用
Serial Old 收集器:Serial 的老年代版本,使用标记 - 整理算法。
Serial Old标记-整理
针对老年代。采用\"标记-整理\"算法。多线程收集。但在单个CPU环境中,不比Serial Old收集器好,多线程使用它比较好。
设置垃圾收集器:\"-XX:+UseParallelOldGC\":指定使用Parallel Old收集器;
Parallnel old 收集器,多线程:Parallel 的老年代版本,使用标记 - 整理算法。
Parallel Old标记-整理
针对老年代,采用标记-清楚法清除垃圾;基于\"标记-清除\"算法(不进行压缩操作,产生内存碎片);以获取最短回收停顿时间为目标;并发收集、低停顿;CMS收集器有3个明显的缺点:1.对CPU资源非常敏感、2.无法处理浮动垃圾,可能出现\"Concurrent Mode Failure\"失败3.产生大量内存碎片垃圾收集线程与用户线程(基本上)可以同时工作
设置垃圾收集器:\"-XX:+UseConcMarkSweepGC\":指定使用CMS收集器;
CMS 收集器:老年代。并发收集、低停顿。是一种以获取最短回收停顿时间为目标的收集器,适用于互联网站或者 B/S 系统的服务端上。
CMS标记-清除
老年代回收器
能充分利用多CPU、多核环境下的硬件优势;可以并行来缩短(Stop The World)停顿时间;也可以并发让垃圾收集与用户程序同时进行;分代收集,收集范围包括新生代和老年代能独立管理整个GC堆(新生代和老年代),而不需要与其他收集器搭配;能够采用不同方式处理不同时期的对象;应用场景可以面向服务端应用,针对具有大内存、多处理器的机器;采用标记-整理 + 复制算法来回收垃圾
G1 收集器:分代收集器。当今收集器技术发展最前沿成果之一,是一款面向服务端应用的垃圾收集器。G1可以说是CMS的终极改进版,解决了CMS内存碎片、更多的内存空间登问题。虽然流程与CMS比较相似,但底层的原理已是完全不同。
初始标记(Initial Marking):这阶段仅仅只是标记GC Roots能直接关联到的对象并修改TAMS(Next Top at Mark Start)的值,让下一阶段用户程序并发运行时,能在正确的可用的Region中创建新对象,这阶段需要停顿线程,但是耗时很短。而且是借用进行Minor GC的时候同步完成的,所以G1收集器在这个阶段实际并没有额外的停顿。并发标记(Concurrent Marking):从GC Roots开始对堆的对象进行可达性分析,递归扫描整个堆里的对象图,找出存活的对象,这阶段耗时较长,但是可以与用户程序并发执行。当对象图扫描完成以后,还要重新处理SATB记录下的在并发时有引用变动的对象。最终标记(Final Marking):对用户线程做另一个短暂的暂停,用于处理并发阶段结束后仍遗留下来的最后那少量的 SATB 记录。筛选回收(Live Data Counting and Evacuation):负责更新 Region 的统计数据,对各个 Region 的回收价值和成本进行排序,根据用户所期望的停顿时间来制定回收计划。可以自由选择多个Region来构成会收集,然后把回收的那一部分Region中的存活对象==复制==到空的Region中,在对那些Region进行清空。
G1 从整体来看是基于“标记-整理”算法实现的收集器;从局部上来看是基于“标记-复制”算法实现的
G1
整个堆
Serial / Serial OldParNew / Serial OldParallel Scavenge / Serial OldSerial / CMSParNew / CMSParallel Scavenge / Parallel OldG1
搭配使用
分类
垃圾回收器
JVM的垃圾回收机制
JVM内存参数简述
-XX:+UseSerialGC:设置串行收集器,年轻带收集器 -XX:+UseParNewGC:设置年轻代为并行收集。可与 CMS 收集同时使用。JDK5.0 以上,JVM 会根据系统配置自行设置,所以无需再设置此值。-XX:+UseParallelGC:设置并行收集器,目标是目标是达到可控制的吞吐量-XX:+UseParallelOldGC:设置并行年老代收集器,JDK6.0 支持对年老代并行收集。 -XX:+UseConcMarkSweepGC:设置年老代并发收集器-XX:+UseG1GC:设置 G1 收集器,JDK1.9默认垃圾收集器
JVM的GC收集器设置
1、在实际工作中,我们可以直接将初始的堆大小与最大堆大小相等,这样的好处是可以减少程序运行时垃圾回收次数,从而提高效率。font color=\"#f44336\
调优总结
JVM参数配置及调优
JVM将类描述数据从.class文件中加载到内存,并对数据进行,解析和初始化,最终形成被JVM直接使用的Java类型。 类从被加载到JVM中开始,到卸载为止,整个生命周期包括:加载、验证、准备、解析、初始化、使用和卸载七个阶段。其中验证,准备,解析三个部分统称为连接。
JVM 中类的装载是由类加载器(ClassLoader)和它的子类来实现的,Java 中的类加载器是一个重要的 Java 运行时系统组件,它负责在运行时查找和装入类文件中的类。由于 Java 的跨平台性,经过编译的 Java 源程序并不是一个可执行程序,而是一个或多个类文件。当 Java 程序需要使用某个类时,JVM 会确保这个类已经被加载、连接(验证、准备和解析)和初始化。类的加载是指把类的.class 文件中的数据读入到内存中,通常是创建一个字节数组读入.class 文件,然后产生与所加载类对应的 Class 对象。加载完成后,Class 对象还不完整,所以此时的类还不可用。当类被加载后就进入连接阶段,这一阶段包括验证、准备(为静态变量分配内存并设置默认的初始值)和解析(将符号引用替换为直接引用)三个步骤。最后 JVM 对类进行初始化,包括:1)如果类存在直接的父类并且这个类还没有被初始化,那么就先初始化父类;2)如果类中存在初始化语句,就依次执行这些初始化语句。类的加载是由类加载器完成的,类加载器包括:根加载器(BootStrap)、扩展加载器(Extension)、系统加载器(System)和用户自定义类加载器(java.lang.ClassLoader 的子类)。从 Java 2(JDK 1.2)开始,类加载过程采取了父亲委托机制(PDM)。PDM 更好的保证了 Java 平台的安全性,在该机制中,JVM 自带的Bootstrap 是根加载器,其他的加载器都有且仅有一个父类加载器。类的加载首先请求父类加载器加载,父类加载器无能为力时才由其子类加载器自行加载。JVM 不会向 Java 程序提供对 Bootstrap 的引用。下面是关于几个类加载器的说明:Bootstrap:一般用本地代码实现,负责加载 JVM 基础核心类库(rt.jar);Extension:从 java.ext.dirs 系统属性所指定的目录中加载类库,它的父加载器是 Bootstrap;System:又叫应用类加载器,其父类是 Extension。它是应用最广泛的类加载器。它从环境变量 classpath 或者系统属性java.class.path 所指定的目录中记载类,是用户自定义加载器的默认父加载器。
类加载过程
类加载时机
类加载器
全盘负责
缓存机制
所谓的双亲委派,则是先让父类加载器试图加载该Class,只有在父类加载器无法加载该类时才尝试从自己的类路径中加载该类。通俗的讲,就是某个特定的类加载器在接到加载类的请求时,首先将加载任务委托给父加载器,依次递归,如果父加载器可以完成类加载任务,就成功返回;只有父加载器无法完成此加载任务时,才自己去加载。
通过这种层级关可以避免类的重复加载,当父亲已经加载了该类时,就没有必要子ClassLoader再加载一次
解决重复加载问题
java核心api中定义类型不会被随意替换,假设通过网络传递一个名为java.lang.Integer的类,通过双亲委托模式传递到启动类加载器,而启动类加载器在核心Java API发现这个名字的类,发现该类已被加载,并不会重新加载网络传递的过来的java.lang.Integer,而直接返回已加载过的Integer.class,这样便可以防止核心API库被随意篡改。
更加安全,保证核心类不会被篡改
你改包名,类加载器会发现同路径已经加载一个同名的了
你在同名类里加方法,引导类加载器的核心类库API里的 String 类中并没有 main() 方法
自定义类:java.lang.String (没用)
在 java.lang 包下整个 ShkStart 类 (自定义类名),java.lang 包下不允许我们自定义类
自定义类:java.lang.ShkStart(报错:阻止创建 java.lang开头的类)
举例
双亲委派
类加载机制
Java类加载过程本质上就是将class文件加载到内存的过程,包括加载、连接(验证、准备、解析)、初始化,加载过程是由类加载器将类的二进制字节流加载存储到运行时内存区的方法区,将其转换为一个与目标类型对应的Class对象实例,这个Class对象会作为方法区中该类的各种数据的访问入口。连接是将类数据信息合并到JVM的运行时状态中,包含分配空间、赋值(final赋值,static只做初始化)、添加引用关系等操作。初始化阶段将所有static域按照程序指定操作执行,过程中保证按照双亲委派策略父类优先于子类,通过这种层级关可以避免类的重复加载,当父亲已经加载了该类时,就没有必要子ClassLoader再加载一次,并且在多线程情况下保证只有一个线程去操作静态数据。
Java类加载过程
双亲委派机制
面试高频
1、 在实际开发中,我们可以通过自定义ClassLoader,并重写父类的loadClass方法,来打破这一机制。2、 SPI就是打破了双亲委托机制的(SPI:服务提供发现)。
Java的类加载是否一定遵循双亲委托模型?
类加载机制和类加载器(ClassLoader)的详解
我们平常典型编码Object obj = new Object()中的obj就是强引用。通过关键字new创建的对象所关联的引用就是强引用。
当JVM内存空间不足,JVM宁愿抛出OutOfMemoryError运行时错误(OOM),使程序异常终止,也不会靠随意回收具有强引用的“存活”对象来解决内存不足的问题。对于一个普通的对象,如果没有其他的引用关系,只要超过了引用的作用域或者显式地将相应(强)引用赋值为 null,就是可以被垃圾收集的了,具体回收时机还是要看垃圾收集策略。
特点
项目中到处都是。
应用
强引用
软引用通过SoftReference类实现。 软引用的生命周期比强引用短一些。
软引用通常用来实现内存敏感的缓存。如果还有空闲内存,就可以暂时保留缓存,当内存不足时清理掉,这样就保证了使用缓存的同时,不会耗尽内存
图片缓存框架中,“内存缓存”中的图片是以这种引用来保存,使得JVM在发生OOM之前,可以回收这部分缓存。
实例
软引用
弱引用通过WeakReference类实现。 弱引用的生命周期比软引用短。
。在垃圾回收器线程扫描它所管辖的内存区域的过程中,一旦发现了具有弱引用的对象,不管当前内存空间足够与否,都会回收它的内存。由于垃圾回收器是一个优先级很低的线程,因此不一定会很快回收弱引用的对象。弱引用可以和一个引用队列(ReferenceQueue)联合使用,如果弱引用所引用的对象被垃圾回收,Java虚拟机就会把这个弱引用加入到与之关联的引用队列中。
弱应用同样可用于内存敏感的缓存。
在静态内部类中,经常会使用虚引用。例如,一个类发送网络请求,承担callback的静态内部类,则常以虚引用的方式来保存外部类(宿主类)的引用,当外部类需要被JVM回收时,不会因为网络请求没有及时回来,导致外部类不能被回收,引起内存泄漏。
弱引用
虚引用也叫幻象引用,通过PhantomReference类来实现。无法通过虚引用访问对象的任何属性或函数。幻象引用仅仅是提供了一种确保对象被 finalize 以后,做某些事情的机制。如果一个对象仅持有虚引用,那么它就和没有任何引用一样,在任何时候都可能被垃圾回收器回收。
可用来跟踪对象被垃圾回收器回收的活动,当一个虚引用关联的对象被垃圾收集器回收之前会收到一条系统通知。
一种引用的get()方法返回总是null,所以,可以想象,在平常的项目开发肯定用的少。
虚引用
强引用就像大老婆,关系很稳固。软引用就像二老婆,随时有失宠的可能,但也有扶正的可能。弱引用就像情人,关系不稳定,可能跟别人跑了。幻像引用就是梦中情人,只在梦里出现过。
强引用、软引用、弱引用、虚引用的区别?
(out of memory)通俗理解就是内存不够,通常在运行大型软件或游戏时,软件或游戏所需要的内存远远超出了你主机内安装的内存所承受大小,就叫内存溢出。
内存溢出(out of memory)
指程序中己[动态分配]的堆内存由于某种原因程序未释放或无法释放,造成系统内存的浪费,导致程序运行速度减慢甚至系统崩溃等严重后果
内存泄漏,即部分对象虽然已经不再使用,但是因为有root持有引用,所以并没有被销毁,所占用的内存一直没有被释放。一次两次发生影响不大。如果频繁发生,那么可用内存会渐渐不足,最终在某一次请求内存时发现内存不足而发生oom。这里要明确一个概念,只有强引用会发生内存泄漏,而weak等引用因为其特殊机制,所以影响不大。泄露影响比较大的就是一些大对象,常见的比如某些资源,bitmap,以及activity。
内存泄漏(Memory Leak)
内存泄漏和内存溢出
第一步,修改JVM启动参数,直接增加内存。(-Xms,-Xmx参数一定不要忘记加。) 第二步,检查错误日志,查看“OutOfMemory”错误前是否有其它异常或错误。 第三步,对代码进行走查和分析,找出可能发生内存溢出的位置第四步,使用内存查看工具动态查看内存使用情况
启动参数内存值设定的过小
1.内存中加载的数据量过于庞大,如一次从数据库取出过多数据; 2.集合类中有对对象的引用,使用完后未清空,使得JVM不能回收; 3.代码中存在死循环或循环产生过多重复的对象实体; 4.使用的第三方软件中的BUG; 5.启动参数内存值设定的过小
内存溢出的原因
static这个关键字使一个变量变为只和这个类相关的类变量,和实例无关。他的生命周期是很长的,贯穿于app的启动到关闭。因此只要用一个static引用一个大对象,就可以泄漏了!举个例子:static Activity activity;这是最简单粗暴的持有一个activity的引用,这样这个activity退出之后对象并没有被销毁。
static
1.检查对数据库查询中,是否有一次获得全部数据的查询。一般来说,如果一次取十万条记录到内存,就可能引起内 存溢出。这个问题比较隐蔽,在上线前,数据库中数据较少,不容易出问题,上线后,数据库中数据多了,一次查 询就有可能引起内存溢出。因此对于数据库查询尽量采用分页的方式查询。 2.检查代码中是否有死循环或递归调用。 3.检查是否有大循环重复产生新对象实体。4.检查List、MAP等集合对象是否有使用完后,未清除的问题。List、MAP等集合对象会始终存有对对象的引用,使得这些对象不能被GC回收
排查对象
思路:泄漏是因为持有了activity引用导致无法被销毁,那么只有两个选择:及时取消引用,或者让这个引用多待一会,但是该gc的时候就销毁。
- 我们在代码中能不用static变量持有contxt就不用,非要用就用weak引用。- 对于内部类,尽量用静态内部类,这样就不会持有外部类引用。如果需要外部类引用做一些事,就手动赋给一个weak引用。- 对于匿名内部类,不要图简单方便,实在不行就乖乖的写成外部类。- 异步操作,尽量用可以方便管理的,比如rxJava,而不是用老古董AsyncTask了。非要用也最好加一个终止条件,在退出Activity时就该结束了。- 在用rx时,可以在subscribe()的时候获取到Subscripeion,在不用的时候手动unSubscribe(),或者直接bind()到Activity的生命周期上,比如使用RxActivity管理。- 在使用handler时,记得在activity的onDestroy()中加上remove()- 在获取到某些资源时,使用完记得释放- 在用到一些大对象比如Bitmap啊什么的,要记得回收- 最后,在使用各种第三方库或者系统服务的时候还要记得有注册或绑定就要有解除注册、解绑定。
解决方法(以activity引用为例子)
Java中,造成内存泄露的原因有很多种。典型的例子是一个没有实现hasCode和equals方法的Key类在HashMap中保存的情况。最后会生成很多重复的对象。所有的内存泄露最后都会抛出OutOfMemoryError异常
没有实现hasCode和equals方法的Key类在HashMap中保存的情况
ThreadLocal放入123,线程结束以后时会有内存泄漏问题,他是服务器本地线程,线程用完以后会把他放回线程池,线程再打过来以后是可以拿到的123的,而且本身是强引用。
ThreadLocal正确的使用方法:- 每次使用完ThreadLocal都调用它的remove()方法清除数据- 将ThreadLocal变量定义成private static,这样就一直存在ThreadLocal的强引用,也就能保证任何时候都能通过ThreadLocal的弱引用访问到Entry的value值,进而清除掉 。
ThreadLocal内存泄漏问题
经典案例
Java OOM 分析
jvm中垃圾回收器介绍下:cms和g1
先找到哪些允许被回收的-->开始回收第一步:哪些对象是垃圾: 1,引用计数法:通过对引用的遍历,找到对应的实例,让对应的实例计数加1,如果引用取消,或者指向null,实例的引用减1 。把找到的引用都遍历一遍之后,如果发现有对象实例的计数是0。那么这个对象 就是垃圾对象了。在通过垃圾回收算法对其进行 回收即可。 缺点:想想一下,有两个类,互相引用,也就是A对象的实例(也就是对象的全局变量)是一个指向B对象的引用,B对象实例是一个指向A对象的引用。那么这两个对象的引用计数,永远不可能是0 。也就不可能对其进行回收了。 2,可达性分析法:这个算法类似于树的遍历,学过数据结构的小伙伴应该会好理解。简单来说,按照一定的规则说明那些可以作为一个根节点(GC root),然后以这些根节点去访问其引用的对象,被访问的对象又会有其他对象的引用。想象一下,是不是像极了树的遍历。这个路径称作引用链,但凡是在引用链上的对象,都是可用的。注意,引用连的起始点都是GC root 哦。虽然有其他对象存在类似于引用链的结构,但是,起始点不是GC root的那一些,都是垃圾,可以被回收的。GC root哪些对象会被认为是root; GC root的查找规则:java栈中的引用,方法区中的静态属性(静态变量 + 静态常量),方法区中常量引用的对象(方法区中有个结构 叫做 常量池 ,存储的一部分是常量),本地方法(线程独占区中有个结构叫做 本地方法栈)。大对象直接分配到老年区:大对象的就是,对象里面有很大数组或者很大的字符串;所以在代码里尽量避免大数组和大字符串的存在,容易长期占用老年区空间,容易触发老年区Full GC,频繁的fullGC会出现cpu内存飙升的问题,毕竟它是异步清理占用CPU线程。一般情况下,都是使用的 可达性分析法去查找垃圾类实例。总结(面试回答这些基本足够):我们常说的JVM垃圾回收主要指堆的垃圾回收问题,堆又分为新生代和老年代,新生代分为伊甸园区和幸存区,幸存区分为from和to区。放入对象时,对象先会来到伊甸区,如果伊甸区的剩余内存大小可以放下,就直接放到伊甸区,如果伊甸内存区不够,就会进行minor GC 将伊甸园区的对象进行回收,并且将存活的对象放置from区;如果对象来到伊甸园区,进行minor GC之后对象仍然放不下,又到幸存区也放不下,就会进入老年代,直到老年代放不下的时候,先会进行一次major gc ,如果进行完之后,还是内存不够,就会进行full GC ,full GC 会将新生代和老年代都会进行回收,并且会使其他的进程全部停止,如果进行full GC 之后,内存还是不够的时候,就会抛出异常OOM;新生代用的是复制算法;老年代用的算法是标记整理算法,具体要看我们选用哪种收集器(如果面试官问那就把上面的CMS和Gone说下)。
jvm如何进行垃圾回收
面试高频问题
IO
分支主题
Java
收藏
收藏
0 条评论
回复 删除
下一页