100道高频题

*JAVA100道高频题(1)

最近看到有人总结了高频面试题,苦于没有答案,然后自己总结了一篇。
注意:因为内容太多,有错别字,自己总结的也不一定是正确的。欢迎大家指出错误之处!谢谢

1、JVM

1.1 jvm的内存结构

  1. 程序计数器:线程私有,是一个行号指示器。
  2. 虚拟机栈:线程私有,每个线程会创建一个栈幀,用于存放临时变量表、动态链接,操作数栈,方法出口等。
  3. 本地方法栈:和虚拟机栈类似的功能,不过这是为了本地方法服务(native).
  4. 堆:JVM管理的主要内存区域,存放字符串常量池,平时new的对象等。
  5. 方法区(元空间):又叫永久代,用于存放类加载的类信息。

1.2 JVM的那个区域不会发生内存溢出?

答:程序计数器

2 类加载器

2.1 JVM的类加载机制

类加载主要三个过程

  1. 加载:将类名讲字节流加载到内存中。
  2. 链接:分为三个阶段 (1):验证:确保加载的字节流符合JVM虚拟机的要求,不会损害虚拟机。(2):准备:对静态变量分配内存,并赋予初始值;(3):解析:将符号引用替换为直接饮用。
  3. 初始化:给静态变量赋值

2.2 class文件如何执行?(和上面一行,类加载机制)

2.3 为什么引入双亲委派机制?

  1. 三种类加载器,启动类加载器(BootStrap):加载lib下的jar包,扩展类加载器:加载/lib/ext的类,应用程序加载器:classPath下的类,平时我们说的类。

  2. 首先要确定的是:jvm如何区分两个实例类是否同一个类型?有以下两个

    1. 加载两个实例类的类名是相同的。
    2. 加载两个实例类的类名的加载器是同一个。
  3. 出于系统安全考虑,像系统类如Object核心类类,JVM需要保证他们生成的对象都会被认定为同一种类型。

  4. 所以能不能自己写个类叫做java.lang.System:通常不可以,因为使用双亲委派模型,它会先看看父类加载器能不能加载,当父类不能加载的时候,才会由子类去加载。系统的三个类加载器都只能加载特定路径下的类,所以我们可以通过自己的类加载器,去加载特定路径下的类,那么系统就无法加载,最终还是由我们自己的加载器加载。

2.4 什么情况下用自定义类加载器?

  1. 加密:java代码很容易被反编译,如果你需要把自己的代码进行加密,可以先将编译后的代码用某种加密算法加密,然后实现自己的类加载器,负责将这段加密后的代码还原。
  2. 从非标准的来源加载代码:例如你的部分字节码是放在数据库中甚至是网络上的,就可以自己写个类加载器,从指定的来源加载类。

3、写一段反射的代码


Constructor<HuangrySingleton> declaredConstructor = HuangrySingleton.class.getDeclaredConstructor();
declaredConstructor.setAccessible(true);


HuangrySingleton instance2 = declaredConstructor.newInstance();

4、GC

4.1、 那些阶段会发生STW现象?

  1. Serial垃圾收回阶段,Paralell垃圾回收阶段,CMS中初始标记和再次标记都会发生STWL

4.2 G1垃圾收集器为什么可停顿。

  1. 在垃圾回收的最开始有一个短暂的时间段(Inital Mark)会停止应用(stop-the-world)
  2. 然后应用继续运行,同时G1开始Concurrent Mark
  3. 再次停止应用,来一个Final Mark (stop-the-world)
  4. 最后根据Garbage First(优先处理那些垃圾多的内存块的意思)的原则,选择一些内存块进行回收。(stop-the-world)

所以:G1回收的第4步,它是“选择一些内存块”,而不是整代内存来回收,这是G1跟其它GC非常不同的一点,其它GC每次回收都会回收整个Generation的内存(Eden, Old), 而回收内存所需的时间就取决于内存的大小,以及实际垃圾的多少,所以垃圾回收时间是不可控的;而G1每次并不会回收整代内存,到底回收多少内存就看用户配置的暂停时间,配置的时间短就少回收点,配置的时间长就多回收点,伸缩自如。

注意:CMS收集器和G1收集器 他们的优缺点对比 G1只有并发标记才不会stop-the-world 其他都会停下来(阿里多次问到)

4.3 GC的工作流程

回答各种垃圾收集器的工作流程。

4.4、FULL GC发生具体场景。

  1. 调用System.gc
  2. 老年代空间不足
  3. 方法区空间不足
  4. 通过Minor GC进入老年代的平均内存大小大于老年代的可用内存大小;
  5. 从Survivor from 复制到 to空间的大对象,大对象大于to的可用内存,在进入老年代后,也大于老年代内存的大小;

4.5、如何打印GC日志?

使用虚拟机参数-XX:+PrintGCDetails

4.6、经常FULL GC怎么定位到错误?

  1. 查看gc日志,发现old区fgc后大小没有变化,如下图:.
  2. 去线上dump内存看是什么对象,用memory analyzer分析(dump文件,然后下载到本地通过jvisualvm分析对象的引用链的方式来定位具体频繁创建对象的地方)

4.7、GC可达性分析怎么做,哪些可以作为GC Root?

通过一些列的GC Root对象作为起始点,然后追踪,能追踪到的就是可达的。

  1. 虚拟机栈(栈幀中的本地变量表)引用的对象
  2. 方法区中类静态属性引用的对象
  3. 方法区中常量引用的对象
  4. 本地方法栈JNI引用的对象。

4.8、CMS收集器和G1垃圾回收器回收垃圾过程?

cms:

  1. 初始标记:简单标记GC ROOTS(STW)
  2. 并发标记:并发标记追踪
  3. 重新标记:标记2产生的垃圾回收对象(STW)
  4. 并发清除:标清(标记-清除算法)

