多線程篇一(Java多線程基礎)

前言
該篇文章主要是書寫一些java線程相關的基礎知識。

正文
一、進程與線程
進程:直白的講,進程就是應用程序的啟動實例。比如我們運行一個游戲,打開一個軟件,就是開啟的一個進程。
線程:線程從屬于進程,是程序的實際執(zhí)行者。一個進程至少包含一個主線程,也可以有更多的子線程。
對操作系統(tǒng)來說,線程是最小的執(zhí)行單元,進程是最小的資源管理單元。
進程間是獨立的,這表現(xiàn)在內存空間,上下文環(huán)境;線程運行在進程空間內。一般來講(不使用特殊技術)進程是無法突破進程邊界存取其它進程內的存儲空間;而線程由于處于進程空間內,所以同一進程所產生的線程共享同一內存空間。

二、線程的優(yōu)劣

1、線程的優(yōu)勢
1)、充分發(fā)揮多處理器的強大能力(提高處理器資源的利用率來提升系統(tǒng)吞吐率)
如果在一個程序中只有一個線程,那么同時最多只能在一個處理器上運行,在雙處理器系統(tǒng)上,單線程程序只能使用一半的CPU資源,而在擁有100個處理器的系統(tǒng)上,將有99%的資源無法使用。
使用多線程還有助于在單處理器系統(tǒng)上獲得更高的吞吐率。如一個線程在等待I/O操作完成時,另一個線程可以繼續(xù)運行,使程序在I/O阻塞期間繼續(xù)運行。
2)、建模的簡單性
舉個例子:如果在程序中只包含一種類型的任務,那么比包含多種不同類型任務的程序要更易于編寫,錯誤更少,也更容易測試。通過使用線程可以把復雜并且異步的工作流進一步分解為一組簡單并且同步的工作流,每個工作流在一個單獨的線程中運行,并在特定的同步位置進行交互。
3)、異步事件的簡化處理
4)、響應更靈敏的用戶界面
2、線程的風險
1)、安全性問題
在多線程的環(huán)境下,由于同一進程下線程共享同一內存空間,當多個線程同時對共享數(shù)據(jù)進行操作的時候很可能會出現(xiàn)非預期的情況,如下代碼片段:

    // 統(tǒng)計accessCount方法被訪問的次數(shù)
    private long count;
    // 并發(fā)的線程數(shù)
    private static final int THREAD_COUNT = 10;

    private CountDownLatch latch = new CountDownLatch(1);

    public void accessCount() throws InterruptedException {
        // 做一些業(yè)務邏輯處理
        System.out.println("線程"+Thread.currentThread().getName()+"獲取到的count="+ ++count);
    }

    private static class AccessCountThread extends Thread {
        private UnSafeCountingFactory factory;

        private AccessCountThread(UnSafeCountingFactory factory) {
            this.factory = factory;
        }

        @Override
        public void run() {
            try {
                factory.latch.await();
                factory.accessCount();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }

    private static class AccessCountThreadTest{
        public static void main(String[] args) throws InterruptedException {
            UnSafeCountingFactory factory = new UnSafeCountingFactory();
            for (int i = 0; i < THREAD_COUNT; i++) {
                Thread thread = new AccessCountThread(factory);
                thread.start();
            }
            Thread.sleep(1000);
            factory.latch.countDown();
        }
    }

程序的執(zhí)行結果

線程Thread-1獲取到的count=3
線程Thread-8獲取到的count=6
線程Thread-0獲取到的count=4
線程Thread-2獲取到的count=5
線程Thread-4獲取到的count=5
線程Thread-6獲取到的count=2
線程Thread-7獲取到的count=7
線程Thread-3獲取到的count=8
線程Thread-9獲取到的count=10
線程Thread-5獲取到的count=9

分析出現(xiàn)兩個5的一種原因:++count實際上包含了3個獨立的操作:讀取count、將count+1、并將結果寫入count。在這3個非原子操作的步驟中,可能有兩個線程同時執(zhí)行讀操作,從而它們得到相同的值,并都將這個值加1。


Image 1.png

2)、活躍性問題
在多線程環(huán)境下可能會發(fā)生死鎖等問題
3)、性能問題
在多線程程序中,當線程調度器掛起活躍線程并轉而運行另一個線程時,就會頻繁出現(xiàn)上下文切換操作,這種操作將帶來極大的開銷:保存和恢復執(zhí)行上下文,丟失局部性,并且CPU時間將更多地花在線程調度而不是線程運行上。
三、多線程常用基礎知識
1、創(chuàng)建線程
java中創(chuàng)建線程有如下幾種方式:
(1)、自定義類繼承Thread類,重寫Thread類中的run方法

