java多線程和併發編程(一)--- java 線程小結

1, 爲什麼wait與notify之前必須要加synchronized?

答案其實很簡單,也是爲了防止等待-通知機制出現race condition

爲什麼會出現race condition ?
答: 對象在被wait之前已經被另一線程notify , 之後的wait 會永久停止,並導致deadlock(死鎖)

理想情況:
1, 第一個線程判斷該對象是否要wait
2, 第一個線程將對象wait
3, 第二個線程再將對象notify

實際情況
1, 第一個線程判斷該對象是否要wait
2, 第二個線程將對象notify
3, 第一個線程將對象wait

爲了防止這些情況,才需要在wait與notify之前加synchronized

java 代碼

A a = A.getInstance();//單例對象,同一份實例不銷燬
synchronized (a) {
a.wait();
}
-------------------------------另一線程
A a = A.getInstance();
synchronized(a) {
a.notify();
}

等待-通知機制必須與sychronized一起用,否則自身也會有 race condition.

2, 靜態同步方法與非靜態同步方法的區別

有時,我們經常會碰到這樣的代碼!

業務邏輯的封裝類:

public class Logic {
    private static final Log log = LogFactory.getLog(Logic.class);
    private static Logic logic;
 
    private Logic() {}
 
    public static Logic getInstance() {
        if (null == logic) {
            logic = new Logic();
        }
 
        return logic;
    }
 
    public static synchronized void testStatic() {
        log.info(Thread.currentThread().getName() + " : static method is running");
    }
 
    public synchronized void testNonStatic() {
        log.info(Thread.currentThread().getName() + " : non static method is running");
    }
}

非靜態方法的執行:

public class ThreadRun1 extends Thread {
    private static final Log log = LogFactory.getLog(ThreadRun1.class);
 
    public void run() {
        Logic logic = Logic.getInstance(); // object reference
 
        try {
            Thread.sleep(3000);
        } catch (InterruptedException e) {
            log.error("some exceptions occured :", e);
        }
 
        logic.testNonStatic();
 
        logEnd();
    }
 
    private void logEnd() {
        log.info("thread run1 end");
    }
}

靜態類方法的執行

public class ThreadRun2 extends Thread {
    private static final Log log = LogFactory.getLog(ThreadRun1.class);
 
    public void run() {
        Logic.testStatic(); // class static reference
 
        try {
            Thread.sleep(5000);
        } catch (InterruptedException e) {
            log.error("some error ocuur :", e);
        }
 
        logEnd();
    }
 
    private void logEnd() {
        log.info("thread run2 end");
    }
}

測試類

public class TestThread {
 
    /**
     * @param args
     */
    public static void main(String[] args) {
        ThreadRun1 run1 = new ThreadRun1();
        run1.start();
        ThreadRun2 run2 = new ThreadRun2();
        run2.start();
    }
}

現在有2根線程,其中一根會調用testStatic() , 而另一根會在testStatic未執行結束前調用testNonStatic!
那麼,按照多線程同步原則,該對象會在調用testStatic()方法時被鎖定,而該方法未結束前如果調用testNonStatic()方法,則必須要等待第一個線程執行完後,纔可以執行繼續執行!

但是,實際情況是兩線程可同時被調用!

區別在於,前者是靜態的,不需要實例化即可調用,那麼既然連實例化的對象都沒創建,何來鎖住對象呢!
大家都知道,靜態的方法一般都是直接調用“類.方法”來執行的,因此,調用testStatic鎖住的其實是類!(鎖住類不等於鎖住該類實例的對象!)

總結:每個class只有一個線程可以執行靜態同步方法,每個類的對象,只有一個線程可以執行同步方法!當對象實例調用同步方法,而同步方法中又調用了class的靜態同步方法,其實此次調用一共鎖住了2個不同的對象監視器!

Class級別的鎖與Object級別的鎖是不一樣的, 兩者相互獨立

3, thread 的 join 方法與 isAlive 方法的區別.

java 代碼

log.info("current thread running");
thread1.join(); // 當前線程在執行到join方法後, 會被block住 , 直到thread1線程處理結束或死亡
log.info("current thread stopping");
java 代碼

log.info("current thread running");
thread1.isAlive(); // 直接返回true or false
log.info("current thread stopping");
join方法是使當前線程阻塞,直到引用的線程結束才激活.

4, wait-notify機制

在一個以上的thread wait住時,調用notify是隨機的喚醒某一thread.

而notifyAll則是喚醒所有等待的線程, 但只有一個線程可以在喚醒後lock object monitor,
所以, notifyAll操作也是有利弊的.

wait-notify機制, 單次喚醒是隨機的, 全部喚醒則會導致大部分線程阻塞.

8, Lock接口替代synchronized

