天天看點

詳解Java多線程(超級爆肝)前言1、線程簡介2、實作線程的兩種方式3、線程的生命周期4、線程的同步5、線程的死鎖問題6、線程的通信總結

文章目錄

  • 前言
  • 1、線程簡介
      • 1.1、線程與程序
      • 1.2、同步與異步
      • 1.3、并行與并發
  • 2、實作線程的兩種方式
      • 2.1、繼承Thread類
      • 2.2、實作Runnable接口
  • 3、線程的生命周期
    • 3.1、操作線程的方法
      • 3.1.1、線程的休眠
      • 3.1.2、線程的加入
      • 3.1.3、線程的中斷
  • 4、線程的同步
      • 4.1、線程安全
      • 4.2、線程同步機制
  • 5、線程的死鎖問題
  • 6、線程的通信
      • 6.1、常用通信方法
  • 總結

前言

如果一次隻完成一件事情,會很容易實作,但在現實生活中很多事情都是同時進行的,是以在Java中為了模拟這種常态,引入了線程機制。簡單來說,當程式同時完成很多事情時,就是所謂的多線程程式。 随着計算機的配置越來越高,我們需要将程序進一步優化,細分為線程,充分提高圖形化界面的多線程的開發。這就要求對線程的掌握很徹底。 那麼話不多說,進入正題,今天小夥子将記錄自己線程的學習。

提示:以下是本篇文章正文内容,下面案例可供參考

1、線程簡介

1.1、線程與程序

程式:是為完成特定任務,用某種語言編寫的一組指令的集合,即指一段靜态的代碼,靜态對象。
程序:是程式的一次執行過程,或是正在運作的一個程式,是一個動态的過程,有它自身的産生,存在和消亡的過程。-------生命周期
線程:程序可進一步細化為線程,是一個程式内部的一條執行路徑
           

1.2、同步與異步

同步:就是在發出一個功能調用時,在沒有得到結果之前,該調用就不傳回。也就是必須一件一件事做,等前一件做完了才能做下一件事.就像早上起床後,先洗涮,然後才能吃飯,不能在洗涮沒有完成時,就開始吃飯.
異步:概念和同步相對。當一個異步過程調用發出後,調用者不能立刻得到結果。實際處理這個調用的部件在完成後,通過狀态、通知和回調來通知調用者。

1.3、并行與并發

并行:多個cpu執行個體或者多台機器同時執行一段處理邏輯,是真正的同時。比如:多個人同時做不同的事
并發:通過cpu排程算法,讓使用者看上去同時執行,實際上從cpu操作層面不是真正的同時。并發往往在場景中有公用的資源,那麼針對這個公用的資源往往産生瓶頸,我們會用TPS或者QPS來反應這個系統的處理能力。比如:一個CPU(采用時間片)同時執行多個任務,比如秒殺平台,多個人做同件事

2、實作線程的兩種方式

2.1、繼承Thread類

思路:

1.建立一個繼承于Thread類的子類 (通過ctrl+o(override)輸入run查找run方法)

2.覆寫(重寫)Thread類的run()方法

3.建立Thread子類(線程類)的對象

4.通過此對象調用start()方法(啟動之後會自動調用重寫的run方法執行線程)

繼承Thread類建立一個新的線程文法如下:

public class MyThread extends Thread {

}
           

start與run方法的差別:

start方法的作用:1.啟動目前線程 2.調用目前線程的重寫的run方法(在主線程中生成子線程,有兩條線程)

調用start方法以後,一條路徑代表一個線程,同時執行兩線程時,因為時間片的輪換,是以執行過程随機配置設定,且一個線程對象隻能調用一次start方法。

run方法的作用:在主線程中調用以後,直接在主線程一條線程中執行了該線程中run的方法。(調用線程中的run方法,隻調用run方法,并不新開線程)

總結:我們不能通過run方法來新開一個線程,隻能調用線程中重寫的run方法(可以線上程中不斷的調用run方法,但是不能開啟子線程,即不能同時幹幾件事),start是開啟線程,再調用方法(即預設開啟一次線程,調用一次run方法,可以同時執行幾件事)

2.2、實作Runnable接口

因為Java語言中不支援多繼承,這時候就需要實作Runnable接口使其具有使用線程的功能。

思路步驟

1、建立Runnable對象
2、使用參數為Runnable對象的構造方法建立Tread執行個體
3、調用start()方法啟動線程
           

