面经总结-30

美团面经

java基本类型和占用字节数?

byte 1;boolean 1;char 2;short 2; int 4;float 4; double 8;long 8

Integer缓冲池,Long有没有缓冲池?

Integer的缓冲池大小是-128-127,因此在此范围内使用的数据并不创建新的空间,每个包装类都有自己的缓冲值,Long的缓冲范围也是-128-127

HashMap的底层实现?

hashmap结合数组结构和链表结构的优点,从而实现了查询和修改效率高,插入和删除效率也高的一种数据结构,在java8以前,其底层结构是数组+链表,java8以后是数组+链表+红黑树;当链表数量超过8的时候,会装换成红黑树,在红黑树节点低于6的时候会转换成链表,之所以不设置成相同的数字,是为了避免频繁转换结构带来的内存开销

HashMap的线程是否安全,为什么会发生链表的死循环?

HashMap的线程不安全,在put操作的时候,导致多线程数据不一致,而链表的死循环是由于resize引起的,不仅会引起死循环还会引起元素丢失;

其Put操作的具体步骤是:

首先将kv封装到节点对象中,然后它的底层会调用k的hashCode()方法得到hash值,通过哈希算法,将hash值转换成数组的下标,下标位置上如果没有任何元素,就把节点添加到这个位置上,如果说下标对应的位置有链表,此时就会拿着k与链表上每个节点的k调用equals方法进行比较,如果所有eqauls返回false那么这个节点就会被添加到链表的末尾,如果其中一个返回了true就会将节点的value值覆盖

其Get操作的实现原理是:

调用k的hashCode方法得到hash值,通过hash算法转换成数组的下标,如果这个位置什么都没有返回null,如果有单向链表,就会遍历该链表,让k进行equals操作,如果所有的equals方法都返回false,那也返回null,只要一个返回了true就应该返回value

比如现在有两个线程a和b,首先a希望插入一个kv到表中,首先计算记录要落到的hash桶的索引坐标,然后获取到桶里的链表头节点,并且遍历到了链表的最后一个元素,此时线程a的时间片用完了,线程b被调度得以执行,b成功的将记录插在了桶里,线程a再次被调度运行的时候,它依然认为当前元素是最后一个元素,继续了插入操作,这将导致b的数据被覆盖,这就导致了数据不一致行为;

一个线程在扩容的时候还没有完全分配完所有的元素,第二个线程就强行插入元素;其主要原因在于在移动到新的bucket的时候,HashMap不会将元素放在链表的尾部而是放在头部

面经总结-30

 Map下有哪些是线程安全的?

hashtable,synchronizedMap,concurrentHashMap

ConcurrentHashMap的实现

其数据结构基本和HashMap雷同

在Java8以前,数组,链表+reentrantLock+segment,使用的是分段锁的概念,将数据一段一段的存储,当一个线程占用锁访问一个数据段的时候,其他数据段依然能被正常访问;它需要使用两次hash,第一次hash到对应segment第二次hash到对应的链表;put方法在指定的segment加锁,get方法使用volatile关键字保证可见性,不加锁,size方法是对其进行两次计算,如果结果不一致再加锁计算一次,一致就直接返回

在java8以后,采用的是数组,红黑树,链表+CAS+Synchronized的方法,以前是对segment加锁,现在是对数组元素加锁,锁的粒度更加细致了;put方法,如果putKey的位置已经存在链表了,采用的是synchronized加锁的方法,,锁的是当前链表否则采用的是CAS;get操作使用volatile保证可见性,不加锁.size方法会在更新链表的时候实时更新,所以也不加锁

HashMap和Hashtable的区别

继承父类不同:前者是AbstractMap后者是Dictionary(已经废弃),但是两者都实现了map接口

线程安全:前者不安全,后者安全

允许null值:前者允许kv为null,后者不允许

计算hash值的方式不同:前者先调用hashCode方法计算出来一个hash值,再将hash与右移16位后相异或,从而得到新的hash值;后者直接使用hashCode

扩容方式不同:hashmap要求每次扩容是原来的两倍,并且要求是2的幂次,后者扩容为两倍+1

解决hash冲突方式不同:冲突数大于8红黑树,小于6链表,hashtable只有链表

C++和java面向对象的区别

封装区别:java多了一个default关键字,表示同一包下可以访问

继承区别:c++可以继承多个类,并且可以指定public,protected,private继承,java中只能继承一个类,方式和public相似

java内存回收,什么时候会内存回收?

一个对象实例化时 先去看伊甸园有没有足够的空间
如果有就不进行垃圾回收 ,对象直接在伊甸园存储.
如果伊甸园内存已满,会进行一次minor gc,然后再进行判断伊甸园中的内存是否足够
如果不足,则去看幸存者区的内存是否足够.
如果内存足够,把伊甸园部分活跃对象保存在幸存者区,然后把对象保存在伊甸园.
如果内存不足,向老年代发送请求,查询老年代的内存是否足够
如果老年代内存足够,将部分幸存者区的活跃对象存入老年代.然后把伊甸园的活跃对象放入存活区,对象依旧保存在伊甸园.
如果老年代内存不足,会进行一次full gc,之后老年代会再进行判断 内存是否足够,如果足够,如果不足 会抛OutOfMemoryError.