G1:

  1. 初始标记:STW
  2. 并发标记:
  3. 最终标记:STW
  4. 并发清除:STW

对比:都是经过四个阶段,但是G1是化整为零,将内存分为多个区域,而且在第四阶段会产生STW,使用的Garbage First(优先处理那些垃圾多的内存块),所以是可停顿的,G1不会产生内存碎片。

4.9、 G1的优点(见上页)

4.10、垃圾回收的过程,什么对象会进入老年代?

  1. 当对象年龄达到阈值的时候,MaxThreashold
  2. 当新生代中,From中的大对象,复制到to对象,且大于to可用内存,然后就会进入老年代。

5、

5.1 JVM拍错方式

  1. java.lang.OutOfMemoryError:Java heap space :解决方案,堆内存使用完了:-Xms512M -Xmx512M:分别表示初始堆大小,最大堆大小;
  2. java.lang.OOM:meta space:解决方案:反射产生大量的类不断加载,导致永久代被占满。
  3. java.lang.StackOverFlowError:栈满。
  4. java.lang.OOM:GC over head limit exceeded:解决:垃圾回收时间过长,指的是98%的时间都用来GC,回到不到2%的内存;
  5. java.lang.OOM:unable to create native thread。:应用程序创建了太多了线程,超出了系统的承载极限,linux下默认是1024;
  6. java.lang.OOM:direct buffer memory:

5.2 内存泄露的原因,举例说明?

内存泄漏:1.存在一些对象是可达的。2.然后这些对象以后再不会使用到,或者说是无用的。满足两个条件就可以认为是内存泄露了。例子:


Vector v = new Vector(10);

for (int i = 1; i < 100; i++) {

Object o = new Object();

v.add(o);

o = null;
}

在这里:仅仅释放引用o本身,然而vector仍然引用该对象,所以这个对象对GC来说是不可回收的。

总来说内存泄露:长生命周期的对象持有短生命周期对象的引用就很可能发生内存泄漏。

  1. 静态集合类引起内存泄露:香HashMap、Vector等的使用容易出现内存泄露,因为这些静态变量的生命周期和应用程序一致。
  2. 当集合里面的对象属性被修改后,再调用remove()方法时不起作用。
public static void main(String[] args)
{
Set<Person> set = new HashSet<Person>();

Person p1 = new Person("唐僧","pwd1",25);

Person p2 = new Person("孙悟空","pwd2",26);

Person p3 = new Person("猪八戒","pwd3",27);

set.add(p1);

set.add(p2);

set.add(p3);

System.out.println("总共有:"+set.size()+" 个元素!"); //结果:总共有:3 个元素!

p3.setAge(2); //修改p3的年龄,此时p3元素对应的hashcode值发生改变

set.remove(p3); //此时remove不掉,造成内存泄漏

set.add(p3); //重新添加,居然添加成功

System.out.println("总共有:"+set.size()+" 个元素!"); //结果:总共有:4 个元素!

for (Person person : set)
{
System.out.println(person);
}
}
  1. 监听器:调用控件如:addXXXListener()等方法增加监听器,在释放对象的时候没有记住去删除这些监听器。
  2. 各种连接:数据库连接,网络连接,io连接,如果没有调用close()方法将其连接关闭, GC就不会回收。
  3. 单例模式:不正确使用单例模式,例如单例模式持有对外部对象的一个引用。因为单例模式初始化之后会在JVM的整个生命周期中。

5.3、 如何排查内存泄露?

  1. 使用jps找出正在运行的虚拟机进程。
  2. 使用jstat命令监控进程id。jstat -gcutil 20954 1000(1000毫秒查询一次已使用空间占总空间的百分比)| 或者查看GCDetails,看哪个空间回收比较频繁。
  3. 用工具生成java程序的heap dump(如jmap)
  4. 使用java heap分析工具(如MAT),找出嫌疑对象
  5. 然后根据情况分析源代码的引用关系。

###5.4、JVM常用的启动参数有哪些,列举熟悉的几个?

  1. -Xms:初始化堆内存大小
  2. -Xmx:最大堆内存大小
  3. -XX:+PringGCDetials 打印垃圾回收日志。
  4. -Xss:设置单个线程栈的大小
  5. -Xmn:设置年轻代的大小
  6. -XX:MetaSpaceSize

synchronized

1.1、synchronized 和ReetrantLock的区别?

  1. synchronized是java的关键字,ReentrantLock是类。
  2. synchronized只有非公平的,ReentrantLock默认非公平,可以通过参数设施为公平的。
  3. synchronized使用完之后会自动释放锁,ReentrantLock需要手动释放锁。
  4. synchronized使用notify或者notifyAll,随机唤醒一个或者全部唤醒,而ReentrantLock,可以实现精确唤醒。
  5. synchronized不可中断,而ReentrantLock可以中断tryLock()方法。

1.2、Synchronized的偏向锁、轻量级锁、重量级锁?

synchronized三种用法:

  1. synchronized修饰普通方法
  2. synchronized修饰同步代码块
  3. synchronized修饰静态方法

修饰普通方法工作原理:

  1. 当前线程调用当前实例对象,然后去访问实例对象对应的类方法的方发表中的访问标志是否被设置。
  2. 如果是,当前线程会去访问当前实例对象的markwork的锁记录标志是否为01,虚拟机首先在当前线程的栈中创建 lock_record(锁记录)用于存放markword的拷贝,官方称这个拷贝为“Displaced Mark”
  3. 当前线程通过 Displaced Mard中指向重量级锁的指针,找到Monitor对象的起始地址,获取到monitor对象,获取成功之后才能继续执行方法体,执行完毕释放monitor对象。其他的线程无法在获取同一个monitor对象。

修饰同步代码块:同上

修饰静态方法:

  1. 如果静态方法中有sychronized,那么在生成一个该类实例后,该实例也就有一个单独的Monitor对象,所有该类的所有实例公用一个Monitor对象。

三种锁描述:默认偏向锁,如果线程竞争程度比较小,也就是锁几乎被同一个线程获得,这时候使用轻量级锁,就减少了线程的加锁,解锁过程。当竞争程度比较小,锁会由偏向锁升级为轻量级锁。当竞争激烈的时候,升级为重量级锁。

锁膨胀过程:

  1. 检测markword里面是否为当前线程的id,如果是,当前锁处于偏向锁。
  2. 如果不是,尝试使用使用CAS替换markword的线程id指向当前形成,如果成功,当前线程获得偏向锁,置偏向标志位为1。
  3. 如果失败,说明发生竞争,撤销偏向锁,膨胀为轻量级锁。
  4. 当前线程使用先复制markword到线程栈,然后CAS将对象头mardword修改为锁记录,如果成功,当前线程获得锁。
  5. 如果失败,说明发生竞争,当前线程自旋获得锁。
  6. 如果自旋获得成功,仍然处于轻量级锁状态。
  7. 如果失败,锁膨胀为重量级锁。

在所有的锁都启用的情况下线程进入临界区时会先去获取偏向锁,如果已经存在偏向锁了,则会尝试获取轻量级锁,启用自旋锁,如果自旋也没有获取到锁,则使用重量级锁,没有获取到锁的线程阻塞挂起,直到持有锁的线程执行完同步块唤醒他们;

偏向锁是在无锁争用的情况下使用的,也就是同步开在当前线程没有执行完之前,没有其它线程会执行该同步块,一旦有了第二个线程的争用,偏向锁就会升级为轻量级锁,如果轻量级锁自旋到达阈值后,没有获取到锁,就会升级为重量级锁;

如果线程争用激烈,那么应该禁用偏向锁。

线程

1.1 线程池是如何实现的?

答:线程池是预先设定好一个池子,然后创建一定的线程数(核心数)放在池子里面,当需要执行任务时直接提交即可。线程的复用原理可以这么理解:因为我们平时线程执行完毕,都会释放线程,而线程池里面是一个死循环,当任务提交的时候,池子里面的线程检测到任务,就会获取该任务然后执行。

1.2 使用线程池的好处?

首先,我们知道线程的创建是会带来一定的开销的。所以线程池为了克服这个缺点,先提前创建一定数量的线程放在池子里面,当有任务的时候直接提交给池子里的线程,而不再需要频繁创建一个新的线程,这就减少了创建线程的开销。

1.3 常见的线程池有哪些,各个参数的含义?

五大参数:

  1. corePoolSize:线程池常驻核心线程数
  2. maxCorePoolSize:线程池能创建的最大线程数
  3. time:
  4. timeUnit:两个参数配置当线程如果空闲超过一定的时间,而且线程数量大于核心线程数,则会回收这个线程。
  5. BlockingQueue:阻塞队列:当提交的任务,如果当前线程数大于等于corePoolSize时候,则会放入任务队列中。

常见线程池

  1. Excetors.newSingleThreadExecutor();//当一线程(Integer.MAX_VALUE)
  2. Excetors.newFixedThreadPool();//固定数
  3. Excetors.newCachedThreadPool();//可变数,线程最大数最Integer.MAX_VALUE:可缓存线程池,可灵活回收线程,使用的时候要注意控制任务的并发数,如果是同时有太多的线程会造成系统瘫痪。
  4. Excetors.newScheduleThreadPool();//创建一个定长的线程池,而且支持定时的以及周期性的任务执行,支持定时及周期性任务执行。同样:也是线程最大数最Integer.MAX_VALUE

1.4 线程池的底层原理?

  1. 创建线程,等待任务的提交

  2. 执行execute()方法提交任务,会做如下判断

    1. 如果当前正在运行的线程小于corePoolSize,则直接运行这个任务。
    2. 如果>=coreCorePoolSize,则将这个放到队列里面。
    3. 如果队列满了,而且线程数<max,还是会创建线程执行这个任务。
    4. 如果队列满了,而且线程数为最大,则会启动饱和拒绝策略:AbrtPolicy:抛出异常,DiscardOldestPolicy,DiscardPolicy,还有一种是返回调用线程策略。
  3. 当一个线程执行完毕后,会从工作队列中获取任务。

  4. 如果一个线程空闲超过一定的时间,则会判断是否>core数,是的话,会回收这个线程。

1.5 执行线程池的两种方式?

  1. execute()方法:只能提交Runnable的任务,如果出现异常会直接抛出。
  2. submit()方法:能提交Callable和Runnbale的任务,如果出现异常不会直接抛出。只有调用get的时候才会抛出。

1.6 线程池怎么定时完成一项任务。

使用Excetors.newScheduleThreadPool(),创建个固定线程池,而且这个线程池可以根据定时执行,或者周期执行。

1.7 线程池线程都有任务,阻塞队列也满了,再来任务怎么办?

工作原理

1.8 Java 线程池 ThreadPoolExecuter 和四个自带的线程池分别适合什么场景?

1.9 ScheduledThreadPoolExecutor 中的使用的是什么队列?内部如何实现任务排序

DelayWorkQueue(),延时工作队列;

实现排序:将传入的任务封装成ScheduledFutureTask,这个类有两个特点,实现了java.lang.Comparable和java.util.concurrent.Delayed接口。通过compareTo比较他们优先级的大小。

#原子类

1.1 怎么实现一个类满足插入和删除操作的原子性?

AtomicReference

1.2 JUC原子类的实现原理

答:主要是通过Unsafe类CAS和自旋来实现的。比如AtomicInterger,就是在Unsaft类的CAS自旋更新。

