Java教程

面试-Java基础篇(一)

本文主要是介绍面试-Java基础篇(一),对大家解决编程问题具有一定的参考价值,需要的程序猿们随着小编来一起学习吧!

技术面试中的几个注意点:

  1. 面试时,你熟悉的问题要和面试官多聊,不要为了回答问题而回答问题
  2. 一个问题的沟通时间最好能多聊一会儿,简单问题说3/5分钟,如果问题的规模比较大,10分钟左右也是可以的
  3. 回答问题时不要为了凑时间而凑时间,聊的内容一定要和问的问题相关,知识点可以连续的引入
  4. 了解的东西多聊,不了解的少说
  5. 对于知识可以有一些自己的见解,自己的想法,清晰表述出来,虽然自己的看法有时候不会特别的恰当

请聊一下java的集合类,以及在实际项目中你是如何用的?

注意说出集合体系,常用类、接口、实现类。

加上你所知道的高并发集合类,JUC ,参照集合增强内容,在实际项目中引用,照实说就好了

Hashmap为什么要使用红黑树?

在jdk1.8版本后,java对HashMap做了改进,在链表长度大于8的时候,将后面的数据存在红黑树中,以加快检索速度 。

红黑树虽然本质上是一棵二叉查找树,但它在二叉查找树的基础上增加了着色和相关的性质。

使得红黑树相对平衡,从而保证了红黑树的查找、插入、删除的时间复杂度最坏为O(log n)。加快检索速率。

集合类是怎么解决高并发中的问题?

思路:先说一下那些是非安全,普通的安全的集合类,JUC中高并发的集合类

线程非安全的集合类 ArrayList、LinkedList、HashSet、TreeSet、HashMap、TreeMap, 实际开发中我们自己用这样的集合最多,因为一般我们自己写的业务代码中,不太涉及到多线程共享同一个集合的问题。

线程安全的集合类 Vector HashTable 虽然效率没有JUC中的高性能集合高,但是也能够适应大部分环境。

高性能线程安全的集合类:

  • ConcurrentHashMap
  • ConcurrentHashMap和HashTable的区别
  • ConcurrentHashMap线程安全的具体实现方式/底层具体实现
  • 说说CopyOnWriteArrayList
ConcurrentHashMap

java5.0在juc包中提供了大量支持并发的容器类,采用“锁分段”机制,Concurrentlevel分段级别,默认16,就是有16个段(segment),每个段默认又有16个哈希表(table),每个又有链表连着。

image-20220118201457432

在JDK1.7的时候,ConcurrentHashMap(分段锁) 对整个桶数组进行了分割分段(Segment),每一把锁只锁容器其中一部分数据,多线程访问容器里不同数据段的数据,就不会存在锁竞争。

JDK1.8 ConcurrentHashMap取消了Segment分段锁,采用CAS和synchronized来保证并发安全。数据结构跟HashMap1.8的结构类似,数组+链表/红黑二叉树。Java 8在链表长度超过一定阈值(8)时将链表(寻址时间复杂度为O(N))转换为红黑树(寻址时间复杂度为O(log(N)))。

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

ConcurrentSkipListMap

是线程安全的有序的哈希表(相当于线程安全的TreeMap); 继承于AbstractMap类,并且实现ConcurrentNavigableMap接口。ConcurrentSkipListMap是通过“跳表”来实现的。

ConcurrentSkipListSet

是线程安全的有序的集合(相当于线程安全的TreeSet);它继承于AbstractSet,并实现了NavigableSet接口。ConcurrentSkipListSet是通过ConcurrentSkipListMap实现的,它也支持并发。

CopyOnWriteArraySet addIfAbsent 和 CopyOnWriteArrayList

CopyOnWriteArraySet addIfAbsent 和 CopyOnWriteArrayList(写入并复制)也是juc里面的,它解决了并发修改异常,每当有写入的时候,就在底层重新复制一个新容器写入,最后把新容器的引用地址赋给旧的容器,在别人写入的时候,其他线程读数据,依然是旧容器的线程。这样是开销很大的,所以不适合频繁写入的操作。适合并发迭代操作多的场景。只能保证数据的最终一致性。

简述一下自定义异常的应用场景?

借助异常机制,我们可以省略很多业务逻辑上的判断处理,直接借助java的异常机制可以简化业务逻辑判断代码的编写。

  1. 当你不想把你的错误直接暴露给前端或者你想让前端从业务角度判断后台的异常,这个时候自定义异常类是你的不二选择
  2. 虽然Java给我们提供了丰富的异常类型,但是在实际的业务上,还有很多情况Java提供的异常类型不能准确的表述出我们业务上的含义
  3. 控制项目的后期服务 … … (挖坑)

描述一下Object类中常用的方法?

toString hashCode equals clone finalized wait notify notifyAll … …

解释每个方法的作用

