Java多線程編程六(使用Lock)


本文主要講述如何解決非線程安全問題,感謝java多線程核心編程一書,爲本系列文章提供參考借鑑

一、使用ReentrantLock

1.使用ReentrantLock方法:
  • lock():鎖定
  • unlock():解除鎖定
  • int getHoldCount():產訊當前線程保持此鎖定的個數,也就是調用lock()方法的次數
  • int getQueueLength():返回正在等待獲取此鎖定的線程估計數,例如:8個線程,3個線程調用了await()方法吧,那麼在調用此方法後返回的值是5,那麼就說嘛有5個線程同時在等待lock的釋放。
  • int getWaitQueueLength():顧名思義,其作用是返回等待與此鎖定相關的給定條件Condition的線程估計數。例如:8個線程,每個線程都調用了同一個Condition的await()方法,則調用此方法返回的值是8.
  • boolean hasQueuedThread():查詢指定線程是否正在等待獲取此鎖定。ReentrantLock.hasQueuedThread(thread);
  • boolean hasWaiters():查詢是否有線程正在等待與此鎖定有關的Condition條件。
  • boolean isFair():判斷是否是公平鎖
  • boolean isHeldByCurrentThread():查詢當前線程是否保持此鎖定。
  • boolean isLocked():查詢此鎖定是否由任意線程保持。
  • void lockInterruptibly():如果當前線程未被中斷,則獲取鎖定,如果已經被中斷則出現異常。
  • boolean tryLock():僅在調用時鎖定未被另一個線程保持的情況下,才獲取該鎖定。
  • boolean tryLock(long timeout,TimeUnit unit):如果鎖定在給定等待時間內沒有被另一個線程保持,且當前線程未被中斷,則獲取該鎖定。
2.使用ReentrantLock實現同步效果:
public class MyServer {
    private ReentrantLock lock = new ReentrantLock();
    public void printReentrantLock() {
        try {
            lock.lock();//獲取鎖
            for (int i = 0; i < 5; i++) {
                System.out.println("ThreadName=" + Thread.currentThread().getName() + ",i=" + i);
            }
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            lock.unlock();//釋放鎖
        }
    }
	public static void main(String[] args) {
        final MyServer myServer = new MyServer();
        for (int i = 0; i < 5; i++) {
            Thread thread = new Thread(new Runnable() {
                public void run() {
                    myServer.printReentrantLock();
                }
            });
            thread.setName(String.valueOf(i));
            thread.start();
        }
    }
}

運行結果爲:
在這裏插入圖片描述
由結果分析知:當前線程獲取鎖對象後其他線程呈阻塞狀態,直到當前線程打印完畢釋放鎖,其他線程才能獲取到鎖進行打印。

這個主要進行單個方法同一個鎖多線程調用的同步效果,下面來看一下多個線程交叉調用不同方法同一個鎖的例子:

public class MyServer {

    private ReentrantLock lock = new ReentrantLock();

    public void methodA() {
        try {
            lock.lock();//獲取鎖
            System.out.println("method A begin ThreadName=" + Thread.currentThread().getName() + " time=" + System.currentTimeMillis());
            methodB();//調用methodB
            Thread.sleep(5000);
            System.out.println("method A end   ThreadName=" + Thread.currentThread().getName() + " time=" + System.currentTimeMillis());
        } catch (Exception e) {
            e.printStackTrace();
        }finally {
            lock.unlock();
        }
    }

    public void methodB() {
        try {
            lock.lock();//獲取鎖
            System.out.println("method B begin ThreadName="+Thread.currentThread().getName()+" time="+System.currentTimeMillis());
            Thread.sleep(5000);
            System.out.println("method B end   ThreadName="+Thread.currentThread().getName()+" time="+System.currentTimeMillis());
        } catch (InterruptedException e) {
            e.printStackTrace();
        }finally {
            lock.unlock();
        }
    }
}
    public static void main(String[] args) {
        String[] names = {"A", "AA", "B", "BB"};
        final MyServer myServer = new MyServer();
        Thread[] threads = new Thread[4];
        for (int i = 0; i < 2; i++) {
            threads[i] = new Thread(new Runnable() {
                public void run() {
                    myServer.methodA();
                }
            });
        }
        for (int i = 2; i < 4; i++) {
            threads[i] = new Thread(new Runnable() {
                public void run() {
                    myServer.methodB();
                }
            });
        }
        for (int i = 0; i < 4; i++) {
            threads[i].setName(names[i]);
            threads[i].start();
        }

    }

