java面试之JVM和并发

文章目录

      • 1、 什么是进程?什么是线程
      • 2、 JVM、JRE、LDK的关系
      • 3、JVM中可以运行多种语言吗?
      • 4、JVM有哪些内存区域
      • 5、内存空间怎么配置?各区域怎么划分
      • 6、JVM中哪些内存区域会发生内存溢出(OOM)
      • 7、JVM在创建对象是采用了哪些并发安全机制
      • 8. 什么是对象头?对象头里面有哪些东西
      • 9.为什么不要使用Finalize方法
      • 10怎么判断对象的存活
      • 11、复制算法
      • 12、标记清除算法
      • 13、标记整理算法
      • 14.为什么扩容新生代可以提高GC的效率
      • 15简述一下CMS垃圾回收器,他有哪些问题
      • 16在java中守护线程和用户线程的区别
      • 17什么是多线程中的上下文切换
      • 18什么是死锁?死锁的危害
      • 19Executor和Executors的区别
      • 20什么是CAS操作,缺点是什么
      • 21、Lock接口是什么?对比synchronized接口他有什么优势
      • 22、什么是阻塞队列?阻塞队列的实现原理是什么
      • 23、什么是Callable和Future?
      • 24、什么是FutureTask
      • 25、什么是并发容器的实现
      • 26、为什么我们调用start()方法是会执行run()方法,为什么不能直接;调用run()方法
      • 27、什么是不可变对象,它对写并发应用有什么帮助
      • 28、乐观锁和悲观锁的理解及如何实现,有哪些实现方式
      • 29、在java中wait和sleep的不同
      • 30、为什么wait、notify、notifyAll这些方法不在Thread类里面
      • 31、什么是线程安全
      • 32、一个线程如果出现了运行时异常会怎么样?
      • 33、java中volatie变量有什么作用
      • 34、Threadlocal的作用
      • 35、生成者消费者模型的作用是什么
      • 36、为什么要使用线程池
      • 37、java中的线程调度算法是什么
      • 38、什么是线程组,为什么在java只不推荐使用
      • 39、java中有几种方法可以实现一个线程
      • 40、SynchronizedMap和ConcurrentHashMap有什么区别
      • 41、ConcurrentHashMap并发度是什么
      • 42、Thread.sleep(0)的作用是什么
      • 43、什么是线程调度器什么是时间片
      • 44、如何确保main()方法所在的线程是java程序最后结束的线程
      • 45、什么是Java Timer类?如何创建一个有特定时间间隔的任务?
      • 46、java中的Semaphore
      • 47、为什么会代码重排序
      • 48、Java中ReadWriteLock是什么?
      • 49、volatile变量和atomic变量有什么区别
      • 50、怎么监测一个线程是否持有对象监视器
      • 51、什么是AQS
      • 52、线程类的构造方法、静态块时被那个线程调用的
      • 53、多线程的同步和互斥有哪几种方法
      • 54、什么是竞争条件?怎样发现和解决竞争条件?
      • 55、为什么wait和notify方法要在同步块中调用?
      • 56、什么是阻塞式方法?
      • 57、什么是安全点什么是安全区域
      • 58、如何打印JVM日志

1、 什么是进程?什么是线程

  • 进程是操作系统分配资源的最小单元
  • 线程是操作系统调度的最小单元
  • 一个程序至少有一个进程,一个进程至少有一个线程

2、 JVM、JRE、LDK的关系

  • JVM(Java Virtual Machine):java虚拟机,他能识别.class后缀的文件,并且能够解析他的指令,最终调用操作系统上的函数,完成我们想要的操作
  • JRE(Java Runtime Environment):java运行环境,JVM标准上实现的一大堆基础类库,就组成了java的运行环境
  • JDK(Java Development Kit):除了包含JRE,JDK还提供了一些非常好用的工具

3、JVM中可以运行多种语言吗?

JVM只识别字节码,所以JVM其实是跟语言是解耦的,没有直接关联,像Scala、Groovy等语言都可以在JVM上运行

4、JVM有哪些内存区域

JVM内存区域分为线程共享区和线程私有区,其中线程私有区可以分为:虚拟机栈、本地方法栈和程序计数器,线程共享区分化为方法区和堆

  • 虚拟机栈:在JVM运行过程中存储当前线程运行方法所需要的数据、指令和返回地址
  • 本地方法栈:本地方法栈和虚拟机栈是非常相似的一个区域,他服务的对象是native方法
  • 程序计数器:主要用来记录各个线程执行的字节码地址
  • 方法区:JDK1.7及以前“永久代”,JDK1.8及以后“元空间”,存放信息、常量池,方法数据、方法代码
  • 堆:堆是JVM上最大的内存区域,我们申请的几乎所有的对象都在堆中存储

5、内存空间怎么配置?各区域怎么划分

  • 总大小:3~4倍活跃数据大小
  • 新生代:1~1.5倍活跃数据大小
  • 老年代:2~3倍活跃数据大小
  • 永久代/元空间:1.2~1.5Full GC后的永久代空间占用

