Java多線程

Java多線程

基本概念

程序:是爲完成特定任務、用某種語言編寫的一組指令的集合。即一段靜態的代碼,靜態對象。java

進程:是程序的一次執行過程,或者是正在運行的一個程序。是一個動態的過程,有其自身的產生、存在和消亡的過程。進程是資源分配的基本單位。進程是動態的。安全

線程:進程進一步細分就能夠分爲線程,是一個程序內部的一條執行路徑。線程是調度和執行的基本單位,每一個線程都擁有獨立的運行棧和程序計數器(pc)。網絡

進程和線程的關係比如是工廠和工人的關係,工廠中有許多不一樣的車間,每一個車間都有許多不一樣的設備,比如是進程中的資源,而調度和執行這些設備的就是工人,那麼工人就比如是線程,一個車間有多個工人,就比如進程中的多線程。一個車間中的工人是共享這個車間的,比如一個進程中的多個線程是共享進程中的資源的。多線程

何時須要多線程

  • 程序須要同時執行兩個或多個任務
  • 程序須要實現一些須要等待的任務時,如用戶輸入、文件讀寫操做、網絡操做、搜索等
  • 須要一些後臺運行的程序時

線程的建立和使用

從建立開始

方式一

繼承Thread類app

  1. 建立一個Thread類的子類
  2. 重寫Thread類的run()方法
  3. 建立Thread類的子類的對象
  4. 經過此對象調用start()

代碼示例:ide

package demo;

/**
 * @Description:多線程的建立和使用--->輸出1-100的偶數
 * @Author:Ameng
 * @Create:2020-09-30-14:21
 */
public class ThreadTest {
    public static void main(String[] args) {
        //實例化Thread子類的對象
        MyThread t1 = new MyThread();
        //經過這個對象調用start方法
        t1.start();//啓動當前線程,調用當前線程的run()方法
    }
}


class MyThread extends Thread {
    @Override
    public void run() {
        for (int i = 0; i < 100; i++) {
            if (i % 2 == 0) {
                System.out.println(i);
            }
        }
    }

}

這裏咱們想一下,既然start最終仍是調用的線程裏重寫的run方法,那麼咱們能不能直接調用run方法呢?咱們來試試看this

package demo;

/**
 * @Description:多線程的建立和使用
 * @Author:Ameng
 * @Create:2020-09-30-14:21
 */
public class ThreadTest {
    public static void main(String[] args) {
        //實例化Thread子類的對象
        MyThread t1 = new MyThread();
        //經過這個對象調用run方法
        t1.run();
    }
}


class MyThread extends Thread {
    @Override
    public void run() {
        for (int i = 0; i < 100; i++) {
            if (i % 2 == 0) {
                System.out.println(Thread.currentThread().getName() + ":" + i);
            }
        }
    }

}

這裏咱們先來直接試試調用run()方法,輸出的格式中咱們是先獲得線程的名字而後再輸出數字。線程

執行結果:code

main:0
main:2
main:4
main:6
main:8
main:10
main:12
main:14
main:16
main:18
main:20
main:22
main:24
main:26
main:28
main:30
main:32
main:34
main:36
main:38
main:40
main:42
main:44
main:46
main:48
main:50
main:52
main:54
main:56
main:58
main:60
main:62
main:64
main:66
main:68
main:70
main:72
main:74
main:76
main:78
main:80
main:82
main:84
main:86
main:88
main:90
main:92
main:94
main:96
main:98

咱們發現輸出的線程的名字其實仍是主線程,那麼咱們要改爲start()呢?咱們來看看結果。對象

Thread-0:0
Thread-0:2
Thread-0:4
Thread-0:6
Thread-0:8
Thread-0:10
Thread-0:12
Thread-0:14
Thread-0:16
Thread-0:18
Thread-0:20
Thread-0:22
Thread-0:24
Thread-0:26
Thread-0:28
Thread-0:30
Thread-0:32
Thread-0:34
Thread-0:36
Thread-0:38
Thread-0:40
Thread-0:42
Thread-0:44
Thread-0:46
Thread-0:48
Thread-0:50
Thread-0:52
Thread-0:54
Thread-0:56
Thread-0:58
Thread-0:60
Thread-0:62
Thread-0:64
Thread-0:66
Thread-0:68
Thread-0:70
Thread-0:72
Thread-0:74
Thread-0:76
Thread-0:78
Thread-0:80
Thread-0:82
Thread-0:84
Thread-0:86
Thread-0:88
Thread-0:90
Thread-0:92
Thread-0:94
Thread-0:96
Thread-0:98

