• 美团上海后端开发一面面经


    主要也就问到了这些问题,当时紧张,回答得不是很好,有很多东西没有讲清楚,现在回过头总结一下,也强化一下记忆。

    HashMap HashTable ConcurrentHashMap的区别? put如何解决hash冲突

    HashTable

       底层数组+链表实现,无论key还是value都不能为null,线程安全,实现线程安全的方式是在修改数据时锁住整个HashTable,效率低,ConcurrentHashMap做了相关优化
       初始size为11,扩容:newsize = olesize*2+1
       计算index的方法:index = (hash & 0x7FFFFFFF) % tab.length
       
    HashMap

       底层数组+链表实现,可以存储null键和null值,线程不安全
       初始size为16,扩容:newsize = oldsize*2,size一定为2的n次幂
       扩容针对整个Map,每次扩容时,原来数组中的元素依次重新计算存放位置,并重新插入
       插入元素后才判断该不该扩容,有可能无效扩容(插入后如果扩容,如果没有再次插入,就会产生无效扩容)
       当Map中元素总数超过Entry数组的75%,触发扩容操作,为了减少链表长度,元素分配更均匀
       计算index方法:index = hash & (tab.length 1)
       
       HashMap的初始值还要考虑加载因子:
        哈希冲突:若干Key的哈希值按数组大小取模后,如果落在同一个数组下标上,将组成一条Entry链,对Key的查找需要遍历Entry链上的每个元素执行equals()比较。
       加载因子:为了降低哈希冲突的概率,默认当HashMap中的键值对达到数组大小的75%时,即会触发扩容。因此,如果预估容量是100,即需要设定100/0.75=134的数组大小。
       空间换时间:如果希望加快Key查找的时间,还可以进一步降低加载因子,加大初始大小,以降低哈希冲突的概率。
       HashMap和Hashtable都是用hash算法来决定其元素的存储,因此HashMap和Hashtable的hash表包含如下属性:
       容量(capacity):hash表中桶的数量
       初始化容量(initial capacity):创建hash表时桶的数量,HashMap允许在构造器中指定初始化容量
       尺寸(size):当前hash表中记录的数量
       负载因子(load factor):负载因子等于“size/capacity”。负载因子为0,表示空的hash表,0.5表示半满的散列表,依此类推。轻负载的散列表具有冲突少、适宜插入与查询的特点(但是使用Iterator迭代元素时比较慢)
       除此之外,hash表里还有一个“负载极限”,“负载极限”是一个0~1的数值,“负载极限”决定了hash表的最大填满程度。当hash表中的负载因子达到指定的“负载极限”时,hash表会自动成倍地增加容量(桶的数量),并将原有的对象重新分配,放入新的桶内,这称为rehashing。
       HashMap和Hashtable的构造器允许指定一个负载极限,HashMap和Hashtable默认的“负载极限”为0.75,这表明当该hash表的3/4已经被填满时,hash表会发生rehashing。
    “负载极限”的默认值(0.75)是时间和空间成本上的一种折中:
       较高的“负载极限”可以降低hash表所占用的内存空间,但会增加查询数据的时间开销,而查询是最频繁的操作(HashMap的get()与put()方法都要用到查询)
       较低的“负载极限”会提高查询数据的性能,但会增加hash表所占用的内存开销
    程序猿可以根据实际情况来调整“负载极限”值

           
    ConcurrentHashMap
           
       底层采用分段的数组+链表实现,线程安全
       通过把整个Map分为N个Segment,可以提供相同的线程安全,但是效率提升N倍,默认提升16倍。(读操作不加锁,由于HashEntry的value变量是 volatile的,也能保证读取到最新的值。)
       Hashtable的synchronized是针对整张Hash表的,即每次锁住整张表让线程独占,ConcurrentHashMap允许多个修改操作并发进行,其关键在于使用了锁分离技术
       有些方法需要跨段,比如size()和containsValue(),它们可能需要锁定整个表而而不仅仅是某个段,这需要按顺序锁定所有段,操作完毕后,又按顺序释放所有段的锁
       扩容:段内扩容(段内元素超过该段对应Entry数组长度的75%触发扩容,不会对整个Map进行扩容),插入前检测需不需要扩容,有效避免无效扩容

       HashMap基于哈希思想,实现对数据的读写。当我们将键值对传递给put()方法时,它调用键对象的hashCode()方法来计算hashcode,然后找到bucket位置来存储值对象。当获取对象时,通过键对象的equals()方法找到正确的键值对,然后返回值对象。HashMap使用链表来解决碰撞问题,当发生碰撞时,对象将会储存在链表的下一个节点中。HashMap在每个链表节点中储存键值对对象。当两个不同的键对象的hashcode相同时,它们会储存在同一个bucket位置的链表中,可通过键对象的equals()方法来找到键值对。如果链表大小超过阈值(TREEIFY_THRESHOLD,8),链表就会被改造为红黑树结构。    
        在HashMap中,null可以作为键,这样的键只有一个,但可以有一个或多个键所对应的值为null。当get()方法返回null值时,即可以表示HashMap中没有该key,也可以表示该key所对应的value为null。因此,在HashMap中不能由get()方法来判断HashMap中是否存在某个key,应该用containsKey()方法来判断。而在Hashtable中,无论是key还是value都不能为null。
        Hashtable与HashMap另一个区别是HashMap的迭代器(Iterator)是fail-fast迭代器,而Hashtable的enumerator迭代器不是fail-fast的。所以当有其它线程改变了HashMap的结构(增加或者移除元素),将会抛出ConcurrentModificationException,但迭代器本身的remove()方法移除元素则不会抛出ConcurrentModificationException异常。但这并不是一个一定发生的行为,要看JVM。
        ConcurrentHashMap比HashMap多出了一个类Segment,而Segment是一个可重入锁。
    ConcurrentHashMap是使用了锁分段技术来保证线程安全的。
    锁分段技术:首先将数据分成一段一段的存储,然后给每一段数据配一把锁,当一个线程占用锁访问其中一个段数据的时候,其他段的数据也能被其他线程访问。

    ConcurrentHashMap提供了与Hashtable和SynchronizedMap不同的锁机制。Hashtable中采用的锁机制是一次锁住整个hash表,从而在同一时刻只能由一个线程对其进行操作;而ConcurrentHashMap中则是一次锁住一个桶。

    ConcurrentHashMap默认将hash表分为16个桶,诸如get、put、remove等常用操作只锁住当前需要用到的桶。这样,原来只能一个线程进入,现在却能同时有16个写线程执行,并发性能的提升是显而易见的
           

    a == b a.equals(b) 的区别

    String a = "abc" ; String b = new String("abc");  a == b    a.equals(b)  的区别

    首先,a==b的结果是false,  a.equals(b) 的结果是true.
    双等号比较的是两个对象是不是同一个对象,比较的是这两个对象的引用,如果这两个对象的引用地址相同,则表明是同一个对象,返回 true ,否则返回false, equals比较的是两个对象的hashCode,如果两个对象是同一个的,那么他们的hashCode一定相同,但如果两个对象的hashCode相同,他们未必是同一个对象,例如我们上面的a 和b
       一般我们比较对象equals之前,需要先重写它的hashCode方法,否则默认返回的是他的地址引用。
    代码验证:
     
    public class demo1 {
       public static void main(String[] args ){
           String a = "abc";
           String b = new String("abc");
           System.out.println(a==b);
           System.out.println(a.equals(b));
      }
    }
    输出结果:
    false
    true
       

    notify 与 notifyAll 的区别

    先说两个概念:锁池和等待池

       锁池:假设线程A已经拥有了某个对象(注意:不是类)的锁,而其它的线程想要调用这个对象的某个synchronized方法(或者synchronized块),由于这些线程在进入对象的synchronized方法之前必须先获得该对象的锁的拥有权,但是该对象的锁目前正被线程A拥有,所以这些线程就进入了该对象的锁池中。
       等待池:假设一个线程A调用了某个对象的wait()方法,线程A就会释放该对象的锁后,进入到了该对象的等待池中
    ————————————————
    如果线程调用了对象的 wait()方法,那么线程便会处于该对象的等待池中,等待池中的线程不会去竞争该对象的锁。
    当有线程调用了对象的 notifyAll()方法(唤醒所有 wait 线程)或 notify()方法(只随机唤醒一个 wait 线程),被唤醒的的线程便会进入该对象的锁池中,锁池中的线程会去竞争该对象锁。也就是说,调用了notify后只要一个线程会由等待池进入锁池,而notifyAll会将该对象等待池内的所有线程移动到锁池中,等待锁竞争
    优先级高的线程竞争到对象锁的概率大,假若某线程没有竞争到该对象锁,它还会留在锁池中,唯有线程再次调用 wait()方法,它才会重新回到等待池中。而竞争到对象锁的线程则继续往下执行,直到执行完了 synchronized 代码块,它会释放掉该对象锁,这时锁池中的线程会继续竞争该对象锁。
    ————————————————
    综上,所谓唤醒线程,另一种解释可以说是将线程由等待池移动到锁池,notifyAll调用后,会将全部线程由等待池移到锁池,然后参与锁的竞争,竞争成功则继续执行,如果不成功则留在锁池等待锁被释放后再次参与竞争。而notify只会唤醒一个线程。

    有了这些理论基础,后面的notify可能会导致死锁,而notifyAll则不会的例子也就好解释了

    wait和sleep的区别

    1、sleep是线程中的方法,但是wait是Object中的方法。

    2、sleep方法不会释放lock,但是wait会释放,而且会加入到等待队列中。

    3、sleep方法不依赖于同步器synchronized,但是wait需要依赖synchronized关键字。

    4、sleep不需要被唤醒(休眠之后推出阻塞),但是wait需要(不指定时间需要被别人中断)。

    列举几个java中线程安全的类

    线程安全(Thread-safe)的集合对象

       Vector
       HashTable
       StringBuffer
    非线程安全的集合对象
       ArrayList
       LinkedList
       HashMap
       HashSet
       TreeMap
       TreeSet
       StringBulider

    寻找第K大


    有一个整数数组,请你根据快速排序的思路,找出数组中第K大的数
    给定一个整数数组a,同时给定它的大小n和要找的K,(K在1到n之间)请返回第K大的数,保证答案存在
    测试样例:
    [1,3,5,2,2]  5 3
    返回:
    2
       public static int getK(int[] nums,int k ){
           if(k>nums.length){
               return -1;
          }
           int temp=0;
           for(int i=nums.length-2;i>0;i--){
               for(int j=0;j<i;j++){
                   if(nums[j]<nums[j+1] ){
                       // 交换
                       temp =  nums[j+1];
                       nums[j+1] = nums[j];
                       nums[j] =  temp;
                  }
              }
          }
           return nums[k-1];
      }

    描述下列代码再内存中的执行过程(堆区栈区方法区)。

    public class Solution{
       public static void main(Strig[] args){
           A a =  new A();
           A b =  new A();
           swap(a,b);
      }
       public static void swap(A x,A y){
          A z = new A();
          z = x;
          x = y;
          y = z;
      }
    }



  • 相关阅读:
    强烈推荐:240多个jQuery插件【转】
    逆变与协变详解
    Mac入门 (二) 使用VMware Fusion虚拟机
    JQUERY UI DOWNLOAD
    几个常用Json组件的性能测试
    jQuery.extend 函数详解
    获取Portal中POWL程序的APPLID
    设计师和开发人员更快完成工作需求的20个惊人的jqury插件教程(上)
    Linux kernel中网络设备的管理
    mongodb修改器
  • 原文地址:https://www.cnblogs.com/1832921tongjieducn/p/13323697.html
Copyright © 2020-2023  润新知