JAVA-併發編程筆記

摘要

這篇文章主要記錄了JAVA併發編程中使用的主要類(包括它們的實現原理)和麪試過程會被問到的一些概念。

什麼是併發

  1. 併發就是指程序同時處理多個任務的能力。
  2. 併發編程的根源在多個任務情況下對訪問資源的有效控制。

線程的幾種狀態

  1. NEW:創建了一個線程對象,還未調用其start方法
  2. RUNNABLE:處於運行狀態的線程指的是正在虛擬機中執行的線程。他們可能等待操作系統的其他資源(處理器)
  3. BLOCKED:等待獲取synchronized
  4. WAITING:Object.wait/Thread.join/LockSupport.park.處於改狀態的線程需要等待其他線程執行一些特定的動作 (Object.notify()/Object.notifyAll())
  5. TIMED_WAITING: Thread.sleep/Object.wait(long)/Thread.join(long)/ LockSupport.parkNano /LockSupport.parkUntil.
  6. TERMINATED:執行完畢的線程.

鎖的分類

AbstractQueuedSynchronizer(隊列同步器)

爲實現依賴於FIFO等待隊列的阻塞鎖和其他相關的同步組件提供基礎框架。它裏面有一個state變量表示同步狀態。通過內置的FIFO隊列(雙向鏈表)完成資源獲取線程的排隊工作。

public abstract class AbstractQueuedSynchronizer extends AbstractOwnableSynchronizer implements java.io.Serializable {
    //等待隊列的頭節點
    private transient volatile Node head;//一個空節點
    //等待隊列的尾節點
    private transient volatile Node tail;//獲取資源失敗的線程都是插入到隊尾,tail節點始終指向隊列中的最後一個元素
    //同步狀態
    private volatile int state;
}

static final class Node {
        static final Node SHARED = new Node();
        static final Node EXCLUSIVE = null;
        static final int CANCELLED =  1;
        static final int SIGNAL    = -1;
        static final int CONDITION = -2;
        static final int PROPAGATE = -3;
        volatile int waitStatus;
        volatile Node prev;
        volatile Node next;
        volatile Thread thread;
        Node nextWaiter;
    }

ReentrantLock(可重入鎖)實現分析

它基於AQS實現的可重入鎖,提供了公平(默認)與非公平兩種鎖。還提供了:鎖的超時等待、可中斷的鎖等待、公平性、以及實現非塊結構的加鎖、Condition。
公平與非公平鎖區別??
公平鎖中, 線程嚴格按照先進先出(FIFO)的順序 獲取鎖資源。
非公平鎖中, 擁有鎖的線程在釋放鎖資源的時候, 當前嘗試獲取鎖資源的線程可以和等待隊列中的第一個線程競爭鎖資源, 這就是ReentrantLcok中非公平鎖的含義; 但是已經進入等待隊列的線程, 依然是按照先進先出的順序獲取鎖資源。

使用:

     Lock lock = new ReentrantLock();
    Condition condition = lock.newCondition();
    lock.lock();
    try {
      while(條件判斷表達式) {
          condition.wait();
      }
     // 處理邏輯
    } finally {
        lock.unlock();
    }

類的關係結構:
在這裏插入圖片描述

非公平鎖的實現:

static final class NonfairSync extends Sync {
        final void lock() {
            if (compareAndSetState(0, 1))//當前鎖未被佔用
                setExclusiveOwnerThread(Thread.currentThread());//設置持有鎖的線程爲當前線程
            else
                acquire(1);
        }

        protected final boolean tryAcquire(int acquires) {
            return nonfairTryAcquire(acquires);
        }
    }
    
 abstract static class Sync extends AbstractQueuedSynchronizer {
        final boolean nonfairTryAcquire(int acquires) {
            final Thread current = Thread.currentThread();
            int c = getState();
            if (c == 0) { //鎖被釋放

              //公平鎖和非公平鎖的主要區別在這裏
                if (compareAndSetState(0, acquires)) {//獲取鎖成功
                    setExclusiveOwnerThread(current);
                    return true;
                }
            }
            else if (current == getExclusiveOwnerThread()) {//持有鎖的線程再次獲去鎖
                int nextc = c + acquires;
                if (nextc < 0) // overflow 2147483647
                    throw new Error("Maximum lock count exceeded");
                setState(nextc);
                return true;
            }
            return false;
        }
    }

