admin管理员组

文章数量:1532742

前言:本文是为了秋招准备,主要以点和面的形式进行复习。(小知识用点,多一点的将以面的形式展示)。

Java的特点

  1. 继承(集成一个父类,实现多个接口)
  2. 封装
  3. 多态
  4. 线程以对象的形式存在,因此多线程编程较为容易。
  5. 半解释半编译语言。(先翻译成class文件,再由JVM执行)。

JDK、JVM、JRE

JDK = JRE + 各种Java的api等开发的工具
JRE = JVM + 各种核心类库
简单来说JDK contains JRE contains JVM

Java和C++的区别

  1. Java有垃圾回收机制,因此不需要像C++那样手动释放内存,所以不用特别去关心内存泄漏的问题(当然Java也是有内存泄漏问题的)
  2. Java是没有指针的,C++是有的
  3. Java是集成单个父类,实现多个接口。而C++是可以多继承的。

抽象类和接口的区别

  1. 概念上的问题:抽象类抽象的是对象,一个模板。而接口抽象的是动作,是行为。
  2. 抽象类:
    • 抽象类中可以有非抽象的方法,但是抽象方法只能存在于抽象类中。因为抽象类是不能呗实例化的。
    • 抽象方法可以没有主体。
  3. 接口:
    • 接口的方法默认是public abstract的,不用显示去写。
    • 接口中的变量是final static的,所以要记得赋予初始值。
    • 接口有一个默认的default,这个default可以让接口的方法不强制被重写。

Protect、Static、final

Protect和static只能修饰内部类
final内外都可以修饰

final、finally、finalize

final
  1. final修饰变量,表示这个是常量,不能被修改。
  2. final修饰方法,表示这个方法,不能被子类重写。
  3. final修饰类,表示这个类不能被集成,里面的方法全是final修饰的。
  4. final修饰对象,对象引用不能变,对象中的值可以变。
finally
  1. try catch语句中常用,在finally中的语句一定会被执行
  2. 常用于释放资源
finalize
  1. Object中的一个方法,在gc的时候会调用对象finalize方法实现对资源的清理
  2. finalize不保证一定会被执行

构造方法

  1. 特点:
    • 没有返回值
    • 与类同名
    • 生成对象的时候自动被调用。
    • 子类对象会调用父类对象来完成自身初始化。
  2. 子类的构造方法和父类的构造方法
    • 在子类中会使用super这个关键字来代表父类。一般子类会有(java会自动生成)空的构造函数来调用父类的构造方法。

== 和equals的区别

  1. == 是值的判断,对于基本类型,就是值的判断,引用类型就是地址的判断。
  2. equlas本身是一个方法,是一个函数。里面的行为是我们自定义的。经典的用法就是String.equls和==的区别。

HashCode和equals

最重要的一点。如果不用散列表,比如HashMap、HashSet这种数据结构,两者其实没有本质上的联系。
如果采用了上述的数据结构:

  1. 会先判断两者的HashCode是否相等,如果两者的HashCode相等,那么就会被判定为同一个对象,这样就会插入失败。当HashCode相等的时候,就会调用equals方法判断两者是否相同,如果两者相同同样会插入失败。

try catch finally

try catch finally的执行顺序是这样子的
先去执行try中的内容,如果出现异常就跳转到catch中执行,最后执行finally中的内容。注意,如果如果再try 和 catch中执行了return,会先去执行finally中的内容在return,这就导致如果再finally执行return,那么在try catch中的return不会生效

Java中的IO部分

其实有两个大类分别是:

  1. 字节流
    • InputStream:
      • FileInputStream
      • dataInputStream
      • BufferInputStream
    • OutputStream
      • FileOutputStream
      • dataOutputStream
      • BufferOutputStream
        类似这样的。在java中字符是通过jvm转换字节流得到的,比较耗时。所以还有住啊们的字符流
  2. 字符流:
    • xxxRead
    • xxxWriter
  3. Java中的IO模型详解
    从应用程序的视角来看的话,我们的应用程序对操作系统的内核发起 IO 调用(系统调用),操作系统负责的内核执行具体的 IO 操作。也就是说,我们的应用程序实际上只是发起了 IO 操作的调用而已,具体 IO 的执行是由操作系统的内核来完成的。
    • BIO(同步阻塞模型):用户在发出IO请求后会陷入阻塞状态。然后等待内核(系统调用)执行,把数据写会用户空见。但是这种模型,无法应对高并发。
      - NIO(No-blocking-IO):对于传统的同步非阻塞IO模型

      应用成语在发送IO请求之后,并不会阻塞自身,而是间隔一段时间,就发起read请求,去观察内核中数据是否准备好了。但是应用程序不断的轮询是挺小号CPU资源的。这个时候就可以使用I/O多路复用模型。

      没戳,通过进行select/poll/epoll就可以实现一次的请求。具体怎么做请详见操作系统。IO多路复用模型减少了无效的系统调用。
      同步IO和异步IO最大的区别在于
  4. 发起IO请求,期间等待CPU指挥DMA搬运树局(这一步异步不管,只发请求,同步产生区别)
  5. 请求进程阻塞自身,发生IO读写(这一步,同步请求进程会阻塞自身,异步不会阻塞自身)

