併發-3-synchronized與ReentrantLock

Coding挖掘機發表於2018-10-17

執行緒安全問題:

在多執行緒中,有可能出現多個執行緒同時使用同一個資源的情況,這個資源可以是變數,資料表,txt檔案等。這個資源稱作"臨界資源"

舉個例子:取錢這個執行緒分為三個步驟:1.讀取金額、2.取款、3.更新金額

有個典型的執行緒安全的例子,倘若A,B兩人使用同一個賬戶(1000元)取款,執行順序如下:

1.A讀取金額,顯示1000

2.A取款300元

3.B讀取金額,顯示為1000(實際應該為700)

4.B取款1000元

5.A更新金額為700

6.B更新金額0

之所以賬戶中莫名地被多取了300,是因為A和B兩個執行緒使用了同一個臨界資源(賬戶)

注意:當多個執行緒執行同一個方法時,方法內部的變數不是臨界資源,因為每個執行緒都有自己獨立的記憶體區域(PC,方法棧,執行緒棧)   
複製程式碼

解決執行緒安全問題:

基本所有的併發方案,都採用“序列化訪問資源”,也就是在同一時間,只有一個執行緒能訪問臨界資源,也稱作同步互斥訪問

方案1:synchronized

在Java中,每個物件都有一個鎖標記,稱為monitor(監視器),多個執行緒訪問這個物件的臨界資源時,只有獲取了該物件的鎖才能訪問

在Java中,可以使用synchronized關鍵字來標記一個方法或者程式碼塊,當某個執行緒呼叫該物件的synchronized方法或者訪問synchronized程式碼塊時,這個執行緒便獲得了該物件的鎖,其他執行緒暫時無法訪問這個方法,只有等待這個方法執行完畢或者程式碼塊執行完畢,這個執行緒才會釋放該物件的鎖,其他執行緒才能執行這個方法或者程式碼塊。
複製程式碼
public class ThreadSynchronized2 {

    public static void main(String[] agrs) {
        InsertData insertData = new InsertData();

        new Thread("執行緒1") {
            @Override
            public void run() {
                insertData.insert(Thread.currentThread());
            }
        }.start();

        new Thread("執行緒2") {
            @Override
            public void run() {
                insertData.insert(Thread.currentThread());
            }
        }.start();
    }
}

class InsertData extends Thread {
    public synchronized void insert(Thread thread) {
        for (int i = 0; i < 5; i++) {
            System.out.println(thread.getName() + "插入: " + i);
        }
    }
}
複製程式碼

輸出:

執行緒1插入: 0
執行緒1插入: 1
執行緒1插入: 2
執行緒1插入: 3
執行緒1插入: 4
執行緒2插入: 0
執行緒2插入: 1
執行緒2插入: 2
執行緒2插入: 3
執行緒2插入: 4
複製程式碼

還可以改成以下兩種方式:

class InsertData extends Thread {
    public void insert(Thread thread) {
        synchronized (this) {
            for (int i = 0; i < 5; i++) {
                System.out.println(thread.getName() + "插入: " + i);
            }
        }
    }
}
複製程式碼
class InsertData extends Thread {
    private Object object = new Object();
    public void insert(Thread thread) {
        synchronized (object) {
            for (int i = 0; i < 5; i++) {
                System.out.println(thread.getName() + "插入: " + i);
            }
        }
    }
}
複製程式碼

說明:

    1)當一個執行緒正在訪問一個物件的synchronized方法,那麼其他執行緒不能訪問該物件的其他synchronized方法。這個原因很簡單,因為一個物件只有一把鎖,當一個執行緒獲取了該物件的鎖之後,其他執行緒無法獲取該物件的鎖,所以無法訪問該物件的其他synchronized方法。

    2)當一個執行緒正在訪問一個物件的synchronized方法,那麼其他執行緒能訪問該物件的非synchronized方法。這個原因很簡單,訪問非synchronized方法不需要獲得該物件的鎖,假如一個方法沒用synchronized關鍵字修飾,說明它不會使用到臨界資源,那麼其他執行緒是可以訪問這個方法的,

    3)如果一個執行緒A需要訪問物件object1的synchronized方法fun1,另外一個執行緒B需要訪問物件object2的synchronized方法fun1,即使object1和object2是同一型別的物件,也不會產生執行緒安全問題,因為他們訪問的是不同的物件,所以不存在互斥問題。