結果如下:
在這裏插入圖片描述
分析結果:在線程A獲取鎖時其他線程呈阻塞狀態直至線程A執行完畢且釋放鎖,其他線程纔可以獲取到鎖,同時在線程A獲取鎖執行過程中又成功調用了methodB,這些說明reentrantlock和synchronized關鍵字一樣線程之間是同步順序執行的,也都具有鎖重入特性。

二、使用Condition實現等待/通知

關鍵字synchronized於wait()和notify()/notifyAll()方法相結合可以實現等待/通知模式,類ReentrantLock也可以試想同樣的功能,但需要藉助於Condition對象。相對於notify和notifyAll,Condition具有跟高的靈活性,在使用notify和notifyAll時,被通知的線程是由JVM隨機選擇的,而ReentrantLock結合Condition類是可以“選擇性通知的”。

1.Condition中通知/等待方法
  • await():相當於Object類中的wait()方法
  • await(long time,TimeUtil unit):相當於Object類中的wait(long timeout)方法
  • signal():相當於Object類中的notify()方法
  • signalAll():相當於Object類中的notifyAll()方法
2.簡單使用:

注意:調用condition.await()之前需要執行lock.lock()來獲取同步監視器。

public class MyServer {
    private ReentrantLock lock = new ReentrantLock();
    private Condition condition = lock.newCondition();
    public void await() {
        try {
            lock.lock();
            System.out.println(" await 時間爲 "+ System.currentTimeMillis());
            condition.await();
        } catch (InterruptedException e) {
            e.printStackTrace();
        } finally {
            lock.unlock();
        }
    }
    public void signal() {
        try {
            lock.lock();
            System.out.println(" signal 時間爲 "+System.currentTimeMillis());
            condition.signal();
        } finally {
            lock.unlock();
        }
    }
        public static void main(String[] args) throws InterruptedException {
        final MyServer myServer = new MyServer();
        new Thread(new Runnable() {
            public void run() {
                myServer.await();
            }
        }).start();
        Thread.sleep(3000);
        myServer.signal();
    }

}

運行結果爲:
在這裏插入圖片描述

3.使用多個Condition實現”選擇性“通知線程

代碼如下:

public class MyServer {
    private ReentrantLock lock = new ReentrantLock();
    private Condition conditionA = lock.newCondition();
    private Condition conditionB = lock.newCondition();
    @SuppressWarnings("all")
    public void awaitA() {
        try {
            lock.lock();
            System.out.println("begin await A 時間爲 "+ System.currentTimeMillis());
            conditionA.await();
            System.out.println("end   await A 時間爲 "+ System.currentTimeMillis());
        } catch (InterruptedException e) {
            e.printStackTrace();
        } finally {
            lock.unlock();
        }
    }
    @SuppressWarnings("all")
    public void awaitB() {
        try {
            lock.lock();
            System.out.println("begin await B 時間爲 "+ System.currentTimeMillis());
            conditionB.await();
            System.out.println("end   await B 時間爲 "+ System.currentTimeMillis());
        } catch (InterruptedException e) {
            e.printStackTrace();
        } finally {
            lock.unlock();
        }
    }
    @SuppressWarnings("all")
    public void signalAll_A() {
        try {
            lock.lock();
            System.out.println(" signalall B 時間爲 "+System.currentTimeMillis());
            conditionA.signalAll();
        } finally {
            lock.unlock();
        }
    }
    @SuppressWarnings("all")
    public void signalAll_B() {
        try {
            lock.lock();
            System.out.println(" signalall B 時間爲 "+System.currentTimeMillis());
            conditionB.signalAll();
        } finally {
            lock.unlock();
        }
    }
    public static void main(String[] args) throws InterruptedException {
        final MyServer myServer = new MyServer();
        Thread threadA = new Thread(new Runnable() {
            public void run() {

                myServer.awaitA();
            }
        });
        threadA.setName("A");
        threadA.start();
        Thread threadB = new Thread(new Runnable() {
            public void run() {
                myServer.awaitB();
            }
        });
        threadB.setName("B");
        threadB.start();
        Thread.sleep(3000);
        myServer.signalAll_B();
    }
}