深拷贝和浅拷贝

  1. 浅拷贝:拷贝一个地址
  2. 深拷贝:生成一个新的对象,这个对象的值和原来的值是一样的。

Java中的数据结构


总共有两个大类
Collection和Map
先说Collection

Collection
  1. List:在List当中,重点关注三个数据结构ArrayList、LinkedList、Vector。
    • LinkedList:
      • LinkedList的性能并不是特别好,一般都是用ArrayList。
      • LinkedLis底层的数据结构是双向链表的数据结构。因此在头和尾的操作都是O(1)其余相同。
      • 虽然用的是离散内存空间,但是需要花一部分空间存储next指正。
    • ArrayList:
      • ArrayList的底层是一个数组,占用的是连续空间。因此它支持随机访问。其次,ArrayList在遍历删除数据的时候是需要注意的。因为索引会搬迁。
      • ArrayList的扩容机制。
        • ArrayList在初始化的时候,如果调用无参构造器,一开始是没有初始化底层的数组elementData()的,等到调用add的时候才会初始化。懒加载来节省内存。
        • ArrayList初始化的底层数组长度为10,需要扩容的时候会扩展到原来的1.5倍。负载因子为1,因为他不需要处理哈希碰撞的问题。
    • Vector:是一个古老的数据类型。它是线程安全的。但是现在也不怎么用,底层和ArrayList很像,但是加入了Synchronized加锁了,所以他是线程安全的,但是他并没有很好的性能。
  • Set:在Set的部分重点关注HashSet、LinkedHashSet、TreeSet三者的异同。

    • HashSet:其实底层就是一个HashMap。无序的
    • LinkeHashSet:底层是一个LinkedHashMap。是有序的
    • TreeSet:底层是红黑树,是有序的。
  • Map:老熟客之HashMap,老八股文了。

    • Hashtable:
      1. 线程安全的
      2. 继承了Dictionary虚拟类,而HashMap是集成了abstractMap这个虚拟类
      3. 底层很相似,只不过Hashtable加了Synchronized
      4. 为什么Hashtable的性能没有ConcurentHashMap好:因为Hashtable在很多实例方法上加入了Synchronized关键字,相当于拿了对象的锁,这锁的粒度太大,并发效果就没有ConcurrentHashMap好了。而ConcurrentHashMap在java1.7之前使用的是分段锁,之后使用的是Node + 红黑树 + 大量的CAS操作,因此比Hashtable的性能好。
    • HashMap:
      • HashMap和HashTable.HashTable是线程安全的,但是现在已经被废弃不怎么使用了。HashMap是线程不安全的,CurrentHashMap是线程安全的,TreeSet的底层数据结构是TreeMap,而TreeMap的底层数据结构是红黑树。这个也是我们需要重点关注的四个数据结构啦。
      • HashMap的底层数据结构:
        • 在java1.7之前:底层的数据结构是数组+链表的数据结构。采用拉链法来解决哈希冲突,链表采用头插法插入数据,在扩容的场景下再加入数据,可能会造成循环链表的问题。
        • 在java1.8之后:底层的数据结构是数组+链表or红黑树的结构。链表的长度在超过8之后会采用新的数据结构红黑树,数组的初始长度为16。
        • 采用红黑树的好处。红黑树是一颗不严格的自平衡树,它允许局部部分不平衡,这样虽然损失一部分查找的性能,但是对于增删有挺大的好处的。
HashMap扩容机制详解

大体的流程是

  1. Map.put(),发现链表的长度已经达到8了,再判断数组的长度,如果数组的长度高于64,会将链表转换为红黑树,否则会优先进行数组扩容。
  2. HashMap的长度为什么是2的幂次方:其核心目的是为了减少哈希碰撞,通过减少哈希碰撞来实现高性能。底层原因是因为通过函数可以发现hash&(length-1),如果是2的幂次方,其实就是很多个1,这样能让值均匀的散步在数组上。
  3. HashMap的负载因子为0.75.这个负载因子过大,会造成频繁的哈希碰撞,这个负载因子过小,空间的利用率就不足了。
  4. HashMap在扩容的时候,在1.7之前是头插法,1.8之后变成了尾插法,虽然还有是覆盖的问题,但是不会造成循环链表了。
  5. 注意HashMap在resize()之后,是用一个大的数组替换了原来的数组,因此原数组中数据的下标是可能发生变换的。抓住本质是hash&(length-1)。length发生变化,下标发生变化。
  6. CurrentHashMap,在1.7之前,是把数据分块,变成一个个Segment,再根据Segment来加锁。但是这样的颗粒度太大了,于是在1.8之后做出了改变,在1.8之后是采用node+红黑树加上大量的CAS操作和Synchronized来完成的。
