Java多線程詳解

1、進程與線程

   進程(Process) 是計算機中的程序關於某數據集合上的一次運行活動,是系統進行資源分配和調度的基本單位,是操作系統結構的基礎。線程(Thread) 是操作系統能夠進行運算調度的最小單位。它被包含在進程之中,是進程中的實際運作單位。
   程序啓動運行main方法的時候,Java虛擬機會啓動一個進程,並創建主線程main。隨着調用Thread對象的 start(),另外一個新的線程也啓動了,整個應用就在多線程下運行。 所以,一個進程可以包括多個線程。而多進程是對於操作系統和CPU而言的,比如電腦同時啓動了QQ、微信、音樂軟件等應用程序時,每個程序都是一個進程,此時CPU就是多進程的。

2、創建多線程

2.1、繼承Thread類

/**
 * @author RuiMing Lin
 * @date 2020-03-12 14:19
 */
public class MyThread extends Thread {
    
    @Override
    public void run() {
        System.out.println("mythread1");
    }
    
    public static void main(String[] args) {
        MyThread mt = new MyThread();
        mt.start();
    }
}

2.2、實現Runnable接口

/**
 * @author RuiMing Lin
 * @date 2020-03-12 14:22
 */
public class MyRunnable implements Runnable{

    @Override
    public void run() {
        System.out.println("MyRunnable 1");
    }

    public static void main(String[] args) {
        MyRunnable myRunnable = new MyRunnable();
        Thread thread = new Thread(myRunnable);
        thread.start();
    }
}

2.3、使用匿名內部類實現

/**
 * @author RuiMing Lin
 * @date 2020-03-12 14:25
 */
public class Demo1 {
    public static void main(String[] args) {
        Thread thread1 = new Thread(new Runnable() {
            @Override
            public void run() {
                System.out.println("使用匿名內部類實現");
            }
        });
        thread1.start();
    }
}

2.4、實現Runnable接口的好處

  實現Runnable接口比繼承Thread類所具有的優勢:

  1. 可以避免java中的單繼承的侷限性;
  2. 增加程序的健壯性,實現解耦操作;
  3. 線程池只能放入實現Runable或Callable類線程,不能直接放入繼承Thread的類。

2.5、使用Callable和Future創建線程

  Callable是一個接口,有點類似於Runnable接口,不過較之更加強大。Callable提供了一個類似於run方法的call(),但call()更加強大。主要體現在兩點:call()可以有返回值,call()可以拋出異常。
  理論上我們可以類似於上述匿名內部類的方法將Callable接口傳遞給Thread的構造方法,但實際上不行,因爲Thread類提供的構造方法沒有傳遞Callable參數類型的,而Callable又不是Runnable的子接口。所以就有了Future接口,該接口是Runnable的子接口,並有一個實現類FutureTask封裝了Callable接口。
在這裏插入圖片描述

import java.util.concurrent.Callable;
import java.util.concurrent.FutureTask;

/**
 * @author RuiMing Lin
 * @date 2020-03-12 14:25
 */
public class Demo1 {
    public static void main(String[] args) throws Exception{
        FutureTask<Integer> futureTask = new FutureTask<Integer>(new Callable<Integer>() {
            @Override
            public Integer call() throws Exception {
                int i = 0;
                for ( ;i < 10; i++){
                    System.out.println(Thread.currentThread().getName() + "i = " + i);
                }
                return i;
            }
        });
        new Thread(futureTask).start();
        Integer integer = futureTask.get();//返回call()的返回值,必須等到子棧結束後纔會得到返回值,會導致程序阻塞
        System.out.println("integer = " + integer);
    }
}

3、線程的生命週期

新建態: 當一個線程被new出來,也就是被創建出來的時候;
就緒態: 被創建的線程調用start()方法
     阻塞態線程休眠時間到或者獲得同步鎖等等;
運行態: 就緒態線程獲得CPU資源;
阻塞態: 運行態線程調用sleep()方法進行休眠或者等待同步鎖中;
死亡態: 線程被關閉或者出現異常等等。
在這裏插入圖片描述

4、幾種特殊線程

4.1、join線程

Thread的join()可以讓一個線程等待調用該方法的線程完全完成的方法:

/**
 * @author RuiMing Lin
 * @date 2020-03-13 20:39
 */
