19.Java線程

19.Java線程
轉載請保留原文鏈接: http://dashidan.com/article/java/basic/19.html

部分內容源自:《深入理解JVM虛擬機》.

① 線程和進程的概念

進程是指一個內存中運行有自己獨立的一塊內存空間的應用程序.一個進程中至少一個線程.

線程是指進程中的一個執行流程.每個線程有獨立的運行棧. 線程從屬於進程. 一個進程中可以運行多個線程.進程中的多個線程共享進程的內存.

線程和進程一樣分爲五個階段:創建、就緒、運行、阻塞、終止.

多進程是指操作系統能同時運行多個任務(程序).

多線程是指在同一程序中,有多個順序流在執行.

18-1

② 線程的實現

線程是比進程更輕量級的調度執行單位,線程的引入,可以把一個進程的資源分配和執行調度分開,各個線程既可以共享進程資源(內存地址、文件I/O等),又可以獨立調度(線程是CPU調度的基本單位).

主流的操作系統都提供了線程實現,Java語言則提供了在不同硬件和操作系統平臺下對線程操作的統一處理.

Thread類中所有關鍵方法都是聲明爲Native的,在Java API中,本地方法往往意味着這個方法沒有使用或無法使用平臺無關的手段來實現(當然也有可能是爲了執行效率而使用Native方法)

③ Java線程調度

Java的線程調度方式是搶佔式調度,雖然Java線程的調度是系統自動完成的,但是我們還是可以“建議”系統給某些線程多分配一點執行時間,另外的一些線程則可以少分配一點——這項操作可以通過設置優先級來完成.

不過,線程的優先級並不是太靠譜,因爲Java線程是通過映射到原生線程上來實現的,所以線程調度最終還是取決於操作系統,雖然現在很多操作系統都提供了優先級的概念,但是並不見得與Java線程的優先級一一對應.比如:Windows中就只有7種線程優先級,而Java語言一共設置了10個級別的線程優先級.

④ Java中線程的實現 — 在Java中想實現多線程有兩種手段,一種是集成Thread類,另一種就是實現Runnable接口. ###1.繼承自Thread類
package com.dashidan.lesson18;

/**
 * 大屎蛋教程網-dashidan.com
 * <p>
 * Java教程基礎篇:  18.Java線程
 * 繼承自Thread類
 */
public class MyThread1 extends Thread {

    @Override
    public void run() {
        System.out.println("MyThread1 run.");
    }
}
###2.實現Runnable接口 首先定義一個線程類繼承自Runnable接口,如:
package com.dashidan.lesson18;

/**
 * 大屎蛋教程網-dashidan.com
 * <p>
 * Java教程基礎篇:  18.Java線程
 * 實現Runnable接口
 */
public class MyThread2 implements Runnable {
    @Override
    public void run() {
        System.out.println("MyThread2 run.");
    }
}
入口類,實例化線程類的對象,發動啓動線程的命令
package com.dashidan.lesson18;

/**
 * 大屎蛋教程網-dashidan.com
 * <p>
 * Java教程基礎篇:  18.Java線程
 */
public class Demo1 {
    public static void main(String[] args) {
        MyThread1 t1 = new MyThread1();
        /** t1 線程啓動*/
        t1.start();
        MyThread2 myThread2 = new MyThread2();
        Thread t2 = new Thread(myThread2);
        /** t2 線程啓動*/
        t2.start();
    }
}
輸出:
    MyThread2 run.
    MyThread1 run.

輸出的順序可能不一致

多運行幾次, 輸出的順序可能不一致, 這個是由於線程的執行順序無法保證導致.

繼承Thread和實現Runnable接口如何選擇

由於Java是單根繼承體系, 當一個類需要繼承與其他類, 還需要具有線程的能力, 這時只能採用實現Runable接口的方式.

⑤ 啓動線程

在線程的Thread對象上調用start()方法,而不是run()或者別的方法.在調用start()方法之前,線程處於新狀態中, 新狀態指有一個Thread對象, 但還沒有一個真正的線程. 在調用start()方法之後,發生了一系列複雜的事情

  1. 啓動新的執行線程(具有新的調用棧).
  2. 該線程從新狀態轉移到可運行狀態.
  3. 當該線程獲得機會執行時,其目標run()方法將運行.

run()方法

對Java來說,run()方法是線程啓動的入口方法, 同時也是一個普通的方法. 因此,在Runnable上或者Thread上調用run方法是合法的.但並不啓動新的線程.所以不建議直接調用run()方法.

⑥ Java線程的狀態

