Java內(nèi)存模型:
運行時數(shù)據(jù)區(qū)域:
根據(jù)JVM規(guī)范,JVM內(nèi)存共分為虛擬機棧、堆搬男、方法區(qū)、程序計數(shù)器彭沼、本地方法棧五個部分缔逛。
程序計數(shù)器:
程序計數(shù)器(Program Counter Register)是一塊較小的內(nèi)存空間,它的作用可以看做是當(dāng)前線程所執(zhí)行的字節(jié)碼的行號指示器姓惑。在虛擬機的概念模型里(僅是概念模型褐奴,各種虛擬機可能會通過一些更高效的方式去實現(xiàn)),字節(jié)碼解釋器工作時就是通過改變這個計數(shù)器的值來選取下一條需要執(zhí)行的字節(jié)碼指令于毙,分支敦冬、循環(huán)、跳轉(zhuǎn)唯沮、異常處理脖旱、線程恢復(fù)等基礎(chǔ)功能都需要依賴這個計數(shù)器來完成堪遂。
每條線程都需要有一個獨立的程序計數(shù)器,各條線程之間的計數(shù)器互不影響萌庆,獨立存儲是線程安全的溶褪。
此內(nèi)存區(qū)域是唯一一個在Java虛擬機規(guī)范中沒有規(guī)定任何OutOfMemoryError
情況的區(qū)域。
- 每個線程都會有一個程序計數(shù)器
- 各線程的程序計數(shù)器是線程私有的践险,互不影響猿妈,是線程安全的
- 程序計數(shù)器記錄線程正在執(zhí)行的內(nèi)存地址,以便被中斷線程恢復(fù)執(zhí)行時再次按照中斷時的指令地址繼續(xù)執(zhí)行
棧(Java虛擬機棧)
與程序計數(shù)器一樣巍虫,Java虛擬機棧(Java Virtual Machine Stacks)也是線程私有的于游,它的生命周期與線程相同。虛擬機棧描述的是Java方法執(zhí)行的內(nèi)存模型:每個方法被執(zhí)行的同時會創(chuàng)建一個棧幀(Stack Frame)用于存儲局部變量表垫言、操作棧贰剥、動態(tài)鏈接、方法出口等信息筷频。每一個方法被調(diào)用直至執(zhí)行完成的過程蚌成,就對應(yīng)著一個棧幀在虛擬機棧中從入棧到出棧的過程。
該區(qū)域在無法申請到足夠內(nèi)存時會拋出StackOverflowError
和OutOfMemoryError
異常
- 每個線程會對應(yīng)一個java棧
- 每個java棧由若干棧幀組成
- 棧幀在方法運行時凛捏,創(chuàng)建并入棧担忧;方法執(zhí)行完,該棧幀彈出棧幀中的元素作為該方法的返回值坯癣,該棧幀被清除
- 棧頂?shù)臈谢顒訔F渴ⅲ硎井?dāng)前執(zhí)行的方法,才可以被CPU執(zhí)行
- 線程請求的棧深度大于虛擬機所允許的深度示罗,會拋出
StackOverFlowError
異常 - 棧擴展時無法申請到足夠的內(nèi)存惩猫,就會拋出
OutOfMemoryError
異常
本地方法棧:
本地方法棧(Native Method Stacks)與虛擬機棧所發(fā)揮的作用是非常相似的,其區(qū)別不過是虛擬機棧為虛擬機執(zhí)行Java方法(也就是字節(jié)碼)服務(wù)蚜点,而本地方法棧則是為虛擬機使用到的Native方法服務(wù)轧房。虛擬機規(guī)范中對本地方法棧中的方法使用的語言、使用方式與數(shù)據(jù)結(jié)構(gòu)并沒有強制規(guī)定绍绘,因此具體的虛擬機可以自由實現(xiàn)它奶镶。
會拋出StackOverflowError
和OutOfMemoryError
異常。
- 本地方法棧和java棧所發(fā)揮的作用非常相似陪拘,區(qū)別不過是java棧為JVM執(zhí)行java方法服務(wù)厂镇,而本地方法棧為JVM執(zhí)行Native方法服務(wù)
- 本地方法棧也會拋出
StackOverflowError
和OutOfMemoryError
異常
堆:
對于大多數(shù)應(yīng)用來說,Java堆(Java Heap)是Java虛擬機所管理的內(nèi)存中最大的一塊左刽。Java堆是被所有線程共享的一塊內(nèi)存區(qū)域捺信,在虛擬機啟動時創(chuàng)建。此內(nèi)存區(qū)域的唯一目的就是存放對象實例悠反,幾乎所有的對象實例都在這里分配內(nèi)存残黑。這一點在Java虛擬機規(guī)范中的描述是:所有的對象實例以及數(shù)組都要在堆上分配.
但是隨著JIT編譯器的發(fā)展與逃逸分析技術(shù)的逐漸成熟,棧上分配斋否、標量替換優(yōu)化技術(shù)將會導(dǎo)致一些微妙的變化發(fā)生梨水,所有的對象都分配在堆上也漸漸變得不是那么“絕對”了。
Java堆是垃圾收集器管理的主要區(qū)域茵臭,因此很多時候也被稱做“GC堆”(Garbage Collected Heap)疫诽。如果從內(nèi)存回收的角度看,如果從內(nèi)存回收的角度看旦委,由于現(xiàn)在收集器基本都是采用的分代收集算法奇徒,所以Java堆中還可以細分為:新生代和老年代;再細致一點的有Eden空間缨硝、From Survivor空間摩钙、To Survivor空間等。
會出OutOfMemoryError
異常查辩。
方法區(qū):
方法區(qū)(Method Area)與Java堆一樣胖笛,是各個線程共享的內(nèi)存區(qū)域,它用于存儲已被虛擬機加載的類信息宜岛、常量长踊、靜態(tài)變量、即時編譯器編譯后的代碼等數(shù)據(jù)萍倡。雖然Java虛擬機規(guī)范把方法區(qū)描述為堆的一個邏輯部分身弊,但是它卻有一個別名叫做Non-Heap(非堆),目的應(yīng)該是與Java堆區(qū)分開來列敲。
這個區(qū)域的內(nèi)存回收目標主要是針對常量池的回收和對類型的卸載阱佛,一般來說這個區(qū)域的回收“成績”比較難以令人滿意,尤其是類型的卸載戴而,條件相當(dāng)苛刻瘫絮。
會拋出OutOfMemoryError
異常。
- 方法區(qū)是java堆的永久區(qū)
- 方法區(qū)存放了要加載的類信息(名稱填硕、修飾符)麦萤、類中的靜態(tài)變量、類中定義為final類型的常量扁眯、類中的Filed信息壮莹、類中的方法信息
- 方法區(qū)是被java線程共享的
- 方法區(qū)要使用的內(nèi)存超過允許的大小時,會拋出
OutOfMemoryError:PremGen space
的錯誤
運行時常量池:
運行時常量池(Runtime Constant Pool)是方法區(qū)的一部分姻檀。Class文件中除了有類的版本命满、字段、方法绣版、接口等描述等信息外胶台,還有一項信息是常量池(Constant Pool Table)歼疮,用于存放編譯期生成的各種字面量和符號引用,這部分內(nèi)容將在類加載后存放到方法區(qū)的運行時常量池中诈唬。具備動態(tài)性(如String類的intern()方法)韩脏。
會受到方法區(qū)內(nèi)存的限制,當(dāng)常量池?zé)o法再申請到內(nèi)存時會拋出OutOfMemoryError
異常铸磅。
- 常量池是方法區(qū)的一部分
- 常量池中存儲兩類數(shù)據(jù):字面量和引用量
字面量:字符串赡矢、final變量等。
引用量:類/接口阅仔、方法和字段的名稱和描述符 - 常量池在編譯期間就被確定吹散,并保存在已編譯的.class文件中
對象訪問:
在Java語言中,對象訪問是如何進行的八酒?
對象訪問在Java語言中無處不在空民,是最普通的程序行為,但即使是最簡單的訪問羞迷,也會卻涉及Java棧袭景、Java堆、方法區(qū)這三個最重要內(nèi)存區(qū)域闭树。
如:
Object obj = new Object();
假設(shè)這句代碼出現(xiàn)在方法體中
- 那“
Object obj
”這部分的語義將會反映到Java棧的本地變量表中耸棒,作為一個reference類型數(shù)據(jù)出現(xiàn)。 - 而“
new Object()”
這部分的語義將會反映到Java堆中报辱,形成一塊存儲了Object類型所有實例數(shù)據(jù)值的結(jié)構(gòu)化內(nèi)存与殃,這塊內(nèi)存的長度是不固定的。 - 在Java堆中還必須包含能查找到此對象類型數(shù)據(jù)(如對象類型碍现、父類幅疼、實現(xiàn)的接口、方法等)的地址信息昼接,這些類型數(shù)據(jù)則存儲在方法區(qū)中爽篷。
常量池分類:
全局字符串池:
string pool也有叫做string literal pool
全局字符串池里的內(nèi)容是在類加載完成,經(jīng)過驗證慢睡,準備階段之后在堆中生成字符串對象實例逐工,然后將該字符串對象實例的引用值存到string pool中(string pool中存的是引用值而不是具體的實例對象,具體的實例對象是在堆中開辟的一塊空間存放漂辐。)泪喊,只有一份, 被所有類髓涯,所有線程共享袒啼。
Class文件常量池:
.java文件被編譯為.class文件時產(chǎn)生。存在于文件中。class文件中除了包含類的版本蚓再、字段滑肉、方法、接口等描述信息外摘仅,還有一項信息就是常量池(constant pool table)靶庙,用于存放編譯器生成的各種字面量和符號引用。
字面量就是我們所說的常量概念实檀,如文本字符串、被聲明為final的常量值等按声。 符號引用是一組符號來描述所引用的目標膳犹,符號可以是任何形式的字面量,只要使用時能無歧義地定位到目標即可
一般包括下面三種:
- 類和接口的全限定名
- 字段的名稱和描述符
- 方法的名稱和描述符
運行時常量池:
runtime constant pool
當(dāng)java文件被編譯成class文件之后签则,也就是會生成我上面所說的class常量池须床,那么運行時常量池又是什么時候產(chǎn)生的呢?
jvm在執(zhí)行某個類的時候渐裂,必須經(jīng)過加載豺旬、連接、初始化柒凉,而連接又包括驗證族阅、準備、解析三個階段膝捞。而當(dāng)類加載到內(nèi)存中后坦刀,jvm就會將class常量池中的內(nèi)容存放到運行時常量池中,由此可知蔬咬,運行時常量池也是每個類都有一個鲤遥。
class常量池中存的是字面量和符號引用,也就是說他們存的并不是對象的實例林艘,而是對象的符號引用值盖奈。而經(jīng)過解析之后,也就是把符號引用替換為直接引用狐援,解析的過程會去查詢?nèi)肿址馗痔梗簿褪俏覀兩厦嫠f的StringTable,以保證運行時常量池所引用的字符串與全局字符串池中所引用的是一致的啥酱。
多線程通信
Lambda捕獲異常:
使用自定義 Lambda 實現(xiàn)方法中拋異常的統(tǒng)一攔截處理
代碼實現(xiàn):
package com.multithreading.auto;
//該注解表示該類是一個函數(shù)式接口
@FunctionalInterface
public interface Runnable {
void run() throws Exception;
}
@FunctionalInterface
public interface Function <T>{
T run() throws Exception;
}
package com.multithreading.auto;
public class AutoThrow {
public static void exec(Runnable runnable) {
try {
runnable.run();
} catch (Exception e) {
e.printStackTrace();
}
}
public static <T> T exec(Function<T> function) {
try {
return function.run();
} catch (Exception e) {
e.printStackTrace();
return null;
}
}
}
具體使用:
//使用自己的函數(shù)式接口的方法來處理this.wait()方法要拋出異常的處理
AutoThrow.exec(this::wait);
休眠和喚醒方式:
示例一:
使用兩個線程操作同一個變量场钉,并根據(jù)不同的情況打印奇數(shù)還是偶數(shù)
代碼實現(xiàn):
import com.multithreading.auto.AutoThrow;
public class Run {
//定義一個初始值
private int i = 0;
/**
* 打印偶數(shù)的線程
*/
private void odd() {
while (i < 10) {
synchronized (this) {
if (i % 2 == 1) {
System.out.println(Thread.currentThread().getName() + " : 類中的變量為奇數(shù) " + i);
i++;
//喚醒其他等待的線程
this.notify();
} else {
AutoThrow.exec(this::wait);
}
}
}
}
/**
* 打印偶數(shù)的線程
*/
private void even() {
while (i < 10) {
synchronized (this) {
if (i % 2 == 0) {
System.out.println(Thread.currentThread().getName() + " : 類中的變量為偶數(shù) " + i);
i++;
//喚醒其他等待的線程
this.notify();
} else {
//讓該線程等待
AutoThrow.exec(this::wait);
}
}
}
}
public static void main(String[] args) {
Run run = new Run();
//創(chuàng)建線程
Thread thread1 = new Thread(run::odd);
Thread thread2 = new Thread(run::even);
//調(diào)用線程
thread1.start();
thread2.start();
}
}
打印結(jié)果:
Thread-1 : 類中的變量為偶數(shù) 0
Thread-0 : 類中的變量為奇數(shù) 1
Thread-1 : 類中的變量為偶數(shù) 2
Thread-0 : 類中的變量為奇數(shù) 3
Thread-1 : 類中的變量為偶數(shù) 4
Thread-0 : 類中的變量為奇數(shù) 5
Thread-1 : 類中的變量為偶數(shù) 6
Thread-0 : 類中的變量為奇數(shù) 7
Thread-1 : 類中的變量為偶數(shù) 8
Thread-0 : 類中的變量為奇數(shù) 9
示例二:
使用 Lock
類來給線程加鎖和解鎖,使用 Condition
的signal()
和await()
方法喚醒和等待線程
import com.multithreading.auto.AutoThrow;
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
public class Run {
//定義一個初始值
private int i = 0;
//鎖對象
private final Lock lock = new ReentrantLock();
private final Condition condition = lock.newCondition();
//打印偶數(shù)的線程
private void odd() {
while (i < 10) {
//加鎖
lock.lock();
if (i % 2 == 1) {
System.out.println(Thread.currentThread().getName() + " : 類中的變量為奇數(shù) " + i);
i++;
//喚醒其他等待的線程
condition.signal();
} else {
AutoThrow.exec(condition::await);
}
//解鎖
lock.unlock();
}
}
//打印偶數(shù)的線程
private void even() {
while (i < 10) {
//加鎖
lock.lock();
if (i % 2 == 0) {
System.out.println(Thread.currentThread().getName() + " : 類中的變量為偶數(shù) " + i);
i++;
//喚醒其他等待的線程
condition.signal();
} else {
//讓該線程等待
AutoThrow.exec(condition::await);
}
//解鎖
lock.unlock();
}
}
public static void main(String[] args) {
Run run = new Run();
//創(chuàng)建線程
Thread thread1 = new Thread(run::odd);
Thread thread2 = new Thread(run::even);
//調(diào)用線程
thread1.start();
thread2.start();
}
}
打印結(jié)果:
Thread-1 : 類中的變量為偶數(shù) 0
Thread-0 : 類中的變量為奇數(shù) 1
Thread-1 : 類中的變量為偶數(shù) 2
Thread-0 : 類中的變量為奇數(shù) 3
Thread-1 : 類中的變量為偶數(shù) 4
Thread-0 : 類中的變量為奇數(shù) 5
Thread-1 : 類中的變量為偶數(shù) 6
Thread-0 : 類中的變量為奇數(shù) 7
Thread-1 : 類中的變量為偶數(shù) 8
Thread-0 : 類中的變量為奇數(shù) 9
Object
和Condition
的等待和喚醒的區(qū)別:
-
Object.wait()
必須是在synchronized(同步鎖)
下使用 -
Object.wait()
喚醒必須通過notify()
方法進行喚醒 -
Condition.await()
必須使用Lock
(互斥鎖/共享鎖) 配合使用 -
Condition.await()
必須使用signal()
方法進行喚醒
CountDownLatch
方式:
-
CountDownLatch
是在java1.5被引入的懈涛,存在于java .util.concurrent
包下逛万。 -
CountDownLatch
能夠使一個線程等待其他線程完成各自的工作后再執(zhí)行。 -
CountDownLatch
是通過一個計數(shù)器來實現(xiàn)的,計數(shù)器的初始值為線程的數(shù)量宇植。
每當(dāng)一個線程完成了自己的任務(wù)后得封,計數(shù)器就會減1。當(dāng)計數(shù)器值達到0時指郁,它表示所有的線程都以及執(zhí)行完了任務(wù)忙上,然后在閉鎖上等待的線程可以恢復(fù)執(zhí)行任務(wù)。
需求:教練訓(xùn)練運動員闲坎,教練需要等待所有運動員到齊才能開始訓(xùn)練任務(wù)疫粥。
代碼實現(xiàn):
import com.multithreading.auto.AutoThrow;
import java.util.concurrent.CountDownLatch;
public class Run {
//定義CountDownLatch 并設(shè)置要等待幾個線程執(zhí)行完
private final CountDownLatch countDownLatch = new CountDownLatch(3);
//教練訓(xùn)練的方法
private void coach() {
//獲取線程名稱
String name = Thread.currentThread().getName();
//等待所有運動員準備完成
System.out.println(name + " 等待運動員準備完成……");
//調(diào)用CountDownLatch.await() 等待其他線程運行完
AutoThrow.exec(countDownLatch::await);
System.out.println(name + " 開始訓(xùn)練任務(wù)");
}
//運動員方法
private void athletes() {
//獲取線程名稱
String name = Thread.currentThread().getName();
System.out.println(name + " 開始準備……");
//線程休眠一秒
AutoThrow.exec(() -> Thread.sleep(1000));
System.out.println(name + " 準備完成");
//調(diào)用countDownLatch.countDown() 方法使等待計數(shù)中減1,否者一直都是設(shè)置的等待數(shù)
countDownLatch.countDown();
}
public static void main(String[] args) {
Run run = new Run();
//創(chuàng)建線程,并設(shè)置線程名字
Thread thread1 = new Thread(run::coach, "教練");
Thread thread2 = new Thread(run::athletes, "運動員1");
Thread thread3 = new Thread(run::athletes, "運動員2");
Thread thread4 = new Thread(run::athletes, "運動員3");
//調(diào)用線程
thread1.start();
thread2.start();
thread3.start();
thread4.start();
}
}
打印結(jié)果:
教練 等待運動員準備完成……
運動員3 開始準備……
運動員1 開始準備……
運動員2 開始準備……
運動員1 準備完成
運動員2 準備完成
運動員3 準備完成
教練 開始訓(xùn)練任務(wù)
CyclicBarrier
方式:
-
CyclicBarrier
是在java1.5被引入的,存在于java.util.concurrent
包下腰懂。 -
CyclicBarrier
實現(xiàn)讓一組線程等待至某個狀態(tài)之后再全部同時執(zhí)行梗逮。 -
CyclicBarrier
底層是基于ReentrantLock
和Condition
實現(xiàn)。
需求:保證三個線程同時啟動
代碼實現(xiàn):
import com.multithreading.auto.AutoThrow;
import java.util.concurrent.CyclicBarrier;
public class Run {
//創(chuàng)建CyclicBarrier 對象,傳遞參數(shù)表示參與CyclicBarrier中的線程數(shù)
private final CyclicBarrier cyclicBarrier = new CyclicBarrier(3);
private void startThread() {
String name = Thread.currentThread().getName();
System.out.println(name + " 正在準備");
//調(diào)用CyclicBarrier的awiat()方法等待線程全部準備完成
AutoThrow.exec(cyclicBarrier::await);
System.out.println(name + " 已經(jīng)啟動完畢 " + System.currentTimeMillis());
}
public static void main(String[] args) {
Run run = new Run();
//創(chuàng)建線程,并設(shè)置線程名字
Thread thread1 = new Thread(run::startThread, "運動員1");
Thread thread2 = new Thread(run::startThread, "運動員2");
Thread thread3 = new Thread(run::startThread, "運動員3");
//調(diào)用線程
thread1.start();
thread2.start();
thread3.start();
}
}
打印結(jié)果:
運動員1 正在準備
運動員3 正在準備
運動員2 正在準備
運動員2 已經(jīng)啟動完畢 1652436202457
運動員3 已經(jīng)啟動完畢 1652436202457
運動員1 已經(jīng)啟動完畢 1652436202457
semaphore方式:
-
semaphore
是在java1.5被引入的绣溜,存在于java.util.concurrent
包下慷彤。 -
semaphore
用于控制對某組資源的訪問權(quán)限。
需求:8個工人使用3臺機器工作怖喻,機器為互斥資源(即每次只能一個人使用)
代碼實現(xiàn):
import com.multithreading.auto.AutoThrow;
import java.util.concurrent.Semaphore;
public class Run {
//內(nèi)部類實現(xiàn)Runnable接口
static class work implements Runnable {
//使用java.util.concurrent包下的類來表示機器數(shù)
private final Semaphore semaphore;
public work(Semaphore semaphore) {
this.semaphore = semaphore;
}
@Override
public void run() {
AutoThrow.exec(() -> {
//工人獲取機器
semaphore.acquire();
String name = Thread.currentThread().getName();
System.out.println(name + " 獲取到機器,開始工作");
//睡眠一秒鐘,模擬工人使用機器的場景
Thread.sleep(1000);
//使用完畢是否資源
semaphore.release();
System.out.println(name + " 使用完畢,釋放資源");
});
}
}
public static void main(String[] args) {
//工人數(shù)
int works = 8;
//代表機器數(shù)
Semaphore semaphore = new Semaphore(3);
for (int i = 0; i < works; i++) {
new Thread(new work(semaphore), "工人" + i).start();
}
}
}
打印結(jié)果:
工人2 獲取到機器,開始工作
工人6 獲取到機器,開始工作
工人4 獲取到機器,開始工作
工人4 使用完畢,釋放資源
工人3 獲取到機器,開始工作
工人5 獲取到機器,開始工作
工人2 使用完畢,釋放資源
工人6 使用完畢,釋放資源
工人7 獲取到機器,開始工作
工人5 使用完畢,釋放資源
工人0 獲取到機器,開始工作
工人1 獲取到機器,開始工作
工人7 使用完畢,釋放資源
工人3 使用完畢,釋放資源
工人0 使用完畢,釋放資源
工人1 使用完畢,釋放資源
總結(jié):
sleep
和wait
的區(qū)別:
wait | sleep | |
---|---|---|
同步 | 只能在同步上下文中調(diào)用wait方法底哗,否則拋出lliegalMonitorStateException 異常 |
不需要在同步方法或同步代碼塊中調(diào)用 |
作用對象 |
wait 方法定義在Object 類中,作用于對象本身 |
sleep方法定義在java.lang.Thread 中锚沸,作用于當(dāng)前線程 |
釋放資源 | 是 | 是 |
喚醒條件 | 其他線程調(diào)用對象的notify() 或者notifyAll() 方法 |
超時或者調(diào)用interrupt() 方法體 |
方法屬性 |
wait 是實例方法 |
sleep 是靜態(tài)方法 |
wait
和notify
區(qū)別:
-
wait
和notify
都是Object
中的方法 -
wait
和notify
執(zhí)行前線程都必須獲取鎖 -
wait
的作用是使當(dāng)前線程進行等待 -
notify
的作用是通知其他等待當(dāng)前線程的對象鎖的過程
多線程特性:
多線程編程要滿足三個特性:原子性跋选,可見性,一致性哗蜈。
- 原子性:原子性野建,即一個操作或多個操作要么全部執(zhí)行并且執(zhí)行的過程不會被任何因素打斷,要么就都不執(zhí)行恬叹。
- 可見性:是指當(dāng)多個線程訪問同一個變量時候生,一個線程修改了這個變量的值,其他線程能夠立即看得到修改的值绽昼。顯然唯鸭,對于單線程來說,可見性問題是不存在的硅确。
- 有序性:有序性即程序執(zhí)行的順序按照代碼的先后順序執(zhí)行目溉。
多線程控制類:
為了保證多線程的三個特性,java引入了很多線程控制機制菱农,下面介紹其中常用的幾種:
-
ThreadLocal
:線程本地變量 - 原子類:保證變量原子操作
-
Lock
類:保證線程有序性 -
Volatile
關(guān)鍵字:保證線程變量可見性
ThreadLocal
:
作用:
ThreadLocal
提供線程局部變量缭付,即為使用相同變量的每一個線程維護一個該變量的副本。當(dāng)某些數(shù)據(jù)是以線程為作用域并且不同線程具有不同的數(shù)據(jù)副本的時候循未,就可以考慮采用ThreadLocal
陷猫,比如數(shù)據(jù)庫連接Connection
,每個請求處理線程都需要,但又不相互影響绣檬,就是用ThreadLocal
實現(xiàn)足陨。
常用方法:
- initialValue:副本創(chuàng)建方法
- get:獲取副本方法
- set:設(shè)置副本方法
示例:
模擬兩個線程轉(zhuǎn)賬:
public class Run {
//創(chuàng)建銀行對象:錢、取款娇未、存款
static class Bank {
//匿名內(nèi)部類
private final ThreadLocal<Integer> threadLocal = new ThreadLocal<Integer>() {
@Override
protected Integer initialValue() {
return 0;
}
};
public Integer get() {
return threadLocal.get();
}
public void set(Integer value) {
threadLocal.set(threadLocal.get() + value);
}
}
//創(chuàng)建轉(zhuǎn)賬對象:從銀行中取錢墨缘,轉(zhuǎn)賬,保存到賬戶
static class Transfer implements Runnable {
private final Bank bank;
public Transfer(Bank bank) {
this.bank = bank;
}
@Override
public void run() {
for (int a = 0; a < 10; a++) {
bank.set(10);
Integer integer = bank.get();
String name = Thread.currentThread().getName();
System.out.println(name + " :賬戶余額 " + integer);
}
}
}
public static void main(String[] args) {
Bank bank = new Bank();
Transfer transfer = new Transfer(bank);
Thread thread1 = new Thread(transfer, "客戶1");
Thread thread2 = new Thread(transfer, "客戶2");
thread1.start();
thread2.start();
}
}
打印結(jié)果:
客戶1 :賬戶余額 10
客戶2 :賬戶余額 10
客戶1 :賬戶余額 20
客戶2 :賬戶余額 20
客戶1 :賬戶余額 30
客戶2 :賬戶余額 30
客戶1 :賬戶余額 40
客戶2 :賬戶余額 40
客戶1 :賬戶余額 50
客戶1 :賬戶余額 60
客戶2 :賬戶余額 50
客戶1 :賬戶余額 70
客戶2 :賬戶余額 60
客戶1 :賬戶余額 80
客戶2 :賬戶余額 70
客戶1 :賬戶余額 90
客戶2 :賬戶余額 80
客戶1 :賬戶余額 100
客戶2 :賬戶余額 90
客戶2 :賬戶余額 100
- 在
ThreadLocal
類中定義了一個ThreadLocalMap
- 每一個
Thread
都有一個ThreadLocalMap
類型的變量ThreadLocals
-
threadLocals
內(nèi)部有一個Entry零抬,Entry的key是ThreadLocal
,對象實例镊讼,value就是共享變量副本 -
ThreadLlocal
的get方法就是根據(jù)ThreadLocal
.對象實例獲取共享變量副本 -
ThreadLlocal
的set方法就是根據(jù)ThreadLocal
對象實例保存共享變量副本
原子類:
Java的 java.util, concurrent.atomic
包里面提供了很多可以進行原子操作的類,分為以下四類:
- 原子更新基本類型:
AtomicInteger
平夜、AtomicBoolean
蝶棋、stomisLong
- 原子更新數(shù)組類型:
AtomicIntegerArray
、AtomicLongArray
- 原子更新引用類型:
AtomicReference
褥芒、AtomicStampedReference
等 - 原子更新屬性類型:
AtomicIntegerFieldUpdater
嚼松、stomicLongFieldUpdater
提供這些原子類的目的就是為了解決基本類型操作的非原子性導(dǎo)致在多線程并發(fā)情況下引發(fā)的問題嫡良。
非原子操作問題演示:
非原子操作會引發(fā)什么問題锰扶?下面以 i++ 為例演示非原子操作問題。
i++ 并不是原子操作寝受,而是由三個操作構(gòu)成:
tp1=i;
tp2=tp1+1;
i=tp1;
所以單線程 i 的值不會有問題的坷牛,但多線程下就會出錯,多線程代碼示例:
import com.multithreading.auto.AutoThrow;
public class Run {
//執(zhí)行n++操作變量
private static int n;
public static void main(String[] args) {
int j = 0;
while (j < 100) {
n = 0;
Thread thread1 = new Thread(() -> {
for (int i = 0; i < 1000; i++) {
n++;
}
});
Thread thread2 = new Thread(() -> {
for (int i = 0; i < 1000; i++) {
n++;
}
});
thread1.start();
thread2.start();
AutoThrow.exec(() -> {
thread1.join();
thread2.join();
});
System.out.println("n的最終值為:" + n);
j++;
}
}
}
打印結(jié)果:發(fā)現(xiàn)最后n的值可能達不到2000
n的最終值為:2000
n的最終值為:2000
n的最終值為:1559
n的最終值為:1645
n的最終值為:2000
n的最終值為:2000
n的最終值為:2000
n的最終值為:2000
n的最終值為:1781
n的最終值為:2000
n的最終值為:1480
n的最終值為:2000
n的最終值為:1945
n的最終值為:2000
……
原子類解決非原子類的操作問題:
AtomicInteger
類可以保證++操作原子性:
//getAndIncrement() 對應(yīng):n++
//incrementAndGet() 對應(yīng):++n
//decrementAndGet() 對應(yīng):--n
//getAndDecrement() 對應(yīng):n--
以上代碼修改如下:
import com.multithreading.auto.AutoThrow;
import java.util.concurrent.atomic.AtomicInteger;
public class Run {
//執(zhí)行n++操作變量
// private static int n;
//定義一個原子類的integer
private static AtomicInteger atomicInteger;
public static void main(String[] args) {
int j = 0;
while (j < 100) {
//給原子類的integer設(shè)置初始值0
atomicInteger = new AtomicInteger(0);
Thread thread1 = new Thread(() -> {
for (int i = 0; i < 1000; i++) {
//用原子方法代替++方法
atomicInteger.getAndIncrement();
}
});
Thread thread2 = new Thread(() -> {
for (int i = 0; i < 1000; i++) {
//用原子方法代替++方法
atomicInteger.getAndIncrement();
}
});
thread1.start();
thread2.start();
AutoThrow.exec(() -> {
thread1.join();
thread2.join();
});
//調(diào)用atomicInteger.get() 獲取值
System.out.println("n的最終值為:" + atomicInteger.get());
j++;
}
}
}
打印結(jié)果:
n的最終值為:2000
n的最終值為:2000
n的最終值為:2000
n的最終值為:2000
n的最終值為:2000
n的最終值為:2000
n的最終值為:2000
……全部都是2000
ABA問題解決:
tomicStampedRefexence
解決ABA問題的方法
-
AtomicStampedReference
(初始值很澄,時間勤):構(gòu)造函數(shù)設(shè)置初始值和初始時間戳 -
getStamp
:獲取時間戳 -
getReference
:獲取預(yù)期值 -
compareAndSet
(預(yù)期值京闰,更新值,預(yù)期時間戳甩苛,更新時間戳):實現(xiàn)CAS時間戳和預(yù)期值的比對
import com.multithreading.auto.AutoThrow;
import java.util.concurrent.atomic.AtomicStampedReference;
public class Run {
//執(zhí)行n++操作變量
// private static int n;
//定義一個原子類的integer
private static AtomicStampedReference<Integer> atomicInteger;
public static void main(String[] args) {
int j = 0;
while (j < 100) {
//給原子類的integer設(shè)置初始值0 時間戳
atomicInteger = new AtomicStampedReference(0, 0);
Thread thread1 = new Thread(() -> {
for (int i = 0; i < 1000; i++) {
//用原子方法代替++方法
int stamp;
Integer reference;
do {
//獲取預(yù)期的時間戳
stamp = atomicInteger.getStamp();
//獲取預(yù)期的值
reference = atomicInteger.getReference();
} while (!atomicInteger.compareAndSet(reference, reference + 1, stamp, stamp + 1));
//CAS比較并替換 第一個參數(shù)是預(yù)期值蹂楣,第二個參數(shù)是更新值,第三個參數(shù)預(yù)取時間戳讯蒲,更新時間戳
}
});
Thread thread2 = new Thread(() -> {
for (int i = 0; i < 1000; i++) {
int stamp;
Integer reference;
do {
//獲取預(yù)期的時間戳:版本號
stamp = atomicInteger.getStamp();
//獲取預(yù)期的值
reference = atomicInteger.getReference();
} while (!atomicInteger.compareAndSet(reference, reference + 1, stamp, stamp + 1));
//CAS比較并替換 第一個參數(shù)是預(yù)期值痊土,第二個參數(shù)是更新值,第三個參數(shù)預(yù)取時間戳墨林,更新時間戳
}
});
thread1.start();
thread2.start();
AutoThrow.exec(() -> {
thread1.join();
thread2.join();
});
//調(diào)用atomicInteger.getReference() 獲取值
System.out.println("n的最終值為:" + atomicInteger.getReference());
j++;
}
}
}
Lock類:
Lock
和ReadWriteLock
是兩大鎖的根接口
Lock 接口支持重入赁酝、公平等的鎖規(guī)則:實現(xiàn)類ReentrantLack
、ReadLock
和 WriteLock
旭等。ReadWriteLock
接口定義讀取者共享而寫入者獨占的鎖酌呆,實現(xiàn)類:ReentrantReadWriteLock
。
可重入鎖:
不可重入鎖搔耕,即線程請求它已經(jīng)擁有的鎖時會阻塞隙袁。
可重入鎖,即線程可以進入它已經(jīng)擁有的鎖的同步代碼塊。
import java.util.concurrent.locks.ReentrantLock;
public class Run {
public static void main(String[] args) {
ReentrantLock reentrantLock = new ReentrantLock();
for (int i = 0; i < 10; i++) {
//加鎖
reentrantLock.lock();
System.out.println("加鎖次數(shù):" + (i + 1));
}
for (int i = 0; i < 10; i++) {
//解鎖
reentrantLock.unlock();
System.out.println("解鎖次數(shù):" + (i + 1));
}
}
}
讀寫鎖:
讀寫鎖藤乙,即可以同時讀猜揪,讀的時候不能寫;不能同時寫坛梁,寫的時候不能讀而姐。
import com.multithreading.auto.AutoThrow;
import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.locks.ReentrantReadWriteLock;
public class Run {
//讀寫對象
private final Map<String, Object> map = new HashMap<>();
//定義鎖
private final ReentrantReadWriteLock reentrantReadWriteLock = new ReentrantReadWriteLock();
//讀鎖
private final ReentrantReadWriteLock.ReadLock readLock = reentrantReadWriteLock.readLock();
//寫鎖
private final ReentrantReadWriteLock.WriteLock writeLock = reentrantReadWriteLock.writeLock();
//讀操作加鎖
public String get(String key) {
String name = Thread.currentThread().getName();
//加鎖
readLock.lock();
Object exec = AutoThrow.exec(() -> {
System.out.println(name + " 讀操作已加鎖,開始讀操作……");
Thread.sleep(3000);
return map.get(key);
});
//解鎖
readLock.unlock();
System.out.println(name + " 讀操作已解鎖");
return String.valueOf(exec);
}
//寫操作加鎖
public void put(String key, String value) {
String name = Thread.currentThread().getName();
//加鎖
writeLock.lock();
AutoThrow.exec(() -> {
System.out.println(name + " 寫操作已加鎖,開始寫操作……");
Thread.sleep(3000);
map.put(key, value);
});
//解鎖
writeLock.unlock();
System.out.println(name + " 寫操作已解鎖");
}
public static void main(String[] args) {
Run run = new Run();
//執(zhí)行寫操作
run.put("te", "value");
//開啟線程多個讀取操作
new Thread(() -> System.out.println(run.get("te")), "線程1").start();
new Thread(() -> System.out.println(run.get("te")), "線程2").start();
new Thread(() -> System.out.println(run.get("te")), "線程3").start();
}
}
打印結(jié)果:
main 寫操作已加鎖,開始寫操作……
main 寫操作已解鎖
線程2 讀操作已加鎖,開始讀操作……
線程3 讀操作已加鎖,開始讀操作……
線程1 讀操作已加鎖,開始讀操作……
線程1 讀操作已解鎖
value
線程3 讀操作已解鎖
value
線程2 讀操作已解鎖
value
Volatitle
關(guān)鍵字:
一個共享變量(類的成員變量、類的靜態(tài)成員變量〉被volatile
修飾之后划咐,那么就具備了兩層語義:
- 保證了不同線程對這個變量進行操作時的可見性拴念,即一個線程修改了某個變量的值,這新值對其他線程來說是立即可見的褐缠。(注意:不保證原子性)
- 禁止進行指令重排序政鼠。(保證變量所在行的有序性)
當(dāng)程序執(zhí)行到volatile
變量的讀操作或者寫操作時,在其前面的操作的更改肯定全部已經(jīng)進行队魏,且結(jié)果已經(jīng)對后面的操作可見;在其后面的操作肯定還沒有進行公般;在進行指令優(yōu)化時,不能將在對volatile
變量訪問的語句放在其后面執(zhí)行胡桨,也不能把volatile
變量后面的語句放到其前面執(zhí)行官帘。
應(yīng)用場景:
基于Volatitle
的作用,使用Volatitle
必須滿足兩個條件
- 對變量的寫操作不依賴當(dāng)前值
- 該變量沒有包含在具體的其他變量的不變式中昧谊,即單獨使用
狀態(tài)量的標記:
volatile boolean flag = false;
雙重校驗:
static class Singleton {
private volatile static Singleton singleton = null;
private Singleton() {
}
public static Singleton getInstance() {
if (singleton == null) {
synchronized (Singleton.class) {
//防止多線程下實例多次創(chuàng)建的情況 兩次if
if (singleton == null) {
singleton = new Singleton();
}
}
}
return singleton;
}
}
線程池:
多線程的缺點:
- 處理任務(wù)的線程創(chuàng)建和銷毀都非常耗時并消耗資源刽虹。
- 多線程之間的切換也會非常耗時并消耗資源。
解決方法:采用線程池
- 使用時線程已存在呢诬,消除了線程創(chuàng)建的時耗
- 通過設(shè)置線程數(shù)目涌哲,防止資源不足
public ThreadPoolExecutor(int corePoolSize,
int maximumPoolSize,
long keepAliveTime,
TimeUnit unit,
BlockingQueue<Runnable> workQueue,
ThreadFactory threadFactory,
RejectedExecutionHandler handler) {}
參數(shù)介紹:
-
corePoolSize
:線程池中核心線程數(shù)的最大值 -
maximumPoolSize
:線程池中能擁有最多連接數(shù) -
workQueue
:用于緩存任務(wù)的阻塞隊列,對于不同的應(yīng)用場景我們可能采取不同的排隊策略尚镰,這就需要不同類型的阻塞隊列阀圾,在線程池中常用的阻塞隊列有以下2種:-
SynchronousQueue<Runnable>
:此隊列中不緩存任何一個任務(wù)。向線程池提交任務(wù)時狗唉,如果沒有空閑線程來運行任務(wù)初烘,則入列操作會阻塞。當(dāng)有線程來獲取任務(wù)時敞曹,出列操作會喚醒執(zhí)行入列操作的線程账月。從這個特性來看,SynchronousQueue
是一個無界入列澳迫,因此當(dāng)使用SynchronousQueue
作為線程池的阻塞隊列時局齿,參數(shù)maximumPoolSize
沒有任何作用。 -
LinkedBlockingQueue<Runnable>
:顧名思義是用鏈表實現(xiàn)的隊列橄登,可以是有界的抓歼,也可以是無界的讥此,但在Executors
中默認使用無界的。
-
以上三個參數(shù)之間的關(guān)系如下:
- 如果沒有空閑的線程執(zhí)行該任務(wù)且當(dāng)前運行的線程數(shù)少于
corePoolSize
谣妻,則添加新的線程執(zhí)行該任務(wù)萄喳。- 如果沒有空閑的線程執(zhí)行該任務(wù)且當(dāng)前的線程數(shù)等于
corePoolSize
同時阻塞隊列未滿巡扇,則將任務(wù)入隊列衅澈,而不添加新的線程。- 如果沒有空閑的線程執(zhí)行該任務(wù)且阻塞隊列已滿同時池中的線程數(shù)小于
maximumPoolSize
蒂秘,則創(chuàng)建新的線程執(zhí)行任務(wù)减江。- 如果沒有空閑的線程執(zhí)行該任務(wù)且阻塞隊列已滿同時池中的線程數(shù)等于
maximumPoolSize
染突,則根據(jù)構(gòu)造函數(shù)中的 handler指定的策略來拒絕新的任務(wù)。
keepAliveTime
:表示空閑線程的存活時間unit
:表示keepAliveTime
的單位-
handler
:表示當(dāng)workQueue
已滿辈灼,且池中的線程數(shù)達到maximumPoolSize
時份企,線程池拒絕添加新任務(wù)時采取的策略。一般可以采取以下四種取值ThreadPoolExecutor.AbortPolicy
拋出 RejectedExecutionHandler
異常ThreadPoolExecutor.CallerRunsPolicy
由線程池提交任務(wù)的線程來執(zhí)行 ThreadPoolExecutor.DiscardOldestPolicy
拋棄最舊的任務(wù)(最先提交而沒有得到執(zhí)行的任務(wù)) ThreadPoolExecutor.DiscardPolicy
拋棄當(dāng)前任務(wù) threadFactory
:指定創(chuàng)建線程的工廠
四種常用的線程池:
ThreadPoolExecutor
構(gòu)造函數(shù)的參數(shù)很多巡莹,使用起來很麻煩司志,為了方便的創(chuàng)建線程池,JavaSE
中又定義了Executors
類降宅,Eexautors
類提供了四個創(chuàng)建線程池的方法骂远,分別如下:
newCachedThreadPool
newFixedThreadPool
newSingleThreadExecutor
newScheduledThreadPool
newCachedThreadPool
:
該方法可以創(chuàng)建一個可緩存線程池,如果線程池長度超過處理需要钉鸯,可靈活回收空閑線程吧史,若無可回收邮辽,則新建線程唠雕。
此類線程池特點是:
- 工作線程的創(chuàng)建數(shù)基本沒有限制(其實也有限制的數(shù)目為
Interger.MAX_VALUE
) - 空閑的工作線程會自動銷毀,有新任務(wù)會重新創(chuàng)建
- 在使用
CachedThreadPool
時吨述,一定要注意控制任務(wù)的數(shù)量岩睁,否則,由于大量線程同時運行揣云,很有會造成系統(tǒng)癱瘓捕儒。
public static void main(String[] args) {
ExecutorService newCachedThreadPool = Executors.newCachedThreadPool();
for (int i = 0; i < 10; i++) {
final int index = 1;
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
newCachedThreadPool.execute(() -> System.out.println(index));
}
}
}
newFixedThreadPool
:
該方法創(chuàng)建一個指定工作線程數(shù)量的線程池。每當(dāng)提交一個任務(wù)就創(chuàng)建一個工作線程邓夕,如果工作線程數(shù)量達到線程池初始的最大數(shù)刘莹,則將提交的任務(wù)存入到池隊列中。
優(yōu)點:具有線程池提高程序效率和節(jié)省創(chuàng)建線程時所耗的開銷焚刚。
缺點:在線程池空閑時点弯,即線程池中沒有可運行任務(wù)時,它不會釋放工作線程矿咕,還會占用一定的系統(tǒng)資源抢肛。
public static void main(String[] args) {
ExecutorService newFixedThreadPool = Executors.newFixedThreadPool(3);
for (int i = 0; i < 10; i++) {
final int index = 1;
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
newFixedThreadPool.execute(() -> System.out.println(index));
}
}
newSingleThreadExecutor
:
該方法創(chuàng)建一個單線程化的Executor
狼钮,即只創(chuàng)建唯一的工作者線程來執(zhí)行任務(wù),它只會用唯一的工作線程來執(zhí)行任務(wù)捡絮,保證所有任務(wù)按照指定順序(FIFO,LIFO,優(yōu)先級)執(zhí)行熬芜。如果這個線程異常結(jié)束,會有另一個取代它福稳,保證順序執(zhí)行涎拉。
單工作線程最大的特點是可保證順序地執(zhí)行各個任務(wù),并且在任意給定的時間不會有多個線程是活動的的圆。
public static void main(String[] args) {
ExecutorService newSingleThreadExecutor = Executors.newSingleThreadExecutor();
for (int i = 0; i < 10; i++) {
final int index = 1;
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
newSingleThreadExecutor.execute(() -> System.out.println(index));
}
}
newScheduledThreadPool
:
該方法創(chuàng)建一個定長的線程池,而且支持定時的以及周期性的任務(wù)執(zhí)行曼库,支持定時及周期性任務(wù)執(zhí)行。
public static void main(String[] args) {
ScheduledExecutorService scheduledExecutorService = Executors.newScheduledThreadPool(3);
for (int i = 0; i < 10; i++) {
final int index = 1;
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
//第2個參數(shù):周期執(zhí)行時間
//第3個參數(shù):時間單位
scheduledExecutorService.schedule(() -> System.out.println(index), 3, TimeUnit.MILLISECONDS);
}
}
}