class AbstractQueuedSynchronizer {
        public final void acquire(int arg) {
            if (!tryAcquire(arg) &&//嘗試獲取鎖
                acquireQueued(addWaiter(Node.EXCLUSIVE), arg))  //將當前線程加入到等待隊列,線程被阻塞
                selfInterrupt();
        }
}

公平鎖的實現:

  static final class FairSync extends Sync {
    final void lock() {
        acquire(1);
    }
    
    protected final boolean tryAcquire(int acquires) {
        final Thread current = Thread.currentThread();
        int c = getState();
        if (c == 0) {
            if (!hasQueuedPredecessors() && //判斷等待隊列中是否還有其他線程在等待資源
                compareAndSetState(0, acquires)) {
                setExclusiveOwnerThread(current);
                return true;
            }
        }
        else if (current == getExclusiveOwnerThread()) {
            int nextc = c + acquires;
            if (nextc < 0)
                throw new Error("Maximum lock count exceeded");
            setState(nextc);
            return true;
        }
        return false;
    }
}

public final boolean hasQueuedPredecessors() {
    // The correctness of this depends on head being initialized
    // before tail and on head.next being accurate if the current
    // thread is first in queue.
    Node t = tail; // Read fields in reverse initialization order
    Node h = head;
    Node s;
    return h != t &&
        ((s = h.next) == null || s.thread != Thread.currentThread());
}    

1 h == t 隊列爲空
2 h != t && (s = h.next) == null 意味着有一個線程嘗試獲取鎖失敗後,正在進行入隊操作,而且 在AQS的enq()方法中, head=tail方法正好還沒執行到, 此時隊列被認爲不空, 返回true
3 h != t && ( s.thread != Thread.currentThread()) 隊列中第一個節點不是當前線程,即還有其他線程在排隊等待資源。

    for (;;) {
        Node t = tail;
        if (t == null) { // Must initialize
            if (compareAndSetHead(new Node()))
                tail = head;
        } else {
            node.prev = t;
            if (compareAndSetTail(t, node)) {
                t.next = node;
                return t;
            }
        }
    }
}

tryLock():

不管Reentrant使用公平鎖(FairLock)還是非公平鎖(NonFairLock), ReentrantLock類的tryLock()方法都會調用Sync類的nonfairTryAcquire()方法,採取非公平的競爭策略.

線程池使用及實現原理

爲什麼使用線程池:

降低資源消耗:通過複用已存在的線程降低線程關閉的次數,儘可能降低系統性能損耗。

提高響應速度:通過複用線程省去創建線程的過程,因此整體上提升了系統的響應速度;

實現原理:

其實java線程池的實現原理很簡單,說白了就是一個線程集合 workers(HashSet) 和一個任務阻塞隊列 workQueue(BlockingQueue)。當用戶向線程池提交一個任務時,線程池會先將任務放入workQueue中。workers 中的線程會不斷的從 workQueue 中獲取任務然後執行。當workQueue中沒有任務的時候,worker就會阻塞,直到隊列中有任務了就取出來繼續執行。
在這裏插入圖片描述
下面就是提交任務後,線程池內部的執行流程:
在這裏插入圖片描述submit: 接收Runnable/Callable參數,返回 Future. submit會將任務包裝成FutureTask(RunnableFuture)然後調用 execute.在任務中發生異常,只有通過 Futrue的get操作纔會拋出異常。
execute: 接受 Runnable參數, 在任務中發生異常,會直接在執行拋出異常導致線程退出。

