java架构复习整理笔记

180 阅读18分钟

2PC 两阶段提交(XA事务,阻塞) 第一个阶段: 发出“准备”命令,所有事务参与者接受指令后进行资源准备,锁准备,undo log准备。如果都返回“准备成功”,如果不能执行,返回终止。 第二个阶段 协调者接受到第一个阶段的回复 如果都是ok,则发出“提交”命令,所有参与者进行commit操作。如果都成功,则事务结束,如果有失败情况,协调者发出“回滚”命令,所有事务参与者利用undo log进行回滚(这个在2PC不存在)。J2EE对JTA就是两阶段提交的实现。 如果有不ok,则发出撤销,所有事物撤销本地资源的锁定等操作

TCC TCC是对二阶段的一个改进,try阶段通过预留资源的方式避免了同步阻塞资源的情况,但是TCC编程需要业务自己实现try,confirm,cancle方法,对业务入侵太大,实现起来也比较复杂。

分布式事务的基本原理本质上都是两阶段提交协议(2PC),TCC (try-confirm-cancel)其实也是一种 2PC,只不过 TCC 规定了在服务层面实现的具体细节,即参与分布式事务的服务方和调用方至少要实现三个方法:try 方法、confirm 方法、cancel 方法。

Saga Saga模式是现实中可行的方案,采用事务补偿机制。每个本地事务都存储一个副本,如果出现失败,则利用补偿机制回滚。

最终一致性(BASE理论)

Paxos

Raft

mysql 索引:

  1. 普通索引
  2. 唯一索引
  3. 单列、多列索引
  4. 组合索引(最左前缀)

JVM : 垃圾回收机制顺序: 1.新生代 Serial (第一代) PraNew (第二代) Parallel Scavenge (第三代) G1收集器(第四代) 2.老年代 Serial Old (第一代) Parallel Old (第二代) CMS (第三代) G1收集器 (第四代)

G1之前的JVM内存模型 新生代:伊甸园区(eden space) + 2个幸存区 老年代:持久代(perm space):JDK1.8之前 元空间(metaspace):JDK1.8之后取代持久代

配置参数:

G1 : G1模糊了内存分代概念,但是也保留了年轻代和老年代。所以G1没有Full GC。Fully young gc和Mixed gc. G1收集器收集范围是老年代和新生代。不需要结合其他收集器使用

CMS: CMS收集器是老年代的收集器,可以配合新生代的Serial和ParNew收集器一起使用

区别一: 使用范围不一样 CMS收集器是老年代的收集器,可以配合新生代的Serial和ParNew收集器一起使用 G1收集器收集范围是老年代和新生代。不需要结合其他收集器使用

区别二: STW的时间 CMS收集器以最小的停顿时间为目标的收集器。 G1收集器可预测垃圾回收的停顿时间(建立可预测的停顿时间模型)

区别三: 垃圾碎片 CMS收集器是使用“标记-清除”算法进行的垃圾回收,容易产生内存碎片 G1收集器使用的是“标记-整理”算法,进行了空间整合,降低了内存空间碎片。

区别四: 垃圾回收的过程不一样 CMS收集器                      G1收集器

  1. 初始标记                   1.初始标记

  2. 并发标记                   2. 并发标记

  3. 重新标记                   3. 最终标记

  4. 并发清楚                   4. 筛选回收

-----------------------------------------------io------------------------------------------------------

bio nio(同步非阻塞) aio(异步非阻塞)

bio: 同步阻塞的实现,代码如下可以看到代码和简单,这也是bio的一个优势,实现简单,但是性能是很低的,作为server端如果有大量链接进来那么,每个read write都是阻塞的一个连接的请求处理完之前,下一个连接就必须等待 nio: aio: Netty不看重Windows上的使用,在Linux系统上,AIO的底层实现仍使用EPOLL,没有很好实现AIO,因此在性能上没有明显的优势,而且被JDK封装了一层不容易深度优化。 Netty整体架构是reactor模型, 而AIO是proactor模型, 混合在一起会非常混乱,把AIO也改造成reactor模型看起来是把epoll绕个弯又绕回来。 AIO还有个缺点是接收数据需要预先分配缓存, 而不是NIO那种需要接收时才需要分配缓存, 所以对连接数量非常大但流量小的情况, 内存浪费很多。 Linux上AIO不够成熟,处理回调结果速度跟不上处理需求,比如外卖员太少,顾客太多,供不应求,造成处理速度有瓶颈(待验证)。

Java NIO知识都在这里 NIO的特性/NIO与IO区别:

1)IO是面向流的,NIO是面向缓冲区的; 2)IO流是阻塞的,NIO流是不阻塞的; 3)NIO有选择器,而IO没有。

