jdk面试篇

96 阅读23分钟

基础篇

String、StringBuffer、StringBuilder

⼋股⽂篇

String、StringBuffer、StringBuilder有什么区别?

  1. String不可变,StringBuffer和StringBuilder是可变的。
  2. String和StringBuffer是线程安全的,StringBuilder⾮线程安全
  3. StringBuilder的执⾏效率⾼于StringBuffer

String str = new String("abc")究竟创建了⼏个对象?

如果字符串常量池中已经存在abc,则创建⼀个对象。如果常量池没有abc则是创建两个对象。

abc是⼀个对象,new String 是⼀个对象。所以,答案是2个或者1个。

知其然篇

String为什么不可变?StringBuffer和StringBuilder是如何做到可变的?

  1. String 和StringBuffer、StringBuilder内部都使⽤char[]保存值。String⾥使⽤final关键字修饰,所以不可变
  2. StringBuffer和StringBuilder没有使⽤final关键字,所以可变
  3. StringBuffer和StringBuilder拼接新字符串时可能包含⼀次数组扩容,如下图:

image.png

为什么StringBuilder的执⾏效率⾼于StringBuffer?

StringBuffer是线程安全的,StringBuffer类的每个⽅法都使⽤关键字synchornized关键字修饰。 多线程操作StringBuffer时会涉及到争抢锁,因此StringBuffer的效率会低。

决浮云篇

请说出下⾯代码的执⾏结果!

StringBuffer stringbuffer = new StringBuffer("abcd");
System.outl.println(stringbuffer.toString() == stringbuffer.toString());

答案为false,为什么呢?

因为每次调⽤toString()⽅法都是⽣成⼀个新的String对象,对象与对象⽤==⽐较的是内存地址,所以为false。如下图:

image.png

如何将⼀个字符串进⾏反转?内部如何实现?

例如:输⼊abcdef,输出fedcba

  1. 使⽤String的toCharArray()或者getChars()⽅法把字符串转为数组,然后倒序重新组合输出。效率慢,⽆法操作太⻓的String
  2. 使⽤StringBuilder或者StringBuffer的reverse()⽅法进⾏反转。效率⾼,可操作很⻓的字符串。

内部同样使⽤数组,但是交换时采⽤第⼀个和最后⼀个,第⼆个和倒数第⼆个依次交换,遍历次数较少。如下图:

image.png

ArryList 、LinkedList

⼋股⽂篇

ArryList 和 LinkedList 有什么区别?

  1. ArryList基于数组存储,连续内存存储;LinkedList 基于链表存储,内存⾮连续
  2. ArryList随机访问速度快;LinkedList随机访问速度慢
  3. ArryList插⼊和删除速度慢;LinkedList插⼊和删除速度快

知其然篇

为什么ArryList插⼊速度慢?

ArryList在插⼊数据时,通过⼀个内部变量size确定数据在数组的位置。当插⼊⼀条新数据时,ArryList会对数组当前的容量进⾏验证,是否⾜以保存当前数据。当底层数组容量不够时,需要对数组进⾏动态扩容。

因为数组的数据结构不能修改的特点,需要重新申请⼀个新的数组,并将原数组⾥的数据copy到新数组⾥。申请数组与copy的过程是相对耗时的,所以效率会⽐较低。

如果ArryList插⼊数据时有⾜够的容量,只需要根据内部变量size直接插⼊到指定位置,效率为O(1)。

image.png

为什么ArrayList的删除速度慢?

如果删除的元素是整个集合的最后⼀个,那么删除速度是很快的。但是,如果删除的元素位于中间或者靠前,删除的速度会很慢,因为需要向前移动被删除元素后⾯的所有元素,保证整个存储没有空位。如下图:

image.png

判断⼀个值是否存在集合中,是ArryList快还是LinkedList快?

速度⼀样快。速度的快慢取决于元素在list中位置。不管是ArryList还是LinkedList都是从第⼀个元素开始依次⽐较每个元素是否和⽬标元素相等。所以,他们的速度是⼀样的。

决浮云篇

ArryList初始容量是多少?扩容之后是多少?

ArryList的默认初始容量为10,当数组扩容时为当前容量的1.5倍。如下图:

image.png

HashSet、LinkedHashSet

⼋股⽂篇