1.3 AQS的原理和应用场景。

  1. AQS(AbstractQueuedSynchronizer)是用来构建锁和同步器的框架。ReentrantLock,Semaphore、CountDownLatch

  2. 同步器的设计是基于模版方法模式的。

    1. 使用者需要继承AbstractQueueSynchronizer
    2. 将AQS组合在自定义同步组件的实现中,调用其模版方法,模版方法会调用使用者重写的方法。
    3. 主要有:
    4. isHeldExclusively()//该线程是否正在独占资源。只有用到condition才需要去实现它。
    5. tryAcquire(int)//独占方式。尝试获取资源,成功则返回true,失败则返回false
    6. tryRelease(int)//独占方式。尝试释放资源,成功则返回true,失败则返回false
    7. tryAcquireShared(int)//共享方式。尝试获取资源。负数表示失败;0表示成功,但没有剩余可用资源;正数表示成功,且有剩余资源。
    8. tryReleaseShared(int)//共享方式。尝试释放资源,成功则返回true,失败则返回false。
  3. CountDownLatch(同步工具类,它允许一个或多个线程一直等待,直到其他线程的操作执行完后再执行。):任务分为N个子线程去执行,state也初始化为N(注意N要与线程个数一致)。这N个子线程是并行执行的,每个子线程执行完后countDown()一次,state会CAS(Compare and Swap)减1。等到所有子线程都执行完后(即state=0),会unpark()主调用线程,然后主调用线程就会从await()函数返回,继续后余动作。

  4. Semaphore(信号量)-允许多个线程同时访问:

    1. Semaphore semaphore = new Semaphore(20);
    2. semaphore.acquire();// 获取一个许可,所以可运行线程数量为20/1=20
    3. test(threadnum);
    4. semaphore.release();// 释放一个许可
    5. semaphore.acquire(4);// 20/4=5 个可以线程同时访问。
  5. CyclicBarrier(循环栅栏):它要做的事情是,让一组线程到达一个屏障(也可以叫同步点)时被阻塞,直到最后一个线程到达屏障时,屏障才会开门,所有被屏障拦截的线程才会继续干活。CyclicBarrier默认的构造方法是 CyclicBarrier(int parties),其参数表示屏障拦截的线程数量,每个线程调用await方法告诉 CyclicBarrier 我已经到达了屏障,然后当前线程被阻塞。

1.4 ThreadLocal的应用场景?

ThreadLocal叫做线程本地变量,ThreadLocal为变量在每个线程中创建了一个副本,每个线程可以访问自己内部的副本变量。

应用场景:数据库连接、Session管理等。:因为数据库连接我们定义为一个connect,如果两个线程共享这个connect,则会创建多次连接,或者一个创建连接,一个关闭连接,这就使得混乱,我们需要进行同步。一旦同步并发就下降,这

1.5 Thread 和ThreadLocal的关系

  1. Thread内存有ThreadLocal的ThreadLocalMap成员
  2. ThreadLoacal对该ThreadLocalMap定义了操作函数,如get,set

1.6 ThreadLocal使用不当为什么会使内存溢出?

ThreadLocal里的ThreadLocalMap其实是实现了HashMap的功能,ThreadLocal的set方法会为每个线程创建一个ThreadLocalMap对象作为Thread的成员变量保存。

内存溢出原因:在配合线程池使用的时候,Entry里面的value是强引用,只要线程不死或者不调用set(),remove()这两个方法志宏任何一个,那么value指向的对象就永远不会被回收。如果线程池里面的线程足够多,而且穿的线程的本地副本变量足够大,毫无以为内存会溢出。

解决方法:使用完毕后及时调用remove方法才是解决内存泄露的王道。

CopyOnWriteArrayList

1.1 CopyOnWriteArrayList和ArrayList的异同?

  1. CA是线程安全的,ArrayList是线程不安全的。
  2. CopyOnWriteArrayList为java.util.concurrent包提供,它实现了读操作无锁,写操作是通过操作底层数组的新副本来实现,是一种读写分离的并发策略。底层原理:修改操作:add、set、remove都会拷贝原数组,修改后替换原来的数组,实现线程安全。使用了ReentrantLock方法。发生修改时候做copy,新老版本分离,保证读的高性能,适用于以读为主的情况。

1.2 CopyOnWriteArrayList 通过哪些手段实现了线程安全?

在修改的时候使用ReentrantLock加锁,修改是对原始底层的数组进行复制了一份,然后新老版本分析,不影响读,然后将复制过来修改后的数组替换原来的数组。

CopyOnWrite只能保证数据最终的一致性,不能保证数据的实时一致性。

为什么 CopyOnWriteArrayList 迭代过程中,数组结构变动,不会抛出ConcurrentModificationException ?

因为修改的是底层数组的拷贝,所以不会发生。迭代时,持有的仍然是老数组的引用,当我们替换老数组时,老数组的结构并没有发生改变,所以不会发生。

#HashMap ConcurrentHashMap

1.1 比较HashMap和ConcurrentHash的异同。

1.2 ConcurrentHashMap 通过哪些手段保证线程安全?

答:它的是Node数组+链表+红黑树的数据结构实现的。

  1. Unsafe和CAS、Volatile变量的操作是对锁住node之前的操作,

1.3 HashMap的底层数据结构?

  1. JDK1.7 数组+单向链表
  2. JDK1.8:数组+链表+红黑树;1.8中即使使用了单向链表,也是使用了双向链表,双向链表主要是为了链表操作方便,在插入,扩容,链表转红黑树,红黑树转链表的过程都要操作链表。

1.4 JKD8中为什么要使用红黑树?

当链表小于一个阈值是,链表的整体插入查询效率要高于红黑树,当元素个数大于此阈值时,链表的整体插入查询效率要低于红黑树。

1.5 JDK8中什么时候将链表转化为红黑树?

当链表中的元素个数大于8是,而且此时node数组的长度大于等于64时才会转。否则会进行扩容。

