java學習day17——線程安全

線程安全的概述

多線程訪問了共享數據,會產生線程安全的問題
只有多線程會產生安全問題

線程安全產生的原理

在這裏插入圖片描述

線程安全解決方法

第一種方法——同步代碼塊

/*
賣票案例出現了線程安全問題,邁出了不存在的票和重複的票
解決線程安全的一種方式:使用同步代碼塊
格式:
    synchronized(鎖對象){
        可能會出現線程安全的代碼(訪問了共享數據的代碼)
    }
注意:
    1. 同步代碼塊中的鎖對象,可以是任意的對象
    2. 必須保證多個線程使用的鎖對象是同一個
    3. 鎖對象作用:
        把同步代碼塊所著,只讓一個線程在同步代碼塊中執行
 */
public class RunnableImpl implements Runnable {
    // 定義一個多個線程共享的票源
    private int ticket = 100;
    // 創建一個鎖對象,必須創建在run的外面
    Object obj = new Object();
    @Override
    public void run() {
        while(true){
            synchronized (obj){
                if (ticket>0){
                    // 提高安全問題出現的概率,讓程序睡眠
                    try {
                        Thread.sleep(10);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    // 票存在,賣票
                    System.out.println(Thread.currentThread().getName()+"-->正在賣第"+ticket+"張票");
                    ticket--;
            }
            }
        }
    }
}
/*
模擬買票
創建三個線程,同時開啓,對共享的票進行出售
 */
public class Demo01Ticket {
    public static void main(String[] args) {
        RunnableImpl run = new RunnableImpl();
        Thread t1 = new Thread(run);
        Thread t2 = new Thread(run);
        Thread t3 = new Thread(run);

        t1.start();
        t2.start();
        t3.start();
    }
}

同步代碼塊的原理
在這裏插入圖片描述

第二種方法——同步方法

/*
第二種方式:使用同步方法
使用步驟:
    1. 把訪問了共享數據的代碼取出來,放到一個方法中
    2. 在方法上添加synchronized修飾符
格式:定義方法的格式
修飾符 synchronized 返回值類型 方法名(參數列表){
    可能會出現線程安全的代碼(訪問了共享數據的代碼)
}
 */
public class RunnableImpl implements Runnable {
    // 定義一個多個線程共享的票源
    private int ticket = 100;
    // 創建一個鎖對象,必須創建在run的外面
    Object obj = new Object();
    @Override
    public void run() {
        while(true){
            payTicket();
        }
    }
    // 定義一個同步方法
    public synchronized void payTicket(){
        if (ticket>0) {
            // 提高安全問題出現的概率,讓程序睡眠
            try {
                Thread.sleep(10);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            // 票存在,賣票
            System.out.println(Thread.currentThread().getName() + "-->正在賣第" + ticket + "張票");
            ticket--;
        }
    }
}

同步方法也會把方法內部的代碼鎖住,只讓一個線程執行
同步方法的鎖對象是誰? 就是實現類對象new Runnable(),也就是this

第三種方法——靜態同步方法(瞭解)

public class RunnableImpl implements Runnable {
    // 定義一個多個線程共享的票源
    private static int ticket = 100;//靜態同步方法訪問的變量也必須是靜態的
    // 創建一個鎖對象,必須創建在run的外面
    Object obj = new Object();
    @Override
    public void run() {
        while(true){
            payTicketStatic();
        }
    }

