多線程在多進(jìn)程的基礎(chǔ)上更好解決了并發(fā)問題,但由于一個(gè)進(jìn)程內(nèi)的多個(gè)線程是資源共享的,就會出現(xiàn)多個(gè)線程在并發(fā)執(zhí)行的時(shí)候造成內(nèi)存中數(shù)據(jù)的混亂。
成都創(chuàng)新互聯(lián)為企業(yè)級客戶提高一站式互聯(lián)網(wǎng)+設(shè)計(jì)服務(wù),主要包括成都網(wǎng)站建設(shè)、成都網(wǎng)站制作、重慶APP軟件開發(fā)、重慶小程序開發(fā)公司、宣傳片制作、LOGO設(shè)計(jì)等,幫助客戶快速提升營銷能力和企業(yè)形象,創(chuàng)新互聯(lián)各部門都有經(jīng)驗(yàn)豐富的經(jīng)驗(yàn),可以確保每一個(gè)作品的質(zhì)量和創(chuàng)作周期,同時(shí)每年都有很多新員工加入,為我們帶來大量新的創(chuàng)意。舉一個(gè)例子:
class Counter {
public int count;
public void add() {
count++;
}
}
public class ThreadDemo1 {
public static void main(String[] args) {
Counter counter = new Counter();
Thread t1 = new Thread(() ->{
for (int i = 0; i< 50000; i++) {
counter.add();
}
});
Thread t2 = new Thread(() ->{
for (int i = 0; i< 50000; i++) {
counter.add();
}
});
t1.start();
t2.start();
try {
t1.join();
t2.join();
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
System.out.println("count = " + counter.count);
}
}
這里定義兩個(gè)線程實(shí)例對象t1,t2(執(zhí)行的任務(wù)分別是循環(huán)調(diào)用50000次add操作),add方法的作用是對成員變量count進(jìn)行++,我們設(shè)想兩個(gè)線程并發(fā)執(zhí)行,結(jié)果輸出的count值應(yīng)該為100000,但是并不是,這就是一個(gè)典型的線程安全問題!?。?/p>2.原因
首先我們需要明確add操作主要干了什么?
可以看出 add操作分為了三個(gè)指令,沒進(jìn)行一次++CPU就會執(zhí)行三條指令(其中這三條指令是串行的)
load :把內(nèi)存中的值加載到CPU的寄存器上
add? :在CPU的寄存器上進(jìn)行++操作
save: 把計(jì)算之后的值再存到內(nèi)存當(dāng)中
那為什么會在進(jìn)行add操作的時(shí)候會出現(xiàn)輸出結(jié)果小于100000呢?
我們知道兩個(gè)線程在并發(fā)編程的時(shí)候是搶占式執(zhí)行的(誰先搶到CPU資源誰就會被優(yōu)先調(diào)度,此時(shí)另一個(gè)線程就會阻塞),此時(shí)兩個(gè)線程中的add操作鎖所對應(yīng)的指令就會出現(xiàn)很多種情況,就會造成計(jì)算結(jié)果出錯(cuò)。
這里舉出了兩個(gè)例子,兩個(gè)線程的所對應(yīng)的三條指令順序都不一樣(是因?yàn)榫€程1,2搶占式執(zhí)行當(dāng)某一個(gè)線程執(zhí)行其中一條指令時(shí),另一個(gè)線程被調(diào)度時(shí)會優(yōu)先執(zhí)行另一個(gè)線程的指令,此時(shí)之前的線程就會被阻塞),第一種情況add了兩次正常輸出2,而第二種情況只被add了一次輸出1;
二,線程安全 1.概念什么是線程安全:線程安全確切的定義十分復(fù)雜,所以我們一般認(rèn)為,如果多線程環(huán)境下代碼運(yùn)行的結(jié)果是符合我們預(yù)期的,即在單線程環(huán)境應(yīng)該的結(jié)果,則說這個(gè)程序是線程安全的;
上述例子若改成單線程:
class Counter2 {
public int count;
public void add() {
count++;
}
}
public class Test {
public static void main(String[] args) {
Counter2 counter2 = new Counter2();
for (int i = 0; i< 50000; i++) {
counter2.add();
}
for (int i = 0; i< 50000; i++) {
counter2.add();
}
System.out.println("count = " + counter2.count);
}
}
此時(shí)輸出正確,所以我們可以認(rèn)為上述輸出結(jié)果不是預(yù)期的那種多線程代碼是線程不安全的。
2.原因1.搶占式執(zhí)行,隨機(jī)調(diào)度(根本原因,由操作系統(tǒng)內(nèi)核決定,無法改變)
2.因?yàn)閱蝹€(gè)進(jìn)程下的多個(gè)線程是資源共享的,所以多個(gè)線程修改同一個(gè)變量時(shí)會線程不安全
?多個(gè)線程修改不同的變量? 沒事
?一個(gè)線程修改同一個(gè)變量? 沒事
?多個(gè)線程讀取同一個(gè)變量? 沒事
3.原子性
?如果修改操作是原子性的,就不會有線程安全問題
?如果修改操作是非原子性的就很大概率出現(xiàn)線程安全問題(上述示例的add操作就是非原子性? ? ? ? ?的,一個(gè)add操作分成了三個(gè)指令去執(zhí)行)
?所以我們?nèi)ケ苊饩€程安全的主要手段就是將非原子性的操作變成原子性---->加鎖
4.內(nèi)存可見性問題
? 當(dāng)一個(gè)線程在讀取數(shù)據(jù),一個(gè)線程在修改數(shù)據(jù)時(shí)就會出現(xiàn)線程安全問題(也就是常說的臟讀問? ? ? ? 題)
5.指令重排序(本質(zhì)上是代碼出現(xiàn)了bug)
三,synchronized關(guān)鍵字針對線程安全問題,我們往往常用的手段就是把非原子性操作變成原子性操作,此時(shí)就需要用到synchronized關(guān)鍵字(該關(guān)鍵字的作用就是對對象加鎖)
針對上述的代碼進(jìn)行修改:
class Counter {
public int count;
synchronized public void add() {
count++;
}
}
public class ThreadDemo1 {
public static void main(String[] args) {
Counter counter = new Counter();
Thread t1 = new Thread(() ->{
for (int i = 0; i< 50000; i++) {
counter.add();
}
});
Thread t2 = new Thread(() ->{
for (int i = 0; i< 50000; i++) {
counter.add();
}
});
t1.start();
t2.start();
try {
t1.join();
t2.join();
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
System.out.println("count = " + counter.count);
}
}
此時(shí)輸出結(jié)果為我們預(yù)期的100000;
1.synchronized 的原理在多線程環(huán)境下,當(dāng)一個(gè)線程在被調(diào)度時(shí)(拿上述例子解釋:當(dāng)線程t1調(diào)用add方法進(jìn)行conut++操作時(shí),因?yàn)榫€程是搶占式執(zhí)行的,此時(shí)線程t2想要調(diào)用add方法時(shí)發(fā)現(xiàn)線程t1正在執(zhí)行add方法,線程t2就會發(fā)生線程阻塞,等待線程t1完全執(zhí)行完時(shí)線程t2才可以進(jìn)行add操作),另一個(gè)線程就會阻塞等待,直到當(dāng)前線程執(zhí)行完畢,另一個(gè)線程才可以執(zhí)行。
當(dāng)有一個(gè)滑稽老鐵在上廁所時(shí)(廁所相當(dāng)于對象,滑稽老鐵相當(dāng)于線程),此時(shí)門就會上鎖(這個(gè)鎖的作用就相當(dāng)于synchronized的作用),其余的滑稽老鐵必須等待當(dāng)前的滑稽老鐵上完廁所,他們才可以使用這個(gè)廁所(相當(dāng)于此時(shí)其他線程是阻塞等待的)。
2.synchronized 的特性1.互斥
指的是當(dāng)一個(gè)線程執(zhí)行到synchronized對象中時(shí),另一個(gè)對象執(zhí)行到這個(gè)對象時(shí)就會阻塞等待;
進(jìn)入 synchronized 修飾的代碼塊, 相當(dāng)于 加鎖
退出 synchronized 修飾的代碼塊 , 相當(dāng)于 解鎖 2.可重入 synchronized 同步塊對同一條線程來說是可重入的,不會出現(xiàn)自己把自己鎖死的問題;當(dāng)同一個(gè)線程對一個(gè)對象多次加鎖時(shí),并不會出現(xiàn)問題時(shí)就稱該鎖為可重入,否則就稱該鎖不可重入(此時(shí)會造成死鎖的問題)
3.synchronized的使用案例synchronized可以修飾方法(包括實(shí)例方法和靜態(tài)方法)也可以修飾代碼塊
修飾實(shí)例方法:鎖的是synchronized對象;
修飾靜態(tài)方法:鎖的是Counter類對象;
修飾代碼塊:鎖的是當(dāng)前對象;
其中的this可以改成任意對象;
4.synchronized總結(jié)?如果兩個(gè)線程針對同一個(gè)對象進(jìn)行加鎖,就會出現(xiàn)鎖競爭/鎖沖突,一個(gè)線程能夠獲取到鎖(先到先得)另一個(gè)線程阻塞等待,等待到上一個(gè)線程解鎖,它才能獲取鎖成功,否則就不會;
如果兩個(gè)線程針對不同對象加鎖,此時(shí)不會發(fā)生鎖競爭/鎖沖突,這倆線程都能獲取到各自的鎖,不會有阻塞等待了;
兩個(gè)線程,一個(gè)線程加鎖,一個(gè)線程不加鎖這個(gè)時(shí)候就不會有鎖競爭。
四,Java 標(biāo)準(zhǔn)庫中的線程安全類? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ??線程不安全的集合類 | 線程安全的集合類 |
ArrayList | Vector ( 不推薦使用 ) |
LinkedList | HashTable ( 不推薦使用 ) |
HashMap | ConcurrentHashMap |
TreeMap | StringBuffer |
HashSet | |
TreeSet | |
StringBuilder |
雖然有的集合類是加鎖了,但是在使用時(shí)并不是建議無腦使用加鎖的集合類,因?yàn)榧渔i也需要很多的時(shí)間開銷(根據(jù)情況進(jìn)行選擇)
還有的雖然沒有加鎖,但是不涉及 "修改",?仍然是線程安全的:String
你是否還在尋找穩(wěn)定的海外服務(wù)器提供商?創(chuàng)新互聯(lián)www.cdcxhl.cn海外機(jī)房具備T級流量清洗系統(tǒng)配攻擊溯源,準(zhǔn)確流量調(diào)度確保服務(wù)器高可用性,企業(yè)級服務(wù)器適合批量采購,新人活動首月15元起,快前往官網(wǎng)查看詳情吧