1 多執行緒的優缺點

weixin_34236869發表於2018-06-13

之前寫的都亂糟糟的,現在也需要重新記憶一遍。所以重新整理一下JUC包。

多執行緒及其優缺點

什麼是執行緒

是作業系統能夠進行運算排程的最小單位。它被包含在程式之中,是程式中的實際運作單位。(wiki百科)

建立執行緒的三種方式

public class ThreadTest {
    public static void main(String[] args) throws InterruptedException {
        //1、繼承Thread方式
        Thread thread1 = new Thread(){
            @Override
            public void run() {
                System.out.println("thread1 start");
            }
        };
        thread1.start();

        //2、實現Runnable介面
        Thread thread2 = new Thread(new Runnable() {
            @Override
            public void run() {
                System.out.println("thread2 start");
            }
        });
        thread2.start();

        //3、實現Callable介面
        ExecutorService executorService = Executors.newSingleThreadExecutor();
        Future<String> future = executorService.submit(new Callable<String>() {
            @Override
            public String call() throws Exception {
                return "future start";
            }
        });
        try {
            String result = future.get();
            System.out.println(result);
        } catch (InterruptedException e) {
            e.printStackTrace();
        } catch (ExecutionException e) {
            e.printStackTrace();
        }

    }
}

在jdk8之後用lambda表示式轉換一下

public class ThreadTest {
    public static void main(String[] args) throws InterruptedException {
        //1、繼承Thread方式
        Thread thread1 = new Thread(() -> System.out.println("thread1 start"));
        thread1.start();

        //2、實現Runnable介面
        Thread thread2 = new Thread(() -> System.out.println("thread2 start"));
        thread2.start();

        //3、實現Callable介面
        ExecutorService executorService = Executors.newSingleThreadExecutor();
        Future<String> future = executorService.submit(() -> "future start");
        try {
            String result = future.get();
            System.out.println(result);
        } catch (InterruptedException e) {
            e.printStackTrace();
        } catch (ExecutionException e) {
            e.printStackTrace();
        }

    }
}

簡化了一點,但是更多是有點懵,lambda為什麼會簡化方法,->是怎麼找到對應的方法,下次在研究。

為什麼要用多執行緒

早期的CPU是單核的,為了提升計算能力,將多個計算單元整合到一起。形成了多核CPU。多執行緒就是為了將多核CPU發揮到極致,一邊提高效能

多執行緒缺點呢

上面說了多執行緒的有點是:為了提高計算效能。那麼一定會提高?
答案是不一定的。有時候多執行緒不一定比單執行緒計算快。引入《java併發程式設計的藝術》上第一個例子

public class ConcurrencyTest {

    /** 執行次數 */
    private static final long count = 10000l;

    public static void main(String[] args) throws InterruptedException {
        //併發計算
        concurrency();
        //單執行緒計算
        serial();
    }

    private static void concurrency() throws InterruptedException {
        long start = System.currentTimeMillis();
        Thread thread = new Thread(new Runnable() {
            @Override
            public void run() {
                int a = 0;
                for (long i = 0; i < count; i++) {
                    a += 5;
                }
                System.out.println(a);
            }
        });
        thread.start();
        int b = 0;
        for (long i = 0; i < count; i++) {
            b--;
        }
        thread.join();
        long time = System.currentTimeMillis() - start;
        System.out.println("concurrency :" + time + "ms,b=" + b);
    }

    private static void serial() {
        long start = System.currentTimeMillis();
        int a = 0;
        for (long i = 0; i < count; i++) {
            a += 5;
        }
        int b = 0;
        for (long i = 0; i < count; i++) {
            b--;
        }
        long time = System.currentTimeMillis() - start;
        System.out.println("serial:" + time + "ms,b=" + b + ",a=" + a);
    }

}

結果為

50000
concurrency :22ms,b=-10000
serial:0ms,b=-10000,a=50000

而且多執行緒會帶來額外的開銷

  • 上下文切換
  • 執行緒安全

上下文切換

