07- 線程高級特性

票務中心案例

票 100

售票途徑 (多線程)

// 票務中心
public class Ticket implements  Runnable{
    // 票的數量爲 100 張
    int count = 100;
    @Override
    public void run() {
        // 循環賣票
        while (true){
            if(count > 0){
            // 爲了讓問題更加突顯
                try {
                    Thread.sleep(100);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                System.out.println(Thread.currentThread().getName()+"\t第"+count+"張");
            }else{
                break;
            }
            // 每循環一次, 總票數 -1
            count--;
        }
    }
}

測試類

public class TicketDemo {
    public static void main(String[] args) {
        Ticket t = new Ticket();

        Thread t1 = new Thread(t,"網絡售票");
        Thread t2 = new Thread(t,"售票窗口");
        Thread t3 = new Thread(t,"黃牛黨");

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

運行結果分析

  • 重複票 一張票賣出兩次
  • 賣票順序不同
  • 負數票

產生該問題的原因

CPU 選擇線程的隨機性

思考解決方式

在容易出現問題的代碼上 上鎖

  • 容易出現問題的代碼
一個售票員 每賣出一張票, 必須重新設置該票的總數, 中間的過程不允許被其它售票員強行中斷
一旦中斷, 很容易發生 線程安全問題

代碼鎖的格式

同步代碼鎖

synchronized (鎖對象){
	// 容易發生問題的代碼
 }
對鎖對象的要求是, 多個線程的鎖對象必須是同一個

代碼鎖演示

@Override
public void run() {
    while(true){
        synchronized (this){
            try {
                Thread.sleep(100);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            if(count <= 0){
                break;
            }
            System.out.println(Thread.currentThread().getName()+"\t第"+count+"張");
            count--;
        }
    }
}

同步方法鎖

成員方法

public void run() {
    while(true){
        try {
            Thread.sleep(10);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        sellTicket();
    }
}
public synchronized void sellTicket(){
    if(count <= 0){
        return;
    }
    System.out.println(Thread.currentThread().getName()+"\t第"+count+"張");
    count--;
    }

synchronized 存在一定有鎖對象存在

成員方法 的鎖對象爲 this

靜態方法

只是把成員方法 添加靜態修飾符

靜態方法 的鎖對象爲 該類的字節碼對象

使用同步代碼鎖優劣分析

優勢

  • 可以避免線程安全問題的出現

劣勢

  • 必須等待一個線程運行完畢同步代碼塊 之後, 其他線程纔可以運行

導致效率變低

速度和安全 對立的雙方

對比之前的對象

ArrayList 和 Vector synchronized

StringBuffer (synchronized) 和 StringBuilder

學習完同步之後再看線程狀態

在這裏插入圖片描述

同步代碼鎖, 把一塊區域的代碼 加鎖, 一次只能允許一條線程 進入加鎖的代碼

提款機, 從一個人進入提款機面前, 插卡, 輸入密碼 , 輸入提款金額, 把錢放進錢包, 退卡

其他線程 在排隊等待 即使CPU 把執行權給了 線程, 但是該線程也無法進入上鎖區域

lock鎖

lock 鎖基本介紹

	Lock實現提供比使用synchronized方法和語句可以獲得的更廣泛的鎖定操作。 它們允許更靈活的結構化,可能具有完全不同的屬性,並且可以支持多個相關聯的對象Condition 。 
	鎖是用於通過多個線程控制對共享資源的訪問的工具。 通常,鎖提供對共享資源的獨佔訪問:一次只能有一個線程可以獲取鎖,並且對共享資源的所有訪問都要求首先獲取鎖

基本使用語法

public void sellTicket(){
	// 獲得鎖。 
    lock.lock();
    if(count <= 0){
        return;
    }
    System.out.println(Thread.currentThread().getName()+"\t第"+count+"張");
    count--;
    // 釋放鎖
    lock.unlock();
}

問題分析

萬一在 lock() 和 unlock() 之間 出現了 異常

將不會執行 unlock() 釋放鎖

代碼優化方式

public void sellTicket(){
	// 獲得鎖。 
    lock.lock();
    try{
        if(count <= 0){
            return;
        }
        System.out.println(Thread.currentThread().getName()+"\t第"+count+"張");
        count--;
    }finally{
         // 釋放鎖
    	lock.unlock();
    }
}

和 synchronized 對比

	1)Lock是一個接口,而synchronized是Java中的關鍵字,synchronized是內置的語言實現,synchronized是在JVM層面上實現的,不但可以通過一些監控工具監控synchronized的鎖定,而且在代碼執行時出現異常,JVM會自動釋放鎖定,但是使用Lock則不行,lock是通過代碼實現的,要保證鎖定一定會被釋放,就必須將 unLock()放到finally{} 中;

  2)synchronized在發生異常時,會自動釋放線程佔有的鎖,因此不會導致死鎖現象發生;而Lock在發生異常時,如果沒有主動通過unLock()去釋放鎖,則很可能造成死鎖現象,因此使用Lock時需要在finally塊中釋放鎖;

  3)Lock可以讓等待鎖的線程響應中斷,線程可以中斷去幹別的事務,而synchronized卻不行,使用synchronized時,等待的線程會一直等待下去,不能夠響應中斷;