HashMap线程不安全详解
  1. 1.7java采用头插法,在扩容的时候会导致环形链表和数据丢失
    在1.7采用头插法是认为,新加入的节点被访问的概率更高
    先扩容,后插入
  2. 在1.8采用尾插法,在并发put的时候会导致数据被覆盖,先插入后扩容
ConcurrentHashMap详解
  1. 在java1.7,采用分段锁的机制进行保护,采用的锁是ReentrantLock。Segment + HashMap + ReentrantLock
  2. 在java1.8 采用 node + 红黑树+ Synchronized + 大量的CAS操作
  3. 为什么要使用Synchronized来代替ReentrantLock,主要原因是Synchronized可以自旋,而ReentrantLock不能自旋,在这一部分开销比较大。
  4. 1.8的ConcurrentHashMap,是针对每一个节点进行加锁操作的。比如红黑树,就针对红黑的头结点加锁。加锁的粒度更小,并发就更高了。
  5. 如何让HashMap变得线程安全
    1. 使用ConcurrentHashMap
    2. 使用HashTable
    3. Collections.synchronizedMap()
HashMap的put get
  1. 传入的时候判断数组是否为空,为空就进行初始化
  2. 计算hash&(length - 1)计算下标,hash是,hashcode异或hashcode右移16位的结果
  3. 查看是否发生了哈希冲突(hashcode相同,再用equals判断),没有就直接加入,有的话就用拉链法解决冲突。当链表长度大于8且数组长度大于64时,链表转换成红黑树
  4. 加入节点之后判断是否需要扩容
    get就是去对应的索引找数据比较简单

有序的Map

  1. TreeMap:底层是一个红黑树,所以是有序的
  2. LinkedHashMap:底层维护了一个双向链表,所以是有序的

Java JVM部分

先介绍一下堆和栈的不同
  1. 数据结构不同:
    栈是一种先进先出的线性,只能在一端操作
    堆是一种树状的数据结构,但是它有唯一后继。分为大根堆和小根堆,子节点也是堆,只能在一端操作。
  2. 在java中的不同之处
    1. 堆对程序员是可见的,程序员可以控制堆的大小,调整新生代老年代的大小,做一些参数的调整,而栈是系统分配的,不受程序员控制
    2. 堆和栈的作用不同
      1. 堆一般用于存放对象,是垃圾回收机制主要处理的地方
      2. 栈一般用于存放线程私有的变量
        Java很特殊的一点就在于他本质上是一个解释与编译共存的语言。编译语言的本质,是把代码翻译成机器指令然后让电脑去运行,而解释语言的本质是一行一行把代码翻译成指令去运行。

        JVM内存分布这张图其实说明的很完整了。
        JVM的内存有四大块
  3. 堆区域:是Java内存中最大最重要的一个部分,基本上来说成员变量都是在这里存储的。他主要被划分为3块
    1. 新生代Eden区和survivor区(survivor区分为survivor1和survivor2)
    2. 老年代
  4. 栈区域:如果说堆区域是线程共享的,那么栈区域就是线程私有的了。主要由
    1. 程序计数器
    2. 虚拟机栈
    3. 本地方法栈
  5. 直接内存:直接内存是Java内存在操作系统内核开辟了的共享内存,数据直接映射在这片区域提升读写能力。
  6. 元空间/永生代:在这个地方主要放一些常量,或者类的一些静态变量等内容。
    关于元空间、方法区、永生代的关系。
    元空间和永生代是方法去的一个实现方式,方法去是Jvm的一个规范
    元空间区在java8取代了永生去
    所以应该问的是:
    方法区是什么:方法区概念上装载了一个class类文件的信息,一些运行时常量、字符串常量。但是1.7后把字符串常量池搬到了堆中去。因为1.7的永生代占用的是java的堆内存,只有在full gc的时候才会被清理造成大量资源浪费
    元空间和永生代的区别在哪儿:元空间用的是直接内存,用的不是Java的内存,而永生代用的是java的内存。这样元空间的大小不归jvm管了。
所谓GC

