Java知识脉络
2024-10-13 02:03:52 45 举报
AI智能生成
2024年4月28日22:46:14:添加ThreadLocal原理 2024年9月3日18:14:47:调整思维导图的样式风格。 2024年10月6日21:56:30:①调整了并发部分的内容结构;②补充了并发的一些内容;③老实说个人目前对这块思维导图的内容并不是特别满意,当时创建的初衷是面向八股文面试的,但是随着内容的不断完善,我觉得它可以具备更高的价值,所以我还是打算进一步的拉高这块的高度,最近是开始啃Oracle的Java语言规范文档了,待我学成归来,我必定狠狠的上强度QAQ 2024年10月13日01:53:33:多线程知识框架重构!!!长期以来,我对多线程这块的知识一直都是模模糊糊的,即使我看过很多培训班视频、权威书籍,我都觉得自己对这块知识吸收的很差。最近打算重构的时候,突发奇想:要不我换一个角度去学习多线程,从它的发展史去入手?结果就是我的想法是正确的,我也明白了自己之前为什么消化不了知识。基于这个角度去学习、梳理多线程,我仿佛打开了新世界,我知道了Java多线程里的各种特性是什么时候出现的、为什么会出现(当然,底层的话目前还没去考究)。基于此,有了这一版更新。我个人认为这个框架基本上是完善的,只是涉及到底层实现、以及代码实践方面的内容我还没有补充,这一块我会循序渐进补充的。如果有网友愿意花时间读到这里,希望这块内容对你学习多线程有帮助,当然,如果发现我写的内容有误,也劳烦指正。
作者其他创作
大纲/内容
byte
short
int(字面量默认类型)
long
整型(默认值0)
float
double(字面量默认类型)
浮点型(默认值0.0)
char
字符型
true
false(默认值)
布尔
基本数据类型(掌握)
小的会自动转大的
byte、short、char在运算中会转为int
自动类型提升
丢失精度
(强制类型)
强制类型转换
只能用+号运算,结果一定是String
基本数据类型和String的运算
基本数据类型变量间运算规则(掌握)
二进制
十进制
八进制
等等
进制
正数:最高位是 0
负数:最高位是 1
正数的补码与反码、原码一样,称为三码合一
负数的原码:把十进制转为二进制,然后最高位设置为 1
负数的补码:反码+1
计算机数据的存储使用二进制补码形式存储,并且最高位是符号位。(了解)
计算机对数据的存储(了解)
Java的包装类有提供对应的静态方法,实现进制转换的操作。一般情况下也用不上,除了在刷题会要求的自己写进制的转换。
关于进制的转换(了解)
Class、Interface、Array
Enum、Annotation、Record
引用数据类型(掌握)
变量
直接使用i++和++i没区别
i++,先用i,在加1
++i,先加1再用i
结合赋值符或者输出使用
++
减减同上
--
不细讲了,主要是++和--两个说一下
算术运算符
这类会带有隐式的强制类型转换,转为=号左边的
主要是说一下+=、-=这类的
赋值运算符
没什么特别需要说明的说法
比较(关系)运算符
两个的,如果左边第一个条件不满足,就会直接返回false;一个的两边都得判断完才能得出结果
面试问的比较多的是|和||的区别、&和&&的区别
逻辑运算符
计算效率上会比一般的算术运算符高,但是难度也会偏高,要求掌握二进制运算的知识。平时基本没有使用场景,不过在源码里偶尔会见到。
位运算符
三元运算符
条件运算符
运算符
没啥好说的,顾名思义
顺序
这个没啥好说的,有手就行
if-else
switch表达式在jdk7后支持String,本质是使用String的Hash值
这块涉及到面试问的比较多的一个题
break和default留意一下就可以了
switch-case
网上的课程说法是能用switch的可以尽量用,效率会高一点,但是我个人实际编码过程中是没怎么用过,直接if就完事了,review的时候要领导提意见了再说
上面两个效率比较
分支
for
while
至少执行一次
do...while
循环
流程控制
声明+初始化
长度 arr.length
索引 arr[i]
使用
在有IDE加持的情况下,这些基础操作都不是事儿,代码提示一把梭
遍历
有一说一我真没见过用多维数组的,甚至一维都少见,更甚数组都少见到用的,基本都是集合框架那块用的多,数组一是定长,而是提供的方法支持没有集合那么多,用起来确实没集合框架那么顺手
多维数组
数组填充我还真没用过
用的比较多的就sort排序、二分查找、数组复制、数组比较
返回的ArrayList是Arrays类里面的一个内部类,和集合框架里的ArrayList并不是一个类。它并没有实现集合的修改方法,如果调用修改方法会抛异常。如果你确实要修改,把它作为集合ArrayList构造函数的入参,包成集合里的ArrayList用
特别注意!
asList
排序
sort
并行排序
parallelSort
二分查找
binarySearch
填充,将一个数组的所有元素设置为同一个指定的值
fill
复制,用于创建一个新数组,并将原数组的内容复制到新数组中
copyOf
copyOfRange
比较,返回值是int
compare
返回值是布尔
equals
用于并行地对数组中的元素执行累积操作(如累加)
parallelPrefix
spliterator
基于当前数组获得一个可操作的Stream流
stream
对应工具类Arrays
下标越界
空指针
数组异常
数组
基础语法
面向对象是将构成问题的事物分解成一个个的对象,使用对象去描述事物在解决问题过程中的行为,而不是专注于使用对象去完成一个步骤
1.1面向对象
面向过程简而言之就是将一个问题的解决划分为多个步骤,使用函数实现一个个的步骤,然后再按照顺序进行调用
1.2面向过程
两者在解决问题时,专注的角度不同,面向对象正如这个名称而言,它更关注问题中设计到了那些对象,有什么属性,涉及到什么行为,去将这样的对象一个个实例化,再通过对象之间的行为去解决一个问题,而,面向过程也是,关注点在于第一步要做什么、第二部要做什么,这样一个循序渐进的过程。
1.3总结
PS:面向对象和面向过程并不是互相对立的,面向过程作为最原始的coding范式,衍生出了面向对象编程,在编写对象编程中我们依旧能看到面向过程的影子。
面向对象和面向过程的区别
让一个类只做一种类型责任,当这个类需要承当其他类型的责任的时候,就需要分解这个类
单一职责
对扩展是开放的,而对修改是封闭的
开闭原则
当一个子类的实例应该能够替换任何其超类的实例时,它们之间才具有is-A关系
里氏替换
高层模块不应该依赖于低层模块,二者都应该依赖于抽象
抽象不应该依赖于细节,细节应该依赖于抽象
倒置依赖
不能强迫用户去依赖那些他们不使用的接口。换句话说,使用多个专门的接口比使用单一的总接口总要好
接口隔离
面向对象的SOLID原则
常量
成员属性
类属性
属性
成员行为(方法)
类行为(方法)
行为(方法)
默认提供无参构造,如果你自己写了构造函数,则不提供无参构造
构造器
构造代码块
静态代码块
代码块
像成员变量和局部变量和静态方法啥的区别,你初学的时候得死记硬背觉得很抽象,但是当你往JVM学习之后就好理解,它们的区别在于它们在运行时数据区中存储的位置不一样,导致了它们的生命周期、默认值、作用范围等的不一样,我个人不太喜欢死记硬背,别人问我的时候我也不能立刻答上来,但是我会从JVM的角度去梳理、对比它们,总而言之,知识的融会贯通很重要。
局部变量在栈帧的局部变量表上,它在编译时就决定了栈帧的大小,所以对于局部变量,你必须进行初始化才能使用,编译器不帮你设置零值
成员变量基于对象的创建过程,这个过程有一个设置零值的操作,所以可以只进行声明,不进行初始化
静态变量在类加载时处理,它也有一个零值的设置阶段,所以也可以只声明不初始
然后就是作用范围,类先加载,然后对象才能在创建,所以在静态方法里肯定是不能用成员方法和成员变量的
总之这些东西罗里吧嗦一堆很难说清楚的,但是你从JVM的角度去推,思路就很清晰
举个例子
这里面的很多细节有一说一我很想讲,但是我不知道怎么讲(生命周期、默认值、作用域相关)
类
new关键字,通过构造函数创建
注意Object的clone是protected权限的
clone
反序列化
Class.newInstance默认调用无参构造函数,并且不检查访问权限,这可能导致安全问题,而新的方式带来了更高的安全性和灵活性
原因
以前:DivClass.class.newInstance()
现在:DivClass.class.getDeclaredConstructor().newInstance()
JDK9以后,Class类的newInstance方法被弃用了。如果要通过反射创建对象,需要获取、调用构造函数。
通过反射创建
对象的创建方式
例:new Person().shout();
一个对象只需要进行一次方法调用,那么就可以使用匿名对象
通过Proxy创建代理对象时,构造参数的第三个参数,我们就可以传入匿名对象
举例:
将匿名对象作为实参传递给一个方法调用
使用场景
匿名对象
对象
封装简而言之就是将类的内部细节隐藏,只对外提供公共的访问方法。JavaBean就是一个很好的例子,将字段私有化,只对外提供Setter和Getter方法进行访问。
封装的优点在于可以提高代码的安全性、降低耦合
封装
继承就是将多个类所拥有共同属性和行为向上抽取,形成一个父类。子类通过extends去继承这个父类就能够获得公共的行为和属性
继承的优点在于可以提高代码的复用性,但是问题也很明显,它破坏了封装性,并且是一种强耦合
PS:继承应该遵循里氏替换原则,子类对象必须能够替换掉所有父类对象。
1、子类拥有父类非private的属性和方法。(另一种说法是,子类也拥有父类的私有属性和方法,但是它并没有权限去访问他们)
2、子类可以拥有自己属性和方法,即子类可以对父类进行扩展
3、子类可以用自己的方式实现父类的方法(重写)
继承注意点:
继承
编译时多态指对象引用所调用的方法在编译期就确定了,主要指方法的重载
编译时多态
运行时多态指程序中定义的对象引用所指向的具体类型在运行期间才确定,主要指方法的重写
运行时多态的必要条件:继承、方法重写、父类引用指向子类对象(向上转型)
PS:重写发生在父子类之间,并有以下规范:子类重写的方法的返回值类型要小于或者等于父类,返回值也一样,而方法的权限修饰符要大于父类,这是为了符合设计模式关于类设计的五大原则之一里氏替换
编译看左边,运行看右边。只能调用父类拥有的方法,子类自己新增的方法不能调用
运行时多态
多态的优点在于提高了程序的可拓展性
关于多态的原理:去看JVM篇的虚方法调用
多态
面向对象的特点
(默认)无参构造
有参构造
构造器问题
同一个类,方法名一样,参数列表不同
重载
@Override注解说明
继承关系
子类重写的方法必须和父类被重写的方法具有相同的方法名称、参数列表
比如父返回一个Object,子可以返回Integer
子返回值能够用父返回值替代
子类重写的方法的返回值类型不能大于父类被重写的方法的返回值类型
子权限>=父权限
子类重写的方法使用的访问权限不能小于父类被重写的方法的访问权限
如父异常抛RuntimeException,你可以抛一个它的子异常
子异常能够用父异常替代
子类方法抛出的异常不能大于父类被重写方法的异常
条件
重写
重载和重写
公共所有人都能访问
public
仅限同一个包下的类以及它的子类能访问
protect
仅限同一个包下的类能访问
默认
只有当前类能访问
private
都可以修饰
方法、字段
修饰问题
访问权限问题
本质是一个指针,在局部变量表索引0的位置
this
关键字
super
this和super
修饰属性
修饰方法
static关键字
修饰类
final关键字
抽象声明的属性都是静态常量;抽象类没限制
接口只能声明方法不能实现方法,但是jdk8有默认方法;抽象类可以声明也可以实现方法
方法
接口不能有构造函数,抽象类可以有
构造函数
接口支持多继承,抽象类不支持
多继承
接口和抽象类
getClass
反射用的
深拷贝
浅拷贝
拓展:
hashcode
toString
集合用的
wait
notify
notify all
并发用的
触发垃圾回收的方法,已经不用了
JVM用的
Object类的方法
当静态变量用
静态成员内部类
当成员变量用
非静态成员内部类
成员内部类
可以作为方法入参,如FunctionalInterface
匿名
非匿名
局部内部类
内部类
本质也是类,但是对象个数有限,只读不可修改
枚举
什么是序列化
Serializable接口
transient关键字
序列化相关
@Override
重写检查
@SuppressWarnings
抑制警告
@Deprecated
弃用标识
常用注解
被javadoc记录
@Document
ElementType.FIELD
ElementType.METHOD
ElementType.PARAMETER
ElementType.CONSTRUCTOR
可选值(比较多,标一些常见常用的)
注解作用范围
@Target
SOURCE
CLASS
RUNTIME
可选值
使用建议:自定义的一律RUNTIME
生命周期
@Retention
允许子类继承父类注解
@Inherited
元注解(java.lang.annotation)
声明自定义注解,配置元注解等信息
读取(用反射)、处理逻辑
自定义注解
注解
基本数据类型不符合面向对象特性
为什么要包装类
直接记,除了浮点都有缓存
留意缓存值,面试有问
拆箱和装箱看看就好了,反正也不用你手动拆或者装
字符串、基本数据类型、和包装类间转换
大小写转换
进制转换
比较
包装类的API,没特别留意过
这里我觉得主要是看对入参的约定吧,如果是基本数据类型的话,在不穿参数时,会有对应的默认值,如果使用的是包装类型,则会是null,一般来说我个人推荐使用包装类型,因为null的语义相比基本数据类型的默认值,来的更加的直观,你能够清晰的确认导入有没有入参进来。
后端接口的方法参数你是使用基本数据类型还在包装类型?
包装类和基本数据类型的区别:默认值、行为,这里拓展一下在面试中遇到的一个问题
包装类
面向对象
jvm处理不了的,出错程序直接停止
像栈溢出、堆溢出、方法区溢出这些
Error类(非受检)
可以进行处理,处理完程序可以继续正常运行,如果不处理,程序直接停止
下标越界、类型转换异常、空指针、运算符异常、非法参数
运行时异常(非受检)
找不到文件
未知host
非运行时异常(受检)
Exception类
Throwable类
有资源要释放的用try resource
try catch finally
在当前方法内捕获处理
在方法声明上通过throws
不在当前方法里处理,往调用者抛出,让调用者处理
异常处理
通过throw关键字抛出异常
手动抛异常
1、继承RuntimeException类
2、提供无参有参构造函数
3、通过throw关键字处理异常
自定义运行时异常
根据Java语言规范,如果finally块中有return语句,那么try或catch块中的return将不会被执行。JVM会使用finally块中的return值作为方法的最终返回值
PS:关于在finally中进行return操作的争议
异常
资源分配的单位:进程
调度单位:线程
进程和线程的区别看操作系统去
并发:同一时间段
并行:同一时刻
并发和并行的区别也看操作系统去
当多个线程同时访问一个对象时,如果不考虑这些线程在运行时环境下的调度和交替执行,也不需要进行额外的同步,或者在调用方进行任何其他协调操作,调用这个对象的行为都可以获得正确的结果,那就称这个对象是线程安全的
线程安全的定义
安全性最直接、最纯粹,例如:final、数值包装类型、大数据类型
不可变
JavaAPI中标注自己是线程安全的类,大多数都不是绝对的线程安全。
绝对线程安全
Vector、HashTable
相对线程安全
ArrayList、HashMap
线程兼容(我们通常说的线程不安全)
Java环境下,这种代码通常是有害的,尽量避免,例如:Thread类的弃用方法就是因为这个,会导致死锁
线程对立
线程安全分类
互斥
请求和保持
不可抢占
循环等待
看操作系统去
避免一个线程同时获取多个锁
避免一个线程在锁内同时占用多个资源,尽量保证每个锁只占用一个资源
死锁的避免
死锁
引子
https://www.raychase.net/698
https://www.cs.umd.edu/~pugh/java/memoryModel/jsr-133-faq.html
https://developer.jdcloud.com/article/2747
好文推荐(审核别卡我,真是好文):
1.0作为Java的第一个版本,原生的支持了多线程。提供了java.lang.Thread 类和 Runnable 接口为基础的多线程编程模型奠定了框架。此时的多线程模型比较简单,开发者需要手动处理线程的创建、管理、和同步(Synchronized、Volatile)。
1.2提供了线程池的雏形,虽然 JDK 1.2 没有提供线程池的标准实现,但开发者开始意识到手动创建和管理线程的成本较高,于是很多开发者自定义线程池模型以提升性能;另外,ThreadLocal的引入,提供了线程局部存储,使变量在每个线程中隔离,避免了多个线程访问共享数据时的冲突。除此之外,Collections工具类的引入,为集合的线程安全提供了一种解决方案。
简述:
继承Thread类,重写run方法
实现Runnable接口,重写run方法
线程的创建方式
NEW
start
RUNNABLE
线程竞争锁产生阻塞
BLOCKED
Object. wait with no timeout
Thread. join with no timeout
WAITING
Thread. sleep
Object. wait with timeout
Thread. join with timeout
TIMED_WAITING
TERMINATED
线程生命周期
注意和run()区别,直接调用run是在主线程里执行方法,调用start()本质是调用了native方法向操作系统申请了新的线程资源
public synchronized void start
在一个线程中调用其他线程的join方法,实现同步执行的效果
通过do-while来持有/释放锁
join带超时时间
死循环,通过判断当前插队的线程是否存活来持有/释放锁
join不带时间
本质是调用了Object里的wait方法,插队的线程通过那到这个锁,以形成同步执行的效果
join(synchronized修饰)
打断当前的线程对象。如果打断前线程就是被阻塞的状态,那么会清除中断标记,并抛出InterruptedException
public void interrupt
判断打断线程的状态,不会清除打断标记,因为就是简单的return了一个中断值
public boolean isInterrupted
判断线程是否已经中断,会清除中断标记。简而言之如果连续调用这个方法两次,第二次必定会返回false
public static boolean interrupted,静态方法
中断
通知调度器当前线程可以让出cpu资源,但是是否让出实际是由调度器来决定的,这个方法源码注释里并不推荐使用该方法,它更多是在调试、测试场景下使用。
public static native void yield
记住会不释放锁就行了
public static native void sleep
native方法
线程常用方法
可见性
原子性
它本身并不会禁止指令重排序,但是他符合了“as-if-serial”原则,在但线程中的执行结果不会改变,因为它恁重,他都直接阻塞其他线程了,其他线程在它释放之前根本就不可能去干扰他,所以它在单线程里怎么执行都是它的事。
某种意义上的保证有序性
可重入
无条件阻塞后面其他线程进入
代码块同步,字节码文件中会多出monitorenter和monitorexit指令
方法同步,字节码文件中的方法信息中会多一个ACC_SYNCHRONIZED同步标识
JVM基于进入和退出Monitor对象来实现方法同步和代码同步,再深入一点就是每一个Java对象都持有一个对应的Monitor对象,这个Monitor对象是底层C++实现的(在Java虚拟机(HotSpot)中,monitor是由OnjectMonitor实现的,其主要的数据结构如下(位于HotSpot虚拟机源码ObjectMonitor.hpp文件,C++实现)),Java对象通过对象头中的指针与它关联,再再深入一点,就是操作系统里说的管程Monitor(尚硅谷阳哥著名言论:天生飞的理念都有落地的实现,这个也是基于操作系统理念进行落地的实现)
底层
特性
无锁
偏向锁
轻量级锁
扩大加锁范围(如:在循环体中使用,导致每次循环都要加锁释放锁,性能开销太大,不如直接扩大范围)
锁粗化
适应性自旋
检测到需要同步的代码段根本不存在竞争,进行优化
锁消除
synchronize锁优化
synchronized关键字
volatile是Java虚拟机提供的最轻量级的同步机制。
强刷缓存实现
禁止指令重排序实现
有序性
比如自增自减
Java运算操作符并非是原子操作,导致volatile变量的运算在并发下是不安全的
不完全保证原子性
特性:
底层:带lock前缀的指令(内存屏障)→处理器嗅探机制,将工作线程的缓存写入内存,同时使其他线程的缓存失效,保证线程读取到的是最新值,保证可见性;插入内存屏障(lock前缀指令),禁止指令的重排序
针对浮点型的变量,除非明确可知变量存在竞争,否则不要刻意的声明为volatile
volatile关键字
天生不可变,天生线程安全,实现原原理是内存屏障
final
补充:
空间换时间,每个线程拥有自己的一份副本变量
当一个线程调用ThreadLocal的Set方法时,首先会从尝试直接从当前Thread的threadLocals成员变量获取,如果这个成员变量为null,则表示当前线程还没有初始化ThreadLocalMap,接着会调用createMap方法初始化当前线程的ThreadLocalMap对象,key值为当前的ThreadLocal对象,创建完成后将这个对象赋值给当前Thread的threadLocalMap对象。
ThreadLocalMap
ThreadLocal只是一个壳子,内部使用的ThreadLocalMap类才是实质
InheritableThreadLocal继承自ThreadLocal,重写了childValue、getMap和createMap方法,实现父子线程共享的核心是对于childValue的重写。结合Thread类的init方法来看,Thread对象在创建的时候,如果父类的inheritableThreadLocals不为空,则会调用ThreadLocal的createInheriteMap方法进行创建,接着通过childValue方法获取父线程的值,把父线程的inheritableThreadLocals的值赋值到新的ThreadLocalMap对象
InheritableThreadLocal
ThreadLocal类
ThreadLocalMap(空间换时间,每个线程持有一份副本)
ThreadLocal
线程的同步方式
notifyall
Object方法(Synchronized)
线程的通信方式
JDK1.0-1.2
虽然 JDK 1.4 并未对多线程进行重大改革,但它引入了NIO,允许异步和非阻塞操作,这对于可扩展的多线程网络应用程序至关重要。
JDK1.4
这是 Java 并发模型的一个重大转折点,Java并发模型的飞跃!Doug Lea大神横空出世!Java原生提供的不好用,我自己写一个!java.util.concurrent 包的引入使得多线程编程更加易用和高效。
JSR 133 重新明确了 Java 内存模型
JSR 133
JSR 166 的贡献就是引入了 java.util.concurrent 这个包
JSR 166
除了JUC包以为,还有几个比较重要的东西
JDK5
JDK6,对Synchronized底层的实现进行了优化,并提供了Fork/Join 框架的雏形。
提高了线程任务的可操作性
意义:
实现Callable接口(可以有返回值、异常),重写call方法,用FutureTask执行、接收返回值
submit和execute的区别
ThreadPoolExecutor√
ExecutorServices接口×
线程池Executor框架
LockSupport. park
LockSupport. parkNanos
LockSupport. parkUntil
线程生命周期(新增的)
ReentrantLock
ReentrantReadWriteLock
CountDownLatch
CyclicBarrier
......
并发工具类
基于Lock接口的阻塞同步
JUC包里提供的原子类
基于Unsafe类的CAS操作+Volatile的非阻塞同步
新增的线程的同步方式
await
signal
Condition(Lock接口)(本质也是基于LockSupport实现)
park
unpark
LockSupport
新增的线程的通信方式
如 ConcurrentHashMap,解决了传统同步集合类(如 Hashtable)性能较差的问题
提供并发数据结构
JDK5-6
Fork/Join 框架的引入专门用于任务递归拆分的并行处理,特别适用于 CPU 密集型任务的处理。这一框架允许将大任务分解成子任务,并通过 ForkJoinPool 并发处理,充分利用多核处理器。
这是一个用于线程协同的高级工具,比 CyclicBarrier 更加灵活,支持多个阶段的并发任务协调
提供了新的线程协调工具Phaser
对Locks框架进行了增强,使得 ReentrantLock 更加灵活和强大
JDK 7
JDK 8 引入了 CompletableFuture,为异步编程提供了强大的支持,支持非阻塞式的并发编程模型。开发者可以更容易地编写响应式和异步流式处理程序。
Stream API的引入。虽然不是直接的并发工具,Stream API 提供了便捷的并行流处理支持,开发者可以通过 parallel() 轻松实现数据的并行化处理。
引入了新的锁机制 StampedLock,它是一种更灵活和高效的读写锁,旨在解决并发编程中读写锁的性能瓶颈。
JDK 8
从JDK的发行版本窥探Java多线程、并发的发展历程:(这里主要是说明这些发行版本做了什么,不会对知识点进行刨根问底)
局限性:不支持多继承
(JDK1.0)①通过继承Thread类,重写run方法创建
new Thread(()-> System.out.println(\"Runnable\")).start();
进阶写法:函数式接口
(JDK1.0)②通过声明Runnable接口,重写run方法创建
定义了操作异步任务执行的一些方法,提供一种异步并行计算的功能,如获取异步任务的执行结果、取消异步任务的执行、判断任务是否被取消、判断任务执行是否完毕等(多线程、异步、有返回、异常处理)
前言:
Runnable接口+Callable接口+Future接口和FutureTask实现类
基础使用:
FutureTask<Integer> task = new FutureTask<>(()->{ System.out.println(\"Callable\"); return 0; }); task.run(); task.get();
进阶:函数式接口(JDK8)
Future+线程池异步多线程任务配合,能显著提高程序的运行效率
优点:
get()阻塞
isDone()轮询
缺点:
1、Callable接口+FutureTask
创建、结果传递、结果合并、任务顺序、异常处理
链式操作
解决Future的阻塞、轮询问题
为什么出现
无返回值
CompletableFuture.runAsync
有返回值
CompletableFuture.supplyAsync
PS:入参的线程池,若没有指定,则使用默认的ForkJoinPoolcommonPool()作为它的线程池执行异步代码
核心静态方法
public T get()
public T join() --->和get一样的作用,只是不需要抛出异常
public T getNow(T valuelfAbsent) --->计算完成就返回正常值,否则返回备胎值(传入的参数),立即获取结果不阻塞
获取结果
public boolean complete(T value) ---->是否打断get方法立即返回括号值
主动触发计算
获得结果和触发计算
thenApply --->计算结果存在依赖关系,这两个线程串行化---->由于存在依赖关系(当前步错,不走下一步),当前步骤有异常的话就叫停
handle --->计算结果存在依赖关系,这两个线程串行化---->有异常也可以往下走一步
对计算结果进行处理
接受任务的处理结果,并消费处理,无返回结果
thenAccept
thenRun(Runnable runnable) :任务A执行完执行B,并且不需要A的结果
thenAccept(Consumer action): 任务A执行完执行B,B需要A的结果,但是任务B没有返回值
thenApply(Function fn): 任务A执行完执行B,B需要A的结果,同时任务B有返回值
对比补充
对计算结果进行消费
applyToEither
对计算速度进行选用
thenCombine
对计算结果进行合并
常用方法
优化:jdk8提供的CompletableFuture类
2、优化
(JDK5)③通过Callable接口+FutureTask实现
降低资源消耗
提高线程响应速度
提高线程的可管理性
使用线程池的优点
execute方法(没有返回值,没有异常抛出)
接口方法声明
submit方法(有返回值,有异常抛出)
比较柔性,先设置线程池状态,等待正在执行的线程执行完成在关闭
shutdown
强硬,直接停止正在执行的线程,关闭线程池
shutdownNow
线程池的关闭
isShutdown
isTerminated
awaitTermination
invokeAll
invokeAny
ScheduledThreadPoolExecutor(JDK1.5)
核心线程数corePoolSize
最大线程数maximumPoolSize
空闲线程存活时间keepAliveTime
keepAliveTime的单位
存活时间的单位unit
基于数组结构的有界阻塞队列,按FIFO排序任务
ArrayBlockingQueue
基于链表结构的阻塞队列,按FIFO排序任务,吞吐量通常要高于ArrayBlockingQueue
LinkedBlockingQueue
一个不存储元素的阻塞队列,每个插入操作必须等到另一个线程调用移除操作,否则插入操作一直处于阻塞状态,吞吐量通常要高于LinkedBlockingQueue
SynchronousQueue
具有优先级的无界阻塞队列
PriorityBlockingQueue
等待任务的阻塞队列workQueue
默认为DefaultThreadFactory
创建线程的工厂threadFactory
AbortPolicy:默认,直接抛出RejectedExcutionException异常,阻止系统正常运行
CallerRunsPolicy:该策略既不会抛弃任务,也不会抛出异常,而是将某些任务回退到调用者
DiscardOldestPolicy:抛弃队列中等待最久的任务,然后把当前任务加入队列中尝试再次提交当前任务
DiscardPolicy:直接丢弃任务,不予任何处理也不抛出异常,如果运行任务丢失,这是一种好方案
自定义:实现RejectedExecutionHandler接口
拒绝策略handler
线程池(ThreadPoolExecutor)的主要参数
如果没满,创建线程执行任务
如果没满,将任务存储到队列里
如果满了,按照策略处理无法执行的任务
如果满了,判断线程池是否已满
如果满了,接着判断阻塞队列是否已满
提交一个任务,首先判断核心线程池是否已满
线程池的处理流程(工作原理)
这种类型的池始终具有指定数量的线程正在运行
FixedThreadPool(JDK1.5)
WorkStealingPool(ForkJoinPool)(JDK1.8)
ScheduledThreadPool(JDK1.5)
只有一个线程
SingleThreadExecutor(JDK1.5)
CachedThreadPool(JDK1.5)
Executor工具类(JDK1.5)
内置实现
通过ThreadPoolExecutor的构造函数进行自定义
Executors返回的线程池对象所使用的阻塞队列要么是无界的,要么上限值太大,都容易引起OOM
自定义线程池
Apache commons-lang
Google Guava
使用权威第三方的工具包
怎么办呢?
为什么实际工作不用内置线程池?
CPU密集型任务配置尽可能少的线程数量:一般公式:CPU核数 + 1个线程数
CPU密集型
IO密集时,大部分线程都被阻塞,故需要多配置线程数:参考公式:CPU核数 / (1 - 阻塞系数) 阻塞系数在0.8 ~ 0.9左右
IO密集型
区分任务类型
CPU密集型、IO密集型或者是混合型
①判断任务特性
②使用有界队列
③合理监控线程池
如何合理的配置线程池
如何配置线程池参数
ThreadPoolExecutor类(JDK1.5)√
fork/join 框架是 ExecutorService 接口的一种实现,它专为可以递归地分解成较小部分的工作而设计。目标是使用所有可用的处理能力来提高应用程序的性能。
fork/join 框架使用工作窃取算法。无事可做的工作线程可能会从仍然繁忙的其他线程中窃取任务。
1、创建一个表示要完成的所有工作的任务(类),该任务继承RecursiveAction
2、创建将运行任务的 ForkJoinPool
3、通过调用invoke方法执行任务
基础使用
拓展:源码中,集合部分许多带parallel前缀的方法,本质都是使用了fork/join实现,如parallelSort(),在大型任务处理的场景上会比一般的方法快。除此之外Stream部分也有使用到fork/join
ForkJoinPool类(JDK1.7)
AbstractExecutorService抽象类
ExecutorService接口(JDK1.5)
Executor接口(JDK1.5)
JDK提供的线程池框架
深度剖析线程池
(JDK5)④通过线程池创建
线程id
线程名
线程优先级
线程状态
线程组
是否守护线程
。。。
线程属性
Object. wait with no timeout(JDK1.0)
Thread. join with no timeout(JDK1.0)(本质也是调的wait)
LockSupport. park(JDK5)
Thread. sleep(JDK1.0)
Object. wait with timeout(JDK1.0)
Thread. join with timeout(JDK1.0)(本质也是调的wait)
LockSupport. parkNanos(JDK5)
LockSupport. parkUntil(JDK5)
使用方式
当前的实例对象
当前类的Class对象
锁对象
重量级锁
synchronize锁优化(JDK6)
synchronized关键字(JDK1.0)
1、ReentrantLock reentrantLock = new ReentrantLock()
2、reentrantLock.lock();
3、reentrantLock.unlock();
PS:加锁和解锁操作要用try- catch-finally包围
等待可中断
可实现公平锁
可以绑定多个条件
相较synchronize的优点
AQS 是用来构建锁或者其它同步器组件的重量级基础框架及整个JUC体系的基石, 通过内置的FIFO队列来完成资源获取线程的排队工作,并通过一个int类变量(state)表示持有锁的状态
深究AQS原理(抽象的队列同步器)
AbstractQueuedSynchronizer抽象类
底层剖析(AQS)
Lock接口的实现类(JDK1.5)
StampedLock(JDK8)
AtomicBoolean
AtomicInteger
AtomicLong
原始类型
AtomicMarkableReference
AtomicReference
AtomicStampedReference
引用类型
JUC包里提供的原子类(原理就是基于Unsafe类提供的CPU指令级CAS操作+Volatile关键字)
内存位置V
旧预期值A
准备设置的新值B
涉及到三个操作数
当且仅当V符合A时,处理器才会用B更新V的值
CAS简述
自旋过久消耗性能
CAS的自旋问题
版本号
其实大部分情况下ABA问题不会影响程序并发的正确性
CAS的ABA问题
tip:
CAS(atomic包)(JDK1.5)
非阻塞
ThreadLocal(JDK1.2)
ThreadLocalRandom(JDK1.7)
天生不可变,天生线程安全,实现原理是内存屏障
final(JDK1.0)
volatile是Java虚拟机提供的最轻量级的同步机制。volatile 关键字可以确保一个变量的更新对于所有线程都是可见的,即当一个线程修改了 volatile 变量的值,其他线程立刻可以看到最新的值,而不是从缓存中读取到旧值。
介绍:
带lock前缀的指令(内存屏障)→处理器嗅探机制,将工作线程的缓存写入内存,同时使其他线程的缓存失效,保证线程读取到的是最新值,保证可见性;插入内存屏障(lock前缀指令),禁止指令的重排序
底层原理:
volatile 关键字在 Java 中不可以完全代替线程同步,它在某些情况下可以提供线程间的可见性保证,但不具备原子性。这意味着在一些复杂的场景中,volatile 不能替代 synchronized 或其他锁机制。
如果你只需要保证某个共享变量的可见性,而不涉及复杂的读写操作,例如标志位、状态指示器等场景,volatile 是合适的选择
什么时候使用Volatile?
如果需要保证原子性操作,或涉及多个步骤的复杂操作(如递增、累加等),synchronized 或其他更高级的同步工具(如 ReentrantLock、AtomicInteger)才是合适的。
什么时候使用Synchronized?
剖析:
这里我又问了?你竟然要用Volatile,为什么不直接用原子包呢?
volatile 不能用于线程同步,但可以用于保证线程间的变量可见性。
如果需要保证操作的原子性,应该使用 synchronized 或其他锁机制。
总结:
关于Volatile(JDK1.0)
Object方法(Synchronized)(JDK1.0)
拓展:Condition通信的实现也是基于LockSupport原语实现的
Condition(Lock)(JDK1.5)
简介:LockSupport是用来创建锁和其他同步类的基本线程阻塞原语,所有的方法都是静态方法
原理
LockSupport(JDK1.5)
顾名思义,倒计时,倒计时介绍之后目标线程执行
CountDownLatch(JDK1.5)
类似游戏里的匹配模式,大家伙都准备好了才能开游戏
CyclicBarrier(JDK1.5)
允许n个任务同时访问某个资源
Semaphore(JDK1.5)
用于两个线程之间交换数据
Exchanger(JDK1.5)
它可以实现CyclicBarrier和CountDownLatch类似的功能,而且它支持对任务的动态调整,并支持分层结构来达到更高的吞吐量
Phaser(JDK1.7)
JUC提供的辅助类
ConcurrentHashMap(JDK1.5)
ConcurrentLinkedDeque(JDK1.7)
ConcurrentLinkedQueue(JDK1.5)
ConcurrentSkipListMap(JDK1.6)
ConcurrentSkipListSet(JDK1.6)
CopyOnWriteArrayList(JDK1.5)
CopyOnWriteArraySet(JDK1.5)
并发数据结构
ArrayBlockingQueue(JDK1.5)
DelayQueue(JDK1.5)
LinkedBlockingDeque(JDK1.5)
LinkedBlockingQueue(JDK1.5)
LinkedTransferQueue(JDK1.7)
PriorityBlockingQueue(JDK1.5)
SynchronousQueue(JDK1.5)
阻塞队列
并发工具
对Java多线程、并发作一个整体性的总结(截止至JDK8)
☆多线程、并发
indexOf
charAt
查找
subString
split
截取
replace
替换
trim
去空
开头结尾
length
getbytes
String
线程不安全
StringBuilder
线程安全
StringBuffer
public final native Class<?> getClass()
public native int hashCode()
public boolean equals(Object obj)
public String toString()
public final native void notify()
public final native void notifyAll()
public final void wait
Object类
Date
SimpleDateFormat
LocalDate
LocalDateTime
LocalTime
日期类
getProperties
createProperties
currentTimeMillis
exit
System类
addShutdownHook
exec
freeMemory
getRuntime
maxMemory
removeShutdownHook
totalMemory
version
Runtime类
Math类
实现 compareTo(Object obj)方法
自然排序java.lang.Comparable接口
自定义排序java.util.Comparator接口
比较器(引用数据类型的比较排序)
常用API/类
int size();
boolean isEmpty();
boolean contains(Object o);
Iterator<E> iterator();
toArray();
boolean add(E e);
boolean remove(Object o);
boolean containsAll(Collection<?> c);
boolean addAll(Collection<? extends E> c);
boolean removeAll(Collection<?> c);
boolean retainAll(Collection<?> c);
void clear();
接口方法
底层数组、线程不安全
1.5倍扩容
未显示初始化,默认0,添加第一个元素时在扩容为10
ArrayList
底层双向链表
LinkedList
数组,线程安全
2倍扩容
未显示初始,默认10
Vector
Queue
List接口(有序可重复)
底层是HashMap的key
集合元素可以是 null
HashSet
LinkedHashSet
TreeSet
Set接口(无序不可重复)
遍历元素
PS:通过迭代器遍历元素时,不要使用集合的remove方法删除元素,会报并发修改异常,请使用迭代器提供的方法删除。这里很容易在用foreach语法糖的时候产生疏忽,因为它本质也是通过迭代器遍历的。
迭代器(Iterator)接口
常用实现类/接口
Collection接口
16
默认的初始容量
索引位置上的链表的长度达到 8,且数组的长度超过 64时,此索引位置上的元素要从单向链表改为红黑树。如果链表长度到8但是数组没到64,会先扩容
链表树化的条件
索引位置上的元素的个数低于 6 时,要从红黑树改为单向链表
红黑树链表化的条件
为了降低哈希冲突的概念,用 key 的 hashCode 值高 16 位与低 16 位进行了异或的干扰运算
哈希值的计算
(n - 1) & hash(√)
hash 值 % table.length,效率没有上面高
下标的技术
可以保证每一个下标位置都有机会被用到
为什么数组是2次幂?
拉链法
尾插法
哈希冲突问题的解决
细节探索:
从设计上来说,理论上当应用程序中的对象达到10w数量级时,才会开始出现哈希冲突。
关于哈希冲突
头插法+链表+多线程并发+扩容
为什么会产生
HashMap 改用尾插法,解决了链表死循环的问题
jdk8怎么解决
JDK1.7 中 HashMap 的循环链表
拓展
Node数组+链表+红黑树
在 HashMap 存储结构的基础上,使用了一对双向链表来记录添加元素的先后顺序,可以保证遍历元素时,与添加的顺序一致
LinkedHashMap类
HashMap类
Properties类
HashTable类(数组+链表)
红黑树
TreeMap类
LinkedListHashMap类
ConcurrentHashMap类
关于equals和hashcode的面试题,我的建议是直接看源码注释,哪里有什么花里胡哨的,人家注释里hashcode方法就是写给map用的,还有这里方法的逻辑关系,是从源码里面推出来的,源码里hashcode结果一致还会接着用equal判断一致,equal一致,那hashcode肯定一致啦
Map(K,V)接口
复制、替换
添加
同步
Collections工具类
☆集合
又名参数化类型,用于解决数据类型的安全性问题
接口
泛型工作中偶尔会用到(封装Result类)
java的泛型是编译擦除的
泛型方法
泛型接口
常用的
extend
上界
下界
上下界
泛型
在程序运行的过程中,动态的获取类的信息、动态的调用对象的属性和方法
反射能做什么
类.class
对象.getClass()
Class调用静态方法forName(String className)
ClassLoader的方法loadClass(String className)
获取Class实例的方式
Class类
Method类
Field类
Constructor类
Annotation接口
核心类
用于声明式的创建一个数组
Array
其他类
反射
InvocationHandler接口
Proxy类
核心类/接口
代理
老实说我不是很想讲这个,工作中要使用基本都是调人家封装好的,像hutool和guava、apach的lang
File类
输入流 :把数据从其他设备上读取到内存中的流(以 InputStream、Reader 结尾)
输出流 :把数据从内存 中写出到其他设备上的流(以 OutputStream、Writer 结尾)
数据的流向不同分为:输入流和输出流
字节流 :以字节为单位,读写数据的流(以 InputStream、OutputStream 结尾)
字符流 :以字符为单位,读写数据的流(以 Reader、Writer 结尾)
按操作数据单位的不同分为:字节流(8bit)和字符流(16bit)。
节点流:直接从数据源或目的地读写数据
处理流:不直接连接到数据源或目的地,而是“连接”在已存在的流(节点流或处理流)之上,通过对数据的处理为程序提供更为强大的读写功能。
根据 IO 流的角色不同分为:节点流和处理流
流的分类
InputStream
OutputStream
Reader
Writer
抽象基类
文件流: FileInputStream、FileOutputStrean、FileReader、FileWriter
字节/字符数组流: ByteArrayInputStream、ByteArrayOutputStream、CharArrayReader、CharArrayWriter
常用节点流
作用:增加缓冲功能,避免频繁读写硬盘,进而提升读写效率
缓冲流:BufferedInputStream、BufferedOutputStream、BufferedReader、BufferedWriter
作用:实现字节流和字符流之间的转换
转换流:InputStreamReader、OutputStreamReader
作用:提供直接读写 Java 对象功能
对象流:ObjectInputStream、ObjectOutputStream
常用处理流
流的API
public final void writeObject (Object obj)
用 ObjectOutputStream 类保存基本类型数据或对象的机制
public final Object readObject ()
用 ObjectInputStream 类读取基本类型数据或对象的机制
类必须实现 java.io.Serializable接口
如果有一个属性不需要可序列化的,则该属性必须注明是瞬态的,使用 transient 关键字修饰
静态(static)变量的值不会序列化
提供了一个序列版本号:serialVersionUID
实现
序列化
IO流
IO(java.io)
老实说我这个也不想讲,写业务根本就没有地方给你用网络编程,除非你自己造轮子
计算机网络的理论知识看计网的思维导图吧
Inet4Address
Inet6Address
InetAddress 类(IP)
ServerSocket(服务端)
Socket(客户端)
流套接字(stream socket):使用 TCP 提供可依赖的字节流服务
DatagramSocket
数据报套接字(datagram socket):使用 UDP 提供“尽力而为”的数据报服务
Socket 类(IP+端口)
API
URL(Uniform Resource Locator):统一资源定位符,它表示 Internet 上某一资源的地址
<传输协议>://<主机名>:<端口号>/<文件名>#片段名?参数列表
URL类
针对 HTTP 协议的 URLConnection 类
URL
网络编程
参数 箭头 方法体
Lambda表达式
用于检查接口是否满足函数式接口的使用条件
标注的接口只能声明一个未实现的方法
@FunctionalInterface
消费型接口:Consumer<T> void accept(T t)
供给型接口:Supplier<T> T get()
判断型接口:Predicate<T> boolean test(T t)
分类
函数式接口
方法引用、构造器引用、数组引用
Stream 自己不会存储元素
Stream 不会改变源对象。相反,他们会返回一个持有结果的新 Stream
Stream 操作是延迟执行的。这意味着他们会等到需要结果的时候才执行。即一旦执行终止操作,就执行中间操作链,并产生结果
Stream 一旦执行了终止操作,就不能再调用其它中间操作或终止操作了
Steam特点
通过集合创建Steam
通过数组创建Stream
通过 Stream 的 of()显示创建
创建 Stream 一个数据源(如:集合、数组),获取一个流
接收 Lambda , 从流中排除某些元素
filter(Predicatep)
筛选,通过流所生成元素的 hashCode() 和 equals() 去除重复元素
distinct()
截断流,使其元素不超过给定数量
limit(long maxSize)
跳过元素,返回一个扔掉了前 n 个元素的流。若流中元素不足 n 个,则返回一个空流。与 limit(n) 互补
skip(long n)
筛选与切片
接收一个函数作为参数,该函数会被应用到每个元素上,并将其映射成一个新的元素。
map(Function f)
接收一个函数作为参数,该函数会被应用到每个元素上,产生一个新的DoubleStream。
mapToDouble(ToDoubleFunction f)
接收一个函数作为参数,将流中的每个值都换成另一个流,然后把所有流连接成一个流
flatMap(Function f)
mapToInt(ToIntFunction f)
mapToLong(ToLongFunction f)
映 射
产生一个新流,其中按自然顺序排序
sorted()
产生一个新流,其中按比较器顺序排序
sorted(Comparator com)
中间操作 每次处理都会返回一个持有结果的新 Stream,即中间操作的方法返回值仍然是 Stream 类型的对象。因此中间操作可以是个操作链,可对数据源的数据进行 n 次处理,但是在终结操作前,并不会真正执行
检查是否匹配所有元素
allMatch(Predicate p)
anyMatch(Predicate p)
noneMatch(Predicate p)
findFirst()
findAny()
count()
max(Comparator c)
min(Comparator c)
forEach(Consumer c)
匹配与查找
可以将流中元素反复结合起来,得到一个值。返回 T
可以将流中元素反复结合起来,得到一个值。返回 Optional
reduce(BinaryOperator b)
归约
将流转换为其他形式。接收一个 Collector 接口的实现,用于给 Stream 中元素做汇总的方法
collect(Collectorc)
收集
终止操作(终端操作) 终止操作的方法返回值类型就不再是 Stream 了,因此一旦执行终止操作,就结束整个 Stream 操作了。一旦执行终止操作,就执行中间操作链,最终产生结果并结束 Stream
三大步骤
StreamAPI
时间API
Record
新特性
高级
Java知识脉络
0 条评论
回复 删除
下一页