JAVA 学习笔记 一
文章目录
数据结构
树
树:只有一个根节点;子树1个前驱,0个多个后驱。子树之间不能有交集。
概念
- 节点的度:一个节点含有子树的个数称为该节点的度
- 叶节点:度为为0
- 树的度:最大节点的度
- 节点的层次:根为1层,往下2层,以此类推
- 树的高度或者深度:最大层数
- 兄弟节点:同根
- 堂兄节点:同层不同跟
二叉树:不存在节点大于2的度;二叉树有左右之分,次序不能颠倒,因此是有序树。
对于任意二叉树都是由以下几种情况复合而成的。
满二叉树:子节点都在一层
完全二叉树:前n-1层是满二叉树,最后一层子节点连续
平衡树:左根右递增,子树高度差小于等于1,追求绝对平衡,插入删除旋转次数未知 增删平均时间复杂度为logn
红黑树:
左根右递增
根节点和叶子结点(null)是黑色;红黑树中的叶节点通常指的是空节点
不允许存在两个相邻的红色节点
任意节点到每个叶子结点路径上的黑节点数量相同
最长路径不大于最短路径的两倍
增删查复杂度最坏为logn,追求大致平衡,插入删除最多三次旋转
树的表示

二叉树的性质
①若规定根结点的层数为1,则一棵非空二叉树的第i层上最多有2i-1个结点。
②若规定根结点的层数为1,则深度为h的二叉树的最大结点数为2h-1个。
③对任何一棵二叉树,如果度为0的叶结点个数为n0,度为2的分支结点个数为n2,则有n0 = n2+1。(常用这个性质解选择题)
④若规定根结点的层数为1,则具有N个结点的满二叉树的深度h = log2(N+1)。
⑤对于具有N个结点的完全二叉树,如果按照从上至下、从左至右的数组顺序对所有结点从0开始编号,则对于序号为i的结点:\n1、若 i > 0,则该结点的父结点序号为:( i - 1) / 2;若 i = 0,则无父结点。\n2、若2i + 1 \u003C N,则该结点的左孩子序号为:2i + 1;若2i + 1 >= N,则无左孩子。\n3、若2i + 2 \u003C N,则该结点的右孩子序号为:2i + 2;若2i + 2 >= N,则无右孩子。
二叉树的存储结构
顺序存储一般用来存储完全二叉树,否则会造成空间浪费。现实只有堆会用数组存储。
链式存储,用链表表示二叉树。每个节点为左右指针域和数据域。

二叉树的遍历
深度遍历
- 前序 根左右
- 中序 左根右
- 后序 左右跟
广度遍历:层次遍历
排序算法,查找算法
常见面试的查找和排序算法
面试高频考点 – 常见的排序算法(7种)
-
插入排序:从第二个元素开始,每选择一个元素,这个元素前面就是有序区间,后面就是无序区间。在有序区间选择合适位置插入。
O(n^2) 稳定 -
希尔排序:将数据分成n组,进行排序,逐渐缩小n值
O(n^1.3~1.5) 不稳定 -
选择排序:每次将最大或者最小放到最后,知道所有都排完
O(n^2)不稳定 -
冒泡排序:在无序区间,通过相邻数的比较,将最大的数据放入一侧,持续整个过程,直到数组整体有序。
时间复杂度:
最坏情况 O(N^2)
最好情况 O(N) (一趟就有序了)
空间复杂度: O(1)
稳定性: 稳定 -
快速排序(重要)
1.找一个基准值,存储
2.比较左边和右边,小于的放到右边,大于的放到左边
3.然后对左右区间按同样的方式处理
nlogn 最坏n^2
选择排序和冒泡排序的区别
- 冒泡排序是左右两个数相比较,而选择排序是用后面的数和每一轮的第一个数相比较;
- 冒泡排序每轮交换的次数比较多,而选择排序每轮只交换一次;
- 冒泡排序是通过数去找位置,选择排序是给定位置去找数;
- 当一个数组遇到相同的数时,冒泡排序相对而言是稳定的,而选择排序便不稳定;
- 在时间效率上,选择排序优于冒泡排序。
- 两种算法的最坏情况复杂度相同,即O(n^2),但最佳复杂度不同。冒泡排序使用n个时间顺序,而选择排序使用 n ^ 2个时间顺序
堆排序
将数组放入堆,调整每一颗子树,使之变成大根堆。
O(n * logN) O(1) 不稳定
package com.ln.mybatis.sort;
import java.util.Comparator;
import java.util.PriorityQueue;
//求前k个最小的元素
public class sortTest {
public static void main(String[] args) {
int test[]={10,11,12,32,434,1,2,3,4,5,6,7,8,9,10,11,12,32,434,54645,6565,757,323,32};
int[] topk=topK(test,3);
for (int i = 0; i < topk.length; i++) {
System.out.println(topk[i]);
}
// System.out.println(topK(test,3));
}
// 找出数组中最小的元素,建立大根堆,然后对根节点进行比较,大则不放,小则替换
public static int[] topK(int[] arr,int k){
// 创建一个大小为k的大根堆
PriorityQueue<Integer> maxHeap =new PriorityQueue<>(k, new Comparator<Integer>() {
@Override
public int compare(Integer o1, Integer o2) {
return 02-01;
}
});
for (int i = 0; i < arr.length; i++) {
if(i<k){
maxHeap.offer(arr[i]);
}else {
if(maxHeap.peek()>arr[i]){
maxHeap.poll();
maxHeap.offer(arr[i]);
}
}
}
int[] ret=new int[k];
for (int i = 0; i <k ; i++) {
ret[i]=maxHeap.poll();
}
return ret;
}
}
雪花算法
组成: 1.第一位为符号位默认0不使用 占用1bit,2.时间戳:41bit 3.机器id 10bit 4.序列号 12bit
同一毫秒同一个id可以生成4096个序列号
时间回拨生成重复id的问题
1.回拨时间短不生成id
2.回拨时间长使用扩展位
布隆过滤器
基于多个哈希函数的概率型数据结构,高效判断元素是否存在于集合中,空间小,实现O(1)时间复杂度的查询
代价是可能存在少量误判。
布隆过滤器是一个位数组+多个哈希函数组成的数据结构。当添加元素时,通过多个哈希函数计算出一组位置,将这些位置标记为1。查询时,同样计算这些位置:如果有任何一位为0,元素肯定不存在;如果所有位都为1,元素可能存在(可能有误判)。
适用于缓存穿透(快速过滤不存在请求),爬虫url去重,垃圾邮件,推荐系统内容排重

java基础
==和equals的区别
== 用于比较两个引用是否指向同一个对象
而 equals() 方法用于比较两个对象的内容是否相等。
默认情况下,equals() 方法也比较引用,但许多类(如 String)重写了该方法以比较对象的实际内容。因此,使用 equals() 来检查对象的逻辑相等性通常更合适。
java1.8的特性
- lambda表达式(在stream和optional用方法引用)
- 函数式接口,如predicate,function和consumer(consumer统一处理外部调用的异常)
- streamAPI(集合处理)
- 接口可以包含默认方法
- Optional类,避免显示的null检查,处理可能为null的值
- 新日期和时间 API:java.time 包,替代了老旧的 java.util.Date 和 java.util.Calendar 类(线程安全,时区处理)
String、StringBuffer、StringBuilder的区别

