Java多線程的應用


一、概述

  提到線程不得不提進行。因為線程是進程的一個執行單元。下面對線程和進程分別進行介紹。

  1、進程

    進程是當前操作系統執行的任務,是並發執行的程序在執行過程中分配和管理資源的基本單位,是一個動態概念,竟爭計算機系統資源的基本單位。一般而言,現在的操作系統都是多進程的。

   進程的執行過程是線狀的, 盡管中間會發生中斷或暫停,但該進程所擁有的資源只為該線狀執行過程服務。一旦發生進程上下文切換,這些資源都是要被保護起來的。

  2、線程

  線程,是進程的一部分,一個沒有線程的進程可以被看作是單線程的。即:每個進程中至少包含一個線程

  線程本身是在CPU上執行的,CPU的每一個核在同一時刻只能執行一個線程,但CPU在底層會對線程進行快速的輪詢切換。

  3、線程的特點

   線程在執行任務的過程大概可以分為2大塊:

  • 在CPU上執行
  • 和計算機的硬件進行交互。當線程和硬件進行交互(例如讀取文件)是不占用CPU的。
  • 提高CPU利用率。理論上,當線程個數足夠多的時候,CPU的利用率是能夠到達100%。
  • 一個程序的主函數所在的類默認是一個單獨的線程。

   二、JAVA中如何定義線程

   1、通過繼承Thread,重寫run方法,將要執行的邏輯放在run方法中,然后創建線程對象調用start方法來開啟線程。示例如下:

public class ThreadDemo {

    public static void main(String[] args) {

        TDemo t1 = new TDemo("A");
        // 啟動線程
        // start方法中會給線程做很多的配置
        // 配置完成之后會自動調用run方法執行指定的任務
        t1.start();
        // t1.run();
        TDemo t2 = new TDemo("B");
        t2.start();
        // t2.run();

    }

}

class TDemo extends Thread {

    private String name;

    public TDemo(String name) {
        this.name = name;
    }

    // 打印0-9
    // 線程要執行的任務就是放在這個方法中
    @Override
    public void run() {
        for (int i = 0; i < 10; i++) {
            System.out.println(name + ":" + i);
        }
    }
}

    2、實現Runnable,重寫run方法,然后利用Runnable對象來構建Thread對象,調用start方法來啟動線程。示例如下:

public class RunnableDemo {

    public static void main(String[] args) {

        RDemo r = new RDemo();
        // 包裝 - 裝飾設計模式
        Thread t = new Thread(r);
        t.start();

    }
}
class RDemo implements Runnable {

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

  3、實現Callable<T>,重寫call方法,通過線程池定義線程。示例如下:

public class CallableDemo {

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

        CDemo c = new CDemo();
        // 執行器服務 執行器助手
        ExecutorService es = Executors.newCachedThreadPool();
        Future<String> f = es.submit(c);
        System.out.println(f.get());
        es.shutdown();
    }

}

// 泛型表示要的結果類型
class CDemo implements Callable<String> {

    @Override
    public String call() throws Exception {
        return "SUCCESS";
    }

}

三、多線程的並發安全問題

 1、線程之間是相互搶占執行,而且搶占是發生在線程執行的每一步;當線程重新搶回執行權之后,會沿着上次被搶占位置繼續向下執行,而不是從頭開始執行

 2、由於線程的搶占而導致出現了不合理的數據的現象:多線程的並發安全問題。

四、線程中的鎖機制

   1、概述

    為了解決線程並發問題,引入了synchronized代碼塊,亦即同步代碼塊。同步代碼塊需要一個鎖對象。

   2、鎖對象及其特點

      鎖對象要求被當前的所有線程都認識。共享資源,方法去中的資源和this都可以作為鎖對象。

      當使用this作為鎖對象的時候,要求利用同一個Runnable對象來構建不同的Thread對象。

     示例如下:利用多線程實現賣票機制

package cn.tedu.thread;

import java.io.FileInputStream;
import java.util.Properties;