1.6 JDK7和JDK8 HashMap的不同点

  1. JDK8使用了红黑树
  2. JDK7使用的头插法(扩容的时候使用头插法,速度比较快),JDK8使用尾插法(扩容的时候先计算好高位和地位元素)
  3. JDK7的hash算法比JDK8的复杂,因为JDK7中没有红黑树,所以使用复杂的hash算法使得生成的hashcode更散列。
  4. 扩容的时候JDK7有可能重新对key进行hash,而JDK8没有这部分逻辑。
  5. JKD8中的扩容条件和JKD7不一样,除了判断size是否大于阈值之外,jdk7中还判断是tab[i]是否为空,不为空的时候才进行扩容,JDK8没有这个条件。
  6. JDK7每次转移一个元素,JDK8是先计算出高位和地位的元素,然后一次性转移。

1.7 JDK8中怎么保证ConcurrentHashMap的并发安全?

  1. 主要是利用了Unsafe操作+synchronized关键字

    1. compareAndSwapObject:通过CAS的方式修改对象的属性
    2. putOrderedObject:并发安全的给数组某个位置复制。
    3. getObjectVolatile:并发安全的获取数组某个位置的元素
  2. synchronized主要是为了在某个位置加锁(该位置不为空),比如像某个位置的链表或者红黑树插入节点。

  3. JDK8中仍然有分段锁的思想,不过是每个位置都有一把梭,而JDK是可控制的。

  4. 当想ConcurrentHashMap中put一个key,value时。

    1. 根据key就散对应的数组的下标i,如果该位置没有元素,自通过自旋的方式去向该位置赋值。
    2. 如果该位置有元素,则synchronized加锁。
    3. 枷锁成功之后会判断元素的类型,如果是链表节点则插入链表,如果是红黑树,则插入红黑树中。
    4. 添加成功之后判断是是否需要树化。
    5. 调用addCount,这个方法的意思是ConcurrentHashMap元素个数+1,这个操作也是并发安全的,并且+成功之后会继续判断是否需要扩容。
    6. 同时一个线程在put操作时如果发现当前ConcurrentHashMap正在扩容,则回去帮助扩容。

1.8 JDK8中HashMap put方法的实现过程?

  1. 根据key生成hashCode

  2. 判断当前HashMap对象的数组中是否为空,如果为空则初始化该数组。

  3. 根据逻辑与运算,算出hashCode在当前数组的下标i。

  4. 判断tab[i]是否为空

    1. 如果为空,则将key,value封装为node对象赋值给tab[i];
    2. 如果不为空:
      1. 首先对传进来的key判断有没有重复的key,如果有,则存在。
      2. 如果没有:(1):如果tab[i]是treeNode,则表示数组i上式红黑树,则将key,value插入到红黑树中。(2):如果不是treeNode,则表示i上为链表,遍历链表查找是否存在相同的key,并且在遍历的时候会对链表的节点数计数,当到最后一个节点是,直接插入链表尾部,同时会在插入新节点之后会判断链表节点数是否大于8,如果是链表转为红黑树。
      3. 在上述存在相同的值得花,会替换掉原先的value,并返回old value;
  5. modCount++;

  6. HashMap的元素个数size+1;

ConcurrentHashMap中:首先先传入一个k和v的键值对,不可为空(HashMap是可以为空的),如果为空就直接报错。红黑树不是TreeNode,而是一个Treebin来封装红黑树,这是为了额方便加锁。

1.9 CAS在ConcurrentHashMap中的应用

  1. put一个key value对的时候,如果table[i]上为空,则会通过CAS自旋的方式尝试给该位置赋值。
  2. 当添加一个元素之后,对baseCount进行+1,就是使用CAS操作
  3. casTabAt(tab, i, null, fwd):通过cas获取数组上某个位置的元素

1.10 ConcurrentHashMap 是如何发现当前槽点正在扩容的?

  1. ForwardingNode<K,v> fdw=new ForwardingNode<k,v>(nextTav),通过fdw作为扩容标志,当其他发现数组上为fdw时,表示正在进行扩容(原容量的两倍)。
  2. 如果发现当前数组位置为ForwardingNode的话,则调用helpTransfer()方法去帮助扩容。
  3. 如果扩容没有完成,当前链表的头节点会被锁住,所以写线程会被阻塞,直到扩容完成。

#阻塞队列

1.1 那些队列具有阻塞的功能,大概是如何阻塞的?

底层原理:lock + 多个条件(condition)阻塞控制

lock = new ReentrantLock(fair);

notEmpty = lock.newCondition();

notFull = lock.newCondition();

  1. ArrayBlockingQueue:基于数组实现的阻塞队列。
  2. LinkedBlockingQueue:基于链表的实现的阻塞队列。
  3. PriorityBlockingQueue:基于优先级的阻塞队列,优先级通过传入的Compator对象来设定。它不能阻塞生产者,所以速度不能太快,如果生产的太快,数据没来得及消费,就会导致传入过多的数据从而导致OOM;栈先(先进后出),队列(FIFO)
  4. SynchronousQueue:无缓冲的等待队列,类似于直接无中介的直接交易;对于每一个take的线程会阻塞直到有一个put的线程放入元素为止,反之亦然。底层数据结构:队列实现公平策略,栈实现非公平策略;

ArrayBlockingQueue和LinkedBlockingQueue都可实现定长的阻塞队列,当put操作发现队列满了,会阻塞线程,当take发现线程为空的,也会阻塞线程;

1.2 LinkedBlockingQueue 和 ArrayBlockingQueue 的区别?

  1. 实现不同,一个基于链表,一个基于数组;
  2. 底层不同,put,take方法不同,LinkedBlockingQueue的put和take是不同的锁,可以同时进行,ArrayListBlocking用的是同一把锁,所以同一个时刻只能有一个操作;

1.3 往队列里 put 数据和take数据是线程安全的么?为什么?

是的,因为put和take方法都是加了锁的。

1.4 take和put方法是不是同一时间只能运行其中一个?

