技能提升:併發編程(五)

3.BlockingQueue(阻塞隊列)

//通過阻塞隊列實現生產者消費者模式
public class Tmall3 {
	private int count=10;//隊列的最大大小
	private BlockingQueue<Integer> blockingQueue=new ArrayBlockingQueue<>(count);
	public void push() {//生產者,當隊列達到最大值,阻塞,停止生產,直到大小小於最大值,在開始生產
		try {
			blockingQueue.put(1);
		} catch (InterruptedException e) {
			e.printStackTrace();
		}
	}
	public void take() {//消費者,當隊列中沒有值時,阻塞等待,有值就獲取
		try {
			System.out.println(blockingQueue.take());
		} catch (InterruptedException e) {
			e.printStackTrace();
		}
	}
}

源碼解析:

put()方法

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();//釋放鎖
	}
}

private void enqueue(E x) {
	final Object[] items = this.items;
	items[putIndex] = x;
	if (++putIndex == items.length)
		putIndex = 0;
	count++;
	notEmpty.signal();//喚醒
}

tack()方法

public E take() throws InterruptedException {
	final ReentrantLock lock = this.lock;
	lock.lockInterruptibly();//加鎖
	try {
		while (count == 0)//隊列元素爲0時
			notEmpty.await();//等待
		return dequeue();//取值
	} finally {
		lock.unlock();//釋放鎖
	}
}

private E dequeue() {
	final Object[] items = this.items;
	@SuppressWarnings("unchecked")
	E x = (E) items[takeIndex];
	items[takeIndex] = null;
	if (++takeIndex == items.length)
		takeIndex = 0;
	count--;
	if (itrs != null)
		itrs.elementDequeued();
	notFull.signal();//喚醒
	return x;
}

阻塞隊列常用方法:

SynchronousQueue:隊列中只有一個數據,不消費就無法插入,生產一個消費一個。

LinkedBlockingQueue:使用鏈表的阻塞隊列。

4.CopyOnWriteArraySet

HashSet線程不安全解決方法:

  • 使用Collections的同步方法
  • CopyOnWriteArraySet:Set<String> set = new CopyOnWriteArraySet<String>();

 CopyOnWriteArraySet底層:

 

二十一:線程池

1.線程池優勢

1)降低資源消耗,通過重複利用已創建的線程降低線程的創建預銷燬消耗。

2)提高響應速度,當任務到達時,任務可以不用等待線程創建就立即執行。

3)提高線程的可管理性,線程是稀缺資源,如果無限創建,不僅會消耗資源,還會降低系統穩定性,使用線程池可以進行統一分配,調優和監控,但是要做到合理使用線程池,還需要懂得其原理。

2.簡單使用

public class Demo1 {
	
	public static void main(String[] args) {
		//參數5用於存儲任務的隊列
		ThreadPoolExecutor poolExecutor = new ThreadPoolExecutor(10, 20, 10, TimeUnit.DAYS, new ArrayBlockingQueue<>(10),new ThreadPoolExecutor.CallerRunsPolicy());
		
		AtomicInteger atomicInteger = new AtomicInteger();
		for (int i = 0; i < 100; i++) {
			poolExecutor.execute(new Runnable() {
				@Override
				public void run() {
					System.out.println(Thread.currentThread().getName());
					atomicInteger.getAndIncrement();
				}
			});
		}
		poolExecutor.shutdown();
		while (Thread.activeCount()>1) {}
		System.out.println(atomicInteger.get());
		
	}
}

參數解析

  • corePoolSize:線程池核心線程數(平時保留的線程數)
  • maximumPoolSize:線程池最大線程數(當workQueue都放不下時,啓動新線程,最大線程數)
  • keepAliveTime:超出corePoolSize數量的線程的保留時間。
  • unit:keepAliveTime單位
  • workQueue:阻塞隊列,存放來不及執行的線程

    • ArrayBlockingQueue:構造函數一定要傳大小
    • LinkedBlockingQueue:構造函數不傳大小會默認爲(Integer.MAX_VALUE ),當大量請求任務時,容易造成內存耗盡。
    • SynchronousQueue:同步隊列,一個沒有存儲空間的阻塞隊列 ,將任務同步交付給工作線程。
    • PriorityBlockingQueue : 優先隊列
  • threadFactory:線程工廠
  • handler:飽和策略

    • AbortPolicy(默認):直接拋出異常
    • CallerRunsPolicy:用調用者的線程執行任務
    • DiscardOldestPolicy:拋棄隊列中最久的任務
    • DiscardPolicy:不處理,丟棄掉

