Java面试题整理

前言

已失业的中年程序猿,在这个卷的不能再卷的Java赛道中,形势恶劣的大环境下,仍不想放弃,苦苦支撑,想寻求一条生路~~都说工作拧螺丝,面试造火箭,八股文还是终极武器。从各渠道获取的八股文以及自己面试遇到的问题,整理汇总起来,愿自己后面的面试能一切顺利。(持续更新)

1.Java基础(集合、异常、IO、网络、多线程、JVM)

1.1 Java基本的数据类型有哪些

 1.2 == 和 equals 的区别是什么

== : 它的作用是判断两个对象的地址是不是相等。即,判断两个对象是不是同一个对象。(基本数 据类型 == 比较的是值,引用数据类型 == 比较的是内存地址)

equals() : 它的作用也是判断两个对象是否相等。( 类没有覆盖 equals() 方法。则通过 equals() 比较该类的两个对象时,等价于通过 “==”比较这两个对象。

类覆盖了 equals() 方法。一般,我们都覆盖 equals() 方法来比较两个对象的内容相等; 若它们的内容相等,则返回 true (即,认为这两个对象相等)。

String中的equals方法是被重写过的,因为object的equals方法是比较的对象的内存地址, 而String的equals方法比较的是对象的值。 当创建String类型的对象时,虚拟机会在常量池中查找有没有已经存在的值和要创建的值相同的对象,如果有就把它赋给当前引用。如果没有就在常量池中重新创建一个String对象。 )

1.3 hashCode 方法的作用

hashCode()的作用是获取哈希码,也成为散列码,它实际上是返回一个int整数。这个哈希码的作用是确定该对象在哈希表中的索引位置,它定义为Object类的方法,就意味着Java中的任何类都包含有hashCode()方法。

散列表存储的是键值对(key-value),它的特点是:能根据“键”快速的检索出对应的“值”。这其中就 利用到了散列码(可以快速找到所需要的对象)

我们以“HashSet 如何检查重复”为例子来说明为什么要有 hashCode:

当你把对象加入 HashSet 时,HashSet 会先计算对象的 hashcode 值来判断对象加入的位置,同 时也会与其他已经加入的对象的 hashcode 值作比较,如果没有相符的hashcode,HashSet会假 设对象没有重复出现。但是如果发现有相同 hashcode 值的对象,这时会调用 equals()方法来检查 hashcode 相等的对象是否真的相同。如果两者相同,HashSet 就不会让其加入操作成功。如果不 同的话,就会重新散列到其他位置。

hashCode()与equals()的相关规定 :

  • 如果两个对象相等,则hashcode一定也是相同的

  • 两个对象相等,对两个对象分别调用equals方法都返回true

  • 两个对象有相同的hashcode值,它们也不一定是相等的

    因此,equals 方法被覆盖过,则 hashCode 方法也必须被覆盖。 hashCode() 的默认行为是对堆上的对象产生独特值。如果没有重写 hashCode(),则该 class 的两个对象无论如何都不会相等(即使这两个对象指向相同的数据)

1.4 常用的集合类有哪些

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

  1. Collection接口的子接口包括:Set接口和List接口

  2. Map接口的实现类主要有:HashMap、TreeMap、Hashtable、ConcurrentHashMap以及 Properties等

  3. Set接口的实现类主要有:HashSet、TreeSet、LinkedHashSet等

  4. List接口的实现类主要有:ArrayList、LinkedList、Stack以及Vector等

1.5 ArrayList 和 LinkedList 的区别是什么?

  1. 数据结构实现:ArrayList 是动态数组的数据结构实现,而 LinkedList 是双向链表的数据结构实现。

  2. 随机访问效率:ArrayList 比 LinkedList 在随机访问的时候效率要高,因为 LinkedList 是线性的数据存储方式,所以需要移动指针从前往后依次查找。

  3. 增加和删除效率:在非首尾的增加和删除操作,LinkedList 要比 ArrayList 效率要高,因为 ArrayList 增删操作要影响数组内的其他数据的下标。

  4. 内存空间占用:LinkedList 比 ArrayList 更占内存,因为 LinkedList 的节点除了存储数据,还存储了两个引用,一个指向前一个元素,一个指向后一个元素。

  5. 线程安全:ArrayList 和 LinkedList 都是不同步的,也就是不保证线程安全。

综合来说,在需要频繁读取集合中的元素时,更推荐使用 ArrayList,而在插入和删除操作较多 时,更推荐使用 LinkedList。LinkedList 的双向链表也叫双链表,是链表的一种,它的每个数据结点中都有两个指针,分别指向 直接后继和直接前驱。所以,从双向链表中的任意一个结点开始,都可以很方便地访问它的前驱结 点和后继结点。

1.6 多线程场景下如何使用 ArrayList?

ArrayList 不是线程安全的,如果遇到多线程场景,可以通过 Collections 的 synchronizedList 方 法将其转换成线程安全的容器后再使用,或者使用CopyOnWriteArrayList

List<String> synchronizedList = Collections.synchronizedList(list);
synchronizedList.add("111");
synchronizedList.add("222");
​
for (int i = 0; i < synchronizedList.size(); i++) {
    System.out.println(synchronizedList.get(i));
}
​

 1.7 HashSet是如何保证数据不重复的?

向HashSet中add()时,判断元素是否存在的依据,不仅要比较hash值,同时还要结合equals方法比较。HashSet中的add()方法会使用HashMap的put()方法。

HashMap 的 key 是唯一的,由源码可以看出 HashSet 添加进去的值就是作为HashMap 的key, 并且在HashMap中如果K/V相同时,会用新的V覆盖掉旧的V,然后返回旧的V。所以不会重复( HashMap 比较key是否相等是先比较hashcode 再比较equals )。

private static final Object PRESENT = new Object();
private transient HashMap<E,Object> map;
public HashSet() {
    map = new HashMap<>();
}
public boolean add(E e) {
    // 调用HashMap的put方法,PRESENT是一个至始至终都相同的虚值
    return map.put(e, PRESENT)==null;
}

1.8 HashSet与HashMap的区别

 

1.9 HashMap的底层实现,jdk1.7和1.8中有何区别? 

在Java中,保存数据有两种比较简单的数据结构:数组和链表。

  • 数组的特点是:寻址容易,插入和 删除困难;

  • 链表的特点是:寻址困难,但插入和删除容易;

所以我们将数组和链表结合在一起,发挥两者各自的优势,使用一种叫做拉链法的方式可以解决哈希冲突。

HashMap JDK1.8之前

JDK1.8之前采用的是拉链法。拉链法:将链表和数组相结合。也就是说创建一个链表数组,数组中每一格就是一个链表。若遇到哈希冲突,则将冲突的值加到链表中即可。

HashMap JDK1.8之后

相比于之前的版本,jdk1.8在解决哈希冲突时有了较大的变化,当链表长度大于阈值(默认为8) 时,将链表转化为红黑树,以减少搜索时间。

 

红黑树是一种含有红黑结点并能自平衡的二叉查找树。它必须满足下面性质:

  • 性质1:每个节点要么是黑色,要么是红色。

  • 性质2:根节点是黑色。

  • 性质3:每个叶子节点(NIL)是黑色。

  • 性质4:每个红色结点的两个子结点一定都是黑色。

  • 性质5:任意一结点到每个叶子结点的路径都包含数量相同的黑结点。

JDK1.7 VS JDK1.8 比较

  1. resize 扩容优化

  2. 引入了红黑树,目的是避免单条链表过长而影响查询效率

  3. 解决了多线程死循环问题,但仍是非线程安全的,多线程时可能会造成数据丢失问题。

1.10 HashMap的put方法的具体流程?

当我们put的时候,首先计算key的hash值,这里调用了hash方法,hash 方法实际是让 key.hashCode() 与 key.hashCode()>>>16 进行异或操作,高16bit补0,一个数和0异或不变, 所以 hash 函数大概的作用就是:高16bit不变,低16bit和高16bit做了一个异或,目的是减少碰撞。按照函数注释,因为bucket数组大小是2的幂,计算下标 index = (table.length - 1) & hash ,如果不做 hash 处理,相当于散列生效的只有几个低 bit 位,为了减少散列的碰撞,设计者综合考虑了速度、作用、质量之后,使用高16bit和低16bit异或来简单处理减少碰撞,而且JDK8中用了复杂度 O(logn)的树结构来提升碰撞下的性能。

public V put(K key, V value) {
    return putVal(hash(key), key, value, false, true);
}

static final int hash(Object key) {
    int h;
    return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}

//实现Map.put和相关方法
final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
                   boolean evict) {
    Node<K,V>[] tab; Node<K,V> p; int n, i;
    // 步骤①:tab为空则创建 
    // table未初始化或者长度为0,进行扩容
    if ((tab = table) == null || (n = tab.length) == 0)
        n = (tab = resize()).length;
    // 步骤②:计算index,并对null做处理  
    // (n - 1) & hash 确定元素存放在哪个桶中,桶为空,新生成结点放入桶中(此时,这个结点是放在数组中)
    if ((p = tab[i = (n - 1) & hash]) == null)
        tab[i] = newNode(hash, key, value, null);
    // 桶中已经存在元素
    else {
        Node<K,V> e; K k;
        // 步骤③:节点key存在,直接覆盖value 
        // 比较桶中第一个元素(数组中的结点)的hash值相等,key相等
        if (p.hash == hash &&
            ((k = p.key) == key || (key != null && key.equals(k))))
                // 将第一个元素赋值给e,用e来记录
                e = p;
        // 步骤④:判断该链为红黑树 
        // hash值不相等,即key不相等;为红黑树结点
        // 如果当前元素类型为TreeNode,表示为红黑树,putTreeVal返回待存放的node, e可能为null
        else if (p instanceof TreeNode)
            // 放入树中
            e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
        // 步骤⑤:该链为链表 
        // 为链表结点
        else {
            // 在链表最末插入结点
            for (int binCount = 0; ; ++binCount) {
                // 到达链表的尾部
                
                //判断该链表尾部指针是不是空的
                if ((e = p.next) == null) {
                    // 在尾部插入新结点
                    p.next = newNode(hash, key, value, null);
                    //判断链表的长度是否达到转化红黑树的临界值,临界值为8
                    if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
                        //链表结构转树形结构
                        treeifyBin(tab, hash);
                    // 跳出循环
                    break;
                }
                // 判断链表中结点的key值与插入的元素的key值是否相等
                if (e.hash == hash &&
                    ((k = e.key) == key || (key != null && key.equals(k))))
                    // 相等,跳出循环
                    break;
                // 用于遍历桶中的链表,与前面的e = p.next组合,可以遍历链表
                p = e;
            }
        }
        //判断当前的key已经存在的情况下,再来一个相同的hash值、key值时,返回新来的value这个值
        if (e != null) { 
            // 记录e的value
            V oldValue = e.value;
            // onlyIfAbsent为false或者旧值为null
            if (!onlyIfAbsent || oldValue == null)
                //用新值替换旧值
                e.value = value;
            // 访问后回调
            afterNodeAccess(e);
            // 返回旧值
            return oldValue;
        }
    }
    // 结构性修改
    ++modCount;
    // 步骤⑥:超过最大容量就扩容 
    // 实际大小大于阈值则扩容
    if (++size > threshold)
        resize();
    // 插入后回调
    afterNodeInsertion(evict);
    return null;
}
  1. 判断键值对数组table[i]是否为空或为null,否则执行resize()进行扩容;

  2. 根据键值key计算hash值得到插入的数组索引i,如果table[i]==null,直接新建节点添加,转向 ⑥,如果table[i]不为空,转向③;

  3. 判断table[i]的首个元素是否和key一样,如果相同直接覆盖value,否则转向④,这里的相同指的是hashCode以及equals;

  4. 判断table[i] 是否为treeNode,即table[i] 是否是红黑树,如果是红黑树,则直接在树中插入键值对,否则转向5;

  5. 遍历table[i],判断链表长度是否大于8,大于8的话把链表转换为红黑树,在红黑树中执行插入操作,否则进行链表的插入操作;遍历过程中若发现key已经存在直接覆盖value即可;

  6. 插入成功后,判断实际存在的键值对数量size是否超多了最大容量threshold,如果超过,进行扩容。

1.11 ConcurrentHashMap 具体实现?

JDK1.7

首先将数据分为一段一段的存储,然后给每一段数据配一把锁,当一个线程占用锁访问其中一个段 数据时,其他段的数据也能被其他线程访问。

在JDK1.7中,ConcurrentHashMap采用Segment + HashEntry的方式进行实现

一个 ConcurrentHashMap 里包含一个 Segment 数组。Segment 的结构和HashMap类似,是一 种数组和链表结构,一个 Segment 包含一个 HashEntry 数组,每个 HashEntry 是一个链表结构 的元素,每个 Segment 守护着一个HashEntry数组里的元素,当对 HashEntry 数组的数据进行修 改时,必须首先获得对应的 Segment的锁。

Segment 是一种可重入的锁 ReentrantLock,每个 Segment 守护一个HashEntry 数组里的元素,当对 HashEntry 数组的数据进行修改时,必须首先获得对应的 Segment 锁。

JDK1.8

在JDK1.8中,放弃了Segment臃肿的设计,取而代之的是采用Node + CAS + Synchronized来保 证并发安全进行实现,synchronized只锁定当前链表或红黑二叉树的首节点,这样只要hash不冲 突,就不会产生并发 , 效率得到提升

 1.12 try catch finally,try里有return,finally还执行么?

执行,并且finally的执行早于try里面的return
结论:

  1. 不管有没有出现异常,finally块中代码都会执行;
  2. 当try和catch中有return时,finally仍然会执行;
  3. finally是在return后面的表达式运算后执行的(此时并没有返回运算后的值,而是先把要返回的值保存起来,管finally中的代码怎么样,返回的值都不会改变,仍然是之前保存的值),所以函数返回值是在finally执行前确定的;
  4. finally中最好不要包含return,否则程序会提前退出,返回值不是try或catch中保存的返回值。

1.13 谈谈Java中的Exception和Error 

Java可抛出(Throwable)的结构分为三种类型:被检查的异常(CheckedException),运行时异常
(RuntimeException),错误(Error)。

  • 运行时异常

定义:RuntimeException及其子类都被称为运行时异常。
特点:Java编译器不会检查它。也就是说,当程序中可能出现这类异常时,倘若既"没有通过throws声明抛出它",也"没有用try-catch语句捕获它",还是会编译通过。例如,除数为零时产生的ArithmeticException异常,数组越界时产生的IndexOutOfBoundsException异常,failfast机制产生的ConcurrentModificationException异常。

  • 被检查异常

定义:Exception类本身,以及Exception的子类中除了"运行时异常"之外的其它子类都属于被检查异常。特点 : Java编译器会检查它。此类异常,要么通过throws进行声明抛出,要么通过try-catch进行捕获处理,否则不能通过编译。例如,CloneNotSupportedException就属于被检查异常。当通过clone()接口去克隆一个对象,而该对象对应的类没有实现Cloneable接口,就会抛出CloneNotSupportedException异常。被检查异常通常都是可以恢复的。
如:
IOException
FileNotFoundException
SQLException
被检查的异常适用于那些不是因程序引起的错误情况,比如:读取文件时文件不存在引发的FileNotFoundException 。然而,不被检查的异常通常都是由于糟糕的编程引起的,比如:在对象引用时没有确保对象非空而引起的 NullPointerException 。

  • 错误

定义 : Error类及其子类。
特点 : 和运行时异常一样,编译器也不会对错误进行检查。当资源不足、约束失败、或是其它程序无法继续运行的条件发生时,就产生错误。程序本身无法修复这些错误的。例如,VirtualMachineError就属于错误。出现这种错误会导致程序终止运行。OutOfMemoryError、
ThreadDeath。

1.14 Java中的IO流有哪些?

按照流的流向分,可以分为输入流和输出流;

按照操作单元划分,可以划分为字节流和字符流;

按照流的角色划分为节点流和处理流。

1.15 BIO,NIO,AIO 有什么区别?介绍一下NIO

BIO:Block IO 同步阻塞式 IO,就是我们平常使用的传统 IO,它的特点是模式简单使用方便,并发处理能力低。

NIO:Non IO 同步非阻塞 IO,是传统 IO 的升级,客户端和服务器端通过 Channel(通道)通讯,实现了多路复用

AIO:Asynchronous IO 是 NIO 的升级,也叫 NIO2,实现了异步非堵塞 IO ,异步 IO 的操作基于事件和回调机制。

NIO 主要有三大核心部分: Channel(通道) Buffer(缓冲区), Selector。传统 IO 基于字节流和字符流进行操作, 而 NIO 基于 Channel 和Buffer(缓冲区)进行操作,数据总是从通道读取到缓冲区中,或者从缓冲区写入到通道中。 Selector(选择区)用于监听多个通道的事件(比如:连接打开,数据到达)。因此,单个线程可以监听多个数据通道。 NIO 和传统 IO 之间第一个最大的区别是, IO 是面向流的, NIO 是面向缓冲区的。

首先说一下 Channel,国内大多翻译成“通道”。 Channel 和 IO 中的 Stream(流)是差不多一个等级的。 只不过 Stream 是单向的,譬如:InputStream, OutputStream, 而 Channel 是双向的,既可以用来进行读操作,又可以用来进行写操作。NIO 中的 Channel 的主要实现有:

1. FileChannel
2. DatagramChannel
3. SocketChannel
4. ServerSocketChannel
这里看名字就可以猜出个所以然来:分别可以对应文件 IO、 UDP 和 TCP(Server 和 Client)。

Buffer,故名思意, 缓冲区,实际上是一个容器,是一个连续数组。 Channel 提供从文件、网络读取数据的渠道,但是读取或写入的数据都必须经由 Buffer

上面的图描述了从一个客户端向服务端发送数据,然后服务端接收数据的过程。客户端发送数据时,必须先将数据存入 Buffer 中,然后将Buffer 中的内容写入通道。服务端这边接收数据必须通过 Channel 将数据读入到 Buffer 中,然后再从 Buffer 中取出数据来处理 

 Selector,是 NIO 的核心类, Selector 能够检测多个注册的通道上是否有事件发生,如果有事件发生,便获取事件然后针对每个事件进行相应的响应处理。这样一来,只是用一个单线程就可以管理多个通道,也就是管理多个连接。这样使得只有在连接真正有读写事件发生时,才会调用函数来进行读写,就大大地减少了系统开销,并且不必为每个连接都创建一个线程,不用去维护多个线程,并且避免了多线程之间的上下文切换导致的开销。 

1.16 什么是TCP/IP和UDP,TCP与UDP区别

TCP/IP即传输控制/网络协议,是面向连接的协议,发送数据前要先建立连接(发送方和接收方的成 对的两个之间必须建立连接),TCP提供可靠的服务,也就是说,通过TCP连接传输的数据不会丢 失,没有重复,并且按顺序到达。

UDP它是属于TCP/IP协议族中的一种。是无连接的协议,发送数据前不需要建立连接,是没有可 靠性的协议。因为不需要建立连接所以可以在在网络上以任何可能的路径传输,因此能否到达目的 地,到达目的地的时间以及内容的正确性都是不能被保证的。

区别:

  1. TCP是面向连接的协议,发送数据前要先建立连接,TCP提供可靠的服务,也就是说,通过TCP连 接传输的数据不会丢失,没有重复,并且按顺序到达;

  2. UDP是无连接的协议,发送数据前不需要建立连接,是没有可靠性;

  3. TCP通信类似于要打个电话,接通了,确认身份后,才开始进行通信;

  4. UDP通信类似于学校广播,靠着广播播报直接进行通信。

  5. TCP只支持点对点通信,UDP支持一对一、一对多、多对一、多对多;

  6. TCP是面向字节流的,UDP是面向报文的; 面向字节流是指发送数据时以字节为单位,一个数据 包可以拆分成若干组进行发送,而UDP一个报文只能一次发完。

  7. TCP首部开销(20字节)比UDP首部开销(8字节)要大 ,UDP 的主机不需要维持复杂的连接状态表

对某些实时性要求比较高的情况使用UDP,比如游戏,媒体通信,实时直播,即使出现传输错误也可以容忍;其它大部分情况下,HTTP都是用TCP,因为要求传输的内容可靠,不出现丢失的情况

1.17  从输入地址到获得页面的过程 ?

  1. 浏览器查询 DNS,获取域名对应的IP地址:具体过程包括浏览器搜索自身的DNS缓存、搜索操作系统的DNS缓存、读取本地的Host文件和向本地DNS服务器进行查询等。对于向本地DNS服务器进行查询,如果要查询的域名包含在本地配置区域资源中,则返回解析结果给客户机,完成域名解析 (此解析具有权威性);如果要查询的域名不由本地DNS服务器区域解析,但该服务器已缓存了此网 址映射关系,则调用这个IP地址映射,完成域名解析(此解析不具有权威性)。如果本地域名服务 器并未缓存该网址映射关系,那么将根据其设置发起递归查询或者迭代查询;

  2. 浏览器获得域名对应的IP地址以后,浏览器向服务器请求建立链接,发起三次握手;

  3. TCP/IP链接建立起来后,浏览器向服务器发送HTTP请求

  4. 服务器接收到这个请求,并根据路径参数映射到特定的请求处理器进行处理,并将处理结果及相应的视图返回给浏览器;

  5. 浏览器解析并渲染视图,若遇到对js文件、css文件及图片等静态资源的引用,则重复上述步骤并向服务器请求这些资源;

  6. 浏览器根据其请求到的资源、数据渲染页面,最终向用户呈现一个完整的页面。

1.18  什么是TCP的三次握手

在网络数据传输中,传输层协议TCP是要建立连接的可靠传输,TCP建立连接的过程,我们称为三 次握手。

 第一次握手:Client将SYN置1,随机产生一个初始序列号seq发送给Server,进入SYN_SENT状 态;

第二次握手:Server收到Client的SYN=1之后,知道客户端请求建立连接,将自己的SYN置1,ACK 置1,产生一个acknowledge number=sequence number+1,并随机产生一个自己的初始序列 号,发送给客户端;进入SYN_RCVD状态;

第三次握手:客户端检查acknowledge number是否为序列号+1,ACK是否为1,检查正确之后将 自己的ACK置为1,产生一个acknowledge number=服务器发的序列号+1,发送给服务器;进入 ESTABLISHED状态;服务器检查ACK为1和acknowledge number为序列号+1之后,也进入 ESTABLISHED状态;完成三次握手,连接建立。

简单来说就是 :

  1. 客户端向服务端发送SYN

  2. 服务端返回SYN,ACK

  3. 客户端发送ACK

1.19 什么是TCP的四次挥手

在网络数据传输中,传输层协议断开连接的过程我们称为四次挥手

第一次挥手:Client将FIN置为1,发送一个序列号seq给Server;进入FIN_WAIT_1状态;

第二次挥手:Server收到FIN之后,发送一个ACK=1,acknowledge number=收到的序列号+1; 进入CLOSE_WAIT状态。此时客户端已经没有要发送的数据了,但仍可以接受服务器发来的数据。

第三次挥手:Server将FIN置1,发送一个序列号给Client;进入LAST_ACK状态;

第四次挥手:Client收到服务器的FIN后,进入TIME_WAIT状态;接着将ACK置1,发送一个 acknowledge number=序列号+1给服务器;服务器收到后,确认acknowledge number后,变为 CLOSED状态,不再向客户端发送数

1.20 http和https的区别?

其实HTTPS就是从HTTP加上加密处理(一般是SSL安全通信线路)+认证+完整性保护

区别:

  1. https需要拿到ca证书,需要钱的

  2. 端口不一样,http是80,https443

  3. http是超文本传输协议,信息是明文传输,https则是具有安全性的ssl加密传输协议。

  4. http和https使用的是完全不同的连接方式(http的连接很简单,是无状态的;HTTPS 协议是 由SSL+HTTP协议构建的可进行加密传输、身份认证的网络协议,比http协议安全。)

 1.21 一次完整的HTTP请求所经历几个步骤?

  1. 建立TCP连接

  2. Web浏览器向Web服务器发送请求行

  3. Web浏览器发送请求头

  4. Web服务器应答

  5. Web服务器发送应答头

  6. Web服务器向浏览器发送数据

  7. Web服务器关闭TCP连接

1.22 常用HTTP状态码是怎么分类的,有哪些常见的状态码?

HTTP状态码表示客户端HTTP请求的返回结果、标识服务器处理是否正常、表明请求出现的错误等

常见状态码 :

 1.23 HTTP请求中GET与POST方式的区别?

1、GET请求:请求的数据会附加在URL之后,以?分割URL和传输数据,多个参数用&连接。URL的编码格式采用的是ASCII编码,而不是uniclde,即是说所有的非ASCII字符都要编码之后再传输。

POST请求:POST请求会把请求的数据放置在HTTP请求包的包体中。

因此,GET请求的数据会暴露在地址栏中,而POST请求则不会。

2、传输数据的大小

在HTTP规范中,没有对URL的长度和传输的数据大小进行限制。但是在实际开发过程中,对于GET,特定的浏览器和服务器对URL的长度有限制。因此,在使用GET请求时,传输数据会受到URL长度的限制。

对于POST,由于不是URL传值,理论上是不会受限制的,但是实际上各个服务器会规定对POST提交数据大小进行限制,Apache、IIS都有各自的配置。

3、安全性

POST的安全性比GET的高。比如,在进行登录操作,通过GET请求,用户名和密码都会暴露再URL上,因为登录页面有可能被浏览器缓存以及其他人查看浏览器的历史记录的原因,此时的用户名和密码就很容易被他人拿到了。除此之外,GET请求提交的数据还可能会造成Cross-site request frogery攻击

4、HTTP中的GET,POST都是在HTTP上运行的

1.24 Java线程实现/创建的方式有哪几种?

  • 继承Thread类

Thread 类本质上是实现了 Runnable 接口的一个实例,代表一个线程的实例。启动线程的唯一方
法就是通过 Thread 类的 start()实例方法。start()方法是一个 native 方法,它将启动一个新线
程,并执行 run()方法

public class MyThread extends Thread { 
 public void run() { 
 System.out.println("MyThread.run()"); 
 } 
} 
MyThread myThread1 = new MyThread(); 
myThread1.start();
  • 实现Runnable接口

如果自己的类已经 extends 另一个类,就无法直接 extends Thread,此时,可以实现一个
Runnable 接口

public class MyThread extends OtherClass implements Runnable { 
 public void run() { 
 System.out.println("MyThread.run()"); 
 } 
} 

//启动 MyThread,需要首先实例化一个 Thread,并传入自己的 MyThread 实例:
MyThread myThread = new MyThread(); 
Thread thread = new Thread(myThread); 
thread.start(); 
  • 使用Callable和Future创建线程

和Runnable接口不一样,Callable接口提供了一个call()方法作为线程执行体,call()方法比run()方法功能要强大。

1.call()方法可以有返回值

2.call()方法可以声明抛出异常

 

public class Test1 {

    public static void main(String[] args) throws ExecutionException, InterruptedException {
        MyThread3 thread3 = new MyThread3();
        FutureTask<Integer> task = new FutureTask<Integer>(thread3);
        Thread thread = new Thread(task);
        thread.start();
        System.out.println("线程返回值:"+task.get());
    }

}

class MyThread3 implements Callable {
    @Override
    public Object call() throws Exception {
        return 100;
    }
}
  • 基于线程池的方式 

线程和数据库连接这些资源都是非常宝贵的资源。那么每次需要的时候创建,不需要的时候销
毁,是非常浪费资源的。那么我们就可以使用缓存的策略,也就是使用线程池。

public class MyThread4 {