HashSet与LinkedHashSet有什么区别?

  1. HashSet是⽆序的,LinkedHashSet是有序的。
  2. 因为LinkedHashSet需要维护元素插⼊顺序,所以性能略低于HashSet
  3. LinkedHashSet 是 HashSet的⼦类

知其然篇

HashSet 是如何保证不重复的?

HashSet内部维护着⼀个HashMap<E,Object> map,所有的元素都存在Map⾥,利⽤map的不可重复性保证元素不重复。

存⼊map时,以元素为key,以⼀个内部变量Object PRESENT = new Object()为value。

LinkedHashSet 中的有序是什么意思?如何保证有序的?

LinkedHashSet有序要从构造函数看起,直接调⽤了⽗类HashSet的构造函数,内部的Map使⽤LinkedHashMap,如下图:

image.png

所以,LinkedHashSet的有序性是依靠LinkedHashMap做到的,⾃⼰本身并没有做扩展,LinkedHashSet通过维护⼀个链表保证有序。

LinkedHashMap继承⾃HashMap,在添加K-V时会调⽤newNode⽅法。LinkedHashMap重写了此⽅法,在newNode的同时,把新加⼊的节点放到有序链表中。如下图(LinkedHashMap中):

image.png

决浮云篇

LinkedHashSet插⼊已存在的值,它的有序性会变化么?

举例:

LinkedHashSet linkedHashSet = new LinkedHashSet();
linkedHashSet.add(1);
linkedHashSet.add(2);
linkedHashSet.add(3);
// 再次插⼊1
// 再次插⼊1
linkedHashSet.add(1);

不会发⽣变化。有序性会不会变化取决于LinkedHashMap的accessOrder内部属性。⽬前LinkedHashSet的构造函数未提供改变此值的⼊⼝,所以LinkedHashMap⾥的accessOrder属性默认为false。具体查看LinkedHashMap的afterNodeAccess()⽅法,如下图:

image.png

HashMap、ConcurrentHashMap

⼋股⽂篇

HashMap、ConcurrentHashMap有什么区别(jdk1.8)?

  1. HashMap是⾮线程安全的;ConcurrentHashMap是线程安全的
  2. HashMap中允许存⼊空值;ConcurrentHashMap不允许空,key和value都不能为null

HashMap、ConcurrentHashMap的底层使⽤的什么数据结构

底层使⽤hash表数据结构,hash表是使⽤数组+链表实现。在jdk1.8⾥,为了解决链表过⻓的效率问题,增加了红⿊树代替链表。

知其然篇

Map中的加载因⼦是什么?为什么要设置为值0.75?

加载因⼦是哈希表在其容量⾃动增加之前可以达到多满的⼀种尺度。当哈希表中的元素数超出了加载因⼦与当前容量的乘积时,则对哈希表进⾏扩容。

加载因⼦默认值为0.75是在空间与时间成本上的⼀种折中。加载因⼦过⾼虽然节省了空间成本,但是增加了查询成本。

加载因⼦越⼩,哈希表越容易产⽣扩容,空间成本越⾼。所以选择折中0.75。

加载因⼦越⼤,哈希表越不容易扩容,就会更部容易产⽣hash冲突。

ConcurrentHashMap什么时候会触发扩容操作?

ConcurrentHashMap扩容同HashMap的扩容时间点不同,它本身存在2个。

第⼀个时间点:当ConcurrentHashMap⾥的元素达到临界值sizeCtl(值为容量的0.75倍)时进⾏扩容

第⼆个时间点:当ConcurrentHashMap某⼀个位置的链表⻓度达到8并且数组容量⼩于64

HashMap、ConcurrentHashMap是如何解决hash冲突的?

解决hash冲突得四种⽅法:

  • 链地址法
  • 再哈希法
  • 建⽴公共溢出区
  • 开放地址法

map⾥得哈希表结构为数组+链表,所以使⽤的是链地址法

决浮云篇

HashMap、ConcurrentHashMap 是如何优化数据过多时链表⻓度过⻓问题的?

当哈希表中,某个链表的⻓度⼤于并且数组⻓度⼤于64时,会将链表转为红⿊树,增加查询效率。因为红⿊树可以保证树的平衡性,所以可以提⾼查询效率。相⽐于平衡⼆叉树,其放弃绝对平衡,追求⼤致平衡。

Comparable、Comparator

⼋股⽂篇

java中如何⽐较两个对象的⼤⼩?

