阿里菜鸟内推 一面

1.自我介绍

2.HashMap和ConcurrentHashMap的区别?

    HashMap线程不安全,ConcurrentHashMap线程安全。

    HashMap是基于哈希表的Map接口的非同步实现。提供所有可选的映射操作,并允许null值和null键。

    基本原理是先声明一个下标范围比较大的数组来存储元素。另外设计一个哈希函数(也叫做散列函数)来获得每一个元素的key的函数值,与数组下标相对应,数组存储的元素是一个Entry类,包括key,value,next。

    ConcurrentHashMap不允许使用null键和null值,使用了分段锁技术,将数据分成了一段一段进行存储,并且为每一段数据配一把锁,当一个线程占用锁访问其中一个段数据时,其他段的数据也能被其他线程访问,能够实现真正的并发访问。读操作大部分时候都不需要用到锁,只有在size()等操作的时候才需要锁住整个hash表。

    它把区间按照并发级别(ConcurrentLevel)分成了若干个Segment。默认情况下内部按并发级别为16来创建。对于每个segement的容量,默认情况也为16。当然,concurrentLevel和segment的初始容量都可以通过构造函数设定。ConcurrentHashMap通过segment来分段和管理锁,segment继承自ReentrantLock,因此ConcurrentHashMap使用ReentrantLock来保证线程安全。

3.HashMap的底层结构和什么情况会导致死锁?

    HashMap是一个基于哈希表的Map接口的非同步实现。允许null键和null值。

    底层结构:先定义一个下标范围比较大的数组,然后定义一个哈希函数把元素的key值所对应的hash值求出来,映射到数组下标,数组元素存储的是一个Entry类,key,value,next。

    多线程情况下,HashMap可能会出现死锁:

(1)多线程put操作以后,get操作导致死循环,导致CPU100%的现象。主要是多线程同时put的时候,如果同时触发了rehash操作,会导致扩容后的HashMap中的链表出现循环节点,进而使得后面get的时候,会出现死循环;

(2)多线程put操作,导致元素丢失,也是发生在多个线程对hashmap扩容时。

4.HashTable和ConcurrentHashMap都是线程安全的,但是有什么区别?

    虽然都是线程安全的,但是两者实现线程安全的方式不一样。(各自说一下底层结构!)

    HashTable是通过synchronized方法来实现同步的,效率比较低;而ConcurrentHashMap是通过锁分离技术来实现线程安全的,效率更高,把数据分段存储,并且为每段都配了一把锁,所以当一个线程获取锁并访问其中的段数据时,并不影响其他线程对其他段数据的访问,所以效率较高。

5.如何检查死锁,java 程序如何查看死锁,你还知道例如 jstack 之类的其他命令吗?

    区分线程状态-查看等待目标-对比Monitor等持有状态。

    java程序查看死锁可以通过JConsole或者jps或者系统管理器等工具确定线程ID,然后通过jstack查看堆栈信息,查看线程状态以及等待目标。

    除了jstack以外还有的命令:

jps:虚拟机进程状况工具

jstat:虚拟机统计信息监视工具(如显示垃圾收集情况,内存使用情况等)。

jmap:java内存映像工具(用于生成堆转储快照)。

jhat:堆转储快照分析工具。

jstack:java堆栈跟踪工具。

6.Object里面有什么方法?

public final native Class<?> getClass();  //返回此Object的运行时类
public native int hashCode();            //返回该对象的哈希码值

public boolean equals(Object obj) {      //指示其他某个对象是否与该对象的地址值相等
    return (this == obj);
}
public String toString() {   //返回该对象的字符串表示
    return getClass().getName() + "@" + Integer.toHexString(hashCode());
}
public final native void notify();   //唤醒在此对象监视器上等待的单个线程

public final native void notifyAll();

