對線程的理解

說到線程,我們一定首先想到的是線程的創建,線程的創建一般有兩種方式 一種是實現 Runnable 接口,另一種就是 繼承 Thread 類 ,因爲Java的單繼承多實現機制,所以,優先選擇 實現 Runnable 接口。

package com.study.test;

/**
 * Create with IntelliJ IDEA.
 *
 * @author: [email protected]
 * Date: 2019/10/17
 * Time: 15:16
 */
public class Test {
    public static void main(String[] args) {
        //直接繼承了Thread ,創建一個Thread的實例
        ThreadTest t1 = new ThreadTest();
        t1.start();
        //如果是實現了接口的線程類,需要用對象的實例作爲Thread類構造方法的參數
        Thread t2 = new Thread(new RunableTest());
        t2.start();
    }
}

class ThreadTest extends Thread{
    @Override
    public void run() {
        System.out.println("繼承了Thread類");
    }
}
class RunableTest implements Runnable{

    @Override
    public void run() {
        System.out.println("實現了Runnable類");
    }
}

使用繼承的方式時,通過new xxxThread()的方式調用Start()方法,但使用接口的方式時 一直也是new xxxThread()d的方式,發現調不了start()方法,就調用了run()方法。.....其實這樣是不對的,對於Java來說,通過new的方式調用內部run()方法一點問題都沒有,但並不會開啓新線程,那樣做只會使用main線程。。正確的方式爲Thread t2 = new Thread(new RunnableTest()); 然後調用start()方法。總之,一定要調用start方法。

1、開啓線程後,那就涉及安全問題

安全即是數據安全,線程是什麼,是否安全,與我無關。但數據必須安全。這裏就要提到內存了,因爲,造成數據不安全的就是內存。

對於碼農來說:程序就是一個進程,一個線程是其中之一。當系統爲進程分配空間時,就會有公共空間(堆,公共方法區),和棧等。而造成不安全的就是這塊公共的內存空間。當一個線程在進行數據處理時而另一個線程也對此數據進行處理,數據不安全,程序紊亂,也就是說線程不安全。

2、如何、如何解決線程安全?

解決線程安全問題,就要找到線程哪裏不安全的根本原因。其次安全與不安全是相對的。如果你的系統只有一個線程,或者線程不可能同時運行,那麼就不存在安全問題。比如:單線程,mian方法。

2.1造成線程不安全原因?

我所知道的就是—線程搶奪

根本原因:線程內的關於外部變量的語境,與真實外部語境不一致。

2.1解決方法一:悲觀鎖

加鎖,只要加鎖,那就要產生線程的阻塞,執行的性能就會降低。悲觀鎖就是悲觀的認爲只要我不加鎖,那我的數據就會被其他線程修改,所以每次操作都要加鎖,直到操作完成。

悲觀鎖:

悲觀鎖就是假設每次操作都會有其他人使用同一個資源, 所以每次執行過程都是; 加鎖-->使用資源-->釋放鎖, 項目中常使用synchronized對需要的代碼部分加鎖。

使用synchronized主要是因爲synchronized使用的是內置鎖, 加鎖和解鎖都由jdk實現, 使用者無需手動控制, 比較方便。

使用場景多是多線程開發時, 並行處理數據, 對方法或者代碼塊使用。

在使用時, 可儘量減少synchronized修飾的方法或代碼塊中的代碼, 減少資源消耗。

一下是對悲觀鎖的使用情況:
 

package com.study.test;

/**
 * Create with IntelliJ IDEA.
 * 模擬火車票多窗口售票流程,總共100張票
 * @author: [email protected]
 * Date: 2019/9/27
 * Time: 10:23
 */
public class TestThread extends Thread{

    //將變量聲明爲static屬性,則所有new出來的該類的對象共用該屬性,
    //這裏聲明爲三個線程,那他們共用ticket屬性,並且該線程的ticket的值是從上一個線程獲取的,不會每次都對其進行初始化
    static int ticket = 100 ;
     Object obj = new Object();
    @Override
    public void run(){
        while (true){
            synchronized (obj){
                if (ticket>0){
                    try {
                        Thread.currentThread().sleep(10);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    System.out.println(Thread.currentThread().getName() + ":" + ticket--);
                }else {
                    break;
                }
            }

        }

    }
}
class TestRunable implements Runnable{
    //將變量聲明爲static屬性,則所有new出來的該類的對象共用該屬性,
    //這裏聲明爲三個線程,那他們共用ticket屬性,並且該線程的ticket的值是從上一個線程獲取的,不會每次都對其進行初始化
    static int ticket = 100;
    static Object obj = new Object();

