Java多线程面试题

2019-08-16  本文已影响0人  红茶玛奇朵鸭

文章的目的主要是针对面试官的提问,做出尽可能精简而全面的回答。若读者对某块的知识不能太理解,还请参阅其他大佬比较详细的博客或者专业书籍,谢谢大家。 


一、synchronized关键字

1.基本使用

        synchronized关键字的主要作用有三个:

1.原子性:能确保线程互斥地访问同步代码。

虚拟机没有吧lock与unlock操作提供给用户使用,但是却提供了更高层次的字节码指令monitorenter与monitorexit来隐式地使用这两个原子操作,这两个字节码指令反映到Java代码当中就是同步块------synchronized关键字,因此在synchronized块之间的操作也具备原子性。

2.可见性:保证共享变量的修改在各线程之间可见。

通过Java内存模型中的 “对一个变量unlock操作之前,必须要同步到主内存中;如果对一个变量进行lock操作,则将会清空工作内存中此变量的值,在执行引擎使用此变量前,需要重新从主内存中load操作或assign操作初始化变量值” 来保证的;

3.有序性:保证线程之间操作的有序性。

一个变量在同一时刻只允许一条线程对其进行lock操作。

        三种应用方式:

1.修饰实例方法,作用于当前实例加锁,进入同步代码前要获得当前实例的锁

2.修饰静态方法,作用于当前类对象加锁,进入同步代码前要获得当前类对象的锁

3.修饰代码块,指定加锁对象,对给定对象加锁,进入同步代码库前要获得给定对象的锁

2.底层原理

参考大佬博客,这里总结一下。深入理解Java并发之synchronized实现原理

        在java虚拟机中,对象在内存中的布局分为三个区域:对象头、实例数据和对齐填充。java对象头是实现synchronized锁对象的基础。对于synchronized对象锁,也就是重量级锁,java对象头中的Mark Word部分包含一个指向monitor对象(也叫管程或者监视器锁)的起始地址,每个对象都存在着一个 monitor 与之关联,对象与其 monitor 之间的关系有存在多种实现方式,如monitor可以与对象一起创建销毁或当线程试图获取对象锁时自动生成,但当一个 monitor 被某个线程持有后,它便处于锁定状态。在Java虚拟机(HotSpot)中,monitor是由ObjectMonitor实现的。

        ObjectMonitor中有一个_owner变量,用于指向当前持有monitor对象即持有监视器锁的线程。ObjectMonitor中还有两个队列, _EntryList和_WaitSet,用于保存_ObjectWaiter对象列表(线程进入时会被封装成这个对象)。

        当多个线程同时访问一段同步代码时,首先会进入 _EntryList 集合,当线程获取到对象的monitor 后进入 _Owner 区域并把monitor中的owner变量设置为当前线程同时monitor中的计数器count加1,若线程调用 wait() 方法,将释放当前持有的monitor,owner变量恢复为null,count自减1,同时该线程进入 WaitSet集合中等待被唤醒。

    (monitor对象存在于每个Java对象的对象头中(存储的指针的指向),synchronized锁便是通过这种方式获取锁的,也是为什么Java中任意对象可以作为锁的原因,同时也是notify/notifyAll/wait等方法存在于顶级对象Object中的原因)

synchronized代码块底层原理

        同步语句块的实现使用的是monitorenter 和 monitorexit 指令,其中monitorenter指令指向同步代码块的开始位置,monitorexit指令则指明同步代码块的结束位置。

        当执行monitorenter指令时,当前线程将试图获取 objectref(即对象锁) 所对应的 monitor 的持有权,当 objectref 的 monitor 的进入计数器为 0,那线程可以成功取得 monitor,并将计数器值设置为 1,取锁成功。如果当前线程已经拥有 objectref 的 monitor 的持有权,那它可以重入这个 monitor (关于重入性稍后会分析),重入时计数器的值也会加 1。倘若其他线程已经拥有 objectref 的 monitor 的所有权,那当前线程将被阻塞,直到正在执行线程执行完毕,即monitorexit指令被执行,执行线程将释放 monitor(锁)并设置计数器值为0 ,其他线程将有机会持有 monitor 。

        值得注意的是编译器将会确保无论方法通过何种方式完成,方法中调用过的每条 monitorenter 指令都有执行其对应 monitorexit 指令,而无论这个方法是正常结束还是异常结束。为了保证在方法异常完成时 monitorenter 和 monitorexit 指令依然可以正确配对执行,编译器会自动产生一个异常处理器,这个异常处理器声明可处理所有的异常,它的目的就是用来执行 monitorexit 指令。(synchronized关键字线程执行完同步代码或者线程执行过程中发生异常会自动释放锁)

synchronized同步方法底层原理

        方法级的同步是隐式,即无需通过字节码指令来控制的,它实现在方法调用和返回操作之中。JVM可以从方法常量池中的方法表结构(method_info Structure) 中的 ACC_SYNCHRONIZED 访问标志区分一个方法是否同步方法。当方法调用时,调用指令将会 检查方法的 ACC_SYNCHRONIZED 访问标志是否被设置,如果设置了,执行线程将先持有monitor(虚拟机规范中用的是管程一词), 然后再执行方法,最后再方法完成(无论是正常完成还是非正常完成)时释放monitor。在方法执行期间,执行线程持有了monitor,其他任何线程都无法再获得同一个monitor。如果一个同步方法执行期间抛 出了异常,并且在方法内部无法处理此异常,那这个同步方法所持有的monitor将在异常抛到同步方法之外时自动释放。

二、volatile关键字

基本使用:

        1.原子性:保证此变量对所有线程的可见性,当一个线程修改了这个变量的值,volatile保证新值能立即同步到主内存以及每次使用前立即从主内存中刷新。

        2.有序性:防止指令重排序优化。有volatile修饰的变量。会在赋值操作后面加一个内存屏障,指令重排序时不能把后面的指令重排序到内存屏障之前的位置。

        volatile变量不会执行加锁操作,因此就不会使执行线程阻塞,volatile是一种比synchronized关键字更轻量级的同步机制。

三、为什么notify/notifyAll/wait等方法不在Thread类而在Object类里面?wait和sleep方法有什么区别?

大佬博客wait()、notify()和notifyAll()方法为什么属于Object

        1.Java中,任何对象都可以作为锁,既然wait是放弃对象锁,当然就要把wait定义在这个对象所属的类中。更通用一些,由于所有类都继承于Object,我们完全可以把wait方法定义在Object类中,这样,当我们定义一个新类,并需要以它的一个对象作为锁时,不需要我们再重新定义wait方法的实现,而是直接调用父类的wait(也就是Object的wait),此处,用到了Java的继承。 

        2.有的人会说,既然是线程放弃对象锁,那也可以把wait定义在Thread类里面啊,新定义的线程继承于Thread类,也不需要重新定义wait方法的实现。然而,这样做有一个非常大的问题,一个线程完全可以持有很多锁,你一个线程放弃锁的时候,到底要放弃哪个锁?当然了,这种设计并不是不能实现,只是管理起来更加复杂。

        wait方法调用完成后,线程将被暂停,但wait方法将会释放当前持有的监视器锁(monitor),直到有线程调用notify/notifyAll方法后方能继续执行,而sleep方法只让线程休眠并不释放锁。同时notify/notifyAll方法调用后,并不会马上释放监视器锁,而是在相应的synchronized(){}/synchronized方法执行结束后才自动释放锁。

四、为什么notify/notifyAll和wait方法必须在synchronized代码块或者synchronized方法调用

        在使用notify/notifyAll和wait方法这3个方法时,必须处于synchronized代码块或者synchronized方法中,否则就会抛出IllegalMonitorStateException异常。

        这是因为调用这几个方法前必须拿到当前对象的监视器monitor对象,也就是notify/notifyAll和wait方法依赖于monitor对象,在前面的分析中,我们知道monitor 存在于对象头的Mark Word 中(存储monitor引用指针),而synchronized关键字可以获取 monitor 。

五、synchronized与lock的区别

从六个方面进行比较

1.存在层次

        Synchronize是一个java关键字,在jvm层面。Lock是一个接口。

2.锁的释放

        使用synchronize关键字,线程执行完同步代码或者线程执行过程中发生异常会自动释放锁。而lock必须在finally中释放锁,否则容易造成死锁。

3.锁的获取

        使用synchronize关键字,如果A线程获取锁,B线程等待,如果A线程阻塞,B线程会一直等待下去。而lock锁有多个锁获取的方式,如果尝试获取不到锁,可以不用一直等待。

4.锁的状态

        Synchronize关键字无法判断锁的状态,lock锁可以。

5.锁类型

        Synchronize可重入、不可中断、非公平;lock锁可重入、可中断、可公平。

6.性能

        竞争资源不激烈,两者的性能是差不多的,而当竞争资源非常激烈时(即有大量线程同时竞争),此时Lock的性能要远远优于synchronized。Lock锁适合大量同步的代码的同步问题,synchronized锁适合代码少量的同步问题。

六、java开启线程的方式

        1.继承Thread类,重写run方法

        2.实现Runnable接口,重写run方法

        3.实现Callable接口,重写call方法(有返回值)

        4.使用线程池,Executors类(有返回值)

七、run()和start()方法区别

        程序调用start()方法会创建一个新线程,并且run()方法中的代码会在新线程上运行。

        直接调用run()方法不会创建新线程,run()方法中的代码在当前线程上运行。

八、什么是Daemon线程?它有什么意义?

        守护线程是一种特殊的线程,当进程中不存在非守护线程时,守护线程自动销毁。任何一个守护线程都是整个JVM中所有非守护线程的保姆,只要当前JVM实例中存在非守护线程且没有结束,守护线程就要工作,当最后一个非守护线程结束时,守护线程随着JVM一同结束工作。

        守护线程的作业是为其他线程的运行提供服务。

        典型的守护线程就是垃圾回收线程,当进程中没有非守护线程了,则垃圾回收线程也没有存在的必要了,自动销毁。

九、乐观锁与悲观锁

        乐观锁,每次去拿数据的时候都认为别人不会修改,所以不会上锁,但是在更新的时候会判断一下在此期间别人有没有去更新这个数据。

        悲观锁,每次去拿数据的时候都认为别人会修改,所以每次拿数据的时候都会上锁,别人想拿这个数据就会阻塞知道它拿到锁。

        乐观锁适用于读操作非常多的场景,悲观锁适用于写操作非常多的场景。

        乐观锁在java中的使用,就是无锁编程,常常采用的是CAS算法,典型的例子就是原子类,通过CAS自旋实现原子操作的更新。

        Java中的悲观锁就是Synchronized, AQS框架下的锁则是先尝试cas乐观锁去获取锁,获取不到,才会转换为悲观锁,如RetreenLock。


持续更新!!!!!觉得如果有帮助到你,麻烦评论喜欢加关注哟。

上一篇下一篇

猜你喜欢

热点阅读