// 利用多線程機制模擬賣票場景
public class SellTicketDemo {
    public static void main(String[] args) throws Exception {
        // 利用properties做到改動數量但是不用改動代碼的效果
        Properties prop = new Properties();
        prop.load(new FileInputStream("ticket.properties"));
        int count = Integer.parseInt(prop.getProperty("count"));
        // 利用ticket對象做到所有的線程共享一個對象
        Ticket t = new Ticket();
        t.setCount(count);
        // 表示四個售票員在分別賣票
        Thread t1 = new Thread(new Seller(t), "A");
        Thread t2 = new Thread(new Seller(t), "B");
        Thread t3 = new Thread(new Seller(t), "C");
        Thread t4 = new Thread(new Seller(t), "D");

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

// 定義了線程類表示售票員
class Seller implements Runnable {

    private Ticket t;
    public Seller(Ticket t) {
        this.t = t;
    }

    @Override
    public void run() {
        // 鎖對象 --- 需要指定一個對象作為鎖來使用
        while (true) {
            // 由於所有的Seller線程都在賣票t,所以t是被所有線程都認識的
            // synchronized (t) {
            // 由於所有的Seller線程都是Seller類產生的,所以Seller類也是被所有線程都認識的
            // synchronized (Seller.class) {
            // synchronized (Thread.class) {
            synchronized ("abc") {
                if (t.getCount() <= 0)
                    break;
                try {
                    // 讓當前線程陷入休眠
                    // 時間單位是毫秒
                    // 不改變線程的執行結果
                    // 只會把線程的執行時間拉長
                    Thread.sleep(10);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                // 票數減少1張
                t.setCount(t.getCount() - 1);
                // currentThread()獲取當前在執行的線程
                // 獲取線程的名字
                String name = Thread.currentThread().getName();
                System.out.println(name + "賣了一張票,剩余" + t.getCount());
            }
        }
    }
}

class Ticket {
    private int count;

    public int getCount() {
        return count;
    }

    public void setCount(int count) {
        this.count = count;
    }
}

   3、線程的同步和異步

    同步:在同一時刻內資源/邏輯只被一個線程占用/執行。

    異步:在同一時刻內資源/邏輯可以被多個線程搶占使用。

   4、多線程死鎖

     由於多個線程之間的鎖形成了嵌套而導致代碼無法繼續執行,這種現象稱之為死鎖。

     我們只能盡量避免出現死鎖,在實際開發中,會做死鎖的檢驗;如果真的出現了死鎖,會根據線程的優先級打破其中一個或者多個鎖。

   死鎖的示例如下:

package cn.tedu.thread;

public class DeadLockDemo {

    static Printer p = new Printer();
    static Scan s = new Scan();

    public static void main(String[] args) {

        // 第一個員工
        Runnable r1 = new Runnable() {
            @Override
            public void run() {
                synchronized (p) {
                    p.print();
                    try {
                        Thread.sleep(10);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    synchronized (s) {
                        s.scan();
                    }
                }
            }
        };
        new Thread(r1).start();
        // 第二個員工
        Runnable r2 = new Runnable() {
            @Override
            public void run() {
                synchronized (s) {
                    s.scan();
                    try {
                        Thread.sleep(10);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    synchronized (p) {
                        p.print();
                    }
                }
            }
        };
        new Thread(r2).start();
    }
}

// 代表打印機的類
class Printer {
    public void print() {
        System.out.println("打印機在吱呦吱呦的打印~~~");
    }
}

// 代表掃描儀的類
class Scan {
    public void scan() {
        System.out.println("掃描儀在哼哧哼哧的掃描~~~");
    }
}

五、線程的優先級

    1、Java中將線程的優先級分為1-10共十個等級。

    2、理論上,數字越大優先級越高,那么該線程能搶到資源的概率也就越大;但實際上,相鄰的兩個優先級之間的差別非常不明顯;如果想要相對明顯一點,至少要相差5個優先級。

  線程優先級示例如下:

   

public class PriorityDemo {

    public static void main(String[] args) {

        Thread t1 = new Thread(new PDemo(), "A");
        Thread t2 = new Thread(new PDemo(), "B");

        // 在默認情況下,線程的優先級都是5
        // System.out.println(t1.getPriority());
        // System.out.println(t2.getPriority());

        // 設置優先級
        t1.setPriority(1);
        t2.setPriority(10);
        t1.start();
        t2.start();
    }
}

class PDemo implements Runnable {
    @Override
    public void run() {
        String name = Thread.currentThread().getName();
        for (int i = 0; i < 10; i++) {
            System.out.println(name + ":" + i);
        }
    }
}

六、線程的等待喚醒機制

  1、利用標記為以及wait、notify、notifyAll方法來調用線程之間的執行順序;

  2、wait、notify、notifyAll和鎖有關,用那個對象作為鎖對象使用,那么就用該鎖對象來調用wait、notify。

   等待和喚醒示例如下: 

package cn.tedu.thread;

public class WaitNotifyAllDemo {
    public static void main(String[] args) {
        Product p = new Product();

        new Thread(new Supplier2(p)).start();
        new Thread(new Supplier2(p)).start();
        new Thread(new Consumer2(p)).start();
        new Thread(new Consumer2(p)).start();
    }
}

// 生產者
class Supplier2 implements Runnable {
    private Product p;

    public Supplier2(Product p) {
        this.p = p;
    }

    @Override
    public void run() {
        while (true) {
            synchronized (p) {
//因為線程被搶斷后,會沿着停止出繼續執行,因為用while循環強制對其進行判斷,滿足條件時才能執行
//不滿足條件就讓其等待
while (p.flag == false){ try { // 讓當前線程陷入等待 p.wait(); } catch (InterruptedException e) { e.printStackTrace(); } } // 計算本次生產的商品數量 int count = (int) (Math.random() * 1000); p.setCount(count); System.out.println("生產者生產了" + count + "件商品~~~"); p.flag = false;
//當多個線程執行時,要喚醒所有的線程,否則可能連續喚起一個線程,導致程序執行混亂 p.notifyAll(); } } } }
// 消費者 class Consumer2 implements Runnable { private Product p; public Consumer2(Product p) { this.p = p; } @Override public void run() { while (true) { synchronized (p) { while (p.flag == true){ try { p.wait(); } catch (InterruptedException e) { e.printStackTrace(); } } int count = p.getCount(); p.setCount(0); System.out.println("消費者消費了" + count + "件商品~~~"); p.flag = true; // 喚醒在等待的線程 p.notifyAll(); } } } }

七、線程的狀態

  線程從創建到開始消亡一般會經歷如下幾種狀態:

八、守護線程

   1、概述

     守護其他的線程被稱為守護線程,只要被守護的線程結束,那么守護線程就會隨之結束。

   2、守護線程的特點

  • 一個線程要么是守護線程,要么是被守護線程
  • 守護線程可以守護其他的守護線程
  • 在Java中,最常見的一個守護線程是GC

守護線程的示例如下:

package cn.tedu.thread;

public class DaemonDemo {

    public static void main(String[] args) throws InterruptedException {

        Thread t1 = new Thread(new Monster(), "小怪1號");
        Thread t2 = new Thread(new Monster(), "小怪2號");
        Thread t3 = new Thread(new Monster(), "小怪3號");
        Thread t4 = new Thread(new Monster(), "小怪4號");

        // 設置為守護線程
        t1.setDaemon(true);
        t2.setDaemon(true);
        t3.setDaemon(true);
        t4.setDaemon(true);

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

        for (int i = 10; i > 0; i--) {
            System.out.println("Boss掉了一滴血,剩余" + i);
            Thread.sleep(50);
        }
    }

}
//守護boss的小怪線程
class Monster implements Runnable {

    @Override
    public void run() {
        String name = Thread.currentThread().getName();
        for (int i = 1000; i > 0; i--) {
            System.out.println(name + "掉了一滴血,剩余" + i);
            try {
                Thread.sleep(10);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
}

  總結:sleep和wait的區別

   1、sleep:在使用的時候需要指定休眠時間,單位是毫秒,到點自然醒。在無鎖狀態下,會釋放CPU;在有鎖狀態下,不釋放CPU。

     sleep方法是一個靜態方法,被設計在了Thread類上。

   2、wait:可以指定等待時間也可以不指定。如果不指定等待時間則需要被喚醒。wait必須結合鎖使用,當線程在wait的時候會釋放鎖。wait方法設計在了Object類上。

九、線程產生和結束的場景

  1、線程產生的場景

  • 系統自啟動:開機默認啟動的程序;
  • 用戶請求:QQ好友聊天;
  • 線程之間的啟動:App軟件之間帶有的插件。

   2、線程結束的場景

  • 壽終正寢:線程自然結束
  • 他殺:被其他線程kill 
  • 意外:線程因為報錯崩潰而退出  

十、JAVA虛擬機方法區和線程的關系

   1、類是存儲在方法區中的,方法區是被所有的線程共享的空間。

    2、每一個線程獨有一個棧內存。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM