java知识点

java知识点

第一部分 Java容器

java容器继承关系2

一、Iterable 接口

  1. 只有一个方法: Iterator iterator() //即返回一个迭代器,迭代器是一种设计模式,它是一个对象,它可以遍历并选择序列中的对象
  2. 使用方法iterator()要求容器返回一个Iterator。第一次调用Iterator的next()方法时,它返回序列的第一个元素。注意:iterator()方法是java.lang.Iterable接口,被Collection继承。
  3. 使用next()获得序列中的下一个元素。
  4. 使用hasNext()检查序列中是否还有元素。
  5. 使用remove()将迭代器新返回的元素删除。

二、Collection 接口

  1. 表示一组对象(元素),有些允许重复,有些不允许重复
  2. JDk不提供该接口的实现,提供更具体的子接口的 实现
  3. 不同的Collection子类对于有序性、重复性、null、线程同步都有不同的策略

三、List 接口

  1. 有序、不允许重复,用户可以对列表中每个元素的插入位置进行精确地控制。用户可以根据元素的整数索引访问元素,并搜索列表中的元素。
  2. 提供了特殊的迭代器,称为 ListIterator,允许元素插入替换,以及双向访问,还提供了一个方法来获取从列表中指定位置开始的列表迭代器ListIterator listIterator(int index)。
  3. 子类:ArrayList、LinkedList、Stack以及Vector等
    • ArrayList
      1. 基于数组实现的List类,封装了一个动态的、增长的、允许再分配的Object[ ]数组。
      2. 当从ArrayList的中间位置插入或者删除元素时,需要对数组进行复制、移动、代价比较高。因此,它适合随机查找和遍历,不适合插入和删除。
      3. 默认扩容50%,线程不安全
    • Vector
      1. 通过数组实现的,不同的是它支持线程的同步,即某一时刻只有一个线程能够写Vector,避免多线程同时写而引起的不一致性,但实现同步需要很高的花费,因此,访问它比访问ArrayList慢
      2. Stack是Vector提供的一个子类,用于模拟"栈"这种数据结构
      3. 默认扩容一倍,线程安全
    • LinkedList
      1. LinkedList是用链表结构存储数据的,很适合数据的动态插入和删除,随机访问和遍历速度比较慢。
      2. 还实现了Deque接口,专门用于操作表头和表尾元素,可以当作堆栈、队列和双向队列使用。

四、Set接口

  1. 满足集合的无序性,确定性,单一性,如果有多个null,则不满足单一性了,所以Set只能有一个null。元素没有先后的差别
  2. Set判断两个对象相同不是使用"=="运算符,而是根据equals方法
  3. 子类:HashSet、TreeSet、LinkedHashSet等
    • HashSet :(底层是HashMap)
      1. 使用HASH算法来存储集合中的元素,因此具有良好的存取和查找性能。
      2. HashSet集合判断两个元素相等的标准是两个对象通过equals()方法比较相等,并且两个对象的hashCode()方法的返回值相等
    • LinkedHashSet同时使用链表维护元素的次序,这样使得元素看起来是以插入的顺序保存的。LinkedHashSet需要维护元素的插入顺序,因此性能略低于HashSet的性能,但在迭代访问Set里的全部元素时(遍历)将有很好的性能(链表很适合进行遍历),底层是LinkedHashMap
    • SortedSet (接口):主要用于排序操作,实现了此接口的子类都属于排序的子类
    • TreeSet:可以确保集合元素处于排序状态,底层是TreeMap
    • EnumSet :专门为枚举类设计的集合类,所有元素都必须是指定枚举类型的枚举值

五、Queue 接口

  1. 用于模拟“队列”数据结构(FIFO)
  2. 子类:
    • riorityQueue—— 优先队列:按照队列中某个属性的大小来排列的。故而叫优先队列
    • Deque——双端队列(接口):
    • ArrayDeque:基于数组的双端队列,类似于ArrayList有一个Object[] 数组
    • LinkedList
    • BlockingQueue:在进行检索或移除一个元素的时候,它会 等待队列变为非空;当在添加一个元素时,它会等待队列中的可用空间。主要用于实现生产者-消费者模式。实现:ArrayBlockingQueue、LinkedBlockingQueue、 PriorityBlockingQueue,、SynchronousQueue等

六、Map 接口

  1. 不是collection的子接口或者实现类,本身是一个接口,Map包含key-value对,它提供抽取key或 value列表集合的方法,但是它不适合“一组对象”规范。

  2. 用于保存具有“映射关系”的数据。每个Entry都持有键-值两个对象。其中,Value可能重复,但是Key不允许重复,可以有多个Value为null,但是只能有一个Key为null。

  3. 子类:HashMap、TreeMap、Hashtable、ConcurrentHashMap以及 Properties等

    • HashMap

      1. 不能保证元素的顺序一样,也不能保证key-value对的顺序,通过equals()方法比较
      2. HashMap允许K/V都为null
      3. HashMap没有考虑同步,是线程不安全的
    • LinkedHashMap:使用双向链表来维护key-value对的次序,与插入顺序一致

    • HashTable:(底层是链地址法组成的哈希表(即数组+单项链表组成))

      1. 定义:
        (1)Hashtable 是一个散列表,它存储的内容是键值对(key-value)映射。
        (2)Hashtable 的函数都是同步的,使用了synchronized关键字,这意味着它是线程安全的。它的key、value都不可以为null。
        (3)HashTable直接使用对象的hashCode。
      2. 子类:Properties---可以把Map对象和属性文件关联,从而把Map对象的key - value对写入到属性文件中,也可把属性文件中的“属性名-属性值”加载进Map对象中
    • TreeMap:红黑树结构,每个键值对都作为红黑树的一个节点,需要根据key对节点进行排序,可以保证所有的key-value对处于有序状态。两种排序方式:自然排序、定制排序

    • WeakHashMap:HashMap的key保留了对实际对象的强引用,这意味着只要该HashMap对象不被销毁,该HashMap所引用的对象就不会被垃圾回收。但WeakHashMap的key只保留了对实际对象的弱引用,这意味着如果WeakHashMap对象的key所引用的对象没有被其他强引用变量所引用,则这些key所引用的对象可能被垃圾回收,当垃圾回收了该key所对应的实际对象之后,WeakHashMap也可能自动删除这些key所对应的key-value对。

    • IdentityHashMap:当且仅当两个key严格相等(key1==key2)时,IdentityHashMap才认为两个key相等

    • EnumMap:与枚举类一起使用的Map实现,EnumMap中的所有key都必须是单个枚举类的枚举值。根据key的自然顺序存储。

hashmap底层实现:存储+扩容+线程不安全

一、存储结构-字段

  1. 数组+链表+红黑树(JDK1.8增加了红黑树部分)

  2. 控制map使得Hash碰撞的概率又小,哈希桶数组(Node[] table)占用空间又少:好的Hash算法和扩容机制

  3. 构造函数

         int threshold;             // 所能容纳的key-value对极限;threshold = length * Load
         final float loadFactor;    // 负载因子
         int modCount;  
         int size;
    
  4. 当链表长度太长(默认超过8—常量TREEIFY_THRESHOLD)时,链表就转换为红黑树

二、功能实现-方法

  1. 确定哈希桶数组索引位置:取key的hashCode值、高位运算、取模运算

  2. put方法:

    ①.判断键值对数组table[i]是否为空或为null,否则执行resize()进行扩容;

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

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

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

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

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

  3. 解决冲突方法:链地址法。

  4. 扩容机制

    • 没有设置初始化容量,系统会默认创建一个容量为16的大小的集合,加载因子默认是0.75
    • 当HashMap中元素数超过容量加载因子时,HashMap会进行扩容,扩容到下一个2的指数幂使用一个容量更大的数组来代替已有的容量小的数组,transfer()方法将原有Entry数组的元素拷贝到新的Entry数组里。transfer方法的作用是把原table的Node放到新的table中,使用的是头插法,也就是说,新table中链表的顺序和旧列表中是相反的,在HashMap线程不安全的情况下,这种头插法可能会导致环状节点。
    • 线程不安全,会出现环,造成死循环:线程1准备处理节点,线程2把HashMap扩容成功,链表已经逆向排序,那么线程1在处理节点时就可能出现环形链表。
      1. 假设有两个线程,线程1和线程2,两个线程进行hashMap的put操作,触发了扩容。
      2. 进程2先倒序链表,进程1执行 e.next = newTable[i]这一句,此时会指向头结点,造成死循环
    void transfer(Entry[] newTable) {
          Entry[] src = table; 
          int newCapacity = newTable.length;
          for (int j = 0; j < src.length; j++) { 
              Entry<K,V> e = src[j];           
              if (e != null) {//两个线程都先进入if
                  src[j] = null; 
                  do { 
                      Entry<K,V> next = e.next; 
                     int i = indexFor(e.hash, newCapacity);
                     e.next = newTable[i]; //线程1 这里还没执行 停下   //线程1刚才在这里停下,所以现在从这一句代码开始执行
                     newTable[i] = e;  
                     e = next;             
                 } while (e != null);
             }
         }
     }
    
    
  5. 线程不安全

    • 数据丢失:如果有两条线程同时执行到这条语句table[i]=null,时两个线程都会区创建Entry,这样存入会出现数据丢失。
    • 数据重复:如果有两个线程同时发现自己都key不存在,而这两个线程的key实际是相同的,在向链表中写入的时候第一线程将e设置为了自己的Entry,而第二个线程执行到了e.next,此时拿到的是最后一个节点,依然会将自己持有是数据插入到链表中,这样就出现了数据重复。
    • 死循环:两个线程同时扩容
    • 在1.8中用的是链表尾插法:避免死循环
  6. hashmap缩容:链表大于8时开始转换为红黑树,小于6时由红黑树转换为链表

  7. 参考链接:

CurrentHashMap底层原理:存储+线程安全

一、实现原理

  1. 解决hashmap在并发环境下不安全

  2. JDK1.8:synchronized+CAS+HashEntry+红黑树;

  3. JDK1.7:ReentrantLock+Segment+HashEntry。

  4. JDK7 ConcurrentHashMap:容器中有多把锁,每一把锁锁一段数据,这样在多线程访问时不同段的数据时,就不会存在锁竞争了,可以有效地提高并发效率

    • 由Segment(分段锁)数组结构和HashEntry数组组成
    • Segment 实现了 ReentrantLock,所以 Segment 是一种可重入锁,扮演锁的角色
    • 一个Segment中包含一个HashEntry数组,每个HashEntry又是一个链表结构,因此在ConcurrentHashMap查询一个元素的过程需要进行两次Hash操作
      • 第一次Hash定位到Segment,
      • 第二次Hash定位到元素所在的链表的头部
    • 将数据分成一段一段的存储,然后给每一段数据配一把锁,当一个线程占用锁访问其中一个段数据的时候,其他段的数据也能被其他线程访问
ConcurrentHashMap分段锁
  1. JDK8 ConcurrentHashMap
    • 数组+链表+红黑树

    • 基于CAS操作保证数据的获取以及使用synchronized关键字对相应数据段加锁来实现线程安全

    • CAS是英文单词Compare and Swap的缩写,翻译过来就是比较并替换。CAS机制中使用了3个基本操作数:内存地址V,旧的预期值A,要修改的新值B。更新一个变量的时候,只有当变量的预期值A和内存地址V当中的实际值相同时,才会将内存地址V对应的值修改为B。CAS属于乐观锁

      -重新获取内存地址V的当前值,并重新计算想要修改的值。这个重新尝试的过程被称为自旋。

      -缺点:CPU开销过大、不能保证代码块的原子性(所保证的只是一个变量的原子性操作,而不能保证整个代码块的原子性)、 ABA问题(加个版本号)

    • synchronized只锁定当前链表或红黑二叉树的首节点,这样只要hash不冲突,就不会产生并发,效率又提升N倍。

JDK1.8-ConcurrentHashMap-Structure
  1. 参考链接

LinkedHashMap原理

  1. 用处:在使用HashMap的时候,可能会遇到需要按照当时put的顺序来进行哈希表的遍历,HashMap中不存在保存顺序的机制。于是使用LinkedHashMap
  2. 可以保持两种顺序,分别是插入顺序和访问顺序
  3. 原理:通过哈希表和链表实现的,它通过维护一个链表来保证对哈希表迭代时的有序性
LinkedHashMap

第二部分 JAVA基础

一、JAVA IO

一、I/O

JAVA IO 包
  1. 流的概念(Stream):指一连串的数据(字符或字节),是以先进先出的方式发送信息的通道。
    • 特性:先进先出、顺序存取、只读或只写
  2. 分类:
    1. 按数据流的方向:输入流、输出流
    2. 按处理数据单位:字节流、字符流
      • 字节流和字符流所操作的数据单元不同
      • 字节流可以处理一切文件,而字符流只能处理纯文本文件。
      • 字节流本身没有缓冲区,缓冲字节流相对于字节流,效率提升非常高。而字符流本身就带有缓冲区,缓冲字符流相对于字符流效率提升不大
    3. 按功能:
      • 节点流:直接操作数据读写的流类
      • 处理流:对一个已存在的流的链接和封装,通过对数据进行处理为程序提供功能强大、灵活的读写功能
      • 都应用了Java的装饰者设计模式
      • 处理流中的缓冲流:较为重要
  3. 使用I/O
    • FileInputStream、FileOutputStream(字节流)
    • BufferedInputStream、BufferedOutputStream(缓冲字节流)
    • InputStreamReader、OutputStreamWriter(字符流)
    • 字符流便捷类:FileWriter和FileReader简化字符流的读写
    • BufferedReader、BufferedWriter(字符缓冲流)
  4. IO流对象:
    • File类
    • 字节流(InputStreamOutputStream是两个抽象类,是字节流的基类):
    • 字符流(ReaderWriter抽象基类)
    • 序列化:详见序列化章节

二、NIO

  1. 概念:NIO 与原来的 IO 有同样的作用和目的,但是使用方式完全不同,NIO 支持面向缓冲区的、基于通道的 IO 操作。NIO 将以更加高效的方式进行文件的读写操作。
  2. 通道和缓冲区和Selector:Channel 负责传输,Buffer 负责存储,Selector(选择区)用于监听多个通道的事件
    • 通道本身不能传输数据,要想传输数据必须要有缓冲区;把数据都写到缓冲区,然后缓冲区通过通道进行传输,最后再把数据从缓冲区拿出来写到文件中
    • 双向,传统的IO是单向的
    • 缓冲区实质上是一个数组。通常它是一个字节数组,但是也可以使用其他种类的数组。 但是一个缓冲区不 仅仅 是一个数组。缓冲区提供了对数据的结构化访问,而且还可以跟 踪系统的读/写进程
    • NIO 基于 Channel 和 Buffer(缓冲区)进行操作,数据总是从通道读取到缓冲区中,或者从缓冲区写入到通道中
    • Selector:Selector 能够检测多个注册的通道上是否有事件发生,如果有事件发生,便获取事件然后针对每个事件进行相应的响应处理
  3. 分散读取与聚集写入:
    • 分散读取(Scattering Reads)是指从 Channel 中读取的数据 "分散" 到多个 Buffer 中
    • 聚集写入(Gathering Writes)是指将多个 Buffer 中的数据 "聚集" 到 Channel
  4. IO多路复用的底层原理:
    • select
    • epoll
  5. NIO 的非阻塞:NIO 的非阻塞模式,使一个线程从某通道发送请求读取数据,但是它仅能得到目前可用的数据,如果目前没有数据可用时,就什么都不会获取

三、IO模型

  1. 阻塞 IO 模型(BIO)

    • 概念:假设应用程序的进程发起IO调用,但是如果内核的数据还没准备好的话,那应用程序进程就一直在阻塞等待,一直等到内核数据准备好了,从内核拷贝到用户空间,才返回成功提示,此次IO操作,称之为阻塞IO
    • 实现方法:阻塞socket、Java BIO
    • 缺点:如果内核数据一直没准备好,那用户进程将一直阻塞,浪费性能,可以使用非阻塞IO优化。

    阻塞IO模型

  2. 非阻塞 IO 模型(NIO)

    • 概念:如果内核数据还没准备好,可以先返回错误信息给用户进程,让它不需要等待,而是通过轮询的方式再来请求。
    • 模型流程:
      1. 应用进程向操作系统内核,发起recvfrom读取数据。
      2. 操作系统内核数据没有准备好,立即返回EWOULDBLOCK错误码。
      3. 应用程序进程轮询调用,继续向操作系统内核发起recvfrom读取数据。
      4. 操作系统内核数据准备好了,从内核缓冲区拷贝到用户空间。
    • 缺点:存在性能问题,即频繁的轮询,导致频繁的系统调用,同样会消耗大量的CPU资源,用户线程需要不断地询问内核数据是否就绪,不会交出 CPU,而会一直占用 CPU,可以考虑IO复用模型,去解决这个问题。

    非阻塞IO模式

  3. 多路复用 IO 模型

    • 概念:系统给我们提供一类函数select、poll、epoll函数),它们可以同时监控多个文件描述符fd(File Descriptor)的操作,任何一个返回内核数据就绪,应用进程再发起recvfrom系统调用。有一个线程不断去轮询多个 socket 的状态,只有当 socket 真正有读写事件时,才真正调用实际的 IO 读写操作
    • 优点:轮询每个 socket 状态是内核在进行的,这个效率要比用户线程要高的多
    • 缺点:一旦事件响应体很大,那么就会导致后续的事件迟迟得不到处理,并且会影响新的事件轮询。
    • select:
      1. 应用进程通过调用select函数,可以同时监控多个fd,在select函数监控的fd中,只要有任何一个数据状态准备就绪了,select函数就会返回可读状态,这时应用进程再发起recvfrom请求去读取数据。
      2. 缺点:(1)监听的IO最大连接数有限,在Linux系统上一般为1024;(2)select函数返回后,是通过遍历fdset,找到就绪的描述符fd。
    • poll:解决了连接数限制问题。但是select和poll一样,还是需要通过遍历文件描述符来获取已经就绪的socket。如果同时连接的大量客户端,在一时刻可能只有极少处于就绪状态,伴随着监视的描述符数量的增长,效率也会线性下降
    • epoll:通过epoll_ctl()来注册一个fd(文件描述符),一旦基于某个fd就绪时,内核会采用回调机制,迅速激活这个fd,当进程调用epoll_wait()时便得到通知。这里去掉了遍历文件描述符的操作,而是采用监听事件回调的机制。

    IO多路复用epoll

    IO多路复用select

  4. 信号驱动 IO 模型

    • 概念:信号驱动IO不再用主动询问的方式去确认数据是否就绪,而是向内核发送一个信号(调用sigaction的时候建立一个SIGIO的信号),然后应用用户进程可以去做别的事,不用阻塞。当内核数据准备好后,再通过SIGIO信号通知应用进程,数据准备好后的可读状态。应用用户进程收到信号之后,立即调用recvfrom,去读取数据。

    IO信号驱动模型

  5. 异步 IO 模型(AIO)

    • 概念:最理想的 IO 模型;BIO,NIO和信号驱动,在数据从内核复制到应用缓冲的时候,都是阻塞的,因此都不算是真正的异步。AIO实现了IO全流程的非阻塞,就是应用进程发出系统调用后,是立即返回的,但是立即返回的不是处理结果,而是表示提交成功类似的意思。等内核数据准备好,将数据拷贝到用户进程缓冲区,发送信号通知用户进程IO操作执行完毕。
    • IO 操作的两个阶段都不会阻塞用户线程,这两个阶段都是由内核自动完成

    异步IO模型

  6. 参考链接

IO方式:https://www.cnblogs.com/zedosu/p/6666984.html

  • BIO:同步阻塞式IO,服务器实现模式为一个连接一个线程,即客户端有连接请求时服务器端就需要启动一个线程进行处理,如果这个连接不做任何事情会造成不必要的线程开销,当然可以通过线程池机制改善。

    1. 同步阻塞式IO,在while循环中服务端会调用accept方法等待接收客户端的连接请求,一旦接收到一个连接请求,就可以建立通信套接字在这个通信套接字上进行读写操作,此时不能再接收其他客户端连接请求,只能等待同当前连接的客户端的操作执行完成。
    2. 如果BIO要能够同时处理多个客户端请求,就必须使用多线程,即每次accept阻塞等待来自客户端请求,一旦受到连接请求就建立通信套接字同时开启一个新的线程来处理这个套接字的数据读写请求,然后立刻又继续accept等待其他客户端连接请求,即为每一个客户端连接请求都创建一个线程来单独处理
    3. 随着开启的线程数目增多,将会消耗过多的内存资源,导致服务器变慢甚至崩溃
  • NIO:同步非阻塞式IO,服务器实现模式为一个请求一个线程,即客户端发送的连接请求都会注册到多路复用器上,多路复用器轮询到连接有I/O请求时才启动一个线程进行处理。

    1. 若服务端监听到客户端连接请求,便为其建立通信套接字(java中就是通道),然后返回继续监听,若同时有多个客户端连接请求到来也可以全部收到,依次为它们都建立通信套接字。
    2. 若服务端监听到来自已经创建了通信套接字的客户端发送来的数据,就会调用对应接口处理接收到的数据,若同时有多个客户端发来数据也可以依次进行处理。
    3. 监听多个客户端的连接请求和接收数据请求同时还能监听自己时候有数据要发送。
  • 当一个连接建立之后,他有两个步骤要做,第一步是接收完客户端发过来的全部数据,第二步是服务端处理完请求业务之后返回response给客户端。NIO和BIO的区别主要是在第一步。

    1. 在BIO中,等待客户端发数据这个过程是阻塞的,这样就造成了一个线程只能处理一个请求的情况,而机器能支持的最大线程数是有限的,这就是为什么BIO不能支持高并发的原因。
    2. 而NIO中,当一个Socket建立好之后,Thread并不会阻塞去接受这个Socket,而是将这个请求交给Selector,Selector会不断的去遍历所有的Socket,一旦有一个Socket建立完成,他会通知Thread,然后Thread处理完数据再返回给客户端——这个过程是不阻塞的,这样就能让一个Thread处理更多的请求了。
  • 应用场景

    (1)NIO适合处理连接数目特别多,但是连接比较短(轻操作)的场景,Jetty,Mina,ZooKeeper等都是基于java nio实现。

    (2)BIO方式适用于连接数目比较小且固定的场景,这种方式对服务器资源要求比较高,并发局限于应用中。

  • AIO(NIO.2):异步非阻塞式IO,服务器实现模式为一个有效请求一个线程,客户端的I/O请求都是由OS先完成了再通知服务器应用去启动线程进行处理。

二、String类

  1. 源码:

    public final class String
        implements java.io.Serializable, Comparable<String>, CharSequence
    {
        /** The value is used for character storage. */
        private final char value[];
    
        /** The offset is the first index of the storage that is used. */
        private final int offset;
    
        /** The count is the number of characters in the String. */
        private final int count;
    
        /** Cache the hash code for the string */
        private int hash; // Default to 0
    
        /** use serialVersionUID from JDK 1.0.2 for interoperability */
        private static final long serialVersionUID = -6849794470754667710L;
    
        ........
    }
    
    • 定义为final类

    • String是引用类型。所以 String 对象存储在堆空间,它的引用在栈空间

    • 不可变:String对象一旦被创建就是固定不变的了,对String对象的任何改变都不影响到原对象,相关的任何change操作都会生成新的对象

      好处:

      • 状态不可变的对象可以保证线程安全,不需要任何加锁操作保证线程安全,提高系统性能。
      • 不可变对象才能实现字符串常量池,将相同字面量放入常量池 同一块内存地址。
  2. String的比较

    • 对于引用类型来说,“==” 比较地址是否相同,当 String 调用 equals 比较时,会比较字符串字面量是否相同

    • 源码中 String 类将 equals 方法重写,判断两个字符串的每一个字符都相同则返回 true。

    • 根类 Object 中,equals 和 “” 是等价的,所以 equals 会对比出什么结果,取决于子类如何重写,如果不重写,默认和 “”是等价的

      public boolean equals(Object obj) {
              return (this == obj);
          }
      
  3. String 常量池

    • 当使用字面量形式声明对象时,会先检查字符串常量池中是否已经存在该对象,如果已存在直接将引用指向已存在的对象,如果不存在,将该对象放入常量池。

    • 当使用 new 关键字声明对象时,会先检查字符串常量池中是否已经存在该对象,如果已存在直接将引用指向已存在的对象,如果不存在,将该对象放入常量池。此外还会在堆空间开辟一块内存地址并且将该引用指向堆中的地址

    • 由于 new 关键字会在堆中开辟空间,所以开发中一般情况不建议使用,直接用字面量形式声明即可

      String str1 = "Hello";//产生1个对象放入常量池
      String str2 = new String("World");//创建两个对象,一个在堆,一个在常量池
      String str3 = new String("Hello");//创建一个对象在堆,由于常量池已经有 Hello 不会再创建
      
  4. String 类的“加法”:对于 String 类的 “+” 和 "+=" 两个特殊重载

    • “+” 将两个字符串连接生成一个新的对象,也就是内存中新开辟了一块空间

    • 当使用 "+" 对两个字符串常量连接时,由于 "+" 号两边都是常量,因此会直接连接放入常量池

    • “+” 两边并不全是常量,有一个是变量,这样它的结果是不能在编译期间确定的,只有在运行时才知道

    • intern():这个方法是个 native 方法,将调用它的对象尝试放入常量池,如果常量池已经有了,就返回指向常量池中的引用,如果没有就放入常量池,并且返回指向常量池中的引用。

    String str1 = "Hello";
    String str2 = "Hello" + "World";
    String str3 = str1 + "World";
    str3 = str3.intern();
    
  5. String 、StringBuilder、StringBuffer

    • StringBuilder:可以在原字符串的基础上进行增删改,并且不会新开辟内存空间。
      • 由于 String 对象不可变,每一个操作都会产生新的对象,这样似乎不太友好,可能会造成内存占用过大,而且会频繁创建对象。
      • String 内部维护了一个 final char[] value,不可变
      • StringBuilder 内部维护了一个 char[] value 没有用 final 修饰
      • String 的 “+”:底层会 new 一个 StringBuilder 对象调用 append 来连接字符串,生成一个新的 String 对象
    • StringBuffer:对于字符串的增删改方法都加上了 synchronized 关键字,对于字符串的操作是线程安全的,由于线程安全所以其性能也次于StringBuilder。

三、封装、继承、多态

封装

  1. 把对象的属性和方法结合成一个独立的整体,隐藏实现细节,并提供对外访问的接口
  2. 隐藏实现细节、安全性、增加代码复用性、模块化

继承

  1. 从已知的一个类中派生出一个新的类,叫子类。子类实现了父类所有非私有化的属性和方法,并根据实际需求扩展出新的行为。

  2. 优点

    • 继承是传递的,易于在其基础上构造和扩充。
    • 简化对事物的描绘,使得层次更加清晰。
    • 减少代码冗余。
    • 提高可维护性。
  3. 覆盖方法:关键字 super--调用父类方法

  4. 多态

    • 是否应该设计为继承关系: “ is - a ” 规则 , 它表明子类的每个对象也是超类的对象;程序中出现超类对象的任何地方都可以用子类对象置换
    • 一个Employee 变量既可以引用一个Employee 类对象 , 也可以引用一个 Employee 类的任何一个子类的对象
    • 不能将一个超类的引用赋给子类变量
  5. 阻止继承 : final 类和方法

  6. 强制类型转换:只能在继承层次内进行类型转换 。在将超类转换成子类之前 , 应该使用instanceof 进行检查。

  7. 抽象类:abstract;抽象类还可以包含具体数据和具体方法 。

    • 扩展抽象类可以有两种选择 。一种是在抽象类中定义部分抽象类方法或不定义抽象类方法 , 这样就必须将子类也标记为抽象类 ; 另一种是定义全部的抽象方法, 这样一来 , 子类就不是抽象的了
    • 抽象类不能被实例化
  8. 受保护访问:project;超类中的某些方法允许被子类访问 ,或允许子类的方法访问超类的某个域。

  9. Object : 所有类的超类

    • equals 方法;判断两个对象是否具有相同的引用
    • hashCode 方法:散列码 ( hash code ) 是由对象导出的一个整型值
    • 如果重新定义 equals 方法, 就必须重新定义hashCode 方法
    • toString 方法:返回表示对象值的字符串

多态

  1. 多个不同的对象对同一消息作出响应。同一消息根据不同的对象而采用各种不同的方法。

    (程序中定义的引用变量所指向的具体类型和通过该引用变量发出的方法调用在编程时并不确定,而是在程序运行期间才确定,即一个引用变量倒底会指向哪个类的实例对象,该引用变量发出的方法调用到底是哪个类中实现的方法,必须在由程序运行期间才能决定)

  2. 优点:主要是利于扩展 ,派生类的功能可以被基类的方法或引用变量所调用,这叫向后兼容,可以提高可扩充性和可维护性。

  3. 实现条件:

    • 继承:在多态中必须存在有继承关系的子类和父类。
    • 重写:子类对父类中某些方法进行重新定义,在调用这些方法时就会调用子类的方法。
    • 向上转型:在多态中需要将子类的引用赋给父类对象,只有这样该引用才能够具备技能调用父类的方法和子类的方法。
  4. 实现形式

    • 基于继承实现的多态:对于引用子类的父类类型,在处理该引用时,它适用于继承该父类的所有子类,子类对象的不同,对方法的实现也就不同,执行相同动作产生的行为也就不同。
    • 基于接口实现的多态:指向接口的引用必须是指定这实现了该接口的一个类的实例程序,在运行时,根据对象引用的实际类型来执行对应的方法。

四、对象与类

  1. 静态域和静态方法

    • 静态域:static,每个类中只有一个这样的域,而对象对于实例域都有自己的一份拷贝
    • 静态方法:不能用对象操作,直接用类使用方法
  2. 对象构造

    • 重载:多个方法有相同的名字,不同的参数
  • 默认域初始化:如果在构造器中没有显式地给域赋予初值 , 那么就会被自动地赋为默认值 : 数值为 0 、布尔值为 false、对象引用为 null 。
    • 无参数的构造器:如果没有编写构造器 , 那么系统就会提供一个无参数构造器;如果提供了至少一个构造器 , 但是没有提供无参数的构造器 , 则在构造对象时如果没有提供参数就会被视为不合法
    • 显式域初始化
    • 初始化块:在一个类的声明中 ,可以包含多个代码块。 只要构造类的对象 , 这些块就会被执行;对类的静态域进行初始化可以使用静态的初始化块
    • 对象析构与 finalize 方法:Java 有自动的垃圾回收器, 所以Java 不支持析构器; 某些对象使用了内存之外的其他资源:加 finalize 方法。 finalize 方法将在垃圾回收器清除对象之前调用

五、反射

  • 一种能够在程序运行时动态访问、修改某个类中任意属性和方法的机制

  • 用处:根据类名创建实例;用Method.invoke执行方法

  • 原理:当我们编写完一个Java项目之后,每个java文件都会被编译成一个.class文件,这些Class对象承载了这个类的所有信息,包括父类、接口、构造函数、方法、属性等,这些class文件在程序运行时会被ClassLoader加载到虚拟机中。当一个类被加载以后,Java虚拟机就会在内存中自动产生一个Class对象。我们通过new的形式创建对象实际上就是通过这些Class来创建,只是这个过程对于我们是不透明的而已;反射的工作原理就是借助Class.java、Constructor.java、Method.java、Field.java这四个类在程序运行时动态访问和修改任何类的行为和状态。

  • 四个核心类

    java.lang.Class.java:类对象;

    java.lang.reflect.Constructor.java:类的构造器对象;

    java.lang.reflect.Method.java:类的方法对象;

    java.lang.reflect.Field.java:类的属性对象;

  • JVM是如何构建一个实例的

    JVM创建类实例

  • 类加载器:loadClass(),告诉它需要加载的类名,它会帮你加载

    检查是否已经加载,有就直接返回,避免重复加载

    当前缓存中确实没有该类,那么遵循父优先加载机制,加载.class文件

    上面两步都失败了,调用findClass()方法加载

  • 反射API

    (1)获取反射中的Class对象

    1.Class clz = Class.forName("java.lang.String");//知道该类的全路径名
    2.Class clz = String.class;//适合在编译前就知道操作的 Class
    3.String str = new String("Hello");
      Class clz = str.getClass();//getClass方法
    

    (2)通过反射创建类对象

    //1.Class 对象的 newInstance() 方法
    Class clz = Apple.class;
    Apple apple = (Apple)clz.newInstance();
    //2.Constructor 对象的 newInstance() 方法
    Class clz = Apple.class;
    Constructor constructor = clz.getConstructor();
    Apple apple = (Apple)constructor.newInstance();
    //注:通过 Constructor 对象创建类对象可以选择特定构造方法,而通过 Class 对象则只能使用默认的无参数构造方法
    

    (3)通过反射获取类属性、方法、构造器

    //通过 Class 对象的 getFields() 方法可以获取 Class 类的属性,但无法获取私有属性
    Class clz = Apple.class;
    Field[] fields = clz.getFields();
    for (Field field : fields) {
        System.out.println(field.getName());
    }
    
    //使用 Class 对象的 getDeclaredFields() 方法则可以获取包括私有属性在内的所有属性
    Class clz = Apple.class;
    Field[] fields = clz.getDeclaredFields();
    for (Field field : fields) {
        System.out.println(field.getName());
    }
    
  • 优点:灵活、自由度高:不受类的访问权限限制,想对类做啥就做啥;

  • 缺点:

    1. 性能问题:通过反射访问、修改类的属性和方法时会远慢于直接操作,但性能问题的严重程度取决于在程序中是如何使用反射的。如果使用得很少,不是很频繁,性能将不会是什么问题;
  1. 安全性问题:反射可以随意访问和修改类的所有状态和行为,破坏了类的封装性,如果不熟悉被反射类的实现原理,随意修改可能导致潜在的逻辑问题;
  2. 兼容性问题:因为反射会涉及到直接访问类的方法名和实例名,不同版本的API如果有变动,反射时找不到对应的属性和方法时会报异常;
  • 实例

    Class clz = Class.forName("com.chenshuyi.reflect.Apple"); 
    Method method = clz.getMethod("setPrice", int.class); 
    Constructor constructor = clz.getConstructor(); 
    

Object object = constructor.newInstance();
method.invoke(object, 4);


