Synchronized相关面试题,你接得住吗?

这道题想考察什么?

是否真正了解synchronized关键字java

考察的知识点

synchronized关键字的使用、原理、优化等面试

考生应该如何回答

一、在Java中,synchronized关键字是一个轻量级的同步机制,也是咱们在工做中用得最频繁的,咱们可使用synchronized修饰一个方法,也能够用来修饰一个代码块。 那么,你真的了解synchronized吗?是骡子是马,咱拿出来溜溜。markdown

二、关于synchronized的使用,我相信只要正常作过Android或Java开发的工做,对此必定不会陌生。 那么请问,在static方法和非static方法前面加synchronized到底有什么不一样呢?这种问题,光文字解释一点说服力都没有,直接撸个代码验证一下。多线程

• static锁并发

public class SynchronizedTest {
   
       private static int number = 0;
   
       public static void main(String[] args) {
           //建立5个线程,制造多线程场景
           for (int i = 0; i < 5; i++) {
               new Thread("Thread-" + i) {
                   @Override
                   public void run() {
                       try {
                           //sleep一个随机时间
                           Thread.sleep(new Random().nextInt(5) * 1000);
                       } catch (InterruptedException e) {
                           e.printStackTrace();
                       }
                       //调用静态方法
                       SynchronizedTest.testStaticSynchronized();
                   }
               }.start();
           }
       }
   
       /**
        * 静态方法加锁
        */
       public synchronized static void testStaticSynchronized() {
           //对number加1操做
           number++;
           //打印(线程名 + number)
           System.out.println(Thread.currentThread().getName() + " -> 当前number为" + number);
       }
   }
   
   
   // logcat日志
   // Thread-4 -> 当前number为1
   // Thread-3 -> 当前number为2
   // Thread-1 -> 当前number为3
   // Thread-0 -> 当前number为4
   // Thread-2 -> 当前number为5
复制代码

代码逻辑很简单,建立5个线程去经过静态方法操做number变量,由于是静态方法,因此直接使用类名调用便可。根据logcat日志的输出,作到了同步,没有并发异常。dom

这里整理了最近BAT最新面试题,2021船新版本!!须要的朋友能够点击:这个,点这个!!,备注:jj。但愿那些有须要朋友能在今年第一波招聘潮找到一个本身满意顺心的工做! 在这里插入图片描述ide

• 非static锁oop

public class SynchronizedTest {
   
       private static int number = 0;
   
       public static void main(String[] args) {
           //建立5个线程,制造多线程场景
           for (int i = 0; i < 5; i++) {
               new Thread("Thread-" + i) {
                   @Override
                   public void run() {
                       try {
                           //sleep一个随机时间
                           Thread.sleep(new Random().nextInt(5) * 1000);
                       } catch (InterruptedException e) {
                           e.printStackTrace();
                       }
                       SynchronizedTest test = new SynchronizedTest();
                       //经过对象,调用非静态方法
                       test.testNonStaticSynchronized();
                   }
               }.start();
           }
       }
   
       /**
        * 非静态方法加锁
        */
       public synchronized void testNonStaticSynchronized() {
           number++;
           System.out.println(Thread.currentThread().getName() + " -> " + number);
       }
   }
   
   // logcat日志
   // Thread-0 -> 当前number为1
   // Thread-4 -> 当前number为1
   // Thread-2 -> 当前number为3
   // Thread-1 -> 当前number为4
   // Thread-3 -> 当前number为4
复制代码

这里的代码与上面的代码逻辑如出一辙,惟一改变的就是由于方法没有使用static修饰,因此使用建立对象并调用方法来操做number变量。看logcat日志,出现了数据异常,很明显不加static修饰,是没办法保证线程同步的。性能

• 另外咱们再作一个测试,卖个关子,先来看代码。学习

public class SynchronizedTest {
   
       public static void main(String[] args) {
           //建立5个线程调用非静态方法
           for (int i = 0; i < 5; i++) {
               new Thread("Thread-" + i) {
                   @Override
                   public void run() {
                       try {
                           Thread.sleep(new Random().nextInt(5) * 1000);
                       } catch (InterruptedException e) {
                           e.printStackTrace();
                       }
   
                       //经过对象调用非静态方法
                       SynchronizedTest test = new SynchronizedTest();
                       test.testNonStaticSynchronized();
                   }
               }.start();
           }
   
           //建立5个线程调用静态方法
           for (int i = 0; i < 5; i++) {
               new Thread("Thread-" + i) {
                   @Override
                   public void run() {
                       try {
                           Thread.sleep(new Random().nextInt(5) * 1000);
                       } catch (InterruptedException e) {
                           e.printStackTrace();
                       }
   
                       //经过类名调用静态方法
                       SynchronizedTest.testStaticSynchronized();
                   }
               }.start();
           }
       }
   
       /**
        * 静态方法加锁
        */
       public synchronized static void testStaticSynchronized() {
           System.out.println("testStaticSynchronized -> running -> " + System.currentTimeMillis());
       }
   
       /**
        * 非静态方法加锁
        */
       public synchronized void testNonStaticSynchronized() {
           System.out.println("testNonStaticSynchronized -> running -> " + System.currentTimeMillis());
       }
   }
   
   // logcat日志
   // testNonStaticSynchronized -> running -> 1603433921735
   // testNonStaticSynchronized -> running -> 1603433921735
   // testStaticSynchronized -> running -> 1603433921735
   // testNonStaticSynchronized -> running -> 1603433922740  ----- 注意这里
   // testStaticSynchronized -> running -> 1603433922740     ----- 注意这里
   // testStaticSynchronized -> running -> 1603433922740
   // testNonStaticSynchronized -> running -> 1603433923735
   // testNonStaticSynchronized -> running -> 1603433924740
   // testStaticSynchronized -> running -> 1603433925736
   // testStaticSynchronized -> running -> 1603433925736
复制代码

代码逻辑是这样的,各建立5个线程分别执行静态方法与非静态方法,看输出日志,特别关注一下最后的时间戳。从日志第咱们能够发现,从日志第4行和第5行发现,testNonStaticSynchronized与testStaticSynchronized方法能够同时执行,那就说明static锁与非statics锁互不干预。

通过上面3个demo的分析,基本能够得出结论了,这里总结一下。

• 类锁: 当synchronized修饰一个static方法时,获取到的是类锁,做用于这个类的全部对象。

• 对象锁: 当synchronized修饰一个非static方法时,获取到的是对象锁,做用于调用该方法的当前对象。

• 类锁和对象锁不一样,他们之间不会产生互斥。

三、固然,关于如何使用的问题,上面就一笔带过了,面试官通常也不会问不少,毕竟体现不出面试官的逼格。 正所谓,知其然也要知其因此然,咱们须要要探讨的重点是synchronized关键字底层是怎么帮咱们实现同步的?没错,这也是面试过程当中问得最多的。synchronized的原理这块,咱们也分两种状况去思考。

• 第一,synchronized修饰代码块。

public class SynchronizedTest {
    public static void main(String[] args) {
        //经过synchronized修饰代码块
        synchronized (SynchronizedTest.class) {
            System.out.println("this is in synchronized");
        }
    }
}
复制代码

上面是一段演示代码,没有实际功能,为了可以看得简单明了,就是经过synchronized对一条输出语句进行加锁。由于synchronized仅仅是Java提供的关键字,那么要想知道底层原理,咱们须要经过javap命令反编译class文件,看看他的字节码到底长啥样。

synchronized相关面试题,你接得住吗?

看反编译的结果,着重看红色标注的地方。咱们能够清楚地发现,代码块同步是使用monitorenter和monitorexit两个指令完成的,monitorenter指令插入到同步代码块的开始位置,monitorexit插入到方法结束处和异常处,被同步的代码块由monitorenter指令进入,而后在monitorexit指令处结束。

这里的重要角色monitor究竟是什么呢?简单来讲,能够直接理解为锁对象,只不过是虚拟机实现的,底层是依赖于操做系统的Mutex Lock实现。任何Java对象都有一个monitor与之关联,或者说全部的Java对象天生就能够成为monitor,这也就能够解释咱们平时在使用synchronized关键字时能够将任意对象做为锁的缘由了。