toString:定义一个对象的字符串表现形式,Object类中定义的规则是:类的全路径名+@+对象的哈希码,重写之后,我们可以自行决定返回的字符串中包含对象的那些属性信息 …

clone:返回一个对象的副本,深克隆,浅克隆,原型模式,重写时实现Cloneable。(clone比直接new效率高)

finalized:GC会调动该方法,自救

1.8的新特性有了解过吗? (注意了解其他版本新特征) +JDK更新认识

重点:jdk9、jdk11、jdk15(其它也需要了解)

  • Lambda表达式
  • 函数式编程
  • 方法引用和构造器调用
  • Stream API
  • 接口中的默认方法和静态方法
  • 新时间日期API

新的日期类

属性含义
Instant代表的是时间戳
LocalDate代表日期,比如2020-01-14
LocalTime代表时刻,比如12:59:59
LocalDateTime代表具体时间 2020-01-12 12:22:26
ZonedDateTime代表一个包含时区的完整的日期时间,偏移量是以UTC/ 格林威治时间为基准的
Period代表时间段
ZoneOffset代表时区偏移量,比如:+8:00
Clock代表时钟,比如获取目前美国纽约的时间
接口的默认方法

Java 8允许我们给接口添加一个非抽象的方法实现,只需要使用 default关键字即可,这个特征又叫做扩展方法,示例如下:

代码如下:

interface Formula { 
    double calculate(int a);
	default double sqrt(int a) { 
        return Math.sqrt(a); 
    } 
}

Formula接口在拥有calculate方法之外同时还定义了sqrt方法,实现了Formula接口的子类只需要实现一个calculate方法,默认方法sqrt将在子类上可以直接使用。

代码如下:

Formula formula = new Formula() { 
    @Override 
    public double calculate(int a) { 
        return sqrt(a * 100); 
    } 
};
formula.calculate(100); // 100.0 formula.sqrt(16); // 4.0

文中的formula被实现为一个匿名类的实例,该代码非常容易理解,6行代码实现了计算 sqrt(a * 100)。在下一节中,我们将会看到实现单方法接口的更简单的做法。

在Java中只有单继承,如果要让一个类赋予新的特性,通常是使用接口来实现,在C++中支持多继承,允许一个子类同时具有多个父类的接口与功能,在其他语言中,让一个类同时具有其他的可复用代码的方法叫做mixin。新的Java 8 的这个特性在编译器实现的角度上来说更加接近Scala的trait。 在C#中也有名为扩展方法的概念,允许给已存在的类型扩展方法,和Java 8的这个在语义上有差别。

Lambda 表达式

首先看看在老版本的Java中是如何排列字符串的,代码如下:

List<String> names = Arrays.asList("peterF", "anna", "mike", "xenia");
Collections.sort(names, new Comparator<String>() { 
    @Override 
    public int compare(String a, String b) { 
        return b.compareTo(a); 
    } 
});

只需要给静态方法 Collections.sort 传入一个List对象以及一个比较器来按指定顺序排列。通常做法都是创建一个匿名的比较器对象然后将其传递给sort方法。

在Java 8 中你就没必要使用这种传统的匿名对象的方式了,Java 8提供了更简洁的语法,lambda表达式,代码如下:

Collections.sort(names, (String a, String b) -> { 
    return b.compareTo(a); 
});

看到了吧,代码变得更段且更具有可读性,但是实际上还可以写得更短,代码如下:

Collections.sort(names, (String a, String b) -> b.compareTo(a));

对于函数体只有一行代码的,你可以去掉大括号{}以及return关键字,但是你还可以写得更短点:

Collections.sort(names, (a, b) -> b.compareTo(a));

Java编译器可以自动推导出参数类型,所以你可以不用再写一次类型。

函数式接口

Lambda表达式是如何在java的类型系统中表示的呢?每一个lambda表达式都对应一个类型,通常是接口类型。而“函数式接口”是指仅仅只包含一个抽象方法的接口,每一个该类型的lambda表达式都会被匹配到这个抽象方法。因为默认方法不算抽象方法,所以你也可以给你的函数式接口添加默认方法

的接口,每一个该类型的lambda表达式都会被匹配到这个抽象方法。因为 默认方法 不算抽象方法,所以你也可以给你的函数式接口添加默认方法。

将lambda表达式映射到一个单方法的接口上,这种做法在Java 8之前就有别的语言实现,比如Rhino JavaScript解释器,如果一个函数参数接收一个单方法的接口而你传递的是一个function,Rhino 解释器会自动做一个单接口的实例到function的适配器,典型的应用场景有 org.w3c.dom.events.EventTarget 的addEventListener 第二个参数 EventListener。

方法与构造函数引用

简述一下Java面向对象的基本特征,继承、封装与多态,以及你自己的应用?

注意单独解释 、继承 、封装 、多态的概念。