```java
Apple apple = new Apple(); //直接初始化,「正射」
apple.setPrice(4);

六、JAVA异常

异常

分类

  1. Error:运行时系统的内部错误和资源耗尽错误
  2. Exception:
    • RuntimeException:可能在 Java 虚拟机正常运行期间抛出的异常的超类
    • CheckedException:一般是外部错误,这种异常都发生在编译阶段

处理

  1. throw 和 throws 的区别:
    • 位置:throws 用在函数上,后面跟的是异常类,可以跟多个;而 throw 用在函数内,后面跟的是异常对象。
    • 功能:throws 用来声明异常,让调用者只知道该功能可能出现的问题,可以给出预先的处理方式;throw抛出具体的问题对象,执行到throw,功能就已经结束了
    • 发生:throws 表示出现异常的一种可能性,并不一定会发生这些异常;throw 则是抛出了异常,执行 throw 则一定抛出了某种异常对象
    • 同:消极处理异常

七、Java注解

  1. 概念:Annotation(注解)是 Java 提供的一种对元程序中元素关联信息和元数据(metadata)的途径和方法。Annatation(注解)是一个接口,程序可以通过反射来获取指定程序中元素的 Annotation对象,然后通过该 Annotation 对象来获取注解中的元数据信息
    • 对于注解,是有专门的程序去读取它,解析它,然后根据得到的消息去执行相应的操作。
    • 注释是给人看到的,注解是给程序看的
    • 注解的本质是个接口
  2. 注解类型:
    1. 自定义注解(实际很少)
    2. JDK内置注解(@Override检验方法重写)
    3. 框架中的注解
  3. 注解流程:定义注解、使用注解、读取并执行相应流程(通过反射机制)
  4. 4 种标准元注解
    • @Target:说明了Annotation所修饰的对象范围
    • @Retention:定义了该 Annotation 被保留的时间长短
    • @Documented:描述其它类型的 annotation 应该被作为被标注的程序成员的公共 API(用于制作文档)
    • @Inherited: 阐述了某个被标注的类型是被继承的
    • @Repeatable:JDK 8中新加入,该注解可以在同一个地方被重复使用多次,用@Repeatable来修饰注解时需要指明一个接受重复注解的容器
  5. 注解处理器

Q:实现原理

八、JAVA自动拆装箱

  1. 目的:原始类型值转自动地转换成对应的对象
  2. 概念:Java自动将原始类型值转换成对应的对象,比如将int的变量转换成Integer对象,这个过程叫做装箱,反之将Integer对象转换成int类型值,这个过程叫做拆箱。原始类型byte,short,char,int,long,float,double和boolean对应的封装类为Byte,Short,Character,Integer,Long,Float,Double,Boolean。
  3. 要点:自动装箱时编译器调用valueOf将原始类型值转换成对象,同时自动拆箱时,编译器通过调用类似intValue(),doubleValue()这类的方法将对象转换成原始类型值。
  4. 发生时机:赋值时、方法调用时
  5. 缺点:一个循环中进行自动装箱操作的情况,就会创建多余的对象,影响程序的性能。
  6. 重载与自动装箱
  7. 注意事项:对象相等比较、容易混乱的对象和原始数据值、缓存的对象、生成无用对象增加GC压力

九、JAVA泛型

  1. 概念:泛型就是把类型明确的工作推迟到创建对象或调用方法的时候才去明确的特殊的类型。
    • 泛型只存在于编译期:泛型的信息不会进行运行阶段
    • 泛型里面数据类型不能是基本类型:虚拟机在编译时会把带泛型的转换成Object类型,而基本类型不属于Object类型,
  2. 使用泛型的原因:安全简单、在编译的时候检查类型安全,并且所有的强制转换都是自动和隐式的,提高代码的重用率。
  3. 使用方式:
    • 泛型类:public class Test<T>}{} T表示未知类型
    • 泛型接口:public interface Test<T>{} 和定义类一样
    • 泛型方法:public <T> void Test(T name){}
  4. 泛型通配符(?)
  5. 类型擦除:
    • Java 中的泛型基本上都是在编译器这个层次来实现的。在生成的 Java 字节代码中是不包含泛型中的类型信息的。使用泛型的时候加上的类型参数,会被编译器在编译的时候去掉。这个过程就称为类型擦除
    • Java语言的泛型采用的是擦除法实现的伪泛型,泛型信息(类型变量、参数化类型)编译之后通通被除掉了。
      1. 优点:实现简单、非常容易Backport,运行期也能够节省一些类型所占的内存空间。
      2. 缺点:不如真泛型灵活和强大
    • 解决:协助泛型类,给定泛型的边界
    • 类型擦除后保留的原始类型:原始类型 就是擦除去了泛型信息,最后在字节码中的类型变量的真正类型,无论何时定义一个泛型,相应的原始类型都会被自动提供,类型变量擦除,并使用其限定类型(无限定的变量用Object)替换。
  6. 参考链接

十、JDK、JRE

JAVA8新特性

  1. Lambda 表达式:Lambda 允许把函数作为一个方法的参数(函数作为参数传递到方法中)
  2. 函数式接口:指的是一个有且仅有一个抽象方法,但是可以有多个非抽象方法的接口,这样的接口可以隐式转换为 Lambda 表达式
  3. 方法引用:方法引用提供了非常有用的语法,可以直接引用已有Java类或对象(实例)的方法或构造器。与lambda联合使用,方法引用可以使语言的构造更紧凑简洁,减少冗余代码
  4. 默认方法:默认方法就是一个在接口里面有了一个实现的方法
  5. Stream API:新添加的Stream API(java.util.stream) 把真正的函数式编程风格引入到Java中。
  6. Optional 类:Optional 类已经成为 Java 8 类库的一部分,用来解决空指针异常。
  7. Date Time API:加强对日期与时间的处理。
  8. Nashorn, JavaScript 引擎:Java 8提供了一个新的Nashorn javascript引擎,它允许我们在JVM上运行特定的javascript应用

十一、序列化与反序列化

  1. 序列化:把对象转换为字节序列的过程称为对象的序列化。

  2. 反序列化:把字节序列恢复为对象的过程称为对象的反序列化。

  3. 什么情况下需要序列化:

    • 内存中的对象状态保存到一个文件中或者数据库中

    • 套接字在网络上传送对象

    • 通过RMI传输对象

    • 序列化能够实现深复制,即可以复制引用的对象。

      //深拷贝:指的是拷贝一个对象时,不仅仅把对象的引用进行复制,还把该对象引用的值也一起拷贝。如果引用类型里面还包含很多引用类型,或者内层引用类型的类里面又包含多层引用类型,那么通过clone()方法逐一拷贝每一个引用类型来实现深拷贝的方法就会很麻烦。这时我们可以用序列化来实现对象的深拷贝。
      
  4. 实现序列化:

    • 实现Serializable接口(java原生)
    • Hessian 序列化:一种支持动态类型、跨语言、基于对象传输的网络协议
    • Json序列化:将数据对象转换为 JSON 字符串,在序列化过程中抛弃了类型信息,所以反序列化时只有提供类型信息才能准确地反序列化
  5. 什么时候不要序列化

    • 在Java进行应用开发的时候,用户的个人信息,包括密码、电话号码、具体住址这些隐私信息的时候,就需要防止对象被序列化,如果被序列化用于网络传输,则很有可能会造成安全问题。
    • 声明为static和transient类型的成员数据不能被序列化。因为static代表类的状态,transient代表对象的临时数据。
  6. 实现原理

十二、深拷贝和浅拷贝

  1. 浅拷贝:

    • 概念:浅拷贝又称为浅复制,浅克隆,浅拷贝是指拷贝时只拷贝对象本身(包括对象中的基本变量),而不拷贝对象包含的引用所指向的对象,拷贝出来的对象的所有变量的值都含有与原来对象相同的值,而所有对其他对象的引用都指向原来的对象,简单地说,浅拷贝只拷贝对象不拷贝引用。
    • 实现:典型实现方式是:让被复制对象的类实现Cloneable接口,并重写clone()方法即可。
    浅拷贝
  2. 深拷贝:

    • 深拷贝又称为深复制,深克隆,深拷贝不仅拷贝对象本身,而且还拷贝对象包含的引用所指向的对象,拷贝出来的对象的所有变量(不包含那些引用其他对象的变量)的值都含有与原来对象的相同的值,那些引用其他对象的变量将指向新复制出来的新对象,而不指向原来的对象,简单地说,深拷贝不仅拷贝对象,而且还拷贝对象包含的引用所指向的对象。
    • 实现:
      1. 实现深拷贝,首先需要对更深一层次的引用类做改造,让其也实现Cloneable接口并重写clone()方法:
      2. 反序列化实现深拷贝,重写clone()方法,将对象本身序列化到字节流,再将字节流通过反序列化方式得到对象副本,要求被引用的子类也必须是可以序列化的,即实现了Serializable接口
    深拷贝
  3. 参考链接:


第三部分 JAVA并发编程

一、voliate关键字

  1. voliate关键字的作用:解决线程并发的问题(本地内存和主存中变量可能出现值不同)

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

    • 基于内存屏障的防止指令重排:底层的实现方式是基于4种内存屏障:读读、读写、写读、读读屏障。

      指令重排序:一般来说,处理器为了提高程序运行效率,可能会对输入代码进行优化,它不保证程序中各个语句的执行先后顺序同代码中的顺序一致,但是它会保证程序最终执行结果和代码顺序执行的结果是一致的。

  2. 对性质的保障

    • 无法保证原子性,虽然可以使得变量立即可见,但是无法保证操作具有原子性
    • volatile关键字能禁止指令重排序,所以volatile能在一定程度上保证有序性
    • 可保证可见性
  3. 原理和实现机制:(如何保证可见性和禁止指令重排序的)内存屏障:

    • 它确保指令重排序时不会把其后面的指令排到内存屏障之前的位置,也不会把前面的指令排到内存屏障的后面;即在执行到内存屏障这句指令时,在它前面的操作已经全部完成;
    • 它会强制将对缓存的修改操作立即写入主存;
    • 如果是写操作,它会导致其他CPU中对应的缓存行无效。
  4. 使用volatile关键字的条件(需要保证操作是原子性操作,才能保证并发执行正确)

    • 对变量的写操作不依赖于当前值
    • 该变量没有包含在具有其他变量的不变式中
  5. 使用volatile的场景:https://www.cnblogs.com/ouyxy/p/7242563.html

    • 模式 #1:状态标志:也许实现 volatile 变量的规范使用仅仅是使用一个布尔状态标志,用于指示发生了一个重要的一次性事件,例如完成初始化或请求停机。
    • 模式 #2:一次性安全发布(one-time safe publication):
    • 模式 #3:独立观察(independent observation):安全使用 volatile 的另一种简单模式是:定期 “发布” 观察结果供程序内部使用。【例如】假设有一种环境传感器能够感觉环境温度。一个后台线程可能会每隔几秒读取一次该传感器,并更新包含当前文档的 volatile 变量。然后,其他线程可以读取这个变量,从而随时能够看到最新的温度值。使用该模式的另一种应用程序就是收集程序的统计信息。
    • 模式 #4:“volatile bean” 模式:
    • 模式 #5:开销较低的“读-写锁”策略:如果读操作远远超过写操作,可以结合使用内部锁volatile 变量来减少公共代码路径的开销

二、线程安全

  1. 概念:当多个线程访问一个对象时,如果不用考虑这些线程在运行时环境下的调度和交替执行,也不需要进行额外的同步,或者在调用方法进行任何其他的协调操作,调用这个对象的行为都可以获得正确的结果,那这个对象是线程安全的。

  2. 操作共享数据:不可变、绝对线程安全、相对线程安全、线程兼容、线程对立

  3. 线程安全实现方法:

    • 互斥同步(阻塞同步):synchronized关键字、重入锁ReentrantLock
    1.同步是指在多线程并发访问共享数据时,保证共享数据同一时刻只被一个线程使用。
    2.synchronized关键字经过编译之后,会在同步块的前后分别形成monitorenter和monitorexit这两个字节码指令,这两个指令都需要一个reference类型的参数来指明要锁定和要解锁的对象。如果Java代码里synchronized同步块明确指定了这个对象参数,那这个参数就是明确指定的这个对象。如果没有明确指定的话,那就根据synchronized关键字修饰的实例方法还是静态方法(类方法),去取对应的对象实例或者取当前类对应的Class类的对象来作为锁对象。
    3.互斥同步最主要的问题就是进行线程阻塞和唤醒需要切换入操作系统的核心态,这个状态转换需要消耗大量的处理器时间,因此互斥同步也称为阻塞同步。从处理问题的方式上来说,互斥同步它其实是属于一种悲观的并发策略,它认为只要不去做同步措施(例如加锁),那就肯定会出问题,无论你的共享数据是不是真的出现了多线程去竞争,它都要进行加锁、用户态核心态转换、维护锁计数器、检查是否有被阻塞的线程需要唤醒等操作
    
    • 非阻塞同步:基于冲突检测的乐观并发策略(最常见的就是CAS)
    • 无同步方案:如果一个方法不涉及数据共享,那么不需要任何手段就能保证其线程安全
    1. 可重入代码:这段代码可以在运行的任何时刻中断,转而去执行另一端代码,而在控制权返回后可以继续执行,原来的程序不会出错,也不会对结果有影响。
    2. 线程本地存储:如果一段代码中所需要的数据必须与其他代码共享,那么就看这些共享的数据能否保证在同一个线程中执行,如果能,则无需同步。对于每一个Thread对象,其中都有一个ThreadLocalMap对象,这个对象中,以本地线程变量的hashCode为键,本地线程变量的值为key-value键值对,因而我们可以通过这个ThreadLocalMap对象来找到对应的本地变量的值。
    
  4. 线程死锁(感觉和操作系统中死锁差不多)

    • 线程死锁是指两个或两个以上的线程互相持有对方所需要的资源,由于synchronized的特性,一个线程持有一个资源,或者说获得一个锁,在该线程释放这个锁之前,其它线程是获取不到这个锁的,而且会一直死等下去,因此这便造成了死锁

三、内存模型(JMM)

  1. 概念:是一种虚拟机规范(标准之类的),在并发过程中如何处理原子性、可见性和有序性这 3 个特征来建立
  2. 目的:屏蔽掉各种硬件和操作系统的内存访问差异,以实现让Java程序在各种平台下都能达到一致的并发效果,JMM规范了Java虚拟机与计算机内存是如何协同工作的:规定了一个线程如何和何时可以看到由其他线程修改过后的共享变量的值,以及在必须时如何同步的访问共享变量。
  3. 主内存与工作内存之间的具体交互协议,即一个变量如何从主内存拷贝到工作内存、如何从工作内存同步到主内存之间的实现细节,Java内存模型定义了以下八种操作:
    • lock(锁定):作用于主内存的变量,把一个变量标识为一条线程独占状态。
    • unlock(解锁):作用于主内存变量,把一个处于锁定状态的变量释放出来,释放后的变量才可以被其他线程锁定。
    • read(读取):作用于主内存变量,把一个变量值从主内存传输到线程的工作内存中,以便随后的load动作使用
    • load(载入):作用于工作内存的变量,它把read操作从主内存中得到的变量值放入工作内存的变量副本中。
    • use(使用):作用于工作内存的变量,把工作内存中的一个变量值传递给执行引擎,每当虚拟机遇到一个需要使用变量的值的字节码指令时将会执行这个操作。
    • assign(赋值):作用于工作内存的变量,它把一个从执行引擎接收到的值赋值给工作内存的变量,每当虚拟机遇到一个给变量赋值的字节码指令时执行这个操作。
    • store(存储):作用于工作内存的变量,把工作内存中的一个变量的值传送到主内存中,以便随后的write的操作。
    • write(写入):作用于主内存的变量,它把store操作从工作内存中一个变量的值传送到主内存的变量中。

且要满足以下规则:

  • 如果要把一个变量从主内存中复制到工作内存,就需要按顺寻地执行read和load操作, 如果把变量从工作内存中同步回主内存中,就要按顺序地执行store和write操作。但Java内存模型只要求上述操作必须按顺序执行,而没有保证必须是连续执行。
  • 不允许read和load、store和write操作之一单独出现
  • 不允许一个线程丢弃它的最近assign的操作,即变量在工作内存中改变了之后必须同步到主内存中。
  • 不允许一个线程无原因地(没有发生过任何assign操作)把数据从工作内存同步回主内存中。
  • 一个新的变量只能在主内存中诞生,不允许在工作内存中直接使用一个未被初始化(load或assign)的变量。即就是对一个变量实施use和store操作之前,必须先执行过了assign和load操作。
  • 一个变量在同一时刻只允许一条线程对其进行lock操作,但lock操作可以被同一条线程重复执行多次,多次执行lock后,只有执行相同次数的unlock操作,变量才会被解锁。lock和unlock必须成对出现
  • 如果对一个变量执行lock操作,将会清空工作内存中此变量的值,在执行引擎使用这个变量前需要重新执行load或assign操作初始化变量的值
  • 如果一个变量事先没有被lock操作锁定,则不允许对它执行unlock操作;也不允许去unlock一个被其他线程锁定的变量。
  • 对一个变量执行unlock操作之前,必须先把此变量同步到主内存中(执行store和write操作)。
  1. Happens-Before:

    • 程序顺序规则:如果程序中操作 A 在操作 B 之前,那么在线程中操作 A 将在操作 B 之前执行。
    • 监视器锁规则:在监视器锁上的解锁操作必须在同一个监视器锁上的加锁操作之前执行。
    • volatile 变量规则:对 volatile 变量的写入操作必须在对该变量的读操作之前执行。
    • 线程启动规则:在线程上对 Thread.start 的调用必须在该线程中执行任何操作之前执行。
    • 线程结束规则:线程中的任何操作都必须在其他线程检测到该线程已经结束之前执行,或者从 Thread.join 中成功返回,或者在调用 Thread.isAlive 时返回 false。
    • 中断规则:当一个线程在另一个线程上调用 interrupt 时,必须在被中断线程检测到 interrupt 调用之前执行(通过抛出 InterruptException,或者调用 isInterrupted 和 interrupted)。
    • 终结器规则:对象的构造函数必须在启动该对象的终结器之前执行完成。
    • 传递性:如果操作 A 在操作 B 之前执行,并且操作 B 在操作 C 之前执行,那么操作 A 必须在操作 C 之前执行。
  2. 三个特性的保障:

    • 原子性:原子性变量操作
    • 可见性:线程修改了共享变量的值,其他线程可以立即得知这个修改(volatile、synchronized、final)
    • 有序性:在本线程内观察,操作有序;在其他线程观察本线程,操作无序(volatile(禁止指令重排序)、synchronized(一个变量同一时刻只允许一个线程对其lock操作))

四、线程池

  1. 优点:降低资源消耗、提高响应速度、提高线程的可管理性

  2. 创建线程池:ThreadPoolExecutor来创建一个线程池

    • corePoolSize(核心线程数,即就是中线程池中长时间稳定存活的线程数)
    • runnableTaskQueue(任务队列):可选
ArrayBlockingQueue(基于数组结构的有界阻塞队列,此队列按FIFO(先进先出)原则对元素进行排序)
LinkedBlockingQueue(基于链表结构的阻塞队列,此队列按FIFO排序元素)
SynchronousQueue(不存储元素的阻塞队列)
PriorityBlockingQueue(具有优先级的无限阻塞队列)
    
(1)有界的任务队列:该功能由ArrayBlockingQueue提供。使用该对象时,若有新任务需要执行,如果线程池的实际线程数少于corePoolSize,则会优先创建新的线程,若大于,则会将新任务加入到等待队列。当队列满时,则在总线程数不大于maximumPoolSize的前提下,创建新的线程执行任务。
(2)无界的任务队列:该功能有LinkedBlockingQueue类实现。使用该对象时,除非系统资源耗尽,否则不存在入队失败的情况。当有新任务来时,若系统的线程数量小于corePoolSize,线程池会生成新的线程执行,当线程数达到corePoolSize时就不会再继续增加了,而是将任务加入到等待队列,该队列会保持无限增长。
(3)直接提交的队列:该功能有SynchronousQueue对象提供。使用该对象,提交的任务不会被真实的保存,而总是将新任务提交到线程执行,没有空闲线程时,则尝试创建新的线程,如果线程数量已经达到最大值,则执行拒绝策略。
(4)优先任务队列:该功能由PriorityBlockingQueue实现。该类是一个特殊的无界队列。ArrayBlockingQueue和LinkedBlockingQueue都是按照先进先出处理任务,而该类则可以根据任务自身的优先级顺序先后执行。
  • maximumPoolSize(线程池最大数量,重点强调线程中最大可包含的线程数。最大线程数的上限需要根据实际情况而定)
  • ThreadFactory:用于设置创建线程的工厂
  • RejectedExecutionHandler(饱和策略):
·AbortPolicy:直接抛出异常。-----默认
//拒绝策略在拒绝任务时,会直接抛出一个类型为 RejectedExecutionException 的 RuntimeException,让你感知到任务被拒绝了,于是你便可以根据业务逻辑选择重试或者放弃提交等策略。
·CallerRunsPolicy:只用调用者所在线程来运行任务。
//当有新任务提交后,如果线程池没被关闭且没有能力执行,则把这个任务交于提交任务的线程执行,也就是谁提交任务,谁就负责执行任务
//第一点新提交的任务不会被丢弃,这样也就不会造成业务损失。
//第二点好处是,由于谁提交任务谁就要负责执行任务,这样提交任务的线程就得负责执行任务,而执行任务又是比较耗时的,在这段期间,提交任务的线程被占用,也就不会再提交新的任务,减缓了任务提交的速度,相当于是一个负反馈。在此期间,线程池中的线程也可以充分利用这段时间来执行掉一部分任务,腾出一定的空间,相当于是给了线程池一定的缓冲期。
·DiscardOldestPolicy:丢弃队列里最近的一个任务,并执行当前任务。
//如果线程池没被关闭且没有能力执行,则会丢弃任务队列中的头结点,通常是存活时间最长的任务,这种策略与第二种不同之处在于它丢弃的不是最新提交的,而是队列中存活时间最长的,这样就可以腾出空间给新提交的任务,但同理它也存在一定的数据丢失风险。
·DiscardPolicy:不处理,丢弃掉。
//当新任务被提交后直接被丢弃掉,也不会给你任何的通知,相对而言存在一定的风险,因为我们提交的时候根本不知道这个任务会被丢弃,可能造成数据丢失。
-也可以实现RejectedExecutionHandler接口自定义策略
  • keepAliveTime(线程活动保持时间,该参数是指非核心线程的存活时间,用来严格控制线程池中线程的数量尽可能的保持在一定的范围内)
  • TimeUnit(线程活动保持时间的单位)
  1. 向线程池提交任务:

    • execute():提交不需要返回值的任务,所以无法判断任务是否被线程池执行成功
    • submit():提交需要返回值的任务。线程池会返回一个future类型的对象,通过这个future对象可以判断任务是否执行成功,并且可以通过future的get()方法来获取返回值,get()方法会阻塞当前线程直到任务完成,而使用get(long timeout,TimeUnit unit)方法则会阻塞当前线程一段时间后立即返回,这时候有可能任务没有执行完
  2. 关闭线程池

    • shutdown或shutdownNow

    • 原理:遍历线程池中的工作线程,然后逐个调用线程的interrupt方法来中断线程,所以无法响应中断的任务可能永远无法终止

    • 区别:

      shutdownNow首先将线程池的状态设置成STOP,然后尝试停止所有的正在执行或暂停任务的线程,并返回等待执行任务的列表

      shutdown只是将线程池的状态设置成SHUTDOWN状态,然后中断所有没有正在执行任务的线程。

  3. 监控:通过线程池提供的参数进行监控

    • taskCount:线程池需要执行的任务数量
    • completedTaskCount:线程池在运行过程中已完成的任务数量,小于或等于taskCount
    • largestPoolSize:线程池里曾经创建过的最大线程数量
    • getPoolSize:线程池的线程数量,只增不减
    • getActiveCount:获取活动的线程数
    • 通过扩展线程池进行监控。可以通过继承线程池来自定义线程池,重写线程池的beforeExecute、afterExecute和terminated方法,也可以在任务执行前、执行后和线程池关闭前执行一些代码来进行监控
  4. 四种线程池:

    • CachedThreadPool:创建一个可根据需要创建新线程的线程池

    • FixedThreadPool:创建一个可重用固定线程数的线程池,以共享的无界队列方式来运行这些线程

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

    • SingleThreadExecutor:返回一个线程池(这个线程池只有一个线程),这个线程池可以在线程死后(或发生异常时)重新启动一个线程来替代原来的线程继续执行下去(corePoolSize为1,maximumPoolSize为1,keepAliveTime为0,阻塞队列使用的是LinkedBlockingQueue)

    • 参考链接:https://blog.csdn.net/qq_38428623/article/details/86688523

    • 创建方法:

      ExecutorService newExecutorService = Executors.newCachedThreadPool();
      ExecutorService newExecutorService = Executors.newFixedThreadPool(3);
      ScheduledExecutorService newScheduledThreadPool = Executors.newScheduledThreadPool(5);
      ExecutorService newSingleThreadExecutor = Executors.newSingleThreadExecutor();
      //https://www.cnblogs.com/-flq/p/15693321.html
      
  5. 线程池原理----控制运行的线程的数量

  6. 线程复用:继承重写Thread 类,在其 start 方法中添加不断循环调用传递过来的 Runnable 对象。 这就是线程池的实现原理。循环方法中不断获取 Runnable 是用 Queue 实现的

  7. 线程池的组成:

    线程池管理器:用于创建并管理线程池

    工作线程:线程池中的线程

    任务接口:每个任务必须实现的接口,用于工作线程调度其运行

    任务队列:用于存放待处理的任务,提供一种缓冲机制

  8. 拒绝策略:拒绝时机——当调用 shutdown 等方法关闭线程池的时候,如果此时继续向线程池提交任务,就会被拒绝;当任务队列(workQueue)已满,而且

    线程达到最大线程数(maximumPoolSize),如果再增加任务,也会被拒绝

  9. 线程池工作过程:

    • 线程池刚创建时,里面没有一个线程。任务队列是作为参数传进来的。不过,就算队列里面有任务,线程池也不会马上执行它们。
    • 当调用 execute() 方法添加一个任务时,线程池会做如下判断:
      1. 如果正在运行的线程数量小于 corePoolSize,那么马上创建线程运行这个任务;
      2. 如果正在运行的线程数量大于或等于 corePoolSize,那么将这个任务放入队列;
      3. 如果这时候队列满了,而且正在运行的线程数量小于 maximumPoolSize,那么还是要创建非核心线程立刻运行这个任务;
      4. 如果队列满了,而且正在运行的线程数量大于或等于 maximumPoolSize,那么线程池会抛出异常 RejectExecutionException。
    • 当一个线程完成任务时,它会从队列中取下一个任务来执行。
    • 当一个线程无事可做,超过一定的时间(keepAliveTime)时,线程池会判断,如果当前运行的线程数大于 corePoolSize,那么这个线程就被停掉。所以线程池的所有任务完成后,它最终会收缩到 corePoolSize 的大小。

五、锁

1.类型

  1. 乐观锁/悲观锁(不是指具体的什么类型的锁,而是指看待并发同步的角度)

    • 乐观锁认为自己在使用数据时不会有别的线程修改数据,所以不会添加锁,只是在更新数据的时候去判断之前有没有别的线程更新了这个数据作如果这个数据没有被更新,当前线程将自己修改的数据成功写入。如果数据已经被其他线程更新,则根据不同的实现方式执行不同的操作。乐观锁在Java中是通过使用无锁编程来实现,最常采用的是CAS算法
    • 悲观锁认为自己在使用数据的时候一定有别的线程来修改数据,因此在获取数据的时候会先加锁,确保数据不会被别的线程修改
    • 悲观锁适合写操作多的场景,先加锁可以保证写操作时数据正确。
    • 乐观锁适合读操作多的场景,不加锁的特点能够使其读操作的性能大幅提升
  2. 自旋锁、适应性自旋锁

    • 自旋锁是指尝试获取锁的线程不会立即阻塞,而是采用循环的方式去尝试获取锁。
    • 优点:对于锁的竞争不激烈,且占用锁时间非常短的代码块来说性能能大幅度的提升,因为自旋的消耗会小于线程阻塞挂起再唤醒的操作的消耗
    • 缺点:但是如果锁的竞争激烈,或者持有锁的线程需要长时间占用锁执行同步块,自旋锁占用 cpu 做无用功(可设定自旋时间阈值)
    • 适应性自旋锁:自旋的时间不固定,由前一次在同一个锁上的自旋时间及锁的拥有者的状态来决定。如果在同一个锁对象上,自旋等待刚刚成功获得过锁,并且持有锁的线程正在运行中,那么虚拟机就会认为这次自旋也是很有可能再次成功,进而它将允许自旋等待持续相对更长的时间。如果对于某个锁,自旋很少成功获得过,那在以后尝试获取这个锁时将可能省略掉自旋过程,直接阻塞线程,避免浪费处理器资源。
  3. 可重入锁:指在同一个线程在外层方法获取锁的时候,再进入该线程的内层方法会自动获取锁。优点是可一定程度避免死锁

  4. 共享锁和独占锁:

    • 独享锁是指该锁一次只能被一个线程所持有。共享锁是指该锁可被多个线程所持有。
    • 独享锁与共享锁也是通过AQS来实现的,通过实现不同的方法,来实现独享或者共享
    • 具体实现:互斥锁/读写锁——ReentrantLock/ReadWriteLock
  5. 公平锁/非公平锁

    • 公平锁是指多个线程按照申请锁的顺序来获取锁。优点是等待锁的线程不会饿死。缺点是整体吞吐效率相对非公平锁要低,等待队列中除第一个线程以外的所有线程都会阻塞,CPU唤醒阻塞线程的开销比非公平锁大。
    • 非公平锁是指多个线程获取锁的顺序并不是按照申请锁的顺序,可能后申请的线程比先申请的线程优先获取锁。优点是可以减少唤起线程的开销,整体的吞吐效率高,因为线程有几率不阻塞直接获得锁,CPU不必唤醒所有线程。缺点是处于等待队列中的线程可能会饿死,或者等很久才会获得锁。
  6. 参考链接:https://www.cnblogs.com/mvpsjf773/p/15253817.html

2.重要的几种锁

  1. Synchronized 同步锁(可重入锁、独享锁、非公平锁):把任意一个非 NULL 的对象当作锁,属于独占式的悲观锁,同时属于可重入锁。

    • 作用:解决的是多个线程之间访问资源的同步性,synchronized关键字可以保证被它修饰的方法或者代码块在任意时刻只能有一个线程执行。

    • 使用方式:

      1. 修饰实例方法: 作用于当前对象实例加锁,进入同步代码前要获得当前对象实例的锁
      2. 修饰静态方法: 也就是给当前类加锁,会作用于类的所有对象实例,因为静态成员不属于任何一个实例对象,是类成员( static 表明这是该类的一个静态资源,不管new了多少个对象,只有一份)。所以如果一个线程A调用一个实例对象的非静态 synchronized 方法,而线程B需要调用这个实例对象所属类的静态 synchronized 方法,是允许的,不会发生互斥现象,因为访问静态 synchronized 方法占用的锁是当前类的锁,而访问非静态 synchronized 方法占用的锁是当前实例对象锁
      3. 修饰代码块: 指定加锁对象,对给定对象加锁,进入同步代码库前要获得给定对象的锁。
    • 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. synchronized 同步语句块的实现使用的是 monitorenter 和 monitorexit 指令,其中 monitorenter 指令指向同步代码块的开始位置,monitorexit 指令则指明同步代码块的结束位置。
      2. synchronized 修饰的方法并没有 monitorenter 指令和 monitorexit 指令,取得代之的确实是 ACC_SYNCHRONIZED 标识,该标识指明了该方法是一个同步方法,JVM 通过该 ACC_SYNCHRONIZED 访问标志来辨别一个方法是否声明为同步方法,从而执行相应的同步调用。
      3. 上面两种方法实际上都是同一个原理:都是需要获取minitor对象的所有权
    • 无锁/重量级锁 ( Mutex Lock )/轻量级锁/偏向锁:指锁的状态,并且是针对Synchronized,是通过对象监视器在对象头中的字段来表明的

      • 重要概念:

        1. Java对象头:Hotspot的对象头主要包括两部分数据:Mark Word(标记字段)Class Pointer(类型指针)。

          • Mark Word:默认存储对象的HashCode,分代年龄和锁标志位信息。

          • Class Point:对象指向它的类元数据的指针,虚拟机通过这个指针来确定这个对象是哪个类的实例。

        2. Monitor:理解为一个同步工具或一种同步机制,通常被描述为一个对象。每一个Java对象就有一把看不见的锁,称为内部锁或者Monitor锁。

          • 两个属性:owner(记录当前拥有锁的进程)、recursion(记录锁被获取的次数)
          • Mark Word中会存储Monitor对象的指针
      • 重量级锁:依赖于操作系统 Mutex Lock 所实现的锁,重量级锁是指当锁为轻量级锁的时候,另一个线程虽然是自旋,但自旋不会一直持续下去,当自旋一定次数的时候,还没有获取到锁,就会进入阻塞,该锁膨胀为重量级锁。重量级锁会让其他申请的线程进入阻塞,性能降低。

      • 偏向锁指一段同步代码一直被一个线程所访问,那么该线程会自动获取锁。降低获取锁的代价。

      • 轻量级锁:当锁是偏向锁的时候,被另一个线程访问,偏向锁就会升级为轻量级锁,其他线程会通过自旋的形式尝试获取锁,不会阻塞,提高性能。

      • 无锁没有对资源进行锁定,所有的线程都能访问并修改同一个资源,但同时只有一个线程能修改成功。

    • CAS原理:CAS操作依赖3个值:内存中的值V,旧的估计值X,要修改的新值B,如果旧的预估值X等于内存中的值V,就将新的值B保存到内存中。

    • 参考链接:https://blog.csdn.net/IT_10/article/details/105237532

  2. ReentrantLock(可重入锁、独享锁、默认是非公平锁)

    ReentrantLock结构

    ReentrantLock方法

    • 概念:继承接口 Lock 并实现了接口中定义的方法,可重入锁;非公平锁:JVM 按随机、就近原则分配锁的机制则称为不公平锁;公平锁:锁的分配机制是公平的,通常先对锁提出获取请求的线程会先被分配到锁

    • 源码分析:

      1. 属性: Sync是ReentrantLock的一个内部抽象类继承了AQS,而ReentrantLock另外两个内部类:FairSync和NonFairSync则同时继承了Sync,并实现了Sync的lock方法

        private final Sync sync;
        
      2. 构造方法:构造公平锁或者非公平锁

        public ReentrantLock() {
            sync = new NonfairSync();
        }
        
        public ReentrantLock(boolean fair) {
            sync = fair ? new FairSync() : new NonfairSync();
        }
        
      3. Lock接口方法的实现:公平锁和非公平锁的实现,非公平锁tryAcquire方法是调用的Sync方法的nonfairTryAcquire,而nonfairTryAcquire与公平锁的tryAcquire唯一的不同就在于如果此时锁空闲,就不会再去管同步队列中是否还有等待线程,而是直接抢锁

        //公平锁lock方法
        final void lock() {
            acquire(1);
        }
        
        //非公平锁lock方法
        final void lock() {
            // 如果cas操作能够将state置为1,则说明此时锁空闲,如果抢锁成功,则将独占线程设置为自己;如果锁此时不空闲,则调用acquire方法
            if (compareAndSetState(0, 1))
                setExclusiveOwnerThread(Thread.currentThread());
            else
                acquire(1);
        }
        
        //FairSync实现的tryAcquire
        protected final boolean tryAcquire(int acquires) {
            final Thread current = Thread.currentThread();
            // 获取锁的状态
            int c = getState();
            // 为0说明锁空闲
            if (c == 0) {
                // 通过hasQueuePredecessors方法判断同步队列是否为空,如果为false则说明为空,则我们再通过cas去获取锁,如果也成功,则将当前线程设置为独占线程,并返回true
                if (!hasQueuedPredecessors() &&
                    compareAndSetState(0, acquires)) {
                    setExclusiveOwnerThread(current);
                    return true;
                }
            }
            // 如果c>0则锁已经被使用
            // 如果此时占用锁的线程就是本线程,说明本线程已经拿到锁(可重入锁)
            // 最后更新state返回true
            else if (current == getExclusiveOwnerThread()) {
                int nextc = c + acquires;
                if (nextc < 0)
                    throw new Error("Maximum lock count exceeded");
                setState(nextc);
                return true;
            }
            // 拿锁不成功返回false
            return false;
        }
                    
        //NonfairSync实现的tryAcquire
        

      protected final boolean tryAcquire(int acquires) {
      return nonfairTryAcquire(acquires);
      }
      final boolean nonfairTryAcquire(int acquires) {
      final Thread current = Thread.currentThread();
      int c = getState();
      if (c == 0) {
      // 与公平锁tryAcquire唯一的不同就是在这没有判断同步队列是否还有等待线程
      if (compareAndSetState(0, acquires)) {
      setExclusiveOwnerThread(current);
      return true;
      }
      }
      else if (current == getExclusiveOwnerThread()) {
      int nextc = c + acquires;
      if (nextc < 0) // overflow
      throw new Error("Maximum lock count exceeded");
      setState(nextc);
      return true;
      }
      return false;
      }

      
      4. LockInterruptibly:和lock方法一样,也是阻塞获取锁,但不同于lock方法,它会响应中断
      
      ```java
      public void lockInterruptibly() throws InterruptedException {
            sync.acquireInterruptibly(1);
      }
      
      
      public final void acquireInterruptibly(int arg)
              throws InterruptedException {
          // 查看标志位判断是否被中断
          if (Thread.interrupted())
              throw new InterruptedException();
          if (!tryAcquire(arg))
              doAcquireInterruptibly(arg);
      }
      
      private void doAcquireInterruptibly(int arg)
          throws InterruptedException {
          final Node node = addWaiter(Node.EXCLUSIVE);
          boolean failed = true;
          try {
              for (;;) {
                  final Node p = node.predecessor();
                  if (p == head && tryAcquire(arg)) {
                      setHead(node);
                      p.next = null; // help GC
                      failed = false;
                      return;
                  }
                  if (shouldParkAfterFailedAcquire(p, node) &&
                      parkAndCheckInterrupt())
                      throw new InterruptedException();
              }
          } finally {
              if (failed)
                  cancelAcquire(node);
          }
      }
      
      1. tryLock方法:非阻塞的获取锁,不管成功与失败,都会立即返回结果

        public boolean tryLock() {
            return sync.nonfairTryAcquire(1);
        }
        
        //带有超时时间的,则可能会阻塞timeout才返回结果
        public boolean tryLock(long timeout, TimeUnit unit)
                throws InterruptedException {
            return sync.tryAcquireNanos(1, unit.toNanos(timeout));
        }
        
        public final boolean tryAcquireNanos(int arg, long nanosTimeout)
                throws InterruptedException {
            if (Thread.interrupted())
                throw new InterruptedException();
            return tryAcquire(arg) ||
                doAcquireNanos(arg, nanosTimeout);
        }
        
      2. unlock方法:

        public void unlock() {
            sync.release(1);
        }
        
      3. newCondition:

        final ConditionObject newCondition() {
            return new ConditionObject();
        }
        
      4. 参考:https://zhuanlan.zhihu.com/p/412119094

  3. Lock接口:与synchronized关键字类似的同步功能,只是在使用时需要显式地获取和释放锁

  4. 锁优化:减少锁持有时间、减小锁粒度、锁分离、锁粗化、锁消除

  5. synchronized与ReentrantLock的区别

    • 底层实现:synchronized 是JVM层面的锁,是Java关键字;ReentrantLock 是从jdk1.5以来(java.util.concurrent.locks.Lock)提供的API层面的锁。
    • 手动释放:synchronized 不需要用户去手动释放锁,synchronized 代码执行完后系统会自动让线程释放对锁的占用; ReentrantLock则需要用户去手动释放锁,如果没有手动释放锁,就可能导致死锁现象。
    • 可中断:synchronized是不可中断类型的锁,除非加锁的代码中出现异常或正常执行完成; ReentrantLock则可以中断
    • 公平锁:synchronized为非公平锁 ReentrantLock则即可以选公平锁也可以选非公平锁
    • 绑定条件Condition:synchronized不能绑定; ReentrantLock通过绑定Condition结合await()/singal()方法实现线程的精确唤醒
    • 锁的对象:synchronzied锁的是对象,锁是保存在对象头里面的,根据对象头数据来标识是否有线程获得锁/争抢锁;ReentrantLock锁的是线程,根据进入的线程和int类型的state标识锁的获得/争抢。
    • 参考链接:https://zhuanlan.zhihu.com/p/126085068

3.AQS框架

  1. 概念:AQS 是⼀个⽤来构建锁和同步器的框架,使⽤ AQS 能简单且⾼效地构造出应⽤⼴泛的⼤量的同步器,⽐如我们提到的 ReentrantLock

  2. 实现原理:state变量+加锁线程+等待队列(FIFO的双向链表)

    • 当线程获取锁失败后,会封装成一个Node节点加入到等待队列中,当获取锁的线程释放锁以 后,会从队列中唤醒一个阻塞的节点
    • 获取锁:通过CAS操作访问并修改state变量的值,并将加锁线程变为当前线程
    • 可重入场景:判断加锁线程是否是自己,并将state+1
  3. 使用方式:继承同步器,将其子类定义为自定义同步组件的静态内部类

  4. 重要方法:

    1、protected boolean isHeldExclusively()
       // 需要被子类实现的方法,调用该方法的线程是否持有独占锁,一般用到了condition的时候才需要实现此方法
    2、protected boolean tryAcquire(int arg)
       // 需要被子类实现的方法,独占方式尝试获取锁,获取锁成功后返回true,获取锁失败后返回false
    3、protected boolean tryRelease(int arg)  
       // 需要被子类实现的方法,独占方式尝试释放锁,释放锁成功后返回true,释放锁失败后返回false
    4、protected int tryAcquireShared(int arg)  
       // 需要被子类实现的方法,共享方式尝试获取锁,获取锁成功后返回正数1,获取锁失败后返回负数-1
    5、protected boolean tryReleaseShared(int arg)   
       // 需要被子类实现的方法,共享方式尝试释放锁,释放锁成功后返回正数1,释放锁失败后返回负数-1
    6、final boolean acquireQueued(final Node node, int arg)
       // 对于进入队尾的结点,检测自己可以休息了,如果可以修改则进入SIGNAL状态且进入park()阻塞状态
    7、private Node addWaiter(Node mode)
       // 添加结点到链表队尾
    8、private Node enq(final Node node)
       // 如果addWaiter尝试添加队尾失败,则再次调用enq此方法自旋将结点加入队尾
    9、private static boolean shouldParkAfterFailedAcquire(Node pred, Node node)
       // 检测结点状态,如果可以休息的话则设置waitStatus=SIGNAL并调用LockSupport.park休息;
    10、private void unparkSuccessor(Node node)   
       // 释放锁时,该方法需要负责唤醒后继节点
    
  5. 实现公平锁和非公平锁:

    • 公平锁:多个线程按照申请锁的顺序去获得锁,线程会直接进入队列去排队,永远都是队列的第一位才能得到锁。
    • 非公平锁:多个线程去获取锁的时候,会直接去尝试获取,获取不到,再去进入等待队列,如果能获取到,就直接获取到锁。在这个过程中,会和队列中的线程竞争,得到锁的顺序并不一定是先到先得。
    • 公平锁和非公平锁,两者区别主要体现在lock方法:
      1. 区别一:非公平锁竞争锁资源会先去竞争锁,而公平锁只会在锁状态为0时才会竞争锁;
      2. 公平锁查看队列里面是否有节点,有的话,加入队列,没有的话直接区抢锁;而非公平锁则是直接抢锁。
    • 参考链接:https://blog.csdn.net/weixin_43823391/article/details/114259418

