java多線程學習(上)

 

說在前面,我自己是不喜歡看特別長篇的博客。我能理解看長篇博客的那種難受,因此,想直接入正題看編碼的不妨跳過前兩塊內容,直接進入“線程的創建”。

線程概述

什麼是線程?

之前看到過某大神這樣一句話:比較糟糕的技術文檔的特徵:用專業名詞來解釋專業名詞 

從這句話中,我受益匪淺,因此以後的文章儘量都避免這樣。相信大家也不喜歡看一些概念性的東西吧。

介紹線程之前,首先得解釋一下進程,因爲要用進程來解釋線程。

進程:是程序的一次動態執行過程。比如打開谷歌瀏覽器,從它打開的時刻開始就啓動了一個進程。

多進程:多進程就像打開了多個程序,比如邊玩QQ,邊聽歌,還可以瀏覽網頁,多個任務同時運行。

線程:比進程更小的執行單位,通常一個進程擁有1-n個線程。

多線程:指在同一程序(進程)中能夠同時處理多個任務,而這些任務就對應多個線程,比如瀏覽器下載圖片能夠同時下載多張圖片。比如ABC三個用戶同時訪問淘寶網,淘寶網的服務器收到A用戶的請求後,爲A用戶創建了一個線程,BC用戶同樣擁有自己對應的線程。注意:多線程不是爲了提高程序執行速度(性能甚至更低),而是提高應用程序的使用效率。

線程的生命週期

每一個生命週期也是一種狀態。

1、新建:創建一個新的線程對象

2、就緒:線程對象創建後,其他線程調用了該對象的start()方法,該線程被放入可運行的線程池中,等待獲取CPU的使用權。

(這裏可以這樣解釋,百米賽跑上,每個運動員對應一個線程,裁判員大喝“預備”這聲預備相當於調用了start()方法,所有運動員進入就緒狀態,開槍這個動作則對應了獲取CPU使用權,運動員得到命令開跑)注意:一個CPU核心在某一時刻只能運行一個線程,這裏的CPU相當於多核CPU,不扯遠了。。。

3、運行:就緒狀態的線程獲取了CPU,執行。(運動員開跑)

4、阻塞:阻塞情況比較複雜,這裏簡單考慮一下就不分類了,即線程因爲某種原因放棄CPU使用權,線程暫停。(某運動員搶跑,此次比賽暫停,所有運動員回起跑線重新準備就緒,注意不能繼續跑!也就是說不能到運行狀態)

5、死亡:線程執行完成或異常退出,該線程生命週期結束。

狀態轉換圖如下:

 

JAVA線程的創建

好了,上面囉嗦了這麼多,不過相信大家已經對線程有了一定理解吧,回到也許是大家最想看到的部分,coding...

1、繼承Thread類

