java基礎(8)-----Lock and synchronized

     synchronized可以實現同步,爲什麼我們還需要Lock呢?jdk5之後增加了一個新的包java.util.concurrent,java在這裏提供了新的併發編程的工具,其中下面的Locks包都是關於Lock這一部分的,如圖:

1.和synchronized比較

     synchronized可以修飾變量、代碼塊、方法,對象中有這個關鍵字的代碼被訪問的時候,線程就可以獲取到此對象的鎖,注意是整個對象的鎖,而不是某個方法或者代碼塊,其他線程再次訪問這個對象的任何一處被synchronized修飾的代碼時都會被阻塞,當然訪問不被synchronized修飾的方法仍然可以訪問,下面是測試代碼:

/**
 * @author zisong yue
 * @date 2018-11-22
 * @description synchronized測試
 */
public class SyncronizedTest {

    synchronized void synA(String threadName){
        System.out.println(threadName + "============excute synA method============");
        try {
            Thread.sleep(5000);
            //wait(5000);
            System.out.println(threadName + "============sleep 5 seconds end============");
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }

    synchronized void synB(String threadName){
        System.out.println(threadName + "============excute synB method============");
    }

    public static void main(String[] args) {

        SyncronizedTest syncronizedTest = new SyncronizedTest();

        new Thread(new Runnable() {
            @Override
            public void run() {
                syncronizedTest.synA(Thread.currentThread().getName());
            }
        }).start();

        new Thread(new Runnable() {
            @Override
            public void run() {
                syncronizedTest.synB(Thread.currentThread().getName());
            }
        }).start();
    }
}

測試結果:

     值得注意的是,Thread.sleep()方法並不會使當前線程釋放鎖,我們看下Thread類對這個方法的介紹就明白了,其中有一段這樣寫的:The thread does not lose ownership of any monitors.翻譯過來就是,當前線程不會失去任何監視器的所有權,這裏的監視器的所有權其實就是我們通常說的鎖,是不是很有意思,哈哈

    /**
     * Causes the currently executing thread to sleep (temporarily cease
     * execution) for the specified number of milliseconds, subject to
     * the precision and accuracy of system timers and schedulers. The thread
     * does not lose ownership of any monitors.
     *
     * @param  millis
     *         the length of time to sleep in milliseconds
     *
     * @throws  IllegalArgumentException
     *          if the value of {@code millis} is negative
     *
     * @throws  InterruptedException
     *          if any thread has interrupted the current thread. The
     *          <i>interrupted status</i> of the current thread is
     *          cleared when this exception is thrown.
     */
    public static native void sleep(long millis) throws InterruptedException;

如果這裏的Thread.sleep改成了Object對象的wait()方法呢,又會是什麼樣子

/**
 * @author zisong yue
 * @date 2018-11-22
 * @description synchronized測試
 */
public class SyncronizedTest {

    synchronized void synA(String threadName){
        System.out.println(threadName + "============excute synA method============");
        try {
            //Thread.sleep(5000);
            wait(5000);
            System.out.println(threadName + "============sleep 5 seconds end============");
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }

    synchronized void synB(String threadName){
        System.out.println(threadName + "============excute synB method============");
    }

    public static void main(String[] args) {

        SyncronizedTest syncronizedTest = new SyncronizedTest();

        new Thread(new Runnable() {
            @Override
            public void run() {
                syncronizedTest.synA(Thread.currentThread().getName());
            }
        }).start();

        new Thread(new Runnable() {
            @Override
            public void run() {
                syncronizedTest.synB(Thread.currentThread().getName());
            }
        }).start();
    }
}

測試結果:

我們發現,線程B在線程A沒有執行完時,居然獲得了鎖,好的,我們看下Object的wait()方法

/**
     * Causes the current thread to wait until another thread invokes the
     * {@link java.lang.Object#notify()} method or the
     * {@link java.lang.Object#notifyAll()} method for this object.
     * In other words, this method behaves exactly as if it simply
     * performs the call {@code wait(0)}.
     * <p>
     * The current thread must own this object's monitor. The thread
     * releases ownership of this monitor and waits until another thread
     * notifies threads waiting on this object's monitor to wake up
     * either through a call to the {@code notify} method or the
     * {@code notifyAll} method. The thread then waits until it can
     * re-obtain ownership of the monitor and resumes execution.
     * <p>
     * As in the one argument version, interrupts and spurious wakeups are
     * possible, and this method should always be used in a loop:
     * <pre>
     *     synchronized (obj) {
     *         while (&lt;condition does not hold&gt;)
     *             obj.wait();
     *         ... // Perform action appropriate to condition
     *     }
     * </pre>
     * This method should only be called by a thread that is the owner
     * of this object's monitor. See the {@code notify} method for a
     * description of the ways in which a thread can become the owner of
     * a monitor.
     *
     * @throws  IllegalMonitorStateException  if the current thread is not
     *               the owner of the object's monitor.
     * @throws  InterruptedException if any thread interrupted the
     *             current thread before or while the current thread
     *             was waiting for a notification.  The <i>interrupted
     *             status</i> of the current thread is cleared when
     *             this exception is thrown.
     * @see        java.lang.Object#notify()
     * @see        java.lang.Object#notifyAll()
     */
    public final void wait() throws InterruptedException {
        wait(0);
    }

裏面註釋有一段說明:The current thread must own this object's monitor. The thread releases ownership of this monitor and waits until another thread notifies threads waiting on this object's monitor to wake up either through a call to the {@code notify} method or the {@code notifyAll} method 翻譯過來就是,當前線程必須擁有對象的監視器(也就是需要配合同步鎖使用)。線程會釋放監視器的所有權並且一直等待,直到另一個線程通過執行notify或者notifyAll方法重新喚醒

我們再看下面一個例子:

/**
 * @author zisong yue
 * @date 2018-11-22
 * @description synchronized測試
 */
public class SyncronizedTest {