執行流程:

  • 判斷當前運行的線程數量是否超過corePoolSize,如果不超過corePoolSize。就創建一個線程直接執行該任務。—— 線程池最開始是沒有線程在運行的 
  • 如果正在運行的線程數量超過或者等於corePoolSize,那麼就將該任務加入到workQueue隊列中去。 
  • 如果workQueue隊列滿了,也就是offer方法返回false的話,就檢查當前運行的線程數量是否小於maximumPoolSize,如果小於就創建一個線程直接執行該任務。 
  • 如果當前運行的線程數量是否大於等於maximumPoolSize,那麼就執行RejectedExecutionHandler來拒絕這個任務的提交。

3.原理解析

ThreadPoolExecutor的狀態變量

//線程的控制狀態:用來表示線程池的運行狀態(整形高3位)和運行的worker數量(整形低29位)
private final AtomicInteger ctl = new AtomicInteger(ctlOf(RUNNING, 0));
//29位偏移量
private static final int COUNT_BITS = Integer.SIZE - 3;
//最大容量(2^29-1)
private static final int CAPACITY   = (1 << COUNT_BITS) - 1;

//線程的運行狀態,需要3位表示,所以偏移量爲32-3=29
private static final int RUNNING    = -1 << COUNT_BITS;//接受新任務並且已處理已經進入阻塞隊列的任務
private static final int SHUTDOWN   =  0 << COUNT_BITS;//不接受新任務但是處理已經進入阻塞隊列的任務
private static final int STOP       =  1 << COUNT_BITS;//不接受新任務,不處理已經進入阻塞隊列的任務,中斷正在運行的任務
private static final int TIDYING    =  2 << COUNT_BITS;//所有任務都已終止,workerCount=0,線程轉化爲 TIDYING,準備執行terminated()方法。
private static final int TERMINATED =  3 << COUNT_BITS;//表示已執行完terminated()方法。

// Packing and unpacking ctl
private static int runStateOf(int c)     { return c & ~CAPACITY; }
private static int workerCountOf(int c)  { return c & CAPACITY; }
private static int ctlOf(int rs, int wc) { return rs | wc; }

當我們向線程池提交任務時,通常使用execute()方法,接下來就先從該方法開始分析。

public void execute(Runnable command) {
	if (command == null)
		throw new NullPointerException();
	
	int c = ctl.get();//獲得線程控制狀態
	
	//出現情況一:線程池的線程數量小於corePoolSize核心線程數量,開啓核心線程執行任務。
	if (workerCountOf(c) < corePoolSize) {
		if (addWorker(command, true))//添加Worker,線程池會將每一個任務包裝爲一個Worker對象
			return;
		c = ctl.get();//不成功再次獲取線程狀態
	}
	
	//出現情況二:線程池的線程數量不小於corePoolSize核心線程數量,或者開啓核心線程失敗,嘗試將任務以非阻塞的方式添加到任務隊列。
	if (isRunning(c) && workQueue.offer(command)) {
		int recheck = ctl.get();
		if (! isRunning(recheck) && remove(command))//線程池不處於Running,將自定義任務從workerQueue中移除
			reject(command);//拒絕執行命令
		else if (workerCountOf(recheck) == 0)//當worker數量==0時
			addWorker(null, false);//添加worker
	}
	
	//出現情況三:任務隊列已滿導致添加任務失敗,開啓新的非核心線程執行任務。
	else if (!addWorker(command, false))//添加worker失敗
		reject(command);//拒絕執行命令
}

addWorker()方法

1)原子性增加WorkerCount

2)將用戶給定的任務封裝爲Worker,並將此任務添加到Workers集合中