JAVA的集合类型以及线程安全的集合
LIST SET HASH
vector,hashtable
CopyOnWriteArrayList,CopyOnWriteArraySet,ConcurrentHashMap
底层大都采用Lock锁 ConcurrentHashMap不用
Java是引用传递还是值传递
是值传递
值传递:传递的是参数的拷贝,操作参数不会影像实际参数
引用传递:传递的是参数的地址,操作参数会影响实际参数
lambda
lambda 可以访问外部变量
但是变量不可变,引用不可变。
c和java的区别
1.c面向过程,执行效率高 j面向对象,执行效率低
2.j跨平台,c,c++,c#都需要在特定的系统中执行
3.c有指针,没有垃圾回收机制,j没有指针,有垃圾回收机制
4.c可以调用系统指令,j不可以,因此j中只有线程没有进程的概念,c两者都有
5.文件组织方式不一样,j是类,c中把全局变量和方法的声明放在头文件中
jvm
概念
1.JVM是java虚拟机,用来执行字节码文件(二进制 class文件)的虚拟计算机。除了java,Scala,Groovy和Python等其他语言经过处理也可以转换成字节码文件。
2.JVM运行在操作系统上,和硬件没有任何关系。
跨平台原理:编译后的字节码文件和平台无关,在java虚拟机上运行。统一的class文件结构,就是jvm的基石。
JVM分类:
- 类加载器子系统
- 运行时数据区
- 执行引擎
- JIT编译器(主要影响性能):编译执行
- 解释器(负责响应时间):逐行解释字节码
程序执行方式有三种,静态编译执行,动态编译执行,动态解释执行。
在java中,程序的执行以动态解释为主,动态编译为辅。(静态编译如C,直接编译成可执行文件exe)
机器码和字节码的区别:
机器码是CPU直接读取,速度快;字节码需要直译器转译后才能变成机器码。
JDK包括了编译器等开发工具和JRE
JRE包括了运行类库和JVM
JVM有两种运行方式 client和server Client启动快,运行慢。Server启动慢,运行快。
JVM流程 .java文件编译器解释为class文件,交给jvm执行引擎执行,执行时会用空间存储数据,就是JVM内存。
JVM内存主要为:堆,栈,方法区,本地方法区,程序计数器。
- 程序计数器
当前线程执行字节码的行数指示器,用来记录虚拟机字节指令地址,线程私有。执行本地方法时为空。也称为PC寄存器。
字节码解释器在工作时,通过改变计数器的值来选取下一跳执行的代码,分支,循环,跳转,异常处理,线程恢复等功能都依赖程序计数器完成。
Java虚拟机的多线程的实现方式:通过轮流切换并分配处理器执行时间实现。 - 方法区(1.8为元数据区)
主要是存储类信息,静态变量,编译后的代码(字节码)等数据,常量池。线程共享 - 栈
栈:创建线程时创建,存储栈帧,线程私有。栈桢在执行方法时创建,包括局部变量表,操作数栈,动态链接,方法出口等信息。
局部变量表:用来存储方法参数和方法中定义的局部变量
操作数栈:用于保存计算中的临时变量和中间结果,是JVM执行引擎的一个工作区,通过入栈和出栈进行数据访问。Java虚拟机的解释引擎是基于栈的执行引擎,其中的栈指的就是操作数栈。
动态链接:指向方法区的运行时常量池
(栈帧中的动态链接
↓ (指向)
运行时常量池中的方法符号引用
↓ (解析)
1.查找对象的实际类
2.在该类的方法表中查找目标方法
3 获取方法的直接引用(入口地址)
↓
执行方法代码) - 本地方法栈
和虚拟机栈作用类似,区别是一个执行java方法,一个执行native方法。线程私有。
与虚拟机栈一样,本地方法栈区域也会抛出StackOverflowError 和OutOfMemoryError异常。
在Hotspot的演变过程中:
- Java6及之前:方法区存在永久代,保存有静态变量
- Java7:进行去永久代工作,虽然还保留着,但静态常量池,如字符串常量池,已经移动到堆中
- Java8:移除永久代,类型信息、域(Field)信息、方法(Method)信息存放在元数据区;字符串常量池、静态变量存放在堆区

堆中分为老年代和年轻代,年轻代中分为eden区和存活区,区中分为s0和s1
新生成的对象在Eden区
触发Minor GC后幸存的对象存入s0,再次触发Minor GC后,eden区和s0的对象存入s1中,s0清空。
每次移动,递增计数器,超过默认值15 (通过 -XX:+MaxTenuringThreshold 设置),移动到老年代中,eden中没有足够内存分配,也会分配到老年代。
老年代靠major GC。
新生代的回收机制采用复制算法,老生代采用的回收算法是标记整理算法。
堆和栈的区别
栈:创建线程时创建,存储栈帧,线程私有。栈桢在执行方法时创建,包括局部变量表(方法参数和局部变量),操作数栈(运算的中间结果),动态链接(将符号引用转为直接引用),方法出口(记录方法返回后执行的位置)等信息。栈中数据生命周期短,出栈即失效。栈超过虚拟机允许最大深度StackOverflow
堆:存储对象,线程共享。堆中数据生命周期长,由垃圾回收机制不定期回收。空间不够扩展申请不到足够的内存,oom
垃圾回收
参考【Java】垃圾回收
作用区域:频繁发生在年轻代,较少发生在老年代,极少发生在方法区(永久代/元空间)
引用类型才需要垃圾回收,基本数据类型不需要。
内存泄漏:这个对象不再使用,但是GC没法回收。
垃圾回收分为标记和清除阶段
标记阶段
引用计数法
引用对象+1,引用失效-1。为0则认为可以进行回收。
优点:实现简单,垃圾容易辨识;判定效率高,回收没有延迟
缺点:
1.需要单独的字段存储计时器,增加空间开销
2.每次赋值都需要进行加减法,增加时间开销
3.无法处理循环引用的情况
可达性分析算法
通过被称为引用链(GC Roots)的对象作为起点,从这些节点开始向下搜索,走过的路径被称为引用链。当一个对象到GC roots没有任何引用相连时,证明该对象不可用。
同样具备实现简单和执行高效的特点,能有效解决循环依赖的问题,防止内存泄漏的发生。
可达性分析必须在一个能保证一致性的环境下进行。这点也是导致GC必须进行"stop the world"的一个重要原因。
所谓GC roots根集合就是一组必须活跃的引用。可以是:
1.虚拟机栈中引用的对象。
2.静态变量引用的对象,除非类卸载,否则他的引用对象一直存在。
3.所有被同步锁持有的对象。(同步锁要是被销毁,同步就失效了)
清除阶段
JVM中常见的清除方法:1.标记清除法 2.标记复制法 3.标记压缩法
标记清除法:
把存活的对象进行标记,清除死亡对象。
当堆中有效空间被用完,就会stw。然后进行标记和清除。要把用户线程停止保持一致性,防止用户线程产生垃圾。
缺点:
1、效率不高,需要遍历
2、进行GC时需要停止整个应用程序,用户体验差。
3、清理出来的空闲空间不是连续的,会产生碎片。
标记复制法:
内存分为两块,每次只用其中的一块。垃圾回收时,将存活的对象复制到未使用的一块,清除不可达的对象。
年轻代S0和S1也是用的复制算法。
优点:1.没有标记和清除的过程,实现简单,运行高效;2.复制后能保证空间的连续性
缺点:损失一半空间。
适合回收对象多的场景,复制少。适用于年轻代。
标记压缩算法
1.第一阶段和标记清除算法相同,从根节点开始标记被引用对象。
2.第二阶段将所有的存活对象压缩到内存的一端,按顺序排放,之后清理边界外所有的空间。
标记压缩算法等同于标记清除算法加压缩。
优点:1.没有碎片 2.不会内存减半
缺点:
1.效率低,因为会进行整理压缩
2.移动对象时,如果对象被其他对象引用,需要调整引用的地址。
3.移动过程中会stw
分代收集算法:
不同生命周期对象采用不同的算法,提高效率。
1.年轻代:区域小,生存周期短,回收频繁。
采用复制算法,内存利用率不高,hotspot中两个survivor的设计得以缓解
2.老年代:区域大,生命周期长,回收不频繁。
采用标记清除或者标记清楚整理算法。
Old GC: 只收集 old gen 的 GC。只有垃圾收集器 CMS 的 concurrent collection 是这个模式
Mixed GC: 收集整个 young gen 以及部分 old gen 的 GC。只有垃圾收集器 G1 有这个模式
年轻代的gc称为Minor GC。新生代Eden区满的时候触发Minor GC
Full GC是回收整个堆。触发条件为:
1.System.gc
2.老年代空间不足
3.方法区空间不足
4.Minor GC后进入老年代的平均大小大于老年代的可用大小
5.由Eden区,from 区向to区复制,对象大于to的内存,也大于老年代的内存。
单例模式是静态的,生命周期长,如果中间引用了别的对象,那么这个对象一直不会被回收。
Major GC通常是跟full GC是等价的,收集整个GC堆,但也有说法是old GC。
异常类型

