《java併發編程實戰》總結

第1章 簡介

線程的優勢:

①發揮多處理器的強大優勢  ②建模的簡單性 ③異步事件的簡化處理④相應更靈敏的用戶界面

線程帶來的風險:

①安全性問②活躍性問題③性能問題

第2章 線程安全性

2.1什麼是線程安全性

       當多個線程訪問某個類時,不管運行時環境採用何種調度方式或者這些線程將如何交替執行,並且在主調代碼中不需要任何額外的同步或協同,這個類都能表現出正確的行爲,那麼就稱這個類是線程安全的。

       在線程安全的類中封裝了必要的同步機制,因此客戶端無需進一步採取同步措施。

       無狀態對象一定是線程安全的。

2.2原子性

2.2.1競態條件

競態條件:當某個計算的正確性取決於多個線程的交替執行時序時,那麼就會發生競態條件。換句話說,就是正確的結果要取決於運氣。最常見的靜態條件類型就是“先檢查後執行(Check-Then-Act)”操作,即通過一個可能失效的觀測結果來決定下一步的動作。

數據競爭:如果在訪問非final類型的域時沒有采用同步來進行協調,那麼就會出現數據競爭。(JMM知識)

2.3加鎖機制

2.3.1內置鎖

Java提供了一種內置的鎖機制來支持原子性synchronized

2.3.2重入

“重入”意味着獲取鎖的操作的粒度是“線程”,而不是“調用”。

如下面代碼所示:如果沒有“重入”,則產生死鎖。

public class Widget{
    public synchronized void doSomething(){
        System.out.println("Widget..doSomething");
    }
}
public class LoggingWidget extends Widget{
    public synchronized void doSomething(){
        System.out.println("LoggingWidget..doSomething");
        super.doSomething();
    }
}

2.4用鎖來保護狀態

       對於可能被多個線程同時訪問的可變狀態變量,在訪問他的時候都需要持有同一個鎖,在這種情況下,我們稱狀態變量是由這個鎖來保護的。

      每個共享和可變的變量都應該由一個鎖來保護,從而使維護人員知道是哪一個鎖。

     對於每個包含多個變量的不變性條件,其中涉及的所所有變量都需要由一個鎖來保護。

2.5活躍性與性能

       通常,在簡單性與性能之間存在相互制約因素。當實現某個同步策略時,一定不要盲目地爲了性能而犧牲簡單性(這可能破壞安全性)。

      當執行時間較長的計算或者可能無法快速完成的操作時(例如,網絡I/O或者控制檯I/O),一定不要持有鎖。

第3章 對象的共享

   我們已經知道了同步代碼塊和同步方法可以確保以原子的方式執行操作,但一種常見的誤解是,認爲關鍵字synchronized只能用於實現原子性或者確定“臨界區(Critical Section)”。同步還有另一個重要的方面:內存可見性(Memory Visibility)。

3.1可見性(JMM知識)

如下面代碼所示,明明flag已經改爲true,爲什麼程序沒有停止?因爲主線程沒有看到最新的flag的值

import java.util.concurrent.TimeUnit;

/**
 * @author CBeann
 * @create 2020-03-26 13:22
 */
public class NoVisibility {
    public static void main(String[] args) {
        ThreadDemo threadDemo = new ThreadDemo();
        new Thread(threadDemo).start();

        while (true) {
            if (threadDemo.isFlag()) {
                System.out.println("------");
                break;
            }
        }


    }


}

class ThreadDemo implements Runnable {

    private boolean flag = false;

    @Override
    public void run() {
        try {
            TimeUnit.MILLISECONDS.sleep(200);
        } catch (Exception e) {
            e.printStackTrace();
        }
        flag = true;
        System.out.println("flag=" + isFlag());
    }

    public boolean isFlag() {
        return flag;
    }

    public void setFlag(boolean flag) {
        this.flag = flag;
    }
}

解決辦法

private volatile boolean flag = false;

3.1.3 加鎖與可見性

  加鎖的含義不僅僅侷限性互斥行爲,還包括內存可見性。爲了確保所有的線程都能見到共享變量的最新值,所有執行讀操作或者寫操作的線程必須在同一個鎖上同步。

3.1.4 Volatile變量

 Java語言提供了一種稍弱的同步機制,即volatile變量,用來確保將變量的更新操作通知到其他線程。

volatile變量的典型用法:

volatile Boolean  flag;
        ...
        while(!flag){
            doSomeThing();
        }

 加鎖機制既可以確保可見性又可以確保原子性,而volatile變量只能確保可見性。

3.2 發佈和逸出

 

“發佈(Publish)”一個對象的意思是指,使對象能夠在當前作用域之外的代碼中使用。

當某個不應該發佈的對象被髮布時,這種情況就被稱爲“逸出(Escape)”。

如下面的代碼所示,demo中的list被髮布,而且逸出。

import java.util.ArrayList;
import java.util.List;

/**
 * @author CBeann
 * @create 2020-02-20 2:49
 */
public class Start {