六、线程

一、JAVA 线程实现/ 创建 方式

  1. 继承 Thread 类

    • 启动线程的唯一方法就是通过 Thread 类的 start()实例方法。
    • start()方法是一个 native 方法,它将启动一个新线程,并执行 run()方法。
    • 优点:编写相对简单,访问当前线程直接使用this就可以获得当前线程。
    • 缺点:由于线程类已经继承了Thread类,所以不能再继承其他的父类
  2. 实现 Runnable 接口

    • 自己的类已经 extends 另一个类,就无法直接 extends Thread
    • 优点:没有继承Thread类,所以可以继承其他的类,适合多线程访问同一资源的情况,将cpu和数据分开,形成清晰的模型,较好体现了面向对象的思想
    • 缺点:编程相对复杂,要想获得当前线程对象,需要使用Thread.currentThread()方法。
  3. Callable +Future 有返回值线程

    (1)创建Callable接口的实现类,并实现call()方法,该call()方法将作为线程执行体,并且有返回值。
    (2)创建Callable实现类的实例,使用FutureTask类来包装Callable对象,该FutureTask对象封装了该Callable对象的call()方法的返回值。
    (3)使用FutureTask对象作为Thread对象的target创建并启动新线程。
    (4)调用FutureTask对象的get()方法来获得子线程执行结束后的返回值
    
    • 使用Future和Callable来使线程具有返回值的功能
    • 执行完Callable接口中的任务后,返回值是通过Future接口进行获得的。
    • 有返回值的任务必须实现 Callable 接口
    • 无返回值的任务必须 Runnable 接口
    • Callable接口的call()方法可以声明抛出异常,而Runnable接口的run()方法不可以声明抛出异常
    • 优点:也可以继承其他的类,多线程可以共享同一个target对象,适合多线程访问同一资源的情况,将cpu和数据分开,形成清晰的模型,较好的体现了面向对象的思想,还有返回值
    • 缺点:编程稍显复杂,如果要访问当前线程,则必须使用Thread.currentThread()方法
  4. 基于线程池的方式

    ExecutorService threadPool = Executors.newFixedThreadPool(10);
    

二、线程 生命周期:

  1. 初始(NEW):新创建了一个线程对象,但还没有调用start()方法。
  2. 运行(RUNNABLE):Java线程中将就绪(ready)和运行中(running)两种状态笼统的称为“运行”。线程对象创建后,其他线程(比如main线程)调用了该对象的start()方法。该状态的线程位于可运行线程池中,等待被线程调度选中,获取CPU的使用权,此时处于就绪状态(ready)。就绪状态的线程在获得CPU时间片后变为运行中状态(running)。
  3. 阻塞(BLOCKED):表示线程阻塞于锁。
  4. 等待(WAITING):进入该状态的线程需要等待其他线程做出一些特定动作(通知或中断)。
  5. 超时等待(TIMED_WAITING):该状态不同于WAITING,它可以在指定的时间后自行返回。该状态不会释放锁
  6. 终止(TERMINATED):表示该线程已经执行完毕。
线程状态转换
  1. 细节说明:
    • LockSupport和CAS都是Java并发包中并发工具控制机制基础,底层都依赖Unsafe实现。LockSupport提供的park()阻塞线程和unpark()解除线程阻塞。
  2. 参考链接:https://blog.csdn.net/mingyuli/article/details/110748806

三、终止 线程 4 种方式

  1. 正常运行结束
  2. 使用退出标志退出线程:设一个boolean 类型的标志,并通过设置这个标志为 true或 false 来控制 while循环是否退出;使用了一个 Java 关键字 volatile修饰退出标志
  3. Interrupt 方法结束线程
    • 线程处于阻塞状态:当调用线程的 interrupt()方法时,会抛出InterruptException 异常。阻塞中的那个方法抛出这个异常,通过代码捕获该异常,然后 break 跳出循环状态
    • 线程未处于阻塞状态:中断标志就会置 true
  4. stop 方法终止线程 (线程不安全)
    • thread.stop()调用之后,创建子线程的线程就会抛出 ThreadDeatherror 的错误,并且会释放子线程所持有的所有锁。

四、JAVA 后台线程

  1. 设置:通过 setDaemon(true)来设置线程为“守护线程”;
  2. 为用户线程 提供 公共服务,在没有用户线程可服务时会自动离开。
  3. 垃圾回收线程就是一个经典的守护线程

五、ThreadLocal

  1. 作用:实现每一个线程都有自己的专属本地变量
  2. 理解:ThreadLocal类主要解决的就是让每个线程绑定自己的值,可以将ThreadLocal类形象的比喻成存放数据的盒子,盒子中可以存储每个线程的私有数据。可以使用 get() 和 set() 方法来获取默认值或将其值更改为当前线程所存的副本的值,从而避免了线程安全问题。
  3. 原理:
    • Thread 类中有一个 threadLocals 和 一个 inheritableThreadLocals 变量,它们都是 ThreadLocalMap 类型的变量(一种HashMap)
    • 最终的变量是放在了当前线程的 ThreadLocalMap 中,并不是在 ThreadLocal 上,ThreadLocal 可以理解为只是ThreadLocalMap的封装,传递了变量值
    • ThrealLocal 类中可以通过Thread.currentThread()获取到当前线程对象后,直接通过getMap(Thread t)可以访问到该线程的ThreadLocalMap对象
    • ThreadLocalMap是ThreadLocal的静态内部类。
  4. 内存泄露问题:
    • 描述:ThreadLocalMap 中使用的 key 为 ThreadLocal 的弱引用,而 value 是强引用。所以,如果 ThreadLocal 没有被外部强引用的情况下,在垃圾回收的时候,key 会被清理掉,而 value 不会被清理掉。
    • 解决:在调用 set()get()remove() 方法的时候,会清理掉 key 为 null 的记录。使用完 ThreadLocal方法后 最好手动调用remove()方法
  5. 参考链接:https://www.cnblogs.com/fsmly/p/11020641.html

六、通信方式

  1. 进程通信方式:

    • 管道(pipe):管道可用于具有亲缘关系的父子进程间的通信。
    1.管道是一种两个进程间进行单向通信的机制。 管道是一种最基本的IPC(进程通信)机制,作用于有血缘关系的进程之间,完成数据传递。调用pipe系统函数即可创建一个管道。管道又分为匿名管道和命名管道。管道有如下特质:
        -其本质是一个伪文件(实为内核缓冲区)
        -由两个文件描述符引用,一个表示读端,一个表示写端。
        -规定数据从管道的写端流入管道,从读端流出。
    2.命名管道(FIFO):有名管道除了具有管道所具有的功能外,它还允许无亲缘关系进程间的通信。FIFO不同于管道之处在于它提供一个路径名与之关联,以FIFO的文件形式存储于文件系统中。命名管道是一个设备文件,因此,即使进程与创建FIFO的进程不存在亲缘关系,只要可以访问该路径,就能够通过FIFO相互通信。值得注意的是,FIFO(first input first output)总是按照先进先出的原则工作,第一个被写入的数据将首先从管道中读出。
    3.匿名管道:(1)只能进行单向通信;(2)只适用于有血缘关系之间的进程;(3)自带同步基质;(4)在进行通信时面向字节流服务;(5)生命进程随周期。
    
    • 信号(signal):信号是在软件层次上对中断机制的一种模拟,它是比较复杂的通信方式,用于通知进程有某事件发生,一个进程收到一个信号与处理器收到一个中断请求效果上可以说是一致的。
    • 消息队列(message queue):消息队列是消息的链接表,它克服了上两种通信方式中信号量有限的缺点,具有写权限得进程可以按照一定得规则向消息队列中添加新信息;对消息队列有读权限得进程则可以从消息队列中读取信息。
    • 共享内存(shared memory):可以说这是最有用的进程间通信方式。它使得多个进程可以访问同一块内存空间,不同进程可以及时看到对方进程中对共享内存中数据得更新。这种方式需要依靠某种同步操作,如互斥锁和信号量等。
    • 信号量(semaphore):主要作为进程之间及同一种进程的不同线程之间得同步和互斥手段。
    • 套接字(socket);这是一种更为一般得进程间通信机制,它可用于网络中不同机器之间的进程间通信,应用非常广泛。
  2. 线程通信方式:

    1.volatile和synchronized关键字
    2.等待/通知机制:
    //指一个线程A调用了对象O的wait()方法进入等待状态,而另一个线程B调用了对象O的notify()或者notifyAll()方法,线程A收到通知后从对象O的wait()方法返回,进而执行后续操作
    //经典范例:生产者、消费者
    3.管道输入/输出流:
    //用于线程之间的数据传输,而传输的媒介为内存
    //种具体实现:PipedOutputStream、PipedInputStream、PipedReader和PipedWriter,前两种面向字节,而后两种面向字符
    4.Thread.join()
    //理解成是线程合并,当在一个线程调用另一个线程的join方法时,当前线程阻塞等待被调用join方法的线程执行完毕才能继续执行
    5.ThreadLocal:
    //ThreadLocal,即线程变量,是一个以ThreadLocal对象为键、任意对象为值的存储结构
    //将当前线程和一个map绑定,在当前线程内可以任意存取数据,减省了方法调用间参数的传递
    //更像是一个线程内部的通信
    