继承, 基本概念解释,后面多态的条件。

封装, 基本概念解释, 隐藏实现细节,公开使用方式

多态, 基本概念解释, 就是处理参数,提接口,打破单继承

设计模式 设计原则…

Java中重写和重载的区别?

联系: 名字相似,都是多个同名方法

重载:在同一个类之中发生的

重写:继承中,子类重写父类方法

目的差别

  • 重载:容易记忆方法(功能相似或相同的方法用相同的名字)
  • 重写:在父类的基础上实现更多功能

语法差别

  • 重载:方法名相同,参数列表不一样
  • 重写:

怎样声明一个类不会被继承,什么场景下会用?

final修饰的类不能有子类 ,大部分都是出于安全考虑(String举例)

Java中的自增是线程安全的吗,如何实现线程安全的自增?

  • 增加synchronized进行线程同步

  • 使用lock、unlock处理Reetrantent 锁进行锁定

  • AtomicInteger >>> Unsafe >>> cas >>> aba

    首先说明,此处 AtomicInteger,一个提供原子操作的 Integer 的类,常见的还有AtomicBoolean、AtomicInteger、AtomicLong、AtomicReference 等,他们的实现原理相同,区别在与运算对象类型的不同。令人兴奋地,还可以通过 AtomicReference将一个对象的所有操作转化成原子操作。

    我们知道,在多线程程序中,诸如++i 或 i++等运算不具有原子性,是不安全的线程操作之一。通常我们会使用 synchronized 将该操作变成一个原子操作,但 JVM 为此类操作特意提供了一些同步类,使得使用更方便,且使程序运行效率变得更高。通过相关资料显示,通常AtomicInteger 的性能是 ReentantLock 的好几倍。

Jdk1.8中的stream有用过吗,详述一下stream的并行操作原理?stream并行的线程池是从哪里来的?

Stream作为Java 8的一大亮点,它与java.io包里的InputStream和OutputStream是完全不同的概念。它是对容器对象功能的增强,它专注于对容器对象进行各种非常便利、高效的聚合操作或者大批量数据操作。

Stream API借助于同样新出现的Lambda表达式,极大的提高编程效率和程序可读性。同时,它提供串行和并行两种模式进行汇聚操作,并发模式能够充分利用多核处理器的优势,使用fork/join并行方式来拆分任务和加速处理过程。所以说,Java8中首次出现的 java.util.stream是一个函数式语言+多核时代综合影响的产物。

Stream有如下三个操作步骤:

  1. 创建Stream:从一个数据源,如集合、数组中获取流。
  2. 中间操作:一个操作的中间链,对数据源的数据进行操作。
  3. 终止操作:一个终止操作,执行中间操作链,并产生结果。

image-20220118212159347

image-20220118212216225

当数据源中的数据上了流水线后,这个过程对数据进行的所有操作都称为“中间操作”。中间操作仍然会返回一个流对象,因此多个中间操作可以串连起来形成一个流水线。比如map (mapToInt, flatMap 等)、filter、distinct、sorted、peek、limit、skip、parallel、sequential、unordered。

当所有的中间操作完成后,若要将数据从流水线上拿下来,则需要执行终止操作。终止操作将返回一个执行结果,这就是你想要的数据。比如:forEach、forEachOrdered、toArray、reduce、collect、min、max、count、anyMatch、allMatch、noneMatch、findFirst、findAny、iterator。

多个中间操作可以连接起来形成一个流水线,除非流水线上触发终止操作,否则中间操作不会执行任何处理!而在终止操作时一次性全部处理,称作“惰性求值”。

stream并行原理: 其实本质上就是在ForkJoin上进行了一层封装,将Stream 不断尝试分解成更小的split,然后使用fork/join 框架分而治之, parallize使用了默认的ForkJoinPool.common 默认的一个静态线程池。

什么是ForkJoin框架? 适用场景?

虽然目前处理器核心数已经发展到很大数目,但是按任务并发处理并不能完全充分的利用处理器资源,因为一般的应用程序没有那么多的并发处理任务。基于这种现状,考虑把一个任务拆分成多个单元,每个单元分别得到执行,最后合并每个单元的结果。

Fork/Join框架是JAVA7提供的一个用于并行执行任务的框架,是一个把大任务分割成若干小任务,最终汇总每个小任务结果得到大任务结果的框架。

image-20220118212550499

image-20220118212555946

工作窃取算法(work-stealing)

一个大任务拆分成多个小任务,为了减少线程间的竞争,把这些子任务分别放到不同的队列中,并且每个队列都有单独的线程来执行队列里的任务,线程和队列一一对应。

但是会出现这样一种情况:A线程处理完了自己队列的任务,B线程的队列里还有很多任务要处理。