    public static void main(String[] args) {
        Demo demo = new Demo();
        //調用getList方法發佈Demo對象中的list對象
        List<String> list = demo.getList();
        //該對象逸出,因爲list對象已經逸出它所在的作用域(Demo的是私有變量域)
        list.add("main-thread-add");
        for (String s : demo.getList()) {
            System.out.println(s);
        }
    }
}

class Demo {
    private List<String> list = null;

    public Demo() {
        list = new ArrayList<>();
        list.add("cbeann");
    }

    public List<String> getList() {
        return list;
    }

    public void setList(List<String> list) {
        this.list = list;
    }
}

 3.3 線程封閉

   當訪問共享的可變數據時,通常需要使用同步。一種避免使用同步的方式就是不共享數據。如果僅在單線程內訪問數據,就不需要同步,這種技術稱爲線程封閉(Thread Confinement),它是實現線程安全性最簡單方式之一。

3.3.2棧封閉

棧封閉是線程封閉的一種特例。在棧封閉中,只能通過局部變量表才能訪問對象。

例如下面代碼中的demo對象是一個局部變量,只要不發佈,其它線程都無法獲得該對象的引用。

    public static void main(String[] args) {
        Demo demo = new Demo();
        }

3.3.3 ThreadLocal類

 把對象存在threadLocal對象中能實現不共享。因爲它的key是thread,所以其它線程取不到數據。

        ThreadLocal threadLocal = new ThreadLocal();
        try {
//        public void set(T value) {
//            Thread t = Thread.currentThread();
//            ThreadLocal.ThreadLocalMap map = getMap(t);
//            if (map != null)
//                map.set(this, value);
//            else
//                createMap(t, value);
//        }
            threadLocal.set(1);
            Object o = threadLocal.get();
        } catch (Exception e) {
            e.printStackTrace();
        }finally {
            threadLocal.remove();//最後要刪除,否則容易出現內存泄露
        }

3.4不變性

 不可變的對象一定是線程安全的。

即使對象中所有的域都是final類型的,這個對象仍然是可變的,因爲在final類型的域中可以保存對可變對象的引用。

如下面代碼所示,demo裏的list用final修飾,但是仍然可以添加。

public class Start {

    public static void main(String[] args) {
        Demo demo = new Demo();
        demo.getList().add(1);
        for (Object o : demo.getList()) {
            System.out.println(o);
        }
    }
}

class Demo {
    private final List list = new ArrayList();
    public List getList() {
        return list;
    }
}

3.5 安全發佈

3.5.6安全的共享對象

在併發程序中使用和共享對象時,可以使用一些實用的策略,包括:

線程封閉。線程封閉的對象只能由一個線程擁有,對象被封閉在該線程中,並且只能由這個線程修改。

只讀共享。在沒有額外同步的情況下,共享的只讀對象可以由多個線程併發訪問,但任何線程都不能修改它。共享的只讀對象包括不可變對象和事實不可變對象。

線程安全共享。線程安全的對象在其內部實現同步,因此多個線程可以通過對象的公有接口來進行訪問而不需要進一步的同步。

保護對象。被保護的對象只能通過持有特定的鎖來訪問。保護對象包括封裝在其他線程安全對象中的對象,以及發佈的並且由某個特定的鎖保護的對象。

 

第4章 對象的組合

對象的組合

在設計線程安全的類的過程中,需要包含以下三個基本要素:

  • 找出構成對象狀態的所有變量。
  • 找出約束狀態變量的不變性條件。
  • 建立對象狀態的併發訪問管理策略。

線程安全的類組合的類不一定是線程安全的

//線程安全
class Demo {
    private Map map = new ConcurrentHashMap();
    public void put(String key, Object val) {
        map.put(key, val);
    }
}
//線程不安全
class Demo {
    private Map map = new ConcurrentHashMap();
    private Map map2 = new ConcurrentHashMap();
    public void put(String key, Object val) {
        if (map.containsKey(key)){
            map2.put(key, val);
        }
    }
}

本章疑問

爲什麼書中說下面的一個線程不安全,一個線程安全?

課本提示:線程不安全的代碼中說list保護的鎖反正不是ListHelper的鎖,大致意思是鎖不同,反正我沒看懂,看懂的可以在下面留言。

//線程不安全
class ListHelper<E> {
    public List<E> list = Collections.singletonList(new ArrayList<>());

    public synchronized boolean putIfAbsent(E e) {
        boolean absent = !list.contains(e);
        if (absent) list.add(e);
        return absent;
    }
}
//線程安全
class ImprovedList<T> implements List<T> {

    private final List<T> list;

    public ImprovedList(List<T> list) {
        this.list = list;
    }

    public synchronized boolean putIfAbsent(T x) {
        boolean contains = list.contains(x);
        if (contains) list.add(x);
        return !contains;
    }

    //還要實現size,isempty等方法
}

第5章 基礎構建模塊

5.1同步類容器

5.1.3隱藏迭代器

雖然加鎖可以防止迭代器拋出ConcurrentModificationException,但是你必須要記住在所有對共享容器進行迭代的地方都需要加速。實際情況要更加複雜,因爲在某些情況下,迭代器會隱藏起來。如下面代碼所示。編輯器將字符串的連接操作轉換爲調用StringBuilder.append(Object),而這個方法又會調用容器的toString方法,標準容器的toString方法將迭代容器,並在每一個元素上調用toString來生成容器內容的格式化表示。