    @Override
    public void run() {
        while(true){
            operation();
        }
    }


    //對operation這個方法進行同步,這個方法同一時刻只允許一個線程執行
    public synchronized void operation(){
        if(ticket > 0){
            try {
                Thread.currentThread().sleep(10);
            } catch (InterruptedException e) {
                // TODO Auto-generated catch block
                e.printStackTrace();
            }
            System.out.println(Thread.currentThread().getName() + ":" + ticket--);
        }
    }
}



悲觀鎖:可以理解爲把需要的數據全部加鎖,在事務提交之前,這些數據全部不可讀取和修改。https://blog.csdn.net/hzz_321

select * from user u where u.id=1 for update;
update user u set u.name='bac'  where u.id=1;

此時,id爲1的數據被加鎖,在事務提交前都不能進行讀取和修改。

解決方法一:樂觀鎖

樂觀鎖與悲觀鎖相對,樂觀鎖認爲,大概率沒有線程會修改我的數據,如果修改了那就只能重新執行操作。如果在高併發情況下使用樂觀鎖,可能會更加浪費系統資源。那具體怎麼操作呢?

CAS操作的就是樂觀鎖,每次不加鎖而是假設沒有衝突而去完成某項操作,如果因爲衝突失敗就重試,直到成功爲止。

JPA通過在實體類(POJO)中使用@Version註解來發現數據庫記錄的併發操作。當JPA運行時檢測到一個併發操作也在試圖更改同一條記錄。它會拋出一個嘗試提交的事務異常

這是關於樂觀鎖的連接:https://blog.csdn.net/u012326462/article/details/81349157

下面是Java線程相關的熱門面試題,你可以用它來好好準備面試。

博主鏈接:https://www.cnblogs.com/linjiqin/p/11315694.html

1) 什麼是線程?
線程是操作系統能夠進行運算調度的最小單位,它被包含在進程之中,是進程中的實際運作單位。程序員可以通過它進行多處理器編程,你可以使用多線程對運算密集型任務提速。比如,如果一個線程完成一個任務要100毫秒,那麼用十個線程完成一個任務只需10毫秒。Java在語言層面對多線程提供了卓越的支持,它也是一個很好的賣點。

2) 線程和進程有什麼區別?
線程是進程的子集,一個進程可以有很多線程,每條線程並行執行不同的任務。不同的進程使用不同的內存空間,而所有的線程共享一片相同的內存空間。別把它和棧內存搞混,每個線程都擁有單獨的棧內存用來存儲本地數據。

3) 如何在Java中實現線程?
3.1 繼承Thread類創建線程
3.2 實現Runnable接口創建線程
3.3 實現Callable接口通過FutureTask包裝器來創建Thread線程
3.4 使用ExecutorService、Callable、Future實現有返回結果的線程

4) 用Runnable還是Thread?
這個問題是上題的後續,大家都知道我們可以通過繼承Thread類或者調用Runnable接口來實現線程,問題是,那個方法更好呢?什麼情況下使用它?這個問題很容易回答,如果你知道Java不支持類的多重繼承,但允許你調用多個接口。所以如果你要繼承其他類,當然是調用Runnable接口好了。

Runnable更容易實現資源共享,能多個線程同時處理一個資源。

5) Thread 類中的start() 和 run() 方法有什麼區別?
這個問題經常被問到,但還是能從此區分出面試者對Java線程模型的理解程度。start()方法被用來啓動新創建的線程,而且start()內部調用了run()方法,這和直接調用run()方法的效果不一樣。當你調用run()方法的時候,只會是在原來的線程中調用執行,沒有新的線程啓動,start()方法纔會啓動新線程。

6) Java中Runnable和Callable有什麼不同?
Runnable和Callable代表那些要在不同的線程中執行的任務。Runnable從JDK1.0開始就有了,Callable是在JDK1.5增加的。它們的主要區別是Callable的 call() 方法可以返回值和拋出異常,而Runnable的run()方法沒有這些功能。Callable可以返回裝載有計算結果的Future對象。

