文章目录
序列化和反序列化
- 概念
- 序列化:把Java对象转换为字节序列的过程
- 反序列化:把字节序列恢复为Java对象的过程
- 用途
- 把对象的字节序列永久地保存到硬盘上,通常存放在一个文件中;(持久化对象)
- 在网络上传送对象的字节序列(网络传输对象)
- 使用
- 只要一个类实现了java.io.Serializable 接口,那么它就可以被序列化
- 这个对象的所有属性(包括private属性、包括其引用的对象)都可以被序列化和反序列化来保存、传递。不想序列化的字段可以使用transient修饰。
- 由于Serializable对象完全以它存储的二进制位为基础来构造,因此并不会调用任何构造函数,因此Serializable类无需默认构造函数,但是当Serializable类的父类没有实现Serializable接口时,反序列化过程会调用父类的默认构造函数,因此该父类必需有默认构造函数,否则会抛异常
- 使用transient关键字阻止序列化虽然简单方便,但被它修饰的属性被完全隔离在序列化机制之外,导致了在反序列化时无法获取该属性的值,而通过在需要序列化的对象的Java类里加入writeObject()方法与readObject()方法可以控制如何序列化各属性,甚至完全不序列化某些属性或者加密序列化某些属性。
- ObjectOutputStream 和 ObjectInputStream 对对象进行序列化及反序列化
-
序列化版本
虚拟机是否允许反序列化,不仅取决于类路径和功能代码是否一致,一个非常重要的一点是两个类的序列化 ID 是否一致(就是 private static final long serialVersionUID) -
其他
序列化并不保存静态变量
序列化子父类说明
要想将父类对象也序列化,就需要让父类也实现 Serializable 接口。
youngGC和FullGC触发条件是什么?
- youngGC触发条件
大多数情况下,对象直接在年轻代中的Eden区进行分配,如果Eden区域没有足够的空间,那么就会触发YGC(Minor GC) - FullGC触发条件
(1)老年代的内存使用率达到了一定阈值(可通过参数调整),直接触发FGC
(2)空间分配担保:在YGC之前,会先检查老年代最大可用的连续空间是否大于新生代所有对象的总空间。如果小于,说明YGC是不安全的,则会查看参数 HandlePromotionFailure 是否被设置成了允许担保失败,如果不允许则直接触发Full GC;如果允许,那么会进一步检查老年代最大可用的连续空间是否大于历次晋升到老年代对象的平均大小,如果小于也会触发 Full GC
(3)Metaspace(元空间)在空间不足时会进行扩容,当扩容到了-XX:MetaspaceSize 参数的指定值时,也会触发FGC
(4)System.gc() 或者Runtime.gc() 被显式调用时,触发FGC - 进入老年代的途径
(1)YGC时,To Survivor区不足以存放存活的对象,对象会直接进入到老年代。
(2)经过多次YGC后,如果存活对象的年龄达到了设定阈值,则会晋升到老年代中。
(3)动态年龄判定规则,To Survivor区中相同年龄的对象,如果其大小之和占到了 To Survivor区一半以上的空间,那么大于此年龄的对象会直接进入老年代,而不需要达到默认的分代年龄。
(4)大对象:由-XX:PretenureSizeThreshold启动参数控制,若对象大小大于此值,就会绕过新生代, 直接在老年代中分配。
创建线程有哪几种方式
https://www.cnblogs.com/nongzihong/p/10512566.html
- 继承Thread类创建线程
(1)定义Thread类的子类,并重写该类的run方法,该run方法的方法体就代表了线程要完成的任务。因此把run()方法称为执行体。
(2)创建Thread子类的实例,即创建了线程对象。
(3)调用线程对象的start()方法来启动该线程。 - 实现Runnable接口
(1)定义runnable接口的实现类,并重写该接口的run()方法,该run()方法的方法体同样是该线程的线程执行体。
(2)创建 Runnable实现类的实例,并依此实例作为Thread的target来创建Thread对象,该Thread对象才是真正的线程对象。
(3)调用线程对象的start()方法来启动该线程。 - 通过Callable和Future创建
(1)创建Callable接口的实现类,并实现call()方法,该call()方法将作为线程执行体,并且有返回值。
(2)创建Callable实现类的实例,使用FutureTask类来包装Callable对象,该FutureTask对象封装了该Callable对象的call()方法的返回值。
(3)使用FutureTask对象作为Thread对象的target创建并启动新线程。
(4)调用FutureTask对象的get()方法来获得子线程执行结束后的返回值 - 基于线程池的execute(), 创建临时线程
(1)创建线程池
(2)调用线程池的execute()方法
(3)采用匿名内部类的方法,创建Runnable对象,并重写run()方法
开发当中遇到的运行时异常有哪些?
常遇到的几个:
- java.lang.NullPointerException
这个异常的解释是 "程序遇上了空指针 ",简单地说就是调用了未经初始化的对象或者是不存在的对象。 - java.lang.ClassNotFoundException
异常的解释是"指定的类不存在",这里主要考虑一下类的名称和路径是否正确即可 - java.lang.ArrayIndexOutOfBoundsException
这个异常的解释是"数组下标越界",现在程序中大多都有对数组的操作,因此在调用数组的时候一定要认真检查,看自己调用的下标是不是超出了数组的范围 - ClassCastException - 类型转换异常
- ArrayStoreException(数据存储异常,操作数组时类型不一致)
- IO操作的BufferOverflowException异常
线程不安全的解决方法
- 不可变 final类,使数据只能读,不能修改
- 互斥同步(悲观锁)synchronized
- 非阻塞同步(乐观锁)CAS
Java中的锁机制
- 锁的一种宏观分类方式
- 锁的一种宏观分类方式是悲观锁和乐观锁。悲观锁与乐观锁并不是特指某个锁(Java中没有哪个Lock实现类就叫PessimisticLock或OptimisticLock),而是在并发情况下的两种不同策略。
- 悲观锁认为每次拿数据的时候都会被别人修改,所以每次拿数据的时候都会上锁,即阻塞事务
- 乐观锁每次去拿数据的时候都认为别人不会修改。所以不会上锁,不会上锁!但是如果想要更新数据,则会在更新前检查在读取至更新这段时间别人有没有修改过这个数据。如果修改过,则重新读取,再次尝试更新,循环上述步骤直到更新成功,即回滚重试
- 乐观锁的基础——CAS算法
- Compare-and-Swap,即比较并替换,也有叫做Compare-and-Set的,比较并设置。
(1)比较:读取到了一个值A,在将其更新为B之前,检查原值是否仍为A(未被其他线程改动)。
(2)设置:如果是,将A更新为B,结束。[1]如果不是,则什么都不做。上面的两步操作是原子性的,可以简单地理解为瞬间完成,在CPU看来就是一步操作。
- 其他锁的介绍(有点多,暂时略过)
写下synchronized关键字锁升级的过程:
(1)偏向锁:初次执行到synchronized代码块的时候,锁对象变成偏向锁
(通过CAS修改对象头里的锁标志位),字面意思是“偏向于第一个获得它的线程”的锁。执行完同步代码块后,线程并不会主动释放偏向锁。
(2)自旋锁: 一旦有第二个线程加入锁竞争,偏向锁就升级为轻量级锁(自旋锁)。
只有当某线程尝试获取锁的时候,发现该锁已经被占用,只能等待其释放,这才发生了锁竞争。在轻量级锁状态下继续锁竞争,没有抢到锁的线程将自旋,即不停地循环判断锁是否能够被成功获取。
获取锁的操作,其实就是通过CAS修改对象头里的锁标志位。先比较当前锁标志位是否为“释放”,如果是则将其设置为“锁定”,比较并设置是原子性发生的。
(3)重量级锁:
长时间的自旋操作是非常消耗资源的,一个线程持有锁,其他线程就只能在原地空耗CPU,执行不了任何有效的任务,这种现象叫做忙等(busy-waiting)。
此忙等是有限度的(有个计数器记录自旋次数,默认允许循环10次,可以通过虚拟机参数更改)。如果锁竞争情况严重,某个达到最大自旋次数的线程,会将轻量级锁升级为重量级锁(依然是CAS修改锁标志位,但不修改持有锁的线程ID)。
当后续线程尝试获取锁时,发现被占用的锁是重量级锁,则直接将自己挂起(而不是忙等),等待将来被唤醒。
- 我们在Java里使用的各种锁,几乎全都是悲观锁。synchronized从偏向锁、轻量级锁到重量级锁,全是悲观锁。JDK提供的Lock实现类全是悲观锁。其实只要有“锁对象”出现,那么就一定是悲观锁。因为乐观锁不是锁,而是一个在循环里尝试CAS的算法。
那JDK并发包里到底有没有乐观锁呢?
有。java.util.concurrent.atomic包里面的原子类都是利用乐观锁实现的。
HashMap相关
- HashMap的数据结构是什么样的?
- 为什么新版本会引入红黑树?
- 为什么不直接使用红黑树?
因为红黑树需要进行左旋,右旋操作, 而单链表不需要,以下都是单链表与红黑树结构对比。如果元素小于8个,查询成本高,新增成本低如果元素大于8个,查询成本低,新增成本高
- HashMap是线程安全的吗?不是
- ConcurrentHashMap的数据结构有什么区别?
红黑树和平衡二叉树AVL的区别?二者的效率?
- 平衡树是为了解决二叉查找树退化为链表的情况,而红黑树是为了解决平衡树在插入、删除等操作需要频繁调整的情况
- 红黑树具有如下特点:
- 具有二叉查找树的特点
- 根节点是黑色的
- 每个叶子节点都是黑色的空节点(NULL),也就是说,叶子节点不存数据
- 任何相邻的节点都不能同时为红色,也就是说,红色节点是被黑色节点隔开的
- 任一节点到其每个叶子节点的简单路径,都包含相同数目的黑色节点。
- 正是由于红黑树的这种特点,使得它能够在最坏情况下,也能在 O(logn) 的时间复杂度查找到某个节点。
操作系统相关
-
进程调度的方式?调度算法
(1)先来先服务:这是最简单的方式,即按照作业提交或进程变更为就绪态的次序分配CPU。这种调度方式明显利于需要长作业的情况,但不利于需要频繁中断的作业。主要用于宏观调度。
(2)时间片轮转:时间片轮转即每个进程都运行一定时间,它主要用于微观调度,目的是提升资源的利用率。时间片的长度可以从几毫秒到数百毫秒不等,又有固定时间片和可变时间片两种。
(3)优先级调度:
这种方式要求进程都有一个优先级,系统调度时总选择优先级更高的进程占用CPU,优先级的分配有两种方式:静态优先级——在创建时就确定进程优先级,直至进程终止优先级也不会发生变化,通常根据三种因素决定:进程类型、资源需求、用户要求。
动态优先级——在创建时也赋予一个优先级,但运行过程中可被改变,以便调度更合理。如在就绪队列中等待越长则优先级将提高,而进程被执行一个时间片后则降低。
(4)多级反馈调度:它是时间片轮转及优先级的综合。优点有三:照顾短进程提高系统吞吐量、缩短平均轮转时间;照顾I/O需求较高的进程,提升I/O设备利用率和缩短响应时间;无需估计进程执行耗时,动态调整优先级。
设计模式
-
都有哪些设计模式(简单记录两种)
(1)单例模式:在它的核心结构中只包含一个被称为单例类的特殊类。通过单例模式可以保证系统中一个类只有一个实例而且该实例易于外界访问,从而方便对实例个数的控制并节约系统资源。
应用场景:如果希望在系统中某个类的对象只能存在一个,单例模式是最好的解决方案。
(2)工厂模式:
工厂模式主要是为创建对象提供了接口。应用场景如下:a、 在编码时不能预见需要创建哪种类的实例。b、 系统不应依赖于产品类实例如何被创建、组合和表达的细节。 -
单例模式的懒汉式和饿汉式?
了解哪些IO模型?
https://zhuanlan.zhihu.com/p/115912936
0. 通信过程中的缓冲区起到什么作用?
- 先结束的进程可以把结果放入缓冲区内,进行下面的工作,而后做完的进程可以从缓冲区内取出原来的数据继续工作。
- :在高速和低速设备之间起一个速度平滑作用;暂时存储数据;
- 经常访问的数据可以放进缓冲区,减少对慢速设备的访问以提高系统的效率。
- 阻塞IO模型
- 术语:在应用调用recvfrom读取数据时,其系统调用直到数据包到达且被复制到应用缓冲区中或者发送错误时才返回,在此期间一直会等待,进程从调用到返回这段时间内都是被阻塞的称为阻塞IO;
- 人话:所谓阻塞IO就是当应用B发起读取数据申请时,在内核数据没有准备好之前,应用B会一直处于等待数据状态,直到内核把数据准备好了交给应用B才结束。
- 非阻塞IO模型
- 术语:非阻塞IO是在应用调用recvfrom读取数据时,如果该缓冲区没有数据的话,就会直接返回一个EWOULDBLOCK错误,不会让应用一直等待中。在没有数据的时候会即刻返回错误标识,那也意味着如果应用要读取数据就需要不断的调用recvfrom请求,直到读取到它数据要的数据为止。
- 人话:所谓非阻塞IO就是当应用B发起读取数据申请时,如果内核数据没有准备好会即刻告诉应用B,不会让B在这里等待。
- IO复用模型
用于多线程和并发情况下,并发情况下服务器很可能一瞬间会收到几十上百万的请求,这种情况下应用B就需要创建几十上百万的线程去读取数据,同时又因为应用线程是不知道什么时候会有数据读取,为了保证消息能及时读取到,那么这些线程自己必须不断的向内核发送recvfrom 请求来读取数据,极大地浪费了操作系统的资源。
所以用一个线程监控多个网络请求(后续称为fd文件描述符)这样只需要一个或几个线程就可以完成数据状态询问的操作,当有数据准备就绪之后再分配对应的线程去读取数据,这样可以节省出大量的线程资源出来。
- 信号驱动IO模型
复用IO模型解决了一个线程可以监控多个fd的问题,但是select是采用轮询的方式来监控多个fd的,通过不断的轮询fd的可读状态来知道是否就可读的数据,而无脑的轮询就显得有点暴力,因为大部分情况下的轮询都是无效的,所以有人就想,能不能不要我总是去问你是否数据准备就绪,能不能我发出请求后等你数据准备好了就通知我,所以就衍生了信号驱动IO模型。
- 异步IO模型
术语描述:应用告知内核启动某个操作,并让内核在整个操作完成之后,通知应用,这种模型与信号驱动模型的主要区别在于,信号驱动IO只是由内核通知我们合适可以开始下一个IO操作,而异步IO模型是由内核通知我们操作什么时候完成。
相当于去掉了信号驱动IO模型中的select线程
- 总结
- 我们通常会说到同步阻塞IO、同步非阻塞IO,异步IO几种术语,通过上面的内容,那么我想你现在肯定已经理解了什么是阻塞什么是非阻塞了,所谓阻塞就是发起读取数据请求的时,当数据还没准备就绪的时候,这时请求是即刻返回,还是在这里等待数据的就绪,如果需要等待的话就是阻塞,反之如果即刻返回就是非阻塞。
- 我们区分了阻塞和非阻塞后再来分别下同步和异步,在IO模型里面如果请求方从发起请求到数据最后完成的这一段过程中都需要自己参与,那么这种我们称为同步请求;反之,如果应用发送完指令后就不再参与过程了,只需要等待最终完成结果的通知,那么这就属于异步。
- 我们再看同步阻塞、同步非阻塞,他们不同的只是发起读取请求的时候一个请求阻塞,一个请求不阻塞,但是相同的是,他们都需要应用自己监控整个数据完成的过程。而为什么只有异步非阻塞 而没有异步阻塞呢,因为异步模型下请求指定发送完后就即刻返回了,没有任何后续流程了,所以它注定不会阻塞,所以也就只会有异步非阻塞模型了。
Java中线程的状态以及转换方法
Java中线程的状态分为6种。
- 初始(NEW):新创建了一个线程对象,但还没有调用start()方法。
- 运行(RUNNABLE):Java线程中将就绪(ready)和运行中(running)两种状态笼统的称为“运行”。
线程对象创建后,其他线程(比如main线程)调用了该对象的start()方法。该状态的线程位于可运行线程池中,等待被线程调度选中,获取CPU的使用权,此时处于就绪状态(ready)。就绪状态的线程在获得CPU时间片后变为运行中状态(running)。- 阻塞(BLOCKED):表示线程阻塞于锁。
- 等待(WAITING):进入该状态的线程需要等待其他线程做出一些特定动作(通知或中断)。
- 超时等待(TIMED_WAITING):该状态不同于WAITING,它可以在指定的时间后自行返回。
- 终止(TERMINATED):表示该线程已经执行完毕。