時間片是CPU分配給各個執行緒的時間,因為時間非常短,所以CPU不斷通過切換執行緒,讓我們覺得多個執行緒是同時執行的,時間片一般是幾十毫秒。而每次切換時,需要儲存當前的狀態起來,以便能夠進行恢復先前狀態,而這個切換時非常損耗效能,過於頻繁反而無法發揮出多執行緒程式設計的優勢。
減少上下文切換可以採用無鎖併發程式設計,CAS演算法,使用最少的執行緒和使用協程。

  • 無鎖併發程式設計:可以參照concurrentHashMap鎖分段的思想,不同的執行緒處理不同段的資料,這樣在多執行緒競爭的條件下,可以減少上下文切換的時間。
  • CAS演算法,利用Atomic下使用CAS演算法來更新資料,使用了樂觀鎖,可以有效的減少一部分不必要的鎖競爭帶來的上下文切換
  • 使用最少執行緒:避免建立不需要的執行緒,比如任務很少,但是建立了很多的執行緒,這樣會造成大量的執行緒都處於等待狀態
  • 協程:在單執行緒裡實現多工的排程,並在單執行緒裡維持多個任務間的切換

執行緒安全的問題

多執行緒程式設計中最難以把握的就是臨界區執行緒安全問題,稍微不注意就會出現死鎖的情況
同樣引入《java併發程式設計的藝術》的一個例子

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) {
                    try {
                        Thread.sleep(3000);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    System.out.println("get resource b");
                    synchronized (resource_a) {
                        System.out.println("get resource a");
                    }
                }
            }
        });
        threadA.start();
        threadB.start();

    }
}

然後通過jps檢視,找個這個類的id
然後通過jstack id來檢視

Found one Java-level deadlock:
=============================
"Thread-1":
  waiting to lock monitor 0x0000000016074808 (object 0x00000000e0b89280, a java.lang.String),
  which is held by "Thread-0"
"Thread-0":
  waiting to lock monitor 0x0000000016075ca8 (object 0x00000000e0b892b0, a java.lang.String),
  which is held by "Thread-1"

Java stack information for the threads listed above:
===================================================


"Thread-1" #11 prio=5 os_prio=0 tid=0x00000000175ba800 nid=0x232c waiting for monitor entry [0x000000001889f000]
   java.lang.Thread.State: BLOCKED (on object monitor)
        at DeadLockDemo$2.run(DeadLockDemo.java:37)
        - waiting to lock <0x00000000e0b89280> (a java.lang.String)
        - locked <0x00000000e0b892b0> (a java.lang.String)
        at java.lang.Thread.run(Thread.java:748)

"Thread-0" #10 prio=5 os_prio=0 tid=0x00000000175b7800 nid=0x234c waiting for monitor entry [0x000000001861f000]
   java.lang.Thread.State: BLOCKED (on object monitor)
        at DeadLockDemo$1.run(DeadLockDemo.java:18)
        - waiting to lock <0x00000000e0b892b0> (a java.lang.String)
        - locked <0x00000000e0b89280> (a java.lang.String)
        at java.lang.Thread.run(Thread.java:748)

兩個執行緒相互等待,仔細看上面的waiting to lock 和locked兩個物件。是相互的。造成死鎖。
造成死鎖的原因和解決方案

死鎖:指兩個或兩個以上的程式在執行過程中,由於競爭資源或者由於彼此通訊而造成的一種阻塞的現象,若無外力作用,它們都將無法推進下去。
造成死鎖的原因是:

  1. 因為系統資源不足。
  2. 程式執行推進的順序不合適。
  3. 資源分配不當等。

如果系統資源充足,程式的資源請求都能夠得到滿足,死鎖出現的可能性就很低,否則
就會因爭奪有限的資源而陷入死鎖。其次,程式執行推進順序與速度不同,也可能產生死鎖。

那麼死鎖的必要條件是:

  1. 互斥條件:一個資源每次只能被一個程式使用。
  2. 請求與保持條件:一個程式因請求資源而阻塞時,對已獲得的資源保持不放。
  3. 不剝奪條件:程式已獲得的資源,在末使用完之前,不能強行剝奪。
  4. 迴圈等待條件:若干程式之間形成一種頭尾相接的迴圈等待資源關係。

這四個條件是 死鎖的必要條件 ,只要系統發生死鎖,這些條件必然成立,而只要上述條件之
一不滿足,就不會發生死鎖。

執行緒的狀態

