Java面试突击3(1):Java基础面试

x33g5p2x  于2021-10-14 转载在 Java  
字(6.4k)|赞(0)|评价(0)|浏览(341)

1、HashMap如何扩容?

底层是⼀个数组,当这个数组满了之后,他就会⾃动进⾏扩容,变成⼀个更⼤的数组,让你在⾥⾯可以去放更多的元素
2倍扩容

[16位的数组,<> -> <> -> <>]
[32位的数组,<> -> <>, <>]

数组长度=16
n - 1      0000 0000 0000 0000 0000 0000 0000 1111

hash1     1111 1111 1111 1111 0000 1111 0000 0101
&结果    0000 0000 0000 0000 0000 0000 0000 0101    = 5(index = 5的位置)

n - 1      0000 0000 0000 0000 0000 0000 0000 1111
hash2     1111 1111 1111 1111 0000 1111 0001 0101

&结果    0000 0000 0000 0000 0000 0000 00000101 = 5(index = 5的位置)
在数组长度为16的时候,他们两个hash值的位置是⼀样的,⽤链表来处理,出现⼀个hash冲突的问题

如果数组的长度扩容之后= 32,重新对每个hash值进⾏寻址,也就是⽤每个hash值跟新数组的length - 1进⾏与操作
n-1        0000 0000 0000 0000 0000 0000 00011111

hash1     1111 1111 1111 1111 0000 1111 0000 0101
&结果    0000 0000 0000 0000 0000 0000 0000 0101 = 5(index = 5的位置)

n-1        0000 0000 0000 0000 0000 0000 00011111
hash2     1111 1111 1111 1111 0000 1111 0001 0101

&结果    0000 0000 0000 0000 0000 0000 00010101 = 21(index = 21的位置)
**        判断⼆进制结果中是否多出⼀个bit的1,如果没多,那么就是原来的index,如果多了出来,那么就是index + oldCap,通过这个⽅式,**就避免了rehash的时候,⽤每个hash对新数组.length取模,取模性能不⾼,位运算的性能⽐较⾼。

2、高频并发问题

synchronized实现原理、CAS⽆锁化的原理、AQS是什么、Lock锁、ConcurrentHashMap的分段加锁的原理、线程池的原理、java内存模型、volatile说⼀下吗、对java并发包有什么了解?⼀连串的问题。

3、synchronized关键字的底层原理是什么?

其实synchronized底层的原理,是跟jvm指令和monitor有关系的
你如果⽤到了synchronized关键字,在底层编译后的jvm指令中,会有monitorenter和monitorexit两个指令monitorenter

//代码对应的指令
monitorexit

那么monitorenter指令执⾏的时候会⼲什么呢?
        每个对象都有⼀个关联的monitor,⽐如⼀个对象实例就有⼀个monitor,⼀个类的Class对象也有⼀个monitor,如果要对这个对象加锁,那么必须获取这个对象关联的monitor的lock锁。

**原理:**monitor⾥⾯有⼀个计数器,从0开始的。如果⼀个线程要获取monitor的锁,就看看他的计数器是不 是0,如果是0的话,那么说明没⼈获取锁,他就可以获取锁了,然后对计数器加1。

这个monitor的锁是⽀持重⼊加锁的,什么意思呢,好⽐下⾯的代码⽚段

// 线程1
synchronized(myObject) {  -> 类的class对象来⾛的
// ⼀⼤堆的代码
synchronized(myObject) {
// ⼀⼤堆的代码
}
}

