多線程知識筆記

減少上下文切換方法

通常減少上下文切換可以採用無鎖併發編程,CAS算法,使用最少的線程和使用協程。

無鎖併發編程:可以參照concurrentHashMap鎖分段的思想,不同的線程處理不同段的數據,這樣在多線程競爭的條件下,可以減少上下文切換的時間。

CAS算法,利用Atomic下使用CAS算法來更新數據,使用了樂觀鎖,可以有效的減少一部分不必要的鎖競爭帶來的上下文切換

使用最少線程:避免創建不需要的線程,比如任務很少,但是創建了很多的線程,這樣會造成大量的線程都處於等待狀態

協程:在單線程裏實現多任務的調度,並在單線程裏維持多個任務間的切換

死鎖

public class DeadLockDemo {
    private static String resource_a = "A";
    private static String resource_b = "B";

    public static void main(String[] args) {
        deadLock();
    }

    public static void deadLock() {
        Thread threadA = new Thread(new Runnable() {
            @Override
            public void run() {
                synchronized (resource_a) {
                    System.out.println("get resource a");
                    try {
                        Thread.sleep(3000);
                        synchronized (resource_b) {
                            System.out.println("get resource b");
                        }
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
            }
        });
        Thread threadB = new Thread(new Runnable() {
            @Override
            public void run() {
                synchronized (resource_b) {
                    System.out.println("get resource b");
                    synchronized (resource_a) {
                        System.out.println("get resource a");
                    }
                }
            }
        });
        threadA.start();
        threadB.start();

    }
}


在上面的這個demo中,開啓了兩個線程threadA, threadB,其中threadA佔用了resource_a, 並等待被threadB釋放的resource _b。threadB佔用了resource _b正在等待被threadA釋放的resource _a。因此threadA,threadB出現線程安全的問題,形成死鎖。同樣可以通過jps,jstack證明這種推論:

"Thread-1":
  waiting to lock monitor 0x000000000b695360 (object 0x00000007d5ff53a8, a java.lang.String),
  which is held by "Thread-0"
"Thread-0":
  waiting to lock monitor 0x000000000b697c10 (object 0x00000007d5ff53d8, a java.lang.String),
  which is held by "Thread-1"

Java stack information for the threads listed above:
===================================================
"Thread-1":
        at learn.DeadLockDemo$2.run(DeadLockDemo.java:34)
        - waiting to lock <0x00000007d5ff53a8(a java.lang.String)
        - locked <0x00000007d5ff53d8(a java.lang.String)
        at java.lang.Thread.run(Thread.java:722)
"Thread-0":
        at learn.DeadLockDemo$1.run(DeadLockDemo.java:20)
        - waiting to lock <0x00000007d5ff53d8(a java.lang.String)
        - locked <0x00000007d5ff53a8(a java.lang.String)
        at java.lang.Thread.run(Thread.java:722)

Found 1 deadlock.

那麼,通常可以用如下方式避免死鎖的情況:

  1. 避免一個線程同時獲得多個鎖;
  2. 避免一個線程在鎖內部佔有多個資源,儘量保證每個鎖只佔用一個資源;
  3. 嘗試使用定時鎖,使用lock.tryLock(timeOut),當超時等待時當前線程不會阻塞;
  4. 對於數據庫鎖,加鎖和解鎖必須在一個數據庫連接裏,否則會出現解鎖失敗的情況

基本概念

同步VS異步

同步和異步通常用來形容一次方法調用。同步方法調用一開始,調用者必須等待被調用的方法結束後,調用者後面的代碼才能執行。
異步調用,指的是,調用者不用管被調用方法是否完成,都會繼續執行後面的代碼,當被調用的方法完成後會通知調用者。

併發與並行

併發和並行是十分容易混淆的概念。併發指的是多個任務交替進行,而並行則是指真正意義上的“同時進行”。實際上,如果系統內只有一個CPU,而使用多線程時,那麼真實系統環境下不能並行,只能通過切換時間片的方式交替進行,而成爲併發執行任務。真正的並行也只能出現在擁有多個CPU的系統中。

阻塞和非阻塞

阻塞和非阻塞通常用來形容多線程間的相互影響,比如一個線程佔有了臨界區資源,那麼其他線程需要這個資源就必須進行等待該資源的釋放,會導致等待的線程掛起,這種情況就是阻塞,而非阻塞就恰好相反,它強調沒有一個線程可以阻塞其他線程,所有的線程都會嘗試地往前運行。

臨界區

臨界區用來表示一種公共資源或者說是共享數據,可以被多個線程使用。但是每個線程使用時,一旦臨界區資源被一個線程佔有,那麼其他線程必須等待。

線程創建三種方式

  1. 通過繼承Thread類,重寫run方法;
  2. 通過實現runable接口;
  3. 通過實現callable接口這三種方式,
public class CreateThreadDemo {
     public static void main(String[] args) {
         //1.繼承Thread
         Thread thread = new Thread() {
             @Override
             public void run() {
                 System.out.println("繼承Thread");
                 super.run();
             }
         };
         thread.start();
         //2.實現runable接口
         Thread thread1 = new Thread(new Runnable() {
             @Override
             public void run() {
                 System.out.println("實現runable接口");
             }
         });
         thread1.start();
         //3.實現callable接口
         ExecutorService service = Executors.newSingleThreadExecutor();
         Future<String> future = service.submit(new Callable() {
             @Override
             public String call() throws Exception {
                 return "通過實現Callable接口";
             }
         });
         try {
             String result = future.get();
             System.out.println(result);
         } catch (InterruptedException e) {
             e.printStackTrace();
         } catch (ExecutionException e) {
             e.printStackTrace();
         }
     }
 }

需要注意的是:
由於java不能多繼承可以實現多個接口,因此,在創建線程的時候儘量多考慮採用實現接口的形式;
實現callable接口,提交給ExecutorService返回的是異步執行的結果,另外,通常也可以利用FutureTask(Callable callable)將callable進行包裝然後FeatureTask提交給ExecutorsService。如圖:
在這裏插入圖片描述
另外由於FeatureTask也實現了Runable接口也可以利用上面第二種方式(實現Runable接口)來新建線程;

可以通過Executors將Runable轉換成Callable,具體方法是:Callable callable(Runnable task, T result), Callable callable(Runnable task)。

在這裏插入圖片描述
java線程線程轉換圖如上圖所示。線程創建之後調用start()方法開始運行,當調用wait(),join(),LockSupport.lock()方法線程會進入到WAITING狀態,而同樣的wait(long timeout),sleep(long),join(long),LockSupport.parkNanos(),LockSupport.parkUtil()增加了超時等待的功能,也就是調用這些方法後線程會進入TIMED_WAITING狀態,當超時等待時間到達後,線程會切換到Runable的狀態,另外當WAITING和TIMED _WAITING狀態時可以通過Object.notify(),Object.notifyAll()方法使線程轉換到Runable狀態。當線程出現資源競爭時,即等待獲取鎖的時候,線程會進入到BLOCKED阻塞狀態,當線程獲取鎖時,線程進入到Runable狀態。線程運行結束後,線程進入到TERMINATED狀態,狀態轉換可以說是線程的生命週期。
另外需要注意的是:

當線程進入到synchronized方法或者synchronized代碼塊時,線程切換到的是BLOCKED狀態,而使用java.util.concurrent.locks下lock進行加鎖的時候線程切換的是WAITING或者TIMED_WAITING狀態,因爲lock會調用LockSupport的方法。

用一個表格將上面六種狀態進行一個總結歸納。
在這裏插入圖片描述

線程狀態的基本操作

interrupted

中斷可以理解爲線程的一個標誌位,它表示了一個運行中的線程是否被其他線程進行了中斷操作。中斷好比其他線程對該線程打了一個招呼。其他線程可以調用該線程的interrupt()方法對其進行中斷操作,同時該線程可以調用
isInterrupted()來感知其他線程對其自身的中斷操作,從而做出響應。另外,同樣可以調用Thread的靜態方法
interrupted()對當前線程進行中斷操作,該方法會清除中斷標誌位。需要注意的是,當拋出InterruptedException時候,會清除中斷標誌位,也就是說在調用isInterrupted會返回false。

在這裏插入圖片描述

下面結合具體的實例來看一看

public class InterruptDemo {
    public static void main(String[] args) throws InterruptedException {
        //sleepThread睡眠1000ms
        final Thread sleepThread = new Thread() {
            @Override
            public void run() {
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                super.run();
            }
        };
        //busyThread一直執行死循環
        Thread busyThread = new Thread() {
            @Override
            public void run() {
                while (true) ;
            }
        };
        sleepThread.start();
        busyThread.start();
        sleepThread.interrupt();
        busyThread.interrupt();
        while (sleepThread.isInterrupted()) ;
        System.out.println("sleepThread isInterrupted: " + sleepThread.isInterrupted());
        System.out.println("busyThread isInterrupted: " + busyThread.isInterrupted());
    }
}

輸出結果

sleepThread isInterrupted: false busyThread isInterrupted: true

開啓了兩個線程分別爲sleepThread和BusyThread, sleepThread睡眠1s,BusyThread執行死循環。然後分別對着兩個線程進行中斷操作,可以看出sleepThread拋出InterruptedException後清除標誌位,而busyThread就不會清除標誌位。
另外,同樣可以通過中斷的方式實現線程間的簡單交互, while (sleepThread.isInterrupted()) 表示在Main中會持續監測sleepThread,一旦sleepThread的中斷標誌位清零,即sleepThread.isInterrupted()返回爲false時纔會繼續Main線程纔會繼續往下執行。
因此,中斷操作可以看做線程間一種簡便的交互方式。一般在結束線程時通過中斷標誌位或者標誌位的方式可以有機會去清理資源,相對於武斷而直接的結束線程,這種方式要優雅和安全。

join

join方法可以看做是線程間協作的一種方式,很多時候,一個線程的輸入可能非常依賴於另一個線程的輸出,這就像兩個好基友,一個基友先走在前面突然看見另一個基友落在後面了,這個時候他就會在原處等一等這個基友,等基友趕上來後,就兩人攜手並進。其實線程間的這種協作方式也符合現實生活。在軟件開發的過程中,從客戶那裏獲取需求後,需要經過需求分析師進行需求分解後,這個時候產品,開發纔會繼續跟進。如果一個線程實例A執行了threadB.join(),其含義是:當前線程A會等待threadB線程終止後threadA纔會繼續執行。關於join方法一共提供如下這些方法:

public final synchronized void join(long millis)
public final synchronized void join(long millis, int nanos)
public final void join() throws InterruptedException

Thread類除了提供join()方法外,另外還提供了超時等待的方法,如果線程threadB在等待的時間內還沒有結束的話,threadA會在超時之後繼續執行。join方法源碼關鍵是:

 while (isAlive()) {
    wait(0);
 }

可以看出來當前等待對象threadA會一直阻塞,直到被等待對象threadB結束後即isAlive()返回false的時候纔會結束while循環,當threadB退出時會調用notifyAll()方法通知所有的等待線程。下面用一個具體的例子來說說join方法的使用:

public class JoinDemo {
    public static void main(String[] args) {
        Thread previousThread = Thread.currentThread();
        for (int i = 1; i <= 10; i++) {
            Thread curThread = new JoinThread(previousThread);
            curThread.start();
            previousThread = curThread;
        }
    }

    static class JoinThread extends Thread {
        private Thread thread;

        public JoinThread(Thread thread) {
            this.thread = thread;
        }

        @Override
        public void run() {
            try {
                thread.join();
                System.out.println(thread.getName() + " terminated.");
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
}

輸出結果爲:

main terminated.
Thread-0 terminated.
Thread-1 terminated.
Thread-2 terminated.
Thread-3 terminated.
Thread-4 terminated.
Thread-5 terminated.
Thread-6 terminated.
Thread-7 terminated.
Thread-8 terminated.

在上面的例子中一個創建了10個線程,每個線程都會等待前一個線程結束纔會繼續運行。可以通俗的理解成接力,前一個線程將接力棒傳給下一個線程,然後又傳給下一個線程…

sleep
public static native void sleep(long millis)方法顯然是Thread的靜態方法,很顯然它是讓當前線程按照指定的時間休眠,其休眠時間的精度取決於處理器的計時器和調度器。需要注意的是如果當前線程獲得了鎖,sleep方法並不會失去鎖。sleep方法經常拿來與Object.wait()方法進行比價,這也是面試經常被問的地方。

sleep() VS wait()

兩者主要的區別:

sleep()方法是Thread的靜態方法,而wait是Object實例方法
wait()方法必須要在同步方法或者同步塊中調用,也就是必須已經獲得對象鎖。而sleep()方法沒有這個限制可以在任何地方種使用。另外,wait()方法會釋放佔有的對象鎖,使得該線程進入等待池中,等待下一次獲取資源。而sleep()方法只是會讓出CPU並不會釋放掉對象鎖;
sleep()方法在休眠時間達到後如果再次獲得CPU時間片就會繼續執行,而wait()方法必須等待Object.notift/Object.notifyAll通知後,纔會離開等待池,並且再次獲得CPU時間片纔會繼續執行。

yield

public static native void yield();
這是一個靜態方法,一旦執行,它會是當前線程讓出CPU,但是,需要注意的是,讓出的CPU並不是代表當前線程不再運行了,如果在下一次競爭中,又獲得了CPU時間片當前線程依然會繼續運行。
另外,讓出的時間片只會分配給當前線程相同優先級的線程。什麼是線程優先級了?下面就來具體聊一聊。
現代操作系統基本採用時分的形式調度運行的線程,操作系統會分出一個個時間片,線程會分配到若干時間片,當前時間片用完後就會發生線程調度,並等待這下次分配。線程分配到的時間多少也就決定了線程使用處理器資源的多少,而線程優先級就是決定線程需要或多或少分配一些處理器資源的線程屬性。
在Java程序中,通過一個整型成員變量Priority來控制優先級,優先級的範圍從1~10.在構建線程的時候可以通過 setPriority(int) 方法進行設置,默認優先級爲5,優先級高的線程相較於優先級低的線程優先獲得處理器時間片。需要注意的是在不同JVM以及操作系統上,線程規劃存在差異,有些操作系統甚至會忽略線程優先級的設定。
另外需要注意的是,sleep()和yield()方法,同樣都是當前線程會交出處理器資源,而它們不同的是,sleep()交出來的時間片其他線程都可以去競爭,也就是說都有機會獲得當前線程讓出的時間片。而yield()方法只允許與當前線程具有相同優先級的線程能夠獲得釋放出來的CPU時間片。

守護線程Daemon

守護線程是一種特殊的線程,就和它的名字一樣,它是系統的守護者,在後臺默默地守護一些系統服務,比如垃圾回收線程,JIT線程就可以理解守護線程。與之對應的就是用戶線程,用戶線程就可以認爲是系統的工作線程,它會完成整個系統的業務操作。用戶線程完全結束後就意味着整個系統的業務任務全部結束了,因此係統就沒有對象需要守護的了,守護線程自然而然就會退。當一個Java應用,只有守護線程的時候,虛擬機就會自然退出。下面以一個簡單的例子來表述Daemon線程的使用。

public class DaemonDemo {
    public static void main(String[] args) {
        Thread daemonThread = new Thread(new Runnable() {
            @Override
            public void run() {
                while (true) {
                    try {
                        System.out.println("i am alive");
                        Thread.sleep(500);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    } finally {
                        System.out.println("finally block");
                    }
                }
            }
        });
        daemonThread.setDaemon(true);
        daemonThread.start();
        //確保main線程結束前能給daemonThread能夠分到時間片
        try {
            Thread.sleep(800);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}

輸出結果爲:

i am alive
finally block
i am alive

上面的例子中daemodThread run方法中是一個while死循環,會一直打印,但是當main線程結束後daemonThread就會退出所以不會出現死循環的情況。
main線程先睡眠800ms保證daemonThread能夠擁有一次時間片的機會,也就是說可以正常執行一次打印“i am alive”操作和一次finally塊中"finally block"操作。緊接着main 線程結束後,daemonThread退出,這個時候只打印了"i am alive"並沒有打印finnal塊中的。
因此,這裏需要注意的是守護線程在退出的時候並不會執行finnaly塊中的代碼,所以將釋放資源等操作不要放在finnaly塊中執行,這種操作是不安全的
線程可以通過setDaemon(true)的方法將線程設置爲守護線程。並且需要注意的是設置守護線程要先於start()方法,否則會報這樣的異常,但是該線程還是會執行,只不過會當做正常的用戶線程執行。

Exception in thread "main" java.lang.IllegalThreadStateException
at java.lang.Thread.setDaemon(Thread.java:1365)
at learn.DaemonDemo.main(DaemonDemo.java:19)

參考鏈接:Java併發專題

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