7)如何強制啓動一個線程?
這個問題就像是如何強制進行Java垃圾回收,目前還沒有這種方法,雖然你可以使用System.gc()來進行垃圾回收,但是不保證能成功。在Java裏面沒有辦法強制啓動一個線程,它是被線程調度器控制着且Java沒有公佈相關的API。

8) Java中CyclicBarrier 和 CountDownLatch有什麼不同?
CyclicBarrier 和 CountDownLatch 都可以用來讓一組線程等待其它線程。與 CyclicBarrier 不同的是,CountdownLatch 不能重新使用。

9) Java內存模型是什麼?

10) Java中的volatile 變量是什麼?
volatile是一個特殊的修飾符,只有成員變量才能使用它。在Java併發程序缺少同步類的情況下,多線程對成員變量的操作對其它線程是透明的。volatile變量可以保證下一個讀取操作會在前一個寫操作之後發生,就是上一題的volatile變量規則。點擊這裏查看更多volatile的相關內容。

https://www.cnblogs.com/linjiqin/p/3212737.html

11) 什麼是線程安全?Vector是一個線程安全類嗎?
如果你的代碼所在的進程中有多個線程在同時運行,而這些線程可能會同時運行這段代碼。如果每次運行結果和單線程運行的結果是一樣的,而且其他的變量的值也和預期的是一樣的,就是線程安全的。一個線程安全的計數器類的同一個實例對象在被多個線程使用的情況下也不會出現計算失誤。很顯然你可以將集合類分成兩組,線程安全和非線程安全的。Vector 是用同步方法來實現線程安全的, 而和它相似的ArrayList不是線程安全的。

12) Java中什麼是競態條件? 舉個例子說明。
競態條件會導致程序在併發情況下出現一些bugs。多線程對一些資源的競爭的時候就會產生競態條件,如果首先要執行的程序競爭失敗排到後面執行了,那麼整個程序就會出現一些不確定的bugs。這種bugs很難發現而且會重複出現,因爲線程間的隨機競爭。

13) Java中如何停止一個線程?
Java提供了很豐富的API但沒有爲停止線程提供API。JDK 1.0本來有一些像stop(), suspend() 和 resume()的控制方法但是由於潛在的死鎖威脅因此在後續的JDK版本中他們被棄用了,之後Java API的設計者就沒有提供一個兼容且線程安全的方法來停止一個線程。當run() 或者 call() 方法執行完的時候線程會自動結束,如果要手動結束一個線程,你可以用volatile 布爾變量來退出run()方法的循環或者是取消任務來中斷線程。

14) 一個線程運行時發生異常會怎樣?
這是我在一次面試中遇到的一個很刁鑽的Java面試題, 簡單的說,如果異常沒有被捕獲該線程將會停止執行。Thread.UncaughtExceptionHandler是用於處理未捕獲異常造成線程突然中斷情況的一個內嵌接口。當一個未捕獲異常將造成線程中斷的時候JVM會使用Thread.getUncaughtExceptionHandler()來查詢線程的UncaughtExceptionHandler並將線程和異常作爲參數傳遞給handler的uncaughtException()方法進行處理。

15) 如何在兩個線程間共享數據?
實現Runnable接口創建線程,如:
public static void main(String[] args) {
Runnable runnable = new MyRunnable();
new Thread(runnable).start();
new Thread(runnable).start();
new Thread(runnable).start();
new Thread(runnable).start();
}

16) Java中notify 和 notifyAll有什麼區別?
這又是一個刁鑽的問題,因爲多線程可以等待單監控鎖,Java API 的設計人員提供了一些方法當等待條件改變的時候通知它們,但是這些方法沒有完全實現。notify()方法不能喚醒某個具體的線程,所以只有一個線程在等待的時候它纔有用武之地。而notifyAll()喚醒所有線程並允許他們爭奪鎖確保了至少有一個線程能繼續運行。

18) 什麼是ThreadLocal變量?
線程範圍內的共享變量,每個線程只能訪問他自己的,不能訪問別的線程。

19) 什麼是FutureTask?
在Java併發程序中FutureTask表示一個可以取消的異步運算。它有啓動和取消運算、查詢運算是否完成和取回運算結果等方法。只有當運算完成的時候結果才能取回,如果運算尚未完成get方法將會阻塞。一個FutureTask對象可以對調用了Callable和Runnable的對象進行包裝,由於FutureTask也是調用了Runnable接口所以它可以提交給Executor來執行。