這個時候咱們發現線程的名字已經再也不是主線程了,而是一個新的線程名字,因此咱們瞭解到經過start()方法是啓動的一個新的線程,而不只僅是調用方法。因此咱們是不能直接調用run()方法的方式來啓動線程。

那麼若是咱們要再建立一個線程該如何操做呢?咱們應該是再實例化一個新的對象,用這個新的對象來啓動一個新的線程。以下所示。

package demo;

/**
 * @Description:多線程的建立和使用
 * @Author:Ameng
 * @Create:2020-09-30-14:21
 */
public class ThreadTest {
    public static void main(String[] args) {
        //實例化Thread子類的對象
        MyThread t1 = new MyThread();
        //經過這個對象調用start方法
        t1.start();

        MyThread t2 = new MyThread();
        t2.start();
    }
}


class MyThread extends Thread {
    @Override
    public void run() {
        for (int i = 0; i < 100; i++) {
            if (i % 2 == 0) {
                System.out.println(Thread.currentThread().getName() + ":" + i);
            }
        }
    }

}

運行結果

Thread-0:0
Thread-1:0
Thread-0:2
Thread-1:2
Thread-0:4
Thread-1:4
Thread-0:6
Thread-1:6
Thread-0:8
Thread-1:8
Thread-0:10
Thread-1:10
Thread-0:12
Thread-1:12
Thread-1:14
Thread-0:14
Thread-1:16
Thread-0:16
Thread-1:18
Thread-1:20
Thread-0:18
Thread-1:22
Thread-0:20
Thread-1:24
Thread-0:22
Thread-1:26
Thread-0:24
Thread-1:28
Thread-0:26
Thread-1:30
Thread-0:28
Thread-1:32
Thread-0:30
Thread-1:34
Thread-0:32
Thread-1:36
Thread-0:34
Thread-1:38
Thread-0:36
Thread-1:40
Thread-0:38
Thread-1:42
Thread-0:40
Thread-1:44
Thread-0:42
Thread-1:46
Thread-0:44
Thread-1:48
Thread-0:46
Thread-1:50
Thread-1:52
Thread-1:54
Thread-1:56
Thread-1:58
Thread-0:48
Thread-1:60
Thread-0:50
Thread-1:62
Thread-0:52
Thread-1:64
Thread-0:54
Thread-1:66
Thread-0:56
Thread-1:68
Thread-0:58
Thread-1:70
Thread-0:60
Thread-1:72
Thread-0:62
Thread-1:74
Thread-0:64
Thread-1:76
Thread-0:66
Thread-1:78
Thread-0:68
Thread-1:80
Thread-0:70
Thread-1:82
Thread-0:72
Thread-1:84
Thread-0:74
Thread-1:86
Thread-0:76
Thread-1:88
Thread-1:90
Thread-0:78
Thread-1:92
Thread-0:80
Thread-1:94
Thread-1:96
Thread-1:98
Thread-0:82
Thread-0:84
Thread-0:86
Thread-0:88
Thread-0:90
Thread-0:92
Thread-0:94
Thread-0:96
Thread-0:98

咱們發如今結果裏面,既出現了Thread-0也出現了Thread-1。這裏就至關於又新啓動了一個新的線程。

匿名對象建立

經過匿名的方式來建立Thread匿名子類。以下所示

package demo;

/**
 * @Description:多線程的建立和使用
 * @Author:Ameng
 * @Create:2020-09-30-14:21
 */
public class ThreadTest {
    public static void main(String[] args) {
        new Thread() {
            @Override
            public void run() {
                for (int i = 0; i < 100; i++) {
                    if (i % 2 == 0) {
                        System.out.println(Thread.currentThread().getName() + ":" + i);
                    }
                }
            }
        }.start();
    }
}

這裏咱們直接建立了一個匿名對象,而後在匿名對象裏面重寫run()方法,在匿名對象的後面直接調用start()方法來啓動線程,從而使代碼更簡單,而不用每次都須要去寫一個本身的類去繼承。

方式二

除了經過建立對象的方式來調用線程的方法,咱們還能夠採用接口的方式來調用線程。一般咱們叫作實現Runnable接口的方式。以下所示

package demo;

/**
 * @Description:建立多線程的方式二,實現Runnable接口
 * @Author:Ameng
 * @Create:2020-10-04-11:01
 */
public class ThreadTest1 {
    public static void main(String[] args) {
        //3.建立類的對象
        MThread mThread = new MThread();
        //4.將此對象做爲參數傳遞到Thread類的構造器中,建立Thread類的對象
        Thread t1 = new Thread(mThread);
        //5.經過Thread類的對象調用start():①啓動線程 ②調用當前線程的run()-->調用了Runnable類型的target的run()
        t1.start();
    }
}

