(四十三)、線程的同步和線程池

線程的同步

同步代碼塊

synchronized放在對象前面限制一段代碼的執行

同步代碼塊定義語法:

synchronized(對象)
{
需要同步的代碼;
}
class TicketOffice implements Runnable {
    private int tickets = 10;

    public void run() {
        while (true) {
            synchronized (this) {
                if (tickets > 0) {
                    try {
                        Thread.sleep(1);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    tickets--;
                    System.out.println(Thread.currentThread().getName()
                            + ":賣出第" + tickets + "張票");
                } else {
                    break;
                }
            }
        }
    }
}

在上面的代碼中,程序將這些需要具有原子性的代碼,放入synchronized語句內,形成了同步代碼塊。在同一時刻只能有一個線程可以進入同步代碼塊內運行,只有當該線程離開同步代碼塊後,其它線程才能進入同步代碼塊內運行。

同步方法

除了可以對代碼塊進行同步外,也可以對函數實現同步,只要在需要同步的函數定義前加上synchronized關鍵字即可。
同步方法定義語法:

  訪問控制符synchronized 返回值類型 方法名稱(參數)
  {
  //…;
  }

等同於:

  訪問控制符 返回值類型 方法名稱(參數)
  {
      Synchronized(this){
  //…;
  }
  }
class TicketOffice implements Runnable {
    private int tickets = 10;

    public synchronized void run() {
        while (true) {

            if (tickets > 0) {
                try {
                    Thread.sleep(1);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                tickets--;
                System.out.println(Thread.currentThread().getName() + ":賣出第"
                        + tickets + "張票");
            } else {
                break;
            }
        }
    }
}

可見,編譯運行後的結果同上面同步代碼塊方式的運行結果完全一樣,也就是說在方法定義前使用synchronized關鍵字也能夠很好地實現線程間的同步。
在同一類中,使用synchronized關鍵字定義的若干方法,可以在多個線程之間同步,當有一個線程進入了有synchronized修飾的方法時,其它線程就不能進入同一個對象使用synchronized 來修飾的所有方法,此時會將自身線程阻塞,直到第一個線程執行完它所進入的synchronized修飾的方法爲止。

BankCard.java 相當於銀行卡

public class BankCard {
    private int currentMoney;

    public int getCurrentMoney() {
        return currentMoney;
    }

    public BankCard(int money) {
        this.currentMoney = money;
    }

    public boolean takeMoney(int money) {
        // 使用公共資源作爲同步鎖
        // 要保證每個線程搶同一個鎖
        synchronized (this) {
            if (money <= currentMoney) {
                try {
                    Thread.sleep(20);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                currentMoney -= money;
                return true;
            }
        }
        return false;
    }
}

模擬取錢操作

public class MyRunnable implements Runnable {
    private BankCard card;

    public MyRunnable(BankCard card) {
        this.card = card;
    }

    @Override
    public void run() {
        String name = Thread.currentThread().getName();
        if (name.equals("丈夫")) {
            if (card.takeMoney(3000)) {
                System.out.println(name + "取錢成功" + ",餘額"
                        + card.getCurrentMoney());
            } else {
                System.out.println(name + "取錢失敗" + ",餘額"
                        + card.getCurrentMoney());
            }
        } else if (name.equals("妻子")) {
            if (card.takeMoney(7000)) {
                System.out.println(name + "取錢成功" + ",餘額"
                        + card.getCurrentMoney());
            } else {
                System.out.println(name + "取錢失敗" + ",餘額"
                        + card.getCurrentMoney());
            }
        }
    }
}

Main.java兩人同時取錢時可能出現同步問題

public class Main {
    /*
     * 多個線程同時修改共享資源時會出現同步問題 
     * 1.同步代碼塊:synchronized(同步鎖){ //需要同步的代碼 }
     * 2.同步方法:synchronized修飾的方法
     */
    public static void main(String[] args) {
        BankCard card = new BankCard(8000);
        MyRunnable runnable = new MyRunnable(card);

        Thread thread = new Thread(runnable, "丈夫");
        Thread thread2 = new Thread(runnable, "妻子");

        thread.start();
        thread2.start();

    }
}

關於同步鎖

任何對象都可以作爲同步鎖。

1、  使用同步代碼塊時必須明確指明使用的同步鎖。同步也可以使用字節碼做爲鎖。int.class、String.class等均可以作爲同步鎖。
2、  同步方法也用到的同步鎖,此時的鎖對象是this。
3、  使用同步鎖建議使用公共資源作爲鎖。換句話說就是,這段代碼要修改哪個公共資源,就使用那個公共資源作爲同步鎖。

死鎖問題

一旦有多個線程,且它們都要爭用對多個鎖的獨佔訪問,那麼就有可能發生死鎖。如果有一組進程或線程,其中每個都在等待一個只有其它進程或線程纔可以執行的操作,那麼就稱它們被死鎖了。

public class MyThread extends Thread {
    private Rice rice;
    private Sorry sorry;

    public MyThread(String name, Rice rice, Sorry sorry) {
        super(name);
        this.rice = rice;
        this.sorry = sorry;
    }

    @Override
    public void run() {
        String name = this.getName();
        if (name.equals("男")) {
            synchronized (sorry) {
                System.out.println("男方說:先給我做飯我在道歉");
                synchronized (rice) {
                    System.out.println("男方喫上了飯");
                }
            }
        } else {
            synchronized (rice) {
                System.out.println("女方說:先給我道歉我在做飯");
                synchronized (sorry) {
                    System.out.println("男方道了歉");
                }
            }
        }
    }
}
/*
 * 死鎖
 */
public class DeathLockDemo {