对象本身实现接⼝Comparable或者使⽤⽐较器Comparator,通过返回负数(⼩于)、0(等于)、正数(⼤于)进⾏⽐较。

前者需要类本身实现Comparable,并重写接⼝compareTo()⽅法,可以直接⽤对象本身调⽤此⽅法与另外⼀个类进⾏⽐较

后者是⼀个⽐较器,可以配合Collections.sort()对集合⾥的对象进⾏排序

Comparable、Comparator有什么区别?

  1. Comparable字⾯含义为⽐较的意思,代表某个类拥有⽐较的能⼒,Comparator字⾯含义为⽐较器,它是⽐较的参与者
  2. ⽤法不同,Comparable的接⼝为compareTo(Object o),是⽤当前对象与传⼊对象进⾏⽐较。Comparator的接⼝为compare(Object o1, Object o2),是对传⼊的两个对象进行⽐较
  3. 代码不同,使⽤Comparable必须修改原类,使其实现此接⼝;⽽Comparator不需要修改类本身。

知其然篇

String对象是如何进⾏排序的?

String对象默认实现了Comparable接⼝,并对compareTo()接⼝进⾏了重写,所以可以直接使⽤Collections.sort()对集合⾥的字符串进⾏排序。

String重写compareTo()接⼝的规则是:

  1. 逐个⽐较每个字符的⼤⼩,如果其中某个字符串不同,则获取⽐较结果
  2. 如果2个字符串⻓度不同,但是前⾯部分相同,则通过⻓度⽐较⼤⼩。

InputStream、OutPutStream

⼋股⽂篇

java 中流都有哪些?

  1. 从流向来看,分为输⼊流和输出流。使⽤输⼊流从⽂件读取数据,使⽤输出流往⽂件写数据
  2. 从读取类型上看,分为字节流和字符流。字节流是从InputStream和OutPutStream派⽣出的⼦类;字符流继承⾃Reader和Writer
  3. 字节流可以处理所有类型的数据,如:图⽚、MP3、视频等。⽽字符流只能处理字符数据。

知其然篇

字节流如何转为字符流?如何读取⼀⾏数据?

字节输⼊流转字符输⼊流通过 InputStreamReader 实现,该类的构造函数可以传⼊InputStream对象。

InputStream inputStream = new FileInputStream(filePath);
// 将字节流转换为字符流
InputStreamReader inputStreamReader = new InputStreamReader(inputStream);

当获取到字符流后,可以利⽤BufferedReader的readLine()每次读取⼀⾏数据

String filePath = "";
InputStream inputStream = new FileInputStream(filePath);
InputStreamReader inputStreamReader = new InputStreamReader(inputStream);
BufferedReader bufferedReader = new BufferedReader(inputStreamReader);
String lineStr = null;
while ((lineStr = bufferedReader.readLine()) != null) {
System.out.println(lineStr);
}

决浮云篇

如何获取某个⽬录下⽂件数有多少?

⽂件⽬录为:String filePath = "C:\Windows\Boot";

{
 public static void main(String[] args) throws IOException {
 String filePath = "C:\\Windows\\Boot";
 System.out.println(getFileCount(filePath));
 }
 public static AtomicInteger getFileCount(String filePath){
 return getFileCount(filePath, new AtomicInteger());
 }
 private static AtomicInteger getFileCount(String filePath, AtomicInteger fileCount)
{
 File rootFile = new File(filePath);
 for (File file : rootFile.listFiles()) {
 if (file.isFile()) {
 fileCount.incrementAndGet();
 }else if (file.isDirectory()) {
 getFileCount(file.getPath(), fileCount);
 }
 }
 return fileCount;
 }
}

泛型T、K、V、E

⼋股⽂篇

什么是泛型?泛型有什么好处?

泛型,即“参数化类型”,把类型明确的⼯作推迟到创建对象或者调⽤⽅法的时候才去明确的特殊类型。在编译的时候能够检查类型安全,并且所有的强制转换都是⾃动和隐式的。

泛型的作⽤:

  1. 代码更加简洁,省去代码中⼤量的强制转换和类型判断
  2. 程序更加健壮,只要编译时期没有类型警告,运⾏时不会抛出ClassCastException警告;除外
  3. 可读性和稳定性,在编译阶段,我们就知道了具体类型

知其然篇