加锁,⼀般来说都是必须对⼀个对象进⾏加锁。

        如果⼀个线程第⼀次synchronized那⾥,获取到了myObject对象的monitor的锁,计数器加1,然后第⼆次synchronized那⾥,会再次获取myObject对象的monitor的锁,这个就是重⼊加锁了,然后计数器会再次加1,变成2。
        这个时候,其他的线程在第⼀次synchronized那⾥,会发现说myObject对象的monitor锁的计数器是⼤于0的,意味着被别⼈加锁了,然后此时线程就会进⼊block阻塞状态,什么都⼲不了,就是等着获取锁 。

        接着如果出了synchronized修饰的代码⽚段的范围,就会有⼀个monitorexit的指令,在底层。此时获取锁的线程就会对那个对象的monitor的计数器减1,如果有多次重⼊加锁就会对应多次减1,直到最后,计数器是0 。
        然后后⾯block住阻塞的线程,会再次尝试获取锁,但是只有⼀个线程可以获取到锁。

4、你对CAS的理解以及其底层实现原理可以吗?

多个线程他们可能要访问同⼀个数据  HashMap map = new HashMap();

此时有多个线程要同时读写类似上⾯的这种内存⾥的数据,此时必然出现多线程的并发安全问题。

4.1、synchronized

        synchronized(map) {

            // 对map⾥的数据进⾏复杂的读写处理

      }

        此时,synchronized意思就是针对当前执⾏这个⽅法的myObject对象进⾏加锁,只有⼀个线程可以成功的对myObject加锁,可以对他关联的monitor的计数器去加1,加锁,⼀旦多个线程并发的去进⾏synchronized加锁,串⾏化,效率并不是太⾼,很多线程,都需要排队去执⾏。

4.2、CAS

        CAS在底层的硬件级别给你保证⼀定是原⼦的,同⼀时间只有⼀个线程可以执⾏CAS,先⽐较再设置,其他的线程的CAS同时间去执⾏此时会失败。

线程1和线程分别读取0到本地内存。线程1货渠到0后+1,然后比较本地值0和内存值0相等,则更新内存值为1。线程2比较本地值0和内存值1不等则更新失败。则尝试设置为2成功。

5、ConcurrentHashMap实现线程安全的底层原理到底是什么?

多个线程要访问同⼀个数据,synchronized加锁,CAS去进⾏安全的累加,去实现多线程场景下的安全的更新⼀个数据的效果,⽐较多的⼀个场景下,可能就是多个线程同时读写⼀个HashMap。使用synchronized,也没这个必要。

 实现1

HashMap的⼀个底层的原理,本⾝是⼀个⼤的⼀个数组,[有很多的元素]        Map map = new Map();

        多个线程过来,线程1要put的位置是数组[5],线程2要put的位置是数组[21]
        map.put(xxxxx,xxx);

        明显不好,数组⾥有很多的元素,插入不同位置时可以并发插入的。所以除⾮是对同⼀个元素执⾏put操作,此时呢需要多线程是需要进⾏同步的。

 实现2

        JDK并发包⾥推出了⼀个ConcurrentHashMap,他默认实现了线程安全性。

**        在JDK 1.7及之前的版本⾥,**分段 即将原数组拆分为多个数组,每个数组分别加锁: [数组1] , [数组2],[数组3] ->每个数组都对应⼀个锁分段加锁,多个线程过来,线程1要put的位置是数组1[5],线程2要put的位置是数组2[21]。此时对于不同线程在不同数组中插入数据没有冲突。

   **     JDK 1.8以及之后**,做了⼀些优化和改进,锁粒度的细化 。[⼀个⼤的数组],数组⾥每个元素进⾏put操作,都是有⼀个不同的锁,刚开始进⾏put的时候,如果两个线程都是在数组[5]这个位置进⾏put,这个时候,对数组[5]这个位置进⾏put的时候,采取的是CAS的策略。

        同⼀个时间,只有⼀个线程能成功执⾏这个CAS,就是说他刚开始先获取⼀下数组[5]这个位置的值,null,然后执⾏CAS,线程1⽐较⼀下put进去我的这条数据,同时间其他的线程执⾏CAS,都会失败。
        分段加锁,通过对数组每个元素执⾏CAS的策略,如果是很多线程对数组⾥不同的元素执⾏put,⼤家是没有关系的,如果其他⼈失败了,其他⼈此时会发现说,数组[5]这位置,已经给刚才⼜⼈放进去值了。就需要在这个位置基于链表+红⿊树来进⾏处理,synchronized(数组[5]),加锁,基于链表或者是红⿊树在这个位置插进去⾃⼰的数据。

        如果你是对数组⾥同⼀个位置的元素进⾏操作,才会加锁串⾏化处理;如果是对数组不同位置的元素操作,此时⼤家可以并发执⾏的。