Java基於ThreadPoolExecutor提供了幾種不同類型的線程池,先介紹線程池的幾個主要參數的作用:
ThreadPoolExecutor(int corePoolSize,
int maximumPoolSize,
long keepAliveTime,
TimeUnit unit,
BlockingQueue workQueue,
ThreadFactory threadFactory,
RejectedExecutionHandler handler);

corePoolSize:核心線程的數量
maximumPoolSize:線程池的最大線程數。當workQueue滿了,不能添加任務的時候,這個參數纔會生效
keepAliveTime:空閒線程存活時間。如果當前線程池的線程個數已經超過了corePoolSize,並且線程空閒時間超過了keepAliveTime的話,就會將這些空閒線程銷燬.默認爲1分鐘.
什麼時候退出? 從隊列獲取任務的時候,首先會根據當前的線程數判斷要不要設置超時間.如果超過這個時間,隊列還沒有任務進來,就會返回null,線程退出。

workQueue:任務隊列
threadFactory:創建線程的工程類。可以通過指定線程工廠爲每個創建出來的線程設置更有意義的名字,如果出現併發問題,也方便查找問題原因。
handler:飽和策略。當workQueue已滿,且線程數超過maximumPoolSize。
AbortPolicy: 直接拒絕所提交的任務,並拋出RejectedExecutionException異常;
CallerRunsPolicy:用調用者所在的線程來執行任務;
DiscardPolicy:不處理直接丟棄掉任務;
DiscardOldestPolicy:丟棄掉阻塞隊列中存放時間最久的任務,執行當前任務

java提供幾種不同類型的線程池:
==newCachedThreadPool: ==
ThreadPoolExecutor(0, Integer.MAX_VALUE,60L, TimeUnit.SECONDS,new SynchronousQueue());
創建一個可緩存線程池。核心線程數爲0且沒有最大線程數的限制。任務隊列採用的是 SynchronousQueue 同步隊列:屬於線程安全的BlockingQueue的一種,此隊列設計的理念類似於"單工模式",對於每個put/offer操作,必須等待一個take/poll操作(否則返回false)。類似於我們的現實生活中的"火把傳遞".因爲這種策略,最終導致隊列中並沒有一個真正的元素;
== 因此當提交任務的時候,如果沒有空閒線程來處理則會創建一個新的線程來處理該任務,這樣可能會導致創建大量的線程從而使系統資源耗光. ==

newFixedThreadPool:
創建一個固定工作線程數量的線程池.
ThreadPoolExecutor(nThreads, nThreads,0L, TimeUnit.MILLISECONDS,new LinkedBlockingQueue()).
核心線程數和最大線程數相等。採用的是無界隊列。

newSingleThreadExecutor:
創建一個單線程化的Executor
ThreadPoolExecutor(1, 1, 0L, TimeUnit.MILLISECONDS,new LinkedBlockingQueue());
核心線程數和最大線程數都是1,所以保證了所提交的任務都是串行執行的。採用的是無界隊列。

newScheduleThreadPool:
支持定時的以及週期性的任務的線程池,線程數是固定的(創建時指定);
ThreadPoolExecutor(corePoolSize, Integer.MAX_VALUE, 0, TimeUntil.NANOSECONDS, new DelayedWorkQueue());
採用DelayedWorkQueue來做任務延時;== 也是無界隊列,因此除了核心線程數,不會創建非核心線程==

線程池的關閉
關閉線程池,可以通過shutdown和shutdownNow這兩個方法。它們的原理都是遍歷線程池中所有的線程,然後依次中斷線程。shutdown和shutdownNow還是有不一樣的地方:
shutdownNow首先將線程池的狀態設置爲STOP,然後嘗試停止所有的正在執行和未執行任務的線程,並返回等待執行的任務的列表;
shutdown只是將線程池的狀態設置爲SHUTDOWN狀態,然後中斷所有沒有正在執行任務的線程。
可以看出shutdown方法會讓正在執行的任務繼續執行完,而shutdownNow會直接中斷正在執行的任務。調用了這兩個方法的任意一個,isShutdown方法都會返回true,當所有的線程都關閉成功,才表示線程池成功關閉,這時調用isTerminated方法纔會返回true。

