Java併發-定義鎖以及消費者-生產者模式實現

一、引子

 對於Java併發的鎖結構,我們常常使用的是synchonized結構,而由於其靈活度較低,所以在Java-5後提出了Lock接口,以及AbstractQueuedSynchronizer抽象類供我們方便且安全地來實現自定義鎖結構,下面從代碼出發來開始這篇文章的閱讀。


 本文就兩個實現方式來闡述“生產者-消費者模式”背景下的鎖應用,第一種方式是使用Lock接口的自定義實現類來實現,第二種方式是使用synchronized關鍵字來實現。願讀者在兩種不同的實現方式對比中發現各自使用的特點。

二、Lock接口實現

 需求:設計一個同步工具:該工具在同一時刻, 只允許至多兩個線程同時訪問, 超過兩個線程的訪問將被阻塞, 我們將這個同步工具命名爲TwinsLock。並且以生產者和消費者的角度來驗證此鎖是否成功編寫。

package concurrency_basic.chapter16_自定義同步組件;


import java.util.Collection;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.TimeoutException;
import java.util.concurrent.locks.AbstractQueuedSynchronizer;
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.Lock;


public class TwinsLock implements Lock {
   private final Sync sync = new Sync(2);

   private static final class Sync extends AbstractQueuedSynchronizer {
       Sync(int count) {
           if (count <= 0) {
               throw new IllegalArgumentException("count must large than zero.");
           }
           setState(count);
       }

       public int tryAcquireShared(int reduceCount) {
           for (; ; ) {
               int current = getState();
               int newCount = current - reduceCount;
               if (newCount < 0 || compareAndSetState(current,
                       newCount)) {

                   return newCount;
               }
           }
       }

       public boolean tryReleaseShared(int returnCount) {
           for (; ; ) {
               int current = getState();
               int newCount = current + returnCount;
               if (compareAndSetState(current, newCount)) {
                   return true;
               }
           }
       }
   }

   public void lock() {
       sync.acquireShared(1);
   }

   @Override
   public void lockInterruptibly() throws InterruptedException {

   }

   @Override
   public boolean tryLock() {
       return false;
   }

   @Override
   public boolean tryLock(long time, TimeUnit unit) throws InterruptedException {
       return false;
   }

   public void unlock() {
       sync.releaseShared(1);
   }

   @Override
   public Condition newCondition() {
       return null;
   }




//以下是TwinsLock類是否成功編寫的測試代碼
   public static void main(String[] args) {
           final Lock lock = new TwinsLock();
           class Worker extends Thread {
               public void run() {
                   
                       lock.lock();

                       try {
                           Thread.sleep(2000);
                           System.out.println(Thread.currentThread().getName());
                           Thread.sleep(100);
                           System.out.println();
                       } catch (InterruptedException e) {
                           e.printStackTrace();
                       } finally {
                           lock.unlock();
                       }


               }
           }
           // 啓動10個線程
           for (int i = 0; i < 10; i++) {
               Worker w = new Worker();
               w.start();
           }

       }
   }

控制檯輸出:

Thread-0
Thread-1


Thread-2
Thread-5


Thread-6
Thread-4


Thread-7
Thread-3


Thread-8
Thread-9

三、代碼分析

 首先,我們由控制檯輸出可見,我們的確成功地創建了一個最多支持兩個線程同時工作的共享鎖。
 但是如果要求讀者朋友不加基礎地直接理解以上代碼,恐怕對於部分人有所難度。所以我先介紹一下由jdk1.5之後提供的鎖設計模式,學了這個之後,理解代碼相對容易非常多了。下面我以ReentrantLock類作爲一個例子來說明自定義鎖的設計模式。
在這裏插入圖片描述
 我們先不管右下角的Condition接口。先看看其餘接口以及類在鎖構造過程中所起到的作用:

