面试91011

1、自我介绍


2、ArrayList和LinkedList的区别

ArrayList是实现了基于动态数组的数据结构,LinkedList基于双向链表的数据结构

、对ArrayList和LinkedList而言,在列表末尾增加一个元素所花的开销都是固定的。对ArrayList而言,主要是在内部数组中增加一项,指向所添加的元素,偶尔可能会导致对数组重新进行分配;而对LinkedList而言,这个开销是统一的,分配一个内部Entry对象。

  2、在ArrayList的中间插入或删除一个元素意味着这个列表中剩余的元素都会被移动;而在LinkedList的中间插入或删除一个元素的开销是固定的。

  3、LinkedList不支持高效的随机元素访问,LinkedList其实是基于双向链表实现的, 因此知道链表的特性就对了。

  4、ArrayList的空间浪费主要体现在在list列表的结尾预留一定的容量空间,而LinkedList的空间花费则体现在它的每一个元素都需要消耗相当的空间

  可以这样说:当操作是在一列数据的后面添加数据而不是在前面或中间,并且需要随机地访问其中的元素时,使用ArrayList会提供比较好的性能;当你的操作是在一列数据的前面或中间添加或删除数据,并且按照顺序访问其中的元素时,就应该使用LinkedList了。

  如果涉及到堆栈,队列等操作,应该考虑用List,对于需要快速插入,删除元素,应该使用LinkedList,如果需要快速随机访问元素,应该使用ArrayList。

  尽量返回接口而非实际的类型,如返回List而非ArrayList,这样如果以后需要将ArrayList换成LinkedList时,客户端代码不用改变,这就是抽象编程


3、讲一讲HashMap,(讲了Map的在1.7,1.8的数据结构,put流程,容量为什么是2的幂,顺带提了1.7存在扩容死链问题,1.7和1.8都存在数据丢失问题)

1.7的数据结构 数组+单链表

1.8的数据结构 数组+单链表/红黑树

1.7的插入方法:头插法

1.8的插入方法:尾插法

1.7扩容机制:先扩容在插入

1.8扩容机制:先插入再扩容

在JDK1.6和JDK1.7中,HashMap采用数组+链表实现,即使用链表处理冲突,同一hash值的key-value键值对都存储在一个链表里。但是当数组中一个位置上的元素较多,即hash值相等的元素较多时,通过key值依次查找的效率较低。而在JDK1.8中,HashMap采用数组+链表+红黑树实现,当链表长度超过阈值8时,并且数组总容量超过64时,将链表转换为红黑树,这样大大减少了查找时间。从链表转换为红黑树后新加入键值对的效率降低,但查询、删除的效率都变高了。而当发生扩容或remove键值对导致原有的红黑树内节点数量小于6时,则又将红黑树转换成链表
以1.8讲HashMap的优化:

1.快速存取:

put

  • ①首先将key-value封装成节点对象
  • ②调用hashcode()方法计算出key的哈希值
  • ③通过哈希算法将哈希值转换为具体的下标值
  • ④根据计算出的下标位置将key-value数据进行存储。但在存储前会先判断该下标是否有数据:
    • 如果没有:将该数据存储在数组的该下标位置,作为链表头节点
    • 如果有:会用key值与链表每个节点的key值比较,如果相同则覆盖,如果全部不同则将数据使用头插法添加到链表的头部(jdk1.8之后是尾插法,追加到链表尾部)

get:

  • ①调用hashcode()方法计算出key的哈希值并计算出具体的下标值
  • ②通过下标值快速定位到数组的某个位置,首先会判断该位置上是否有数据:
    • 如果没有:代表该位置还不存在链表,直接返回null
    • 如果有:会用key值与链表每个节点的key值进行比较,相同则获取该节点的数据返回,如果遍历完整个链表后,不存在key值相同的节点,同样会返回null

当底层数组的length为2的n次方时, hash & (length - 1)就相当于对length取模,其效率要比直接取模高得多,这是HashMap在效率上的一个优化

2.扩容

系统必须要在某个临界点进行扩容处理,该临界点就是HashMap中元素的数量在数值上等于threshold(table数组长度*加载因子)