Java線程具有五中基本狀態

  • 新建狀態(New)

在生成線程對象,並沒有調用該對象的start方法.如:

Thread t = new MyThread1();
  • 就緒狀態(Runnable)

當調用了線程對象的start方法之後,該線程就進入了就緒狀態,但是此時線程調度程序還沒有把該線程設置爲當前線程,此時處於就緒狀態.隨時等待CPU調度執行,並不是說執行了t.start()此線程立即就會執行. 在線程運行之後,從等待或者睡眠中回來之後,也會處於就緒狀態.

  • 運行狀態(Running)

線程調度程序將處於就緒狀態的線程設置爲當前線程,此時線程就進入了運行狀態,開始運行run函數當中的代碼.就緒狀態是進入到運行狀態的唯一入口,也就是說,線程要想進入運行狀態執行,首先必須處於就緒狀態中.

  • 阻塞狀態(Blocked)

線程正在運行的時候,暫時放棄對CPU的使用權,停止執行,此時進入阻塞狀態.通常是爲了等待某個時間的發生(比如說某項資源就緒)之後再繼續運行.sleep,suspend,wait等方法都可以導致線程阻塞.

處於運行狀態中的線程直到其進入到就緒狀態,纔有機會再次被CPU調用以進入到運行狀態.根據阻塞產生的原因不同,阻塞狀態又可以分爲三種:

  • 等待阻塞

運行狀態中的線程執行wait()方法,使本線程進入到等待阻塞狀態.wait()、notify()、notifyAll()是三個定義在Object類裏的方法,可以用來控制線程的狀態.任何一個時刻,對象的控制權(monitor)只能被一個線程擁有.

無論是執行對象的wait、notify還是notifyAll方法,必須保證當前運行的線程取得了該對象的控制權(monitor)
如果在沒有控制權的線程裏執行對象的以上三種方法,就會報java.lang.IllegalMonitorStateException異常.

執行時wait(), sleep()時會拋出InterruptedException, 需要放在try-catch語句中執行.

報錯示例:

package com.dashidan.lesson18;

/**
 * 大屎蛋教程網-dashidan.com
 * <p>
 * Java教程基礎篇:  18.Java線程
 * 線程阻塞狀態-wait()
 */