泛型⾥的T、K、V、E分别什么意思? 是否可以⽤ M、N等?

  • ? 代表不确定的类型
  • T(type) 表示具体的⼀个java类型
  • K V 分表代表java键值对中的key和value
  • E(element) 代表Element
  • M、N可以使⽤,泛型可以使⽤任何合法标识命名,约定俗称⽤简短⼤写字⺟

决浮云篇

List<? extends Object> 和 List<? supper Object>有什么区别

  • ? 是通配符
  • <? extends Object>是上限通配符,规定了类型的上限,可传⼊指定类及其⼦类。特点是可取不可放。
  • <? supper Object>是下限通配符,规定了类型的下限,可传⼊指定类及其⽗类。特点是可放不可取。

Date、SimpleDateFormat、Calendar

⼋股⽂篇

java⾥如何操作⼀个⽇期和格式化⽇期的?

通过new Date()可以获取当前⽇期,通过SimpleDateFormat可以将⽇期在字符串与Date()对象之间进⾏转换。

当需要对⽇期的年⽉⽇时分秒等单位进⾏修改时,可以使⽤Calendar类

知其然篇

SimpleDateFormat是线程安全的么?为什么?

SimpleDateFormat⾮线程安全的。

SimpleDateFormat的⽅法没有使⽤synchornized关键字修饰,⽅法内部操作时也没有使⽤锁。

在进⾏format时,将传⼊的⽇期赋值给了⼀个成员变量Calendar calendar;后续操作使⽤此变量。

所以是⾮线程安全的。

SimpleDateFormat效率如何?我们⼜该如何解决?

SimpleDateFormat的创建⾮常耗时,效率低下。但是SimpleDateFormat⼜是⾮线程安全的,所以要求最好⼀个线程⼀个SimpleDateFormat实例,这样即保证了线程安全,⼜提⾼了代码执⾏效率。

即保证线程安全⼜保证⼀个线程⼀个实例,此时需要使⽤ThreadLocal保存我们的SimpleDateFormat对象。

决浮云篇

System.currentTimeMillis()和new Date().getTime()的区别

两者都是获取当前时间的毫秒值。但是如果在系统⾥只是为了记录当前毫秒值,推荐使⽤ System.currentTimeMillis()。因为new Date()构造函数内部还是要先调⽤System.currentTimeMillis(),只是其信息更加丰富。

进阶篇

jdk动态代理、cglib代理

⼋股⽂篇

jdk动态代理和cglib代理的区别?

  1. jdk动态代理只能对实现了接⼝的类⽣成代理,不能针对类;cglib是针对类实现代理,是对指定类⽣成⼀个⼦类,覆盖其⽅法
  2. cglib底层采⽤ASM字节码⽣成框架,使⽤字节码技术⽣成代理类;jdk动态代理是使⽤反射技术。
  3. cglib采⽤的是⽣成⼦类,所以不能代理被final修饰的类

spring是使⽤的jdk动态代理还是cglib代理?

  1. 当Bean实现接⼝时,Spring就会⽤jdk的动态代理
  2. 当Bean没有实现接⼝时,Spring使⽤cglib代理
  3. 可以强制使⽤cglib代理,增加配置:<aop:aspectj-autoproxy proxy-target-class="true"/>或 @EnableAspectJAutoProxy(proxyTargetClass = true)

image.png

知其然篇

java⾥如何使⽤动态代理?

  1. 增加代理类具体实现处理器,实现InvocationHandler接⼝,实现invoke⽅法,如下图:

image.png 2. 提供获取代理对象的⼯具类,通过Proxy类构建创建代理对象,如下图:

image.png 3. 调⽤代理的对象的⽅法,会执⾏ProxyHandler的invoke()⽅法,满⾜⾃⼰的业务需求

决浮云篇

什么场景下会⽤到代理,说下你知道的!

  1. Sping的AOP功能
  2. 对已有类的功能进⾏增强,⼜不⽅便每个类都修改
  3. 通过代理,实现远程Http调⽤⼯具类。如以下三⽅⼯具:ForestHttp等。

BIO、NIO、IO多路复⽤、AIO

⼋股⽂篇

什么是IO? 都有哪⼏种类型?

java中I/O是以流为基础进⾏数据输⼊输出的。简单说就是java通过IO流⽅式和外部设备进⾏交互。⽆论是Socket的读写还是⽂件的读写,在Java层⾯的应⽤开发或者是linux系统底层开发,都属于输⼊input和输出output的处理,简称为IO读写。

