1. 程式人生 > >多執行緒(四):鎖

多執行緒(四):鎖

當我們修改多個執行緒共享的例項時,例項就會失去安全性,所以我們應該仔細找出例項狀態不穩定的範圍,將這個範圍設為臨界區,並對臨界區進行保護,使其只允許一個執行緒同時執行。Java使用synchronized或Lock來定義臨界區,保護多個執行緒共享的欄位。

例項的全域性變數(共享資源)被修改時,會出現執行緒安全,需要對修改的方法加鎖,注意:只要要訪問多個執行緒共享的欄位的方法都需要加鎖保護。不能前門(方法A)鎖上了,後門(方法B)就不鎖了,或者窗戶(方法C)開著了,這都不行,都必須鎖住。
臨界區:只允許單個執行緒執行的程式範圍。

一:synchronized

synchronized是Java語言的關鍵字,當它用來修飾一個方法或者一個程式碼塊的時候,能夠保證在同一時刻最多隻有一個執行緒執行該段程式碼,也就是保證synchronized內的程式碼塊同步執行,而不會並行執行。

synchronized可以作為程式碼塊使用,也可以直接放在方法(物件方法、靜態方法)的簽名上。作為程式碼塊必須指定一個物件鎖,物件鎖就是一個物件,可以是this物件(即呼叫該方法的例項物件)也可以是自己建立的某個物件,也可以是某個類的位元組碼物件,synchronized修飾的程式碼屬於原子操作,不可再分割!

// 例項方法使用的是this鎖,即呼叫該物件的例項
synchronized(this) {
    // code
}

Object object = new Object();
synchronized(object) {
    // code
}