读数据和写数据方式: 从通道进行数据读取 :创建一个缓冲区,然后请求通道读取数据。 从通道进行数据写入 :创建一个缓冲区,填充数据,并要求通道写入数据。 NIO核心组件简单介绍

Channels 通常来说NIO中的所有IO都是从 Channel(通道) 开始的。 NIO Channel通道和流的区别:

Buffers Java NIO Buffers用于和NIO Channel交互。 我们从Channel中读取数据到buffers里,从Buffer把数据写入到Channels; Buffer本质上就是一块内存区; 一个Buffer有三个属性是必须掌握的,分别是:capacity容量、position位置、limit限制。

Selectors Selector 一般称 为选择器 ,当然你也可以翻译为 多路复用器 。它是Java NIO核心组件中的一个,用于检查一个或多个NIO Channel(通道)的状态是否处于可读、可写。如此可以实现单线程管理多个channels,也就是可以管理多个网络链接。 使用Selector的好处在于: 使用更少的线程来就可以来处理通道了, 相比使用多个线程,避免了线程上下文切换带来的开销。

netty 源码其实就是用了nio设计,并且使用独有的reactor

粘包问题的解决策略 由于底层的TCP无法理解上层的业务数据,所以在底层是无法保证数据包不被拆分和重组的,这 个问题只能通过上层的应用协议栈设计来解决。业界的主流协议的解决方案,可以归纳如下:

1.消息定长,报文大小固定长度,例如毎个报文的长度固定为200字节,如果不够空位补空格; 2.包尾添加特殊分隔符,例如毎条报文结朿都添加回车换行符(例如FTP协议)或者指定特殊 字符作为报文分隔符,接收方通过特殊分隔符切分报文区分; 3.将消息分为消息头和消息体,消息头中包含表示信患的总长度(或者消息体长度)的字段; 4.更复杂的自定义应用层协议。

优点:
减少线程切换的开销。
复用channel,可以选择池化channel EventLoopGroup EventLoop

zero copy的应用 减少并发下的竞态情况

首先来看看Reactor模式,Reactor模式应用于同步I/O的场景。我们以读操作为例来看看Reactor中的具体步骤:

读取操作:

  1. 应用程序注册读就需事件和相关联的事件处理器
  2. 事件分离器等待事件的发生
  3. 当发生读就需事件的时候,事件分离器调用第一步注册的事件处理器
  4. 事件处理器首先执行实际的读取操作,然后根据读取到的内容进行进一步的处理

下面我们来看看Proactor模式中读取操作和写入操作的过程:

读取操作:

  1. 应用程序初始化一个异步读取操作,然后注册相应的事件处理器,此时事件处理器不关注读取就绪事件,而是关注读取完成事件,这是区别于Reactor的关键。
  2. 事件分离器等待读取操作完成事件
  3. 在事件分离器等待读取操作完成的时候,操作系统调用内核线程完成读取操作,并将读取的内容放入用户传递过来的缓存区中。这也是区别于Reactor的一点,Proactor中,应用程序需要传递缓存区。
  4. 事件分离器捕获到读取完成事件后,激活应用程序注册的事件处理器,事件处理器直接从缓存区读取数据,而不需要进行实际的读取操作。 Proactor中写入操作和读取操作,只不过感兴趣的事件是写入完成事件。

从上面可以看出,Reactor和Proactor模式的主要区别就是真正的读取和写入操作是有谁来完成的,Reactor中需要应用程序自己读取或者写入数据,而Proactor模式中,应用程序不需要进行实际的读写过程,它只需要从缓存区读取或者写入即可,操作系统会读取缓存区或者写入缓存区到真正的IO设备.

综上所述,同步和异步是相对于应用和内核的交互方式而言的,同步需要主动去询问,而异步的时候内核在IO事件发生的时候通知应用程序,而阻塞和非阻塞仅仅是系统在调用系统调用的时候函数的实现方式而已。

--------------------spring------ aop ioc

Spring的AOP是基于动态代理实现的,Spring会在运行时动态创建代理类,代理类中引用被代理类,在被代理的方法执行前后进行一些神秘的操作。 BTrace基于ASM、Java Attach Api、Instruments开发,为用户提供了很多注解。依靠这些注解,我们可以编写BTrace脚本(简单的Java代码)达到我们想要的效果,而不必深陷于ASM对字节码的操作中不可自拔。

----------------------------锁

synchronized\ReentrantLock\volatile\Atomic(cas)

synchronized : 基于Monitor实现,底层使用操作系统的mutex lock实现的。Class和Object都关联了一个Monitor。

