多線程問題——輪流打印、死鎖、讀寫鎖實現

概述

最近閱讀了《Java高併發實戰》一書,也瞭解了一些多線程方面的知識,但是一直沒有嘗試過寫Coding。畢竟紙上得來終覺淺,因此通過本篇文章,對多個線程輪流打印、死鎖、讀寫鎖的實現問題進行總結,算是對多線程的一種鞏固。主要涉及到的知識點就是synchronized鎖和waitnotify線程通信機制。

線程輪流打印

問題描述

給定三個線程,代碼的邏輯順序是A->B->C,每個線程內分別打印一條“This is x”語句,如何做到最終打印順序是C->B->A

問題思考

要把多個線程的執行順序給安排上,聽起來就感覺要加鎖,某個線程加鎖執行完成,釋放鎖再進行下一個線程的執行,這隻能保證可以以一定的順序執行而不會亂序,但是具體的順序就需要引入一個變量,用於標識當前應該由幾號線程進行打印。在每個線程內去不斷輪詢是否輪到自己打印,如果沒有輪到自己打印就wait進入阻塞狀態,當別的線程打印完後,自己被喚醒繼續去輪詢是否輪到自己打印,當輪到自己時候,打印完成修改變量,並喚醒其它的所有線程,讓其它的線程去判斷是否輪到自己打印。整個邏輯就是如此,如果需要輪流打印C->B->A多次,使用for循環調用orderThread.printX即可。

具體實現
public class OrderThread {
    private int orderNum = 3;
    public static void main(String[] args) {
        OrderThread orderThread = new OrderThread();
        Thread t1 = new Thread(new Runnable() {
            @Override
            public void run() {
                orderThread.printA();
            }
        });
        Thread t2 = new Thread(new Runnable() {
            @Override
            public void run() {
                orderThread.printB();
            }
        });
        Thread t3 = new Thread(new Runnable() {
            @Override
            public void run() {
                orderThread.printC();
            }
        });
        t1.start();
        t2.start();
        t3.start();
    }

    public synchronized void printA(){
        while (orderNum != 1) {
            try {
                wait();
            } catch (Exception e) {
                e.printStackTrace();
            }
        }
        orderNum = 3;
        System.out.println("This is A");
        notifyAll();
    }

    public synchronized void printB() {
        while (orderNum != 2) {
            try {
                wait();
            } catch (Exception e) {
                e.printStackTrace();
            }
        }
        orderNum = 1;
        System.out.println("This is B");
        notifyAll();
    }

    public synchronized void printC() {
        while (orderNum != 3) {
            try {
                wait();
            } catch (Exception e) {
                e.printStackTrace();
            }
        }
        orderNum = 2;
        System.out.println("This is C");
        notifyAll();
    }
}
運行結果

This is C
This is B
This is A

線程輪流打印-拓展

問題描述

給定三個線程,代碼的邏輯順序是A->B->C,三個線程按照C->B->A的順序輪流打印1-100

問題思考

該問題是“線程輪流打印”問題的拓展,依然是三個線程輪流打印,只是打印的內容進行了改變,需要從1開始計數打印。這就需要我們設置一個全局變量,每個線程內都對該全局變量進行打印並進行加一操作,以便下一個線程能夠打印出正確的數字。需要注意的是:在while循環和線程執行體內都需要對待打印的變量num進行判斷。while循環中的num判斷是爲了多次調用print方法,線程執行體內的num判斷是爲了避免在調用print方法時num尚未達到100,但是print方法執行過程中num已經超出100的情況,如果取消掉線程執行體內的num判斷,程序會打印1-102。

具體實現
public class ThreadPrint {
    private int orderNum = 3;
    public static int num = 1;
    public static void main(String[] args) {
        ThreadPrint orderThread = new ThreadPrint();
        Thread t1 = new Thread(new Runnable() {
            @Override
            public void run() {
                while (num <= 100) {
                    orderThread.printA();
                }
            }
        });
        Thread t2 = new Thread(new Runnable() {
            @Override
            public void run() {
                while (num <= 100) {
                    orderThread.printB();
                }
            }
        });
        Thread t3 = new Thread(new Runnable() {
            @Override
            public void run() {
                while (num <= 100) {
                    orderThread.printC();
                }
            }
        });
        t1.start();
        t2.start();
        t3.start();
    }

    public synchronized void printA(){
        while (orderNum != 1) {
            try {
                wait();
            } catch (Exception e) {
                e.printStackTrace();
            }
        }
        orderNum = 3;
        if (num <= 100) {
            System.out.println("This is A " + num);
        }
        num++;
        notifyAll();
    }

    public synchronized void printB() {
        while (orderNum != 2) {
            try {
                wait();
            } catch (Exception e) {
                e.printStackTrace();
            }
        }
        orderNum = 1;
        if (num <= 100) {
            System.out.println("This is B " + num);
        }
        num++;
        notifyAll();
    }

    public synchronized void printC() {
        while (orderNum != 3) {
            try {
                wait();
            } catch (Exception e) {
                e.printStackTrace();
            }
        }
        orderNum = 2;
        if (num <= 100) {
            System.out.println("This is C " + num);
        }
        num++;
        notifyAll();
    }
}

死鎖

問題描述

給定兩個線程,使得程序產生死鎖。

問題思考

既然需要產生死鎖,就必須考慮到死鎖產生的條件。
我們來逐條分析一下死鎖的產生條件:
1、互斥,要做到資源互斥,只需要給資源加個鎖,每個時刻就只能有一個線程能夠獲取到該資源;
2、不可剝奪,一般對於正常的程序而言,除非發生中斷或者程序故障,否則對於線程已獲得的資源,在未使用完成之前都是不可剝奪的,只能在使用後自己釋放;
3、請求和保持,意味着當前線程需要持有一個資源,並去請求另一個資源;
4、循環等待,由上述條件進行引申,意味着A線程持有資源1並請求資源2,B線程持有資源2並請求資源1,形成環路。
綜上,我們只需要創建兩個不可剝奪的資源,並在不同的線程中通過synchronized關鍵字,根據不同的順序對資源進行持有,這樣即可實現死鎖。