class HiddenIterator{
    private final Set set = new HashSet();
    public void addTenThings(){
        Random random = new Random();
        for (int i = 0; i < 10; i++) {
            set.add(random.nextInt());
        }
        //存在隱藏迭代器
        System.out.println("DEBUG: added ten elements to " + set);
    }
}

容器的hashCode和equals等方法也會間接的執行迭代操作,當容器作爲另一個容器的元素或鍵值時,就會出現這種情況。同樣,containsAll、removeAll和retainAll等方法,以及把容器作爲參賽的構造函數,都會對容器進行迭代。所有這些間接的迭代操作都可以拋出 ConcurrentModificationException。

5.2併發容器

 通過併發容器來代替同步容器,可以極大的提高伸縮性並降低風險。

Map map = new HashMap();//不安全的容器
Map map1 = new Hashtable();//同步容器
Map map2 = new ConcurrentHashMap();//併發容器

5.3阻塞隊列和生產者-消費者模式

在構建高可靠的應用程序時,有界隊列是一種強大的資源管理工具:它們能抑制並防止生產過度的工作項,使應用程序在負荷過載的情況下變的更加健壯。

5.5同步工具類

阻塞隊列可以作爲同步工具類,其它類型的同步工具類還包括信號量(Semaphore)、柵欄(Barrier)以及閉鎖(Latch)。

5.5.1閉鎖

閉鎖是一種同步工具類。閉鎖的作用相當於一扇門:在閉鎖到達結束狀態之前,這扇門一直是關閉的,並且沒有任何線程可以通過,當到達結束狀態時,這扇門會打開並且允許所有的線程通過。當閉鎖到達結束狀態後,將不會再改變狀態,因此這扇門將永遠保持打開狀態。閉鎖可以用來確保某些活動直到其它活動都完成後才繼續執行。

public class CountDownLatchDemo {
    //測試10個線程併發執行需要多長時間
    public static void main(String[] args) throws Exception {
        int threadNum = 10;
        CountDownLatch startGate = new CountDownLatch(1);
        CountDownLatch endGate = new CountDownLatch(threadNum);
        for (int i = 0; i < threadNum; i++) {
            new Thread(new Runnable() {
                @Override
                public void run() {
                    try {
                        startGate.await();
                        System.out.println("tun task...");
                    } catch (Exception e) {
                        e.printStackTrace();
                    } finally {
                        endGate.countDown();
                    }

                }
            }).start();
        }

        long start = System.currentTimeMillis();

        startGate.countDown();
        endGate.await();
        long end = System.currentTimeMillis();

        System.out.println(end - start);


    }
}

5.5.3信號量

計數信號量(Semaphore)用來控制同時訪問某個特定資源的操作數量,或者同時執行某個操作的數量。計數信號量還可以用來實現某種資源池,或者對容器施加邊界。當信號量的參數爲1時,作用和排它鎖相似。

public class SemaphoreDemo {

    public static void main(String[] args) throws Exception {
        //設置3個資源
        Semaphore semaphore = new Semaphore(3);

        for (int i = 0; i < 10; i++) {
            new Thread(new Runnable() {
                @Override
                public void run() {

                    String name = Thread.currentThread().getName();
                    try {
                        semaphore.acquire();
                        System.out.println(name+"獲得信號量");

                        System.out.println("do task...");


                    } catch (Exception e) {
                        e.printStackTrace();
                    } finally {
                        semaphore.release();
                        System.out.println(name+"釋放信號量");
                    }


                }
            }, "threadID-" + i).start();
        }


    }
}

5.5.4柵欄

CyclicBarrier

總結

  • 可變變量是至關重要的。

所有的併發問題都可以歸結爲如何協調對併發狀態的訪問。可變狀態越少,就越容易確保線程安全性。 

  • 儘量將域聲明爲final 類型,除非需要它們是可變的。
  • 不可變對象一定是線程安全的。

不可變對象能極大地降低併發編程的複雜性。它們更爲簡單而且安全,可以任意共享而無須使用加鎖或保護性複製等機制。

  • 封裝有助於管理複雜性。

在編寫線程安全的程序時,雖然可以將所有數據都保存在全局變量中,但爲什麼要這樣做?將數據封裝在對象中,更易於維持不變性條件:將同步機制封裝在對象中,更易於遵循同步策略。

  • 用鎖來保護每個可變變量。
  • 當保護同一個不變性條件中的所有變量時,要使用同一個鎖。
  • 在執行復合操作期間,要持有鎖。
  •  如果從多個線程中訪問同一個可變變量時沒有同步機制,那麼程序會出現問題。
  • 不要故作聰明地推斷出不需要使用同步。
  •  在設計過程中考慮線程安全,或者在文檔中明確地指出它不是線程安全的。
  • 將同步策略文檔化。

第6章 任務執行

6.1在線程中執行任務

6.1.3無限制創建線程的不足