a, Lock接口可以比sychronized提供更廣泛的鎖定操作.可以提供多把不同的鎖.且鎖之間互不干涉.
b, Lock接口提供lock()與unlock()方法, 使用明確調用來完成同步的, OO思想好於前者.
c, Lock可以自由操控同步範圍(scope).
d, Lock接口支持nested lock(嵌套鎖定).並提供了豐富的api.
e, Lock接口提供了tryLock()方法, 支持嘗試取得某個object lock.

5, Condition替代wait與notify

// 生產/消費者模式
public class Basket {
    Lock lock = new ReentrantLock();
 
    //產生Condition對象
    Condition produced = lock.newCondition();
    Condition consumed = lock.newCondition();
    boolean available = false;
 
    public void produce() throws InterruptedException {
        lock.lock();
 
        try {
            if (available) {
                produced.await(); //放棄lock進入睡眠
            }
 
            System.out.println("Apple produced.");
 
            available = true;
 
            consumed.signal(); //發信號喚醒等待這個Condition的線程
        } finally {
            lock.unlock();
        }
    }
 
    public void consume() throws InterruptedException {
        lock.lock();
 
        try {
            if (!available) {
                consumed.await(); //放棄lock進入睡眠
            }
 
            /*吃蘋果*/
            System.out.println("Apple consumed.");
 
            available = false;
 
            produced.signal(); //發信號喚醒等待這個Condition的線程
        } finally {
            lock.unlock();
        }
    }
}
 
// 測試用類
public class ConditionTester {
    public static void main(String[] args) throws InterruptedException {
        final Basket basket = new Basket();
 
        //定義一個producer
        Runnable producer = new Runnable() {
                public void run() {
                    try {
                        basket.produce();
                    } catch (InterruptedException ex) {
                        ex.printStackTrace();
                    }
                }
            };
 
        //定義一個consumer
        Runnable consumer = new Runnable() {
                public void run() {
                    try {
                        basket.consume();
                    } catch (InterruptedException ex) {
                        ex.printStackTrace();
                    }
                }
            };
 
        //各產生10個consumer和producer
        ExecutorService service = Executors.newCachedThreadPool();
 
        for (int i = 0; i < 10; i++)
            service.submit(consumer);
 
        Thread.sleep(2000);
 
        for (int i = 0; i < 10; i++)
            service.submit(producer);
 
        service.shutdown();
    }
}

Condition配合Lock接口可以輕鬆實現,比sychronized配合wait,notify更
強大的功能.

Condition接口可以爲單個對象鎖生成多個類似wait-notify機制的條件變量.

每個條件變量在執行wait-notify時,只會控制自身條件的線程,即觸發notify時,只喚醒
自身條件變量上的wait線程,不會喚醒其他條件變量的wait線程.

建議: 同一把鎖下, 允許有多個Condition, 且相互不干涉, 但是, 每個Condition都是按順序執行的.
(java關鍵字, 如果使用this, 則範圍過大, 自己創建object來局部控制, 又不優雅)

注意: Condition的wait操作, 允許出現人爲或意外的”虛假喚醒”, 所以, 爲了保證Condition的作用域.
當調用wait時, 嘗試使用循環結構.其中condition爲await-singal的操作標示.

boolean condition = true;
while(condition) {
condition.await();
condition = false;
}
...
condition = true;
condition.singal();
6, 使用java.util.concurrent.atomic包,原子操作及解決volatile變量計算的race condition

private static AtomicInteger i = new AtomicInteger(0);
 
public void run() {
    int v = i.incrementAndGet();  // 相當於++i
    log.info("i = " + v);
}

包的特色:
1, 普通原子數值類型AtomicInteger, AtomicLong提供一些原子操作的加減運算.

2, 解決race condition問題的經典模式-”比對後設定”, 即 查看主存中數據是否與
預期提供的值一致,如果一致,才更新.

// 這邊採用無限循環
for (;;) {
            int current = get();
            if (compareAndSet(current, newValue))
                return current;
}
3, 使用AtomicReference可以實現對所有對象的原子引用及賦值.包括Double與Float,
但不包括對其的計算.浮點的計算,只能依靠同步關鍵字或Lock接口來實現了.

4, 對數組元素裏的對象,符合以上特點的, 也可採用原子操作.包裏提供了一些數組原子操作類

建議: 針對非浮點類型的數值計算, 數組元素及對象的引用/賦值, 優先採用原子類型.

優先考慮使用atmoic框架 .

7, 利用java semaphore信號量機制,控制某操作上線程的數量

java信號量的實現邏輯與操作系統解決進程同步問題時採用的PV操作類似.
即 P -> 臨界區 -> V
其中P爲消費,V生產,臨界區是同步區域.