总结:

首先对整个数组加锁  ------》  对数组分段加锁  ------》   对每个元素加锁。

6、你对JDK中的AQS理解吗?AQS的实现原理是什么?

        多线程同时访问⼀个共享数据,可以使用sychronized,CAS,ConcurrentHashMap(并发安全的数据结构可以来⽤),Lock等方式处理。

        synchronized就有点不⼀样了,底层基于 AQS,Abstract Queue Synchronizer,抽象队列同步器 。以及Semaphore、其他⼀些的并发包下的都是基于AQS。
ReentrantLock lock = new ReentrantLock(true);  => ⾮公平锁

//多个线程过来,都尝试

lock.lock();
lock.unlock();

**原理分析:**AQS内部的重要变量state=0。此时有多个线程都执行lock.lock()进行加锁。

  •  首先线程1和线程2通过CAS去更新state的值为1, 同一时间线程1执行成功(线程1获取state0值,比较0与0相等则更新AQS中的state为1)。此时线程1加锁成功线程2加锁失败。
  •  然后AQS内部有一个加锁线程记录加锁成功的线程(加锁线程=线程1)。
  •  然后AQS内部有一个等待队列记录加锁失败的线程,故线程2进入等待队列。
  • 此时线程1执行成功后释放锁,将state=0,加锁线程=null,唤醒等待队列队头的线程即线程2,线程2开始CAS更新state=1和加锁线程=线程2执行。。。

公平锁和非公平锁

**非公平锁:**当上图线程2被线程1唤醒后,此时线程3直接先进行CAS将state=1和加锁线程=线程3。然后线程2进行CAS失败继续进入等待队列。故此时线程2时不公平的。
**公平锁:**当上图线程2被线程1唤醒后,此时线程3首先会判断队列:发现队列有线程2,线程3直接进入等待队列排队。

总结

state变量 -> CAS ->失败后进⼊队列等待->释放锁后唤醒   公平与非公平

7、线程池

7.1、说说线程池的底层⼯作原理可以吗?

        系统是不可能说让他⽆限制的创建很多很多的线程的,会构建⼀个线程池,有⼀定数量的线程,让他们执⾏各种各样的任务,线程执⾏完任务之后,不要销毁掉⾃⼰,继续去等待执⾏下⼀个任务。

ExecutorService threadPool = Executors.newFixedThreadPool(3) -> 3: corePoolSize

threadPool.submit(new Callable() {
       public void run() {}

});

  •  提交任务,先看⼀下线程池⾥的线程数量是否⼩于corePoolSize,也就是3,如果⼩于,直接创建⼀个线程出来执⾏你的任务。

  • 如果执⾏完你的任务之后,这个线程是不会死掉,他会尝试从⼀个⽆界LinkedBlockingQueue⾥获取新的任务,如果没有新的任务,此时就会阻塞住,等待新的任务到来。

  • 你持续提交任务,上述流程反复执⾏,只要线程池的线程数量⼩于corePoolSize,都会直接创建新线程来执⾏这个任务,执⾏完了就尝试从⽆界队列⾥获取任务,直到线程池⾥有corePoolSize个线程。

  •  接着再次提交任务,会发现线程数量已经跟corePoolSize⼀样⼤了,此时就直接把任务放⼊队列中就可以了,线程会争抢获取任务执⾏的,如果所有的线程此时都在执⾏任务,那么⽆界队列⾥的任务就可能会越来越多。

  • newFixedThreadPool的队列是LinkedBlockingQueue,一个⽆界阻塞队列。