年轻代(Young Gen)
年轻代主要存放新创建的对象,内存大小相对会比较小,垃圾回收会比较频繁。年轻代分成1个Eden Space和2个Survivor Space(命名为A和B)。当对象在堆创建时,将进入年轻代的Eden Space。垃圾回收器进行垃圾回收时,扫描Eden Space和A Survivor Space,如果对象仍然存活,则复制到B Survivor Space,如果B Survivor Space已经满,则复制到Old Gen。同时,在扫描Survivor Space时,如果对象已经经过了几次的扫描仍然存活,JVM认为其为一个持久化对象,则将其移到Old Gen。扫描完毕后,JVM将Eden Space和A Survivor Space清空,然后交换A和B的角色(即下次垃圾回收时会扫描Eden Space和B Survivor Space。这么做主要是为了减少内存碎片的产生。(采用赋值算法)

我们可以看到:Young Gen垃圾回收时,采用将存活对象复制到到空的Suvivor Space的方式来确保尽量不存在内存碎片,采用空间换时间的方式来加速内存中不再被持有的对象尽快能够得到回收。
年老代(Tenured Gen)
年老代主要存放JVM认为生命周期比较长的对象(经过几次的Young Gen的垃圾回收后仍然存在),内存大小相对会比较大,垃圾回收也相对没有那么频繁(譬如可能几个小时一次)。年老代主要采用压缩的方式来避免内存碎片(将存活对象移动到内存片的一边,也就是内存整理)。当然,有些垃圾回收器(譬如CMS垃圾回收器)出于效率的原因,可能会不进行压缩。

持久代(Perm Gen):持久代主要存放类定义、字节码和常量等很少会变更的信息。

JVM内存回收过程
对象在Eden Space创建,当Eden Space满了的时候,gc就把所有在Eden Space中的对象扫描一次,把所有有效的对象复制到第一个Survivor Space,同时把无效的对象所占用的空间释放。当Eden Space再次变满了的时候,就启动移动程序把Eden Space中有效的对象复制到第二个Survivor Space,同时,也将第一个Survivor Space中的有效对象复制到第二个Survivor Space。如果填充到第二个Survivor Space中的有效对象被第一个Survivor Space或Eden Space中的对象引用,那么这些对象就是长期存在的,此时这些对象将被复制到Permanent Generation。若垃圾收集器依据这种小幅度的调整收集不能腾出足够的空间,就会运行Full GC,此时JVM GC停止所有在堆中运行的线程并执行清除动作。

年轻代中的GC
HotSpot JVM把年轻代分为了三部分:1个Eden区和2个Survivor区(分别叫from和to)。默认比例为8:1,为啥默认会是这个比例,接下来我们会聊到。一般情况下,新创建的对象都会被分配到Eden区(一些大对象特殊处理),这些对象经过第一次Minor GC后,如果仍然存活,将会被移到Survivor区。对象在Survivor区中每熬过一次Minor GC,年龄就会增加1岁,当它的年龄增加到一定程度时,就会被移动到年老代中。因为年轻代中的对象基本都是朝生夕死的(80%以上),所以在年轻代的垃圾回收算法使用的是复制算法,复制算法的基本思想就是将内存分为两块,每次只用其中一块,当这一块内存用完,就将还活着的对象复制到另外一块上面。复制算法不会产生内存碎片。
在GC开始的时候,对象只会存在于Eden区和名为“From”的Survivor区,Survivor区“To”是空的。紧接着进行GC,Eden区中所有存活的对象都会被复制到“To”,而在“From”区中,仍存活的对象会根据他们的年龄值来决定去向。年龄达到一定值(年龄阈值,可以通过-XX:MaxTenuringThreshold来设置)的对象会被移动到年老代中,没有达到阈值的对象会被复制到“To”区域。经过这次GC后,Eden区和From区已经被清空。这个时候,“From”和“To”会交换他们的角色,也就是新的“To”就是上次GC前的“From”,新的“From”就是上次GC前的“To”。不管怎样,都会保证名为To的Survivor区域是空的。Minor GC会一直重复这样的过程,直到“To”区被填满,“To”区被填满之后,会将所有对象移动到年老代中。

java面试之JVM和并发_第1张图片
新生代进入老年代的条件

  1. 躲过15次minor gc之后

也就是分代年龄达到15之后会进入老年代。这个具体是多少岁进入老年代,可以通过JVM参数 “-XX:MaxTenuringThreshold”来设置,默认情况是15岁
2. 动态对象年龄判断

假如说当前放对象的Survivor区域里一批对象的 总大小大于了这块Survivor区域的内存大小的50% ,那么此时大于等于这批对象年龄的对象,就可以直接进入老年代了
另外我们要理清楚一个概念,这个实际这个规则运行的时候是如下的逻辑:年龄1+年龄2+年龄n的多个年龄对象总和超过了Survivor区域的50%,此时就会把年龄n以上的对象都放入老年代

在没有回收的情况下 所有对象存活着
比如说一块s区块 100M 如果第一次有20M 不到老年代 第二次来了51M存活 如果之前的20M全部存活,那么这51M和20M将全部到老年代

另外一种情况 在这3次的对象都持续引用,不能回收的情况下,比如说一块s区块 100M 如果第一次有10M 不到老年代 第二次20M 第三次 31M 那么就会由于20+31>50了 那么第一次的10M就会到老年代了。

  1. 大对象直接进入老年代

有一个JvM参数,就是 -XX:PretenureSizeThreshold“,可以把他的值设置为字节数,比如“1048576”,就是1M

如果你创建一个大于这个大小的对象,比如一个超大的数组,或者是别的啥东西,此时就直接把这个大对象放在老年代中,压根不会经过新生代,这样可以避免新生代出现那种大对象,然后在2个Survivor区域里回来复制多次之后才能进入老年代

4.MinorGC后的对象太多无法放入Survivor区怎么办?

如果在Minor GC之后发现剩余的存活对象太多了,没办法放入另外一块Survivor,那么这个时候就必须得把这些对象直接转移到老年代中去

5.老年代空间分配担保规则

在执行任何一次Minor GC之前,JVM会检查一下老年代可用的可用内存空间,是否大于新生代所有对象的总大小

为啥会检查这个呢?因为最极端的情况下,可能新生代的Minor GC过后,所有对象都存活下来了,那岂不是新生代所有对象全部都要进入老年代?

如果说发现老年代的内存大小是大于新生代所有对象的,此时就可以放心大胆的对新生代发起一次Minor GC了,也可以转移到老年代去。

但是假如执行Minor GC之前,发现老年代的可用内存已经小于了新生代的全部对象大小了,那么这个时候是不是有可能在Minor GC之后新生代的对象全部存活下来,然后全部需要转移到老年代去,但是老年代空间又不够?

所以假如Minor Gc之前,发现老年代的可用内存已经小于看新生代的全部对象大小了,就会看一个-XX:-HandlePromotionFailure的参数是否设置了,如果有这个参数,那么就会继续进行下一步判断,

下一步判断,就是看老年代的内存大小,是否大于之前每一次Minor GC后进入老年代的对象的平均大小。

举个例子,之前每次Minor GC后,平均都有10MB左右的对象会进入老年代,那么此时老年代可用内存大于10MB

这就说明很可能这次Minor GC过后也是差不多10MB左右的对象会进入老年代,此时老年代空间是够的

如果上面那个步骤判断失败了,或者是 -XX:-HandlePromotionFailure“参数没设置,此时就会直接触发一次Full GC,就是对老年代进行垃圾回收,尽量腾出来一些内存空间,然后再执行Minor GC

如果上面2个步骤都判断成功了,那么就是说可以冒点风险尝试一下Minor GC 此时进行Minor GC,此时进行Minor GC有几种可能:

(1)Minor GC过后,剩余的存活对象的大小,是小于Survivor区的大小的,那么此时存活对象进入Survicor区域即可

(2)Minor GC过后,剩余的存活对象的大小是大于Survivor区域的大小,但是是小于老年代可用内存大小的,此时就直接进入老年代即可

(3)Minor GC过后,剩余的存活对象的大小,大于了Survivor区域的大小,也大于了老年代可用内存的大小,此时老年代都放不下这些存活对象了,就会发生Handle Promotion Failure的情况,这个时候就会触发一次Full GC

Full GC就是对老年代进行垃圾回收,同时也一般会对新生代进行垃圾回收。
因为这个时候必须把老年代理的没人引用的对象给回收掉,然后才可能让Minor GC过后剩余的存活对象进入老年代里面
如果要Full GC过后,老年代还是没有足够的空间存放Minor GC过后的剩余存活对象,那么此时就会导致所谓的OOM内存溢出了

Major GC和Full GC区别
Full GC:收集young gen、old gen、perm gen
Major GC:有时又叫old gc,只收集old gen

6、JVM中哪些内存区域会发生内存溢出(OOM)

栈溢出、堆溢出、方法区溢出、本机直接内存溢出

7、JVM在创建对象是采用了哪些并发安全机制

JVM创建对象的过程:类加载、检查加载、分配内存、内存空间初始化、设置对象头、对象初始化
划分内存的方式:指针碰撞、空闲列表
解决并发安全:CAS+失败重试(使用乐观锁的机制,有一定的内存开销)、本地线程分配缓冲
“指针碰撞”(Bump the Pointer)(默认用指针碰撞)
 如果Java堆中内存是绝对规整的,所有用过的内存都放在一边,空闲的内存放在另一边,中间放着一个指针作为分界点 的指示器,当创建新的对象时,所分配内存就仅仅是把那个指针向空闲空间那边挪动一段与对象大小相等的距离。

“空闲列表”(Free List)
  如果Java堆中的内存并不是规整的,已使用的内存和空闲的内存相互交错,那就没有办法简单地进行指针碰撞了,虚拟机就必须维护一个列表,记录上哪些内存块是可用的,在分配的时候从列表中找到一块足够大的空间划分给对象实例,并更新列表上的记录。

CAS
  虚拟机采用CAS配上失败重试的方式保证更新操作的原子性来对分配内存空间的动作进行同步处理。
本地线程分配缓冲
每个线程在java堆中预先分配一小块私有内存,也就是本地线程分配缓冲,这样没有线程都独立拥有一个buffer,如果需要分配内存,就在自己的buffer上分配,这样就不存在竞争的情况,可以大大提升分配效率

8. 什么是对象头?对象头里面有哪些东西

初始化零值之后,虚拟机要对对象进行必要的设置,例如这个对象是哪个类的实例、如何才能找到类的元数据信息、对象的哈希码、对象的GC分代年龄等信息。
java对象的内存布局分为三部分,对象头、实例数据、对齐补白
对象头:

  1. 存储;对象自身的运行数据(Mark Word)其中包含哈希值、GC分代年龄、锁状态标识、线程持有的锁、偏向线程ID、偏向时间戳
  2. 类型指针:即对象;指向它的类元数据的指针
  3. 若为对象数组,还应有记录数组长度的数据
    对齐补白:vm要求java对象的起始地址必须是8的倍数,如果对象头+实例长度不足8的倍数,就需要补齐

9.为什么不要使用Finalize方法

一个对象要被回收,需要经过两次过程,一次是没有找到GCRoots的引用链,它将被第一次标记,随后进行一次筛选(如果对象覆盖了finalize),我们可以在finalize方法中去拯救(变为存活对象)

  1. finalize方法执行的优先级很低(需要等待)
  2. finalize方法只执行一次
public class Demo {
    public static Demo instance=null;
    public void iaAlive()
    {
        System.out.println("I am still Alive ");
    }

    @Override
    protected void finalize() throws Throwable {
        super.finalize();
        System.out.println("finalize method executed");
        Demo.instance=this;
    }


    public static void main(String[] args) throws InterruptedException {
        instance=new Demo();
        //对象第一次进行gc
        instance=null;
        System.gc();
        Thread.sleep(1000);//方法优先级低,需要等待
        if(instance!=null)
        {
            instance.iaAlive();
        }
        else
        {
            System.out.println("I am dead");
        }
        //对象第二次GC
        instance=null;
        System.gc();
        Thread.sleep(1000);
        if(instance!=null)
        {
            instance.iaAlive();
        }
        else
        {
            System.out.println("I am dead");
        }

    }
}

10怎么判断对象的存活

  1. 引用计数法
    引用计数器法:每个对象设置一个引用计数器,当对象被引用,计算器加1,当引用失效,计算器减一。当计数器为0时,表示引用失效,也就是"死对象",可以被垃圾回收机制回收。
    缺陷:无法解决循环依赖的问题。有两个对象A、B。当A引用B,B引用A时,那么此时A、B对象都不为0,垃圾回收机制无法被回收。
  2. 可达性分析法
    从GCRoot开始向下搜索,如果一个对象没有与任何引用链相连时,表示这个对象是垃圾对象,可以被垃圾回收机制回收。
    GC Roots的对象:
  • 虚拟机栈中的引用对象
  • 方法区类静态引用对象
  • 方法区常量池引用对象
  • 本地方法栈JNI引用的对象

11、复制算法

算法就是将可用内存按容量划分为大小相等的两块,每次只使用其中的一块。当这一块内存用完后,将活的对象标记出来,然后把这些活对象复制到另外一块空闲区域上,最后再把已使用过的内存空间完全清理掉。这样每次都是对整个半区进行内存回收,内存分配时也不用考虑内存碎片等复杂情况,只要移动堆顶指针,按顺序分配内存即可,实现方法简单,运行高效。但是这种算法的代价是将内存缩小了一半。

12、标记清除算法

  • 标记阶段 : cellector从mutator根对象开始进行遍历,对从mutator根对象开始就可以访问到的对象都打上一个标识,一般实在对象的header中,将其记录为可达对象.
  • 清除阶段 : collector从堆内存从头至尾开始线性遍历,如果发现某个对象没有被标记为可达对象,则将其回收.
  • 标记方法:引用计数法、可达性分析
  • 不足:标记和清除效率都比较低,标记会产生大量不连续的内存碎片

13、标记整理算法

  • 使用场景:老年代、适合对象存活率较高的
  • 概述:先标记,然后对存活的对象进行移动,全部移动到一端,然后再对其它的内存进行清理。空间利用率100%,涉及对象的移动效率低

14.为什么扩容新生代可以提高GC的效率

java面试之JVM和并发_第2张图片
垃圾回收一般分为两个部分:1.扫描新生代判断对象是否存活,2. 将存活对象复制到S区,复制对象地方时间远远大于扫描的时间
假设对象A存活时间为7秒
新生代空间200M,每隔5秒GC一次,每次GC耗时100ms
此时gc一次时间为T1+T2
对新生代进行扩容:空间为400M,没10秒GC一次,每次GC耗时200ms
此时gc耗时2T1(对象A的存活时间已经到了,其自动消亡,不要复制)

15简述一下CMS垃圾回收器,他有哪些问题

java面试之JVM和并发_第3张图片

Concurrent Mark Sweep(并发标记清除算法的垃圾回收器,适用于老年代)
取最短回收停顿时间为目标的老年代收集器,适合基于B/S的服务器上,系统停顿时间短,用户体验较好。CMS也是一款真正意义上的并发收集器,能够与用户线程同时进行。虽然,并发回收过程中也有几个阶段需要Stop the world,但是由于任务简单,所以停顿时间非常短。
步骤

  • 初始标记:标记老年代中所有的GC Roots引用的对象,标记老年代中被年轻代中活着的对象引用的对象
  • 并发标记:从“初始标记”阶段标记的对象开始找出所有存活的对象;并发标记的特点是和应用程序线程同时运行。并不是老年代的所有存活对象都会被标记,因为标记的同时应用程序会改变一些对象的引用等。
  • 重新标记:该阶段的任务是完成标记整个年老代的所有的存活对象。暂停所有的线程
  • 并发消除:除那些没有标记的对象并且回收空间

CMS中的问题

  • CPU敏感:并发决定因素是CPU核心数(要求CPU多)
  • 浮动垃圾:这里并发清除的时候,用户线程也在运行,这时候,还是会产生垃圾,这就是浮动垃圾
  • 内存碎片:CMS是基于标记-清除算法的,只会将标记为为存活的对象删除,并不会移动对象整理内存空间,会造成内存碎片(会发生垃圾回收器退化为serialOld 单线程的垃圾回收标记清除算法)

16在java中守护线程和用户线程的区别

java中的线程分为两种守护线程(Daemon)和用户线程(user)
守护线程通过调用Thread.setDaemon(true)设置
一般程序使用的都是用户线程
守护线程我们一般用不上,比如垃圾回收线程就是守护线程(Daemon)

使用守护线程的注意点

  1. Thread.setDaemon()必须在Thread.start()之前调用,否则运行时会抛出异常
  2. 守护线程是为其他线程提供服务的,如果全部的用户线程已结束,守护线程没有可服务的线程,JVM关闭

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

CPU时间片:CPU时间片是CPU分配给每个线程执行的时间段,一般为几十毫秒
上下文切换:当一个线程的时间片用完了,或者因自身原因被迫暂停运行了,这个时候另外一个线程(可以是同一个线程或者其他进程中的线程)就会被操作系统选中。来占用处理器。这个一个线程被暂停剥夺使用权,另外一个线程被选中或者继续进行的过程叫作上下文切换。
上下文:在这种切入切出的过程中,操作系统需要保存和恢复响应的进度信息,这个进度信息就是上下文

18什么是死锁?死锁的危害

死锁:是指两个或两个以上的进程(或线程)在执行的过程中,因争夺资源而造成的一种相互等待的现象,若无外力作用,他们都将无法进行下去

危害

  1. 死锁会使进程得不到正确的结果
  2. 死锁会使资源的利用率降低
  3. 死锁还会导致产生新的死锁

19Executor和Executors的区别

Executor 接口对象能执行我们的线程任务
Executors 工具类的不同方法按照我们的需求创建了不同的线程池,来满足业务的需求。

20什么是CAS操作,缺点是什么

CAS(Compare And Swap),即比较并交换 CAS(V,E,N)。是解决多线程并行情况下使用锁造成性能损耗的一种机制,CAS操作包含三个操作数——要更新的变量(V)、预期原值(E)和新值(N)。核心算法是如果V 值等于E 值,则将V 的值设为N 。若V 值和E 值不同,则说明已经有其他线程做了更新,则当前线程不做更新,直到V、E两个值相等,才更新V的值。
缺点

  1. 开销大
  2. 只能保存一个共享变量
  3. ABA问题(因为CAS需要在操作值的时候检查下值有没有发生变化,如果没有发生变化则更新,但是如果一个值原来是A,变成了B,又变成了A,那么使用CAS进行检查时会发现它的值没有发生变化,但是实际上却变化了。ABA问题的解决思路就是使用版本号。在变量前面追加上版本号,每次变量更新的时候把版本号加一,那么A-B-A 就会变成1A-2B-3A。)

21、Lock接口是什么?对比synchronized接口他有什么优势

Lock接口比同步方法同步代码块提供了更具扩展性的锁操作
Lock是synchronized的扩展版,Lock提供了无条件的,可轮询的(tryLock方法)、定时的(tryLock带参方法)、可中断的锁等操作,另外Lock的实现类基本都支持非公平锁(默认锁)和公平锁,synchronize只支持非公平锁,当然在大部分情况下,非公平锁是最高效的选择

22、什么是阻塞队列?阻塞队列的实现原理是什么

阻塞队列(BlockingQueue)是一个支持两个附加操作的队列
这两个附加的操作是:当队列为空时,获取元素的线程会等待队列变为非空;当队列满时,存储元素的线程会等待队列可用
阻塞 队列在实现上,主要利用了Condition和Lock的等待通知模式

23、什么是Callable和Future?

Callable接口类似于Runnable接口,但是Runnable不会返回结果,无法抛出返回结果的异常,而Callable的功能更强大一些,被线程执行后可以返回值 ,这个返回值可以被Future拿到,也就是说,Future可以拿到异步执行任务的返回值。
Callable可以认为带有回调的Runnable
Future接口表示异步任务,是还没有完成任务给出的 未知结果,所有Callable用于产生结果,Future用于获取结果

24、什么是FutureTask

FutureTask表示一个可以取消的异步运算,他有启动和取消运算、查询运算是否完成和取回运算结果等方法,只有当运算完成的时候结果才能取回,如果运算尚未完成get方法将会阻塞
一个FutureTask对象可以调用Callable和Runnable的对象进行包装,由于FutureTask也是调用了Runnable接口所有他可以交给Executor执行

25、什么是并发容器的实现

并发容器可以简单的理解为通过synchronized来实现同步的容器,如果有多个线程调用同步容器的方法,他们将会串行执行。比如vector、HashTable以及Collections.synchronizedSet,synchronizedList等方法返回容器

26、为什么我们调用start()方法是会执行run()方法,为什么不能直接;调用run()方法

当你调用start()方法是你将创建新的线程,并且执行run()方法里面的代码
如果你直接调用run()方法,他不会创建新的线程也不会调用线程的代码,只是将run()方法作为普通的方法区执行

27、什么是不可变对象,它对写并发应用有什么帮助

不可变对象(Immntable Objects)即对象一旦被创建他的状态(对象的数据即对象的属性值)就不能改变,反之即为可变对象(Mutable Objects)
Java平台类库中包含许多不可变类,如String、基本类型的包装类、BigInteger和BigDecimal等。
不可变对象天生是线程安全的。他们的常量(域)是在构造函数中创建的。既然他们的状态无法修改,这些常量永远不会变

28、乐观锁和悲观锁的理解及如何实现,有哪些实现方式

悲观锁:总是假设最坏的情况,每次去拿数据的时候都认为别人会修改,所有每次在拿数据的时候都会上锁,这样别人想拿数据的时候就会阻塞直到他拿到锁。java里面synchronize关键字实现的是悲观锁
乐观锁:顾名思义就是很乐观,每次去拿数据的时候都认为别人不会修改,所有不会上锁,但是在更新的时候会判断一下在此期间别人有没有去更新这个数据,可以使用版本号等机制,java中原子变量就是使用了乐观锁

乐观锁的实现方式:使用版本标识来确定读到的数据和提交时的数据是否一致。提交后修改版本标识,不一致时可以采用丢弃和再次尝试的策略
java中的Compare and Swap即CAS,当多个线程尝试使用CAS同时更新同一个变量时,只有其中一个线程可以更新变量的值,而其他线程都失败,失败的线程并不会被挂起,而是被告知这次竞争中失败,并且可以再次尝试

29、在java中wait和sleep的不同

  1. 两个方法声明的位置不同: Thread类中用sleep(),Object类中用wait()
  2. 调用的要求不同:sleep()可以使用在任何需要的场景下调用,wait()的使用必须的同步代码块或同步方法中使用
  3. 关于是否释放同步监视器:如何两个方法都在同步代码块或同步方法中,sleep()不会释放同步监视器,wait会释放同步监视器
  4. wait会释放锁而sleep不会释放锁
  5. wait常常用于线程间的交互,sleep通常被用于暂停执行

30、为什么wait、notify、notifyAll这些方法不在Thread类里面

java提供的锁是对象级的而不是线程级的。
每个对象都有锁,通过线程获得,如果线程需要等待某些锁那么调用对象中的wait()方法就有意义了。
如果wait方法定义在Thread类中,线程正在等待的是哪个锁就不明显了。
简单的锁,由于wait、notify、notifyAll都是锁级别的操作,所有把他们定义为Object类中因为锁属于对象。

31、什么是线程安全

当多个线程访问某个类时,不管运行环境采用了何种调度方式或者这些线程将如何交替运行,并且在调用代码中不需要额外的同步或者协同,这个类都能表现出正确的行为,那么就称这个类是线程安全的

实现线程安全的方式

  1. 不可变
    像String、Integer、Long这些都是final类型的类,任何一个线程都改不了他们的值,要改变除非新建一个,因此这些不可变的对象不需要任何同步收到就可以直接在多线程的环境下使用
  2. 加锁和CAS
    我们最常用的保证线程安全的手段,使用synchronize关键字,使用显示锁、使用各种原子变量、修改数据时使用CAS机制等等

32、一个线程如果出现了运行时异常会怎么样?

  1. 如果这个线程没有捕获的话,这个线程就停止执行
  2. 如果该异常被捕获或者抛出,则程序继续运行

另外重要的一点是:如果这个线程持有某个对象的监视器,那么这个对象监视器会被立即释放

33、java中volatie变量有什么作用

关键字 volatile 是 Java 虚拟机提供的最轻量级的同步机制。
一个变量被定义为 volatile 的特性:

  1. 保证此变量对所有线程的可见性。但是操作并非原子操作,并发情况下不安全。
如果不符合 运算结果并不依赖变量当前值,或者能够确保只有单一的线程修改变量的值 和 变量不需要与其他的状态
变量共同参与不变约束 就要通过加锁(使用 synchronize 或 java.util.concurrent 中的原子类)来保证原子性。
  1. 禁止指令重排序优化。
通过插入内存屏障保证一致性。

34、Threadlocal的作用

ThreadLocal解决多线程的并发问题,是Thread的局部变量,使用它维护变量,会使该变量的线程提供一个独立的副本,可以独立修改,不会影响其他线程的副本

35、生成者消费者模型的作用是什么

  1. 通过生产者的生产能力和消费者的消费能力来提升整个系统的运行效率,这是生产消费模型中最重要的作用
  2. 解耦,这是生产者消费者模型的附带作用,解耦有以为着生产者消费者直接的联系少,联系越少制约就越少

36、为什么要使用线程池

  1. 节约资源:避免频繁的创建和销毁线程,达到线程对象的重用
  2. 灵活:使用线程池还可以根据项目灵活的控制并发项目

37、java中的线程调度算法是什么

抢占式:一个线程执行完CPU之后,操作系统会根据线程优先级、线程饥饿情况等数据算出一个总的优先级并分配下一个时间片给某个线程执行

38、什么是线程组,为什么在java只不推荐使用

线程组和线程池是两个不同的概念,他们的作用完全不同
线程组是为了方便线程的管理
线程池是为了管理线程的生命周期,复用线程,减少常见销毁线程的开销

  • 线程组ThreadGroup对象中的stop,resume,suspend会导致安全问题,主要是死锁问题,已经被官方废弃,许多价值以及大不如前
  • 线程组ThreadGroup不是线程安全的,在使用的过程中不能及时获取安全的信息

39、java中有几种方法可以实现一个线程

  1. 继承Thread类
  2. 实现Runnable接口
  3. Callable接口和FutureTask接口,需要实现的是call()方法
  4. 线程池创建线程

40、SynchronizedMap和ConcurrentHashMap有什么区别

SynchronizedMap一次锁住整张表来保证线程安全,所以每次只有一个线程来访问map(强一致性)
ConcurrentHashMap使用分段锁来保证多线程下的性能(弱一致性)
强一致性:任何时刻线程读到的缓冲数据都是一样的
弱一致性:不能保证任何一次都能读到最近一次写入的数据,但能保证最终可以读到写入的数据

41、ConcurrentHashMap并发度是什么

JDK1.7
ConcurrentHashMap把实际的map分成若干部分来实现他的可扩展性和线程安全。这种划分是使用并发度获得的,他是ConcurrentHashMap类构造函数的一个可选参数,默认值为16,这样在多线程的情况下可以避免使用
在JDK1.8后。他摒弃了Segment(段锁)的概念,而是启用了一种全新的方式实现利用CAS算法。同时加入更多的辅助变量来提高并发度

42、Thread.sleep(0)的作用是什么

由于大部分操作系统采用抢占式的线程调度算法,因此可能出现出现某条线程常常获取到CPU控制权的情况,为了让某些某些优先级比较低的也能获取到CPU的使用权,可以使用Thread.sleep(0)手动触发一次操作系统分配时间片段的操作,这也是平衡CPU控制权的一种操作
大循环里面可以写一句Thread.sleep(0),因为这样给了其他线程获取CPU控制权的权力,这样线程就不会处于假死状态

43、什么是线程调度器什么是时间片

线程调度器是一个操作系统的服务,他负责为Runnable状态的线程分配CPU时间,一旦我们创建线程并启动他,他的执行便依赖于线程调度器得实现
CPU时间片是CPU分配给每个线程执行的时间片,一般为几十毫秒
时间分片是指将可用的CPU时间分配给可用的Runnable线程的过程,分配CPU的时间可以基于线程优先级或线程的等待时间

44、如何确保main()方法所在的线程是java程序最后结束的线程

可以使用Thread类的join方法来确保所有程序创建的线程在main()方法退出去结束

45、什么是Java Timer类?如何创建一个有特定时间间隔的任务?

java.util.Timer是一个工具类,可以用于安排一个线程在未来的某个特定时间执行。Timer类可以用安排一次性任务或者周期性任务

46、java中的Semaphore

Semaphore就是一个信号量,他的作用是现在某段代码块的并发数
Semaphore有一个构造函数,可以传入一个int型的整数n,表示某个代码最多只有n个线程可以访问,如果超出了n,那么请等待,等到某个线程执行完毕这个代码块,下一个线程在进入
由此可以看出如果Semaphore构造函数中传入的int类型的整数n=1,相当于变成了一个synchronize了

47、为什么会代码重排序

在不影响单线程程序执行结果的前提下,计算机为了最大限度的发挥机器性能,会对指令重排序优化

48、Java中ReadWriteLock是什么?

ReadWriteLock接口的实现是ReentrantReadWriteLock
ReentrantLock等大部分锁是排它锁,这些锁在同一时刻只允许一个线程进行访问,而读写锁在同一时刻允许多个读线程访问,但是在写线程访问时,所有的读线程和其他写线程均被阻塞,读写锁维护了一对锁,一个读锁和一个写锁,通过分离读锁和写锁,使得并发性相对于一般的排它锁有了很大的提升
一般情况下,读写锁的性能都会比排它锁好,因为大多数常见读是多于写的。在读多于写的情况下,读写锁能够提供比排它锁更好的并发性和吞吐量

49、volatile变量和atomic变量有什么区别

Volatile变量可以确保先行关系,即写操作会发生在后续的读操作之前,但他并不能保证原子性,例如volatile修饰count变量那么count++操作就不是原子性。
而AtomicInterger类提供的atomic方法可以让这种操作具有原子性如getAndIncrement()方法会原子性进行增量操作把当前值加一,其他数据类型和引用变量也可以进行相似的操作

50、怎么监测一个线程是否持有对象监视器

Thread.holdsLock(Object obj)方法,当且仅当对象的obj的监视器被某条线程持有的时候才会返回true
注意这里是一个static方法,这意味着**“某条线程”指的是当前线程**

51、什么是AQS

用来构建锁或其他同步组件的基础框架,比如ReentrantLock、ReentrantReadWriteLock和CountDownLatch就是基于AQS实现的,他使用了一个int成员变量表示同步状态,通过内置FIFO队列俩完成资源获取线程的队列工作。他是CLH队列锁的一种变体实现,他有两种实现方式:独占式、共享式
AQS主要使用方式是继承,子类通过继承;AQS并实现他的抽象方法来管理他的同步状态,同步器的设计基于模板方法模式,所有要实现我们自己的同步工具类就需要覆盖其中几个可重写的方法,如:tryAcquire,tryReleaseShared等等

52、线程类的构造方法、静态块时被那个线程调用的

线程类的构造方法、静态方法是被new这个线程类所在的线程所调用的,而run方法里面的代码才是被线程自身所调用的

假设Thread2中new了Thread1,main函数中new了Thread2 那么
(1)Thread2的构造方法、静态方法是main线程调用,Thread2的run方法是Thread1自己调用的
(2) Thread1的构造方法、静态方法是Thread2调用,Thread1的run方法是Thread1自己调用的

53、多线程的同步和互斥有哪几种方法

  1. 临界区:适合一个进程内的多线程访问公共区域或代码段时使用
  2. 互斥量:适合不同进程内的多个线程访问公共区域或代码段时使用
  3. 事件:通过线程间的触发事件实现同步互斥
  4. 信号量:与临界区和互斥量不同,可以实现多个线程同时访问公共区域,原理与操作系统中的PV操作相似,先设置一个访问公共区域的线程最大连接数,没有一个线程访问共享区资源数就减一,直到资源数小于等于0

54、什么是竞争条件?怎样发现和解决竞争条件?

多线程中线程不确定的执行时序导致不正确的结果,这就是竞争(竞态)条件
可以采用加锁的方式使线程串行访问临界区

55、为什么wait和notify方法要在同步块中调用?

wait/notify是线程之间的通信,他们存在竞态,我们必须保证在满足条件的情况下才进行wait。换句话说,如果不加锁的话,那么wait被调用的时候可能wait的条件已经不满足了

56、什么是阻塞式方法?

阻塞式方法是指程序会一直等待该方法完成期间不做其他事情,ServerSocket的accept()方法就是一直等待客户端连接。这里的阻塞是指调用结果返回之前,当前线程会被挂起,直到得到结果之后返回

57、什么是安全点什么是安全区域

安全点
用户线程暂停,GC 线程要开始工作,但是要确保用户线程暂停的这行字节码指令是不会导致引用关系的变化。所以 JVM 会在字节码指令中,选一些指令,作为“安全点”,比如方法调用、循环跳转、异常跳转等,一般是这些指令才会产生安全点。
为什么它叫安全点,是这样的,GC 时要暂停业务线程,并不是抢占式中断(立马把业务线程中断)而是主动是中断。
主动式中断是设置一个标志,这个标志是中断标志,各业务线程在运行过程中会不停的主动去轮询这个标志,一旦发现中断标志为 True,就会在自己最近的“安全点”上主动中断挂起。
安全区域
为什么需要安全区域?
要是业务线程都不执行(业务线程处于 Sleep 或者是 Blocked 状态),那么程序就没办法进入安全点,对于这种情况,就必须引入安全区域。
安全区域是指能够确保在某一段代码片段之中, 引用关系不会发生变化,因此,在这个区域中任意地方开始垃圾收集都是安全的。我们也可以把安全区城看作被扩展拉伸了的安全点。
当用户线程执行到安全区域里面的代码时,首先会标识自己已经进入了安全区域,这段时间里 JVM 要发起 GC 就不必去管这个线程了。
当线程要离开安全区域时,它要 JVM 是否已经完成了(根节点枚举,或者其他 GC 中需要暂停用户线程的阶段)
1、如果完成了,那线程就当作没事发生过,继续执行。
2、否则它就必须一直等待, 直到收到可以离开安全区域的信号为止。

58、如何打印JVM日志

一般只要开启gc日志打印,都会默认开启简单日志模式,生成环境强烈建议开启详细gc日志模式,两种模式互斥,同时开启详细的gc日志模式

  • -XX:+PrintGC //简单gc日志模式
  • -XX:+PrintGCDetails //详细GC日志模式

你可能感兴趣的:(java,java,面试)