Lock接口:鎖是面向使用者的,它定義了使用者與鎖交互的接口(比如可以允許兩個線程並行訪問),隱藏了實現細節;
AQS抽象類:同步器面向的是鎖的實現者,它簡化了鎖的實現方式, 隱藏了同步狀態管理、 線程的排隊、 等待與喚醒等底層操作。

 所以上述代碼的邏輯是:首先我們寫一個靜態的內部類Sync,其需要繼承AQS抽象類。其主要功能是:重寫AQS類內部獲取資源方法:tryAcquireShared以及釋放資源的方法:tryReleaseShared,資源獲取和釋放中涉及了同步器狀態的變化,而狀態的變化需要調用AQS內部提供了CAS方法:compareAndSetState。而AQS中涉及線程排隊、休眠、喚醒等操作代碼我們並不需要實現,我們所需做的就是關於線程獲得到資源/釋放資源時,修改同步器的狀態,而這個狀態將決定線程是否被喚醒,是否將嘗試搶奪資源的鎖放入等待隊列並休眠。

        public int tryAcquireShared(int reduceCount) {
            for (; ; ) {
                int current = getState();
                int newCount = current - reduceCount;
                if (newCount < 0 || compareAndSetState(current,
                        newCount)) {
                    return newCount;
                }
            }
        }

        public boolean tryReleaseShared(int returnCount) {
            for (; ; ) {
                int current = getState();
                int newCount = current + returnCount;
                if (compareAndSetState(current, newCount)) {
                    return true;
                }
            }
        }

 而Lock方法的實現TwinsLock類的相關方法重寫,只需簡單地調用AQS抽象類實現:Sync靜態內部類對象的若干方法即可:

    public void lock() {
        sync.acquireShared(1);
    }
    
        public void unlock() {
        sync.releaseShared(1);
    }

 其中acquireShared(1)以及releaseShared(1)方法的入口參數值的大小可以認爲是對線程資源消耗程度的描述,在這個類中,我們可以認爲其都會消耗同步器中資源單位1(總共資源單位爲2),如果將值改爲2,相當於兩個資源會被一個線程鎖佔據,這樣一來,鎖就只允許只有一個線程佔據資源,進行執行了。

四、傳統方式實現生產者消費者模式


import java.util.Map;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.concurrent.locks.Lock;

/**
 * @author Fisherman
 */
public class TraditionalLock {

    private static final Object MONITOR = new Object();

    private AtomicInteger number = new AtomicInteger(2);


    public void lock() {
        synchronized (MONITOR) {
            while (number.get() <= 0) {
                try {
                    MONITOR.wait();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
            number.decrementAndGet();

        }

    }

    public void unlock() {
        synchronized (MONITOR) {
            number.incrementAndGet();
            MONITOR.notifyAll();
        }
    }


    public static void main(String[] args) {
        final TraditionalLock lock = new TraditionalLock();
        class Worker extends Thread {
            public void run() {

                lock.lock();

                try {
                    Thread.sleep(2000);
                    System.out.println(Thread.currentThread().getName());
                    Thread.sleep(100);
                    System.out.println();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                } finally {
                    lock.unlock();
                }


            }
        }
        // 啓動10個線程
        for (int i = 0; i < 10; i++) {
            Worker w = new Worker();
            w.start();
        }
    }

}


控制檯輸出:

Thread-1
Thread-0


Thread-8
Thread-9


Thread-2
Thread-7


Thread-4
Thread-3


Thread-5
Thread-6

 可見我們使用傳統方式也實現了限制線程運行數目爲2的生產者消費者模式,但是與Lock接口實現的鎖相比,顯然傳統的實現方式在lockunlock方法需要更多的細節。需要設置同步監視器,需要額外調用 wait/notifyAll方法,並且由於使用了synchronized進行上鎖,所以在資源消耗上比CAS實現的同步操作更加耗費內存資源。綜上所述,使用Lock接口實現的自定義鎖更加靈活、耗費更少的資源、對開發者更加友善。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章