淺談Java并發(fā)編程基礎(chǔ)知識
進(jìn)程和線程
在并行程序中進(jìn)程和線程是兩個(gè)基本的運(yùn)行單元,在Java并發(fā)編程中,并發(fā)主要核心在于線程
1. 進(jìn)程
一個(gè)進(jìn)程有其專屬的運(yùn)行環(huán)境,一個(gè)進(jìn)程通常有一套完整、私有的運(yùn)行時(shí)資源;尤其是每個(gè)進(jìn)程都有其專屬的內(nèi)存空間。
通常情況下,進(jìn)程等同于運(yùn)行的程序或者應(yīng)用,然而很多情況下用戶看到的一個(gè)應(yīng)用實(shí)際上可能是多個(gè)進(jìn)程協(xié)作的。為了達(dá)到進(jìn)程通信的目的,主要的操作系統(tǒng)都實(shí)現(xiàn)了Inter Process Communication(IPC)資源,例如pipe和sockets,IPC不僅能支持同一個(gè)系統(tǒng)中的進(jìn)程通信,還能支持跨系統(tǒng)進(jìn)程通信。
2. 線程
線程通常也被叫做輕量級進(jìn)程,進(jìn)程線程都提供執(zhí)行環(huán)境,但是創(chuàng)建一個(gè)線程需要的資源更少,線程在進(jìn)程中,每個(gè)進(jìn)程至少有一條線程,線程共享進(jìn)程的資源,包括內(nèi)存空間和文件資源,這種機(jī)制會使得處理更高效但是也存在很多問題。
多線程運(yùn)行是Java的一個(gè)主要特性,每個(gè)應(yīng)用至少包含一個(gè)線程或者更多。從應(yīng)用程序角度來講,我們從一條叫做主線程的線程開始,主線程可以創(chuàng)建別的其他的線程。
線程生命周期
一個(gè)線程的生命周期包含了一下幾種狀態(tài)
1、新建狀態(tài)
該狀態(tài)線程已經(jīng)被創(chuàng)建,但未進(jìn)入運(yùn)行狀態(tài),我們可以通過start()方法來調(diào)用線程使其進(jìn)入可執(zhí)行狀態(tài)。
2、可執(zhí)行狀態(tài)/就緒狀態(tài)
在該狀態(tài)下,線程在排隊(duì)等待任務(wù)調(diào)度器對其進(jìn)行調(diào)度執(zhí)行。
3、運(yùn)行狀態(tài)
在該狀態(tài)下,線程獲得了CPU的使用權(quán)并在CPU中運(yùn)行,在這種狀態(tài)下我們可以通過yield()方法來使得該線程讓出時(shí)間片給自己或者其他線程執(zhí)行,若讓出了時(shí)間片,則進(jìn)入就緒隊(duì)列等待調(diào)度。
4、阻塞狀態(tài)
在阻塞狀態(tài)下,線程不可運(yùn)行,并且被異除出等待隊(duì)列,沒有機(jī)會進(jìn)行CPU執(zhí)行,在以下情況出現(xiàn)時(shí)線程會進(jìn)入阻塞狀態(tài)
- 調(diào)用suspend()方法
- 調(diào)用sleep()方法
- 調(diào)用wait()方法
- 等待IO操作
線程可以從阻塞狀態(tài)重回就緒狀態(tài)等待調(diào)度,如IO操作完畢后。
5、終止?fàn)顟B(tài)
當(dāng)線程執(zhí)行完畢或被終止執(zhí)行后便會進(jìn)入終止?fàn)顟B(tài),進(jìn)入終止?fàn)顟B(tài)后線程將無法再被調(diào)度執(zhí)行,徹底喪失被調(diào)度的機(jī)會。
線程對象
每一條線程都有一個(gè)關(guān)聯(lián)的Thread對象,在并發(fā)編程中Java提供了兩個(gè)基本策略來使用線程對象
- 直接控制線程的創(chuàng)建和管理,在需要?jiǎng)?chuàng)建異步任務(wù)時(shí)直接通過實(shí)例化Thread來創(chuàng)建和使用線程。
- 或者將抽象好的任務(wù)傳遞給一個(gè)任務(wù)執(zhí)行器 executor
1. 定義和開始一條線程
在創(chuàng)建一個(gè)線程實(shí)例時(shí)需要提供在線程中執(zhí)行的代碼,有兩種方式可以實(shí)現(xiàn)。
提供一個(gè)Runnable對象,Runnable接口定義了一個(gè)run方法,我們將要在線程中執(zhí)行的方法放到run方法內(nèi)部,再將Runnable對象傳遞給一個(gè)Thread構(gòu)造器,代碼如下。
public class ThreadObject {
public static void main(String args[]) {
new Thread(new HelloRunnable()).start();
}
}
// 實(shí)現(xiàn)Runnable接口
class HelloRunnable implements Runnable {
@Override
public void run() {
System.out.println("Say hello to world!!!");
}
}
繼承Thread,Thread類自身實(shí)現(xiàn)了Runnable接口,但是其run方法什么都沒做,由我們自己根據(jù)需求去擴(kuò)展。
public class ThreadObject {
public static void main(String args[]) {
new HelloThread().start();
}
}
// 繼承Thread,擴(kuò)展run方法
class HelloThread extends Thread {
public void run() {
System.out.println("Say hello to world!!!");
}
}
兩種實(shí)現(xiàn)方式的選取根據(jù)業(yè)務(wù)場景和Java中單繼承,多實(shí)現(xiàn)的特性來綜合考量。
2. 利用Sleep暫停線程執(zhí)行
sleep()方法會使線程進(jìn)入阻塞隊(duì)列,進(jìn)入阻塞隊(duì)列后,線程會將CPU時(shí)間片讓給其他線程執(zhí)行,sleep()有兩個(gè)重載方法sleep(long millis)和sleep(long millis, int nanos)當(dāng)?shù)搅酥付ǖ男菝邥r(shí)間后,線程將會重新進(jìn)入就緒隊(duì)列等待調(diào)度管理器進(jìn)行調(diào)度
public static void main(String args[]) throws InterruptedException {
for (int i = 0; i < 4; i++) {
System.out.println("print number "+ i);
// 將主線程暫停4秒后執(zhí)行,4秒后重新獲得調(diào)度執(zhí)行的機(jī)會
Thread.sleep(4*1000);
}
}
3. 中斷
當(dāng)一個(gè)線程被中斷后就代表這個(gè)線程再無法繼續(xù)執(zhí)行,將放棄所有在執(zhí)行的任務(wù),程序可以自己決定如何處理中斷請求,但通常都是終止執(zhí)行。
在Java中與中斷相關(guān)的有Thread.interrupt()、Thread.isInterrupted()、Thread.interrupted()三個(gè)方法
Thread.interrupt()為設(shè)置中斷的方法,該方法會將線程狀態(tài)設(shè)置為確認(rèn)中斷狀態(tài),但程序并不會立馬中斷執(zhí)行只是設(shè)置了狀態(tài),而Thread.isInterrupted()、Thread.interrupted()這兩個(gè)方法可以用于捕獲中斷狀態(tài),區(qū)別在于Thread.interrupted()會重置中斷狀態(tài)。
4. Join
join方法允許一條線程等待另一條線程執(zhí)行完畢,例如t是一條線程,若調(diào)用t.join()方法,則當(dāng)前線程會等待t線程執(zhí)行完畢后再執(zhí)行。
線程同步 Synchronization
各線通信方式
- 共享對象的訪問權(quán)限 如. A和B線程都有訪問和操作某一個(gè)對象的權(quán)限
- 共享 對象的引用對象的訪問權(quán)限 如. A和B線程都能訪問C對象,C對象引用了D對象,則A和B能通過C訪問D對象
這種通信方式使得線程通訊變得高效,但是也帶來一些列的問題例如線程干擾和內(nèi)存一致性錯(cuò)誤。那些用于防止出現(xiàn)這些類型的錯(cuò)誤出現(xiàn)的工具或者策略就叫做同步。
1. 線程干擾 Thread Interference
線程干擾是指多條線同時(shí)操作某一個(gè)引用對象時(shí)造成計(jì)算結(jié)果與預(yù)期不符,彼此之間相互干擾。如例
public class ThreadInterference{
public static void main(String args[]) throws InterruptedException {
Counter ctr = new Counter();
// 累加線程
Thread incrementThread = new Thread(()->{
for(int i = 0; i<10000;i++) {
ctr.increment();
}
});
// 累減線程
Thread decrementThread = new Thread(()->{
for(int i = 0; i<10000;i++) {
ctr.decrement();
}
});
incrementThread.start();
decrementThread.start();
incrementThread.join();
decrementThread.join();
System.out.println(String.format("最終執(zhí)行結(jié)果:%d", ctr.get()));
}
}
class Counter{
private int count = 0;
// 自增
public void increment() {
++this.count;
}
// 自減
public void decrement() {
--this.count;
}
public int get() {
return this.count;
}
}
理論上來講,如果按照正常的思路理解,一個(gè)累加10000次一個(gè)累減10000次最終結(jié)果應(yīng)該是0 ,但實(shí)際結(jié)果卻是每次運(yùn)行結(jié)果都不一致,產(chǎn)生這個(gè)結(jié)果的原因便是線程之間相互干擾。
我們可以把自增和自減操作拆解為以下幾個(gè)步驟
- 獲取count變量當(dāng)前值
- 自增/自減 獲取到的值
- 將結(jié)果保存回count變量
當(dāng)多個(gè)線程同時(shí)對count進(jìn)行操作時(shí),便可能產(chǎn)生如下這一種狀態(tài)
- 線程A : 獲取count
- 線程B : 獲取count
- 線程A: 自增,結(jié)果 為 1
- 線程B: 自減,結(jié)果為 -1
- 線程A: 將結(jié)果1 保存到count; 當(dāng)前count = 1
- 線程B: 將結(jié)果-1 保存到count; 當(dāng)前count = -1
當(dāng)線程以上面所示的順序執(zhí)行時(shí),線程B就會覆蓋掉線程A的結(jié)果,當(dāng)然這只是其中一種情況。
2. 內(nèi)存一致性錯(cuò)誤 Memory Consistency Errors
當(dāng)不同的線程對應(yīng)相同數(shù)據(jù)具有不一致的視圖時(shí),會發(fā)生內(nèi)存一致性錯(cuò)誤,詳細(xì)信息參見 JVM內(nèi)存模型
3. 同步方法
Java提供了兩種同步的慣用方法:同步方法 synchronized methods 、同步語句 synchronized statements 。要使方法變成同步方法只需要在方法聲明時(shí)加入synchronized關(guān)鍵字,如
class Counter{
private int count = 0;
// 自增
public synchronized void increment() {
++this.count;
}
// 自減
public synchronized void decrement() {
--this.count;
}
public synchronized int get() {
return this.count;
}
}
聲明為同步方法之后將會使得對象產(chǎn)生如下所述的影響
- 首先,不可以在同一對象上多次調(diào)用同步方法來交錯(cuò)執(zhí)行,同步聲明使得同一個(gè)時(shí)間只能有一條線程調(diào)用該對象的同步方法,當(dāng)一條線程已經(jīng)在調(diào)用同步方法時(shí),其他線程會被阻塞block,無法調(diào)用該對象的所有同步方法。
- 其次,當(dāng)同步方法調(diào)用結(jié)束時(shí),會自動(dòng)與同一對象的任何后續(xù)調(diào)用方法建立一個(gè)happens-before關(guān)聯(lián),這保證對對象狀態(tài)的更改對所有線程可見。
4. 內(nèi)部鎖和同步
同步是圍繞對象內(nèi)部實(shí)體構(gòu)建的,API規(guī)范通常將此類實(shí)體稱之為監(jiān)視器,內(nèi)部鎖有兩個(gè)至關(guān)重要的作用
- 強(qiáng)制對對象狀態(tài)的獨(dú)占訪問
- 建立至關(guān)重要的happens-before關(guān)系
每個(gè)對象都有與其關(guān)聯(lián)的固有鎖,通常,需要對對象的字段進(jìn)行獨(dú)占且一致的訪問前需要獲取對象的內(nèi)部鎖,然后再使用完成時(shí)釋放內(nèi)部鎖,線程在獲取后釋放前擁有該對象的內(nèi)部鎖。只要線程擁有了內(nèi)部鎖其他任何線程都無法獲取相同的鎖,其他線程在嘗試獲取鎖時(shí)將被阻塞。在線程釋放內(nèi)部鎖時(shí),該操作將會在該對象的任何后續(xù)操作間建立happens-before關(guān)系。
4.1 同步方法中的鎖
當(dāng)線程調(diào)用同步方法時(shí),線程會自動(dòng)獲得該方法所屬對象得內(nèi)部鎖,并且在方法返回時(shí)自動(dòng)釋放,即使返回是由未捕獲異常導(dǎo)致。靜態(tài)同步方法的鎖不同于實(shí)例方法的鎖,靜態(tài)方法是圍繞該類進(jìn)行控制而非該類的某一個(gè)實(shí)例。
4.2 同步語句
另外一個(gè)提供同步的方法是同步代語句,與同步方法不同的是,同步語句必須指定一個(gè)對象來提供內(nèi)部鎖。
public class IntrinsicLock {
private List<String> nameList = new LinkedList<String>();
private String lastName;
private int nameCount;
public void addName(String name) {
// 當(dāng)多條線程對同一個(gè)實(shí)例對象的addName()方法操作時(shí)將會是同步的,提供鎖的對象為該實(shí)例對象本身
synchronized(this) {
lastName = name;
nameCount++;
}
nameList.add(name);
}
}
同步語句對細(xì)粒度同步提高并發(fā)性也很有用,比如我們需要對同一個(gè)對象的不同屬性進(jìn)行同步修改我們可以通過如下代碼來提高細(xì)粒度同步控制下的并發(fā)。
public class IntrinsicLock {
// 1. 該屬性需要基于同步的修改
private String lastName;
// 1. 該屬性也需要基于同步的修改
private int count;
// 該對象用于對lastName提供內(nèi)部鎖
private Object nameLock = new Object();
// 該對象用于對nameCount提供內(nèi)部鎖
private Object countLock = new Object();
public void addName(String name) {
synchronized(nameLock) {
lastName = name;
}
}
public void increment() {
synchronized(countLock) {
count++;
}
}
}
這樣,對lastName的操作不會阻塞count屬性的自增操作,因?yàn)樗麄兎謩e使用了不同的對象來提供鎖。若像上一個(gè)例子中使用this來提供鎖的話,則在調(diào)用addName()方法時(shí)increment()也被阻塞,反之亦然,這樣將會增加不必要的阻塞。
4.3 可重入同步
線程無法獲取另外一個(gè)線程已經(jīng)擁有的鎖,但是線程可以多次獲取它已經(jīng)擁有的鎖,允許線程多次獲取同一鎖可以實(shí)現(xiàn)可重入的同步,即同步方法或者同步代碼塊中又調(diào)用了由同一個(gè)對象提供鎖的其他同步方法時(shí),該鎖可以多次被獲取
public class IntrinsicLock {
private int count;
public void decrement(String name) {
synchronized(this) {
count--;
// 調(diào)用其他由同一個(gè)對象提供鎖的同步方法時(shí),鎖可以重復(fù)獲取
// 但只能由當(dāng)前有用鎖的線程重復(fù)獲取
increment();
}
}
public void increment() {
synchronized(this) {
count++;
}
}
}
4.4 原子訪問
在編程中,原子操作指的是指所有操作一行性完成,原子操作不可能執(zhí)行一半,要么全都執(zhí)行,要么都不執(zhí)行。在原子操作完成之前,其修改都是不可見的。在Java中以下操作是原子性的。
- 讀寫大部分原始變量(除了long和double)
- 讀寫所有使用volatile聲明的變量
原子操作的特性使得我們不必?fù)?dān)心線程干擾帶來的同步問題,但是原子操作依然會發(fā)生內(nèi)存一致性錯(cuò)誤。需要使用volatile聲明變量以有效防止內(nèi)存一致性錯(cuò)誤,因?yàn)閷憊olatile標(biāo)記的變量時(shí)會與讀取該變量的后續(xù)操作建立happens-before關(guān)系,所以改變使用volatile標(biāo)記變量時(shí)對其他線程總是可見的。也就是它不僅可以觀測最新的改變,也能觀測到尚未使其改變的操作。
5. 死鎖
死鎖是描述一種兩條或多條線程相互等待(阻塞)的場景,如下例子所示
public class DeadLock {
static class Friend {
String name;
public Friend(String name) {
super();
this.name = name;
}
public String getName() {
return name;
}
public synchronized void call(Friend friend) {
System.out.println(String.format("%s被%s呼叫...", name,friend.getName()));
friend.callBack(this);
}
public synchronized void callBack(Friend friend) {
System.out.println(String.format("%s呼叫%s...", friend.getName(),name));
}
}
public static void main(String args[]) {
final Friend zhangSan = new Friend("張三");
final Friend liSi = new Friend("李四");
new Thread(new Runnable() {
public void run() { zhangSan.call(liSi); }
}).start();
new Thread(new Runnable() {
public void run() { liSi.call(zhangSan); }
}).start();
}
}
如果張三呼叫李四的同時(shí),李四呼叫張三,那么他們會永遠(yuǎn)等待對方,線程永遠(yuǎn)阻塞。
6. 饑餓和活鎖
相對死鎖而言,饑餓和活鎖問題要少得多,但是也應(yīng)注意。
6.1 饑餓
饑餓是一種描述線程無法定期訪問共享資源,程序無法取得正常執(zhí)行的一種場景,比如一個(gè)同步方法執(zhí)行時(shí)間很長,但是多條線程爭搶且頻繁的執(zhí)行,那么將會有大量線程無法在正常的情況下獲得使用權(quán),造成大量阻塞和積壓,我們使用饑餓來描述這種并發(fā)場景。
6.2 活鎖
活鎖是一種描述線程在執(zhí)行同步方法的過程中依賴其他外部資源,而該部分獲取緩慢而無保障造成無法進(jìn)一步執(zhí)行的的場景,相對于死鎖,活鎖是有機(jī)會進(jìn)一步執(zhí)行的,只是執(zhí)行過程緩慢,造成部分資源被 正在等待其他資源的線程占用。
7. 保護(hù)塊/守護(hù)塊
通常,線程會根據(jù)其需要來協(xié)調(diào)其操作。最常用的協(xié)調(diào)方式便是通過守護(hù)塊的方式,用一個(gè)代碼塊來輪詢一個(gè)一條件,只有到該條件滿足時(shí),程序才繼續(xù)執(zhí)行。要實(shí)現(xiàn)這個(gè)功能通常有幾個(gè)要遵循的步驟,先給出一個(gè)并不是那么好的例子請勿在生產(chǎn)代碼使用以下示例
public void guardedJoy() {
// 這是一個(gè)簡單的輪詢守護(hù)塊,但是極其消耗資源
// 請勿在生產(chǎn)環(huán)境中使用此類代碼,這是一個(gè)不好的示例
while(!joy) {}
System.out.println("Joy has been achieved!");
}
這個(gè)例子中,只有當(dāng)別的線程講joy變量設(shè)置為true時(shí),程序才會繼續(xù)往下執(zhí)行,在理論上該方法確實(shí)能實(shí)現(xiàn)守護(hù)的功能,利用簡單的輪詢,一直等待條件滿足后,才繼續(xù)往下執(zhí)行,這是這種輪詢方式是極其消耗資源的,因?yàn)檩喸儠恢闭加肅PU資源。別的線程便無法獲得CPU進(jìn)行處理。
一個(gè)更為有效的守護(hù)方式是調(diào)用Object.wait方法來暫停線程執(zhí)行,暫停后線程會被阻塞,讓出CPU時(shí)間片給其他線程使用,直到其他線程發(fā)出一個(gè)某些條件已經(jīng)滿足的通知事件后,該線程會被喚醒重新執(zhí)行,即使其他線程完成的條件并非它等的哪一個(gè)條件。更改上面的代碼
public synchronized void guardedJoy() {
// 正確的例子,該守護(hù)快每次被其他線程喚醒之后只會輪詢一次,
while(!joy) {
try{
wait();
}catch(Exception e) {}
}
System.out.println("Joy has been achieved!");
}
為什么這個(gè)版本的守護(hù)塊需要同步的?假設(shè)d是一個(gè)我們調(diào)用wait方法的對象,當(dāng)線程調(diào)用d.wait()方法時(shí)線程必須擁有對象d的內(nèi)部鎖,否則將會拋出異常。在一個(gè)同步方法內(nèi)部調(diào)用wait()方法是一個(gè)簡單的獲取對象內(nèi)部鎖的方式。當(dāng)wait()方法被調(diào)用后,當(dāng)前線程會釋放內(nèi)部鎖并暫停執(zhí)行,在將來的某一刻,其他線程將會獲得d的內(nèi)部鎖,并調(diào)用d.notifyAll()方法,來喚醒由對象d.wait()方法暫停執(zhí)行的線程。
public synchronized notifyJoy() {
joy = true;
// 喚醒所有被wait()方法暫停的線程
notifyAll();
}
以上就是本文的全部內(nèi)容,希望對大家的學(xué)習(xí)有所幫助,也希望大家多多支持腳本之家。
相關(guān)文章
datax-web在windows環(huán)境idea中模塊化打包部署操作步驟
這篇文章主要介紹了datax-web在windows環(huán)境idea中模塊化打包部署操作步驟,本文給大家介紹的非常詳細(xì),對大家的學(xué)習(xí)或工作具有一定的參考借鑒價(jià)值,需要的朋友可以參考下2021-05-05
springboot的EnvironmentPostProcessor接口方法源碼解析
這篇文章主要介紹了springboot的EnvironmentPostProcessor接口方法源碼解析,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪2023-08-08
關(guān)于SpringMVC中數(shù)據(jù)綁定@ModelAttribute注解的使用
這篇文章主要介紹了關(guān)于SpringMVC中數(shù)據(jù)綁定@ModelAttribute注解的使用,SpringMVC是一個(gè)基于Spring框架的Web框架,它提供了一種簡單、靈活的方式來開發(fā)Web應(yīng)用程序,在開發(fā)Web應(yīng)用程序時(shí),我們需要將用戶提交的數(shù)據(jù)綁定到我們的Java對象上,需要的朋友可以參考下2023-07-07
SpringBoot優(yōu)化啟動(dòng)速度的方法實(shí)現(xiàn)
本篇文章主要介紹了SpringBoot優(yōu)化啟動(dòng)速度的方法實(shí)現(xiàn),小編覺得挺不錯(cuò)的,現(xiàn)在分享給大家,也給大家做個(gè)參考。一起跟隨小編過來看看吧2018-01-01
spring boot 添加admin監(jiān)控的方法
這篇文章主要介紹了spring boot 添加admin監(jiān)控的相關(guān)知識,非常不錯(cuò),具有參考借鑒價(jià)值,需要的朋友可以參考下2018-02-02
SpringBoot項(xiàng)目中JDK動(dòng)態(tài)代理和CGLIB動(dòng)態(tài)代理的使用詳解
JDK動(dòng)態(tài)代理和CGLIB動(dòng)態(tài)代理都是SpringBoot中實(shí)現(xiàn)AOP的重要技術(shù),JDK動(dòng)態(tài)代理通過反射生成代理類,適用于目標(biāo)類實(shí)現(xiàn)了接口的場景,性能較好,易用性高,但必須實(shí)現(xiàn)接口且不能代理final方法,CGLIB動(dòng)態(tài)代理通過生成子類實(shí)現(xiàn)代理2025-03-03
超細(xì)講解Java調(diào)用python文件的幾種方式
有時(shí)候我們在寫java的時(shí)候需要調(diào)用python文件,下面這篇文章主要給大家介紹了關(guān)于Java調(diào)用python文件的幾種方式,文中通過示例代碼介紹的非常詳細(xì),需要的朋友可以參考下2022-12-12