A是一个很热情的线程,想过去帮忙,但是如果两个线程访问同一个队列,会产生竞争,所以A想了一个办法,从双端队列的尾部拿任务执行。而B线程永远是从双端队列的头部拿任务执行。

image-20220118213230346

注意:线程池中的每个线程都有自己的工作队列(PS,这一点和ThreadPoolExecutor不同,ThreadPoolExecutor是所有线程公用一个工作队列,所有线程都从这个工作队列中取任务),当自己队列中的任务都完成以后,会从其它线程的工作队列中偷一个任务执行,这样可以充分利用资源。

工作窃取算法的优点:利用了线程进行并行计算,减少了线程间的竞争。

工作窃取算法的缺点:任务争夺问题(最后一个任务会“打架”)

Java种的代理有几种实现方式?

动态代理

JDK >>> Proxy

  1. 面向接口的动态代理 代理一个对象去增强面向某个接口中定义的方法
  2. 没有接口不可用
  3. 只能读取到接口上的一些注解

MyBatis

DeptMapper dm = sqlSession.getMapper(DeptMapper.class)

第三方 CGlib

  1. 面向父类的动态代理
  2. 有没有接口都可以使用
  3. 可以读取类上的注解

AOP 日志 性能检测 事务

MyBatis 源码 spring源码

asm字节码操作框架

equals()和==区别?为什么重写equal要重写hashcode?

== 是运算符 equals来自于Object类定义的一个方法

== 可以用于基本数据类型和引用类型

equals只能用于引用类型

== 两端如果是基本数据类型,就是判断值是否相同

equals在重写之后,判断两个对象的属性值是否相同

equals如果不重写,其实就是 ==

有人说,== 判断栈中的值是否相同,equals判断堆中的值是否相同…(有一定的道理)

重写equals可以让我们自己定义判断两个对象是否相同的条件。

Object中定义的hashcode方法生成的哈希码能保证同一个类的对象的哈希码一定是不同的

当equals 返回为true,我们在逻辑上可以认为是同一个对象,但是查看哈希码,发现哈希码不同,和equals方法的返回结果违背,所以一般会重写hashcode 方法。

Object中定义的hashcode方法生成的哈希码跟对象的本身属性值是无关的,重写hashcode之后,我们可以自定义哈希码的生成规则,可以通过对象的属性值计算出哈希码。

HashMap中,借助equals和hashcode方法来完成数据的存储。

hashmap在1.8中做了哪些优化?

数据结构

在Java1.7中,HashMap的数据结构为数组+单向链表。Java1.8中变成了数组+单向链表+红黑树。

链表插入节点的方式:在Java1.7中,插入链表节点使用头插法。Java1.8中变成了尾插法**。**

hash函数

Java1.8的hash()中,将hash值高位(前16位)参与到取模的运算中,使得计算结果的不确定性增强,降低发生哈希碰撞的概率。

扩容优化

扩容以后,1.7对元素进行rehash算法,计算原来每个元素在扩容之后的哈希表中的位置,1.8借助2倍扩容机制,元素不需要进行重新计算位置。

JDK 1.8 在扩容时并没有像 JDK 1.7 那样,重新计算每个元素的哈希值,而是通过高位运算(e.hash & oldCap)来确定元素是否需要移动,比如 key1 的信息如下:

image-20220119120927896

使用 e.hash & oldCap 得到的结果,高一位为 0,当结果为 0 时表示元素在扩容时位置不会发生任何变化,而 key 2 信息如下:

image-20220119120942238

img

image-20220119120947399

高一位为 1,当结果为 1 时,表示元素在扩容时位置发生了变化,新的下标位置等于原下标位置 + 原数组长度hashmap,不必像1.7一样全部重新计算位置。

hashmap线程安全的方式?

HashMap不是线程安全的,往往在写程序时需要通过一些方法来回避.其实JDK原生的提供了2种方法让HashMap支持线程安全。

方法一:通过Collections.synchronizedMap()返回一个新的Map,这个新的map就是线程安全的. 这个要求大家习惯基于接口编程,因为返回的并不是HashMap,而是一个Map的实现。

方法二:重新改写了HashMap,具体的可以查看java.util.concurrent.ConcurrentHashMap. 这个方法比方法一有了很大的改进。

下面对这2中实现方法从各个角度进行分析和比较:

方法一特点:通过Collections.synchronizedMap()来封装所有不安全的HashMap的方法,就连toString, hashCode都进行了封装。 封装的关键点有2处:

  • 使用了经典的synchronized来进行互斥
  • 使用了代理模式new了一个新的类,这个类同样实现了Map接口。在Hashmap上面,synchronized锁住的是对象,所以第一个申请的得到锁,其他线程将进入阻塞,等待唤醒。

优点:代码实现十分简单,一看就懂

缺点:从锁的角度来看,方法一直接使用了锁住方法,基本上是锁住了尽可能大的代码块.性能会比较差。