    public static void main(String[] args) {

        ExecutorService threadPool = Executors.newFixedThreadPool(10);
        while (true) {
            // 提交多个线程任务,并执行
            threadPool.execute(new Runnable() {
                @Override
                public void run() {
                    System.out.println(Thread.currentThread().getName()+":is running...");
                    try {
                        Thread.sleep(3000);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
            });
        }

    }
}

1.25 常见的4种线程池是哪些?

 Java里面线程池的顶级接口是Executor,但是严格意义上讲Executor并不是一个线程池,而是一个执行线程的工具,真正的线程池接口是ExecutorService。

  • newSingleThreadExecutor

Executors.newSingleThreadExecutor()返回一个线程池(这个线程池只有一个线程),这个线程池可以在线程死后(或发生异常时)重新启动一个线程来替代原来的线程继续执行下去。

  • newFixedThreadPool

创建一个可重用固定线程数的线程池,以共享的无界队列方式来运行这些线程。在任意点,在大
多数 nThreads 线程会处于处理任务的活动状态。如果在所有线程处于活动状态时提交附加任务,
则在有可用线程之前,附加任务将在队列中等待。如果在关闭前的执行期间由于失败而导致任何
线程终止,那么一个新线程将代替它执行后续的任务(如果需要)。在某个线程被显式地关闭之
前,池中的线程将一直存在

  • newCachedThreadPool

创建一个可根据需要创建新线程的线程池,但是在以前构造的线程可用时将重用它们。对于执行
很多短期异步任务的程序而言,这些线程池通常可提高程序性能。调用 execute 将重用以前构造
的线程(如果线程可用)。如果现有线程没有可用的,则创建一个新线程并添加到池中。终止并
从缓存中移除那些已有 60 秒钟未被使用的线程。因此,长时间保持空闲的线程池不会使用任何资

  • newScheduledThreadPool

创建一个线程池,它可安排在给定延迟后运行命令或者定期地执行。

ScheduledExecutorService scheduledThreadPool= Executors.newScheduledThreadPool(3); 
 scheduledThreadPool.schedule(new Runnable(){ 
 @Override 
 public void run() {
 System.out.println("延迟三秒");
 }
 }, 3, TimeUnit.SECONDS);
scheduledThreadPool.scheduleAtFixedRate(new Runnable(){ 
 @Override 
 public void run() {
 System.out.println("延迟 1 秒后每三秒执行一次");
 }
 },1,3,TimeUnit.SECONDS);

注:

项目中创建多线程时,使用常见的三种线程池创建方式,单一、可变、定长都有一定问题,原因是 FixedThreadPool 和 SingleThreadExecutor 底层都是用LinkedBlockingQueue 实现的,这个队列最大长度为 Integer.MAX_VALUE,容易导致 OOM。所以实际生产一般自己通过 ThreadPoolExecutor 的 7 个参数,自定义线程池。

为什么不允许适用不允许 Executors.的方式手动创建线程池,如下图

 

 1.26 线程的生命周期

当线程被创建并启动以后,它既不是一启动就进入执行状态,也不是一直处于执行状态。在线程的生命周期中,它要经过新建(New)、就绪(Runnable)、运行(Running)、阻塞(Blocked)和死亡(Dead)5种状态。尤其是当线程启动以后,它不可能一直“霸占”着CPU独自运行,所以CPU需要在多条线程之间切换,于是线程状态也会多次在运行、阻塞之间切换。

1.26.1 新建状态(New)

当程序使用 new 关键字创建了一个线程之后,该线程就处于新建状态,此时仅由 JVM 为其分配
内存,并初始化其成员变量的值

1.26.2 就绪状态(Runnable)

当线程对象调用了 start()方法之后,该线程处于就绪状态。Java 虚拟机会为其创建方法调用栈和
程序计数器,等待调度运行。

1.26.3 运行状态(Running)

如果处于就绪状态的线程获得了 CPU,开始执行 run()方法的线程执行体,则该线程处于运行状

1.26.4 阻塞状态(Blocked)

阻塞状态是指线程因为某种原因放弃了 cpu 使用权,也即让出了 cpu timeslice,暂时停止运行。
直到线程进入可运行(runnable)状态,才有机会再次获得 cpu timeslice 转到运行(running)状
态。阻塞的情况分三种:

  • 等待阻塞(o.wait->等待对列):

运行(running)的线程执行 o.wait()方法,JVM 会把该线程放入等待队列(waitting queue)中。

  • 同步阻塞(lock->锁池)

运行(running)的线程在获取对象的同步锁时,若该同步锁被别的线程占用,则 JVM 会把该线
程放入锁池(lock pool)中。

  • 其他阻塞(sleep/join)

运行(running)的线程执行 Thread.sleep(long ms)或 t.join()方法,或者发出了 I/O 请求时,
JVM 会把该线程置为阻塞状态。当 sleep()状态超时、join()等待线程终止或者超时、或者 I/O
处理完毕时,线程重新转入可运行(runnable)状态。

1.26.5 线程死亡(Dead)

线程会以下面三种方式结束,结束后就是死亡状态。

  • 正常结束

run()或 call()方法执行完成,线程正常结束。

  • 异常结束

线程抛出一个未捕获的 Exception 或 Error。

  • 调用 stop

直接调用该线程的 stop()方法来结束该线程。该方法通常容易导致死锁,不推荐使用

1.27 sleep()和wait()方法的区别?

  1. sleep()属于Thread类的,而wait()属于Object类
  2. 最重要的区别:sleep()没有释放锁,而wait()释放了锁,进入等待此对象的等待锁定池,只有针对此对象调用 notify()方法后本线程才进入对象锁定池准备获取对象锁进入运行状态
  3. wait(),notify()和notifyAll()只能在同步控制方法或者同步代码块里面使用,而sleep()可以在任何地方使用
  4. wait()是实例方法,sleep()是静态方法

1.28 Java中的锁有哪些?

1.28.1 乐观锁

乐观锁是一种乐观思想,即认为读多写少,遇到并发写的可能性低,每次去拿数据的时候都认为
别人不会修改,所以不会上锁,但是在更新的时候会判断一下在此期间别人有没有去更新这个数
据,采取在写时先读出当前版本号,然后加锁操作(比较跟上一次的版本号,如果一样则更新),
如果失败则要重复读-比较-写的操作。

Java中的乐观锁基本都是通过CAS操作实现的,CAS是一种更新的原子操作,比较当前值跟传入值是否一样,一样则更新,否则失败。

1.28.2 悲观锁

悲观锁就是悲观思想,认为写多,遇到并发的可能性高,每次去拿数据的时候都认为别人会修改,所以每次在读写数据的时候都会上锁,这样别人读写数据就会阻塞直到拿到锁。

Java中的悲观锁就是synchronized,AQS框架下的锁则是先尝试CAS乐观锁去获取锁,获取不到才会转为悲观锁,如ReentrantLock。

1.28.3 自旋锁

自旋锁是指当一个线程在获取锁的时候,如果锁已经被其它线程获取,那么该线程将循环等待,然后不断的判断锁是否能够被成功获取,直到获取到锁才会退出循环。

自旋锁原理很简单,如果持有锁的线程能在很短时间内释放锁资源,那么那些等待竞争锁资源的线程就不需要做用户态和内核态之间的切换进入阻塞挂起状态,他们只需要等一等(自旋),等持有锁的线程释放锁后即可立即获取锁,这样就避免了用户线程和内核的切换的消耗。

线程自旋是需要消耗CPU的,说白了就是让CPU在做无用功,如果一直获取不到锁,那么线程也不能一直占用CPU自旋做无用功,所以需要设定一个自旋等待的最大时间。如果持有锁的线程执行的时间超过自旋等待的最大时间仍没有释放锁,这会导致其他争用锁的线程在最大等待时间内还是获取不到锁,这时争用线程会停止自旋进入阻塞状态。

一个自旋锁的简单例子:

private AtomicReference<Thread> cas = new AtomicReference<Thread>();

    public void lock() {
        Thread current = Thread.currentThread();
        // 利用CAS
        while (!cas.compareAndSet(null, current)) {
            // DO nothing
        }
    }
    public void unlock() {
        Thread current = Thread.currentThread();
        cas.compareAndSet(current, null);
    }

自旋锁的优点:

  1. 自旋锁不会使线程状态发生切换,一直处于用户态,即线程一直都是active的;不会使线程进入阻塞状态,减少了不必要的上下文切换,执行速度快
  2. 非自旋锁在获取不到锁的时候会进入阻塞状态,从而进入内核态,当获取到锁的时候需要从内核态恢复,需要线程上下文切换。 (线程被阻塞后便进入内核(Linux)调度状态,这个会导致系统在用户态与内核态之间来回切换,严重影响锁的性能)

自旋锁的缺点:

  1. 如果某个线程持有锁的时间过长,就会导致其它等待获取锁的线程进入循环等待,消耗CPU。使用不当会造成CPU使用率极高。
  2. Java实现的自旋锁不是公平的,即无法满足等待时间最长的线程优先获取锁。不公平的锁就会存在“线程饥饿”问题。

1.28.4 Synchronized同步锁

synchronized 它可以把任意一个非 NULL 的对象当作锁。他属于独占式的悲观锁,同时属于可重
入锁。

Synchronized 作用范围

  • 作用于方法时,锁住的是对象的实例(this);
  • 当作用于静态方法时,锁住的是Class实例,又因为Class的相关数据存储在永久代PermGen(jdk1.8 则是 metaspace),永久代是全局共享的,因此静态方法锁相当于类的一个全局锁,会锁所有调用该方法的线程;
  • synchronized 作用于一个对象实例时,锁住的是所有以该对象为锁的代码块。它有多个队列,当多个线程一起访问某个对象监视器的时候,对象监视器会将这些线程存储在不同的容器中。

Synchronized 核心组件
        1) Wait Set:哪些调用 wait 方法被阻塞的线程被放置在这里;
        2) Contention List:竞争队列,所有请求锁的线程首先被放在这个竞争队列中;
        3) Entry List:Contention List 中那些有资格成为候选资源的线程被移动到 Entry List 中;
        4) OnDeck:任意时刻,最多只有一个线程正在竞争锁资源,该线程被成为 OnDeck;
        5) Owner:当前已经获取到所资源的线程被称为 Owner;
        6) !Owner:当前释放锁的线程。

Synchronized 实现

1. JVM 每次从队列的尾部取出一个数据用于锁竞争候选者(OnDeck),但是并发情况下,
ContentionList 会被大量的并发线程进行 CAS 访问,为了降低对尾部元素的竞争,JVM 会将
一部分线程移动到 EntryList 中作为候选竞争线程。
2. Owner 线程会在 unlock 时,将 ContentionList 中的部分线程迁移到 EntryList 中,并指定
EntryList 中的某个线程为 OnDeck 线程(一般是最先进去的那个线程)。
3. Owner 线程并不直接把锁传递给 OnDeck 线程,而是把锁竞争的权利交给 OnDeck,
OnDeck 需要重新竞争锁。这样虽然牺牲了一些公平性,但是能极大的提升系统的吞吐量,在
JVM 中,也把这种选择行为称之为“竞争切换”。
4. OnDeck 线程获取到锁资源后会变为 Owner 线程,而没有得到锁资源的仍然停留在 EntryList
中。如果 Owner 线程被 wait 方法阻塞,则转移到 WaitSet 队列中,直到某个时刻通过 notify
或者 notifyAll 唤醒,会重新进去 EntryList 中。
5. 处于 ContentionList、EntryList、WaitSet 中的线程都处于阻塞状态,该阻塞是由操作系统
来完成的(Linux 内核下采用 pthread_mutex_lock 内核函数实现的)。
6. Synchronized 是非公平锁。 Synchronized 在线程进入 ContentionList 时,等待的线程会先
尝试自旋获取锁,如果获取不到就进入 ContentionList,这明显对于已经进入队列的线程是
不公平的,还有一个不公平的事情就是自旋获取锁的线程还可能直接抢占 OnDeck 线程的锁
资源。
参考:https://blog.csdn.net/zqz_zqz/article/details/70233767
7. 每个对象都有个 monitor 对象,加锁就是在竞争 monitor 对象,代码块加锁是在前后分别加
上 monitorenter 和 monitorexit 指令来实现的,方法加锁是通过一个标记位来判断的
8. synchronized 是一个重量级操作,需要调用操作系统相关接口,性能是低效的,有可能给线
程加锁消耗的时间比有用操作消耗的时间更多。
9. Java1.6,synchronized 进行了很多的优化,有适应自旋、锁消除、锁粗化、轻量级锁及偏向
锁等,效率有了本质上的提高。在之后推出的 Java1.7 与 1.8 中,均对该关键字的实现机理做
了优化。引入了偏向锁和轻量级锁。都是在对象头中有标记位,不需要经过操作系统加锁。
10. 锁可以从偏向锁升级到轻量级锁,再升级到重量级锁。这种升级过程叫做锁膨胀;
11. JDK 1.6 中默认是开启偏向锁和轻量级锁,可以通过-XX:-UseBiasedLocking 来禁用偏向锁

 1.28.5 ReentrantLock

ReentantLock 继承接口 Lock 并实现了接口中定义的方法,他是一种可重入锁,除了能完
成 synchronized 所能完成的所有工作外,还提供了诸如可响应中断锁、可轮询锁请求、定时锁等
避免多线程死锁的方法。

Lock 接口的主要方法:

  1.   void lock(): 执行此方法时, 如果锁处于空闲状态, 当前线程将获取到锁. 相反, 如果锁已经被其他线程持有, 将禁用当前线程, 直到当前线程获取到锁.
  2.   boolean tryLock():如果锁可用, 则获取锁, 并立即返回 true, 否则返回 false. 该方法和lock()的区别在于, tryLock()只是"试图"获取锁, 如果锁不可用, 不会导致当前线程被禁用, 当前线程仍然继续往下执行代码. 而 lock()方法则是一定要获取到锁, 如果锁不可用, 就一直等待, 在未获得锁之前,当前线程并不继续向下执行. 
  3.   void unlock():执行此方法时, 当前线程将释放持有的锁. 锁只能由持有者释放, 如果线程并不持有锁, 却执行该方法, 可能导致异常的发生.
  4.   Condition newCondition():条件对象,获取等待通知组件。该组件和当前的锁绑定,当前线程只有获取了锁,才能调用该组件的 await()方法,而调用后,当前线程将释放锁。
  5.   getHoldCount() :查询当前线程保持此锁的次数,也就是执行此线程执行 lock 方法的次数。
  6.   getQueueLength():返回正等待获取此锁的线程估计数,比如启动 10 个线程,1 个线程获得锁,此时返回的是 9
  7.   getWaitQueueLength:(Condition condition)返回等待与此锁相关的给定条件的线程估计数。比如 10 个线程,用同一个 condition 对象,并且此时这 10 个线程都执行了condition 对象的 await 方法,那么此时执行此方法返回 10
  8.   hasWaiters(Condition condition):查询是否有线程等待与此锁有关的给定条(condition),对于指定 contidion 对象,有多少线程执行了 condition.await 方法
  9.   hasQueuedThread(Thread thread):查询给定线程是否等待获取此锁
  10.    hasQueuedThreads():是否有线程等待此锁
  11.   isFair():该锁是否公平锁
  12.   isHeldByCurrentThread(): 当前线程是否保持锁锁定,线程的执行 lock 方法的前后分别是 false 和 true
  13.   isLock():此锁是否有任意线程占用
  14.   lockInterruptibly():如果当前线程未被中断,获取锁
  15.   tryLock():尝试获得锁,仅在调用时锁未被线程占用,获得锁
  16.   tryLock(long timeout TimeUnit unit):如果锁在给定等待时间内没有被另一个线程保持,则获取该锁