具體實現
public class DeadTest {
    public static void main(String[] args) {
        DeadLock deadLock1 = new DeadLock(true);
        DeadLock deadLock2 = new DeadLock(false);
        Thread t1 = new Thread(deadLock1);
        Thread t2 = new Thread(deadLock2);
        t1.start();
        t2.start();
    }
}

class DeadLock implements Runnable {
    //用於標識兩個線程
    private boolean flag;
    //兩個資源
    static final Object obj1 = new Object();
    static final Object obj2 = new Object();
    public DeadLock(boolean flag) {
        this.flag = flag;
    }
    @Override
    public void run() {
        if (flag) {
            while (true) {
                //保持1並請求2
                synchronized (obj1) {
                    System.out.println(Thread.currentThread().getName() + "持有obj1");
                    synchronized (obj2) {
                        System.out.println(Thread.currentThread().getName() + "持有obj2");
                    }
                }
            }
        } else {
            while (true) {
                //保持2並請求1
                synchronized (obj2) {
                    System.out.println(Thread.currentThread().getName() + "持有obj2");
                    synchronized (obj1) {
                        System.out.println(Thread.currentThread().getName() + "持有obj1");
                    }
                }
            }
        }
    }
}

讀寫鎖

問題描述

實現一個讀寫鎖。讀鎖可以在沒有寫鎖時被多個線程同時持有,寫鎖是獨佔的,每次只能有一個寫線程,但是可以有多個線程併發地讀數據。

問題思考

如果我們讀寫共用一把鎖,那麼實際上整個程序的讀寫就是串行的,同一時刻只能有一個線程對數據進行讀或寫,效率顯然比較低,讀寫鎖比互斥鎖允許對於共享數據更大程度的併發。讀寫鎖主要是爲了能夠將讀寫分離,因爲程序其實是允許同一時刻,多個線程同時讀取數據的,只要在讀的期間沒有寫操作,即可保證所有讀線程都能讀到一致的數據。因此我們需要考慮到讀寫鎖的原則:
1、讀讀能共存;
2、讀寫不能共存;
3、寫寫不能共存。
那麼對於讀鎖而言,如果當前有線程在進行寫入操作,則進入等待狀態,直到所有的寫鎖釋放即可獲得讀鎖;對於寫鎖而言,只要當前沒有寫鎖存在,則優先預搶佔到寫鎖,避免被其它線程搶佔,是一種先來先服務的實現,但是此時還沒有真正獲得寫鎖,直到判斷當前不存在讀鎖,才能真正獲取到寫鎖進行數據的寫入。無論對於讀鎖還是寫鎖而言,鎖的釋放直接對鎖資源進行更新,然後通知其它所有線程即可。

具體實現
public class ReadWriteLockTest {
    public static void main(String[] args) {
        ReadWriteLockDemo readWriteLock = new ReadWriteLockDemo();
        //啓動寫線程
        new Thread(new Runnable() {
            @Override
            public void run() {
                readWriteLock.write(1);
            }
        }, "Write1").start();
        //啓動10個讀線程
        for (int i = 0; i < 10; i++) {
            new Thread(new Runnable() {
                @Override
                public void run() {
                    readWriteLock.read();
                }
            }).start();
        }
        //啓動寫線程
        new Thread(new Runnable() {
            @Override
            public void run() {
                readWriteLock.write(2);
            }
        }, "Write2").start();
    }
}

class ReadWriteLockDemo {
    private ReadWriteLock readWriteLock = new ReadWriteLock();
    private int num = 0; //共享資源

    //讀
    public void read() {
        try {
            readWriteLock.lockRead();
            System.out.println(Thread.currentThread().getName() + " read " + num);
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            readWriteLock.unlockRead();
        }
    }

    //寫
    public void write(int number) {
        try {
            readWriteLock.lockWrite();
            this.num = number;
            System.out.println(Thread.currentThread().getName() + " write " + num);
        } catch (Exception e) {
            e.printStackTrace();
        }  finally {
            readWriteLock.unlockWrite();
        }
    }
}

class ReadWriteLock {
    private int readLock = 0;
    private int writeLock = 0;

    public synchronized void lockRead() throws Exception{
        while (writeLock > 0) {
            wait();
        }
        readLock++;
    }

    public synchronized void unlockRead() {
        readLock--;
        notifyAll();
    }

    public synchronized void lockWrite() throws Exception{
        while (writeLock > 0) {
            wait();
        }
        writeLock++;
        while (readLock > 0) {
            wait();
        }
    }

    public synchronized void unlockWrite() {
        writeLock--;
        notifyAll();
    }
}
運行結果

Write1 write 1
Thread-1 read 1
Thread-2 read 1
Thread-5 read 1
Thread-0 read 1
Thread-3 read 1
Thread-4 read 1
Thread-8 read 1
Thread-6 read 1
Thread-9 read 1
Thread-7 read 1
Write2 write 2

從程序的運行結果中可以看出,對於讀操作而言,只有在寫操作結束後才能進行,並且不能保證線程執行的先後順序,因爲讀操作並不是互斥的;而對於寫操作而言,在讀鎖和寫鎖全部釋放之後才能進行。因此,Read線程必須在Write1線程完成並釋放寫鎖後才能執行,並且所有Read線程的執行是無序的,Write2線程只能等待所有Read線程完成並釋放讀鎖後才能執行。

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