方法二特点:重新写了HashMap,比较大的改变有如下几点。使用了新的锁机制,把HashMap进行了拆分,拆分成了多个独立的块,这样在高并发的情况下减少了锁冲突的可能,使用的是NonfairSync。这个特性调用CAS指令来确保原子性与互斥性。当如果多个线程恰好操作到同一个segment上面,那么只会有一个线程得到运行。

  • 优点: 需要互斥的代码段比较少,性能会比较好. ConcurrentHashMap把整个Map切分成了多个块,发生锁碰撞的几率大大降低,性能会比较好。
  • 缺点: 代码繁琐
为什么hashmap扩容的时候是两倍?

查看源代码,在存入元素时,放入元素位置有一个 (n-1)&hash 的一个算法,和hash&(newCap-1),这里用到了一个&位运算符

image-20220119132738879

image-20220119132750207

当HashMap的容量是16时,它的二进制是10000,(n-1)的二进制是01111,与hash值得计算结果如下

image-20220119132801287

下面就来看一下HashMap的容量不是2的n次幂的情况,当容量为10时,二进制为01010,(n-1)的二进制是01001,向里面添加同样的元素,结果为

image-20220119133015194

可以看出,有三个不同的元素进过&运算得出了同样的结果,严重的hash碰撞了

解决hash冲突的方式有哪些?
  1. 开放定址法

    所谓的开放定址法就是一旦发生了冲突,就去寻找下一个空的散列地址(以当前位置为基准),只要散列表足够大,空的散列地址总能找到,并将记录存入

  2. 再哈希法

    再哈希法又叫双哈希法,有多个不同的Hash函数,当发生冲突时,使用第二个,第三个,….,等哈希函数计算地址,直到无冲突。虽然不易发生聚集,但是增加了计算时间。

  3. 链地址法

    链地址法的基本思想是:每个哈希表节点都有一个next指针,多个哈希表节点可以用next指针构成一个单向链表,被分配到同一个索引上的多个节点可以用这个单向 链表连接起来

  4. 建立公共溢出区

    这种方法的基本思想是:将哈希表分为基本表和溢出表两部分,凡是和基本表发生冲突的元素,一律填入溢出表

Tomcat为什么要重写类加载器?

  • 实现隔离性:如果使用默认的类加载器机制,那么是无法加载两个相同类库的不同版本的,默认的类加器是不管你是什么版本的,只在乎你的全限定类名,并且只有一份。一个web容器可能要部署两个或者多个应用程序,不同的应用程序,可能会依赖同一个第三方类库的不同版本,因此要保证每一个应用程序的类库都是独立、相互隔离的。部署在同一个web容器中的相同类库的相同版本可以共享,否则,会有重复的类库被加载进JVM, web容器也有自己的类库,不能和应用程序的类库混淆,需要相互隔离

  • 实现热替换:jsp 文件其实也就是class文件,那么如果修改了,但类名还是一样,类加载器会直接取方法区中已经存在的,修改后的jsp是不会重新加载的。

打破双亲委派机制(参照JVM中的内容)OSGI是基于Java语言的动态模块化规范,类加载器之间是网状结构,更加灵活,但是也更复杂,JNDI服务,使用线程上线文类加载器,父类加载器去使用子类加载器

image-20220119133718258

tomcat自己定义的类加载器:

  • CommonClassLoader:tomcat最基本的类加载器,加载路径中的class可以被tomcat和各个webapp访问

  • CatalinaClassLoader:tomcat私有的类加载器,webapp不能访问其加载路径下的class,即对webapp不可见

  • SharedClassLoader:各个webapp共享的类加载器,对tomcat不可见

  • WebappClassLoader:webapp私有的类加载器,只对当前webapp可见

    每一个web应用程序对应一个WebappClassLoader,每一个jsp文件对应一个JspClassLoader,所以这两个类加载器有多个实例

工作原理:

  1. CommonClassLoader能加载的类都可以被CatalinaClassLoader和SharedClassLoader使用,从而实现了公有类库的共用

  2. CatalinaClassLoader和SharedClassLoader自己能加载的类则与对方相互隔离

  3. WebAppClassLoader可以使用SharedClassLoader加载到的类,但各个WebAppClassLoader实例之间相互隔离,多个WebAppClassLoader是同级关系

  4. 而JasperLoader的加载范围仅仅是这个JSP文件所编译出来的那一个.Class文件,它出现的目的就是为了被丢弃:当Web容器检测到JSP文件被修改时,会替换掉目前的JasperLoader的实例,并通过再建立一个新的Jsp类加载器来实现JSP文件的HotSwap功能

  5. tomcat目录结构,与上面的类加载器对应