垃圾回收机制发生在堆区域中。

  1. 什么是垃圾?怎边辨别这个对象是垃圾?
    • 垃圾就不会再次被用到的对象。这个对象还占据堆空间的内存,因此需要释放这部分内存。
    • 如何判断这个对象已经死亡:
      1. 引用计数法:给对象添加一个引用计数器。如果有地方引用到它,计数器加1,引用失效计数器-1。问题在于如果存在相互引用,那么这个计数器永远不会归零。
      2. 可达性判断,通过GC ROOT作为节点访问,如果能访问到就不算死亡不算垃圾(一般来说要经过两次判断才会真正被判定为死亡)
      3. 判断一个无用类:ClassLoader被回收、class对象未被使用、不存在对应实例。
  2. 垃圾收集算法
    • 标记清除算法:先标记哪些对象是要使用的,然后全部释放未被标记的内存空间。这个算法带来的问题是,内存空间都是离散的
    • 标记复制算法:把内存分为了两块,他会标记还存活的对象,然后一次性复制到另外一块去。并且释放原来那一块的空间,优点是性能比较高,缺点是浪费内存
    • 标记整理算法:标记存活的对象,然后开始移动,把端后的空间释放。
    • 这就是为什么要分代了。根据代不同的特性可以选择不同的算法。比如新生代,对象的存活周期短,用标记复制算法性能较高,而老年代用标记整理或者标记清除的算法性能较高。
  3. JVM中的垃圾回收器
    1. Serial:单线程垃圾回收器,运行时暂停其他工作线程Stop The World。在新生代采用标记复制算法,在老年代采用标记整理算法。
    2. ParallelNew:并行垃圾回收器。是Serial的多线程版本。也是Stop THe World的。新生代标记复制,老年代标记整理。
    3. ParallelScavenge:和ParrallelNew差不多。只不过前者注重吞吐量,后者注重实时性。新生代采用标记复制,老年代采用标记整理
    4. 注意在java1.8版本,通过命令看出是UseParallelGC也就是默认是ParallerScavenge 收集新生代用Serial Old手机老年代
    5. CMS收集器:一个跨时代的收集器。他最大的特点就是并发,他不需要停止用户线程。采用的是垃圾清除算法,所以会有一定的内存碎片。他共有四个阶段,而且CMS及其注重用户体验,会有较低的停顿时间。
      1. 初始标记
      2. 并发标记
      3. 重新标记
      4. 并发清除
    6. G1收集器:
      1. 也是并发的
      2. 不需要搭配其他的垃圾回收器
      3. 可以预测停顿时间
      4. 基于标记整理算法
  4. GC的分类
    1. young gc/minor gc
    2. old gc
    3. mixed gc
    4. full gc
  5. 什么时候会触发minor gc:进程创建对象的时候,发现内存不足,会触发minor gc。或者新生代到达一定阈值会触发minor gc。
  6. 什么时候会触发Full GC?
    1. 主动调用system.gc
    2. 老年代空间不足的时候。比如发生minor gc之前,使用空间担保机制(事实上每次minor gc都会触发空间担保机制),发现老年代的空间不够大,就会触发full gc
  7. JVM的空间分配担保机制
    所谓的空间担保机制。在发生minor gc的时候,会先使用空间担保机制,保证老年代有足够的空间去装载本次会进入老年代的对象。如果没有就会发生full gc。
类的加载

在java中,类的加载就是.class文件被jvm生成一个类对象的过程。类的加载是通过几个类加载器来实现的
- bootstrap classloader
- extension classloader
- application classloader

  1. 双亲委派机制:如果父类加载器能够加载这个类,用父类加载器来加载,这样的好处是,避免重复加载。
  2. 类的加载过程
    1. 加载:获取字节流生成对应的class对象
    2. 验证:进行一个语法的检查
    3. 准备:会为static的一些变量附上一些初始值
    4. 解析:将符号引用替换成直接引用,符号引用可以理解为com.ice.people,他唯一标识了一个资源,直接引用可以是,指向这个类对象的指针。
    5. 初始化
  3. 双亲
new一个对象的流程

在Java中new一个对象的流程分为两步,第一步查看对应的类是否被加载进入内存,如果不存先加载类,如果已经被加载进了内存,执行第二步生成对象

  1. 第一步:加载类
    1. 双亲委派机制:加载会把类信息传递到顶层的类加载器,当顶层的类加载器不能加载才会逐步往下传递,直到找到能加载该类的类加载器。
      1. 好处是不会重复加载类
      2. 而且能保证用户不会破坏java中的核心类
    2. 执行流程,Java会先把.java文件编译成.class文件(java的半解释半编译,.java编译成.class,交于不同的jvm去解释)
      1. 加载:生成一个class对象,并且在元空间存放必要的信息
      2. 验证:验证语法、
      3. 准备:为类变量划分空间并设置初始值
      4. 解析:符号引用替换成直接引用
      5. 初始化:调用java程序,把类变量定义成程序员定义的值
    3. 有了类对象之后就开始生成一个对象
      1. 判断类是否被加载过了,没有加载类需要先加载类。
      2. 在堆内存中划分空间
      3. 设置变量默认值,这个是把boolean = false这样,不是调用程序员定义的构造方法
      4. 设置对象头,设置偏向锁的、年龄、hashcode等等
      5. 初始化,先调用实例代码,再调用构造方法,先执行父类的代码
