面試官:java基礎怎麼樣?多線程一定會引發多線程安全問題嗎?說說你的理解

java基礎對於學習安卓是很重要的,比如說線程,多線程。我們做安卓開發可能不太需要去研究高併發這些高深的問題,但是基礎的知識要掌握,特別是要理解爲什麼會這樣?以及它的使用場景。本篇文章主要是結合常規面試題去講解基礎。現在來看看一些非常基礎的面試題。

  1. 實現線程有幾種方式?
  2. 如何啓動線程?執行run()和start()的區別。
  3. 什麼情況下才會發生線程安全問題?
  4. 怎麼樣解決線程安全問題?

以上問題是在網上搜的,也許還可以問得更細,比如多線程開啓時,它們是同一時間運行的嗎?再比如,是不是多線程就一定會發生線程安全問題?只要理解了多線程,無論面試官怎麼樣問,都能回答上。

多線程使用場景

應用場景有很多,比如打遊戲和售票。打遊戲時,如果對方打你,要等他打完你,你才能出招,這種事情你能忍?分分鐘會爆粗口。這個時候就得用到多線程,同時對打才刺激。還有我們平時春節多個窗口售票,開售時候上千人搶幾百張票,這也要用多線程才能實現。

實現線程的方式

實現線程的方式通常有2種

第一種方式是繼承Thread

public class Thread1 extends Thread {
    @Override
    public void run() {
        super.run();
        for (int i = 0 ; i< 1000 ; i++){
            Log.i("thread","i======" +i);
        }
    }
}

第二種方式是實現Runnable接口

public class Thread2 implements Runnable{
    @Override
    public void run() {
        for (int j = 0 ; j< 1000 ; j++){
            Log.i("thread","j--------------------------" +j);
        }
    }
}  

啓動線程

現在調用start()開啓上面兩個線程

public class TestActivity extends Activity {
    @Override
    protected void onCreate(@Nullable Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        Thread1 thread1 = new Thread1();
        thread1.start();
        Thread thread2 = new Thread(new Thread2());
        thread2.start();
    }
}

通過查看Api文檔,知道start()方法是啓動線程。運行以後,看看打印的內容。

考慮到圖太長,我只截取一部分,真實情況是一開始打印的全是i,直到i = 130的時候纔開始打印j,j打印一會又開始打印i。同時開啓,按道理應該i和j輪流打印?結果證明兩個線程實際上並不是同一時間同時執行的。這就涉及到CPU對於時間的調度了,Thread1和Tread2就是兩個任務,以單核cpu爲例,我把這個過程簡單歸結爲下圖。

cpu可能先分配1ms給Task1執行,再到分配2ms給Task2執行,然後再分配10ms給Task1執行,以此類推。所以cpu並不是同時處理兩個線程,而是同一時間段交替運行,但是由於處理的時候非常的快,以ms計算甚至更快,所以感覺兩個任務是同時執行的。(cpu分配時間我們預估不了,這只是我隨意取的時間)

start()改爲調用run()

public class TestActivity extends Activity {
    @Override
    protected void onCreate(@Nullable Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        Thread1 thread1 = new Thread1();
        thread1.run();
        Thread thread2 = new Thread(new Thread2());
        thread2.run();
    }
}

運行後打印結果如下

我只截了部分,實際情況是打印完i以後,纔開始打印j,這就說明,是執行完thread1.run();以後,纔開始執行thread2.run();,這只是單純的按順序執行相應run方法裏面的內容。
調用run方法並不是開啓線程,是執行run裏面的內容,而start()是開啓線程。

多線程會有可能發生什麼問題

以多窗口售票爲例子,假設有3個窗口售200張票,每個窗口排隊的人都有1000人。

先寫一個簡易的售票系統。