强调个基础知识:read系统调⽤,并不是把数据直接从物理设备读数据到内存。write系统调⽤,也不是直接把数据写⼊到物理设备。

根据实现类型可分为: BIO(同步阻塞IO)、NIO(同步⾮阻塞IO)、AIO(异步⾮阻塞IO)、IO多路复⽤模型

知其然篇

什么是同步、异步,什么是阻塞、⾮阻塞?

在读取数据和写⼊数据时,整个流程会涉及到⽤户空间和内核空间2个概念,底层的读写是由操作系统kernel内核完成。

image.png

同步:指⽤户空间线程是主动发起IO请求的⼀⽅,内核空间是被动接受⽅。

异步:相⽐同步则反过来,是指内核kernel是主动发起IO请求的⼀⽅,⽤户线程是被动接受⽅。

阻塞IO:指需要内核IO操作彻底完成后,才返回到⽤户空间,执⾏⽤户的操作。阻塞指的是⽤户空间程序的执⾏状态,⽤户空间程序需等到内核IO操作彻底完成。传统的IO模型都是同步阻塞IO。在java中,默认创建的socket都是阻塞的。

⾮阻塞IO:指的是⽤户程序不需要等待内核IO操作完成后,内核⽴即返回给⽤户⼀个状态值,⽤户空间⽆需等到内核的IO操作彻底完成,可以⽴即返回⽤户空间,执⾏⽤户的操作,处于⾮阻塞的状态。

BIO、NIO、IO多路复⽤、AIO的区别?

BIO(Blocking IO):默认情况下所有的socket都是blocking IO。在阻塞式 I/O 模型中,应⽤程序在从IO系统调⽤开始,⼀直到到系统调⽤返回,这段时间是阻塞的。返回成功后,应⽤进程开始处理⽤户空间的缓存数据。

image.png

优点:程序简单,在阻塞等待数据期间,⽤户线程挂起。⽤户线程基本不会占⽤ CPU 资源。

缺点:⼀般情况下,会为每个连接配套⼀条独⽴的线程。当在⾼并发的场景下,需要⼤量的线程来维护⼤量的⽹络连接,内存、线程切换开销会⾮常巨⼤。因此,BIO模型在⾼并发场景下是不可⽤的。

NIO(None Blocking IO):同步⾮阻塞IO,⽤户线程不需要等待内核IO操作完成,应⽤程序需要不断的进⾏IO系统调⽤,轮询数据是否已经准备好。

image.png

优点:每次发起的 IO 系统调⽤,在内核的等待数据过程中可以⽴即返回。⽤户线程不会阻塞,实时性较好

缺点:不断的重复发起IO系统调⽤,这种不断的轮询,将会不断地询问内核,这将占⽤⼤量的 CPU 时间,系统资源利⽤率较低。

IO多路复⽤模型:如何避免同步⾮阻塞NIO模型中轮询等待的问题呢?这就是IO多路复⽤模型。通过⼀种新的系统调⽤,⼀个进程可以监视多个⽂件描述符,⼀旦某个描述符就绪,内核kernel能够通知程序进⾏相应的IO系统调⽤,Java⾥的NIO采⽤的就是此模型。IO多路复⽤模型的基本原理就是select/epoll系统调⽤

image.png

优点:⽤select/epoll的优势在于,它可以同时处理成千上万个连接(connection)。与⼀条线程维护⼀个连接相⽐,I/O多路复⽤技术的最⼤优势是:系统不必创建线程,也不必维护这些线程,从⽽⼤⼤减⼩了系统的开销。

缺点:本质上,select/epoll系统调⽤,属于同步IO,也是阻塞IO。都需要在读写事件就绪后,⾃⼰负责进⾏读写,也就是说这个读写过程是阻塞的。

AIO:实现了异步⾮阻塞 IO ,异步 IO 的操作基于事件和回调机制,客户端发送的请求先交给操作系统处理,处理完成后再通知线程。

image.png

优点:在内核kernel的等待数据和复制数据的两个阶段,⽤户线程都不是block(阻塞)的。⽤户线程需要接受kernel的IO操作完成的事件,或者说注册IO操作完成的回调函数,到操作系统的内核。所以说,异步IO有的时候,也叫做信号驱动 IO 。