七、线程同步方案

  1. 概念:同步是指在多个线程并发访问共享数据时保证共享数据在同一时刻只被一条线程使用。
  2. 互斥同步即阻塞同步
    • 临界区、互斥量、信号量都是常见的互斥实现方式。在 Java 中,最基本的同步方式就是 synchronized 关键字
    • 缺点:在主流的 Java 虚拟机实现中,Java 的线程是映射到操作系统的原生内核线程之上的(内核线程实现),如果需要阻塞或唤醒一条线程,则需要操作系统来帮忙完成,这将会导致用户态到核心态的转换、维护锁计数器和检查是否有被阻塞的线程需要被唤醒等开销
  3. 非阻塞同步:基于冲突检查的乐观并发策略。不管风险先进行操作,如果没有其他线程争用共享数据,那操作就直接成功了;如果共享的数据的确被争用,产生了冲突,再进行其他的补偿措施,最常用的措施就是不断的重试,直到出现没有竞争的共享数据为止。
  4. 无同步方案:
    • 可重入代码:指可以在代码执行的任何时刻中断它,转而去执行另外一段代码(包括递归调用他本身),而在控制权返回后,原来的程序不会出现任何错误,也不会对结果有影响。如果一个方法的返回结果是可以预测的,只要输入了相同的数据,就都能返回相同的结果,那它就满足可重入性的要求,当然也就是线程安全的
    • 线程本地存储:即将需要共享数据的代码保证在同一条线程中执行

八、守护线程与非守护线程

  1. 概念:在 Java 中有两类线程:User Thread(用户线程)、Daemon Thread(守护线程) ,任何一个守护线程都是整个JVM中所有非守护线程的保姆。
  2. 只要当前JVM实例中尚存在任何一个非守护线程没有结束,守护线程就全部工作;只有当最后一个非守护线程结束时,守护线程随着JVM一同结束工作。Daemon的作用是为其他线程的运行提供便利服务,守护线程最典型的应用就是 GC (垃圾回收器),它就是一个很称职的守护者。
  3. User和Daemon两者几乎没有区别,唯一的不同之处就在于虚拟机的离开:如果 User Thread已经全部退出运行了,只剩下Daemon Thread存在了,虚拟机也就退出了。 因为没有了被守护者,Daemon也就没有工作可做了,也就没有继续运行程序的必要了。
  4. 参考链接:https://blog.csdn.net/chejinqiang/article/details/80199312

第四部分 JVM虚拟机

一、JVM内存区域

JVM内存

  • 线程私有数据区域生命周期与线程相同, 依赖用户线程的启动/结束 而 创建/销毁在 HotspotVM 内
  • 线程共享区域随虚拟机的启动/关闭而创建/销毁

一、内存区域

  1. 程序计数器( 线程私有):是当前线程所执行的字节码的行号指示器,没有规定任何 OutOfMemoryError 情况
  2. 虚拟机栈( 线程私有):是描述java方法执行的内存模型,每个方法在执行的同时都会创建一个栈帧(Stack Frame),用于存储局部变量表、操作数栈、动态链接、方法出口等信息
    • 每一个方法从调用直至执行完成的过程,就对应着一个栈帧在虚拟机栈中入栈到出栈的过程。
  3. 本地方法区( 线程私有):本地方法区和 Java Stack 作用类似, 区别是虚拟机栈为执行 Java 方法服务, 而本地方法栈则为Native 方法服务
  4. 堆(Heap- 线程共享):创建的对象和数组都保存在 Java 堆内存中,也是垃圾收集器进行
    垃圾收集的最重要的内存区域
    • VM 采用分代收集算法: 新生代( Eden 区 、 From Survivor 区 和 To Survivor 区 )和老年代
  5. 方法区/ 永久代:用于存储被 JVM 加载的类信息、常量、静态变量、即时编译器编译后的代码等数据
    • 运行时常量池(Runtime Constant Pool)是方法区的一部分。Class 文件中除了有类的版
      本、字段、方法、接口等描述等信息外,还有一项信息是常量池(Constant Pool Table),用于存放编译期生成的各种字面量和符号引用,这部分内容将在类加载后存放到方法区的运行时常量池中。

二、JVM 运行时内存

  1. 新生代:存放新生的对象。一般占据堆的1/3空间

    • Eden 区:Java新对象的出生地

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

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

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

      eden 、 servicorFrom 复制到 ServicorTo,年龄+1

      清空 eden 、 servicorFrom

      ServicorTo 和 ServicorFrom 互换

  2. 老年代:存放应用程序中生命周期长的内存对象

    • 空间不够用或者无法找到足够大的连续空间分配给新创建的较大对象,触发一次 MajorGC
    • 标记清除算法:首先扫描一次所有老年代,标记出存活的对象,然后回收没有标记的对象
  3. 永久代

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

  4. JAVA8 与元数据:在Java8中,永久代已经被移除,被一个称为“元数据区”(元空间)的区域所取代,元空间并不在虚拟机中,而是使用本地内存

二、垃圾回收算法

  1. 确定垃圾

    • 引用计数法:引用和对象是有关联的。如果要操作对象则必须用引用进行,对象如果没有任何与之关联的引用则可以回收

    • 可达性分析:解决引用计数法的循环引用问题;如果在“GC roots”和一个对象之间没有可达路径,则称该对象是不可达的;不可达对象变为可回收对象至少要经过两次标记过程

      可以作为GC roots的对象:

      1. 虚拟机栈(栈桢中的本地变量表)中的引用的对象
      2. 方法区中的类静态属性引用的对象
      3. 方法区中的常量引用的对象
      4. 本地方法栈中JNI(Native方法)的引用的对象
  2. 标记清除算法( Mark-Sweep ):分为两个阶段,标注和清除。

    • 标记阶段标记出所有需要回收的对象,清除阶段回收被标记的对象所占用的空间
    • 快速回收对象,不用移动对象成为了速度上的优势,但又因为优势的原因产生了过多的内存碎片,内存碎片化严重
  3. 复制算法(copying )

    • 解决 Mark-Sweep 算法内存碎片化的缺陷
    • 按内存容量将内存划分为等大小的两块。每次只使用其中一块,当这一块内存满后将尚存活的对象复制到另一块上去,把已使用的内存清掉
    • 可用内存被压缩到了原本的一半。且存活对象增多的话,Copying 算法的效率会大大降低
  4. 标记整理算法(Mark-Compact)

    • 先标记,与Mark-Sweep 类似
    • 然后将存活对象移向内存的一端。然后清除端边界外的对象
    • 移动对象的工作并不是那么简单,首先修改内存地址,修改内存地址后引用该对象的对象也要修改,所以整体上这也是一个非常比较庞大的工程。
  5. 分代收集算法

    • 大部分 JVM 所采用的方法

    • 据对象存活的不同生命周期将内存划分为不同的域,一般情况下将 GC 堆划分为老生代和新生代

    • 老生代的特只有少量对象需要被回收;新生代有大量垃圾需要被回收,可以根据不同区域选择不同的算法

      • 新生代与复制算法:因为新生代中每次垃圾回收都要回收大部分对象,即要复制的操作比较少

      • 老年代与标记复制算法:

        1. 永生代的回收主要包括废弃常量和无用的类
        2. 当新生代的 Eden Space 和 From Space 空间不足时就会发生一次 GC
        3. 如果 To Space 无法足够存储某个对象,则将这个对象存储到老生代
        4. 默认情况下年龄到达 15 的对象会被移到老生代中

三、JAVA四种引用类型

  1. 强引用

    • 把一个对象赋给一个引用变量,这个引用变量就是一个强引用。
    • 当一个对象被强引用变量引用时,它处于可达状态,它是不可能被垃圾回收机制回收(不用也不能被回收)
    • 造成 Java 内存泄漏的主要原因之一
  2. 软引用:用 SoftReference 类来实现,当系统内存足够时它不会被回收

  3. 弱引用:用 WeakReference 类来实现,只要垃圾回收机制一运行,不管 JVM 的内存空间是否足够,总会回收该对象占用的内存

  4. 虚引用:PhantomReference 类来实现,不能单独使用,必须和引用队列联合使用,主要用于跟踪对象被垃圾回收的状态

四、GC 分代收集算法 VS 分区收集算法

  1. 分代收集算法:根据对象存活周期的不同将内存划分为几块, 如 JVM 中的 新生代、老年代、永久代,这样就可以根据各年代特点分别采用最适当的 GC 算法

  2. 分区收集算法

    • 分区算法则将整个堆空间划分为连续的不同小区间, 每个小区间独立使用, 独立回收
    • 可以控制一次回收多少个小区间 , 根据目标停顿时间, 每次合理地回收若干个小区间
    • 可以减少一次 GC 所产生的停顿

五、GC 垃圾收集器

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

    • 复制算法
    • 使用一个 CPU 或一条线程去完成垃圾收集工作,必须暂停其他所有的工作线程。
    • 简单高效,对于限定单个 CPU 环境来说,没有线程交互的开销,可以获得最高的单线程垃圾收集效率
    • java 虚拟机运行在 Client 模式下默认的新生代垃圾收集器
  2. ParNew 垃圾收集器 (Serial+ 多线程)

    • Serial 收集器的多线程版本;复制算法
    • 暂停所有其他的工作线程;默认开启和 CPU 数目相同的线程数;java虚拟机运行在 Server 模式下新生代的默认垃圾收集器
  3. Parallel Scavenge 收集器 (多线程复制算法、高效):

    • 复制算法;多线程的垃圾收集器
    • 关注Thoughput,CPU 用于运行用户代码的时间/CPU 总消耗时间,即吞吐量=运行用户代码时间/(运行用户代码时间+垃圾收集时间)
  4. Serial Old 收集器 (单线程标记整理算法 ):使用标记-整理算法;运行在 Client 默认的 java 虚拟机默认的年老代垃圾收集器

  5. Parallel Old 收集器 (多线程标记整理算法):使用多线程的标记-整理算法

  6. CMS 收集器

    • 概念:年老代垃圾收集器;主要目标:获取最短垃圾回收停顿时间;使用多线程的标记-清除算法
    • 工作机制分为四个阶段:耗时最长的并发标记和并发清除过程中,垃圾收集线程可以和用户现在一起并发工作,所以总体上来看CMS 收集器的内存回收和用户线程是一起并发地执行。
      1. 初始标记:标记一下 GC Roots 能直接关联的对象,暂停所有的工作线程
      2. 并发标记:进行 GC Roots 跟踪的过程,,不需要暂停工作线程
      3. 重新标记:修正在并发标记期间,因用户程序继续运行而导致标记产生变动的对象的标记记录,暂停所有的工作线程
      4. 并发清除:清除 GC Roots 不可达对象,不需要暂停工作线程
  7. G1 收集器(Garbage first)

    • 基于标记-整理算法,不产生内存碎片。
    • 可以非常精确控制停顿时间,在不牺牲吞吐量前提下,实现低停顿垃圾回收
    • 原理:避免全区域垃圾收集,它把堆内存划分为大小固定的几个独立区域,并且跟踪这些区域的垃圾收集进度,同时在后台维护一个优先级列表,每次根据所允许的收集时间,优先回收垃圾最多的区域

六、JVM 类加载机制

一、类加载步骤:加载,验证,准备,解析,初始化

  1. 加载:在内存中生成一个代表这个类的 java.lang.Class 对象,作为方法区这个类的各种数据的入口
  2. 验证:确保 Class 文件的字节流中包含的信息是否符合当前虚拟机的要求
  3. 准备:正式为类变量(静态变量)分配内存并设置类变量的初始值阶段,即在方法区中分配这些变量所使用的内存空间
  4. 解析:虚拟机将常量池中的符号引用替换为直接引用的过程
    • 符号引用:引用的目标并不一定要已经加载到内存中
    • 直接引用:引用的目标必定已经在内存中存在
  5. 初始化:执行类中定义的 Java 程序代码
    • 执行类构造器方法
    • 方法是由编译器自动收集类中的类变量的赋值操作和静态语句块中的语句合并而成的
  6. 参考链接;https://blog.csdn.net/hc1428090104/article/details/117333938?spm=1001.2101.3001.6661.1&utm_medium=distribute.pc_relevant_t0.none-task-blog-2~default~CTRLIST~default-1-117333938-blog-124719590.pc_relevant_multi_platform_whitelistv1&depth_1-utm_source=distribute.pc_relevant_t0.none-task-blog-2~default~CTRLIST~default-1-117333938-blog-124719590.pc_relevant_multi_platform_whitelistv1&utm_relevant_index=1

二、类加载器:把加载动作放到 JVM 外部实现,以便让应用程序决定如何获取所需的类

  1. 启动类加载器(Bootstrap ClassLoader)
  2. 扩展类加载器(Extension ClassLoader)
  3. 应用程序类加载器(Application ClassLoader)
  4. 也可以通过继承 java.lang.ClassLoader实现自定义的类加载器。

三、双亲委派

  1. 当一个类收到了类加载请求,他首先不会尝试自己去加载这个类,而是把这个请求委派给父类去完成,只有当父类加载器反馈自己无法完成这个请求的时候,子类加载器才会尝试自己去加载
  2. 因此所有的加载请求都应该传送到启动类加载其中
  3. 优点:使用不同的类加载器最终得到的都是同样一个 Object 对象。
  4. 打破:JDBC、JNDI、Tomcat等
    • 重写loadClass():双亲委派机制是通过这个方法实现的,改写加载规则相当于打破机制,可以实现自定义加载委派机制
    • 使用线程上下文类加载器:基础类又要调用回用户代码
    • 代码热替换(追求动态性)

类加载器

四、OSGI ( 动态模型系统 )

七、JVM常见参数

  1. jdk1.8之前:

    • -Xms:初始堆大小。只要启动,就占用的堆大小。
    • -Xmx:最大堆大小。java.lang.OutOfMemoryError:Java heap这个错误可以通过配置-Xms和-Xmx参数来设置。
    • -Xss:栈大小分配。栈是每个线程私有的区域,通常只有几百K大小,决定了函数调用的深度,而局部变量、参数都分配到栈上。当出现大量局部变量,递归时,会发生栈空间OOM(java.lang.StackOverflowError)之类的错误。
    • XX:NewSize:设置新生代大小的绝对值。
    • -XX:NewRatio:设置年轻代和年老代的比值。比如设置为3,则新生代:老年代=1:3,新生代占总heap的1/4。
    • -XX:MaxPermSize:设置持久代大小。java.lang.OutOfMemoryError:PermGenspace这个OOM错误需要合理调大PermSize和MaxPermSize大小。
    • -XX:SurvivorRatio:年轻代中Eden区与两个Survivor区的比值。注意,Survivor区有form和to两个。比如设置为8时,那么eden:form:to=8:1:1。
    • -XX:HeapDumpOnOutOfMemoryError:发生OOM时转储堆到文件,这是一个非常好的诊断方法。
    • -XX:HeapDumpPath:导出堆的转储文件路径。
    • -XX:OnOutOfMemoryError:OOM时,执行一个脚本,比如发送邮件报警,重启程序。后面跟着一个脚本的路径。
  2. jdk1.8之后:在移除了Perm区域之后,JDK 8中使用MetaSpace来替代,这些空间都直接在堆上来进行分配。 在JDK8中,类的元数据存放在native堆中,这个空间被叫做:元数据区。JDK8中给元数据区添加了一些新的参数。

    • -XX:MetaspaceSize= 是分配给类元数据区(以字节计)的初始大小(初始高水位),超过会导致垃圾收集器卸载类。这个数量是一个估计值。当第一次到达高水位的时候,下一个高水位是由垃圾收集器来管理的。
    • -XX:MaxMetaspaceSize= 是分配给类元数据区的最大值(以字节计)。这个参数可以用来限制分配给类元数据区的大小。这个值也是个估计值。默认无上限。
    • -XX:MinMetaspaceFreeRatio=,是一次GC以后,为了避免增加元数据区(高水位)的大小,空闲的类元数据区的容量的最小比例,不够就会导致垃圾回收。
    • -XX:MaxMetaspaceFreeRatio=,是一次GC以后,为了避免减少元数据区(高水位)的大小,空闲的类元数据区的容量的最大比例,超过就会导致垃圾回收。

八、Stop the world

  1. 概念:指的是Gc事件发生过程中,会产生应用程序的停顿。停顿产生时整个应用程序线程都会被暂停,没有任何响应,有点像卡死的感觉,这个停顿称为STW。
  2. 安全点:
  3. 发生时机:https://www.jianshu.com/p/2938053d619b

第五部分 面试题

一、JVM

  1. 讲一下JVM的1.8之前和1.8之后内存模型?

    • JVM内存模型是指JVM的内存区域划分
    • JVM1.8的修改:
      1. 修改内容:将方法区移除,添加了元数据区,而且元数据区是在本地内存中,不再受限制于JVM内存的大小,而是和机器内存有关。
      2. 修改原因:随着现今框架和程序都包含很多依赖,而这些依赖有很多类对象,都存于永久代中,因此这部分内容往往会内存溢出,于是干脆把这部分内容放到堆内存或本地内存中存储。
      3. 元空间有以下特点:a.每个加载器有专门的存储空间。b.不会单独回收某个类。c.元空间里的对象的位置是固定的。d.如果发现某个加载器不再存活了,会把相关的空间整个回收。
  2. 为什么需要程序计数器?

    • 程序计数器记录当前线程正在执行的字节码的地址或行号。JVM中存在线程切换,主要作用是为了确保多线程情况下JVM程序的正常执行。
  3. 说一下 Java 方法执行的过程,说详细一点。编译、加载(类加载)、执行解释

    • 加载:在内存中生成一个代表这个类的 java.lang.Class 对象,作为方法区这个类的各种数据的入口
    • 验证:确保 Class 文件的字节流中包含的信息是否符合当前虚拟机的要求
    • 准备:正式为类变量(静态变量)分配内存并设置类变量的初始值阶段,即在方法区中分配这些变量所使用的内存空间
    • 解析:虚拟机将常量池中的符号引用替换为直接引用的过程;(1)符号引用:引用的目标并不一定要已经加载到内存中;(2)直接引用:引用的目标必定已经在内存中存在
    • 初始化:执行类中定义的 Java 程序代码
      1. 执行类构造器方法
      2. 方法是由编译器自动收集类中的类变量的赋值操作和静态语句块中的语句合并而成的
  4. Java 8 新特性

    • Lambda 表达式:Lambda 允许把函数作为一个方法的参数(函数作为参数传递到方法中)
    1. 函数式接口:指的是一个有且仅有一个抽象方法,但是可以有多个非抽象方法的接口,这样的接口可以隐式转换为 Lambda 表达式
    2. 方法引用:方法引用提供了非常有用的语法,可以直接引用已有Java类或对象(实例)的方法或构造器。与lambda联合使用,方法引用可以使语言的构造更紧凑简洁,减少冗余代码
    3. 默认方法:默认方法就是一个在接口里面有了一个实现的方法
    4. Stream API:新添加的Stream API(java.util.stream) 把真正的函数式编程风格引入到Java中。
    5. Optional 类:Optional 类已经成为 Java 8 类库的一部分,用来解决空指针异常。
    6. Date Time API:加强对日期与时间的处理。
    7. Nashorn, JavaScript 引擎:Java 8提供了一个新的Nashorn javascript引擎,它允许我们在JVM上运行特定的javascript应用
  5. String s = new String("abc"); 代码运行完在内存中存什么东西?s这个变量放在哪里呢?

    • 第一种是用new()来创建对象的,它会存放在堆中,每调用一次就会创建一个新的对象;
    • 第二种是先在栈中创建一个对String类的对象引用变量str ,然后查找栈中有没有存放"abc",如果没有,则将"abc"存放进栈,并令str 指向"abc",如果已经有"abc",则直接令str 指向"abc"。
    • 比较类里面的数值是否相等时,用equals()方法;当测试两个包装类的引用是否指向同一个对象时,用 ==
    String str = new String("abc");
    String str = "abc";
    
  6. 发生内存溢出可能的原因有哪些?

    • 内存溢出 out of memory:是指程序在申请内存时,没有足够的内存空间供其使用,出现out of memory;比如申请了一个integer,但给它存了long才能存下的数,那就是内存溢出。
    • 内存泄露 memory leak:是指程序在申请内存后,无法释放已申请的内存空间,一次内存泄露危害可以忽略,但内存泄露堆积后果很严重,无论多少内存,迟早会被占光。
    • 内存泄露类型:常发性内存泄漏、偶发性内存泄漏、一次性内存泄漏、隐式内存泄漏
      1. 常发性内存泄漏。发生内存泄漏的代码会被多次执行到,每次被执行的时候都会导致一块内存泄漏。
      2. 偶发性内存泄漏。发生内存泄漏的代码只有在某些特定环境或操作过程下才会发生。常发性和偶发性是相对的。对于特定的环境,偶发性的也许就变成了常发性的。所以测试环境和测试方法对检测内存泄漏至关重要。
      3. 一次性内存泄漏。发生内存泄漏的代码只会被执行一次,或者由于算法上的缺陷,导致总会有一块仅且一块内存发生泄漏。比如,在类的构造函数中分配内存,在析构函数中却没有释放该内存,所以内存泄漏只会发生一次。
      4. 隐式内存泄漏。程序在运行过程中不停的分配内存,但是直到结束的时候才释放内存。严格的说这里并没有发生内存泄漏,因为最终程序释放了所有申请的内存。但是对于一个服务器程序,需要运行几天,几周甚至几个月,不及时释放内存也可能导致最终耗尽系统的所有内存。所以,我们称这类内存泄漏为隐式内存泄漏。
    • 内存溢出原因:
      1. 内存中加载的数据量过于庞大,如一次从数据库取出过多数据;
      2. 集合类中有对对象的引用,使用完后未清空,使得JVM不能回收;
      3. 代码中存在死循环或循环产生过多重复的对象实体;
      4. 使用的第三方软件中的BUG;
      5. 启动参数内存值设定的过小
    • 内存溢出解决办法
      1. 第一步,就是修改JVM启动参数,直接增加内存。
      2. 第二步,检查错误日志,查看"OutOfMemory"错误前是否有其它异常或错误。
      3. 第三步,安排有经验的编程人员对代码进行走查和分析,找出可能发生内存溢出的位置。
      4. 第四步,使用内存查看工具动态查看内存使用情况。
    • 内存泄露原因:
      1. 一种情况如在C/C++ 语言中的,在堆中的分配的内存,在没有将其释放掉的时候,就将所有能访问这块内存的方式都删掉(如指针重新赋值)
      2. 另一种情况则是在内存对象明明已经不需要的时候,还仍然保留着这块内存和它的访问方式(引用)
      3. 第一种情况,在 Java 中已经由于垃圾回收机制的引入,得到了很好的解决。所以, Java 中的内存泄漏,主要指的是第二种情况。
  7. 内存泄漏、OOM等问题怎么排查?

  8. 栈什么情况下会发生内存溢出?

  9. Java虚拟机栈在什么情况下线程请求栈的深度超过当前Java虚拟机栈的最⼤深度?有什么典型的场景会发生这种情况?

  10. CMS中那个阶段会stop the world?CSM用的哪种垃圾回收 算法 ?G1用的哪种垃圾回收 算法 ?为什么CMS要用标记-清除,不用标记-整理 算法 ?垃圾回收怎么解决跨代引用问题?

  11. 两个不同的JVM的进程如何同步?

  12. JVM对于Stop the world现象做的优化

  13. 对象进入老年代的四种情况

    • 存活对象达到年龄阈值:JVM参数"-XX:MaxTenuring Threshold" 来设置年龄,默认为15
    • 大对象直接进入老年代:超过了JVM中-XX:PretenureSizeThreshold参数的设置。
    • 动态对象年龄判定:幸存者区中如果有相同年龄的对象所占空间大于幸存者区的一半,那么年龄大于等于该年龄的对象就可以直接进入老年代。
    • 老年代空间担保机制:在一次安全Minor GC 中,仍然存活的对象不能在另一个Survivor 完全容纳,则会通过担保机制进入老年代。
    • https://www.csdn.net/tags/MtjacgzsNzY1MzgtYmxvZwO0O0OO0O0O.html
  14. g1和cms的区别

  15. cms重新标记的时候三色算法了解吗

  16. 场景:计算机性能好 但Idea(也是一个Java程序)但比较卡,原因:可能是因为频繁产生Full GC 怎么排查问题进行调整

  17. Full GC效果不好 每次只能从90%->85%之后又90%了,这种情况下应该怎么办比较好

    • 如果是一次fullgc后,剩余对象不多。那么说明你eden区设置太小,导致短生命周期的对象进入了old区。如果一次fullgc后,old区回收率不大,那么说明old区太小。
  18. 了解最新的ZGC回收器吗?

  19. JVM调试指令

    • jps:JVM Process Status Tool,显示指定系统内所有的HotSpot虚拟机进程。
    • jstat:JVM statistics Monitoring,用于监视虚拟机运行时状态信息的命令,它可以显示出虚拟机进程中的类装载、内存、垃圾收集、JIT编译等运行数据。
    • jmap:JVM Memory Map,用于生成heap dump文件。
    • jhat:JVM Heap Analysis Tool,与jmap搭配使用,用来分析jmap生成的dump,jhat内置了一个微型的HTTP/HTML服务器,生成dump的分析结果后,可以在浏览器中查看。
    • jstack:用于生成java虚拟机当前时刻的线程快照。
    • jinfo:JVM Configuration info,用于实时查看和调整虚拟机运行参数。
    • 参考链接:https://blog.csdn.net/m0_37741420/article/details/124925123

四、JAVA基础知识

  1. 说下hashCode( )?什么场景下会重写hashCode()?

    • hashCode()的作用:获取哈希码,也称为散列码;它实际上是返回一个int整数。这个哈希码的作用是确定该对象在哈希表中的索引位置。
    • hashCode()方法和equals()方法的作用其实一样,都是比较两个对象是否相等
      1. 重写的equals()一般比较全面比较复杂,这样效率就比较低,而利用HashCode()进行双向对比,则只要生成一个hash值就可以进行比较,效率较高
      2. HashCode()不完全可靠:equals()相等的对象肯定相等,HashCode相等的对象不一定相等,不相等的对象一定不相等。
    • 不重写:不重写的HashCode比较的是地址值,对于引用数据类型不能比较。所以必须重写。
    • 重写:a.HashTable、HashMap、HashSet b.重写了equals就一定要重写HashCode
    • 如果两个对象相等,则hashcode一定也是相同的;两个对象相等,对两个对象分别调用equals方法都返回true;两个对象有相同的hashcode值,它们也不一定是相等的;因此,equals 方法被覆盖过,则 hashCode 方法也必须被覆盖
    • hashCode() 的默认行为是对堆上的对象产生独特值。如果没有重写 hashCode(),则该 class 的两个对象无论如何都不会相等(即使这两个对象指向相同的数据)
  2. ==和equals的区别?

    • (==):
      1. 基本数据类型 :byte,short,char,int,long,float,double,boolean。他们之间的比较,应用双等号(==),比较的是他们的值。
      2. 引用数据类型:当他们用(==)进行比较的时候,比较的是他们在内存中的存放地址
    • equals():
      1. equals方法不能作用于基本数据类型的变量
      2. 如果没有对equals方法进行重写,则比较的是引用类型的变量所指向的对象的地址(若某个类没有覆盖equals()方法,当它的通过equals()比较两个对象时,实际上是比较两个对象是不是同一个对象。这时,等价于通过“==”去比较这两个对象。)
      3. 诸如String、Date等类对equals方法进行了重写的话,比较的是所指向的对象的内容
  3. 你对Java反射的理解?在Java命名规范中一般采用驼峰命名的方式,假设想基于反射做一件事,有一个包,包里面有很多类,找出命名不符合规范的类名、方法 名、属性名,怎么实现?

  4. 写一个Java类,属性中可能是一个类对象,怎么解决一层一层往下扫的问题?(属性是一个类,类中又有属性)

  5. 什么情况下会使用序列化?

    • 内存中的对象状态保存到一个文件中或者数据库中
    • 套接字在网络上传送对象
    • 通过RMI传输对象
  6. 多态的应用场景?

  7. 接口和抽象类有什么相同点和不同点?接口里面一定有抽象的方法吗?抽象类中一定有抽象的方法吗?

    • 不同点:
      1. 接口的方法默认是 public,所有方法在接口中不能有实现(Java 8 开始接口方法可以有默认实现),而抽象类可以有非抽象的方法。
      2. 接口中除了static、final变量,不能有其他变量,而抽象类中则不一定。
      3. 一个类可以实现多个接口,但只能实现一个抽象类。接口自己本身可以通过extends关键字扩展多个接口。
      4. 接口方法默认修饰符是public,抽象方法可以有public、protected和default这些修饰符(抽象方法就是为了被重写所以不能使用private关键字修饰!)。
      5. 从设计层面来说,抽象是对类的抽象,是一种模板设计,而接口是对行为的抽象,是一种行为的规范。
    • 抽象类中除了有抽象方法外,也可以有数据成员和非抽象方法,抽象类不一定有抽象方法;但是包含一个抽象方法的类一定是抽象类
    • 而接口中所有的方法必须都是抽象的(java 8之后可以定义default方法,包含方法体。),接口中也可以定义数据成员,但必须是常量。
  8. try catch会影响性能吗?为什么抛出异常的时候会影响性能?

    • 如果try catch没有抛出异常,那么其对性能几乎没有影响。但如果抛出异常,那对程序将造成几百倍的性能影响。
    • 如果发生异常,两者的处理逻辑不一样,已经不具有比较的意义了。
  9. for-each语法糖和for有啥区别?为啥尽量不用for-each?

    • 编译后是不一样的! 如果遍历的数据是数组,则就跟原来的for循环时一致的,如果是实现了迭代器接口比如集合库,则就用迭代器。
    • 会创建迭代器对象,占用内存
  10. static数据存在哪?生命周期什么样的

    • static 变量保存在 Class 实例的尾部。存储在堆中(1.8之前存储在方法区,之后存储在堆中)
    • 在加载class文件的时候会产生字节码对象,在加载,准备,链接的阶段会把字节码对象的静态变量直接赋值
    • 生命周期:在类加载的时候被分配空间;类被卸载时,静态变量被销毁,并释放内存空间。static变量的生命周期取决于类的生命周期
    • 参考链接:https://blog.csdn.net/x_iya/article/details/81260154/
  11. hashMap为什么大小是幂次?

  12. 输出结果:

    1. 第一种情况:
      • String s = "hello2"; //变量,没有new,栈中直接指向了常量池。常量池初始化创建一个hello2
      • final String s2 = "hello"; //被final修饰的就是是常量
      • String s3 = s2+2;//常量和常量相加以后,先判断常量池里有没有,有的话直接引用,没有的话开辟空间存
      • System.out.println(s==s3);//true 地址一致
    2. 第二种情况:
      • String s = "hello2"; //变量,没有new,栈中直接指向了常量池。常量池初始化创建一个hello2
      • String s2 = "hello"; //变量,没有new,栈中直接指向了常量池。常量池初始化创建一个hello
      • String s3 = s2+2;//变量和常量相加,先在常量池中开辟空间,再相加
      • System.out.println(s==s3);//false 地址不一致
  13. 了解集合的fail-fast机制吗,这个机制怎么形成的? 如何解决?

    • 概念:fail-fast是Java集合(Collection)中的一种错误机制。当多个线程对同一个集合的内容进行操作时,就有可能会发生fail-fast事件。抛出java.util.ConcurrentModificationException异常
    • 解决办法:fail-fast机制,是一种错误检测机制,JDK并不保证fail-fast机制一定会发生。若在多线程环境下使用fail-fast机制的集合,建议使用java.util.concurrent包下的类去取代java.util包下的类。
    • fail-fast原理:
    • 参考链接:https://blog.csdn.net/ljcaidn/article/details/85306094
  14. jdk jre的区别

    • JRE:java运行时环境,包含了java虚拟机,java基础类库。是使用java语言编写的程序运行所需要的软件环境,是提供给想运行java程序的用户使用的。
    • JDK:java开发工具包,是程序员使用java语言编写java程序所需的开发工具包,是提供给程序员使用的。
    • JDK包含了JRE,同时还包含了编译java源码的编译器javac,还包含了很多java程序调试和分析的工具:jconsole,jvisualvm等工具软件,还包含了java程序编写所需的文档和demo例子程序。
    • 如果你需要运行java程序,只需安装JRE就可以了。如果你需要编写java程序,需要安装JDK。
  15. Java项目打包成jar包和war包的区别,什么情况打成jar,什么情况打成war

  16. java数据类型

    • 基本数据类型:整数型 byte [1]、short[2] 、int[4] 、long[8];浮点型 float [4]、 double[8];字符型 char[2],存放单个字符,单个字母占1个字节,单个汉字占2个字节;布尔型 boolean
    • 引用类型:类 class、接口 interface、数组 [ ]
  17. 重写和重载的区别是什么?

    • 定义不同:重载是定义相同的方法名、参数不同,重写是子类重写父类的方法
    • 范围不同:重载是在一个类中,重写是子类与父类之间的
    • 多态不同:重载是编译时的多态性,重写是运行时的多态性
    • 参数不同:重载的参数个数、参数类型、参数的顺序可以不同,重写父类子方法参数必须相同
    • 修饰不同:重载对修饰范围没有要求,重写要求重写方法的修饰范围大于被重写方法的修饰范围
    • 补充:
      1. 重载属于编译时多态。运行时会根据参数的个数和类型调用相应的方法。
  18. hashp什么时候退回链表?为什么不是7

    • 若桶中元素小于等于6时,树结构还原成链表形式。
    • 红黑树的平均查找长度是log(n),长度为8,查找长度为log(8)=3,链表的平均查找长度为n/2,当长度为8时,平均查找长度为8/2=4,这才有转换成树的必要;链表长度如果是小于等于6,6/2=3,红黑树此时虽然速度也很快的,但是转化为树结构和生成树的时间并不会太短。
    • 中间有个差值7可以防止链表和树之间频繁的转换。假设一下,如果设计成链表个数超过8则链表转换成树结构,链表个数小于8则树结构转换成链表,如果一个HashMap不停的插入、删除元素,链表个数在8左右徘徊,就会频繁的发生树转链表、链表转树,效率会很低。
  19. hashmap容量不是2的整数次幂 还用&(length-1) 影响会怎样?

    • 容量是2的整数次幂,n -1 后,高位为1后的0都变为1,如 16:10000, 16-1=15:1111, 1111 再与 hash 做 & 运算的时候,各个位置的取值取决于 hash;如果不是2的整数次幂,必然会有的0的位,0与任何数&肯定为0,会造成更多的哈希冲突
  20. Object类有哪些方法?

    • registerNatives() //私有方法
    • getClass() //返回此 Object 的运行类。
    • hashCode() //用于获取对象的哈希值。
    • equals(Object obj) //用于确认两个对象是否“相同”。
    • clone() //创建并返回此对象的一个副本。
    • toString() //返回该对象的字符串表示。
    • notify() //唤醒在此对象监视器上等待的单个线程。
    • notifyAll() //唤醒在此对象监视器上等待的所有线程。
    • wait(long timeout) //在其他线程调用此对象的 notify() 方法或 notifyAll() 方法,或 者超过指定的时间量前,导致当前线程等待。
    • wait(long timeout, int nanos) //在其他线程调用此对象的 notify() 方法或 notifyAll() 方法,或者其他某个线程中断当前线程,或者已超过某个实际时间量前,导致当前线程等待。
    • wait() //用于让当前线程失去操作权限,当前线程进入等待序列
    • finalize() //当垃圾回收器确定不存在对该对象的更多引用时,由对象的垃圾回收器调用此方法。
  21. java8里面新出了一些时间的类,比如比如LocalDate,与Date、 Calendar等其他时间类有什么区别?

  22. 怎么实现一个栈的呢?

  23. 基本数据类型存放在哪里?基本数据类型跟引用类型有什么区别

  24. hashmap的原理,红黑树按照什么排序的?

  25. Map实现线程安全的3种方式?

    • HashTable:在增删改查的方法上使用了synchronized锁机制
    • Collections.synchronizedMap:在SynchronizedMap类中使用了synchronized同步关键字来保证对Map的操作是安全的;用工具类里的静态方法,把传入的HashMap包装成同步的,即在增删改查的方法上增加了synchronized锁机制,每次操作hashmap都需要先获取到这个对象锁,这个对象锁加了synchronized修饰,实现方式和HashTable差不多,效率也很低
    • ConcurrentHashMap:
    • 参考链接:https://blog.csdn.net/weixin_53975556/article/details/125037750
  26. static和final的区别

    • 成员变量 (能否修改值):final成员变量表示常量,只能被赋值一次,赋值后不能再被改变;被static修饰的成员变量独立于该类的任何对象, static 修饰的变量可以被赋值多次
    • 类 (类是否可以不用初始化就访问):final类不能被继承,没有子类,final类中的方法默认是 final 的;static 类也不能被继承,可以不用初始化而访问
    • 方法:final 方法不能被子类的方法重写,但可以被继承。final 不能用于修饰构造方法。private 不能被子类方法覆盖,private类型的方法默认是final类型的;static 方法可以被继承,但是不能重写。被static修饰的成员方法独立于该类的任何对象, 不依赖类特定的实例,被类的所有实例共享。只要这个类被加载,Java虚拟机就能根据类名在运行时数据区的方法区内定找到他们。因此,static对象可以在它的任何对象创建之前访问,无需引用任何对象。static方法是不在对象上执行的方法,不需要类的实例化,可以直接通过类调用。
  27. List<? super T>、List<? extends T>的区别

    • List<? extends T>:这里T是泛型,而?是通配符,"? extends T"表示T是父类,?是子类,该list只能容纳T类型及T类型的子类。只能从List<? extends T>中获取元素,而不能向它添加元素,所以称之为生产者
    • List<? super T>:"? super T"表示T是子类,?是父类,该list只能容纳T类型及T类型的父类。只能向List<? super T>添加元素,而不能从它里面获取元素,所以称之为消费者
    • 参考链接:https://blog.csdn.net/Temp_1998_H/article/details/125392942

三、JAVA线程和多线程

  1. 讲一下Java中线程的六种状态及其转换,创建线程的方式?

    • 初始(NEW):新创建了一个线程对象,但还没有调用start()方法。
    1. 运行(RUNNABLE):Java线程中将就绪(ready)和运行中(running)两种状态笼统的称为“运行”。
      线程对象创建后,其他线程(比如main线程)调用了该对象的start()方法。该状态的线程位于可运行线程池中,等待被线程调度选中,获取CPU的使用权,此时处于就绪状态(ready)。就绪状态的线程在获得CPU时间片后变为运行中状态(running)。
    2. 阻塞(BLOCKED):表示线程阻塞于锁。
    3. 等待(WAITING):进入该状态的线程需要等待其他线程做出一些特定动作(通知或中断)。
    4. 超时等待(TIMED_WAITING):该状态不同于WAITING,它可以在指定的时间后自行返回。
    5. 终止(TERMINATED):表示该线程已经执行完毕。
  2. 讲一下Java里面的线程安全、概念、如何实现写一个两个线程死锁的案例,或者表述一下

  3. 什么场景下会使用volatile?举个实际的场景?

    • 1)对变量的写操作不依赖于当前值 2)该变量没有包含在具有其他变量的不变式中
  4. volatile 的底层实现,如何防止指令重排线程,有哪些状态,说一下,并且说一下这些状态之间如何转移。

  5. 为什么线程访问volatile的值是主内存中的值?

  6. 如果让你实现具有缓存功能的线程池的类怎么实现?

  7. sleep和wait有什么区别?

    • 对sleep()方法属于 Thread 类中的。而 wait()方法属于Object 类中的
    1. sleep()方法导致了程序暂停执行指定的时间,让出 cpu 该其他线程,但是他的监控状态依然保持者,当指定的时间到了又会自动恢复运行状态。
    2. 在调用 sleep()方法的过程中,线程不会释放对象锁。
    3. 而当调用 wait()方法的时候,线程会放弃对象锁,进入等待此对象的等待锁定池,只有针对此对象调用 notify()方法后本线程才进入对象锁定池准备获取对象锁进入运行状态。
  8. run()和start()方法的区别?

    • start()方法来启动线程,真正实现了多线程运行。这时无需等待 run 方法体代码执行完毕,可以直接继续执行下面的代码。
    1. 通过调用 Thread 类的 start()方法来启动一个线程, 这时此线程是处于就绪状态, 并没有运行。
    2. 方法 run()称为线程体,它包含了要执行的这个线程的内容,线程就进入了运行状态,开始运行 run 函数当中的代码。 Run 方法运行结束, 此线程终止。然后 CPU 再调度其它线程。
  9. Callable()和Future()用过吗?使用Callable()创建线程比另外两种方式有什么优势吗?

    • 创建有返回值的线程
    • 也可以继承其他的类,多线程可以共享同一个target对象,适合多线程访问同一资源的情况,将cpu和数据分开,形成清晰的模型,较好的体现了面向对象的思想,还有返回值
  10. volatile int a=1,写一个方法对a进行累加,这个方法是有多个线程去访问的,这样实现能保证线程安全吗?

  11. 详细讲下为什么两个线程同时访问不能保证线程安全?(如果线程安全a应该为3,但是线程不安全就不会是3)

  12. 有一个文件,有很多人在读。并发读,没有影响,但是当一个人在写入文件时,不允许其他人写入,如果已经在写这个文件时,是不允许读,因为在修改过程中读的不是最新的,如果有人读的情况下,不希望有人能写,如果要实现这个功能,代码应该怎么去实现,去做多线程的控制?

  13. new一个线程放在哪里?

    • 首先java里的对象一般是在堆内存中(如果JIT编译优化过可能会经过逃逸分析在栈上分配对象),所以java里Thread对象本身必然也是在堆内存的,只有在调用start()方法时会调用native方法start0(),该方法用来创建对应操作系统的os thread(与java thread 1对1关系),其开辟的线程栈空间不属于jvm管理的堆内存空间.
  14. Java中的valatile和syncharonized关键字的区别?使用场景?

    • volatile关键字是线程同步的轻量级实现,所以volatile性能肯定比synchronized关键字要好。但是volatile关键字只能用于变量而synchronized关键字可以修饰方法以及代码块。synchronized关键字在JavaSE1.6之后进行了主要包括为了减少获得锁和释放锁带来的性能消耗而引入的偏向锁和轻量级锁以及其它各种优化之后执行效率有了显著提升,实际开发中使用 synchronized 关键字的场景还是更多一些。
    • 多线程访问volatile关键字不会发生阻塞,而synchronized关键字可能会发生阻塞
    • volatile关键字能保证数据的可见性,但不能保证数据的原子性。synchronized关键字两者都能保证。
    • volatile关键字主要用于解决变量在多个线程之间的可见性,而 synchronized关键字解决的是多个线程之间访问资源的同步性。
  15. synchronized和ReentrantLock 的区别

    • 两者都是可重入锁:“可重入锁”概念是:自己可以再次获取自己的内部锁
    • 实现:synchronized 依赖于 JVM 而 ReentrantLock 依赖于 API
    • 功能:ReentrantLock 比 synchronized 增加了一些高级功能:①等待可中断;②可实现公平锁;③可实现选择性通知(锁可以绑定多个条件)
    • 便利性:Synchronized的使用比较方便简洁,并且由编译器去保证锁的加锁和释放,而ReenTrantLock需要手工声明来加锁和释放锁,为了避免忘记手工释放锁造成死锁,所以最好在finally中声明释放锁。
    • 性能:Synchronized优化以前,和ReenTrantLock差很多的,从Synchronized引入了偏向锁,轻量级锁(自旋锁)后,两者的性能就差不多了
    • 参考链接:https://blog.csdn.net/qq_42773863/article/details/107915474
  16. 进程和线程的区别?

    • 调度:线程作为调度和分配的基本单位,进程作为拥有资源的基本单位
    • 并发性:不仅进程之间可以并发执行,同一个进程的多个线程之间也可并发执行
    • 拥有资源:进程是拥有资源的一个独立单位,线程不拥有系统资源,但可以访问隶属于进程的资源.
    • 系统开销:在创建或撤消进程时,由于系统都要为之分配和回收资源,导致系统的开销明显大于创建或撤消线程时的开销。
  17. Java的线程和操作系统的线程是什么关系?

    参考:https://www.cnblogs.com/cswiki/p/14676264.html
    1.操作系统线程
        //在用户空间中实现线程:
            a.在早期的操作系统中,所有的线程都是在用户空间下实现的,操作系统只能看到线程所属的进程,而不能看到线程
            b.在这种模型下,我们需要自己定义线程的数据结构、创建、销毁、调度和维护等,这些线程运行在操作系统的某个进程内,然后操作系统直接对进程进行调度。
            c.好处:第一点,就是即使操作系统原生不支持线程,我们也可以通过库函数来支持线程;第二点,线程的调度只发生在用户态,避免了操作系统从内核态到用户态的转换开销。
            d.缺点:由于操作系统看不见线程,不知道线程的存在,而 CPU 的时间片切换是以进程为维度的,所以如果进程中某个线程进行了耗时比较长的操作,那么由于用户空间中没有时钟中断机制,就会导致此进程中的其它线程因为得不到 CPU 资源而长时间的持续等待;另外,如果某个线程进行系统调用时比如缺页中断而导致了线程阻塞,此时操作系统也会阻塞住整个进程,即使这个进程中其它线程还在工作。
        //在内核空间中实现线程:
            a.内核级线程就是运行在内核空间的线程, 直接由内核负责,只能由内核来完成线程的调度。
            b.我们可以直接使用操作系统中已经内置好的线程,线程的创建、销毁、调度和维护等,都是直接由操作系统的内核来实现,我们只需要使用系统调用就好了,不需要像用户级线程那样自己设计线程调度等。
            c.类别:多对一线程模型、一对一线程模型、多对多线程模型
    2.Java线程
        -在 JDK 1.2 之前,Java 线程是基于称为 "绿色线程"(Green Threads)的用户级线程实现的,也就是说程序员大佬们为 JVM 开发了自己的一套线程库或者说线程管理机制。
        -在 JDK 1.2 及以后,JVM 选择了更加稳定且方便使用的操作系统原生的内核级线程,通过系统调用,将线程的调度交给了操作系统内核
        -也就是说,在 JDK 1.2 及之后的版本中,Java 的线程很大程度上依赖于操作系统采用什么样的线程模型,这点在不同的平台上没有办法达成一致,JVM 规范中也并未限定 Java 线程需要使用哪种线程模型来实现,可能是一对一,也可能是多对多或多对一。
        -现今 Java 中线程的本质,其实就是操作系统中的线程,其线程库和线程模型很大程度上依赖于操作系统(宿主系统)的具体实现,比如在 Windows 中 Java 就是基于 Wind32 线程库来管理线程,且 Windows 采用的是一对一的线程模型。
    
  18. 核心线程数、最大线程数、队列三者之间的关系?

    • 新任务提交在线程池内部处理的优先级:核心线程 > 阻塞队列 > 扩容的线程

    • 一种方法:

      1. 随着任务数量的增加,会增加活跃的线程数。
      2. 当活跃的线程数 = 核心线程数,此时不再增加活跃线程数,而是往任务队列里堆积。
      3. 当任务队列堆满了,随着任务数量的增加,会在核心线程数的基础上加开线程。
      4. 直到活跃线程数 = 最大线程数,就不能增加线程了。
      5. 如果此时任务还在增加,则: 任务数11 > 最大线程数8 + 队列长度2 ,抛出异常RejectedExecutionException,拒绝任务。
    • 另一种说法:

      1. 线程池初始化的活跃线程数为0
      2. 当活跃线程数<核心线程数,且活跃线程在执行任务,线程池会新生成线程用以执行提交的任务
      3. 当活跃线程数=核心线程数,且活跃线程在执行任务,新任务会优先被放置到阻塞队列
      4. 当活跃线程数=核心线程数,且活跃线程在执行任务,且阻塞队列已满,且没有活跃线程数<最大线程数,则线程池会新生成一个线程来执行任务
      5. 当线程空闲,会被逐步回收,如果持续没有新任务提交,线程池活跃线程数会降低为0
  19. 单例模式了解吗?来给我手写一下!给我解释一下双重检验锁方式实现单例模式的原理

    public class Singleton { 
        private volatile static Singleton uniqueInstance; // volatile 可以禁止 JVM 的指令重排
        
        private Singleton() { } public static Singleton getUniqueInstance() { //先判断对象是否已经实例过,没有实例化过才进入加锁代码 
            if (uniqueInstance == null) { //类对象加锁 
                synchronized (Singleton.class) { 
                    if (uniqueInstance == null) { 
                        uniqueInstance = new Singleton(); 
                    } 
                } 
            } 
            return uniqueInstance; 
        } 
    }
    
  20. Java线程的 runable=ready+running,操作系统线程分为 running和 ready,并不是合在一起的,为什么Java把这两个状态放在一起?

    • running和ready的线程状态转换,是由操作系统的系统调度完成的,Java不需要来关注
  21. 堆为什么要分代

    • 堆内存分代是为了提高对象内存分配和垃圾回收的效率
    • 如果堆内存没有区域划分,所有的新创建的对象和生命周期很长的对象放在一起,随着程序的执行,堆内存需要频繁进行垃圾收集,而每次回收都要遍历所有的对象,遍历这些对象所花费的时间代价是巨大的,会严重影响GC效率
  22. 多线程的实质了解吗?

  23. select、poll、epoll 区别

    • 支持一个进程所能打开的最大连接数
      1. select:单个进程所能打开的最大连接数有FD_SETSIZE宏定义,其大小是32个整数的大小(在32位的机器上,大小就是3232,同理64位机器上FD_SETSIZE为3264),当然我们可以对进行修改,然后重新编译内核,但是性能可能会受到影响,这需要进一步的测试。
      2. poll:poll本质上和select没有区别,但是它没有最大连接数的限制,原因是它是基于链表来存储的。
      3. epoll:虽然连接数有上限,但是很大,1G内存的机器上可以打开10万左右的连接,2G内存的机器可以打开20万左右的连接。
    • FD剧增后带来的IO效率问题
      1. select:因为每次调用时都会对连接进行线性遍历,所以随着FD的增加会造成遍历速度慢的“线性下降性能问题”。
      2. poll:同上
      3. epoll:因为epoll内核中实现是根据每个fd上的callback函数来实现的,只有活跃的socket才会主动调用callback,所以在活跃socket较少的情况下,使用epoll没有前面两者的线性下降的性能问题,但是所有socket都很活跃的情况下,可能会有性能问题。
    • 消息传递方式
      1. select:内核需要将消息传递到用户空间,都需要内核拷贝动作
      2. poll:同上
      3. epoll:epoll通过内核和用户空间共享一块内存来实现的。
    • 参考链接:https://blog.csdn.net/liangwenmail/article/details/120312826
  24. 线程池里线程的创建与销毁,核心线程可以销毁吗?

  25. 高并发怎么减少锁的竞争?

    • 缩小锁范围:缩小锁的范围可以减少持有锁的时间;如果使用synchronized块,并且块中只包含一行或几行代码,那么持有锁的时间将大大降低。
    • 锁分解:将多个对象共用的一个锁分解成各自的锁
    • 锁分段:将一个对象分成多个部分,每个部分使用一个锁。数组分段锁
    • 参考链接:https://zhuanlan.zhihu.com/p/42047538
  26. 256M内存如何排序10G大数据

  27. ready -> waiting 有哪些方式:看图

  28. 启动两个线程交替串行打印奇偶数,线程池怎么实现?

  29. 从volatile出发, 要求写出volatile解决可见性的代码。

  30. 从可重入锁出发, 要求写出基于可重入锁的阻塞队列,怎么实现。

  31. 线程切换的时机?给线程分配时间片,操作系统怎么知道时间片用完了?线程切换 一定会引起内核态和用户态的切换吗?

  32. 了解Java的线程池吗?里面有一个ForkJoinPool,有没有用过?说一下

  33. ForkJoinPool和其他的线程池有什么不一样的地方吗?比如用到了哪些算法

  34. 那个CountDownLatch你知道吗?说一下。信号量呢?

  35. 关闭或者重启线程池的时候,队列中的任务要不要执行?

  36. 让调用线程来执行任务有什么问题?

  37. atomic 的实现原理

    • 基本特性:多个线程同时对该变量的值进行更新时,仅有一个线程能成功,而未成功的线程可以向自旋锁一样,继续尝试,一直等到执行成功。
    • 原理:自旋 + CAS(乐观锁)
      1. 首先,声明共享变量为 volatile;
      2. 然后,使用 CAS 的原子条件更新来实现线程之间的同步;
      3. 同时,配合以 volatile 的读 / 写和 CAS 所具有的 volatile 读和写的内存语义来实现线程之间的通信。
  38. 内置锁和显式锁区别:即内置锁(synchronized)和显式锁(ReentrantLock)两种同步方式区别

posted @ 2021-12-18 15:39  汤十五  阅读(121)  评论(0)    收藏  举报