Java 同步機(jī)制

前言:

多線程開發(fā)中往往需要同步處理了,這是因?yàn)橐粋€(gè)進(jìn)程中的線程是共享JVM中的方法區(qū)和堆區(qū),同時(shí)操作臨界區(qū)資源的時(shí)候會(huì)破壞了原子性,導(dǎo)致數(shù)據(jù)出現(xiàn)錯(cuò)誤。就需要同步操作,也就有了鎖。

先從一個(gè)簡(jiǎn)單的銀行轉(zhuǎn)賬例子開始:

 public class Bank{
    List<Account> accounts = new ArrayList<>();
    
    // 虛擬創(chuàng)建10個(gè)賬號(hào)
    public Bank(){
        for(int i=0;i<10;i++){
            accounts.add(new Account());
        }
    }
    
    // 獲取總資金
    public int getTotalMoney(){
        int total = 0;
        for(int i = 0;i<accounts.size();i++){
            total+=accounts.get(i).money;
        }
        return total;
    }
    
    // 轉(zhuǎn)賬操作
    public void transfers(int from,int to,int money){
        if(accounts.get(from).money<money)
            return;
        accounts.get(from).money -=money;
        accounts.get(to).money +=money;
        System.out.printf("Bank總共money = %d  \n",getTotalMoney());
    }
    

    // 測(cè)試兩個(gè)Thread轉(zhuǎn)賬
    public void start(){

        Thread t1 = new Thread(new Runnable() {
            @Override
            public void run() {
                while(true){
                    int money = (int) ((double)50*Math.random());
                    int from =  (int) ((double)9*Math.random());
                    try {
                        Thread.sleep(2);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    int to =  (int) ((double)9*Math.random());
                    transfers(from, to, money);
                }
            
            }
        });
        Thread t2 = new Thread(new Runnable() {
            @Override
            public void run() {
                  ......省略 與t1一致
            }
        });
        t1.start();
        t2.start();
    }
    // 程序入口
    public static void main(String[] args) {
        Bank b = new Bank();
        b.start();
    }
}

 // 用戶賬號(hào)
class Account{
    int money = 1000; // 原始資金
}
結(jié)果

原因:轉(zhuǎn)賬的時(shí)候,轉(zhuǎn)出和轉(zhuǎn)入是個(gè)原子的操作,兩個(gè)線程同時(shí)操作同一個(gè)賬戶的時(shí)候就很容易出錯(cuò)。線程的執(zhí)行是沒有順序可言的,一行代碼的指令會(huì)有多行,沒執(zhí)行完就被剝奪了運(yùn)行權(quán),另一個(gè)Thread再次處理就會(huì)導(dǎo)致數(shù)據(jù)不一致。

一、ReentrantLock鎖對(duì)象

java5.0版本引入了ReentrantLock類,它位于java.util.concurrent包下面。它是一個(gè)可以被用來保護(hù)臨界區(qū)的可重入鎖,只能有一個(gè)線程獲得鎖對(duì)象,其它線程執(zhí)行l(wèi)ock()方法時(shí),會(huì)阻塞在這里,直到當(dāng)前獲得鎖對(duì)象的線程釋放了鎖即unlock(),其它線程才可以競(jìng)爭(zhēng)。

// ReentrantLock使用步驟
myLock.lock();
try {
    同步代碼
} finally {
myLock.unlock();
}

在上面的例子中,只要改變給臨界區(qū)加上ReentrantLock就可以了。但是同一個(gè)線程可以多次獲得鎖對(duì)象(即lock.lock()操作),該ReentrantLock會(huì)有一個(gè)計(jì)數(shù)加鎖幾次,必須全部釋放鎖的時(shí)候才是線程真正的釋放當(dāng)前鎖對(duì)象,這時(shí)鎖計(jì)數(shù)為0。

    Lock lock = new ReentrantLock();
    // 轉(zhuǎn)賬操作
    public void transfers(int from,int to,int money){
        lock.lock(); // 加鎖
        if(accounts.get(from).money<money)
            return;
        accounts.get(from).money -=money;
        accounts.get(to).money +=money;
        System.out.printf("Bank總共money = %d  \n",getTotalMoney());
        lock.unlock(); // 轉(zhuǎn)賬完成后釋放鎖
    }

二、條件對(duì)象Condition

條件對(duì)象,是配合ReentrantLock對(duì)象使用的,他也是在java.util.concurrent包下面的。應(yīng)用場(chǎng)景:剛獲得鎖的線程,并不滿足一些必備的條件,如賬號(hào)金額不足。這個(gè)時(shí)候就必須阻塞當(dāng)前線程,釋放當(dāng)前鎖對(duì)象。其它線程獲得鎖對(duì)象,執(zhí)行成功后再通知解除等待線程的阻塞,但不是立即的就能獲得鎖對(duì)象,想要獲得鎖對(duì)象,還是要重新的競(jìng)爭(zhēng)。

    Lock lock = new ReentrantLock();
    Condition condition = lock.newCondition(); // 增加一個(gè)條件對(duì)象,用ReentrantLock創(chuàng)建條件對(duì)象

    // 轉(zhuǎn)賬操作
    public void transfers(int from, int to, int money) {
        lock.lock();
        try {
            while (accounts.get(from).money < money) { // 通常都是用循環(huán),防止重新獲得鎖的時(shí)候,條件依舊不能保證是否能滿足條件
                condition.await(); // 將線程加入等待集,阻塞當(dāng)前線程
            }
            accounts.get(from).money -= money;
            accounts.get(to).money += money;
            System.out.printf("Bank總共money = %d  \n", getTotalMoney());
            condition.signalAll(); //必須要通知解除阻塞
        } catch (InterruptedException e) {
            // TODO Auto-generated catch block
            e.printStackTrace();
        } finally {
            lock.unlock();
        }
        
    }

三、synchronized關(guān)鍵字

有了對(duì)象鎖和條件對(duì)象Condition后,為什么會(huì)有synchronized了。synchronized更加的簡(jiǎn)潔減少出錯(cuò)的概率,鎖的開啟和釋放均有JVM來操作,ReentrantLock則需要手動(dòng)的調(diào)動(dòng)加鎖和釋放鎖。ReentrantLock是可重入鎖,synchronized鎖僅有單一的條件。synchronized只能是非公平鎖,而ReentrantLock可以自己設(shè)置公平和非公平。總的來說java希望兩者最好都不使用,而是用阻塞隊(duì)列等來實(shí)現(xiàn)。
java中存在類鎖和對(duì)象鎖,作用如字面所描述。猜測(cè)java類鎖應(yīng)該作用于方法區(qū)當(dāng)中,對(duì)象鎖則是作用在堆區(qū)中。因?yàn)轭愋畔⒓虞d在方法區(qū),對(duì)象則分配中堆中。
synchronized代碼塊是由一對(duì)monitorenter/monitorexit指令實(shí)現(xiàn)的,Monitor對(duì)象是同步的基本實(shí)現(xiàn)單元。