20) Java中interrupted 和 isInterruptedd方法的區別?
interrupted() 和 isInterrupted()的主要區別是前者會將中斷狀態清除而後者不會。Java多線程的中斷機制是用內部標識來實現的,調用Thread.interrupt()來中斷一個線程就會設置中斷標識爲true。當中斷線程調用靜態方法Thread.interrupted()來檢查中斷狀態時,中斷狀態會被清零。而非靜態方法isInterrupted()用來查詢其它線程的中斷狀態且不會改變中斷狀態標識。簡單的說就是任何拋出InterruptedException異常的方法都會將中斷狀態清零。無論如何,一個線程的中斷狀態有有可能被其它線程調用中斷來改變。

21) 爲什麼wait和notify方法要在同步塊中調用?
主要是因爲Java API強制要求這樣做,如果你不這麼做,你的代碼會拋出IllegalMonitorStateException異常。還有一個原因是爲了避免wait和notify之間產生競態條件。

23) Java中的同步集合與併發集合有什麼區別?

同步集合與併發集合都爲多線程和併發提供了合適的線程安全的集合,不過併發集合的可擴展性更高。在Java1.5之前程序員們只有同步集合來用且在多線程併發的時候會導致爭用,阻礙了系統的擴展性。Java5介紹了併發集合像HashMap,不僅提供線程安全還用鎖分離和內部分區等現代技術提高了可擴展性。

24) Java中堆和棧有什麼不同?
爲什麼把這個問題歸類在多線程和併發面試題裏?因爲棧是一塊和線程緊密相關的內存區域。每個線程都有自己的棧內存,用於存儲本地變量,方法參數和棧調用,一個線程中存儲的變量對其它線程是不可見的。而堆是所有線程共享的一片公用內存區域。對象都在堆裏創建,爲了提升效率線程會從堆中弄一個緩存到自己的棧,如果多個線程使用該變量就可能引發問題,這時volatile 變量就可以發揮作用了,它要求線程從主存中讀取變量的值。

(1)存放內容不同:
棧內存:用來存放基本數據類型變量和引用類型變量。
堆內存:用來存放運行時通過new關鍵字創建的對象。

(2)生命週期不同:
棧的生命週期與線程相同,隨線程而生,隨線程而亡,是線程私有的。
堆的生命週期與JVM相同,JVM啓動時創建,JVM停止時銷燬,是線程共享的。

25)什麼是線程池? 爲什麼要使用它?
創建線程要花費昂貴的資源和時間,如果任務來了才創建線程那麼響應時間會變長,而且一個進程能創建的線程數有限。爲了避免這些問題,在程序啓動的時候就創建若干線程來響應處理,它們被稱爲線程池,裏面的線程叫工作線程。

緩存線程池(可變尺寸的線程池)、固定大小的線程池、調度線程池、單例線程池、自定義線程池

26)如何寫代碼來解決生產者消費者問題?
在現實中你解決的許多線程問題都屬於生產者消費者模型,就是一個線程生產任務供其它線程進行消費,你必須知道怎麼進行線程間通信來解決這個問題。比較低級的辦法是用wait和notify來解決這個問題,比較讚的辦法是用Semaphore 或者 BlockingQueue來實現生產者消費者模型,這篇教程有實現它。

用wait和notify實現
用Semaphore信號量實現
BlockingQueue/ConcurrentLinkedQueue

29) 怎麼檢測一個線程是否擁有鎖?
我一直不知道我們竟然可以檢測一個線程是否擁有鎖,直到我參加了一次電話面試。在java.lang.Thread中有一個方法叫holdsLock(),它返回true如果當且僅當當前線程擁有某個具體對象的鎖。

30) 你如何在Java中獲取線程堆棧?
對於不同的操作系統,有多種方法來獲得Java進程的線程堆棧。當你獲取線程堆棧時,JVM會把所有線程的狀態存到日誌文件或者輸出到控制檯。在Windows你可以使用Ctrl + Break組合鍵來獲取線程堆棧,Linux下用kill -3命令。你也可以用jstack這個工具來獲取,它對線程id進行操作,你可以用jps這個工具找到id。

31) JVM中哪個參數是用來控制線程的棧堆棧小的
這個問題很簡單, -Xss參數用來控制線程的堆棧大小。你可以查看JVM配置列表來了解這個參數的更多信息。