實作Runnable接口的文法如下:

public class MyThread extends Thread implements Runnable {

}
           

比較建立線程的兩種方式:

開發中,優先選擇實作Runable接口的方式

原因

1:實作的方式沒有類的單繼承性的局限性

2:實作的方式更适合用來處理多個線程有共享資料的情況

聯系:Thread也是實作自Runable,兩種方式都需要重寫run()方法,将線程要執行的 邏輯聲明在run中

3、線程的生命周期

概述:

線程具有生命周期,其中包含5種狀态: 出生狀态、就緒狀态、運作狀态、暫停狀态(包括休眠、等待和阻塞等)以及死亡狀态。

出生狀态就是線程被建立時處于的狀态,在使用者使用該線程執行個體調用start()方法之前,線程都處于出生狀态;當使用者調用start()方法後,線程處于就緒狀态(又被稱為可執行狀态);當線程得到系統資源後就進入運作狀态。

一旦進入運作狀态,它就會在就緒和運作狀态下轉換,同時也有可能進入暫停或者死亡狀态。當處于運作狀态下的線程調用 sleep()方法,wait()方或者阻塞解除時,會進入暫停狀态;當休眠結束、調用notify()方法、notifyAll()方法或者阻塞解除時,會進入就緒狀态;當線程的run()方法執行完畢,或者線程發生錯誤、異常時,線程進入死亡狀态。

詳解Java多線程(超級爆肝)前言1、線程簡介2、實作線程的兩種方式3、線程的生命周期4、線程的同步5、線程的死鎖問題6、線程的通信總結

3.1、操作線程的方法

3.1.1、線程的休眠

sleep()方法的使用:

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

3.1.2、線程的加入

join()方法的使用:

當某個線程使用join()方法加入另外一個線程時,另一個線程會等待該線程執行完畢後再繼續執行。

3.1.3、線程的中斷

以往有的時候會使用stop()方法停止線程,但JDK早已廢除了stop()方法,不建議使用stop()方法來 停止一個線程的運作。現在提倡在run()方法中使用無限循環的形式,然後使用一個布爾類型标記控制循環的停止。

例如,建立一個InterruptedTest類,該類實作了Runnable接口,并設定線程正确的停止方式,

public class InterrupedTest implements Runnable{
        private boolean isContinue = false;    //設定一個标記變量,預設值為false
        @Override
        public void run() {
            while(true){
                //...
                if(isContinue){
                    break;
                }
            }
        }
        public void setContinue(){
            this.isContinue = true;
        }
    }
           

4、線程的同步

4.1、線程安全

什麼是線程安全問題呢?

線程安全問題是指,多個線程對同一個共享資料進行操作時,線程沒來得及更新共享資料,進而導緻另外線程沒得到最新的資料,進而産生線程安全問題。

上述例子中:建立三個視窗賣票,總票數為100張票

1.賣票過程中,出現了重票(票被反複的賣出,ticket未被減少時就列印出了)錯票。

2.問題出現的原因:當某個線程操作車票的過程中,尚未完成操作時,其他線程參與進 來,也來操作車票。(将此過程的代碼看作一個區域,當有線程進去時,裝鎖,不讓别的線程進去)

生動了解的例子:有一個廁所,有人進去了,但是沒有上鎖,于是别人不知道你進去了,别人也進去了對廁所也使用造成錯誤。

3.如何解決:當一個線程在操作ticket時,其他線程不能參與進來,直到此線程的生命周期結束

4.在java中,我們通過同步機制,來解決線程的安全問題。

4.2、線程同步機制

方式一: 同步代碼塊

使用關鍵字Synchronized同步螢幕(鎖)

Synchronized(object){

//需要被同步的代碼

}

public static void main(String[] args) {
        //線程不安全
        //解決方案1:同步代碼塊

        //Object o = new Object();
        Runnable run = new Ticket();

        new Thread(run).start();
        new Thread(run).start();
        new Thread(run).start();

    }
    static class Ticket implements Runnable{
        private  int count = 10;
        private Object o = new Object();
        @Override
        public void run() {
            while (true){
                synchronized(o){
                    if(count > 0){
                        System.out.println("正在準備賣票!");
                        count--;
                        System.out.println(Thread.currentThread().getName()+"出票成功,餘票:"+count);
                        try{
                            Thread.sleep(1000);
                        } catch (InterruptedException e) {
                            e.printStackTrace();
                        }
                    }else{
                        break;
                    }
                }
            }
        }
    }
           