//1.建立一個繼承於Thread類的子類
class MThread implements Runnable {
    //2.重寫Thread類的run()方法
    @Override
    public void run() {
        for (int i = 0; i < 100; i++) {
            if (i % 2 == 0) {
                System.out.println(i);
            }
        }
    }
}

運行結果

線程1:0
線程2:0
線程1:2
線程2:2
線程1:4
線程2:4
線程1:6
線程2:6
線程1:8
線程2:8
線程1:10
線程2:10
線程1:12
線程2:12
線程1:14
線程2:14
線程1:16
線程2:16
線程1:18
線程2:18
線程1:20
線程2:20
線程1:22
線程2:22
線程1:24
線程2:24
線程1:26
線程2:26
線程1:28
線程2:28
線程1:30
線程2:30
線程1:32
線程2:32
線程1:34
線程2:34
線程1:36
線程2:36
線程1:38
線程2:38
線程1:40
線程2:40
線程1:42
線程2:42
線程1:44
線程2:44
線程1:46
線程2:46
線程1:48
線程2:48
線程1:50
線程2:50
線程1:52
線程2:52
線程1:54
線程2:54
線程1:56
線程2:56
線程1:58
線程2:58
線程1:60
線程2:60
線程1:62
線程2:62
線程1:64
線程2:64
線程1:66
線程2:66
線程1:68
線程2:68
線程1:70
線程2:70
線程1:72
線程2:72
線程1:74
線程2:74
線程1:76
線程2:76
線程1:78
線程2:78
線程1:80
線程2:80
線程1:82
線程2:82
線程1:84
線程2:84
線程1:86
線程2:86
線程1:88
線程2:88
線程1:90
線程2:90
線程1:92
線程2:92
線程1:94
線程2:94
線程1:96
線程2:96
線程1:98
線程2:98

方式三

實現Callable接口

過程:

  1. 建立一個實現Callbale的類
  2. 實現call()方法,將此線程須要執行的操做聲明在這個方法中,相似run()方法
  3. 建立Callbale接口實現類的對象
  4. 將此Callable接口實現類的對象做爲參數傳遞到FutureTask構造器中,建立FutureTask的對象
  5. 將FutureTask的對象做爲參數傳遞到Thread類的構造器中,創造Thread對象,並調用start()

與使用Runnable接口相比,Callbale接口更增強大

  • 相比run()方法,call()方法能夠有返回值
  • call()方法能夠拋出異常,能夠獲取到異常的詳細信息
  • 支持泛型的返回值
  • 須要藉助FutureTask類,好比獲取返回結果

代碼示例

package demo.java2;

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

/**
 * @Description:建立線程的方式三:實現Callbale接口
 * @Author:Ameng
 * @Create:2020-10-08-11:13
 */
public class ThreadNew {
    public static void main(String[] args) {
        NumThread numThread = new NumThread();

        FutureTask futureTask = new FutureTask(numThread);

        new Thread(futureTask).start();


        try {
            Object sum = futureTask.get();
            System.out.println("偶數總和爲:" + sum);
        } catch (InterruptedException e) {
            e.printStackTrace();
        } catch (ExecutionException e) {
            e.printStackTrace();
        }
    }
}

class NumThread implements Callable {

    @Override
    public Object call() throws Exception {
        int sum = 0;
        for (int i = 1; i <= 100; i++) {
            if (i % 2 == 0) {
                System.out.println(i);
                sum += i;
            }
        }
        return sum;
    }
}

方式四

使用線程池

優勢:

  • 提升相應速度(減小了建立新線程的時間)
  • 下降資源消耗(重複利用線程池中線程,不須要每次都建立)
  • 便於線程管理
    • corePoolSize:核心池的大小
    • maximumPoolSize:最大線程數
    • keepAliveTime:線程沒有任務時最多保持多長時間後會終止

代碼示例

package demo.java2;

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

/**
 * @Description:建立線程的方式四:使用線程池
 * @Author:Ameng
 * @Create:2020-10-08-12:13
 */
public class ThreadPool {
    public static void main(String[] args) {
        ExecutorService service = Executors.newFixedThreadPool(10);

        //線程的設置和管理
//        ThreadPoolExecutor service1 = (ThreadPoolExecutor) service;
//        service1.setCorePoolSize(15);
//        service1.setKeepAliveTime();

        service.execute(new NumberThread());//適合使用於Runnable
        service.execute(new NumberThread1());//適合使用於Runnable
//        service.submit();//適合使用於Callbale
        service.shutdown();
    }
}

class NumberThread implements Runnable {

