作者:~小明學(xué)編程?
文章專欄:JavaEE
格言:熱愛編程的,終將被編程所厚愛。
目錄
多線程所帶來的不安全問題
什么是線程安全
線程不安全的原因
修改共享數(shù)據(jù)
修改操作不是原子的
內(nèi)存可見性對(duì)線程的影響
指令重排序
解決線程不安全的問題
synchronized關(guān)鍵字
互斥
刷新內(nèi)存
可重入
synchronized 的幾種用法
直接修飾普通方法:
修飾靜態(tài)方法
修飾代碼塊
鎖類對(duì)象
volatile
Java 標(biāo)準(zhǔn)庫中的線程安全類
死鎖
什么是死鎖
死鎖的情況
死鎖的必要條件
wait 和 notify
wait()
notify()
notifyAll()方法
wait()和sleep()的對(duì)比
多線程所帶來的不安全問題
我們來看一下下面的這一段代碼,代碼的內(nèi)容主要就是,一個(gè)變量count,我們用兩個(gè)線程同時(shí)對(duì)其進(jìn)行操作,每個(gè)線程都讓其自增50000,但是我們最終看到的結(jié)果確是count不到100000,在50000和100000之間。
class MyClass{
public static int count;
public void increase() {
count++;
}
}
public class Demo2 {
private static int count1;
public static void main(String[] args) throws InterruptedException {
MyClass myClass = new MyClass();
Thread thread = new Thread(new Runnable() {
@Override
public void run() {
for (int i = 0; i < 50000; i++) {
count1++;
myClass.increase();
}
}
});
Thread thread1 = new Thread(()->{
for (int i = 0; i < 50000; i++) {
count1++;
myClass.increase();
}
});
thread.start();
thread1.start();
thread.join();
thread1.join();
System.out.println(count1);//65584
System.out.println(MyClass.count);//65478
}
}
這是什么原因呢?
什么是線程安全
所謂的線程安全就是:我們?cè)诙嗑€程代碼之下的運(yùn)行結(jié)果是符合我們預(yù)期的并且和單線程下的運(yùn)行結(jié)果一致,我們就說這是線程安全的。
上面的代碼肯定不符合我們的預(yù)期也不是線程安全的。
線程不安全的原因
總體回答這個(gè)問題的話就是:
1.線程是搶占式執(zhí)行的,線程之間的調(diào)度充滿著隨機(jī)性。
2.多個(gè)線程對(duì)同一個(gè)變量進(jìn)行修改操作。
3.針對(duì)變量的操作不是原子性的。
4.內(nèi)存的可見性也會(huì)影響線程的安全。
5.代碼的順序性。
修改共享數(shù)據(jù)
我們上面的代碼就是屬于修改共享的數(shù)據(jù),其中我們的count是在堆上因此可以被多個(gè)線程共享。
修改操作不是原子的
所謂的原子性就是不可再分割的意思,例如我們上面的++操作其實(shí)是由三部分組成的,首先是要把數(shù)據(jù)從內(nèi)存讀到cpu上,然后++,最后再寫回去,如果在這中間我們一個(gè)線程讀到數(shù)據(jù)了,然后另外的一個(gè)線程也讀到數(shù)據(jù)了,這時(shí)候兩個(gè)線程++完畢返回的是同樣的值,這也是我們上面產(chǎn)生問題的原因。
內(nèi)存可見性對(duì)線程的影響
因?yàn)槲覀兪嵌嗑€程的操作所以共享同一塊資源,當(dāng)我們?cè)趯?duì)同一塊資源下執(zhí)行時(shí)候就能看到彼此。
我們的線程想要獲取到內(nèi)存里面的東西的話,都是先從內(nèi)存中去拿然后放到寄存器里面去,然后再我們線程再去從寄存器里面去拿,當(dāng)我們想要修改數(shù)據(jù)的時(shí)候就先放到寄存器再去放回內(nèi)存中,這就導(dǎo)致了一個(gè)問題,如果我們改完了一個(gè)數(shù)據(jù)放到了寄存器還沒放回內(nèi)存的時(shí)候,這個(gè)時(shí)候我們另外線程從內(nèi)存中拿數(shù)據(jù)就拿不到最新的數(shù)據(jù)了。
這就是內(nèi)存可見性對(duì)線程的影響。
指令重排序
?指令的重排序是我們編譯器對(duì)我們代碼的執(zhí)行順序進(jìn)行的調(diào)整,同樣的目的但是順序不一樣我們所消耗的資源可能也不一樣,我們編譯器一般會(huì)保證我們執(zhí)行的高效會(huì)對(duì)代碼的順序進(jìn)行調(diào)整,但是當(dāng)多線程的時(shí)候就不安全了,指令的重排序可能會(huì)使我們的線程發(fā)生混亂。
解決線程不安全的問題
synchronized關(guān)鍵字
互斥
synchronized 會(huì)起到互斥效果, 某個(gè)線程執(zhí)行到某個(gè)對(duì)象的 synchronized 中時(shí), 其他線程如果也執(zhí)行到同一個(gè)對(duì)象 synchronized 就會(huì)阻塞等待,這就解決了我們剛才不同的線程操作同一個(gè)變量的問題了,當(dāng)我們一個(gè)線程去操作那個(gè)count的時(shí)候其它的線程加了鎖此時(shí)別的線程就不能再去操作那個(gè)count了。
刷新內(nèi)存
我們的刷新內(nèi)存就是為了解決我們的共享內(nèi)存的問題,我們前面說到我們拷貝內(nèi)存到我們的寄存器里面再到我們的線程中,我們修改數(shù)據(jù)再原路返回,在這中間可能會(huì)有其它的線程再讀這塊內(nèi)存,這就可能導(dǎo)致我們讀到的數(shù)據(jù)不是最新的數(shù)據(jù),然而加上我們的synchronized之后
1.我們首先會(huì)加鎖,加鎖之后別的線程就不能再去訪問和讀取這塊內(nèi)存了。
2.從內(nèi)存中讀取數(shù)據(jù)到寄存器和高速緩存中。
3.處理數(shù)據(jù)。
4.再將寄存器和高速緩存中的數(shù)據(jù)返回到內(nèi)存中。
5.開鎖,其它的線程可以讀取內(nèi)存中的數(shù)據(jù)了。
可重入
可重入是我們 synchronized 可以讓我們的程序避免產(chǎn)生自己將自己鎖住的關(guān)鍵。
所謂的自己將自己給鎖住就是我們想要給同一塊的代碼重復(fù)的上鎖,而且必須重復(fù)上鎖才能繼續(xù)的運(yùn)行下去,如果我們不能重復(fù)上鎖的話,我們就要等待該鎖解除才能繼續(xù)的上鎖,但是要想解除該所就必須得執(zhí)行重復(fù)上鎖的代碼,這就矛盾了,也就產(chǎn)生了死鎖(下面詳細(xì)介紹)。
static class Counter {
public int count = 0;
synchronized void increase() {
count++;
}
synchronized void increase2() {
increase();
}
}
例如上面這段代碼,我們調(diào)用increase2()的時(shí)候會(huì)對(duì)當(dāng)前的對(duì)象加鎖,然后我們?cè)偃フ{(diào)用increase()就又對(duì)當(dāng)前的對(duì)象加了一次鎖,這里不會(huì)產(chǎn)生錯(cuò)誤是因?yàn)槲覀冎С种貜?fù)加鎖,
在可重入鎖的內(nèi)部, 包含了 "線程持有者" 和 "計(jì)數(shù)器" 兩個(gè)信息:
如果某個(gè)線程加鎖的時(shí)候, 發(fā)現(xiàn)鎖已經(jīng)被人占用, 但是恰好占用的正是自己, 那么仍然可以繼續(xù)獲取
到鎖, 并讓計(jì)數(shù)器自增.解鎖的時(shí)候計(jì)數(shù)器遞減為 0 的時(shí)候, 才真正釋放鎖. (才能被別的線程獲取到)。
synchronized 的幾種用法
直接修飾普通方法:
鎖的 SynchronizedDemo 對(duì)象
public class SynchronizedDemo {
public synchronized void methond() {
}
}
修飾靜態(tài)方法
鎖的 SynchronizedDemo 類的對(duì)象
public class SynchronizedDemo {
public synchronized static void method() {
}
}
修飾代碼塊
明確指定鎖哪個(gè)對(duì)象
public class SynchronizedDemo {
public void method() {
synchronized (this) {
}
}
}
鎖類對(duì)象
public class SynchronizedDemo {
public void method() {
synchronized (SynchronizedDemo.class) {
}
}
}
volatile
volatile可以保證我們的數(shù)據(jù)是從內(nèi)存中讀取的,防止優(yōu)化而導(dǎo)致的線程不安全的問題。
我們的線程在操作內(nèi)存的時(shí)候會(huì)先把內(nèi)存里的數(shù)據(jù)放到寄存器中然后再從寄存器中拿到數(shù)據(jù),但是從內(nèi)存中拿數(shù)據(jù)是一個(gè)很慢的操作,所以有些時(shí)候進(jìn)行一些優(yōu)化然后就會(huì)直接從寄存器中拿數(shù)據(jù),這個(gè)時(shí)候如果其它的線程更改了數(shù)據(jù),這個(gè)時(shí)候我們拿到的就是舊的了。
我們的volatile就可以保證我們的內(nèi)存可見性,保證我們拿到的數(shù)據(jù)都是從內(nèi)存中拿到的,而不是工作內(nèi)存(寄存器,緩存)中偷懶拿到。
當(dāng)然我們的synchronized()也能保證我們內(nèi)存的可見性,但是我們不能無腦的頻繁使用synchronized(),因?yàn)槠涫褂枚嗔丝赡軙?huì)造成線程阻塞等問題大大降低了我們的性能,解決內(nèi)存可見性的問題的時(shí)候使用synchronized()所要付出的代碼往往更高。
Java 標(biāo)準(zhǔn)庫中的線程安全類
我們Java標(biāo)準(zhǔn)庫中有很多的線程不安全的類常見的有
ArrayList
LinkedList
HashMap
TreeMap
HashSet
TreeSet
StringBuilder
因?yàn)檫@些類里面的代碼都沒有加鎖,所以我們?cè)谑褂玫臅r(shí)候要格外的注意,為了解決部分的問題我們也提供了一些線程安全的類。
Vector
HashTable
ConcurrentHashMap
StringBuffer
這些類里面的關(guān)鍵方法都加了鎖,所以在進(jìn)行多線程的時(shí)候不用擔(dān)心線程安全的問題。
其中我們的String類也是線程安全的雖然沒有加鎖但是,其本身的特性不可變讓其具有線程安全。
死鎖
什么是死鎖
所謂死鎖,是指多個(gè)進(jìn)程在運(yùn)行過程中因爭奪資源而造成的一種僵局,當(dāng)進(jìn)程處于這種僵持狀態(tài)時(shí),若無外力作用,它們都將無法再向前推進(jìn)。?
在我們的Java多線程操作的時(shí)候各個(gè)線程去爭奪同一資源也會(huì)陷入到僵局這個(gè)時(shí)候也會(huì)產(chǎn)生死鎖。
比如說我們前面的synchronized這個(gè)方法可以重復(fù)加鎖,如果不能重復(fù)加鎖的話,那么我們就會(huì)產(chǎn)生死鎖,也就是上面說的不可重入性而導(dǎo)致的死鎖。
死鎖的情況
1.一個(gè)線程一把鎖上面自己鎖自己的情況。
2.兩個(gè)線程兩把鎖,我們兩個(gè)線程對(duì)兩個(gè)對(duì)象分別上了鎖,然后剛好這兩個(gè)線程又要去操作兩個(gè)對(duì)象,因?yàn)槎紝?duì)彼此上鎖了,都到等對(duì)方結(jié)束,但是不執(zhí)行又不能結(jié)束這就有產(chǎn)生了死鎖。
2.n個(gè)線程m把鎖。
死鎖的必要條件
1.互斥使用:一個(gè)鎖被一個(gè)線程占用以后,其它的線程就用不了了。
2.不可搶占:一個(gè)鎖被一個(gè)線程占用以后,其它的線程不能搶占。
3.請(qǐng)求和保持:當(dāng)一個(gè)線程占據(jù)多把鎖的時(shí)候,除非顯示的釋放鎖否則,否則這些鎖始終都被占用。
4.環(huán)路等待,各個(gè)線程之間互相等待彼此解鎖。
wait 和 notify
wait() ?wait(long timeout): 讓當(dāng)前線程進(jìn)入等待狀態(tài)。
notify() ?notifyAll(): 喚醒在當(dāng)前對(duì)象上等待的線程。
注意:
wait, notify, notifyAll 都是 Object 類的方法。
wait()
我們的wait()方法主要是為了讓我們當(dāng)前所在的線程進(jìn)入到一個(gè)等待的狀態(tài),其工作原理分為三個(gè)步驟:
1.讓我們當(dāng)前所在的線程進(jìn)入到一個(gè)等待的狀態(tài)。
2.釋放當(dāng)前線程所在的鎖。(所以我們?cè)谟脀ait()方法之前一定要有鎖才行)。
3.等待條件被喚醒。
結(jié)束等待條件:
1.其他線程調(diào)用該對(duì)象的 notify 方法.
2.wait 等待時(shí)間超時(shí) (wait 方法提供一個(gè)帶有 timeout 參數(shù)的版本, 來指定等待時(shí)間).
3.其他線程調(diào)用該等待線程的 interrupted 方法, 導(dǎo)致 wait 拋出 InterruptedException 異常
public static void main(String[] args) throws InterruptedException {
Object object = new Object();
synchronized (object) {
System.out.println("等待前");
object.wait();
System.out.println("等待后");
}
}
?執(zhí)行代碼我們會(huì)發(fā)現(xiàn)我們一直處于等待的狀態(tài)。
notify()
notify()方法是用來通知在等待被wait()等待的線程,這個(gè)線程已經(jīng)失去了鎖,我們別的線程通知wait()所在的線程后繼續(xù)執(zhí)行當(dāng)前的代碼,執(zhí)行完畢之后退出當(dāng)前的線程,然后wait所在的線程重新獲得鎖接著執(zhí)行后面的代碼。當(dāng)我們有多個(gè)線程都在等待一個(gè)對(duì)象的鎖的時(shí)候我們notify()會(huì)隨機(jī)的釋放一個(gè)線程。
public static void main(String[] args) throws InterruptedException {
Object object = new Object();
Thread thread = new Thread(()->{
System.out.println("thread等待前");
synchronized (object) {
try {
object.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
System.out.println("thread等待后");
});
thread.start();
Thread.sleep(3000);//主線程休眠
Thread thread1 = new Thread(()->{
System.out.println("thread1通知前");
synchronized (object) {
object.notify();
}
System.out.println("thread1通知后");
});
thread1.start();
}
notifyAll()方法
相比于notify()方法,notifyAll()的方法在對(duì)多個(gè)線程同時(shí)等待的情況下會(huì)將會(huì)喚醒所有等待的線程,但是這個(gè)線程回去競(jìng)爭當(dāng)前的鎖,競(jìng)爭到然后去執(zhí)行自己剩下的代碼。
wait()和sleep()的對(duì)比
1.我們的sleep()是休眠我們當(dāng)前的線程,而wait()是用于線程通信的。
2.wait()要搭配synchronized 使用. sleep ()不需要。文章來源:http://www.zghlxwxcb.cn/news/detail-434798.html
3.wait()是Object的方法而sleep()是Thread 的靜態(tài)方法。文章來源地址http://www.zghlxwxcb.cn/news/detail-434798.html
到了這里,關(guān)于Java中的多線程——線程安全問題的文章就介紹完了。如果您還想了解更多內(nèi)容,請(qǐng)?jiān)谟疑辖撬阉鱐OY模板網(wǎng)以前的文章或繼續(xù)瀏覽下面的相關(guān)文章,希望大家以后多多支持TOY模板網(wǎng)!