  4)通過Lock可以知道有沒有成功獲取鎖,而synchronized卻無法辦到。

  5)Lock可以提高多個線程進行讀操作的效率。

  在性能上來說,如果競爭資源不激烈,兩者的性能是差不多的,而當競爭資源非常激烈時(即有大量線程同時競爭),此時Lock的性能要遠遠優於synchronized。所以說,在具體使用時要根據適當情況選擇。

死鎖問題

// 場景模擬
張三  和  李四 

在這裏插入圖片描述

public class MyThread extends Thread{
    public static final Object left = new Object();
    public static final Object right = new Object();
    public boolean boo ;
    public MyThread(){}
    public MyThread(boolean boo) {
        this.boo = boo;
    }

    @Override
    public void run() {
        try {
            Thread.sleep(10);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        // true  張三開始從左向右走
        if(boo){
            while (true){
                leftRight();
            }
        }else{
            // false  李四開始從右向左走
            while(true){
                rightLeft();
            }
        }
    }
    public void leftRight(){
        synchronized (left){
            System.out.println("張三進入了左側房間!");
            synchronized (right){
                System.out.println("張三進入了右側房間!");
            }
        }
    }
    public void rightLeft(){
        synchronized (right){
            System.out.println("李四進入了右側房間!");
            synchronized (left){
                System.out.println("李四進入了左側房間!");
            }
        }
    }
}

測試

public class ThreadDemo {
    public static void main(String[] args) {

        MyThread t1 = new MyThread(true);
        t1.start();
        MyThread t2 = new MyThread(false);
        t2.start();
    }
}

線程池

線程的生命週期

用戶使用線程的步驟爲

創建線程 => 啓動線程 => 可運行=運行 => 阻塞或銷燬..... 

把線程看做是一輛自行車

用戶 想要從家去公司, 想要一輛自行車

買自行車 => 騎自行車 => 銷燬自行車

程序的運行結束 是需要銷燬已經創建的線程對象的, 根據自行車的案例, 現實生活中有共享單車, 可以很好地優化線程的獲取方式

租車 => 用車 => 還車
租用線程 => 使用線程 => 歸還線程

線程池

Executor

Interface Executor
界面提供了一種將任務提交從每個任務的運行機制分解的方式,包括線程使用,調度等的Executor 

Executors

static ThreadFactory defaultThreadFactory() 
返回用於創建新線程的默認線程工廠。  
static ExecutorService newCachedThreadPool() 
創建一個根據需要創建新線程的線程池,但在可用時將重新使用以前構造的線程。  
static ExecutorService newCachedThreadPool(ThreadFactory threadFactory) 
創建一個根據需要創建新線程的線程池,但在可用時將重新使用以前構造的線程,並在需要時使用提供的ThreadFactory創建新線程。  
static ExecutorService newFixedThreadPool(int nThreads) 
創建一個線程池,該線程池重用固定數量的從共享無界隊列中運行的線程。  
static ExecutorService newFixedThreadPool(int nThreads, ThreadFactory threadFactory) 
創建一個線程池,重用固定數量的線程,從共享無界隊列中運行,使用提供的ThreadFactory在需要時創建新線程。  
static ScheduledExecutorService newScheduledThreadPool(int corePoolSize) 
創建一個線程池,可以調度命令在給定的延遲之後運行,或定期執行。  
static ScheduledExecutorService newScheduledThreadPool(int corePoolSize, ThreadFactory threadFactory) 
創建一個線程池,可以調度命令在給定的延遲之後運行,或定期執行。  

案例

public class Demo1_Pool {
    public static void main(String[] args) {
        Ticket ticket = new Ticket();
        // 線程池本質上就是一個容器, 該容器存放的內容是 線程 
        // 用戶使用線程, 從該容器中自動獲取
        ExecutorService pool = Executors.newCachedThreadPool();
        pool.submit(ticket);
        pool.submit(ticket);
        pool.submit(ticket);
    }
}

優勢