生成的对象一定在堆空间中

java生成的空间不一定在堆空间中

  1. 逃逸分析:经过JIT分析,如果一个对象不会逃逸,就会被优化在栈上创建,而不是创建在堆中
  2. 什么是逃逸:
    1. 这个对象作为参数传递就是逃逸
    2. 只在当前代码块中使用就不是逃逸
  3. 标量替代:被认定为不会逃逸的对象,会被分配在栈空间中,并且会被分解成一个个不能分割标量。

JMM

JMM是java的内存模型而不是内存结构
JMM是抽象的概念,核心目的是让java程序在各个平台都能达到一致的并发效果。

  1. 两种区域
    在JMM中划分了两种区域,一种是主内存,一种是线程的工作内存。Java的所有变量都存储在主内存中(静态变量,实例变量),但是方法参数、局部变量等,不需要。工作内存存放了线程会用到的变量和主内存的部分拷贝。线程不能直接读写主内存的数据。线程之间也不能直接通信。一般都是,线程把主存的数据读进工作内存,修改之后再写回。

  2. JMM定义了什么?JMM是围绕着三个特性建立的

    1. 原子性:一个原子操作是不可分割的
    2. 可见性:当一个线程修改了主存的数据,其他线程能够感知。
      1. 通过volatile关键字实现。
      2. Synchronized 在 unlock结束之后也会把数据同步到主存
      3. final修饰的也可以实现可见性
    3. 有序性:
      1. 指令重排是指:指令执行顺序与代码不同,但是结果相同。
      2. volatile通过内存屏障,防止指令重排序保证有序性
      3. Synchronize是通过线程同步实现有序性的
  3. JMM的八个基本操作

    1. lock
    2. read
    3. load
    4. use
    5. Assign
    6. store
    7. write
    8. unlock
  4. volatile的工作原理:

    1. 在线程的工作区域修改变量
    2. 同步主存
    3. 修改主存变量
    4. 失效其他线程的变量,通过CPU总线嗅探机制
    5. 其他线程因变量失效,就会返回主存中读取新的数据。
    6. volatile会在读写前面插入内存屏障

Java死锁案例

public class DeadLockTest {
    public static void main(String[] args) {
        Thread A = new Thread(new ThreadA());
        Thread B = new Thread(new ThreadB());
        A.start();
        B.start();
    }
}
class Resource {
    public static Resource resourceA = new Resource();
    public static Resource resourceB = new Resource();
}
class ThreadA implements Runnable {

    @Override
    public void run() {
        System.out.println("线程A启动");
        synchronized (Resource.resourceA) {
            System.out.println("获取resourceA成功");
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            synchronized (Resource.resourceB) {
                System.out.println("获取resourceB成功");
            }
        }
        return;
    }
}
class ThreadB implements Runnable {

    @Override
    public void run() {
        System.out.println("线程B启动");
        synchronized (Resource.resourceB) {
            System.out.println("获取resourceB成功");
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            synchronized (Resource.resourceA) {
                System.out.println("获取resourceA成功");
            }
        }
        return;
    }
}

Java同步锁案例

public class SynchronizedTest {
    public static void main(String[] args) {
        Ticket ticket = new Ticket();
        Thread threadA = new Thread(ticket,"a");
        Thread threadB = new Thread(ticket,"b");
        Thread threadC = new Thread(ticket,"c");
        threadA.start();
        threadB.start();
        threadC.start();
    }
}
class Ticket implements Runnable{
    // 假定有20张票
    private int ticket = 200;
    @Override
    public void run() {
        while (ticket > 0) {
            synchronized (this) {
                if (ticket > 0) {
                    System.out.println(Thread.currentThread().getName() + "售票员卖出"+ticket+"号票");
                    ticket--;
                }
            }
        }
    }
}

多线程部分
  1. volatile关键字:这个关键字可以保证,线程本身不适用自己的缓存,每次访问这个关键字的时候,都去内存中拿出对应数据。
  2. ThreadLocal的原理
    1. ThreadLocal出现,是为了保证每一个线程都有一个变量的副本,实现数据的隔离。每一个线程都有ThreadLocalMap,里面存放着变量副本。
      ThreadLocal vs Synchronized
    2. ThreadLocal本质上,是为了每一个线程创建了一个数据的副本,所以多线程在同一时间段内,访问的变量是不同的。而Synchroniezd是通过锁的机制实现了数据的共享,在同一个时间段内,多个线程访问的变量是相同的,这就是两者最本质的区别。
    3. ThreadLocal在我眼中是用空间换时间,而Synchronized是用时间换空间。

