前言(此文只是概念)
什么是AIP,有什么好处?
概括成一句话:帮助我们站在巨人的肩膀上,实现更加高效的开发
一些预先定义好的函数,无需理解其内部机制和细节,就可以使用其功能,作为规则,面向接口开发
常用的api包
- Java.util工具包:包含各种实用工具类/集合类/日期时间工具等各种常用工具包
- java.lang:java的核心包,包含了java基础类
Object类的方法
1. tostring();
父类默认是对象内存地址值
如果想看对象的属性方法值,必须重写Object.toString的方法
直接写对象的“名称”默认输出打印就是toString()
2. hashCode();
1.经过地址的hash算法后返回对象hash值
3. equals();
- 底层用了==进行对象间的比较,==比较的是两个对象的地址值
- 如果要判断两个对象间的所有属性值相同,比较结果为true,就需要重写equals();
- 否则使用的是Object的默认实现,通过==比较的是两个对象的地址值,不是属性
- String底层重写了equals(),所以equals()比较的是具体的值
StringBuilder和StringBuffer类
StringBuffer:JDK1.0中就有了,线程是安全
StringBuilder:JDK1.5推出的,线程不安全
执行效率:StringBuilder >> StringBuffer >> String
实际上区别不大父类都继承的是AbstractStringBuilder,只不过Buffer把代码加了同步关键字
String底层
String底层存的是常量 char[],存在堆中常量池中,同样的数据首次创建时新建,第二次不新建,到常量池中 找之前数据直接使用
包装类
Number抽象类
Number类是包装类的抽象父类
Integer类
Integer.alueOf()高效效果
在-128~127范围内,相同的数据只会存一次,后续在存都是使用之前存过的数据地址即数据
自动装箱
// 底层自动调用了Integer.valueOf(50);是高效效果创建的
Integer i = 50;//即可完成装箱
自动拆箱
//编译器会自动把包装类型的类拆掉箱子,
//变回基本类型数据,然后交给int i来保存
//自动拆箱调用的底层代码是intValue();
int i = new Integer(50);//自动拆箱
BigDecimal类,精准计算浮点型
此类主要用来解决浮点数计算不精准的问题
BigDecimal的坑
- 类构造方法请不要填写Double类型数据
- 因为二进制浮点值十进制表示形式,它本身储存就不精准
BigDecimal的除法运算
错误写法
//着种写法不正确,在除不尽的情况下会报算数异常除不尽
divide(BigDecimal);
正确写法
/**
* BigDecimal:除数
* n:除完需要保留几位小数
* o: 舍入方式
*/
divide(BigDecimal,n,o);
舍入方式:具体也可参考API
ROUND_HALF_UP
四舍五入,五入 如:4.4结果是4; 4.5结果是5
ROUND_HALF_DOWN
五舍六入,五不入 如:4.5结果是4; 4.6结果是5
ROUND_HALF_EVEN
公平舍入(银行常用)如:在5和6之间,靠近5就舍弃成5,靠近6就进位成6,如果是5.5,就找偶数,变成6
ROUND_UP
直接进位,不算0.1还是0.9,都进位
ROUND_DOWN
直接舍弃,不算0.1还是0.9,都舍弃
ROUND_CEILING
(天花板) 向上取整,取实际值的大值
朝正无穷方向round 如果为正数,行为和round_up一样,如果为负数,行为和round_down一样
ROUND_FLOOR
(地板) 向下取整,取实际值的小值
朝负无穷方向round 如果为正数,行为和round_down一样,如果为负数,行为和round_up一样
IO流
字节流和字符流区别
- 字节流:真对二进制文件读取的(音频和视频及三维媒体等)
- 字符流:针对文本文件,读写容易出现乱码的现象,在读写时,最好指定编码集为UTF-8,常用于处理纯文本数据
对程序而言
- in: (进)相当于从外部导入java内存中
- out: (出)相当于从java内存中导出数据
- 流只能单方向流动
- 数据只能从头到尾顺序的读写一次
IO类结构图
流的关闭顺序:
1.在使用流时从下到上关闭流(后进先关,先进后关)
缓冲流要注意:
请尽量使用缓冲流(Buffered)因为它在底层维护了一个byte[8192]的数组,一次性读取的数据变多了所以读取速度非常快,有高效效果
使用写到文件时(Out),记得最后要flush()刷新一下
因为java底层默认在未存满byte数组时会等待存满在一次性存到文件
Java序列化与反序列化
概述:
序列化的功能是指将对象信息转换为可以存储或传输形式的过程,对象将其当前对象成员信息写入到临时或持久性存储区以后可以通过从存储区中读取或者反序列化对象的状态,重新创建该对象。常用与服务器之间的数据传输,序列化成文件,反序列化读取数据
序列化:
把对象的信息,按照固定的格式转成一串字节值输出并持久保存到磁盘或传输
序列化文件保存的是 包名.类名+属性+属性值
反序列化
读取磁盘中之前序列化好的数据,或者套接字收到的数据进行重新恢复成对象
如何序列化与反序列化
需要使用的类
- 序列化类:ObjectOutputStream类
- 反序列化:ObjectInputStream类
- 注意:先出后进==先序列才能反序列
1.需要序列化类必须实现Serializable接口,作为标志着这个类可以被序列化,否则报NotSerializableException
2.private static final long serialVersionUID = 1201L;
- UID字编译时会自动生成一个UID密码
- 这里的值相当于序列化文件的钥匙,类中保存一份,文件中2进制保存一份
- 当反序列时钥匙相同才可以读取,否则报错InvalidClassException
知识点:
- 序列化的类文件必须实现Serializable接口,用来启用序列化功能
- 不需要序列化的数据可以修饰成static,因为static资源属于类资源,不随着对象被序列化输出
- 被序列化的文件都有一个唯一的uid,没有添加此id的类,编译器会自动根据类的定义信息计算产生一个
- 反序列化时,如果类的uid和序列化的uid版本号不一致,无法完成反序列化+报错
- 不想被序列话也可以变量修饰关键字transient(临时的),只在程序运行期间在内存中存在,不会被序列化持久保存
集合与Map集合
前言
集合的英文名称是Collection,是用来存放对象的数据结构,而且长度可变,可以存放不同类型的对象,并且还提供了一组操作成批对象的方法.Collection接口层次结构 中的根接口,接口不能直接使用,但是该接口提供了添加元素/删除元素/管理元素的父接口公共方法.
由于List接口与Set接口都继承了Collection接口,因此这些方法对于List集合和Set集合是通用的.
继承关系结构图
集合的特点
链表式的储存结构
Map存储思想
1.底层是一个Entry[ ]数组,当存放数据时,会根据hash算法来计算数据的存放位置
2.通过key键hash值取余数组的长度(集合的容量),得到的长度按下标存入数组
- 当计算的结果对应的数组下标没有数据的时候,会直接存放在一个Entry
- 当计算的位置,有数据时,会发生hash冲突/hash碰撞,解决的办法就是采用链表的结构,在数组中指定位置处已有元素之后插入新的元素,也就是说数组中的元素都是最早加入的节点。当链表的长度大于8时,会由链表的形式转换成红黑树
Map的两种迭代方式思维图
Map里是没有迭代器的,需要转换成Set集合,在迭代
进程和线程
进程的概念以及特点:
进程就是正在运行的程序,它代表了程序所占用的内存区域,是系统中独立存在的实体
进程有以下三大特点:
独立性:
- 进程是系统中独立存在的实体,拥有自己独立的资源,都拥有自己私有的地址空间
- 在没经进程本身允许的情况下,用户进程是不可以直接访问其他进程的地址空间的
动态性:
- 进程与程序的区别在于,程序只是一个静态的指令集合
- 进程是一个正在系统中活动的指令集合,程序加入了时间的概念后,称为进程,具有自己的生命周期和各种不同的状态,这些概念都是程序所不具备的.
并发性:
- 多个进程可以在单个处理器CPU上并发执行,多个进程之间不会互相影响.
进程的并发和高并发
高可用HA(High Availability):
在高并发的情景中,尽可能的保证程序的可用性,减少系统不能提供服务的时间
线程的概念
线程具有随机性,且只有一个程序在执行,执行结果是不可控的
宏观上觉得这些程序像是同时运行,但实际上从微观时间看是因为CPU在高效的切换着,这使得各个程序从表面上看是同时进行的,所以看着看似进程/线程都同时运行但是微观层面上,同一时刻,一个CPU只能处理一件事.切换的速度甚至是纳秒级别的,非常快
线程和进程的区别
线程是操作系统OS能够进行运算调度的最小单位,它被包含在进程之中,是进程中的实际运作单位.
一台电脑可以有多个进程,而一个进程里可以有多个线程(线程是最小单位)
每个线程在共享同一个进程中内存的同时,又有自己独立的内存空间。想使用线程技术,得先有进程,进程的创建是OS操作系统来创建的,一般都是C或者C++完成
多线程的特性
- 在同一时刻,一个CPU只能处理一件事,不过切换的速度甚至是纳秒级别的,非常快使的同一个进程可以同时并发处理多个任务,但是无法决定CPU执行线程的顺序,是随机性的,
- 一个进程只能有一个主线程,由主进程可以开启并调用多个线程
CPU时间片(分时调度)
CPU分配给各个线程的一个时间段,称作为它的时间片(操作系统控制权),该线程在被允许运行的时间如果在时间片用完时线程还在执行,那CPU将被剥夺并分配给另一个线程,将当前线程挂起,如果线程在时间片用完之前阻塞或结束,则CPU当即进行切换,从而避免CPU资源浪费,当再次切换到之前挂起的线程,恢复现场,继续执行。
注意:但是我们无法决定控制OS选择执行哪些线程,OS底层有自己规则!
- FCFS(First Come First Service 先来先服务算法)
- SJS(Short Job Service短服务算法)
FCFS调度算法
FCFS是最简单的调度算法,该算法既可用于作业调度,也可用于进程调度。当在作业调度中采用该算法时,系统将按照作业到达的先后次序来进行调度,或者说它是优先考虑在系统中等待时间最长的作业,而不管该作业所需执行的时间的长短,从后备作业队列中选择几个最先进入该队列的作业,将它们调入内存,为它们分配资源和创建进程。然后把它放入就绪队列。
总结:先来先服务直到此进程本次时间片用完(进程结束,阻塞不算)
SJS调度算法
在已经到达的进程中,选择所需运行时间最短的先执行。
1.将缓冲池中的进程信息根据服务时间进行排序
2.没选择缓冲池中所需服务时间最短的优先执行
线程的状态
线程的状态比较复杂,这里只简单介绍3种基础的状态模型
- 就绪:线程已经准备好运行,只要获得CPU,就可立即执行
- 执行:线程已经获得CPU,其程序正在运行的状态
- 阻塞:正在运行的线程由于某些事件(I/O请求等)暂时无法执行的状态,即线程执行阻塞
当就绪线程分配CPU即可变为执行状态
执行的线程由于时间片用完被剥夺CPU暂停执行,就变为就绪状态
如果发生某事件,使正在执行的线程受阻,无法执行,则由执行变为阻塞
三态模型
五态模型
- 创建:需要先申请PCB,然后为该线程运行分配必须的资源,并将该线程转为就绪状态插入到就绪队列中
- :等待OS进行善后处理,最后将PCB清零,并将PCB返回给系统
PCB是什么呢?
- 英文全名Process Control Block,为了保证参与并发执行的每个线程都能独立运行,OS配置了特有的数据结构PCB来描述线程的基本情况和活动过程,进而控制和管理线程
- 参加了线程状态与代码结合图
新建:当java在new Thread();时就变成了新建状态
就绪:线程对象的start()方法,线程即为进入就绪状态,随时等待CPU调度执行
运行:CPU调度了处于就绪状态的线程时,此线程才是真正的执行,即进入到运行状态(就绪状态是进入运行状态的唯一入口)
阻塞:运状态中的线程由于某种原因,暂时放弃对CPU的使用权停止执行,此时进入阻塞状态,直到其进入就绪状态才有机会被CPU选中再次执行,根据阻塞状态产生的原因不同,阻塞状态又可以细分成三种:
- 等待阻塞:运行状态中的线程执行wait()方法,本线程进入到等待阻塞状态
- 同步阻塞:线程在获取synchronized同步锁失败(因为锁被其他线程占用),它会进入同步阻塞状态
- 其他阻塞:调用线程的sleep()或者join()或发出了I/O请求时,线程会进入到阻塞状态.当sleep()状态超时.join()等待线程终止或者超时或者I/O处理完毕时线程重新转入就绪状态
死亡:(Dead):线程执行完了或者因异常退出了run()方法,该线程结束生命周期
Java线程类
开启线程有两种方式实现:
- 一个类继承Thread类,去重写Thread的run方法。缺点是一个类只能单继承,无法在继承其他父类。因为接口可以多实现,所以又出现了新的方式实现接口获得线程。
- Runnable接口只有一个run的抽象方法,可以把该接口实现类交给Thread线程对象调用start()方法开启线程
- 创建线程池,将执行的代码交给线程池统一管理,开启和关闭
线程的概念
线程是操作系统OS能够进行运算调度的最小单位,它被包含在进程之中,是进程中的实际运作单位.
一个进程可以开启多个线程,其中有一个主线程来调用本进程中的其他线程我们看到>的进程的切换,切换的也是不同进程的主线程
多线程扩展了多进程的概念,使的同一个进程可以同时并发处理多个任务
单个进程运行时数据的区域
多线程的安全
在多线程中,线程不安全会发生什么?
当多个线程操作一个变量值时,可能1个共享数据被多线程操作多次
甚至出现共享数据出现意外的值
多线程安全问题是如何出现的?
常见情况是由于 (线程的随机性) + (访问延迟)
如何判断程序有没有线程安全问题呢?
如果同时满足以下3点就可能存在线程安全
- 在多线程程序中 + 有共享数据 + 多条语句操作共享数据
这里首先要知道什么是 同步与异步
同步:
- 优点:体现了排队的效果,同一时刻只能有一个线程独占资源,其他没有权利的线程排队。
- 缺点:效率会降低,不过保证了安全。
异步:
- 优点:体现了多线程抢占资源的效果,线程间互相不等待,互相抢占资源。
- 有安全隐患,效率要高一些。
java通过"synchronized"关键字来同步线程,那么如何定义呢(同步锁)?
同步锁:
- 给容易出现问题的代码加了一把锁,包裹了所有可能出现安全隐患的代码
- 枷锁后,就有同步(排队),但是枷锁的范围,需要考虑
- 范围太大,运行效率太低; 范围太小:可能出现锁不住的情况共享数据的代码
synchronized同步关键字:
- 可以用来修饰方法,称为同步方法,使用的锁对象是this,
- 可以用来修饰代码块,称为同步代码块,使用的锁对象可以任意
- 但要注意使用枷锁对象必须是唯一的
synchronized同步的缺点:
- 缺点是会降低程序的执行效率,但我们为了保证线程的安全,有些性能是必须要牺牲的
- 但是为了性能,加锁的范围需要控制好,比如我们不需要给整个商场加锁,试衣间加锁就可以了
//这种方式一般用于写在run方法中
synchronized (this) {// this是锁的对象,当前类即可;或者任何一个对象都可以比如:Object
// 这是操作共享数据的修改放在这里
// 当一个线程在这个代码块运行,synchronized 会自动加锁,其他线程想进来必须排队
}
//方法定义锁,可以在线程的run方法调用
public synchronized void lock() {
//与上面的功能有相同的作用
}
使用线程锁的时候,有时共享数据还会不完全
- 加锁的范围小了,还有可能加锁代码块之外还有,操作共享数据的代码
- 加锁的对象必须是同一个索引指向堆中的对象,否则加锁是无效的
- 如果上述都没有问题,就要考虑自己代码的逻辑了
线程池
概念:
用来存储线程的池子,把新建线程/启动线程/关闭线程的任务都交给池来管理
通过BlockingQueue类来实现县城的阻塞式队列(也是java中的阻塞式队列)
操作的类:
ExecutorServiced:用来存储线程的池子,把新建线程、启动线程、关闭线程的任务都交给池来管理
Executors:用来创建线程池对象的工具
ExecutorService pool = Executors.newFixedThreadPool(4);//最多n个线程的线程池
ExecutorService pool = Executors.newCachedThreadPool();//足够多的线程,使任务不必等待
ExecutorService pool = Executors.newSingleThreadExecutor();//只有一个线程的线程池
pool.execute(Runnable任务对象);//把任务丢到线程池
pool.shutdown();//关闭线程池,不关闭程序会一直等待加入线程运行
线程的延伸
java中线程锁也有很多种,不止上述的一种,以下举例,待深入研究
悲观锁:悲观锁认为竞争总是会发生,因此每次对某资源进行操作时,都会持有一个独占的锁,就像synchronized,不管三七二十一,直接上了锁就操作资源了。
乐观锁:乐观锁认为竞争不总是会发生,因此它不需要持有锁,将”比较-替换”这两个动作作为一个原子操作尝试去修改内存中的变量,如果失败则表示发生冲突,那么就应该有相应的重试逻辑。
//悲观锁,有罪假设(常见)
互斥锁:采用synchronized修饰符实现的同步机制叫做互斥锁机制,它所获得的锁叫做互斥锁。每个对象都有一个monitor(锁标记),当线程拥有这个锁标记时才能访问这个资源,没有锁标记便进入锁池。任何一个对象系统都会为其创建一个互斥锁,这个锁是为了分配给线程的,防止打断原子操作。每个对象的锁只能分配给一个线程,因此叫做互斥锁。
排他锁:ReentrantLock是排他锁,排他锁在同一时刻仅有一个线程可以进行访问,实际上独占锁是一种相对比较保守的锁策略,在这种情况下任何“读/读”、“读/写”、“写/写”操作都不能同时发生,这在一定程度上降低了吞吐量。然而读操作之间不存在数据竞争问题,如果”读/读”操作能够以共享锁的方式进行,那会进一步提升性能。
//乐观锁,无罪假设(常见)
ReentrantReadWriteLock,顾名思义,ReentrantReadWriteLock是Reentrant(可重入)Read(读)Write(写)Lock(锁),我们下面称它为读写锁。读写锁内部又分为读锁和写锁,读锁可以在没有写锁的时候被多个线程同时持有,写锁是独占的。读锁和写锁分离从而提升程序性能,读写锁主要应用于读多写少的场景。
举例
//定义可重入读写锁对象,静态保证全局唯一
static ReentrantReadWriteLock lock = new ReentrantReadWriteLock(true);
lock.writeLock().lock();//在操作共享资源前上锁
lock.writeLock().unlock();//注意一定要手动释放,且要做finally中解锁,防止死锁,否则就独占报错了
两种方式的区别
sychronized修饰的方法或者语句块在代码执行完之后锁会自动释放, 而用Lock需要我们手动释放锁,所以为了保证锁最终被释放(发生异常情况),要把互斥区放在try内,释放锁放在finally内!
与互斥锁相比,读-写锁允许对共享数据进行更高级别的并发访问。虽然一次只有一个线程(writer 线程)可以修改共享数据,但在许多情况下,任何数量的线程可以同时读取共享数据(reader 线程)从理论上讲,与互斥锁定相比,使用读-写锁允许的并发性增强将带来更大的性能提高。