1. 程式人生 > >【併發】併發程式設計的挑戰

【併發】併發程式設計的挑戰

1. 上下文切換
a. 概念:
i. CPU通過時間片分配演算法來迴圈執行任務,當前任務執行一個時間片後會切換到下一個任務。但是,在切換前會儲存上一個任務的狀態,以便下次切換回這個任務時,可以再載入這個任務的狀態。所以任務從儲存到再載入的過程就是一次上下文切換
b. 舉例:讀英文書遇到不會的單詞查單詞,記錄所讀書的頁數
c. 多執行緒一定快嗎?
i. 程式碼 package com.multithread;/*
* Created by 楊倩
* @auther 楊倩
* @DESCRIPTION ${DESCRIPTION}
* @create 2018/6/12
*/

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;
                }
            }
        });
        thread.start();
        int b = 0;
        for (long i = 0; i < count; i++) {
            b--;
        }
        long time = System.currentTimeMillis() - start;
        thread.join();
        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);
    }

}
console concurrency :1ms,b=-10000
serial:0ms,b=-10000,a=50000
測試結果
        
結論 當併發執行累加操作不超過百萬次時,速度會比序列執行累加操作要
慢。那麼,為什麼併發執行的速度會比序列慢呢?這是因為執行緒有建立和上下文切換的開銷。
d. 測試上下文切換次數和市場
i. 使用Lmbench3[1]可以測量上下文切換的時長。
ii. 使用vmstat可以測量上下文切換的次數
1) 示例
a) 
2) 結論
a) 上下文每1秒切換1000多次
e. 如何減少上下文切換
i. 無鎖併發程式設計。多執行緒競爭鎖時,會引起上下文切換,所以多執行緒處理資料時,可以用一些辦法來避免使用鎖,如將資料的ID按照Hash演算法取模分段,不同的執行緒處理不同段的資料。
ii. CAS演算法。Java的Atomic包使用CAS演算法來更新資料,而不需要加鎖。
iii. 使用最少執行緒。避免建立不需要的執行緒,比如任務很少,但是建立了很多執行緒來處理,這樣會造成大量執行緒都處於等待狀態。
iv. 協程:在單執行緒裡實現多工的排程,並在單執行緒裡維持多個任務間的切換
f. 減少上下文切換實戰
i. 通過減少線上大量WAITING的執行緒
ii. 例子 第一步:用jstack命令dump執行緒資訊,看看pid為3117的程序裡的執行緒都在做什麼。
sudo -u admin /opt/ifeve/java/bin/jstack 31177 > /home/tengfei.fangtf/dump17
第二步:統計所有執行緒分別處於什麼狀態,發現300多個執行緒處於WAITING(onobjectmonitor)狀態。
[[email protected] ~]$ grep java.lang.Thread.State dump17 | awk '{print $2$3$4$5}'
| sort | uniq -c
39 RUNNABLE
21 TIMED_WAITING(onobjectmonitor)
6 TIMED_WAITING(parking)
51 TIMED_WAITING(sleeping)
305 WAITING(onobjectmonitor)
3 WAITING(parking)
第三步:開啟dump檔案檢視處於WAITING(onobjectmonitor)的執行緒在做什麼。發現這些線
程基本全是JBOSS的工作執行緒,在await。說明JBOSS執行緒池裡執行緒接收到的任務太少,大量線
程都閒著。
"http-0.0.0.0-7001-97" daemon prio=10 tid=0x000000004f6a8000 nid=0x555e in
Object.wait() [0x0000000052423000]
java.lang.Thread.State: WAITING (on object monitor)
at java.lang.Object.wait(Native Method)
- waiting on <0x00000007969b2280> (a org.apache.tomcat.util.net.AprEndpoint$Worker)
at java.lang.Object.wait(Object.java:485)
at org.apache.tomcat.util.net.AprEndpoint$Worker.await(AprEndpoint.java:1464)
- locked <0x00000007969b2280> (a org.apache.tomcat.util.net.AprEndpoint$Worker)
at org.apache.tomcat.util.net.AprEndpoint$Worker.run(AprEndpoint.java:1489)
at java.lang.Thread.run(Thread.java:662)
第四步:減少JBOSS的工作執行緒數,找到JBOSS的執行緒池配置資訊,將maxThreads降到
100。
<maxThreads="250" maxHttpHeaderSize="8192"
emptySessionPath="false" minSpareThreads="40" maxSpareThreads="75"
maxPostSize="512000" protocol="HTTP/1.1"
enableLookups="false" redirectPort="8443" acceptCount="200" bufferSize="16384"
connectionTimeout="15000" disableUploadTimeout="false" useBodyEncodingForURI= "true">
第五步:重啟JBOSS,再dump執行緒資訊,然後統計WAITING(onobjectmonitor)的執行緒,發現
減少了175個。WAITING的執行緒少了,系統上下文切換的次數就會少,因為每一次從
WAITTING到RUNNABLE都會進行一次上下文的切換。讀者也可以使用vmstat命令測試一下。
[[email protected] ~]$ grep java.lang.Thread.State dump17 | awk '{print $2$3$4$5}'
| sort | uniq -c
44 RUNNABLE
22 TIMED_WAITING(onobjectmonitor)
9 TIMED_WAITING(parking)
36 TIMED_WAITING(sleeping)
130 WAITING(onobjectmonitor)
1 WAITING(parking 

2. 死鎖
a. 例子 package com.multithread;/*
* Created by 楊倩
* @auther 楊倩
* @DESCRIPTION ${DESCRIPTION}
* @create 2018/6/12
*/

public class DeadLockDemo {

    private static String A = "A";
    private static String B = "B";

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

    private void deadLock() {
        Thread t1 = new Thread(new Runnable() {
            @Override
            public void run() {
                synchronized (A) {
                    try {
                        Thread.currentThread().sleep(2000);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    synchronized (B) {
                        System.out.println("1");
                    }
                }
            }
        });
        Thread t2 = new Thread(new Runnable() {
            @Override
            public void run() {
                synchronized (B) {
                    synchronized (A) {
                        System.out.println("2");
                    }
                }
            }
        });
        t1.start();
        t2.start();
    }

}
b. 避免死鎖
i. 避免一個執行緒同時獲取多個鎖。
ii. ·避免一個執行緒在鎖內同時佔用多個資源,儘量保證每個鎖只佔用一個資源。
iii. ·嘗試使用定時鎖,使用lock.tryLock(timeout)來替代使用內部鎖機制。
iv. ·對於資料庫鎖,加鎖和解鎖必須在一個數據庫連線裡,否則會出現解鎖失敗的情況


3. 資源限制的挑戰
a. 什麼是資源限制
i. 資源限制是指在進行併發程式設計時,程式的執行速度受限於計算機硬體資源或軟體資源
ii. 硬體資源限制:頻寬的上傳/下載速度、硬碟讀寫速度和CPU的處理速度
iii. 軟體資源限制有資料庫的連線數和socket連線數
iv. 舉例:伺服器的頻寬只有2Mb/s,某個資源的下載速度是1Mb/s每秒,系統啟動10個執行緒下載資源,下載速度不會變成10Mb/s
b. 引發的問題
i. 在併發程式設計中,將程式碼執行速度加快的原則是將程式碼中序列執行的部分變成併發執行,但是如果將某段序列的程式碼併發執行,因為受限於資源,仍然在序列執行,這時候程式不僅不會加快執行,反而會更慢,因為增加了上下文切換和資源排程的時間。例如,之前看到一段程式使用多執行緒在辦公網併發地下載和處理資料時,導致CPU利用率達到100%,幾個小時都不能執行完成任務,後來修改成單執行緒,一個小時就執行完成了
c. 解決
i. 硬體資源限制:使用叢集並行執行程式
ii. 軟體資源限制,可以考慮使用資源池將資源複用
d. 在資源限制情況下進行併發程式設計
i. 如何在資源限制的情況下,讓程式執行得更快呢?
1) 根據不同的資源限制調整程式的併發度,比如下載檔案程式依賴於兩個資源——頻寬和硬碟讀寫速度。
2) 有資料庫操作時,涉及資料庫連線數,如果SQL語句執行非常快,而執行緒的數量比資料庫連線數大很多,則某些執行緒會被阻塞,等待資料庫連線。