線程數量
在併發程序中,並不是啟動更多的線程就能讓程序最大限度地併發執行線程數量設置太小,會導致程序不能充分地利用系統資源線程數量設置太大,可能帶來資源的過度競爭,導致上下文切換,帶來的額外的系統開銷上下文切換
1.在單處理器時期,操作系統就能處理多線程併發任務,處理器給每個線程分配CPU時間片,線程在CPU時間片內執行任務
CPU時間片是CPU分配給每個線程執行的時間段,一般為幾十毫秒2.時間片決定了一個線程可以連續佔用處理器運行的時長
當一個線程的時間片用完,或者因自身原因被迫暫停運行,此時另一個線程會被操作系統選中來佔用處理器3.上下文的內容
寄存器的存儲內容:CPU寄存器負責存儲已經、正在和將要執行的任務程序計數器存儲的指令內容:程序計數器負責存儲CPU正在執行的指令位置、即將執行的下一條指令的位置4.當CPU數量遠遠不止1個的情況下,操作系統將CPU輪流分配給線程任務,此時的上下文切換會變得更加
切換誘因
1.在操作系統中,上下文切換的類型可以分為進程間的上下文切換和線程間的上下文切換
2.線程狀態:NEW、RUNNABLE、RUNNING、BLOCKED、DEAD
Java線程狀態:NEW、RUNNABLE、BLOCKED、WAITING、TIMED_WAITING、TERMINATED3.線程上下文切換:RUNNING -> BLOCKED -> RUNNABLE -> 被調度器選中執行
一個線程從RUNNING狀態轉為BLOCKED狀態,稱為一個線程的暫停線程暫停被切出後,操作系統會保存相應的上下文以便該線程再次進入RUNNABLE狀態時能夠在之前執行進度的基礎上繼續執行一個線程從BLOCKED狀態進入RUNNABLE狀態,稱為一個線程的喚醒4.誘因:程序本身觸發的自發性上下文切換、系統或虛擬機觸發的非自發性上下文切換
自發性上下文切換sleep、wait、yield、join、park、synchronized、lock非自發性上下文切換線程被分配的時間片用完、JVM垃圾回收(STW、線程暫停)、線程執行優先級監控切換
樣例代碼
public static void main(String[] args) { new MultiThreadTesterAbstract().start(); new SerialThreadTesterAbstract().start(); // multi thread take 5401ms // serial take 692ms } static abstract class AbstractTheadContextSwitchTester { static final int COUNT = 100_000_000; volatile int counter = 0; void increaseCounter() { counter++; } public abstract void start(); } static class MultiThreadTesterAbstract extends AbstractTheadContextSwitchTester { @Override public void start() { Stopwatch stopwatch = Stopwatch.createStarted(); Thread[] threads = new Thread[4]; for (int i = 0; i < 4; i++) { threads[i] = new Thread(new Runnable() { @Override public void run() { while (counter < COUNT) { synchronized (this) { if (counter < COUNT) { increaseCounter(); } } } } }); threads[i].start(); } for (int i = 0; i < 4; i++) { try { threads[i].join(); } catch (InterruptedException e) { e.printStackTrace(); } } log.info("multi thread take {}ms", stopwatch.elapsed(TimeUnit.MILLISECONDS)); } } static class SerialThreadTesterAbstract extends AbstractTheadContextSwitchTester { @Override public void start() { Stopwatch stopwatch = Stopwatch.createStarted(); for (int i = 0; i < COUNT; i++) { increaseCounter(); } log.info("serial take {}ms", stopwatch.elapsed(TimeUnit.MILLISECONDS)); } }
1.串行的執行速度比並發執行的速度要快,因為線程的
2.Redis的設計很好地體現了單線程串行的優勢
從內存中快速讀取值,不用考慮IO瓶頸帶來的阻塞問題監控工具
vmstat
cs:系統的上下文切換頻率
root@5d15480e8112:/# vmstat procs -----------memory---------- ---swap-- -----io---- -system-- ------cpu----- r b swpd free buff cache si so bi bo in cs us sy id wa st 3 0 0 693416 33588 951508 0 0 77 154 116 253 1 1 98 0 0
pidstat
-w Report task switching activity (kernels 2.6.23 and later only). The following values may be displayed: UID The real user identification number of the task being monitored. USER The name of the real user owning the task being monitored. PID The identification number of the task being monitored. cswch/s Total number of voluntary context switches the task made per second. A voluntary context switch occurs when a task blocks because it requires a resource that is unavailable. nvcswch/s Total number of non voluntary context switches the task made per second. A involuntary context switch takes place when a task executes for the duration of its time slice and then is forced to relinquish the processor. Command The command name of the task.
root@5d15480e8112:/# pidstat -w -l -p 1 2 5 Linux 4.9.184-linuxkit (5d15480e8112) 09/16/2019 _x86_64_ (2 CPU) 07:28:03 UID PID cswch/s nvcswch/s Command 07:28:05 0 1 0.00 0.00 /bin/bash 07:28:07 0 1 0.00 0.00 /bin/bash 07:28:09 0 1 0.00 0.00 /bin/bash 07:28:11 0 1 0.00 0.00 /bin/bash 07:28:13 0 1 0.00 0.00 /bin/bash Average: 0 1 0.00 0.00 /bin/bash
切換的系統開銷
操作系統保存和恢復上下文調度器進行線程調度處理器高速緩存重新加載可能導致整個高速緩存區被沖刷,從而帶來時間開銷競爭鎖優化
多線程對鎖資源的競爭會引起上下文切換,鎖競爭導致的線程阻塞越多,上下文切換就越頻繁,系統的性能開銷就越大在多線程編程中,鎖本身不是性能開銷的根源,鎖競爭才是性能開銷的根源鎖優化歸根到底是減少競爭減少鎖的持有時間
鎖的持有時間越長,意味著越多的線程在等待該競爭鎖釋放如果是synchronized同步鎖資源,不僅帶來了線程間減少鎖粒度
鎖分離
讀寫鎖實現了鎖分離,由讀鎖和寫鎖兩個鎖實現,可以共享讀,但只有一個寫讀寫鎖在多線程讀寫時,讀讀不互斥,讀寫互斥,寫寫互斥傳統的獨佔鎖在多線程讀寫時,讀讀互斥,讀寫互斥,寫寫互斥在讀遠大於寫的多線程場景中,鎖分離避免了高併發讀情況下的資源競爭,從而避免了上下文切換鎖分段
在使用鎖來保證非阻塞樂觀鎖代替競爭鎖
volatilevolatile關鍵字的作用是保證可見性和有序性,volatile的讀寫操作不會導致上下文切換,開銷較小由於volatile關鍵字沒有鎖的排它性,因此不能保證操作變量的原子性CASCAS是一個原子的if-then-act操作CAS是一個無鎖算法實現,保障了對一個共享變量讀寫操作的一致性CAS不會導致上下文切換synchronized鎖優化
在JDK 1.6中,JVM將synchronized同步鎖分為偏向鎖、輕量級鎖、自旋鎖、重量級鎖JIT編譯器在動態編譯同步代碼塊時,也會通過鎖消除、鎖粗化的方式來優化synchronized同步鎖wait/notify優化
可以通過Object對象的wait、notify、notifyAll來實現線程間的通信,例如生產者-消費者模型
public class WaitNotifyTest { public static void main(String[] args) { Vector pool = new Vector<>(); Producer producer = new Producer(pool, 10); Consumer consumer = new Consumer(pool); new Thread(producer).start(); new Thread(consumer).start(); } } @AllArgsConstructor class Producer implements Runnable { private final Vector pool; private Integer size; @Override public void run() { for (; ; ) { try { produce((int) System.currentTimeMillis()); } catch (InterruptedException e) { e.printStackTrace(); } } } private void produce(int i) throws InterruptedException { while (pool.size() == size) { synchronized (pool) { pool.wait(); } } synchronized (pool) { pool.add(i); pool.notifyAll(); } } } @AllArgsConstructor class Consumer implements Runnable { private final Vector pool; @Override public void run() { for (; ; ) { try { consume(); } catch (InterruptedException e) { e.printStackTrace(); } } } private void consume() throws InterruptedException { synchronized (pool) { while (pool.isEmpty()) { pool.wait(); } } synchronized (pool) { pool.remove(0); pool.notifyAll(); } } }
1.wait/notify的使用導致了較多的上下文切換
2.消費者第一次申請到鎖,卻發現沒有內容可消費,執行wait,這會導致線程掛起,進入阻塞狀態
3.當生產者獲得鎖並執行notifyAll之後,會喚醒處於阻塞狀態的消費者線程,又會發生一次上下文切換
4.被喚醒的線程在繼續運行時,需要再次申請相應對象的內部鎖,此時可能需要與其他新來的活躍線程競爭,導致上下文切換
5.如果多個消費者線程同時被阻塞,用notifyAll將喚醒所有阻塞線程,但此時依然沒有內容可消費
因此過早地喚醒,也可能導致線程再次進入阻塞狀態,從而引起不必要的上下文切換6.優化方法
可以考慮使用notify代替notifyAll,減少上下文切換生產者執行完notify/notifyAll之後,儘快釋放內部鎖,避免被喚醒的線程再次等待該內部鎖為了避免長時間等待,使用wait(long),但線程無法區分其返回是由於等待超時