从java的调度看进程和线程

  1. 线程在java中就是一个对象,因此在Java的角度上,线程的管理比较方便
  2. 通常情况,在多线程并发时,如果有一个线程崩溃了,或者非法访问内存地址,会导致整个进程崩溃,这是通过信号来实现的,而在java中jvm重写了这个过程,所以在java中,线程的崩溃不会引起进程的崩溃。
  3. 在java中可以通过集成Thread类和实现runnable接口来编写一个线程
  4. 在java中,一个进程的内存被分为
    1. 堆空间
    2. 栈空间
      1. 程序计数器
      2. 虚拟机栈
      3. 本地方法栈
    3. 元空间
    4. 直接内存

Java线程池

  1. 创建线程池的优缺点
    优点
    1. 减少了创建销毁线程带来的开销
    2. 方便管理线程
    3. 响应比较快
      缺点
    4. 需要维护一个线程池
  2. 创建线程池:
    1. 创建线程池可以通过ThreadPoolExecutor,或者通过工具类来实现
    2. 核心参数
      1. corePoolSize:核心线程的数量,初期线程池中是没有线程的,只有任务来了,才会创建线程。核心线程池的数量设计。如果是IO密集型,也就是说对CPU压力负担不大的可以多设置一些,比如设计成CPU核数/1-阻塞系数。CPU密集型就设计的少一些,最好和CPU核数相同
      2. maxPoolSize:线程池中最多容纳线程的数量。注意只有当线程池中所有线程都不空闲,并且workQueue队列都满的情况,才会去临时的线程
      3. keepAlive:多出来的线程存活的时间
      4. unit:keepAlive的单位
      5. workQueue:
        1. 有界队列:ArrayBlockingQueue
        2. 无解队列:LinkedBlockingQueue
        3. 同步队列:
      6. threadFactory:制定创建线程的工厂
      7. handler:饱和策略,当workQueue已满,并且线程池中的线程池已经达到maxPoolSize时,对于新加入的任务的策略
        1. 丢弃,悄悄丢弃
        2. 选择最早没有执行的任务丢弃
        3. 抛出异常 Abort,默认策略
        4. 把这个任务返回给调用者(让提交这个任务的线程去执行)
java线程池踩坑记录
  1. 调用了Executor来创建线程池,而Executor创建线程池都是有无界队列,因此会出现OOM问题。比如遇到执行很久的任务,那工作队列会一直加任务,直到溢出。

  2. Java的阻塞队列:

    1. 有界队列:底层是个Array
    2. 无界队列:默认长度为Integer.Max_value
    3. 同步队列:相当于只有一个单位的长度
  3. Runnalbe和Callable

    1. Runnable不能抛出异常,没有返回值,而Callable可以跑出异常,有返回值
    2. Runnable可以作为Thread的参数,开启线程来使用,也可以通过线程池来使用,而Callable只能通过线程池

CAS详解

  1. CAS = compare and swap
  2. 三个概念,旧值 = 新值才会更新变量
    V:var,需要更新的变量
    E:expect,预期值
    N:new,新值
  3. 在Java中有一个Unsafe类,里面定义了CAS操作,在Java中的CAS操作是被native修饰,也就是并不是由Java去实现的。
  4. CAS底层是操作系统的原语,保证安全性
  5. 在Java的Atomic底层就是调用Unsafe类完成CAS操作的
  6. CAS的弊端 ABA问题,在java中通过一个stamp版本号来解决这个问题
  7. CAS修改不成功会自身进入自旋,高并发大量的自旋浪费了cpu功能。但是CAS产生的原因,就是经过判断发生冲突的可能不大才会用乐观锁的。
  8. 思想之狭隘:i++的过程思考CAS的作用

AQS

  1. AQS维护了一个先进先出FIFO的队列CLH
  2. 用voliate修饰一个变量state = 0,表示资源空闲让进,= 1表示资源不用先,线程进队列
  3. 有两种第一资源的方式
    1. 独占资源:ReentrantLock
    2. 共享资源:ReentrantReadWriteLock,这个lock是独占资源和共享资源都用的。

JUC

JUC是java.util.concurrent,是java为了支持应对高并发提供的类。
对于JUC你需要掌握的

  1. Automic类在JUC中
  2. ThreadPoolExcutor
  3. Lock
  4. AQS

