2020-01-31-Java多線程

線程生命周期

線程生命周期.jpg

NEW:創(chuàng)建了一個線程對象,但是還沒有調用start()方法。此時稱為初始狀態(tài)NEW
RUNNABLE:調用了start()方法后,進入就緒狀態(tài),此時已經進入run()方法
BLOCKED:如果run()方法中有用synchronized包含的代碼塊,則需要等其他線程釋放鎖對象才能進入synchronized代碼塊。
BLOCKED狀態(tài)是不能被interrupt打斷的,只有其他線程釋放鎖之后,才能回到RUNNABLE狀態(tài)。
WAITING:如果線程主動調用wait()或join()方法,則進入WAITING狀態(tài),
直到join()的線程返回;
或者wait()的對象執(zhí)行notify()或notifyAll()方法被喚醒;
或者其他線程調用interrupt()方法。
如果沒有其它線程喚醒,WATING狀態(tài)會無限期等待下去,形成死鎖。
TIMED_WAITING:如果線程主動調用sleep(long millis),wait(long millis),或join(long millis)方法,則進入TIMED_WAITING狀態(tài),
直到超時返回或join()的線程返回;
或者wait()的對象執(zhí)行notify()或notifyAll()方法被喚醒;
或者其他線程調用interrupt()方法。
TERMINATED:如果run()方法執(zhí)行完畢,或者線程異常退出,進入TERMINATED狀態(tài)。進入此狀態(tài)后不能再調用start()方法。

線程等待

  1. sleep()和wait()
    sleep()沒有鎖的概念,wait()有。wait()方法會釋放線程本身持有的鎖。
    sleep()方法會進入TIMED_WAITING,直到超時返回,跟鎖沒有關系,如果sleep()方法被synchronized代碼塊包含,線程本身持有的鎖不會釋放。
    不帶參數的wait()方法會進入WAITING狀態(tài),無限期等待,直到滿足條件,或者interrupt()方法被調用。
    帶參數的wait(long millis)方法會進入TIMED_WAITING狀態(tài),能實現(xiàn)超時返回。
  2. sleep()和join()
    sleep()和join()都沒有鎖的概念,只是等待返回的條件不一樣。
    不帶參數的join()方法會進入WAITING狀態(tài),無限期等待,直到該線程返回,或者interrupt()方法被調用。
    帶參數的join(long millis)方法會進入TIMED_WAITING狀態(tài),能實現(xiàn)超時返回。
  3. sleep()和yield()
    sleep()和yield()都沒有鎖的概念。
    yield()方法不會改變線程的狀態(tài),只是讓出CPU使用權。有可能下一次CPU會立即執(zhí)行。
    sleep()方法會讓出CPU使用權,直到超時返回。
  4. sleep()和suspend()
    suspend()方法不會改變線程狀態(tài),依然是RUNNABLE,但是被暫停了不會執(zhí)行,只有通過resume()方法能夠喚醒。suspend()和resume()已經不推薦使用了。

線程安全

多線程并發(fā)可能會遇到的問題是同樣的輸入,每次的輸出不一樣,需要了解Java內存模型(JMM)的幾個概念:

  1. 原子性:操作中途是不能被其他線程打斷的;
  2. 可見性:一個線程對共享變量的修改,其他線程是立即可見的;
  3. 有序性:在多核處理器的環(huán)境下,代碼的執(zhí)行順序是沒保障的,需要其他條件來保證。
    在Java內存模型中,允許編譯器和處理器對指令進行重排序。重排序過程不會影響到單線程程序的執(zhí)行,卻會影響到多線程并發(fā)執(zhí)行的正確性。


    內存模型.jpg

為了實現(xiàn)操作的原子性,可見性和有序性,通常會用到鎖。
Java提供了synchronized關鍵字來實現(xiàn)內部鎖。被 synchronized 關鍵字修飾的方法和代碼塊叫同步方法和同步代碼塊。

  1. synchronized 方法
    鎖的對象是每個類實例,只有兩個線程對同一個類實例的synchronized方法進行訪問才會競爭鎖資源,分別對兩個實例的同一個synchronized方法訪問是不影響的。
    例如下面這個例子:
    當兩個線程分別去訪問同個對象的兩個synchronized方法時,就可能出現(xiàn)時序問題。
    如果count()方法先執(zhí)行,兩個線程都能夠正常運行。
    如果waitCount()方法先執(zhí)行,就會出現(xiàn)死鎖,waitCount線程進入死循環(huán),一直在TIMED_WAITING狀態(tài),count線程沒有獲取到對象鎖,一直在BLOCKED狀態(tài)。
public class ThreadTest {
    
    private volatile int count = 0;
    
