Java多線程詳解——一篇文章搞懂Java多線程
點擊上方藍色字體,選擇“標星公眾號”
優(yōu)質(zhì)文章,第一時間送達
作者 | 13roky
來源 | urlify.cn/7zuYfm
1. 基本概念
程序(program)
程序是為完成特定任務(wù)、用某種語言編寫的一組指令的集合。即指一段靜態(tài)的代碼(還沒有運行起來),靜態(tài)對象。
進程(process)
進程是程序的一次執(zhí)行過程,也就是說程序運行起來了,加載到了內(nèi)存中,并占用了cpu的資源。這是一個動態(tài)的過程:有自身的產(chǎn)生、存在和消亡的過程,這也是進程的生命周期。
進程是系統(tǒng)資源分配的單位,系統(tǒng)在運行時會為每個進程分配不同的內(nèi)存區(qū)域。
線程(thread)
進程可進一步細化為線程,是一個程序內(nèi)部的執(zhí)行路徑。
若一個進程同一時間并行執(zhí)行多個線程,那么這個進程就是支持多線程的。
線程是cpu調(diào)度和執(zhí)行的單位,每個線程擁有獨立的運行棧和程序計數(shù)器(pc),線程切換的開銷小。
一個進程中的多個線程共享相同的內(nèi)存單元/內(nèi)存地址空間——》他們從同一堆中分配對象,可以訪問相同的變量和對象。這就使得相乘間通信更簡便、搞笑。但索格線程操作共享的系統(tǒng)資源可能就會帶來安全隱患(隱患為到底哪個線程操作這個數(shù)據(jù),可能一個線程正在操作這個數(shù)據(jù),有一個線程也來操作了這個數(shù)據(jù)v)。
配合JVM內(nèi)存結(jié)構(gòu)了解(只做了解即可)

class文件會通過類加載器加載到內(nèi)存空間。
其中內(nèi)存區(qū)域中每個線程都會有虛擬機棧和程序計數(shù)器。
每個進程都會有一個方法區(qū)和堆,多個線程共享同一進程下的方法區(qū)和堆。
CPU單核和多核的理解
單核的CPU是一種假的多線程,因為在一個時間單元內(nèi),也只能執(zhí)行一個線程的任務(wù)。同時間段內(nèi)有多個線程需要CPU去運行時,CPU也只能交替去執(zhí)行多個線程中的一個線程,但是由于其執(zhí)行速度特別快,因此感覺不出來。
多核的CPU才能更好的發(fā)揮多線程的效率。
對于Java應(yīng)用程序java.exe來講,至少會存在三個線程:main()主線程,gc()垃圾回收線程,異常處理線程。如過發(fā)生異常時會影響主線程。
Java線程的分類:用戶線程 和 守護線程
Java的gc()垃圾回收線程就是一個守護線程
守護線程是用來服務(wù)用戶線程的,通過在start()方法前調(diào)用thread.setDaemon(true)可以吧一個用戶線程變成一個守護線程。
并行和并發(fā)
并行:多個cpu同時執(zhí)行多個任務(wù)。比如,多個人做不同的事。
并發(fā):一個cpu(采用時間片)同時執(zhí)行多個任務(wù)。比如,渺少、多個人做同一件事。
多線程的優(yōu)點
提高應(yīng)用程序的響應(yīng)。堆圖像化界面更有意義,可以增強用戶體驗。
提高計算機系CPU的利用率。
改善程序結(jié)構(gòu)。將既長又復(fù)雜的進程分為多個線程,獨立運行,利于理解和修改。
何時需要多線程
程序需要同時執(zhí)行兩個或多個任務(wù)。
程序需要實現(xiàn)一些需要等待的任務(wù)時,如用戶輸入、文件讀寫操作、網(wǎng)絡(luò)操作、搜索等。
需要一些后臺運行的程序時。
2. 線程的創(chuàng)建和啟動
2.1. 多線程實現(xiàn)的原理
Java語言的JVM允許程序運行多個線程,多線程可以通過Java中的
java.lang.Thread類來體現(xiàn)。
Thread類的特性
每個線程都是通過某個特定的Thread對象的run()方法來完成操作的,經(jīng)常吧run()方法的主體稱為線程體。
通過Thread方法的start()方法來啟動這個線程,而非直接調(diào)用run()。
2.2.多線程的創(chuàng)建,方式一:繼承于Thread類
創(chuàng)建一個繼承于Thread類的子類。
重寫Thread類的run()方法。
創(chuàng)建Thread類的子類的對象。
通過此對象調(diào)用start()來啟動一個線程。
代碼實現(xiàn):多線程執(zhí)行同一段代碼
package com.broky.multiThread;
/**
* @author 13roky
* @date 2021-04-19 21:22
*/
public class ThreadTest extends Thread{
@Override
//線程體,啟動線程時會運行run()方法中的代碼
public void run() {
//輸出100以內(nèi)的偶數(shù)
for (int i = 0; i < 100; i++) {
if (i % 2 == 0){
System.out.println(Thread.currentThread().getName()+":\t"+i);
}
}
}
public static void main(String[] args) {
//創(chuàng)建一個Thread類的子類對象
ThreadTest t1 = new ThreadTest();
//通過此對象調(diào)用start()啟動一個線程
t1.start();
//注意:已經(jīng)啟動過一次的線程無法再次啟動
//再創(chuàng)建一個線程
ThreadTest t2 = new ThreadTest();
t2.start();
//另一種調(diào)用方法,此方法并沒有給對象命名
new ThreadTest().start();
System.out.println("主線程");
}
}
多線程代碼運行圖解