Condition 类和 Object 类锁方法区别:
1. Condition 类的 awiat 方法和 Object 类的 wait 方法等效
2. Condition 类的 signal 方法和 Object 类的 notify 方法等效
3. Condition 类的 signalAll 方法和 Object 类的 notifyAll 方法等效
4. ReentrantLock 类可以唤醒指定条件的线程,而 object 的唤醒是随机的

1.28.6 可重入锁(递归锁)

可重入锁,也叫做递归锁,指的是同一线程外层函数获得锁之后 ,内层递归函数仍然有获取该锁的代码,但不受影响。在 JAVA 环境下 ReentrantLock 和 synchronized 都是可重入锁。

1.28.7 公平锁与非公平锁

公平锁(Fair)
加锁前检查是否有排队等待的线程,优先排队等待的线程,先来先得

非公平锁(Nonfair)

JVM 按随机、就近原则分配锁的机制则称为非公平锁,ReentrantLock 在构造函数中提供了
是否公平锁的初始化方式,默认为非公平锁。非公平锁实际执行的效率要远远超出公平锁,除非
程序有特殊需要,否则最常用非公平锁的分配机制。

加锁时不考虑排队等待问题,直接尝试获取锁,获取不到自动到队尾等待
1. 非公平锁性能比公平锁高 5~10 倍,因为公平锁需要在多核的情况下维护一个队列
2. Java 中的 synchronized 是非公平锁,ReentrantLock 默认的 lock()方法采用的是非公平锁。

1.28.8 ReadWriteLock(读写锁)

为了提高性能,Java 提供了读写锁,在读的地方使用读锁,在写的地方使用写锁,灵活控制,如
果没有写锁的情况下,读是无阻塞的,在一定程度上提高了程序的执行效率。读写锁分为读锁和写
锁,多个读锁不互斥,读锁与写锁互斥,这是由 jvm 自己控制的,你只要上好相应的锁即可。

  • 读锁

如果你的代码只读数据,可以很多人同时读,但不能同时写,那就上读锁

  • 写锁

如果你的代码修改数据,只能有一个人在写,且不能同时读取,那就上写锁。总之,读的时候上
读锁,写的时候上写锁!Java 中读写锁有个接口 java.util.concurrent.locks.ReadWriteLock ,也有具体的实现ReentrantReadWriteLock

1.28.9 共享锁和独占锁

  • 独占锁

  独占锁模式下,每次只能有一个线程能持有锁,ReentrantLock 就是以独占方式实现的互斥锁。
独占锁是一种悲观保守的加锁策略,它避免了读/读冲突,如果某个只读线程获取锁,则其他读线
程都只能等待,这种情况下就限制了不必要的并发性,因为读操作并不会影响数据的一致性。

  • 共享锁

  共享锁则允许多个线程同时获取锁,并发访问共享资源,如:ReadWriteLock。共享锁则是一种
乐观锁,它放宽了加锁策略,允许多个执行读操作的线程同时访问共享资源。
1. AQS 的内部类 Node 定义了两个常量 SHARED 和 EXCLUSIVE,他们分别标识 AQS 队列中等
待线程的锁获取模式。
2. java 的并发包中提供了 ReadWriteLock,读-写锁。它允许一个资源可以被多个读操作访问,
或者被一个写操作访问,但两者不能同时进行。

1.28.10 重量级锁

  Synchronized 是通过对象内部的一个叫做监视器锁(monitor)来实现的。但是监视器锁本质又
是依赖于底层的操作系统的 Mutex Lock (互斥锁)来实现的。而操作系统实现线程之间的切换这就需要从用户态转换到核心态,这个成本非常高,状态之间的转换需要相对比较长的时间,这就是为什么Synchronized 效率低的原因。因此,这种依赖于操作系统 Mutex Lock 所实现的锁我们称之为“重量级锁”。JDK 中对 Synchronized 做的种种优化,其核心都是为了减少这种重量级锁的使用。JDK1.6 以后,为了减少获得锁和释放锁所带来的性能消耗,提高性能,引入了“轻量级锁”和“偏向锁"

1.28.11 轻量级锁

  锁的状态总共有四种:无锁状态偏向锁轻量级锁重量级锁
锁升级
  随着锁的竞争,锁可以从偏向锁升级到轻量级锁,再升级到重量级锁(但是锁的升级是单向的,
也就是说只能从低到高升级,不会出现锁的降级)。

轻量级锁一般也被叫做非阻塞同步、乐观锁,因为它执行的这个过程并没有把线程阻塞挂起,反而让线程空循环等待,串行执行。

轻量级锁不是用来替代传统的重量级锁的,而是在没有多线程竞争的情况下,使用轻量级锁能够减少性能消耗,但是当多个线程同时竞争锁时,轻量级锁会膨胀为重量级锁。

轻量级锁主要有两种:自旋锁自适应自旋锁

1.28.12 偏向锁

通俗的讲,偏向锁就是在运行的过程中,对象的锁偏向某个线程。即在开启偏向锁机制的情况下,某个线程获得锁,当该线程下次再想获得锁时,不需要再获得锁(即忽略synchronized关键字),直接就可以执行同步代码,比较适合竞争较少的情况。

1.28.13 分段锁

分段锁也并非一种实际的锁,而是一种思想, ConcurrentHashMap 是学习分段锁的最好实践

1.28.14 死锁 

多个线程同时被阻塞,它们中的一个或者全部都在等待某个资源被释放。由于线程被无限期地阻塞,因此程序不可能正常终止,就导致了死锁

Java 死锁产生的四个必要条件:

  • 互斥使用,即当资源被一个线程使用(占有)时,别的线程不能使用
  • 不可抢占,资源请求者不能强制从资源占有者手中夺取资源,资源只能由资源占有者主动释放。
  • 请求和保持,即当资源请求者在请求其他的资源的同时保持对原有资源的占有。
  • 循环等待,即存在一个等待队列:P1占有P2的资源,P2占有P3的资源,P3占有P1的资源。这样就形成了一个等待环路。

当上述四个条件都成立的时候,便形成死锁。当然,死锁的情况下如果打破上述任何一个条件,便可让死锁消失。

死锁示例:

public class DeadLock {

    public static String obj1 = "obj1";
    public static String obj2 = "obj2";

    public static void main(String[] args) {
        LockA lockA = new LockA();
        LockB lockB = new LockB();
        new Thread(lockA).start();
        new Thread(lockB).start();
    }

}

class LockA implements Runnable{
    @Override
    public void run() {

        try {
            System.out.println("LockA开始执行。。。。");
            while (true) {

                synchronized (DeadLock.obj1) {
                    System.out.println("LockA锁住obj1");
                    Thread.sleep(3000);
                    synchronized (DeadLock.obj2) {
                        System.out.println("LockA锁住obj2");
                    }
                }

            }
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}

class LockB implements Runnable{
    @Override
    public void run() {

        try {
            System.out.println("LockB开始执行。。。。");
            while (true) {

                synchronized (DeadLock.obj2) {
                    System.out.println("LockB锁住obj2");
                    Thread.sleep(3000);
                    synchronized (DeadLock.obj1) {
                        System.out.println("LockA锁住obj1");
                    }
                }

            }
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}

1.29 锁的优化方式有哪些? 

  • 减少锁持有时间

只用在有线程安全要求的程序上加锁

  • 减小锁粒度

将大对象(这个对象可能会被很多线程访问),拆成小对象,大大增加并行度,降低锁竞争。
降低了锁的竞争,偏向锁,轻量级锁成功率才会提高。最最典型的减小锁粒度的案例就是
ConcurrentHashMap。

  • 锁分离

最常见的锁分离就是读写锁 ReadWriteLock,根据功能进行分离成读锁和写锁,这样读读不互斥,读写互斥,写写互斥,即保证了线程安全,又提高了性能。读写分离思想可以延伸,只要操作互不影响,锁就可以分离。比如 LinkedBlockingQueue 从头部取出,从尾部放数据

  • 锁粗化

通常情况下,为了保证多线程间的有效并发,会要求每个线程持有锁的时间尽量短,即在使用完
公共资源后,应该立即释放锁。但是,凡事都有一个度,如果对同一个锁不停的进行请求、同步
和释放,其本身也会消耗系统宝贵的资源,反而不利于性能的优化 。

  • 锁消除

锁消除是在编译器级别的事情。在即时编译器时,如果发现不可能被共享的对象,则可以消除这
些对象的锁操作,多数是因为程序员编码不规范引起。

1.30 谈谈对线程池的了解?

线程池(英语:thread pool):一种线程使用模式。线程过多会带来调度开销,进而影响缓存局部性和整体性能。而线程池维护着多个线程,等待着监督管理者分配可并发执行的任务。这避免了在处理短时间任务时创建与销毁线程的代价。线程池不仅能够保证内核的充分利用,还能防止过分调度。

一般的线程池主要分为以下4个部分组成:

  1.  线程池管理器:用于创建并管理线程池
  2. 工作线程:线程池中的线程
  3. 任务接口:每个任务必须实现的接口,用于工作线程调度其运行
  4.  任务队列:用于存放待处理的任务,提供一种缓冲机制

Java 中的线程池是通过 Executor 框架实现的,该框架中用到了 Executor,Executors,
ExecutorService,ThreadPoolExecutor ,Callable 和 Future、FutureTask 这几个类。

 ThreadPoolExecutor的构造方法如下:

public ThreadPoolExecutor(int corePoolSize,
                              int maximumPoolSize,
                              long keepAliveTime,
                              TimeUnit unit,
                              BlockingQueue<Runnable> workQueue,
                              ThreadFactory threadFactory,
                              RejectedExecutionHandler handler) {
}

 线程池7大参数:

1. corePoolSize:指定了线程池中的核心线程数量。
2. maximumPoolSize:指定了线程池中的最大线程数量。
3. keepAliveTime:空闲线程的存活时间
4. unit:keepAliveTime 的单位。
5. workQueue:存放提交但未执行任务的队列
6. threadFactory:线程工厂,用于创建线程,一般用默认的即可。
7. handler:拒绝策略,当任务太多来不及处理,如何拒绝任务

线程池中的线程已经用完了,无法继续为新任务服务,同时,等待队列也已经排满了,再也
塞不下新任务了。这时候我们就需要拒绝策略机制合理的处理这个问题。
JDK 内置的拒绝策略如下:
1. AbortPolicy : 直接抛出异常,阻止系统正常运行。
2. CallerRunsPolicy : 只要线程池未关闭,该策略直接在调用者线程中,运行当前被丢弃的
任务。显然这样做不会真的丢弃任务,但是,任务提交线程的性能极有可能会急剧下降。
3. DiscardOldestPolicy : 丢弃最老的一个请求,也就是即将被执行的一个任务,并尝试再
次提交当前任务。
4. DiscardPolicy : 该策略默默地丢弃无法处理的任务,不予任何处理。如果允许任务丢
失,这是最好的一种方案。
以上内置拒绝策略均实现了 RejectedExecutionHandler 接口,若以上策略仍无法满足实际
需要,完全可以自己扩展 RejectedExecutionHandler 接口。

1.31 线程池的工作原理?

  • 线程池刚创建时,里面没有一个线程。任务队列是作为参数传进来的。不过就算队列里有任务,线程池也不会马上执行他们
  • 当调用execute()方法添加一个任务时,线程池会做如下判断:

        a) 如果正在运行的线程数量小于corePoolSize,那么马上创建线程运行这个任务;

        b) 如果正在运行的线程数量大于或等于corePoolSize,那么将这个任务放入队列;

        c) 如果这时候队列满了,而且正在运行的线程数量小于maximumPoolSize,那么还是要创建非核心线程立即运行这个任务;

        d) 如果队列满了,而且正在运行的线程数量大于或等于maximumPoolSize,那么线程池会  启动饱和拒绝策略来执行

  • 当一个线程完成任务时,它会从队列中取下一个任务来执行
  • 当一个线程空闲超过一定时间(keepAliveTime)时,线程池会判断,如果当前运行的线程数大于corePoolSize,那么这个线程就被停掉。所以线程池的所有任务完成后,它最终会收缩到corePoolSize的大小

总结起来,也就是一句话,当提交的任务数大于(workQueue.size() + maximumPoolSize ),就会触发线程池的拒绝策略。

1.32 阻塞队列原理?阻塞队列有哪些?

阻塞队列,关键字是阻塞,先理解阻塞的含义,在阻塞队列中,线程阻塞有这样的两种情况:

1. 当队列中没有数据的情况下,消费者端的所有线程都会被自动阻塞(挂起),直到有数据放
入队列。

2. 当队列中填满数据的情况下,生产者端的所有线程都会被自动阻塞(挂起),直到队列中有
空的位置,线程被自动唤醒。

阻塞队列的主要方法:

  •  抛出异常:抛出一个异常;
  •  特殊值:返回一个特殊值(null 或 false,视情况而定)
  • 阻塞:在成功操作之前,一直阻塞线程
  • 超时:放弃前只在最大的时间内阻塞

Java中的阻塞队列:

1. ArrayBlockingQueue :由数组结构组成的有界阻塞队列。
2. LinkedBlockingQueue :由链表结构组成的有界阻塞队列。
3. PriorityBlockingQueue :支持优先级排序的无界阻塞队列。
4. DelayQueue:使用优先级队列实现的延迟无界阻塞队列。
5. SynchronousQueue:不存储元素的阻塞队列。
6. LinkedTransferQueue:由链表结构组成的无界阻塞队列。
7. LinkedBlockingDeque:由链表结构组成的双向阻塞队列。

 小结:

1. 在多线程领域:所谓阻塞,在某些情况下会挂起线程(即阻塞),一旦条件满足,被挂起的线程又会自动被唤起
2. 为什么需要 BlockingQueue? 在 concurrent 包发布以前,在多线程环境下,我们每个程序员都必须去自己控制这些细节,尤其还要兼顾效率和线程安全,而这会给我们的程序带来不小的复杂度。使用后我们不需要关心什么时候需要阻塞线程,什么时候需要唤醒线程,因为这一切 BlockingQueue 都给你一手包办了

 1.33 写出一个线程通信的例子(生产者消费者问题)