//火車售票系統
public class TicketSystem implements Runnable {
    public static int ticketNum = 200;
    @Override
    public void run() {
        for (int i = 1; i <= 1000; i++){//步驟1 1ms

            if (ticketNum > 0){//步驟2 2ms
                try {
                    Thread.sleep(50);//需要輸入相關信息之類,需要時間,而且只是假設,沒有這麼快可以買到票的。 步驟3 50ms
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                ticketNum --;
                System.out.println("恭喜您,成功搶到票,還剩下:"+ticketNum+"張票");
            }
        }
    }
}

上面的代碼很好理解,不多作解釋,上面看不懂的註釋可以先忽略,下面會介紹。開啓3個窗口去搶票

public class TestActivity extends Activity {
    @Override
    protected void onCreate(@Nullable Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        TicketSystem ts = new TicketSystem();
        //創建3個窗口
        Thread thread1 = new Thread(ts);
        Thread thread2 = new Thread(ts);
        Thread thread3 = new Thread(ts);
        thread1.start();
        thread2.start();
        thread3.start();

    }}

運行後看下打印結果,截取了部分打印結果。

出現了兩次都剩一張票,還有剩下負數的票的情況,這就是多線程有可能導致的併發問題。三個窗口就是三個線程,三個線程同時開啓。上面部分有提到,cpu是通過時間調度去交替執行這些任務。假設步驟1的for循環需要執行1ms,步驟2中的if條件判斷語句需要執行2ms,步驟3的購買操作需要50ms。票還剩下最後一張的時候,線程thread1分配到的時間是2ms,剛執行完if語句,這個時候ticketNum還是爲1,然後切換到thread2,分配的時間是54ms,剛好執行完買票操作,這個時候ticketNum已經爲0,但是當thread1再執行的時候,它之前已經進入了if語句,會把剩下的代碼執行完,ticketNum就爲-1了,其它的情況也是同理。cpu分配的時間是我們不能掌控的,而三個線程同時操作的是同一數據ticketNum,這樣引發了不正常的結果。

在文章最開始打印i和j的時候,也是開啓了多線程,沒有出現問題。在多窗口售票開啓多線程,出現了問題,這兩個例子的區別在哪裏?區別在於多窗口售票,幾個線程訪問的是同一個共享數據,就是200張票,而i和j的例子,兩個線程訪問的數據是互不相關的。從這裏就知道,並不能說多線程就一定會發生線程安全問題,當多個線程操作同一共享數據的時候,纔會引發線程安全問題。

解決線程安全問題

上述的多線程共享了同一數據,出現了線程安全問題。我們不妨把這個問題想成火車上的乘客上廁所的問題,這是一個有點味道的例子,哈哈。整條車廂有20個人同時想使用廁所,而廁所只有一個可以使用,大家是不是得要共享這個廁所?不可能讓20個人同時一起上廁所,所以在設計廁所的時候會加鎖,只要有一個人進去,把門鎖住,不管外面的人有多着急,也必須等裏面的人開鎖出來,下一個人才能進去。程序也是來源於生活 ,解決線程安全問題,我們可以在公共的核心部分加一把鎖。代碼如下:

public class TicketSystem implements Runnable {
    public static int ticketNum = 200;
    @Override
    public void run() {
        for (int i = 1; i <= 1000; i++){//步驟1 1ms
            synchronized (TicketSystem.class){
            if (ticketNum > 0){//步驟2 2ms
                try {
                    Thread.sleep(50);//需要輸入相關信息之類,需要時間,而且只是假設,沒有這麼快可以買到票的。 步驟3 50ms
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                ticketNum --;
                System.out.println("恭喜您,成功搶到票,還剩下:"+ticketNum+"張票");
            }}
        }
    }
}

再運行就沒有問題了。是不是感覺很簡單?因爲java語言提供了這個解決辦法,不用我們自己實現。簡單的問題要力求做到最好,上了鎖就會影響運行效率,所以我們只給核心部分上鎖,核心部分越細越好,節省時間。

文章寫到這裏,開篇問的幾個問題也有了答案,現在來簡短的答一下。

1. 實現線程的幾種方式?

通常有兩種方式,繼承Thread,實現Runnable接口

2. 如何啓動線程?執行run()start()的區別。

調用start()。執行run()是執行方法裏面的內容,start()纔是開啓線程。

3. 什麼情況下才會發生線程安全問題?

當多個線程操作同一共享數據的時候。

4. 怎麼樣解決線程安全問題?

加鎖,給公共核心部分加鎖。

以上只是給出很簡短的答案,真正面試的時候還是要加上自己的理解。任何面試都一樣,只有理解了知識,才能正確的去回答問題,死記硬背答案是不可行的。

關於多線程就寫到這裏了。最近疫情還在持續,大家一起加油,堅持到可以脫口罩敲碼那天。

最後

最後我想說:對於程序員來說,要學習的知識內容、技術有太多太多,要想不被環境淘汰就只有不斷提升自己,從來都是我們去適應環境,而不是環境來適應我們!

 

 

當程序員容易,當一個優秀的程序員是需要不斷學習的,從初級程序員到高級程序員,從初級架構師到資深架構師,或者走向管理,從技術經理到技術總監,每個階段都需要掌握不同的能力。早早確定自己的職業方向,才能在工作和能力提升中甩開同齡人。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章