【JAVA提升】- 線程、線程池、并發(fā)包(2)

1.java的線程Thread

現(xiàn)在的操作系統(tǒng)是多任務(wù)操作系統(tǒng)。多線程是實現(xiàn)多任務(wù)的一種方式。

進程是指一個內(nèi)存中運行的應(yīng)用程序,每個進程都有自己獨立的一塊內(nèi)存空間,一個進程中可以啟動多個線程。比如在Windows系統(tǒng)中,一個運行的exe就是一個進程。

線程是指進程中的一個執(zhí)行流程,一個進程中可以運行多個線程。比如java.exe進程中可以運行很多線程。線程總是屬于某個進程,進程中的多個線程共享進程的內(nèi)存。

引用網(wǎng)上對線程的一個說法,個人覺得比較的形象

1.1 線程的創(chuàng)建和啟動

1.1.1 線程創(chuàng)建

創(chuàng)建線程方式主要有兩個:

  1. 繼承Thread類,利用構(gòu)造方法創(chuàng)建一個線程
  2. 實現(xiàn)Runnable接口。在利用帶Runnable參數(shù)的構(gòu)造方法

看例子:

1. 實現(xiàn)Thread類

 class PrimeThread extends Thread {
         long minPrime;
         PrimeThread(long minPrime) {
             this.minPrime = minPrime;
         }

         public void run() {
             // compute primes larger than minPrime
              . . .
         }
     }
     
 PrimeThread p = new PrimeThread(143);
 p.start();

2. 實現(xiàn)Runnable 接口

 class PrimeRun implements Runnable {
         long minPrime;
         PrimeRun(long minPrime) {
             this.minPrime = minPrime;
         }

         public void run() {
             // compute primes larger than minPrime
              . . .
         }
     }

PrimeRun p = new PrimeRun(143);
     new Thread(p).start();

1.1.2 Thread和Runnable

看了上面分別使用繼承的方式和runnable接口的方式,那他們又有何不同呢

其實看看兩者的代碼區(qū)別就知道了,如果繼承的話,每次new Thread創(chuàng)建一個新的線程,然而runnable的方式雖然也是每次new Thread() ,但是,構(gòu)造方法中的runnable可以是同一個也可以是每次new一個。這點可以有很大的區(qū)別,可以很好利用

假如我們線程有個自己的私有成員,對應(yīng)使用繼承Thread 的方式,每次new ,這個私有成員一定是自己所有的。但是使用runnable的話,就不一定了。


package com.fun.thread;

/**
 * 實現(xiàn)runnable接口的任務(wù)類
 *
 * @author fun
 * @version v1.0.0
 * @create 2017-03-13 21:46
 */
public class TestTask implements Runnable {

    private int taskId;
    volatile private int count; // 可以做共享變量

    public TestTask(int taskId,int count) {
        this.taskId = taskId;
        this.count = count;
    }