7.2、线程池的核⼼配置参数都是⼲什么的?平时我们应该怎么⽤?

代表线程池的类是ThreadPoolExecutor。

        创建⼀个线程池参数corePoolSize,maximumPoolSize,keepAliveTimequeue,如果你不⽤fixed之类的线程池,⾃⼰完全可以通过这个构造函数就创建⾃⼰的线程池。corePoolSize:3

maximumPoolSize:Integer.MAX_VALUE
keepAliveTime:60s

new ArrayBlockingQueue<Runnable>(200)

        如果说你把queue做成有界队列,⽐如说new ArrayBlockingQueue<Runnable>(200),那么假设corePoolSize个线程都在繁忙的⼯作,⼤量任务进⼊有界队列,队列满了,此时怎么办?

        这个时候假设你的maximumPoolSize是⽐corePoolSize⼤的,此时会继续创建额外的线程放

⼊线程池⾥,来处理这些任务,然后超过corePoolSize数量的线程如果处理完了⼀个任务也会尝试
从队列⾥去获取任务来执⾏。

        如果额外线程都创建完了去处理任务,队列还是满的,此时还有新的任务来怎么办?
        只能reject掉,他有⼏种reject策略,可以传⼊RejectedExecutionHandler。(1)AbortPolicy  

(2)DiscardPolicy  
(3)DiscardOldestPolicy 

 (4)CallerRunsPolicy  
(5)⾃定义

        如果后续慢慢的队列⾥没任务了,线程空闲了,超过corePoolSize的线程会⾃动释放掉,在keepAliveTime之后就会释放。

根据上述原理去定制⾃⼰的线程池,考虑到corePoolSize的数量,队列类型,最⼤线程数量,拒绝策略,线程释放时间

⼀般⽐较常⽤的是:fixed线程。

7.3、如果在线程池中使⽤⽆界阻塞队列会发⽣什么问题?

⾯试题:在远程服务异常的情况下,使⽤⽆界阻塞队列,是否会导致内存异常飙升?

        当线程调用远程服务调⽤超时,导致任务处理很慢,而任务进来会很快导致 。队列变得越来越⼤,此时会导致内存飙升起来,⽽且还可能会导致你会OOM,内存溢出。

7.4、如果线程池的队列满了之后,会发⽣什么事情吗?

有界队列,可以避免内存溢出。corePoolSize: 10

maximumPoolSize : 200
ArrayBlockingQueue(200)

        ⾃定义⼀个reject策略,如果线程池⽆法执⾏更多的任务了,此时建议你可以把这个任务信息持久化写⼊磁盘⾥去,后台专门启动⼀个线程,后续等待你的线程池的⼯作负载降低了,他可以慢慢的从磁盘⾥读取之前持久化的任务,重新提交到线程池⾥去执⾏。
       

         你可以⽆限制的不停的创建额外的线程出来,⼀台机器上,有⼏千个线程,甚⾄是⼏万个线程,每个线程都有⾃⼰的栈内存,占⽤⼀定的内存资源,会导致内存资源耗尽,系统也会崩溃掉
即使内存没有崩溃,会导致你的机器的cpu load,负载,特别的⾼。

7.5、如果线上机器突然宕机,线程池的阻塞队列中的请求怎么办?

        必然会导致线程池⾥的积压的任务实际上来说都是会丢失的。如果说你要提交⼀个任务到线程池⾥去,在提交之前,⿇烦你先在数据库⾥插⼊这个任务的信息,更新他的状态:未提交、已提交、已完成。提交成功之后,更新他的状态是已提交状态

        系统重启,后台线程去扫描数据库⾥的未提交和已提交状态的任务,可以把任务的信息读取出来,重新提交到线程池⾥去,继续进⾏执⾏。

总结

无界队列不断产生任务:导致内存飙升,内存溢出,OOM

有界队列不断创建线程:线程栈导致内存资源耗尽,cpu负载高。

8、谈谈你对Java内存模型的理解可以吗?

相关文章