Fighting!!!

网络部分:

OSI七层模型:应用层、表示层、会话层、传输层、网络层、链路层、物理层;

  • 应用层:为用户直接提供各种网络服务,如http,HTTPS,SMTP,FTP,POP3等;
  • 表示层:提供各种用于应用层数据的编码和转换功能,确保一个系统的应用层发送的数据能被另一个系统的应用层识别;
  • 会话层:就是负责建立、管理和终止表示层实体之间的通信会话;
  • 传输层:(TCP,UDP)为上层协议提供端到端的可靠和透明的数据传输服务。包括处理差错控制和流量控制等问题;
  • 网络层:(IP)本层通过IP寻址来建立两个节点之间的连接,为源端的运输层送来的分组,选择合适的路由和交换节点,正确无误地按照地址传送给目的端的运输层。;
  • 链路层:
  • 物理层:

TCP/IP五层模型:应用层、传输层、网络层、链路层、物理层;

Http和Https的区别:

  • HTTPS需要到ca申请证书,一般免费证书较少,因而需要一定费用;
  • http和https使用的是完全不同的连接方式,用的端口也不一样,前者是80,后者是443;
  • http的连接很简单,是无状态的;HTTPS协议是由SSL+HTTP协议构建的可进行加密传输、身份认证的网络协议,比http协议安全;
  • http是超文本传输协议,信息是明文传输,https则是具有安全性的ssl加密传输协议;
  • http是基于TCP的,所有的传输内容都是明文的,客户端和服务器端都无法验证对方的身份;
  • HTTPS是HTTP运行在SSL/TSL之上,SSL/TSL运行在TCP之上。所有传输的内容都经过加密,加密采用对称加密,但对称加密的密钥用服务器方的证书进行了非对称加密。此外客户端可以验证服务器端的身份,如果配置了客户端验证,服务器方也可以验证客户端的身份;

SSL协议位于TCP/IP协议与各种应用层协议之间,为数据通讯提供安全支持。SSL协议可分为两层:

SSL记录协议(SSL Record Protocol):它建立在可靠的传输协议(如TCP)之上,为高层协议提供数据封装、压缩、加密等基本功能的支持。

SSL握手协议(SSL Handshake Protocol):它建立在SSL记录协议之上,用于在实际的数据传输开始前,通讯双方进行身份认证、协商加密算法、交换加密密钥等。
对称密钥加密和非对称密钥加密:

  • 对称加密:信息的发送方和接受方用同一个密钥去加密和解密数据;
  • 非对称加密:它需要一对密钥来分别完成加密和解密操作,一个公开发布,即公开密钥,另一个由用户自己秘密保存,即私用密钥。信息发送者用公开密钥去加密,而信息接收者则用私用密钥去解密;

从输入网址到获得页面的过程:

  • 查询DNS,获取域名对应的IP地址;
  • 浏览器获得域名对应的IP地址后,发起HTTP三次握手;
  • TCP/IP连接建立起来后,浏览器就可以向服务器发送HTTP请求;
  • 服务器接收到这个请求后,根据路径参数,进过后端的一些处理生成HTML页面代码返回给浏览器;
  • 浏览器拿到完整的HTML页面代码开始解析和渲染,如果遇到引用的外部JS,CSS,图片等静态资源,它们同样是一个个的HTTP请求,都需要经过上面的步骤;
  • 浏览器根据拿到的资源对页面进行渲染,最终把一个完整的页面呈现给用户;

TCP/UDP:

三次握手:

  1. 第一次握手:Client将SYN置为1,随机产生一个seq=J,并将该数据包发送给Server,Client进入SYN_SENT状态,等待server确认;
  2. 第二次握手:server收到数据包后由标志位SYN=1知道client请求连接,server将SYN和ACK都置为1,ack=J+1,随机产生seq=K,并将该数据包发送给client确认请求连接,serve进入SYN_RCVD状态;
  3. 第三次握手:client收到确认后,检查ack是否为J+1,ACK是否为1,如果正确则将ACK置为1,ack=K+1,并将该数据包发送给server,server检查ack是否为K+1,ACK是否为1,正确则连接建立成功,双方进入就绪状态,完成三次握手;

SYN攻击:

在三次握手过程中,Server发送SYN-ACK之后,收到Client的ACK之前的TCP连接称为半连接(half-open connect),此时Server处于SYN_RCVD状态,当收到ACK后,Server转入ESTABLISHED状态。SYN攻击就是Client在短时间内伪造大量不存在的IP地址,并向Server不断地发送SYN包,Server回复确认包,并等待Client的确认,由于源地址是不存在的,因此,Server需要不断重发直至超时,这些伪造的SYN包将长时间占用未连接队列,导致正常的SYN请求因为队列满而被丢弃,从而引起网络堵塞甚至系统瘫痪。SYN攻击时一种典型的DDOS(分布式拒绝服务攻击)攻击,检测SYN攻击的方式非常简单,即当Server上有大量半连接状态且源IP地址是随机的,则可以断定遭到SYN攻击了,使用如下命令可以让之现行:

#netstat -nap | grep SYN_RECV

DDOS预防:

  • 限制同时打开SYN半连接的数目;
  • 缩短半连接的time out时间;

TCP不能两次握手:

为了防止已失效的连接请求报文段突然又传送到了服务端,因而产生错误;

如A发出连接请求,但因连接请求报文丢失而未收到确认,于是A再重传一次连接请求。后来收到了确认,建立了连接。数据传输完毕后,就释放了连接,A工发出了两个连接请求报文段,其中第一个丢失,第二个到达了B,但是第一个丢失的报文段只是在某些网络结点长时间滞留了,延误到连接释放以后的某个时间才到达B,此时B误认为A又发出一次新的连接请求,于是就向A发出确认报文段,同意建立连接,不采用三次握手,只要B发出确认,就建立新的连接了,此时A不理睬B的确认且不发送数据,则B一致等待A发送数据,浪费资源。

四次挥手:

由于TCP连接时全双工的,因此,每个方向都必须要单独进行关闭,这一原则是当一方完成数据发送任务后,发送一个FIN来终止这一方向的连接,收到一个FIN只是意味着这一方向上没有数据流动了,即不会再收到数据了,但是在这个TCP连接上仍然能够发送数据,直到这一方向也发送了FIN。首先进行关闭的一方将执行主动关闭,而另一方则执行被动关闭,上图描述的即是如此。

  1. 第一次挥手:client发送一个FIN,用来关闭client到server的数据发送,client进入FIN_WAIT_1;
  2. 第二次挥手:server收到FIN后,发送一个ACK给client,确认序号为收到序号+1(与SYN相同,一个FIN占用一个序号),server进入CLOSE_WAIT状态;
  3. 第三次挥手:server发送一个FIN后,用来关闭server到client的数据发送,server进入LAST_ACK状态;
  4. 第四次挥手:client收到FIN后,client进入TIME_WAIT状态,接着给server发送一个ACK给server,确认序号为收到序号+1,server进入CLOSED状态,完成四次挥手;

为什么TCP连接需要三次握手,关闭连接需要四次挥手:

因为当Server端收到Client端的SYN连接请求报文后,可以直接发送SYN+ACK报文。其中ACK报文是用来应答的,SYN报文是用来同步的。但是关闭连接时,当Server端收到FIN报文时,很可能并不会立即关闭SOCKET,所以只能先回复一个ACK报文,告诉Client端,"你发的FIN报文我收到了"。只有等到我Server端所有的报文都发送完了,我才能发送FIN报文,因此不能一起发送。故需要四步握手。

为什么TIME_WAIT状态需要经过2MSL(最大报文段生存时间)才能返回到CLOSE状态:

有可以最后一个ACK丢失。所以TIME_WAIT状态就是用来重发可能丢失的ACK报文。

**TCP:**TCP(Transmission Control Protocol传输控制协议)是一种面向连接的,可靠的,基于字节流的传输通信协议。

  • 原因:应用层需要可靠的连接,但是IP层没有这样的流机制;
  • 面向连接,即在客户端和服务器之间发送数据之间,必须先建立连接;
  • 位于应用层和IP层之间;
  • 连接需要建立三次握手、四次挥手断开连接;
  • 传输数据时可靠的;

**UDP:**UDP(User Datagram Protocol用户数据报协议)

  • 传输层协议;
  • 无连接的数据报协议;
  • 主要用于不要求分组顺序到达的传输中,分组传输顺序的检查和排序有应用层完成;
  • 提供面向事务的简单不可靠传递服务;
  • 功能:为了在给定的主机上能识别多个目的的地址,同时允许多个应用程序在同一台主机上工作并能够独立地进行数据包的发送和接受,设计用户数据报协议UDP;

TCP和UDP的区别:

  • TCP是面向连接的,UDP是无连接的;
  • TCP提供可靠的服务(通过TCP传输的数据。无差错,不丢失,不重复,且按序到达);UDP提供面向事务的简单的不可靠的传输;
  • UDP具有较好的实时性,工作效率比TCP高,适用于对高速传输和实时性比较高的通讯或广播通信。随着网速的提高,UDP使用越来越多;
  • 每一条TCP连接只能是点到点的,UDP支持一对一,一对多和多对多的交互通信;
  • TCP是流模式,UDP是数据报模式;

**TCP粘包:**如果客户端连续不断的向服务端发送数据包时,服务端接收的数据会出现两个数据包粘在一起的情况;

UDP是基于报文发送的,从UDP的帧结构可以看出,在UDP首部采用了16bit来指示报文的长度,而TCP是基于字节流的,虽然应用层和TCP传输层之间的数据交互是大小不等的数据块,但是TCP把这些数据块仅仅看成一连串无结构的字节流,没有边界;另外从TCP的帧结构也可以看出,在TCP的首部没有表示数据长度的字段;

