记录一次京东面试吧,面试的是虚拟平台的经验.

下午去的京东面试,虽然工作了五年,但是面试的时候还是怂了,哈哈。感觉自己还是什么都不会。但是面试中面试官说了一句话:所有都技术都是需要和场景结合起来。
接下来聊一聊问题吧:
正常流程:现自我介绍,然后聊一聊做过的项目。然后就开始聊Java基本功啦。 问题记得不是那么的全,想起哪个就写哪个啦。答案是找的。
创建线程有哪些方式?

1.继承Thread类创建线程类
(1)定义Thread类的子类,并重写run()方法,该run方法的方法体就代表了线程要完成的任务。因此把run() 方法称为执行体。
(2)创建Thread子类的实例,即创建了线程对象。
(3)调用线程对象的start()方法来启动该线程。
2.通过Runnable接口创建线程类
(1)定义runnable接口的实现类,并重写该接口的run方法,该run方法的方法体同样是该线程的线程执行体。
(2)创建Runnable实现类的实例,并依次实例作为Thread的target来创建Thread对象,该Thread对象才是真正的线程对象。
(3)调用线程对象的start()方法来创建该线程。
3. 通过Callable和Future创建线程
(1)创建Callable接口的实现类,并实现call()方法,该call()方法将作为线程执行体,并且有返回值。
(2)创建Callable实现类的实例,使用FutureTask类来包装Callable对象,该FutureTask对象封装了该Callable对象的call()方法的返回值。
(3)使用FutureTask对象最为Thread对象的target创建并启动新线程。
(4)调用FutureTask对象的get()方法来获得自线程执行结束后的返回值。
采用实现Runnable、Callable接口的方式创建线程时,
优势是:
线程类只是实现了Runnable接口或Callable接口,还可以继承其他类。
这种方式下,多线程可以共享一个target对象,所以非常适合多个相同线程来处理同一份资源的情况,从而可以将CPU、代码和数据分开,形成清晰的模型,较好地体现了面向对象的思想。
劣势是:
线程稍微复杂,如果要访问当前线程,则必须使用Thread.currentThread()方法。
使用继承Thread类的方式创建多线程时,
优势是:
编写简单,如果需要访问当前线程,则无需使用Thread.currentThread()方法,直接使用this即可获得当前线程。
劣势是:
线程类已经继承了Thread类,所以不能再继承其他父类。


如何控制所有线程执行完成后再进行其他的操作?

用CountDownLatch和CyclicBarrier都可以控制线程执行完成。

CountDownLatch内部维护了一个整数n,n(要大于等于0)在当前线程初始化CountDownLatch方法指定。当前线程调用CountDownLatch的await()方法阻塞当前线程,等待其他调用CountDownLatch对象的CountDown()方法的线程执行完毕。其他线程调用该CountDownLatch的CountDown()方法,该方法会把n-1,直到所有线程执行完成,n等于0,当前线程就恢复执行。

CyclicBarrier是一个同步线程辅助类,允许一组线程互相等待,直到到达某个公共屏障点(common barrier point)。因为该barrier在释放等待线程后可以重用,所以称它为循环的barrier。


线程池的原理:

当提交一个新任务到线程池时,线程池的处理流程如下。
(1)线程池判断核心线程池里的线程是否都在执行任务。如果不是,则创建一个新的工作线程来执行任务。如果核心线程池里的线程都在执行任务,则进行下个流程。
(2)线程池判断工作队列是否已经满。如果工作队列没有满,则将新提交的任务存储在这个工作队列里。如果工作队列满了,则进入下个流程。
(3)线程池判断线程池的线程是否都处于工作状态。如果没有,则创建一个新的工作线程来执行任务。如果已经满了,则交给饱和策略来处理这个任务。


你在项目中用过多线程吗?用过锁来解决问题吗?

我收到这个问题一下子懵啦,回来想了一些,比如导出的时候,先用Spring的RPC调用导出方法,导出方法用多线程来封装数据,导出的数据发送到MQ中异步生成导出文件。
比如数据对接的时候采用多线程来接受数据,对数据处理的时候需要加锁。
当所有线程执行完成后再执行的,需要用到CountDownLatch或者CyclicBarrier
当测试时候,需要用信号量(Semaphone)来一次开多个线程.


HashMap工作原理