①.同步实例方法,锁是当前实例对象 ②.同步类方法,锁是当前类对象 ③.同步代码块,锁是括号里面的对象

Monitor;每个对象都拥有自己的监视器,当这个对象由同步块或者这个对象的同步方法调用时,执行方法的线程必须先获取该对象的监视器才能进入同步块和同步方法,如果没有获取到监视器的线程将会被阻塞在同步块和同步方法的入口处,进入到BLOCKED状态

synchronized修饰代方法:使用ACC_SYNCHRONIZED标记符隐式的实现

synchronized修饰代码块:字节码里面 monitorenter(进去) 每一个对象都有一个monitor,一个monitor只能被一个线程拥有。当一个线程执行到monitorenter指令时会尝试获取相应对象的monitor,获取规则如下:

如果monitor的进入数为0,则该线程可以进入monitor,并将monitor进入数设置为1,该线程即为monitor的拥有者。 如果当前线程已经拥有该monitor,只是重新进入,则进入monitor的进入数加1,所以synchronized关键字实现的锁是可重入的锁。 如果monitor已被其他线程拥有,则当前线程进入阻塞状态,直到monitor的进入数为0,再重新尝试获取monitor。

monitorexit(出去) 只有拥有相应对象的monitor的线程才能执行monitorexit指令。每执行一次该指令monitor进入数减1,当进入数为0时当前线程释放monitor,此时其他阻塞的线程将可以尝试获取该monitor。

缺点: 它无法中断一个正在等候获得锁的线程; 也无法通过投票得到锁,如果不想等下去,也就没法得到锁

ReentrantLock\读写锁 更加精细,稍微公平、提供了更多选择(lock,try、try(time)、lockInterruptibly)

性能不一致:资源竞争激励的情况下,lock性能会比synchronize好,竞争不激励的情况下,synchronize比lock性能好。 锁机制不一样:synchronize是在JVM层面实现的,系统会监控锁的释放与否。lock是代码实现的,需要手动释放,在finally块中释放。可以采用非阻塞的方式获取锁。 用法不一样:synchronize可以用在代码块上,方法上。lock通过代码实现,有更精确的线程语义。


volatile: 在多处理器开发中保证了共享变量的“ 可见性”。可见性的意思是当一个线程修改一个共享变量时,另外一个线程能读到这个修改的值。 volatile只保证可见性,不保证原子性

Atomic 原理: JDK通过CPU的cmpxchgl指令的支持,实现AtomicInteger的CAS操作的原子性

CAS :在Java并发应用中通常指CompareAndSwap或CompareAndSet,即比较并交换。

CAS是一个原子操作,它比较一个内存位置的值并且只有相等时修改这个内存位置的值为新的值,保证了新的值总是基于最新的信息计算的,如果有其他线程在这期间修改了这个值则CAS失败。CAS返回是否成功或者内存位置原来的值用于判断是否CAS成功。

有点: 竞争不大的时候系统开销小。

问题:

  1. ABA问题 CAS需要在操作值的时候检查下值有没有发生变化,如果没有发生变化则更新,但是如果一个值原来是A,变成了B,又变成了A,那么使用CAS进行检查时会发现它的值没有发生变化,但是实际上却变化了。这就是CAS的ABA问题。 常见的解决思路是使用版本号。在变量前面追加上版本号,每次变量更新的时候把版本号加一,那么A-B-A 就会变成1A-2B-3A。 目前在JDK的atomic包里提供了一个类AtomicStampedReference来解决ABA问题。这个类的compareAndSet方法作用是首先检查当前引用是否等于预期引用,并且当前标志是否等于预期标志,如果全部相等,则以原子方式将该引用和该标志的值设置为给定的更新值。
  2. 循环时间长开销大 上面我们说过如果CAS不成功,则会原地自旋,如果长时间自旋会给CPU带来非常大的执行开销。
  3. 只能保证一个共享变量的原子操作

-----包 ReenterLock Atomic ConcurrentMap Executors ThreadFactory

locks

-------concurrenthashmap---- jdk1.7 ConcurrentHashMap写操作只会锁一段(锁住Segment中所有元素),对不同Segment元素的操作不会互相阻塞,而HashTable用的是synchronized,会锁住整个对象,相当于一个HashTable上的操作都是并行的,连get方法都会阻塞其他操作。 换个说法吧,一个HashTable只有一把锁,最多只有一个线程获取到锁。

jdk1.8

1)Node的val和next均为volatile型 2)tabAt和casTabAt对应的unsafe操作实现了volatile语义