複製程式碼

注意事項:

static方法,使用的是類鎖(多個物件使用同一個類的monitor)

和非static方法,使用的是物件鎖(每個物件維護一個monitor)
複製程式碼

synchronized的優缺點:

1.使用synchronized包住的程式碼塊,只可能有兩種狀態:順利執行完畢釋放鎖、執行時發生異常釋放鎖,不會由於異常導致出現死鎖現象

2.如果synchronized包住的程式碼塊中有sleep等操作,比如I/O阻塞,但是其他執行緒還是需要等待,這樣程式的效率就比較低了

3.等待synchronized釋放鎖的執行緒會一直等待下去(死心塌地,不到黃河心不死)
複製程式碼

舉例場景:

對a.txt這個檔案,A,B執行緒都可以進行讀寫,寫操作與寫操作會發生衝突,寫操作與讀操作會發生衝突,但是,讀操作與讀操作不會發生衝突

這個時候synchronized滿足不了需求,就需要用高階的鎖
複製程式碼

鎖的介紹:

先看JDK中對於Lock的定義:

public interface Lock {

    void lock();

    void lockInterruptibly() throws InterruptedException;

    boolean tryLock();

    boolean tryLock(long time, TimeUnit unit) throws InterruptedException;

    void unlock();

    Condition newCondition();

}
複製程式碼
通過Lock的原始碼可以知道,lock(),tryLock(),tryLock(long time,Time Unit),lockInterruptiably()是用來獲取鎖的

lock()方法是平常使用得最多的一個方法,用來獲取鎖,如果鎖已經被其他執行緒獲取,則進行等待,可以說和synchronized沒有差別
複製程式碼
Lock lock = ...;
lock.lock();
try{
    //處理任務
}catch(Exception ex){
}finally{
    lock.unlock();   //釋放鎖
}
複製程式碼

實際例子:

public class ThreadLock {

    private ReentrantLock lock = new ReentrantLock();

    public static void main(String[] args) {
        ThreadLock mt = new ThreadLock();
        new Thread(() -> mt.insert()).start();
        new Thread(() -> mt.insert()).start();
    }

    public void insert() {
        lock.lock();
        try {
            //睡眠10s,等待鎖的執行緒此時掛起
            System.out.println(Thread.currentThread().getName() + "獲得鎖");
            Thread.currentThread().sleep(10000);
        } catch (Exception e) {
        } finally {
            System.out.println(Thread.currentThread().getName() + "釋放鎖");
            lock.unlock();
        }
    }
}
複製程式碼

輸出:

Thread-0獲得鎖
Thread-0釋放鎖
Thread-1獲得鎖
Thread-1釋放鎖
複製程式碼

tryLock()是有返回值的,且會立即返回,不會一直等待,如果獲取到鎖,則返回true,否則返回false

public class ThreadLock2 {

    private ReentrantLock lock = new ReentrantLock();

    public static void main(String[] args) {
        ThreadLock2 mt = new ThreadLock2();
        new Thread(() -> mt.insert(Thread.currentThread())).start();
        new Thread(() -> mt.insert(Thread.currentThread())).start();
    }