    @Override
    public void run() {
        System.out.println("taskId is:"+taskId+" , count is:"+count);
        try {
            System.out.println("threadId: "+Thread.currentThread().getId()+
                    ", threadName: "+Thread.currentThread().getName()+
                    ",isDaemon " + Thread.currentThread().isDaemon());
            Thread.sleep(200);
            count--;
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}

上面這段代碼是一個實現(xiàn)runnable接口的任務(wù)。請看在使用時候的區(qū)別

@Test
    public void test1() throws InterruptedException {
        // 1. 每個線程都有一個新的Runnable
        for (int i = 0; i < 10; i++) {
            new Thread(new TestTask(i + 1, 10)).start();
            Thread.sleep(200);
        }
    }

    @Test
    public void test2()  throws InterruptedException {
        TestTask testTask = new TestTask(1, 10);
        for (int i = 0; i < 10; i++) {
            new Thread(testTask).start();
            Thread.sleep(200);
        }
    }

上面結(jié)果

  1. test1中,每次new TestTask傳遞給Thread,所以打印的都是count=10
  2. test2中,每個Thread其實都是用的一個runnable構(gòu)造,這個時候他們共享TestTask的count值。打印的count減小了

所以這里可以利用這個特點處理共享資源,只要合理加鎖,就可以處理好共享資源,如上面count加上volatile 保證可見性,再count-- 加塊級鎖就ok

1.1.3 線程啟動

之前也有代碼使用過線程,線程啟動一般使用 start() 或者 run() 但是一般建議是start()

為什么建議使用start() ?

其實使用run()和start(),最終都是調(diào)用的run,最重要的區(qū)別在于,執(zhí)行方法的線程是誰。
使用 start() 方法,是新建立的線程在執(zhí)行,然而使用run()時候,是run()調(diào)用處的線程
(如果在主線程直接調(diào)用了run() ,操作run()的線程就是main,并不是生成的新的線程)


@Test
public void testRunAndStartDiff() {

    System.out.println("main threadId: "+Thread.currentThread().getId()+
                    ",main threadName: "+Thread.currentThread().getName()+
                    ",isDaemon " + Thread.currentThread().isDaemon());
    Thread t = new Thread(new TestTask(1,10));
    t.start();

    Thread t2 = new Thread(new TestTask(2,20));
    t2.run();
}

main threadId: 1,main threadName: main,isDaemon false
taskId is:2 , count is:20
threadId: 1, threadName: main,isDaemon false
taskId is:1 , count is:10
threadId: 11, threadName: Thread-0,isDaemon false   


可以看到 Task 2 是用的run() ,實際上是ThreadId=1 的線程執(zhí)行的(main)
Task1 是自己生成的線程(ThreadId=11)執(zhí)行的

所以,注意是誰執(zhí)行自己,在多線程處理的時候,取Thread.currentThread()注意,是start()啟動還是run啟動

生活一個開關(guān)我們打開了,自己有可能把它關(guān)掉在打開。同樣,如果一個線程start()之后,我還可以拿著這個Thread 在開始一次嗎

例如:

@Test
public void testMultiCallStart() {

    Thread t = new Thread(new TestTask(1,10));
    t.start();
    t.start(); 
}

測試結(jié)構(gòu)是不能再次調(diào)用start,直接報java.lang.IllegalThreadStateException

跟進start() 方法源碼就會發(fā)現(xiàn)

if (threadStatus != 0)
            throw new IllegalThreadStateException();

start()之前會先判斷線程狀態(tài),但是如果用 t.run() 是可以多次調(diào)用的。也算是start() 和 run()的區(qū)別吧 ,因為直接調(diào)用run()其實都沒有新建線程

說到線程狀態(tài),那么來看看線程的狀態(tài)到底有哪些?

1.2 線程狀態(tài)

1.2.1 線程狀態(tài)分析

public enum State {
        /**
         * Thread state for a thread which has not yet started.
         */
        NEW,

        /**
         * Thread state for a runnable thread.  A thread in the runnable
         * state is executing in the Java virtual machine but it may
         * be waiting for other resources from the operating system
         * such as processor.
         */
        RUNNABLE,

        /**
         * Thread state for a thread blocked waiting for a monitor lock.
         * A thread in the blocked state is waiting for a monitor lock
         * to enter a synchronized block/method or
         * reenter a synchronized block/method after calling
         * {@link Object#wait() Object.wait}.
         */
        BLOCKED,

        /**
         * Thread state for a waiting thread.
         * A thread is in the waiting state due to calling one of the
         * following methods:
         * <ul>
         *   <li>{@link Object#wait() Object.wait} with no timeout</li>
         *   <li>{@link #join() Thread.join} with no timeout</li>
         *   <li>{@link LockSupport#park() LockSupport.park}</li>
         * </ul>
         *
         * <p>A thread in the waiting state is waiting for another thread to
         * perform a particular action.
         *
         * For example, a thread that has called <tt>Object.wait()</tt>
         * on an object is waiting for another thread to call
         * <tt>Object.notify()</tt> or <tt>Object.notifyAll()</tt> on
         * that object. A thread that has called <tt>Thread.join()</tt>
         * is waiting for a specified thread to terminate.
         */
        WAITING,

        /**
         * Thread state for a waiting thread with a specified waiting time.
         * A thread is in the timed waiting state due to calling one of
         * the following methods with a specified positive waiting time:
         * <ul>
         *   <li>{@link #sleep Thread.sleep}</li>
         *   <li>{@link Object#wait(long) Object.wait} with timeout</li>
         *   <li>{@link #join(long) Thread.join} with timeout</li>
         *   <li>{@link LockSupport#parkNanos LockSupport.parkNanos}</li>
         *   <li>{@link LockSupport#parkUntil LockSupport.parkUntil}</li>
         * </ul>
         */
        TIMED_WAITING,

        /**
         * Thread state for a terminated thread.
         * The thread has completed execution.
         */
        TERMINATED;
    }
  1. NEW 創(chuàng)建完成,但是沒有啟動
  2. RUNABLE 運行狀態(tài)。正在java虛擬中被執(zhí)行,但是有可能正在等待系統(tǒng)資源,比如處理器資源
  3. BLOCKED 受阻塞,并在等待監(jiān)視器鎖。線程正在等待監(jiān)視器鎖,以便進入同步方法/塊,或者這調(diào)用Object.wait()方法后再次進入同步方法/塊
  4. WAITING 等待中,線程調(diào)用如下方法會進入等待狀態(tài)
    1. Object.wait()并且沒有超時時間
    2. Thread.join() 并且沒有超時時間
    3. LockSupport.park()

例如:已經(jīng)在某一對象上調(diào)用了 Object.wait() 的線程正等待另一個線程,以便在該對象上調(diào)用 Object.notify() 或 Object.notifyAll()。

  1. TIMED_WAITING 指定等待時間的等待,調(diào)用如下方法會進入此狀態(tài)
    1. Thread.sleep()
    2. Object.wait() 指定超時時間
    3. Thread.join() 執(zhí)行超時時間
    4. LockSupport.parkNanos
    5. LockSupport.parkUntil
  2. TERMINATED 線程結(jié)束,完成執(zhí)行

1.2.2 線程狀態(tài)轉(zhuǎn)換圖

線程狀態(tài)之間的轉(zhuǎn)換圖

線程狀態(tài)轉(zhuǎn)換圖

1.3 關(guān)于守護線程 Daemon Thread

java中的線程分為兩類:用戶線程(User Thread)、守護線程(Daemon Thread)

守護線程就是程序運行的時候在后臺提供一種通用的服務(wù)的線程。比如:垃圾回收線程。這種線程并不是程序中不可或缺的,因此,當(dāng)所有的非守護線程結(jié)束時候,程序也會終止,同時會殺死進程中所有的守護線程。

用戶線程和守護線程幾乎沒有什么區(qū)別,唯一的不同之處在于虛擬機的離開:如果所有的用戶線程結(jié)束了,守護線程沒有守護對象,程序還是會結(jié)束。

將線程轉(zhuǎn)換成守護線程可以通過Thread對象的setDaemon(true)方法來實現(xiàn)。使用守護線程需要注意:

  1. thread.setDaemon(true)必須在thread.start()之前設(shè)置,否則會跑出一個IllegalThreadStateException異常。你不能把正在運行的常規(guī)線程設(shè)置為守護線程。
  2. 在Daemon線程中產(chǎn)生的新線程也是Daemon的
  3. 守護線程應(yīng)該永遠(yuǎn)不去訪問固有資源,如文件、數(shù)據(jù)庫,因為它會在任何時候甚至在一個操作的中間發(fā)生中斷(如:非守護線程都停止了)。

1.4 Thread類常用方法

1.4.1 start()

start作用就是啟動一個線程,他和run()的區(qū)別在前面也有說過

需要注意的是,如果多個線程在程序代碼中順序的調(diào)用start方法,并能保證兩個線程的啟動順序,例如:

Thread
    t1 = new Thread(),
    t1 = new Thread(),
    t1 = new Thread();

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

實際的啟動順序是隨機,和cpu的調(diào)度有關(guān)

1.4.2 sleep()

sleep(long mills) 是Thread 類的一個今天native的方法,調(diào)用sleep線程進入阻塞。參數(shù)為0則一直等待。

需要注意的是,如果線程中獲得某個對象的內(nèi)置鎖,在sleep的時候是不會釋放鎖的,這點和后面要說的wait()不同,wait()是會釋放鎖的

Causes the currently executing thread to sleep (temporarily cease execution) for the specified number of milliseconds, subject to the precision and accuracy of system timers and schedulers. The thread does not lose ownership of any monitors.

以上引用自sleep方法源碼上面的解釋,最后一句說明了,sleep不釋放鎖

1.4.3 interrupt()

調(diào)用線程打斷,如果線程正因為調(diào)用了wait() ,sleep(),join等方法阻塞的時候,就會拋出一個InterruptedException

1.4.4 wait、notify()/notifyAll()

這三個方法都是Object類實例的方法,由于這三個方法在使用的時候都涉及到鎖的操作(獲取和釋放),因此,這三個方法必須要在同步代碼塊中執(zhí)行,否則拋出IllegalMonitorStateException異常。

使用示例:

/**
 * Thread wait方法學(xué)習(xí)
 *
 * @author fun
 * @version v0.0.1
 * @date 2017-03-21 14:45
 */
public class WaitTest {
    private static volatile Integer o = 0;

    public static void main(String[] args) {

        Thread t1 = new Thread(new Runnable() {
            @Override
            public void run() {
                System.out.println("thread one start..."+System.currentTimeMillis());
                try {
                    Thread.sleep(3000);// 讓t2先獲得o的內(nèi)置鎖
                    synchronized (o) {
                        System.out.println("notify thread two on object o before..."+System.currentTimeMillis());
                        o.notify();
                        System.out.println("notify thread two on object o end..."+System.currentTimeMillis());
                    }
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                System.out.println("thread one end..."+System.currentTimeMillis());

            }
        });

        Thread t2 = new Thread(new Runnable() {
            @Override
            public void run() {
                synchronized (o) {
                    System.out.println("thread two start..."+System.currentTimeMillis());
                    try {
                        o.wait(0);
                        System.out.println("awake...."+System.currentTimeMillis());
                        Thread.sleep(1000);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    System.out.println("thread two end..."+System.currentTimeMillis());
                }
            }
        });

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

    }
}
// output
/*
thread two start...1490176028853
thread one start...1490176028853
notify thread two on object o before...1490176031854
notify thread two on object o end...1490176031854
thread one end...1490176031854
awake....1490176031854
thread two end...1490176032854

*/

可以看到,t1 和 t2 都是對Object o 加鎖,但是在t2里面o.wait之后,t1就能拿到鎖了(awake.... 比 notify xxxx 后打印,t2的同步塊沒有執(zhí)行完,鎖就釋放了),所以可以看出,wait是會釋放鎖的。

同時程序中為了使t2先拿到鎖o從而先wait住,然后讓t1 中釋放鎖o,故意在t1進來后先sleep了。那實際開發(fā)中肯定不能這樣,實際應(yīng)該怎樣做呢 ?

在多線環(huán)境先一般建議在循環(huán)中使用wait,使用循環(huán)的條件做判斷,例如在join的源碼中有一段,如果join的是時間傳的是0的情況的處理:

if (millis == 0) {
            while (isAlive()) {
                wait(0);
            }
        }

這樣在線程順序未知的情況下,依然可以讓wait生效

所有借鑒這個,實際生產(chǎn)中,我們控制好循環(huán)條件,就可以正確的使用wait了。

總的來說就是: wait方法會釋放鎖,當(dāng)前的線程(上例中的t2)被掛起,且wait方法要在循環(huán)中使用,控制好條件來跳出循環(huán),notify/notifyAll 配飾wait使用

結(jié)合上面的例子就是,t2中調(diào)用o.wait的時候,t2線程被掛起,不在執(zhí)行,需要等待喚醒。o.wait() 釋放掉t2對o的鎖,使t1能夠獲得o的鎖,執(zhí)行o.notify喚醒t2,然后t2繼續(xù)執(zhí)行完成。

1.4.5 yield()

簡單講就是告訴cpu我可以讓出資源,注意是可以,也就是說,具體會不會讓出,看cpu的調(diào)度了。此方法一般少用

1.4.6 join()

join方法的實質(zhì)是wait, 理解join單詞的字面意思,也許會更好理解join做的事情。join就是加入,如果線程A里面執(zhí)行了線程b.join() 就是A線程進入等待,等b線程執(zhí)行完。再接著執(zhí)行。 換個角度就像是A在完成一件事的時候,把另外一件事B加進來,所以join就很形象。

示例代碼:

/**
 * join方法測試
 *
 * @author fun
 * @version v0.0.1
 * @date 2017-03-21 9:37
 */
public class JoinTest {
    public static void main(String[] args) throws InterruptedException {
        Thread t1 = new Thread(new Runnable() {
            public void run()
            {
                System.out.println("First task started");
                System.out.println("Sleeping for 2 seconds");
                try
                {
                    Thread.sleep(2000);
                } catch (InterruptedException e)
                {
                    e.printStackTrace();
                }
                System.out.println("First task completed");
            }
        });
        Thread t2 = new Thread(new Runnable(){
            public void run()
            {
                try {
                    t1.join();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                System.out.println("Second task completed");
            }
        });
        t1.start();
        t2.start();
    }
}
// ouput
/*
First task started
Sleeping for 2 seconds
First task completed
Second task completed
*/

從結(jié)果可以很清楚的看到,t2是在等t1執(zhí)行完在執(zhí)行的,哪怕t1中有sleep

關(guān)于join的執(zhí)行過程,他本質(zhì)上是執(zhí)行wait,那又是在哪notify的呢 ?調(diào)用join的過程是怎樣的呢 ?

先看源碼:

    public final synchronized void join(long millis)
    throws InterruptedException {
        long base = System.currentTimeMillis();
        long now = 0;

        if (millis < 0) {
            throw new IllegalArgumentException("timeout value is negative");
        }

        if (millis == 0) {
            while (isAlive()) {
                wait(0);
            }
        } else {
            while (isAlive()) {
                long delay = millis - now;
                if (delay <= 0) {
                    break;
                }
                wait(delay);
                now = System.currentTimeMillis() - base;
            }
        }
    }
    
    public final void join() throws InterruptedException {
        join(0);
    }

首先這個方法是一個Thread的實例的方法,并且注意是個同步的方法(很好理解,前面說的wait方法會操作鎖嘛)

就那示例程序分析吧,t1.join() 最后調(diào)用到了join(0) ,那么就會進入如下代碼塊

    if (millis == 0) {
        while (isAlive()) {
            wait(0);
        }
    }

這段代碼,isAlive是誰在調(diào)用,wait(0) 是誰在調(diào)用?肯定是調(diào)用join的實例也就是t1,那么也就是說這段的邏輯就是如果t1還存活,就一直調(diào)用t1.wait() ,而整個代碼(t1.join)是在t2里面調(diào)用的。

那么,整個意思就是 t2中調(diào)用t1.join實際就是判斷如果t1.isAlive == true 就調(diào)用t1.wait() ,t2 需要獲取一個t1內(nèi)置鎖。直到某個地方調(diào)用t1.notify 釋放t1的內(nèi)置鎖,t2才繼續(xù)執(zhí)行。

以上就是join的過程,那么,t1內(nèi)置鎖什么時候釋放的呢 ?誰通知的t2(即執(zhí)行t1.notify)的呢?剛才源碼分析沒見哪里notify,示例運行t2確實執(zhí)行了啊,沒有一直等待鎖啊?

這個之前也困擾我很久,后來在知乎上看到了答案 https://www.zhihu.com/question/44621343 回答者cao解釋了。

在線程退出的jvm源碼中有如下一段:

作者:cao
鏈接:https://www.zhihu.com/question/44621343/answer/97640972
來源:知乎
著作權(quán)歸作者所有。商業(yè)轉(zhuǎn)載請聯(lián)系作者獲得授權(quán),非商業(yè)轉(zhuǎn)載請注明出處。

//一個c++函數(shù):
void JavaThread::exit(bool destroy_vm, ExitType exit_type) ;

//這家伙是啥,就是一個線程執(zhí)行完畢之后,jvm會做的事,做清理啊收尾工作,
//里面有一個賊不起眼的一行代碼,眼神不好還看不到的呢,就是這個:

ensure_join(this);

//翻譯成中文叫 確保_join(這個);代碼如下:

static void ensure_join(JavaThread* thread) {
  Handle threadObj(thread, thread->threadObj());

  ObjectLocker lock(threadObj, thread);

  thread->clear_pending_exception();

  java_lang_Thread::set_thread_status(threadObj(), java_lang_Thread::TERMINATED);

  java_lang_Thread::set_thread(threadObj(), NULL);

  //同志們看到了沒,別的不用看,就看這一句,媽了個淡淡,
//thread就是當(dāng)前線程,是啥是啥?就是剛才說的b線程啊。
  lock.notify_all(thread);

  thread->clear_pending_exception();
}

這樣整個過程就清晰了。t1執(zhí)行完了之后,對t1內(nèi)置鎖執(zhí)行了notifyAll(),所有t2被喚醒,執(zhí)行完成。

1.5 wait-notify/notifyAll 和 循環(huán)檢測等待的區(qū)別

之前有說過,wait和notify可以類似個等待通知,其實不用wait-notify模式也是可以做的,例如現(xiàn)有如下場景:

A讓B幫自己去買包煙回來,A等到B把煙買回交給自己的時候,A才給B錢

wait-notify的模式

A中

while(!isGetCigarette) { //沒有得到煙
    cigarette.wait(0)
}
giveMoneyToB();

B中

    // 如果自己買到煙,就通知A 
    cigarette.notify() 

不用wait-notify

A中

while(!isGetCigarette) { //沒有得到煙
    // doNothing
}
giveMoneyToB();

B中

    
    // 如果自己買到煙就設(shè)置標(biāo)識為true
    isGetCigarette = true;

兩種都要求isGetCigarette是一個共享的變量。

那么這兩種有什么區(qū)別呢 ? 如果沒有區(qū)別,是不是wait-notify豈不是沒有存在意義 ?

原因就在于:處于wait()中的線程是中斷的,被掛起的,不會搶占cpu的計算時間;而相反的,無線循環(huán)保證了線程的就緒態(tài),會占用cpu時間。占用cpu即會減少其他線程的計算資源,導(dǎo)致性能下降

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

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容