多線程執(zhí)行多段代碼
package com.broky.multiThread.exer;
/**
* @author 13roky
* @date 2021-04-19 22:43
*/
public class ThreadExerDemo01 {
public static void main(String[] args) {
new Thread01().start();
new Thread02().start();
}
}
class Thread01 extends Thread {
@Override
public void run() {
for (int i = 0; i < 100; i++) {
if (i % 2 == 0) System.out.println(Thread.currentThread().getName() + ":\t" + i);
}
}
}
class Thread02 extends Thread {
@Override
public void run() {
for (int i = 0; i < 100; i++) {
if (i % 2 != 0) System.out.println(Thread.currentThread().getName() + ":\t" + i);
}
}
}
2.3.多線程的創(chuàng)建,方式一:創(chuàng)建Thread匿名子類(也屬于方法一)
package com.broky.multiThread;
/**
* @author 13roky
* @date 2021-04-19 22:53
*/
public class AnonymousSubClass {
public static void main(String[] args) {
new Thread(){
@Override
public void run() {
for (int i = 0; i < 100; i++) {
if (i % 2 == 0) System.out.println(Thread.currentThread().getName() + ":\t" + i);
}
}
}.start();
}
}
2.4. 多線程的創(chuàng)建,方式二:實現(xiàn)Runnable接口
創(chuàng)建一個實現(xiàn)Runnable接口的類。
實現(xiàn)類去實現(xiàn)Runnable接口中的抽象方法:run()。
創(chuàng)建實現(xiàn)類的對象。
將此對象作為參數(shù)傳到Thread類的構(gòu)造器中,創(chuàng)建Thread類的對象。
通過Thread類的對象調(diào)用start()方法。
package com.broky.multiThread;
/**
* @author 13roky
* @date 2021-04-20 23:16
*/
public class RunnableThread {
public static void main(String[] args) {
//創(chuàng)建實現(xiàn)類的對象
RunnableThread01 runnableThread01 = new RunnableThread01();
//創(chuàng)建Thread類的對象,并將實現(xiàn)類的對象當做參數(shù)傳入構(gòu)造器
Thread t1 = new Thread(runnableThread01);
//使用Thread類的對象去調(diào)用Thread類的start()方法:①啟動了線程 ②Thread中的run()調(diào)用了Runnable中的run()
t1.start();
//在創(chuàng)建一個線程時,只需要new一個Thread類就可,不需要new實現(xiàn)類
Thread t2 = new Thread(runnableThread01);
t2.start();
}
}
//RunnableThread01實現(xiàn)Runnable接口的run()抽象方法
class RunnableThread01 implements Runnable {
@Override
public void run() {
for (int i = 0; i < 100; i++) {
if (i % 2 == 0) System.out.println(Thread.currentThread().getName() + ":\t" + i);
}
}
}
2.4.1. 比較創(chuàng)建線程的兩種方式
Java中只允許單進程,以賣票程序TiketSales類來說,很有可能這個類本來就有父類,這樣一來就不可以繼承Thread類來完成多線程了,但是一個類可以實現(xiàn)多個接口,因此
實現(xiàn)的方式?jīng)]有類的單繼承性的局限性,用實現(xiàn)Runnable接口的方式來完成多線程更加實用。
實現(xiàn)Runnable接口的方式天然具有共享數(shù)據(jù)的特性(不用static變量)。因為繼承Thread的實現(xiàn)方式,需要創(chuàng)建多個子類的對象來進行多線程,如果子類中有變量A,而不使用static約束變量的話,每個子類的對象都會有自己獨立的變量A,只有static約束A后,子類的對象才共享變量A。而實現(xiàn)Runnable接口的方式,只需要創(chuàng)建一個實現(xiàn)類的對象,要將這個對象傳入Thread類并創(chuàng)建多個Thread類的對象來完成多線程,而這多個Thread類對象實際上就是調(diào)用一個實現(xiàn)類對象而已。
實現(xiàn)的方式更適合來處理多個線程有共享數(shù)據(jù)的情況。聯(lián)系:Thread類中也實現(xiàn)了Runnable接口
相同點兩種方式都需要重寫run()方法,線程的執(zhí)行邏輯都在run()方法中
2.5. 多線程的創(chuàng)建,方式三:實現(xiàn)Callable接口
與Runnable相比,Callable功能更強大
相比run()方法,可以有返回值
方法可以拋出異常
支持泛型的返回值
需要借助FutureTask類,比如獲取返回結(jié)果
package com.broky.multiThread;
import java.util.concurrent.Callable;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.FutureTask;
/**
* 創(chuàng)建線程的方式三:實現(xiàn)Callable接口。 ---JDK5新特性
* 如何理解Callable比Runnable強大?
* 1.call()可以有返回值
* 2.call()可以拋出異常被外面的操作捕獲
* @author 13roky
* @date 2021-04-22 21:04
*/
//1.創(chuàng)建一個實現(xiàn)Callable的實現(xiàn)類
class NumThread implements Callable<Integer>{
//2.實現(xiàn)call方法,將此線程需要執(zhí)行的操作聲明在call()中
@Override
public Integer call() throws Exception {
int sum = 0;
for (int i = 1; i < 100; i++) {
if(i%2==0){
System.out.println(i);
sum += i;
}
}
return sum;
}
}
public class ThreadNew {
public static void main(String[] args) {
//3.創(chuàng)建Callable接口實現(xiàn)類的對象
NumThread numThread = new NumThread();
//4.將此Callable接口實現(xiàn)類的對象作為參數(shù)傳遞到FutureTask構(gòu)造器中,創(chuàng)建FutureTask對象
FutureTask<Integer> futureTask = new FutureTask(numThread);
//5.將FutureTask的對象作為參數(shù)傳遞到Thread類的構(gòu)造器中,創(chuàng)建Thread對象,并調(diào)用start()
new Thread(futureTask).start();
try {
//6.獲取Callable中Call方法的返回值
Integer sum = futureTask.get();
System.out.println("總和為"+sum);
} catch (InterruptedException e) {
e.printStackTrace();
} catch (ExecutionException e) {
e.printStackTrace();
}
}
}
2.6. 多線程的創(chuàng)建,方式四:線程池
背景:
經(jīng)常創(chuàng)建和銷毀、使用量特別大的資源、比如并發(fā)情況下的線程、對性能影響很大。
思路:
提前創(chuàng)建好多個線程,放入線程池中,使用時直接獲取,使用完放回池中。可以避免頻繁創(chuàng)建銷毀、實現(xiàn)重復(fù)利用。類似生活中的公共交通工具。
優(yōu)點:
提高響應(yīng)速度(減少了創(chuàng)建新線程的時間)
降低資源消耗(重復(fù)利用線程池中線程,不需要每次都創(chuàng)建)
便于線程管理
package com.broky.multiThread;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.ThreadPoolExecutor;
/**
* 創(chuàng)建線程的方式四:使用線程池
* <p>
* 面試題:創(chuàng)建多線程有幾種方式
*
* @author 13roky
* @date 2021-04-22 21:49
*/
class NumberThread implements Runnable {
@Override
public void run() {
for (int i = 0; i < 100; i++) {
if (i % 2 == 0) {
System.out.println(Thread.currentThread().getName() + ":\t" + i);
}
}
}
}
public class ThreadPool {
public static void main(String[] args) {
//1.提供指定線程數(shù)量的線程池
ExecutorService service = Executors.newFixedThreadPool(10);
ThreadPoolExecutor service1 = (ThreadPoolExecutor) service;
//設(shè)置線程池的屬性
// System.out.println(service.getClass());
// service1.setCorePoolSize(15);
// service1.setKeepAliveTime();
//2.執(zhí)行指定的線程的操作。需要提供實現(xiàn)Runnable接口或Callable接口實現(xiàn)類的對象。
service.execute(new NumberThread()); //適合用于Runnable
// service.submit(); 適合適用于Callable
//關(guān)閉線程池
service.shutdown();
}
}
3. Thread類的常用方法
start() : 啟動當前線程, 調(diào)用當前線程的run()方法
run() : 通常需要重寫Thread類中的此方法, 將創(chuàng)建的線程要執(zhí)行的操作聲明在此方法中
currentThread() : 靜態(tài)方法, 返回當前代碼執(zhí)行的線程
getName() : 獲取當前線程的名字
setName() : 設(shè)置當前線程的名字
yield() : 釋放當前CPU的執(zhí)行權(quán)
join() : 在線程a中調(diào)用線程b的join(), 此時線程a進入阻塞狀態(tài), 知道線程b完全執(zhí)行完以后, 線程a才結(jié)束阻塞狀態(tài)
stop() : 已過時. 當執(zhí)行此方法時,強制結(jié)束當前線程.
sleep(long militime) : 讓線程睡眠指定的毫秒數(shù),在指定時間內(nèi),線程是阻塞狀態(tài)
isAlive() :判斷當前線程是否存活
4. 線程的調(diào)度
4.1. cpu的調(diào)度策略
時間片:cpu正常情況下的調(diào)度策略。即CPU分配給各個程序的時間,每個線程被分配一個時間段,稱作它的時間片,即該進程允許運行的時間,使各個程序從表面上看是同時進行的。如果在時間片結(jié)束時進程還在運行,則CPU將被剝奪并分配給另一個進程。如果進程在時間片結(jié)束前阻塞或結(jié)束,則CPU當即進行切換。而不會造成CPU資源浪費。在宏觀上:我們可以同時打開多個應(yīng)用程序,每個程序并行不悖,同時運行。但在微觀上:由于只有一個CPU,一次只能處理程序要求的一部分,如何處理公平,一種方法就是引入時間片,每個程序輪流執(zhí)行。
搶占式:高優(yōu)先級的線程搶占cpu。
4.2. Java的調(diào)度算法:
同優(yōu)先級線程組成先進先出隊列(先到先服務(wù)),使用時間片策略。
堆高優(yōu)先級,使用優(yōu)先調(diào)度的搶占式策略。
線程的優(yōu)先級等級(一共有10擋)
MAX_PRIORITY:10
MIN_PRIORITY:1
NORM_PRIORITY:5 (默認優(yōu)先級)
獲取和設(shè)置當前線程的優(yōu)先級
getPriority();獲取setPriority(int p);設(shè)置
說明:高優(yōu)先級的線程要搶占低優(yōu)先級線程cpu的執(zhí)行權(quán)。但是只是從概率上講,高優(yōu)先級的線程高概率的情況下被執(zhí)行。并不意味著只有高優(yōu)先級的線程執(zhí)行完成以后,低優(yōu)先級的線程才執(zhí)行。
5. 線程的生命周期
JDk中用Thread.State類定義了線程的幾種狀態(tài)
想要實現(xiàn)多線程,必須在主線程中創(chuàng)建新的線程對象。Java語言使用Thread類及其子類的對象來表示線程,在他的一個完整的生命周期中通常要經(jīng)歷如下的五種狀態(tài):
新建:當一個Thread類或其子類的對象被聲明并創(chuàng)建時,新的線程對象處于新建狀態(tài)。
就緒:處于新建狀態(tài)的線程被start()后,將進入線程隊列等待CPU時間片,此時它已具備了運行的條件,只是沒分配到CPU資源。
運行:當就緒的線程被調(diào)度并獲得CPU資源時,便進入運行狀態(tài),run()方法定義了線程的操作和功能。
阻塞:在某種特殊情況下,被認為掛起或執(zhí)行輸入輸出操作時,讓出CPU并臨時中止自己的執(zhí)行,進入阻塞狀態(tài)。
死亡:線程完成了它的全部工作或線程被提前強制性的中止或出現(xiàn)異常倒置導致結(jié)束。