产生粘包、拆包的情况:

  • 要发送的数据大于TCP发送缓冲区剩余空间大小,将会发生拆包。

  • 待发送数据大于MSS(最大报文长度),TCP在传输前将进行拆包。

  • 要发送的数据小于TCP发送缓冲区的大小,TCP将多次写入缓冲区的数据一次发送出去,将会发生粘包。

  • 接收数据端的应用层没有及时读取接收缓冲区中的数据,将发生粘包。

处理方法:

  • 发送端给每个数据包添加包首部,首部中应该至少包含数据包的长度,这样接收端在接收到数据后,通过读取包首部的长度字段,便知道每一个数据包的实际长度了。

  • 发送端将每个数据包封装为固定长度(不够的可以通过补0填充),这样接收端每次从接收缓冲区中读取固定长度的数据就自然而然的把每个数据包拆分开来。

  • 可以在数据包之间设置边界,如添加特殊符号,这样,接收端通过这个边界就可以将不同的数据包拆分开。

集合:

常见的集合:Map和Collection接口是所有集合框架的父接口;

  • Collection接口的子接口:Set接口和List接口;
  • Map接口的实现类主要有:HashMap、TreeMap、Hashtable、ConcurrentHashMap以及Properties等;
  • Set接口的实现类主要有:HashSet、TreeSet、LinkedHashSet等;
  • List接口的实现类主要有:ArrayList、LinkedList、Stack以及Vector等;

ArrayList、LinkedList、Vector的区别:

  • Vector是Java中线程安全的集合类(所有方法都是线程同步synchronized);
  • ArrayList是应用广泛的动态数组实现的集合类,不过线程不安全,所以性能要好的多,也可以根据需要增加数组容量,不过与Vector的调整逻辑不同,ArrayList增加50%,而Vector会扩容1倍;
  • Vector和ArrayList内部都是以数组的形式来保存集合中的元素,因此在随机访问集合元素上又较好的性能;而LinkedList内部以链表的形式保存集合中的元素,因此随机访问集合元素时性能较差,但在删除、插入元素时性能很好;

HashMap、Hashtable的区别:

  • Hashtable继承Dictionary类,HashMap继承自AbstractMap抽象类;
  • HashMap和Hashtable都是Map接口的典型实现类;
  • 在HashMap中,null可以作为键且只能有一个,可以有一个或多个key所对应的值为null,Hashtable中不允许键值为null;
  • Hashtable中的方法是同步的,而HashMap中的方法在缺省情况下是非同步的,因此Hashtable比HashMap慢;
  • 两者哈希值的使用不同,Hashtable直接使用对象的hashcode,而HashMap需要重新计算hash值,而且用于代替求模;
  • Hashtable中hash数组默认大小是11,增加的方式是old*2+1,HashMap中hash数组的默认大小是16,增加为old*2;
  • 当使用自定义类作为HashMap,Hashtable的key时,要使重写该类的equals(Object obj)和hashCode方法的判断标准一致;

HashMap:

在1.8之前HashMap采用位桶+链表实现,即使用链表处理冲突,同一hash值的链表都存储在一个链表里,但是当位于一个桶中的元素较多的时候,通过key值一次查找的效率将变低,而1.8中HashMap采用位桶+链表+红黑树实现,当链表长度超过阈值(8)时,将链表转换为红黑树,减少查找时间;

  • HashMap 的底层是个 Node 数组(Node<K,V>[] table),在数组的具体索引位置,如果存在多个节点,则可能是以链表或红黑树的形式存在;
  • 增加、删除、查找键值对时,定位到哈希桶数组的位置是很关键的一步,源码中是通过下面3个操作来完成这一步:1)拿到 key 的 hashCode 值;2)将 hashCode 的高位参与运算,重新计算 hash 值;3)将计算出来的 hash 值与 "table.length - 1" 进行 & 运算;
  • HashMap 的默认初始容量(capacity)是 16,capacity 必须为 2 的幂次方;默认负载因子(load factor)是 0.75;实际能存放的节点个数(threshold,即触发扩容的阈值)= capacity * load factor;
  • HashMap 在触发扩容后,阈值会变为原来的 2 倍,并且会对所有节点进行重 hash 分布,重 hash 分布后节点的新分布位置只可能有两个:"原索引位置" 或 "原索引+oldCap位置"。例如 capacity 为16,索引位置 5 的节点扩容后,只可能分布在新表 "索引位置5" 和 "索引位置21(5+16)"(1.7的时候是重新计算hash值,1.8时只需看原来hash值新增的那个bit是1还是0,是0的话索引没变,是1的话索引变为原索引+oldCap);
  • 当同一个索引位置的节点在增加后达到 9 个时,并且此时数组的长度大于等于 64,则会触发链表节点(Node)转红黑树节点(TreeNode),转成红黑树节点后,其实链表的结构还存在,通过 next 属性维持。链表节点转红黑树节点的具体方法为源码中的 treeifyBin 方法。而如果数组长度小于64,则不会触发链表转红黑树,而是会进行扩容;
  • HashMap 在 JDK 1.8 之后不再有死循环的问题,JDK 1.8 之前存在死循环的根本原因是在扩容后同一索引位置的节点顺序会反掉(1.8之前是在头端添加元素,1.8之后是依次在尾端添加)。
  • HashMap 是非线程安全的,在并发场景下使用 ConcurrentHashMap 来代替;