public class WaitThread extends Thread {
    @Override
    public void run() {
        System.out.println("WaitThread run.");
        try {
            System.out.println("WaitThread before wait.");
            this.wait();
            System.out.println("WaitThread after wait.");
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}
入口類:
package com.dashidan.lesson18;

/**
 * 大屎蛋教程網-dashidan.com
 * <p>
 * Java教程基礎篇:  18.Java線程
 * 線程阻塞狀態 wait()
 */
public class Demo2 {
    public static void main(String[] args) {
        WaitThread waitThread = new WaitThread();
        waitThread.start();
    }
}
輸出:

    WaitThread run.
    WaitThread before wait.
    Exception in thread "Thread-0" java.lang.IllegalMonitorStateException
        at java.lang.Object.wait(Native Method)
        at java.lang.Object.wait(Object.java:502)
        at com.dashidan.lesson18.WaitThread.run(WaitThread.java:15)

線程取得控制權的方法有三種:

  • 執行對象的某個同步實例方法.
  • 執行對象對應類的同步靜態方法.
  • 執行對該對象加同步鎖的同步塊.

如果對象調用了wait方法就會使持有該對象的線程把該對象的控制權交出去,然後處於等待狀態.如果對象調用了notify方法就會通知某個正在等待這個對象的控制權的線程可以繼續運行, 由Java虛擬機來決定哪個線程繼續運行.

如果對象調用了notifyAll方法就會通知所有等待這個對象控制權的線程繼續運行可以通過synchronize關鍵字來獲取同步鎖, 然後再調用wait(), notify(), notifyAll().

package com.dashidan.lesson18;

/**
 * 大屎蛋教程網-dashidan.com
 * <p>
 * Java教程基礎篇:  18.Java線程
 * 線程阻塞狀態 wait() , 獲得同步鎖
 */
public class Demo3 {

    public static Object object = new Object();

    public static void main(String[] args) {
        WaitThread waitThread = new WaitThread(object);
        waitThread.start();

        try {
            /** 主線程休眠3秒*/
            System.out.println("主線程休眠3秒");
            Thread.sleep(3000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

        /** 3秒後執行 notifyAll */
        synchronized (object) {
            object.notifyAll();
        }
    }
}

class WaitThread extends Thread {
    Object object;

    public WaitThread(Object object) {
        this.object = object;
    }

    @Override
    public void run() {
        System.out.println("WaitThread run.");
        synchronized (object) {
            try {
                System.out.println("WaitThread before lock " + this.getName());
                object.wait();
                System.out.println("WaitThread after lock " + this.getName());
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
}
輸出:
    主線程休眠3秒
    WaitThread run.
    WaitThread before lock Thread-0
    WaitThread after lock Thread-0
  • 同步阻塞

線程在獲取synchronized同步鎖失敗(因爲鎖被其它線程所佔用),它會進入同步阻塞狀態. 當鎖被釋放後, 其他獲得該鎖的線程繼續執行.

package com.dashidan.lesson18;

/**
 * 大屎蛋教程網-dashidan.com
 * <p>
 * Java教程基礎篇:  18.Java線程
 * 線程阻塞狀態 同步阻塞
 */
public class Demo5 {
    public static Object lock = new Object();

    public static void main(String[] args) {
        SyncThread syncThread = new SyncThread("t1", lock);
        syncThread.start();

        SyncThread syncThread1 = new SyncThread("t2",lock);
        syncThread1.start();
    }
}

class SyncThread extends Thread {
    public Object lock;

    public SyncThread(String name, Object lock) {
        super(name);
        this.lock = lock;
    }

    @Override
    public void run() {
        try {
            synchronized (lock) {
                /** 一個線程執行這個語句塊的時候,另一個線程等待. */
                System.out.println(this.getName() + " before sleep.");
                Thread.sleep(3000);
                System.out.println(this.getName() + " after sleep.");
            }
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}
輸出:
    t2 before sleep.
    t2 after sleep.
    t1 before sleep.
    t1 after sleep.

多運行幾次, 輸出結果可能不一致, 但每次都是執行完一個線程, 再執行另一個.

sleep()方法會釋放cpu, 進入等待執行的狀態, 但不會釋放所持對象的鎖.

  • 其他阻塞

通過調用線程的sleep()或join()或發出了I/O請求時,線程會進入到阻塞狀態.當sleep()狀態超時、join()等待線程終止或者超時、或者I/O處理完畢時,線程重新轉入就緒狀態.

sleep示例:

package com.dashidan.lesson18;

/**
 * 大屎蛋教程網-dashidan.com
 * <p>
 * Java教程基礎篇:  18.Java線程
 * 線程阻塞狀態 sleep()
 */
public class Demo4 {
    public static void main(String[] args) {
        SleepThread thread = new SleepThread();
        thread.start();
    }
}

class SleepThread extends Thread {
    @Override
    public void run() {
        try {
            System.out.println("before sleep.");
            /** 休眠3秒*/
            Thread.sleep(3000);
            /** 3秒後輸出*/
            System.out.println("after sleep.");
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}
輸出:
    before sleep.
    after sleep.
  • 死亡狀態(Dead)

如果一個線程的run方法執行結束或者調用stop方法後,該線程就會死亡結束生命週期.對於已經死亡的線程,無法再使用start方法令其進入就緒.

⑦ 線程死鎖

線程死鎖是指2個線程都在同步阻塞狀態, 等待對象的鎖, 在對方的線程中無法釋放. 這樣會導致這兩個線程卡死, 無法繼續執行. 開發時應極力避免.

package com.dashidan.lesson18;


/**
 * 大屎蛋教程網-dashidan.com
 * <p>
 * Java教程基礎篇:  18.Java線程
 * 線程死鎖
 */
public class Demo6 {

    public static LockObject lockA = new LockObject("LOCK-A");
    public static LockObject lockB = new LockObject("LOCK-B");

    public static void main(String[] args) {
        /** 注意這兩個線程傳入的參數,順序不一樣, lockA, lockB*/
        LockThread thread1 = new LockThread("t1", lockA, lockB);
        thread1.start();
        /** 注意這兩個線程傳入的參數,順序不一樣, lockB, lockA*/
        LockThread thread2 = new LockThread("t2", lockB, lockA);
        thread2.start();
    }
}

class LockThread extends Thread {

    LockObject lock0;
    LockObject lock1;

    public LockThread(String name, LockObject lock0, LockObject lock1) {
        super(name);
        this.lock0 = lock0;
        this.lock1 = lock1;
    }

    @Override
    public void run() {
        try {
            synchronized (lock0) {
                System.out.println(getName() + "持有對象鎖 " + lock0.getName());
                Thread.sleep(3000);
                System.out.println(getName() + "等待對象鎖 " + lock1.getName() + " ... ");
                synchronized (lock1) {
                    System.out.println(getName() + "持有對象鎖 " + lock1.getName());
                    Thread.sleep(3000);
                }
                /** Attention!這一行並沒有輸出*/
                System.out.println(getName() + "執行完畢");
            }
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}

/**
 * 一個簡單的鎖對象
 */
class LockObject {
    /**
     * 對象名字
     */
    private String name;

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

    public String getName() {
        return name;
    }
}
輸出:
    t1持有對象鎖 LOCK-A
    t2持有對象鎖 LOCK-B
    t2等待對象鎖 LOCK-A ... 
    t1等待對象鎖 LOCK-B ... 

執行完畢這一句並沒有輸出.

是因爲兩個線程進入了線程死鎖狀態, 互相等待對方釋放鎖. 編碼過程中要極力避免嵌套調用鎖的情況出現..

⑧ synchronized用在對象, 方法, 代碼塊的區別 — synchronized關鍵字的本質是`鎖對象`. 用在對象類型上, 即爲鎖定目標對象. 用在方法和代碼塊中, 表示鎖定當前對象.對象一旦被鎖定, 所有的synchonized關鍵字修飾的區域都需要等待鎖釋放後才能執行. 前邊幾個例子都是synchronized鎖對象的方式.下面來看看用鎖方法和鎖代碼塊.
package com.dashidan.lesson18;

/**
 * 大屎蛋教程網-dashidan.com
 * <p>
 * Java教程基礎篇:  18.Java線程
 * synchronized鎖方法和區塊
 */
public class Demo7 {
    public static void main(String[] args) {
        SyncObject syncObject = new SyncObject();
        Demo7Thread t1 = new Demo7Thread("t1", syncObject);
        t1.start();
        Demo7Thread t2 = new Demo7Thread("t2", syncObject);
        t2.start();
    }
}

class SyncObject {

    /**
     * synchronized 鎖方法
     */
    public synchronized void testSyncMethod(String name) {
        try {
            System.out.println(name + "運行testSyncMethod開始.");
            Thread.sleep(300);
            System.out.println(name + "運行testSyncMethod結束.");
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }

    /**
     * synchronized 鎖代碼區塊
     */
    public void testSyncCode(String name) {
        synchronized (this) {
            try {
                System.out.println(name + "運行testSyncCode開始.");
                Thread.sleep(300);
                System.out.println(name + "運行testSyncCode結束.");
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
}

class Demo7Thread extends Thread {
    SyncObject syncObject;

    public Demo7Thread(String name, SyncObject syncObject) {
        super(name);
        this.syncObject = syncObject;
    }

    @Override
    public void run() {
        syncObject.testSyncMethod(this.getName());
        syncObject.testSyncCode(this.getName());
    }
}
輸出:
    t1運行testSyncMethod開始.
    t1運行testSyncMethod結束.
    t2運行testSyncMethod開始.
    t2運行testSyncMethod結束.
    t1運行testSyncCode開始.
    t1運行testSyncCode結束.
    t2運行testSyncCode開始.
    t2運行testSyncCode結束.

運行結果可能不一樣, 但開始和結束總是一一對應.在開始和結束之間沒有執行其他方法, 說明都是鎖的同一個對象.可以將任意一個synchronized屏蔽,再跑一下程序,就會發現輸出中的開始和結束不再一一對應.

⑨ Lock對象

JDK1.5開始提供了一個更加高效的鎖的方式.concurrent包中Lock對象.最大的優勢是可以讀寫鎖分離, 加入寫鎖後, 讀鎖需要等寫鎖釋放後再進行. 加入讀鎖後, 還是可以獲取寫鎖, 爲了保證數據安全, 使用寫鎖的時候. 同一個對象可以使用多個Lock對象, 鎖定對應的範圍, 而不像synchonized鎖整個對象.
官方例子

class CachedData {
    Object data;
    volatile boolean cacheValid;
    final ReentrantReadWriteLock rwl = new ReentrantReadWriteLock();

    void processCachedData() {
        rwl.readLock().lock();
        if (!cacheValid) {
            // Must release read lock before acquiring write lock
            rwl.readLock().unlock();
            rwl.writeLock().lock();
            try {
                // Recheck state because another thread might have
                // acquired write lock and changed state before we did.
                if (!cacheValid) {
                    data =...
                    cacheValid = true;
                }
                // Downgrade by acquiring read lock before releasing write lock
                rwl.readLock().lock();
            } finally {
                rwl.writeLock().unlock(); // Unlock write, still hold read
            }
        }
        try {
            use(data);
        } finally {
            rwl.readLock().unlock();
        }
    }
}
一個簡單的讀寫鎖例子
package com.dashidan.lesson18;

import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantReadWriteLock;

/**
 * 大屎蛋教程網-dashidan.com
 * <p>
 * Java教程基礎篇:  18.Java線程
 * Lock對象
 */
public class Demo8 {

    public static void main(String[] args) {
        LockObjectReadAndWrite lockObj = new LockObjectReadAndWrite();
        /** 寫線程*/
        WriteThread writeThread = new WriteThread("w1", lockObj);
        writeThread.start();
        /** 讀線程*/
        ReadThread readThread = new ReadThread("r1", lockObj);
        readThread.start();
    }
}

class LockObjectReadAndWrite {

    private final Map<String, Integer> m = new HashMap<>();
    private final ReentrantReadWriteLock rwl = new ReentrantReadWriteLock();
    private final Lock readLock = rwl.readLock();
    private final Lock writeLock = rwl.writeLock();

    public Integer get(String key, String threadName) {
        System.out.println("加入讀鎖 " + threadName);
        readLock.lock();
        try {
            System.out.println("讀取休息1秒");
            Thread.sleep(1000);
            return m.get(key);
        } catch (InterruptedException e) {
            e.printStackTrace();
            return -1;
        } finally {
            readLock.unlock();
            System.out.println("釋放讀鎖 " + threadName);
        }
    }

    public Integer put(String key, Integer value, String threadName) {
        System.out.println("------WriteThread 加入寫鎖------ : " + threadName);
        writeLock.lock();
        try {
            System.out.println("寫入休息6秒.....");
            Thread.sleep(6000);
            return m.put(key, value);
        } catch (InterruptedException e) {
            e.printStackTrace();
            return -1;
        } finally {
            writeLock.unlock();
            System.out.println("------WriteThread 釋放寫鎖------: " + threadName);
        }
    }
}

class WriteThread extends Thread {

    LockObjectReadAndWrite lockObj;

    public WriteThread(String name, LockObjectReadAndWrite lockObj) {
        super(name);
        this.lockObj = lockObj;
    }

    @Override
    public void run() {
        while (true) {
            /** 持續寫入*/
            lockObj.put("a", 1, this.getName());
            /** 寫入線程休息3秒*/
            try {
                Thread.sleep(3000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }

        }
    }
}

class ReadThread extends Thread {

    LockObjectReadAndWrite lockObj;

    public ReadThread(String name, LockObjectReadAndWrite lockObj) {
        super(name);
        this.lockObj = lockObj;
    }

    @Override
    public void run() {
        while (true) {
            /** 持續讀取*/
            lockObj.get("a", this.getName());
        }
    }
}
輸出:
    加入讀鎖 r1
    讀取休息1秒
    ------WriteThread 加入寫鎖------ : w1
    釋放讀鎖 r1
    寫入休息6秒.....
    加入讀鎖 r1
    ------WriteThread 釋放寫鎖------: w1
    讀取休息1秒
    釋放讀鎖 r1
    加入讀鎖 r1
    讀取休息1秒
    釋放讀鎖 r1
    加入讀鎖 r1
    讀取休息1秒
    ------WriteThread 加入寫鎖------ : w1
    寫入休息6秒.....
    釋放讀鎖 r1
    加入讀鎖 r1

輸出結果可能不一樣. 但每次加上寫鎖之後, 讀取的操作便停止. 釋放寫鎖後, 讀取繼續.

⑩ 線程常見問題

  • 線程的名字

默認線程名:Thread-下一線程數:

public Thread() {
    init(null, null, "Thread-" + nextThreadNum(), 0);
}

自定義線程名:

在構造函數中傳入線程名.

public Thread(Runnable target, String name) {
    init(null, target, name, 0);
}
  • 獲取當前線程對象的方法
Thread.currentThread().
  • 當線程目標run()方法結束時, 該線程結束.
  • 一個線程只能啓動一次, 一旦線程啓動, 不能再重新啓動.
  • 線程的調度由Java虛擬機(JVM)控制.

在單核CPU的電腦上,實際上一次只能運行一個線程,CPU分時處理,看上去向同步運行一樣.多核CPU的電腦同時可以運行多個線程.Java虛擬機決定實際運行哪個線程.有多個可運行線程時,其中的某一個會被Java虛擬機選爲當前線程.默認情況下不能保證線程運行的先後順序.量子理論的上帝擲骰子.

ayst

⑪ 相關文章

java教程從入門到精通

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