    public synchronized void count() {
        for (int i = 0 ; i < 10; i++) {
            count++;
        }
        System.out.println(Thread.currentThread().getName() + " count=" + count);
    }
    
    public synchronized void waitCount() {
        System.out.println(Thread.currentThread().getName() + " wait start");
        while (count < 10) {
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
                break;
            }
        }
        System.out.println(Thread.currentThread().getName() + " wait finish");
    }
}
        ThreadTest test = new ThreadTest();
        Thread thread1 = new Thread(new Runnable() {
            
            @Override
            public void run() {
                test.waitCount();
            }
        }, "thread1");
        Thread thread2 = new Thread(new Runnable() {
            
            @Override
            public void run() {
                test.count();
            }
        }, "thread2");

synchronized方法保證了操作的原子性,所以synchronized內部最好不要對共享變量使用一些while判斷,防止出現(xiàn)死循環(huán)。

  1. synchronized代碼塊
    synchronized代碼塊跟synchronized方法是類似的,只不過可以指定鎖的對象,使用比較靈活,可以把盡可能少的操作放進同步代碼,避免其他線程等待。
    例如上面的例子,只需要把count()的同步方法改成同步代碼塊,就能解決死循環(huán)的問題。
public class ThreadTest {
    
    private volatile int count = 0;
    
    public void count() {
        for (int i = 0 ; i < 10; i++) {
            count++;
        }
        synchronized (this) {
            System.out.println(Thread.currentThread().getName() + " count=" + count);
        }
    }
    
    public synchronized void waitCount() {
        System.out.println(Thread.currentThread().getName() + " wait start");
        while (count < 10) {
            try {
                Thread.sleep(100);
            } catch (InterruptedException e) {
                e.printStackTrace();
                break;
            }
        }
        System.out.println(Thread.currentThread().getName() + " wait finish");
    }
}
  1. 顯式鎖
    常用的顯式鎖有ReentrantLock,實現(xiàn)了Lock接口的四個方法:
    lock()和lock.unlock()方法類似synchronized,等待鎖的過程中線程阻塞,只不過synchronized是進入BLOCKED狀態(tài),lock()是進入WAITING狀態(tài)。
    lockInterruptibly()可以被中斷,需要catch InterruptedException異常。
    tryLock()成功獲取鎖返回true,否則false,不會阻塞。
    tryLock(long time, TimeUnit unit),阻塞等待一段時間,然后返回。
    此外還有讀寫鎖,這里不介紹,可以看下ReadWriteLock這個類。
  2. volatile
    一般變量保存在高速緩存區(qū),不會立刻同步到主內存,導致不同線程間的數據不同步,使用volatile關鍵字就是為了保證數據的可見性。

下面寫了一個簡單例子,兩個線程thread1和thread2會競爭lock對象鎖

package com.one.thread;

import java.util.Random;

import com.one.log.Log;

public class ThreadTest {
    
    private volatile int count = 0;
    private Log log = new Log("ThreadTest");
    private Object lock = new Object();
    
    public void run() {
        thread1.start();
        thread2.start();
    }
    
    private void sleepRandom() {
        try {
            Thread.currentThread().sleep(new Random().nextInt(1000));
        } catch (InterruptedException e) {
            log.log("sleep interrupt");
        }
    }
    
    private Thread thread1 = new Thread(new Runnable() {
        
        @Override
        public void run() {
            try {
                thread2.join(1000);
            } catch (InterruptedException e) {
                log.log("thread1 joinInterrupt");
            }
            for (int i = 0; i < 10; i++) {
                synchronized (lock) {
                    sleepRandom();
                    count++;
                    log.log("count=" + count + " thread2=" + thread2.getState());
                    lock.notifyAll();
                }
            }
        }
    }, "thread1");
    
    private Thread thread2 = new Thread(new Runnable() {
        
        @Override
        public void run() {
            synchronized (lock) {
                sleepRandom();
                log.log("thread1=" + thread1.getState());
                while (count < 6) {
                    try {
                        lock.wait();
                        sleepRandom();
                        log.log("waitting count=" + count+ " thread1=" + thread1.getState());
                    } catch (InterruptedException e) {
                        log.log("thread2 waitInterrupt");
                        break;
                    }
                }
                log.log("thread2 waitFinished");
            }
        }
    }, "thread2");
}

這兩個線程的狀態(tài)如下


Thread.png

如果代碼中的join()方法沒有加超時,就會形成死鎖。
參考:

https://www.cnblogs.com/dolphin0520/p/3920373.html
https://www.cnblogs.com/trust-freedom/p/6606594.html

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

相關閱讀更多精彩內容

友情鏈接更多精彩內容