3.1、synchronized作用在方法中

// 這個(gè)就是對(duì)象鎖
public synchronized void method(){
         //同步代碼塊
}

// 這個(gè)就是類鎖
public static synchronized void method(){
         //同步代碼塊
}

對(duì)象鎖和類鎖的區(qū)別,簡(jiǎn)單來說就是,類鎖方法怎么調(diào)用都是排斥的,而不同的對(duì)象調(diào)用同一個(gè)對(duì)象鎖方法是不互斥的,不同對(duì)象間沒有任何關(guān)系。如果不同線程,調(diào)用一個(gè)對(duì)象的對(duì)象鎖方法,那么就會(huì)互斥。具體的可以看透徹理解 Java synchronized 對(duì)象鎖和類鎖的區(qū)別,使用了synchronized非常簡(jiǎn)單。

在synchronized 對(duì)象鎖同步代碼塊中,就意味著已經(jīng)獲得了該對(duì)象鎖了,這對(duì)下面的wait()和notifyAll()方法也有用。wait()和notifyAll()方法是Object類的,屬于final不能被修改。需要和synchronized配合使用。

將代碼改成如下就可以了,如果沒有加入synchronized就調(diào)用wait()是會(huì)拋異常的

    // 轉(zhuǎn)賬操作
    public synchronized void transfers(int from, int to, int money) {

        try {
            while (accounts.get(from).money < money) {
                wait();
            }
            accounts.get(from).money -= money;
            accounts.get(to).money += money;
            System.out.printf(Thread.currentThread().getName() + "Bank總共money = %d  \n", getTotalMoney());
            notifyAll();
        } catch (InterruptedException e) {
            // TODO Auto-generated catch block
            e.printStackTrace();
        }

    }

當(dāng)線程執(zhí)行wait()方法時(shí)候,會(huì)釋放當(dāng)前的鎖,然后讓出CPU,進(jìn)入等待狀態(tài)。只有當(dāng) notify/notifyAll() 被執(zhí)行時(shí)候,才會(huì)喚醒一個(gè)或多個(gè)正處于等待狀態(tài)的線程,然后繼續(xù)往下執(zhí)行,直到執(zhí)行完synchronized 代碼塊的代碼或是中途遇到wait() ,再次釋放鎖。

3.2、同步阻塞

格式如下:是對(duì)該obj對(duì)象加入對(duì)象鎖

synchronized  (obj){
    ... 同步代碼塊
}

四、volatile域用法(可見性無原子性)