線程生命週期的開銷非常高。線程的創建與銷燬不是沒有代價的。線程的創建過程會需要時間,延遲處理的請求。

資源消耗。如果你已經擁有足夠多的線程使所有的CPU處於忙碌狀態,那麼創建更多的線程反而會降低性能。

穩定性。在可創建線程的數量上存在一個限制,如果破壞了這些限制,那麼很有可能出現OOM異常。

6.2 Executor框架

6.2.2 執行策略

每當看到下面這種形式的代碼時:

 new Thread(runnable).start();

並且你希望獲得一種更加靈活的執行策略時,請考慮使用Executor來替代Thread。

6.2.3 線程池

https://blog.csdn.net/qq_37171353/article/details/103794099

6.3找出可利用的並行性

6.3.5 CompletionService: Executor與BlockingQueue

如果向Executor提交一組任務,並且希望計算完成後獲得結果,那麼你可以保留與每一個任務關聯的Future,然後反覆使用get方法,這種方法可行,但是繁瑣。

如下面代碼所示,我提交一5個執行隨機時間的任務,當執行完畢後,completionService.take()就會返回執行完畢的那一個。

import java.util.Random;
import java.util.concurrent.*;

/**
 * @author CBeann
 * @create 2020-02-20 2:49
 */
public class CompletionServiceDemo{

    public static void main(String[] args) throws Exception {

        CompletionService completionService = new ExecutorCompletionService<Integer>(new Myexecutor());
        Random random = new Random();
        int threadNum = 5;

        for (int i = 0; i < threadNum; i++) {
            completionService.submit(new Callable() {
                @Override
                public Object call() throws Exception {

                    int nextInt = random.nextInt() % 10;//10秒以內
                    if (nextInt <= 0) nextInt += 10;
                    try {
                        System.out.println("業務邏輯執行時間: " + nextInt);
                        TimeUnit.SECONDS.sleep(nextInt);
                    } catch (Exception e) {
                        e.printStackTrace();
                    }

                    return nextInt;
                }
            });
        }

        int sum = 0;
        for (int i = 0; i < threadNum; i++) {
            Future take = completionService.take();
            Integer o = (Integer) take.get();
            sum += o;

        }
        System.out.println(sum);

    }
}


class Myexecutor implements Executor {
    @Override
    public void execute(Runnable command) {
        new Thread(command).start();

    }
}

第7章 取消與關閉

很重要,因爲我看不懂(我好菜啊)

第8章 線程池的使用

https://blog.csdn.net/qq_37171353/article/details/103794099

在一些任務中,需要擁有或排除某種特定的執行策略。如果某些任務依賴其他的任務,那麼會要求線程池足夠大,從而確保它們依賴的任務不會被放入等待隊列中或被拒絕,而採用線程封閉機制的任務需要串行執行。通過將這些需求寫入文檔,將來的代碼維護人員就不會由於使用了某種不合適的執行策略而破壞安全性或活躍性。

每當提交了一個由依賴性的Executor任務時,要清楚的知道可能會出現線程“飢餓”死鎖,因此需要在代碼或配置Executor的配置中心記錄線程池的大小限制或配置限制。

第10章 避免活躍性危險

在安全性與活躍性之間通常存在着某種制衡。我們使用加鎖機制來確保線程安全,但是如果過度的使用加鎖,則可能導致順序死鎖。同樣,我們使用線程池和信號量來限制資源的使用,但這些限制的行爲可能會導致資源死鎖。

10.1死鎖

線程A等待線程B所佔有的資源,而線程B等待線程A所佔有的資源,如果在圖中形成一個環路,那麼就存在一個死鎖。

10.1.1鎖順序死鎖

如下圖所示,一個線程擁有left鎖,去嘗試right鎖,而一個線程擁有right鎖,去嘗試left鎖,就產生死鎖。

class LeftRighrDeadLock {
    private final Object left = new Object();
    private final Object right = new Object();

    public void leftRight() {
        synchronized (left) {
            synchronized (right) {
                System.out.println();
            }
        }
    }

    public void rightLeft() {
        synchronized (right) {
            synchronized (left) {
                System.out.println();
            }
        }
    }
}

10.1.2動態的鎖順序死鎖 

有的時候我們並不清楚是否在鎖順序上有足夠的控制權來避免死鎖的發生。正如轉賬所示,所有的線程都似乎安裝相同的順序來獲得鎖,但是事實上鎖的順序取決於參數順序,如下面的代碼所示。

    transferMoney(myAccount,yourAccount,10);
    transferMoney(myourAccount,myAccount,,20);

 要解決上面的問題,必須定義鎖順序,該方法將返回由Object.hashcode返回的值定義鎖的順序。雖然增加了額一些代碼,但是消除了發生死鎖的可能性。如果Account中包含一個唯一的,不可變的,並且具備可比性的鍵值,那就不需要例如下面代碼中“加時賽”鎖作用的lock。

    private final Object lock = new Object();
    public void transferMoney(Object fromAcct, Object toAcct, int num) {
        int fromHash = fromAcct.hashCode();
        int toHash = toAcct.hashCode();
        if (fromHash < toHash) {
            synchronized (fromAcct) {
                synchronized (toAcct) {
                    System.out.println("do something...");
                }
            }
        } else if (toHash < fromHash) {
            synchronized (toHash) {
                synchronized (fromHash) {
                    System.out.println("do something...");
                }
            }
        } else {
            synchronized (lock) {
                synchronized (toHash) {
                    synchronized (fromHash) {
                        System.out.println("do something...");
                    }
                }
            }
        }

    }