HashMap基于hashing原理,我们通过put()和get()方法存储和获取对象。当我们将键值对传递给put()方法时,它调用键对象的hashCode()方法来计算hashcode,然后找到bucket位置来储存值对象。当获取对象时,通过键对象的equals()方法找到正确的键值对,然后返回对象。HashMap使用链表来解决碰撞问题,当发生碰撞了,对象将会存在链表的下一个节点中。HashMap在每个链表节点中存储键值对对象。


HashMap 在 JDK1.8中的代码:

    public V put(K key, V value) {
        return putVal(hash(key), key, value, false, true);
    }
    /**
     * Implements Map.put and related methods
     *
     * @param hash hash for key
     * @param key the key
     * @param value the value to put
     * @param onlyIfAbsent if true, don't change existing value
     * @param evict if false, the table is in creation mode.
     * @return previous value, or null if none
     */
    final V putVal(int hash, K key, V value, boolean onlyIfAbsent, boolean evict) {
        Node<K,V>[] tab; Node<K,V> p; int n, i;
        //空
        if ((tab = table) == null || (n = tab.length) == 0)
            n = (tab = resize()).length;
        //这个key的链表是空的时候
        if ((p = tab[i = (n - 1) & hash]) == null)
            tab[i] = newNode(hash, key, value, null);
        else {
            Node<K,V> e; K k;
            //链表的第一个节点和插入的节点相同的时候,直接覆盖value
            if (p.hash == hash &&
                ((k = p.key) == key || (key != null && key.equals(k))))
                e = p;
            //此处采用红黑树
            else if (p instanceof TreeNode)
                e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
            else {
                //循环判断链表是否存在这个key
                for (int binCount = 0; ; ++binCount) {
                    if ((e = p.next) == null) {
                        p.next = newNode(hash, key, value, null);
                        //TREEIFY_THRESHOLD=8 如果链表大于8就转向红黑树
                        if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
                            treeifyBin(tab, hash);
                        break;
                    }
                    //如果碰到key相同就跳出循环
                    if (e.hash == hash &&
                        ((k = e.key) == key || (key != null && key.equals(k))))
                        break;
                    p = e;
                }
            }
            //如果是存在,就返回之前的value值。
            if (e != null) { // existing mapping for key
                V oldValue = e.value;
                if (!onlyIfAbsent || oldValue == null)
                    e.value = value;
                afterNodeAccess(e);
                return oldValue;
            }
        }
        ++modCount;
        //长度超长就扩容
        if (++size > threshold)
            resize();
        afterNodeInsertion(evict);
        return null;
    }

ConcurrentHashMap工作原理及代码实现

这是1.7中的概念。
ConcurrentHashMap采用了非常精妙的分段锁策略,ConcurrentHashMap的主干是个Segment数组。Segment继承了ReentrantLock,所以它就是一种可重入锁(ReentrantLock)。在ConcurrentHashMap中,一个Segment就是一个子哈希表,Segment里维护了一个HashEntry数组,并发环境下,对于不同的Segment的数据进行操作是不用考虑锁竞争的。
JDK1.8中,它摒弃了Segment(锁段)的概念,而是启用了一种全新的方式实现,利用CAS算法


