學習目標
● 掌握線程相關概念
● 掌握線程的基本使用
● 掌握線程池的使用
● 了解解決線程安全方式
1.為什么要學習線程?
● 從1946年2月14日世界上第一臺計算機在美國賓夕法尼亞大學誕生到今天,計算和處理的模式早已從單用戶單任務的串行模式發展到了多用戶多任務的高并發處理模式。計算機處理的任務調度單位就是我們今天講的進程和線程。
● 隨著計算機的發展,CPU的性能越來越高,從早期的20MHZ到現在的2GHZ以上,從單核CPU發展到了多核CPU,性能提升了千萬倍。為了充分發揮CPU的計算性能,提高CPU的資源利用率,同時彌補進程調度過于繁重產生的問題,進程內部演進出了并發調度的訴求,于是就發明了線程。
2.有關概念
2.1 進程
● 什么是進程?
● 在windows操作系統中,進程被分為后臺進程和應用進程兩類。大部分后臺進程在系統開始運行時被操作系統啟動,完成操作系統的基礎服務功能。大部分應用進程由用戶啟動,完成用戶所需要的具體應用功能。比如聽音樂,社交聊天,瀏覽網站等。
● 簡單來說,進程就是程序的一次啟動執行。每一個進程都是要占用系統資源,包括CPU、內存、磁盤、線程數等。
● 進程與程序是什么關系呢?
● 程序是存放在硬盤中的可執行的文件,主要包括指令和數據。一個進程是一個程序的一次啟動和執行,是操作系統將程序裝入內存,給程序分配必要的系統資源,并且開始運行程序的指令。
● 同一個程序可以多次啟動,對應多個進程。比如,多次打開Chrome瀏覽器程序,在任務管理器或者Process Explorer中可以看到多個Chrome進程。
● Process Explorer:是由Sysinternals出品的免費產品,是一個輕量級進程管理器。
進程組成部分
● 1、程序段(代碼段)
● 2、數據段
● 3、進程控制塊(PCB)
● 3.1、進程描述信息
● 3.2、進程調度信息
● 3.3、進程資源信息
● 3.4、進程上下文
● 現代操作系統中,進程是并發執行的,任何進程都可以與其它進程一起執行。在進程內部,代碼段和數據段有自己獨立的地址空間,不同進程的地址空間是相互隔離的。也可以任務,進程之間是相互隔離的。
對于Java工程師來說,這里有一個問題:什么是Java程序的進程呢?
● Java編寫的程序都運行在Java虛擬機(JVM)中,每當使用Java命令啟動一個Java應用程序時,就會啟動一個JVM進程。
● 在這個JVM進程內部,所有的Java程序代碼都是以線程來運行的。JVM找到程序的入口點main(),然后運行main(),這樣就產生一個線程。這個線程被稱為主線程。當main()方法結束后,主線程運行完成,JVM進程也隨即退出。
2.2 線程
● 什么是線程?
● 線程是指"進程代碼段"的一次順序執行流程。線程是CPU調度的最小單位一個進程可以有一個或者多個線程,因此可以說線程隸屬于進程。各個線程之間共享進程的內存空間、系統資源。
線程的組成部分
● 一個標準的線程主要由3部分組成:
● 1、線程描述信息:線程的基本信息
● 2、程序計數器:記錄線程下一條指令的代碼段內存地址
● 3、棧內存:是代碼段中局部變量的存儲空間,為線程獨立擁有,線程之間是不共享的。在JDK1.8中,每個線程在創建時默認分配1MB大小的棧內存(不受GC管理)。
2.3 進程 VS 線程
● 線程是"進程代碼段"的一次順序執行流程。一個進程包含一個或者多個線程,一個進程至少由一個線程。
● 線程是CPU調度的最小單位,進程是操作系統分配資源的最小單位。線程的劃分尺度小于進程,使得多線程的并發性高。
● 線程是出于高并發的調度需求從進程內部演進而來的。線程的出現充分發揮了CPU的計算性能,又彌補了進程過度笨重的問題。
● 進程之間是相互獨立的,但是進程之間的各個線程之間并不完全獨立。各個線程之間共享進程的方法區內存、堆內存、系統資源(文件句柄、系統信號等)。
● 切換速度不同。線程上下文切換比進程上下文切換要快的多。所以有的時候,線程也會被稱為輕量級進程。
● 注意: 雖然一個進程有很多線程,但是在一個CPU的內核中,同一時刻只能有一個線程是正在執行的,該線程也被稱為當前線程。
2.4 并行與并發
● 并發(concurrency):指兩個或多個事件在同一個時間段內發生。指在同一個時刻只能有一條指令執行,但多個進程的指令被快速輪換執行,使得在宏觀上具有多個進程同時執行的效果;例如多個請求同時發給服務器,同時得到了處理(一般稱為用戶級別的并發).
● 并行(parallel):指兩個或多個事件在同一時刻發生(同時發生)。指在同一時刻,有多條指令在多個處理器上同時執行;
● 串行:多個指令一條一條的執行;或者對任務一個一個的進行處理;
2.5 同步與異步
● 同步:一個任務的開始必須等待另外一個任務的結束;例如生活中的 接力棒比賽,當上一個人完成任務之后才能把接力棒傳遞給下一個人。
● 異步:一個任務的開始 無需等待其他任務的結束;任務之間沒有毫無關聯,無需相互等待;
2.6 組合
● 與多線程進行關聯
● 同步非阻塞: 多個線程同時執行(一個一個進行執行) 不會出現阻塞
● 同步阻塞: 多個線程都不做任務了
● 異步阻塞: 多個線程并發執行 都阻塞了
● 異步非阻塞: 多個線程并發執行 都正常執行
3.Thread
● java.lang.Thread類是Java多線程編程的基礎。
● 在Java程序中,一個線程使用一個Thread類對象(實例)進行描述。
3.1 層級
public class Thread extends Object implements Runnable{}
3.2 常用常量
static int MAX_PRIORITY //線程可以擁有的最大優先級(10)。
static int MIN_PRIORITY //線程可以擁有的最低優先級(1)。
static int NORM_PRIORITY //分配給線程的默認優先級(5)。
3.3 常用構造
構造方法 | 描述 |
---|---|
Thread() 創建一個新的線程 | |
Thread(String name) | 創建一個指定名稱的線程 |
Thread(Runnable target) | 利用Runnable對象創建線程對象 |
Thread(Runnable target,String name) | 利用Runnable對象創建線程對象,并指定線程名稱 |
3.4 常用方法
方法 | 描述 |
---|---|
static Thread currentThread() | 獲得當前正在運行的線程對象 |
static void sleep(long millis) | 線程在指定時間內(毫秒數)休眠 |
static void yield() | 當前線程讓出使用CPU的使用權,立馬處于就緒狀態。 |
static boolean interrupted() | 當前線程是否已被中斷(如果調用了interrupt()方法 底層清除打斷狀態) |
long getId() | 獲得線程的標識符 |
String getName() | 獲得當前線程的名稱(未自定義,使用Thread-n) |
int getPriority() | 獲得線程 的優先級 |
Thread.State getState() | 獲得線程的狀態 |
void interrupt() | 中斷線程。(其實不能終止線程,底層僅僅是修改了一個屬性值) |
boolean isAlive() | 判斷線程是否存活 |
boolean isInterrupted() | 當前線程是否已被中斷(如果調用了interrupt()方法 底層不清除打斷狀態) |
void join() | 等待當前線程死亡 |
void join(long millis) | |
setName(String name) | 修改線程的名稱 |
setPriority(int newPriority) | 修改線程的優先級別 |
start() | 啟動線程 |
3.5 使用方法
public static void main(String[] args) {Thread thread = Thread.currentThread();//獲得當前正在運行的線程//獲得線程相關的信息System.out.println(thread.getId());//獲得線程的唯一標識System.out.println(thread.getName());//獲得線程的名稱System.out.println(thread.getPriority());//獲得線程的優先級thread.setPriority(Thread.MAX_PRIORITY);//修改線程的優先級thread.setName("main主線程");//修改線程的名稱try {Thread.sleep(200);//當前線程休眠200毫秒TimeUnit.MILLISECONDS.sleep(200);} catch (InterruptedException e) {throw new RuntimeException(e);}System.out.println("111111111111111111");}
4.創建線程
4.1 繼承Thread類
- 模擬電腦管家,可以同時執行病毒查殺,清理垃圾,等功能。
public class GarbageThread extends Thread {//重寫run方法@Overridepublic void run() {for (int i = 1; i <= 10; i++) {System.out.println(Thread.currentThread().getName() + "正在清理垃圾.....");try {Thread.sleep(400);} catch (InterruptedException e) {throw new RuntimeException(e);}}}public GarbageThread(String name) {super(name);}
}
public class HealthCheckThread extends Thread {//重寫run方法@Overridepublic void run() {for (int i = 1; i <= 10; i++) {System.out.println(Thread.currentThread().getName() + "正在電腦體檢....");try {Thread.sleep(400);} catch (InterruptedException e) {throw new RuntimeException(e);}}}public HealthCheckThread(String name) {super(name);}
}
public class VirusThread extends Thread {//重寫run方法@Overridepublic void run() {for (int i = 1; i <= 10; i++) {System.out.println(Thread.currentThread().getName() + "正在查殺病毒....");try {Thread.sleep(400);} catch (InterruptedException e) {throw new RuntimeException(e);}}}public VirusThread(String name) {super(name);}
}
//在主線程開啟多個子線程 cpu進行調度
public class ComputerManager {public static void main(String[] args) {//同時執行多個功能//1.垃圾清理//2.查找木馬//3.電腦體檢//.....GarbageThread garbageThread = new GarbageThread("垃圾清理線程");HealthCheckThread checkThread = new HealthCheckThread("電腦體檢線程");VirusThread virusThread = new VirusThread("病毒查殺線程");garbageThread.start();//啟動線程checkThread.start();virusThread.start();}
}
4.2 實現Runnable接口
- 多線程下載網絡小說資源。
public class DownloadNovel implements Runnable {private String novelPath;@Overridepublic void run() {try{download();}catch(IOException e){e.printStackTrace();}}public DownloadNovel(String novelPath) {this.novelPath = novelPath;}public static void downloadNovel() throws IOException {BufferedReader reader = new BufferedReader(new InputStreamReader(new URL(novelPath).openStream()));String line;String fileName = "";//獲得文件名+小說名字while ((line = reader.readLine()) != null) {if (line.matches("(.*)<title>(.*)")) {fileName = line.split("_")[0].split(">")[1];fileName+=".txt";break;}}BufferedWriter writer = new BufferedWriter(new FileWriter(new File(NOVEL_DIR, fileName)));while ((line = reader.readLine()) != null) {if (line.matches("(.*)<main\\s(.*)")) {line = line.substring(line.indexOf("<p>"), line.lastIndexOf("<p>")).replaceAll("<p>", "\n");writer.write(line);writer.newLine();break;}}writer.close();reader.close();}}
public class ThreadDemo {public static void main(String[] args) {//開啟多線程 下載小說資源long begin = System.currentTimeMillis();List<String> novelPathList = List.of("https://read.qidian.com/chapter/D4Pic8a3frqLTMDvzUJZaQ2/pdyqJ3Rujv62uJcMpdsVgA2/","https://read.qidian.com/chapter/D4Pic8a3frqLTMDvzUJZaQ2/w0Xq2d90qPxOBDFlr9quQA2/","https://read.qidian.com/chapter/D4Pic8a3frqLTMDvzUJZaQ2/o9u2qont57Rp4rPq4Fd4KQ2/","https://read.qidian.com/chapter/D4Pic8a3frqLTMDvzUJZaQ2/V_KClkdhrbvgn4oDUcDQ2/","https://read.qidian.com/chapter/D4Pic8a3frqLTMDvzUJZaQ2/Iw-c6HDPysHwrjbX3WA1AA2/");List<Thread> threadList = new ArrayList<>();for (String path : novelPathList) {DownloadNovel downloadNovel = new DownloadNovel(path);threadList.add(new Thread(downloadNovel));}threadList.forEach(Thread::start);try {for (Thread thread : threadList) {thread.join();}} catch (InterruptedException e) {throw new RuntimeException(e);}long end = System.currentTimeMillis();System.out.println(end - begin);}
}
4.3 實現Callable接口
public class SaleTicket implements Callable<Integer> {private static int ticketNum = 30;@Overridepublic Integer call() throws Exception {while (ticketNum > 0) {System.out.println(Thread.currentThread().getName() + "售賣了第" + ticketNum + "張票");ticketNum--;TimeUnit.MILLISECONDS.sleep(500);}return ticketNum;}
}
public class ThreadDemo {public static void main(String[] args) {//創建3個售票窗口 同時進行賣票SaleTicket saleTicket = new SaleTicket();FutureTask<Integer> task1 = new FutureTask<>(saleTicket);FutureTask<Integer> task2 = new FutureTask<>(saleTicket);FutureTask<Integer> task3 = new FutureTask<>(saleTicket);Thread window1 = new Thread(task1, "窗口1");Thread window2 = new Thread(task2, "窗口2");Thread window3 = new Thread(task3, "窗口3");window1.start();window2.start();window3.start();try {System.out.println(task1.get());//提前調用get會阻塞主線程 一定要在start之后調用get方法。 避免出現阻塞。//獲得的數據就是重寫call方法的返回值數據。System.out.println(task2.get());System.out.println(task3.get());} catch (InterruptedException | ExecutionException e) {throw new RuntimeException(e);}}
}
4.4 線程池
4.4.1 優勢
● 在上面幾種方式中,多線程頻繁創建和銷毀非常消耗性能(用戶態和核心態的轉換)。導致出現線程資源的浪費,要提高資源利用率。
● 降低資源消耗-重用存在的線程,減少對象創建、消亡的開銷,性能好
● 提高響應速度 -可有效控制最大并發線程數,提高系統資源利用率,同時可以避免過多資源競爭,避免阻塞。當任務到達時,任務可不用等待線程創建就能立即執行
● 提高線程的可管理性-提供定時執行、定期執行、單線程、并發數控制等功能。
● 池化技術。使用線程池提高資源的利用率。線程池里面的線程用來接任務,當線程完成任務,回到池子中繼續接任務。 當線程池銷毀的時候,內部線程全部銷毀。
● 有2種方式可以創建線程池對象:
- Executors
- ThreadPoolExecutor
4.4.2 Executors
● 在jdk中,提供了java.util.concurrent.Executors來快捷的創建線程池對象。
● Executors提供的創建線程池方式如下,了解:
● Executor:運行新任務的簡單接口
● ExecutorService:擴展了Executor,添加了用來管理執行器生命周期和任務生命周期的方法
● newSingleThreadExecutor()://創建單個線程的線程池
● newFixedThreadPool()://創建線程數量固定的線程池
● newCachedThreadPool()://創建線程數量可伸縮的線程池
● newScheduledThreadPool()://創建執行延時任務的線程池
private static void demo1() {ExecutorService executorService = Executors.newFixedThreadPool(3);//創建一個數量固定的線程池,超出的任務會在隊列中等待空閑的線程,可用于控制程序的最大并發數./*for (int num = 0; num < 5; num++) {executorService.submit(() -> {System.out.println(Thread.currentThread().getName() + "------");try {TimeUnit.MILLISECONDS.sleep(500);} catch (InterruptedException e) {throw new RuntimeException(e);}});}*/ExecutorService executorService1 = Executors.newCachedThreadPool();//短時間內處理大量工作的線程池,會根據任務數量產生對應的線程,并試圖緩存線程以便重復使用.//新的任務提交,沒有可用的線程,將會自動創建新的線程并新增至池子中.未在六十秒內使用線程,這些線程將被終止并從緩存中刪除。ScheduledExecutorService scheduledService = Executors.newScheduledThreadPool(5);//創建執行周期性或定時性任務的線程池.scheduledService.schedule(()->{System.out.println("3秒之后執行........"+LocalDateTime.now());},3,TimeUnit.SECONDS);System.out.println("當前時間:"+ LocalDateTime.now());
}
4.4.3 ThreadPoolExecutor
private static void demo2() {
// public ThreadPoolExecutor(int corePoolSize, //初始化池子中核心線程數量
// int maximumPoolSize, //可允許的最大的線程數量
// long keepAliveTime, //超過core數量的線程的最大存活時間
// TimeUnit unit, //keepAliveTime的時間單位
// BlockingQueue<Runnable> workQueue, //任務隊列(維護未執行的新任務)
// ThreadFactory threadFactory, //創建線程對象
// RejectedExecutionHandler handler//拒絕策略 任務量>max+隊列任務數量) {}//1.有界隊列 ArrayBlockingQueue 100//2.無界隊列 LinkedBlockingDeque (隊列大小無限制) PriorityBlockingQueue 優先隊列//3.同步隊列 SynchronousQueue 一個任務的提交必須等待另外一個線程的消費。ThreadPoolExecutor poolExecutor = new ThreadPoolExecutor(10,100,10,TimeUnit.SECONDS,new ArrayBlockingQueue<>(30),Executors.defaultThreadFactory(),new ThreadPoolExecutor.CallerRunsPolicy());//提交任務//poolExecutor.submit()//poolExecutor.execute();}
5. 線程安全
線程安全是并發編程中的術語,指某個程序在并發環境中被調用時,能夠正確地處理多線程之間的公共變量,使程序能正確完成。
5.1 緩存一致性(了解)
簡單了解 即可
● 計算機內存模型:
● CPU 與緩存一致性 :
● 我們應該都知道,計算機在執行程序的時候,每條指令都是在CPU中執行的,而執行的時候,又免不了要和數據打交道。而計算機上面的數據,是存放在主存當中的,也就是計算機的物理內存。
● 剛開始,CPU的性能與數據讀寫對比差異不大。但是隨著CPU技術的發展,CPU的執行速度越來越快。而由于內存的技術并沒有太大的變化,所以從內存中讀取和寫入數據的過程和CPU的執行速度比起來差距就會越來越大, 這就導致CPU每次操作內存都要耗費很多等待時間。
● 可是,不能因為內存的讀寫速度慢,就不發展CPU技術了,總不能讓內存成為計算機處理的瓶頸。
● 因此,人們就提出一個新的概念—>緩存。就是在CPU和內存之間增加高速緩存。緩存的概念大家都知道,就是保存一份數據拷貝。他的特點是速度快,內存小,并且昂貴。
● 程序的執行過程: 當程序在運行過程中,會將運算需要的數據從主存復制一份到CPU的高速緩存當中,那么CPU進行計算時就可以直接從它的高速緩存讀取數據和向其中寫入數據,當運算結束之后,再將高速緩存中的數據刷新到主存當中。
● 隨著CPU能力的不斷提升,一層緩存就慢慢的無法滿足要求了,就逐漸的衍生出多級緩存.
● CPU緩存可以分為一級緩存(L1),二級緩存(L2),部分高端CPU還具有三級緩存(L3),每一級緩存中所儲存的全部數據都是下一級緩存的一部分.
● 有了多級緩存之后,程序的執行就變成了: 當CPU要讀取一個數據時,首先從一級緩存中查找,如果沒有找到再從二級緩存中查找,如果還是沒有就從三級緩存或內存中查找.
● 單核CPU只含有一套L1,L2,L3緩存;如果CPU含有多個核,即多核CPU,則每個核都含有一套L1(甚至和L2)緩存,而共享L3(或者和L2)緩存.
● 隨著計算機能力不斷提升,開始支持多線程。那么問題就來了。我們分別來分析下單線程、多線程在單核CPU、多核CPU中的影響.
● 單線程: cpu核心的緩存只被一個線程訪問。緩存獨占,不會出現訪問沖突等問題。
● 單核CPU,多線程:進程中的多個線程會同時訪問進程中的共享數據,CPU將某塊內存加載到緩存后,不同線程在訪問相同的物理地址的時候,都會映射到相同的緩存位置,這樣即使發生線程的切換,緩存仍然不會失效。但由于任何時刻只能有一個線程在執行,因此不會出現緩存訪問沖突。
● 多核CPU,多線程: 每個核都至少有一個L1 緩存。多個線程訪問進程中的某個共享內存,且這多個線程分別在不同的核心上執行,則每個核心都會在各自的cache中保留一份共享內存的緩沖。由于多核是可以并行的,可能會出現多個線程同時寫各自的緩存的情況,而各自的cache之間的數據就有可能不同。
● 在CPU和主存之間增加緩存,在多線程場景下就可能存在緩存一致性問題,也就是說,在多核CPU中,每個核的自己的緩存中,關于同一個數據的緩存內容可能不一致。
● CPU和主存之間增加緩存,在多線程場景下會存在緩存一致性問題。除了這種情況,還有一種硬件問題也比較重要。那就是為了使處理器內部的運算單元能夠盡量的被充分利用,處理器可能會對輸入代碼進行亂序執行處理。這就是處理器優化。
● 除了現在很多流行的處理器會對代碼進行優化亂序處理,很多編程語言的編譯器也會有類似的優化,比如Java虛擬機的即時編譯器(JIT)也會做指令重排。
● 任由處理器優化和編譯器對指令重排的話,就可能導致各種各樣的問題.
5.2 并發編程的問題
● 并發編程里面出現的問題,其實與上面所提到的3個問題一致(緩存一致性,處理器優化,指令重排序。)
● 只不過并發編程里面所描述的,是人們使用抽象思維進行定義的。稱為:
● 原子性問題
● 可見性問題
● 有序性問題。
● 原子性是指在一個操作中就是cpu不可以在中途暫停然后再調度,既不被中斷操作,要不全部執行完成,要不就不執行。
● 可見性是指當多個線程訪問同一個變量時,一個線程修改了這個變量的值,其他線程能夠立即看得到修改的值
● 有序性即程序執行的順序按照代碼的先后順序執行。
● 緩存一致性問題其實就是可見性問題。而處理器優化是可以導致原子性問題的。指令重排即會導致有序性問題。
5.3 JMM
● 所以,為了保證并發編程中可以滿足原子性、可見性及有序性。有一個重要的概念,那就是——內存模型
● 為了保證共享內存的正確性(可見性、有序性、原子性),內存模型定義了共享內存系統中多線程程序讀寫操作行為的規范.通過這些規則來規范對內存的讀寫操作,從而保證指令執行的正確性。它與處理器有關、與緩存有關、與并發有關、與編譯器也有關。他解決了CPU多級緩存、處理器優化、指令重排等導致的內存訪問問題,保證了并發場景下的一致性、原子性和有序性
● 內存模型解決并發問題主要采用兩種方式:限制處理器優化和使用內存屏障.
● Java內存模型:
● Java內存模型(Java Memory Model ,JMM)就是一種符合內存模型規范的,屏蔽了各種硬件和操作系統的訪問差異的,保證了Java程序在各種平臺下對內存的訪問都能保證效果一致的機制及規范。
● 提到Java內存模型,一般指的是JDK 5 開始使用的新的內存模型,主要由JSR-133: JavaTM Memory Model and Thread Specification
● Java內存模型規定了所有的變量都存儲在主內存中,每條線程還有自己的工作內存,線程的工作內存中保存了該線程中是用到的變量的主內存副本拷貝,線程對變量的所有操作都必須在工作內存中進行,而不能直接讀寫主內存。不同的線程之間也無法直接訪問對方工作內存中的變量,線程間變量的傳遞均需要自己的工作內存和主存之間進行數據同步進行。而JMM就作用于工作內存和主存之間數據同步過程。他規定了如何做數據同步以及什么時候做數據同步.
● JMM是一種規范,目的是解決由于多線程通過共享內存進行通信時,存在的本地內存數據不一致、編譯器會對代碼指令重排序、處理器會對代碼亂序執行等帶來的問題
5.4 JMM的實現
● 在Java中提供了一系列和并發處理相關的關鍵字,比如
volatile
、synchronized
、final
、concurren
包等。其實這些就是Java內存模型封裝了底層的實現后提供給程序員使用的一些關鍵字。
● 在開發多線程的代碼的時候,我們可以直接使用synchronized
等關鍵字來控制并發,從來就不需要關心底層的編譯器優化、緩存一致性等問題。所以,Java內存模型,除了定義了一套規范,還提供了一系列原語,封裝了底層實現后,供開發者直接使用。
原子性
● 在Java中,為了保證原子性,提供了兩個高級的字節碼指令monitorenter和monitorexit,在Java中對應的關鍵字就是synchronized。
● 因此,在Java中可以使用synchronized來保證方法和代碼塊內的操作是原子性。
可見性
● Java內存模型是通過在變量修改后將新值同步回主內存,在變量讀取前從主內存刷新變量值的這種依賴主內存作為傳遞媒介的方式來實現的。
● Java中的volatile關鍵字提供了一個功能,那就是被其修飾的變量在被修改后可以立即同步到主內存,被其修飾的變量在每次是用之前都從主內存刷新。因此,可以使用volatile來保證多線程操作時變量的可見性。
● 除了volatile,Java中的synchronized和final兩個關鍵字也可以實現可見性。只不過實現方式不同。
有序性
● 在Java中,可以使用synchronized和volatile來保證多線程之間操作的有序性。實現方式有所區別:
● volatile關鍵字會禁止指令重排。synchronized關鍵字保證同一時刻只允許一條線程操作。
synchronized 在并發中,可以保證程序的有序性,可見性,以及原子性。
volatile在并發中,可以保證程序的有序性,可見性,但是不能保證原子性。但是它可以限制指令重排。
5.5 synchronized
public class SaleTicket {private static int ticketNum = 30;public static void sale() {while (ticketNum > 0) {System.out.println(Thread.currentThread().getName() + "正在賣第" + (ticketNum--) + "票");try {TimeUnit.MILLISECONDS.sleep(400);} catch (InterruptedException e) {e.printStackTrace();}}}
}
class Test {public static void main(String[] args) {new Thread(SaleTicket::sale, "窗口1").start();new Thread(SaleTicket::sale, "窗口2").start();new Thread(SaleTicket::sale, "窗口3").start();}
}
- 問題:出現了一票多賣,或者少賣等情況。多個窗口同時操作同一個變量ticketNum。出現了并發編程里面的問題,沒有保證可見性。
- 解決方案:
//在方法簽名處使用synchronized關鍵字(必須是同一個監視器對象)。
public class SaleTicket {private static int ticketNum = 30;public static void sale() {while (ticketNum > 0) {ticketNum();try {TimeUnit.MILLISECONDS.sleep(400);} catch (InterruptedException e) {e.printStackTrace();}}}private static synchronized void ticketNum(){System.out.println(Thread.currentThread().getName() + "正在賣第" + (ticketNum--) + "票");}}
- 同步代碼塊
//在方法體里面 使用synchronized(監視器對象){}
//了解鎖對象相關的概念: https://zhuanlan.zhihu.com/p/71156910
public class SaleTicket {private static int ticketNum = 30;public static void sale() {while (ticketNum > 0) {synchronized (SaleTicket.class){System.out.println(Thread.currentThread().getName() + "正在賣第" + (ticketNum--) + "票");}try {TimeUnit.MILLISECONDS.sleep(400);} catch (InterruptedException e) {e.printStackTrace();}}}
5.6 Lock
- 模擬存錢/取錢功能:
public class AccountBalance {@Getterprivate BigDecimal balance = new BigDecimal("1000");private final String NAME = "張三";//存錢public void cunMoney(double money) {balance = balance.add(BigDecimal.valueOf(money));System.out.println(Thread.currentThread().getName() + "正在給" + NAME + "轉賬" + money + ",目前賬戶的余額:" + balance);}//取錢public void quMoney(double money) {balance = balance.subtract(BigDecimal.valueOf(money));System.out.println(Thread.currentThread().getName() + "取了" + NAME + money + ",目前賬戶的余額:" + balance);}
}class Test {public static void main(String[] args) {AccountBalance balance = new AccountBalance();Thread thread1 = new Thread(() -> balance.cunMoney(500), "李四");Thread thread2 = new Thread(() -> balance.quMoney(1000), "王五");Thread thread3 = new Thread(() -> balance.cunMoney(1000), "趙六");thread1.start();thread2.start();thread3.start();try {thread1.join();thread2.join();thread3.join();} catch (InterruptedException e) {throw new RuntimeException(e);}System.out.println("操作之后的余額還剩下:"+balance.getBalance());}
}
- 使用Lock加鎖解決
public class AccountBalance {@Getterprivate BigDecimal balance = new BigDecimal("1000");private final String NAME = "張三";private final Lock lock = new ReentrantLock();//默認是可重入鎖,一個線程可以多次獲得同一把鎖。//存錢public void cunMoney(double money) {try {lock.lock();balance = balance.add(BigDecimal.valueOf(money));System.out.println(Thread.currentThread().getName() + "正在給" + NAME + "轉賬" + money + ",目前賬戶的余額:" + balance);} finally {lock.unlock();}}//取錢public void quMoney(double money) {try {lock.lock();balance = balance.subtract(BigDecimal.valueOf(money));System.out.println(Thread.currentThread().getName() + "取了" + NAME + money + ",目前賬戶的余額:" + balance);} finally {lock.unlock();}}
}
5.7 volatile
怎么才能保證程序中的原子性呢?
- 案例: 模擬自動生成id。(id生成器)
public class IdGenerator {private static int id = 0;//維護idpublic static void idIncrement() {id++;}public static int getId() {return id;}
}class IdTest {@SneakyThrowspublic static void main(String[] args) {List<Thread> threadList = new LinkedList<>();for (int i = 0; i < 10000; i++) {threadList.add(new Thread(IdGenerator::idIncrement));}threadList.forEach(Thread::start);for (Thread thread : threadList) {thread.join();}System.out.println("最后的id:" + IdGenerator.getId());}
}
- 使用synchronized能夠保證原子性。 效率偏慢。
public class IdGenerator {//并發編程里面: 能夠保證原子性類//AtomicInteger AtomicLong....//private static int id = 0;// private static AtomicLong id = new AtomicLong(1000);//LongAdderprivate static LongAdder id = new LongAdder();static {//設置初始值id.add(1000);}public static void idIncrement() {// id.incrementAndGet();// CAS 樂觀鎖id.increment();}public static long getId() {//return id.get();return id.longValue();}
}
● 什么是CAS呢?Compare-and-Swap,即比較并替換,也有叫做Compare-and-Set的,比較并設置。
● 1、比較:讀取到了一個值A,在將其更新為B之前,檢查原值是否仍為A(未被其他線程改動)。
● 2、設置:如果是,將A更新為B,結束。如果不是,則什么都不做,循環重試。
● 上面的兩步操作是原子性的,可以簡單地理解為瞬間完成,在CPU看來就是一步操作。
● 有了CAS,就可以實現一個樂觀鎖。優點是無鎖編程,所以沒有上鎖和釋放鎖的性能損失。
● 它允許多個線程同時讀取(因為根本沒有加鎖操作),但是只有一個線程可以成功更新數據,并導致其他要更新數據的線程回滾重試。 CAS利用CPU指令,從硬件層面保證了操作的原子性,以達到類似于鎖的效果。
解決單例(懶漢模式問題)問題
單例模式: 一個進程(多個線程)有且只有1個對象。
● 餓漢模式:
優點: 單線程/并發環境 對象只有1個。
弊端: 沒有體現懶加載。
● 懶漢模式:
優點: 體現懶加載。在單線程環境下,保證對象只有1個。
弊端: 并發環境下,不能保證對象有且只有1個。線程安全的問題。
public class SysUser implements Cloneable, Serializable {private SysUser() {}private volatile static SysUser sysUser;//并發10個線程 每個線程循環10次//第一次循環的時候 1個線程執行這些邏輯 其他9個排隊等待//剩下的循環 并發執行public static SysUser getInstance() {if (sysUser == null) {synchronized (SysUser.class) {if (sysUser == null) {sysUser = new SysUser();//jvm底層 可能會出現 指令重排//1. new 開辟空間 5s//2. SysUser() 初始化成員變量數據 6s//3. 賦值 2s// 1 3 5+2}}}return sysUser;}//禁止/避免 克隆打破單例@Overridepublic SysUser clone() {/* try {SysUser clone = (SysUser) super.clone();return clone;} catch (CloneNotSupportedException e) {throw new AssertionError();}*/return this;}public Object readResolve() {return sysUser;}
}
class Test1 {public static void main(String[] args) throws Exception {/* SysUser instance = SysUser.getInstance();System.out.println(instance);SysUser clone = instance.clone();System.out.println(clone);*/SysUser instance = SysUser.getInstance();ObjectOutputStream outputStream = new ObjectOutputStream(new FileOutputStream("src/a.txt"));outputStream.writeObject(instance);System.out.println("instance:" + instance);outputStream.close();ObjectInputStream inputStream = new ObjectInputStream(new FileInputStream("src/a.txt"));Object object = inputStream.readObject();//自動檢測類中是否有 readResolveSystem.out.println("obj:" + object);inputStream.close();//單例模式: 一個進程里面有且只有1個類對象。//并發的環境下 獲得SysUser類對象 保證SysUser類對象有且只有1個//10個 每個線程執行10次獲得SysUser類對象// List<Thread> threadList = new ArrayList<>(10);// for (int i = 0; i < 20; i++) {// threadList.add(new Thread(() -> {// System.out.println(Thread.currentThread().getName() + "---" + SysUser.getInstance());// try {// Thread.sleep(500);// } catch (InterruptedException e) {// throw new RuntimeException(e);// }// }));// }// threadList.forEach(Thread::start);}//Thread-6---com.java.thread4.SysUser@44c924be//Thread-0---com.java.thread4.SysUser@43785832//懶漢模式在并發編程里面 不能保證單例。//解決: 保證單例//1. synchronized// 1.1 同步方法 性能低。 鎖粒度比較大 有且只有1個線程執行getInstance方法 其他19個全部排隊等待// 1.2 同步代碼塊 鎖粒度比較小,性能偏低。// 1.3 建議: DCL double check lock 雙重檢測鎖 if+同步代碼塊// 1.4 JIT指令重排序---->程序的有序性。 DCL+volatile//克隆/序列化/反射,都會打破單例(餓漢/懶漢)//使用枚舉類創建單例---->屬性、方法
}
5.8 ThreadLoacal
- 解決SimpleDateFormat問題。
public class ThreadLocalDemo {//日期格式化 DateFormat---->SimpleDateFormat(Date 與 String之間的相互轉換)private static final String PATTERN = "yyyy-MM-dd HH:mm:ss";private static final ThreadLocal<SimpleDateFormat> THREAD_LOCAL = new ThreadLocal<>() {@Overrideprotected SimpleDateFormat initialValue() {return new SimpleDateFormat(PATTERN);}};/*** string轉Date** @param dateStr 字符串時間* @return Date類的對象*/public static Date strToDate(String dateStr) {Date date = null;try {date = THREAD_LOCAL.get().parse(dateStr);} catch (ParseException e) {throw new RuntimeException(e);}return date;}/*** date轉String** @param date 日期對象* @return 指定pattern格式下的字符串數據*/public static String dateToStr(Date date) {return THREAD_LOCAL.get().format(date);}
結論:
- 在dateToStr/strToDate 各自new一次new SimpleDateFormat(PATTERN) 在并發里面沒有任何問題。
- 弊端: 5個線程 每個線程調用15次strToDate 創建了30個SimpleDateFormat對象。
- 節約堆內存,是否可以創建1次new SimpleDateFormat(PATTERN)? 作為全局變量 提高內存利用率
- 作為全局,在并發環境下,出現了線程安全的問題。
- 解決線程安全: synchronized
- 弊端: 性能低
- 節約內存,提高性能角度解決以上所有問題。
- 出現線程安全的根本原因: 多個線程共同操作/使用同一個SimpleDateFormat
- 5個線程----> 各自都有各自的SimpleDateFormat (創建5個)-----> ThreadLocal
- 將SimpleDateFormat對象的創建,維護,銷毀,交給ThreadLocal管理
public static void main(String[] args) {//本地線程//作用: 為每個線程創建各自的數據副本(對象)。//每個線程都有各自的資源操作。//需求: 并發: 將多個字符串數據轉換成指定的Date類對象List<String> strList = new ArrayList<>();//size=15Collections.addAll(strList,"2020-01-01 12:00:00", "2020-01-01 12:00:00", "2020-01-01 12:00:00","2020-01-01 12:00:00", "2020-01-01 12:00:00", "2020-01-01 12:00:00", "2020-01-01 12:00:00","2020-01-01 12:00:00", "2020-01-01 12:00:00", "2020-01-01 12:00:00", "2020-01-01 12:00:00","2020-01-01 12:00:00", "2020-01-01 12:00:00", "2020-01-01 12:00:00", "2020-01-01 12:00:00");//創建2個線程 同時解析strList里面的字符串日期 轉換成Date類對象new Thread(() -> getListStr(strList)).start();new Thread(() -> getListStr(strList)).start();new Thread(() -> getListStr(strList)).start();new Thread(() -> getListStr(strList)).start();/*String str = "2020-01-01 12:00:00";Date date = strToDate(str);System.out.println(date);//Wed Jan 01 12:00:00 CST 2020Date date1 = new Date();System.out.println(dateToStr(date1));*/}private static void getListStr(List<String> list) {for (String dateStr : list) {Date date = strToDate(dateStr);System.out.println(Thread.currentThread().getName() + "---" + date);}}
}
5.9 集合類型
● 感受集合類型所謂線程安全?
Collection
1.1 List
ArrayList LinkedList Vector
1.2 Set
Map
HashMap
HashTable---->synchronized(hash)
ConcurrentHashMap---->CAS(位桶里面指定index的那一個對象)
public class ListDemo {//模擬: 并發多個線程,同時操作一個List集合對象。新增數據。//100個線程 每個線程循環100次新增數據 list元素: 10000//1.使用安全的集合類型//2.將不安全的集合對象轉換成線程安全的集合對象private static List<Integer> list = Collections.synchronizedList(new ArrayList<>());//private static List<Integer> list =new Vector<>();public static void insertDataToList(){List<Thread> threadList = new ArrayList<>(100);for (int i = 0; i < 100; i++) {threadList.add(new Thread(()->{for (int j = 0; j < 100; j++) {list.add(j);}}));}threadList.forEach(Thread::start);try {for (Thread thread : threadList) {thread.join();}} catch (InterruptedException e) {throw new RuntimeException(e);}System.out.println("list.size():"+list.size());}public static void main(String[] args) {insertDataToList();}
}
6. 線程通信
生產者與消費者現象。
● 多個線程在處理同一個資源,但是處理的動作(線程的任務)卻不相同。而多個線程并發執行時, 在默認情況下CPU是隨機切換線程的,當我們需要多個線程來共同完成一件任務,并且我們希望他們有規律的執行, 那么多線程之間需要一些通信機制,可以協調它們的工作,以此來幫我們達到多線程共同操作一份數據;
● 線程間通信的常見方式:共享主存 ,等待-通知。
● 等待喚醒機制:這是多個線程間的一種協作機制。談到線程我們經常想到的是線程間的競爭(race),比如去爭奪鎖,但這并不是故事的全部,線程間也會有協作機制。就是在一個線程滿足某個條件時,就進入等待狀態(wait()/wait(time)), 等待其他線程執行完他們的指定代碼過后再將其喚醒(notify());或可以指定wait的時間,等時間到了自動喚醒;在有多個線程進行等待時,如果需要,可以使用 notifyAll()來喚醒所有的等待線程。wait/notify 就是線程間的一種協作機制。
● 需求: 在雙十一,很多用戶提交很多訂單執行支付功能。支付寶支付/微信支付---->支付成功 訂單處理完畢。
生產者: 用戶提交(生產)訂單-----> 存儲緩沖區(容器)
消費者: 淘寶服務器消費用戶提交的訂單。
只要用戶提交一個訂單,就可以通知服務器處理訂單了。
服務器從緩沖區獲得一個訂單進行處理,這個時候緩沖區有足夠空間維護新的訂單,通知用戶提交新的訂單。
6.1 synchronized+wait+notify
- 同步代碼塊+wait+notify
public class OrderPool {private static List<String> pool = Collections.synchronizedList(new ArrayList<>());//訂單池----> 最大數量的限定private static final int MAX = 10;//提交訂單到池子里public static void putOrder() {while (true) {synchronized (OrderPool.class) {try {while (pool.size()==MAX){//當前生產者線程等待OrderPool.class.wait();}} catch (InterruptedException e) {throw new RuntimeException(e);}pool.add("order");//喚醒消費者線程---> 隨機喚醒一個OrderPool.class.notify();System.out.println(Thread.currentThread().getName() + "提交1個訂單,池子里面:" + pool.size());}try {TimeUnit.MILLISECONDS.sleep(500);} catch (InterruptedException e) {throw new RuntimeException(e);}}}//從池子里面獲得訂單進行處理public static void consumeOrder() {while (true) {synchronized (OrderPool.class) {try {while (pool.size() == 0) {//池子沒有訂單//消費者等待OrderPool.class.wait(); //自動釋放鎖//線程新的狀態 WAITING}} catch (InterruptedException e) {throw new RuntimeException(e);}pool.remove(0);//消費//喚醒生產者OrderPool.class.notify();System.out.println(Thread.currentThread().getName() + "處理1個訂單,池子里面:" + pool.size());}try {TimeUnit.MILLISECONDS.sleep(500);} catch (InterruptedException e) {throw new RuntimeException(e);}}}}
- 同步代碼塊+wait+notify
public class OrderPool {private static List<String> pool = Collections.synchronizedList(new ArrayList<>());//訂單池----> 最大數量的限定private static final int MAX = 10;//提交訂單到池子里public synchronized void putOrder() {try {while (pool.size() == MAX) {//當前生產者線程等待this.wait();}} catch (InterruptedException e) {throw new RuntimeException(e);}pool.add("order");//喚醒消費者線程---> 隨機喚醒一個this.notify();System.out.println(Thread.currentThread().getName() + "提交1個訂單,池子里面:" + pool.size());}//從池子里面獲得訂單進行處理public synchronized void consumeOrder() {try {while (pool.size() == 0) {//池子沒有訂單//消費者等待this.wait(); //自動釋放鎖//線程新的狀態 WAITING}} catch (InterruptedException e) {throw new RuntimeException(e);}pool.remove(0);//消費//喚醒生產者this.notify();System.out.println(Thread.currentThread().getName() + "處理1個訂單,池子里面:" + pool.size());}}
- 測試
public static void main(String[] args) {OrderPool orderPool = new OrderPool();//有很多生產者線程---->用戶線程生成訂單---->池子List<Thread> threadList1 = new ArrayList<>();for (int i = 1; i <= 3; i++) {threadList1.add(new Thread(()->{while (true){orderPool.putOrder();try {TimeUnit.MILLISECONDS.sleep(700);} catch (InterruptedException e) {throw new RuntimeException(e);}}}, "用戶" + i));}//有很多消費者線程----->服務器處理訂單List<Thread> threadList2 = new ArrayList<>();for (int i = 1; i <= 2; i++) {threadList2.add(new Thread(()->{while (true){orderPool.consumeOrder();try {TimeUnit.MILLISECONDS.sleep(500);} catch (InterruptedException e) {throw new RuntimeException(e);}}}, "服務器" + i));}threadList1.forEach(Thread::start);threadList2.forEach(Thread::start);//并發8個線程}
6.2 Lock+Condition
public class OrderPool {private static List<String> pool = Collections.synchronizedList(new ArrayList<>());//訂單池----> 最大數量的限定private static final int MAX = 10;private static final Lock myLock = new ReentrantLock();private static final Condition condition = myLock.newCondition();//提交訂單到池子里public void putOrder() {try {myLock.lock();try {while (pool.size() == MAX) {//當前生產者線程等待condition.await();}} catch (InterruptedException e) {throw new RuntimeException(e);}pool.add("order");//喚醒消費者線程---> 隨機喚醒一個condition.signal();System.out.println(Thread.currentThread().getName() + "提交1個訂單,池子里面:" + pool.size());} finally {myLock.unlock();}}//從池子里面獲得訂單進行處理public void consumeOrder() {try {myLock.lock();try {while (pool.size() == 0) {//池子沒有訂單//消費者等待//線程新的狀態 WAITINGcondition.await();}} catch (InterruptedException e) {throw new RuntimeException(e);}pool.remove(0);//消費//喚醒生產者condition.signal();System.out.println(Thread.currentThread().getName() + "處理1個訂單,池子里面:" + pool.size());} finally {myLock.unlock();}}}
7. 線程狀態
public enum State {NEW,// new 線程對象 新建狀態RUNNABLE,// run方法 運行狀態BLOCKED,//阻塞狀態----> 釋放鎖對象---> notifyWAITING,//等待 notify TIMED_WAITING,//時間內等待TERMINATED;//死亡狀態}