Java併發編程實戰-多線程和鎖

一.概念

進程和線程

1.什麼是進程? 進程簡單來說就是系統中一個應用程序,進程是程序的基本實體,是系統進行資源和分配調度的一個獨立單位。
2.什麼是線程? 線程是進程的一個實體(執行單元), 是CPU調度和分配的基本單位,比進程小。

進程和線程的區別?

1.一個進程可以有多個線程(至少一個),一個線程只能屬於一個線程
2.同一進程內的線程共享進程的資源
3.線程是CPU調度的單位,不擁有線程的資源,進程是擁有資源的基本單位。

多進程

在操作系統上能運行多個程序,系統爲每個進程分配資源,如果需要的話進程需要相互通信,這就需要進程的通信。
進程的通信分爲
socket套接字
信號
信號量
共享內存
管道

多線程

現在大部分操作系統都是用線程做基本的調度單位。

併發

多個程序同時執行。
作用:提高資源利用率,讓資源的使用更公平,開發更容易。

要考慮的問題

安全性:線程交替運行同時操作會存在安全性的問題。
活躍性:可能會產生死鎖,飢餓,以及活鎖。
性能: 線程的上下文切換會造成極大開銷。引入同步機制的清華下,往往會抑制編譯器的不斷優化。

二.線程安全性

要編寫線程安全的代碼,其核心在於對狀態訪問(例如共享可變的狀態變量)的操作管理。

概念

多個線程訪問某個類,無論調度方式和交替運行,都能保證正確的行爲。

競態條件

不正確的執行時序得到不正確的結果。

三.對象的共享

可見性

通常我們不能確保執行讀操作的線程都實時的看到其他線程寫入的值,所以爲了確保多個線程對內存的寫入操作,必須保證可見性。

volatile

說到可見性就不得不說volatile 這個關鍵字,當然加鎖同步的方式也能達到內存可見性的目的
volatile是Java提供的一種弱於鎖的同步機制,把變量聲明爲volatile類型後,編譯器和運行時都能注意到這個變量是共享的,都不會使這個變量的操作進行“重排序”。並且不會緩存在寄存器中。但是volatile不能保證原子性,如果想讓操作保證原子性需要加鎖。

線程封閉(ThreadLocal)

當然訪問共享數據時需要同步,但是還有一種方式避免同步那就是不共享數據。
volatile就存在了一種線程封閉,是一種內存屏障。還有一種更規範的方法那就是ThreadLocal。
ThreadLocal是線程的局部變量隨着線程生滅。

Final

不變的對象一定是線程安全的。
final修飾的變量會在編譯期就獲取到值並且不可變。

四.Java內存模型(JMM)

在上一節中我們說到了volatile和ThreadLocal,這一節就說一下Java的內存模型
JVM是Java虛擬機:方法區,虛擬機棧,本地方法棧,堆,程序計數器。其中堆和方法區是共享的其他是私有的。

JMM是Java的內存模型
調用棧和本地變量存放在線程棧上,對象存放在堆上。

在這裏插入圖片描述
如圖,每一個線程都有一個自己的工作空間(也叫棧空間),線程共享主內存當中的內容,線程對共享變量的讀寫操作都會在線程的私有內存中拷貝一個共享變量副本,操作完成回寫到主存中。

happenBefore

JMM對所有操作定義了一個偏序關係也就是happenBefore。但是同步操作,volatile和鎖機制都是全序關係。
AQS就是說明了如何使用藉助這種技能。AQS自己維護了一個同步器狀態的證書,Future用這個整數保存用戶的狀態。
缺少Happens-before關係時 就可能出現重排許問題

🏁Unsafe


在解讀CAS之前先說一個類Unsafe

首先unsafe可以直接操作內存,慣例看下源碼

public final class Unsafe {
  // 單例對象
  private static final Unsafe theUnsafe;

  private Unsafe() {
  }
  @CallerSensitive
  public static Unsafe getUnsafe() {
    Class var0 = Reflection.getCallerClass();
    // 僅在引導類加載器`BootstrapClassLoader`加載時才合法
    if(!VM.isSystemDomainLoader(var0.getClassLoader())) {    
      throw new SecurityException("Unsafe");
    } else {
      return theUnsafe;
    }
  }
}

通常,我們在Java中創建的對象都處於堆內內存(heap)中,堆內內存是由JVM所管控的Java進程內存,JVM會採用垃圾回收機制統一管理堆內存。堆外內存,存在於JVM管控之外的內存區域,Java中對堆外內存的操作,依賴於Unsafe提供的操作堆外內存的native方法。

堆外內存通常在通信中做緩衝池,Netty之類的NIO框架。

線程調度
Java鎖和同步器框架的核心類AbstractQueuedSynchronizer,就是通過調用LockSupport.park()和LockSupport.unpark()實現線程的阻塞和喚醒的,而LockSupport的park、unpark方法實際是調用Unsafe的park、unpark方式來實現。


🏁CAS

CAS 全稱是 compare and swap,即比較並交換,它是一種原子操作,同時 CAS 是一種樂觀機制。