缺点:需要完成事件的注册与传递,这⾥边需要底层操作系统提供⼤量的⽀持,去做⼤量的⼯作。

Linux 系统下,异步IO模型在2.6版本才引⼊,⽬前并不完善。所以,这也是在 Linux下,实现⾼并发⽹络编程时都是以 IO 复⽤模型模式为主。

决浮云篇

什么是零拷⻉,其实现原理是什么?

零拷⻉是指计算机执⾏IO操作时,CPU不需要将数据从⼀个存储区域复制到另⼀个存储区域,从⽽可以减少上下⽂切换以及CPU的拷⻉时间。它是⼀种IO操作优化技术。 传统数据拷⻉如下图:

image.png 零拷⻉并不是没有拷⻉数据,⽽是减少⽤户空间和内核空间的拷⻉次数。如下图:

image.png 通过使⽤mmap+write技术,可减少⼀次cpu拷⻉,从⽽提升效率。⽽sendfile技术可以减少2次空间切换+⼀次cpu拷⻉,从⽽提升效率

在linux2.4版本以后,对sendfile做了优化升级,引⼊了SG-DMA技术,对DMA拷⻉加⼊了scatter/gather操作,它可以直接从⽹卡读取数据到内核空间,如下图:

image.png

java对象内存结构

⼋股⽂篇

java中new Object()⼀个对象占多少空间?

⼀共占⽤16个字节,其中对象头8个字节,对象类型指针4个字节,对象填充4个字节。

new数组和new对象占⽤的空间⼀样么?为什么?

数组⽐对象多占⽤4个字节,⽤来表示数组的⻓度。

知其然篇

简单描述下,java中对象内存结构!

对象在内存中存储的结构由三部分组成:对象头、实例数据、对⻬填充。对象填充部分不是必然的,hotspot虚拟机要求对象的⼤⼩为8的整数倍,当对象⼤⼩是8的整数倍时,则会存在对⻬填充。

image.png

对象头:

MarkWord(标记字段) :哈希码、分代年龄、锁标志位、偏向线程ID、偏向时间戳等信息。Mark Word被设计成了⼀个⾮固定的数据结构以便在极⼩的空间内存储尽量多的信息,它会根据对象的状态复⽤⾃⼰的存储空间。

类型指针:即指向当前对象的类的元数据的指针,虚拟机通过这个指针来确定这个对象是哪个类的实例

数组⻓度:如果是数组,对象头中还有⼀块⽤于存放数组⻓度的数据

实例数据:

实例数据部分是对象真正存储的有效信息,也就是我们在程序代码⾥⾯所定义的各种类型的字段内容,⽆论是从⽗类继承下来的,还是在⼦类中定义的都需要记录下来

对⻬填充:

第三部分对⻬填充并不是必然存在的,也没有特别的含义,它仅仅起着占位符的作⽤。由于HotSpot VM的⾃动内存管理系统要求对象起始地址必须是8字节的整数倍,换句话说就是对象的⼤⼩必须是8字节的整数倍

synchornized的锁信息保存在哪⾥?

锁信息保存在对象头(Mark Word)⾥,⽤2个bit位标识锁信息。01标识未锁定,00表示轻量锁,01表示偏向锁,10表示重量级锁,如下图:

image.png

java新⽣代年龄最⼤是多少?为什么是这个值,能不能改成更⾼呢?

最⼤值为15,可以配置成更⾼,但是不⽣效。

因为java⾥⼀个对象的年龄是保存在对象头,⽤4个bit表示年龄⼤⼩,所以最⼤值为15。

决浮云篇

堆⾥对象的访问⽅式有哪⼏种,有什么区别?

访问⽅式有两种:

  1. 句柄访问,如下图:

image.png

  1. 直接指针访问,如下图

image.png

这两种对象访问⽅式各有优势:

  1. 句柄池最⼤的好处就是reference中存储的是稳定句柄地址,再对象被移动时只会改变句柄中的实例数据指针,⽽reference不改变
  2. 直接指针最⼤的好处就是速度更快,节省⼀次指针定位的时间开销。java没有规定必须使⽤哪种,具体实现由各个虚拟机决定,常⽤的hotsopt虚拟机使⽤的是直接指针。

java⾥如何判断⼀个对象是否可以回收?

