線程(英語:thread)是操做系統可以進行運算調度的最小單位。它被包含在進程之中,是進程中的實際運做單位。一條線程指的是進程中一個單一順序的控制流,一個進程中能夠併發多個線程,每條線程並行執行不一樣的任務。Java 虛擬機容許應用程序併發地運行多個執行線程。 php
Thread類:該類的對象表明一個線程css
主要方法:html
Start()方法: Java虛擬機調用該線程的 run 方法,使該線程開始執行(啓動線程)前端
sleep()方法: 限時等待 休眠,sleep有時間java
join()方法: 當前線程進入等待狀態,沒有時間,要等到該線程終止。node
①繼承Thread類重寫run方法mysql
②實現Runnable接口,實現run方法nginx
③實現Callable接口,新建當前類對象c++
當多線程共同訪問同一個對象(臨界資源)的時候, 若是破壞了不可分割的操做(原子操做),就可能發生數據不一致,有可能出現多個線程前後更改數據,形成所獲得的數據是髒數據web
解決方法:用鎖。
在Java中一般實現鎖有兩種方式,一種是synchronized關鍵字,另外一種是Lock。
①使用 synchronized。必需要獲取當前對象的互斥鎖標記,若是得不到就被阻塞,直到獲得互斥鎖標記。線程執行完同步方法,會自動歸還互斥鎖標記
②使用Lock。 Lock接口的經常使用實現類 ReentrantLock /riː'entrənt/ :互斥鎖
二者的區別:
①首先最大的不一樣:synchronized是基於JVM層面實現的,而Lock是基於JDK層面實現的。
②synchronized是一個關鍵字,Lock是一個接口.
③synchronized代碼塊執行完成以後會自動釋放鎖對象,Lock必須手動調用方法釋放鎖對象。
④synchronized代碼塊出現了異常也會自動釋放鎖對象,Lock接口中出現異常也須要手動釋放鎖對象。
⑤在併發量比較小的狀況下,使用synchronized;可是在併發量比較高的狀況下,其性能降低會很嚴重,此時推薦使用ReentrantLock。
package day20; import java.util.concurrent.locks.Lock; import java.util.concurrent.locks.ReentrantLock; public class TestLock { public static void main(String[] args) throws Exception { MyList2 list = new MyList2(); Thread t1 = new Thread(new Runnable(){ public void run(){ list.add("C"); } }); Thread t2 = new Thread(new Runnable(){ public void run(){ list.add("D"); } }); t1.start(); t2.start(); t1.join(); t2.join(); list.add("E"); list.print(); } } class MyList2{ String[] data = {"A","B","","","",""}; int index = 2; Lock lock = new ReentrantLock();//Lock接口,ReentrantLock爲實現類 public void add(String s){ try{ lock.lock();//加鎖 //lock.tryLock();嘗試加鎖,失敗時返回false,此時可進行其餘操做,但有可能形成活鎖。 data[index] = s ; try { Thread.sleep(100); } catch (InterruptedException e) { e.printStackTrace(); } index++; } finally{ lock.unlock();//釋放鎖,爲了不鎖內的代碼塊出現異常後直接返回而沒有釋放鎖的問題,將此句代碼放到Finally中 } } public void print(){ for(int i = 0 ; i < data.length ; i++){ System.out.println(data[i]); } } }
o.notify()/o.notifyAll():從等待狀態中釋放一個/所有線程
以上三個方法必須出如今對o加鎖的同步代碼塊中,
1 永遠在synchronized的方法或對象裏使用wait、notify和notifyAll,否則Java虛擬機會生成 IllegalMonitorStateException。
2 永遠在while循環裏而不是if語句下使用wait。這樣,循環會在線程睡眠先後都檢查wait的條件,並在條件實際上並未改變的狀況下處理喚醒通知。
3 永遠在多線程間共享的對象(在生產者消費者模型裏即緩衝區隊列)上使用wait。
import java.util.concurrent.locks.Condition; import java.util.concurrent.locks.Lock; import java.util.concurrent.locks.ReentrantLock; public class TestProducerConsumer { public static void main(String[] args) { MyStack stack = new MyStack(); Runnable task1 = new Runnable(){ public void run(){ for(char c = 'A' ; c<='Z' ; c++){ stack.push(c+""); } } }; Runnable task2 = new Runnable(){ public void run(){ for(int i = 1 ; i <= 26; i++){ stack.pop(); } } }; new Thread(task1).start(); new Thread(task1).start(); new Thread(task2).start(); new Thread(task2).start(); } } class MyStack{ String[] data = {"","","","","",""}; int index; Lock lock = new ReentrantLock(); Condition full = lock.newCondition();//得到Condition實例 Condition empty = lock.newCondition(); public void push(String s){ try { lock.lock(); while (data.length == index) { try { full.await();//(不符合條件的等待)滿了即等待 } catch (InterruptedException e) { e.printStackTrace(); } } System.out.print(s + " pushed "); data[index] = s; index++; print(); empty.signalAll();//通知消費者 } finally{ lock.unlock(); } } public void pop(){ try { lock.lock(); while (index == 0) { try { empty.await(); } catch (InterruptedException e) { e.printStackTrace(); } } index--; String o = data[index]; data[index] = ""; System.out.print(o + " poped "); print(); full.signalAll(); //通知生產者 } finally{ lock.unlock(); } } public void print(){ for(int i = 0 ; i < data.length ; i++){ System.out.print(data[i]+" "); } System.out.println(); } }
條件(Conditio也稱爲條件隊列或條件變量 )爲一個線程的暫停執行(「等待」)提供了一種方法,直到另外一個線程通知某些狀態如今可能爲真。
Condition取代了對象監視器方法的使用。能夠使用兩個Condition實例來實現
一個Condition實例本質上綁定到一個鎖。 要得到特定Condition實例的Condition實例,使用其newCondition()方法。
void | await() 致使當前線程等到發信號或 interrupted 。 |
---|---|
void | signal() 喚醒一個等待線程。 |
void | signalAll() 喚醒全部等待線程。 |
例如,假設咱們有一個有限的緩衝區,它支持put
和take
方法。 若是在一個空的緩衝區嘗試一個take
,則線程將阻塞直到一個項目可用; 若是put
試圖在一個完整的緩衝區,那麼線程將阻塞,直到空間變得可用。 咱們但願在單獨的等待集中等待put
線程和take
線程,以便咱們能夠在緩衝區中的項目或空間可用的時候使用僅通知單個線程的優化。 這能夠使用兩個Condition
實例來實現。
class BoundedBuffer { final Lock lock = new ReentrantLock(); final Condition notFull = lock.newCondition(); final Condition notEmpty = lock.newCondition(); final Object[] items = new Object[100]; int putptr, takeptr, count; public void put(Object x) throws InterruptedException { lock.lock(); try { while (count == items.length) notFull.await();//滿了,put等待 items[putptr] = x; if (++putptr == items.length) putptr = 0; ++count; notEmpty.signal();//喚醒take } finally { lock.unlock(); } } public Object take() throws InterruptedException { lock.lock(); try { while (count == 0) notEmpty.await();//空了,take等待 Object x = items[takeptr]; if (++takeptr == items.length) takeptr = 0; --count; notFull.signal();//喚醒put return x; } finally { lock.unlock(); } } }
public class TestNumberCharPrint { public static void main(String[] args) throws InterruptedException { final Object o = new Object();//全局對象,用於分別不一樣時間拿到鎖標記來交替 Runnable task1 = new Runnable(){ public void run(){ synchronized (o) {//加鎖保證此處原子操做 for (int i = 1; i <= 52; i++) { System.out.println(i); if (i % 2 ==0){ o.notifyAll();//釋放字母線程 try { if(i!=52) o.wait();//若等於52時進入等待,則此線程已完成所有任務單還沒結束 } catch (InterruptedException e) { e.printStackTrace(); } } } } } }; Runnable task2 = new Runnable(){ public void run(){ synchronized (o) { for (char c = 'A'; c <= 'Z'; c++) { System.out.println(c); o.notifyAll();//釋放數字線程 try { if (c!='Z') o.wait(); } catch (InterruptedException e) { e.printStackTrace(); } } } } }; Thread t1 = new Thread(task1); Thread t2 = new Thread(task2); t1.start(); Thread.sleep(1); t2.start(); } }
Lock接口的一個實現類
ReadWriteLock維護了一對相關的鎖,一個用於只讀操做,另外一個用於寫入操做。
讀鎖和寫鎖不能被同時加載,寫鎖加載則不能讀,讀鎖加載則不能寫。
若寫鎖未被加載,讀取鎖能夠多個讀線程同時保持,
若讀鎖未被加載,寫入鎖也是獨佔的,不能同時寫。
package day20; import java.util.ArrayList; import java.util.concurrent.CopyOnWriteArrayList; import java.util.concurrent.locks.Lock; import java.util.concurrent.locks.ReadWriteLock; import java.util.concurrent.locks.ReentrantReadWriteLock; public class TestMyList { public static void main(String[] args) { CopyOnWriteArrayList<String> list = new CopyOnWriteArrayList<>(); list.add("A"); } } //改造ArrayList爲線程安全的(部分方法,經過加讀或寫鎖來實現) class MyList extends ArrayList{ ReadWriteLock rwl = new ReentrantReadWriteLock();//讀寫鎖 Lock rl = rwl.readLock();//讀鎖 Lock wl = rwl.writeLock();//寫鎖 @Override public int size() { try{ rl.lock(); return super.size(); } finally{ rl.unlock(); } } @Override public Object get(int index) { try{ rl.lock(); return super.get(index); } finally{ rl.unlock(); } } @Override public boolean add(Object e) { try{ wl.lock(); return super.add(e); } finally{ wl.unlock(); } } @Override public Object remove(int index) { try{ wl.lock(); return super.remove(index); } finally{ wl.unlock(); } } @Override public void clear() { try{ wl.lock(); super.clear(); } finally{ wl.unlock(); } } }
CopyOnWriteArrayList 利用複製數組的方式實現數組元素的修改, 寫效率低 讀效率高(讀操做遠多於寫操做) 整體效率提升
CopyOnWriteArraySet 線程安全的Set集合
ConcurrentHashMap 分段鎖,將HashMap的數組鏈表分爲16段,多個線程讀取和寫入同一段時,需依次進行(需等待),讀取或寫入不一樣段時互不影響,因爲HashCode相等的機率不大,因此效率遠高於HashTable。
ConcurrentLinkedQueue 線程安全的隊列(鏈表實現的) 利用一個無鎖算法(CAS,和預期值比較,不一樣則重試)實現線程安全——效率高
經常使用方法:
add() :添加元素
offer():添加元素 優先使用
remove():刪除元素
poll ():刪除元素,優先使用
element():獲取隊列的頭元素
peek():獲取隊列的頭元素 優先使用
實現類:LinkedList ConcurrentLinkedQueue
put () 添加元素到隊列中 若是隊列滿,則等待
take()刪除隊列頭元素 , 若是隊列空,則等待
實現類:ArrayBlockingQueue 數組實現 有界隊列 put方法可能會等待
LinkedBlockingQueue 鏈表實現 無界隊列 put方法不等待
package day21; import java.util.concurrent.ArrayBlockingQueue; import java.util.concurrent.BlockingQueue; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; public class TestBlockingQueue { public static void main(String[] args) { BlockingQueue<String> queue = new ArrayBlockingQueue<String>(6);//隊列 Runnable task1 = new Runnable(){ public void run(){ for(int i = 1 ; i<= 100; i++){ try { queue.put("A"+i); } catch (InterruptedException e) { e.printStackTrace(); } } } }; Runnable task2 = new Runnable(){ public void run(){ for(int i = 1 ; i<= 100; i++){ try { queue.take(); } catch (InterruptedException e) { e.printStackTrace(); } } } }; ExecutorService es = Executors.newFixedThreadPool(2); es.submit(task1); es.submit(task2); es.shutdown(); } }
Collection |- List (ArrayList LinkedList Vector CopyOnWriteArrayList) |- Set (HashSet LinkedHashSet CopyOnWriteArraySet) |- SortedSet (TreeSet) |- Queue(LinkedList ConcurrentLinkedQueue) |- BlockingQueue (ArrayBlockingQueue LinkedBlockingQueue)
Map (HashMap LinkedHashMap Hashtable Properties ConcurrentHashMap ) |- SortedMap (TreeMap)
i++是先把數i讀到另一個寄存器,加1運算後再寫回到原寄存器,中間過程被另一個線程打斷時就不是原子操做了,會形成結果不一致
package day21; import java.util.concurrent.atomic.AtomicInteger; public class TestAtomicInteger { static int i = 0 ; //error! static AtomicInteger a = new AtomicInteger(0);//利用AtomicInteger解決,還有AtomicBoolean等,利用了不加鎖的比較算法(不是預期值時,撤回,從新加) static Integer b = Integer.valueOf(0);//error!無臨界資源,由於Integer+1後成爲了另一個對象Integer,能夠定義其餘類型的對象來加鎖,以下列的obj static MyObject obj = new MyObject(); public static void main(String[] args) throws Exception{ Thread[] ts = new Thread[10]; for(int k = 0 ; k<ts.length ; k++){ ts[k] = new Thread(new Runnable(){ public void run(){ for(int k = 1 ; k <= 10000; k++){ i++; a.incrementAndGet(); synchronized(b){ b = Integer.valueOf(b.intValue()+1); } synchronized(obj){ obj.x++; } } } }); ts[k].start(); } for(int k=0; k <ts.length ; k++){ ts[k].join(); } System.out.println(i); System.out.println(a); System.out.println(b); System.out.println(obj.x); } } class MyObject{ public int x=0; }
Fork/Join框架是Java7提供了的一個用於並行執行任務的框架, 是一個把大任務分割成若干個小任務,最終彙總每一個小任務結果後獲得大任務結果的框架。
Fork就是把一個大任務切分爲若干子任務並行的執行,Join就是合併這些子任務的執行結果,最後獲得這個大任務的結果。Fork/Join的運行流程圖以下:
工做竊取(work-stealing)算法是指某個線程從其餘隊列裏竊取任務來執行。工做竊取的運行流程圖以下:
優勢是充分利用線程進行並行計算,並減小了線程間的競爭,其缺點是在某些狀況下仍是存在競爭,好比雙端隊列裏只有一個任務時。而且消耗了更多的系統資源,好比建立多個線程和多個雙端隊列。
ForkJoinPool是ExecutorService的實現類,所以是一種特殊的線程池。ForkJoinPool提供了以下兩個經常使用的構造器。
public ForkJoinPool(int parallelism):建立一個包含parallelism個並行線程的ForkJoinPool public ForkJoinPool() :以Runtime.getRuntime().availableProcessors()的返回值做爲parallelism來建立ForkJoinPool 建立ForkJoinPool實例後,能夠調用ForkJoinPool的
submit(ForkJoinTask<T> task)或者invoke(ForkJoinTask<T> task)來執行指定任務。其中ForkJoinTask表明一個能夠並行、合併的任務。ForkJoinTask是一個抽象類,它有兩個抽象子類:RecursiveAction和RecursiveTask。
RecursiveTask表明有返回值的任務————join()方法來啓動RecursiveAction表明沒有返回值的任務。————fork()方法來啓動
Recursive 遞歸的,循環的 fork叉,搬走
RecursiveTask的例子:
import java.util.concurrent.ForkJoinPool; import java.util.concurrent.RecursiveTask; public class TestForkJoinAdd { public static void main(String[] args) { System.out.println(Runtime.getRuntime().availableProcessors()); ForkJoinPool pool = new ForkJoinPool();//線程池 AddTask main = new AddTask(1 , 100000);//任務 Long result = pool.invoke(main); //<T> T invoke(ForkJoinTask<T> task) 執行給定的任務,在完成後返回其結果。 System.out.println(result); } } class AddTask extends RecursiveTask<Long>{ int start; int end; static final int THRESHOLD = 1000; public AddTask(int start, int end) { super(); this.start = start; this.end = end; } @Override public Long compute() { //若是start和end之間差距低於THRESHOLD 直接計算,THRESHOLD是定下的是否分割任務的臨界值 if (end - start <= THRESHOLD){ long result = 0 ; for(int i = start ; i<= end ; i++){ result += i; } return result; } //不然 就要把任務分割爲兩個子任務 else{ int middle = (start+end)/2; AddTask task1 = new AddTask(start , middle); AddTask task2 = new AddTask(middle+1 , end); invokeAll(task1 , task2); long r1 = task1.join(); long r2 = task2.join(); return r1+r2; } } }
Semaphore 是 synchronized 的增強版,做用是控制線程的併發數量(控制線程的數量)。
方法 acquire( int permits ) 參數做用,及動態添加 permits 許可數量
acquire( int permits ) 中的參數是什麼意思呢? new Semaphore(6) 表示初始化了 6個通路, semaphore.acquire(2) 表示每次線程進入將會佔用2個通路,semaphore.release(2) 運行時表示歸還2個通路。沒有通路,則線程就沒法進入代碼塊。
void | acquire() 從該信號量獲取許可證,阻止直到可用,或線程爲 interrupted 。 |
---|---|
void | acquire(int permits) 從該信號量獲取給定數量的許可證,阻止直到全部可用,不然線程爲 interrupted 。 |
void | release() 釋放許可證,將其返回到信號量。 |
---|---|
void | release(int permits) 釋放給定數量的許可證,將其返回到信號量。 |
package day21; import java.util.ArrayList; import java.util.List; import java.util.concurrent.Semaphore; import java.util.concurrent.atomic.AtomicBoolean; public class TestSemaphore { public static void main(String[] args) { List<PhoneRoom> rooms = new ArrayList<>(); rooms.add(new PhoneRoom("Room 1")); rooms.add(new PhoneRoom("Room 2")); rooms.add(new PhoneRoom("Room 3")); rooms.add(new PhoneRoom("Room 4")); rooms.add(new PhoneRoom("Room 5")); Semaphore s = new Semaphore(5); class Task implements Runnable{ public void run(){ try { s.acquire(); } catch (InterruptedException e1) { e1.printStackTrace(); } for(int i = 0 ; i < rooms.size() ; i++){ PhoneRoom room = rooms.get(i); if (room.isFree()){ room.setFree(false); System.out.println(Thread.currentThread().getName()+" entered "+room.getName()); try { Thread.sleep(2000); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println(Thread.currentThread().getName()+" exited "+room.getName()); room.setFree(true); s.release(); return; } } } } for(int i = 1 ; i <= 10 ; i++){ Runnable task = new Task(); Thread t = new Thread(task); t.start(); } } } class PhoneRoom{ AtomicBoolean isFree = new AtomicBoolean(true);//原子操做的boolean,只容許一個線程拿到 String name; public PhoneRoom(String name) { super(); this.name = name; } public boolean isFree() { return isFree.get(); } public void setFree(boolean flag) { this.isFree.set(flag); } public String getName(){ return name; } }
Package java.util.concurrent.atomic
一個小型工具包,支持單個變量上的無鎖線程安全編程。
經常使用:
AtomicBoolean | 一個 boolean 值能夠用原子更新。 |
---|---|
AtomicInteger | 可能原子更新的 int 值。 |
java.util.concurrent.CountDownLatch
容許一個或多個線程等待直到在其餘線程中執行的一組操做完成的同步輔助。
A CountDownLatch
用給定的計數初始化。 await
方法阻塞,直到因爲countDown()
方法的調用而致使當前計數達到零,以後全部等待線程被釋放,而且任何後續的await
調用當即返回。 這是一個一次性的現象 - 計數沒法重置。 若是您須要重置計數的版本,請考慮使用CyclicBarrier
CountDownLatch是一個線程計數器,在建立線程的時候能夠設置任務數量 每執行完一個線程 調用方法讓計數器減一 若是計數器減爲了0 主線程再向下執行。
CountDownLatch主要有兩個方法:countDown()和await()。countDown()方法用於使計數器減一,通常是執行任務的線程調用,await()方法則使調用該方法的線程處於等待狀態,通常是主線程調用。
package day21; import java.util.concurrent.CountDownLatch; import java.util.concurrent.CyclicBarrier; public class TestCountDownLatch { public static void main(String[] args) { CountDownLatch cdl = new CountDownLatch(2); CyclicBarrier cb = new CyclicBarrier(3); Thread t1 = new Thread(){ public void run(){ for(int i = 1 ; i <= 100 ; i++){ System.out.println("### "+i); if (i == 50) cdl.countDown(); try { Thread.sleep(100); if (i == 99) cb.await(); } catch (Exception e) { e.printStackTrace(); } } } }; Thread t2 = new Thread(){ public void run(){ for(int i = 1 ; i <= 100 ; i++){ System.out.println("$$$ "+i); if (i == 50) cdl.countDown(); try { Thread.sleep(100); if (i==99) cb.await(); } catch (Exception e) { e.printStackTrace(); } } } }; Thread t3 = new Thread(){ public void run(){ try { cdl.await(); } catch (InterruptedException e1) { e1.printStackTrace(); } for(int i = 1 ; i <= 100 ; i++){ System.out.println("*** "+i); try { Thread.sleep(100); if (i==99) cb.await(); } catch (Exception e) { e.printStackTrace(); } } } }; t1.start(); t2.start(); t3.start(); } }
java.lang.ThreadLocal<T>
ThreadLocal爲解決多線程程序的併發問題提供了一種新的思路。
當使用ThreadLocal維護變量時,ThreadLocal爲每一個使用該變量的線程提供獨立的變量副本,因此每個線程均可以獨立地改變本身的副本,而不會影響其它線程所對應的副本。
從線程的角度看,目標變量就象是線程的本地變量,這也是類名中「Local」所要表達的意思。
它只有4個方法 :
T | get() 返回當前線程的此線程局部變量的副本中的值。 |
---|---|
protected T | initialValue() 返回此線程局部變量的當前線程的「初始值」。 |
void | remove() 刪除此線程局部變量的當前線程的值。 |
void | set(T value) 將當前線程的此線程局部變量的副本設置爲指定的值。 |
static <S> ThreadLocal<S> | withInitial(Supplier<? extends S> supplier) Creates a thread local variable. |
set(T value)和T get()分別爲設置和得到當前線程的線程局部變量的值。
remove()是將當前線程局部變量的值刪除,目的是爲了減小內存的佔。須要指出的是,當線程結束後,對應該線程的局部變量將自動被垃圾回收,因此顯式調用該方法清除線程的局部變量並非必須的操做,但它能夠加快內存回收的速度。
T initialValue()返回該線程局部變量的初始值,該方法是一個protected的方法,顯然是爲了讓子類覆蓋而設計的。這個方法是一個延遲調用方法,在線程第1次調用get()或set(Object)時才執行,而且僅執行1次。ThreadLocal中的缺省實現直接返回一個null。
ThreadLocal是如何作到爲每個線程維護變量的副本的呢?
其實實現的思路很簡單:在ThreadLocal類中有一個Map,用於存儲每個線程的變量副本,Map中元素的鍵爲線程對象,而值對應線程的變量副本。 咱們本身就能夠提供一個簡單的實現版本:
package com.test; public class TestNum { // ①經過匿名內部類覆蓋ThreadLocal的initialValue()方法,指定初始值 private static ThreadLocal<Integer> seqNum = new ThreadLocal<Integer>() { public Integer initialValue() { return 0; } }; // ②獲取下一個序列值 public int getNextNum() { seqNum.set(seqNum.get() + 1); return seqNum.get(); } public static void main(String[] args) { TestNum sn = new TestNum(); // ③ 3個線程共享sn,各自產生序列號 TestClient t1 = new TestClient(sn); TestClient t2 = new TestClient(sn); TestClient t3 = new TestClient(sn); t1.start(); t2.start(); t3.start(); } private static class TestClient extends Thread { private TestNum sn; public TestClient(TestNum sn) { this.sn = sn; } public void run() { for (int i = 0; i < 3; i++) { // ④每一個線程打出3個序列值 System.out.println("thread[" + Thread.currentThread().getName() + "] --> sn[" + sn.getNextNum() + "]"); } } } }
ThreadLocal和線程同步機制相比有什麼優點呢?ThreadLocal和線程同步機制都是爲了解決多線程中相同變量的訪問衝突問題。
在同步機制中,經過對象的鎖機制保證同一時間只有一個線程訪問變量。這時該變量是多個線程共享的,使用同步機制要求程序慎密地分析何時對變量進行讀寫,何時須要鎖定某個對象,何時釋放對象鎖等繁雜的問題,程序設計和編寫難度相對較大。
而ThreadLocal則從另外一個角度來解決多線程的併發訪問。ThreadLocal會爲每個線程提供一個獨立的變量副本,從而隔離了多個線程對數據的訪問衝突。由於每個線程都擁有本身的變量副本,從而也就沒有必要對該變量進行同步了。ThreadLocal提供了線程安全的共享對象,在編寫多線程代碼時,能夠把不安全的變量封裝進ThreadLocal。
因爲ThreadLocal中能夠持有任何類型的對象,低版本JDK所提供的get()返回的是Object對象,須要強制類型轉換。但JDK 5.0經過泛型很好的解決了這個問題,在必定程度地簡化ThreadLocal的使用,
歸納起來講,對於多線程資源共享的問題,同步機制採用了「以時間換空間」的方式,而ThreadLocal採用了「以空間換時間」的方式。前者僅提供一份變量,讓不一樣的線程排隊訪問,然後者爲每個線程都提供了一份變量,所以能夠同時訪問而互不影響。
Spring使用ThreadLocal解決線程安全問題咱們知道在通常狀況下,只有無狀態的Bean才能夠在多線程環境下共享,在Spring中,絕大部分Bean均可以聲明爲singleton做用域。就是由於Spring對一些Bean(如RequestContextHolder、TransactionSynchronizationManager、LocaleContextHolder等)中非線程安全狀態採用ThreadLocal進行處理,讓它們也成爲線程安全的狀態,由於有狀態的Bean就能夠在多線程中共享了。
通常的Web應用劃分爲展示層、服務層和持久層三個層次,在不一樣的層中編寫對應的邏輯,下層經過接口向上層開放功能調用。在通常狀況下,從接收請求到返回響應所通過的全部程序調用都同屬於一個線程,如圖所示:
同一線程貫通三層這樣你就能夠根據須要,將一些非線程安全的變量以ThreadLocal存放,在同一次請求響應的調用線程中,全部關聯的對象引用到的都是同一個變量。
/** * Sets the current thread's copy of this thread-local variable * to the specified value. Most subclasses will have no need to * override this method, relying solely on the {@link #initialValue} * method to set the values of thread-locals. * * @param value the value to be stored in the current thread's copy of * this thread-local. */ public void set(T value) { Thread t = Thread.currentThread(); ThreadLocalMap map = getMap(t); if (map != null) map.set(this, value); else createMap(t, value); }
ThreadLocal類可讓你建立的變量只被同一個線程進行讀和寫操做,線程與線程之間獨立,單一線程內共享數據,是線程安全的,
①做用:單一線程內共享數據。
ThreadLocal能夠操做線程內部的Map,能夠存取值,而在web環境下一個業務操做過程當中的類與方法的調用都是處於一個線程內部的,那麼就能夠使用ThreadLocal
將一個對象存入當前Thread,而後處於當前線程下的任意類和任意方法中得到該對象。
2.TreadLocal原理:
ThreadLocal是如何操做Thread當中的Map對象的呢?
ThreadLocal#set(Value):
把當前的ThreadLocal做爲key,將Value做爲值 存儲到當前線程的Map屬性中了。
ThreadLocal#get() :
將ThreadLocal自己做爲key 去查詢Thread的Map對象。
ThreadLcoal#remove:
將ThreadLocal自己做爲key 重Thread 的Map屬性刪除
· 原子性原子,即一個不可再被分割的顆粒。在Java中原子性指的是一個或多個操做要麼所有執行成功要麼所有執行失敗。
· 有序性程序執行的順序按照代碼的前後順序執行。(處理器可能會對指令進行重排序)
· 可見性當多個線程訪問同一個變量時,若是其中一個線程對其做了修改,其餘線程能當即獲取到最新的值。
建立,就緒,運行,阻塞,死亡
New、start()、run()、sleep或join、run執行完,或者遇到異常
sleep是Thread類的方法
wait是object類的方法,進入這個狀態後,是不能自動喚醒的,必須依靠其餘線程調用notify()或notifyAll()方法才能被喚醒
主線程建立並啓動子線程,若是子線程中須要進行大量的耗時計算,主線程每每早於子線程結束。這時,若是主線程想等待子線程執行結束以後再結束,好比子線程處理一個數據,主線程要取得這個數據,就要調用join() 方法(誰須要誰等待)。
sleep(long)方法在睡眠時不釋放對象鎖,
join(long)方法在等待的過程當中釋放對象鎖
wait和sleep的區別
wait | sleep |
---|---|
wait()方法是Object類裏的方法 | sleep()是Thread類的static(靜態)的方法 |
wait()睡眠時,釋放對象鎖 | sleep()睡眠時,保持對象鎖,仍然佔有該鎖 |
經常使用於線程間通訊 | 經常使用於暫停執行 |
wait和notify/notifyAll是成對出現的, 必須在synchronize塊中被調用 |
阻塞狀態其實是一種比較特殊的等待狀態,
處於其餘等待狀態的線程是在等着別的線程執行結束,等着拿CPU的使用權;而處於阻塞狀態的線程等待的不只僅是CPU的使用權,主要是鎖標記,沒有拿到鎖標記,即使是CPU有空也沒有辦法執行。(關於鎖見下節:線程同步)
等待和阻塞的區別
等待 | 阻塞 |
---|---|
已經拿到鎖對象,或者說不存在拿不到執行不了的狀況 | 等待拿到鎖對象 |
等待被喚醒 | 等待拿到鎖對象 |
已經終止的線程會處於該種狀態。
· 悲觀鎖:每次操做都會加鎖,會形成線程阻塞。
· 樂觀鎖:每次操做不加鎖,而是假設沒有衝突而去完成某項操做,若是由於衝突失敗就重試,直到成功爲止,不會形成線程阻塞。
在一個方法中執行多個操做(3個以上)
單個任務執行時間都很長
多任務並行執行的代碼思路
進入service方法以後
建立三個線程
三個線程分別執行三個查詢
須要獲得查詢結果封裝一個map中統一返回
建立線程的方式
①Thread
②Runable
③Callable
3.主線程等待
Future + Callable
CountDownLatch 類
能夠認爲是一個線程計數器 在建立線程的時候能夠設置任務數量 每次執行完一個線程 調用方法讓計數器減一 若是計數器減爲了0 主線程向下執行
java.util.concurrent
All Known Subinterfaces:
全部已知實現類:
AbstractExecutorService , ForkJoinPool , ScheduledThreadPoolExecutor , ThreadPoolExecutor
提供了一種將任務提交從每一個任務的運行機制分解的方式,包括線程使用,調度等的Executor
。一般使用Executor而不是顯式建立線程。
例如,不是爲一組任務調用new Thread(new(RunnableTask())).start()
,您能夠使用:
Executor executor = anExecutor;
executor.execute(new RunnableTask1());
executor.execute(new RunnableTask2());
方法:void execute(Runnable command)
在未來的某個時間執行給定的命令。 該命令能夠在一個新線程,一個合併的線程中或在調用線程中執行,由Executor實現。 command - 可運行的任務
concurrent ,該包中提供的Executor
實現了ExecutorService
,這是一個更普遍的界面。 ThreadPoolExecutor
類提供了一個可擴展的線程池實現。 Executors
類爲這些執行人員提供了方便的工廠方法。
ThreadPoolExecutor是線程池的核心類,此類的構造方法以下:
public ThreadPoolExecutor(int corePoolSize,int maximumPoolSize,long keepAliveTime,TimeUnit unit, BlockingQueue<Runnable> workQueue); public ThreadPoolExecutor(int corePoolSize,int maximumPoolSize,long keepAliveTime,TimeUnit unit, BlockingQueue<Runnable> workQueue,ThreadFactory threadFactory); public ThreadPoolExecutor(int corePoolSize,int maximumPoolSize,long keepAliveTime,TimeUnit unit, BlockingQueue<Runnable> workQueue,RejectedExecutionHandler handler); public ThreadPoolExecutor(int corePoolSize,int maximumPoolSize,long keepAliveTime,TimeUnit unit, BlockingQueue<Runnable> workQueue,ThreadFactory threadFactory,RejectedExecutionHandler handler);
corePoolSize:核心線程池的大小,若是核心線程池有空閒位置,這時新的任務就會被核心線程池新建一個線程執行,執行完畢後不會銷燬線程,線程會進入緩存隊列等待再次被運行。
maximunPoolSize:線程池能建立最大的線程數量。若是核心線程池和緩存隊列都已經滿了,新的任務進來就會建立新的線程來執行。可是數量不能超過maximunPoolSize,否側會採起拒絕接受任務策略,咱們下面會具體分析。
keepAliveTime:非核心線程可以空閒的最長時間,超過期間,線程終止。這個參數默認只有在線程數量超過核心線程池大小時纔會起做用。只要線程數量不超過核心線程大小,就不會起做用。
unit:時間單位,和keepAliveTime配合使用。
workQueue:緩存隊列,用來存放等待被執行的任務。
threadFactory:線程工廠,用來建立線程,通常有三種選擇策略。
ArrayBlockingQueue;
LinkedBlockingQueue;
SynchronousQueue;
handler:拒絕處理策略,線程數量大於最大線程數就會採用拒絕處理策略,四種策略爲
ThreadPoolExecutor.AbortPolicy:丟棄任務並拋出RejectedExecutionException異常。
ThreadPoolExecutor.DiscardPolicy:也是丟棄任務,可是不拋出異常。
ThreadPoolExecutor.DiscardOldestPolicy:丟棄隊列最前面的任務,而後從新嘗試執行任務(重複此過程)
ThreadPoolExecutor.CallerRunsPolicy:由調用線程處理該任務
二.線程池實現原理
線程池圖:
1.線程池狀態
線程池和線程同樣擁有本身的狀態,
在ThreadPoolExecutor類中定義了一個volatile變量runState來表示線程池的狀態,線程池有四種狀態,分別爲RUNNING、SHUTDOWN、STOP、TERMINATED。
線程池建立後處於RUNNING狀態。
調用shutdown後處於SHUTDOWN狀態,線程池不能接受新的任務,會等待緩衝隊列的任務完成。
調用shutdownNow後處於STOP狀態,線程池不能接受新的任務,並嘗試終止正在執行的任務。
當線程池處於SHUTDOWN或STOP狀態,而且全部工做線程已經銷燬,任務緩存隊列已經清空或執行結束後,線程池被設置爲TERMINATED狀態。
總結:
若是當前線程池中的線程數目小於corePoolSize,則每來一個任務,就會建立一個線程去執行這個任務;
若是當前線程池中的線程數目>=corePoolSize,則每來一個任務,會嘗試將其添加到任務緩存隊列當中,若添加成功,則該任務會等待空閒線程將其取出去執行;若添加失敗(通常來講是任務緩存隊列已滿),則會嘗試建立新的線程去執行這個任務;
若是當前線程池中的線程數目達到maximumPoolSize,則會採起任務拒絕策略進行處理;
若是線程池中的線程數量大於 corePoolSize時,若是某線程空閒時間超過keepAliveTime,線程將被終止,直至線程池中的線程數目不大於corePoolSize;若是容許爲核心池中的線程設置存活時間,那麼核心池中的線程空閒時間超過keepAliveTime,線程也會被終止。
Executors是工具類,裏面全是靜態方法,直接經過類名來調用。
不定長的線程池——newCachedThreadPool()線程數有任務數決定,不夠時新建線程,結束後全部線程不銷燬
定長的線程池——newFixedThreadPool(2)建立的線程池線程數量固定
package day20; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; public class TestExecutor { public static void main(String[] args) { //ExecutorService es = Executors.newFixedThreadPool(2); //Executors是工具類,裏面全是靜態方法,直接經過類名來調用newFixedThreadPool(2)建立的線程池是固定的線程數,結束後全部線程不銷燬 ExecutorService es = Executors.newCachedThreadPool(); //newCachedThreadPool()建立的是不定長的線程池,線程數有任務數決定,不夠時新建線程,結束後全部線程不銷燬 Runnable r1 = new Runnable(){ public void run(){ for(int i = 1 ; i <= 100 ; i++){ System.out.println("### "+i); try { Thread.sleep(100); } catch (InterruptedException e) { e.printStackTrace(); } } } }; Runnable r2 = new Runnable(){ public void run(){ for(int i = 1 ; i <= 100 ; i++){ System.out.println("$$$ "+i); try { Thread.sleep(100); } catch (InterruptedException e) { e.printStackTrace(); } } } }; Runnable r3 = new Runnable(){ public void run(){ for(int i = 1 ; i <= 100 ; i++){ System.out.println("*** "+i); try { Thread.sleep(100); } catch (InterruptedException e) { e.printStackTrace(); } } } }; es.submit(r1);//線程池的提交方法,將線程對象提交來執行 es.submit(r2); es.submit(r3); es.shutdown();//關閉線程池 } }
此接口有個V call()方法,返回值V是泛型,返回值用Future對象來接收。Future中有個方法有get()方法能夠拿到返回值。
Callable有返回值,可拋異常,在多線程併發的時候,異步通訊,利用Future來接收返回值
package day20; import java.util.concurrent.Callable; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; import java.util.concurrent.Future; public class TestCallable { public static void main(String[] args) throws Exception{ ExecutorService es = Executors.newCachedThreadPool(); Callable<Integer> task1 = new Callable<Integer>(){ //Callable接口 public Integer call() throws Exception{ //方法call至關於run方法,只不過有返回值 System.out.println("task1 start working"); int result = 0 ; for(int i = 1 ; i < 100 ; i+=2){ result += i; Thread.sleep(100); } System.out.println("task1 end working"); return result; } }; Callable<Integer> task2 = new Callable<Integer>(){ public Integer call() throws Exception{ System.out.println("task2 start working"); int result = 0 ; for(int i = 2 ; i <= 100 ; i+=2){ result += i; Thread.sleep(100); } System.out.println("task2 end working"); return result; } }; Future<Integer> f1 = es.submit(task1);//返回值對象用Future來接收 Future<Integer> f2 = es.submit(task2); System.out.println("main do sth"); int result = f1.get()+f2.get(); System.out.println(result); es.shutdown(); } }
第二,線程中的基本概念,線程的生命週期
第三,單線程和多線程
第四,線程池的原理解析
第五,常見的幾種線程池的特色以及各自的應用場景
1、
線程,程序執行流的最小執行單位,是行程中的實際運做單位,
線程和進程究竟有什麼區別呢?
首先,進程是一個動態的過程,是一個活動的實體。簡單來講,一個應用程序的運行就能夠被看作是一個進程,
而線程,是運行中的實際的任務執行者。能夠說,進程中包含了多個能夠同時運行的線程。
2、
線程的生命週期,線程的生命週期能夠利用如下的圖解來更好的理解:
第一步,是用new Thread()的方法新建一個線程,在線程建立完成以後,線程就進入了就緒(Runnable)狀態,此時建立出來的線程進入搶佔CPU資源的狀態,當線程搶到了CPU的執行權以後,線程就進入了運行狀態(Running),當該線程的任務執行完成以後或者是很是態的調用的stop()方法以後,線程就進入了死亡狀態。而咱們在圖解中能夠看出,線程還具備一個則色的過程,這是怎麼回事呢?當面對如下幾種狀況的時候,容易形成線程阻塞,第一種,當線程主動調用了sleep()方法時,線程會進入則阻塞狀態,除此以外,當線程中主動調用了阻塞時的IO方法時,這個方法有一個返回參數,當參數返回以前,線程也會進入阻塞狀態,還有一種狀況,當線程進入正在等待某個通知時,會進入阻塞狀態。那麼,爲何會有阻塞狀態出現呢?咱們都知道,CPU的資源是十分寶貴的,因此,當線程正在進行某種不肯定時長的任務時,Java就會收回CPU的執行權,從而合理應用CPU的資源。咱們根據圖能夠看出,線程在阻塞過程結束以後,會從新進入就緒狀態,從新搶奪CPU資源。這時候,咱們可能會產生一個疑問,如何跳出阻塞過程呢?又以上幾種可能形成線程阻塞的狀況來看,都是存在一個時間限制的,當sleep()方法的睡眠時長過去後,線程就自動跳出了阻塞狀態,第二種則是在返回了一個參數以後,在獲取到了等待的通知時,就自動跳出了線程的阻塞過程
3、
什麼是單線程和多線程?
單線程,顧名思義便是隻有一條線程在執行任務,這種狀況在咱們平常的工做學習中不多遇到,因此咱們只是簡單作一下了解
多線程,建立多條線程同時執行任務,這種方式在咱們的平常生活中比較常見。可是,在多線程的使用過程當中,還有許多須要咱們瞭解的概念。好比,在理解上並行和併發的區別,以及在實際應用的過程當中多線程的安全問題,對此,咱們須要進行詳細的瞭解。
並行和併發:在咱們看來,都是能夠同時執行多種任務,那麼,到底他們兩者有什麼區別呢?
併發,從宏觀方面來講,併發就是同時進行多種時間,實際上,這幾種時間,並非同時進行的,而是交替進行的,而因爲CPU的運算速度很是的快,會形成咱們的一種錯覺,就是在同一時間內進行了多種事情
而併發,則是真正意義上的同時進行多種事情。這種只能夠在多核CPU的基礎下完成。
還有就是多線程的安全問題?爲何會形成多線程的安全問題呢?咱們能夠想象一下,若是多個線程同時執行一個任務,name意味着他們共享同一種資源,因爲線程CPU的資源不必定能夠被誰搶佔到,這是,第一條線程先搶佔到CPU資源,他剛剛進行了第一次操做,而此時第二條線程搶佔到了CPU的資源,name,共享資源還來不及發生變化,就同時有兩條數據使用了同一條資源,具體請參考多線程買票問題。這個問題咱們應該如何解決那?
有形成問題的緣由咱們能夠看出,這個問題主要的矛盾在於,CPU的使用權搶佔和資源的共享發生了衝突,解決時,咱們只須要讓一條線程戰歌了CPU的資源時,阻止第二條線程同時搶佔CPU的執行權,在代碼中,咱們只須要在方法中使用同步代碼塊便可。在這裏,同步代碼塊很少進行贅述,能夠自行了解。
四,線程池
又以上介紹咱們能夠看出,在一個應用程序中,咱們須要屢次使用線程,也就意味着,咱們須要屢次建立並銷燬線程。而建立並銷燬線程的過程勢必會消耗內存。而在Java中,內存資源是及其寶貴的,因此,咱們就提出了線程池的概念。
線程池:Java中開闢出了一種管理線程的概念,這個概念叫作線程池,從概念以及應用場景中,咱們能夠看出,線程池的好處,就是能夠方便的管理線程,也能夠減小內存的消耗。
那麼,咱們應該如何建立一個線程池那?Java中已經提供了建立線程池的一個類:Executor
而咱們建立時,通常使用它的子類:ThreadPoolExecutor.
public ThreadPoolExecutor(int corePoolSize,
int maximumPoolSize,
long keepAliveTime,
TimeUnit unit,
BlockingQueue<Runnable> workQueue,
ThreadFactory threadFactory,
RejectedExecutionHandler handler)
這是其中最重要的一個構造方法,這個方法決定了建立出來的線程池的各類屬性,下面依靠一張圖來更好的理解線程池和這幾個參數:
又圖中,咱們能夠看出,線程池中的corePoolSize就是線程池中的核心線程數量,這幾個核心線程,只是在沒有用的時候,也不會被回收,maximumPoolSize就是線程池中能夠容納的最大線程的數量,而keepAliveTime,就是線程池中除了核心線程以外的其餘的最長能夠保留的時間,由於在線程池中,除了核心線程即便在無任務的狀況下也不能被清除,其他的都是有存活時間的,意思就是非核心線程能夠保留的最長的空閒時間,而util,就是計算這個時間的一個單位,workQueue,就是等待隊列,任務能夠儲存在任務隊列中等待被執行,執行的是FIFIO原則(先進先出)。threadFactory,就是建立線程的線程工廠,最後一個handler,是一種拒絕策略,咱們能夠在任務滿了知乎,拒絕執行某些任務。
線程池的執行流程又是怎樣的呢?
有圖咱們能夠看出,任務進來時,首先執行判斷,判斷核心線程是否處於空閒狀態,若是不是,核心線程就先就執行任務,若是核心線程已滿,則判斷任務隊列是否有地方存放該任務,若果有,就將任務保存在任務隊列中,等待執行,若是滿了,再判斷最大可容納的線程數,若是沒有超出這個數量,就開創非核心線程執行任務,若是超出了,就調用handler實現拒絕策略。
handler的拒絕策略:
有四種:第一種AbortPolicy:不執行新任務,直接拋出異常,提示線程池已滿
第二種DisCardPolicy:不執行新任務,也不拋出異常
第三種DisCardOldSetPolicy:將消息隊列中的第一個任務替換爲當前新進來的任務執行
第四種CallerRunsPolicy:直接調用execute來執行當前任務
五,四種常見的線程池:
CachedThreadPool:可緩存的線程池,沒有核心線程,非核心線程的數量爲Integer.max_value,就是無限大,當有須要時建立線程來執行任務,沒有須要時回收線程,
適用於耗時少,任務量大的狀況。
SecudleThreadPool:週期性執行任務的線程池,按照某種特定的計劃執行線程中的任務,有核心線程,但也有非核心線程,非核心線程的大小也爲無限大。
適用於執行週期性的任務。
SingleThreadPool:只有一條線程來執行任務,
適用於有順序的任務的應用場景。
FixedThreadPool:定長的線程池,有核心線程,核心線程的即爲最大的線程數量,沒有非核心線程
Q:談談對線程池的認識?
A:在一個應用程序中咱們須要屢次使用線程,每一次線程的建立及銷燬都會消耗內存,而內存是寶貴的資源,使用線程池能夠方便的管理線程,也能夠減小內存的消耗。
java中提供了兩種建立線程池的方法:
java.util.concurrent.ThreadPoolExecutor類直接建立
new ThreadPoolExecutor(2, 4, 1, TimeUnit.SECONDS, new LinkedBlockingDeque());
1
java.util.concurrent.Executors類直接建立
ExecutorService executorService = Executors.newFixedThreadPool(1);
ExecutorService executorService1 = Executors.newCachedThreadPool();
ExecutorService executorService2 = Executors.newSingleThreadExecutor();
ScheduledExecutorService scheduledExecutorService = Executors.newScheduledThreadPool(6);
須要配置參數:
一、corePoolSize 線程池線程基本數量;
二、maximumPoolSize 線程池最大建立的最大線程數;
三、keepAliveTime 線程最大活躍時間;
四、TimeUnit 線程活躍的時間單位;
五、BlockingQueue 承載任務的阻塞隊列;
阻塞隊列分爲四種:
ArrayBlockingQueue 基於數組的有界阻塞隊列,實行先進先出規則(FIFO);
LinkedBlockingQueue 基於鏈表的阻塞隊列,實行先進先出規則(FIFO),newFixedThreadPool()方法建立的線程池使用此隊列,吞吐量大於ArrayBlockingQueue ;
SynchronousQueue 不存儲元素的阻塞隊列,每次插入必須等到另外一個線程調用移除操做,不然一直阻塞,newCachedThreadPool()方法建立的線程使用此隊列,吞吐量大於LinkedBlockingQueue ;
PriorityQueue 具備優先級的無界阻塞隊列;
六、RejectedExecutionHandler 拒絕策略,當線程池及隊列都飽和時採起的拒絕策略;
AbortPolicy 默認的拒絕策略,直接拋出異常;
CallerRunsPolicy 只有調用者所在線程執行任務;
DiscardOldestPolicy 拋棄隊列中最近的一個任務,執行當前任務;
DiscardPolicy 直接拋棄,不執行;
七、threadFactory 建立線程的工廠;
Q:怎麼建立自定義註解?
A:
@Documented//聲明式註解
@Inherited//聲明式註解
@Target({ElementType.METHOD,ElementType.TYPE})//定義註解的做用域
@Retention(RetentionPolicy.RUNTIME)//定義註解保留的時間
public @interface AnnotationTest {
public String value() default "";
}
一、newSingleThreadExecutor建立一個單線程化的線程池,它只會用惟一的工做線程來執行任務,保證全部任務按照指定順序(FIFO, LIFO, 優先級)執行。
二、newFixedThreadPool建立一個定長線程池,可控制線程最大併發數,超出的線程會在隊列中等待。
三、newScheduledThreadPool建立一個定長線程池,支持定時及週期性任務執行。
四、newCachedThreadPool建立一個可緩存線程池,若是線程池長度超過處理須要,可靈活回收空閒線程,若無可回收,則新建線程。
線程池不容許使用Executors去建立,而是經過ThreadPoolExecutor的方式,這樣的處理方式讓寫的同窗更加明確線程池的運行規則,規避資源耗盡的風險。 說明:Executors各個方法的弊端:1)newFixedThreadPool和newSingleThreadExecutor: 主要問題是堆積的請求處理隊列可能會耗費很是大的內存,甚至OOM。2)newCachedThreadPool和newScheduledThreadPool: 主要問題是線程數最大數是Integer.MAX_VALUE,可能會建立數量很是多的線程,甚至OOM。
一、ArrayBlockingQueue是一個基於數組結構的有界阻塞隊列,此隊列按 FIFO(先進先出)原則對元素進行排序。二、LinkedBlockingQueue一個基於鏈表結構的阻塞隊列,此隊列按FIFO (先進先出) 排序元素,吞吐量一般要高於ArrayBlockingQueue。靜態工廠方法Executors.newFixedThreadPool()使用了這個隊列三、SynchronousQueue一個不存儲元素的阻塞隊列。每一個插入操做必須等到另外一個線程調用移除操做,不然插入操做一直處於阻塞狀態,吞吐量一般要高於LinkedBlockingQueue,靜態工廠方法Executors.newCachedThreadPool使用了這個隊列。四、PriorityBlockingQueue一個具備優先級的無限阻塞隊列。
corePoolSize:核心池的大小,這個參數跟後面講述的線程池的實現原理有很是大的關係。在建立了線程池後,默認狀況下,線程池中並無任何線程,而是等待有任務到來才建立線程去執行任務,除非調用了prestartAllCoreThreads()或者prestartCoreThread()方法,從這2個方法的名字就能夠看出,是預建立線程的意思,即在沒有任務到來以前就建立corePoolSize個線程或者一個線程。默認狀況下,在建立了線程池後,線程池中的線程數爲0,當有任務來以後,就會建立一個線程去執行任務,當線程池中的線程數目達到corePoolSize後,就會把到達的任務放到緩存隊列當中;
maximumPoolSize:線程池最大線程數,這個參數也是一個很是重要的參數,它表示在線程池中最多能建立多少個線程;
keepAliveTime:表示線程沒有任務執行時最多保持多久時間會終止。默認狀況下,只有當線程池中的線程數大於corePoolSize時,keepAliveTime纔會起做用,直到線程池中的線程數不大於corePoolSize,即當線程池中的線程數大於corePoolSize時,若是一個線程空閒的時間達到keepAliveTime,則會終止,直到線程池中的線程數不超過corePoolSize。可是若是調用了allowCoreThreadTimeOut(boolean)方法,在線程池中的線程數不大於corePoolSize時,keepAliveTime參數也會起做用,直到線程池中的線程數爲0;
unit:參數keepAliveTime的時間單位,有7種取值,在TimeUnit類中有7種靜態屬性:
workQueue:一個阻塞隊列,用來存儲等待執行的任務,這個參數的選擇也很重要,會對線程池的運行過程產生重大影響,通常來講,這裏的阻塞隊列有如下幾種選擇:
ArrayBlockingQueue
LinkedBlockingQueue
SynchronousQueue
PriorityBlockingQueue
ArrayBlockingQueue和PriorityBlockingQueue使用較少,通常使用LinkedBlockingQueue和SynchronousQueue。線程池的排隊策略與BlockingQueue有關。
threadFactory:用於設置建立線程的工廠,能夠經過線程工廠給每一個建立出來的線程作些更有意義的事情,好比設置daemon和優先級等等
handler:表示當拒絕處理任務時的策略,有如下四種取值:
一、AbortPolicy:直接拋出異常。二、CallerRunsPolicy:只用調用者所在線程來運行任務。三、DiscardOldestPolicy:丟棄隊列裏最近的一個任務,並執行當前任務。四、DiscardPolicy:不處理,丟棄掉。五、也能夠根據應用場景須要來實現RejectedExecutionHandler接口自定義策略。如記錄日誌或持久化不能處理的任務。
線程池任務執行流程:
當線程池小於corePoolSize時,新提交任務將建立一個新線程執行任務,即便此時線程池中存在空閒線程。
當線程池達到corePoolSize時,新提交任務將被放入workQueue中,等待線程池中任務調度執行
當workQueue已滿,且maximumPoolSize>corePoolSize時,新提交任務會建立新線程執行任務
當提交任務數超過maximumPoolSize時,新提交任務由RejectedExecutionHandler處理
當線程池中超過corePoolSize線程,空閒時間達到keepAliveTime時,關閉空閒線程
當設置allowCoreThreadTimeOut(true)時,線程池中corePoolSize線程空閒時間達到keepAliveTime也將關閉
高併發架構相關概念 併發:在操做系統中,是指一個時間段中有幾個程序都處於已啓動運行到運行完畢之間,且這幾個程序都是在同一個處理機上運行,但任一個時刻點上只有一個程序在處理機上運行;在互聯網時代,所講的併發,高併發一般是指併發訪問,也就是在某個時間點,有多少個訪問同時到來。一般一個系統的日PV在千萬以上,有多是一個高併發的系統。有的公司徹底不走技術路線,全靠機器堆,這不在討論範圍內。 QPS:每秒鐘請求或者查詢的數量,在互聯網領域,指每秒響應請求數(指HTTP請求);併發鏈接數是系統同時處理的請求數量 吞吐量:單位時間內處理的請求數量(一般由QPS與併發數決定) 響應時間:從請求發出到收到響應花費的時間。例如系統處理一個HTTP請求須要100ms。 PV:綜合瀏覽量(page view),即頁面瀏覽量或者點擊量,一個訪客在24小時內訪問的頁面數量;同一我的瀏覽網站同一頁面,只記做一次PV UV:獨立訪客(unique visitor),即必定時間範圍內相同訪客屢次訪問網站,只計算爲一個獨立訪客 帶寬:計算帶寬大小需關注兩個指標,峯值流量和頁面的平均大小 日網站帶寬=PV/統計時間(換算到s)平均頁面大小(單位KB)8;峯值通常是平均值的倍數,根據實際狀況來定 峯值每秒請求數(QPS)=(總PV數80%)/(6小時秒數20%);80%的訪問量集中在20%的時間 壓力測試:測試能承受的最大併發,測試最大承受的QPS值 經常使用性能測試工具:ab,wrk,http_load,web_bench,siege,apache jmeter;ab全稱是apache benchmark,apache官方推出的工具,建立多個併發訪問線程,模擬多個訪問者同時對某一trl地址進行訪問,它的測試目標是基於url的,所以既能夠用來測試apache的負載能力,也能夠測試nginx,lighthttp,tomcat,IIS等其它web服務器的壓力;ab的使用:模擬併發請求100次,總共請求5000次,ab -c 100 -n 5000 待測試網站;測試機器與被測試機器分開,不要對線上服務作壓力測試,觀察測試工具ab所在機器,以及被測試的前端機的CPU,內存,網絡等都不超過最高限度的75% QPS達到極限:隨着QPS的增加,每一個階段須要根據實際狀況來進行優化,優化的方案也與硬件條件、網絡帶寬息息相關;QPS達到50,能夠稱之爲小型網站,通常的服務器就能夠應付;QPS達到100,假設關係型數據庫的每次請求在0.01s完成,假設單頁面只有一個SQL查詢,那麼100QPS意味着1s完成100次請求,可是此時並不能保證數據庫查詢能完成100次,數據庫緩存層,數據庫的負載均衡;QPS達到800,假設使用百兆帶寬,意味着網站出口的實際帶寬是8M左右,假設每一個頁面只有10k,在這個併發條件下,百兆帶寬已經吃完,CDN加速,負載均衡;QPS達到1000,假設使用memcache緩存數據庫查詢數據,每一個頁面對memcache的請求遠大於直接對db的請求,memcache的悲觀併發數在2w左右,但有可能在以前內網帶寬已經吃光,表現出不穩定,靜態HTML緩存;QPS達到2000,這個級別下,文件系統訪問鎖都成爲了災難,作業務分離,分佈式存儲
高併發解決方案案例 流量優化:防盜鏈處理 前端優化:減小HTTP請求,合併css或js,添加異步請求,啓用瀏覽器緩存和文件壓縮,CDN加速,創建獨立圖片服務器, 服務端優化:頁面靜態化,併發處理,隊列處理 數據庫優化:數據庫緩存,分庫分表,分區操做,讀寫分離,負載均衡 web服務器優化:負載均衡,nginx反向代理,7,4層LVS軟件
盜鏈:在本身的頁面上展現一些並不在本身服務器上的內容,得到他人服務器上的資源地址,繞過別人的資源展現頁面,直接在本身的頁面上向最終用戶提供此內容,常見的是小站盜用大站的圖片,音樂,視頻,軟件等資源,經過盜鏈的方法能夠減輕本身服務器的負擔,由於真實的空間和流量均是來自別人的服務器
防盜鏈:防止別人經過一些技術手段繞過本站的資源展現頁面,盜用本站的資源,讓繞開本站資源展現頁面的資源連接失效,能夠大大減輕服務器及帶寬的壓力
工做原理:經過請求頭中的referer或者簽名,網站能夠檢測目標網頁訪問的來源網頁,若是是資源文件,則能夠跟蹤到顯示它的網頁地址,一旦檢測到來源不是本站即進行阻止或者返回制定的頁面,經過計算簽名的方式,判斷請求是否合法,若是合法則顯示,不然返回錯誤信息
實現方法:referer:nginx模塊ngx_http_referer_module用於阻擋來源非法的域名請求,nginx指令valid_referers none | blocked | server_names | string...,none表示referer來源頭部爲空的狀況,blocked表示referer來源頭部不爲空,可是裏面的值被代理或者防火牆刪除了,這些值都不以http://或者https://開頭,server_names表示referer來源頭部包含當前的server_names,全局變量$invalid_referer。不能完全防範,只能提升門檻。也能夠針對目錄進行防盜鏈。
//在nginx的conf中配置location ~.*.(gif|jpg|png|flv|swf|rar|zip)$
{
valid_referers none blocked zi.com *.zi.com;
if($invalid_referer)
{
#return 403;
rewrite ^/ http://www.zi.com/403.jpg; }
}
傳統防盜鏈遇到的問題:僞造referer:能夠使用加密簽名解決
加密簽名:使用第三方模塊HttpAccessKeyModule實現Nginx防盜鏈。accesskey on|off 模塊開關,accesskey_hashmethod md5|sha-1 簽名加密方式,accesskey_arg GET參數名稱,accesskey_signature 加密規則,在nginx的conf中設置
location ~.*.(gif|jpg|png|flv|swf|rar|zip)$
{
accesskey on;
accesskey_hashmethod md5;
accesskey_arg sign;
accesskey_signature "jason$remote_addr";
}
<?php$sign = md5('jason'.$SERVER['REMOTE_ADDR']);echo '';
性能黃金法則:只有10%-20%的最終用戶響應時間花在接收請求的HTML文檔上,剩下的80%-90%時間花在HTML文檔所引用的全部組件(img,script,css,flash等)進行的HTTP請求上。
如何改善:改善響應時間的最簡單途徑就是減小組件的數量,並由此減小HTTP請求的數量
HTTP鏈接產生的開銷:域名解析--TCP鏈接--發送請求--等待--下載資源--解析時間
疑問:DNS緩存,查找DNS緩存也須要時間,多個緩存就要查找屢次有可能緩存會被清除;Keep-Alive,HTTP1.1協議規定請求只能串行發送,前面的一個請求完成才能開始下個請求
減小HTTP請求的方式:圖片地圖:容許在一個圖片上關聯多個URL,目標URL的選擇取決於用戶單擊了圖片上的哪一個位置,以位置信息定位超連接,把HTTP請求減小爲一個,能夠保證設計的完整性和功能的齊全性,使用map和area標籤;
<map name="map">
<area shape="rect" coords="0,0,30,30" href=... title="">
... </map>
CSS Sprites:CSS精靈,經過使用合併圖片,經過指定css的background-image和background-position來顯示元素。圖片地圖與css精靈的響應時間基本上相同,但比使用各自獨立圖片的方式要快50%以上
合併腳本和樣式表:使用外部的js和css文件引用的方式,由於這要比直接寫在頁面中性能要更好一點;獨立的一個js比用多個js文件組成的頁面載入要快38%;把多個腳本合併爲一個腳本,把多個樣式表合併爲一個樣式表
圖片使用base64編碼減小頁面請求數:採用base64的編碼方式將圖片直接嵌入到網頁中,而不是從外部載入
HTTP緩存機制:若是請求成功會有三種狀況:200 from cache:直接從本地緩存中獲取相應,最快速,最省流量,由於根本沒有向服務器進行請求;304 not modified:協商緩存,瀏覽器在本地沒有命中的狀況下請求頭中發送必定的校驗數據到服務端,若是服務端數據沒有改變瀏覽器從本地緩存響應,返回304,快速,發送的數據不多,只返回一些基本的響應頭信息,數據量很小,不發送實際響應體;200 OK:以上兩種緩存所有失敗,服務器返回完整響應,沒有用到緩存,相對最慢。
瀏覽器認爲本地緩存能夠使用,不會去請求服務端。相關header:pragma:HTTP1.0時代的遺留產物,該字段被設置爲no-cache時,會告知瀏覽器禁用本地緩存,即每次都向服務器發送請求;expires:HTTP1.0時代用來啓用本地緩存的字段,瀏覽器與服務器的時間沒法保持一致,若是時間差距大,就會影響緩存結果;cache-control:HTTP1.1針對expires時間不一致的解決方案,告知瀏覽器緩存過時的時間間隔而不是時刻,即便具體時間不一致,也不影響緩存的管理;能夠設置的值:no-store:禁止瀏覽器緩存響應;no-cache:不容許直接使用本地緩存,先發起請求和服務器協商;max-age=delta-seconds:告知瀏覽器該響應本地緩存有效的最長期限,以秒爲單位;優先級:pragma >cache-control > expires。當瀏覽器沒有命中本地緩存,如本地緩存過時或者響應中聲明不容許直接使用本地緩存,那麼瀏覽器確定會發起服務端請求;
服務端會驗證數據是否修改,若是沒有通知瀏覽器使用本地緩存。相關header:last-modified:通知瀏覽器資源的最後修改時間;if-modified-since:獲得資源的最後修改時間後,會將這個信息經過它提交到服務器作檢查,若是沒有修改,返回304狀態碼;ETag:HTTP1.1推出,文件的指紋標識符,若是文件內容修改,指紋會改變;if-none-match:本地緩存失效,會攜帶此值去請求服務端,服務端判斷該資源是否改變,若是沒有改變,直接使用本地緩存,返回304
緩存策略的選擇:適合緩存的內容:不變的圖像,如logo,圖標等,js,css靜態文件,可下載的內容,媒體文件;建議使用協商緩存:html文件,常常替換的圖片,常常修改的js,css文件,js和css文件的加載能夠加入文件的簽名來拒絕緩存,如a.css?簽名或a.簽名.js;不建議緩存的內容:用戶隱私等敏感數據,常常改變的api數據接口
nginx配置緩存策略: 本地緩存配置:add_header指令:添加狀態碼爲2xx和3xx的響應頭信息,add_header name value [always];,能夠設置Pragma/Expires/Cache-Control,能夠繼承;expires指令:通知瀏覽器過時時長,expires time;,爲負值時表示Cache-Control: no-cache;,當爲正或者0時,就表示Cache-Control: max-age=指定的時間;;當爲max時,Cache-Control設置到10年; 協商緩存相關配置:Etag指令:指定簽名;etag on|off;,默認是on
前端代碼和資源的壓縮:讓資源文件更小,加快文件在網絡中的傳輸,讓網頁更快的展示,下降帶寬和流量開銷;壓縮方式:js,css,圖片,html代碼的壓縮,Gzip壓縮。js代碼壓縮:通常是去掉多餘的空格和回車,替換長變量名,簡化一些代碼寫法等,代碼壓縮工具不少UglifyJS(壓縮,語法檢查,美化代碼,代碼縮減,轉化)、YUI Compressor(來自yahoo,只有壓縮功能)、Closure Compiler(來自google,功能和UglifyJS相似,壓縮的方式不同),有在線工具tool.css-js.com,應用程序,編輯器插件。css代碼壓縮:原理和js壓縮原理相似,一樣是去除空白符,註釋而且優化一些css語義規則等,壓縮工具CSS Compressor(能夠選擇模式)。html代碼壓縮:不建議使用代碼壓縮,有時會破壞代碼結構,能夠使用Gzip壓縮,固然也能夠使用htmlcompressor工具,不過轉換後必定要檢查代碼結構。img壓縮:通常圖片在web系統的比重都比較大,壓縮工具:tinypng,JpegMini,ImageOptim。Gzip壓縮:配置nginx服務,gzip on|off,gzip_buffers 32 4K|16 8K #緩衝(在內存中緩存幾塊?每塊多大),gzip_comp_level [1-9] #推薦6 壓縮級別(級別越高,壓的越小,越浪費CPU計算資源),gzip_disable #正則匹配UA 什麼樣的uri不進行gzip,gzip_min_length 200 #開始壓縮的最小長度,gzip_http_version 1.0|1.1 #開始壓縮的http協議版本,gzip_proxied #設置請求者代理服務器,該如何緩存內容,gzip_types text/plain applocation/xml #對哪些類型的文件用壓縮,gzip_vary on|off #是否傳輸gzip壓縮標誌。其餘工具:自動化構建工具Grunt。
CDN:Content Delivery Network,內容分發網絡,儘量避開互聯網上有可能影響數據傳輸速度和穩定性的瓶頸和環節,使內容傳輸的更快更穩定;在網絡各處放置節點服務器所構成的在現有的互聯網基礎之上的一層智能虛擬網絡;CDN系統可以實時的根據網絡流量和各節點的鏈接,負載狀況以及到用戶的距離和響應時間等綜合信息將用戶的請求從新導向離用戶最近的服務節點上。本地cache加速,提升了企業站點(尤爲含有大量img和靜態頁面站點)的訪問速度;跨運營商的網絡加速,保證不一樣網絡的用戶都獲得良好的訪問質量;遠程訪問用戶根據DNS負載均衡技術智能自動選擇cache服務器;自動生成服務器的遠程Mirror cache服務器,遠程用戶訪問時從cache服務器上讀數據,減小遠程訪問的帶寬,分擔網絡流量,減輕原站點web服務器負載等功能;普遍分佈的CDN節點加上節點之間的只能智能冗餘機制,能夠有效的預防黑客入侵
CDN的工做原理:傳統訪問:用戶在瀏覽器輸入域名發起請求--解析域名獲取服務器IP地址--根據IP地址找到對應的服務器--服務器響應並返回數據;使用CDN訪問:用戶發起請求--智能DNS的解析(根據IP判斷地理位置,接入網類型,選擇路由最短和負載最輕的服務器)--取得緩存服務器IP--把內容返回給用戶(若是緩存中有)--向源站發起請求--將結果返回給用戶--將結果存入緩存服務器
CDN適用場景:站點或者應用中大量靜態資源的加速分發,如css,js,img和html;大文件下載;直播網站等
CDN的實現:BAT等都有提供CDN服務,可用LVS作4層負載均衡;可用nginx,Varnish,Squid,Apache TrafficServer作7層負載均衡和cache;使用squid反向代理,或者nginx等的反向代理
獨立的必要性:分擔web服務器的I/O負載-將耗費資源的圖片服務分離出來,提升服務器的性能和穩定性;可以專門的圖片服務器進行優化-爲圖片服務設置有針對性的緩存方案,減小帶寬成本,提升訪問速度;提升網站的可擴展性-經過增長圖片服務器,提升圖片吞吐能力
採用獨立域名:緣由:同一域名下瀏覽器的併發鏈接數有限制,突破瀏覽器鏈接數的限制;因爲cookie的緣由,對緩存不利,大部分web cache都只緩存不帶cookie的請求,致使每次的圖片請求都不能命中cache
獨立後的問題:如何進行圖片上傳和圖片同步:NFS共享方式;利用FTP同步
將現有PHP等動態語言的邏輯代碼生成爲靜態HTML文件,用戶訪問動態腳本重定向到靜態HTML文件的過程。對實時性要求不高的頁面比較適合。緣由:動態腳本一般會作邏輯計算和數據查詢,訪問量越大,服務器壓力越大;訪問量大時可能會形成CPU負載太高,數據庫服務器壓力過大;靜態化能夠下降邏輯處理壓力,下降數據庫服務器查詢壓力
靜態化的實現方式: 使用模板引擎:能夠使用smarty的緩存機制生成靜態HTML緩存文件;$smarty->cache-dir = $ROOT."/cache";//緩存目錄,$smarty->caching=true;//是否開啓緩存,$smarty->cache_lifetime="3600";//緩存時間,$smarty->display(string template[, string cache_id[, string compile_id]]);,$smarty->clear_all_cache();//清除全部緩存,$smarty->clear_cache('a.html');//清除指定的緩存,$smarty->clear_cache('a.html', $art_id);//清除同一個模板下的指定緩存號的緩存 利用ob系列的函數:ob_start():打開輸出控制緩衝,ob_ge_contents():返回輸出緩衝區內容,ob_clean():清空輸出緩衝區,ob_end_flush():沖刷出(送出)輸出緩衝區內容並關閉緩衝,能夠判斷文件的inode修改時間,判斷是否過時使用filectime函數
進程:計算機中的程序關於某數據集合上的一次運行活動,是系統進行資源分配和調度的基本單位,是操做數據結構的基礎,是一個「執行中的程序」;進程的三態模型:多道程序系統中,進程在處理器上交替運行,狀態不斷的發生變化;運行:當一個進程在處理機上運行時,稱該進程處於運行狀態,處於此狀態的進程的數目小於等於處理器的數目,對於單處理機系統,處於運行狀態的進程只有一個,在沒有其餘進程能夠執行時(如全部進程都在阻塞狀態),一般會自動執行系統的空閒進程;就緒:當一個進程得到了除處理機之外的一切所需資源,一旦獲得處理機便可運行,則稱此進程處於就緒狀態,就緒進程能夠按多個優先級來劃分隊列,如當一個進程因爲時間片用完而進入就緒狀態時,排入低優先級隊列,當進程由I/O操做完成而進入就緒狀態時,排入高優先級隊列;阻塞:也稱爲等待或睡眠狀態,一個進程正在等待某一事件發生(如請求I/O而等待I/O完成等)而暫時中止運行,這時即便把處理機分配給進程也沒法運行;進程的五態模型:對於一個實際的系統,進程的狀態及其轉換更爲複雜,新建態:對應於進程剛剛被建立時沒有被提交的狀態,並等待系統完成建立進程的全部必要信息;活躍就緒/靜止就緒:進程在主存而且可被調度的狀態/指進程被對換到輔存時的就緒狀態,是不能被直接調度的狀態,只有當主存中沒有活躍就緒態進程,或者是掛起就緒態進程具備更高的優先級,系統將把掛起就緒態進程調回主存並轉換爲活躍就緒;運行,活躍阻塞/靜止阻塞:指進程已在主存,一旦等待的時間產生便進入活躍就緒狀態/進程對換到輔存時的阻塞狀態,一旦等待的事件產生便進入靜止就緒狀態;終止態:進程已結束運行,回收除進程控制塊以外的其餘資源,並讓其餘進程從進程控制塊中收集有關信息;因爲用戶的併發請求,爲每個請求都建立一個進程顯然是行不通的,從系統資源開銷方面或是響應用戶請求的效率方面來看,所以線程的概念被引進。
線程:有時被稱爲輕量級進程,是程序執行流的最小單元。是進程中的一個實體,是被系統獨立調度和分派的基本單位,本身不擁有系統資源,只擁有一點在運行中必不可少的資源但它可與同屬一個進程的其它進程共享進程所擁有的所有資源。一個線程能夠建立和撤銷另外一個線程,同一進程中的多個線程之間能夠併發執行。線程是程序中一個單一的順序控制流程,進程內一個相對獨立的、可調度的執行單元,是系統獨立調度和分派CPU的基本單位指運行中的程序的調度單位。在單個程序中同時運行多個線程完成不一樣的工做成爲多線程。每個程序都至少有一個線程,若程序只有一個線程,那就是程序自己。線程的狀態:就緒:線程具有運行的全部條件,邏輯上能夠運行,在等待處理機;運行:線程佔有處理機正在運行;阻塞:線程在等待一個事件(如某個信號量),邏輯上不可執行。
協程:是一種用戶態的輕量級線程,調度徹底由用戶控制;協程擁有本身的寄存器上下文和棧;協程調度切換時,將寄存器上下文和棧保存到其餘地方,在切回來的時候,恢復先前保存的寄存器上下文和棧,直接操做棧則基本沒有內核切換的開銷,能夠不加鎖的訪問全局變量,因此上下文的切換很是快。
進程和線程的區別:線程是進程內的一個執行單元,進程內至少有一個線程,共享進程的地址空間,而進程有本身獨立的地址空間;進程是資源分配和擁有的單元,同一個進程內的線程共享進程的資源;線程是處理器調度的基本單位,但進程不是;兩者都可併發執行;每一個獨立的線程有一個程序運行的入口,順序執行序列和程序的出口,可是線程不可以獨立執行,必須依存在應用程序中,由應用程序提供多個線程執行控制
線程和協程的區別:一個線程能夠多個協程,一個進程也能夠單獨擁有多個協程;進程線程都是同步機制,而協程則是異步;協程能保留上一次調用時的狀態,每次過程重入時,就至關於進入上一次調用的狀態。
多進程:同一時間裏,同一個計算機系統中若是容許兩個或兩個以上的進程處於運行狀態;多開一個進程,多分配一份資源,進程間通信不方便;
多線程:線程就是把一個進程分爲不少片,每一片均可以是一個獨立的流程,與多進程的區別是隻會使用一個進程的資源,線程間能夠直接通訊;
同步阻塞:多進程:最先的服務器端程序都是經過多進程,多線程來解決併發I/O的問題;一個請求建立一個進程,而後子進程進入循環同步阻塞地與客戶端鏈接進行交互,收發處理數據;多線程:線程中能夠直接向某一個客戶端鏈接發送數據;步驟:建立一個socket,進入while循環,阻塞在進程accept操做上,等待客戶端鏈接進入,主進程在多進程模型下經過fork建立子進程,多線程模型下能夠建立子線程,子進程/線程建立成功後進入while循環,阻塞在recv調用上,等待客戶端向服務器發送數據,收到數據後服務器程序進行處理而後使用send向客戶端發送響應,當客戶端鏈接關閉時,子進程/線程退出並銷燬全部資源。主進程/線程會回收掉此子進程/線程;缺點:這種模型嚴重依賴進程的數量解決併發問題,啓動大量進程會帶來額外的進程調度消耗
異步非阻塞:如今各類高併發異步IO的服務器程序都是基於epoll(無限數量鏈接,無需輪詢)實現的。IO複用異步非阻塞程序使用經典的Reactor模型,Reactor顧名思義就是反應堆的意思,它自己不處理任何數據收發,只是能夠監視一個socket句柄的事件變化。Reactor模型:Add:添加一個socket到Reactor,Set:修改socket對應的事件,如可讀可寫,Del:從Reactor中移除,Callback:事件發生後回調指定的函數。Nginx:多線程Reactor,swoole:多線程Reactor+多進程Worker
PHP的swoole擴展:PHP的異步,並行,高性能網絡通訊引擎,使用純c語言編寫,提供了PHP語言的異步多線程服務器,異步TCP/UDP網絡客戶端,異步mysql,異步redis,數據庫鏈接池,asynctask,消息隊列,毫秒定時器,異步文件讀寫,異步DNS查詢;除了異步IO的支持以外,swoole爲PHP多進程的模式設計了多個併發數據結構和IPC通訊機制,能夠大大簡化多進程併發編程的工做;swoole2.0支持了相似Go語言的協程,能夠使用徹底同步的代碼實現異步程序
消息隊列:用戶註冊後,須要發註冊郵件和註冊短信;串行方式:將註冊信息寫入數據庫成功後,發送註冊郵件,再發送註冊短信;並行方式:將註冊信息寫入數據庫成功後,發送註冊郵件的同時,發送註冊短信;消息隊列方式:將註冊信息寫入數據庫成功後,將成功信息寫入隊列,此時直接返回成功給用戶,寫入隊列的時間很是短,能夠忽略不計,而後異步發送郵件和短信。應用解耦:場景說明:用戶下單後,訂單系統須要通知庫存系統。假如庫存系統沒法訪問,則訂單減庫存將失敗,從而致使訂單失敗;訂單系統與庫存系統耦合;引用隊列:用戶下單後,訂單系統完成持久化處理,將消息寫入消息隊列,返回用戶訂單下單成功,訂閱下單的消息,採用拉/推的方式,獲取下單信息,庫存系統根據下單信息,進行庫存操做。流量削峯:應用場景:秒殺活動,流量瞬時激增,服務器壓力大。用戶發起請求,服務器接收後,先寫入消息隊列,假如消息隊列長度超過最大值,則直接報錯或提示用戶,後續程序讀取消息隊列再作處理,控制請求量,緩解高流量。日誌處理:應用場景:解決大量日誌的傳輸。日誌採集程序將程序寫入消息隊列,而後經過日誌處理程序的訂閱消費日誌。消息通信:應用場景:聊天室。多個客戶端訂閱同一主題,進行消息發佈和接收。常見消息隊列產品:Kafka,ActiveMQ,ZeroMQ,RabbitMQ,Redis等
接口的併發請求:curl_multi_init
mysql等一些常見的關係型數據庫的數據都存儲在磁盤當中,在高併發場景下,業務應用對mysql產生的增刪改查的操做形成的巨大的IO開銷和查詢壓力,這無疑對數據庫和服務器都是一種巨大的壓力,爲了解決此類問題,緩存數據的概念應運而生。極大的解決數據庫服務器的壓力,提升應用數據的響應速度。常見的緩存形式:內存緩存,文件緩存。
緩存數據是爲了讓客戶端不多甚至不訪問數據庫服務器進行數據的查詢,高併發下,能最大程度的下降對數據庫服務器的訪問壓力。默認狀況下:用戶請求->數據查詢->鏈接數據庫服務器並查詢數據->將數據緩存起來(html,內存,json,序列化數據)->顯示給客戶端;用戶再次請求或者新用戶訪問->數據查詢->直接從緩存中獲取數據->顯示給客戶端
mysql的查詢緩存:query_cache_type:查詢緩存類型,有0,1,2三個取值,0則不使用查詢緩存,1表示始終使用查詢緩存,2表示按需使用查詢緩存。query_cache_type爲1時,亦可關閉查詢緩存,SELECT SQL_NO_CACHE * FROM my_table WHERE condition。query_cache_type爲2時,可按需使用查詢緩存,SELECT SQL_CACHE * FROM my_table WHERE condition。query_cache_size:默認狀況下爲0,表示爲查詢緩存預留的內存爲0,則沒法使用查詢緩存。SET GLOBAL query_cache_size = 134217728。查詢緩存能夠看作是SQL文本和查詢結果的映射。第二次查詢的SQL和第一次查詢的SQL徹底相同,則會使用緩存。SHOW STATUS LIKE 'Qcache_hits'; 查看命中次數。表的結構或數據發生改變時,查詢緩存中的數據再也不有效。清理緩存:FLUSH QUERY CACHE; //清理查詢緩存內存碎片,RESET QUERY CACHE; //從查詢緩存中移出全部查詢,FLUSH TABLES; //關閉全部打開的表,同時該操做將會清空查詢緩存中的內容
使用memcache緩存查詢數據:對於大型站點,若是沒有中間緩存層,當流量打入數據庫層時,即使有以前的幾層爲咱們擋住一部分流量,可是在大併發的狀況下,仍是會有大量請求涌入數據庫層,這樣對於數據庫服務器的壓力衝擊很大,響應速度也會降低,所以添加中間緩存層頗有必要。memcache是一套分佈式的高速緩存系統,由livejournal的bradfitzpatrick開發,但目前被許多網站使用以提高網站的訪問速度,尤爲對於一些大型的,須要頻繁訪問數據庫的網站訪問速度提高效果十分顯著。
memcache工做原理:是一個高性能的分佈式的內存對象緩存系統,經過在內存裏維護一個統一的巨大的hash表,可以用來存儲各類格式的數據,包括圖像,視頻,文件以及數據庫檢索的結果等,簡單的說就是將數據調用到內存,而後從內存中讀取,從而大大提升讀取速度。
memcache工做流程:先檢查客戶端的請求數據是否在memcache中,若有,直接把請求數據返回,再也不對數據庫進行任何操做;若是請求的數據不在memcache中,就去查數據庫,把從數據庫中獲取的數據返回給客戶端,同時把數據緩存一份到memcache中。
memcache方法:獲取:get(key) 設置:set(key, val, expire) 刪除:delete(key)
通用緩存機制:用查詢的方法名+參數做爲查詢時的key value對中的key值
使用redis緩存查詢數據:與memcache的區別:性能相差不大,redis在2.0版本後增長了本身的VM特性,突破物理內存的限制,memcache能夠修改最大可用內存,採用LRU算法;redis依賴客戶端來實現分佈式讀寫,memcache自己沒有數據冗餘機制;redis支持快照,AOF,依賴快照進行持久化,aof加強了可靠性的同時,對性能有所影響,memcache不支持持久化,一般作緩存,提高性能;memcache在併發場景下,用cas保證一致性,redis事務支持比較弱,只能保證事務中的每一個操做連續執行;redis支持多種類的數據類型;redis用於數據量較小的高性能操做和運算上,memcache用於在動態系統中減小數據庫負載,提高性能,適合作緩存,提升性能
緩存其餘數據:session:session_set_save_handler
優化方向:數據表數據類型優化,索引優化,sql語句優化,存儲引擎的優化,數據表結構設計的優化,數據庫服務器架構的優化
數據表數據類型優化:字段使用什麼樣的數據類型更合適,性能更快,tinyint、smallint、bigint,考慮空間和範圍的問題;char、varchar,存儲字符串長度是否固定;enum,特定固定的分類能夠使用enum存儲,效率更快;IP地址的存儲,ip2long(),使用整型存儲IP地址
索引的優化:創建合適的索引,索引在什麼場景下效率最高,索引的建立原則:不是越多越好,在合適的字段上建立合適的索引,複合索引的前綴原則,like查詢%的問題,全表掃描優化,or條件索引使用狀況,字符串類型索引失效的問題
sql語句的優化:優化查詢過程當中的數據訪問,優化長難句、特定類型的查詢語句。使用limit,返回列不用*,變複雜爲簡單,切分查詢,分解關聯查詢,優化count(),優化關聯查詢,優化子查詢,優化group by和distinct,優化limit和union
存儲引擎的優化:儘可能使用innoDB存儲引擎
數據表結構設計的優化:分區操做,經過特定的策略對數據表進行物理拆分,對用戶透明,partition by;分庫分表,水平拆分,垂直拆分
數據庫架構的優化:主從複製,讀寫分離,雙主熱備,binlog日誌,中繼日誌,主從庫binlog的交換,事件傳輸;負載均衡,經過LVS的三種基本模式實現負載均衡,mycat數據庫中間件實現負載均衡
七層負載均衡的實現:基於URL等應用層信息的負載均衡,nginx的proxy是它一個很強大的功能,實現了7層負載均衡,功能強大,性能卓越,運行穩定,配置簡單靈活,可以自動剔除工做不正常的後端服務器,上傳文件使用異步模式,支持多種分配策略,能夠分配權重,分配方式靈活。
nginx負載均衡:內置策略:IP Hash,加權輪詢;擴展策略:fair策略,通用hash,一致性hash 加權輪詢:首先將請求都分給高權重的機器,直到該機器的權值降到了比其餘機器低,纔開始將請求分給下一個高權重的機器,當全部後端機器都down掉時,nginx會當即將全部機器的標誌位清成初始狀態,以免形成全部的機器都處在timeout的狀態;IP Hash:流程和輪詢很相似,只是其中的算法和具體的策略有些變化,算法是一種變相的輪詢算法;fair:根據後端服務器的響應時間判斷負載狀況,從中選出負載最輕的機器進行分流;通用hash,一致性hash:通用hash比較簡單,能夠以nginx內置的變量爲key進行hash,一致性hash採用了nginx內置的一致性hash環,支持memcache
nginx配置:
http {
upstream cluster{ #ip_hash;
server srv1 weight=1;
server srv2;
server srv3;
}
server {
listen 80;
location / {
proxy_pass http://cluster;
}
}
}
四層負載均衡的實現:經過報文中的目標地址和端口,再加上負載均衡設備設置的服務器選擇方式,決定最終選擇的內部服務器。LVS實現服務器集羣負載均衡有三種方式,NAT,DR和TUN。