3)啓動worker對應的線程,執行器run()方法

4)回滾worker創建動作,將worker重workers中移除,並原子性減少workerCount

private boolean addWorker(Runnable firstTask, boolean core) {
    retry:
    //使用CAS機制輪詢線程池的狀態,如果線程池處於SHUTDOWN及大於它的狀態則拒絕執行任務
    for (;;) {
        int c = ctl.get();//獲取線程池狀態
        int rs = runStateOf(c);//獲取運行時狀態
        if (rs >= SHUTDOWN &&
            ! (rs == SHUTDOWN &&
               firstTask == null &&
               ! workQueue.isEmpty()))
            return false;

        //使用CAS機制嘗試將當前線程數+1
        //如果是核心線程當前線程數必須小於corePoolSize 
        //如果是非核心線程則當前線程數必須小於maximumPoolSize
        //如果當前線程數小於線程池支持的最大線程數CAPACITY 也會返回失敗
        for (;;) {
            int wc = workerCountOf(c);//獲取workerCount
            if (wc >= CAPACITY ||//如果workerCount數量大於等於最大容量
                wc >= (core ? corePoolSize : maximumPoolSize))//workerCount>=核心線程數大小||workerCount>=最大線程數大小
                return false;
            if (compareAndIncrementWorkerCount(c))//比較並增加worker數量
                break retry;//跳出外層循環
            c = ctl.get();//獲取線程池狀態
            if (runStateOf(c) != rs)//此次的狀態與上次狀態不同
                continue retry;//跳過剩餘部分繼續循環
        }
    }

    //這裏已經成功執行了CAS操作將線程池數量+1,下面創建線程
    boolean workerStarted = false;
    boolean workerAdded = false;
    Worker w = null;
    try {
        w = new Worker(firstTask);
        //Worker內部有一個Thread,並且執行Worker的run方法,因爲Worker實現了Runnable
        final Thread t = w.thread;
        if (t != null) {
            //這裏必須同步在狀態爲運行的情況下將Worker添加到workers中
            final ReentrantLock mainLock = this.mainLock;
            mainLock.lock();
            try {
                int rs = runStateOf(ctl.get());

                if (rs < SHUTDOWN ||
                    (rs == SHUTDOWN && firstTask == null)) {
                    if (t.isAlive()) // precheck that t is startable
                        throw new IllegalThreadStateException();
                    workers.add(w);  //把新建的woker線程放入集合保存,這裏使用的是HashSet
                    int s = workers.size();
                    if (s > largestPoolSize)
                        largestPoolSize = s;
                    workerAdded = true;
                }
            } finally {
                mainLock.unlock();
            }
            //如果添加成功則運行線程
            if (workerAdded) {
                t.start();//啓動worker中線程,啓動後會調用worker.run()方法,進而調用runWorker()方法
                workerStarted = true;
            }
        }
    } finally {
        //如果woker啓動失敗,則進行一些善後工作,比如說修改當前woker數量等等
        if (! workerStarted)
            addWorkerFailed(w);
    }
    return workerStarted;
}

addWorker這個方法先嚐試在線程池運行狀態爲RUNNING並且線程數量未達上限的情況下通過CAS操作將線程池數量+1,接着在ReentrantLock同步鎖的同步保證下判斷線程池爲運行狀態,然後把Worker添加到HashSet workers中。如果添加成功則執行Worker的內部線程。
Worker是ThreadPoolExecutor的內部類:

private final class Worker
        extends AbstractQueuedSynchronizer
        implements Runnable
    {
    /**
     * This class will never be serialized, but we provide a
     * serialVersionUID to suppress a javac warning.
     */
    private static final long serialVersionUID = 6138294804551838833L;

    /** Thread this worker is running in.  Null if factory fails. */
    final Thread thread;
    /** Initial task to run.  Possibly null. */
    Runnable firstTask;
    /** Per-thread task counter */
    volatile long completedTasks;

    /**
     * Creates with given first task and thread from ThreadFactory.
     * @param firstTask the first task (null if none)
     */
    Worker(Runnable firstTask) {
        setState(-1); // inhibit interrupts until runWorker
        this.firstTask = firstTask;
        this.thread = getThreadFactory().newThread(this);
    }

    /** Delegates main run loop to outer runWorker. */
    public void run() {
        runWorker(this);
    }

    // Lock methods
    //
    // The value 0 represents the unlocked state.
    // The value 1 represents the locked state.

    protected boolean isHeldExclusively() {
        return getState() != 0;
    }

    protected boolean tryAcquire(int unused) {
        if (compareAndSetState(0, 1)) {
            setExclusiveOwnerThread(Thread.currentThread());
            return true;
        }
        return false;
    }

    protected boolean tryRelease(int unused) {
        setExclusiveOwnerThread(null);
        setState(0);
        return true;
    }

    public void lock()        { acquire(1); }
    public boolean tryLock()  { return tryAcquire(1); }
    public void unlock()      { release(1); }
    public boolean isLocked() { return isHeldExclusively(); }

    void interruptIfStarted() {
        Thread t;
        if (getState() >= 0 && (t = thread) != null && !t.isInterrupted()) {
            try {
                t.interrupt();
            } catch (SecurityException ignore) {
            }
        }
    }
}

runWorker()方法

//ThreadPoolExecutor類中
final void runWorker(Worker w) {
    Thread wt = Thread.currentThread();
    Runnable task = w.firstTask;
    w.firstTask = null;
    // 因爲Worker的構造函數中setState(-1)禁止了中斷,這裏的unclock用於恢復中斷
    w.unlock();
    boolean completedAbruptly = true;
    try {
        //一般情況下,task都不會爲空,因此會直接進入循環體中
        while (task != null || (task = getTask()) != null) {
            w.lock();
            if ((runStateAtLeast(ctl.get(), STOP) ||
                 (Thread.interrupted() &&
                  runStateAtLeast(ctl.get(), STOP))) &&
                !wt.isInterrupted())
                wt.interrupt();
            try {
                //該方法是個空的實現,如果有需要用戶可以自己繼承該類進行實現
                beforeExecute(wt, task);
                Throwable thrown = null;
                try {
                    //真正的任務執行邏輯
                    task.run();
                } catch (RuntimeException x) {
                    thrown = x; throw x;
                } catch (Error x) {
                    thrown = x; throw x;
                } catch (Throwable x) {
                    thrown = x; throw new Error(x);
                } finally {
                    //該方法是個空的實現,如果有需要用戶可以自己繼承該類進行實現
                    afterExecute(task, thrown);
                }
            } finally {
                //這裏設爲null,也就是循環體再執行的時候會調用getTask方法
                task = null;
                w.completedTasks++;
                w.unlock();
            }
        }
        completedAbruptly = false;
    } finally {
        //當指定任務執行完成,阻塞隊列中也取不到可執行任務時,會進入這裏,做一些善後工作
        //比如在corePoolSize跟maximumPoolSize之間的woker會進行回收
        processWorkerExit(w, completedAbruptly);
    }
}

runWorker方法實際會執行給定任務(用戶傳入的runnable的run()方法),當給定任務執行完畢,會繼續從阻塞隊列中獲取任務,之道阻塞隊列爲空(表示任務執行完畢),在執行給定任務時,回使用鉤子函數,通過鉤子函數完成用戶自定義的一些邏輯,在runWorker中會調用getTask()方法及鉤子函數processWorkerExit()

getTask()方法

private Runnable getTask() {
    boolean timedOut = false;

    for (;;) {
        int c = ctl.get();
        int rs = runStateOf(c);
        if (rs >= SHUTDOWN && (rs >= STOP || workQueue.isEmpty())) {
            decrementWorkerCount();
            return null;
        }
        int wc = workerCountOf(c);
        boolean timed = allowCoreThreadTimeOut || wc > corePoolSize;
        if ((wc > maximumPoolSize || (timed && timedOut))
            && (wc > 1 || workQueue.isEmpty())) {
            if (compareAndDecrementWorkerCount(c))
                return null;
            continue;
        }
        try {
            //根據超時配置有兩種方法取出任務
            Runnable r = timed ?
                workQueue.poll(keepAliveTime, TimeUnit.NANOSECONDS) :
                workQueue.take();
            if (r != null)
                return r;
            timedOut = true;
        } catch (InterruptedException retry) {
            timedOut = false;
        }
    }
}