HashMap的遍历:

复制代码
public void test1(){
        Map<String,Integer> map=new HashMap<>();
        map.put("A",1);
        map.put("B",2);
        map.put("C",3);
        for(String key:map.keySet()){
            System.out.println(key+":"+map.get(key));
        }

        Iterator<Map.Entry<String,Integer>> iterator=map.entrySet().iterator();
        while (iterator.hasNext()){
            Map.Entry<String,Integer> entry=iterator.next();
            System.out.println(entry.getKey()+":"+entry.getValue());
        }

        for (Map.Entry<String,Integer> entry:map.entrySet()){
            System.out.println(entry.getKey()+":"+entry.getValue());
        }
    }

ConcurrentHashMap:

在JDK1.8之前是用Segment+HashEntry+ReentrantLock的方式进行的,而1.8中是采用Node+CAS+Synchronized来保证并发安全;

  • JDK1.8的实现降低锁的粒度,JDK1.7版本锁的粒度是基于Segment的,包含多个HashEntry,而1.8锁的粒度就是HashEntry(首节点);
  • JDK1.8版本的数据结构变得更加简单,使得操作也更加清晰流畅,因为已经使用synchronized来进行同步,所以不需要分段锁的概念,也就不需要Segment这种数据结构了;
  • JDK1.8使用红黑树来优化链表,基于长度很长的链表的遍历是一个很漫长的过程,而红黑树的遍历效率是很快的,代替一定阈值的链表,这样形成一个最佳拍档;

get()方法:

  • 首先计算hash值,定位到该table索引位置,如果是首节点符合就返回

  • 如果遇到扩容的时候,会调用标志正在扩容节点ForwardingNode的find方法,查找该节点,匹配就返回

  • 以上都不符合的话,就往下遍历节点,匹配就返回,否则最后就返回null

    //会发现源码中没有一处加了锁
    public V get(Object key) {
    Node<K,V>[] tab; Node<K,V> e, p; int n, eh; K ek;
    int h = spread(key.hashCode()); //计算hash
    if ((tab = table) != null && (n = tab.length) > 0 &&
    (e = tabAt(tab, (n - 1) & h)) != null) {//读取首节点的Node元素
    if ((eh = e.hash) == h) { //如果该节点就是首节点就返回
    if ((ek = e.key) == key || (ek != null && key.equals(ek)))
    return e.val;
    }
    //hash值为负值表示正在扩容,这个时候查的是ForwardingNode的find方法来定位到nextTable来
    //eh=-1,说明该节点是一个ForwardingNode,正在迁移,此时调用ForwardingNode的find方法去nextTable里找。
    //eh=-2,说明该节点是一个TreeBin,此时调用TreeBin的find方法遍历红黑树,由于红黑树有可能正在旋转变色,所以find里会有读写锁。
    //eh>=0,说明该节点下挂的是一个链表,直接遍历该链表即可。
    else if (eh < 0)
    return (p = e.find(h, key)) != null ? p.val : null;
    while ((e = e.next) != null) {//既不是首节点也不是ForwardingNode,那就往下遍历
    if (e.hash == h &&
    ((ek = e.key) == key || (ek != null && key.equals(ek))))
    return e.val;
    }
    }
    return null;
    }

  • volatile关键字对于基本类型的修改可以在随后对多个线程的读保持一致,但是对于引用类型如数组,实体bean,仅仅保证引用的可见性,但并不保证引用内容的可见性。。
  • 禁止进行指令重排序。
  • 如果对声明了volatile的变量进行写操作,JVM就会向处理器发送一条指令,将这个变量所在缓存行的数据写回到系统内存。但是,就算写回到内存,如果其他处理器缓存的值还是旧的,再执行计算操作就会有问题。
  • 在多处理器下,为了保证各个处理器的缓存是一致的,就会实现缓存一致性协议,当某个CPU在写数据时,如果发现操作的变量是共享变量,则会通知其他CPU告知该变量的缓存行是无效的,因此其他CPU在读取该变量时,发现其无效会重新从主存中加载数据。
  • 在1.8中ConcurrentHashMap的get操作全程不需要加锁,这也是它比其他并发集合比如hashtable、用Collections.synchronizedMap()包装的hashmap;安全效率高的原因之一。
  • get操作全程不需要加锁是因为Node的成员val是用volatile修饰的和数组用volatile修饰没有关系。
  • 数组用volatile修饰主要是保证在数组扩容的时候保证可见性。

线程:

乐观锁的两种实现方式:

  • 使用版本标识来确定读到的数据与提交时的数据是否一致。提交后修改版本标识,不一致时可以采取丢弃和再次尝试的策略;
  • CAS:compare and swap;

CAS:内存地址V,旧的预期值A,要修改的新值B

更新一个变量的时候,只有当变量的预期值A和内存地址V当中的实际值相同时,才会将内存地址V对应的值修改为B;

ABA问题:

CAS实现的过程是先取出内存中某时刻的数据,在下一时刻比较并替换,那么在这个时间差会导致数据的变化,此时就会导致出现"ABA"问题;比如说一个线程one从内存位置V中取出A,这时候另一个线程two也从内存中取出A,并且two进行了一些操作变成了B,然后two又将V位置的数据变成A,这时候线程one进行CAS操作发现内存中仍然是A,然后one操作成功;尽管线程one的CAS操作成功,但是不代表这个过程就是没有问题的;

解决方法之一:通过版本号;

Reentrantlock:

Reentrantlock重入锁,是实现Lock接口的一个类,支持重入性,表示能够对共享资源重复加锁,即当前线程获取锁后再次获取不会被阻塞,sychronized隐式支持重入性,synchronized通过获取自增,释放自减的方式实现重入;ReentrantLock还支持公平锁和非公平锁两种方式;

公平锁是指多个线程在等待同一个锁时,必须按照申请锁的先后顺序来一次获得锁。

公平锁的好处是等待锁的线程不会饿死,但是整体效率相对低一些;非公平锁的好处是整体效率相对高一些,但是有些线程可能会饿死或者说很早就在等待锁,但要等很久才会获得锁。其中的原因是公平锁是严格按照请求所的顺序来排队获得锁的,而非公平锁时可以抢占的,即如果在某个时刻有线程需要获取锁,而这个时候刚好锁可用,那么这个线程会直接抢占,而这时阻塞在等待队列的线程则不会被唤醒。公平锁可以使用new ReentrantLock(true)实现。

死锁:

是指两个或两个以上的进程在执行过程中,因争夺资源而造成的一种互相等待的现象,若无外力作用,它们都将无法推进下去;

  • 互斥条件:一个资源每次只能被一个进程使用;
  • 请求和保持:一个进程因请求资源而阻塞时,对已获得的资源保持不放;
  • 不剥夺条件:进程已获得的资源,在未使用完之前,不能强行剥夺;
  • 循环等待条件:若干进程之间形成一种头尾相接的循环等待资源关系;

预防死锁:

  • 资源一次性分配:一次性分配所有资源,这样就不会再有请求了:(破坏请求条件)
  • 只要有一个资源得不到分配,也不给这个进程分配其他的资源:(破坏保持条件)
  • 可剥夺资源:即当某进程获得了部分资源,但得不到其它资源,则释放已占有的资源(破坏不可剥夺条件)
  • 资源有序分配法:系统给每类资源赋予一个编号,每一个进程按编号递增的顺序请求资源,释放则相反(破坏环路等待条件)

进程和线程:

  • 进程是资源分配的基本单位,它是程序执行的一个实例,程序运行时系统就会创建一个进程,并为它分配资源,然后把该进程放入进程就绪队列,进程调度器选中它的时候就会为它分配CPU时间,程序开始真正运行;
  • 线程是程序执行时的最小单位,它是进程的一个执行流,是CPU调度和分派的基本单位,一个进程可以由很多个线程组成,线程间共享进程的所有资源,每个线程有自己的堆栈和局部变量。线程由CPU独立调度执行,在多CPU环境下就允许多个线程同时运行。同样多线程也可以实现并发操作,每个请求分配一个线程来处理。

线程和进程各自有什么区别和优劣:

  • 进程是资源分配的最小单位,线程是程序执行的最小单位。

  • 进程有自己的独立地址空间,每启动一个进程,系统就会为它分配地址空间,建立数据表来维护代码段、堆栈段和数据段,这种操作非常昂贵。而线程是共享进程中的数据的,使用相同的地址空间,因此CPU切换一个线程的花费远比进程要小很多,同时创建一个线程的开销也比进程要小很多。

  • 线程之间的通信更方便,同一进程下的线程共享全局变量、静态变量等数据,而进程之间的通信需要以通信的方式(IPC)进行。不过如何处理好同步与互斥是编写多线程程序的难点。

  • 但是多进程程序更健壮,多线程程序只要有一个线程死掉,整个进程也死掉了,而一个进程死掉并不会对另外一个进程造成影响,因为进程有自己独立的地址空间。

进程通信:

  • 匿名管道:匿名管道是半双工方式,数据只能单向流动,而且只能在亲缘关系的进程中通信(匿名管道是通过fork()来创建一个子进程,然后子进程和父进程之间的管道可以读写);
  • 命名管道:也是半双工,允许无亲缘关系的进程间通信;
  • 信号量:信号量是一个计数器,用来控制多个进程对共享资源的访问,作为一种机制锁,可以防止某进程访问共享资源时,其他进程也访问该资源;
  • 共享内存:共享内存就是映射一段能够被其它进程访问的内存,这段内存由一个进程创建,但是可以被多个进程访问。共享内存是最快的IPC(Inter-Process Communication)方式 。通常共享内存要配合信号量使用。
  • 套接字:用于不同进程间的通信;
  • 消息队列:

线程通信:

  • 锁机制:互斥锁,读写锁和条件变量;互斥锁:以排它式方式数据结构被并发修改读写锁:允许多个线程同时读数据,但是每次只允许一个线程写数据。条件变量:当不满足某些条件时会一直阻塞进程,直到满足某个条件为止。条件变量与互斥锁配合使用;
  • 信号量机制;

sleep()与wait()的区别:

  • sleep()是Thread类的方法,wait()是Object类的方法;
  • sleep()是Thread类的静态方法,调用此方法会让当前线程暂停执行指定的时间,让出CPU,但不释放锁,休眠时间结束后回到就绪状态;
  • wait()是Object类的方法,它会使当前线程放弃对象锁,进入对象的等待池,只有调用notify()/notifyAll()时才能唤醒等待池中的线程进入等锁池,如果线程重新获得对象的锁就可以进入就绪状态

线程池:

是一种多线程处理方式,处理过程中将任务提交到线程池,任务的执行交由线程池管理;如果每个请求都创建一个线程去处理,那么服务器的资源很快就会被耗尽,使用线程池可以减少创建和销毁线程的次数,每个工作线程都可以被重复利用,可执行多个任务。

常见的线程池:

  • newSingleThreadExecutor:创建一个单线程化的线程池,它只会用唯一的工作线程来执行任务,保证所有任务按照指定顺序执行(FIFO,LIFO,优先级);

  • newFixedThreadPool:创建一个定长线程池,可控制线程最大并发数,超出的线程会在队列中等待;

  • newCachedThreadPool:创建一个可缓存线程池,如果线程池长度超过处理需要,可灵活回收空闲线程,若无可回收,则新建线程;

  • newScheduledThreadPool:创建一个定长线程池,支持定时及周期性任务执行。

    public static ExecutorService newSingleThreadExecutor() {
    return new FinalizableDelegatedExecutorService
    (new ThreadPoolExecutor(1, 1,
    0L, TimeUnit.MILLISECONDS,
    new LinkedBlockingQueue<Runnable>()));
    }

多线程上下文切换:CPU通过时间片的算法来循环执行线程任务,而循环执行即每个线程允许运行的时间后的切换,而这种循环的切换使各个程序从表面上看是同时的,而切换时会保存之前的线程任务状态,当切换到该线程任务的时候,会重新加载该线程的任务状态。而这个从保存到加载的过程称为上下文切换;

JVM:

内存分区:

  • 方法区:主要用来存储已被虚拟机加载的类的信息、常量、静态变量和即时编译后的代码等数据;线程共享;运行时常量池;
  • 虚拟机栈:为Java方法服务,每个方法在执行的时候会创建一个栈帧用于存储局部变量表、操作数栈、动态链接和方法出口等信息;线程私有;
  • 本地方法栈:与虚拟机栈类似,为本地方法服务;
  • 堆:所有线程共享,几乎所有对象实例都在这创建;垃圾回收操作;
  • 程序计数器:内存空间小,字节码解释器工作时通过改变这个计数值可以选取下一条需要执行的字节码指令,分支、循环、跳转、异常处理和线程恢复等功能都需要依赖这个计数器完成

对象存活:

  • 引用计数法:所谓引用计数法就是给每一个对象设置一个引用计数器,每当有一个地方引用这个对象时,就将计数器加一,引用失效时,计数器就减一。当一个对象的引用计数器为零时,说明此对象没有被引用,也就是"死对象",将会被垃圾回收.
    引用计数法有一个缺陷就是无法解决循环引用问题,也就是说当对象A引用对象B,对象B又引用者对象A,那么此时A,B对象的引用计数器都不为零,也就造成无法完成垃圾回收,所以主流的虚拟机都没有采用这种算法;
  • 可达性算法:从一个被称为GC Roots的对象开始向下搜索,如果一个对象到GC Roots没有任何引用链相连时,则说明此对象不可用。

在java中可以作为GC Roots的对象有以下几种:

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

垃圾回收方法:

  • 标记清除:标记哪些要被回收的对象,然后统一回收;产生的问题:效率不高,标记和清除的效率都很低;.会产生大量不连续的内存碎片,导致以后程序在分配较大的对象时,由于没有充足的连续内存而提前触发一次GC动作;
  • 复制算法:复制算法将可用内存按容量划分为相等的两部分,然后每次只使用其中的一块,当一块内存用完时,就将还存活的对象复制到第二块内存上,然后一次性清楚完第一块内存,再将第二块上的对象复制到第一块。但是这种方式,内存的代价太高,每次基本上都要浪费一般的内存;
  • 标记-整理:标记-整理算法采用标记-清除算法一样的方式进行对象的标记,但在清除时不同,在回收不存活的对象占用的空间后,会将所有的存活对象往左端空闲空间移动,并更新对应的指针。标记-整理算法是在标记-清除算法的基础上,又进行了对象的移动,因此成本更高,但是却解决了内存碎片的问题;
  • 分代收集:现在的虚拟机垃圾收集大多采用这种方式,它根据对象的生存周期,将堆分为新生代和老年代。在新生代中,由于对象生存期短,每次回收都会有大量对象死去,那么这时就采用复制 算法。老年代里的对象存活率较高,没有额外的空间进行分配担保,所以可以使用标记-整理 或者 标记-清除