    private Object locker = new Object();

    synchronized void synA(String threadName){
        System.out.println(threadName + "============excute synA method============");
        try {
            //Thread.sleep(5000);
            wait(5000);
            System.out.println(threadName + "============sleep 5 seconds end============");
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }

    synchronized void synB(String threadName){
        System.out.println(threadName + "============excute synB method============");
    }

    void synC(String threadName){
        synchronized (locker){
            System.out.println(threadName + "============excute synC method============");
        }

    }

    public static void main(String[] args) {

        final SyncronizedTest syncronizedTest = new SyncronizedTest();

        new Thread(new Runnable() {
            @Override
            public void run() {
                syncronizedTest.synA(Thread.currentThread().getName());
            }
        }).start();

        new Thread(new Runnable() {
            @Override
            public void run() {
                syncronizedTest.synC(Thread.currentThread().getName());
            }
        }).start();
    }
}

測試結果:

可以發現,locker是另外一個對象,因此訪問此synchronized內的代碼塊是不需要獲取SynchronizedTest對象的鎖的。這種也常被用來設計線程互斥的實現。

扯遠了.......我們接着說synchronized

當其他線層訪問被synchronized修飾的代碼時,被阻塞,如果之前獲取到鎖的線程在執行一個非常耗時的操作,那麼其他線程就只能一直等待下去,這會非常耗費資源。如果只阻塞一段時間或者可以響應中斷,那麼就可以在阻塞超時或者中斷之後,線程可以接着執行下面的代碼,Lock這裏就可以辦到了。注意synchronized的獲取鎖和釋放鎖完全由jvm完成,Lock需要手動獲取鎖和釋放鎖,因此釋放鎖必須要放在finally{}代碼塊中

2.java.util.concurrent.locks包下面的類,源碼分析,結合實例理解應用場景

2.1lock

lock是一個接口,有以下幾個方法,上源碼

public interface Lock {

    /**
     * Acquires the lock.
     * <p>If the lock is not available then the current thread becomes
     * disabled for thread scheduling purposes and lies dormant until the
     * lock has been acquired.
     */
    void lock();

    /**
     * Acquires the lock unless the current thread is
     * {@linkplain Thread#interrupt interrupted}.
     *
     * <p>Acquires the lock if it is available and returns immediately.
     *
     * <p>If the lock is not available then the current thread becomes
     * disabled for thread scheduling purposes and lies dormant until
     * one of two things happens:
     *
     * <ul>
     * <li>The lock is acquired by the current thread; or
     * <li>Some other thread {@linkplain Thread#interrupt interrupts} the
     * current thread, and interruption of lock acquisition is supported.
     * </ul>
     */
    void lockInterruptibly() throws InterruptedException;

    /**
     * Acquires the lock only if it is free at the time of invocation.
     *
     * <p>Acquires the lock if it is available and returns immediately
     * with the value {@code true}.
     * If the lock is not available then this method will return
     * immediately with the value {@code false}.
     */
    boolean tryLock();