  • 不需要創建線程類 , 直接使用線程池, 實現業務邏輯和線程分離
一個人 開了一個 淘寶網店賣零食, 需要組建自己的快遞團隊嗎?
人負責好自己的主業就可以了, 不要在快遞上分心, 直接把業務打包給快遞公司負責
需要很多輛自行車, 直接找共享單車合作
線程池就是屬於第三方公司
  • 使用線程池, 可以節約資源, 提高系統效率
同一輛車 可以提供給多人重複使用

線程之間的通訊問題

之前的賣票案例中 多個線程共同爭搶CPU資源

但是線程之間不存在依賴關係

兩個賣票窗口之間是相互獨立的, 雖然有共享資源存在, 兩者之間不存在先後關係!

通訊問題引入

存在依賴關係的兩個線程之間 比如超市案例 共享同一個超市共享資源的兩個線程 , 供貨商線程 消費者線程, 如果供貨商沒有供貨,消費者將無法消費, 消費者線程依賴於 供貨商線程, 一旦存在依賴關係, 需要線程之間進行通訊

案例資源

共享資源類

public class SharedData {
    private char c;
    // 是否存在 已經生產完成的字符
    private boolean isProduced  = false;

    // 生產字一個符的方法
    public void putShareChar(char c){

        this.c = c;
        // 修改狀態
        isProduced = true;
        System.out.println("生產了一個字符! "+c);
    }
    public char getShareChar(){
        // 調用該方法, 是從共享數據中 拿取一個字符
        // 把字符取出之後, 修改狀態
        isProduced = false;
        System.out.println("消費了一個字符"+c);
        return c;
    }
}

生產者類

public class Producer extends Thread{
    private SharedData sd;

    public Producer(SharedData sd) {
        this.sd = sd;
    }

    @Override
    public void run() {
        // 生產者 循環 向倉庫 放入字符, 生產商品放入倉庫
        for(char ch = 'A' ; ch <= 'D' ; ch++){
            // 時間差
            try {
                Thread.sleep((long)(Math.random()*3000));
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            sd.putShareChar(ch);
        }
    }
}

消費者類

public class Consumer extends Thread{
    private SharedData sd;
    public Consumer(SharedData sd) {
        this.sd = sd;
    }

    @Override
    public void run() {
        // 消費者去買東西
        char ch;
        do{
            // 時間差
            try {
                Thread.sleep((long)(Math.random()*3000));
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            ch = sd.getShareChar();
        }while(ch != 'D');
    }
}

測試類

public class Demo {
    public static void main(String[] args) {
        // 共享資源對象
        SharedData sd = new SharedData();

        // 生產者線程
        Producer pro = new Producer(sd);
        Consumer con = new Consumer(sd);
        pro.start();
        con.start();
    }
}

沒有同步, 沒線程之間也沒有通訊

生產者還未生產, 消費者就已經開始消費了

案例優化

public class SharedData {
    private char c;
    // 是否存在 已經生產完成的字符
    private boolean isProduced  = false;