看情况LinkedBlockingQueue可以同时进行,ArrayBlocking只能运行其中一个,SynchronousQueue,对于每一个take的线程都会阻塞,知道来一个put的线程放入元素,反之亦然;

1.5 SynchronousQueue 底层有几种数据结构,两者有何不同?

底层数据结构:队列实现公平策略,栈实现非公平策略;

1.6 工作中经常使用队列的 put、take 方法有什么危害,如何避免。

put,take如果一直没有满足条件会一直阻塞,如果流量大的话可能导致线程耗尽,使用offer和poll来代替两者,设置超时阻塞时间。

1.7 ArrayBlockingQueue take 和 put 都是怎么找到索引位置的?是利用 hash 算法计算得到的么?

队里里面维护了两个属性,takeIndex和putIndex用来记录下次take和put的位置。

#线程

1、进程和线程的区别?

进程是操作系统分配资源的单位;线程(Thread)是进程的一个实体,是CPU调度和分派的基本单位。

  1. 进程间切换代价大,线程间切换代价小
  2. 进程拥有资源多,拥有独立的内存单元,线程拥有资源少,多个线程共享内存,每个线程有自己的程序计数器,虚拟机栈,本地方法栈。
  3. 多个线程共享进程的资源

2、Java进程与线程的关系(如上)

3、进程间常见的通信方式?

  1. 管道pipe
  2. 命名管道FIFO
  3. 消息队列:MessageQueue
  4. 共享内存ShareMomery;
  5. 信号量Semaphore
  6. 套接字Socket
  7. 信号signal:

4、有多少实现线程的方式?

  1. 继承Thread类
  2. 实现Runnable接口
  3. 实现Callbale接口
  4. 使用线程池

5、java中线程如何交互

  1. synchronized 通过wait(),notify()/notifyAll()实现线程间通信;
  2. Lock,通过条件await,signal()/signalAll()实现线程间通信;
  3. 管道通信:通过管道,将一个线程中的二进制数据消息发送给另一个。

6、 实现Runnable接口和继承Thread类哪种方式更好?

实现Runnable接口要更好,因为突破了单线程的限制。

7、线程的五个状态(标准答案)

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-biz7rXsP-1593415349460)(state.jpg)]

  1. new:新建

  2. 就绪状态:

    1. 只能说有资格运行,调度程序没有调度到你,就只能是就绪状态;
    2. Runable:调用start进入就绪状态。
    3. Running:运行状态,
  3. 阻塞状态:线程进入synchronized或者锁进入阻塞状态

  4. 等待状态

    1. Waiting:调用Object.wait(),Thread.join(),LockSupport.park(),都会是县城进入无限期等待;
    2. Time—Waiting:调用Object.wati(long),Thread.join(long),Thread.sleep(),LockSupport.partNanos都可以使线程进入限期等待状态。
  5. 终止状态:

8、一个线程两次调用start()方法会出现什么情况?为什么?

答:会报IllegalStateException,因为调用start()之后线程进入了就绪状态,再调用的时候已经不是new状态,所以会报错。

9、既然start()方法会调用run方法,为什么我们选择调用start方法而不是直接调用run方法呢?

答:调用start()方法会使线程进入就绪状态,当cpu给线程分配资源时会自动去执行run()方法,而run()方法是main线程里面的普通方法,直接调用不会产生多线程;

10、多线程中断的原理

三个方法:

  1. t.interrupt()方法,设置t线程的中断标志位为true,仅仅是设置中断标志,调用线程不一定会马上终止当前线程。如果当前线程处于几种状态(object.wait,Thread.join,Thread.sleep),则会抛出InterupptException,如果不处理,则线程就会进入终止状态。
  2. isInterrupted获取线程中断标志位
  3. interrupted:测试当前线程是否已经中断,同时清除中断标志位。

11 如何结束一个线程?

线程调用interrupt()方法,仅仅是设置中断标志位,线程并不一定会马上会终止。

12 请解释一下什么是生产者消费者模式?

答:通过一个容器来解决生产者与消费者的强耦合关系,生产者生成数据无需消费者索取,消费者无需直接索要数据。两者不进行通信,直接通过容器来操作。常用的有容器有阻塞队列;

13 为什么线程通信的方法wait、notify、notifyAll被定义在Object类中?而slepp方法被定义在Thread类中?

  1. 因为java提供的锁是对象级的,所有对象都可以作为锁,所以把他们(前三个)定义在Object中合乎这个设计;
  2. 而sleep()的意思是让当前线程让出cpu,进入睡眠状态,它作用的是线程级别,所以定义在Thread中;

14 wait/notify、sleep异同?(yield和sleep的区别)

常用方法比较:

  1. object.wait():当前线程让出cpu,并进入等待状态,调用该方法会释放对象锁。
  2. object.notify():唤醒等待队列中的一个线程,随机唤醒一个。notifyAll()则是唤醒所有的线程。
  3. Thread.sleep():当前线程让出cpu,线程进入TIMED_WAITING状态,不会释放锁。
  4. Thread.yield():当线程放弃获取的cpu时间片,但不释放锁资源,线程有运行状态进入就绪状态。
  5. t1.join()/join(long millis):当前线程调用,则线程进入等待状态/TIME_WAITING状态,当t1执行完毕后,当前线程进入就绪状态或者BLOCKED;
  6. LockkSupport.park()/.parkNanos():不需要获得锁就进入等待状态(对比wait());

15、在join期间,线程处于哪种线程状态?

等待状态/TIME_WAITING状态

16、守护线程和普通线程的区别?

守护线程(Daemon)定义:守护线程又称为“服务线程”,在没有用户线程可服务时会自动离开。

1.用途:周期性地执行某种任务或等待处理某些发生的事件。

  1. JVM中垃圾回收线程就是经典的守护线程。

17、什么是多线程的上下文切换?

