一. 對象及變量的并發(fā)訪問
非線程安全會發(fā)生在多個線程并發(fā)訪問同一個對象的實例變量時,會產(chǎn)生臟讀,讀取到的數(shù)據(jù)是被更改過的。而線程安全各個線程獲得的實例變量的值都是經(jīng)過同步處理的,不會出現(xiàn)臟讀。
1.線程是否安全呢?
(1) 如果是方法內(nèi)部的私有變量,不存在非線程安全問題。
(2) 多線程訪問的是同一個對象的實例變量時,有可能出現(xiàn)線程不安全問題。
(3) 多個線程訪問的是同步方法的話,一定是線程安全的。
(4) 多個線程對應的是多個對象時,出現(xiàn)的結(jié)果就會是異步的,但是線程安全。
2. synchronized關鍵字
(1) synchronized獲得的是對象鎖,而不是把synchronized下面的方法或者代碼塊當做鎖。
(2) synchronized聲明的方法一定是排隊執(zhí)行的(同步的),只有共享資源的讀寫訪問才需要同步化。
(3) 線程A和線程B訪問同一個object對象的兩個同步的方法,線程A先獲取object對象的Lock鎖,B線程可以以異步的方式調(diào)用object對象中的非同步方法,但是想訪問該對象的同步方法的話,必須得等待,不管想訪問的是不是和線程A同一個同步方法。
(4) synchronized有鎖重入的功能,即自己可以再次獲取自己的內(nèi)部鎖,可重入鎖也支持父子類繼承關系中,即子類可以通過可重入鎖訪問父類的方法。若不可重入的話,就會造成死鎖。
(5) 當一個線程執(zhí)行的代碼出現(xiàn)異常時,其持有的鎖會自動釋放(即該線程結(jié)束執(zhí)行)。
(6) 同步不具有繼承性。
(7) 鎖定的對象改變,比如String,可能導致同步鎖無效(因為鎖變了)。但是只要對象不變,對象的屬性被改變,鎖還是同一個。
3.synchronized同步語句塊
synchronized同步方法是對當前對象加鎖,同步代碼塊則是對某一個對象加鎖。synchronized同步代碼塊運行效率應該大于同步方法。
synchronized(this):也是鎖定當前對象的。
synchronized(非this對象):使用同步代碼塊來鎖定非this對象,則synchronized(非this對象)與同步方法是異步的,不與其他鎖this同步方法爭搶this鎖,可以大大提高效率。synchronized同步代碼塊都不采用String作為鎖對象,易造成死鎖。
4.synchronized關鍵字加到static靜態(tài)方法上是給Class類加上鎖(Class鎖可以對類得所有對象實例起作用),而加到非static靜態(tài)方法上是給對象上鎖。
synchronized關鍵字加到static靜態(tài)方法上是給Class類加上鎖 = synchronized(xxx.Class){}
5.多線程的死鎖
因為不同的線程都在等待根本不可能被釋放的鎖,從而導致所有的任務都無法繼續(xù)執(zhí)行。
比如線程A持有了鎖1在等待鎖2,線程A持有了鎖2在等待鎖1--》導致死鎖。
解決方案:不使用嵌套的synchronized代碼結(jié)構(gòu)。
6.內(nèi)置類與靜態(tài)內(nèi)置類(補充介紹)
非靜態(tài)內(nèi)置類:指定對象.new 內(nèi)置類();
靜態(tài)內(nèi)置類:可直接new 內(nèi)置類();
7.volatile關鍵字:使變量在多個線程中可見
作用:強制從公共堆棧中獲取變量的值,而不是從線程私有數(shù)據(jù)棧中獲取。
※ 在JVM被設置為-server模式時是為了線程運行的效率,線程一直在私有堆棧中獲取變量的值。
在-server模式下,公共堆棧的值和線程私有數(shù)據(jù)棧的值不同步,加了volatile后就會強制從公共堆棧中讀寫。
※volatile和synchronized的比較:
(1) volatile只能修飾變量,是輕量級實現(xiàn),所以性能比synchronized好。
(2) 多線程訪問volatile不會阻塞,而訪問synchronized會阻塞。
(3) volatile能保證數(shù)據(jù)可見性,但不具備同步性,不支持原子性;而synchronized可以保證原子性,也可以間接保證可見性,因為他會將私有內(nèi)存和共有內(nèi)存中的數(shù)據(jù)做同步。
(4) 兩者功能屬性不同,synchronized解決的是多個線程之間訪問資源的同步性;
volatile解決變量在多個線程之間的可見性,即:在多個線程可以感知實例變量被修改了,并且可以獲得最新的值引用,也就是用多線程讀取共享變量時能獲得最新值引用。
volatile int i
i++;
i++有如下三個步驟:
(1) 從內(nèi)存中獲取i的值;
(2)計算i的值;
(3) 將i的值寫入內(nèi)存中。
這樣的操作不是一個原子操作(聯(lián)想:synchronized修飾的方法或者代碼段可以看做一個整體,因此具有原子性),比如線程B要提取i的值時,線程A還未將計算好的i的值放回內(nèi)存,則線程B取出來的i的值還是線程A計算前的值。--》線程不安全
8.AtomicInteger(AtomicLong等)
private AtomicInteger count = new AtomicInteger(0);
System.out.println(count.incrementAndGet());//自動加1//decrementAndGet()自動減1
public final int incrementAndGet() { for (;;) { int current = get(); int next = current + 1; if (compareAndSet(current, next)) return next; } }
Compare And Swap(CAS):首先,CPU 會將內(nèi)存中將要被更改的數(shù)據(jù)與期望的值做比較。然后,當這兩個值相等時,CPU 才會將內(nèi)存中的數(shù)值替換為新的值。否則便不做操作。最后,CPU 會將舊的數(shù)值返回。這一系列的操作是原子的。簡單來說,CAS 的含義是“我認為原有的值應該是什么,如果是,則將原有的值更新為新值,否則不做修改,并告訴我原來的值是多少”。
CAS有3個操作數(shù),內(nèi)存值V,舊的預期值A,要修改的新值B。當且僅當預期值A和內(nèi)存值V相同時,將內(nèi)存值V修改為B,否則返回V。這是一種樂觀鎖的思路,它相信在它修改之前,沒有其它線程去修改它;而Synchronized是一種悲觀鎖,它認為在它修改之前,一定會有其它線程去修改它,悲觀鎖效率很低。下面來看一下AtomicInteger是如何利用CAS實現(xiàn)原子性操作的。
但是CAS也是有問題存在的:
CAS的ABA問題
1.進程P1在共享變量中讀到值為A
2.P1被搶占了,進程P2執(zhí)行
3.P2把共享變量里的值從A改成了B,再改回到A,此時被P1搶占。
4.P1回來看到共享變量里的值沒有被改變,于是繼續(xù)執(zhí)行。
雖然P1以為變量值沒有改變,繼續(xù)執(zhí)行了,但是這個會引發(fā)一些潛在的問題。ABA問題最容易發(fā)生在lock free 的算法中的,CAS首當其沖,因為CAS判斷的是指針的地址。如果這個地址被重用了呢,問題就很大了。(地址被重用是很經(jīng)常發(fā)生的,一個內(nèi)存分配后釋放了,再分配,很有可能還是原來的地址)
還有一種情況是:單獨一個AtomicInteger.incrementAndGet()是線程安全的,但是同時兩個AtomicInteger.incrementAndGet()就不一定是線程安全的了,即兩個方法之間不是原子的。
public static AtomicLong count = new AtomicLong();
public void addNum(){
System.out.println(count.addAndGet(100));
System.out.println(count.addAndGet(1));
}
多個線程調(diào)用addNum()時,線程A加了100,還沒來得及加1,線程B就進來加了100。
解決方案:
public static AtomicLong count = new AtomicLong();
synchronized public void addNum(){
System.out.println(count.addAndGet(100));
System.out.println(count.addAndGet(1));
}
9.synchronized代碼塊也具有volatile同步的功能
線程A調(diào)用runMethod(),線程B調(diào)用stopMethod(),持有的是同一把鎖。
當線程A調(diào)用完runMethod()后,打印不出"停下來了!"的,因為死循環(huán),被A鎖死了。
各線程間的數(shù)據(jù)值沒有可見性。
private Boolean isContinueRun = true;
public void runMethod(){
??? while(isContinueRun){
??? }
??? System.out.println("停下來了!");
}
public void stopMethod(){
??? isContinueRun = false;
}
解決方案如下,成功打印"停下來了!"
private Boolean isContinueRun = true;
public void runMethod(){
??? private anyString = new String();
??? while(isContinueRun){
??????? synchronized(anyString){
??????? }
??? }
??? System.out.println("停下來了!");
}
public void stopMethod(){
??? isContinueRun = false;
}
關鍵字synchronized 保證同一時刻,只有一個線程可以執(zhí)行某一個方法或某一個代碼塊。包含兩種特性:互斥性和可見性。
?不僅可以解決一個線程看到對象處于不一致的狀態(tài),還可以保證進入同步方法或代碼塊的每個線程,都可以看到由同一個鎖保護之前所有的修改結(jié)果。(外連互斥,內(nèi)修可見。)
二. 鎖的使用
Lock也能實現(xiàn)同步的效果,在使用上更加方便。
1. ReentrantLock類? -- Lock lock = new ReentrantLock();
(1) 使用ReentrantLock.lock()獲取鎖(加鎖),線程就擁有了“對象監(jiān)視器”;
其他線程只有等待ReentrantLock.unlock()釋放鎖(解鎖),再次爭搶獲得鎖。
效果和synchronized一致,但線程執(zhí)行順序是隨機的。
(2) 關鍵字與wait()/notify()/notifyAll():實現(xiàn)等待/通知模式,但是notifyAll()的話,需要通知所有處于WAITING狀態(tài)的線程,會出現(xiàn)相當大的效率問題。
ReentrantLock和Condition對象也同樣可以實現(xiàn)。在一個Lock對象里可以創(chuàng)建多個Condition(即對象監(jiān)視器)實例,可以實現(xiàn)多路通知功能;實例對象可以注冊在指定的Condition中,從而可以有選擇地進行線程通知,在調(diào)度線程上更加靈活。
package lock;
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;public class LockService {
?private Lock lock = new ReentrantLock();
?public Condition condition = lock.newCondition();
?public void await(){
??try {
???lock.lock();
???condition.await();//必須在調(diào)用await()之前先調(diào)用lock()以獲得同步監(jiān)視器
??} catch (InterruptedException e) {
???e.printStackTrace();
??}
?}
?
?public void signal(){
??try {
???lock.lock();
???condition.signal();//還有condition.signalAll();
??} finally{
???lock.unlock();
??}??
?}
}
(3) signalAll()
public Condition conditionA = lock.newCondition();
public Condition conditionB = lock.newCondition();
//use:
conditionA.signal(); //or conditionB.signal();
喚醒指定種類的線程,如conditionA.signal(); 只有用了conditionA的線程被喚醒。
(4) 公平鎖與非公平鎖
公平鎖:線程獲取鎖的順序是按照線程加鎖的順序來分配的(FIFO);new ReentrantLock(true);//不一定百分百FIFO,但是基本呈有序。
非公平鎖(默認):鎖的搶占機制,隨機獲得鎖。new ReentrantLock(false);
2.相關方法介紹
(1) int getHoldCount():查詢當前線程保持此鎖定的個數(shù),也就是調(diào)用lock()的次數(shù)。
(2) int getQueueLength():返回正獲取此鎖定的線程估計數(shù),如5個線程,一個線程調(diào)用了await(),還有4個線程在等待鎖的釋放。
(3) int getWaitQueueLength(Condition condition):比如有5個線程,每個線程都執(zhí)行了同一個condition對象的await(),則結(jié)果為5。
(4) boolean hasQueuedThread(Thread thread):查詢指定的線程是否正在等待此鎖定。
(5) boolean hasWaiters(Condition condition):是否有線程正在等待與此鎖定有關的condition條件。
(6) boolean isFair():判斷是不是公平鎖。
(7) boolean isHeldByCurrentThread():查詢當前線程是否保持此鎖定。
(8) boolean? isLocked():查詢此鎖定是否由任意線程保持。
(9) void lockInterruptibly():如果當前線程未被中斷,則獲取鎖定,如果已經(jīng)被中斷則出現(xiàn)異常。
(10) boolean tryLock():僅在調(diào)用時鎖定未被另一個線程保持的情況下,才獲取該鎖定。
(11) boolean tryLock(long timeout,TimeUnit unit):若在給定等待時間內(nèi)沒有被另一個線程保持,且當前線程未被中斷,則獲取該鎖定。
(12) Condition.awaitUninterruptibly():在WAITING情況下interrupt()不會拋出異常。
(13) Condition.awaitUntil(time):線程在等待時間到達前,可以被其他線程喚醒。
3.ReentranReadWriteLock類
共享鎖:讀操作相關的鎖;排他鎖:寫操作相關的鎖。
讀寫,寫讀,寫寫都是互斥的;讀讀是異步的,非互斥的。