public class HowStartThread {

    private static class TestThread extends Thread {
        @Override
        public void run() {
            System.out.println("TestThread is running");
        }
    }

    public static void main(String[] args) {
        Thread t1 = new TestThread();
        t1.start();
    }
}

(2)、自定義類實現(xiàn)Runnable接口,實現(xiàn)Runnable接口中的run方法

public class HowStartThread {

    private static class TestRunnable implements Runnable {

        @Override
        public void run() {
            System.out.println("TestRunnable is running");
        }
    }

    public static void main(String[] args) {
        Thread t2 = new Thread(new TestRunnable());
        t2.start();
    }
}

(3)、結合JDK線程池技術(ThreadPoolExecutor或ExecutorService)、Callable、Future實現(xiàn)有返回值的線程

public class HowStartThread {

    private static class TestCallable implements Callable<Integer>{
        @Override
        public Integer call() {
            System.out.println("TestCallable is running");
            return 1;
        }
    }

    public static void main(String[] args) throws ExecutionException, InterruptedException {
        // 創(chuàng)建一個使用從無界隊列運行的單個工作線程的執(zhí)行程序。
        ExecutorService executorService = Executors.newSingleThreadExecutor();
        // 提交一個Callable任務
        Future<Integer> testCallable = executorService.submit(new TestCallable());
        // get方法會阻塞TestCallable線程執(zhí)行完成
        Integer integer = testCallable.get();
        System.out.println(integer);
        // 關閉線程池
        executorService.shutdown();
    }
}

2、線程完成
(1)、run方法執(zhí)行完成
(2)、拋出一個未處理的異常,導致線程提前結束
3、線程的狀態(tài)
java中在Thread類中的枚舉類State中定義了如下幾種線程的狀態(tài)
(1)、新創(chuàng)建NEW:線程被創(chuàng)建,但是還沒有調用start方法
(2)、可運行RUNNABLE:可運行的線程狀態(tài)在Java虛擬機中執(zhí)行,但它可能執(zhí)行等待操作系統(tǒng)的其他資源如處理器。
(3)、阻塞BLOCKED:線程被阻塞于鎖,等待監(jiān)視器鎖:同步代碼塊,I/O操作,lock等。
(4)、等待WAITING:由于調用以下方法之一,線程處于等待狀態(tài):Object#wait(),join()
(5)、計時等待TIMED_WAITING:線程處于定時等待狀態(tài),因為它調用了下列方法之一,并指定了正等待時間:Thread.sleep,Object#wait(long),join(long),LockSupport#parkNanos,LockSupport#parkUntil
(6)、被終止TERMINATED:線程執(zhí)行完成

下面舉個例子來通過idea來查看線程的狀態(tài)

/**
 * @Author YUBIN
 * @create 2018-01-07
 */