6. 線程的同步
6.1. 多線程的安全性問題解析
線程的安全問題
多個線程執(zhí)行的不確定性硬氣執(zhí)行結(jié)果的不穩(wěn)定性
多個線程對賬本的共享, 會造成操作的不完整性, 會破壞數(shù)據(jù).
多個線程訪問共享的數(shù)據(jù)時可能存在安全性問題
線程的安全問題Demo: 賣票過程中出現(xiàn)了重票和錯票的情況 (以下多窗口售票demo存在多線程安全問題)
package com.broky.multiThread.safeThread;
/**
* @author 13roky
* @date 2021-04-21 20:39
*/
public class SafeTicketsWindow {
public static void main(String[] args) {
WindowThread ticketsThread02 = new WindowThread();
Thread t1 = new Thread(ticketsThread02);
Thread t2 = new Thread(ticketsThread02);
Thread t3 = new Thread(ticketsThread02);
t1.setName("窗口1");
t2.setName("窗口2");
t3.setName("窗口3");
t1.start();
t2.start();
t3.start();
}
}
class WindowThread implements Runnable {
private int tiketsNum = 100;
public void run() {
while (true) {
if (tiketsNum > 0) {
try {
//手動讓線程進入阻塞,增大錯票概率
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + ":\t票號:" + tiketsNum);
/*try {
//手動讓線程進入阻塞,增大重票的概率
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}*/
tiketsNum--;
} else {
break;
}
}
}
}
錯票分析:
當票數(shù)為1的時候,三個線程中有線程被阻塞沒有執(zhí)行票數(shù)-1的操作,這是其它線程就會通過if語句的判斷,這樣一來就會造成多賣了一張票,出現(xiàn)錯票的情況。
極端情況為,當票數(shù)為1時,三個線程同時判斷通過,進入阻塞,然后多執(zhí)行兩側(cè)賣票操作。

重票分析:
如果t1在輸出票號22和票數(shù)-1的操作之間被阻塞,這就導致這時候t1賣出了22號票,但是總票數(shù)沒有減少。在t1被阻塞期間,如果t2運行到輸出票號時,那么t2也會輸出和t1相同的票號22.
通過以上兩種情況可以看出,線程的安全性問題時因為多個線程正在執(zhí)行代碼的過程中,并且尚未完成的時候,其他線程參與進來執(zhí)行代碼所導致的。
6.2. 多線程安全性問題的解決
原理:
當一個線程在操作共享數(shù)據(jù)的時候,其他線程不能參與進來。知道這個線程操作完共享數(shù)據(jù)的時候,其他線程才可以操作。即使當這個線程操作共享數(shù)據(jù)的時候發(fā)生了阻塞,依舊無法改變這種情況。
在Java中,我們通過同步機制,來解決線程的安全問題。

6.2.1. 多線程安全問題的解決方式一:同步代碼塊
synchronized(同步監(jiān)視器){需要被同步的代碼}
說明:
操作共享數(shù)據(jù)(多個線程共同操作的變量)的代碼,即為需要被同步的代碼。不能多包涵代碼(效率低,如果包到while前面就變成了單線程了),也不能少包含代碼
共享數(shù)據(jù):多個線程共同操作的變量。
同步監(jiān)視器:俗稱,鎖。任何一個類的對象都可以充當鎖。但是所有的線程都必須共用一把鎖,共用一個對象。
鎖的選擇:
自行創(chuàng)建,共用對象,如下面demo中的Object對象。
使用this表示當前類的對象
繼承Thread的方法中的鎖不能使用this代替,因為繼承thread實現(xiàn)多線程時,會創(chuàng)建多個子類對象來代表多個線程,這個時候this指的時當前這個類的多個對象,不唯一,無法當作鎖。
實現(xiàn)Runnable接口的方式中,this可以當作鎖,因為這種方式只需要創(chuàng)建一個實現(xiàn)類的對象,將實現(xiàn)類的對象傳遞給多個Thread類對象來當作多個線程,this就是這個一個實現(xiàn)類的對象,是唯一的,被所有線程所共用的對象。
使用類當作鎖,以下面demo為例,其中的鎖可以寫為
WindowThread.class, 從這里可以得出結(jié)論,類也是一個對象
優(yōu)點:同步的方式,解決了線程安全的問題
缺點:操作同步代碼時,只能有一個線程參與,其他線程等待。相當于時一個單線程的過程,效率低。
Demo
package com.broky.multiThread.safeThread;
/**
* @author 13roky
* @date 2021-04-21 20:39
*/
public class SafeTicketsWindow {
public static void main(String[] args) {
WindowThread ticketsThread02 = new WindowThread();
Thread t1 = new Thread(ticketsThread02);
Thread t2 = new Thread(ticketsThread02);
Thread t3 = new Thread(ticketsThread02);
t1.setName("窗口1");
t2.setName("窗口2");
t3.setName("窗口3");
t1.start();
t2.start();
t3.start();
}
}
class WindowThread implements Runnable {
private int tiketsNum = 100;
//由于,Runnable實現(xiàn)多線程,所有線程共用一個實現(xiàn)類的對象,所以三個線程都共用實現(xiàn)類中的這個Object類的對象。
Object obj = new Object();
//如果時繼承Thread類實現(xiàn)多線程,那么需要使用到static Object obj = new Object();
public void run() {
//Object obj = new Object();
//如果Object對象在run()方法中創(chuàng)建,那么每個線程運行都會生成自己的Object類的對象,并不是三個線程的共享對象,所以并沒有給加上鎖。
while (true) {
synchronized (obj) {
if (tiketsNum > 0) {
try {
//手動讓線程進入阻塞,增大安全性發(fā)生的概率
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + ":\t票號:" + tiketsNum + "\t剩余票數(shù):" + --tiketsNum);
} else {
break;
}
}
}
}
}
6.3.2. 多線程安全問題的解決方式二:同步方法
將所要同步的代碼放到一個方法中,將方法聲明為synchronized同步方法。之后可以在run()方法中調(diào)用同步方法。
要點:
同步方法仍然涉及到同步監(jiān)視器,只是不需要我們顯示的聲明。
非靜態(tài)的同步方法,同步監(jiān)視器是:this。
靜態(tài)的同步方法,同步監(jiān)視器是:當前類本身。
Demo
package com.broky.multiThread.safeThread;
/**
* @author 13roky
* @date 2021-04-21 22:39
*/
public class Window02 {
public static void main(String[] args) {
Window02Thread ticketsThread02 = new Window02Thread();
Thread t1 = new Thread(ticketsThread02);
Thread t2 = new Thread(ticketsThread02);
Thread t3 = new Thread(ticketsThread02);
t1.setName("窗口1");
t2.setName("窗口2");
t3.setName("窗口3");
t1.start();
t2.start();
t3.start();
}
}
class Window02Thread implements Runnable {
private int tiketsNum = 100;
@Override
public void run() {
while (tiketsNum > 0) {
show();
}
}
private synchronized void show() { //同步監(jiān)視器:this
if (tiketsNum > 0) {
try {
//手動讓線程進入阻塞,增大安全性發(fā)生的概率
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + ":\t票號:" + tiketsNum + "\t剩余票數(shù):" + --tiketsNum);
}
}
}
package com.broky.multiThread.safeThread;
/**
* @author 13roky
* @date 2021-04-21 22:59
*/
public class Window03 {
public static void main(String[] args) {
Window03Thread t1 = new Window03Thread();
Window03Thread t2 = new Window03Thread();
Window03Thread t3 = new Window03Thread();
t1.setName("窗口1");
t2.setName("窗口2");
t3.setName("窗口3");
t1.setPriority(Thread.MIN_PRIORITY);
t3.setPriority(Thread.MAX_PRIORITY);
t1.start();
t2.start();
t3.start();
}
}
class Window03Thread extends Thread {
public static int tiketsNum = 100;
@Override
public void run() {
while (tiketsNum > 0) {
show();
}
}
public static synchronized void show() {//同步監(jiān)視器:Winddoe03Thread.class 不加static話同步監(jiān)視器為t1 t2 t3所以錯誤
if (tiketsNum > 0) {
try {
//手動讓線程進入阻塞,增大安全性發(fā)生的概率
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + ":\t票號:" + tiketsNum + "\t剩余票數(shù):" + --tiketsNum);
}
}
}
使用同步解決懶漢模式的線程安全問題
package com.broky.multiThread.safeThread;
/**
* @author 13roky
* @date 2021-04-22 7:24
*/
public class BankTest {
}
class Bank {
private Bank() {
}
private static Bank instance = null;
public static Bank getInstance() {
//方式一:效率性差,每個等待線程都會進入同步代碼塊
// synchronized (Bank.class) {
// if (instance == null) {
// instance = new Bank();
// }
// }
//方式二:在同步代碼塊外層在判斷一次,就防止所有線程進入同步代碼塊。
if (instance == null) {
synchronized (Bank.class) {
if (instance == null) {
instance = new Bank();
}
}
}
return instance;
}
}
6.2.3. 多線程安全問題的解決方式二:Lock鎖 -JDK5.0新特性
JDK5.0之后,可以通過實例化ReentrantLock對象,在所需要同步的語句前,調(diào)用ReentrantLock對象的lock()方法,實現(xiàn)同步鎖,在同步語句結(jié)束時,調(diào)用unlock()方法結(jié)束同步鎖
synchronized和lock的異同:(面試題)
1. Lcok是顯式鎖(需要手動開啟和關(guān)閉鎖),synchronized是隱式鎖,除了作用域自動釋放。
2. Lock只有代碼塊鎖,synchronized有代碼塊鎖和方法鎖。
3. 使用Lcok鎖,JVM將花費較少的時間來調(diào)度線程,性能更好。并且具有更好的拓展性(提供更多的子類)
建議使用順序:Lock—》同步代碼塊(已經(jīng)進入了方法體,分配了相應(yīng)的資源)—》同步方法(在方法體之外)
Demo:
package com.broky.multiThread.safeThread;
import java.util.concurrent.locks.ReentrantLock;
/**
* @author 13roky
* @date 2021-04-22 9:36
*/
public class SafeLock {
public static void main(String[] args) {
SafeLockThread safeLockThread = new SafeLockThread();
Thread t1 = new Thread(safeLockThread);
Thread t2 = new Thread(safeLockThread);
Thread t3 = new Thread(safeLockThread);
t1.start();
t2.start();
t3.start();
}
}
class SafeLockThread implements Runnable{
private int tickets = 100;
private ReentrantLock lock = new ReentrantLock();
@Override
public void run() {
while (tickets>0) {
try {
//在這里鎖住,有點類似同步監(jiān)視器
lock.lock();
if (tickets > 0) {
Thread.sleep(100);
System.out.println(Thread.currentThread().getName() + ":\t票號:" + tickets + "\t剩余票數(shù):" + --tickets);
}
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
//操作完成共享數(shù)據(jù)后在這里解鎖
lock.unlock();
}
}
}
}
6.3. 線程同步的死鎖問題
原理:
不同的線程分別占用對方需要的同步資源不放棄,都在等待對方放棄自己需要的同步資源,就形成了死鎖。
出現(xiàn)死鎖后,并不會出現(xiàn)異常,不會出現(xiàn)提示,只是所有的線程都處于阻塞狀態(tài),無法繼續(xù)。
使用同步時應(yīng)避免出現(xiàn)死鎖。
Java中死鎖最簡單的情況:
一個線程T1持有鎖L1并且申請獲得鎖L2,而另一個線程T2持有鎖L2并且申請獲得鎖L1,因為默認的鎖申請操作都是阻塞的,所以線程T1和T2永遠被阻塞了。導致了死鎖。這是最容易理解也是最簡單的死鎖的形式。但是實際環(huán)境中的死鎖往往比這個復(fù)雜的多。可能會有多個線程形成了一個死鎖的環(huán)路,比如:線程T1持有鎖L1并且申請獲得鎖L2,而線程T2持有鎖L2并且申請獲得鎖L3,而線程T3持有鎖L3并且申請獲得鎖L1,這樣導致了一個鎖依賴的環(huán)路:T1依賴T2的鎖L2,T2依賴T3的鎖L3,而T3依賴T1的鎖L1。從而導致了死鎖。
從這兩個例子,我們可以得出結(jié)論,產(chǎn)生死鎖可能性的最根本原因是:線程在獲得一個鎖L1的情況下再去申請另外一個鎖L2,也就是鎖L1想要包含了鎖L2,也就是說在獲得了鎖L1,并且沒有釋放鎖L1的情況下,又去申請獲得鎖L2,這個是產(chǎn)生死鎖的最根本原因。另一個原因是默認的鎖申請操作是阻塞的。
死鎖的解決方法:
1. 專門的算法、原則。
2. 盡量減少同步資源的定義。
3. 盡量避免嵌套同步。
package com.broky.multiThread.safeThread;
/**
* @author 13roky
* @date 2021-04-22 8:34
*/
public class DeadLock {
public static void main(String[] args) {
StringBuffer s1 = new StringBuffer();
StringBuffer s2 = new StringBuffer();
new Thread() {
public void run() {
synchronized (s1) {
s1.append("a");
s2.append("1");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
synchronized (s2) {
s1.append("b");
s2.append("2");
System.out.println(s1);
System.out.println(s2);
}
}
}
}.start();
new Thread(new Runnable() {
public void run() {
synchronized (s2) {
s1.append("c");
s2.append("3");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
synchronized (s1) {
s1.append("d");
s2.append("4");
System.out.println(s1);
System.out.println(s2);
}
}
}
}).start();
}
}
7. 線程的通信
很多情況下,盡管我們創(chuàng)建了多個線程,也會出現(xiàn)幾乎一個線程執(zhí)行完所有操作的時候,這時候我們就需要讓線程間相互交流。
原理:
當一個線程執(zhí)行完成其所應(yīng)該執(zhí)行的代碼后,手動讓這個線程進入阻塞狀態(tài),這樣一來,接下來的操作只能由其他線程來操作。當其他線程執(zhí)行的開始階段,再手動讓已經(jīng)阻塞的線程停止阻塞,進入就緒狀態(tài),雖說這時候阻塞的線程停止了阻塞,但是由于現(xiàn)在正在運行的線程拿著同步鎖,所以停止阻塞的線程也無法立馬執(zhí)行。如此操作就可以完成線程間的通信。
所用的到方法:
wait():一旦執(zhí)行此方法,當前線程就會進入阻塞,一旦執(zhí)行wait()會釋放同步監(jiān)視器。
notify():一旦執(zhí)行此方法,將會喚醒被wait的一個線程。如果有多個線程被wait,就喚醒優(yōu)先度最高的。
notifyAll() :一旦執(zhí)行此方法,就會喚醒所有被wait的線程
說明:
這三個方法必須在同步代碼塊或同步方法中使用。
三個方法的調(diào)用者必須是同步代碼塊或同步方法中的同步監(jiān)視器。
這三個方法并不時定義在Thread類中的,而是定義在Object類當中的。因為所有的對象都可以作為同步監(jiān)視器,而這三個方法需要由同步監(jiān)視器調(diào)用,所以任何一個類都要滿足,那么只能寫在Object類中。
sleep()和wait()的異同:(面試題)
相同點:兩個方法一旦執(zhí)行,都可以讓線程進入阻塞狀態(tài)。
不同點:1) 兩個方法聲明的位置不同:Thread類中聲明sleep(),Object類中聲明wait()
2) 調(diào)用要求不同:sleep()可以在任何需要的場景下調(diào)用。wait()必須在同步代碼塊中調(diào)用。
2) 關(guān)于是否釋放同步監(jiān)視器:如果兩個方法都使用在同步代碼塊呵呵同步方法中,sleep不會釋放鎖,wait會釋放鎖。
Demo:
package com.broky.multiThread;
/**
* @author 13roky
* @date 2021-04-22 13:29
*/
public class Communication {
public static void main(String[] args) {
CommunicationThread communicationThread = new CommunicationThread();
Thread t1 = new Thread(communicationThread);
Thread t2 = new Thread(communicationThread);
Thread t3 = new Thread(communicationThread);
t1.start();
t2.start();
t3.start();
}
}
class CommunicationThread implements Runnable {
int Num = 1;
@Override
public void run() {
while (true) {
synchronized (this) {
notifyAll();
if (Num <= 100) {
System.out.println(Thread.currentThread().getName() + ":\t" + Num);
Num++;
try {
wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}else{
break;
}
}
}
}
}
練習
練習1:
銀行有一個賬戶。
有兩個儲戶分別向同一個賬戶存3000元,每次存1000,存3次。每次存完打印賬戶余額。
package com.broky.multiThread.exer;
/**
* 練習1
* 銀行有一個賬戶
* 有兩個儲戶分別向同一個賬戶存3000元,每次存1000,存3次。每次存完打印賬戶余額。
* 分析:
* 1.是否有多個線程問題? 是,有兩個儲戶線程。
* 2.是否有共享數(shù)據(jù)? 是,兩個儲戶向同一個賬戶存錢
* 3.是否有線程安全問題: 有
*
* @author 13roky
* @date 2021-04-22 12:38
*/
public class AccountTest {
public static void main(String[] args) {
Account acct = new Account();
Customer c1 = new Customer(acct);
Customer c2 = new Customer(acct);
c1.setName("儲戶1");
c2.setName("儲戶2");
c1.start();
c2.start();
}
}
class Account {
private double accountSum;
public Account() {
this.accountSum = 0;
}
public Account(double accountSum) {
this.accountSum = accountSum;
}
//存錢
public void deppsit(double depositNum) {
synchronized (this) {
if (depositNum > 0) {
accountSum = accountSum + depositNum;
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + ": 存錢成功,當前余額為:\t" + accountSum);
}
}
}
}
class Customer extends Thread {
private Account acct;
public Customer(Account acct) {
this.acct = acct;
}
@Override
public void run() {
for (int i = 0; i < 3; i++) {
acct.deppsit(1000);
}
}
}
經(jīng)典例題:生產(chǎn)者和消費著問題
生產(chǎn)者( Productor)將產(chǎn)品交給店員( Clerk),而消費者( (Customer)從店員處取走產(chǎn)品, 店員一次只能持有固定數(shù)量的產(chǎn)品(比如:20),如果生產(chǎn)者試圖生產(chǎn)更多的產(chǎn)品,店員會叫生產(chǎn)者停一下,如果店中有空位放產(chǎn)品了再通知生產(chǎn)者繼續(xù)生產(chǎn); 如果店中沒有產(chǎn)品了,店員會告訴消費者等一下,如果店中有產(chǎn)品了再通知消費者來取走產(chǎn)品。
package com.broky.multiThread.exer;
/**
* - 經(jīng)典例題:生產(chǎn)者和消費著問題
* 生產(chǎn)者( Productor)將產(chǎn)品交給店員( Clerk),而消費者( (Customer)從店員處取走產(chǎn)品,
* 店員一次只能持有固定數(shù)量的產(chǎn)品(比如:20),如果生產(chǎn)者試圖生產(chǎn)更多的產(chǎn)品,店員會叫生產(chǎn)者停一下,
* 如果店中有空位放產(chǎn)品了再通知生產(chǎn)者繼續(xù)生產(chǎn); 如果店中沒有產(chǎn)品了,店員會告訴消費者等一下,
* 如果店中有產(chǎn)品了再通知消費者來取走產(chǎn)品。
*
* 分析:
* 1.是多線程問題,可以假設(shè)多個消費這和多個生產(chǎn)者是多線程的
* 2.存在操作的共享數(shù)據(jù),生產(chǎn)和購買時都需要操作經(jīng)銷商的庫存存量。
* 3.處理線程安全問題。
* 4.三個類:生產(chǎn)者,經(jīng)銷商,消費者。經(jīng)銷商被生產(chǎn)者和消費者共享。生產(chǎn)者讀取經(jīng)銷商庫存,當庫存不夠時,生產(chǎn)產(chǎn)品
* 并發(fā)給經(jīng)銷商,操作經(jīng)銷商庫存+1。消費者讀取經(jīng)銷商庫存,當有庫存時,方可進行購買,購買完成后,經(jīng)銷商庫存-1.
* @author 13roky
* @date 2021-04-22 14:36
*/
public class ProductTest {
public static void main(String[] args) {
Clerk clerk = new Clerk();
Producer p1 = new Producer(clerk);
Producer p2 = new Producer(clerk);
p1.setName("生產(chǎn)者1");
p2.setName("生產(chǎn)者2");
Consumer c1 = new Consumer(clerk);
Consumer c2 = new Consumer(clerk);
c1.setName("消費者1");
c2.setName("消費者2");
p1.start();
c1.start();
}
}
class Clerk {
private int productNum;
public Clerk() {
this.productNum = 0;
}
public int getProductNum() {
return productNum;
}
public void setProductNum(int productNum) {
this.productNum = productNum;
}
}
class Producer extends Thread {
private Clerk clerk;
@Override
public void run() {
System.out.println(Thread.currentThread().getName() + "開始生產(chǎn)......");
while(true){
try {
Thread.sleep(500);
} catch (InterruptedException e) {
e.printStackTrace();
}
produce();
}
}
public Producer(Clerk clerk) {
if (clerk != null) {
this.clerk = clerk;
}
}
private void produce() {
synchronized (ProductTest.class) {
ProductTest.class.notify();
if (clerk.getProductNum() < 20) {
clerk.setProductNum(clerk.getProductNum() + 1);
System.out.println(Thread.currentThread().getName() + ":\t生產(chǎn)完成第 " + clerk.getProductNum() + " 個產(chǎn)品");
}else {
try {
ProductTest.class.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
}
class Consumer extends Thread {
private Clerk clerk;
@Override
public void run() {
System.out.println(Thread.currentThread().getName() + "開始消費......");
while(true){
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
buy();
}
}
public Consumer(Clerk clerk) {
if (clerk != null) {
this.clerk = clerk;
}
}
private void buy(){
synchronized (ProductTest.class) {
ProductTest.class.notify();
if (clerk.getProductNum() > 0) {
System.out.println(Thread.currentThread().getName() + ":\t購買完成第 " + clerk.getProductNum() + " 個產(chǎn)品");
clerk.setProductNum(clerk.getProductNum() - 1);
}else {
try {
ProductTest.class.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
}
粉絲福利:Java從入門到入土學習路線圖
??????

??長按上方微信二維碼 2 秒
感謝點贊支持下哈 