幸存者区的空间有两块,他们是相对的,分为from和to,发生一次minor gc之后两块的区域会互换
伊甸园区丢给幸存者区的对象,或是幸存者区丢给老年代的对象都是to区来的

GC算法?

引用计数(java中没有应用,Python中有使用),被引用+1,失去引用-1,为0的时候清除,存在循环引用问题

标记清除:标记是把所有活动的对象打上标记,清除是清除没有打上标记的对象,存在内存碎片问题

复制算法:一块内存分成两块,如果一边用完了就把所有的存活对象丢到另一块上面,然后对其进行整个清空,缺点是递归调用函数

标记整理:与标记清除类似,只是后续有整理内存的步骤,会把存活的内存移动到一端,然后清除掉边界外的内存,缺点是压缩需要时间成本

垃圾收集器有几种?

7种,新生代使用复制算法进行垃圾回收,老年代使用的是标记整理,特殊的是CMS-标记清除,G1是复制+标记整理算法

Serial收集器(新生代)-默认,以串行的方式执行,单线程收集器,它工作的时候所有线程都要停止工作,收集新生代垃圾,使用参数-XX:+Use SerialGC,它拥有最高的单线程收集效率

ParNew收集器(新生代),Serial的多线程版本,单核效率不如Serial,只有它可以和CMS收集器配合使用,-XX +UseParNewGC

Parallel Scavenge(新生代):多线程,吞吐量大,-XX +useParallelGC

Serial Old(老年代)-默认:Serial的老年代版本,-XX +useSerialOldGC

Parallel Old(老年代):关注吞吐量 --XX:+UseParallelOldGC

CMS(老年代):垃圾回收线程与用户线程同时工作:缺点在于:吞吐量低,无法处理浮动垃圾(标记之后在此次gc无法清理的垃圾),内存碎片

-XX:+UseConcMarkSweepGC

  • 初始标记:记录GC Roots能关联的对象,速度快,需要停顿
  • 并发标记:进行GC RootsTracing的过程,不需要停顿
  • 重新标记:修正并发标记期间因为程序运作导致标记变动的对象的标记记录,需要停顿
  • 并发清除:清理垃圾不需要停顿

G1垃圾收集器:面向服务端的垃圾收集器,G1把堆划分成多个大小相等的独立区域,新生代老年代不再物理隔离,它只有并发标记的时候才不会stop the word其他时候都会停下来-XX:+UseG1GC

其相比于CMS的特点在于:

  • 分代收集:不需要配合其他收集器就可以管理整个GC堆
  • 可预测的停顿:可以建立可以预测的停顿时间模型

Mysql的引擎Innodb和Myisam的区别(8个)

  1. 是否支持事务:前者支持,后者不支持
  2. 是否支持外建:前者支持,后者不支持
  3. 前者是聚集索引,使用B+树为索引结构,数据文件是和主键索引绑在一起的,必须要有主键,通过主键索引的效率很高,辅助索引需要两次查询,先查到主键,再找到数据;后者是非聚集索引,也是B+数为索引结构其主键索引和辅助索引的叶子节点都是数据文件的地址指针
  4. 前者不保存表的具体行数,执行select count(*)from table的时候要全表扫描(原因是事务的特性,同一时刻表的行数对不同事务是不一样的),后者用一个变量保存了
  5. 前者不支持全文索引(5.7以后支持),后者支持
  6. 后者的表格可以被压缩后进行查询操作
  7. 前者支持表,行级锁(它的锁是实现在索引上的,如果没有命中索引,也会退化成表锁),后者只支持表锁
  8. 前者必须要有唯一主键,后者可以没有

如何选择MyISAM和Innodb?多读少写:MyISAM;事务,多读多写:Innodb

MyISAM的优点:count算的快,表锁批量插入速度快,可以使用压缩表减少空间占用

事务的概念?

一组操作要么同时成功,要么一起失败,他们是不可分割的工作单元,拥有ACID的特点

原子性:不可分割;一致性:a给b转账1000,a少1000,b多1000,这个操作是整体;隔离性:事务之间互不影响,不可以被其他事务干扰;持续性:事故提交之后,发生的变化是永久的

Mysql的默认隔离级别--可重复读

为什么索引底层不用B(B-)树要用B+树

  • B-树的每一个节点,存了关键字和对应的数据地址,而B+树的非叶子节点只存关键字,不存数据地址。因此B+树的每一个非叶子节点存储的关键字是远远多于B-树的,B+树的叶子节点存放关键字和数据,因此,从树的高度上来说,B+树的高度要小于B-树,使用的磁盘I/O次数少,因此查询会更快一些
  • B-树由于每个节点都存储关键字和数据,因此离根节点进的数据,查询的就快,离根节点远的数据,查询的就慢;B+树所有的数据都存在叶子节点上,因此在B+树上搜索关键字,找到对应数据的时间是比较平均的,没有快慢之分
  • 在B-树上如果做区间查找,遍历的节点是非常多的;B+树所有叶子节点被连接成了有序链表结构,因此做整表遍历和区间查找是非常容易的

mysql中having和where的区别

where和having都可以使用的场景

select goods_price,goods_name from sw_goods where goods_price > 100
select goods_price,goods_name from sw_goods having goods_price > 100

只能用where,不能用having

select goods_name,goods_number from sw_goods where goods_price > 100
select goods_name,goods_number from sw_goods having goods_price > 100 
//报错!!!因为前面并没有筛选出goods_price 字

只能用having不能用where

select goods_category_id , avg(goods_price) as ag from sw_goods group by goods_category having ag > 1000
select goods_category_id , avg(goods_price) as ag from sw_goods where ag>1000 group by goods_category //报错!!因为from sw_goods 这张数据表里面没有ag这个字段

Sql的执行顺序

select distinct 
        <select_list>
from
    <left_table><join_type>
join <right_table> on <join_condition>
where
    <where_condition>
group by
    <group_by_list>
having
    <having_condition>
order by
    <order_by_condition>
limit <limit number>
1、from <left_table><join_type>
2、on <join_condition>
3、<join_type> join <right_table>
4、where <where_condition>
5、group by <group_by_list>
6、having <having_condition>
7、select
8、distinct <select_list>
9、order by <order_by_condition>
10、limit <limit_number>

sql调优?explain关键字?

关注type属性:all<index<range<ref<eq_ref<const<system,一般保证查询等级至少达到range,至少要是ref

关注extra属性中的:

  • Using filesort:说明Mysql中无法利用索引完成的排序操作称为文件排序
  • Using temporary:表示使用了临时表储存了中间结果
  • Using index:表示用到了索引

10亿个手机号查找,如何存储?

设置手机号phone为主键(因为不设置为主键Innodb会自动生成一个,浪费空间),用户肯触发插入和删除操作,需要使用Innodb引擎,手机号不更新,不同国家的手机号位数不同,使用varchar类型,,可以按照范围进行分区,对于查询某个手机号是否存在可以加上布隆过滤器(bitmap)提高效率

三个线程交替循环打印?

 //num=1就是线程1执行,num=2就是线程2执行,num=3就是线程3执行
    private int num = 1;
    private Lock lock = new ReentrantLock();//创建锁对象
    private Condition condition1 = lock.newCondition();//条件1
    private Condition condition2 = lock.newCondition();//条件2
    private Condition condition3 = lock.newCondition();//条件3

    public void sub1() {
        try {
            lock.lock();//开启锁
            while (num != 1) {//这里jdk源码里推荐用while,因为有可能出现虚假唤醒,所以要再次确认
                try {
                    condition1.await();//条件1线程等待,并释放锁
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
            //运行到这里,说明num=1
            num = 2;
            Thread.sleep(1000);
            System.out.println("线程1运行完毕");
            condition2.signal();//唤醒条件2线程
        } catch (InterruptedException e) {
            e.printStackTrace();
        } finally {
            lock.unlock();//释放锁
        }

    }

    public void sub2() {
        try {
            lock.lock();//开启锁
            while (num != 2) {//这里jdk源码里推荐用while,因为有可能出现虚假唤醒,所以要再次确认
                try {
                    condition2.await();//条件2线程等待,并释放锁
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
            //如果线程执行到这里,说明num=2
            num = 3;
            Thread.sleep(1000);
            System.out.println("线程2运行完毕");
            condition3.signal();//唤醒条件3线程
        } catch (InterruptedException e) {
            e.printStackTrace();
        } finally {
            lock.unlock();//释放锁
        }

    }

    public void sub3() {
        try {
            lock.lock();//开启锁
            while (num != 3) {//这里jdk源码里推荐用while,因为有可能出现虚假唤醒,所以要再次确认
                try {
                    condition3.await();//条件3线程等待,并释放锁
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
            //如果线程执行到这里,说明num=3
            num = 1;
            Thread.sleep(1000);
            System.out.println("线程3运行完毕");
            condition1.signal();//唤醒条件1线程
        } catch (InterruptedException e) {
            e.printStackTrace();
        } finally {
            lock.unlock();//释放锁
        }

    }

    public static void main(String[] args) {
        Parallel parallel = new Parallel();
        new Thread(parallel::sub1).start();
        new Thread(parallel::sub2).start();
        new Thread(parallel::sub3).start();

    }
上一篇:3.Linux下的输入输出管理及如何获得帮助


下一篇:Java官方教程(六-1)Classes 类 & Declaring Classes 声明类(2021.2.9)