在Java中的锁

  1. 乐观锁:CAS
  2. 悲观锁:ReentrantLock、Sychronized、
  3. 轻量锁:Synchronized
  4. 偏向锁:Synchronized
  5. 重量锁:Synchronized
  6. 公平锁:ReentrantLock可以制定是不是公平锁
  7. 不公平锁:Synchronized是不公平锁,ReentrantLock可以是不公平锁
    1. 公平锁vs不公平锁:公平锁,当前线程或许对象锁资源,先进入一个等待队列,如果没有其他线程竞争资源,它就是对头,可以获得资源。不公平锁,就是先尝试获取对象,然后再进入队列
  8. 自旋锁:
  9. 可重入锁:Synchronized、ReentrantLock就是可重入锁
  10. 分段锁:ConcurrentMp
  11. 读写锁:ReadWriteLock
  12. 自适应锁:自适应锁和自旋锁放一起,因为在Synchronized的轻量锁,如果线程没有竞争到锁资源,就会陷入自适应自旋锁状态(其实不是加锁,就是不断循环)。自适应的原因是,想根据竞争到这个锁的可能性做出调整,如果竞争到锁资源的可能比较大,自适应久一些,如果竞争到资源的可能性比较小,就自适应短一些。
谈一谈在java中Synchronized和lock

浅谈一下Synchronized

  1. 使用方法

    1. 作用在实例方法上:锁定的是这个对象
    2. 作用在静态方法上:锁定的是这个类对象
    3. 作用在代码块上:看的是你传入的值
  2. Synchronized的实现原理:
    在一个对象的内部有一个monitor锁通过这个锁来完成Synchronized。通过这个monitor锁,最后再字节码锁住这一块代码的前后会加两条指令,monitorenter和monitorexsit,通过这两条指令,再调用操作系统的Mutex Lock完成,在这个过程中会从用户态切换进内核态,所以说Synchronized是重量锁。考虑到这一点对Synchronized做出了优化。
    现在的Synchronized有四种状态

    1. 无锁状态:
    2. 偏向锁状态:处于这个状态,被偏向的线程执行操作不需要加同步操作,一般会偏向第一个访问的对象
    3. 轻量锁状态:处于这个状态下,没有竞争到锁资源的线程会进入自适应自旋状态(不断轮询CPU)
    4. 重量锁状态:处于这个状态下,没有竞争到锁资源的线程会陷入阻塞的状态(切记,阻塞状态会携带着资源阻塞)
      Synchronized只能升级,不能降级。升级的驱动力是多线程竞争,修改了Mark Word的字段
  3. 可重入锁
    概念,当一个线程获得了对象的锁资源,这个线程的其他流程同样可以获得这个对象的锁资源。会有一个计数器+1
    在java中,Synchronized和ReentrantLock都是可重入锁

synchronized和lock

  1. 在java中Synchronized和Lock
  2. synchronized是一个关键字,而lock是一个接口,他的实现类有ReentrantLock
  3. synchronized执行不受我们控制,只有在程序执行结束或者抛出异常之后才会释放自动释放锁资源,而lock必须手动去释放锁资源
  4. 在Synchronized中,如果竞争不到锁资源,会陷入阻塞,这个情况会携带着资源一起阻塞,很容易就造成了死锁,而在lock中这个问题就得到了很好的解决。我们可以设定等待时间,过了这个等待时间就不再加锁,或者加锁失败抛异常等策略
  5. 可以说Synchronized比Lock来说,lock更加灵活一些
Synchronized和ReentrantLock

ReentrantLock就是lock的一个实现类

  1. 等可以终端
  2. 可以实现公平锁
  3. 可以根据Condition分组唤醒线程

Unicode 和 Ascii的区别

  1. Unicode通常用两个字节表现一个字符,ASCii是用单字节表现一个字符,因此Unicode的作用范围更广
  2. 如果用Unicode来表示Ascii码,前一个字节为0
  3. Utf-8是Unicode的一种实现方式

在java中实现线程同步的方法

https://blog.csdn/qq_40178533/article/details/119698602
  1. Synchronized + notify + wait
  2. ReentrantLock + Condition
  3. Semaphore/CountDownLatch/CyclicBarrier
  4. AtomicInteger
  5. volatile + 阻塞队列
从java和Os的角度来程序运行的过程

从Java的角度

  1. 编写Java程序,行程.java文件
  2. 再通过编译器编译,生成.class字节码文件
  3. jvm翻译字节码文件,针对不同的操作系统生成不同的机器码,并执行,会找到main方法作为入口执行

从os的角度来看
预编译、编译、汇编、链接、载入
其中预编译、编译把代码转换成汇编码
汇编把汇编码转换成机器码
链接:分为静态链接和动态链接
- 静态链接:把引用的库全部加入可执行文件中,缺点是文件变得很大
- 动态链接:在引用处加一些小的描述,等到真正用的时候再把引入的代码加载入内存。
载入:将可执行文件载入到内存中区