32) Java中synchronized 和 ReentrantLock 有什麼不同?
Java在過去很長一段時間只能通過synchronized關鍵字來實現互斥,它有一些缺點。比如你不能擴展鎖之外的方法或者塊邊界,嘗試獲取鎖時不能中途取消等。
Java 5 通過Lock接口提供了更復雜的控制來解決這些問題。 ReentrantLock 類實現了 Lock,它擁有與 synchronized 相同的併發性和內存語義且它還具有可擴展性。

33) 有三個線程T1,T2,T3,怎麼確保它們按順序執行?
在多線程中有多種方法讓線程按特定順序執行,你可以用線程類的join()方法在一個線程中啓動另一個線程,另外一個線程完成該線程繼續執行。爲了確保三個線程的順序你應該先啓動最後一個(T3調用T2,T2調用T1),這樣T1就會先完成而T3最後完成。

35) Java中ConcurrentHashMap的併發度是什麼?
ConcurrentHashMap把實際map劃分成若干部分來實現它的可擴展性和線程安全。這種劃分是使用併發度獲得的,它是ConcurrentHashMap類構造函數的一個可選參數,默認值爲16,這樣在多線程情況下就能避免爭用。

36) Java中Semaphore是什麼?
Java中的Semaphore是一種新的同步類,它是一個計數信號。從概念上講,從概念上講,信號量維護了一個許可集合。如有必要,在許可可用前會阻塞每一個 acquire(),然後再獲取該許可。每個 release()添加一個許可,從而可能釋放一個正在阻塞的獲取者。但是,不使用實際的許可對象,Semaphore只對可用許可的號碼進行計數,並採取相應的行動。


37)如果你提交任務時,線程池隊列已滿。會時發會生什麼?

這個問題問得很狡猾,許多程序員會認爲該任務會阻塞直到線程池隊列有空位。事實上如果一個任務不能被調度執行那麼ThreadPoolExecutor’s submit()方法將會拋出一個RejectedExecutionException異常。

38) Java線程池中submit() 和 execute()方法有什麼區別?

兩個方法都可以向線程池提交任務,execute()方法的返回類型是void,它定義在Executor接口中, 而submit()方法可以返回持有計算結果的Future對象,它定義在ExecutorService接口中,它擴展了Executor接口,其它線程池類像ThreadPoolExecutor和ScheduledThreadPoolExecutor都有這些方法。

39) 什麼是阻塞式方法?

阻塞式方法是指程序會一直等待該方法完成期間不做其他事情,ServerSocket的accept()方法就是一直等待客戶端連接。這裏的阻塞是指調用結果返回之前,當前線程會被掛起,直到得到結果之後纔會返回。此外,還有異步和非阻塞式方法在任務完成前就返回。

44) Java中的ReadWriteLock是什麼?
一般而言,讀寫鎖是用來提升併發程序性能的鎖分離技術的成果。Java中的ReadWriteLock是Java 5 中新增的一個接口,一個ReadWriteLock維護一對關聯的鎖,一個用於只讀操作一個用於寫。在沒有寫線程的情況下一個讀鎖可能會同時被多個讀線程持有。寫鎖是獨佔的,你可以使用JDK中的ReentrantReadWriteLock來實現這個規則,它最多支持65535個寫鎖和65535個讀鎖。

46)volatile 變量和 atomic 變量有什麼不同?
這是個有趣的問題。首先,volatile 變量和 atomic 變量看起來很像,但功能卻不一樣。Volatile變量可以確保先行關係,即寫操作會發生在後續的讀操作之前, 但它並不能保證原子性。例如用volatile修飾count變量那麼 count++ 操作就不是原子性的。而AtomicInteger類提供的atomic方法可以讓這種操作具有原子性如getAndIncrement()方法會原子性的進行增量操作把當前值加一,其它數據類型和引用變量也可以進行相似操作。

47) 如果同步塊內的線程拋出異常會發生什麼?

這個問題坑了很多Java程序員,若你能想到鎖是否釋放這條線索來回答還有點希望答對。無論你的同步塊是正常還是異常退出的,裏面的線程都會釋放鎖,所以對比鎖接口我更喜歡同步塊,因爲它不用我花費精力去釋放鎖,該功能可以在finally block裏釋放鎖實現。

 

 

 

 

 

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