答:多线程会共同使用一组CPU,当线程数大于程序分配的cpu数时,为了让各个cpu都有执行的机会,会轮训使用cpu;不同的线程切换使用cpu产生的数据切换就是上下文切换。

18、为什么多线程会带来性能问题?:如上

19、单例模式的作用和应用场景。

作用:只产生一个实例对象。

  1. 整个程序运行中只允许一个类的实例;
  2. 需要频繁实例化然后销毁的对象;
  3. 创建对象耗时过多(耗资源过多),又经常用到的对象。

20 分布式集群中如何保证线程安全?

答:使用分布式锁,如:redis锁,zookeeper分布式锁

Voatile

1、Java内存模型和JVM内存结构有什么不同?

  1. Java内存模型是一种抽象的定义,它定义了三个特性:1.可见性 2.原子性 3. 有序性
  2. JVM内存模型是对内存的划分,实实在在的存在,分为五个区。

2、什么是Java内存模型,为什么会出现Java内存模型?

答:定义JAVA内存模型试图用来屏蔽掉各个硬件和操作系统的内存访问差异。

3、i++是线程安全的么,从 Java 内存模型来分析,如何保证它是安全的?

不是线程安全的,首先如果i没有volatile没有定义为volatile,则它遵循可见性和有序性,最算是定义volatile也不是原子性的。使用synchronized加锁,或者lock加锁再或者,使用AtomicInteger替换i,都能保证它是线程安全的。

4、 什么是原子性问题?

答:就是一个不可中断的操作。

5、new一个对象的过程是否是原子的?

6、什么是可见性问题,为什么会出现,又如何解决?

CPU和工作内存之间存在缓存,也就是每个线程都有自己的工作内存;线程每次读取主内存的数据到自己到自己的工作内存,如果写回到主内存,对于其他的线程来说能不能看到这个变化,这个就是可见性问题。

7、列举几条happens-before规则

  1. 程序次序规则:写在前面的代码先行发生于书写在后面的操作;
  2. 管理锁定规则:lock操作先行发生于unlock
  3. volatile变量规则:对volatile变量的写操作先行发生于后面对这个变量的读操作;
  4. 线程启动规则:Thread对象的start()方法先行发生于此线程的每一个操作。
  5. 线程中断规则:对线程interrupt()方法的调用先行发生于被中断线程的代码检测到中断时间的发生;
  6. 传递性:A先行B,B先行C,则A先行C。

8、volatile的底层原理

  1. 可见性:对volatile变量,底层会多出一个Lock指令,这个指令的作用就是当写回主内存的时候,会使其他的线程的缓存无效。
  2. 有序性:
    1. 对于每个写操作之前插入StoreStore指令
    2. 每个写操作之后插入StoreLoad指令。
    3. 对读操作之前插入LoadLoad指令。
    4. 对每个读操作之后插入LoadStore指令。

java基础

1、String几种拼接方式区别,+和 append 底层有没有区别

  1. +操作中如果有字符串变量,底层会new出一个StringBuileder;
    1. String str=“1”;str=str+“1”;
  2. append不会new出一个对象,而是直接返回自身对象。

2、String在jvm中的存储;

答:对于字面量,则直接存放在字符串常量池中,对于new出来的对象,则直接在堆中创建,并不会把它加入到字符串常量池中,可以调用str.intern()方法加入到字符串常量池中;

3 JDK JRE JVM区别?

说JDK是用于java程序的开发,而jre则是只能运行class而没有编译的功能(不能调试,编译之类的功能);JVM是虚拟机。

4、 float和double的精度,底层存储方式,和decimal有什么区别?

  1. float占用4个字节,最多能保存7个有效数字位;0.99999999f==1f返回true,但是去掉一个9返回false
  2. double占用8个字节,最多能保存15个有效位;
  3. decimal:数字型,128bit,不存在精度损失,常用于银行帐目计算。(28个有效位)

5、java中小数是怎么存的?

float和double,存在精度损失,可以使用decimal(128位),java中封装了BigDecimal,常用于银行货币记账;

6、接口可以定义方法体吗?(jdk 1.7 和jdk1.8区别)

  1. jkd1.7不可以定义方法体、
  2. jdk1.8可以定义静态方法体和默认方法体

7 强引用、软引用,弱引用、幻象引用区别?

幻像引用(虚拟用或者幽灵引用):幻象引用仅仅是提供了一种确保对象被finalize以后,做某些事情的机制。get()都是返回空;
##集合类

1、HashMap和HashTable的区别?

  1. HashTable从实现角度和HashMap大致相同。
  2. HashTable不允许key和value为null,HashMap允许key和value为空
  3. HashTable是线程安全的,而HashMap不是安全的,因为HashTable加了synchronized;
  4. Hashtable的初始容量与HashMap不同。HashTable初始容量为11:这是考虑到了使用合数,使用hash算法会使得散列值更加散,而HashMap初始容量为16,2的n次方,因为考虑效率问题,计算的下标的时候使用&运算;
  5. 扩容方式不一样,HashTable为两倍+1,HashMap为两倍扩容;
  6. 底层结构不太一样:当链表达到8时,hashtable不会转化为红黑树,而HashMap会转化为红黑树。

2、 举例说明HashMap是不安全的;

开多个线程,向map里面添加元素;

3、vector,ArrayList 和LinkedList的区别;

  1. vector线程安全的,因为底层使用了synchronized来修饰方法。扩容为原来的两倍;
  2. ArrayList不是线程安全的,扩容为1.5倍
  3. LinkedList底层使用双向链表实现;不安全

4、 ArrayList 是怎么扩容的?初始化时给定 ArrayList 的 size,数组大小一定就是给定值吗

  1. 当数组的大小大于初始容量的时候(比如初始为10,当添加第11个元素的时候),就会进行扩容,新的容量为旧的容量的1.5倍。
  2. 是的。