红黑树

说到红黑树不得不说一下
树的结构从
二叉树 -> 二叉搜索树 -> AVL平衡树 ->红黑树
说一下为什么不用AVL树和红黑树
AVL树是严格的平衡,所以每一次插入修改,会频繁的引擎树的结构变化,所以适用于插入修改不多的场景。
而红黑树不那么严格的要求,所以插入修改带来的树的结构变化不太多。所以整体性能更好。

AVL的左旋与红黑树的左旋

补充

volatileto通过内存屏障,在写操作读操作前都会加上内存屏障,保证写操作立刻刷入内存,如果数据被修改读操作能感知

ThreadFactory是一个接口,里面只有一个方法newThread方法。实现了这个工厂类,可以帮助我们自定义一些线程放入线程池。

引用

在java中内存泄漏通常有两种情况。
内存中有不能被使用的数据段
内存中有大量不能被回收,长时间存活的大对象

  1. 强引用:被强引用修饰的对象,在java在GC过程中一定不会被回收,即使内存不够也不会被回收
  2. 软引用:内存不够才会被回收的对象,平时不会被回收。
  3. 若引用:无论内存是否充足,GC的时候会回收这些对象
  4. 虚引用:主要用于跟踪对象的被垃圾回收的状态

JVM何时启动

一般来说当我们执行
java -jar xxx.jar的时候
会先去找到 jvm.cfg文件 在找到jvm.ddl文件,执行,此时就运行了一个jvm实例了

JVM 8-18的一些新特性

  1. lamda表达式,允许我们把一个函数作为参数传递进入
  2. 用::实现方法引用
  3. stream流:一般用来做一些数据的过滤
  4. 引入新的关于时间的api
  5. 函数式接口
  6. 默认方法,default关键字,接口不一定要实现默认方法

JAVA异常体系


Error:这种错误不可捕获一定会导致进程终止,常见的OutOfMemeoryError、StackOverError
Exception:运行时异常和编译时异常

throw:代码中写,抛出一个异常,交由上层处理
throws:方法后面跟上,声明这个方法可能会抛出一个异常

自己实现一个锁系列

使用mutext实现一个读写锁

class ReadWriteLock` {
	Mutext readLock;
	Mutext writeLock;
	volatile int readCnt;
	void readLock(
		readLock.lock();
		readCnt++;
		if(readCnt == 1) {
			writeLock.lock();
		}
	)
	void readUnLock() {
		readLock.unLock();
		radCnt--;
		if(readCnt == 0) {
		writeLock.unlock();
	}
	void wirteLock() {
		writeLock.lock()
	}
	void writeUnlock(){
		writeLock.unLock();
	}
}
java的泛型

泛型可以理解为java引入的一个编译检测机制。
如果没有使用泛型,可能就会在list中加入不同类型的数据,这后续时候可能会有问题。

  1. 为了保护安全性,引入了泛型在编译时做了检查
  2. 除了保证安全,在取用数据的时候也避免了做一个强制转换
  3. 提高了代码的重用性
    泛型可以不指定的类型的比如
    泛型:T表示任意类型、E表示元素等等
  4. 泛型擦除:如果这个类没有界(也就是没有用extends或者super语法指定父类或者界限),那么最后会被编程Object,如果制定了就会被编程指定的界限
java的修饰符
  1. private:作用范围是这个当前类,同一个包下,或者子类不可见
  2. public:所有可见
  3. protect:同包下,或者子类可见,其余不可见
  4. default
String StringBuffer StringBuilder
  • String StringBuffer StringBuilder都是final类,不允许被继承
  • StringBuffer是线程安全的,StringBuilder是线程不安全的。Buffer锁的粒度是这个对象。
  • 因为String是一个不可变对象,所以频繁的增删改效率不高,因此产生了StringBuffer、StringBuilder这个东西。
String str1 = "abc";
String str2 = new String(abc);

第一句话是,查看abc存在不存在,不存在就在堆中生成一个对象,这个对象最后会被放入常量池,然后str1指向这个变量。如果常量池中存在这个对象直接引用。
第二句,不管abc是否在常量池中存在,直接生成一个新的abc放入常量池,并且str2指向这个abc。所以str1 != str2

java的装箱和自动拆箱

java有很多装箱拆箱的类
Integer 与 int等等
而且这个装箱拆箱的会加载自己的缓存。

java的反射

创建一个对象
new
newInstance
clone
反序列化
反射的坏处是会暴露一些类的实现细节,但是反射的作用是帮助java在运行时动态的创建一些对象,并且调用他的方法

反射的一些api

类.class
Class.forName
对象.class

newInstance
getFiled
getMethod
getAnnotation
setAccessable

本文标签: Java