有了鎖機(jī)制,為什么又有了volatile了,難道volatile有什么更優(yōu)的地方。無論是synchronized 還是 ReentrantLock都是比較重量級(jí)的,有時(shí)只是一個(gè)變量的同步問題,所有java引入了更為精簡(jiǎn)的volatile修飾。

volatile是修飾變量,當(dāng)一個(gè)變量被volatile修飾后會(huì)有以下功能:

  • 保證了不同線程對(duì)這個(gè)變量進(jìn)行操作時(shí)的可見性,即一個(gè)線程修改了某個(gè)變量的值,對(duì)其他線程來說是立即可見的。造成不一致的原因在于,電腦是有高速緩存和內(nèi)存的。如果這兩個(gè)內(nèi)存中的數(shù)據(jù)不一致,就會(huì)造成錯(cuò)誤。如果加入volatile后,就會(huì)強(qiáng)制將修改的值立即寫入到內(nèi)存中。
  • 禁止進(jìn)行指令重排序。CPU會(huì)優(yōu)化指令,以此增加速度。加入volatile之后的變量,不會(huì)采用優(yōu)化策略。volatile前面的指令全部執(zhí)行完才能執(zhí)行volatile的代碼,同樣volatile代碼沒執(zhí)行完成,不能開始后面的指令執(zhí)行。

五、AtomicInteger

先看下下面這個(gè)例子:

public class Test {
     public  int  num = 0;
     
        public void increase() {
            num++;
        }
         
        public static void main(String[] args) {
            final Test test = new Test();
            for(int i=0;i<10;i++){
                new Thread(){
                    public void run() {
                        for(int j=0;j<100;j++)
                            test.increase();
                    };
                }.start();
            }
             
            while(Thread.activeCount()>1)  //保證前面的線程都執(zhí)行完
                Thread.yield();
            System.out.println(test.num);
        }
}

結(jié)果不意外的是小于1000,我這個(gè)運(yùn)行結(jié)果是9191。這是因?yàn)閚um++這個(gè)操作不是原子性的,所以這會(huì)導(dǎo)致操作是小于1000,若加入volatile修飾結(jié)果也是一樣,volatile不能保證操作的原子性,只能讓多線程的正確結(jié)果可見。
AtomicInteger就是這個(gè)int原子性操作問題的。得到的結(jié)果才是期望的1000,簡(jiǎn)單用法如下:

public class Test {
     public  AtomicInteger  num = new AtomicInteger(0);
     
        public void increase() {
            num.getAndIncrement();
        }
         
        public static void main(String[] args) {
            final Test test = new Test();
            for(int i=0;i<10;i++){
                new Thread(){
                    public void run() {
                        for(int j=0;j<1000;j++)
                            test.increase();
                    };
                }.start();
            }
             
            while(Thread.activeCount()>1)  //保證前面的線程都執(zhí)行完
                Thread.yield();
            System.out.println(test.num);
        }
}

六、讀寫鎖

摘自《java核心技術(shù)卷一》第663頁ReentrantReadWriteLock讀寫鎖描述。

  • 1、首先構(gòu)造一個(gè)讀寫鎖
        ReentrantReadWriteLock rwl = new ReentrantReadWriteLock();
        
        Lock readLock = rwl.readLock();
        Lock writeLock = rwl.writeLock();
  • 2、讀數(shù)據(jù)加鎖操作
    public double getTotalBalance() {
        readLock.lock();
        
        try {
            
        } finally {
            readLock.unlock();
        }
    }
  • 3、寫數(shù)據(jù)加鎖操作
    public void transfer() {
        writeLock.lock();
        
        try {
            
        } finally {
            writeLock.unlock();
        }
    }

小結(jié):如果多線程中,大量的會(huì)用到數(shù)據(jù)的讀取工作,只有少量的寫數(shù)據(jù)操作,這個(gè)時(shí)候可以考慮采用讀寫鎖分離控制。

七、同步器

  • 1、CountDownLatch(倒計(jì)時(shí)門栓)
    讓一個(gè)線程集等待,直到計(jì)數(shù)變成0。await()之后的線程才停止阻塞。一但計(jì)數(shù)變成0之后,就不能再次利用了。
public static void main(String[] args) {
        final int count = 10; // 計(jì)數(shù)次數(shù)  
        final CountDownLatch latch = new CountDownLatch(10);  
        for (int i = 0; i < count; i++) {  
            new Thread(new Runnable() {  
                @Override  
                public void run() {  
                    try {  
                        // do anything  
                        System.out.println("線程"  
                                + Thread.currentThread().getName());  
                    } catch (Throwable e) {  
                        // whatever  
                    } finally {  
                        // 很關(guān)鍵, 無論上面程序是否異常必須執(zhí)行countDown,否則await無法釋放  
                        latch.countDown();  
                    }  
                }  
            }).start();  
        }  
        try {  
            // 10個(gè)線程countDown()都執(zhí)行之后才會(huì)釋放當(dāng)前線程,程序才能繼續(xù)往后執(zhí)行  
            latch.await();  
        } catch (InterruptedException e) {  
            
        }  
        System.out.println("main thread Finish");  

    }