先插入后扩容。通过查看putVal方法的源码可以发现是先执行完新节点的插入后,然后再做size是否大于threshold的判断的先比较是否是新增的数据,再判断增加数据后是否要扩容,这样比较会浪费时间,而先插入后扩容,就有可能在中途直接通过return返回了(本次put是覆盖操作,size不变不需要扩容),这样可以提高效率的

扩容优化:

  • 为什么(e.hash & oldCap)== 0时就放入lo链表,否则就是hi链表;
  • 为什么 j + oldCap就是键值对在新的table数组中的位置;

只需要看看原来的hash值新增的那个bit是1还是0就好了,是0的话索引没变,是1的话索引变成“原索引+oldCap”,这个设计确实非常的巧妙,既省去了重新hash值的时间,而且同时,由于新增的1bit是0还是1可以认为是随机的,因此resize的过程,均匀的把之前的冲突的节点分散到新的bucket了,这一块就是JDK1.8新增的优化点。
 

3.引入红黑树而不是AVL

红黑树是不符合AVL树的平衡条件的,即每个节点的左子树和右子树的高度最多差1的二叉查找树,但是提出了为节点增加颜色,红黑树是用非严格的平衡来换取增删节点时候旋转次数的降低,任何不平衡都会在三次旋转之内解决,相较于AVL树为了维持平衡的开销要小得多

4.HashMap的线程不安全体现在会造成死循环、数据丢失、数据覆盖这些问题。其中死循环和数据丢失是在JDK1.7中出现的问题,在JDK1.8中已经得到解决,然而1.8中仍会有数据覆盖的问题


4、怎么实现一个线程安全的HashMap(ConcurrentHashMap怎么加锁)

在JDK1.7中,ConcurrentHashMap使用Segment数组+HashEntry数组+单向链表的方式实现。而Segment继承了ReentrantLock,所以Segment对象也可以作为ConcurrentHashMap中的锁资源使用

ConcurrentHashMap的每个Segment(段)相当于一个HashTable容器。而Segment数组长度默认为16,但在创建时可以指定段数,必须为2的次幂,如果不为2的次幂则会自优化。在写入数据时都会分段上锁,每个段之间互不影响。而当有线程读取数据时则不会加锁,但是在一个数据在读的时候发生了修改则会重新加锁读取一次。

在ConcurrentHashMap的每个段(Segment对象)中都存在一个计数器:volatile修饰的count变量,count表示每个段中的所有HashEntry数组中所有链表的数据总和数量。除此之外,在每个段中还有一个modCount计数器,记录着当前这个段的写入操作次数,主要用于跨段操作时判断段中是否发生了更改操作。

ConcurrentHashMap中不允许key=null,也不允许value=null。

ConcurrentHashMap.size():

  • ①先记录所有段的modCount值且统计总和
  • ②统计所有段中记录元素个数的count成员总和
  • ③统计完成后再将之前记录的modCount与现在每个段中的modCount进行判断是否相同:
    • 相同:代表统计前后没有发生写操作,直接返回求和所有段count的总数
    • 不同:代表统计前后发生了写操作,重新再执行①②③步骤重新统计一次(最多执行三次)
  • ④如果统计三次后,每次统计总和前后都发生了写入操作,则对容器所有段上锁进行统计并返回

1.8:Node数组+链表+红黑树+CAS+Synchronized关键字实现

JDK1.8版本的ConcurrentHashMap的数据结构已经接近HashMap,相对而言,ConcurrentHashMap只是增加了同步的操作来控制并发,从JDK1.7版本的ReentrantLock+Segment+volatile+HashEntry,到JDK1.8版本中synchronized+CAS+volatile+Node+红黑树,相对而言:

JDK1.8的实现降低锁的粒度,JDK1.7版本锁的粒度是基于Segment的,包含多个HashEntry,而JDK1.8锁的粒度就是Node(有链表或红黑树时就是首节点)

JDK1.8使用红黑树来优化链表,基于长度很长的链表的遍历是一个很漫长的过程,而红黑树的遍历效率是很快的,代替一定阈值的链表,这样形成一个最佳拍档
 


5、CAS是什么

CAS全称Compare And Swap(比较并交换)

这个算法的基本思想就是不断地去比较当前内存中的变量值与你指定的一个变量值是否相等,如果相等,则接受你指定的修改的值,否则拒绝你的操作。因为当前线程中的值已经不是最新的值,你的修改很可能会覆盖掉其他线程修改的结果。这一点与乐观锁的思想是比较类似的

CAS机制的最终实现是依赖于CPU原子性指令实现,是原子操作


6、synchronized和lock有什么区别,提到了lock支持公平锁,追问什么是公平锁

Lock接口 实现类ReentrantLock、ReentrantReadWriteLock,CountDownLatch,Semphore

实现方式不同:synchronized基于对象,ReentrantLock基于AQS

lock的实现类要显示的获取释放锁 

可中断

公平与非公平 等待时间最久的线程(最先请求的线程)会获得该所,这种就是公平锁

读写锁

public interface ReadWriteLock { Lock readLock(); Lock writeLock(); }

可重入


7、如果lock优点比较多,为什么还会有synchronized关键字(只提了synchronized做了优化)

  1. 隐式锁定和释放:synchronized关键字会在方法执行完成后自动释放锁,确保不会出现忘记释放锁的情况。而在使用Lock接口时,需要手动调用lock()unlock()方法,容易在某些情况下忘记释放锁,导致潜在的死锁问题。

  2. 内置优化:JVM对synchronized关键字进行了优化,并在多线程竞争时自动优化为适应不同场景的锁机制,例如偏向锁、轻量级锁和重量级锁等。这使得synchronized在某些情况下性能可能会比Lock接口更好。

8、AQS框架原理

三大件 用volite 维护

它内部通过一个用volatile关键字修饰的int类型全局变量state作为标识来控制同步状态。当状态标识state为0时,代表着当前没有线程占用锁资源,反之当状态标识state不为0时,代表着锁资源已经被线程持有,其他想要获取锁资源的线程必须进入同步队列等待当前持有锁的线程释放。AQS通过内部类Node构建FIFO(先进先出)的同步队列用来处理未获取到锁资源的线程,将等待获取锁资源的线程加入到同步队列中进行排队等待。同时AQS使用内部类ConditionObject用来构建等待队列,当Condition调用await()方法后,等待获取锁资源的线程将会加入等待队列中,而当Condition调用signal()方法后,线程将从等待队列转移到同步队列中进行锁资源的竞争

AQS使用模板方法的设计模式,提供构建同步队列,控制同步状态代码,真正的加锁释放锁的逻辑由子类完成


9、Java里面怎么实现线程,分别有什么区别

Java创建线程有很多种方式啊,像实现Runnable、Callable接口、继承Thread类、创建线程池等等,不过这些方式并没有真正创建出线程,严格来说,Java就只有一种方式可以创建线程,那就是通过new Thread().start()创建。


10、Java类加载过程,双亲委派机制,为什么需要双亲委派,父加载器和子加载器通过继承实现的吗?

防止重复加载类以及核心类篡改

不是,是一种委托方式

11、volatile关键字是干啥用的,可见性是如何做的(答:加屏障,追问:屏障是什么屏障,不会)

volatile是Java中的一个关键字,用于修饰变量。它的作用是保证变量的可见性和禁止指令重排序。

实际当写一个volatile变量时,JMM会把该线程工作内存中的共享变量值刷新到主内存中;当读取一个volatile变量时,JMM会把该线程对应的工作内存置为无效,那么该线程只能从主内存中重新读取共享变量。

保证内存数据的可见性,原理还是内存屏障,但用的是读+写屏障,当多个线程读共享变量时,会触发读屏障,读屏障中会记录哪些线程读了这个变量,然后当一条线程写回数据时,就会触发写屏障,此时写屏障里面,就会根据前面“读屏障”记录下来的线程,去通知所有还未刷回的线程,重新再来读取一次最新值,以此实现了内存中共享数据的可见性


12、什么是JVM

JVM是一个虚拟的计算机,它允许Java字节码(Java bytecode)在不同的计算机体系结构上运行,实现了Java的"一次编写,到处运行"(Write Once, Run Anywhere,WORA)的特性。


13、堆划分了哪些区域

VM内存区域也被称为JVM运行时数据区,主要包含程序计数器、虚拟机栈、本地方法栈、堆空间、元数据空间(方法区)、运行时常量池、字符串常量池、直接内存(本地内存)

  • JDK7及之前:堆空间包含新生代、年老代以及永久代。
  • JDK8:堆空间包含新生代和年老代,永久代被改为元数据空间,位于堆之外。
  • JDK9:堆空间从逻辑上保留了分代的概念,但物理上本身不分代。
  • JDK11:堆空间从此以后逻辑和物理上都不分代。

本质上来说,影响堆空间结构的并不是Java版本的不同,Java堆结构是跟JVM运行时所使用的垃圾回收器息息相关的,由GC器决定了运行时的堆空间会被划分为何种结构


14、新生代和老年代是怎么划分的,垃圾回收算法分别是什么

新生代主要用于存储未达到年老代分配条件的对象,其中Eden区是专门用来存储刚创建出来的对象实例,两个Survivor区主要用于垃圾回收时给存活对象“避难”。
年老代主要用于存储达到符合分配条件的对象实例,比如达到“年龄”的对象以及过大“体积”的大对象等。
方法区/永久代主要用于存储类的元数据信息,如类描述信息、字段信息、方法信息、静态变量信息、异常表、方法表等。

标清

复制

标整


15、标记复制有什么问题

与标记-清除算法相比,复制算法明显是一种更为高效的回收方法,但该算法想要实现则必须要牺牲一部分内存,所以会存在一定程度上的内存浪费,在HotSpotVM中将浪费空间比例控制到了10%。同时,该算法不适用于存活对象较多的场景,如年老代,因为每次GC发生时都需要将存活对象迁移至另一块内存区域中,如果存活对象过多,那么对象的移动也会耗费大量的时间。所以一般复制算法主要作为新生代中的垃圾收集策略。


16、新生代和老年代使用内存的比例是多少

默认情况下新生代和年老代的空间比例为1:2,新生代占1/3,年老代占2/3


17、怎么确定哪些对象需要被清理(答:可达性分析),除了可达性分析,还有别的算法吗(答:引用计数法,追问:为什么没用引用计数法,没回答出来)

循环引用


18、新生代和老年代垃圾回收算法不一样,是和垃圾回收器有关系?(没太理解,回答了回收器是算法的具体实现,提了G1,混合收集)

不一样,老年代一般使用标清和标整

新生代是复制算法


19、G1有什么优点(G1兼顾了时间与吞吐量,分为了新生代回收、并发标记、混合收集三个阶段)


20、GC需要STW吗(回答了G1的过程)

一个是尽量为了避免浮动垃圾产生,第二个则是为了确保一致性。


21、什么叫并发标记,什么是重新标记,怎么知道哪些是漏标的(三色标记法?)

  • ①初始标记:仅标记GcRoot节点直接关联的对象,该阶段速度会很快,需在STW中进行。
  • ②并发标记:该阶段主要是做GC溯源工作(GcTracing),从根节点出发,对整个堆空间进行可达性分析,找出所有存活对象,该阶段的GC线程会与用户线程同时执行。
  • ③重新标记:这个阶段主要是为了修正“并发标记”阶段由于用户线程执行造成的GC标记变动的那部分对象,该阶段需要在STW中执行,并且该阶段的停顿时间会比初始阶段要长不少。
  • ④并发清除:在该阶段主要是对存活对象之外的垃圾对象进行清除,该阶段不需要停止用户线程,是并发执行的。


22、OSI七层模型

  1. 物理层:传输比特流,处理物理连接和信号传输。

  2. 数据链路层:在相邻设备间传输数据帧。

  3. 网络层:处理数据包在网络中的路由和转发。

  4. 传输层:提供端到端的数据传输,确保可靠性和流控制。

  5. 会话层:管理会话和同步通信。

  6. 表示层:数据的格式转换、加密和压缩。

  7. 应用层:为用户提供网络服务和接口。


23、应用层有哪些协议(回答了HTTP、FTP)

WebSocket:Java 可以使用 Java WebSocket API 实现对 WebSocket 协议的支持,实现实时双向通信的 Web 应用程序

HTTP(Hypertext Transfer Protocol):Java 在 Web 开发中广泛应用,可以使用 Java 的 Servlet 和 JSP 技术来开发 Web 服务器端应用程序,同时也可以使用 Java 的 HTTPURLConnection 或 Apache HttpClient 等库来开发 HTTP 客户端应用程序。