getTask()的實現跟我們構造參數keepAliveTime存活時間有關。我們都知道keepAliveTime代表了線程池中的線程(即woker線程)的存活時間,如果到期則回收woker線程,這個邏輯的實現就在getTask中,getTask()方法就是去阻塞隊列中取任務,用戶設置的存活時間,就是從這個阻塞隊列中取任務等待的最大時間,如果getTask返回null,意思就是woker等待了指定時間仍然沒有取到任務,此時就會跳過循環體,進入woker線程的銷燬邏輯。

processWorkerExit()方法

processWorkerExit方法是在worker退出調用的鉤子函數,引起worker退出的原因:

1)阻塞隊列爲空,沒有可以運行的任務

2)調用了shutdown()/shutdownNow()方法

private void processWorkerExit(Worker w, boolean completedAbruptly) {
	if (completedAbruptly)
		decrementWorkerCount();

	final ReentrantLock mainLock = this.mainLock;
	mainLock.lock();
	try {
		completedTaskCount += w.completedTasks;
		workers.remove(w);
	} finally {
		mainLock.unlock();
	}

	tryTerminate();

	int c = ctl.get();
	if (runStateLessThan(c, STOP)) {
		if (!completedAbruptly) {
			int min = allowCoreThreadTimeOut ? 0 : corePoolSize;
			if (min == 0 && ! workQueue.isEmpty())
				min = 1;
			if (workerCountOf(c) >= min)
				return; // replacement not needed
		}
		addWorker(null, false);
	}
}

線程池的關閉

關閉線程池,可以通過shutdownshutdownNow這兩個方法。它們的原理都是遍歷線程池中所有的線程,然後依次中斷線程。shutdownshutdownNow還是有不一樣的地方:

  • shutdownNow首先將線程池的狀態設置爲STOP,然後嘗試停止所有的正在執行和未執行任務的線程,並返回等待執行任務的列表;
  • shutdown只是將線程池的狀態設置爲SHUTDOWN狀態,然後中斷所有沒有正在執行任務的線程。

4.工作中如何使用線程池?

在阿里巴巴開發者手冊中指出,現線程的創建不允許通過new顯示的創建,必須要通過線程池,而線程池的使用,不允許使用Executors的靜態方法創建,必須通過 ThreadPoolExecutor創建。

5.如何合理配置線程池參數:通過Runtime.getRuntime().availableProcessors();獲取服務器核心線程數

  • 如果是CPU密集型:核心線程數+1個線程的線程池-->8核+1
  • 如果是IO密集型:
  • IO密集型線程池並不是一直在執行任務,則儘可能配置多個線程,核心線程數*2
  • 核心線程數/(1-阻塞係數(0.8-0.9))-->8/(1-0.9)=80

二十二:happens-before 規則

使用happens-before的概念來指定兩個操作之間的執行順序,由於這兩個操作可以在一個線程之內,也可以是在不同線程之間。因此,JMM可以通過happens-before關係向程序員提供跨線程的內存可見性保證(如果A線程的寫操作a與B線程的讀操作b之間存在happens-before關係,儘管a操作和b操作在不同的線程中執行,但JMM向程序員保證a操作將對b操作可見),兩個操作之間有happens-before關係,並不意味着前一個操作必須要在後一個操作之前執行。happens-before僅僅要求前一個操作的(執行的結果)對後一個操作可見。

happens-before規則:

1)程序次序原則:一個線程內,按照程序代碼順序,書寫在前面的操作先行發生與書寫在後面的操作。

2)監視器鎖規則:一個unlock操作先行發生與後面對同一個鎖的lock操作者,這裏必須指同一個鎖,後面指的是時間上的先後順序。

3)volatile變量規則:對一個volatile變量的寫操作先行發生於後面對這個變量的讀操作,這裏的後面同樣指時間上的先後順序。