ConcurrentHashMap 在JDK1.8中的代码:

    public V put(K key, V value) {
        return putVal(key, value, false);
    }
    final V putVal(K key, V value, boolean onlyIfAbsent) {
        //不允许为空
        if (key == null || value == null) throw new NullPointerException();
        //取到key的hash值
        int hash = spread(key.hashCode());
        int binCount = 0;

        for (Node<K,V>[] tab = table;;) {
            //如果是空,则初始化。
            if (tab == null || (n = tab.length) == 0)
                tab = initTable();
            //如果这个链表是空采用CAS的方式将新的Node放入到这个Key对应的链表中。
            else if ((f = tabAt(tab, i = (n - 1) & hash)) == null) {
                if (casTabAt(tab, i, null, new Node<K,V>(hash, key, value, null)))
                    break;                   // no lock when adding to empty bin
            }
            //static final int MOVED     = -1;
            //如果检测到正在扩容,则帮助其扩容
            else if ((fh = f.hash) == MOVED)
                tab = helpTransfer(tab, f);
            else {
                V oldVal = null;
                //锁住头节点
                synchronized (f) {
                    //i = (n - 1) & hash 查看是否是头节点
                    if (tabAt(tab, i) == f) {
                        if (fh >= 0) {
                            binCount = 1;
                            for (Node<K,V> e = f;; ++binCount) {
                                K ek;
                                //如果节点存在,就覆盖之前的value
                                if (e.hash == hash &&
                                    ((ek = e.key) == key ||
                                     (ek != null && key.equals(ek)))) {
                                    oldVal = e.val;
                                    if (!onlyIfAbsent)
                                        e.val = value;
                                    break;
                                }
                                Node<K,V> pred = e;
                                //节点不存在就将新创建一个绩点放到后面。
                                if ((e = e.next) == null) {
                                    pred.next = new Node<K,V>(hash, key,
                                                              value, null);
                                    break;
                                }
                            }
                        }
                        //是否是树节点,TreeBin是对TreeNode的封装但是自身继承了Node.
                        else if (f instanceof TreeBin) {
                            Node<K,V> p;
                            binCount = 2;
                            //如果红黑树中存在此节点
                            if ((p = ((TreeBin<K,V>)f).putTreeVal(hash, key, value)) != null) {
                                oldVal = p.val;
                                //这个可以自己规定是否用新值替换旧值
                                if (!onlyIfAbsent)
                                    p.val = value;
                            }
                        }
                    }
                }
                if (binCount != 0) {
                    //TREEIFY_THRESHOLD = 8; 是否采用红黑树的形势插入
                    if (binCount >= TREEIFY_THRESHOLD)
                        treeifyBin(tab, i);
                    if (oldVal != null)
                        return oldVal;
                    break;
                }
            }
        }
        //采用红黑树的方式减少数据重排带来的消耗
        addCount(1L, binCount);
        return null;
    }

synchronized 的实现原理

同步代码块是使用monitorenter和monitorexit指令实现的,同步方法(在这看不出来需要看JVM底层实现)依靠的是方法修饰符上的ACC_SYNCHRONIZED实现。


什么是乐观锁和悲观锁

乐观锁(Optimistic Lock),顾名思义,就是很乐观,每次去拿数据的时候都认为别人不会修改,所以不会上锁,但是在提交更新的时候会判断一下在此期间别人有没有去更新这个数据。乐观锁适用于读多写少的应用场景,这样可以提高吞吐量。
乐观锁:假设不会发生并发冲突,只在提交操作时检查是否违反数据完整性。
乐观锁一般来说有以下2种方式:
1。使用数据版本(Version)记录机制实现,这是乐观锁最常用的一种实现方式。何谓数据版本?即为数据增加一个版本标识,一般是通过为数据库表增加一个数字类型的 “version” 字段来实现。当读取数据时,将version字段的值一同读出,数据每更新一次,对此version值加一。当我们提交更新的时候,判断数据库表对应记录的当前版本信息与第一次取出来的version值进行比对,如果数据库表当前版本号与第一次取出来的version值相等,则予以更新,否则认为是过期数据。
2。使用时间戳(timestamp)。乐观锁定的第二种实现方式和第一种差不多,同样是在需要乐观锁控制的table中增加一个字段,名称无所谓,字段类型使用时间戳(timestamp), 和上面的version类似,也是在更新提交的时候检查当前数据库中数据的时间戳和自己更新前取到的时间戳进行对比,如果一致则OK,否则就是版本冲突。
Java JUC中的atomic包就是乐观锁的一种实现,AtomicInteger 通过CAS(Compare And Set)操作实现线程安全的自增。

悲观锁(Pessimistic Lock),顾名思义,就是很悲观,每次去拿数据的时候都认为别人会修改,所以每次在拿数据的时候都会上锁,这样别人想拿这个数据就会block直到它拿到锁。
悲观锁:假定会发生并发冲突,屏蔽一切可能违反数据完整性的操作。
Java synchronized 就属于悲观锁的一种实现,每次线程要修改数据时都先获得锁,保证同一时刻只有一个线程能操作数据,其他线程则会被block。