HTTPS(Hypertext Transfer Protocol Secure):Java 支持通过 SSL/TLS 加密的安全 HTTP 通信。Java 的 JSSE(Java Secure Socket Extension)提供了对 SSL/TLS 协议的支持,使得 Java 应用程序能够实现安全的数据传输。

DNS(Domain Name System):Java 应用程序可以通过 Java 的 InetAddress 类来进行 DNS 解析,将域名转换为对应的 IP 地址。

FTP(File Transfer Protocol):Java 的 URLConnection 和 FTPClient 等类库提供了对 FTP 协议的支持,可以用于在 Java 应用程序中实现文件传输功能。


24、HTTPS知道吗,加密过程知道吗

  • ①客户端先向服务端请求公钥,其实就是与服务端443端口建立连接的过程。
  • ②服务端接收到请求后,会将数字证书返回给客户端。
  • ③客户端收到证书后,会经过图中的几步效验操作,确认证书合法后,会先生成一个对称密钥,然后通过证书中的公钥对其加密,并发给服务端。
  • 服务端先在本地生成一对密钥,然后携带公钥、网站信息、企业信息等数据去CA机构申请;
  • CA机构根据提交的信息核实身份后,会通过单向的哈希算法(如MD5)对这些信息进行加密,加密后的内容被称为“信息摘要”,因为单向哈希算法的不可逆特性,所以只要被加密的内容发生了丁点改变,加密后得到的内容也会存在天差地别,因此可以有效防止信息被篡改;
  • ③紧接着CA机构会通过自己的密钥对“信息摘要”进行再次加密,加密后的内容被称为「数字签名」,而「申请信息、服务器公钥、数字签名」组合在一起,最终被称为「数字证书」。

通过CA机构的公钥解密证书中的「数字签名」,能够解密成功则代表该证书来自于值得信赖的权威机构,最终就得到了CA哈希后的“信息摘要”,然后客户端会使用与CA机构相同的Hash算法对「数字证书」中的「申请信息、服务器公钥」再次生成一份“摘要”,最后拿着生成的“信息摘要”与解密后的“信息摘要”对比,如果一致则代表数据未被篡改过,最终客户端就可以使用证书中的公钥进行加密通信,

  • ④服务端收到公钥加密后的对称密钥后,通过自己的私钥对其解密。
  • ⑤最终客户端、服务端都拥有了一把对称密钥,接下来则通过对称密钥传输数据。


25、中间人攻击知道吗(不会)


26、GET和POST的区别,两种协议都有Header、Body这些东西吗(回答有)

GET请求:

  • 使用GET请求时,参数会附加在URL的末尾,作为查询字符串。例如:http://example.com/search?keyword=java
  • GET请求通常用于请求获取资源,是幂等的,多次请求相同的URL返回的结果应该是相同的,不会对服务器资源产生影响。
  • GET请求的数据在URL中,因此有长度限制,不适合传输大量数据。
  • GET请求的Header中会包含一些元数据信息,例如User-Agent、Accept等。
  • GET请求的Body部分是空的,因为参数已经附加在URL中了。

POST请求:

  • 使用POST请求时,参数放在请求的Body中。这使得POST请求适合传输大量数据或敏感信息。
  • POST请求通常用于向服务器提交数据,可能会对服务器资源产生影响,因此不是幂等的。
  • POST请求的Header中也包含一些元数据信息,类似于GET请求的Header。
  • POST请求的Body中包含请求所需的具体数据。

虽然GET和POST请求有区别,但它们都是HTTP协议的一部分,都包含Header和Body这两个部分,只是在使用方式和场景上有所不同。在实际开发中,根据需求选择使用GET或POST请求来满足特定的业务需求。

27、TCP和UDP的区别

TCP:

  • 面向连接,建立连接后传输数据。
  • 提供可靠的数据传输,保证数据按顺序到达,支持重传机制。
  • 数据传输效率较低。

UDP:

  • 无连接,直接传输数据。
  • 不保证数据可靠性,无重传机制。
  • 数据传输效率较高。


28、算法题:最长回文子串(力扣原题)

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值