結(jié)果:
線程Thread-2
線程Thread-1
線程Thread-0
線程Thread-3
線程Thread-4
線程Thread-5
線程Thread-6
線程Thread-7
線程Thread-8
線程Thread-9
main thread Finish

等到前面的全部執(zhí)行完才會(huì)放行。
  • 2、CyclicBarrier (障柵)
    大量線程運(yùn)行在一次計(jì)算的不同部分的情形,當(dāng)所有的部分都準(zhǔn)備好了,需要把結(jié)果組合在一起。當(dāng)一個(gè)線程完成他的那部分任務(wù)后,就讓他運(yùn)行到障柵處。
    CountDownLatch的計(jì)數(shù)器只能使用一次。而CyclicBarrier的計(jì)數(shù)器可以使用reset() 方法重置。所以CyclicBarrier能處理更為復(fù)雜的業(yè)務(wù)場(chǎng)景。
public static void main(String[] args) {
        
        final CyclicBarrier c = new CyclicBarrier(2);
        new Thread(new Runnable() {

            @Override
            public void run() {
                try {
                    System.out.println(Thread.currentThread().getName()+" start");
                    Thread.sleep(1000);
                    c.await();
                } catch (Exception e) {

                }
                System.out.println(Thread.currentThread().getName()+" Finish");
            }
        }).start();

        try {
            System.out.println(Thread.currentThread().getName()+" start");
            Thread.sleep(1000);
            c.await();
            System.out.println(Thread.currentThread().getName()+" Finish");
        } catch (Exception e) {

        }
    }

一種結(jié)果為:
Thread-0 start
main start
Thread-0 Finish
main Finish
設(shè)置攔截兩個(gè)數(shù)量的障柵,等到兩個(gè)線程都執(zhí)行到await()之前,才允許后續(xù)執(zhí)行。
  • 3、semaphore (信號(hào)量)
    通常是用來限制訪問資源的總數(shù)
public class SemaphoreTest {

    final Semaphore semaphore = new Semaphore(1);
    
    public void start() {
        try {
            semaphore.acquire(1);
            System.out.println(Thread.currentThread().getName() + " start ");
            Thread.sleep(1000);
            System.out.println(Thread.currentThread().getName() + " finash ");
        } catch (InterruptedException e) {
            e.printStackTrace();
        } finally {
            semaphore.release();
        }
    }
    
    public static void main(String[] args) {
        SemaphoreTest test = new SemaphoreTest();
        for(int i=0;i<5;i++) {
            new Thread(new Runnable() {
                
                @Override
                public void run() {
                    test.start();
                }
            }).start();
        }
        
    }

}

因?yàn)槭敲看沃荒茉试S一個(gè)線程訪問臨界資源,所以結(jié)果也是線性執(zhí)行的:
Thread-0 start
Thread-0 finash
Thread-2 start
Thread-2 finash
Thread-1 start
Thread-1 finash
Thread-3 start
Thread-3 finash
Thread-4 start
Thread-4 finash

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

相關(guān)閱讀更多精彩內(nèi)容

  • 本文是我自己在秋招復(fù)習(xí)時(shí)的讀書筆記,整理的知識(shí)點(diǎn),也是為了防止忘記,尊重勞動(dòng)成果,轉(zhuǎn)載注明出處哦!如果你也喜歡,那...
    波波波先森閱讀 11,606評(píng)論 4 56
  • Java8張圖 11、字符串不變性 12、equals()方法、hashCode()方法的區(qū)別 13、...
    Miley_MOJIE閱讀 3,896評(píng)論 0 11
  • 2016年一去不復(fù)返,很多人都沉浸在17年的喜悅氣氛中,各種總結(jié)和計(jì)劃不斷出爐,小糊涂蟲一路看下來,發(fā)現(xiàn)很少有對(duì)自...
    e8d83e9aa398閱讀 765評(píng)論 21 6
  • 第一次嘗試,一個(gè)下午的成果! 一位老師說還沒有完全達(dá)到效果,可是我已經(jīng)很滿意了!嗯! 就是這么容易滿足!
    鹿精靈閱讀 327評(píng)論 3 4
  • 今天是5.12護(hù)士節(jié),祝姐妹們節(jié)日快樂!明天是母親節(jié),祝愿所有的母親節(jié)日快樂! 可能是周末的關(guān)系,...
    劉學(xué)穎媽媽閱讀 136評(píng)論 0 0

友情鏈接更多精彩內(nèi)容