两个线程,一个线程对当前数值加 1,另一个线程对当前数值减 1,要求用线程间通信

示例:

public class MyThread7 {

    public static void main(String[] args) {

        ThreadDemo threadDemo = new ThreadDemo();
        new Thread(() -> {
            for (int i = 1; i <= 20; i++) {
                threadDemo.incr();
            }
        }, "线程a").start();
        new Thread(() -> {
            for (int i = 1; i <= 20; i++) {
                threadDemo.decr();
            }
        }, "线程b").start();

    }

}

class ThreadDemo {

    private int number = 0;

    private Lock lock = new ReentrantLock();

    private Condition condition = lock.newCondition();

    public void incr() {

        try {
            lock.lock();
            while (number != 0) {
                condition.await();
            }
            number++;
            System.out.println("*****" + Thread.currentThread().getName() + "加1操作成功,值为:" + number);
            condition.signalAll();
        } catch (InterruptedException e) {
            e.printStackTrace();
        } finally {
            lock.unlock();
        }

    }

    public void decr() {

        try {
            lock.lock();
            while (number == 0) {
                condition.await();
            }
            number--;
            System.out.println("*****" + Thread.currentThread().getName() + "减1操作成功,值为:" + number);
            condition.signalAll();
        } catch (InterruptedException e) {
            e.printStackTrace();
        } finally {
            lock.unlock();
        }

    }


}

1.34 JUC三大辅助类 CountDownLatch(线程计数器)、 CyclicBarrier(回环栅栏) 、 Semaphore(信号量)

1.34.1 CountDownLatch

        CountDownLatch 类可以设置一个计数器,然后通过 countDown 方法来进行减 1 的操作,使用 await 方法等待计数器不大于 0,然后继续执行 await 方法之后的语句。

        CountDownLatch 主要有两个方法,当一个或多个线程调用 await 方法时,这
些线程会阻塞。其它线程调用 countDown 方法会将计数器减 1(调用 countDown 方法的线程
不会阻塞) 当计数器的值变为 0 时,因 await 方法阻塞的线程会被唤醒,继续执行。

示例:6 个同学陆续离开教室后值班同学才可以关门。

public class ThreadDemo6 {

    public static void main(String[] args) throws InterruptedException {

        // 定义一个数值为6的计数器
        CountDownLatch countDownLatch = new CountDownLatch(6);

        for (int i = 1; i <= 6; i++) {
            new Thread(() -> {
                try {
                    if (Thread.currentThread().getName().equals("同学6")) {
                        Thread.sleep(6000);
                    }
                    System.out.println(Thread.currentThread().getName() + "离开了");
                    // 计数器减1,不会阻塞
                    countDownLatch.countDown();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }, "同学" + i).start();
        }

        // 主线程await休息
        System.out.println("主线程睡觉");
        countDownLatch.await();
        System.out.println("全部离开了,现在的计数器为:" + countDownLatch.getCount());
    }


}

1.34.2  CyclicBarrier

        CyclicBarrier 看英文单词可以看出大概就是循环阻塞的意思,在使用中。CyclicBarrier 的构造方法第一个参数是目标障碍数,每次执行 CyclicBarrier 一次障碍数会加一,如果达到了目标障碍数,才会执行 cyclicBarrier.await()之后的语句。可以将 CyclicBarrier 理解为加 1 操作

 示例: 集齐 7 颗龙珠就可以召唤神龙

public class ThreadDemo7 {
    private final static int NUMBER = 7;

    public static void main(String[] args) {

        CyclicBarrier cyclicBarrier = new CyclicBarrier(NUMBER, () -> {
            System.out.println("集齐" + NUMBER + "颗龙珠,开始召唤神龙。。。");
        });

        for (int i = 1; i <= 7; i++) {
            new Thread(() -> {
                System.out.println(Thread.currentThread().getName() + "收集到了!!!!");
                try {
                    cyclicBarrier.await();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                } catch (BrokenBarrierException e) {
                    e.printStackTrace();
                }
            }, "龙珠" + i + "号").start();
        }


    }

}

1.34.3  Semaphore

        Semaphore 翻译成字面意思为 信号量,Semaphore 可以控制同时访问的线程个数,通过
acquire() 获取一个许可,如果没有就等待,而 release() 释放一个许可。

示例: 抢车位, 6 台汽车 3 个停车位

public class ThreadDemo8 {

    public static void main(String[] args) throws Exception {

        Semaphore semaphore = new Semaphore(3);
        for (int i = 1; i <= 6; i++) {
            Thread.sleep(300);

            new Thread(() -> {
                System.out.println(Thread.currentThread().getName() + "找车位ing。。。。");
                try {
                    semaphore.acquire();
                    System.out.println(Thread.currentThread().getName() + "汽车停成功。。。。。");
                    Thread.sleep(5000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                } finally {
                    System.out.println(Thread.currentThread().getName() + "离开了,,,,,,,");
                    semaphore.release();
                }
            }, "汽车" + i).start();
        }


    }

}

小结:

    CountDownLatch 和 CyclicBarrier 都能够实现线程之间的等待,只不过它们侧重点不
同;CountDownLatch 一般用于某个线程 A 等待若干个其他线程执行完任务之后,它才
执行(减法计数);而 CyclicBarrier 一般用于一组线程互相等待至某个状态,然后这一组线程再同时执行(加法计数);另外,CountDownLatch 是不能够重用的,而 CyclicBarrier 是可以重用的。Semaphore 其实和锁有点类似,它一般用于控制对某组资源的访问权限。

1.35 volatile 关键字的作用

        Java 语言提供了一种稍弱的同步机制,即 volatile 变量,用来确保将变量的更新操作通知到其他线程。volatile 变量具备两种特性,volatile 变量不会被缓存在寄存器或者对其他处理器不可见的地方,因此在读取 volatile 类型的变量时总会返回最新写入的值。

主要作用:

  • 变量可见性

这里的可见性指的是当一个线程修改了变量的值,那么新的值对于其他线程是可以立即获取的

  • 禁止重排序

volatile禁止了指令重排

注:在访问 volatile 变量时不会执行加锁操作,因此也就不会使执行线程阻塞,因此 volatile 变量是一种比 sychronized 关键字更轻量级的同步机制。volatile 适合这种场景:一个变量被多个线程共享,线程直接给这个变量赋值。

当对非 volatile 变量进行读写的时候,每个线程先从内存拷贝变量到 CPU 缓存中。如果计算机有
多个 CPU,每个线程可能在不同的 CPU 上被处理,这意味着每个线程可以拷贝到不同的 CPU 
cache 中。而声明变量是 volatile 的,JVM 保证了每次读变量都从内存中读,跳过 CPU cache 
这一步。 

适用场景:

值得说明的是对volatile变量的单次读/写操作可以保证原子性的,如long和double类型变量,但是并不能保证i++这种操作的原子性,因为本质上i++是读、写两次操作。在某些场景下可以代替synchronized。但是volatile不能完全取代synchronized,只有在一些特殊场景下,才能适用volatile。总的来说,必须同时满足下面两个条件才能保证在并发环境的线程安全:

(1)对变量的写操作不依赖于当前值(比如i++),或者说是单纯的变量赋值(boolean flag =true)

(2)该变量没有包含在具有其他变量的不变式中,也就是说,不同的volatile变量之间,不能互相依赖。只有在状态真正独立于程序内其他内容时才能使用 volatile。

1.36 ThreadLocal 作用(线程本地存储)

ThreadLocal,很多地方叫线程本地变量,也有些地方叫线程本地存储,它的作用是提供线程内的局部变量,这种变量在线程的生命周期内起作用,减少同一个线程内多个函数或者组件之间一些公共变量的传递的复杂度。

ThreadLocalMap(线程的一个属性)

        1. 每个线程中都有一个自己的 ThreadLocalMap 类对象,可以将线程自己的对象保持到其中,各管各的,线程可以正确的访问到自己的对象。
        2. 将一个共用的 ThreadLocal 静态实例作为 key,将不同对象的引用保存到不同线程的
ThreadLocalMap 中,然后在线程执行的各处通过这个静态 ThreadLocal 实例的 get()方法取
得自己线程保存的那个对象,避免了将这个对象作为参数传递的麻烦。
        3. ThreadLocalMap 其实就是线程里面的一个属性,它在 Thread 类中定义
ThreadLocal.ThreadLocalMap threadLocals = null;

使用场景:
最常见的 ThreadLocal 使用场景为用来解决数据库连接、Session 管理等。

示例:

private static final ThreadLocal threadSession = new ThreadLocal(); 
public static Session getSession() throws InfrastructureException { 
 Session s = (Session) threadSession.get(); 
 try { 
 if (s == null) { 
 s = getSessionFactory().openSession(); 
 threadSession.set(s); 
 } 
 } catch (HibernateException ex) { 
 throw new InfrastructureException(ex); 
 } 
 return s; 
}

1.37 synchronized 和 ReentrantLock 的区别

两者的共同点:

  1. 都是用来协调多线程对共享对象、变量的访问
  2. 都是可重入锁,同一线程可以多次获得同一个锁
  3. 都保证了可见性和互斥性

两者的不同点:

  1.  ReentrantLock 显示的获得、释放锁,synchronized隐式获得、释放锁
  2. ReentrantLock是API级别的,synchronized是JVM级别的
  3. ReentrantLock可以实现公平锁
  4.  ReentrantLock 通过Condition可以绑定多个条件
  5. 底层实现不一样,synchronized是同步阻塞,使用的是悲观并发策略,lock是同步非阻塞,采用的是乐观并发策略
  6. Lock是一个接口,而synchronized是Java中的关键字,synchronized是内置的语言实现
  7. synchronized在发生异常时,会自动释放线程占有的锁,因此不会导致死锁现象发生,而Lock在发生异常时,如果没有主动通过unLock()去释放锁,则很可能造成死锁现象,因此使用Lock时需要在finally块中释放锁
  8. Lock 可以让等待锁的线程响应中断,而 synchronized 却不行,使用 synchronized 时,
    等待的线程会一直等待下去,不能够响应中断
  9. 通过 Lock 可以知道有没有成功获取锁,而 synchronized 却无法办到。
  10. Lock 可以提高多个线程进行读操作的效率,即就是实现读写锁等。
     

1.38 介绍下ConcurrentHashMap

  • 减小锁粒度:是指缩小锁定对象的范围,从而减小锁冲突的可能性,从而提高系统的并发能力。减小锁粒度是一种削弱多线程锁竞争的有效手段,这种技术典型的应用是 ConcurrentHashMap(高性能的 HashMap)类的实现。对于 HashMap 而言,最重要的两个方法是 get 与 set 方法,如果我们对整个 HashMap 加锁,可以得到线程安全的对象,但是加锁粒度太大。Segment 的大小也被称为 ConcurrentHashMap 的并发度。
  • ConcurrentHashMap分段锁:ConcurrentHashMap,它内部细分了若干个小的 HashMap,称之为段(Segment)。默认情况下一个 ConcurrentHashMap 被进一步细分为 16 个段,既就是锁的并发度。如果需要在 ConcurrentHashMap 中添加一个新的表项,并不是将整个 HashMap 加锁,而是首先根据 hashcode 得到该表项应该存放在哪个段中,然后对该段加锁,并完成 put 操作。在多线程环境中,如果多个线程同时进行 put操作,只要被加入的表项不存放在同一个段中,则线程间可以做到真正的并行。
  • ConcurrentHashMap 是由 Segment 数组结构和 HashEntry 数组结构组成:Segment 是一种可重入锁 ReentrantLock,在 ConcurrentHashMap 里扮演锁的角色,HashEntry 则用于存储键值对数据。一个 ConcurrentHashMap 里包含一个 Segment 数组,Segment 的结构和 HashMap类似,是一种数组和链表结构, 一个 Segment 里包含一个 HashEntry 数组,每个 HashEntry 是一个链表结构的元素, 每个 Segment 守护一个 HashEntry 数组里的元素,当对 HashEntry 数组的数据进行修改时,必须首先获得它对应的 Segment 锁。

1.39 什么是 CAS(比较并交换-乐观锁机制-锁自旋)

1.39.1 概念及特性

CAS(Compare And Swap/Set)比较并交换,CAS 算法的过程是这样:它包含 3 个参数
CAS(V,E,N)。V 表示要更新的变量(内存值),E 表示预期值(旧的),N 表示新值。当且仅当 V 值等于 E 值时,才会将 V 的值设为 N,如果 V 值和 E 值不同,则说明已经有其他线程做了更新,则当
前线程什么都不做。最后,CAS 返回当前 V 的真实值。CAS 操作是抱着乐观的态度进行的(乐观锁),它总是认为自己可以成功完成操作。当多个线程同时使用 CAS 操作一个变量时,只有一个会胜出,并成功更新,其余均会失败。失败的线程不会被挂起,仅是被告知失败,并且允许再次尝试,当然也允许失败的线程放弃操作。基于这样的原理,CAS 操作即使没有锁,也可以发现其他线程对当前线程的干扰,并进行恰当的处理。

1.39.2 原子包 java.util.concurrent.atomic(锁自旋)

JDK1.5 的原子包:java.util.concurrent.atomic 这个包里面提供了一组原子类。其基本的特性就
是在多线程环境下,当有多个线程同时执行这些类的实例包含的方法时,具有排他性,即当某个
线程进入方法,执行其中的指令时,不会被其他线程打断,而别的线程就像自旋锁一样,一直等
到该方法执行完成,才由 JVM 从等待队列中选择一个另一个线程进入,这只是一种逻辑上的理解。相对于对于 synchronized 这种阻塞算法,CAS 是非阻塞算法的一种常见实现。由于一般 CPU 切换时间比 CPU 指令集操作更加长, 所以 J.U.C 在性能上有了很大的提升。如下代码:

public class AtomicInteger extends Number implements java.io.Serializable {
        private volatile int value;

        public final int get() {
            return value;
        }

        public final int getAndIncrement() {
            for (; ; ) { //CAS 自旋,一直尝试,直达成功
                int current = get();
                int next = current + 1;
                if (compareAndSet(current, next))
                    return current;
            }
        }