4)start()規則:如果線程A執行操作ThreadB.start()(啓動線程B),那麼A線程的ThreadB.start()操作happens-before於線程B中的任意操作。

5)join()規則:如果線程A執行操作ThreadB.join()併成功返回,那麼線程B中的任意操作happens-before於線程A從ThreadB.join()操作成功返回。

6)傳遞性規則:如果A先行發生於B,B先行發生於C,則A先行發生於C

7)程序中斷規則:對線程interrupted()方法的調用先行於被中斷線程的代碼檢測到中斷時間的發生。

8)對象終結規則:一個對象的初始化完成,先行發生於它的finalize方法的開始

二十三:內存語義

1.鎖的內存語義

鎖的釋放與獲取所建立的happens-before關係

/**
 * 程序釋放規則:1 hb 2  2 hb 3   4 hb 5  5hb 6
 * 監視器規則 3 hb 4
 * 傳遞性 1 hb 4 ......
 */
public class Demo {
	private int value;
	private synchronized void a() {//1 獲取鎖
		value++;//2
	}//3 釋放鎖
	private synchronized void b() {//4 獲取鎖
		int a=value;//5
		//處理其他操作    
	}//6 釋放鎖
}

鎖的釋放與獲取的內存語義

當線程釋放鎖時,JMM會把該線程對應的本地內存中的共享變量刷新到主內存中 ,當線程獲取鎖時,JMM會把該線程對應的本地內存置爲無效。從而使得被監視器保護的臨界區代碼必須要從主內存中去讀取共享變量。

總結:
線程A釋放一個鎖,實質上是線程A向接下來將要獲取這個鎖的某個線程發出了(線程A對共享變量所做修改的)消息。
線程B獲取一個鎖,實質上是線程B接收了之前某個線程發出的(在釋放這個鎖之前對共享變量所做修改的)消息。
線程A釋放鎖,隨後線程B獲取這個鎖,這個過程實質上是線程A通過主內存向線程B發送消息。
2.volatile內存語義

volatile建立的happens-before關係

/**
 * 程序釋放規則:1 hb 2  3 hb 4   4 hb 5
 * volatile變量規則 2 hb 3
 * 傳遞性 1 hb 4 ......
 */
public class Demo2 {
	
	private int a;
	private volatile boolean flag;
	
	public void a() {
		a=10;//1
		flag=true;//2
	}
	public void b() {
		if (flag) {//3
			int b=a+1;//4
			System.out.println(b);//5
		}
	}
}

volatile讀寫的內存語義

當寫一個 volatile 變量時,JMM 會把該線程對應的本地內存中的共享變量值刷新到主內存,當讀一個 volatile 變量時,JMM 會把該線程對應的本地內存置爲無效。線程接下來將從主內存中讀取共享變量。

3.final域的內存語義

寫final域的重排序規則

寫final域的重排序規則禁止把final域的寫重排序到構建函數之外。

1)JMM禁止編譯器把final域的寫重排序到構建函數之外。
2)編譯器會在final域的寫之後,構造函數return之前插入一個StoreStore屏障。這個屏障禁止處理器把final域的寫重排序到構建函數之外。

寫final域的重排序規則可以確保:在對象引用爲任意線程可見之前,對象的final域已經被正確初始化過了,而普通域不具有這個保障:寫普通域的操作可能會被編譯器重排序到構建函數之外,而寫final域的操作被寫final域的重排序規則“限定”在了構造函數之內。

讀final域的重排序規則

1)在一個線程中,初次讀對象引用與初次讀該對象包含的final域,JMM禁止處理器重排序這兩個操作(該規則僅針對處理器)。

2)編譯器會在讀final域操作的前面插入一個LoadLoad屏障。

讀final域的重排序規則可以確保:在讀一個對象的final域之前,一定會先讀包含這個final域的對象的引用。如果該引用不爲null,則引用對象的final域一定被已經被初始化過了。

final域爲引用類型

在構造函數內對一個final引用的對象的成員域的寫入,與隨後在構造函數外把這個被構造對象的引用賦值給一個引用變量,這兩個操作不能重排序。

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