判断⼀个对象是否可以被回收,可以通过判断是否存在引⽤决定是否被回收。但是,这个⽅法在循环引⽤时候是⽆效的,会造成内存泄漏。

java⾥判断⼀个对象是否可以被回收,使⽤的是可达性分析。通过⼀系列称为“GC Roots”的根对象作为起始节点集,从这些节点开始,根据引⽤关系向下搜索,搜索过程所⾛过的路径称为“引⽤链”(Reference Chain),如果某个对象到GC Roots间没有任何引⽤链相连,就说明从GC Roots到这个对象不可达时,则证明此对象是不可能再被使⽤的,就是可以回收的对象。

image.png

  • 在JVM虚拟机中,可作为GC Roots的对象包括以下⼏种:
  • 在虚拟机栈(栈帧中的本地变量表)中引⽤的对象
  • 在⽅法区中类静态属性引⽤的对象(类变量)。
  • 在⽅法区中常量引⽤的对象,譬如字符串常量池(String Table)⾥的引⽤。
  • 在本地⽅法栈中JNI(即通常所说的Native⽅法)引⽤的对象。
  • 所有被同步锁(synchronized关键字)持有的对象。
  • Java虚拟机内部的引⽤,如基本数据类型对应的Class对象,⼀些常驻的异常对象(⽐如NullPointExcepiton、OutOfMemoryError)等,还有系统类加载器。

Java 线程协同(等待-通知)

⼋股⽂篇

如何暂停⼀个执⾏中的线程?

执⾏wait(),LockSupport.park(), sleep(),yield()可以让⼀个执⾏中的线程暂停执⾏

线程⾥执⾏wait和sleep的区别?

  1. wait是让⼀个线程等待,需要其他线程调⽤对象的notify()或者notifyAll()进⾏唤醒;sleep是让线程休眠⼀定时间,时间到期继续执⾏
  2. wait在等待期间会释放⾃⼰占有的锁;sleep休眠期间不释放⾃⼰占有的锁。
  3. wait是对象⽅法,来⾃Object类,必须搭配关键字synchornized关键字使⽤;sleep是线程⽅法,来⾃Thread类;

知其然篇

如何终⽌⼀个正在运⾏中的线程?

  1. 通过线程的stop()⽅法强制关闭⼀个线程,此操作是⾮安全的,可能引发线上问题
  2. 通过调⽤线程interrupt()关闭⼀个线程,此⽅法需要线程内部⾃⾏判断,并且决定是否退出

如何让3个线程交替打印⼀句话?

public class ThreadAlternate{
	private static Lock lock = new ReentrantLock();
	private static Condition condition_a = lock.newCondition();
	private static Condition condition_b = lock.newCondition();
	private static Condition condition_c = lock.newCondition();
	
	public static void main(String[] args) throws InterruptedException {
		ExecutorService executorService = Executors.newFixedThreadPool(3);
		executorService.execute(new ReentrantLock_Wait_Runnable(condition_a,condition_b));
		executorService.execute(new ReentrantLock_Wait_Runnable(condition_b,condition_c));
		executorService.execute(new ReentrantLock_Wait_Runnable(condition_c,condition_a));
		TimeUnit.SECONDS.sleep(1);
		lock.lock();
		condition_a.signal();
		lock.unlock();
	}
	
	static class ReentrantLock_Wait_Runnable implements Runnable{
		// 等带可执⾏条件
		private Condition wait;
		// 等待我通知的条件
		private Condition signal;
		
		ReentrantLock_Wait_Runnable(Condition wait, Condition signal){
			this.wait = wait;
			this.signal = signal;
		}
		
		@Override
		public void run() {
			lock.lock();
			try {
				while (true) {
					wait.await();
					System.out.println(Thread.currentThread().getName());
					TimeUnit.SECONDS.sleep(1);
					signal.signal();
				}
			} catch (InterruptedException e) {
				e.printStackTrace();
			}finally {
				lock.unlock();
			}
		}
	}
}

决浮云篇

为什么Thread类的sleep()和yield()⽅法是静态的?

Thread类的sleep()和yield()⽅法将在当前正在执⾏的线程上运⾏。所以在其他处于等待状态的线程上调⽤这些⽅法是没有意义的。这就是为什么这些⽅法是静态的。它们可以在当前正在执⾏的线程中⼯作,并避免程序员错误的认为可以在其他⾮运⾏线程调⽤这些⽅法。