注意:

操作共享資料的代碼(所有線程共享的資料的操作的代碼)(視作衛生間區域(所有人共享的廁所)),即為需要共享的代碼(同步代碼塊,在同步代碼塊中,相當于是一個單線程,效率低)

共享資料:多個線程共同操作的資料,比如公共廁所就類比共享資料

同步螢幕(俗稱:鎖):任何一個的對象都可以充當鎖。(但是為了可讀性一般設定英文成lock)當鎖住以後隻能有一個線程能進去(要求:多個線程必須要共用同一把鎖,比如火車上的廁所,同一個标志表示有人)

Runable天生共享鎖,而Thread中需要用static對象或者this關鍵字或者目前類(window。class)來充當唯一鎖

方式二: 同步方法

使用同步方法,對方法進行synchronized關鍵字修飾

将同步代碼塊提取出來成為一個方法,用synchronized關鍵字修飾此方法。

對于runnable接口實作多線程,隻需要将同步方法用synchronized修飾

而對于繼承自Thread方式,需要将同步方法用static和synchronized修飾,因為對象不唯一(鎖不唯一)

public static void main(String[] args) {
        //線程不安全
        //解決方案2:同步方法

        //Object o = new Object();
        Runnable run = new demo2.Ticket();

        new Thread(run).start();
        new Thread(run).start();
        new Thread(run).start();

    }
    static class Ticket implements Runnable{
        private  int count = 10;

        @Override
        public void run() {
            while (true){
                boolean flag = sale();
                if(!flag){
                    break;
                }
            }
        }
        public synchronized boolean sale(){
            if(count > 0){
                System.out.println("正在準備賣票!");
                count--;
                System.out.println(Thread.currentThread().getName()+"出票成功,餘票:"+count);
                try{
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                return true;
            }
            return false;
        }
    }
           

總結:

1.同步方法仍然涉及到同步螢幕,隻是不需要我們顯示的聲明。

2.非靜态的同步方法,同步螢幕是this靜态的同步方法,同步螢幕是目前類本身。繼承自Thread class

方式三: 顯示鎖 Lock類 ReentrantLock子類

public static void main(String[] args) {
        //線程不安全
        //解決方案3:顯示鎖 Lock類  ReentrantLock子類
        //同步代碼塊與同步方法都是隐式鎖

        Runnable run = new demo2.Ticket();

        new Thread(run).start();
        new Thread(run).start();
        new Thread(run).start();

    }
    static class Ticket implements Runnable{
        private  int count = 10;
        private Lock l = new ReentrantLock();
        @Override
        public void run() {
            while (true){
                l.lock();
                    if(count > 0){
                        System.out.println("正在準備賣票!");
                        count--;
                        System.out.println(Thread.currentThread().getName()+"出票成功,餘票:"+count);
                        try{
                            Thread.sleep(1000);
                        } catch (InterruptedException e) {
                            e.printStackTrace();
                        }
                    }else{
                        break;
                    }
                    l.unlock();
                }
            }
        }
           

總結:Synchronized與lock的異同?

相同:二者都可以用來解決線程安全問題

不同:synchronized機制在執行完相應的代碼邏輯以後,自動 的釋放同步螢幕

lock 需要手動 的啟動同步(lock()),同時結束同步也需要手動的實作(unlock())(同時以為着lock的方式更為靈活)

優先使用順序:

LOCK -->> 同步代碼塊 -->> 同步方法

遇到問題解決步驟:

判斷線程是否有安全問題,以及如何解決:

1.先判斷是否多線程

2.再判斷是否有共享資料

3.是否并發的對共享資料進行操作

4.選擇上述三種方法解決線程安全問題

5、線程的死鎖問題

線程死鎖的了解:僵持,誰都不放手,一雙筷子,我一隻你一隻,都等對方放手(死鎖,兩者都進入阻塞,誰都吃不了飯,進行不了下面吃飯的操作)

出現死鎖以後,不會出現提示,隻是所有線程都處于阻塞狀态,無法繼續

public class demo {
    public static void main(String[] args) {
        Police p = new Police();
        Culrite c = new Culrite();

        new MyThread(p,c).start();
        p.say(c);
    }
    static class MyThread extends Thread{
        private Police p;
        private Culrite c;
        public MyThread(Police p, Culrite c){
            this.p = p;
            this.c = c;
        }

        @Override
        public void run() {
            c.say(p);
        }
    }
    static class Culrite{
        public synchronized void say(Police p){
            System.out.println("罪犯:你放了我,我放人質");
            p.react();
        }
        public synchronized void react(){
            System.out.println("罪犯被放走了,人質也被放了");
        }
    }
    static class Police{
        public synchronized void say(Culrite c){
            System.out.println("警察:你放了人質,我放你走");
            c.react();
        }
        public synchronized void react(){
            System.out.println("人質被救了,但罪犯逃走了");
        }
    }
           

代碼運作如下(執行不下去了(苦笑)):

詳解Java多線程(超級爆肝)前言1、線程簡介2、實作線程的兩種方式3、線程的生命周期4、線程的同步5、線程的死鎖問題6、線程的通信總結

如何解決死鎖問題:

1.減少同步共享變量
2.采用專門的算法,多個線程之間規定先後執行的順序,規避死鎖問題
3.減少鎖的嵌套。
           

6、線程的通信

6.1、常用通信方法

通信方法 描述
wait() 一旦執行此方法,目前線程就進入阻塞狀态,并釋放同步螢幕
notify 一旦執行此方法,就會喚醒被wait的一個線程,如果有多個線程,就喚醒優先級高的線程
notifyAll 一旦執行此方法,就會喚醒所有被wait()的線程

特别提醒:這三個方法均隻能使用在同步代碼塊或者同步方法中。

sleep和wait的異同:

相同點 一旦執行方法以後,都會使得目前的程序進入阻塞狀态
不同點

1.兩個方法聲明的位置不同,Thread類中聲明sleep,Object類中聲明wait。

2.調用的要求不同,sleep可以在任何需要的場景下調用,wait必須使用在同步代碼塊或者同步方法中

3.關于是否釋放同步螢幕,如果兩個方法都使用在同步代碼塊或同步方法中,sleep不會釋放,wait會釋放**

經典例題:生産者/消費者問題:

生産者(Priductor)将産品交給店員(Clerk),而消費者(Customer)從店員處取走産品,店員一次隻能持有固定數量的産品(比如20個),如果生産者視圖生産更多的産品,店員會叫生産者停一下,如果店中有空位放産品了再通知生産者繼續生産:如果店中沒有産品了,店員會告訴消費者等一下,如果店中有産品了再通知消費者來取走産品。

這裡可能出現兩個問題:

生産者比消費者快的時候,消費者會漏掉一些資料沒有收到。

消費者比生産者快時,消費者會去相同的資料。

//交替實作,保證線程安全,
// 經典例題:生産者(廚師)與消費者(服務員)問題
public class demo {
    public static void main(String[] args) {
        Food f = new Food();
        new Cook(f).start();
        new Waiter(f).start();

    }
    //廚師
    static class Cook extends Thread{
        private Food f;
        public Cook(Food f) {
            this.f = f;
        }

        @Override
        public void run() {
            for(int i=0; i<100;i++){
                if(i%2==0){
                    f.setNameTaste("老幹媽小米粥","香辣味");
                }else{
                    f.setNameTaste("煎餅果子","甜辣味");
                }
            }
        }
    }

    //服務生
    static class Waiter extends Thread{
        private Food f;
        public Waiter(Food f) {
            this.f = f;
        }

        @Override
        public void run() {
            for(int i=0; i<100;i++){
                try {
                    Thread.sleep(100);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                f.get();
            }
        }
    }

    //食物
    static class Food{
        private String name;
        private String taste;
        private boolean flag = true;

        public synchronized void setNameTaste(String name, String taste) {
            if(flag){
                this.name = name;
                this.taste = taste;
                try {
                    Thread.sleep(100);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                flag = false;
                this.notifyAll();
                try {
                    this.wait();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }
        public synchronized void get(){
            if(!flag){
                System.out.println("服務生端走的菜的名稱是:"+name+",味道是:"+taste);
            }
            flag = true;
            this.notifyAll();
            try {
                this.wait();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
}

           

總結

以上就是今天要講的内容,本文簡單介紹了多線程的一部分内容。學習多線程程式設計就像進入了一個全新的領域,它與以往的程式設計思想截然不同,初學者應該積極轉換程式設計思維,進入多線程程式設計的思維模式。 好了,今天就到這裡吧

繼續閱讀