Java面试(基础篇)——解构Java常见的基础面试题 & 结合Java源码分析

目录

  • [fail-safe 和fail-fast机制分别有什么作用?](#fail-safe 和fail-fast机制分别有什么作用?)
  • HashMap
  • 受检异常和非受检异常
  • [为什么阿里巴巴的Java 开发手册不建议使用Java 自带的线程池](#为什么阿里巴巴的Java 开发手册不建议使用Java 自带的线程池)
  • [JDK 动态代理为什么只能代理有接口的类](#JDK 动态代理为什么只能代理有接口的类)
  • Java对象相关的面试题
  • String相关的面试题
    • [new String("abc")到底创建了几个对象](#new String("abc")到底创建了几个对象)
    • [String、StringBuffer、StringBuilder 区别](#String、StringBuffer、StringBuilder 区别)
  • Integer相关的面试题
    • [Integer 和int 的区别?Java 为什么要设计封装类?](#Integer 和int 的区别?Java 为什么要设计封装类?)
    • [Integer 使用不当导致生产的事故](#Integer 使用不当导致生产的事故)
    • [Integer a1 =100 Integer a2 =100 ,a1 ==a2?的运行结果?](#Integer a1 =100 Integer a2 =100 ,a1 ==a2?的运行结果?)
  • [ArrayList 的自动扩容机制](#ArrayList 的自动扩容机制)
  • 强引用、软引用、弱引用、虚引用有什么区别
  • [Java 有几种文件拷贝方式,哪一种效率最高](#Java 有几种文件拷贝方式,哪一种效率最高)
    • [谈谈什么是零拷贝DMA ( Direct Memory Access,直接内存存取)?](#谈谈什么是零拷贝DMA ( Direct Memory Access,直接内存存取)?)
  • 设计模式
    • [在Java 中实现单例模式有哪些方法](#在Java 中实现单例模式有哪些方法)
  • [finally 块一定会执行吗](#finally 块一定会执行吗)
  • [Java SPI 是什么?有什么用?](#Java SPI 是什么?有什么用?)

fail-safe 和fail-fast机制分别有什么作用?

Fail-fast:快速失败

Fail-fast : 表示快速失败,在集合遍历过程中,一旦发现容器中的数据被修改了,会立刻抛出ConcurrentModificationException 异常,从而导致遍历失败

java 复制代码
package com.tianju.test;

import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;

public class DemoTest {

    public static void main(String[] args) {
        ArrayList<Integer> list = new ArrayList<>();
        list.add(1);
        list.add(2);
        Iterator<Integer> iterator = list.iterator();
        while (iterator.hasNext()){
            Integer next = iterator.next();
            list.add(55);
            System.out.println(next);
        }
    }
}

Fail-safe:失败安全

fail-safe:表示失败安全,也就是在这种机制下, 出现集合元素的修改,不会抛出

ConcurrentModificationException。

原因是采用安全失败机制的集合容器,在遍历时不是直接在集合内容上访问的,而是先

复制原有集合内容,在拷贝的集合上进行遍历。由于迭代时是对原集合的拷贝进行遍历,所以在遍历过程中对原集合所作的修改并不能被迭代器检测到。

java 复制代码
package com.tianju.test;

import java.util.ArrayList;
import java.util.Iterator;
import java.util.concurrent.CopyOnWriteArrayList;

public class DemoTest2 {

    public static void main(String[] args) {
        CopyOnWriteArrayList<Integer> list = new CopyOnWriteArrayList<>();
        list.add(1);
        list.add(2);
        Iterator<Integer> iterator = list.iterator();
        while (iterator.hasNext()){
            Integer next = iterator.next();
            list.add(55);
            System.out.println(next);
        }
    }
}

java.util.concurrent 包下的容器都是安全失败的,可以在多线程下并发使用,并发修改。

常见的的使用fail-safe 方式遍历的容器有ConcerrentHashMap 和 CopyOnWriteArrayList 等。

HashMap

hash冲突的问题

散列表Hash table & 散列函数 & 哈希冲突

Hash 算法,就是把任意长度的输入,通过散列算法输出结果是散列值。

在hashMap中,每个关键字被映射到从0到TableSize-1这个范围中的某个数,并且被放到适当的单元中 。这个映射就叫作散列函数(hash function),理想情况下它应该计算起来简单,并且应该保证任何两个不同的关键字映射到不同的单元。不过,这是不可能的,因为单元的数目是有限的,而关键字实际上是用不完的。因此,我们寻找一个散列函数,该函数要在单元之间均匀地分配关键字。

java 复制代码
        public final int hashCode() {
            return Objects.hashCode(key) ^ Objects.hashCode(value);
        }

如何解决hash冲突:

  • (1)开放定址法:

    也称为线性探测法,就是从发生冲突的那个位置开始,按照一定的次序从hash 表中找到一个空闲的位置,然后把发生冲突的元素存入到这个空闲位置中。ThreadLocal 就用到了线性探测法来解决hash 冲突的。

  • (2)链式寻址法:

    这是一种非常常见的方法,简单理解就是把存在hash 冲突的key ,以单向链表的方式来存储,比如HashMap 就是采用链式寻址法来实现的。

  • (3)再hash 法:

    就是当通过某个hash 函数计算的key 存在冲突时,再用另外一个hash 函数对这个key 做hash,一直运算直到不再产生冲突。这种方式会增加计算时间,性能影响较大。

  • (4)建立公共溢出区:

    就是把hash 表分为基本表和溢出表两个部分,在冲突的元素,一律放入到溢出表中。

HashMap怎么解决哈希冲突的?

链式寻址法+红黑树解决hash 冲突

HashMap 在JDK1.8 版本中,通过链式寻址法+红黑树的方式来解决hash 冲突问题,其中红黑树是为了优化Hash 表链表过长导致时间复杂度增加的问题。当链表长度大于8 并且hash 表的容量大于64 的时候,再向链表中添加元素就会触发转化。

当链表长度大于8 并且hash 表的容量大于64 的时候,再向链表中添加元素就会触发转化。

HashMap 中的hash 方法为什么要右移16 位异或

之所以要对hashCode 无符号右移16 位并且异或,核心目的是为了让hash 值的散列度更高,尽可能减少hash 表的hash 冲突,从而提升数据查找的性能。

首先使用key 的hashCode 无符号右移16 位,意味着把hashCode 的高位移动到了低位。

然后再用hashCode 与右移之后的值进行异或运算,就相当于把高位和低位的特征进行和组合。

从而降低了hash 冲突的概率。

受检异常和非受检异常

Java基础(8)------java的异常机制初步 & 异常的捕获和处理 & 自定义异常

受检异常和非受检异常,都是继承自Throwable 这个类中,分别是Error 和Exception,

  • Error 是程序报错,系统收到无法处理的错误消息,它和程序本身无关。
  • Excetpion 是指程序运行时抛出需要处理的异常信息如果不主动捕获,则会被jvm 处理。
  1. 受检异常的定义是程序在编译阶段必须要主动捕获的异常,遇到该异常有两种处理方法
    (1)通过try/catch 捕获该异常;
    (2)通过throw 把异常抛出去;
  2. 非受检异常的定义是程序不需要主动捕获该异常,一般发生在程序运行期间,比如
    NullPointException

受检异常的定义是程序在编译阶段必须要主动捕获的异常,遇到该异常有两种处理方法

为什么阿里巴巴的Java 开发手册不建议使用Java 自带的线程池

了解Java中线程池

Java进阶(5)------创建多线程的方法extends Thread和implements Runnable的对比 & 线程池及常用的线程池

4.【强制】线程池不允许使用Executors去创建,而是通过ThreadPoolExecutor的方式,这

样的处理方式让写的同学更加明确线程池的运行规则,规避资源耗尽的风险。

Executors 里面默认提供的几个线程池是有一些弊端的,如果是不懂多线程、或者是新手直接盲目使用,就可能会造成比较严重的生产事故。

为什么不能用

  • 1.FixedThreadPool 和SingleThreadPool 中,阻塞队列长度是Integer.Max_Value,一旦请求量增加,就会堆积大量请求阻塞在队列中,可能会造成内存溢出的问题;
  • 2.CachedThreadPool 和ScheduledThreadPool 中最大线程数量是Integer.Max_value,一旦请求量增加,导致创建大量的线程,使得处理性能下降。

JDK 动态代理为什么只能代理有接口的类

在Java 里面,动态代理是通过Proxy.newProxyInstance()方法来实现的,它需要传入被动态代理的接口类。

加入如下代码进行运行:

java 复制代码
System.getProperties().put("sun.misc.ProxyGenerator.saveGeneratedFiles", "true");

或者加入下面这句

java 复制代码
System.getProperties().put("jdk.proxy.ProxyGenerator.saveGeneratedFiles", "true");

JDK 动态代理会在程序运行期间动态生成一个代理类$Proxy0,这个动态生成的代理类会继承java.lang.reflect.Proxy 类,同时还会实现被代理类的接口。

在Java 中,是不支持多重继承的,而每个动态代理类都会继承Proxy 类(这也是JDK动态代理的实现规范) ,所以就导致JDK 里面的动态代理只能代理接口,而不能代理实现类。

spring中的代理

Spring进阶(AOP的理解)------静态/动态代理 & 面向切面编程AOP(Aspect Oriented Programming) & 日志记录 & 增强方法

如果一定要针对普通类来做动态代理,可以选择cglib 这个组件,它会动态生成一个被代理类的子类,子类重写了父类中所有非final 修饰的方法,在子类中拦截父类的所有方法调用从而实现动态代理。

Java对象相关的面试题

对象的创建过程

(1)类加载检查

JVM 首先会去检查目标对象是否已经被加载并初始化了。

如果没有,JVM 需要立刻去加载目标类,然后调用目标类的构造器完成初始化。目标类的加载是通过类加载器来实现的,主要就是把一个类加载到内存里面。

然后初始化的过程,主要是对目标类里面的静态变量、成员变量、静态代码块进行初始化。

(2)分配内存空间

当目标类被初始化以后,就可以从常量池里面找到对应的类元信息,并且目标对象的大小在类加载之后就已经确定了,所以这个时候就需要为新创建的对象,根据目标对象的大小在堆内存里面分配内存空间。

内存分配的方式一般有两种,一种指针碰撞,另一种是空闲列表,JVM 会根据Java 堆内存是否规整来决定内存分配方式。

(3)初始化 "零值"

JVM 会把目标对象里面的普通成员变量初始化为零值,比如int 类型初始化为0 ,对象类型初始化为null, (类变量在类加载的准备阶段就已经初始化过了) 。

这一步操作主要是保证对象里面的实例字段,不用初始化就可以直接使用,也就是程序能够获得这些字段对应数据类型的零值。

(3)设置对象头"

然后,JVM 还需要对目标对象的对象头做一些设置,比如对象所属的类元信息、对象的GC 分代年龄、hashcode、锁标记等等。

(4)执行init方法

完成这些步骤以后,对于JVM 来说,新对象的创建工作已经完成。但是对于Java 语言来说,对象创建才算是开始。

接下来要做的,就是执行目标对象内部生成的init 方法,初始化成员变量的值、执行构造块、最后执行目标对象的构造方法,完成对象的创建。

其中,init 方法是Java 文件编译之后在字节码文件中生成的,它是一个实例构造器,这个构造器会把语句块、变量初始化、调用父类构造器等操作组织在一起。

所以调用init方法能够完成一系列的初始化动作。

什么是深拷贝和浅拷贝?

Java进阶(4)------结合类加载JVM的过程理解创建对象的几种方式:new,反射Class,克隆clone(拷贝),序列化反序列化

浅拷贝

深拷贝

深拷贝和浅拷贝是用来描述对象或者对象数组这种引用数据类型的复制场景的。

浅拷贝, 就是只复制某个对象的指针,而不复制对象本身。这种复制方式意味着两个引用指针指向被复制对象的同一块内存地址。

在Java 里面,无论是深拷贝还是浅拷贝,都需要通过实现Cloneable 接口,并实现clone()方法。然后我们可以在clone()方法里面实现浅拷贝或者深拷贝的逻辑。

实现深拷贝的方法有很多,比如

  1. 通过序列化的方式实现,也就是把一个对象先序列化一遍,然后再反序列化回来,就会得到一个完整的新对象。

  2. 在clone()方法里面重写克隆逻辑,也就是对克隆对象内部的引用变量再进行一次克隆。

Book.java实体类

implements Cloneable{ // 可以克隆的

java 复制代码
package com.tianju.auth.reflect;

import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;

@Data
@NoArgsConstructor
@AllArgsConstructor
public class Book implements Cloneable{ // 可以克隆的
    private String title;
    private Author author;
    public double price;

    static {
        System.out.println("book的静态代码块");
    }

    // protected:代表本包或者继承
    // 继承的时候,可以将子类的访问控制符扩大,但不能缩小;
    // 子类不能比父类抛出更多的异常
    @Override
    public Object clone() throws CloneNotSupportedException {
        return super.clone();
    }

    public Book deepClone(){
        Book book = new Book();
        Author au = new Author();
        au.setName(author.getName());
        book.setAuthor(au);
        book.setTitle(this.title);
        book.setPrice(this.price);
        return book;
    }
}

进行测试

java 复制代码
package com.tianju.auth.reflect;

public class TestDemo{
    public static void main(String[] args) throws CloneNotSupportedException {
        Author author = new Author();
        author.setName("吴承恩");
        Book book = new Book("三国演义", author,12.56);
        Book book1 = book;

        System.out.println(book1==book);// == 两个引用是否指向同一个对象

        // clone创建了一个新的对象,只是值一样
        Book bookClone = (Book) book.clone();
        // 深拷贝,创建了新的对象,上面的浅拷贝,只是拷贝了引用
        Book deepClone = book.deepClone();

        System.out.println(bookClone==book);
        System.out.println("克隆前:"+book);
        System.out.println("克隆后:"+bookClone);

        author.setName("小柯基");
        System.out.println("修改后的原对象:"+book);
        System.out.println("修改后的clone对象:"+bookClone);

        // 深拷贝
        System.out.println("***********");
        System.out.println("深拷贝的方法:"+deepClone);
    }
}

String相关的面试题

new String("abc")到底创建了几个对象

  1. 如果abc 这个字符串常量不存在,则创建两个对象,分别是abc 这个字符串常量,以及new String 这个实例对象。
  2. 如果abc 这字符串常量存在,则只会创建一个对象

Java基础(1)------数据类型&包装类,引用类型String&StringBuilder,正则表达式,定点数,日期类

(1)String str = new String("hello");

执行上述代码,底层进行了如下工作,在栈内存里存放变量str,在堆内存新创建一个String的对象,在常量池空间中创建常量hello,如果存在则不创建;创建了一个或两个对象。

(2)String str = "hello";

栈内存中的str指向常量池中的hello

String、StringBuffer、StringBuilder 区别

(1)可变性

String 内部的value 值是final 修饰的,所以它是不可变类。所以每次修改String 的值,都会产生一个新的对象。

StringBuffer 和StringBuilder 是可变类,字符串的变更不会产生新的对象。

StringBuffer 和StringBuilder 是可变类

(2)线程安全性

String 是不可变类,所以它是线程安全的。

StringBuffer 是线程安全的,因为它每个操作方法都加了synchronized 同步关键字。

StringBuilder 不是线程安全的,所以在多线程环境下对字符串进行操作,应该使用StringBuffer ,否则使用StringBuilder

(3)性能方面

String 的性能是最的低的,因为不可变意味着在做字符串拼接和修改的时候,需要重新

创建新的对象以及分配内存。

其次是StringBuffer 要比String 性能高,因为它的可变性使得字符串可以直接被修改,最后是StringBuilder ,它比StringBuffer 的性能高,因为StringBuffer 加了同步锁。

(4)存储方面

String 存储在字符串常量池里面;

StringBuffer 和StringBuilder 存储在堆内存空间。

最后再补充一下, StringBuilder 和StringBuffer 都是派生自AbstractStringBuilder这个抽象类。

Integer相关的面试题

Integer 和int 的区别?Java 为什么要设计封装类?

Integer 是基本数据类型int 的封装类,在Java 里面,有八种基本数据类型,他们都有一一对应的封装类型。

基本类型和封装类型的区别有很多,比如

  • int 类型,我们可以直接定义一个变量名赋值即可,但是Integer 需要使用new 关键字创建对象;
  • 基本类型和Integer 类型混合使用时,Java 会自动通过拆箱和装箱实现类型转换
  • Integer 作为一个对象类型,封装了一些方法和属性,我们可以利用这些方法来操作数据。
  • 作为成员变量, Integer 的默认值是null ,而int 的默认值是0

在Java 里面,之所以要对基础类型设计一个对应的封装类型。是因为Java 本身是一门面向对象的语言,对象是Java 语言的基础单元,我们时时刻刻都在创建对象,也随时都在使用对象,很多时候在传递数据时也需要对象类型,比如像ArrayList、HashMap 这些集合,只能存储对象类型,因此从这个点来说,封装类型存在的意义就很大。

其次,封装类型还有很多好处,比如

  • 安全性较好,可以避免外部操作随意修改成员变量的值,保证了成员变量和数据传递的安全性
  • 隐藏了实现细节,对使用者更加友好,只需要调用对象提供的方法就可以完成对应的操作

Integer 和int 的区别有很多,总结以下3点:

(1)Integer 的初始值是null ,int 的初始值是0

(2)Integer 存储在堆内存,int 类型是直接存储在栈空间

(3)Integer 是对象类型,它封装了很多的方法和属性,我们在使用的时候更加灵活。

至于为什么要设计封装类型,最主要的原因是Java 本身是面向对象的语言,一切操作都是以对象作为基础。比如像集合里面存储的元素,也只支持存储Object 类型,普通类型无法通过集合来存储。

Integer 使用不当导致生产的事故

为什么两个Integer 的对象不能用==号来判断?

Integer 是一个封装类型。它是对应一个int 类型的包装。

在Java 里面之所以要提供Integer 这种基本类型的封装类,是因为Java 是一个面向对象的语言,而基本类型不具备对象的特征,所以在基本类型上做了一层对象的包装并且提供了相关的属性和访问方法来完善基本类型的操作。

在Integer 这个封装类里面,除了基本的int 类型的操作之外,还引入了享元模式的设计,对-128 到127 之间的数据做了一层缓存,也就是说,如果Integer 类型的目标值在-128 到127 之间,就直接从缓存里面获取Integer 这个对象实例并返回,否则创建一个新的Integer 对象。

这么设计的好处是减少频繁创建Integer 对象带来的内存消耗从而提升性能。

因此在这样一个前提下,如果定义两个Integer 对象,并且这两个Integer 的取值范围正好在-128 到127 之间。

如果直接用==号来判断,返回的结果必然是true ,因为这两个Integer 指向的内存地址是同一个。否则,返回的结果是false。

Integer a1 =100 Integer a2 =100 ,a1 ==a2?的运行结果?

按照大家对于Java 基础的认知,两个独立的对象用==进行比较,是比较两个对象的内存地址。那得到的结果必然是false。但是在这个场景中,得到的结果是true。

为什么呢?

首先, Integer a1 =100, 把一个int 数字赋值给一个封装类型,Java 会默认进行装箱操作,也就是调用Integer.valueOf()方法,把数字100 包装成封装类型Integer。

其次,在Integer 内部设计中,用到了享元模式的设计,享元模式的核心思想是通过复用对象,减少对象的创建数量,从而减少内存占用和提升性能。

Integer 内部维护了一个IntegerCache,它缓存了-128 到127 这个区间的数值对应的Integer 类型。

一旦程序调用valueOf 方法,如果数字是在-128 到127 之间就直接在cache 缓存数组中去取Integer 对象;否则,就会创建一个新的对象。

所以,对于这个面试题来说,两个Integer 对象,因为值都是100,并且默认通过装箱机制调用了valueOf 方法。从IntegerCache 中拿到了两个完全相同的Integer 实例。因此用等号比较得到的结果必然是true。

总结,a1 ==a2 的执行结果是true。原因是Integer 内部用到了享元模式的设计,针对-128 到127 之间的数字做了缓存。使用Integer a1 =100 这个方式赋值时,Java 默认会通过valueOf 对100 这个数字进行装箱操作,从而触发了缓存机制,使得a1 和a2 指向了同一个Integer 地址空间。

**Tips:**在工作中直接把两个Integer 封装类型用等号去比较,就有可能导致生产故障。

ArrayList 的自动扩容机制

Java进阶(3)------手动实现ArrayList & 源码的初步理解分析 & 数组插入数据和删除数据的问题

ArrayList 是一个数组结构的存储容器,默认情况下,数组的长度是10.

当然我们也可以在构建ArrayList 对象的时候自己指定初始长度。

随着在程序里面不断的往ArrayList 中添加数据,当添加的数据达到10 个的时候,ArrayList 就没有多余容量可以存储后续的数据。

这个时候ArrayList 会自动触发扩容。

扩容的具体流程很简单,

  1. 首先,创建一个新的数组,这个新数组的长度是原来数组长度的1.5 倍。
  2. 然后使用Arrays.copyOf 方法把老数组里面的数据拷贝到新的数组里面。扩容完成后再把当前要添加的元素加入到新的数组里面,从而完成动态扩容的过程。

强引用、软引用、弱引用、虚引用有什么区别

不同的引用类型,主要体现的是对象不同的可达性状态和对垃圾收集的影响。

(1)强引用

就是普通对象的引用,只要还有强引用指向一个对象,就能表示对象还"活着",垃圾收集器无法回收这一类对象。

只有在没有其他引用关系,或者超过了引用的作用域,再或者显示的把引用赋值为null的时候,垃圾回收器才能进行内存回收。

(2)软引用

是一种相对强引用弱化一些的引用,可以让对象豁免一些垃圾收集,只有当JVM 认为内存不足时,才会去试图回收软引用指向的对象。

软引用通常用来实现内存敏感的缓存,如果还有空闲内存,就可以暂时保留缓存,当内存不足时清理掉,这样就保证了使用缓存的同时,不会耗尽内存。

(3)弱引用

相对强引用而言,它允许在存在引用关联的情况下被垃圾回收的对象在垃圾回收器线程扫描它所管辖的内存区域的过程中,

一旦发现了只具有弱引用的对象,不管当前内存空间足够与否,垃圾回收期都会回收该内存

(4)虚引用

它不会决定对象的生命周期,它提供了一种确保对象被finalize 以后,去做某些事情的机制。

当垃圾回收器准备回收一个对象时,如果发现它还有虚引用,就会在回收对象的内存之前,把这个虚引用加入到与之关联的引用队列中。

程序可以通过判断引用队列中是否已经加入了虚引用,来了解被引用的对象是否将要进行垃圾回收,然后我们就可以在引用的对象的内存回收之前采取必要的行动。

Java 有几种文件拷贝方式,哪一种效率最高

  • 第一种,使用java.io 包下的库,使用FileInputStream 读取,再使用FileOutputStream写出。
  • 第二种,利用java.nio 包下的库,使用transferTo 或transfFrom 方法实现。
  • 第三种,Java 标准类库本身已经提供了Files.copy 的实现。

对于Copy 的效率,这个其实与操作系统和配置等情况相关,在传统的文件IO 操作里面,我们都是调用操作系统提供的底层标准IO 系统调用函数read()、write() ,由于内核指令的调用会使得当前用户线程切换到内核态,然后内核线程负责把相应的文件数据读取到内核的IO 缓冲区,再把数据从内核IO 缓冲区拷贝到进程的私有地址空间中去,这样便完成了一次IO 操作。

而NIO 里面提供的NIO transferTo 和transfFrom 方法,也就是常说的零拷贝实现。

它能够利用现代操作系统底层机制,避免不必要拷贝和上下文切换,因此在性能上表现比较好。

谈谈什么是零拷贝DMA ( Direct Memory Access,直接内存存取)?

零复制(英语:Zero-copy;也译零拷贝)技术是指计算机执行操作时,CPU不需要先将数据从某处内存复制到另一个特定区域。 这种技术通常用于通过网络传输文件时节省CPU周期和内存带宽。

在实际应用中,如果我们需要把磁盘中的某个文件内容发送到远程服务器上,那么它必须要经过几个拷贝的过程。

(1)从磁盘中读取目标文件内容拷贝到内核缓冲区;

(2)CPU 控制器再把内核缓冲区的数据赋值到用户空间的缓冲区中;【浪费】

(3)接着在应用程序中,调用write() 方法,把用户空间缓冲区中的数据拷贝到内核下的Socket Buffer 中;【浪费】

(4)最后,把在内核模式下的SocketBuffer 中的数据赋值到网卡缓冲区( NIC Buffer)网卡缓冲区再把数据传输到目标服务器上;

在这个过程中我们可以发现,数据从磁盘到最终发送出去,要经历4 次拷贝,而在这四次拷贝过程中,有两次拷贝是浪费的,分别是:

  • 从内核空间赋值到用户空间
  • 从用户空间再次复制到内核空间

除此之外,由于用户空间和内核空间的切换会带来CPU 的上线文切换,对于CPU 性能也会造成性能影响。

而零拷贝,就是把这两次多于的拷贝省略掉,应用程序可以直接把磁盘中的数据从内核中直接传输给Socket ,而不需要再经过应用程序所在的用户空间。

(1)零拷贝通过DMA ( Direct Memory Access) 技术把文件内容复制到内核空间中的Read Buffer;

(2)接着把包含数据位置和长度信息的文件描述符加载到Socket Buffer 中,DMA 引擎直接可以把数据从内核空间中传递给网卡设备;

在这个流程中,数据只经历了两次拷贝就发送到了网卡中,并且减少了2 次cpu 的上下文切换,对于效率有非常大的提高

所以,所谓零拷贝,并不是完全没有数据复制,只是相对于用户空间来说,不再需要进行数据拷贝。对于前面说的整个流程来说,零拷贝只是减少了不必要的拷贝次数而已。

在程序中如何实现零拷贝呢?

  • 在Linux 中,零拷贝技术依赖于底层的sendfile()方法实现
  • 在Java 中,FileChannal.transferTo()方法的底层实现就是sendfile()方法
  • 除此之外,还有一个mmap 的文件映射机制

设计模式

大致按照模式的应用目标分类,设计模式可以分为创建型模式、结构型模式和行为型模式。

(1)创建型模式

是对对象创建过程的各种问题和解决方案的总结,包括各种工厂模式、单例模式、构建器模式、原型模式。

(2)结构型模式

是针对软件设计结构的总结,关注于类、对象继承、组合方式的实践经验。

常见的结构型模式,包括桥接模式、适配器模式、装饰者模式、代理模式、组合模式、

外观模式、享元模式等。

(3)行为型模式

行为型模式,是从类或对象之间交互、职责划分等角度总结的模式。

比较常见的行为型模式有策略模式、解释器模式、命令模式、观察者模式、迭代器模式、模板方法模式、访问者模式。

在Java 中实现单例模式有哪些方法

单例模式,就是一个类在任何情况下绝对只有一个实例,并且提供一个全局访问点来获取该实例。

要实现单例,至少需要满足两个点:

  • 私有化构造方法,防止被外部实例化造成多实例问题
  • 提供一个静态方位作为全局访问点来获取唯一的实例对象

(1)第一种,是最简单的实现,通过延迟加载的方式进行实例化,并且增加了同步锁机制避免多线程环境下的线程安全问题。但是这种加锁会造成性能问题,而且同步锁只有在第一次实例化的时候才产生作用,后续不需要。

(2)第二种改进方案,通过双重检查锁的方式,减少了锁的范围来提升性能

(3)第三种,通过饿汉式实现单例。

这种方式在类加载的时候就触发了实例化,从而避免了多线程同步问题。

还有一种与这个方式类似的实现,通过在静态块里面实例化,而静态块是在类加载的时候触发执行的,所以也只会执行一次。

java 复制代码
package com.tianju.test;

public class Singleton {
    private static Singleton instance = null;

    static {
        instance = new Singleton();
    }
    private Singleton(){}
    public static Singleton getInstance(){
        return instance;
    }
}

上面两种方式,都是在类加载的时候初始化,没有达到延迟加载的效果,当然本身影响不大,但是其实还是可以更进一步优化,就是可以在使用的时候去触发初始化(如图) 。

像这种写法,把INSTANCE 写在一个静态内部类里面,

java 复制代码
package com.tianju.demo;

public class Singleton {
    private static class SingletonHolder{
        private static final Singleton INSTANCE = new Singleton();
    }
    private Singleton(){}
    public static final Singleton getInstance(){
        return SingletonHolder.INSTANCE;
    }
}

(4)使用枚举类来实现

既能避免多线程同步问题,又能防止反序列化重新创建新对象,也是一个比较好的方案

第一种是通过双重检查锁的方式,它是一种线程安全并且是延迟实例化的方式,但是因为加锁,所以会有性能上的影响。

第二种是通过静态内部类的方式实现,它也是一种延迟实例化,由于它是静态内部类,所以只会使用的时候加载一次,不存在线程安全问题。

第三种是通过枚举类的方式实现,它既是线程安全的,又能防止反序列化导致破坏单例问题。

但是,多线程、克隆、反序列化、反射,都有可能会造成单例的破坏,通过枚举的方式实现单例,是能够解决所有可能被破坏的情况。

finally 块一定会执行吗

finally 语句块在实际开发中使用得非常多,它是和try 语句块组合使用通常情况下,不管有没有触发异常,finally 语句块中的代码是必然会执行的所以我们会把资源的释放、或者业务日志的打印放在finally 语句块里面。

finally 语句块在两种情况下不会执行:

  • 程序没有进入到try 语句块因为异常导致程序终止,这个问题主要是开发人员在编写代码的时候,异常捕获的范围不够。
  • 在try 或者cache 语句块中,执行了System.exit(0)语句,导致JVM 直接退出

Java SPI 是什么?有什么用?

相关推荐
Prejudices2 分钟前
C++如何调用Python脚本
开发语言·c++·python
Daniel 大东3 分钟前
idea 解决缓存损坏问题
java·缓存·intellij-idea
wind瑞9 分钟前
IntelliJ IDEA插件开发-代码补全插件入门开发
java·ide·intellij-idea
HappyAcmen9 分钟前
IDEA部署AI代写插件
java·人工智能·intellij-idea
马剑威(威哥爱编程)15 分钟前
读写锁分离设计模式详解
java·设计模式·java-ee
我狠狠地刷刷刷刷刷15 分钟前
中文分词模拟器
开发语言·python·算法
鸽鸽程序猿16 分钟前
【算法】【优选算法】前缀和(上)
java·算法·前缀和
修道-032316 分钟前
【JAVA】二、设计模式之策略模式
java·设计模式·策略模式
wyh要好好学习19 分钟前
C# WPF 记录DataGrid的表头顺序,下次打开界面时应用到表格中
开发语言·c#·wpf
AitTech19 分钟前
C#实现:电脑系统信息的全面获取与监控
开发语言·c#