    @Override
    public void run() {
        for (int i = 1; i <= 100; i++) {
            if (i % 2 == 0) {
                System.out.println(Thread.currentThread().getName() + ":" + i);
            }
        }
    }
}

class NumberThread1 implements Runnable {

    @Override
    public void run() {
        for (int i = 1; i <= 100; i++) {
            if (i % 2 != 0) {
                System.out.println(Thread.currentThread().getName() + ":" + i);
            }
        }
    }
}

線程中經常使用方法

  • void start():啓動線程,並執行對象的run()方法
  • run():線程在被調度時執行的操做,一般須要重寫此方法
  • currentThread():靜態方法,返回執行當前代碼的線程
  • String getName():返回線程的名稱
  • void setName(String name):設置線程的名稱
  • static Thread currentThread():返回當前線程。在Thread子類中就是this,一般用於主線程和Runnable實現類
  • static void yield():線程讓步
    • 暫停當前正在執行的線程,把執行機會讓給優先級相同或更高的線程
    • 若隊列中沒有同優先級的線程,忽略此方法
  • join():當某個線程執行流中調用其餘線程的join()方法時,調用線程將被阻塞,直到join()方法加入的join線程執行完爲止
    • 低優先級的線程也能夠得到執行
  • static void sleep(long millis):(指定時間:毫秒)
    • 令當前活動線程在指定時間段內放棄對CPU控制,使其餘線程有機會被執行,時間到後從新排隊
    • 拋出interruptedException異常
  • stop():強制線程生命期結束,不推薦使用
  • boolean isAlive():返回boolean,判斷線程是否還活着

線程的調度

調度策略

  • 時間片
  • 搶佔式:高優先級的線程搶佔CPU

Java的調度方法

  • 同優先級線程組成先進先出隊列(先到先服務),使用時間片策略
  • 對高優先級,使用優先調度的搶佔式策略

線程的優先級

優先級等級
  • MAX_PRIORITY:10
  • MIN_PRIORITY:1
  • NORM_PRIORITY:5
獲取和設置當前線程優先級涉及的方法
  • getPriority():返回線程的優先值
  • setPriority(int newPriority):改變線程的優先級
說明
  • 線程建立時繼承父線程的優先級
  • 低優先級只是得到調度的機率低,並不是必定是在高優先級以後才被調用

線程的生命週期

Java中定義了線程的幾種狀態,一個完整的生命週期一般要經歷以下的五種狀態:

  • 新建:當一個Thread類或其子類的對象被聲明並建立時,新生的線程對象處於新建狀態
  • 就緒:處於新建狀態的線程被start()後,將進入線程隊列等待CPU時間片,此時它已具有了運行的條件,只是沒有分配到CPU資源
  • 運行:當就緒的線程被調度並得到CPU資源時,便進入運行狀態,run()方法定義了線程的操做和功能
  • 阻塞:在某種特殊狀況下,被人爲掛起或執行輸入輸出操做時,讓出CPU並臨時終止本身的執行,進入阻塞狀態
  • 死亡:線程完成了它的所有工做或線程被提早強制性地終止或出現異常致使結束

線程的同步(安全問題)

問題:

  1. 多個線程執行的不肯定性引發執行結果的不穩定
  2. 多個線程對同一結構的共享,會形成操做的不完整性,可能會破壞數據

出現這些問題的緣由主要就是在線程正在操做的過程中,其餘線程參與進來,也參與一樣操做從而致使問題出現。因此要解決線程安全問題就是須要當某個線程在進行操做時,其餘線程不能參與進來。

同步機制

方法一:同步代碼塊
synchronized(同步監視器){
//須要被同步的代碼,即操做共享數據的代碼
}
  • 同步監視器:鎖。任何一個類的對象,均可以充當鎖,多個線程必須共用同一把鎖。
  • 共享數據:多個線程共同操做的數據

代碼示例

package demo;

/**
 * @Description:經過加一個鎖的方式來進行線程之間的同步
 * @Author:Ameng
 * @Create:2020-10-04-20:29
 */
