java多線程并發(fā)(一)——Semaphore,volatile,synchronized ,Lock

在并發(fā)編程中,我們通常會遇到以下三個問題:原子性問題,可見性問題,有序性問題。我們先看具體看一下這三個概念:

1.原子性

原子性:即一個操作或者多個操作 要么全部執(zhí)行并且執(zhí)行的過程不會被任何因素打斷,要么就都不執(zhí)行。

一個很經典的例子就是銀行賬戶轉賬問題
2.可見性

可見性是指當多個線程訪問同一個變量時,一個線程修改了這個變量的值,其他線程能夠立即看得到修改的值。

3.有序性

有序性:即程序執(zhí)行的順序按照代碼的先后順序執(zhí)行。

Semaphore

簡介
信號量(Semaphore),有時被稱為信號燈,是在多線程環(huán)境下使用的一種設施, 它負責協(xié)調各個線程, 以保證它們能夠正確、合理的使用公共資源。

一個計數(shù)信號量。從概念上講,信號量維護了一個許可集。如有必要,在許可可用前會阻塞每一個 acquire(),然后再獲取該許可。每個
release() 添加一個許可,從而可能釋放一個正在阻塞的獲取者。但是,不使用實際的許可對象,Semaphore
只對可用許可的號碼進行計數(shù),并采取相應的行動。拿到信號量的線程可以進入代碼,否則就等待。通過acquire()和release()獲取和釋放訪問許可。

概念
Semaphore分為單值和多值兩種,前者只能被一個線程獲得,后者可以被若干個線程獲得。

以一個停車場運作為例。為了簡單起見,假設停車場只有三個車位,一開始三個車位都是空的。這時如果同時來了五輛車,看門人允許其中三輛不受阻礙的進入,然后放下車攔,剩下的車則必須在入口等待,此后來的車也都不得不在入口處等待。這時,有一輛車離開停車場,看門人得知后,打開車攔,放入一輛,如果又離開兩輛,則又可以放入兩輛,如此往復。

在這個停車場系統(tǒng)中,車位是公共資源,每輛車好比一個線程,看門人起的就是信號量的作用。
代碼示例:

public class BeautySeekBarView extends View {

 private Semaphore sePoolTH=new Semaphore(0);//信號量,解決并發(fā)問題

@Override
 protected void onDraw(Canvas canvas) {
 // TODO Auto-generated method stub
 super.onDraw(canvas);

 float PointX = 0;
 float PointY=getHeight()/2; 
 canvas.drawLine(0+getPaddingLeft(),PointY, getWidth()-

 //當繪制完成后釋放信號,可以后續(xù)操作了
 sePoolTH.release();

//設置默認位置
 public void setPointLocation(final int location){
 new Thread(new Runnable() {

 @Override
 public void run() { 
 try {
 //等待信號,直到收到上面釋放的信號了,開始進行一下邏輯
 sePoolTH.acquire();
 } catch (InterruptedException e) {
 // TODO Auto-generated catch block
 e.printStackTrace();
 } 
 if(location>0&&pointList!=null&& !pointList.isEmpty()){
 bitmapPointX=pointList.get(location-1);
 postInvalidate();
 }

 }
 }).start();

 }

volatile

1.volatile關鍵字的兩層語義

一旦一個共享變量(類的成員變量、類的靜態(tài)成員變量)被volatile修飾之后,那么就具備了一下語義:

1。保證了不同線程對這個變量進行操作時的可見性,即一個線程修改了某個變量的值,這新值對其他線程來說是立即可見的。

2。禁止進行指令重排序。
  3. 使用volatile關鍵字會強制將修改的值立即寫入主存;

在 Java 垃圾回收整理一文中,描述了jvm運行時刻內存的分配。其中有一個內存區(qū)域是jvm虛擬機棧,每一個線程運行時都有一個線程棧,線程棧保存了線程運行時候變量值信息。當線程訪問某一個對象時候值的時候,首先通過對象的引用找到對應在堆內存的變量的值,然后把堆內存變量的具體值load到線程本地內存中,建立一個變量副本,之后線程就不再和對象在堆內存變量值有任何關系,而是直接修改副本變量的值,在修改完之后的某一個時刻(線程退出之前),自動把線程變量副本的值回寫到對象在堆中變量。這樣在堆中的對象的值就產生變化了。

對于volatile修飾的變量,jvm虛擬機只是保證從主內存加載到線程工作內存的值是最新的

例如假如線程1,線程2 在進行read,load 操作中,發(fā)現(xiàn)主內存中count的值都是5,那么都會加載這個最新的值

在線程1堆count進行修改之后,會write到主內存中,主內存中的count變量就會變?yōu)?

線程2由于已經進行read,load操作,在進行運算之后,也會更新主內存count的變量值為6

導致兩個線程及時用volatile關鍵字修改之后,還是會存在并發(fā)的情況。

通常來說,使用volatile必須具備以下2個條件:

1)對變量的寫操作不依賴于當前值

2)該變量沒有包含在具有其他變量的不變式中。

