2.1 输入输出流
流可以被看作一组有序的字节集合,即数据在两个设备间的传输。
字节流:以字节作为单位,读到一个字节就返回一个字节;InputStream & OutputStream。
字符流:使用字节流读到一个到多个字节先查询码表再返回;Reader & Writer。会使用缓存。
Java IO 类设计时采用了 Decorator 模式(装饰者)
2.1 补充 - 装饰者模式
装饰者模式的组成部分:
-
Component(抽象组件):定义了一个接口,描述了可以动态添加的责任。
-
ConcreteComponent(具体组件):定义了Component接口的具体实现。
-
Decorator(抽象装饰者):抽象类,实现了Component接口,并持有Component接口的一个实例。
-
ConcreteDecorator(具体装饰者):具体装饰者类,实现Decorator的抽象方法,并添加额外的功能。
装饰者模式的实现步骤:
-
定义组件接口(Component),它有一个方法,比如
operation()
。 -
创建具体组件类(ConcreteComponent),实现Component接口。
-
创建装饰者抽象类(Decorator),实现Component接口,并包含一个Component接口的引用。
-
实现具体装饰者类(ConcreteDecorator),继承Decorator类,并添加额外的功能。
-
通过组合,Decorator可以动态地给Component添加功能。
装饰者模式的好处主要包括:
-
动态扩展性:可以在运行时动态地给一个对象添加额外的职责,而不需要修改原有的代码结构。
-
灵活性:装饰者模式提供了一种灵活的替代方案,用于继承,可以基于需要向对象添加任意数量的职责。
-
低耦合性:装饰者模式允许系统在对象间保持较低的耦合度,因为对象不需要知道它是由哪些装饰者组成的。
-
可维护性:当需要添加新的功能时,可以简单地创建新的装饰者类,而不是修改现有的类,这符合开闭原则(对扩展开放,对修改封闭)。
-
责任分离:装饰者模式有助于将类的不同职责分离开来,使得各个职责可以独立地变化和扩展。
使用装饰者模式而不是继承的理由:
-
避免类的爆炸式增长:如果使用继承来扩展功能,每个类的新组合都会产生一个新的子类,这可能导致类的数量急剧增加。
-
减少继承的缺陷:继承是一种静态的、静态绑定的关系,它限制了灵活性。装饰者模式使用组合和动态绑定,提供了更大的灵活性。
-
继承是强耦合的:继承关系使得基类和子类之间存在强耦合,基类的任何变化都可能影响到子类。装饰者模式通过组合来实现,耦合度较低。
-
继承层次结构可能很复杂:随着功能的增加,基于继承的层次结构可能变得复杂且难以管理。装饰者模式提供了一种更扁平化和灵活的结构。
-
多重继承问题:Java不支持多重类继承,但可以有多多个接口。如果需要实现多个不相关的功能扩展,继承可能无法满足需求,装饰者模式可以解决这个问题。
总之,装饰者模式提供了一种更加灵活和动态的方式来扩展对象的功能,同时避免了继承可能带来的问题和限制。
2.1 Java Socket \ TCP & UDP
Socket 由IP地址和端口号唯一确定。
面向链接的 Socket (TCP)
面向无连接的 Socket (UDP)
TCP(传输控制协议)
-
连接导向:TCP 需要在数据传输之前建立连接,通过三次握手过程。
-
可靠的:TCP 提供可靠的数据传输服务,确保数据包正确、按顺序地到达目的地。
-
面向字节流:TCP 没有消息边界,它将数据视为字节流。
-
错误恢复:TCP 有错误检测和重传机制,如果数据包丢失或损坏,TCP 会重新发送它们。
-
拥塞控制:TCP 有拥塞控制机制,可以在网络拥塞时减慢数据传输速度。
-
有序传输:TCP 保证数据包的顺序传输,如果出现乱序,接收方会缓存数据直到可以按顺序重组。
-
带宽消耗:由于 TCP 的可靠性和控制机制,它可能会消耗更多的带宽。
UDP(用户数据报协议)
-
无连接:UDP 是无连接的,它在数据传输前不需要建立连接。
-
不可靠的:UDP 不保证数据包的到达、顺序或完整性,它只是尽可能快地发送数据。
-
面向消息:UDP 面向消息,发送的数据被分割成数据报,每个数据报都是独立的。
-
错误检测有限:UDP 只提供了最基本的错误检测,不负责重传丢失的数据包。
-
无拥塞控制:UDP 没有拥塞控制,即使网络拥堵,它也会继续以全速发送数据。
-
有序性不保证:UDP 不保证数据包的顺序,如果数据包乱序到达,接收方需要自己处理。
-
带宽消耗较少:UDP 由于其简单性,通常消耗较少的带宽。
TCP(传输控制协议)的三次握手是建立一个可靠的连接所必须的过程。这个过程确保了两个端点(客户端和服务器)都能够接收和发送数据。以下是三次握手的步骤:
-
SYN(同步序列编号):
- 客户端选择一个初始序列号(ISN,Initial Sequence Number)并发送一个带有 SYN 标志位设置为 1 的数据包给服务器,以请求建立连接。这表示客户端准备好发送数据了。
-
SYN-ACK(同步-确认):
- 服务器接收到客户端的 SYN 数据包后,会用自己的初始序列号响应一个 SYN-ACK 数据包。这个响应中 SYN 标志位和 ACK(确认)标志位都被设置为 1。服务器的序列号是它选择的 ISN,而 ACK 值是客户端的 ISN 加 1。
-
ACK(确认):
- 客户端接收到服务器的 SYN-ACK 数据包后,会发送一个带有 ACK 标志位设置为 1 的数据包给服务器,以完成连接建立。客户端的 ACK 值是服务器的 ISN 加 1。此时,连接建立完成,客户端和服务器都可以开始发送数据。
2.1 Java 序列化
- 序列化:实现序列化的类需要实现Serializable 接口(标记接口),调用方法为:使用一个输出流构造一个 ObjectOutputStream 对象,使用其 writeObject 方法来写出对象。被声明为 static 和 transient 的数据成员不可以被序列化。
-
对于 static 数据成员:
- 由于
static
字段是类级别的,通常不需要序列化。如果你需要保存类的状态,可以考虑将static
字段的值存储在某个地方,然后在对象反序列化后恢复它们。 - 例如,你可以使用一个静态方法来获取和设置
static
字段的值,然后在序列化和反序列化过程中手动调用这个方法。
- 由于
-
对于 transient 数据成员:
- 如果你需要在反序列化后恢复
transient
字段的状态,你可以在反序列化过程中显式地重新赋值。 - 一种常见的做法是在类的构造函数或一个单独的初始化方法中重新设置
transient
字段的值。
- 如果你需要在反序列化后恢复
- 外部序列化:自定义读写接口
2.2 同步 \ 异步 \ 阻塞 \ 非阻塞
多线程语境下:
- 同步 & 异步:关注任务是否可以被多个线程同时调用,同步是仅可以被一个线程访问。
- 阻塞 & 非阻塞:关注线程的状态,阻塞代表线程挂起。
IO语境下:
- 同步 & 异步:关注消息发起和接受的机制,同步是发起一个IO操作后得到返回才进行后续操作,异步是指发起IO操作后不等待返回。通过轮询、回调等方式等待结果。
- 阻塞 & 非阻塞:关注等待结果的状态:阻塞指需要等待IO操作结束。
并发 & 并行:并发在同一时刻只有一条指令执行;并行同一时刻多条指令同时执行。
2.3 BIO \ NIO \ AIO
BIO :阻塞式IO;
NIO:基于Selector 的异步网络 IO( Selector 轮询所有被注册的 channel ,一旦发现 Channel 上被注册的事件发生就可以进行处理)
AIO:基于 Proactor 实现基于事件和回调机制的 I/O 操作方式,允许应用程序在执行 I/O 操作时不被阻塞,从而可以处理其他任务。
1. 每个 socket 链接在事件分离器注册IO完成事件和回调处理;
2. 应用程序需要进行IO时,向分离器发出IO请求,分离器通知系统处理;
3. 系统尝试IO操作,完成后通知分离器;
4. 分离器检测到IO完成事件后,激活回调。
2.3 补充 Channel
在 Java NIO(New Input/Output)库中,"Channel" 是指可以用于执行 I/O 操作的通道。Java NIO 中的通道类似于传统的"流",但有一些重要的区别:
- 通道可以非阻塞,允许单线程处理多个输入/输出通道。
- 通道总是基于缓冲区的,数据从通道读取到缓冲区,或从缓冲区写入到通道。
3.1 Collections
Java 中的容器可以分为两类
Collection:存储独立的元素,包括
- List:按插入顺序保存元素;eg:LinkedList & ArrayList & Vector;
- Set:不可有重复元素,通过equals 方法来保证唯一;eg:HashSet & TreeSet;
- Queue: 队列
- Stack:堆栈
Map:存储键值对;eg: HashMap、TreeMap、LinkedHashMap;
3.2 LinkedList & ArrayList & Vector
ArrayList: 数组实现。读取快,扩容慢。
LinkedList:双向链表;非线程安全。注意:Java标准库并没有直接提供一个现成的线程安全的双向链表实现。
Vector:与 ArrayList 相比是线程安全的。
3.3 Map
HashMap: 键值与下标的关系由 hashcode 决定,即 hash 桶。仅当 hashcode 和 equals 相同才被认为是一个对象。
Java 8 之前的实现是数组加链表
Java 8 之后:采用了数组+树+链表的结构,当链表达到最大深度时,重构为红黑树。
TreeMap:完全由红黑树实现,元素有序。
LinkedHashMap:
Java 8 之前:为每个数据节点的引用多维护了一份链表。
Java 8 中
HashTable : hashtable 为线程安全的,不可存储 null 值;
WeakHashTable: key值如果没有外部强引用,垃圾回收时,对应内容也会被移除掉。
ConcurrentHashMap: HashMap 中支持高并发、高吞吐的线程安全版本。包含一个 Segment 数组,结构和 HashMap类似,每个Segment 守护着一个 HashEntry 里的元素,对 HashEntry 数组进行修改时需要先获得 Segment 锁。注意,在某些情况下还是存在线程不安全的可能,例如 map.pub 方法不是一个原子操作。所以在进行操作时最好在线程里对操作加锁。
3.4 Set
HashSet:HashSet 内部通过 HashMap 实现,所有的值使用相同的 value。同样,其不是线程安全的。
LinkedHashSet:可维护插入数据的顺序。底层是 LinedHashMap。
TreeSet:底层使用 TreeMap 来存储数据
3.5 BlockingQueue
生产者线程在仓库装满之后会被阻塞,消费者线程则实在仓库清空后阻塞。
ArrayBlockingQueue:基于数组实现的有界 BlockingQueue,陷入先出。线程安全。
LinkedBlockingQueue:使用了双锁队列算法。线程安全。
PriorityBlockingQueue:队头元素是队列的最小元素。使用的最小堆结构。
ConcurrentLinkedQueue: 非阻塞的线程安全队列。采用的CAS 方式保证。
DelayQueue:阻塞的优先队列,管理的对象必须要实现util.concurrent.delayed接口,其线程安全由重入锁实现。
3.5 - 补充 CAS 算法
CAS(Compare-And-Swap,比较并交换)算法是一种用于并发控制的技术,主要用于多处理器系统中实现原子操作。CAS操作通常由三个参数组成:内存位置(V),预期值(A)和新值(B)。基本思想是,如果内存位置的当前值与预期值相匹配,那么将内存位置的值更新为新值。如果不相匹配,操作则不执行任何操作或回滚。
3.7 迭代器
使用 Iterator 遍历容器时,如果对容器增加或者删除操作操作就会改变容器数量,导致抛出异常。解决方法:使用线程安全的容器来做迭代器。eg: ConcurrentHashMap 等。
3.8 并行数组操作
例如 parallexXXX 方法,使用多线程进行操作。