public class MyRunnable1 implements Runnable{
    @Override
    public void run() {
        for (int i = 0; i < 20; i++) {
            System.out.println(Thread.currentThread().getName() + "執行了" + i + "次");
        }
    }
    public static void main(String[] args) {
        for (int i = 0; i < 100; i++) {
            if (i == 20){
                Thread thread = new Thread(new MyRunnable1());
                thread.setName("我的線程");
                thread.start();
                try {
                    thread.join();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
            System.out.println("主線程main方法執行了" + i + "次");
        }
    }
}

結果輸出爲:
在這裏插入圖片描述
如果不使用join()方法:

/**
 * @author RuiMing Lin
 * @date 2020-03-13 20:39
 */
public class MyRunnable1 implements Runnable{
    @Override
    public void run() {
        for (int i = 0; i < 20; i++) {
            System.out.println(Thread.currentThread().getName() + "執行了" + i + "次");
        }
    }
    public static void main(String[] args) {
        for (int i = 0; i < 100; i++) {
            if (i == 20){
                Thread thread = new Thread(new MyRunnable1());
                thread.setName("我的線程");
                thread.start();
//                try {
//                    thread.join();
//                } catch (InterruptedException e) {
//                    e.printStackTrace();
//                }
            }
            System.out.println("主線程main方法執行了" + i + "次");
        }
    }
}

結果輸出爲:
在這裏插入圖片描述
  原因分析:當主線程的i = 20時,創建自定義線程,開啓自定義線程並調用join方法,此時主線程main()被掛起,直到自定義線程完全執行結束後才放行讓主線程執行。

4.2、守護線程

守護線程表示只要被守護的線程一結束,它也就被結束了。調用方法:

thread.setDaemon(true);

4.3、休眠線程

運行態的線程調用sleep()進入休眠線程,調用方法:

Thread.sleep(1000);

5、線程安全與線程同步

5.1、安全問題

先拋出一個線程之間的安全問題:如今有十張火車票,兩個窗口同時售賣

/**
 * @author RuiMing Lin
 * @date 2020-03-13 15:57
 */
public class Ticket1 implements Runnable{
    
    private int ticket = 10;     // 表示有十張火車票

    @Override
    public void run() {
        for (int i = 0; i < 10; i++) {
            if (ticket > 0){
                System.out.println(Thread.currentThread().getName()
                        + "賣了編號爲" + ticket + "的火車票");
                ticket --;
                try {
                    Thread.sleep(500);
                }catch (InterruptedException e){
                    e.printStackTrace();
                }
            }
        }
    }

    public static void main(String[] args) {
        Ticket1 ticket = new Ticket1();
        Thread thread1 = new Thread(ticket);
        Thread thread2 = new Thread(ticket);
        thread1.setName("窗口A");
        thread2.setName("窗口B");
        thread1.start();
        thread2.start();
    }
}

結果輸出爲:

窗口A賣了編號爲10的火車票
窗口B賣了編號爲9的火車票
窗口A賣了編號爲8的火車票
窗口B賣了編號爲8的火車票
窗口A賣了編號爲6的火車票
窗口B賣了編號爲6的火車票
窗口B賣了編號爲4的火車票
窗口A賣了編號爲4的火車票
窗口B賣了編號爲2的火車票
窗口A賣了編號爲2的火車票

  很明顯,這樣的結果是不滿足需求的。原因分析:因爲多線程是CPU隨機執行的,當線程一執行System.out.println(Thread.currentThread().getName()+ “賣了編號爲” + ticket + “的火車票”)語句時,還未執行ticket --跳轉到線程二,此時就會出現數據沒有及時更新、同時售賣同一張票的情況。

5.2、解決方案一 ---- 同步代碼塊

  對需要連續執行的代碼加一把鎖,稱爲同步代碼塊。代碼代碼塊中的代碼必須執行完纔會切換到其他線程,這樣就能保證了線程之間的安全。

/**
 * @author RuiMing Lin
 * @date 2020-03-13 15:57
 */
public class Ticket2 implements Runnable{

    private int ticket = 10;     // 表示有十張火車票

    @Override
    public void run() {
        for (int i = 0; i < 10; i++) {
            synchronized (this){
                if (ticket > 0){
                    System.out.println(Thread.currentThread().getName()
                            + "賣了編號爲" + ticket + "的火車票");
                    ticket --;
                    try {
                        Thread.sleep(400);
                    }catch (InterruptedException e){
                        e.printStackTrace();
                    }
                }
            }
        }
    }

    public static void main(String[] args) {
        Ticket2 ticket = new Ticket2();
        Thread thread1 = new Thread(ticket);
        Thread thread2 = new Thread(ticket);
        thread1.setName("窗口A");
        thread2.setName("窗口B");
        thread1.start();
        thread2.start();
    }
}

結果輸出爲:

窗口A賣了編號爲10的火車票
窗口B賣了編號爲9的火車票
窗口B賣了編號爲8的火車票
窗口B賣了編號爲7的火車票
窗口B賣了編號爲6的火車票
窗口B賣了編號爲5的火車票
窗口B賣了編號爲4的火車票
窗口B賣了編號爲3的火車票
窗口B賣了編號爲2的火車票
窗口A賣了編號爲1的火車票

可以看出,這樣就滿足需求了。

5.3、解決方案二 ---- 同步方法

/**
 * @author RuiMing Lin
 * @date 2020-03-13 15:57
 */
public class Ticket2 implements Runnable{

    private int ticket = 10;     // 表示有十張火車票
    private synchronized void sale(){
        if (ticket > 0){
            System.out.println(Thread.currentThread().getName()
                    + "賣了編號爲" + ticket + "的火車票");
            ticket --;
            try {
                Thread.sleep(400);
            }catch (InterruptedException e){
                e.printStackTrace();
            }
        }
    }

    @Override
    public void run() {
        for (int i = 0; i < 10; i++) {
            sale();
        }
    }

    public static void main(String[] args) {
        Ticket2 ticket = new Ticket2();
        Thread thread1 = new Thread(ticket);
        Thread thread2 = new Thread(ticket);
        thread1.setName("窗口A");
        thread2.setName("窗口B");
        thread1.start();
        thread2.start();
    }
}

結果輸出:

窗口A賣了編號爲10的火車票
窗口A賣了編號爲9的火車票
窗口A賣了編號爲8的火車票
窗口A賣了編號爲7的火車票
窗口A賣了編號爲6的火車票
窗口A賣了編號爲5的火車票
窗口B賣了編號爲4的火車票
窗口A賣了編號爲3的火車票
窗口B賣了編號爲2的火車票
窗口A賣了編號爲1的火車票

6、線程通信

6.1、API

wait():導致當前線程等待;
notify():喚醒共享當前同一把鎖的線程其中之一;
notifyAll():喚醒共享當前同一把鎖的所有線程。

6.2、案例

需求:實現1與2交替出現

/**
 * @author RuiMing Lin
 * @date 2020-03-13 21:03
 */
public class Communication {
    public static Object lock = new Object();   // 定義一把鎖

    public static void main(String[] args) {
        MyThread1 myThread1 = new MyThread1();
        MyThread2 myThread2 = new MyThread2();
        myThread1.start();
        myThread2.start();

    }
}

// 定義通訊類一
class MyThread1 extends Thread{
    @Override
    public void run() {
        for (int i = 0; i < 10; i++) {
            synchronized (Communication.lock){      //互相通信的線程必須共享同一把鎖
                System.out.print(1);
                Communication.lock.notify();
                try {
                    Communication.lock.wait();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }
    }
}
class MyThread2 extends Thread{
    @Override
    public void run() {
        for (int j = 0; j < 10; j++) {
            synchronized (Communication.lock){
                System.out.print(2);
                Communication.lock.notify();
                try {
                    Communication.lock.wait();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }
    }
}

7、線程池

7.1、線程池介紹

  線程池是一個容納多個線程的容器,其中的線程可以反覆使用,省去了頻繁創建線程對象的操作, 無需反覆創建線程而消耗過多資源。這樣做有三個好處:1.降低資源消耗,減少了創建和銷燬線程的次數,每個工作線程都可以被重複利用,可執行多個任務;2. 提高響應速度,當任務到達時,任務可以不需要的等到線程創建就能立即執行;3. 提高線程的可管理性。可以根據系統的承受能力,調整線程池中工作線線程的數目,防止因爲消耗過多的內 存,而把服務器累趴下。

7.2、線程池的使用

import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

/**
 * @author RuiMing Lin
 * @date 2020-03-13 21:22
 */
public class Pool {
    public static void main(String[] args) {
        // 1.創建一個容量爲5的線程池
        ExecutorService service = Executors.newFixedThreadPool(5);
        // 2.創建Runnable對象
        MyRunnable runnable1 = new MyRunnable();
        MyRunnable runnable2 = new MyRunnable();
        MyRunnable runnable3 = new MyRunnable();
        MyRunnable runnable4 = new MyRunnable();
        MyRunnable runnable5 = new MyRunnable();
        // 3.調用線程池方法
        service.submit(runnable1);
        service.submit(runnable2);
        service.submit(runnable3);
        service.submit(runnable4);
        service.submit(runnable5);
        // 4.無需關閉線程,run()結束後自動將線程歸還給線程池
        // 5.關閉線程池
        service.shutdown();
    }
}

有錯誤的地方敬請指出!覺得寫得可以的話麻煩給個贊!歡迎大家評論區或者私信交流!

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