MySql中的InnoDB与MyISAM区别:

  1. InnoDB支持事务,MyISAM不支持,对于InnoDB每一条SQL语言都默认封装成事务,自动提交,这样会影响速度,所以最好把多条SQL语言放在begin和commit之间,组成一个事务;
  2. InnoDB支持外键,而MyISAM不支持。对一个包含外键的InnoDB表转为MYISAM会失败;
  3. InnoDB是聚集索引,数据文件是和索引绑在一起的,必须要有主键,通过主键索引效率很高。但是辅助索引需要两次查询,先查询到主键,然后再通过主键查询到数据。因此,主键不应该过大,因为主键太大,其他索引也都会很大。而MyISAM是非聚集索引,数据文件是分离的,索引保存的是数据文件的指针。主键索引和辅助索引是独立的。
  4. Innodb不支持全文索引,而MyISAM支持全文索引,查询效率上MyISAM要高;
  5. InnoDB中不保存表的具体行数,也就是说,执行select count() from table时,InnoDB要扫描一遍整个表来计算有多少行,但是MyISAM只要简单的读出保存好的行数即可,注意的是,当count()语句包含where条件时,两种表的操作是一样的。
  6. 对于AUTO_INCREMENT类型的字段,InnoDB中必须包含只有该字段的索引,但是在MyISAM表中,可以和其他字段一起建立联合索引。
  7. DELETE FROM table时,InnoDB不会重新建立表,而是一行一行的删除。
  8. LOAD TABLE FROM MASTER操作对InnoDB是不起作用的,解决方法是首先把InnoDB表改成MyISAM表,导入数据后再改成InnoDB,但是对于使用的额外的InnoDB特性(例如外键)的表不适用。
  9. InnoDB表的行锁也不是绝对的,假如在执行一个SQL语句时MySQL不能确定要扫描的范围,InnoDB表同样会锁全表,例如update table set num=1 where name like “%aaa%”

equals相等的时候,hashCode是否相等,hashCode相同的时候,equals是否相同

下面是IDEA生成的equals和hashCode的代码,只有一个name属性:
    @Override
    public boolean equals(Object o) {
        if (this == o) return true;
        if (o == null || getClass() != o.getClass()) return false;
        Test test = (Test) o;
        return name != null ? name.equals(test.name) : test.name == null;
    }

    @Override
    public int hashCode() {
        return name != null ? name.hashCode() : 0;
    }

其实是实现的方法是不是随意呢?但是有意义么?
    public boolean equals(Object o) {
        return new Random().nextInt(100)%2==1;
    }
    public int hashCode() {
        return new Random().nextInt(100);
    }

SpringAOP的哪些场景用到了?

AOP用来封装横切关注点,具体可以在下面的场景中使用
Authentication 权限
Caching 缓存
Context passing 内容传递
Error handling 错误处理
Lazy loading 懒加载
Debugging  调试
logging, tracing, profiling and monitoring 记录跟踪 优化 校准
Performance optimization 性能优化
Persistence  持久化
Resource pooling 资源池
Synchronization 同步
Transactions 事务


elasticsearch 都有哪些索引

B-Tree索引,倒排索引,
http://blog.pengqiuyuan.com/ji-chu-jie-shao-ji-suo-yin-yuan-li-fen-xi/


elasticsearch 中的函数

https://www.elastic.co/guide/cn/elasticsearch/guide/current/foreword_id.html


JVM中的是怎么划分内存区域?

1、线程独有的内存区域
(1)PROGRAM COUNTER REGISTER,程序计数器
(2)JAVA STACK,虚拟机栈
(3)NATIVE METHOD STACK,方法栈
2、线程间共享的内存区域
(1)HEAP,堆
(2)METHOD AREA,方法区
(3)RUNTIME CONSTANT POOL,运行时常量池
3、直接内存


class文件和实例都存在哪个区域?

堆:
大多数应用,堆都是Java虚拟机所管理的内存中最大的一块,它在虚拟机启动时创建,此内存唯一的目的就是存放对象实例。由于现在垃圾收集器采用的基本都是分代收集算法,所以堆还可以细分为新生代和老年代,再细致一点还有Eden区、From Survivior区、To Survivor区,这个后面都会讲到的。
方法区:
这块区域用于存储虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码等数据,虚拟机规范是把这块区域描述为堆的一个逻辑部分的,但实际它应该是要和堆区分开的。从上面提到的分代收集算法的角度看,HotSpot中,方法区≈永久代。不过JDK 7之后,我们使用的HotSpot应该就没有永久代这个概念了,会采用Native Memory来实现方法区的规划了。
运行时常量池:
它是方法区的一部分。Class文件中除了有类的版本信息、字段、方法、接口等描述信息外,还有一项信息就是常量池,用于存放编译期间生成的各种字面量和符号引用,这部分内容将在类加载后进入方法区的运行时常量池中,另外翻译出来的直接引用也会存储在这个区域中。这个区域另外一个特点就是动态性,Java并不要求常量就一定要在编译期间才能产生,运行期间也可以在这个区域放入新的内容,String.intern()方法就是这个特性的应用。


