一.壓榨歷史
1.單進程人工切換。紙帶機。只能解決簡單的數學問題。
2.單道批處理。多進程批處理。多個任務批量執行。解決手動操作時需要人工切換作業導致的系統利用率低的問題
3.多進程并行處理。把程序寫在不同的內存位置來回切換。當一個作業在等待I/O處理時,多批處理系統會通過相應調度算法調度另外一個作業讓計算機執行
4.多線程。一個程序內部不同任務的來回切換。實現進程中任務的切換,又可以避免進程切換內存地址空間(將計算機實際調度的單元轉到線程)。
5.纖程/協程與管程
二.相關含義介紹
什么是程序?什么是進程?什么是進程?什么是纖程/協程、管程?
1.程序-->抽象概念
操作系統可以執行的一個計算機文件。是一組計算機能識別和執行的指令序列。如QQ.exe
2.進程-->靜態概念
進程是程序計算機(內存)中的一次運行活動。更通俗一點來說:進程是程序的實例化(類似于程序是class,進程是class的對象)。
進程是系統進行資源分配的基本單位,進程是線程的容器。
3.線程-->動態概念
一條線程指的是進程中一個單一順序的執行路線(也可以說是執行流、控制流)。即進程中的實際運行單位。
資源調度的基本單位。
4.線程上下文
線程上下文是指某一時間點 CPU 寄存器和程序計數器的內容。
4.1.使用場景
上下文切換 (context switch) 。即任務切換, 或者CPU寄存器切換。
當多任務內核決定運行另外的任務時, 它保存正在運行任務的當前狀態, 也就是CPU寄存器中的全部內容。這些內容被保存在任務自己的堆棧中, 入棧工作完成后就把下一個將要運行的任務的當前狀況從該任務的棧中重新裝入CPU寄存器, 并開始下一個任務的運行, 這一過程就是context switch。
4.2.上下文切換帶來的問題
程序執行效率與線程并發數,從正相關變為負相關;
三.思考問題
1.單核的CPU設定多線程是否有意義?
其實個人的觀點是,需要分析多線程的本質-->是對cpu性能的壓榨。
那么,如果說單線程已經達到非常好的cpu利用率,則使用多線程意義不是太大。這種作業就稱為cpu密集型(性能瓶頸是CPU運算)。
相對的,將性能瓶頸是IO(網絡通信、硬盤讀寫、阻塞等待等)的作業稱為io密集型。因為這種作業會造成cpu空閑,而使用多線程可顯著減少此情況。
2.工作線程數是不是設置得越大越好?
a.先看一個示例:
package com.pavin.thread; ? import java.text.DecimalFormat; import java.util.Random; import java.util.concurrent.CountDownLatch; ? public class multiThread_01 { ?private static double[] nums = new double[1_0000_0000];private static Random r = new Random();private static DecimalFormat df = new DecimalFormat("0.00");static {for (int j = 0; j < nums.length; j++) {nums[j] = r.nextDouble();}} ?private static void singleThread() {long start = System.currentTimeMillis(); ?double result = 0.0;for (int j = 0; j < nums.length; j++) {result += nums[j];} ?long end = System.currentTimeMillis();System.out.println("1 ? " + " singleThread: cost " + (end-start) + "ms result: " + df.format(result));} ?static double result1 = 0.0, result2 = 0.0, result3 = 0.0;private static void twoThreads() throws InterruptedException { ?Thread t1 = new Thread(() -> {for (int j = 0; j < nums.length / 2; j++) {result1 += nums[j];}}); ?Thread t2 = new Thread(() -> {for (int j = nums.length / 2; j < nums.length; j++) {result2 += nums[j];}}); ?long start = System.currentTimeMillis();t1.start();t2.start();t1.join();t2.join(); ?result3 = result1 + result2;long end = System.currentTimeMillis();System.out.println("2 ? " + " Threads: cost " + (end-start) + "ms result: " + df.format(result3));} ?private static void multiThreads(int threadCount) throws InterruptedException { ?Thread[] threads = new Thread[threadCount];double[] results = new double[threadCount];final int segmentCount = nums.length / threadCount;CountDownLatch latch = new CountDownLatch(threadCount); ?for (int i = 0; i < threadCount; i++) {int m = i; ?threads[i] = new Thread(() -> {for (int j = m * segmentCount; j < (m+1) * segmentCount && j < nums.length; j++) {results[m] += nums[j];}}); ?latch.countDown();} ?double result = 0.0;long start = System.currentTimeMillis();for (Thread t : threads) {t.start();} ?latch.await();for (double v : results) {result += v;} ?long end = System.currentTimeMillis();System.out.println(threadCount + " Threads: cost " + (end-start) + "ms result: " + df.format(result));} ?public static void main(String[] args) throws InterruptedException {singleThread();twoThreads(); ?multiThreads(10000);} }
輸出結果:
1 ? ?singleThread: cost 134ms result: 49997084.08 2 ? ?Threads: cost 78ms result: 49997084.08 10000 Threads: cost 1012ms result: 49997084.08
由此可見,使用兩個線程時明顯比一個線程更快,但是使用10000個線程時,非常慢。所以線程并不是越大越好。
b.造成效率下降的原因
見線程上下文
3.工作線程數(線程池中的線程數量)設置為多少合適?
公式+壓測
a.公式
CPU密集型:理論上線程的數量=CPU核數最合適。
不過實際中一般會設為CPU核數+1。此時當線程因為偶爾的內存頁失效或其他原因導致阻塞時,這個額外的線程可以頂上,從而保證CPU的利用率
IO密集型 :線程數 = CPU核心數 * 目標CPU利用率 *(1+平均等待時間/平均工作時間)
b.實際中的問題
i.環境開銷
比如一個普通的SpringBoot 為基礎的業務系統,默認Tomcat容器+HikariCP連接池+G1回收器。
Tomcat有自己的線程池,HikariCP也有自己的后臺線程,JVM也有一些編譯的線程,連G1都有自己的后臺線程。這些線程也是運行在當前進程、當前主機上的,也會占用CPU的資源。
ii.測算"平均等待時間"、“平均工作時間”
方法1,通過日志和統計的方式得出。
方法2,第三方工具:profiler/Jprofiler
c.實際策略
一般情況下,內部業務系統相對于性能,更注重穩定好用、符合需求。實際生產推薦的線程數:CPU核心數+1