java semaphore提供了acquire()與release()兩種操作,類似Lock的lock()與unlock.
區別在於, java semaphore對acquire有數量控制,即利用它的計數器大小,來控制多少線程可執行,其餘全部阻塞.
而Lock中的lock()方法,一次只能允許一根線程執行,其餘全部阻塞.

semaphore接口的構造函數中還提供了 一個boolean型的fair變量,表示,是否公平.
如果爲ture,則每個線程會根據到達的順序執行,而默認是false.

// 業務邏輯實現類
public class Logic {
    private static final Log log = LogFactory.getLog(Logic.class);
    private AtomicInteger sum = new AtomicInteger(0);
    private Semaphore sp = new Semaphore(5); // 吞吐量爲5條線程
 
    public void test() {
        try {
            sp.acquire();
            log.info(Thread.currentThread().getName() + " entered");
            Thread.sleep(2000);
            log.info(sum.getAndIncrement());
            sp.release();
        } catch (InterruptedException e) {
            log.error("sleep error:", e);
        }
    }
}
 
// 線程測試類
public class RunThread {
    public static void main(String[] args) {
        final Logic logic = new Logic();
 
        //定義一個producer
        Runnable test = new Runnable() {
            public void run() {
                logic.test();
            }
        };
 
        ExecutorService service = Executors.newCachedThreadPool();
 
        for (int i = 0; i < 10; i++) {
            service.submit(test);
        }
 
        service.shutdown();
    }
}

注意; semaphore可以控制某個資源上讀取操作的線程數量, 但是, semaphore本身是線程不安全的,
如果資源涉及到寫入操作, 那麼在操作中加上同步後, 信號量的作用也就跟Lock接口一樣了.(一次只能執行一根線程)

8, 利用CyclicBarrier屏障接口實現,線程集合/解散功能

java有好多種的屏障實現, 簡單的幾種如下:

a, 利用條件變量Condition實現wait-notify機制,等待所有的線程都wait在某一個
集合點時,notifyAll一下. 缺點是需要一根監控線程

b, 利用join方法,開一個監視線程, 每次調用這個線程取被block住的線程數量.
當達到指定數量後, 監視線程自動死亡,以放開所有的被block threads.

c, 利用CyclicBarrier提供的功能,只需要在集合點處調用await()方法,即可.

// 試驗屏障功能的類
public class Logic {
    private static final Log log = LogFactory.getLog(Logic.class);
    private int value = 21;
    private CyclicBarrier cyclic = new CyclicBarrier(3);
 
    public int getValue() {
        return value;
    }
 
    public void setValue(int value) {
        this.value = value;
    }
 
    public void expression1() {
        try {
            Thread.sleep(1000);
            log.info(value/2);
            cyclic.await();
            log.info(Thread.currentThread().getName() + " end.");
        } catch (InterruptedException e) {
            log.error(e);
        } catch (BrokenBarrierException e) {
            log.error(e);
        }
    }
 
    public void expression2() {
        try {
            Thread.sleep(2000);
            log.info(value*2);
            cyclic.await();
            log.info(Thread.currentThread().getName() + " end.");
        } catch (InterruptedException e) {
            log.error(e);
        } catch (BrokenBarrierException e) {
            log.error(e);
        }
    }
 
    public void expression3() {
        try {
            Thread.sleep(3000);
            log.info(value+2);
            cyclic.await();
            log.info(Thread.currentThread().getName() + " end.");
        } catch (InterruptedException e) {
            log.error(e);
        } catch (BrokenBarrierException e) {
            log.error(e);
        }
    }
}
 
// 線程測試類
public class RunThread {
    public static void main(String[] args) {
        final Logic logic = new Logic();
        Runnable run1 = new Runnable() {
            public void run() {
                logic.expression1();
            }
        };
 
        Runnable run2 = new Runnable() {
            public void run() {
                logic.expression2();
            }
        };
 
        Runnable run3 = new Runnable() {
            public void run() {
                logic.expression3();
            }
        };
 
        //各產生10個consumer和producer
        ExecutorService service = Executors.newCachedThreadPool();
 
        service.submit(run1);
        service.submit(run2);
        service.submit(run3);
 
        service.shutdown();
    }
}

注意: 使用屏障的時候, 小心異常的放生,當發生異常,所有線程都會被釋放
等待中的線程將被中斷. 且發生異常的屏障將不可用,需要屏障的實例reset一下.

9, 利用CountDownLatch接口實現線程集合/解散功能,類似CyclicBarrier,區別是倒數且只跑一次

接口方法與CyclicBarrier基本相同,不同在於構造函數需要傳入一數量,表示
倒數的開始數量.以後會遞減這個值

轉載請註明原文鏈接:http://kenwublog.com/java-thread-summary


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