    /**
     * Acquires the lock if it is free within the given waiting time and the
     * current thread has not been {@linkplain Thread#interrupt interrupted}.
     *
     * <p>If the lock is available this method returns immediately
     * with the value {@code true}.
     * If the lock is not available then
     * the current thread becomes disabled for thread scheduling
     * purposes and lies dormant until one of three things happens:
     * <ul>
     * <li>The lock is acquired by the current thread; or
     * <li>Some other thread {@linkplain Thread#interrupt interrupts} the
     * current thread, and interruption of lock acquisition is supported; or
     * <li>The specified waiting time elapses
     */
    boolean tryLock(long time, TimeUnit unit) throws InterruptedException;

    /**
     * Releases the lock.
     */
    void unlock();

    /**
     * Returns a new {@link Condition} instance that is bound to this
     * {@code Lock} instance.
     */
    Condition newCondition();
}

void lock()方法:如果鎖可用,那麼獲取到鎖,如果所有不可用,則進入僞休眠狀態直到獲取到鎖,繼續執行下面程序

void lockInterruptibly():如果鎖可用,那麼獲取到鎖,如果所有不可用,則進入僞休眠狀態直到獲取到鎖或者被其他線程中斷(執行Thread.interrupt()方法),繼續執行下面程序

boolean trylock():如果鎖可用,那麼獲取到鎖並且立即返回true,如果所有不可用,則立即返回false

boolean trylock(long time, TimeUnit unit):如果鎖可用,那麼獲取到鎖並且立即返回true,如果所有不可用,則進入僞休眠狀態直到獲取到鎖或者被其他線程中斷(拋出中斷異常InterruptedException,這個時候可以catch異常並處理異常的邏輯,之後執行下面的程序)或者超出了指定的時間(返回false)

void unlock():釋放鎖

Condition newConditon():返回一個綁定到lock實例上的condition實例

如果是synchronized修飾的代碼,阻塞時不能被中斷的,這裏Lock提供的lockInterruptibly()可以響應中斷,tryLock(long time, TimeUnit unit)可以在等待一段時間之後返回false,也相當與中斷了

2.2ReenTrantLock可重入鎖

怎麼理解可重入鎖呢,我們看到上面提到的源碼註釋,都是在說明線程是否獲得對象監視器的擁有權,很顯然,這裏的對象鎖(即對象監視器的所有權)是以線程爲單位的。所以,如果一個線程獲取到了對象鎖,那麼這個對象內的任何一處需要獲取鎖才能執行的代碼塊,都可以執行。重入鎖測試:

import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;

/**
 * @author zisong yue
 * @date 2018-11-22
 * @description ReenTrantLock測試
 */
public class SyncronizedTest {
    final Lock reentrantLock = new ReentrantLock();

    void synA(String threadName){
        reentrantLock.lock();
        System.out.println(threadName + "============excute synA method============");
        this.synB(threadName);
        try {
            Thread.sleep(5000);
            //wait(5000);
            System.out.println(threadName + "============sleep 5 seconds end============");
        } catch (InterruptedException e) {
            e.printStackTrace();
        }finally {
            reentrantLock.unlock();
        }
    }

    void synB(String threadName){
        reentrantLock.lock();
        System.out.println(threadName + "============excute synB method============");
        reentrantLock.unlock();
    }

    public static void main(String[] args) {

        SyncronizedTest syncronizedTest = new SyncronizedTest();

        new Thread(new Runnable() {
            @Override
            public void run() {
                syncronizedTest.synA(Thread.currentThread().getName());
            }
        }).start();
    }
}

測試結果:

關聯condition使用場景

     java官方網站給出一個用condition實現bounded-buffer(有界緩衝區)例子,其實這也是PriorityBlokingQueue(可控制優先級的阻塞隊列)的實現原理,我們執行復制過來:

class BoundedBuffer {
   final Lock lock = new ReentrantLock();
   final Condition notFull  = lock.newCondition(); 
   final Condition notEmpty = lock.newCondition(); 

   final Object[] items = new Object[100];
   int putptr, takeptr, count;

   public void put(Object x) throws InterruptedException {
     lock.lock();
     try {
       while (count == items.length)
         notFull.await();
       items[putptr] = x;
       if (++putptr == items.length) putptr = 0;
       ++count;
       notEmpty.signal();
     } finally {
       lock.unlock();
     }
   }

   public Object take() throws InterruptedException {
     lock.lock();
     try {
       while (count == 0)
         notEmpty.await();
       Object x = items[takeptr];
       if (++takeptr == items.length) takeptr = 0;
       --count;
       notFull.signal();
       return x;
     } finally {
       lock.unlock();
     }
   }
 }

使用了兩個condition,如果緩衝數組滿了,所有的寫線程都會被阻塞,這時有一個讀線程衝數組中讀走了一個數據,通過notFull.sinal()方法喚醒隊列中的一個寫線程開始寫數據。如果緩衝數組空了,所有的讀線程會被阻塞,這時有一個寫線程向數組中寫入一個數據,notEmpty.singal()方法喚醒隊列中的一個讀線程開始讀數據。

還有一個典型的應用,多線程的時序控制。

import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;

public class Main {
    static class NumberWrapper {
        public int value = 1;
    }