    // 生產字一個符的方法
    public synchronized void putShareChar(char c)  {
        if(isProduced){
            //
            System.out.println("倉庫的商品 還未銷售完畢, 生產者停止生產");
            try {
                this.wait();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
        this.c = c;
        // 修改狀態
        isProduced = true;
        System.out.println("生產了一個字符! "+c);
        this.notify();
    }
    public synchronized char getShareChar(){
        // 只有當 產品有的時候, 纔可以買,
        if(!isProduced){
            // 沒有的時候只能等
            System.out.println("生產者還未生產完畢, 請等待");
            try {
                this.wait();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
        // 調用該方法, 是從共享數據中 拿取一個字符
        // 把字符取出之後, 修改狀態
        isProduced = false;
        System.out.println("消費了一個字符"+c);
        this.notify();
        return c;
    }
}

wait 和 notify

wait 等待 掛起

需要被喚醒

notify() 喚醒的方法 可以喚醒正在等待的線程

實際上是屬於 Object 類的兩個方法

調用兩個方法的主體是 共享資源對象, 因爲任何類都可以作爲共享資源而存在

wait() 和 sleep() 的區別

wait() 在 線程暫停執行之後 會釋放鎖

sleep() 在線程暫停執行之後, 不會釋放鎖

類似於 銀行提款機,張三在提款機中取錢, 突發疾病 在提款機旁 暈倒
wait()  在暈倒之前把門打開
sleep() 直接暈倒 

線程和IO流的綜合案例

使用原始方式複製多文件

package s0805;
import java.io.*;

public class Demo1_copy {
    /*
    // 1- 判斷 目標文件夾是否存在, 不存在創建
    // 2- 創建 源文件 輸入流對象
    // 3- 創建 目標文件 輸出流對象
    // 4- 使用包裝流 buffered
    // 5- 使用 for循環 多次進行文件複製
    // 6- 關閉資源
     */
    public void copyFiles(String targetDir,String ...files) throws Exception {
        // 1- 判斷 目標文件夾是否存在, 不存在創建
        File dir = new File(targetDir);
        if(!dir.exists()){
            dir.mkdirs();
        }
        // 2- 創建 源文件 輸入流對象
        // 使用 for循環
        for(String file : files){
            File sourceFile = new File(file);
            FileInputStream fis = new FileInputStream(sourceFile);
            // 3- 創建 目標文件 輸出流對象
            File targetFile = new File(targetDir,sourceFile.getName());
            FileOutputStream fos = new FileOutputStream(targetFile);
            // 4- 使用包裝流 buffered
            BufferedInputStream bis = new BufferedInputStream(fis);
            BufferedOutputStream bos = new BufferedOutputStream(fos);
            // 5-  多次進行文件複製
            byte [] bys = new byte[1024];
            int len;
            // 文件開始複製之前 打印一句話
            System.out.println(sourceFile.getName()+"開始複製");
            while( (len = bis.read(bys)) != -1){
                bos.write(bys,0,len);
            }
            System.out.println(sourceFile.getName()+"複製完成");
            // 6- 關閉資源
            bis.close();
            bos.close();
        }
    }
}

測試類

public class Demo2_copytest {
    public static void main(String[] args) throws Exception {

        Demo1_copy copy = new Demo1_copy();
        String dir = "D:\\testCopy";
        String f1 = "D:\\55- java 軟件\\Mysql\\mysql-installer-community-5.7.19.0.msi";
        String f2 = "D:\\55- java 軟件\\Mysql\\Navicat for MySQL.rar";
        String f3 = "D:\\55- java 軟件\\IDEA\\ideaIU-2017.2.6.exe";

        copy.copyFiles(dir,f1,f2,f3);
    }
}

使用多線程 完成多文件複製案例

需求分析

// 1- 新建線程類, 該類主要負責 一個文件的複製工作
// 2- 線程類需要兩個屬性, 分別表示 源文件, 和目標文件夾
// 3- 在run 方法中  把文件複製的代碼 補充完畢, (注意必須內部處理異常)
// 4- 在測試類中, 多文件複製, 每一個文件都需要開啓一條線程
// 5- 使用 線程池來優化 線程的邏輯和效率

線程類

package s0805.copyThread;

import java.io.*;
/**
 * 該線程 主要功能是 每一條線程 負責一個文件的複製拷貝工作
 */
public class CopyRunnable implements Runnable{
    // 源文件
    private String file;
    // 目標文件夾
    private String targetDir;
    public CopyRunnable(){}
    public CopyRunnable(String file, String targetDir) {
        this.file = file;
        this.targetDir = targetDir;
    }

    @Override
    public void run() {
        BufferedInputStream bis = null;
        BufferedOutputStream bos = null;
        try{
            File sourceFile = new File(file);
            FileInputStream fis = new FileInputStream(sourceFile);
            // 3- 創建 目標文件 輸出流對象
            File targetFile = new File(targetDir,sourceFile.getName());
            FileOutputStream fos = new FileOutputStream(targetFile);
            // 4- 使用包裝流 buffered
            bis = new BufferedInputStream(fis);
            bos = new BufferedOutputStream(fos);
            // 5-  多次進行文件複製
            byte [] bys = new byte[1024];
            int len;
            // 文件開始複製之前 打印一句話
            System.out.println(sourceFile.getName()+"開始複製");
            while( (len = bis.read(bys)) != -1){
                bos.write(bys,0,len);
            }
            System.out.println(sourceFile.getName()+"複製完成");
        }catch(Exception e){
            e.printStackTrace();
        }finally{
            // 6- 關閉資源
            if(bis != null){
                try {
                    bis.close();
                } catch (IOException e) {
                    bis = null;
                }
            }
            if(bos != null){
                try {
                    bos.close();
                } catch (IOException e) {
                    bos = null;
                }
            }
        }
    }
}

文件複製類

public void copyFiles(String targetDir,String ...files) throws Exception {
    // 1- 判斷 目標文件夾是否存在, 不存在創建
    File dir = new File(targetDir);
    if(!dir.exists()){
        dir.mkdirs();
    }
    // 2- 創建 源文件 輸入流對象
    // 使用 for循環
    for(String file : files){
        // 1- 創建線程對象
        CopyRunnable copyRunnable = new CopyRunnable(file, targetDir);
        // 2- 使用線程池 來執行多條線程
        ExecutorService executorService = Executors.newCachedThreadPool();
        executorService.submit(copyRunnable);
    }
}

測試類不變

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