在上述代碼中我們定義了兩個Condition,分別用於兩個線程的等待和喚醒,而我們只喚醒線程B而線程A則繼續等待。結果如下:
在這裏插入圖片描述

三、公平鎖與非公平鎖

公平鎖就是線程獲取鎖的順序是按照線程加鎖的順序來分配的,即先來先得的FIFO先進先出的順序。非公平鎖就是隨機獲取鎖的,屬於一種槍戰機制。先來不一定先得到,誰搶到屬於誰,所以是不公平的。
通過構造方法ReetrantLock(boolean isFair)來創建對應公平鎖或不公平鎖。無參默認爲非公平鎖。

四、使用ReentrantReadWriteLock類

使用ReetrantLock具有完全互斥排他的效果,即同一時間只有一個線程能夠執行Reentrantlock.lock()方法後的代碼。雖然這樣能保持實例變量的線程安全性,但是效率確實非常低的。然而使用讀寫鎖ReentrantReadWriteLock類,可以提高運行效率,在不需要操作實例變量的情況下,可以使用讀寫鎖ReentrantReadWriteLock來提高任務的執行效率。
讀寫鎖ReentrantReadWriteLock有兩個鎖,一個是讀操作的鎖即共享鎖,一個是寫操作鎖即排它鎖。在多少個讀鎖之間不互斥,讀鎖和寫鎖互斥,寫鎖和寫鎖互斥。在多個線程中,如果沒有線程進行寫操作的時,可以有多個線程同時進行讀操作;而其中一個線程正在寫操作時,其他線程的讀操作或寫操作都只能等當前的寫線程執行完畢。

1.讀寫或寫讀互斥
public class MyServer {
    private ReentrantReadWriteLock lock = new ReentrantReadWriteLock();

    public void read() {
        try {
            lock.readLock().lock();
            System.out.println("獲得讀鎖: "+Thread.currentThread().getName()+" "+System.currentTimeMillis());
            Thread.sleep(10000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        } finally {
            lock.readLock().unlock();
        }
    }

    public void write() {
        try {
            lock.writeLock().lock();
            System.out.println("獲得寫鎖: "+Thread.currentThread().getName()+" "+System.currentTimeMillis());
            Thread.sleep(10000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        } finally {
            lock.writeLock().unlock();
        }
    }
    public static void main(String[] args) throws InterruptedException {
        final MyServer myServer = new MyServer();
        Thread threadA = new Thread(new Runnable() {
            public void run() {
                myServer.read();
            }
        });
        threadA.setName("A");
        threadA.start();
        Thread threadB = new Thread(new Runnable() {
            public void run() {
                myServer.write();
            }
        });
        threadB.setName("B");
        threadB.start();
        Thread threadC = new Thread(new Runnable() {
            public void run() {
                myServer.read();
            }
        });
        threadC.setName("C");
        threadC.start();
    }
}

結果如:在這裏插入圖片描述

由結果可知,讀寫和寫讀是互斥的

2.讀讀共享

更改上面的main方法:

    public static void main(String[] args) throws InterruptedException {
        final MyServer myServer = new MyServer();
        Thread threadA = new Thread(new Runnable() {
            public void run() {
                myServer.read();
            }
        });
        threadA.setName("A");
        threadA.start();
        Thread threadB = new Thread(new Runnable() {
            public void run() {
                myServer.read();
            }
        });
        threadB.setName("B");
        threadB.start();
    }

結果如:
在這裏插入圖片描述

由結果可知:讀和讀共享

3.寫寫互斥

更改上面的main方法:

    public static void main(String[] args) throws InterruptedException {
        final MyServer myServer = new MyServer();
        Thread threadA = new Thread(new Runnable() {
            public void run() {
                myServer.write();
            }
        });
        threadA.setName("A");
        threadA.start();
        Thread threadB = new Thread(new Runnable() {
            public void run() {
                myServer.write();
            }
        });
        threadB.setName("B");
        threadB.start();
    }

結果如:
在這裏插入圖片描述

由結果知:寫寫互斥

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