    /*
    靜態同步方法的鎖對象是誰?
    不能是this,this是創建對象之後產生的,靜態方法優先於對象,
    靜態方法的鎖對象是本類的class屬性——class文件對象(反射)
     */
    public static synchronized void payTicketStatic(){
        if (ticket>0) {
            // 提高安全問題出現的概率,讓程序睡眠
            try {
                Thread.sleep(10);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            // 票存在,賣票
            System.out.println(Thread.currentThread().getName() + "-->正在賣第" + ticket + "張票");
            ticket--;
        }
    }
}

第四種方法——Lock鎖

/*
解決線程安全的第四種解決方案:Lock鎖
java.util.concurrent.locks
Lock實現提供了比使用synchronized方法和語句可獲得更廣泛的鎖定操作
Lock接口種的方法:
 void lock() 獲取鎖。
 void unlock() 釋放鎖。
java.util.concurrent.locks.ReentrantLock implements Lock接口
使用步驟:
    1. 在成員位置創建一個ReentrantLock對象
    2. 在可能會出現安全問題的代碼前調用Lock接口種的方法Lock獲取鎖
    3. 在可能會出現安全問題的代碼後調用Lock接口種的方法unLock釋放鎖
 */
public class RunnableImpl implements Runnable {
    // 定義一個多個線程共享的票源
    private int ticket = 100;
    // 1. 在成員位置創建一個ReentrantLock對象
    Lock l = new ReentrantLock();
    @Override
    public void run() {
        while(true){
            // 2. 在可能會出現安全問題的代碼前調用Lock接口種的方法Lock獲取鎖
            l.lock();
            if (ticket>0) {
                // 提高安全問題出現的概率,讓程序睡眠
                try {
                    Thread.sleep(10);
                    // 票存在,賣票
                    System.out.println(Thread.currentThread().getName() + "-->正在賣第" + ticket + "張票");
                    ticket--;
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }finally {
                    // 無論程序是否異常,否會把鎖釋放
                    //3. 在可能會出現安全問題的代碼後調用Lock接口種的方法unLock釋放鎖
                    l.unlock();
                }
            }
        }
    }
}

線程狀態

在這裏插入圖片描述
TimeWaiting線程狀態圖
在這裏插入圖片描述
Blocked狀態
在這裏插入圖片描述

Waiting(無限等待)

等待喚醒案例代碼實現

/*
等待喚醒案例:線程之間的通信
    創建一個顧客線程(消費者):告知老闆要的包子的種類和數量,調用wait方法,進入到WAITING狀態(無限等待)
    創建一個老闆線程(生產者):花了5秒做包子,做好包子後,調用notify方法,喚醒顧客喫包子
注意:
    顧客和老闆線程必須使用同步代碼塊包裹起來,保證等待和喚醒只能由一個在執行
    同步使用的鎖對象必須保證唯一
    只有鎖對象才能調用wait和notify方法
Object類種的方法
 void wait()
          在其他線程調用此對象的 notify() 方法或 notifyAll() 方法前,導致當前線程等待。
 void notify()
          喚醒在此對象監視器上等待的單個線程。
 */
public class Demo01WaitAndNotify {
    public static void main(String[] args) {
        Object obj = new Object();
        // 顧客的線程
        new Thread(){
            @Override
            public void run() {
                // 保證等待和喚醒的線程只能有一個執行,需要使用同步技術
                synchronized (obj){
                    System.out.println("告知老闆要的包子的種類和數量");
                    try {
                        obj.wait();
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    // 喚醒之後執行的代碼
                    System.out.println("開喫");
                }
            }
        }.start();
        // 老闆的線程
        new Thread(){
            @Override
            public void run() {
                try {
                    Thread.sleep(5000);//畫5s做包子
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                synchronized (obj){
                    System.out.println("告知顧客,可以喫包子");
                    obj.notify();
                }
            }
        }.start();
    }
}

進入到TimeWaiting(計時等待)有兩種方式

  1. 使用sleep(long m)方法,在毫秒值結束之後,線程睡醒進入到Runnable/Blocked狀態
  2. 使用wait(long m)方法如果在毫秒值結束後還沒有被notify喚醒,就會自動醒來,線程睡醒進入到Runnable/Blocked狀態
    喚醒方法:
    void notify() 喚醒在此對象監視器上等待的單個線程。有多個等待線程時隨機喚醒一個
    void notifyAll() 喚醒在此對象監視器上等待的所有線程。

線程間通信——等待喚醒機制

在這裏插入圖片描述
在這裏插入圖片描述

線程池

頻繁創建線程和銷燬線程需要時間
在這裏插入圖片描述

/*
線程池:JDK1.5之後提供的
java.util.concurrent.Executors:線程池的工廠類,用來生成線程池
Executors類種的靜態方法
    static ExecutorService newFixedThreadPool(int nThreads) 創建一個可重用固定線程數的線程池,以共享的無界隊列方式來運行這些線程。
    返回值:
        ExecutorService接口:返回的是ExecutorService接口的實現類對象,我們可以使用ExecutorService接口接收(面向接口編程)
    java.util.concurrent.ExecutorService:線程池接口
        用來從線程池獲取線程,調用start方法,執行線程任務
        submit(Runnable task) 提交一個 Runnable 任務用於執行,並返回一個表示該任務的 Future。
        關閉線程池的方法:
            void shutdown() 啓動一次順序關閉,執行以前提交的任務,但不接受新任務。
        線程池的使用步驟:
            1. 使用線程池的工廠類Executors裏面提供的newFixedThreadPool生產一個指定線程數量的線程池
            2. 創建一個類,實現Runnable接口,重寫run方法,設置線程任務
            3. 調用ExecutorService中的submit,傳遞線程任務(實現類),開啓線程,執行run方法
            4. 調用ExecutorService中的shutdown,銷燬線程池(不建議使用)
 */
public class Demo01ThreadPool {
    public static void main(String[] args) {
        ExecutorService es = Executors.newFixedThreadPool(2);
        es.submit(new RunnableImpl());//pool-1-thread-1創建了一個新的線程執行
        // 線程池會一直開啓,會自動把線程歸還給線程池
        es.submit(new RunnableImpl());//pool-1-thread-2創建了一個新的線程執行
        es.submit(new RunnableImpl());//pool-1-thread-2創建了一個新的線程執行
    }
}
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章