CAS 的思想很簡單:三個參數,一個當前內存值 V、舊的預期值 A、即將更新的值 B,當且僅當預期值 A 和內存值 V 相同時,將內存值修改爲 B 並返回 true,否則什麼都不做,並返回 false。
一般在源碼中,cas操作一般都伴隨着unsafe(硬件級別的從內存拿值的東西)。unsafe的一段代碼解釋下CAS。

public final int getAndAddInt(Object var1, long var2, int var4) {
  int var5;
  do {
    var5 = this.getIntVolatile(var1, var2);
  } while(!this.compareAndSwapInt(var1, var2, var5, var5 + var4));// 自旋
  return var5;
}

compareAndSwapInt 就是實現 CAS 的核心方法,其原理是如果 var1 中的 value 值和 var5 相等,就證明沒有其他線程改變過這個變量,那麼就把 value 值更新爲 var5 + var4,其中 var4 是更新的增量值;反之如果沒有更新,那麼 CAS 就一直採用自旋的方式繼續進行操作(其實就是個 while 循環),這一步也是一個原子操作。

ABA 問題

CAS 看起來很爽,但它也有缺點,那就是“ABA”問題。
例如線程 1 從內存位置 V 取出 A,這時候線程 2 也從內存位置 V 取出 A,此時線程 1 處於掛起狀態,線程 2 將位置 V 的值改成 B,最後再改成 A,這時候線程 1 再執行,發現位置 V 的值沒有變化,儘管線程 1 也更改成功了,但是不代表這個過程就是沒有問題的。

五. 線程安全的對象

我們常用的線程安全的對象

concurrent下的Atomic :非阻塞方法來實現併發控制。

看源碼

public class AtomicInteger extends Number implements java.io.Serializable {
    private static final long serialVersionUID = 6214790243416807050L;
//unsafe	 就是java提供的獲得對象內存地址訪問的類,作用就是CAS(比較並交換) 
    // setup to use Unsafe.compareAndSwapInt for updates
    private static final Unsafe unsafe = Unsafe.getUnsafe();
    private static final long valueOffset;

    static {
        try {
            valueOffset = unsafe.objectFieldOffset
                (AtomicInteger.class.getDeclaredField("value"));
        } catch (Exception ex) { throw new Error(ex); }
    }

    private volatile int value;

...
    /**
     * Atomically sets to the given value and returns the old value.
     * @param newValue the new value
     * @return the previous value
     */  我們可以看到是採用Cas的方式
    public final int getAndSet(int newValue) {
        return unsafe.getAndSetInt(this, valueOffset, newValue);
    }