monitorenter

在执行monitorenter时,当前线程会尝试获取锁,若是这个monitor没有被锁定,或者当前线程已经拥有了这个对象的锁,那么就把锁的计数器加1,获取锁成功,继续执行下面的代码。若是获取锁失败了,那当前线程就要阻塞等待,直到对象锁被另外一个线程释放为止。

monitorexit

与此对应的,当执行monitorexit指令时,锁的计数器也会减1,当计数器等于0时,当前线程就释放锁,再也不是这个monitor的全部者。这个时候,其余被这个monitor阻塞的线程即可以尝试去获取这个monitor的全部权了。

到这里,synchronized修饰代码块实现同步的原理,我相信你已经搞懂了吧,那趁热打铁,继续看看修饰方法又是怎么处理的。

• 第二,synchronized修饰方法。

public class SynchronizedTest {


    public static void main(String[] args) {
        doSynchronizedTest();
    }
    //经过synchronized修饰方法
    public static synchronized void doSynchronizedTest(){
        System.out.println("this is in synchronized");
    }
}
复制代码

按照上面的老规矩,直接javap进行反编译,看字节码的变化。

synchronized相关面试题,你接得住吗?

从反编译的结果来看,此次方法的同步并无直接经过指令monitorenter和monitorexit来实现,可是相对于其余普通的方法,它的方法描述多了一个ACC_SYNCHRONIZED标识符。想必你都能猜出来,虚拟机无非就是根据这个标识符来实现方法同步,其实现原理大体是这样的:虚拟机调用某个方法时,调用指令首先会检查该方法的ACC_SYNCHRONIZED访问标志是否被设置,若是设置了,执行线程将先获取monitor,获取成功以后执行方法体,方法执行完后再释放monitor。在方法执行期间,其余任何线程都没法再得到同一个monitor对象,这样也保证了同步。固然这里monitor其实会有类锁和对象锁两种状况,上面就有说到。

关于synchronized的原理,这边再简单总结一下。synchronized关键字的实现同步分两种场景,代码块同步是使用monitorenter和monitorexit指令的形式,而方法同步使用的ACC_SYNCHRONIZED标识符的形式。但万变不离其宗,这两种形式的根本都是基于JVM进入和退出monitor对象锁来实现操做同步。

四、扛到了这里,是该小小的开心一下啦,不过并无彻底结束呢! 从上面的原理分析知道,synchronized关键字是基于JVM进入和退出monitor对象锁来实现操做同步,这种抢占式获取monitor锁,性能上铁定堪忧呀。这时候烦人的面试官又上线了,请问JDK1.6之后对synchronized锁作了哪些优化?这个问题难度较大,咱们细细地说。

其实在JDK1.6以前,synchronized内部实现的锁都是重量级锁,也就是说没有抢到CPU使用权的线程都得堵塞,然而在程序真正运行过程当中,其实不少状况下并不须要这么重,每次都直接堵塞反而会致使更多的线程上下文切换,消耗更多的资源。因此在JDK1.6之后,对synchronized锁进行了优化,引入偏向锁,轻量级锁,重量级锁的概念。

• 锁信息

熟悉synchronized原理的同窗应该都知道,当一个线程访问synchronized包裹的同步代码块时,必须获取monitor锁才能进入代码块,退出或抛出异常时再去释放monitor锁。这里就有问题了,线程须要获取的synchronized锁信息是存在哪里的呢?因此在介绍各类锁的概念以前,咱们必须先尝试解答这个疑惑。

在学习JVM时,咱们了解过一个对象是由三部分组成的,分别是对象头、实例数据以及对齐填充。其中对象头里又存储了对象自己运行时数据,包括哈希码、GC分代年龄,固然还有咱们这里要讲的与锁相关的标识,好比锁状态标志、线程持有的锁、偏向线程ID、偏向时间戳等等。

synchronized相关面试题,你接得住吗?

对象头默认存储的是无锁状态,随着程序的运行,对象头里存储的数据会随着锁标志位的变化而变化,大体结构以下图所示。

synchronized相关面试题,你接得住吗?