10.1.3 在協作對象直接發生的死鎖

如下面代碼所示,線程A調用car.carMethod方法時,擁有自己鎖並且嘗試carList的鎖。如果此時線程B調用carList.carListMethod方法時,擁有自己的鎖,並且嘗試car的鎖時,就發生了死鎖。

class Car {
    private String name;
    private CarList carList;
    public synchronized void carMethod(String name) {
        this.name = name;
        carList.carListMethod(this);
    }
}

class CarList {
    private List list = new ArrayList();
    public synchronized void carListMethod(Car car) {
        boolean contains = list.contains(car);
        if (!contains) {
            car.carMethod();
        }
    }
}

 10.1.4 開放調用

如果在調用某個方法時不需要持有鎖,那麼這種調用方法就是開放調用。

    //開放調用的反例
   public synchronized void method(){
       otherClassInstence.synchronizedMethod();
   }
   //開放調用
    public  void method(){
       synchronized (this){
           doSomeThing();
       }
        otherClassInstence.synchronizedMethod();
    }

有的時候會丟失原子性。

在程序中應儘量使用開放調用。與那些在持有鎖的時候調用外部方法的程序相比,更容易對依賴開放調用的程序進行死鎖分析。 

10.2死鎖的避免與診斷

10.2.1 支持定時的鎖

 Lock lock = new ReentrantLock();
        try {
            lock.tryLock(10, TimeUnit.SECONDS);
            //logic
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            lock.unlock();
        }

10.2.2通過線程轉儲信息來分析死鎖

 https://blog.csdn.net/jiankunking/article/details/72850092

10.3其他活躍性危險

10.3.1飢餓

 要避免使用線程優先級,因爲這會增加平臺依賴性,並可能導致活躍性問題。在大多數併發應用程序中,都可以使用默認的線程優先級

10.3.3活鎖

     活鎖(Livelock)是另一種形式的活躍性問題,該問題儘管不會阻塞線程,但也不能繼續執行,因爲線程將不斷重複執行相同的操作,而且,總會失敗。 活鎖通常發生在處理事務消息的應用程序中:如果不能成功地處理某個消息,那麼消息處理機制將回滾整個事務,並將它重新放到隊列的開頭。如果消息處理器在處理某種特定類型的消息時存在錯誤並導致它失敗,那麼每當這個消息從隊列中取出並傳遞到存在錯誤的處理器時,都會發生事務回滾。由於這條消息又被放回到隊列開頭,因此處理器將被反覆調用,並返回相同的結果。(有時候也被稱爲毒藥消息,Poison Message. )雖然處理消息的線程並沒有阻塞,但也無法繼續執行下去。這種形式的活鎖通常是由過度的錯誤恢復代碼造成的,因爲它錯誤地將不可修復的錯誤作爲可修復的錯誤。
 

第11章 性能與可伸縮性

11.1 對性能的思考

要想通過併發來獲得更好的性能,需要努力做好兩件事情:更有效地利用現有的處理資源,以及在出現新的處理資源時使程序儘可能地利用這些新資源。

11.1.1 性能與可伸縮性

應用程序的性能可以採用多個指標來衡量,例如服務時間、延遲時間、吞吐率、效率、可伸縮性以及容量等。其中一些指標(服務時間、等待時間)用於衡量程序的“運行速度”,即某個指定的任務單元需要“多快”才能處理完成。另一些指標(生產量、吞吐量)用於程序的“處理能力”,即在計算資源一 定的情況下,能完成“多少”工作。

可伸縮性指的是:當增加計算資源時(例如CPU、內存、存儲容量或1/O帶寬),程序的吞吐量或者處理能力能相應地增加。

11.1.2 評估各種性能權衡因素

避免不成熟的優化。首先使程序正確,然後在提高運行速度-----如果它還運行的不夠快。

以測試爲基準,不要猜測。

11.2 Amdahl定律

Amdahl定律:在增加計算資源的情況下,程序理論上能夠實現最高加速比,取決於程序中可並行組件與串行組件所佔的比重。

如下面代碼所示,串行化的部分是queue.take()

class WorkerThread extends Thread {
    private final BlockingQueue<Runnable> queue;
    public WorkerThread(BlockingQueue<Runnable> queue) {
        this.queue = queue;
    }
    @Override
    public void run() {
        while (true) {
            try {
                Runnable task = queue.take();
                task.run();
            } catch (Exception e) {
                break;
            }
        }
    }
}

在所有的併發程序中都包含一些串行化部分。如果你認爲在你的程序中不存在串行部分,那麼可以再仔細的檢查一遍。

