1. 程式人生 > >java.util.concurrent.locks與synchronized及其異同

java.util.concurrent.locks與synchronized及其異同

關鍵字:synchronized、java.util.concurrent.locks.Lock、同步、併發、鎖

一、【引言】

JDK1.5之前,實現同步主要是使用synchronized,而在JDK1.5中新增了java.util.concurrent包及其兩個子包locks和atomic,其中子包locks中定義了系列關於鎖的抽象的類。本文主要介紹java.util.concurrent.locks的使用及其與synchronized兩種方式實現同步的異同。

二、【synchronized同步】

synchronized相信很多熟悉J2SE的人都不會對這個關鍵字陌生,它用於實現多個執行緒之間的同步,一般有兩種使用方式:
1、在方法上加synchronized關鍵字
public synchronized void f() {
//do something
}
2、synchronized同步程式碼塊
synchronized (mutex) {
// do something
}
對於這兩種方式又應該著重區分是否為“static”的,因為static的成員是屬於類的,非staitc的是屬於具體例項的,所以在使用synchronized時應該注意方法或選擇的同步變數是否為static的,如下程式碼:
package test.lock;

/**
 * @author whwang
 * 2012-1-10 下午11:19:04
 */
public class SyncTest {
    
    private Object mutex = new Object();
    