5. ArrayList和LinkedList在尾部添加一个元素哪一个更快?

当操作是在一列数据的后面添加数据而不是在前面或中间,使用ArrayList会提供比较好的性能;

6、HashSet和TreeSet有什么区别?

  1. HashSet

    1. HashSet随机存储,
    2. 底层是基于HashMap实现的,不过是Value是一个固定的 Object类
    3. 集合元素可以是null(只能是一个null)
  2. TreeSet

    1. TreeSet中是排好序,不允许放入null
    2. TreeSet通过TreeMap实现,Set用得Map的key
    3. 底层实现为红黑树。

7、Collection和Collections有什么关系?

Collection是接口,实现继承它的有Set,List,实现的类有ArrayList,LinkedList,hashSet,TreeSet。Collections提供了操作Collection的方法,比如同步,遍历;

8、LinkedHashMap和TreeMap的区别(两个有序)

  1. LinkedHashMap是用来底层是用双向链表来记录插入顺序的,所以它是记录了插入顺序。
  2. TreeMap底层是红黑树,数据是排序的,而不是插入顺序。

9、序列化和反序列化怎么实现,自己设计会怎么做

  1. 序列化:将实例化对象实现Serialable接口,然后调用ObjectOutputStrem生成一个对象,调用这个对象的writeObject()进行序列化。(对于不想序列化的可以用transient修饰)。
  2. 反序列化:调用ObjectInputStream生成一个对象,然后调用这个对象的readObject()方法,完成反序列化。
  3. 序列化应用场景:比如讲对象保存到数据库,或者将对象进行网络传输。

10 jdk7,jdk8对比,增加了什么新特性?

  1. lambda表达式
  2. 接口增加了静态方法实现和默认方法实现

11、BIO、NIO,AIO

  1. BIO(同步并阻塞):服务器实现模式一个连接一个线程,即客户端有连接请求时服务器端就要启动一个线程进行处理,如果这个连接不做任何事情就会造成不必要的线程开销,可以通过线程池机制改善。
  2. NIO(同步非阻塞):服务器实现模式一个请求一个线程,即客户端的连接请求都会注册多路复用器上,多路复用器轮训到连接有I/O请求时才启动一个线程进行处理,(当线程处理I/O请求时还是同步阻塞的)(多路复用的机制,利用单线程轮询事件)。
  3. AIO(异步非阻塞):服务器实现模式为一个有效请求一个线程,客户端的I/O请求都是由OS先完成了再通知服务器应用线程进行处理。

12、main 方法声明成 private 可以通过编译嘛?

可以编译,无法运行

13、对称加密和非对称加密

  1. 对称加密:加密和解密使用同一把秘钥,被拦截还是有风险。
  2. 非对称加密:加密和解密使用不同的秘钥,分别为公钥和私钥。加密使用公钥,那么解密只能用自己的私钥。比如A和B通信,A把自己的公钥发给B,B然后把使用A的公钥加密自己的公钥再发送给A,A用自己的私钥解密之后得到B的公钥。这样子就算被拦截,没有私钥还是解不开。

14、对象头中有哪些信息

  1. MarkWord:存储有对象的hashCode,分代年龄,锁标记位,是否持有偏向锁,偏向线程ID,偏向时间戳。
  2. 类型指针:存储对象类型的指针,通过这个指针可以知道是按个类的实例。

15、什么是 rpc 框架?和 http 调用的区别是什么?

OSI七层模型:

  1. 应用层:定义了用于在网络中进行通信和传输数据的接口
  2. 表示层:定义不同的系统中数据的传输格式,编码和接码规范等。
  3. 会话层:管理用户的会话,控制用户间逻辑连接的建立和中断。
  4. 运输层:管理网络中的端到端的数据传输。
  5. 网络层:定义网络设备间如何传输数据
  6. 链路层:将网络层的数据包封装成数据帧,便于物理层传输。
  7. 物理层:传输二进制数据。

RPC:远程过程调用协议,区别:

  1. RPC基于TCP/UDP,http基于http协议;
  2. RPC比HTTP效率高
  3. RPC比HTTP复杂,RPC长连接,不用像http每次通信都要3次握手。
  4. RPC通常在大型网站中使用,小型网站使用http就可以。
    以编译,无法运行

13、对称加密和非对称加密

  1. 对称加密:加密和解密使用同一把秘钥,被拦截还是有风险。
  2. 非对称加密:加密和解密使用不同的秘钥,分别为公钥和私钥。加密使用公钥,那么解密只能用自己的私钥。比如A和B通信,A把自己的公钥发给B,B然后把使用A的公钥加密自己的公钥再发送给A,A用自己的私钥解密之后得到B的公钥。这样子就算被拦截,没有私钥还是解不开。

14、对象头中有哪些信息

  1. MarkWord:存储有对象的hashCode,分代年龄,锁标记位,是否持有偏向锁,偏向线程ID,偏向时间戳。
  2. 类型指针:存储对象类型的指针,通过这个指针可以知道是按个类的实例。

15、什么是 rpc 框架?和 http 调用的区别是什么?

OSI七层模型:

  1. 应用层:定义了用于在网络中进行通信和传输数据的接口
  2. 表示层:定义不同的系统中数据的传输格式,编码和接码规范等。
  3. 会话层:管理用户的会话,控制用户间逻辑连接的建立和中断。
  4. 运输层:管理网络中的端到端的数据传输。
  5. 网络层:定义网络设备间如何传输数据
  6. 链路层:将网络层的数据包封装成数据帧,便于物理层传输。
  7. 物理层:传输二进制数据。

RPC:远程过程调用协议,区别:

  1. RPC基于TCP/UDP,http基于http协议;
  2. RPC比HTTP效率高
  3. RPC比HTTP复杂,RPC长连接,不用像http每次通信都要3次握手。
  4. RPC通常在大型网站中使用,小型网站使用http就可以。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值