public final native void wait(long timeout) throws InterruptedException;
protected void finalize() throws Throwable { }  //当垃圾收集器确定不存在对该对象的引用时,由对象的垃圾收集器调用该方法
protected native Object clone() throws CloneNotSupportedException; //创建并返回该对象的一个副本

7.hashcode 是如何生成的,equals 和 hashcode 的联系,当新建一个类时,如何重写 equals 和 hashcode?

    hashcode方法是从object类继承过来的,object类中的hashcode()方法返回的是对象在内存中地址转换成的int值,如果对象没有重写hashcode()方法,那么任何对象的hashcode()返回的值都是不相等的。

    重写方法:java中的hashcode方法就是根据一定的规则将与对象相关的信息(比如对象的存储地址、对象的字段等)映射成一个数值,这个数值称为散列值。

    主要作用是用于查找的,为了配合基于散列的集合一起正常运行,比如HashMap,HashSet,以及HashTable,hashcode是用来在散列存储结构中确定对象存储地址的。

 

equals和==:

==可以用来比较基本数据类型的值,或者也可以用来比较对象在内存中的存放地址是否相等。

java中所有的类都是继承于Object的,Object的基类中定义了一个equals方法,这个方法的初始行为也是比较两个对象的内存地址,但在一些类库中这个方法被重写了,如String,在这些包装类中equals有其自身的实现,而不再是比较类在堆内存中的存放地址了(String中equals是比较两个对象的内容是否相等)。

   

往集合中添加新元素过程:

    当往集合中添加新的元素时,首先调用这个对象的hashcode函数,如果返回结果相等,那么调用equals函数,如果相同的话就不存了,不同的话再寻找其他地址。

 

扩展:为什么在重写equals方法的时候,必须重写hashcode方法?

    比如在使用set集合的时候,往其中放入内容相同的对象,如果没有重写hashcode()方法,那么集合众将会放入内容相同的对象(因为两个对象地址不同),这和set集合的性质不符。因此需要在重写equals方法的同时,必须重写hashcode方法。

 

equals方法和hashcode方法联系:equals true====>hashcode 同

  • equals方法返回true,hashcode方法返回一定相同;
  • equals方法返回false,hashcode有可能相同,有可能不相同;
  • hashcode方法返回相同,equals有可能true,有可能false;
  • hashcode方法返回不同,equals一定返回false;

 

equals和hashcode重写:

  • 尽量保证使用对象的同一个属性来生成hashcode和equals方法;
  • 在重写equals方法的同时,必须重写hashcode方法。

 

8.介绍一下BIO和NIO的区别?NIO的原理?AIO。

首先对同步、异步、阻塞、非阻塞理解一下,参考https://blog.csdn.net/u013851082/article/details/53942947

    IO的方式分为几种,BIO(同步阻塞),NIO(同步非阻塞),AIO(异步非阻塞)。

  • BIO:在JDK1.4出来之前,我们建立网络连接的时候采用BIO模式,服务器实现模式为一个连接一个线程,即客户端有连接请求时服务器端就需要启动一个线程进行处理,如果这个连接不做任何事情就会造成不必要的线程开销,当然可以通过线程池改善。
  • NIO:同步非阻塞,是对BIO的改进,服务器实现模式为一个请求一个线程,即客户端发起的连接请求都会注册到多路复用器上,多路复用器轮询到连接有I/O请求时才启动一个线程进行处理。用户进程也需要时不时地询问IO操作是否就绪,这就需要用户进程不停地去询问。
  • AIO,异步非阻塞,这种模式下,用户进程只需要发起一个IO操作然后立刻返回,等IO操作真正完成以后,应用程序会得到IO操作完成的通知,此时用户进程只需要对数据进行处理就好了,不需要进行实际的IO操作,因为真正的IO读取或者写入操作已经由内核完成了。

    所以BIO和NIO的区别就是BIO同步阻塞,NIO同步非阻塞。NIO本身是基于事件驱动思想来完成的,主要想解决的就是BIO的大并发问题,在使用同步IO的网络应用中,如果同时处理多个客户端请求,或是客户端要同时和多个服务器进行通讯,就必须使用多线程来处理,即将每一个客户端请求分配给一个线程来单独处理。这样做虽然可以达到我们的要求,但是同时又会带来另一个问题,就是每创建一个线程就要为这个线程分配一定的内存空间,而且操作系统本身也对线程的总数有一些限制。如果客户端的请求太多,服务端可能因为不堪重负而拒绝客户端的请求,甚至服务器可能会因此而瘫痪。

    NIO基于Reactor,当socket有流可读或可写入socket时,操作系统会相应的通知应用程序进行处理,应用程序将流读取到缓冲区或写入操作系统,这个时候已经不是一个连接就要对应一个线程了,而是有效的请求,对应一个线程,当连接没有数据时,是没有工作线程来处理的。

    AIO,与NIO不同,当进行读写操作时,只需要直接调用API的read或write方法即可。这两种方法均为异步的,对于读操作而言,当有流可读时,操作系统会将可读的流传入read方法的缓冲区,并通知应用程序;对于写操作而言,当操作系统将write方法传递的流写入完毕时,操作系统主动通知应用程序。可以理解为,read、write方法都是异步的,完成后会主动调用回调函数。