ThreadLocal

ThreadLocal是一個本地線程副本變量工具類.爲變量在每個線程中都創建一個副本,這樣每個線程都可以訪問自己內部的副本變量,各個線程之間互不干擾。

使用場景

One possible (and common) use is when you have some object that is not thread-safe, but you want to avoid synchronizing access to that object (I’m looking at you, SimpleDateFormat). Instead, give each thread its own instance of the object.
如果程序中有一些非線程安全的對象,但你有不希望通過同步的方式訪問這些對象(SimpleDateFormat)。取而代之,給每個線程創建一個副本對象。比如:數據庫連接管理,線程會話管理等場景

實現原理

從線程的角度來看,每個線程的內部都有一個ThreadLocalMap的的實例(相當於線程的局部變量空間,存儲着線程各自的私有數據)。
ThreadLocalMap 內部使用 Entry數組,默認大小INITIAL_CAPACITY(16)。
Entry 包含線程本地對象(key)和線程的變量副本(value)。
hreshold:table大小的2/3,當size >= threshold時,遍歷table並刪除key爲null的元素,如果刪除後size >= threshold*3/4時,需要對table進行擴容.

內部數據結構

在這裏插入圖片描述

get()

在這裏插入圖片描述

set()

在這裏插入圖片描述

Tips

Hash衝突怎麼解決:
和HashMap的最大的不同在於,ThreadLocalMap結構非常簡單,沒有next引用,也就是說ThreadLocalMap中解決Hash衝突的方式並非鏈表的方式,而是採用線性探測的方式,所謂線性探測,就是根據初始key的hashcode值確定元素在table數組中的位置,如果發現這個位置上已經有其他key值的元素被佔用,則利用固定的算法尋找一定步長的下個位置,依次判斷,直至找到能夠存放的位置。
ThreadLocalMap解決Hash衝突的方式就是簡單的步長加1或減1,尋找下一個相鄰的位置。
table擴容
如果table中的元素數量達到閾值threshold的3/4,會進行擴容操作,

ThreadLocalMap的問題
由於ThreadLocalMap的key是弱引用,而Value是強引用。這就導致了一個問題,ThreadLocal在沒有外部對象強引用時,發生GC時弱引用Key會被回收,而Value不會回收,如果創建ThreadLocal的線程一直持續運行,那麼這個Entry對象中的value就有可能一直得不到回收,發生內存泄露。

如何避免泄漏
既然Key是弱引用,那麼我們要做的事,就是在調用ThreadLocal的get()、set()方法時完成後再調用remove方法,將Entry節點和Map的引用關係移除,這樣整個Entry對象在GC Roots分析後就變成不可達了,下次GC的時候就可以被回收。
如果使用ThreadLocal的set方法之後,沒有顯示的調用remove方法,就有可能發生內存泄露,所以養成良好的編程習慣十分重要,使用完ThreadLocal之後,記得調用remove方法。

[Runnable/Callable/FutureTask(繼承自RunnableFuture)]

Callable callable = () -> {
try {
return “callable”;
} catch (Exception ex) {
ex.printStackTrace();
}
return “error”;
};

FutureTask futureTask = new FutureTask<>(callable);
new Thread(futureTask).start();
System.out.println(System.currentTimeMillis() + " " + Thread.currentThread().getName() + " " + futureTask.get());

Future future = threadPoolExecutor.submit(callable);
System.out.println(System.currentTimeMillis() + " " + Thread.currentThread().getName() + " " + future.get());

FutureTask futureTask2 = new FutureTask<>(callable);
threadPoolExecutor.execute(futureTask2);

鎖降級:

寫線程獲取寫入鎖後可以獲取讀取鎖,然後釋放寫入鎖,這樣就從寫入鎖變成讀取鎖,從而實現鎖降級的特性。