/common/*
/server/*
/shared/*
/WEB-INF/*
  1. 默认情况下,conf目录下的catalina.properties文件,没有指定server.loader以及shared.loader,所以tomcat没有建立CatalinaClassLoader和SharedClassLoader的实例,这两个都会使用CommonClassLoader来代替。Tomcat6之后,把common、shared、server目录合成了一个lib目录。所以在我们的服务器里看不到common、shared、server目录。

简述一下Java运行时数据区?

image-20220119134319688

Java虚拟机栈

与程序计数器一样,Java 虚拟机栈(Java Virtual Machine Stacks)也是线程私有的,它的生命周期与线程相同。

虚拟机栈描述的是 Java 方法执行的内存模型:每个方法在执行的同时都会创建一个栈帧(Stack Frame,是方法运行时的基础数据结构)

程序计数器

程序计数器(Program Counter Register)是一块较小的内存空间,它可以看作是当前线程所执行的字节码的行号指示器。

由于 Java 虚拟机的多线程是通过线程轮流切换并分配处理器执行时间的方式来实现的,在任何一个确定的时刻,一个处理器内核都只会执行一条线程中的指令。

本地方法栈

本地方法栈(Native Method Stack)与虚拟机栈所发挥的作用是非常相似的,它们之间的区别不过是虚拟机栈为虚拟机执行 Java 方法(也就是字节码)服务,而本地方法栈则为虚拟机使用到的 Native 方法服务。Sun HotSpot 虚拟机直接就把本地方法栈和虚拟机栈合二为一。与虚拟机栈一样,本地方法栈区域也会抛出 StackOverflowError 和 OutOfMemoryError 异常。

Java堆

对于大多数应用来说,Java 堆(Java Heap)是 Java 虚拟机所管理的内存中最大的一块。Java 堆是被所有线程共享的一块内存区域,在虚拟机启动时创建。此内存区域的唯一目的就是存放对象实例,几乎所有的对象实例都在这里分配内存。

方法区

方法区(Method Area)与 Java 堆一样,是各个线程共享的内存区域,它用于存储已被虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码等数据。

说一下反射,反射会影响性能吗?

JAVA反射机制是在运行状态中,对于任意一个类,都能够知道这个类的所有属性和方法;对于任意一个对象,都能够调用它的任意一个方法和属性;这种动态获取的信息以及动态调用对象的方法的功能称为java语言的反射机制。反射这种运行时动态的功能可以说是非常重要的,可以说无反射不框架!!!,反射方式实例化对象和,属性赋值和调用方法肯定比直接的慢,但是程序运行的快慢原因有很多,不能主要归于反射,如果你只是偶尔调用一下反射,反射的影响可以忽略不计,如果你需要大量调用反射,会产生一些影响,适当考虑减少使用或者使用缓存,你的编程的思想才是限制你程序性能的最主要的因素。

hashmap为什么用红黑树不用普通的AVL树?

image-20220119134610259

AVL树

一般用平衡因子判断是否平衡并通过旋转来实现平衡,左右子树树高不超过1,和红黑树相比,AVL树是高度平衡的二叉树,平衡条件必须满足(所有节点的左右子树高度差不超过1)。不管我们是执行插入还是删除操作,只要不满足上面的条件,就要通过旋转来保持平衡,而的由于旋转比较耗时,由此我们可以知道AVL树适合用于插入与删除次数比较少,但查找多的情况。

在计算机科学中,AVL是最先发明的自平衡二叉查找树。在AVL树中任何节点的两个子树的高度最大差别为1,所以它也被称为高度平衡树。增加和删除可能需要通过一次或多次树旋转来重新平衡这个树。

image-20220119134809360

红黑树

也是一种平衡二叉树,但每个节点有一个存储位表示节点的颜色,可以是红或黑。通过对任何一条从根到叶子的路径上各个节点着色的方式的限制,红黑树确保没有一条路径会比其它路径长出两倍,因此,红黑树是一种弱平衡二叉树,红黑树从根到叶子的最长路径不会超过最短路径的2倍(由于是弱平衡,可以看到,在相同的节点情况下,AVL树的高度<=红黑树),相对于要求严格的AVL树来说,它的旋转次数少,所以对于搜索,插入,删除操作较多的情况下,用红黑树。

image-20220119134849487

sleep 与 wait 区别

  1. 对于 sleep()方法,我们首先要知道该方法是属于 Thread 类中的。而 wait()方法,则是属于 Object 类中的。

  2. sleep()方法导致了程序暂停执行指定的时间,让出 cpu,但是他的监控状态依然保持者,当指定的时间到了又会自动恢复运行状态。在调用 sleep()方法的过程中,线程不会释放对象锁。

  3. 而当调用 wait()方法的时候,线程会放弃对象锁,进入等待此对象的等待锁定池,只有针对此对象调用 notify()方法后本线程才进入对象锁定池准备获取对象锁进入运行状态。

  4. sleep用Thread调用,在非同步状态下就可以调用, wait用同步监视器调用,必须在同名代码中调用

synchronized 和 ReentrantLock 的区别

两者的共同点:

  1. 都是用来协调多线程对共享对象、变量的访问

  2. 都是可重入锁,同一线程可以多次获得同一个锁

  3. 都保证了可见性和互斥性

两者的不同点:

  1. ReentrantLock 显示的获得、释放锁,synchronized 隐式获得释放锁

  2. ReentrantLock 可响应中断、可轮回,synchronized 是不可以响应中断的,为处理锁的不可用性提供了更高的灵活性

  3. ReentrantLock 是 API 级别的,synchronized 是 JVM 级别的

  4. ReentrantLock 可以实现公平锁

  5. ReentrantLock 通过 Condition 可以绑定多个条件

  6. 底层实现不一样, synchronized 是同步阻塞,使用的是悲观并发策略,lock 是同步非阻塞,采用的是乐观并发策略

  7. Lock 是一个接口,而 synchronized 是 Java 中的关键字,synchronized 是内置的语言实现。

  8. synchronized 在发生异常时,会自动释放线程占有的锁,因此不会导致死锁现象发生;而 Lock 在发生异常时,如果没有主动通过 unLock()去释放锁,则很可能造成死锁现象,因此使用 Lock 时需要在 finally 块中释放锁。

  9. Lock 可以让等待锁的线程响应中断,而 synchronized 却不行,使用 synchronized 时,等待的线程会一直等待下去,不能够响应中断。

  10. 通过 Lock 可以知道有没有成功获取锁,而 synchronized 却无法办到。

  11. Lock 可以提高多个线程进行读操作的效率,既就是实现读写锁等。多个读取线程使用共享锁,写线程使用排它锁/独占

Condition 类和Object 类锁方法区别

  1. Condition 类的 awiat 方法和 Object 类的 wait 方法等效

  2. Condition 类的 signal 方法和 Object 类的 notify 方法等效

  3. Condition 类的 signalAll 方法和 Object 类的 notifyAll 方法等效

  4. ReentrantLock 类可以唤醒指定条件的线程,而 object 的唤醒是随机的

tryLock和Lock和lockInterruptibly 的区别

  1. tryLock 能获得锁就返回 true,不能就立即返回 false,tryLock(long timeout,TimeUnit unit),可以增加时间限制,如果超过该时间段还没获得锁,返回 false

  2. lock 能获得锁就返回 true,不能的话一直等待获得锁

  3. lock 和 lockInterruptibly,如果两个线程分别执行这两个方法,但此时中断这两个线程, lock 不会抛出异常,而 lockInterruptibly 会抛出异常。

单例模式有哪些实现方式,有什么优缺点

个教室里面有很多同学,每个同学都要有自己的一个水杯.教室里还有一个饮水机,一个饮水机可以为教室内所有的同学提供用水,没有必要每个同学都准备一个饮水机.程序中往往一个类只需要一个对象就可以为整个系统服务,如果产生多个对象,消耗更多的资源.单例模式就是为了实现如何控制一个类只能产生一个对象. 单例模式控制控制对象不要反复创建,提高我们工作的效率.减少资源的占用

单例模式下类的组成部分

  1. 私有的构造方法

  2. 私有的当前类对象作为静态属性

  3. 公有的向外界提供当前类对象的静态方法

但凡是控制一个类只能产生一个对象的模式都叫做单例模式,常见的有饿汉式,懒汉式,内部类式(接口/抽象类),静态内部类式 … …

饿汉式代码实现
/*
多例
只要调用了构造方法 就会在内存上产生一个独立的空间
1将构造方法私有化
构造方法私有化了,外界不能new对象了?对象怎么产生?
2组合当前类本身作为私有静态属性并调用构造方法实例化
如何让外界获取属性值呢?
3在当前类中准备一个共有的静态方法向外界提供当前类对象
 */