NIO和IO的区别:

  • IO是面向流的,NIO是面向缓冲区的。
  • IO是阻塞的,NIO是非阻塞的。
  • Selector,NIO的选择器允许一个单独的线程来监视多个输入通道,可以注册多个通道使用一个选择器,然后使用一个线程来选择通道。

 

NIO的原理:

    在NIO中有几个核心对象,Channel,Buffer,Selector!

(1)Buffer:缓冲区实际上是一个容器对象,其实就是一个数组,在NIO中,所有的数据都是用缓冲区处理的。在读取数据时,它是直接读到缓冲区中的,在写入数据时,也是写入到缓冲区的,任何时候访问NIO中的数据,都是将它放到缓冲区。在NIO中,所有缓冲区类型都是继承自抽象类Buffer,最常用的就是ByteBuffer。

而在面向流I/O中,所有数据都是直接写入或者将数据读取到Stream对象中。

(2)Channel:通道是一个对象,通过它可以读取和写入数据,所有数据都可以通过Buffer对象来处理,我们永远不会将字节写入通道中,而不是把数据写入一个Buffer中。同样也不会从通道中读取字节,而是将数据从通道读入缓冲区,再从缓冲区获取这个字节。通道与流的不同之处在于,流是单向的,而通道是双向的,流只能在一个方向上移动,一个流只能是InputStream或者OutputStream的子类,InputStream只能进行读取操作,OutputStream只能进行写操作,而通道是双向的,可以用于读、写或者同时用于读写。

(3)Selector:NIO有一个主要的类Selector,类似一个观察者,我们把需要探知的socketchannel告诉Selector,我们就可以做别的事情,当有事件发生时,它会通知我们,传回一组SelectionKey,我们读取这些Key,就会获得我们刚刚注册的socketchannel,接下来从这个channel中读数据,读完以后我们就可以处理这些数据。

    Selector内部其实是在做一个对所注册的Channel轮询访问,不断地轮询,一旦轮询到一个Channel有注册的事件发生,那么就会站起来,交出一把钥匙,让我们通过这把钥匙来读取这个Channel的内容。用单线程来处理一个Selector,然后通过Selector。select()来获取到达事件,在获取了到达事件以后,就可以逐个对事件进行处理。

    Selector是NIO的核心类,能够检测到注册的多个通道上是否有事件发生,如果是有事件发生,那么进行响应处理。这样一来,只需要用单个线程就可以管理多个通道,也就是管理多个连接,大大减少了系统开销,并且不必为每个连接都创建一个线程,不用去维护多个线程,并且避免了线程上下文切换导致的开销。

阅读更多
换一批

没有更多推荐了,返回首页