    public void insert(Thread thread) {
        if (lock.tryLock()) {
            try {
                System.out.println(thread.getName() + "得到了鎖");
            } catch (Exception e) {
            } finally {
                try {
                    Thread.sleep(10000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                System.out.println(thread.getName() + "釋放了鎖");
                lock.unlock();
            }
        } else {
            System.out.println(thread.getName() + "獲取鎖失敗");
        }
    }
}
複製程式碼

輸出:

Thread-0得到了鎖
Thread-1獲取鎖失敗
Thread-0釋放了鎖
複製程式碼

lockInterruptibly允許在等待時由其它執行緒呼叫等待執行緒的Thread.interrupt方法來中斷等待執行緒的等待而直接返回,這時不用獲取鎖,而會丟擲一個InterruptedException。

lock方法不允許Thread.interrupt中斷,即使檢測到Thread.isInterrupted,一樣會繼續嘗試獲取鎖,失敗則繼續休眠。只是在最後獲取鎖成功後再把當前執行緒置為interrupted狀態,然後再中斷執行緒。

示例程式碼:

public class ThreadLock3 {

    private static Lock lock = new ReentrantLock();

    public static void main(String[] args) throws InterruptedException {
        MyThread thread1 = new MyThread("1", lock);
        MyThread thread2 = new MyThread("2", lock);
        thread1.start();
        Thread.sleep(100);
        thread2.start();
        Thread.sleep(1000);
        thread2.interrupt();
    }
}

class MyThread extends Thread {
    private Lock lock = null;

    public MyThread(String name, Lock lock) {
        super(name);
        this.lock = lock;
    }

    @Override
    public void run() {
        //注意,如果需要正確中斷等待鎖的執行緒,必須將獲取鎖放在外面,然後將InterruptedException丟擲
        try {
            lock.lockInterruptibly();
            out.println(this.getName() + "得到了鎖");
            long startTime = System.currentTimeMillis();
            for (; ; ) {
                if (System.currentTimeMillis() - startTime >= Integer.MAX_VALUE) {
                    break;
                }
            }
        } catch (InterruptedException e) {
            out.println(Thread.currentThread().getName() + "被中斷");
        } finally {
            out.println(Thread.currentThread().getName() + "執行finally");
        }
    }
}
複製程式碼

輸出:

1得到了鎖
2被中斷
2執行finally
複製程式碼

ReentranReadWriteLock分為readLock()和writeLock()來獲取讀寫鎖

public class ReadAndWriteLock {
    private ReentrantReadWriteLock lock = new ReentrantReadWriteLock();

    public static void main(String[] args) {
        ReadAndWriteLock readAndWriteLock = new ReadAndWriteLock();
        new Thread(() -> readAndWriteLock.read()).start();
        new Thread(() -> readAndWriteLock.read()).start();
        new Thread(() -> readAndWriteLock.write()).start();
        new Thread(() -> readAndWriteLock.write()).start();
    }

    public void read() {
        lock.readLock().lock();
        try {
            System.out.println(Thread.currentThread().getName() + "正在進行讀操作");
            long startTime = System.currentTimeMillis();
            while ((System.currentTimeMillis() - startTime) < 10000){}
        } catch (Exception e) {
        } finally {
            System.out.println(Thread.currentThread().getName() + "讀操作完畢");
            lock.readLock().unlock();
        }
    }

    public void write() {
        lock.writeLock().lock();
        try {
            System.out.println(Thread.currentThread().getName() + "正在進行寫操作");
            long startTime = System.currentTimeMillis();
            while ((System.currentTimeMillis() - startTime) < 10000){}
        } catch (Exception e) {
        } finally {
            System.out.println(Thread.currentThread().getName() + "寫操作完畢");
            lock.writeLock().unlock();
        }
    }
}
複製程式碼

輸出:

Thread-0正在進行讀操作
Thread-1正在進行讀操作
Thread-0讀操作完畢
Thread-1讀操作完畢
Thread-2正在進行寫操作
Thread-2寫操作完畢
Thread-3正在進行寫操作
Thread-3寫操作完畢
複製程式碼

Lock和synchronized的選擇:

1.Lock是JDK併發包的一個類,synchronized是關鍵字

2.Lock必須要使用unlock()方法在finally中釋放,如果沒有主動釋放鎖,就有可能導致出現死鎖現象,發生異常時不會自動釋放,synchronized發生異常時會自動釋放

3.Lock在等待獲取鎖的時候可以響應中斷,synchronized則不會。

4.Lock可以知道獲取鎖是否成功

5.Lock有讀寫鎖功能

6.Lock可以在指定的時間範圍內獲取所,如果截止時間到了,依然沒有獲得鎖,則返回複製程式碼

相關文章