SQL 优化之道:

一、一些常见的SQL实践
(1)负向条件查询不能使用索引
select from order where status!=0 and stauts!=1
not in/not exists都不是好习惯
可以优化为in查询:select from order where status in(2,3)

(2)前导模糊查询不能使用索引
select from order where desc like ‘%XX’
而非前导模糊查询则可以:select from order where desc like ‘XX%’

(3)数据区分度不大的字段不宜使用索引
select from user where sex=1
原因:性别只有男,女,每次过滤掉的数据很少,不宜使用索引。
经验上,能过滤80%数据时就可以使用索引。对于订单状态,如果状态值很少,不宜使用索引,如果状态值很多,能够过滤大量数据,则应该建立索引。

(4)在属性上进行计算不能命中索引
select from order where YEAR(date) < = ‘2017’
即使date上建立了索引,也会全表扫描,可优化为值计算:select from order where date < = CURDATE()
或者:select from order where date < = ‘2017-01-01’

二、并非周知的SQL实践
(5)如果业务大部分是单条查询,使用Hash索引性能更好,例如用户中心
select from user where uid=? select from user where login_name=?
原因:B-Tree索引的时间复杂度是O(log(n));Hash索引的时间复杂度是O(1)

(6)允许为null的列,查询有潜在大坑
单列索引不存null值,复合索引不存全为null的值,如果列允许为null,可能会得到“不符合预期”的结果集
select from user where name != ‘shenjian’
如果name允许为null,索引不存储null值,结果集中不会包含这些记录。所以,请使用not null约束以及默认值。

(7)复合索引最左前缀,并不是值SQL语句的where顺序要和复合索引一致
用户中心建立了(login_name, passwd)的复合索引
select from user where login_name=? and passwd=?
select from user where passwd=? and login_name=?
都能够命中索引
select from user where login_name=?
也能命中索引,满足复合索引最左前缀
select from user where passwd=?
不能命中索引,不满足复合索引最左前缀

(8)使用ENUM而不是字符串
ENUM保存的是TINYINT,别在枚举中搞一些“中国”“北京”“技术部”这样的字符串,字符串空间又大,效率又低。

三、小众但有用的SQL实践
(9)如果明确知道只有一条结果返回,limit 1能够提高效率
select from user where login_name=?
可以优化为:
select from user where login_name=? limit 1
原因:你知道只有一条结果,但数据库并不知道,明确告诉它,让它主动停止游标移动

(10)把计算放到业务层而不是数据库层,除了节省数据的CPU,还有意想不到的查询缓存优化效果
select from order where date < = CURDATE()
这不是一个好的SQL实践,应该优化为:
curDate=date(Ymd); c u r D a t e = d a t e ( ‘ Y − m − d ′ ) ; res = mysqlquery('select from order where date < = $curDate’);
原因:
释放了数据库的CPU
多次调用,传入的SQL相同,才可以利用查询缓存

(11)强制类型转换会全表扫描
select from user where phone=13800001234
你以为会命中phone索引么?大错特错了,这个语句究竟要怎么改?
末了,再加一条,不要使用select *(潜台词,文章的SQL都不合格 ==),只返回需要的列,能够大大的节省数据传输量,与数据库的内存使用量哟。
整理自:https://cloud.tencent.com/developer/article/1054203


秒杀的流程

https://www.ibm.com/developerworks/cn/web/wa-design-small-and-good-kill-system/index.html

https://www.zhihu.com/question/54895548


大概就记住了这些,就写这些吧。希望这些能够帮助到你,一些读源代码的事情,只是要自己看看。看源码的博客好也要和源码对应这看。这样才能真正理解。

如果哪些答案有问题,可以给我留言,我好修改。毕竟技术一直在更新。

  • 0
    点赞
  • 6
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值