11.3 線程引入的開銷

     對於爲了提升性能而引入的線程來說,並行帶來的性能提升必須超過併發導致的開銷。

11.3.1 上下文切換

      切換上下文需要一定的開銷,而在線程調度過程中需要訪問由操作系統和JVM共享的數據結構。應用程序、操作系統以及JVM都使用一組相同的CPU。在JVM和操作系統的代碼中消耗越多的CPU時鐘週期,應用程序的可用CPU時鐘週期就越少。但上下文切換的開銷並不只是包含JVM和操作系統的開銷。當一個新的線程被切換進來時,它所需要的數據可能不在當前處理器的本地緩存中,因此上下文切換將導致一一些緩存缺失,因而線程在首次調度運行時會更加緩慢。這就是爲什麼調度器會爲每個可運行的線程分配一一個最小執行時間,即使有許多其他的線程正在等待執行:它將上下文切換的開銷分攤到更多不會中斷的執行時間上,從而提高整體的吞吐量(以損失響應性爲代價)。

11.3.2 內存同步

現代的JVM能通過優化去掉一些不會發生競爭的鎖,從而減少不必要的同步開銷。如果一個鎖對象只能由當前線程訪問,那麼JVM就可以通過優化去掉這個鎖獲取操作,因爲另一個線程無法與當前線程在這個鎖上發生同步。例如,JVM通常會去掉下面代碼中的鎖獲取操作。

//沒有作用的同步(不要這麼做)
        synchronized (new Object()){
            //do
        }

JVM也可以執行鎖粗化(Lock Coarsening)操作,將臨近的同步代碼塊用一個鎖合併起來。如下面代碼所示,3個add和1個toString調用合併爲單個鎖獲取/釋放操作。

    public String getNames(){
        List list = new Vector();
        list.add("張三1");
        list.add("張三2");
        list.add("張三3");
        return list.toString();
    }

 11.4 減少鎖的競爭

在併發程序中,對可伸縮性的最主要微威脅就是獨佔方式的資源鎖

11.4.1 縮小鎖的範圍("快進快出")

降低發生競爭可能性的一種有效方法是儘可能的縮短鎖的持有時間。

class Demo {
    private final Map map = new HashMap();

    //錯誤案例
    public synchronized void putIfAbsent(String name, String val) {
        String key = "users." + name + ".location";
        if (!map.containsKey(key)) {
            map.put(key, val);
        }
    }
    //正確案例
    public void putIfAbsent(String name, String val) {
        String key = "users." + name + ".location";
        synchronized (this){
            if (!map.containsKey(key)) {
                map.put(key, val);
            }
        }
    }
    
}

11.4.2 減小鎖的粒度

鎖分解前

//使用一個鎖
class Demo {
    private final Map map = new HashMap();
    private final Set set = new HashSet();

   
  
    public synchronized void putIfAbsentMap(String name, String val) {
        String key = "users." + name + ".location";
        synchronized (this){
            if (!map.containsKey(key)) {
                map.put(key, val);
            }
        }
    }

    public synchronized void putIfAbsentSet(String name) {
        String key = "users." + name + ".location";
        synchronized (this){
            if (!set.contains(key)) {
                set.add(key);
            }
        }
    }

}

鎖分解後

//鎖分解
class Demo {
    private final Map map = new HashMap();
    private final Set set = new HashSet();


    public void putIfAbsentMap(String name, String val) {
        String key = "users." + name + ".location";
        synchronized (map) {
            if (!map.containsKey(key)) {
                map.put(key, val);
            }
        }
    }

    public void putIfAbsentSet(String name) {
        String key = "users." + name + ".location";
        synchronized (set) {
            if (!set.contains(key)) {
                set.add(key);
            }
        }
    }

}

 

11.4.3 鎖分段

每一段使用一個鎖。可以看jdk1.8以前(不包括jdk1.8)的ConcurrentHashMap中的分段鎖。

//鎖分段
class Demo {


    //比如我設置16個鎖
    Object[] locks = new Object[16];

    private int[] data = new int[32];

    public void updateData(int index, int val) {
        synchronized (locks[index % 16]) {
            data[index] = val;
        }
    }

}

      鎖分段的一個劣勢在於:與採用單個鎖來實現獨佔訪問相比,要獲取多個鎖來實現獨佔訪問將更加困難並且開銷更高。通常,在執行一個操作時最多隻需要獲得一個鎖,但是在某些情況下需要加鎖整個容器,例如當ConcurrentHashMap需要擴展映射範圍以及重新計算鍵值的散列值要分佈到更大的桶集合中時,就需要獲取分段所集合中所有的鎖。

11.4.5 一些替代獨佔鎖的方法

併發容器、讀-寫鎖、不可變對象以及原子變量。

11.4.7  向對象池說“不”

通常,對象分配操作的開銷比同步的開銷更低。

小結

    由於使用線程常常是爲了充分利用多個處理器的計算能力,因此在併發程序性能的討論中,通常更多地將側重點放在吞吐量和可伸縮性上,而不是服務時間。Amdahl定律告訴我們,程序的可伸縮性取決於在所有代碼中必須被串行執行的代碼比例。因爲Java程序中串行操作的主要來源是獨佔方式的資源鎖,因此通常可以通過以下方式來提升可伸縮性:減少鎖的持有時間,降低鎖的粒度,以及採用非獨佔的鎖或非阻塞鎖來代替獨佔鎖。

 

第13章 顯式鎖

13.1 Lock與ReentrantLock

Lock提供了一種無條件的、可輪詢的、定時的以及可中斷的鎖獲取操作,所有的加鎖和解鎖的方法都是顯示的。ReentrantLock實現了Lock接口,並且提供了與synchronized相同的互斥性和內存可見性。

  • 輪詢鎖與定時鎖
  • 可中斷的鎖獲取操作
  • 非塊結構的加鎖

Lock接口的標準使用形式如下:

        Lock lock = new ReentrantLock();
        lock.lock();
        try {
            //do logic
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            lock.unlock();
        }

 

13.2 性能考慮因素

 性能是一個不斷變化的指標,如果在昨天的測試基準中發現X比Y更快,那麼在今天可能已經過時了。

13.3 公平性

       在ReentrantLock的構造函數中提供了兩種公平性選擇:創建一 個非公平的鎖(默認)或者一個公平的鎖。在公平的鎖上,線程將按照它們發出請求的順序來獲得鎖,但在非公平的鎖上,則允許“插隊”:當一個線程請求非公平的鎖時,如果在發出請求的同時該鎖的狀態變爲可用,那麼這個線程將跳過隊列中所有的等待線程並獲得這個鎖。(在Semaphore中同樣可以選擇採用公平的或非公平的獲取順序。)非公平的ReentrantLock並不提倡“插隊”行爲,但無法防止某個線程在合適的時候進行“插隊”。在公平的鎖中,如果有另一個線程持有這個鎖或者有其他線程在隊列中等待這個鎖,那麼新發出請求的線程將被放入隊列中。在非公平的鎖中,只有當鎖被某個線程持有時,新發出的請求的線程纔會被放入隊列中。
       等執行加鎖操作時,公平性將由於在掛起線程和恢復線程時存在的開銷而極大的降低性能。在大多數情況下,非公平性鎖的性能要高於公平性鎖。

       在激烈競爭的情況下,非公平鎖的性能高於公平鎖的性能的一個原因是:在恢復一個被掛的線程與該線程真正開始運行之間存在着嚴重的延遲。假設線程A持有一個鋪,並且線程B青求這個鎖。由於這個鎖已被線程A持有,因此B將被掛起。當A釋放鎖時,B將被喚醒,因北會再次嘗試獲取鎖。 與此同時,如果C也請求這個鎖,那麼C很可能會在B被完全嗅醒之前獲得、使用以及釋放這個鎖。這樣的情況是一種“雙贏”的局面:B獲得鎖的時刻並沒有推遲,C更早的獲得鎖,並且吞吐量也獲得了提高。

13.4 在synchronized和ReentrantLock之間進行選擇

        在一些內置鎖無法滿足需求的情況下,ReentrantLock可以作爲一種高級工具。當需要一些高級功能時才應該使用ReentrantLock,這些功能包括:可定時的、可輪詢的與可中斷的鎖獲取操作,公平隊列已經非塊結構的鎖。否則,還是應該優先使用synchronized。

13.5 讀寫鎖

當訪問以讀操作爲主的數據結構時,它能提高程序的可伸縮性。

        ReadWriteLock lock = new ReentrantReadWriteLock();
        Lock writeLock = lock.writeLock();
        Lock readLock = lock.readLock();

        writeLock.lock();
        try {
            
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            writeLock.unlock();
        }

第14章 構建自定義的同步工具 

14.1 狀態依賴的管理

通常,如果線程在休眠或者被阻塞時持有一個鎖,那麼這通常是一種不好的做法,因爲只要線程不釋放這個鎖,有些條件就永遠無法成真。

“條件隊列”這個名字來源:它使得一組線程(稱之爲等待線程的集合)能夠通過某種方式來等待特定的條件變爲真。傳統隊列的元素是一個個數據,而與之不同的是,條件隊列中的元素是一個個正在等待相關條件的線程。

14.2 使用條件隊列

14.2.1 條件謂語

條件謂語是使某個操作稱爲狀態依賴操作的前提條件。在阻塞隊列中,只有當隊列不爲空 時,take方法才能執行,否則必須等待。對take方法來說,它的條件謂詞就是“隊列不空”。

每一次wait調用都會隱式的域特定的條件謂語關聯起來。當調用某個特定條件謂詞的wait時,調用者必須已經持有與條件隊列相關的鎖,並且這個鎖必須保護這構成條件謂詞的狀態變量。

14.2.2 過早喚醒

當使用條件等特時(例如Object.wait或Condition.await):

  • 通常都有一個條件謂詞一 包括一 些對象狀態的測試,線程在執行前必須首先通過這些測試。
  • 在調用wait之前測試條件謂詞,並且從wait中返回時再次進行測試。
  • 在一個循環中調用wait。
  • 確保使用與條件隊列相關的鎖來保護構成條件謂詞的各個狀態變量。
  • 當調用wait、 notify或notifyAll等方法時,一定要持有與條件隊列相關的鎖。
  • 在檢查條件謂詞之後以及開始執行相應的操作之前,不要釋放鎖。
     

14.2.3 丟失的信號

只有同時滿足以下兩個條件時,才能使用單一的notify而不是notifyAll:

所有等待線程的類型相同。只有一個條件謂語與條件隊列相關,並且每個線程在從wait返回後將執行相同的操作。

單進單出。 在條件變量上每次通知,最多隻能喚醒一個線程來執行。

14.3 顯式的Condition對象

在Condition對象中,與wait、notify和notifyAll方法對應的分別是await、signal和signalAll。但是,Condition對Object進行可擴展,因此也包含wait、notify和notifyAll方法。一定要使用正確的版本。

14.5 AbstractQueuedSynchronizer(AQS)(☆☆☆☆☆)

很重要,但是書中將的很粗略

14.6 java.util.concurrent同步器類中的AQS(☆☆☆☆☆)

ReentrantLock

Semaphore與CountDownLatch

FutureTask

ReentrantReadWriteLock

第15章 原子變量與非阻塞同步機制

15.1 鎖的劣勢

1)如果有多個線程同時請求鎖,那麼一些線程將被掛起並且稍後恢復運行。當線程恢復時,必須等待其他線程執行完他們的時間片以後,才能被調度使用。在掛起和恢復線程等過程中存在着很大的開銷,並且通常存在着較長時間的中斷。

2)當一個線程正在等待鎖時,它不能做任何事情。如果一個持有鎖的線程被延遲執行(例如發生了缺頁錯誤、調度延遲等),那麼所有需要這個鎖的線程都無法執行下去。

3)如果被阻塞的線程的優先級較高,而持有鎖的線程的優先級較低,那麼這將是一個嚴重的問題-----優先級反轉。

15.2 硬件對併發的支持

CAS的主要缺點:使調用者處理競爭問題(通過重試、回退、放棄),而在鎖中能自動處理競爭問題(線程在獲得鎖之前一直阻塞)。

在大多數處理器上,在無競爭的鎖獲取和釋放的“快速代碼路徑”上的開銷,大約是CAS開銷的兩倍。

15.3 原子變量類

AtomicInteger、AtomicLong、AtomicReference、AtomicReferenceFieldUpdater、AtomicStampedReference、AtomicMarkableReference

鎖與原子變量在不同競爭程度上的性能差異很好得說明了各自的優勢和劣勢。在中低程度的競爭下,原子變量能提供更高的可伸縮性。在高強度的競爭下,鎖能幹有效的避免競爭。

15.4 非阻塞算法

創建非阻塞算法的關鍵在於:找出如何將原子修改的範圍縮小到單個變量上,同時還要維護數據的一致性。

第16章 Java內存模型(JMM)

此內容參考《深入理解java虛擬機》

Java內存模型(Java Memory Model ,JMM)就是一種符合內存模型規範的,屏蔽了各種硬件和操作系統的訪問差異的,保證了Java程序在各種平臺下對內存的訪問都能得到一致效果的機制及規範。目的是解決由於多線程通過共享內存進行通信時,存在的原子性、可見性(緩存一致性)以及有序性問題。

原子性

線程是CPU調度的基本單位。CPU有時間片的概念,會根據不同的調度算法進行線程調度。所以在多線程場景下,就會發生原子性問題。因爲線程在執行一個讀改寫操作時,在執行完讀改之後,時間片耗完,就會被要求放棄CPU,並等待重新調度。這種情況下,讀改寫就不是一個原子操作。即存在原子性問題。

緩存一致性(可見性)

在多核CPU,多線程的場景中,每個核都至少有一個L1 緩存。多個線程訪問進程中的某個共享內存,且這多個線程分別在不同的核心上執行,則每個核心都會在各自的caehe中保留一份共享內存的緩衝。由於多核是可以並行的,可能會出現多個線程同時寫各自的緩存的情況,而各自的cache之間的數據就有可能不同。

在CPU和主存之間增加緩存,在多線程場景下就可能存在緩存一致性問題,也就是說,在多核CPU中,每個核的自己的緩存中,關於同一個數據的緩存內容可能不一致。

有序性

除了引入了時間片以外,由於處理器優化和指令重排等,CPU還可能對輸入代碼進行亂序執行,比如load->add->save 有可能被優化成load->save->add 。這就是有序性問題。

 

 

Java內存模型規定了所有的變量都存儲在主內存中,每條線程還有自己的工作內存,線程的工作內存中保存了該線程中是用到的變量的主內存副本拷貝,線程對變量的所有操作都必須在工作內存中進行,而不能直接讀寫主內存。不同的線程之間也無法直接訪問對方工作內存中的變量,線程間變量的傳遞均需要自己的工作內存和主存之間進行數據同步進行。
 

總結

1)總結的不是很到位,有些沒看懂就省略了。

2)學習知識是一個潛移默化的過程,學完後也許看不出成果。

3)年輕人的世界沒有容易的,每天進步,足矣。

參考

《java併發編程實戰》

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