为了及时有效的处理异常,java引入了异常类。所有的异常都是Thorwable的子类。
Throwable下有两个分支Excepiton和Error。
异常类主要分为三种类型:
-
系统错误Error
系统错误是由虚拟机抛出的,用户无法处理,如
OutOfMemoryError :内存耗尽 ;
NoClassDefFoundError :无法加载某个Class ;
StackOverflowError :栈溢出 -
编译时异常:Exception (除了其子类RuntimeException)
在编译时期抛出的异常,在编译期间检查程序可能出现的问题,如果有提前防范,捕获处理
应用逻辑处理:
- NumberFormatException :数值类型的格式错误;
- FileNotFoundException :未找到文件;
- SocketException :读取网络失败。
编写逻辑造成:
- NullPointerException :对某个 null 的对象调用方法或字段;
- IndexOutOfBoundsException :数组索引越界
-
运行时异常 RuntimeException
-
java虚拟机正常运行期间抛出的异常。这类异常只有在运行时才能发现是否有异常。
RuntimeException,Error以及他们的子类都被称为免检类,其他异常被称为必检类(Checked Exception),编译器会强制程序员检查并try-catch处理,或者在方法头进行声明。如数组越界和空指针。
-
双亲委派机制
参考【Code皮皮虾】带你盘点双亲委派机制【原理、优缺点】,以及如何打破它?
双亲委派机制是在JDK1.2后才引入的。
加载类时不直接加载,委托给自己的父类加载器,递归直到加载成功。否则自己加载。
目的:1.防止类的重复加载。2.避免核心类遭到修改
Java提供四种类加载器:
- BootStrap 启动类加载器:加载java核心类库 ,javahome/lib下的jar包,rt.jar等
- Ext 扩展类加载器:加载java_home/ext/lib
- Application 应用程序类加载器:主要用来加载当前应用claspath下的所有类。
- User 用户自定义类加载器:用户自定义,加载指定路径下的类
什么时候破坏这个机制?
JDBC
Connection conn = DriverManager.getConnection("jdbc:mysql://localhost:3306/mysql", "root", "0000");
获取连接时的DriverManager因为处于rt下,会被启动类加载器加载。
类加载时,会执行静态方法。其中会加载所有实现了Driver接口的实现类,但是这些实现类都是第三方提供的,启动类加载器无法加载,因此引入了ThreadContextClassLoader(线程上下文类加载器,默认情况下是AppClassLoader)来使用应用程序加载器,破坏双亲委派机制。
tomcat
比如tomcat web容器里面部署很多应用程序,但是每个应用依赖的第三方类库版本不同,但是类的全路径名可能相同。
双亲委派无法加载多个相同的class文件,因此tomcat给每个web容器单独同一个webAppClassLoader加载器。实现隔离性,优先加载Web应用自己定义的类,加载不到再交给CommonClassLoader加载,这和双亲委派机制恰好相反。
如何打破双亲委派机制?
1.自定义类加载器:继承ClassLoader,不想打破,只需要重写findClass,想打破,重写整个loadClass方法,设定自己的类加载逻辑。
2.使用线程上下文类加载器
public class Main {
public static void main(String[] args) {
ClassLoader contextClassLoader = Thread.currentThread().getContextClassLoader();
}
}
hashmap
迭代方式
entryset
keyset
values
迭代器iterator
顺序
hashmap无序,基于哈希表
linkedhashmap,插入顺序,基于链表加哈希表
treepmap,自然排序或者比较器,基于红黑树
hashmap
1.7 数组+链表
1.8 数组+链表+红黑树
ConcurrentHashMap简介
hashmap线程不安全。
hashtable线程安全,方法直接加synchronize锁,性能低下。
concurrentHashMap
对数组增加了voliate关键字
1.7 segment+hashentry 分段锁
1.8 cas+synchronized
getSize
1.7获取三次,两次一致返回,三次拿不到加锁进行计算
1.8 获取basecount
put方法
计算哈希值;
当前map是否为空,为空先初始化;
判断哈希值所在位置是否有值,没值直接cas替换
有值判断key是否相等,相同覆盖value
不相等判断是红黑树还是链表,进行循环,key相同替换,未找到相同的进行增加
扩容过程
1.触发扩容条件:负载因子默认0.75,当size超过的时候触发扩容
2.扩容过程:
2.1. 创建新数组,一般是容量的2倍
2.2. 更新阈值
2.3. 迁移旧数据:
遍历旧数组中的每一个桶
对于非空桶,将元素迁移到新数组中
在java8中,如果一个桶中的元素超过8,并且数组的容量大于64,链表转红黑树,否则继续使用链表
2.4.重新哈希
重新计算每个元素的哈希位置,使用新的数组长度来确定其在新数组中的位置。新的哈希位置通常通过 hash & (newCapacity - 1) 计算得到。
hashMap 存储大量数据
参考准备用HashMap存1W条数据,构造时传10000还会触发扩容吗?存1000呢?
不指定容量,会随着数据的增加不断扩容,影响性能。
在指定调用容量的构造方法时,会重新调用另一个构造方法,传入默认的负载因子0.75
public HashMap(int initialCapacity) {
this(initialCapacity, DEFAULT_LOAD_FACTOR);
}
public HashMap(int initialCapacity, float loadFactor) {
if (initialCapacity < 0)
throw new IllegalArgumentException("Illegal initial capacity: " +
initialCapacity);
if (initialCapacity > MAXIMUM_CAPACITY)
initialCapacity = MAXIMUM_CAPACITY;
if (loadFactor <= 0 || Float.isNaN(loadFactor))
throw new IllegalArgumentException("Illegal load factor: " +
loadFactor);
this.loadFactor = loadFactor;
this.threshold = tableSizeFor(initialCapacity);
}
构造方法中初始化了两个成员变量。threadHold 扩容阈值和 loadFactor 负载因子。
tableSizeFor就是找到大于入参的2的整数次方,如传入10,会得到16.
设置容量为2的整数次方是为了减少哈希冲突。
推荐在集合初始化的过程中指定集合初始化大小为 ((需要存储的元素个数)/0.75)+1
为什么HashMap中的键往往都使用String?
1.String重写了hashCode,两个不同引用的String类型,只要值相等,hashcode就相等,而非地址相等。(设计 hashCode() 时最重要的因素就是对同一个对象调用 hashCode() 都应该产生相同的值。)
2.String不可变,每当创建一个字符串对象,他的hashcode就被缓存下来,所以存储hashMap不用重新计算,相比于其他对象更快。
hashmap红黑树转链表
resize方法:红黑树节点元素小于等于6,untreeify转化为链表
removenode方法:判断根节点和子节点是否为空来判断是否解除红黑树
为什么使用红黑树?
链表插入查询时间复杂度为O n,红黑树为O log n
hashMap和hashtable的区别
- hashMap线程不安全
hashtable线程安全,用synchronized关键字实现 - hashMap允许null作为键或者Value,HashTable会抛出异常
- hashtbale使用的是key的hashcode,hashMap计算hash对key的hashcode进行了二次hash,以获得更好的散列值,然后对table数组长度取摸。
- HashMap在数组+链表的结构中引入了红黑树,Hashtable没有
5.HashMap初始容量为16,Hashtable初始容量为11 - HashMap扩容是当前容量翻倍,Hashtable是当前容量翻倍+1
- HashMap只支持Iterator遍历,Hashtable支持Iterator和Enumeration
- linkedhashmap 保留插入顺序,允许键和值为null,treemap 基于键的compareTo方法排序,不允许null键,允许null值
使用场景:
- 非并发场景(单线程)使用HashMap,并发场景(多线程)可以使用Hashtable,但是推荐使用ConcurrentHashMap(锁粒度更低、效率更高)。
- 另外使用在使用HashMap时要注意null值的判断,
Hashtable也要注意防止put null key和 null value。
ThreadLocal
可以理解为线程本地变量,每个线程中都创建一个副本,在线程之间访问内部副本变量即可,做到了线程隔离,相比于synchronized的做法是用空间换时间。
在使用完成之后需要remove掉,避免内存泄漏。ThreadLocal变量的key为弱引用,使用完成后TheadLocal没有使用的强引用后会释放,但是value是强引用,只要线程存活,一直存在强引用,需要通过remove删除Entry.线程池尤为严重。
单例模式
public class SingleTonObj {
private static volatile SingleTonObj singleTonObj;
private SingleTonObj() {
}
public static SingleTonObj getObj() {
if (singleTonObj == null) {
synchronized (SingleTonObj.class) {
if (singleTonObj == null) {
singleTonObj = new SingleTonObj();
}
}
}
return singleTonObj;
}
}
- volatile
其中volatile关键字是为了防止指令重排
jvm创建对象分三步:1.分配空间,2.实例化对象,3.将对象指向空间
如果不使用volatile,jvm会优化,将3移动到2前面,这样a线程,走到了3,还没2。b线程第一个判null已经不成立,返回了没有实例化完成的对象 - 第二个判空的原因是 a,b都经过了第一次判空,但是a先拿到了class的锁,进行了实例化,然后进行释放锁,b拿到锁之后需要在进行判空防止重复实例化破坏单例
- Happens-Beforene内存模型和程序模型顺序
程序A在B前,线程中A就会在B前执行
Happens-Beforene不会破坏代码中的先后顺序,但是在不同代码或者不同线程中的顺序无关
常量池
通过javap命令生成更可读的JVM字节码指令文件:javap -v Math.class
Class常量池可以理解为Class文件中的资源仓库。Class文件中除了包含版本,字段,方法,接口,还有一项信息就是常量池,用于存放编译器生成的各种字面量和符号引用。
int a=1 1为字面量,a为符号引用
字面量是指由字母,数字构成的字符串和数值常量,字面量只可以右值出现。
符号引用是编译原理中的概念,是相对于直接引用来说的,主要包括了以下三大类:
- 类和接口的全限定类名
- 字段的名称和描述符
- 方法的名称和描述符
运行时常量池:只有运行时被加载到内存中,这些符号才有对应的内存地址,那么这些常量池一旦被装入内存就变成运行时常量池,对应的符号引用会转变为加载到内存区域的代码的直接引用,也就是动态链接。
运行时常量池,1,7在方法区,永久代,1.8放在元空间。
字符串常量池:
jdk1.6以及之前,运行时常量池在永久代,运行时常量池包含字符串常量池。
jdk1.7: 有永久代,但是逐渐去永久代,字符串常量池从永久代的运行时常量池分配到堆中。
jdk1.8:无永久代,运行时常量池在元空间,字符串常量池依然在堆中。
1.6 在常量池中寻找equal()相等的字符串,存在则返回常量池中的引用。不存在就在永久代的常量池中新建一个实例,放入常量池中并返回。
1.7:不存在永久代,常量池存在返回,不存在可以直接指向堆上的实例。
> String s0="zhigan";
String s1="zhigan";
String s2="zhi" + "gan";
System.out.println( s0==s1 ); //true
System.out.println( s0==s2 );//true
字面量声明的字符串常量在编译期就能确定,会存储在常量池中,地址相同。
String s0="zhigan";
String s1=new String("zhigan");
String s2=“zhi”+new String("gan");
System . out . println ( s0 == s1 ); // false
System . out . println ( s0 == s2 ); // false
System . out . println ( s1 == s2 ); // false
new String() 的字符串不是常量,不能在编译期确定,不放入常量池,他们有自己的地址空间。
String a="a3.4";
String b="a"+3.4;
System . out . println ( a == b ); // true
jvm对于加号连接,在编译器就会进行优化,将常量字符串连接。
String a="ab";
String bb="b";
String b="a"+bb;
System . out . println ( a == b ); // false
在+中带有引用,JVM无法优化,因为引用无法在编译期确认,只能在程序运行是动态分配,并将连接后的新地址赋值给B,因此为false(如果bb是一个方法的返回结果,同样的原因)如果bb用final修饰,那么他在编译期会被解析为常量,比较的结果为true。
String s = "a" + "b" + "c" ; // 就等价于 String s = "abc";
String a = "a" ;
String b = "b" ;
String c = "c" ;
String s1 = a + b + c ;
s1这个就不一样,可以通过观察器JVM指令码发现s1的"+"操作会变成如下:
StringBuilder temp=new StringBuilder();
temp.append(a).append(b).append( c );
String s=temp.toString();
Java八大基本对象的包装类型除了两个浮点型,其他都有常量池,在堆上。另外Byte,Short,Int,Long,Character这五种整形的包装类也只是对应值-128到127才可以使用对象池。
if,else嵌套优化
参考Java—优化 if-else 代码的 8 种方案
1.提前return,去除不必要的else:快速失败
if(xx){
}
else{
return
}
--------
if(!xx){
return
}
2.使用三元表达式
3.使用枚举类
String OrderStatusDes;
if(orderStatus==0){
OrderStatusDes="订单未支付";
}else if(OrderStatus==1){
OrderStatusDes="订单已支付";
}else if(OrderStatus==2){
OrderStatusDes="已发货";
}
---------------------
String OrderStatusDes = OrderStatusEnum.0f(orderStatus).getDesc();
4.合并条件表达式
结果相同,合并表达式
5.使用Optional优化if,else
String str = "jay@huaxiao";
if(str != null) {
System.out.println(str);
} else{
System.out.println("Null");
}
-----------------------------
Optional<String> strOptional = Optional.of("jay@huaxiao");
strOptional.ifPresentOrElse(System.out::println, () -> System.out.println("Null"));
6.表驱动法
又称之为表驱动、表驱动方法。表驱动方法是一种使你可以在表中查找信息,而不必用很多的逻辑语句(if或case)来把它们找出来的方法。以下的demo,把map抽象成表,在map中查找信息,而省去不必要的逻辑语句。
if(param.equals(value1)) {
doAction1(someParams);
} else if(param.equals(value2)) {
doAction2(someParams);
} elseif(param.equals(value3)) {
doAction3(someParams);
}
---------------
// 这里泛型 ? 是为方便演示,实际可替换为你需要的类型
Map<?, Function<?> action> actionMappings = newHashMap<>();
// 初始化
actionMappings.put(value1, (someParams) -> { doAction1(someParams)});
actionMappings.put(value2, (someParams) -> { doAction2(someParams)});
actionMappings.put(value3, (someParams) -> { doAction3(someParams)});
// 省略多余逻辑语句
actionMappings.get(param).apply(someParams);
7.使用策略模式
例如支付场景下,支持多种支付方式
public class PaymentService {
CreditService creditService;
WeChatService weChatService;
AlipayService alipayService;
public void payment(PaymentType paymentType, BigDecimal amount) {
if (PaymentType.Credit == paymentType) {
creditService.payment();
} else if (PaymentType.WECHAT == paymentType) {
weChatService.payment();
} else if (PaymentType.ALIPAY == paymentType) {
alipayService.payment();
} else {
throw new NotSupportPaymentException("paymentType not support");
}
}
}
enum PaymentType {
Credit, WECHAT, ALIPAY;
}
作者:小黑说Java
链接:https://juejin.cn/post/7030976391596212255
来源:稀土掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
这种不满足开闭原则(对修改关闭,对扩展开放),修改后需要对其他支付方式进行测试。
策略设计模式是一种行为设计模式。当在处理一个业务时,有多种处理方式,并且需要再运行时决定使哪一种具体实现时,就会使用策略模式。
抽象支付方式为一个策略接口
public interface PaymentStrategy {
public void payment(BigDecimal amount);
}
针对具体的支付方式做实现
public class CreditPaymentStrategy implements PaymentStrategy{
@Override
public void payment(BigDecimal amount) {
System.out.println("使用银行卡支付" + amount);
// 去调用网联接口
}
}
public class WechatPaymentStrategy implements PaymentStrategy{
@Override
public void payment(BigDecimal amount) {
System.out.println("使用微信支付" + amount);
// 调用微信支付API
}
}
重新实现支付服务paymentservice
public class PaymentService {
/**
* 将strategy作为参数传递给支付服务
*/
public void payment(PaymentStrategy strategy, BigDecimal amount) {
strategy.payment(amount);
}
}
策略模式优化后
public class StrategyTest {
public static void main(String[] args) {
PaymentService paymentService = new PaymentService();
// 使用微信支付
paymentService.payment(new WechatPaymentStrategy(), new BigDecimal("100"));
//使用支付宝支付
paymentService.payment(new AlipayPaymentStrategy(), new BigDecimal("100"));
}
}
在使用了策略模式之后,在我们的支付服务PaymentService中便不需要写复杂的if…else,如果需要新增加一种支付方式,只需要新增一个新的支付策略实现,这样就满足了开闭原则,并且对其他支付方式的业务逻辑也不会造成影响,扩展性很好。
数组和链表的区别
数组:随机查询快,增删慢;内存连续;
链表:随机查询慢,增删快;空间分散,不需要连续;链表内存利用率更高;
数组固定大小,在编译期间分配内存;链表动态灵活,在执行或者运行时分配内存;
链表因为要存储上一个和下一个的引用元素,因此需要更多的内存;
对于想要快速访问数据,不经常有插入和删除元素的时候,选择数组。
对于需要经常的插入和删除元素,而对访问元素时的效率没有很高要求的话,选择链表。
ArrayList,LinkedList,Vector的区别
ArrayList动态数组,默认容量10,扩容为1.5倍,新建数组,复制数据。
LinkedList双向链表
LinkedList还实现了Deque接口,所以LinkedList还可以用作队列
Vector也是数组,线程安全,每次扩容一倍。
常用注解
@Autowired @Component @RestController @Cacheable @RequestMapping @Value @Bean @Import
@Autowired和@Resource的区别
A默认byType,可以通过@Qualify指定bean名称。是spring的注解,默认必须存在bean,可以用required=false设置
R默认ByName,有name和type两种属性,先找name,name没有匹配type。是java的注解。
set如何保证不重复
1.通过hashcode方法获取hash值
2.在hash表中查找,如果不存在则添加成功。如果hash表中含有该值,则进行equals比较,相同添加失败,不同添加到已有对象链末尾。
深拷贝和浅拷贝
浅拷贝 对象的引用变量还是指向原对象地址
深拷贝 对象的引用变量指向不同,会新建引用对象
一般都是浅拷贝,深拷贝的是实现方式:1.重写clone方法,克隆引用成员变量;2.字节流写入文件再读出来;3.构造函数传参等。
设计模式
工厂模式
参考工厂模式
工厂模式属于创建型模式。
意图:定义一个创建接口的接口,让其子类字节决定实例化哪个类,工厂模式使其创建过程延迟到子类执行
主要解决:接口选择的问题
如何解决:让子类实现工厂接口,返回的也是也是一个抽象的产品
关键代码:创建过程在其子类实现
在任何需要生成复杂对象的地方,都可以使用工厂方法模式。简单对象,只需要new就能完成创建的对象,无需工厂模式。使用工厂模式,需要引入一个工厂类,增加系统的复杂度。
举例,抽象一个形状接口,圆,方块实现这个接口,定义一个工厂类提供给获取形状的方法,根据入参判断实例化圆/方块
抽象工厂模式