类加载过程:加载、链接、初始化

  • 加载:把class字节码文件从各个来源通过类加载器装载入内存中;
  • 验证:为了确保Class文件的字节流中的信息不回危害到虚拟机;
  • 准备:类变量(注意,不是实例变量)分配内存,并且赋予初值;
  • 解析:将常量池内的符号引用替换为直接引用的过程;
  • 初始化:对类变量初始化,是执行类构造器的过程;

类加载机制:

虚拟机把描述类的数据从class文件加载到内存,并对数据进行校验,解析和初始化,最终形成可以被虚拟机直接使用的java类型;

  • 全盘负责:所谓全盘负责,就是当一个类加载器负责加载某个Class时,该Class所依赖和引用其他Class也将由该类加载器负责载入,除非显示使用另外一个类加载器来载入;
  • 双亲委派:当一个类收到了类加载请求时,不会自己先去加载这个类,而是将其委派给父类,由父类去加载,如果此时父类不能加载,反馈给子类,由子类去完成类的加载。
  • 缓存机制:缓存机制将会保证所有加载过的Class都会被缓存,当程序中需要使用某个Class时,类加载器先从缓存区中搜寻该Class,只有当缓存区中不存在该Class对象时,系统才会读取该类对应的二进制数据,并将其转换成Class对象,存入缓冲区中。这就是为什么修改了Class后,必须重新启动JVM,程序所做的修改才会生效的原因;

类加载器:实现通过类的全限定名获取该类的二进制字节流的代码块叫做类加载器;

  • 引导类加载器:用来加载Java核心类库;
  • 扩展类加载器:用来加载Java的扩展库,Java虚拟机的实现会提供一个扩展库目录,该类加载器在此目录里查找并加载Java类;
  • 系统类加载器:根据类路径来加载Java类,可以通过ClassLoader.getSystemClassLoader()获取;

MySQL:

数据引擎MyISAM和InnoDB的区别:

  • InnoDB不支持FULLTEXT类型的索引;
  • InnoDB不保存表的具体行数,即select count(*) from table,InnoDB要扫描一遍整个表来计算有多少行,但是MyISAM只要简单的读出保存的行数就行,但当 count(*) 后有where时,两种表的操作是一样的;
  • 对AUTO_INCREAMENT类型的字段,InnoDB必须包含只有该字段的索引,但是在MyISAM中,可以和其他字段一起建立联合索引;
  • 删除表时,InnoDB不会重建表,而是一行一行的删除;
  • InnoDB支持MVCC,而MyISAM不支持;
  • InnoDB支持行级锁,而MyISAM支持表级锁;
  • InooDB支持事务,而MyISAM不支持事务;
  • InnoDB支持外键,而MyISAM不支持;

Executor、ExecutorService、Executors三者的区别:

  • ExecutorService接口继承了Executor接口,是Executor的子接口;
  • Executor接口中定义了execute()方法,用来接收一个Runnable接口的对象,而ExecutorService接口中定义的submit()方法可以接收Runnable和Callable对象;
  • ExecutorService提供了用来控制线程池的方法;
  • Executors 类提供工厂方法用来创建不同类型的线程池;

Redis:

使用Redis的好处:

  • 速度快,数据存在内存中;
  • 支持丰富数据类型,支持string,list,set,sorted set,hash;
  • 支持事务,操作都是原子性;
  • 丰富的特性:可用于缓存,消息,按key设置过期时间,过期后自动删除;

Redis的线程模型:

IO:

  • 同步阻塞I/O:
  • 同步非阻塞I/O:
  • I/O多路复用:
  • 信号驱动I/O模型:
  • 异步I/O模型:

同步和异步的概念描述的是用户线程与内核的交互方式:同步是指用户线程发起IO请求后需要等待或者轮询内核IO操作完成后才能继续执行;而异步是指用户线程发起IO请求后仍继续执行,当内核IO操作完成后会通知用户线程,或者调用用户线程注册的回调函数;

阻塞和非阻塞的概念描述的是用户线程调用内核IO操作的方式:阻塞是指IO操作需要彻底完成后才返回到用户空间;非阻塞是指IO操作被调用后立即返回给用户一个状态值,无需等到IO操作彻底完成;

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

select:时间复杂度O(n)