public class SingleTon {
    private static SingleTon singleTon =new SingleTon();
    private SingleTon(){
    }
    public static SingleTon getSingleTon(){
        return singleTon;
    }
}
class Test{
    public static void main(String[] args) {
        SingleTon st =SingleTon.getSingleTon();
        SingleTon st2=SingleTon.getSingleTon();
        System.out.println(st==st2);
        System.out.println(st);
        System.out.println(st2);
    }
}

好处: 饿汉式单例模式在类加载进入内存初始化static变量是会初始化当前类对象,此时也不会涉及多个线程对象访问该对象的问题。虚拟机保证只会装载一次该类,肯定不会发生并发访问的问题。因此,可以省略synchronized关键字。

问题:如果只是加载本类,而不是要调用getInstance(),甚至永远没有调用,则会造成资源浪费,不能延迟加载!

懒汉式单例模式
/*
多例
只要调用了构造方法 就会在内存上产生一个独立的空间
1将构造方法私有化
构造方法私有化了,外界不能new对象了?对象怎么产生?
2组合当前类本身作为私有静态属性并调用构造方法实例化
如何让外界获取属性值呢?
3在当前类中准备一个共有的静态方法向外界提供当前类对象
 */
public class SingleTon {
    private static SingleTon singleTon;
    private SingleTon(){
    }
    public static SingleTon getSingleTon(){
        if(null == singleTon){
            singleTon=new SingleTon();
        }
        return singleTon;
    }
}
class Test{
    public static void main(String[] args) {
        SingleTon st =SingleTon.getSingleTon();
        SingleTon st2=SingleTon.getSingleTon();
        System.out.println(st==st2);
        System.out.println(st);
        System.out.println(st2);
    }
}