這裏寫一個簡單的類,主線程輸出1-5,兩個子線程輸出1-10


 
public class FirstThread extends Thread {
    public void run() {
        for (int i = 1; i <= 10; i++) {
            System.out.println(getName() + " " + i);
            try {
                Thread.sleep(500);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }

    public static void main(String args[]) {
        for (int i = 1; i <= 5; i++) {
            System.out.println(Thread.currentThread().getName() + " " + i);
        }
        new FirstThread().start();
        new FirstThread().start();
    }
}

運行截圖(部分):

可以看出,線程的執行是無序的,線程的執行是根據CPU分配情況來決定的。

2、實現Runnable接口

package cn.thread;

public class SecondThread implements Runnable {
    private int money=500;//卡上還剩500元
    @Override
    public void run() {
        while(true) {
            String name = Thread.currentThread().getName();
            if (name.equals("Son")) {
                if (money <= 0) {
                    System.out.println("爸,我沒錢了!");
                    return;//Son線程的run方法結束
                }
                money = money - 300;//兒子一次取300元
                System.out.println(name + "卡上還剩:" + money + "元");
            }
            if (name.equals("Father")) {
                if (money >= 500) {
                    System.out.println("卡上錢還夠用!");
                    return;//Father線程的run方法結束
                }
                money = money + 1000;//父親一次存1000
                System.out.println(name + "卡上還剩:" + money + "元");
            }
            try {
                Thread.sleep(500);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }

    public static void main(String args[]) {
        SecondThread st = new SecondThread();
        new Thread(st, "Son").start();
        new Thread(st, "Father").start();
    }
}

 

運行截圖:

 

繼承Thread類和實現Runnable接口該用哪一個?

結論網上一大堆,都可以搜到:繼承Thread類不適合資源共享,實現Runnable接口更具有靈活性。

我這裏就來解釋一下這個結論,深入理解一下它實現的原理

分析一下這兩種方法創建線程的原理,看Thread類的源碼,如圖:

Thread類實現了Runnable接口並重寫了run()方法,如果target(目標)爲空,則不執行代碼。

所以我們這裏繼承Thread類重寫run()方法的原因就在此。代碼如下:

public class FirstThread extends Thread {
    public void run() { //該線程要執行的操作       
        }
    public static void main(String args[]) {
        new FirstThread().start();
    }
}

 

繼續看,當target不爲空時,運行target的run()方法,而變量target看源碼

 

是一個Runnable對象,也就是說只要target這個對象不爲空,則調用Runnable對象的run()方法,再把Runnable對象傳遞給Thread類。代碼如下:


public class SecondThread implements Runnable {
    @Override
    public void run() {
    }
    public static void main(String args[]) {
        SecondThread st = new SecondThread();
        new Thread(st, "線程1").start();
        new Thread(st, "線程2").start();
    }
}

注意看這兩行代碼:

 

        new Thread(st, "線程1").start();
        new Thread(st, "線程2").start();

傳給Thread的是同一個Runnable對象,這也就是爲什麼說實現Runnable接口,可以資源共享,因爲操作的都是同一個對象啊。而繼承Thread每次都要new一個新的對象,對象自然就不同了。並且,實現Runnable接口這種方式更體現了面向對象這種思維,new一個線程,線程裏面傳一個對象,這個對象封裝了一系列操作。

 

JAVA線程同步的與通信

主要是以實際例子,應用來說明。

這裏先解釋一下互斥和同步(之後的緩存池也要用到)

互斥:是指散佈在不同進程之間的若干程序片斷,當某個進程運行其中一個程序片段時,其它進程就不能運行它們之中的任一程序片段,只能等到該進程運行完這個程序片段後纔可以運行。

同步:上一段代碼沒的完成,下一段必須等到上一段代碼完成後纔可以執行(必須嚴格按照規定的 某種先後次序來運行)。如買票排隊

(同步是一種更爲複雜的互斥,而互斥是一種特殊的同步。)

異步:上一段代碼沒的完成,下一段不必等到上一段代碼完成就可以執行。如手機發送短信。

1、synchronized同步

線程安全問題這裏舉個例子用火車購票來解釋,庫存100張票,三個窗口同時售票。

方法一:直接給方法加上synchronized關鍵字修飾

package cn.thread;

public class SynchronizedSaleTickets implements Runnable {
    private static int tickets = 100;

    @Override
    public void run() {
        while (tickets>0) {//餘票充足
            try {
                Thread.sleep(100);//延遲
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            sale();
        }
        if(tickets==0){
            System.out.println("票已售完!");
        }
    }

    synchronized static void sale() {
        tickets--;
        if(tickets>0) {
            System.out.println(Thread.currentThread().getName() + "當前餘票:" + tickets);
        }
    }
    public static void main(String[] args){
        SynchronizedSaleTickets sst=new SynchronizedSaleTickets();
        new Thread(sst).start();
        new Thread(sst).start();
        new Thread(sst).start();
        new Thread(sst).start();

    }
}

運行結果(部分):

方法二:定義一個共同的“鎖”

代碼和上面大致相同,修改了一點:

package cn.thread;

public class SynchronizedSaleTickets implements Runnable {
    private static int tickets = 100;
    /*
    鎖住一個“對象”(門栓),這個對象可以是一個引用對象,也可以是此對象內部的某個數據
    這裏定義了一個對象
     */
    private static Object lock=new Object();
    @Override
    public void run() {
        while (tickets>0) {//餘票充足
            try {
                Thread.sleep(100);//延遲
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            sale();
        }
        if(tickets==0){
            System.out.println("票已售完!");
        }
    }

    static void sale() {
        synchronized (lock) {//“鎖住”要執行的操作
            tickets--;
            if (tickets > 0) {
                System.out.println(Thread.currentThread().getName() + "當前餘票:" + tickets);
            }
        }
    }
    public static void main(String[] args){
        SynchronizedSaleTickets sst=new SynchronizedSaleTickets();
        new Thread(sst).start();
        new Thread(sst).start();
        new Thread(sst).start();
        new Thread(sst).start();

    }
}

注意:鎖是上在要操作的資源內部方法中,而不是上在線程代碼中!

2、wait、notify通信

這裏舉一個例子,也是一道java線程面試題

子線程循環10次,接着主線程循環100次,又接着回到子線程循環10次,再接着回到主線程又循環100次,如此循環50次。

代碼如下:

package cn.thread;

public class ThreadCommunication {
    public static void main(String[] args) {
        final Business business = new Business();
        new Thread(new Runnable() {
            @Override
            public void run() {
                for (int i = 1; i <= 50; i++) {
                    business.sub(i);
                }
            }
        }).start();
        for (int i = 1; i <= 50; i++) {
            business.main(i);
        }
    }
//互斥代碼放在資源內部方法中,而不是放在線程代碼中!
    static class Business {
        private boolean shouldSub = true;
        //子線程業務邏輯
        public synchronized void sub(int i) {
            //這裏用while比用if好,用if表示如果被通知喚醒,則繼續執行後續的代碼,而用while表示被通知喚醒過後
            //再回來檢查參數是否該執行,邏輯更嚴密
            while (!shouldSub) {//不該子線程執行
                try {
                    this.wait();//等待
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
            for (int j = 1; j <= 10; j++) {
                System.out.println("sub thread:" + j + ",loop of " + i);
            }
            //子線程執行完通知主線程
            shouldSub = false;
            this.notify();
        }
        //主線程業務邏輯
        public synchronized void main(int i) {
            while (shouldSub) {
                try {
                    this.wait();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
            for (int j = 1; j <= 100; j++) {
                System.out.println("main thread:" + j + ",loop of" + i);
            }
            //主線程執行完通知子線程
            shouldSub = true;
            this.notify();
        }
    }
}

運行結果(部分):

3、線程間共享數據

這個例子和火車售票不一樣,四個線程,兩個線程對每次J+2,兩個線程每次對J-1。

注意:共享的數據封裝到了一個單獨的類中

package cn.thread;

public class MultiThreadShareData {
    public static void main(String[] args) {
        final ShareData1 data2 = new ShareData1();//共享數據data2
        //Thread-0和Thread-1每次-1
        new Thread(new MyRunnable1(data2)).start();
        new Thread(new MyRunnable1(data2)).start();
        ////Thread-2和Thread-3每次+2
        new Thread(new MyRunnable2(data2)).start();
        new Thread(new MyRunnable2(data2)).start();
    }
}

class MyRunnable1 implements Runnable {//減法
    private ShareData1 data1;

    public MyRunnable1(ShareData1 data1) {
        this.data1 = data1;
    }

    public void run() {
        while (true) {
            data1.decrement();
            System.out.println(Thread.currentThread().getName() + "線程每次-1,當前:" + data1.getJ());
            try {
                Thread.sleep(200);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
}

class MyRunnable2 implements Runnable {//加法
    private ShareData1 data1;

    public MyRunnable2(ShareData1 data1) {
        this.data1 = data1;
    }

    public void run() {
        while (true) {
            data1.increment();
            System.out.println(Thread.currentThread().getName() + "線程每次+2,當前:" + data1.getJ());
            try {
                Thread.sleep(200);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
}

class ShareData1 {//共享數據存放在此類的對象中,還有基本的操作
    private int j = 0;

    public int getJ() {
        return j;
    }

    public void setJ(int j) {
        this.j = j;
    }

    public synchronized void increment() {
        j = j + 2;
    }

    public synchronized void decrement() {
        j--;
    }
}

下篇繼續講解:

線程池(緩存池)

線程相關類

線程面試經典題目

......

 

 

原文出自:https://my.csdn.net/qq_37094660(如需轉載請註明出處)
 

 

 

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