1. 程式人生 > >從一道阿里面試題說起

從一道阿里面試題說起

前言

        昨晚老東家微信群裡一堆前同事充滿興致的在討論一道據說是阿里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無限迴圈優雅一些了 (><)

本次“注水”博文到此結束,謝謝觀看!