    public static void main(String[] args) {
        Rice rice = new Rice();
        Sorry sorry = new Sorry();
        new MyThread("男", rice, sorry).start();
        new MyThread("女", rice, sorry).start();
    }
}

線程池

爲什麼需要線程池

一個線程完成一項任務所需時間爲:T1創建線程時間,T2在線程中執行任務的時間,T3銷燬線程時間。
線程池技術正是關注如何縮短或調整T1、T3時間的技術,從而提高程序的性能。它把T1,T3分別安排在服務器程序的啓動和結束的時間段或者一些空閒的時間段,這樣在服務器程序處理客戶請求時,不會有T1,T3的開銷了。
線程池不僅調整T1,T3產生的時間段,而且它還顯著減少了創建線程的數目。

系統啓動一個新線程的成本是比較高的,因爲涉及與操作系統的交互,在這種情形下,使用線程池可以很好地提高性能,尤其是當程序中需要創建大量生存期很短暫的線程時,優先考慮使用線程池。
線程池在系統啓動時即創建大量空閒的線程,程序將一個Runnable對象傳給線程池,線程池就會啓動一條線程來執行該對象的run方法,當run方法執行結束以後,該線程並不會死亡,而是再次返回線程池中成爲空閒狀態,等待執行下一個Runnable對象的Run方法。
JDK1.5以前,開發者必須手動實現自己的線程池,JDK1.5開始,Java內建支持線程池

線程池組成部分

一個線程池包括以下四個基本組成部分:
1、線程池管理器(ThreadPool):用於創建並管理線程池,包括:創建線程池、銷燬線程池和添加新任務;
2、工作線程(PoolWorker):線程池中線程,在沒有任務時處於等待狀態,可以循環的執行任務;
3、任務接口(Task):每個任務必須實現的接口,以供工作線程調度任務的執行,它主要規定了任務的入口,任務執行完後的收尾工作,任務的執行狀態等;
4、任務隊列(taskQueue):用於存放沒有處理的任務。提供一種緩衝機制。

線程池的創建

線程池通過java.util.concurrent.Executors工廠類來創建。該工廠類包含以下幾個靜態工廠方法來創建:

  • newCachedThreadPool():創建一個具有緩存功能的線程池,系統根據需要創建線程,這些線程將會被緩存在線程池中。返回一個ExecutorService對象,代表一個線程池。
  • newFixedThreadPool(int nThreads):創建一個可以重用的、具有固定線程數的線程池。返回一個ExecutorService對象,代表一個線程池。
    • newSingleThreadExecutor():創建一個只有單線程的線程池,它相當於newFixedThreadPool(1)。返回一個ExecutorService對象,代表一個線程池。
  • newScheduledThreadPool(int corePoolSize):創建具有指定線程數的線程池,它可以再指定延遲後執行線程任務。 返回ScheduledExecutorService對象。它可以在指定延遲後執行線程任務。
  • newSingleThreadScheduledExecutor():創建只有一條線程的線程池,它可以在指定延遲後執行線程任務。

ThreadPoolDemo.java

public class ThreadPoolDemo {
    public static void main(String[] args) throws InterruptedException,
            ExecutionException {

        Runnable runnable = new Runnable() {

            @Override
            public void run() {
                for (int i = 0; i < 10; i++) {
                    System.out.println(Thread.currentThread().getName() + " "
                            + i);
                }
            }
        };

        // 創建線程池對象
        ExecutorService pool = Executors.newFixedThreadPool(10);// 創建一個固定線程數的線程池,最多同時運行10個線程
        Future<String> submit = pool.submit(runnable, "ok");// 線程結束時,get()返回第二個參數
        System.out.println("==========");
        System.out.println(submit.get());//阻塞式方法,等線程完成返回一個值
        System.out.println("----------");

        // 關閉線程池
        pool.shutdown();// 等正在運行和正在等待的線程結束在關閉
        pool.shutdownNow();// 直接關閉,並返回正在等待的runnable對象一個list集合
    }
}

ExecutorService

代表一個線程池,只要線程池中有空閒線程立即執行線程任務,程序只要將一個Runnable對象或Callable對象提交給該線程池即可,該線程池就會盡快執行該任務。

  • Future < ? > submit(Runnable task):將一個Runnable對象提交給指定的線程池,線程池將在有空閒線程時執行Runnable對象代表的任務,其中Future對象代表Runnable任務的返回值,但run方法沒有返回值,所有Future對象在run方法執行結束以後返回null,但可以調用Future的isDone、isCancelled方法來獲得Runnable對象的執行狀態。
  • < T > Future< T > submit(Runnable task, T result):result顯示指定線程執行結束後的返回值,所有Future對象將在run方法執行結束後返回result。
public class ThreadPoolDemo2 {
    public static void main(String[] args) throws InterruptedException,
            ExecutionException {
        Callable<Integer> callable = new Callable<Integer>() {

            @Override
            public Integer call() throws Exception {
                int sum = 0;
                for (int i = 0; i <= 100; i++) {
                    sum += i;
                }
                return sum;
            }
        };

        ExecutorService pool = Executors.newFixedThreadPool(10);
        Future<Integer> submit = pool.submit(callable);
        Integer integer = submit.get();
        System.out.println(integer);

        pool.shutdown();
    }
}

線程的關閉

  • shutdown():啓動線程池的關閉序列,執行之後不再接受新任務,但會將以前所有已提交任務執行完畢,當線程池中的所有任務都執行完畢後,線程池中的線程都會死亡。
  • shutdownNow():該方法視圖停止所有正在執行的活動任務,暫停處理在等待的任務,並返回等待執行的任務列表。
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章