延迟加载,也叫作懒加载,等到真正用的时候才加载.

懒汉式代理模式在多线程并发情况下仍然是有可能创建多次,是线程非安全的

public class Test1 {
    public static void main(String[] args) {
        for (int i = 0; i < 10; i++) {
            new Thread(new Runnable() {
                @Override
                public void run() {
                    SingleTon.getSingleTon();
                }
            }).start();
        }
    }
}
class SingleTon {
    private static SingleTon singleTon;
    private SingleTon(){
        System.out.println(Thread.currentThread().getName()+"创建了对象");
    }
    public static SingleTon getSingleTon(){
        if(null == singleTon){
            singleTon=new SingleTon();
        }
        return singleTon;
    }
}

image-20220119135242488

双重检测式单例模式

为了解决线程并发问题我们需要对其进行优化,作为一个双重检测式的单例模式,就是我们说的DCL单例模式

package com.msb.singleTon;

public class Test1 {
    public static void main(String[] args) {
        for (int i = 0; i < 10; i++) {
            new Thread(new Runnable() {
                @Override
                public void run() {
                    SingleTon.getSingleTon();
                }
            }).start();
        }
    }
}
class SingleTon {
    private volatile static SingleTon singleTon;
    private SingleTon(){
        System.out.println(Thread.currentThread().getName()+"创建了对象");
    }
    public static SingleTon getSingleTon(){
        if(null ==singleTon){
            synchronized (SingleTon.class){
                if(null == singleTon){
                    singleTon=new SingleTon();
                    /**
                     * 1分配空间
                     * 2执行构造方法
                     * 3将创建对象的引用地址赋值值singleTon变量
                     * 为了避免多线程下的指令重拍问题和多线程缓存造成的数据更新不及时问题
                     * 我们应该在加上volatile处理
                     */
                }
            }
        }
        return singleTon;
    }
}
静态内部类单例模式

除此之外,我们还可以使用内部类实现单例模式的控制

class Single{
    /*
    * 私有构造方法
    * */
    private  Single(){

    }
    /*
    * 范围内部类的属性
    * */
    public static Single getSingle(){
        return InnerClass.single;
    }
    /*
    * 静态内部类
    * */
    public static class InnerClass{
        /*
        * 组合外部类对象作为属性
        * */
        private static final Single single=new Single();
    }
}

外部类没有static属性,则不会像饿汉式那样立即加载对象,只有真正调用getInstance(),才会加载静态内部类。加载类时是线程安全的。 instance是static final 类型,保证了内存中只有这样一个实例存在,而且只能被赋值一次,从而保证了线程安全性.兼备了并发高效调用和延迟加载的优势

枚举式单例模式
public class Test3 {
    public static void main(String[] args) {
        SingleTon1 s1=SingleTon1.INSTANCE;
        SingleTon1 s2=SingleTon1.INSTANCE;
        s1.singleTonOperation();
        System.out.println(s1==s2);
    }
}

enum SingleTon1{
    INSTANCE;
    public void singleTonOperation(){
        System.out.println("operation");
    }
}

优点:实现简单,枚举本身就是单例模式。由JVM从根本上提供保障!避免通过反射和反序列化的漏洞!

缺点:无延迟加载

单例模式总结:

单例模式主要的两种实现方式

​ 饿汉式 线程安全,调用效率高,不能延时加载

​ 懒汉式 线程安全,调用效率不高,可以延时加载

其他方式:

​ 双重检测锁式 极端情况下偶尔会出现问题,不建议使用

​ 静态内部类式 线程安全,调用效率高,可以延时加载

​ 枚举式 线程安全,调用效率高,不能延时加载

image-20220119135442065

关于intern

String a=new String("123")+new String("456");
//String b=new String("123456");
String intern = a.intern();
System.out.println(intern==a);

注释输出true,取消注释 输出false

https://blog.csdn.net/qq_41884976/article/details/83353389

这篇关于面试-Java基础篇(一)的文章就介绍到这儿,希望我们推荐的文章对大家有所帮助,也希望大家多多支持为之网!