    /**
     * Atomically sets the value to the given updated value
     * if the current value {@code ==} the expected value.
     *
     * @param expect the expected value
     * @param update the new value
     * @return {@code true} if successful. False return indicates that
     * the actual value was not equal to the expected value.
     */
    public final boolean compareAndSet(int expect, int update) {
        return unsafe.compareAndSwapInt(this, valueOffset, expect, update);
    }
}
    /**

同步容器類Vector和HashTable

add()和put()方法用synchronized關鍵字修飾,

BlockingQueue 和BlockingDeque

BlockingQueue 使用了顯式鎖Lock
BlockingDeque 是雙端阻塞隊列

    public void put(E e) throws InterruptedException {
        checkNotNull(e);
        final ReentrantLock lock = this.lock;
        lock.lockInterruptibly();
        try {
            while (count == items.length)
                notFull.await();
            enqueue(e);
        } finally {
            lock.unlock();
        }
    }
       public E takeFirst() throws InterruptedException {
        final ReentrantLock lock = this.lock;
        lock.lock();
        try {
            E x;
            while ( (x = unlinkFirst()) == null)
                notEmpty.await();
            return x;
        } finally {
            lock.unlock();
        }
    }

concurrentHashMap

在1.8中:

 final V putVal(K key, V value, boolean onlyIfAbsent) {
        if (key == null || value == null) throw new NullPointerException();
        int hash = spread(key.hashCode());//計算hash
        int binCount = 0;
        for (Node<K,V>[] tab = table;;) {
            Node<K,V> f; int n, i, fh;
            if (tab == null || (n = tab.length) == 0)
                tab = initTable();
            else if ((f = tabAt(tab, i = (n - 1) & hash)) == null) {
                if (casTabAt(tab, i, null,
                             new Node<K,V>(hash, key, value, null)))
                    break;                   // no lock when adding to empty bin
            }
            else if ((fh = f.hash) == MOVED)
                tab = helpTransfer(tab, f);
            else {
                V oldVal = null;
                synchronized (f) {
                    if (tabAt(tab, i) == f) {
                        if (fh >= 0) {
                            binCount = 1;
                            for (Node<K,V> e = f;; ++binCount) {
                                K ek;
                                if (e.hash == hash &&
                                    ((ek = e.key) == key ||
                                     (ek != null && key.equals(ek)))) {
                                    oldVal = e.val;
                                    if (!onlyIfAbsent)
                                        e.val = value;
                                    break;
                                }
                                Node<K,V> pred = e;
                                if ((e = e.next) == null) {
                                    pred.next = new Node<K,V>(hash, key,
                                                              value, null);
                                    break;
                                }
                            }
                        }
                        else if (f instanceof TreeBin) {
                            Node<K,V> p;
                            binCount = 2;
                            if ((p = ((TreeBin<K,V>)f).putTreeVal(hash, key,
                                                           value)) != null) {
                                oldVal = p.val;
                                if (!onlyIfAbsent)
                                    p.val = value;
                            }
                        }
                    }
                }
                if (binCount != 0) {
                    if (binCount >= TREEIFY_THRESHOLD)
                        treeifyBin(tab, i);
                    if (oldVal != null)
                        return oldVal;
                    break;
                }
            }
        }
        addCount(1L, binCount);
        return null;

從源碼可以看出,1.8中的concurrentHashMap使用了synchronized和CAS的方式進行的,擴容使用了Cas

六.閉鎖

CountDownLatch

 private static final class Sync extends AbstractQueuedSynchronizer 

從源碼上看CountDownLatch繼承了AQS,在AtQS中維護了一個volatile類型的整數state,volatile可以保證多線程環境下該變量的修改對每個線程都可見,並且由於該屬性爲整型,因而對該變量的修改也是原子的。創建一個CountDownLatch對象時,所傳入的整數n就會賦值給state屬性,當countDown()方法調用時,該線程就會嘗試對state減一,而調用await()方法時,當前線程就會判斷state屬性是否爲0,如果爲0,則繼續往下執行,如果不爲0,則使當前線程進入等待狀態,直到某個線程將state屬性置爲0,其就會喚醒在await()方法中等待的線程。

FutureTasK

在這裏插入圖片描述
源碼上來看

    private static final int NEW          = 0;
    private static final int COMPLETING   = 1;
    private static final int NORMAL       = 2;
    private static final int EXCEPTIONAL  = 3;
    private static final int CANCELLED    = 4;
    private static final int INTERRUPTING = 5;
    private static final int INTERRUPTED  = 6;
    
protected void set(V v) {
    if (UNSAFE.compareAndSwapInt(this, stateOffset, NEW, COMPLETING)) {
        outcome = v;
        UNSAFE.putOrderedInt(this, stateOffset, NORMAL); // final state
        finishCompletion();
    }
}

從源碼看出也是CAS操作,unsafe是Jvm定義的獲取內存中引用值的對象。

信號量Semaphore

   final int nonfairTryAcquireShared(int acquires) {
            for (;;) {
                int available = getState();
                int remaining = available - acquires;
                if (remaining < 0 ||
                    compareAndSetState(available, remaining))
                    return remaining;
            }
        }

輪訓CAS,

七.終端和取消任務

1.使用volatile類型的變量保存取消狀態,但是當調用的時候阻塞了 ,任務可能檢測不到取消標誌,永遠不會結束。

volatile boolean cancelled;
。。。
public void cancel(){
    cancelled = true;
};

2.使用interrupt 但是sleep和wait不一定能檢測到清除中斷狀態拋出異常。
通常終端是取消的最合理方式。

public Class Thread{
public boolean interrupted(){}
public static boolean interrupted(){}
public void boolean interrupted(){}
}

3.Future.cancel

生產者消費者關閉

1.ExecutorService shutdown和shutdownNow
2.毒丸 --也就是一個對象,得到這個對象時候停止

八.線程池

前文說到線程的創建是非常消耗資源的一件事,所以我們要使用線程池,它還能將任務提交和執行解耦。
飢餓死鎖:線程一直等待線程池的任務完成。剛巧線程池中的完成需要等待線程的返回值。
線程池構造屬性:

publicThreadPoolExecutor(int corePoolSize,//線程池基本大小
你太maximumPoosize,//最大大小
ThreadFactory,線程工廠
BlockingWueue,阻塞隊列
                                           )

九.鎖

死鎖:多線程鎖相互等待。單線程重複申請鎖。
鎖順序死鎖,循環死鎖。

解決

1.定時鎖
2.中斷或者回滾

Synchronzied

對象鎖,互斥性,和可見性,可以修飾方法類代碼塊
鎖的是對象,在對象頭中插入鎖狀態

  1. 根據修飾對象分類
修飾代碼塊
   synchronized(this|object) {}
   synchronized(類.class) {}
   
修飾方法
   修飾非靜態方法
   修飾靜態方法
獲取對象鎖
synchronized(this|object) {}
修飾非靜態方法

獲取類鎖
synchronized(類.class) {}
修飾靜態方法

synchronized關鍵字不能繼承。可重入
對於父類中的 synchronized 修飾方法,子類在覆蓋該方法時,默認情況下不是同步的,必須顯示的使用 synchronized 關鍵字修飾才行。
在定義接口方法時不能使用synchronized關鍵字。
構造方法不能使用synchronized關鍵字,但可以使用synchronized代碼塊來進行同步。
不同的線程可以訪問一個帶有synchronized方法的類其他方法
不同的線程可以訪問一個帶有synchronized代碼的類其他操作

LOCK

顯式鎖,可以定時可以手動加鎖解鎖。

參考資料
Java魔法類:Unsafe應用解析
深入瞭解Java虛擬機
Java併發編程實戰

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