public class SleepUtils {
    public static final void second(long seconds) {
        try {
            TimeUnit.SECONDS.sleep(seconds);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}
/**
 * 線程狀態(tài)的演示
 *
 * @Author YUBIN
 * @create 2018-01-07
 */
public class ThreadState {
    private static Lock lock = new ReentrantLock();

    public static void main(String[] args) {
        new Thread(new SleepAlways(), "SleepAlwaysThread").start();
        new Thread(new Waiting(),"WaitingThread").start();
        // 使用兩個Blocked線程,一個獲取鎖成功,另外一個被阻塞
        new Thread(new Blocked(),"BlockedThread-1").start();
        new Thread(new Blocked(),"BlockedThread-2").start();
        new Thread(new Sync(),"SyncThread-1").start();
        new Thread(new Sync(),"SyncThread-2").start();
        new Thread().setPriority(5);
    }

    /**
     * 該線程不斷的進行睡眠
     */
    private static class SleepAlways implements Runnable {
        @Override
        public void run() {
            while (true) {
                SleepUtils.second(10000);
            }
        }
    }

    /**
     * 該線程在Waiting.class 實例上等待
     */
    private static class Waiting implements Runnable {
        @Override
        public void run() {
            while (true) {
                synchronized (Waiting.class) {
                    try {
                        Waiting.class.wait();
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
            }
        }
    }

    /**
     * 該線程在Blocked.class加鎖后,不會釋放鎖
     */
    private static class Blocked implements Runnable {
        @Override
        public void run() {
            synchronized (Blocked.class) {
                while (true) {
                    SleepUtils.second(1000);
                }
            }
        }
    }

    /**
     * 該線程獲得鎖后又釋放鎖
     */
    private static class Sync implements Runnable {
        @Override
        public void run() {
            lock.lock();
            try {
                SleepUtils.second(3000);
            } finally {
                lock.unlock();
            }
        }
    }
}

啟動main方法,點擊控制臺處的小相機按鈕可查看線程的狀態(tài)

線程狀態(tài)圖.png

4、線程的常用方法
(1)、start方法和run方法
start()用來啟動一個線程,當調用start方法后,系統(tǒng)才會開啟一個新的線程來執(zhí)行用戶定義的子任務,在這個過程中,會為相應的線程分配需要的資源。
run()方法是不需要用戶來調用的,當通過start方法啟動一個線程之后,當線程獲得了CPU執(zhí)行時間,便進入run方法體去執(zhí)行具體的任務。注意,繼承Thread類必須重寫run方法,在run方法中定義具體要執(zhí)行的任務。
(2)、sleep方法
sleep方法的作用是在指定的毫秒數(shù)內讓當前正在執(zhí)行的線程休眠。
在Thread類中有兩個重載的sleep方法

sleep(long millis)     //參數(shù)為毫秒
sleep(long millis,int nanoseconds)    //第一參數(shù)為毫秒,第二個參數(shù)為納秒

sleep相當于讓線程睡眠,交出CPU,讓CPU去執(zhí)行其他的任務。
但是有一點要非常注意,sleep方法不會釋放鎖,也就是說如果當前線程持有對某個對象的鎖,則即使調用sleep方法,其他線程也無法訪問這個對象。
(3)、wait、notify和notifyAll
這3個方法在調用以前,當前線程必須持有鎖。調用了wait()、notify()、notifyAll()會釋放鎖
案例:結合wait()和notifyAll()自定義一個有界隊列

/**
 * 自定義有界隊列
 *
 * @Author YUBIN
 * @create 2018-01-10
 */
public class BlockQueueWN<T> {

    private List blockQueue = new LinkedList<>();

    private final int limit;

    public BlockQueueWN(int limit) {
        this.limit = limit;
    }

    // 入隊 對象鎖
    public synchronized void enqueue(T t) throws InterruptedException {
        while (blockQueue.size() == limit) {
            // 當隊列滿的時候等待
            wait();
        }
        // 將數(shù)據(jù)入列之前如果隊列為空,那么出隊的隊列可能正在等待,需要執(zhí)行喚醒操作
        if (blockQueue.size() == 0) {
            notifyAll();
        }
        blockQueue.add(t);
    }

    // 出隊 對象鎖
    public synchronized T dequeue() throws InterruptedException {
        while (blockQueue.size() == 0) {
            // 當隊列為空的時候等待
            wait();
        }
        // 將數(shù)據(jù)出列之前如果隊列是滿的,那么入隊的隊列可能正在等待,需要執(zhí)行喚醒操作
        if (blockQueue.size() == limit) {
            notifyAll();
        }
        T t = (T)blockQueue.remove(0);
        return t;
    }
}

測試類

public class BqTest {
    public static void main(String[] args) {
        BlockQueueWN bq = new BlockQueueWN(10);
        Thread threadA = new ThreadPush(bq);
        threadA.setName("Push");
        Thread threadB = new ThreadPop(bq);
        threadB.setName("Pop");
        threadB.start();
        threadA.start();
    }

    /**
     * 往隊列中插入元素
     */
    private static class ThreadPush extends Thread {
        private BlockQueueWN<Integer> blockQueue;

        private ThreadPush(BlockQueueWN<Integer> blockQueue) {
            this.blockQueue = blockQueue;
        }

        @Override
        public void run() {
            int i = 20;
            while (i > 0) {
                try {
                    System.out.println("i=" + i + " will push");
                    blockQueue.enqueue(i--);
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }
    }

    private static class ThreadPop extends Thread {
        private BlockQueueWN<Integer> blockQueue;

        private ThreadPop(BlockQueueWN<Integer> blockQueue) {
            this.blockQueue = blockQueue;
        }

        @Override
        public void run() {
            while(true){
                try {
                    System.out.println(Thread.currentThread().getName() + " will pop.....");
                    Integer i = blockQueue.dequeue();
                    System.out.println(" i=" + i.intValue() + " alread pop");
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }
    }
}

(4)、yield()方法
當前線程出讓CPU使用權,當前線程變成Runnable狀態(tài),下一刻仍然可能被CPU選中,它與sleep一樣不會釋放鎖。(個人感覺這個方法有點雞肋,如有合適的應用場景請留言)
(5)、join()、join(long millis)方法
等待這個線程死亡,或者等待這個線程死亡最多 等待millis毫秒。
在很多情況下,主線程創(chuàng)建并啟動了線程,如果子線程中要進行大量耗時運算,主線程往往將早于子線程結束之前結束。這時,如果主線程想等待子線程執(zhí)行完成之后再進行某些操作,比如子線程處理一個數(shù)據(jù),主線程要取得這個數(shù)據(jù)中的值,就要用到join()方法了。方法join()的作用是等待線程對象銷毀。
注意:join()不會釋放鎖
代碼演示:

/**
 * join方法演示
 *
 * @Author YUBIN
 * @create 2018-06-20
 */
public class JoinTest {
    private static class JoinThread extends Thread {

        private boolean flag;

        private JoinThread(boolean flag) {
            this.flag = flag;
        }

        @Override
        public void run() {
            if (flag) {
                // 執(zhí)行數(shù)據(jù)庫插入操作

                System.out.println(getName() + "數(shù)據(jù)庫插入操作完成");
            } else {
                // 執(zhí)行緩存操作
                System.out.println(getName() + "緩存操作執(zhí)行完成");
            }
        }
    }

    public static void main(String[] args) throws InterruptedException {
        Thread threadA = new JoinThread(true);
        Thread threadB = new JoinThread(false);
        threadA.setName("ThreadA");
        threadB.setName("ThreadB");
        threadA.start();
        threadB.start();
        // 主線程上的一些自己的操作
        System.out.println("主線程自己的業(yè)務邏輯執(zhí)行完成");
        // 等待線程A消亡
        threadA.join();
        // 等待線程B消亡
        threadB.join();
        System.out.println("主線程結合線程A和線程B處理一些業(yè)務");
    }
}

5、線程優(yōu)先級
Thread類中的成員變量priority控制優(yōu)先級,范圍1-10之間,缺省為5.可以通過setPriority()設置線程的優(yōu)先級,數(shù)字越大優(yōu)先級越高。

    /**
     * The minimum priority that a thread can have.
     */
    public final static int MIN_PRIORITY = 1;

   /**
     * The default priority that is assigned to a thread.
     */
    public final static int NORM_PRIORITY = 5;

    /**
     * The maximum priority that a thread can have.
     */
    public final static int MAX_PRIORITY = 10;

對于線程優(yōu)先級,我們需要注意:
Thread.setPriority()可能根本不做任何事情,這跟你的操作系統(tǒng)和虛擬機版本有關
線程優(yōu)先級對于不同的線程調度器可能有不同的含義,可能并不是你直觀的推測。特別地,優(yōu)先級并不一定是指CPU的分享。在UNIX系統(tǒng),優(yōu)先級或多或少可以認為是CPU的分配,但Windows不是這樣
線程的優(yōu)先級通常是全局的和局部的優(yōu)先級設定的組合。Java的setPriority()方法只應用于局部的優(yōu)先級。換句話說,你不能在整個可能的范圍 內設定優(yōu)先級。(這通常是一種保護的方式,你大概不希望鼠標指針的線程或者處理音頻數(shù)據(jù)的線程被其它隨機的用戶線程所搶占)
不同的系統(tǒng)有不同的線程優(yōu)先級的取值范圍,但是Java定義了10個級別(1-10)。這樣就有可能出現(xiàn)幾個線程在一個操作系統(tǒng)里有不同的優(yōu)先級,在另外一個操作系統(tǒng)里卻有相同的優(yōu)先級(并因此可能有意想不到的行為)
操作系統(tǒng)可能(并通常這么做)根據(jù)線程的優(yōu)先級給線程添加一些專有的行為(例如”only give a quantum boost if the priority is below X“)。這里再重復一次,優(yōu)先級的定義有部分在不同系統(tǒng)間有差別。
大多數(shù)操作系統(tǒng)的線程調度器實際上執(zhí)行的是在戰(zhàn)略的角度上對線程的優(yōu)先級做臨時操作(例如當一個線程接收到它所等待的一個事件或者I/O),通常操作系統(tǒng)知道最多,試圖手工控制優(yōu)先級可能只會干擾這個系統(tǒng)。
你的應用程序通常不知道有哪些其它進程運行的線程,所以對于整個系統(tǒng)來說,變更一個線程的優(yōu)先級所帶來的影響是難于預測的。例如你可能發(fā)現(xiàn),你有一個預期 為偶爾在后臺運行的低優(yōu)先級的線程幾乎沒有運行,原因是一個病毒監(jiān)控程序在一個稍微高一點的優(yōu)先級(但仍然低于普通的優(yōu)先級)上運行,并且無法預計你程序 的性能,它會根據(jù)你的客戶使用的防病毒程序不同而不同。
注意:不要假定高優(yōu)先級的線程一定先于低優(yōu)先級的線程執(zhí)行,不要有邏輯依賴于線程優(yōu)先級,否則可能產生意外結果。
6、守護線程Daemon
守護線程(如GC線程),程序里沒有非守護線程時,java程序就會推出。一般用不上,也不建議我們平時開發(fā)的時候使用,因為try/finally中的代碼不一定會執(zhí)行,如下代碼演示,如果將t1.setDaemon(true)代碼放開,則線程中的打印語句則不會執(zhí)行。

/**
 * 演示守護線程
 * @Author YUBIN
 * @create 2018-01-07
 */
public class DaemonThread {
    private static class TestRunnable implements Runnable {

        @Override
        public void run() {
            try {
                SleepUtils.second(2);
            }finally {
                System.out.println("TestRunnable is Daemon");
            }
        }
    }

    public static void main(String[] args) {
        Thread t1 = new Thread(new TestRunnable());
        //t1.setDaemon(true);
        t1.start();
    }
}

7、線程的中斷interrupt
(1)、不安全的取消
單獨使用一個取消標識(如果當前線程處于阻塞狀態(tài)是無法判斷標識位的)
stop()、suspend()、resume()是過期的api,很大的副作用,容易導致死鎖或者數(shù)據(jù)不一致的問題
(2)、安全的終止線程
使用線程的中斷:
interrupt():中斷線程,本質將線程的中斷標志位設為true,其它線程向需要中斷的線程打個招呼,是否真正進行中斷由線程自己決定
isInterruped():線程檢查自己的中斷標志位,如果未中斷返回false
interrupted():這是一個靜態(tài)的方法,判斷線程是否中斷。

演示安全的終止線程

/**
 * 安全的中斷線程
 *
 * @Author YUBIN
 * @create 2018-01-07
 */
public class SafeInterrupt {

    private static Object o = new Object(); // 鎖對象

    private static class TestThread extends Thread {

        private volatile boolean on = true;

        private Socket socket;

        private long i = 0;

        @Override
        public void run() {
            while (on && !Thread.currentThread().isInterrupted()) {
                System.out.println(i++);
                try {
                    synchronized (o) {
                        o.wait();
                    }
                } catch (InterruptedException e) {
                    // 當wait方法檢測到當前線程被中斷了,則拋出中斷異常,同時將標志位設為false
                    System.out.println("當前執(zhí)行線程的中斷標志位:" + Thread.currentThread().getId() + "," + Thread.currentThread().isInterrupted());
                    // 重新設置一下中斷標志位
                    interrupt();
                    System.out.println("被中斷的線程_"+getId()+":"+isInterrupted());
                }
            }
            System.out.println("TestRunnable is stop!" + i);
        }

        public void cancle() {
            System.out.println("Ready stop thread......");
            on = false;
            interrupt(); // 注意此處不能使用Thread.currentThread().interrupt()  這個方法是main線程調用的
        }
    }

    public static void main(String[] args) {
        TestThread t1 = new TestThread();
        t1.start();
        try {
            Thread.sleep(300);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        t1.cancle();
    }
}

運行結果

0
Ready stop thread......
當前執(zhí)行線程的中斷標志位:11,false
被中斷的線程_11:true
TestRunnable is stop!
1

當在main方法中調用線程的cancle()方法是,將標識位設為false同時將中斷標識位設為true;如果在while判斷中沒有中斷標識位的判斷,該線程是無法被終止的。
wait()方法在阻塞的過程中會循環(huán)的判斷當前線程的中斷標識位是否為false,未過不為false的時候會拋出中斷異常,同時會將中斷標識位設為false,因此在上述的代碼的catch語句中需要將線程重新中斷下。

總結


多線程.png
最后編輯于
?著作權歸作者所有,轉載或內容合作請聯(lián)系作者
【社區(qū)內容提示】社區(qū)部分內容疑似由AI輔助生成,瀏覽時請結合常識與多方信息審慎甄別。
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發(fā)布,文章內容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務。

相關閱讀更多精彩內容

  • 單任務 單任務的特點是排隊執(zhí)行,也就是同步,就像再cmd輸入一條命令后,必須等待這條命令執(zhí)行完才可以執(zhí)行下一條命令...
    Steven1997閱讀 1,356評論 0 6
  • 轉自 http://blog.csdn.net/ChatHello/article/details/6906097...
    呂品?閱讀 5,511評論 0 100
  • 進程和線程 進程 所有運行中的任務通常對應一個進程,當一個程序進入內存運行時,即變成一個進程.進程是處于運行過程中...
    小徐andorid閱讀 2,988評論 3 53
  • 寫在前面的話: 這篇博客是我從這里“轉載”的,為什么轉載兩個字加“”呢?因為這絕不是簡單的復制粘貼,我花了五六個小...
    SmartSean閱讀 4,941評論 12 45
  • 兩個面試題 實現(xiàn)一個reduce函數(shù),作用和原生的reduce類似下面的例子。 實現(xiàn)一個flatten函數(shù),將一個...
    jrg_tzc閱讀 314評論 0 0

友情鏈接更多精彩內容