从一道阿里面试题说起

  • 2019 年 10 月 3 日
  • 筆記

前言

        昨晚老东家微信群里一堆前同事充满兴致的在讨论一道据说是阿里P7的面试题,不管题目来源是不是真的,但题目本身却比较有意思,虚虚实实去繁化简,却能看出一个人对Java知识掌握的深度以及灵活度。

        闲话少叙,咱们直接“上菜”。

正文

1、原代码如下所示,问执行之后打印的数是什么?

 1     static Integer count = 0;   2     public static void main(String[] args) {   3         for (int i = 0; i < 1000; i++) {   4             new Thread(() -> {   5                 synchronized (count) {   6                     count++;   7                 }   8             }).start();   9         }  10  11         System.out.println(count);  12     }

相信只要对多线程的执行机制有了解的道友应该都会知道,上文中的同步块只是一个幌子,因为这一千个子线程不一定都会在main方法所在的主线程执行到第11行时都执行完,跟同步块没有半毛钱关系。所以第11行输出的结果是从0到1000不等的(理论上会出现的结果范围,实际很难出现)。

2、以上面的为基础,延伸一下呢,比如加个while循环后最终打印的又是什么?

 1     static Integer count = 0;   2     public static void main(String[] args) {   3         for (int i = 0; i < 1000; i++) {   4             new Thread(() -> {   5                 synchronized (count) {   6                     count++;   7                 }   8             }).start();   9         }  10  11         while (true) {  12             System.out.println(count);  13         }  14     }

首先我们需要知道count++这种操作是非原子操作;其次我们需要了解synchronized同步块的作用机制。

synchronized同步是对一个对象加锁,如果synchronized加在非静态方法上,锁的是当前对象实例;如果加在静态方法上,锁的是当前类的Class对象;如果是一个单独的块,锁的就是括号后面的对象。可知此处是同步块,锁的就是count这个Integer对象了。

如果我们的知识掌握到这里,得出的答案就是1000了,因为同步块能保证多个线程对同一个对象的操作是顺序执行的。但是实际执行的时候,你会发现很多时候最终打印的数据不是1000,是999或者998这种数,那这是为什么呢?

其中的关键就出在count这个对象身上。synchronized实现的是对同一个对象加锁,但看一下Integer源码你会发现,它是final类型的,就是说当你对它进行+1的操作之后,得到的这个新的count对象已经不是之前的count对象了。既然锁的对象都不一样,自然就不会触发synchronized的同步机制了。

至此可以看出,本题目不知考查了对同步块的理解,还附带了对jdk源码的考查。另,java中的装包类,都是final类型的。

后记

        到此本应结束,但我后来觉得用while无限循环这种方式获取主线程的最终执行结果有点蠢,于是我给改造了一下:

 1     static Integer count = 0;   2     public static void main(String[] args) throws InterruptedException {   3         for (int i = 0; i < 1000; i++) {   4             Thread thread = new Thread(() -> {   5                 synchronized (count) {   6                     count++;   7                 }   8             });   9             thread.start();  10             thread.join();  11         }  12  13         System.out.println(count);  14     }

用join来确保主线程最后执行(可参照博主之前的一篇文章 https://www.cnblogs.com/zzq6032010/p/10921870.html 了解join方法的作用),但是执行完之后,发现结果总是1000。待检查一番之后才恍然,

此处用join方法是不合适的。因为当主线程执行到thread.join()这一行之后,正常的话会继续执行for循环的下一次循环,但是由于被子线程join了,所以需先执行完这个子线程才能继续走下一次for循环,这样造成的效果就是这一千个线程都是顺序启动顺序执行,不存在并发现象,所以结果也就都是1000了。可以发现,利用join有时也能做到同步的效果。

既然join方法不行,那就用并发包中的CountDownLatch吧。

 1     static Integer count = 0;   2     public static void main(String[] args) throws InterruptedException {   3         CountDownLatch countDownLatch = new CountDownLatch(1000);   4         for (int i = 0; i < 1000; i++) {   5             new Thread(() -> {   6                 synchronized (count) {   7                     count++;   8                     countDownLatch.countDown();   9                 }  10             }).start();  11         }  12         countDownLatch.await();  13         System.out.println(count);  14     }

这样就比while无限循环优雅一些了 (><)

本次“注水”博文到此结束,谢谢观看!