如今即可以解开上面的疑惑了,原来线程是经过获取对象头里的相关锁标识来获取锁信息的。有了这个基础,咱们如今能够上正菜了,看synchronized锁是怎么一步一步升级优化的。

• 偏向锁

锁是用于并发场景的,然而,在大多数状况下,锁其实并不存在多线程竞争,甚至都是由同一个线程屡次获取,因此没有必要花太多代价去放在锁的获取上,这时偏向锁就应运而生了。

偏向锁,顾名思义,它会偏向于第一个访问锁的线程。当一个线程第一次访问同步代码块并尝试获取锁时,会直接给线程加一个偏向锁,并在对象头的锁记录里存储锁偏向的线程ID,这样的话,之后该线程再次进入和退出同步代码块时就不须要进行CAS等操做来加锁和解锁,只需查看对象头里是否存储着指向当前线程的偏向锁便可。很明显,偏向锁的作法无疑是消除了同一线程竞争锁的开销,大大提升了程序的运行性能。

synchronized相关面试题,你接得住吗?

固然,若是在运行过程当中,忽然有其余线程抢占该锁,若是经过CAS操做获取锁成功,直接替换对象头中的线程ID为新的线程ID,继续会保持偏向锁状态;反之若是没有抢成功时,那么持有偏向锁的线程会被挂起,形成STW现象,JVM会自动消除它身上的偏向锁,偏向锁升级为轻量级锁。

synchronized相关面试题,你接得住吗?

• 轻量级锁

轻量级锁位于偏向锁与重量级锁之间,其主要目的是在没有多线程竞争的前提下,减小传统的重量级锁使用操做系统互斥量产生的性能消耗。自旋锁就是轻量级锁的一种典型实现。

自旋锁原理很是简单,若是持有锁的线程能在很短期内释放锁资源,那么那些等待竞争锁的线程就不须要作进入阻塞挂起状态,它们只须要稍微等一等,其实就是进行自旋操做,等持有锁的线程释放锁后便可当即获取锁,这样就进一步避免切换线程引发的消耗。

synchronized相关面试题,你接得住吗?

固然,自旋锁也不是最终解决方案,好比遇到锁的竞争很是激烈,或者持有锁的线程须要长时间占用锁执行同步块,这时候就不适合使用自旋锁了,由于自旋锁在获取锁前一直都是占用cpu进行自旋检查,这对于业务来说就是无用功。若是线程自旋带来的消耗大于线程阻塞挂起操做的消耗,那么自旋锁就弊大于利了,因此这个自旋的次数是个很重要的阈值,JDK1.5默认为10次,在1.6引入了适应性自旋锁,适应性自旋锁意味着自旋的时间再也不是固定的了,而是由前一次在同一个锁上的自旋时间以及锁的拥有者的状态来决定,基本认为一个线程上下文切换的时间是最佳的一个时间。

• 重量级锁

自旋屡次仍是失败后,通常就直接升级成重量级锁了,也就是锁的最高级别了,在上一篇synchronized的原理里有讲到其底层基于monitor对象实现,而monitor的本质又是依赖于操做系统的Mutex Lock实现。这里其实又涉及到咱们以前有篇文章讲过的一个知识,频繁切换线程的危害?由于操做系统实现线程之间的切换须要进行用户态到内核态的切换,不用想就知道,切换成本固然就很高了。

当JVM检查到重量级锁以后,会把想要得到锁的线程进行阻塞,插入到一个阻塞队列,被阻塞的线程不会消耗CPU,可是阻塞或者唤醒一个线程,都须要上面所说的从用户态转换到内核态,这个成本比较高,有可能比真正须要执行的同步代码的消耗还要大。

咱们依次理了一遍,由无锁到偏向锁,再到轻量级锁,最后升级为重量级锁,具体升级流程参考下面这张总体图。这一切的出发点都是为了优化性能,其实也给咱们一线开发者一个启示,并非功能实现了,编码也就结束了,后面还有很长的优化之路等待着你我,加油!

synchronized相关面试题,你接得住吗?

最后

以为不错小伙伴记得三连支持哦,后续会持续更新精选技术文章!

相关文章
相关标签/搜索