    public static void main(String[] args)  {
        //初始化可重入鎖
        final Lock lock = new ReentrantLock();

        //第一個條件當屏幕上輸出到3
        final Condition reachThreeCondition = lock.newCondition();
        //第二個條件當屏幕上輸出到6
        final Condition reachSixCondition = lock.newCondition();

        //NumberWrapper只是爲了封裝一個數字,一邊可以將數字對象共享,並可以設置爲final
        //注意這裏不要用Integer, Integer 是不可變對象
        final NumberWrapper num = new NumberWrapper();
        //初始化A線程
        Thread threadA = new Thread(new Runnable() {
            @Override
            public void run() {
                //需要先獲得鎖
                lock.lock();
                try {
                    System.out.println("threadA start write");
                    //A線程先輸出前3個數
                    while (num.value <= 3) {
                        System.out.println(num.value);
                        num.value++;
                    }
                    //輸出到3時要signal,告訴B線程可以開始了
                    reachThreeCondition.signal();
                } finally {
                    lock.unlock();
                }
                lock.lock();
                try {
                    //等待輸出6的條件
                    reachSixCondition.await();
                    System.out.println("threadA start write");
                    //輸出剩餘數字
                    while (num.value <= 9) {
                        System.out.println(num.value);
                        num.value++;
                    }

                } catch (InterruptedException e) {
                    e.printStackTrace();
                } finally {
                    lock.unlock();
                }
            }

        });


        Thread threadB = new Thread(new Runnable() {
            @Override
            public void run() {
                try {
                    lock.lock();

                    while (num.value <= 3) {
                        //等待3輸出完畢的信號
                        reachThreeCondition.await();
                    }
                } catch (InterruptedException e) {
                    e.printStackTrace();
                } finally {
                    lock.unlock();
                }
                try {
                    lock.lock();
                    //已經收到信號,開始輸出4,5,6
                    System.out.println("threadB start write");
                    while (num.value <= 6) {
                        System.out.println(num.value);
                        num.value++;
                    }
                    //4,5,6輸出完畢,告訴A線程6輸出完了
                    reachSixCondition.signal();
                } finally {
                    lock.unlock();
                }
            }

        });

        //啓動兩個線程
        threadB.start();
        threadA.start();
    }
}

這個例子是從別人那裏複製過來的,看的時候你可能會有一個和我一樣的疑問,threadB執行lock.lock()方法獲取到鎖,之後調用了reachThreeCondition.await()方法阻塞,後面不會再執行lock.unlock()方法釋放鎖,threadA在執行lock.lock()是怎麼獲取到鎖的呢。我們看下await()方法做了什麼,還是上源碼:

 /**
     * Causes the current thread to wait until it is signalled or
     * {@linkplain Thread#interrupt interrupted}.
     *
     * <p>The lock associated with this {@code Condition} is atomically
     * released and the current thread becomes disabled for thread scheduling
     * purposes and lies dormant until <em>one</em> of four things happens:
     * <ul>
     * <li>Some other thread invokes the {@link #signal} method for this
     * {@code Condition} and the current thread happens to be chosen as the
     * thread to be awakened; or
     * <li>Some other thread invokes the {@link #signalAll} method for this
     * {@code Condition}; or
     * <li>Some other thread {@linkplain Thread#interrupt interrupts} the
     * current thread, and interruption of thread suspension is supported; or
     * <li>A &quot;<em>spurious wakeup</em>&quot; occurs.
     * </ul>
     */
    void await() throws InterruptedException;
The lock associated with this {@code Condition} is atomically released and the current thread becomes disabled for thread scheduling purposes and lies dormant until <em>one</em> of four things happens 翻譯過來就是:和這個condition關聯的鎖會被原子的釋放,並且當前線程進入休眠狀態直到一下四件事中一件發生,那四件事就不翻譯了,自己可以看看。

現在我們看到了它相對於Object 的wait()方法的強大之處,對線程有了更加精細的控制,通過condition方法await阻塞一類線程,sigal方法喚醒指定一類線程,而Object的notify方法無法做到,比如官網的例子,它無法分別喚醒的是讀線程還是寫線程

這個包下面還有幾個類ReenTrantReadWriteLock、StampedLock,以後再補充吧,有不完善的地方,歡迎評論指正

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