    public synchronized void f1() {
        synchronized (mutex) {
            System.err.println("nonstatic method f1....");
            try {
                Thread.sleep(2 * 1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
    
    public static void main(String[] args) {
        SycnThread thread1 = new SycnThread(new SyncTest());
        SycnThread thread2 = new SycnThread(new SyncTest());
        SycnThread thread3 = new SycnThread(new SyncTest());
        thread1.start();
        thread2.start();
        thread3.start();
    }
    
}

class SycnThread extends Thread {
    
    private SyncTest st;
    
    public SycnThread(SyncTest st) {
        this.st = st;
    }
    
    @Override
    public void run() {
        while (true) {
            st.f1();
        }
    }
}

在main方法,建立thread1、2、3三個執行緒,它們都呼叫SyncTest的f1()方法,而方法f1()使用mutex(非static)來做同步變數,如果你的意圖是實現這3個執行緒對方法f1的同步,那麼執行的結果會讓你大失所望,因為這樣根本就無法使得這3個執行緒同步,原因在於:mutex是一個非static的成員變數,也就是說每new SyncTest(),它們的mutex變數都是不相同的。這樣,對於上面這個程式來說,意味著每一個執行緒都使用了一個mutex來做它們各自的不同變數,如果希望上面3個執行緒同步,可以把mutex改為static或在建立SycnThread時,傳入的SyncTest都為同一個物件即可。
還有當使用String常量或全域性變數時都應該引起注意,
Java執行緒同步小陷阱,你掉進去過嗎?

三、【java.util.concurrent.locks下的鎖實現同步】

自JDK1.5以為,Java提供了java.util.concurrent這個併發包,在它的子包locks中,提供了一系列關於鎖的抽象的類。主要有兩種鎖ReentrantLockReentrantReadWriteLock,而其他的兩個類,都是“輔助”類,如AbstractQueuedSynchronizer就是一個用於實現特殊規則鎖的抽象類,ReentrantLock和ReentrantReadWriteLock內部都有一個繼承了該抽象類的內部類,用於實現特定鎖的功能。下文主要介紹:ReentrantLock和ReentrantReadWriteLock

1、可重入的鎖ReentrantLock

使用ReentrantLock鎖最簡單的一個例子:
Lock lock = new ReentrantLock();
try {
    lock.lcok();
    // do something
} finally {
    lock.unlock();
}

上面這段程式碼,首先建立了一個lock,然後呼叫它的lock()方法,開啟鎖定,在最後呼叫它的unlock()解除鎖定。值得注意的時,一般在使用鎖時,都應該按上面的風格書寫程式碼,即lock.unlock()最好放在finally塊,這樣可以防止,執行do something時發生異常後,導致鎖永遠無法被釋放。

到此,還沒發現Lock與synchronized有什麼不同,Lock與synchronized不同之處主要體現在Lock比synchronized更靈活得多,而這些靈活又主要體現在如下的幾個方法:

//lock()
tryLock()
tryLock(long timeout, TimeUnit timeUnit)
lockInterruptibly()

//unlock()

A、trylock()方法:如果獲取了鎖立即返回true,如果別的執行緒正持有鎖,立即返回false;
B、tryLock(long timeout, TimeUnit timeUnit)方法:如果獲取了鎖定立即返回true,如果別的執行緒正持有鎖,會等待引數給定的時間,在等待的過程中,如果獲取了鎖定,就返回true,如果等待超時,返回false;
是不是比synchronized靈活就體現出來了,打個不是很恰當的比分:你現在正在忙於工作,突然感覺內急,於是你跑向洗手間,到門口發現一個“清潔中,暫停使用”的牌牌。沒辦法,工作又忙,所以你只好先放棄去洗手間回去忙工作,可能如此反覆,終於你發現可以進了,於是......
像這樣的場景用synchronized你怎麼實現?沒辦法,如果synchronized,當你發現洗手間無法暫時無法進入時,就只能乖乖在門口乾等了。而使用trylock()呢,首先你試著去洗手間,發現暫時無法進入(trylock返回false),於是你繼續忙你的工作,如此反覆,直到可以進入洗手間為止(trylock返回true)。甚至,你非常急,你可以嘗試性的在門口等20秒,不行再去忙工作(trylock(20, TimeUnit.SECONDS);)。
C、lockInterruptibly()方法
lockInterruptibly()方法的執行如下:

如果該鎖定沒有被另一個執行緒保持,則獲取該鎖定並立即返回,將鎖定的保持計數設定為 1。
如果當前執行緒已經保持此鎖定,則將保持計數加 1,並且該方法立即返回。
如果鎖定被另一個執行緒保持,則出於執行緒排程目的,禁用當前執行緒,並且在發生以下兩種情況之一以前,該執行緒將一直處於休眠狀態:
    a、鎖定由當前執行緒獲得;
    b、或者其他某個執行緒中斷當前執行緒。
如果當前執行緒獲得該鎖定,則將鎖定保持計數設定為1。
如果當前執行緒:
    a、在進入此方法時已經設定了該執行緒的中斷狀態;
    b、或者在等待獲取鎖定的同時被中斷。
則丟擲 InterruptedException,並且清除當前執行緒的已中斷狀態。
即lockInterruptibly()方法允許在等待時由其它執行緒呼叫它的Thread.interrupt方法來中斷等待而直接返回,這時不再獲取鎖,而會丟擲一個InterruptedException。
D、lockInterruptibly()方法原始碼介紹
public void lockInterruptibly() throws InterruptedException {
    sync.acquireInterruptibly(1);
}

a、首先lockInterruptibly呼叫了內部類sync的acquireInterruptibly(1)方法,這個sync就是前面提到的AbstractQueuedSynchronizer的子類
abstract static class Sync extends AbstractQueuedSynchronizer {
    // ....
}
public final void acquireInterruptibly(int arg)
            throws InterruptedException {
    if (Thread.interrupted())
        throw new InterruptedException();
    if (!tryAcquire(arg))
        doAcquireInterruptibly(arg);
}

b、在sync的acquireInterruptibly方法中,首先檢查當前現場是否已經中斷,如果已經中斷,丟擲InterruptedException異常,否則呼叫呼叫sync的doAcquireInterruptibly方法。
private void doAcquireInterruptibly(int arg)
        throws InterruptedException {
        final Node node = addWaiter(Node.EXCLUSIVE);
        boolean failed = true;
        try {
            for (;;) {
                final Node p = node.predecessor();
                if (p == head && tryAcquire(arg)) {
                    setHead(node);
                    p.next = null; // help GC
                    failed = false;
                    return;
                }
        // 丟擲InterruptedException異常
                if (shouldParkAfterFailedAcquire(p, node) &&
                    parkAndCheckInterrupt())
                    throw new InterruptedException();
            }
        } finally {
            if (failed)
                cancelAcquire(node);
        }
    }

c、在sync的方法doAcquireInterruptibly中,關鍵在於檢測到中斷則直接退出迴圈(不在等待獲取鎖),而是直接丟擲InterruptedException異常,最後在finally裡呼叫cancelAcquire取消獲鎖操作。
E、除了這些方法之外,ReentrantLock還提供了很多實用的方法,這裡就不再一一講述
對於Lock,還有一個特別值得注意的地方,請看下面的程式碼:
Lock lock = new ReentrantLock();
// ....
try {
    lock.lock();
    lock.lock();
    // do something...
} finally {
    lock.unlock();
}
// ....

可以看到上面,上面程式碼呼叫lock()方法和呼叫unlock()方法的次數不同,這樣的一段程式碼執行完後,別的執行緒是否已經可以獲取該lock鎖呢?
package test.mult;

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

/**
 * @ClassName: Test
 * @author whwang
 * @date 2012-1-11 下午02:04:04
 */
public class Test {

    private String name;
    
    public Test(String name) {
        this.name = name;
    }
    
    public static void main(String[] args) {
        // 這樣建立一個"公平競爭"的鎖
        Lock lock = new ReentrantLock(true);
        MyThread t1 = new MyThread(lock, new Test("test1"));
        MyThread t2 = new MyThread(lock, new Test("test2"));
        t1.start();
        t2.start();
    }

    private static class MyThread extends Thread {
        Lock lock = null;
        Test test = null;

        public MyThread(Lock lock, Test test) {
            this.lock = lock;
            this.test = test;
        }

        @Override
        public void run() {
            while (true) {
                try {
                    // 呼叫兩次lock
                    lock.lock();
                    lock.lock();
                    System.err.println(test.name + " locked...");
                    try {
                        Thread.sleep(3 * 1000);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                } finally {
                    lock.unlock();
                }
            }
        }

    }
}

執行的結果:
test1 locked...
test1 locked...
test1 locked...
test1 locked...
test1 locked...
test1 locked...

永遠都是持有test1這個類的執行緒才能獲取鎖,其實是第一個獲取鎖的執行緒,他永遠都拿著鎖不放。
所以在使用Lock的時候,lock與unlock一定要配對

2、可重入的讀寫鎖ReentrantReadWriteLock

該鎖的用法與ReentrantLock基本一樣,只是ReentrantReadWriteLock實現了特殊規則(讀寫鎖),在ReentrantReadWriteLock中有兩個內部類ReentrantReadWriteLock.ReadLock和ReentrantReadWriteLock.WriteLock(實際上不止兩個內部類,還有實現AbstractQueuedSynchronizer的Sync等等),這兩個類分別可以使用ReentrantReadWriteLock的readLock()和writeLock()返回,該讀寫鎖的規則是:只要沒有writer,讀取鎖定可以由多個reader 執行緒同時保持,而寫入鎖定是獨佔的。下面通過一個簡單的例子來了解它:
package test.mult;

import java.util.concurrent.locks.ReentrantReadWriteLock;

/**
* @ClassName: ReadWriteLockTest
* @author whwang
* @date 2012-1-11 下午02:20:59
 */
public class ReadWriteLockTest {

    static ReentrantReadWriteLock lock = new ReentrantReadWriteLock(true);

    public static void main(String[] args) {
        // 是否可以進入多個reader - 可以

        // 是否可以進入多個writer - 不可以

        // 當有reader進入後, writer是否可以進入 - 不可以

        // 當有writer進入後, reader是否可以進入 - 不可以

        MyThread t1 = new MyThread(0, "t1");
        MyThread t2 = new MyThread(0, "t2");
        MyThread t3 = new MyThread(1, "t3");
        t1.start();
        t2.start();
        t3.start();
    }

    private static class MyThread extends Thread {

        private int type;

        private String threadName;

        public MyThread(int type, String threadName) {
            this.threadName = threadName;
            this.type = type;
        }

        @Override
        public void run() {
            while (true) {
                if (type == 0) {
                    // read
                    ReentrantReadWriteLock.ReadLock readLock = null;
                    try {
                        readLock = lock.readLock();
                        readLock.lock();
                        System.err.println("to read...." + threadName);
                        try {
                            Thread.sleep(5 * 1000);
                        } catch (InterruptedException e) {
                            e.printStackTrace();
                        }
                    } finally {
                        readLock.unlock();
                    }
                } else {
                    // write
                    ReentrantReadWriteLock.WriteLock writeLock = null;
                    try {
                        writeLock = lock.writeLock();
                        writeLock.lock();
                        System.err.println("to write...." + threadName);
                        try {
                            Thread.sleep(5 * 1000);
                        } catch (InterruptedException e) {
                            e.printStackTrace();
                        }
                    } finally {
                        writeLock.unlock();
                    }
                }
            }
        }
    }
}

3、AbstractQueuedSynchronizer:如果需要自己實現一些特殊規則的鎖,可以通過拓展該類來實現。

參考文件:
http://wenku.baidu.com/view/41480552f01dc281e53af090.html
http://tutorials.jenkov.com/java-concurrency/index.html