// 靜態方法是使用的類鎖
synchronized
(Xxx.class) { // code } // 物件鎖(修飾物件方法,鎖為this,即呼叫該方法的例項物件) public synchronized void foo() { } // 類鎖(修飾靜態方法,鎖為這個類,不是某一個物件) public synchronized static void bar() { }

當多個執行緒過來同時執行,如何保證同一時刻只有一個執行緒在執行(即同步執行)?

synchronized無論作為程式碼塊還是直接修飾方法都會指定一個鎖,當一個執行緒要想執行執行緒體就必須拿到這個鎖,只有拿到鎖的執行緒才能執行,這樣只有一個執行緒能拿到鎖,其他執行緒就拿不到鎖,這樣其它執行緒就執行不了,就必須等待,當拿到鎖的那個執行緒執行完就去釋放這個鎖,這樣其它等待的執行緒就去搶這個鎖,哪個執行緒搶到了鎖就去執行,搶不到就繼續下一輪的搶。鎖分為物件鎖和類鎖。

在用synchronized關鍵字的時候,儘量縮小程式碼塊的範圍,能在程式碼段上加同步就不要再整個方法上加同步。減小鎖的粒度,使程式碼更大程度的併發。

1. 物件鎖

public class ThisLock {
    public static void main(String[] args) {
        MyRunnable runnable = new MyRunnable();
        System.out.println("runnable=" + runnable);
        new Thread(runnable, "Thread-A").start();
        new Thread(runnable, "Thread-B").start();
    }
}

class MyRunnable implements Runnable {
    @Override
    public void run() {
        System.out.println(new Date() + "\t" + Thread.currentThread().getName() + "\t" + this);
        synchronized (this) {
            for(int i = 0; i < 5; i++) {
                System.out.println(new Date() + "\t" + Thread.currentThread().getName() +  "\ti=" + i);
                try { Thread.sleep(1000); } catch (InterruptedException e) { }
            }
        }
    }
}

這裡寫圖片描述

通過列印結果可以看到this鎖就是當前的執行緒物件runnable,可以看到程式碼塊外的程式碼是併發執行的,而程式碼塊內部的程式碼是同步執行的,即同一時刻只有一個執行緒在執行同步程式碼塊。Thread-A先拿到鎖就先執行,直到執行完畢,釋放鎖,然後Thread-B拿到鎖就執行。

示例程式碼中兩個執行緒都是使用同一個runnable物件,如果兩個執行緒各自使用各自的MyRunnable物件,則是併發的,不會是同步的。

// new Thread(runnable, "Thread-A").start();
// new Thread(runnable, "Thread-B").start();

// 鎖無效,因為每個new就是一個新的物件,兩個執行緒使用兩個不同的鎖,達不到互斥的效果
new Thread(new MyRunnable(), "Thread-A").start();
new Thread(new MyRunnable(), "Thread-B").start();

2. 修飾物件方法

public class MethodLock {
    public static void main(String[] args) {
        MyRunnable runnable = new MyRunnable();
        System.out.println("runnable=" + runnable);
        new Thread(runnable, "Thread-A").start();
        new Thread(runnable, "Thread-B").start();
    }
}

class MyRunnable implements Runnable {
    @Override
    public void run() {
        System.out.println(new Date() + "\t" + Thread.currentThread().getName() + "\t" + this +  "\trunning...");
        myRun();
    }

    private synchronized void myRun() {
        for(int i = 0; i < 4; i++) {
            System.out.println(new Date() + "\t" + Thread.currentThread().getName() +  "\ti=" + i);
            try { Thread.sleep(1000); } catch (InterruptedException e) { }
        }
    }
}

這裡寫圖片描述

synchronized修飾方法,表示將會鎖住整個方法體,鎖的是當前物件,這個synchronized(this)程式碼塊包括住方法的所有程式碼是一樣的效果

// 兩種方式效果一樣
synchronized(this) {
    for(int i = 0; i < 4; i++) {
        System.out.println(new Date() + "\t" + Thread.currentThread().getName() +  "\ti=" + i);
        try { Thread.sleep(1000); } catch (InterruptedException e) { }
    }
}

private synchronized void myRun() {
    for(int i = 0; i < 4; i++) {
        System.out.println(new Date() + "\t" + Thread.currentThread().getName() +  "\ti=" + i);
        try { Thread.sleep(1000); } catch (InterruptedException e) { }
    }
}

3. 類鎖

public class ClassLock {
    public static void main(String[] args) {

        new Thread(new Runnable() {
            @Override
            public void run() {
                ExampleService.test();
            }
        }, "Thread-A").start();


        new Thread(new Runnable() {
            @Override
            public void run() {
                ExampleService.test2();
            }
        }, "Thread-B").start();
    }
}

class ExampleService {
    // 靜態方法
    public synchronized static void test() {
        for(int i = 0; i < 4; i++) {
            System.out.println(new Date() + "\t" + Thread.currentThread().getName() +  "\ti=" + i);
            try { Thread.sleep(500); } catch (InterruptedException e) { }
        }
    }

    // 靜態方法
    public synchronized static void test2() {
        for(int i = 0; i < 4; i++) {
            System.out.println(new Date() + "\t" + Thread.currentThread().getName() +  "\ti=" + i);
        }
    }
}

這裡寫圖片描述

兩個執行緒分別呼叫不同的靜態方法,兩個方法是同步執行,而不是併發執行。因為兩個執行緒都是使用的類鎖ExampleService.class, 當Thread-A執行時拿到了ExampleService的類鎖,那麼Thread-B就拿不到這個類鎖了,只有Thread-A執行完畢後釋放類鎖,Thread-B才能拿到類鎖繼續執行,所以是同步執行的。

4. 物件鎖和類鎖

public class ObjectClassLock {
    public static void main(String[] args) {
        TestService testService = new TestService();

        new Thread(new Runnable() {
            @Override
            public void run() {
                testService.test();
            }
        }, "Thread-A").start();


        new Thread(new Runnable() {
            @Override
            public void run() {
                testService.test2();
            }
        }, "Thread-B").start();
    }
}

class TestService {
    // 鎖的是testService這個物件
    public synchronized void test() {
        System.out.println(new Date() + "\t" + Thread.currentThread().getName() + "\t running...");
        try {
            Thread.sleep(2000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println((new Date() + "\t" + Thread.currentThread().getName() + "\t over"));
    }

    // 靜態是鎖的TestService這個類(TestService.class)
    // 物件鎖和類鎖不是同一個鎖,所以兩個方法不會互斥
    public synchronized static void test2() {
        System.out.println(new Date() + "\t" + Thread.currentThread().getName() + "\t running...");
        System.out.println((new Date() + "\t" + Thread.currentThread().getName() + "\t over"));
    }
}

這裡寫圖片描述

Thread-A是訪問的物件方法,鎖為當前呼叫者即鎖物件為testService,Thread-B雖然是通過物件呼叫的但是是訪問的類方法,鎖為類鎖,即TestService.class, 因兩個執行緒使用的是不同的鎖,自然就互不影響,各自執行各自的,所以是併發執行的,而不是同步執行的。

5. 賣火車票示例

public class TrainTicketTest {
    int num = 2;
    public static void main(String[] args) {
        // 搶火車票功能
        TrainTicketTest synchronizedTest = new TrainTicketTest();
        for (int i = 0; i < 5; i ++) {
            new Thread(synchronizedTest.new TrainTicketThread()).start();
        }
    }

    class TrainTicketThread implements Runnable {
        @Override
        public void run() {
            System.out.println(Thread.currentThread().getName() + "\trunning");
            // 模擬業務耗時的時間
            try { Thread.sleep(500);} catch (InterruptedException e) { }
            if (num > 0) {
                System.out.println(Thread.currentThread().getName() + "\t搶到一張票\t目前還有" + num + "張\t");
                int temp = num - 1;
                num = temp;
                System.out.println(Thread.currentThread().getName() + "還剩餘" + num + "張");
            }
        }
    }
}

因多執行緒可能每次執行效果都不同,這裡給出一種效果,總共2張票,賣出去3張,賣超了
這裡寫圖片描述

根據上面的執行結果試圖對多執行緒程式碼執行分析(僅僅是自己的猜測)
這裡寫圖片描述

出現多賣的原因是由於多執行緒CPU時間片“隨機”的切換,同一段程式碼被多個執行緒同時執行,導致值的錯亂,解決這種問題的辦法就是同一段程式碼同一時間不允許多個執行緒執行,也就是隻能允許一個執行緒來執行。
我們可以為程式碼加上一個鎖,哪個執行緒想要執行這段程式碼必須手裡拿著鎖才能執行,當執行完畢就把鎖丟出去,其它執行緒如果想要執行就必須搶到鎖(注意多個執行緒必須使用同一個鎖),只有搶到鎖了才能執行加鎖的程式碼,為程式碼加鎖有兩種方式synchronized和lock,鎖就是一個物件。

public class TrainTicketTest {
    // 全域性變數,必須保證所有執行緒都在使用同一個鎖
    Object object = new Object();
    int num = 2;

    public static void main(String[] args) {
        // 搶火車票功能
        TrainTicketTest synchronizedTest = new TrainTicketTest();
        for (int i = 0; i < 5; i ++) {
            new Thread(synchronizedTest.new TrainTicketThread()).start();
        }
    }

    class TrainTicketThread implements Runnable {
        @Override
        public void run() {
            System.out.println(Thread.currentThread().getName() + "running");
            try { Thread.sleep(500);} catch (InterruptedException e) { }
            synchronized (object) {
                if (num > 0) {
                    System.out.println(Thread.currentThread().getName() + "\t搶到一張票\t目前還有" + num + "張\t");
                    int temp = num - 1;
                    num = temp;
                    System.out.println(Thread.currentThread().getName() + "還剩餘" + num + "張");
                }
            }
        }
    }
}
public class TrainTicket2Test {
    int num = 2;
    public static void main(String[] args) {
        // 搶火車票功能
        TrainTicket2Test synchronizedTest = new TrainTicket2Test();
        for (int i = 0; i < 5; i ++) {
            new Thread(synchronizedTest.new TrainTicketThread()).start();
        }
    }

    class TrainTicketThread implements Runnable {
        @Override
        public void run() {
            getTrainTicket();
        }
    }

    private synchronized void getTrainTicket() {
        System.out.println(Thread.currentThread().getName() + "running");
        try { Thread.sleep(500);} catch (InterruptedException e) { }
        if (num > 0) {
            System.out.println(Thread.currentThread().getName() + "\t搶到一張票\t目前還有" + num + "張\t");
            int temp = num - 1;
            num = temp;
            System.out.println(Thread.currentThread().getName() + "還剩餘" + num + "張");
        }
    }
}

三個人在不停的過一個門,一個門一次只能通過一個,每通過一個人就記錄通過的總人數及最後通過的人的姓名和地址。

Gate: 模擬門類

public class Gate {
    private int counter = 0;
    private String name = "Noboday";
    private String address = "Nowhere";

    public synchronized void pass(String name, String addrees) {
        this.counter++;
        this.name = name;
        this.address = addrees;
        check();
    }


    private void check() {
        if (name.charAt(0) != address.charAt(0)) {
            System.out.println("****BROKEN****" + toString());
        }
    }

    @Override
    public synchronized String toString() {
        return "No." + counter + ":" + name + "," + address;
    }
}

UserThread: 模擬人不停的過門

public class UserThread extends Thread {
    private final Gate gate;
    private final String myName;
    private final String myAddress;

    public UserThread(Gate gate, String myName, String myAddress){
        this.gate = gate;
        this.myName = myName;
        this.myAddress = myAddress;
    }

    @Override
    public void run() {
        System.out.println(myName + "BEGIN");
        while (true) {
            gate.pass(myName, myAddress);
        }
    }
}

Main: 三個人不停的過門,為了便於區分,每個人的姓名和地址首字母是相同的

public class Main {
    public static void main(String[] args) {
        Gate gate = new Gate();

        // 3個人過的是同一個門gate,即synchronized修飾方法=synchronized(this)=synchronized(gate)
        // 3個人是使用的同一把gate鎖
        new UserThread(gate, "Alice", "Alaska").start();
        new UserThread(gate, "Bobby", "Brazil").start();
        new UserThread(gate, "Chris", "Canada").start();
    }
}

去掉Gate類中的pass和toString方法的synchronized,執行效果如圖
這裡寫圖片描述

這裡寫圖片描述

  • Alice, Canada: Thread-0先執行this.name = name, 此時name=Chris, 此時CPU切換到Thread-1,然後執行了this.name = name; this.address = address; 此時name=Alice, address= Alaska; 此時CPU切換到Thread-0的address賦值,此時address=Canada, 所以最終的name=Alice,address=Canada, 兩個值狀態不一致。

  • Bobby,Brazil: 假如Thread-0是Bobby,假如Thread-0先執行this.name = name; 此時name=bobby;此時CPU切換到Thread-1, 假如Thread-1是Chris,Thread分別執行了給name和address分別賦值,此時name=Chris,address=Canada, 然後CPU切回到Thread-0,執行this.address=address, 此時adress為Brazil, 接著Thread-0又執行完了check()方法,此時CPU又切到Thread-1執行,開始執行check()方法,但是這個方法並沒有一下子執行完,而是執行到if(name.charAt(0) != address.charAt(0))這個判斷的時候又切到Thread-0了,注意此時沒有沒有執行if體中的程式碼,只是僅僅執行了條件判斷,此時還沒有進入if體就被切走了,切到Thread-0執行this.name=name,此時name=Bobby,此時又被切到Thread-1,執行剛才的if體,一列印,結果name=Bobby,address=Brazil, 這種問題的出現就是if判斷和if體分開執行了,當再次執行if體的時候結果欄位的值改變了

為什麼toString方法也加鎖?
所有訪問或者操作多執行緒共享的全域性變數時都應該加鎖。


6. 生產一個消費一個示例

生產一個就要消費一個,消費不完不能生產。此示例生產者和消費者執行緒使用的是同一個物件鎖person

wait和notify通常操作的是鎖物件

  • wait的作用:暫停執行緒執行,將當前執行緒的狀態置為阻塞狀態,並釋放鎖
  • notify的作用:隨機將呼叫notify方法的物件所對應的執行緒的狀態置為就緒狀態
public class MQExample {

    public static void main(String[] args) {
        Person person = new Person();
        new Thread(new Producer(person)).start();
        new Thread(new Consumer(person)).start();
    }
}

class Producer implements Runnable {
    private Person person;
    public Producer(Person person) {
        this.person = person;
    }

    @Override
    public void run() {
        synchronized (person) {
            int i = 0;
            while (true) {
                if (i % 2 == 0) {
                    person.setName("小紅");
                    person.setGender("女");
                } else {
                    person.setName("小明");
                    person.setGender("男");
                }
                System.out.println(new Date() + "\t生產者生產了一個物件【" + person.getName() + ", " + person.getGender() +"】");
                i++;

                // 當生產完一個要釋放鎖
                try {
                    person.wait();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }

                System.out.println("----------------------------------------------------------");
                person.notify();
            }
        }
    }
}

class Consumer implements Runnable {
    private Person person;

    public Consumer(Person person) {
        this.person = person;
    }

    @Override
    public void run() {
        synchronized (person) {
            while (true) {
                System.out.println(new Date() + "\t消費者消費了一個物件《" + person.getName() + ", " + person.getGender() + "》");
                person.notify();

                try {
                    person.wait();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }
    }
}

class Person {
    private String name;
    private String gender;

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public String getGender() {
        return gender;
    }

    public void setGender(String gender) {
        this.gender = gender;
    }
}

這裡寫圖片描述

該程式碼比較繞,進行分析一下

  1. 首先啟動Producer生產者執行緒,進入run()方法拿到person鎖,執行第一次while迴圈,列印【小紅, 女】,執行person.wait()暫停當前執行緒執行,釋放person物件鎖
  2. 因Producer生產者執行緒在執行person.wait()釋放了鎖,而且生產者執行緒還被阻塞了,此時CPU該執行消費者執行緒Consumer,消費者執行緒能拿到person物件鎖,就可以進入同步程式碼塊從而執行第一個迴圈列印《小紅, 女》,接下來執行person.notify() 會喚醒生產者執行緒Producer,將生產者執行緒的狀態從阻塞狀態設定為就緒狀態,再接下來執行消費者的person.wait(),這行程式碼讓消費者執行緒暫停執行,同時釋放person鎖
  3. 消費者因執行person.wait()導致消費者執行緒釋放了person物件鎖而且使消費者暫停執行,從而CPU就有機會執行生產者執行緒了,生成者能拿到person物件鎖,而且當前狀態為就緒狀態,就有資格進入執行狀態,此時生產者執行緒會接著上次沒有執行完的程式碼繼續執行,所以就執行了列印“——-”,接著執行生產者執行緒中的person.notify();此行程式碼的作用是讓消費者置為就緒狀態,此時因為生產者還持有鎖,雖然消費者執行緒置為就緒狀態,因拿不到鎖,所以不具備執行的條件。當執行完生產者的person.notify();這行程式碼時,此時第一輪迴圈就結束了,接著繼續迴圈第二輪,就繼續列印【小明, 男】,然後執行person.wait()暫停當前執行緒執行,釋放person物件鎖,就這樣步驟2,步驟3 一下迴圈下去執行

注意:Object.wait()和Object.notify() 只能用在同步方法或者同步程式碼塊中synchronized


二:Lock

Lock需要手動加鎖,釋放鎖也需要手動釋放,釋放鎖最好(必須)放到finally程式碼塊中釋放。
注意:同一個鎖可以加鎖多次(呼叫多次lock() ),相應的加N次鎖也必須解N次鎖(unlock()),一般情況下都是加一次鎖和解一次鎖

package java.util.concurrent.locks;

public interface Lock {
    // 加鎖
    void lock();
    void lockInterruptibly() throws InterruptedException;
    boolean tryLock();
    boolean tryLock(long time, TimeUnit unit) throws InterruptedException;
    // 釋放鎖
    void unlock();
    Condition newCondition();
}

重入鎖

package java.util.concurrent.locks;
public class ReentrantLock implements Lock, java.io.Serializable {
    public boolean isLocked();
}

讀寫鎖

package java.util.concurrent.locks;
public interface ReadWriteLock {
    // 讀鎖
    Lock readLock();

    // 寫鎖
    Lock writeLock();
}

ReentrantLockTest

public class ReentrantLockTest {
    public static void main(String[] args) {
        Lock lock = new ReentrantLock();

        Runnable runnable = () -> {
            System.out.println(new Date() + "\t" + Thread.currentThread().getName() + "\trunning...");
            lock.lock();
            try {
                for(int i = 0; i < 4; i++) {
                    System.out.println(new Date() + "\t" + Thread.currentThread().getName() +  "\ti=" + i);
                    try { Thread.sleep(1000); } catch (InterruptedException e) { }
                }
            } catch (Exception e) {
                e.printStackTrace();
            } finally {
                lock.unlock();
            }
        };

        new Thread(runnable, "Thread-A").start();
        new Thread(runnable, "Thread-B").start();
    }
}

這裡寫圖片描述

使用ReentrantLock和使用synchronized 程式碼塊都能達到上鎖的目的。先執行Thread-A執行緒,當指定到lock.lock()當前執行緒就拿到鎖,然後執行執行緒體,最後執行lock.unlock()釋放鎖,這樣Thread-B就能拿到執行緒繼續執行。

public class ReadWriteLockTest {
    private volatile static Map<String, Object> cacheMap = new HashMap<>();
    private static ReentrantReadWriteLock readWriteLock = new ReentrantReadWriteLock();
    static ReentrantReadWriteLock.ReadLock readLock = readWriteLock.readLock();
    static ReentrantReadWriteLock.WriteLock writeLock = readWriteLock.writeLock();

    public static void main(String[] args) {

        // 寫的時候不能讀,讀的時候不能寫,寫的時候不能寫,讀的時候可以讀
        new Thread(() -> {
            for(int i = 0;i < 5; i++) {
                put(i + "", i);
            }
        }).start();

        new Thread(() -> {
            for(int i = 0; i < 5; i++) {
                get(i + "");
            }
        }).start();

        new Thread(() -> {
            for(int i = 0;i < 5; i++) {
                put(i + "", i);
            }
        }).start();
    }

    public static Object put(String key, Object value) {
        try {
            writeLock.lock();
            System.out.println(new Date() + "\t" + Thread.currentThread().getName() + "\t寫\t" + key + "=" + value + "\tstart...");
            try { Thread.sleep(1000); } catch (InterruptedException e) { }
            cacheMap.put(key, value);
            System.out.println(new Date() + "\t" + Thread.currentThread().getName() + "\t寫\t" + key + "=" + value + "\tend");
        } catch (Exception e) {
        } finally {
            writeLock.unlock();
        }

        return value;
    }

    public static Object get(String key) {
        try {
            readLock.lock();
            System.out.println(new Date() + "\t" + Thread.currentThread().getName() + "\t讀\t" + key + "\tstart...");
            try { Thread.sleep(1000); } catch (InterruptedException e) { }
            Object value = cacheMap.get(key);
            System.out.println(new Date() + "\t" + Thread.currentThread().getName() + "\t讀\t" + key + "\tend");
            return value;
        } catch (Exception e) {
        } finally {
            readLock.unlock();
        }

        return null;
    }
}

這裡寫圖片描述


Object.wait和notify只能用於synchronized,如果想使用Lock就需要使用Condition來代替

public interface Condition {
    // 等待,相當於Object.wait()
    void await() throws InterruptedException;
    long awaitNanos(long nanosTimeout) throws InterruptedException;
    boolean await(long time, TimeUnit unit) throws InterruptedException;

    // 喚醒,相當於Object.notify()
    void signal();  
    void signalAll();
}

ConditionTest

public class ConditionTest {
    public static void main(String[] args) throws InterruptedException {
        // 多個執行緒必須使用相同的鎖和條件
        Lock lock = new ReentrantLock();
        Condition condition = lock.newCondition();
        System.out.println(condition);

        new Thread(new MyRunnable(lock, condition), "Thread-A").start();
        Thread.sleep(1000);
        new Thread(new MyRunnable2(lock, condition), "Thread-B").start();
    }
}

class MyRunnable implements Runnable {
    private Lock lock;
    private Condition condition;
    public MyRunnable(Lock lock, Condition condition) {
        this.lock = lock;
        this.condition = condition;
    }

    @Override
    public void run() {
        System.out.println(new Date() + "\t" + Thread.currentThread().getName() + "\t開始等待>>>");
        lock.lock();
        try {
            condition.await();
        } catch (InterruptedException e) {
            e.printStackTrace();
        } finally {
            lock.unlock();
        }
        System.out.println(new Date() + "\t" + Thread.currentThread().getName() + "\t已 蘇 醒^_^");
    }
}

class MyRunnable2 implements Runnable {
    private Lock lock;
    private Condition condition;
    public MyRunnable2(Lock lock, Condition condition) {
        this.lock = lock;
        this.condition = condition;
    }

    @Override
    public void run() {
        lock.lock();
        try {
            System.out.println(new Date() + "\t" + Thread.currentThread().getName() + "\t開始通知...");
            condition.signal();
            System.out.println(new Date() + "\t" + Thread.currentThread().getName() + "\t通知完成。。。");
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            lock.unlock();
        }
    }
}

這裡寫圖片描述

三:synchronized與Lock的區別

死鎖

一個執行緒中有多個鎖,多個執行緒同時執行,可能執行緒1獲得A鎖,執行緒2獲得了B鎖,然後執行緒A想得到B鎖就一直等待有人釋放,執行緒2想得到Asuo就等待兩個人一直等待,兩個執行緒各自持有一個,而且只有某個執行緒同時擁有AB鎖才能執行,兩個人都只得到一個,卻誰都不願意釋放,導致兩個執行緒都在等待。
就像一個門同時需要兩把鑰匙同時開啟,張三拿一把,李四拿一把,他們兩個誰都不把自己的那把給對方,張三說“李四你先把你的那把給我,我使用者就還你”,李四說“張三你先把你的那把給我,我使用者就還你”,兩個人都比較固執,結果兩個人一直僵持著

public class DeadLockTest {
    public static String a = "a";
    public static String b = "b";

    public static void main(String[] args) {
        new Thread(new MyTask0()).start();
        new Thread(new MyTask1()).start();
    }
}

class MyTask0 implements Runnable {
    @Override
    public void run() {
        System.out.println(new Date() + "\t" + Thread.currentThread().getName() + "\t running...");
        synchronized (DeadLockTest.a) {
            System.out.println(new Date() + "\t" + Thread.currentThread().getName() + "\t first synchronized ...");
            try { Thread.sleep(1000); } catch (InterruptedException e) { }

            synchronized (DeadLockTest.b) {
                System.out.println(new Date() + "\t" + Thread.currentThread().getName() + "\t second synchronized ...");
            }
        }
    }
}

class MyTask1 implements Runnable {
    @Override
    public void run() {
        System.out.println(new Date() + "\t" + Thread.currentThread().getName() + "\t running...");
        synchronized (DeadLockTest.b) {
            System.out.println(new Date() + "\t" + Thread.currentThread().getName() + "\t first synchronized ...");
            try { Thread.sleep(1000); } catch (InterruptedException e) { }

            synchronized (DeadLockTest.a) {
                System.out.println(new Date() + "\t" + Thread.currentThread().getName() + "\t second synchronized...");
            }
        }
    }
}

這裡寫圖片描述
兩個執行緒的第二個程式碼塊都沒有執行,main方法也一直在執行狀態,沒有結束。

悲觀鎖和樂觀鎖

在查詢的sql上加for update 子句,每次查詢時會對錶或者行加鎖,事務提交後會釋放鎖

public class DatabaseLock {
    public static void main(String[] args) {
        new Thread(new Runnable() {
            @Override
            public void run() {
               new OrderService().updateOrder();
            }
        }).start();
    }
}

class OrderService {
    @Transactional
    public void updateOrder() {
          // for update 每次查詢時會對行加鎖,事務提交後會釋放鎖
        System.out.println("select * from tbl_order where id = 1 for update");
        System.out.println("update tbl_order set status = 1 where id = 1");
        System.out.println("update tbl_payment set amount = 6 where id = 1");
    }
}

樂觀鎖:在表上加一個version欄位,當update操作是即給version自增也同時將version作為查詢條件,如果update操作受影響的行數大於0就執行其它業務邏輯

樂觀鎖原理虛擬碼:

public class DatabaseLock {
    public static void main(String[] args) {
        new Thread(new Runnable() {
            @Override
            public void run() {
               new OrderService().updateOrder();
            }
        }).start();
    }
}

class OrderService {
    @Transactional
    public void updateOrder() {
        String order = "select id, version from tbl_order where id = 1";
        String affectedRows = "update tbl_order set status = 1, version = version + 1 where id = 1 and version = #{order.version}";
        if (affectedRows > 0) {
            System.out.println("update tbl_payment set amount = 6 where id = 1");
        } else {
            // error
        }
    }
}

悲觀鎖每次查詢就會加鎖,這樣同一時刻只能有一個數據庫連線操作,效率非常低。開發一般都使用樂觀鎖。