執行緒有6種狀態

  1. NEW:新建,執行緒被構建,但是還沒有start()
  2. RUNNABLE:執行,java中將就緒和執行統稱為執行中
  3. BLOCKED:阻塞,執行緒阻塞於鎖
  4. WAITING:等待,表示執行緒進入等待狀態,需要其他執行緒的特定動作(通知或中斷)
  5. TIMED_WAITING:帶超時的等待,可以在指定的時間內自動返還
  6. TERMINATED:終止,表示執行緒已經執行完畢

4714843-192a9d07199f3ecb.png
狀態轉換

執行緒建立之後呼叫start()方法開始執行。
當呼叫wait(),join(),LockSupport.lock()方法執行緒會進入到WAITING狀態,而同樣的wait(long timeout)sleep(long), join(long), LockSupport.parkNanos(), LockSupport.parkUtil()增加了超時等待的功能,也就是呼叫這些方法後執行緒會進入TIMED_WAITING狀態,當超時等待時間到達後,執行緒會切換到Runable的狀態,另外當WAITINGTIMED _WAITING狀態時可以通過Object.notify(),Object.notifyAll()方法使執行緒轉換到Runable狀態。當執行緒出現資源競爭時,即等待獲取鎖的時候,執行緒會進入到BLOCKED阻塞狀態,當執行緒獲取鎖時,執行緒進入到Runable狀態。執行緒執行結束後,執行緒進入到TERMINATED狀態,狀態轉換可以說是執行緒的生命週期。
注意
當執行緒進入到synchronized方法或者synchronized程式碼塊時,執行緒切換到的是BLOCKED狀態.
而使用java.util.concurrent.lockslock進行加鎖的時候執行緒切換的是WAITING或者TIMED_WAITING狀態,因為lock會呼叫LockSupport的方法。

執行緒狀態的操作

interrupted()

中斷可以理解為執行緒的一個標誌位,它表示了一個執行中的執行緒是否被其他執行緒進行了中斷操作。中斷好比其他執行緒對該執行緒打了一個招呼。
其他執行緒可以呼叫該執行緒的interrupt()方法對其進行中斷操作,同時該執行緒可以呼叫 isInterrupted()來感知其他執行緒對其自身的中斷操作,從而做出響應。
另外,同樣可以呼叫Thread的靜態方法 interrupted()對當前執行緒進行中斷操作,該方法會清除中斷標誌位。

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());
    }
}

執行結果是:


4714843-70719c7cb7642fe4.png

對著兩個執行緒進行中斷操作,可以看出sleepThread丟擲InterruptedException後清除標誌位,而busyThread就不會清除標誌位。

join()

join方法可以看做是執行緒間協作的一種方式。
如果一個執行緒例項A執行了threadB.join(),其含義是:當前執行緒A會等待threadB執行緒終止後threadA才會繼續執行。

public class JoinDemo {
    public static void main(String[] args) {
        Thread previousThread = Thread.currentThread();
        for (int i = 1; i <= 5; 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 {
               //join
               thread.join();
                System.out.println(thread.getName() + " terminated.");
            } catch (Exception e) {
                e.printStackTrace();
            }
        }
    }
}
4714843-55e27cb3c980e720.png
執行結果

如果註釋了上面的thread.join();

4714843-35da46c3f9cb1752.png
執行結果

每個執行緒都會等待前一個執行緒結束才會繼續執行。

sleep() VS wait()

兩者主要的區別:

  1. sleep()方法是Thread的靜態方法,而wait是Object例項方法
  2. wait()方法必須要在同步方法或者同步塊中呼叫,也就是必須已經獲得物件鎖。而sleep()方法沒有這個限制可以在任何地方種使用。另外,wait()方法會釋放佔有的物件鎖,使得該執行緒進入等待池中,等待下一次獲取資源。而sleep()方法只是會讓出CPU並不會釋放掉物件鎖;
  3. sleep()方法在休眠時間達到後如果再次獲得CPU時間片就會繼續執行,而wait()方法必須等待Object.notift/Object.notifyAll通知後,才會離開等待池,並且再次獲得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();
        }
    }
}

執行結果:

4714843-1d1dd4e851ea5495.png
結果

守護執行緒應該先於start()方法之前。如果在之後,但是該執行緒還是會執行,只不過會當做正常的使用者執行緒執行。

其他的一些概念

同步和非同步

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

併發與並行

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

阻塞和非阻塞

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

臨界區

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

相關文章