        public final boolean compareAndSet(int expect, int update) {
            return unsafe.compareAndSwapInt(this, valueOffset, expect, update);
        }
    }

getAndIncrement 采用了 CAS 操作,每次从内存中读取数据然后将此数据和+1 后的结果进行
CAS 操作,如果成功就返回结果,否则重试直到成功为止。而 compareAndSet 利用 JNI 来完成
CPU 指令的操作。 

1.39.3 ABA问题

CAS 会导致“ABA 问题”。CAS 算法实现一个重要前提需要取出内存中某时刻的数据,而在下时
刻比较并替换,那么在这个时间差类会导致数据的变化。

比如说一个线程 one 从内存位置 V 中取出 A,这时候另一个线程 two 也从内存中取出 A,并且
two 进行了一些操作变成了 B,然后 two 又将 V 位置的数据变成 A,这时候线程 one 进行 CAS 操
作发现内存中仍然是 A,然后 one 操作成功。尽管线程 one 的 CAS 操作成功,但是不代表这个过
程就是没有问题的。
部分乐观锁的实现是通过版本号(version)的方式来解决 ABA 问题,乐观锁每次在执行数据的修改操作时,都会带上一个版本号,一旦版本号和数据的版本号一致就可以执行修改操作并对版本
号执行+1 操作,否则就执行失败。因为每次操作的版本号都会随之增加,所以不会出现 ABA 问
题,因为版本号只会增加不会减少。

1.40 什么是 AQS(抽象的队列同步器)

 AbstractQueuedSynchronizer 类如其名,抽象的队列式的同步器,AQS 定义了一套多线程访问
共享资源的同步器框架,许多同步类实现都依赖于它,如常用的
ReentrantLock/Semaphore/CountDownLatch

 它维护了一个 volatile int state(代表共享资源)和一个 FIFO 线程等待队列(多线程争用资源被
阻塞时会进入此队列)。这里 volatile 是核心关键词,具体 volatile 的语义,在此不述。state 的
访问方式有三种:
getState()
setState()
compareAndSetState()

AQS 定义两种资源共享方式:

  • Exclusive 独占资源-ReentrantLock

Exclusive(独占,只有一个线程能执行,如 ReentrantLock)

  • Share 共享资源-Semaphore/CountDownLatch

Share(共享,多个线程可同时执行,如 Semaphore/CountDownLatch)。


AQS 只是一个框架,具体资源的获取/释放方式交由自定义同步器去实现,AQS 这里只定义了一个
接口,具体资源的获取交由自定义同步器去实现了(通过 state 的 get/set/CAS)之所以没有定义成
abstract ,是因为独占模式下只用实现 tryAcquire-tryRelease ,而共享模式下只用实现
tryAcquireShared-tryReleaseShared。如果都定义成abstract,那么每个模式也要去实现另一模
式下的接口。不同的自定义同步器争用共享资源的方式也不同。自定义同步器在实现时只需要实
现共享资源 state 的获取与释放方式即可,至于具体线程等待队列的维护(如获取资源失败入队/
唤醒出队等),AQS 已经在顶层实现好了。

1.41 介绍下JVM的组成部分及运行流程

经典结构图

 JVM包含两个子系统和两个组件:

  • 两个子系统为ClassLoader(类装载器子系统)、Execution engine(执行引擎)
  • 两个组件为Runtime data area(运行时数据区)、Native Interface(本地接口)

类加载子系统:根据给定的全限定类名来装载class文件到运行时数据区的方法区中

执行引擎:执行字节码文件中的指令

本地接口:与本地库交互,是其他编程语言交互的接口

运行时数据区:就是我们常说的JVM内存区域

运行流程:首先通过编译器将Java代码转换成字节码,类加载器(ClassLoader)再把字节码加载到内存中,将其放在运行时数据区中的方法区内,而字节码文件只是JVM的一套指令集规范,并不能直接交给底层操作系统去执行,因此需要特定的命令解析器执行引擎,将字节码翻译成底层系统指令,再交由CPU去执行,而这个过程中需要调用其他语言的本地库接口来实现整个程序的功能。

1.42 介绍下JVM的内存区域

JVM内存区域主要分为:

  • 线程私有区域:程序计数器、虚拟机栈、本地方法栈;
  • 线程共享区域:堆和方法区;
  • 直接内存

线程私有区域生命周期与线程生命周期相同,依赖用户线程的启动/结束 而 创建/销毁。

线程共享区域随虚拟机的启动/关闭 而 创建/销毁。

直接内存并不是 JVM 运行时数据区的一部分, 但也会被频繁的使用: 在 JDK 1.4 引入的 NIO 提 供了基于 Channel 与 Buffer 的 IO 方式, 它可以使用 Native 函数库直接分配堆外内存, 然后使用 DirectByteBuffer 对象作为这块内存的引用进行操作。 这样就避免了在 Java 堆和 Native 堆中来回复制数据, 因此在一些场景中可以显著提高性能。

1.42.1 程序计数器

一块小的内存空间,是当前线程所执行的字节码的行号指示器,每条线程都要有一个独立的程序计数器,这类内存也称为“线程私有”的内存。正在执行java方法的话,计数器记录的是虚拟机字节码指令的地址(当前指令的地址)。如果还是Native方法,则为空。

这个内存区域是唯一一个在虚拟机中没有规定任何OutOfMemoryError情况的区域。

1.42.2 虚拟机栈

是描述Java方法执行的内存模型,每个方法在执行的同时都会创建一个栈帧(Stack Frame)用于存储局部变量表、操作数栈、动态链接、方法出口等信息。每一个方法从调用到执行完成的过程,就对应着一个栈帧在虚拟机中入栈到出栈的过程。

 栈帧(Frame)是用来存储数据和部分过程结果的数据结构,同时也被用来处理动态链接、方法返回值、异常分派。栈帧随着方法调用而创建,随着方法结束而销毁--无论方法是正常完成还是异常完成(抛出了方法内未捕获的异常)都算作方法结束。

1.42.3 本地方法栈

与虚拟机栈的作用是一样的,只不过虚拟机栈是服务 Java 方法的,而本地方法栈是为虚拟机调用 Native 方法服务的; Native 关键字修饰的方法是看不到的,Native 方法的源码大部分都是 C和C++ 的代码

1.42.4 堆

是被线程共享的一块内存区域,创建的对象和数组都保存在Java堆内存中,也是垃圾收集器进行垃圾收集的最重要的内存区域。Java堆从GC的角度还可以分为:新生代(Eden区、From Survivor区和To Survivor区)和老年代

1.42.5 方法区 

即我们常说的永久代(Java8之前)/元空间(Java8之后),用于存储被JVM加载的类信息、常量、静态变量、即时编译器编译后的代码等数据。HotSpot VM把GC分代收集扩展至方法区,这样HotSpot的垃圾收集器就可以像管理Java堆一样管理这部分内存,而不必为方法区开发专门的内存管理器(永久代的内存回收主要目标是针对常量池的回收和类型的卸载,因此收益一般很小)

  运行时常量池是方法区的一部分。Class文件中除了有类的版本、字段、方法、接口等描述信息外,还有一项信息是常量池,用于存放编译器生成的各种字面量和符号引用,这部分内容将在类加载后存放到方法区的运行时常量池中。Java虚拟机对Class文件中的每一部分的格式都有严格的规定,每一个字节用于存储哪种数据都必须符合规范上的要求,这样才被虚拟机认可,装载和执行。

1.43 JVM运行时内存介绍

Java 堆从 GC 的角度还可以细分为: 新生代(Eden 区、From Survivor 区和 To Survivor 区)和老年 代。

 1.43.1 新生代

用来存放新生的对象。一般占据堆的1/3空间。由于频繁创建对象,所以新生代会频繁触发MinorGC进行垃圾回收。新生代又分为Eden 区、SurvivorFrom、SurvivorTo 三个区。

  • Eden区

Java 新对象的出生地(如果新创建的对象占用内存很大,则直接分配到老年代)。当 Eden 区内存不够的时候就会触发 MinorGC,对新生代区进行一次垃圾回收。

  • SurvivorFrom

上一次GC的幸存者,作为这一次GC的被扫描者

  • SurvivorTo

保留了一次MinorGC过程中的幸存者

MinorGC的过程(复制--清空--互换)

注:MinorGC采用复制算法

  1. 把 Eden 和 SurvivorFrom 区域中存活的对象复制到 SurvivorTo 区域(如果有对象的年龄已经达到了老年的标准,则复制到老年代区),同时把这些对象的年龄+1
  2. 清空 Eden 和 SurvivorFrom 中的对象;
  3. SurvivorTo 和 SurvivorFrom 互换,原 SurvivorTo 成为下一次 GC 时的 SurvivorFrom 区

1.43.2 老年代

主要存放应用程序中生命周期长的内存对象。

  老年代的对象比较稳定,所以 MajorGC 不会频繁执行。在进行 MajorGC 前一般都先进行了一次 MinorGC,使得有新生代的对象晋身入老年代,导致空间不够用时才触发。当无法找到足够大的连续空间分配给新创建的较大对象时也会提前触发一次 MajorGC 进行垃圾回收腾出空间。       

  MajorGC 采用标记清除算法:首先扫描一次所有老年代,标记出存活的对象,然后回收没有标记的对象。MajorGC 的耗时比较长,因为要扫描再回收。MajorGC 会产生内存碎片,为了减少内存损耗,我们一般需要进行合并或者标记出来方便下次直接分配。当老年代也满了装不下的时候,就会抛出 OOM(Out of Memory)异常。

1.43.4 永久代

    指内存的永久保存区域,主要存放Class和Meta(元数据)的信息,Class在被加载的时候放入永久区域,它和存放实例的区域不同,GC不会在主程序运行期对永久区域进行清理。所以这也导致了永久代的区域会随着加载的 Class 的增多而胀满,最终抛出 OOM 异常。

    在 Java8 中,永久代已经被移除,被一个称为“元数据区”(元空间)的区域所取代。元空间的本质和永久代类似,元空间与永久代之间最大的区别在于:元空间并不在虚拟机中,而是使用本地内存。因此,默认情况下,元空间的大小仅受本地内存限制。类的元数据放入 native memory, 字符串池和类的静态变量放入 java 堆中,这样可以加载多少类的元数据就不再由 MaxPermSize 控制, 而由系统的实际可用空间来控制  

1.44 介绍JVM的垃圾回收与算法

1.44.1 引用计数法

它实际上是通过在对象头中分配一个空间来保存该对象被引用的次数。如果该对象被其它对象引用,则它的引用计数加一,如果删除对该对象的引用,那么它的引用计数就减一,当该对象的引用计数为0时,那么该对象就会被回收。(JVM中没有使用此算法)

1.44.2 可达性分析算法

可达性分析算法:也可以称为根搜索算法、追踪性垃圾收集。

相对于引用计数算法而言,可达性分析算法不仅同样具备实现简单和执行高效等特点,更重要的是该算法可以有效地解决在引用计数算法中循环引用的问题,防止内存泄漏的发生。

相较于引用计数算法,这里的可达性分析就是 Java、C# 选择的。这种类型的垃圾收集通常也叫作追踪性垃圾收集(Tracing Garbage Collection)。

所谓 "GC Roots”根集合就是一组必须活跃的引用。

  • 可达性分析算法是以根对象集合(GCRoots)为起始点,按照从上至下的方式搜索被根对象集合所连接的目标对象是否可达。
  • 使用可达性分析算法后,内存中的存活对象都会被根对象集合直接或间接连接着,搜索所走过的路径称为引用链(Reference Chain)。
  • 如果目标对象没有任何引用链相连,则是不可达的,就意味着该对象己经死亡,可以标记为垃圾对象。
  • 在可达性分析算法中,只有能够被根对象集合直接或者间接连接的对象才是存活对象。

 1.44.3 标记清除算法

最基础的垃圾回收算法,分为两个阶段,标记和清除。标记阶段标记出所有需要回收的对象,清除阶段回收被标记的对象所占用的空间。

从图中我们就可以发现,该算法最大的问题是内存碎片化严重,后续可能发生大对象不能找到可利用空间的问题。 

 1.44.4 复制算法

为了解决标记清除算法内存碎片化的缺陷而被提出的算法。按内存容量将内存划分为等大小的两块。每次只使用其中一块,当这一块内存满后将尚存活的对象复制到另一块上去,把已使用的内存清掉,如图:

 1.44.5 分代收集算法

分代收集算法是目前大部分JVM所采用的方法,其核心思想是根据对象存活的不同生命周期将内存划分为不同的域,一般情况下将GC 堆划分为老年代(Tenured/Old Generation)和新生代(Young Generation)。老年代的特点是每次垃圾回收时只有少量对象需要被回收,新生代的特点是每次垃 圾回收时都有大量垃圾需要被回收,因此可以根据不同区域选择不同的算法。

  • 目前大部分 JVM 的 GC 对于新生代都采取 复制Copying 算法,因为新生代中每次垃圾回收都要回收大部分对象,即要复制的操作比较少,但通常并不是按照 1:1 来划分新生代。一般将新生代划分为一块较大的 Eden 空间和两个较小的 Survivor 空间(From Space, To Space),每次使用 Eden 空间和其中的一块 Survivor 空间,当进行回收时,将该两块空间中还存活的对象复制到另一块 Survivor 空间中

  • 而老年代因为每次只回收少量对象,因而采用标记整理(Mark-Compact )算法

1. JAVA 虚拟机提到过的处于方法区的永久代(Permanet Generation),它用来存储 class 类, 常量,方法描述等。对永久代的回收主要包括废弃常量和无用的类。

2. 对象的内存分配主要在新生代的 Eden Space 和 Survivor Space 的 From Space(Survivor目前存放对象的那一块),少数情况会直接分配到老年代。

3. 当新生代的 Eden Space 和 From Space 空间不足时就会发生一次 GC,进行 GC 后,Eden Space 和 From Space 区的存活对象会被挪到 To Space,然后将 Eden Space 和 From Space 进行清理。

4. 如果 To Space 无法足够存储某个对象,则将这个对象存储到老年代。

5. 在进行 GC 后,使用的便是 Eden Space 和 To Space 了,如此反复循环。

6. 当对象在 Survivor 区躲过一次 GC 后,其年龄就会+1。默认情况下年龄到达 15 的对象会被移到老年代中

 1.45 介绍下Java中的4种引用类型