public class AbstractFactoryPatternDemo {
public static void main(String[] args) {
//获取形状工厂
AbstractFactory shapeFactory = FactoryProducer.getFactory("SHAPE");
//获取形状为 Circle 的对象
Shape shape1 = shapeFactory.getShape("CIRCLE");
//调用 Circle 的 draw 方法
shape1.draw();
//获取形状为 Rectangle 的对象
Shape shape2 = shapeFactory.getShape("RECTANGLE");
//调用 Rectangle 的 draw 方法
shape2.draw();
//获取形状为 Square 的对象
Shape shape3 = shapeFactory.getShape("SQUARE");
//调用 Square 的 draw 方法
shape3.draw();
//获取颜色工厂
AbstractFactory colorFactory = FactoryProducer.getFactory("COLOR");
//获取颜色为 Red 的对象
Color color1 = colorFactory.getColor("RED");
//调用 Red 的 fill 方法
color1.fill();
//获取颜色为 Green 的对象
Color color2 = colorFactory.getColor("GREEN");
//调用 Green 的 fill 方法
color2.fill();
//获取颜色为 Blue 的对象
Color color3 = colorFactory.getColor("BLUE");
//调用 Blue 的 fill 方法
color3.fill();
}
}
在公共微服务调用模块使用,根据接口标识获取类名,从spring工厂中获取spring对象进行处理。
- 单例模式:spring bean 默认单例
- 代理模式:AOP的实现方式是通过代理实现,Spring主要使用JDK动态代理和CGLIB代理
- 模板模式方法:对数据库类的操作,JDBCtemplate,数据库建立连接,执行查询,关闭连接几个过程非常适合模板方法
redisTemplate,restFulTemplate - 观察者模式:spring的事件驱动模型使用的是观察者模式,常用的是listener的实现
事件机制的实现有三个部分,事件源,事件,事件监听器。
srping容器初始化时,会注册事件监听器。
事件源:继承ApplicationEvent
监听器: 实现ApplicationListener<TestEvent>
事件发布器:ApplicationEventPublisher 发布时获取事件广播器ApplicationEventMulticaster,讲自定义事件告诉广播器
getApplicationEventMulticaster().multicastEvent(applicationEvent, eventType)
multicastEvent的方法功能是遍历事件监听器列表,逐个发布事件到监听器中。
SimpleApplicationEventMulticaster 内部维护了监听器列表,用ConcurrentHashMap管理
最终调用 SimpleApplicationEventMulticaster 的 invokeListener() 方法进行实质事件处理。
doInvokeListener() 最终会调用监听器的 onApplicationEvent 方法,实现监听效果。
TCP如何保证可靠性?
-
序列号和确认号机制:TCP发送数据,会带一个序列号,服务端在检测数据完整后会发送一个确认号表示确认收到了数据包
-
超时重发机制:tcp发送数据包后会启动一个定时器,如果一定时间没有接收到接收端的确认,将会重新发送
-
去重:从IP网络传输层到TCP层数据可能重复,TCP会对数据去重
-
顺序:从IP网络传输层到TCP层数据可能乱序,TCP会对数据重新排序
-
流量控制:客户端和服务端的缓存大小一定,为了防止数据溢出,通过滑动窗口协议保证数据大小
三次握手和四次挥手
握手:c发送序列号x;
s响应 确认号x+1,序列号y;
c发送确认号y+1,x+1
挥手:c发送fin,s响应ack,s发送fin,c响应ack
第三次握手是可以发送的数据的
如果接收方没收到数据,确认号+1,否则就加上收到的数据量
HTTP
HTTP是基于TCP的应用层的超文本传输协议
优点
- 简单:报文格式为header+body,头部信息也是kv格式
- 灵活易扩展:http协议中的请求方法,url,状态码,header都没固定死,允许开发人员自定义
- 应用广泛跨平台
缺点:
- 无状态:没有记忆能力
- 不安全:明文传输
反射的优化
1.缓存Consturctor,Method等对象
2.setAccessible(true) 关闭安全检查
3.利用反射工具包ReflectASM,通过字节码生成的方式来实现反射机制
并发编程
synchronized
参考 面试官:请详细说下synchronized的实现原理 - 知乎
概念:在多线程情况下,多个线程访问共享资源会出现问题,而synchronized关键字则是用来保证线程同步的
synchronized解决可见性的方式是,每次都清除工作内存,从主内存中重新获取。
synchronized可以保证并发编程的三大特性:原子性,可见性,有序性。
synchronized可以实现悲观锁,非公平锁,可重入锁,独占锁或者排它锁。
实现原理:
Java虚拟机是通过进入和退出Monitor对象来实现代码块同步和方法同步的,代码块同步使用的是monitorenter和 monitorexit 指令实现的,而方法同步是通过Access flags后面的标识来确定该方法是否为同步方法。
jDK1.6对synchronized做了哪些优化?
引入偏向锁和轻量级锁,随着竞争的激烈而升级。
引入偏向锁的目的:减少只有一个线程执行同步代码块时的性能消耗,即在没有其他线程竞争的情况下,一个线程获得了锁。
使用CAS操作将当前线程的ID记录到对象的Mark Word中。
引入轻量级锁的目的:在多线程交替执行同步代码块时(未发生竞争),避免使用互斥量(重量锁)带来的性能消耗。但多个线程同时进入临界区(发生竞争)则会使得轻量级锁膨胀为重量级锁。
将对象的Mark Word复制到当前线程的Lock Record中,并将对象的Mark Word更新为指向Lock Record的指针。
锁消除是指java编译时,消除不可能发生共享资源竞争的锁。
锁粗化是指java编译时,将不必要的重复加锁粗化到整个操作的外部。
for(int i=0;i<n;i++){
synchronized(lock){
}
}
//粗化后
synchronized(lock){
for(int i=0;i<n;i++){
}
}
synchronize和lock的区别和使用场景
参考粗谈synchronize和Lock锁的区别及使用场景
区别
- synchronize是java关键字,内置特性。Lock是一个接口,通过这个接口的实现类可以实现同步访问。
- synchronize 在代码执行结束后或者代码执行异常后会自动释放;而lock必须要用户手动去释放锁,否则会造成死锁。
- synchronize可以锁住代码块,类,对象,lock只能锁代码块
- synchronize只能是非公平锁,而lock可以是公平锁,也能是非公平锁。
- synchronize等待不中断,而lock可中断。
- synchronize不知道线程有没有获得锁,但是lock可以知道。
- synchronize是隐式锁。Lock是显示锁。显示和隐式就是在使用的时候,使用者要不要手动写代码去获取锁和释放锁的操作。
- synchronize是悲观锁的一种实现.lock的实现类ReentrantLock主要用到unsafe的CAS和park两个功能实现锁,乐观锁的一种实现。
- 性能比较:竞争不激烈时,synchronize的性能优于ReentrantLock,但是在竞争激烈的情况下,synchronize性能下降几十倍,ReentrantLock的性能可以维持常态。
重入锁提供多样化的同步,如时间限制的同步,被打断的同步。
synchronize的释放: - 占有锁线程代码执行完成
- 占有锁线程出现了异常
- 占有锁线程调用wait方法,进入waiting状态需要释放锁
- 执行完成后可以通过notifyAll或notify等object对象的api来唤醒其他等待线程立马执行。
public interface Lock {
void lock();
void lockInterruptibly() throws InterruptedException;
boolean tryLock();
boolean tryLock(long time, TimeUnit unit) throws InterruptedException;
void unlock();
Condition newCondition();
}
接口方法
- lock();用来获取锁,如果锁被其他线程获取,则进行等待
Lock lock = ...; //声明锁
lock.lock(); //获得锁
try{
//处理任务
}catch(Exception ex){
}finally{
lock.unlock(); //释放锁
}
- tryLock();尝试获取锁,立即返回
- tryLock(long time, TimeUnit unit); 尝试在一定时间内获取锁
Lock lock = ...;
if(lock.tryLock()) {
try{
//处理任务
}catch(Exception ex){
}finally{
lock.unlock(); //释放锁
}
}else {
//如果不能获取锁,则直接做其他事情
}
- lockInterruptibly()
它是对于那些未竞争的到锁,而 可以被外部调用interrupt()来中断,从而达到不在等候锁资源,不再去竞争锁
synchronize和reentrantlock都是可重入锁。
就是一个线程不用释放,可以重复的获取一个锁n次,只是在释放的时候,也需要相应的释放n次。synchronize不用手动释放。
实现
- ReentrantLock的基本实现可以概括为:先通过CAS尝试获取锁。如果此时已经有线程占据了锁,那就加入AQS队列并且被挂起。当锁被释放之后,排在CLH队列队首的线程会被唤醒,然后CAS再次尝试获取锁。在这个时候,如果:
非公平锁:如果同时还有另一个线程进来尝试获取,那么有可能会让这个线程抢先获取;
公平锁:如果同时还有另一个线程进来尝试获取,当它发现自己不是在队首的话,就会排到队尾,由队首的线程获取到锁。
- Synchronized进过编译,会在同步块的前后分别形成monitorenter和monitorexit这个两个字节码指令。在执行monitorenter指令时,首先要尝试获取对象锁。如果这个对象没被锁定,或者当前线程已经拥有了那个对象锁,把锁的计算器加1,相应的,在执行monitorexit指令时会将锁计算器就减1,当计算器为0时,锁就被释放了。如果获取对象锁失败,那当前线程就要阻塞,直到对象锁被另一个线程释放为止
LOCK的实现类
Lock定义了标准Lock的API
AQS(AbstractQueuedSynchronizer)
ReentrantLock:重入锁,支持公平和非公平
ReentrantReadWriteLock:在ReentrantLock上基础上支持读写分离是和多读少写的场景CountDownLatch:
Semphore:
Java的Lock实现类介绍
AQS
AbstractQuenedSynchronizer 抽象的队列同步器。是除了java自带的synchronized关键字之外的锁机制
核心思想是,如果共享资源空闲,请求资源的线程设置为有效线程,将共享资源设置为锁定状态,如果请求的共享资源被占用,则将线程加入到CLH队列(Craig-Landin-Hagersten)中。
CLH是一个虚拟的双向队列,不存储队列实例,仅存储节点之间的关联关系。
AQS是将每一条请求共享资源的线程封装成一个CLH锁队列的一个节点Node,来实现锁的分配。
AQS是基于CLH队列,用volatile修饰共享变量state,线程通过CAS去改变状态符,成功获得锁,否则加入队列中,等待被唤醒。
CLH锁是一种自旋公平锁,
AQS定义了两种资源共享方式
1.Exclusize:独占,只有一个线程能执行,如ReentranLock
2.Share 共享,多个线程可以同时执行,如Semphore,CountDownLatch,ReadWriteLock,CyclicBarrier
死锁
在 Java 开发中,死锁发生的条件通常有四个:
互斥条件:存在独占资源
持有并等待:至少有一个线程持有一个资源,并等待获取被其他线程持有的资源。
不剥夺条件:已经分配给线程的资源在其未使用完之前,不能被其他线程强制剥夺。
循环等待:存在一种线程资源的循环链,每个线程持有一个资源并等待下一个线程持有的资源。
如何避免死锁
1.死锁预防
1.1.破坏占有并且等待
1.一次性申请运行过程中需要的所有资源
2.允许只获得初期资源就开始运行,运行后逐步释放使用完毕的资源,然后再去请求新的资源。
1.2.破坏不可抢占条件
当获取锁失败,释放之前获取的资源
1.3.破坏循环等待的条件
定义资源的线性顺序来预防
2.避免死锁,在使用前进行判断,只允许不会产生死锁的进程申请资源.
一般采用银行家算法来避免。需要知道进程请求资源的最大数目。
线程池
作用
限制系统中执行线程的数量
1.降低资源消耗:复用线程。
2.提高效率,提前创建,使用从中获取节省创建时间。
3.增加线程的可管理型。线程是稀缺资源,使用线程池可以进行统一分配,调优和监控。
常见线程池
1.SingleThreadExecutor
只有一个线程,保证任务按顺序执行(FIFO,LIFO,优先级)
任务队列为链表结构的有界队列
2.FixedThreadPool
定长线程池,超出等待。只有核心线程,执行完成后回收。
任务队列为链表结构的有界队列。
3.CachedThreadPool
超出回收空闲线程,没有则创建线程。核心线程固定,非核心线程无限。使用完成后闲置10分钟回收。任务队列为延时阻塞队列。
4.ScheduledThreadPool
定时执行任务线程池
无核心线程,非核心线程数量无限,执行完闲置 60s 后回收,任务队列为不存储元素的阻塞队列。
5.WorkStealingPool:一个拥有多个任务队列的线程池,可以减少连接数,创建当前可用cpu数量的线程来并行执行。
线程池中的几个重要参数
- corePoolSize 核心线程数量,用不到也不会回收。allowCoreThreadTimeout 设置为 true 时,核心线程也会超时回收。
- maximumPoolSize 最大线程数量,活跃线程数量达到该值,阻塞新任务。
- keepAliveTime 非核心线程最长存活时间
如果将 allowCoreThreadTimeout 设置为 true 时,核心线程也会超时回收。 - unit(必需):指定 keepAliveTime 参数的时间单位。常用的有:TimeUnit.MILLISECONDS(毫秒)、TimeUnit.SECONDS(秒)、TimeUnit.MINUTES(分)。
- workQueue(必需):任务队列。通过线程池的 execute() 方法提交的 Runnable 对象将存储在该参数中。其采用阻塞队列实现。
- threadFactory 线程工厂,指定线程池创建新线程的方式。
- handler 拒绝策略,达到最大线程要执行的饱和策略。
//TreadPoolExecutor(自定义参数线程池)(推荐使用)
public class ThreadPoolDemo {
public static void main(String[] args) {
//1. 使用ThreadPoolExecutor指定具体参数的方式创建线程池
ThreadPoolExecutor poolExecutor = new ThreadPoolExecutor(
2, //核心线程数
5, //池中允许的最大线程数
2, //空闲线程最大存活时间
TimeUnit.SECONDS, //秒
new ArrayBlockingQueue<>(10),//被添加到线程池中,但尚未被执行的任务
Executors.defaultThreadFactory(), //创建线程工厂,默认
new ThreadPoolExecutor.AbortPolicy()//,如何拒绝任务
);
//2. 执行具体任务
poolExecutor.submit(new MyRunnable());
poolExecutor.submit(new MyRunnable());
//3. 关闭线程池
poolExecutor.shutdown();
}
}
public class MyRunnable implements Runnable{
@Override
public void run() {
System.out.println(Thread.currentThread().getName()+"执行了");
}
}
拒绝策略
任务不断过来,系统无法及时处理,就要拒绝。
- AbortPolicy(默认) 抛出RejectedExecutionException
- CallerRunsPolicy :由调用线程处理该任务。
- DiscardOleddestPolicy: 该策略将丢弃最早的未处理任务,并尝试再次提交当前任务
- DiscardPolicy:该策略默默的丢弃无法处理的任务,不予任何处理。
可以通过实现RejectedExecutionHandler接口自定义接口。
execute和submit的区别
execute适用于不需要关注返回值的场景,只需要将线程丢到线程池中去执行就可以了。
submit方法适用于需要关注返回值的场景
线程池的关闭
shutdownNow:对正在执行的任务全部发出interrupt(),停止执行,对还未开始执行的任务全部取消,并且返回还没开始的任务列表。
shutdown:当我们调用shutdown后,线程池将不再接受新的任务,但也不会去强制终止已经提交或者正在执行中的任务。
线程数的选择
计算密集型:应为 cpu核数+1 减少上下文切换
即使当密集型的线程由于偶尔的内存页失效或其他原因导致阻塞时,这个额外的线程也能确保 CPU 的时钟周期不会被浪费,从而保证 CPU 的利用率。
IO密集型:
线程数 = CPU 核心数 * (1 + IO 耗时/ CPU 耗时)
IO比cpu慢,设置过少线程数会造成cpu资源的浪费。
等待时间越长,线程越多。
什么时候使用线程池
1.任务数量大,单个任务处理时间短,频繁创建销毁线程的场景
2.线程只涉及创建没有销毁,如保持长连接,心跳,消费者线程。
线程池都有哪几种工作队列
1、ArrayBlockingQueue
是一个基于数组结构的有界阻塞队列,此队列按 FIFO(先进先出)原则对元素进行排序。
2、LinkedBlockingQueue
一个基于链表结构的阻塞队列,在未指定容量时,容量默认为 Integer.MAX_VALUE.此队列按FIFO (先进先出) 排序元素,吞吐量通常要高于ArrayBlockingQueue。静态工厂方法Executors.newFixedThreadPool()使用了这个队列
3、SynchronousQueue
一个不存储元素的阻塞队列。每个插入操作必须等到另一个线程调用移除操作,否则插入操作一直处于阻塞状态,吞吐量通常要高于LinkedBlockingQueue,静态工厂方法Executors.newCachedThreadPool使用了这个队列。
4、PriorityBlockingQueue
一个具有优先级的无限阻塞队列。
5、DelayQueue:类似于PriorityBlockingQueue,是二叉堆实现的无界优先级阻塞队列。要求元素都实现 Delayed 接口,通过执行时延从队列中提取任务,时间没到任务取不出来。
线程工作流程
提交任务,线程是否达到核心线程数,未达到,创建核心线程,否则下一步
查看任务队列是否已满,未满,放入任务队列,否则,进入下一步
线程是否到达最大线程数,未到,创建非核心线程执行任务,否则执行饱和策略,默认抛出异常。
线程池优化
- ThreadPoolExecutor自定义线程池,任务量不大,使用无界队列。任务量大,使用有界队列,防止OOM。
- 如果任务量很大,还要求每个任务都处理成功,要对提交的任务进行阻塞提交,重写拒绝机制,改为阻塞提交。保证不抛弃一个任务
- 最大线程数一般设为2N+1最好,N是CPU核数
- 核心线程数,看应用,如果是任务,一天跑一次,设置为0,合适,因为跑完就停掉了,如果是常用线程池,看任务量,是保留一个核心还是几个核心线程数
- 如果要获取任务执行结果,用CompletionService,但是注意,获取任务的结果的要重新开一个线程获取,如果在主线程获取,就要等任务都提交后才获取,就会阻塞大量任务结果,队列过大OOM,所以最好异步开个线程获取结果。
线程的状态
新建 就绪 运行 阻塞 死亡
wait和sleep的区别
wait释放锁,需要notify唤醒,sleep不释放锁,自动唤醒。
run和start的区别
run直接运行,start是进入就绪状态。
- run() 方法
直接调用:如果直接调用 run() 方法,它将在当前线程中执行,像普通的方法调用一样。
无新线程:不会创建新的线程,所有代码在调用 run() 的线程中运行。
适用场景:通常用于实现 Runnable 接口中的逻辑,或者在测试时直接调用。 - start() 方法
启动新线程:调用 start() 方法会创建一个新的线程,并在新的线程中执行 run() 方法。
异步执行:新线程会并行运行,主线程和新线程可以同时执行。
适用场景:用于需要并发执行的场景。
线程间通信的方式
- 通过 volatile 关键字
- 通过 Object类的 wait/notify 方法
wait,notify,notifyAll都必须是在同步代码块中执行- wait:需要先获得锁,执行wait后释放资源,进入阻塞状态,直到被notify方法唤醒
- notify:会唤醒一个等待锁的线程,执行同步代码块后释放锁
- notifyAll:和notify相同,但是会唤醒所有等待锁的线程,进入就绪队列
- 通过 condition 的 await/signal 方法
condiction是通过lock对象来创建,使用前也需要获得锁。lock替代了synchronize方法和语句的使用,condition替代了object监视器方法的使用- await 自主释放锁,进入沉睡状态,直到被再次唤醒
- await(long time,TimeUnit unit) 线程自主释放锁,进入沉睡,在被唤醒和等待时间内一直处在等待状态
- signal() 唤醒一个等待线程
- signalAll() 唤醒所有等待线程,能够从等待方法返回的线程必须获得condition相关的锁。
- 通过 join 的方式
A.start();
B.start();
B.join();
join方法本质上是调用wait方法,让当前线程阻塞,直到另一个线程执行完毕。(当前线程wait后,执行join方法的线程大概率抢到锁资源,而且当一个线程执行完毕后,会默认调用notifyAll方法。)
volatile 如何保证可见性和指令重排
造成可见性的原因是JAVA内存模型JMM,在java内存模型中,共享变量存放在主内存中,每个线程都有自己的工作内存,操作共享变量需要从主内存中获取,但是何时写回主内存不可预知,这就导致每个线程变量的操作是封闭的,其他线程不可见的。
CPU快内存慢,一般都用寄存器解决。
可以加synchronized关键字,进入synchronize代码块,会清除缓存,从主内存中获取共享变量,进行操作,刷新回主内存,然后释放锁。
效率低下,出现了缓存一致性协议,有MSI,MESI,MOSI等,最出名的是Intel的MESI协议,保证了每个缓存中使用的共享变量的副本是一致的。
使用volatile等于告诉CPU需要MESI协议和嗅探机制来保证可见性。
MESI机制:
1.Modify:当缓存中的数据被修改时,该缓存设置为M状态
2.Eclusive(独占):当只有一个缓存使用某行数据时,设置为E状态
3.Share(共享):当多个CPU有数据的缓存,该数据的缓存设置为S状态
4.Invalid(无效):当某个数据的缓存修改时,其他持有该数据的缓存更新为I状态
核心思想:CPU修改数据,发现该数据是共享变量,会发出通知让其他CPU将该变量的缓存置为无效状态,因此当其他CPU需要读取这个变量的时候,发现自己的缓存行是无效的,那么他就会重新读取。
监听和通知基于总线机制。总线嗅探机制就是一个监听器。
2.有volatile修饰的共享变量在写之前会多出一条lock指令
lock前缀会触发
1.将当前缓存行的数据写会主内存
2.这个写回操作会使其他CPU中缓存了该内存地址的数据无效。
JMM:
1.lock前缀会将线程工作内存中的缓存数据写回主内存
2.通过缓存一致性协议,其他线程如果工作内存中使用了该变量的值,就会失效
3.其他线程会重新从主内存获取新的值、
大量使用volatile会导致总线风暴。
volatile保证数据的可见性,但不保证数据操作的原子性。在多线程环境下,使用volatile变量是线程不安全的,可以使用锁机制或者原子类。
禁止指令重排
编译器不会对volatile读以及volatile后面的任务内存操作重排序。
通过内存屏障来实现。
异步开发
- 线程异步,可以使用线程池
- CompletableFuture异步,它是基于异步函数式编程。相对阻塞式等待返回结果,CompletableFuture 可以通过回调的方式来处理计算结果,实现了异步非阻塞,性能更优。
- SpringBoot @Async 异步
并发编程/多线程开发
继承Thread,实现runnable,实现callable,使用线程池
callable和runnable的区别
1.runnable通过创建线程执行 start
callable通过executorservice执行 submit 或者作为FeatureTask的参数
2.r没有返回值,c有返回值,可以通过泛型指定
3.r不能异常处理,c的call()方法可以抛出异常,并由其执行者Handler进行捕获并处理
4.r适用于不需要返回值,不会抛出异常的场景,c适用于需要返回值,或者需要抛出异常的场景
CAS
compare and swap
乐观锁,假设不会发生冲突去完成操作,因为冲突失败就重试,知道成功为止。
CAS中使用了3个基本操作数:
共享变量的内存地址V
工作内存中共享变量的副本值,也就是旧的预期值A
更改的值B
只有当内存地址V的值和预期值A相等的时候才进行更新,否则提交失败,重新尝试,这个过程称为自旋。
缺点:
-
ABA问题:通过增加版本号解决,如AtomicStampedReference类使用pair内部类实现,包括版本号和引用,都相等才更新
-
竞争激烈,自旋可能会消耗较高的CPU。可以使用AtomicLong的替代类:LongAdder。
-
不能保证代码的原子性:只能保证共享变量操作的原子性,而不能保证代码块的原子性
优点
- 保证变量操作的原子性
- 并发量不是很高的情况下,使用CAS机制比使用锁机制效率更高
- 在线程对共享资源占用时间较短的情况下,使用CAS效率也会较高
Java提供的CAS操作类,unsafe类,Atomic系类底层调用unsafe的API
CAS使用场景:
- 使用变量统计网站访问量
- Atomic类操作
- 数据库乐观锁更新
AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。
更多推荐



所有评论(0)