它仅仅知道有I/O事件发生,但并不知道是哪几个流(一个或者多个),只能无差别轮询所有流,找出能读出数据或者写入数据的流然后进行操作,所以select具有O(n)的无差别轮询复杂度,同时处理的流越多,无差别轮询时间就越长;select本质上是通过设置或者检查存放fd标志位的数据结构来进行下一步处理,缺点是:

  • 单个进程可监视的fd数量被限制,即能监听端口的大小有限;
  • 对socket进行扫描时是线性扫描,即采用轮询的方法,效率较低;
  • 需要维护一个用来存放大量fd的数据结构,这样会使得用户空间和内核空间在传递该结构时复制开销大;
  • select支持的文件描述符数量太小了,默认是1024

poll:时间复杂度O(n)

本质上和select没有区别,它将用户传入的数据拷贝到内核空间,然后查询每个fd对应的设备状态,但是它没有最大连接数的限制,它是基于链表来存储的;poll还有一个特点是"水平触发",如果报告了fd后,没有被处理,那么下次poll时会再次报告该fd。缺点:

  • poll() 和 select() 同样存在一个缺点就是,包含大量文件描述符的数组被整体复制于用户态和内核的地址空间之间,而不论这些文件描述符是否就绪,它的开销随着文件描述符数量的增加而线性增大

epoll:时间复杂度O(1)

可以理解为event poll,epoll会把哪个流发生了怎样的IO事件通知我们,epoll有EPOLLLT和EPOLLET两种触发模式,LT是默认的,ET是高速模式;LT模式下,只要这个fd还有数据可读,每次epoll_wait都会返回它的事件,提醒用户程序去操作,而在ET(边缘触发)中,它只会提示一次,直到下一次再有数据流入之前都不会再提示,无论fd中是否还有数据可读;

epoll为什么要有EPOLLET触发模式?

如果采用EPOLLLT模式的话,系统中一旦有大量你不需要读写的就绪文件描述符,它们每次调用epoll_wait都会返回,这样会大大降低处理程序检索自己关心的就绪文件描述符的效率.。而采用EPOLLET这种边沿触发模式的话,当被监控的文件描述符上有可读写事件发生时,epoll_wait()会通知处理程序去读写。如果这次没有把数据全部读写完(如读写缓冲区太小),那么下次调用epoll_wait()时,它不会通知你,也就是它只会通知你一次,直到该文件描述符上出现第二次可读写事件才会通知你!!!这种模式比水平触发效率高,系统不会充斥大量你不关心的就绪文件描述符

epoll的优点:

  • 没有最大并发连接的限制,能打开的FD的上限远大于1024(1G的内存上能监听约10万个端口)
  • 效率提升,不是轮询的方式,不会随着FD数目的增加效率下降。只有活跃可用的FD才会调用callback函数;
  • 即Epoll最大的优点就在于它只管你"活跃"的连接,而跟连接总数无关,因此在实际的网络环境中,Epoll的效率就会远远高于select和poll。
  • 内存拷贝,利用mmap()文件映射内存加速与内核空间的消息传递;即epoll使用mmap减少复制开销。

线程池:参考

线程池=线程集合workerSet+阻塞队列workQueue;

当用户向线程池提交一个任务(线程)时,线程池会先将其放入workQueue中,workSet中的线程会不断的从workQueue中获取线程然后执行,当workQueue中没有任务的时候,worker就会阻塞,直到队列中有任务后就取出来继续执行;

线程池参数:

复制代码
public ThreadPoolExecutor(int corePoolSize,
                              int maximumPoolSize,
                              long keepAliveTime,
                              TimeUnit unit,
                              BlockingQueue<Runnable> workQueue,
                              ThreadFactory threadFactory,
                              RejectedExecutionHandler handler)
  • corePoolSize:规定线程池中有几个线程在运行;
  • maximumPoolSize:当workQueue满了,不能添加任务的时候,这个参数生效,规定线程池最多有多少个线程在执行;
  • keepAliveTime:超出corePoolSize大小的那些线程的生存时间,这些线程如果长时间没有执行任务并超过了keepAliveTime设定的时间就会死亡;
  • unit:生存时间的单位;
  • workQueue:存放任务的队列;
  • threadFactory:创建线程的工厂;
  • handler:当workQueue已经满了,并且线程池线程数已经达到maximumPoolSize,将执行拒绝策略;

任务提交后的线程分析:

用户通过submit提交一个任务,线程池执行流程:

  • 判断当前运行的worker数量是否超过corePoolSize,如果没有超过corePoolSize,那么就创建一个worker直接去执行该任务(线程池最开始没有没有worker在执行);
  • 如果正在运行的worker数量超过或者等于corePoolSize,那么就将该任务加入workQueue中去;
  • 如果corePoolSize也满了,也就是offer()方法返回false的话,就检查当前运行的worker数量是否小于maximumPoolSize,如果小于就直接创建一个worker直接执行该任务;
  • 如果当前运行的worker数量大于等于maximumPoolSize,那么就执行RejectedExecutionHandler来拒绝这个任务的提交;