  1. 强引用:在 Java 中最常见的就是强引用,把一个对象赋给一个引用变量,这个引用变量就是一个强引 用。当一个对象被强引用变量引用时,它处于可达状态,它是不可能被垃圾回收机制回收的,即 使该对象以后永远都不会被用到 JVM 也不会回收。因此强引用是造成 Java 内存泄漏的主要原因之 一。
  2. 软引用:软引用需要用 SoftReference 类来实现,对于只有软引用的对象来说,当系统内存足够时它 不会被回收,当系统内存空间不足时它会被回收。软引用通常用在对内存敏感的程序中。
  3. 弱引用:弱引用需要用 WeakReference 类来实现,它比软引用的生存期更短,对于只有弱引用的对象 来说,只要垃圾回收机制一运行,不管 JVM 的内存空间是否足够,总会回收该对象占用的内存
  4. 虚引用:虚引用需要 PhantomReference 类来实现,它不能单独使用,必须和引用队列联合使用。虚 引用的主要作用是跟踪对象被垃圾回收的状态

1.46 了解哪些垃圾收集器?

 1.46.1 Serial 垃圾收集器(单线程、复制算法)

Serial(英文连续)是最基本垃圾收集器,使用复制算法,曾经是JDK1.3.1 之前新生代唯一的垃圾 收集器。Serial 是一个单线程的收集器,它不但只会使用一个 CPU 或一条线程去完成垃圾收集工 作,并且在进行垃圾收集的同时,必须暂停其他所有的工作线程,直到垃圾收集结束。 Serial 垃圾收集器虽然在收集垃圾过程中需要暂停所有其他的工作线程,但是它简单高效,对于限 定单个 CPU 环境来说,没有线程交互的开销,可以获得最高的单线程垃圾收集效率,因此 Serial 垃圾收集器依然是 java 虚拟机运行在 Client 模式下默认的新生代垃圾收集器

1.46.2 ParNew 垃圾收集器(Serial+多线程)

ParNew 垃圾收集器其实是 Serial 收集器的多线程版本,也使用复制算法,除了使用多线程进行垃 圾收集之外,其余的行为和 Serial 收集器完全一样,ParNew 垃圾收集器在垃圾收集过程中同样也 要暂停所有其他的工作线程。

ParNew 收集器默认开启和 CPU 数目相同的线程数,可以通过-XX:ParallelGCThreads 参数来限 制垃圾收集器的线程数。【Parallel:平行的】 ParNew虽然是除了多线程外和Serial 收集器几乎完全一样,但是ParNew垃圾收集器是很多 java 虚拟机运行在 Server 模式下新生代的默认垃圾收集器。

1.46.3 Parallel Scavenge 收集器(多线程复制算法、高效)

Parallel Scavenge 收集器也是一个新生代垃圾收集器,同样使用复制算法,也是一个多线程的垃 圾收集器,它重点关注的是程序达到一个可控制的吞吐量(Thoughput,CPU 用于运行用户代码 的时间/CPU 总消耗时间,即吞吐量=运行用户代码时间/(运行用户代码时间+垃圾收集时间)), 高吞吐量可以最高效率地利用 CPU 时间,尽快地完成程序的运算任务,主要适用于在后台运算而 不需要太多交互的任务。自适应调节策略也是 ParallelScavenge 收集器与 ParNew 收集器的一个重要区别

1.46.4 Serial Old 收集器(单线程标记整理算法 )

Serial Old 是 Serial 垃圾收集器老年代版本,它同样是个单线程的收集器,使用标记-整理算法, 这个收集器也主要是运行在 Client 默认的 java 虚拟机默认的老年代垃圾收集器

在 Server 模式下,主要有两个用途:

1. 在 JDK1.5 之前版本中与新生代的 Parallel Scavenge 收集器搭配使用。

2. 作为老年代中使用 CMS 收集器的后备垃圾收集方案。

1.46.5 Parallel Old 收集器(多线程标记整理算法)

Parallel Old 收集器是Parallel Scavenge的老年代版本,使用多线程的标记-整理算法,在 JDK1.6 才开始提供。

在 JDK1.6 之前,新生代使用 ParallelScavenge 收集器只能搭配老年代的 Serial Old 收集器,只 能保证新生代的吞吐量优先,无法保证整体的吞吐量,Parallel Old 正是为了在老年代同样提供吞 吐量优先的垃圾收集器,如果系统对吞吐量要求比较高,可以优先考虑新生代 Parallel Scavenge 和年老代 Parallel Old 收集器的搭配策略。

新生代 Parallel Scavenge 和老年代Parallel Old 收集器搭配运行过程图:

 1.46.6 CMS收集器(多线程标记清除算法)

Concurrent mark sweep(CMS)收集器是一种老年代垃圾收集器,其最主要目标是获取最短垃圾回收停顿时间,和其他年老代使用标记-整理算法不同,它使用多线程的标记-清除算法。 最短的垃圾收集停顿时间可以为交互比较高的程序提高用户体验。 CMS 工作机制相比其他的垃圾收集器来说更复杂,整个过程分为以下 4 个阶段:

1.初始标记: 只是标记一下 GC Roots 能直接关联的对象,速度很快,仍然需要暂停所有的工作线程。 

2. 并发标记: 进行 GC Roots 跟踪的过程,和用户线程一起工作,不需要暂停工作线程。

3. 重新标记: 为了修正在并发标记期间,因用户程序继续运行而导致标记产生变动的那一部分对象的标记记录,仍然需要暂停所有的工作线程。

4. 并发清除: 清除 GC Roots 不可达对象,和用户线程一起工作,不需要暂停工作线程。由于耗时最长的并发标记和并发清除过程中,垃圾收集线程可以和用户现在一起并发工作,所以总体上来看 CMS 收集器的内存回收和用户线程是一起并发地执行

1.46.7 G1收集器

G1垃圾收集器是目前垃圾收集器理论发展的最前沿成果,相比与 CMS 收集器,G1 收 集器两个最突出的改进是:

1. 基于标记-整理算法,不产生内存碎片。

2. 可以非常精确控制停顿时间,在不牺牲吞吐量前提下,实现低停顿垃圾回收。

G1 收集器避免全区域垃圾收集,它把堆内存划分为大小固定的几个独立区域,并且跟踪这些区域 的垃圾收集进度,同时在后台维护一个优先级列表,每次根据所允许的收集时间,优先回收垃圾 最多的区域。区域划分和优先级区域回收机制,确保 G1 收集器可以在有限时间获得最高的垃圾收 集效率

1.47 介绍下JVM的类加载机制

JVM 类加载机制分为五个部分:加载,验证,准备,解析,初始化

  •  加载

加载是类加载过程中的一个阶段,这个阶段会在内存中生成一个代表这个类的 java.lang.Class 对 象,作为方法区这个类的各种数据的入口。注意这里不一定非得要从一个 Class 文件获取,这里既 可以从 ZIP 包中读取(比如从 jar 包和 war 包中读取),也可以在运行时计算生成(动态代理), 也可以由其它文件生成(比如将 JSP 文件转换成对应的 Class 类)。

  • 验证

这一阶段的主要目的是为了确保 Class 文件的字节流中包含的信息是否符合当前虚拟机的要求,并 且不会危害虚拟机自身的安全

  •  准备

准备阶段是正式为类变量分配内存并设置类变量的初始值阶段,即在方法区中分配这些变量所使 用的内存空间

  • 解析

解析阶段是指虚拟机将常量池中的符号引用替换为直接引用的过程。符号引用就是 class 文件中的:1. CONSTANT_Class_info

2. CONSTANT_Field_info

3. CONSTANT_Method_info

等类型的常量

注:符号引用与虚拟机实现的布局无关,引用的目标并不一定要已经加载到内存中。各种虚拟机实现的内存布局可以各不相同,但是它们能接受的符号引用必须是一致的,因为符号引用的字面量形式明确定义在 Java 虚拟机规范的 Class 文件格式中。

直接引用可以是指向目标的指针,相对偏移量或是一个能间接定位到目标的句柄。如果有了直接引用,那引用的目标必定已经在内存中存在

  • 初始化

初始化阶段是类加载最后一个阶段,前面的类加载阶段之后,除了在加载阶段可以自定义类加载 器以外,其它操作都由 JVM 主导。到了初始化阶段,才开始真正执行类中定义的 Java 程序代码。

初始化阶段是执行类构造器clinit方法的过程。clinit方法是由编译器自动收集类中的类变量的赋值操作和静态语句块中的语句合并而成的。虚拟机会保证子clinit方法执行之前,父类的clinit方法已经执行完毕,如果一个类中没有对静态变量赋值也没有静态语句块,那么编译器可以不为这个类生成clinit方法

注意以下几种情况不会执行类初始化:

1. 通过子类引用父类的静态字段,只会触发父类的初始化,而不会触发子类的初始化。

2. 定义对象数组,不会触发该类的初始化。

3. 常量在编译期间会存入调用类的常量池中,本质上并没有直接引用定义常量的类,不会触发定义常量所在的类。

4. 通过类名获取 Class 对象,不会触发类的初始化。

5. 通过 Class.forName 加载指定类时,如果指定参数 initialize 为 false 时,也不会触发类初始化,其实这个参数是告诉虚拟机,是否要对类进行初始化。

6. 通过 ClassLoader 默认的 loadClass 方法,也不会触发初始化动作

1.48 有哪些类加载器?

  1. 启动类加载器(Bootstrap ClassLoader):负责加载 JAVA_HOME\lib 目录中的,或通过-Xbootclasspath 参数指定路径中的,且被虚拟机认可(按文件名识别,如 rt.jar)的类)。加载核心类库
  2. 扩展类加载器(Extension ClassLoader):负责加载 JAVA_HOME\lib\ext 目录中的,或通过 java.ext.dirs 系统变量指定路径中的类库。加载Java的扩展库
  3. 应用程序类加载器(Application ClassLoader):负责加载用户路径(classpath)上的类库。加载Java应用的类
  4. 用户自定义加载器:通过继承 java.lang.ClassLoader类的方式实现

 1.49 什么是双亲委派机制?优缺点?

双亲委派机制:如果一个类加载器收到了类加载的请求,它首先不会自己去加载这个类,而是把这 个请求委派给父类加载器去完成,每一层的类加载器都是如此,这样所有的加载请求都会被传送到 顶层的启动类加载器中,只有当父加载无法完成加载请求(它的搜索范围中没找到所需的类)时, 子加载器才会尝试去加载类。

总结就是: 当一个类收到了类加载请求时,不会自己先去加载这个类,而是将其委派给父类,由父类去加载,如果此时父类不能加载,反馈给子类,由子类去完成类的加载。

优点:避免了类的重复加载;保护程序安全,防止核心API被随意篡改

缺点:在某些场景下双亲委派制过于局限,所以有时候必须打破双亲委派机制来达到目的。例如:SPI机制

注:如何打破双亲委派机制?可以自定义类加载器,继承ClassLoader类,重写loadClass方法和findClass方法;

1.50 什么是内存泄漏和内存溢出?有什么关系?

  • 内存泄漏:对象已经不再使用了,但还是占用着内存空间,没有被释放,导致了内存空间浪费
  • 内存溢出:申请内存时,JVM没有足够的内存空间分配,通常表现为OutOfMemoryError

二者关系:内存泄露最终会导致内存溢出,由于系统中的内存是有限的,如果过度占用资源而不及时释放,最后会导致内存不足,从而无法给所需要存储的数据提供足够的内存,从而导致内存溢出。导致内存溢出也可能是由于在给数据分配大小时没有根据实际要求分配,最后导致分配的内存无法满足数据的需求,从而导致内存溢出。

二者区别:内存泄露是由于GC无法及时或者无法识别可以回收的数据进行及时的回收,导致内存的浪费;内存溢出是由于数据所需要的内存无法得到满足,导致数据无法正常存储到内存中。内存泄露的多次表现就是会导致内存溢出。

1.51 如何排查内存泄漏或内存溢出问题?

服务隔一段时间不重启,就会发现CPU空间快占满了,这种情况一般都是发生了内存泄漏问题。

排查方式如下(生产环境):

  1. 使用命令:top -c 显示运行中的进程列表信息, shift + m 按内存使用率进行排序,找到内存占用率高的进程号

  2. 使用jmap生成堆转储快照。命令:jmap -dump:format=b,file={path} {pid}  注意:如果生成文件比较大,会对服务有短暂的影响导致不可用,所以这个操作避免常规时间操作

  3. 使用MAT工具分析(Java内存分析工具):导入hprof文件,通过工具分析后定位到具体内存泄漏的原因以及具体代码位置

内存溢出一般都是由内存泄漏问题导致的,排查问题方式同上。

1.52 哪些情况会导致内存泄漏?

  • 静态集合类

静态集合类,如 HashMap、LinkedList 等等。如果这些容器为静态的,那么它们的生命周期与 JVM 一致,则容器中的对象在程序结束之前不能被释放,从而造成内存泄漏。简单而言,长生命周期对象持有短生命周期对象的引用,尽管短生命周期的对象不再使用,但是因为长生命周期对象持有它的引用而导致不能被回收。

  • 单例模式

单例模式,和静态集合导致内存泄漏原因类似,因为单例的静态特性,它的生命周期和 JVM 的生命周期一样长,所以如果单例对象如果持有外部对象引用,那么这个外部对象也不会被回收,那么就会发生内存泄漏。

  • 内部类持有外部类

内部类持有外部类,如果一个外部类的实例的方法返回一个内部类的实例对象。

这个内部类对象被长期引用了,即使那个外部类实例对象不再被使用,但由于内部类持有外部类的实例对象,这个外部类对象不会被垃圾回收,这也造成内存泄漏。

  • 各种连接未显示关闭,如数据库连接,io连接

在对数据库进行操作时,首先需要建立与数据库的连接,当不再使用时,需要调用 close 方法来释放与数据库的连接。只有连接被关闭,垃圾回收器才会回收对应对象

  • 变量不合理的作用域

一般而言,一个变量定义的作用范围大于其使用范围,很有可能造成内存泄漏,另一方面,如果没有及时的把对象置为NULL,也有可能发生内存泄漏

  • 改变hash值

当一个对象被存储进HashSet后,就不能修改这个对象中的那些参与计算哈希值的字段了。

  • 缓存泄漏
  • 监听器和回调

1.53 常用JVM调优工具有哪些?参数及命令?

1.53.1 可视化调优工具

  • JConsole,JVisualVM(这两个jdk自带的可视化工具)
  • jprofiler(收费版)

1.53.2 常用的jdk自带命令行排查工具