synchronized

同步的實現(xiàn)當然是采用鎖了,java中使用鎖的兩個基本工具是 synchronized 和 Lock。

一直很喜歡synchronized,因為使用它很方便。比如,需要對一個方法進行同步,那么只需在方法的簽名添加一個synchronized關鍵字。

// 未同步的方法
public void test() {}
// 同步的方法
pubilc synchronized void test() {}

synchronized 也可以用在一個代碼塊上,看

public void test() {
 synchronized(obj) {
 System.out.println("===");
 }
}

synchronized 用在方法和代碼塊上有什么區(qū)別呢?

synchronized
用在方法簽名上(以test為例),當某個線程調用此方法時,會獲取該實例的對象鎖,方法未結束之前,其他線程只能去等待。當這個方法執(zhí)行完時,才會釋放對象鎖。其他線程才有機會去搶占這把鎖,去執(zhí)行方法test,但是發(fā)生這一切的基礎應當是所有線程使用的同一個對象實例,才能實現(xiàn)互斥的現(xiàn)象。否則synchronized關鍵字將失去意義。

(但是如果該方法為類方法,即其修飾符為static,那么synchronized 意味著某個調用此方法的線程當前會擁有該類的鎖,只要該線程持續(xù)在當前方法內運行,其他線程依然無法獲得方法的使用權?。?/p>

synchronized 用在代碼塊的使用方式:synchronized(obj){//todo code here}

當線程運行到該代碼塊內,就會擁有obj對象的對象鎖,如果多個線程共享同一個Object對象,那么此時就會形成互斥!特別的,當obj == this時,表示當前調用該方法的實例對象。即

public void test() {
...
synchronized(this) {
// todo your code
}
...
}

此時,其效果等同于

public synchronized void test() {
// todo your code
}

使用synchronized代碼塊,可以只對需要同步的代碼進行同步,這樣可以大大的提高效率。

小結:
使用synchronized 代碼塊相比方法有兩點優(yōu)勢:
1、可以只對需要同步的使用
2、與wait()/notify()/nitifyAll()一起使用時,比較方便

wait() 與notify()/notifyAll()

這三個方法都是Object的方法,并不是線程的方法!
wait():釋放占有的對象鎖,線程進入等待池,釋放cpu,而其他正在等待的線程即可搶占此鎖,獲得鎖的線程即可運行程序。而sleep()不同的是,線程調用此方法后,會休眠一段時間,休眠期間,會暫時釋放cpu,但并不釋放對象鎖。也就是說,在休眠期間,其他線程依然無法進入此代碼內部。休眠結束,線程重新獲得cpu,執(zhí)行代碼。wait()和sleep()最大的不同在于wait()會釋放對象鎖,而sleep()不會!

notify():
該方法會喚醒因為調用對象的wait()而等待的線程,其實就是對對象鎖的喚醒,從而使得wait()的線程可以有機會獲取對象鎖。調用notify()后,并不會立即釋放鎖,而是繼續(xù)執(zhí)行當前代碼,直到synchronized中的代碼全部執(zhí)行完畢,才會釋放對象鎖。JVM則會在等待的線程中調度一個線程去獲得對象鎖,執(zhí)行代碼。需要注意的是,wait()和notify()必須在synchronized代碼塊中調用。

notifyAll()則是喚醒所有等待的線程。

為了說明這一點,舉例如下:
兩個線程依次打印”A”“B”,總共打印10次。

public class Consumer implements Runnable {

 @Override
 public synchronized void run() {
 // TODO Auto-generated method stub
 int count = 10;
 while(count > 0) {
 synchronized (Test. obj) {

 System. out.print( "B");
 count --;
 Test. obj.notify(); // 主動釋放對象鎖

 try {
 Test. obj.wait();

 } catch (InterruptedException e) {
 // TODO Auto-generated catch block
 e.printStackTrace();
 }
 }

 }
 }
}

public class Produce implements Runnable {

 @Override
 public void run() {
 // TODO Auto-generated method stub
 int count = 10;
 while(count > 0) {
 synchronized (Test. obj) {

 //System.out.print("count = " + count);
 System. out.print( "A");
 count --;
 Test. obj.notify();

 try {
 Test. obj.wait();
 } catch (InterruptedException e) {
 // TODO Auto-generated catch block
 e.printStackTrace();
 }
 }

 }

 }

}

測試類如下:

public class Test {

 public static final Object obj = new Object();

 public static void main(String[] args) {

 new Thread( new Produce()).start();
 new Thread( new Consumer()).start();

 }
}

這里使用static
obj作為鎖的對象,當線程Produce啟動時(假如Produce首先獲得鎖,則Consumer會等待),打印“A”后,會先主動釋放鎖,然后阻塞自己。Consumer獲得對象鎖,打印“B”,然后釋放鎖,阻塞自己,那么Produce又會獲得鎖,然后…一直循環(huán)下去,直到count
= 0.這樣,使用Synchronized和wait()以及notify()就可以達到線程同步的目的。

除了wait()和notify()協(xié)作完成線程同步之外,使用Lock也可以完成同樣的目的。

ReentrantLock 與synchronized有相同的并發(fā)性和內存語義,還包含了中斷鎖等候和定時鎖等候,意味著線程A如果先獲得了對象obj的鎖,那么線程B可以在等待指定時間內依然無法獲取鎖,那么就會自動放棄該鎖。

但是由于synchronized是在JVM層面實現(xiàn)的,因此系統(tǒng)可以監(jiān)控鎖的釋放與否,而ReentrantLock使用代碼實現(xiàn)的,系統(tǒng)無法自動釋放鎖,需要在代碼中finally子句中顯式釋放鎖lock.unlock();

同樣的例子,使用lock 如何實現(xiàn)呢?

lock

public class Consumer implements Runnable {

 private Lock lock;
 public Consumer(Lock lock) {
 this. lock = lock;
 }
 @Override
 public void run() {
 // TODO Auto-generated method stub
 int count = 10;
 while( count > 0 ) {
 try {
 lock.lock();
 count --;
 System. out.print( "B");
 } finally {
 lock.unlock(); //主動釋放鎖
 try {
 Thread. sleep(91L);
 } catch (InterruptedException e) {
 // TODO Auto-generated catch block
 e.printStackTrace();
 }
 }
 }

 }

}

public class Producer implements Runnable{

 private Lock lock;
 public Producer(Lock lock) {
 this. lock = lock;
 }
 @Override
 public void run() {
 // TODO Auto-generated method stub
 int count = 10;
 while (count > 0) {
 try {
 lock.lock();
 count --;
 System. out.print( "A");
 } finally {
 lock.unlock();
 try {
 Thread. sleep(90L);
 } catch (InterruptedException e) {
 // TODO Auto-generated catch block
 e.printStackTrace();
 }
 }
 }
 }
}

調用代碼:

public class Test {

 public static void main(String[] args) {
 Lock lock = new ReentrantLock();

 Consumer consumer = new Consumer(lock);
 Producer producer = new Producer(lock);

 new Thread(consumer).start();
 new Thread( producer).start();

 }
}

使用建議:

在并發(fā)量比較小的情況下,使用synchronized是個不錯的選擇,但是在并發(fā)量比較高的情況下,其性能下降很嚴重,此時ReentrantLock是個不錯的方案。

此系列更多文章目前還在CSDN: http://blog.csdn.net/King1425/article/category/6814582 敬請關注

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯(lián)系作者
【社區(qū)內容提示】社區(qū)部分內容疑似由AI輔助生成,瀏覽時請結合常識與多方信息審慎甄別。
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發(fā)布,文章內容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務。

相關閱讀更多精彩內容

  • 從三月份找實習到現(xiàn)在,面了一些公司,掛了不少,但最終還是拿到小米、百度、阿里、京東、新浪、CVTE、樂視家的研發(fā)崗...
    時芥藍閱讀 42,804評論 11 349
  • Java8張圖 11、字符串不變性 12、equals()方法、hashCode()方法的區(qū)別 13、...
    Miley_MOJIE閱讀 3,897評論 0 11
  • 一:java概述:1,JDK:Java Development Kit,java的開發(fā)和運行環(huán)境,java的開發(fā)工...
    ZaneInTheSun閱讀 2,812評論 0 11
  • 1. Java基礎部分 基礎部分的順序:基本語法,類相關的語法,內部類的語法,繼承相關的語法,異常的語法,線程的語...
    子非魚_t_閱讀 34,706評論 18 399
  • 一直以來這是我第五次在這里寫東西了 也是最后一次為你寫了 我們之間發(fā)生了太多太多的事了 有開心的 不開心...
    唯有你愛閱讀 739評論 0 0

友情鏈接更多精彩內容