改进一:取消segments字段,直接采用transient volatile HashEntry<K,V>[] table保存数据,采用table数组元素作为锁,从而实现了对每一行数据进行加锁,进一步减少并发冲突的概率。 改进二:将原先table数组+单向链表的数据结构,变更为table数组+单向链表+红黑树的结构。对于hash表来说,最核心的能力在于将key hash之后能均匀的分布在数组中。如果hash之后散列的很均匀,那么table数组中的每个队列长度主要为0或者1。但实际情况并非总是如此理想,虽然ConcurrentHashMap类默认的加载因子为0.75,但是在数据量过大或者运气不佳的情况下,还是会存在一些队列长度过长的情况,如果还是采用单向列表方式,那么查询某个节点的时间复杂度为O(n);因此,对于个数超过8(默认值)的列表,jdk1.8中采用了红黑树的结构,那么查询的时间复杂度可以降低到O(logN),可以改进性能。

------- classLaoader 有BootStrap , Ext , App和用户自定义加载器,加载类的时候采用双亲委托机制。特别需要注意的是:加载器ClassLoader的父子关系和继承关系无关,也和加载器是有哪个加载器加载的无关,而是在创建加载器时指定的父加载器有关,即是由人工指定的。比如说要确定加载器A的父加载器,仅仅是由创建对象A时传进去的父加载器决定,而不管A的类型是什么,也不管A是由哪个加载器加载的。

1, 双亲委托机制。 2, 同一个加载器:类A引用到类B,则由类A的加载器去加载类B,保证引用到的类由同一个加载器加载。

ContextClassLoader: Class.forname() DriverManager是JDK的基础类由BootstrapClassLoader加载,DriverManager加载第三方实现的话,一定会加载BootstrapClassLoader里面。Java引入了ContextClassLoader。 ContextClassLoader是线程的一个属性,getter和setter方法

-----二叉树 红黑树: 红黑树的特性: (1)每个节点或者是黑色,或者是红色。 (2)根节点是黑色。 (3)每个叶子节点(NIL)是黑色。 [注意:这里叶子节点,是指为空(NIL或NULL)的叶子节点!] (4)如果一个节点是红色的,则它的子节点必须是黑色的。 (5)从一个节点到该节点的子孙节点的所有路径上包含相同数目的黑节点。 有点:

B-树的特性: 1.关键字集合分布在整颗树中;

2.任何一个关键字出现且只出现在一个结点中;

3.搜索有可能在非叶子结点结束;

4.其搜索性能等价于在关键字全集内做一次二分查找;

5.自动层次控制

B+树(高度低)

1.所有关键字都出现在叶子结点的链表中(稠密索引),且链表中的关键字恰好是有序的; 2.不可能在非叶子结点命中; 3.非叶子结点相当于是叶子结点的索引(稀疏索引),叶子结点相当于是存储关键字)数据的数据层; 4.更适合文件索引系统

----threadlocal

它是一个数据结构,有点像HashMap,可以保存"key : value"键值对,但是一个ThreadLocal只能保存一个,并且各个线程的数据互不干扰。

  1. 保存线程上下文信息,在任意需要的地方可以获取!!!
  2. 线程安全的,避免某些情况需要考虑线程安全必须同步带来的性能损失!!!

threadlocal并不能解决多线程共享变量的问题,同一个 threadlocal所包含的对象,在不同的thread中有不同的副本,互不干扰 用于存放线程上下文变量,方便同一线程对变量的前后多次读取,如事务、数据库connection连接,在web编程中使用的更多 问题: 注意线程池场景使用threadlocal,因为实际变量值存放在了thread的threadlocalmap类型变量中,如果该值没有remove,也没有先set的话,可能会得到以前的旧值 问题: 注意线程池场景下的内存泄露,虽然threadlocal的get/set会清除key(key为threadlocal的弱引用,value是强引用,导致value不释放)为null的entry,但是最好remove。

key 使用强引用:这样会导致一个问题,引用的 ThreadLocal 的对象被回收了,但是 ThreadLocalMap 还持有 ThreadLocal 的强引用,如果没有手动删除,ThreadLocal 不会被回收,则会导致内存泄漏。 key 使用弱引用:这样的话,引用的 ThreadLocal 的对象被回收了,由于 ThreadLocalMap 持有 ThreadLocal 的弱引用,即使没有手动删除,ThreadLocal 也会被回收。value 在下一次 ThreadLocalMap 调用 set、get、remove 的时候会被清除。 比较以上两种情况,我们可以发现:由于 ThreadLocalMap 的生命周期跟 Thread 一样长,如果都没有手动删除对应 key,都会导致内存泄漏,但是使用弱引用可以多一层保障,弱引用 ThreadLocal 不会内存泄漏,对应的 value 在下一次 ThreadLocalMap 调用 set、get、remove 的时候被清除,算是最优的解决方案。