public class WindowTest1 {
    public static void main(String[] args) {
        Window1 w = new Window1();
        Thread t1 = new Thread(w);
        Thread t2 = new Thread(w);
        Thread t3 = new Thread(w);

        t1.setName("窗口1");
        t2.setName("窗口2");
        t3.setName("窗口3");

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

class Window1 implements Runnable {
    private int ticket = 100;
    Object obj = new Object();

    @Override
    public void run() {
        while (true) {
            synchronized (obj) {
                if (ticket > 0) {

                    try {
                        Thread.sleep(100);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    System.out.println(Thread.currentThread().getName() + ":賣票,票號爲:" + ticket);
                    ticket--;

                } else {
                    break;
                }
            }
        }
    }
}

執行結果

窗口1:賣票,票號爲:100
窗口1:賣票,票號爲:99
窗口1:賣票,票號爲:98
窗口3:賣票,票號爲:97
窗口3:賣票,票號爲:96
窗口3:賣票,票號爲:95
窗口3:賣票,票號爲:94
窗口3:賣票,票號爲:93
窗口3:賣票,票號爲:92
窗口3:賣票,票號爲:91
窗口3:賣票,票號爲:90
窗口3:賣票,票號爲:89
窗口3:賣票,票號爲:88
窗口3:賣票,票號爲:87
窗口3:賣票,票號爲:86
窗口3:賣票,票號爲:85
窗口3:賣票,票號爲:84
窗口3:賣票,票號爲:83
窗口3:賣票,票號爲:82
窗口3:賣票,票號爲:81
窗口3:賣票,票號爲:80
窗口3:賣票,票號爲:79
窗口3:賣票,票號爲:78
窗口3:賣票,票號爲:77
窗口3:賣票,票號爲:76
窗口3:賣票,票號爲:75
窗口3:賣票,票號爲:74
窗口3:賣票,票號爲:73
窗口3:賣票,票號爲:72
窗口3:賣票,票號爲:71
窗口3:賣票,票號爲:70
窗口3:賣票,票號爲:69
窗口3:賣票,票號爲:68
窗口3:賣票,票號爲:67
窗口3:賣票,票號爲:66
窗口3:賣票,票號爲:65
窗口3:賣票,票號爲:64
窗口3:賣票,票號爲:63
窗口3:賣票,票號爲:62
窗口3:賣票,票號爲:61
窗口3:賣票,票號爲:60
窗口3:賣票,票號爲:59
窗口3:賣票,票號爲:58
窗口3:賣票,票號爲:57
窗口3:賣票,票號爲:56
窗口3:賣票,票號爲:55
窗口3:賣票,票號爲:54
窗口3:賣票,票號爲:53
窗口3:賣票,票號爲:52
窗口3:賣票,票號爲:51
窗口3:賣票,票號爲:50
窗口3:賣票,票號爲:49
窗口3:賣票,票號爲:48
窗口3:賣票,票號爲:47
窗口3:賣票,票號爲:46
窗口3:賣票,票號爲:45
窗口3:賣票,票號爲:44
窗口3:賣票,票號爲:43
窗口3:賣票,票號爲:42
窗口3:賣票,票號爲:41
窗口3:賣票,票號爲:40
窗口3:賣票,票號爲:39
窗口3:賣票,票號爲:38
窗口3:賣票,票號爲:37
窗口3:賣票,票號爲:36
窗口3:賣票,票號爲:35
窗口3:賣票,票號爲:34
窗口3:賣票,票號爲:33
窗口3:賣票,票號爲:32
窗口3:賣票,票號爲:31
窗口3:賣票,票號爲:30
窗口3:賣票,票號爲:29
窗口3:賣票,票號爲:28
窗口3:賣票,票號爲:27
窗口3:賣票,票號爲:26
窗口3:賣票,票號爲:25
窗口3:賣票,票號爲:24
窗口3:賣票,票號爲:23
窗口3:賣票,票號爲:22
窗口3:賣票,票號爲:21
窗口3:賣票,票號爲:20
窗口3:賣票,票號爲:19
窗口3:賣票,票號爲:18
窗口3:賣票,票號爲:17
窗口3:賣票,票號爲:16
窗口3:賣票,票號爲:15
窗口3:賣票,票號爲:14
窗口3:賣票,票號爲:13
窗口3:賣票,票號爲:12
窗口3:賣票,票號爲:11
窗口3:賣票,票號爲:10
窗口3:賣票,票號爲:9
窗口3:賣票,票號爲:8
窗口3:賣票,票號爲:7
窗口3:賣票,票號爲:6
窗口3:賣票,票號爲:5
窗口3:賣票,票號爲:4
窗口3:賣票,票號爲:3
窗口3:賣票,票號爲:2
窗口3:賣票,票號爲:1

發如今結果中沒有出現重票或者錯票的狀況,這裏須要注意一點的是,加的鎖必須是多個線程共用的一個鎖,任何一個類的對象均可以。

上面咱們是使用Runnable接口的方式實現同步機制,那麼若是要是繼承Thread類的時候應該如何寫呢?以下所示

package demo;

/**
 * @Description:使用同步代碼塊來解決繼承Thread類的安全問題
 * @Author:Ameng
 * @Create:2020-10-04-20:51
 */
public class WindowTest2 {
    public static void main(String[] args) {
        Window2 w1 = new Window2();
        Window2 w2 = new Window2();
        Window2 w3 = new Window2();

        w1.setName("窗口1");
        w2.setName("窗口2");
        w3.setName("窗口3");

        w1.start();
        w2.start();
        w3.start();
    }
}


class Window2 extends Thread {

    private static int ticket = 100;
    private static Object obj = new Object();

    @Override
    public void run() {
        while (true) {
            synchronized (obj) {
                if (ticket > 0) {
                    try {
                        Thread.sleep(50);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    System.out.println(getName() + ":賣票,票號爲:" + ticket);
                    ticket--;
                } else {
                    break;
                }
            }
        }
    }
}

咱們須要作的改變就是將對象obj寫成靜態對象,這樣在每次對象實例化的時候,它們用的就是同一個obj對象(同步鎖)

那麼是否是咱們的同步鎖就只能是聲明一個對象呢?其實否則,除此以外,咱們還可使用this來充當同步監視器(同步鎖),可是必定要慎用,不然可能這個鎖會不起做用。另外還可使用類來充當同步監視器(同步鎖),以下所示

package demo;

/**
 * @Description:使用同步代碼塊來解決繼承Thread類的安全問題
 * @Author:Ameng
 * @Create:2020-10-04-20:51
 */
public class WindowTest2 {
    public static void main(String[] args) {
        Window2 w1 = new Window2();
        Window2 w2 = new Window2();
        Window2 w3 = new Window2();

        w1.setName("窗口1");
        w2.setName("窗口2");
        w3.setName("窗口3");

        w1.start();
        w2.start();
        w3.start();
    }
}


class Window2 extends Thread {

    private static int ticket = 100;

    @Override
    public void run() {
        while (true) {
            synchronized (Window.class) {
                if (ticket > 0) {
                    try {
                        Thread.sleep(50);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    System.out.println(getName() + ":賣票,票號爲:" + ticket);
                    ticket--;
                } else {
                    break;
                }
            }
        }
    }
}

Window.class只會被加載一次,可充當同步鎖。

方法二:同步方法

若是操做共享數據的代碼正好完整聲明在一個方法中,那麼就能夠將此方法聲明爲同步方法。以下所示

package demo;

/**
 * @Description:使用同步方法來實現Runnable接口
 * @Author:Ameng
 * @Create:2020-10-05-18:15
 */
public class WindowTest3 {
    public static void main(String[] args) {
        Window3 w = new Window3();
        Thread t1 = new Thread(w);
        Thread t2 = new Thread(w);
        Thread t3 = new Thread(w);

        t1.setName("窗口1");
        t2.setName("窗口2");
        t3.setName("窗口3");

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


class Window3 implements Runnable {
    private int ticket = 100;

    @Override
    public void run() {
        while (true) {
            show();
        }
    }

    private synchronized void show() {//默認同步監視器爲this
        if (ticket > 0) {

            try {
                Thread.sleep(50);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println(Thread.currentThread().getName() + ":賣票,票號爲:" + ticket);
            ticket--;
        }
    }
}

這裏用幾個使用一個加了鎖的方法,從而達到了同步的目的。

package demo;

/**
 * @Description:同步方法實現繼承類Thread類的線程安全問題
 * @Author:Ameng
 * @Create:2020-10-05-18:27
 */
public class WindowTest4 {
    public static void main(String[] args) {
        Window4 w1 = new Window4();
        Window4 w2 = new Window4();
        Window4 w3 = new Window4();

        w1.setName("窗口1");
        w2.setName("窗口2");
        w3.setName("窗口3");

        w1.start();
        w2.start();
        w3.start();
    }
}


class Window4 extends Thread {

    private static int ticket = 100;

    @Override
    public void run() {
        while (true) {
            show1();
        }
    }

    private static synchronized void show1() {
        if (ticket > 0) {
            try {
                Thread.sleep(50);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println(Thread.currentThread().getName() + ":賣票,票號爲:" + ticket);
            ticket--;
        }
    }
}

說明:

  • 同步方法仍然涉及到同步監視器,只是不須要咱們顯示聲明
  • 非靜態的同步方法,同步監視器:this
  • 靜態的同步方法,同步監視器:當前類

線程的死鎖

死鎖:不一樣的線程分別佔用對方須要的同步資源不放棄,都在等待對方放棄本身須要的同步資源,就造成了線程的死鎖。當出現死鎖的時候不會出現異常也不會出現提示,而只是全部線程都處於阻塞狀態,沒法繼續。

來看一個簡單的死鎖示例

package demo.java;

/**
 * @Description:線程的死鎖問題
 * @Author:Ameng
 * @Create:2020-10-05-20:31
 */
public class ThreadTest1 {
    public static void main(String[] args) {

        StringBuffer s1 = new StringBuffer();
        StringBuffer s2 = new StringBuffer();
        new Thread() {
            @Override
            public void run() {
                synchronized (s1) {
                    s1.append("a");
                    s2.append("1");

                    try {
                        Thread.sleep(100);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }

                    synchronized (s2) {
                        s1.append("b");
                        s2.append("2");

                        System.out.println(s1);
                        System.out.println(s2);
                    }
                }
            }
        }.start();
        new Thread(new Runnable() {
            @Override
            public void run() {
                synchronized (s2) {
                    s1.append("c");
                    s2.append("3");

                    try {
                        Thread.sleep(100);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }

                    synchronized (s1) {
                        s1.append("d");
                        s1.append("4");

                        System.out.println(s1);
                        System.out.println(s2);
                    }
                }
            }
        }).start();
    }
}

這段代碼程序將直接靜止,沒法執行出結果。

Lock(鎖)

在JDK5以後增長了經過顯示定義同步鎖的對象的方法來實現同步。鎖提供了對共享資源的獨佔訪問,每次只能有一個線程對Lock對象加鎖,線程開始訪問共享資源以前應先得到Lock對象。

代碼示例

package demo.java;

import java.util.concurrent.locks.ReentrantLock;

/**
 * @Description:使用鎖的方式來實現線程同步
 * @Author:Ameng
 * @Create:2020-10-07-16:46
 */
public class LockTest {

    public static void main(String[] args) {
        Window window = new Window();

        Thread t1 = new Thread(window);
        Thread t2 = new Thread(window);
        Thread t3 = new Thread(window);

        t1.setName("窗口1");
        t2.setName("窗口2");
        t3.setName("窗口3");


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

    }
}

class Window implements Runnable {
    private int ticket = 100;
    //實例化ReentrantLock
    private ReentrantLock lock = new ReentrantLock();

    @Override
    public void run() {
        while (true) {
            try {
                //調用lock方法
                lock.lock();

                if (ticket > 0) {

                    try {
                        Thread.sleep(100);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    System.out.println(Thread.currentThread().getName() + "售票,票號爲:" + ticket);
                    ticket--;
                } else {
                    break;
                }
            } finally {
                //調用unlock方法
                lock.unlock();
            }
        }
    }
}

執行結果

窗口1售票,票號爲:100
窗口1售票,票號爲:99
窗口1售票,票號爲:98
窗口1售票,票號爲:97
窗口1售票,票號爲:96
窗口1售票,票號爲:95
窗口1售票,票號爲:94
窗口1售票,票號爲:93
窗口1售票,票號爲:92
窗口1售票,票號爲:91
窗口1售票,票號爲:90
窗口1售票,票號爲:89
窗口1售票,票號爲:88
窗口1售票,票號爲:87
窗口1售票,票號爲:86
窗口2售票,票號爲:85
窗口2售票,票號爲:84
窗口2售票,票號爲:83
窗口2售票,票號爲:82
窗口2售票,票號爲:81
窗口3售票,票號爲:80
窗口3售票,票號爲:79
窗口3售票,票號爲:78
窗口3售票,票號爲:77
窗口3售票,票號爲:76
窗口3售票,票號爲:75
窗口3售票,票號爲:74
窗口3售票,票號爲:73
窗口3售票,票號爲:72
窗口3售票,票號爲:71
窗口3售票,票號爲:70
窗口3售票,票號爲:69
窗口3售票,票號爲:68
窗口3售票,票號爲:67
窗口3售票,票號爲:66
窗口3售票,票號爲:65
窗口3售票,票號爲:64
窗口3售票,票號爲:63
窗口3售票,票號爲:62
窗口3售票,票號爲:61
窗口3售票,票號爲:60
窗口3售票,票號爲:59
窗口3售票,票號爲:58
窗口3售票,票號爲:57
窗口3售票,票號爲:56
窗口3售票,票號爲:55
窗口3售票,票號爲:54
窗口3售票,票號爲:53
窗口3售票,票號爲:52
窗口3售票,票號爲:51
窗口3售票,票號爲:50
窗口3售票,票號爲:49
窗口3售票,票號爲:48
窗口3售票,票號爲:47
窗口3售票,票號爲:46
窗口3售票,票號爲:45
窗口3售票,票號爲:44
窗口3售票,票號爲:43
窗口3售票,票號爲:42
窗口3售票,票號爲:41
窗口3售票,票號爲:40
窗口3售票,票號爲:39
窗口3售票,票號爲:38
窗口3售票,票號爲:37
窗口3售票,票號爲:36
窗口3售票,票號爲:35
窗口3售票,票號爲:34
窗口3售票,票號爲:33
窗口3售票,票號爲:32
窗口3售票,票號爲:31
窗口3售票,票號爲:30
窗口3售票,票號爲:29
窗口3售票,票號爲:28
窗口3售票,票號爲:27
窗口3售票,票號爲:26
窗口3售票,票號爲:25
窗口3售票,票號爲:24
窗口3售票,票號爲:23
窗口3售票,票號爲:22
窗口3售票,票號爲:21
窗口3售票,票號爲:20
窗口3售票,票號爲:19
窗口3售票,票號爲:18
窗口3售票,票號爲:17
窗口3售票,票號爲:16
窗口3售票,票號爲:15
窗口3售票,票號爲:14
窗口3售票,票號爲:13
窗口3售票,票號爲:12
窗口3售票,票號爲:11
窗口3售票,票號爲:10
窗口3售票,票號爲:9
窗口3售票,票號爲:8
窗口3售票,票號爲:7
窗口3售票,票號爲:6
窗口3售票,票號爲:5
窗口3售票,票號爲:4
窗口3售票,票號爲:3
窗口3售票,票號爲:2
窗口3售票,票號爲:1
synchronized和lock的異同

同:

  • 都用來解決線程的安全問題

異:

  • synchronized能夠自動釋放同步監視器,而Lock須要手動的啓動同步(lock())和結束同步(unlock())

線程的通訊

線程通訊的三個方法

  • wait():執行此方法,那麼當前線程進入阻塞狀態,並釋放同步監視器
  • notify():執行此方法,就會喚醒被wait的線程,如有多個線程是wait狀態,那麼就喚醒優先級高的線程
  • notify():喚醒全部的wait狀態的線程

說明:這三個方法必須使用在同步代碼塊或同步方法中

package demo.java2;

/**
 * @Description:使用兩個線程交替打印1-100
 * @Author:Ameng
 * @Create:2020-10-07-17:15
 */
public class CommunicationTest {
    public static void main(String[] args) {
        Number number = new Number();

        Thread t1 = new Thread(number);
        Thread t2 = new Thread(number);

        t1.setName("線程1");
        t2.setName("線程2");

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

class Number implements Runnable {

    private int number = 1;

    @Override
    public void run() {
        while (true) {
            synchronized (this) {

                notify();
                try {
                    Thread.sleep(10);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                if (number <= 100) {
                    System.out.println(Thread.currentThread().getName() + ":" + number);
                    number++;

                    try {
                        wait();
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                } else {
                    break;
                }
            }
        }
    }
}

執行結果

線程1:1
線程2:2
線程1:3
線程2:4
線程1:5
線程2:6
線程1:7
線程2:8
線程1:9
線程2:10
線程1:11
線程2:12
線程1:13
線程2:14
線程1:15
線程2:16
線程1:17
線程2:18
線程1:19
線程2:20
線程1:21
線程2:22
線程1:23
線程2:24
線程1:25
線程2:26
線程1:27
線程2:28
線程1:29
線程2:30
線程1:31
線程2:32
線程1:33
線程2:34
線程1:35
線程2:36
線程1:37
線程2:38
線程1:39
線程2:40
線程1:41
線程2:42
線程1:43
線程2:44
線程1:45
線程2:46
線程1:47
線程2:48
線程1:49
線程2:50
線程1:51
線程2:52
線程1:53
線程2:54
線程1:55
線程2:56
線程1:57
線程2:58
線程1:59
線程2:60
線程1:61
線程2:62
線程1:63
線程2:64
線程1:65
線程2:66
線程1:67
線程2:68
線程1:69
線程2:70
線程1:71
線程2:72
線程1:73
線程2:74
線程1:75
線程2:76
線程1:77
線程2:78
線程1:79
線程2:80
線程1:81
線程2:82
線程1:83
線程2:84
線程1:85
線程2:86
線程1:87
線程2:88
線程1:89
線程2:90
線程1:91
線程2:92
線程1:93
線程2:94
線程1:95
線程2:96
線程1:97
線程2:98
線程1:99
線程2:100

sleep()和wait()的異同

同:

  • 均可以使得當前線程進入阻塞狀態

異:

  • 兩個方法聲明的位置不一樣,Thread類中聲明sleep(),Object類中聲明wait()
  • 調用的要求不一樣,sleep()能夠在任何須要的情景下調用,而wait()必須使用在同步代碼塊或同步方法中
  • 若兩個方法都使用在同步代碼塊或同步方法中,slepp()不會釋放鎖,而wait()會釋放同步監視器
相關文章
相關標籤/搜索