*** 鎖降級之後,寫鎖並不會直接降級成讀鎖,不會隨着讀鎖的釋放而釋放,因此需要顯示地釋放寫鎖

** 不存在鎖升級

應用:用於對數據比較敏感,需要在對數據修改之後,獲取到修改後的值,並進行接下來的其他操作。private ReentrantReadWriteLock reentrantReadWriteLock = new ReentrantReadWriteLock();
private ReentrantReadWriteLock.ReadLock readLock = reentrantReadWriteLock.readLock();
private ReentrantReadWriteLock.WriteLock writeLock = reentrantReadWriteLock.writeLock();
writeLock.lock();
readLock.lock();
System.out.println(“get read lock”);
writeLock.unlock();
readLock.unlock();

[StampedLock]

[線程間通信]
wait/notify/notifyAll
管道 PipedInputStream / PipedOutputStream
[Thread.join]
核心思想是
while(isAlive()) {
wait(0);
}

[ThreadLocal]
ThreadLocal num = ThreadLocal.withInitial(() -> 0);

[Condition ]
private Lock lock = new ReentrantLock();
private Condition notFullCondition = lock.newCondition();
private Condition notEmptyCondition = lock.newCondition();
notEmptyCondition.notifyAll();
notEmptyCondition.await();

=======原子類
AtomicBoolean AtomicInteger AtomicLong
DoubleAdder LongAdder 對Double,Long的原子更新性能進行提升
DoubleAccumulator LongAccumulator 支持自定義運算
AtomicIntegerArray AtomicLongArray AtomicReferenceArray 原子更新數組類型
AtomicIntegerFieldUpdater AtomicLongFieldUpdater 原子的更新屬性

[併發容器]
CopyOnWrite Concurrent ConcurrentBlocking
不能在 CopyOnWrite 容器的迭代操作中使用remove操作;

[併發工具類]
[] LinkedBlockingDeque blockingDeque = new LinkedBlockingDeque();
blockingDeque.add(“a”); //添加一個元素,如果添加失敗則立刻拋出異常
blockingDeque.offer(“b”);//添加一個元素,如果添加失敗則立刻返回false 上面兩個都調用 offerLast
blockingDeque.put(“c”); //添加元素,如果中間失敗則會繼續重試直到添加成功
blockingDeque.remove();//從隊頭取出元素,如果失敗則立刻拋出異常
blockingDeque.poll();//從隊頭取出元素,如果失敗則立刻返回 null
blockingDeque.take();//從隊頭取出元素,如果中間失敗則會繼續重試直到取得元素

[] Semaphore semaphore = new Semaphore(2); //控制併發數量-接口限流 基於AQS實現的
try {
semaphore.acquire();
System.out.println(System.currentTimeMillis() / 1000 + " : " + Thread.currentThread().getName() + " 開始執行");
} finally {
semaphore.release();
}

[] CyclicBarrier barrier = new CyclicBarrier(4); //一般用於一組線程相互等待至某個狀態後,然後這一組線程再一起執行-可重複
//基於 ReentrantLock 和 Condition 實現的:當 status爲非0時await 將處於阻塞狀態,當狀態爲0時則喚醒所以阻塞在Condition的線程
barrier.await();

[] CountDownLatch countDownLatch = new CountDownLatch(2); //一般用於某個線程等待多個其他線程執行完之後,它才執行:不可重複
//基於AQS實現的
countDownLatch.countDown(); //當狀態爲0時,喚醒所有處於等待隊裏的阻塞線程
System.out.println(System.currentTimeMillis() / 1000 + " : " + Thread.currentThread().getName()+ " 等待");
countDownLatch.await(); //狀態爲0則馬上返回
System.out.println(System.currentTimeMillis() / 1000 + " : " + Thread.currentThread().getName() + " 等待完畢");

[] Exchanger 線程間交換數據-必須成對出現

///
def gupao_併發編程原理():
aba 問題
CAS
CDN

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