  • jps虚拟机进程状况工具

jps(JVM Process Status tool,虚拟机进程状况工具),用于列出正在运行的 JVM 的 LVMID(Local Virtual Machine IDentifier,本地虚拟机唯一 ID),以及 JVM 的执行主类、JVM 启动参数等信息。

  • jstat(虚拟机统计信息监视工具)

jstat(JVM Statistics Monitoring Tool,虚拟机统计信息监视工具)用于监控虚拟机的运行状态信息。

  • jinfo(查询虚拟机参数配置工具)

  • jmap(堆快照生成工具)

  • jhat(堆快照分析功能)

  • jstack(查询虚拟机当前的线程快照信息)

另外极力推荐使用的一个命令行工具arthas(阿里开源,命令行模式)

1.53.3 常用的JVM调优参数
-Xms:初始堆大小,JVM 启动的时候,给定堆空间大小。
-Xmx:最大堆大小,JVM 运行过程中,如果初始堆空间不足的时候,最大可以扩展到多少。
-Xmn:设置堆中年轻代大小。整个堆大小=年轻代大小+年老代大小+持久代大小。
-XX:NewSize=n 设置年轻代初始化大小大小
-XX:MaxNewSize=n 设置年轻代最大值
-XX:NewRation 设置年轻代和年老代的比值。如: -XX:NewRatio=3,表示年轻代与年老代比值为 1:3,年轻代占整个年轻代+年老代和的 1/4
-XX:SurvivorRatio=n 年轻代中 Eden 区与两个 Survivor 区的比值。注意 Survivor 区有两个。8表示两个Survivor :eden=2:8 , 即一个Survivor占年轻代的1/10,默认就为8
-Xss:设置每个线程的堆栈大小。JDK5后每个线程 Java 栈大小为 1M,以前每个线程堆栈大小为 256K。
-XX:ThreadStackSize=n 线程堆栈大小
-XX:PermSize=n 设置持久代初始值
-XX:MaxPermSize=n 设置持久代大小
-XX:MaxTenuringThreshold=n 设置年轻带垃圾对象最大年龄。如果设置为 0 的话,则年轻代对象不经过 Survivor 区,直接进入年老代。

 

2.MySQL

2.1 MySQL存储引擎InnoDB与MyISAM的区别?

  • InnoDB支持事务,MyISAM不支持事务
  • InnoDB支持外键,MyISAM不支持外键
  • InnoDB支持表、行级锁,MyISAM只支持表级锁
  • InnoDB支持 MVCC(多版本并发控制),MyISAM 不支持

  • select count(*) from table时,MyISAM更快,因为它有一个变量保存了整个表的总行数,可以直接读取,InnoDB就需要全表扫描。

  • InnoDB表必须有主键,而MyISAM可以没有主键

  • InnoDB表需要更多的内存和存储,而MyISAM可被压缩,存储空间较小

  • InnoDB按主键大小有序插入,MyISAM记录插入顺序是,按记录插入顺序保存。

2.2 聚簇索引和非聚簇索引的区别?

  • 聚簇索引数据和索引放在了一起,且叶子节点存放的行数据,支持覆盖索引;非聚簇索引数据和索引分开存储,叶子节点存放的是主键值或指向数据行的指针
  • 一张表只能有一个聚簇索引,一张表可以有多个非聚簇索引
  • 聚簇索引表数据按照索引的顺序来存储的,也就是说索引项的顺序与表中记录的物理顺序一致,而非聚簇索引表数据存储顺序与索引顺序无关
  • 聚簇索引默认是主键,如果表中没有定义主键,InnoDB会选择一个唯一的非空索引代替,如果没有这样的索引,InnoDB会在内部生成一个隐式的聚簇索

2.3 哪些情况适合或不适合创建索引?

https://blog.csdn.net/weixin_39764056/article/details/127288001

2.4 哪些情况索引会失效?

https://blog.csdn.net/weixin_39764056/article/details/127288001

2.5 谈谈关于优化SQL的一些方法

可以从这几个维度回答这个问题:

  • 加索引

  • 避免返回不必要的数据

  • 适当分批量进行

  • 优化sql结构

  • 分库分表

  • 读写分离

后端程序员必备:书写高质量SQL的30条建议

2.6 说说分库分表的设计 

分库分表方案:

  • 水平分库:以字段为依据,按照一定策略(hash、range等),将一个库中的数据拆分到多个库中
  • 水平分表:以字段为依据,按照一定策略(hash、range等),将一个表中的数据拆分到多个表中。

  • 垂直分库:以表为依据,按照业务归属不同,将不同的表拆分到不同的库中。

  • 垂直分表:以字段为依据,按照字段的活跃性,将表中字段拆到不同的表(主表和扩展表)中。

常用的分库分表中间件:

  • sharding-jdbc(当当)

  • Mycat

  • TDDL(淘宝)

  • Oceanus(58同城数据库中间件)

  • vitess(谷歌开发的数据库中间件)

  • Atlas(Qihoo 360)

分库分表可能遇到的问题

  • 事务问题:需要用分布式事务

  • 跨节点Join的问题:解决这一问题可以分两次查询实现

  • 跨节点的count,order by,group by以及聚合函数问题:分别在各个节点上得到结果后在应用程序端进行合并。

  • 数据迁移,容量规划,扩容等问题

  • ID问题:数据库被切分后,不能再依赖数据库自身的主键生成机制啦,最简单可以考虑UUID

  • 跨分片的排序分页问题(后台加大pagesize处理)

参考:https://www.cnblogs.com/littlecharacter/p/9342129.html

2.7 如何选择合适的分布式主键方案

  • 数据库自增长序列或字段。

  • UUID

  • Redis生成ID

  • Twitter的snowflake算法

  • 利用zookeeper生成唯一ID

  • MongoDB的ObjectId

2.8 事务的隔离级别有哪些

  • 读未提交(Read Uncommitted)

  • 读已提交(Read Committed)

  • 可重复读(Repeatable Read)

  • 串行化(Serializable)

Mysql默认的事务隔离级别是可重复读(Repeatable Read)

  • 事务A、B交替执行,事务A被事务B干扰到了,因为事务A读取到事务B未提交的数据,这就是脏读

  • 在一个事务范围内,两个相同的查询,读取同一条记录,却返回了不同的数据,这就是不可重复读。

  • 事务A查询一个范围的结果集,另一个并发事务B往这个范围中插入了数据,并静悄悄地提交,然后事务A再次查询相同的范围,两次读取得到的结果集不一样了,这就是幻读。

参考:一文彻底读懂MySQL事务的四大隔离级别 - 掘金

 可重复读(RR)隔离级别下,加锁的select, update, delete等语句,会使用间隙锁+ 临键锁,锁住索引记录之间的范围,避免范围间插入记录,以避免产生幻影行记录,也就解决了幻读的问题

 2.9 执行计划Explain有哪些参数?各参数含义?

MySQL之索引,执行计划及SQL优化_·欣·的博客-CSDN博客

2.10 in和exists的区别?

假设表A表示某企业的员工表,表B表示部门表,查询所有部门的所有员工,很容易有以下SQL:

select * from A where deptId in (select deptId from B);

这样写等价于:

先查询部门表B

select deptId from B

再由部门deptId,查询A的员工

select * from A where A.deptId = B.deptId

显然,除了使用in,我们也可以用exists实现一样的查询功能,如下:

select * from A where exists (select 1 from B where A.deptId = B.deptId);

因为exists查询的理解就是,先执行主查询,获得数据后,再放到子查询中做条件验证,根据验证结果(true或者false),来决定主查询的数据结果是否得保留。

那么,这样写就等价于:

select * from A,先从A表做循环

select * from B where A.deptId = B.deptId,再从B表做循环.

数据库最费劲的就是跟程序链接释放。假设链接了两次,每次做上百万次的数据集查询,查完就走,这样就只做了两次;相反建立了上百万次链接,申请链接释放反复重复,这样系统就受不了了。即mysql优化原则,就是小表驱动大表,小的数据集驱动大的数据集,从而让性能更优。

因此,我们要选择最外层循环小的,也就是,如果B的数据量小于A,适合使用in,如果B的数据量大于A,即适合选择exists,这就是in和exists的区别。

 简言之,就是子查询表大的用exists,子查询表小的用in

2.11 MySQL的主从延迟如何解决?

主从复制的原理图如下:

主从复制分了五个步骤进行:

  •  主库的更新事件(update、insert、delete)被写到binlog
  • 从库发起连接,连接到主库
  • 此时主库创建一个binlog dump thread,把binlog的内容发送到从库
  • 从库启动之后,创建一个I/O线程读取主库传过来的binlog内容并写到relay log中
  • 还会创建一个SQL线程,从relay log中读取内容从Exec_Master_Log_Pos位置开始执行读取到的更新事件,将更新内容写入到slave的db

主从同步延迟的原因:

一个服务器开放N个链接给客户端来连接的,这样有会有大并发的更新操作, 但是从服务器的里面读取binlog的线程仅有一个,当某个SQL在从服务器上执行的时间稍长或者由于某个SQL要进行锁表就会导致主服务器的SQL大量积压,未被同步到从服务器里。这就导致了主从不一致, 也就是主从延迟。

主从同步延迟的解决办法:

  • 主服务器要负责更新操作,对安全性的要求比从服务器要高,所以有些设置参数可以修改,比如sync_binlog=1,innodb_flush_log_at_trx_commit = 1 之类的设置等。

  • 选择更好的硬件设备作为slave。

  • 把一台从服务器当度作为备份使用, 而不提供查询, 那边他的负载下来了, 执行relay log 里面的SQL效率自然就高了。

  • 增加从服务器,这个目的还是分散读的压力,从而降低服务器负载。

 2.12 一条SQL语句在MySQL中是如何执行的?

先看一张图:

  1. 客户端发送SQL语句到MySQL服务端
  2. 验证连接合法性
  3. 查询缓存(MySQL8以后禁用了缓存)
  4. 语法解析和预处理 
  5. 查询优化
  6. 调用存储引擎执行
  7. 将结果返回给客户端

2.13 一条查询SQL的执行顺序?

 2.14 MySQL中有哪些锁?

2.14.1 加锁机制

乐观锁与悲观锁是两种并发控制的思想,可用于解决丢失更新问题。

乐观锁

  • 每次去取数据,都很乐观,觉得不会出现并发问题。
  • 因此,访问、处理数据每次都不上锁。
  • 但是在更新的时候,再根据版本号或时间戳判断是否有冲突,有则处理,无则提交事务。

悲观锁

  • 每次去取数据,很悲观,都觉得会被别人修改,会有并发问题。
  • 因此,访问、处理数据前就加排他锁
  • 在整个数据处理过程中锁定数据,事务提交或回滚后才释放锁.

2.14.2 锁粒度

  • 表锁: 开销小,加锁快;锁定力度大,发生锁冲突概率高,并发度最低;不会出现死锁。
  • 行锁: 开销大,加锁慢;会出现死锁;锁定粒度小,发生锁冲突的概率低,并发度高。
  • 页锁: 开销和加锁速度介于表锁和行锁之间;会出现死锁;锁定粒度介于表锁和行锁之间,并发度一般

2.14.3 兼容性

共享锁:

  • 又称读锁(S锁)。
  • 一个事务获取了共享锁,其他事务可以获取共享锁,不能获取排他锁,其他事务可以进行读操作,不能进行写操作。
  • SELECT ... LOCK IN SHARE MODE 显示加共享锁。

排他锁:

  • 又称写锁(X锁)。
  • 如果事务T对数据A加上排他锁后,则其他事务不能再对A加任任何类型的封锁。获准排他锁的事务既能读数据,又能修改数据。
  • SELECT ... FOR UPDATE 显示添加排他锁。

2.14.4 锁模式

  • 记录锁: 在行相应的索引记录上的锁,锁定一个行记录
  • gap锁: 是在索引记录间歇上的锁,锁定一个区间
  • next-key锁: 是记录锁和在此索引记录之前的gap上的锁的结合,锁定行记录+区间
  • 意向锁 是为了支持多种粒度锁同时存在

 2.15 MySQL的内连接,左连接,右连接有什么区别?

  • Inner join 内连接,在两张表进行连接查询时,只保留两张表中完全匹配的结果集

  • left join 在两张表进行连接查询时,会返回左表所有的行,即使在右表中没有匹配的记录

  • right join 在两张表进行连接查询时,会返回右表所有的行,即使在左表中没有匹配的记录

 2.16 什么是覆盖索引?什么是回表?

  • 覆盖索引: 查询列要被所建的索引覆盖,不必从数据表中读取,换句话说查询列要被所使用的索引覆盖。

  • 回表:二级索引无法直接查询所有列的数据,所以通过二级索引查询到聚簇索引后,再查询到想要的数据,这种通过二级索引查询出来的过程,就叫做回表。

注:B+树在满足聚簇索引或者覆盖索引的时候不需要回表查询数据

2.17 MySQL数据库cpu飙升的话,要怎么处理呢?

排查过程:

  • 使用top 命令观察,确定是mysqld导致还是其他原因。

  • 如果是mysqld导致的,show processlist,查看session情况,确定是不是有消耗资源的sql在运行。

  • 找出消耗高的 sql,看看执行计划是否准确, 索引是否缺失,数据量是否太大。

处理:

  • kill 掉这些线程(同时观察 cpu 使用率是否下降),

  • 进行相应的调整(比如说加索引、改 sql、改内存参数)

  • 重新跑这些 SQL。

其他情况:

也有可能是每个 sql 消耗资源并不多,但是突然之间,有大量的 session 连进来导致 cpu 飙升,这种情况就需要跟应用一起来分析为何连接数会激增,再做出相应的调整,比如说限制连接数等

2.18 一个6亿的表a,一个3亿的表b,通过外间tid关联,你如何最快的查询出满足条件的第50000到第50200中的这200条数据记录?

1、如果a表TID是自增长,并且是连续的,b表的ID为索引

select * from a,b where a.tid = b.id and a.tid>500000 limit 200;

2、如果a表的TID不是连续的,那么就需要使用覆盖索引.TID要么是主键,要么是辅助索引,b表ID也需要有索引。

select * from b , (select tid from a limit 50000,200) a where b.id = a .tid;

3.Redis

4.Mybatis

5.Spring

6.SpringMVC

7.SpringBoot

8.SpringCloud

9.RabbitMQ

10.Kafka

11.Dubbo

12.Zookeeper

13.Nginx

14.Elasticsearch

15.Linux

16.Docker

17.K8S

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值