JAVASE-多線程+鎖

1進程和線程

進程:正在運行的程序。也就是表明了程序鎖佔用的內存區域。
線程:線程(thread)是操做系統可以進行運算調度的最小單位;一個進程能夠開啓多個線程。
進程和線程的區別:
一個軟件運行至少依賴一個進程。
一個進程的運行至少依賴一個線程。面試

2多線程

多線程:多線程擴展了多進程的概念,使得同一個進程能夠同時併發處理多個任務。編程

2.1線程狀態

image.png
線程生命週期,總共有五種狀態:
1) 新建狀態(New):當線程對象對建立後,即進入了新建狀態,如:Thread t = new MyThread();
2) 就緒狀態(Runnable):當調用線程對象的start()方法(t.start();),線程即進入就緒狀態。處於就緒狀態的線程,只是說明此線程已經作好了準備,隨時等待CPU調度執行,並非說執行了t.start()此線程當即就會執行;
3) 運行狀態(Running):當CPU開始調度處於就緒狀態的線程時,此時線程才得以真正執行,即進入到運行狀態。注:就緒狀態是進入到運行狀態的惟一入口,也就是說,線程要想進入運行狀態執行,首先必須處於就緒狀態中;
4) 阻塞狀態(Blocked):處於運行狀態中的線程因爲某種緣由,暫時放棄對CPU的使用權,中止執行,此時進入阻塞狀態,直到其進入到就緒狀態,纔有機會再次被CPU調用以進入到運行狀態;
5) 根據阻塞產生的緣由不一樣,阻塞狀態又能夠分爲三種:設計模式

a)   等待阻塞:運行狀態中的線程執行wait()方法,使本線程進入到等待阻塞狀態;
b)   同步阻塞:線程在獲取synchronized同步鎖失敗(由於鎖被其它線程所佔用),它會進入同步阻塞狀態;
c)   其餘阻塞:經過調用線程的sleep()或join()或發出了I/O請求時,線程會進入到阻塞狀態。當sleep()狀態超時、join()等待線程終止或者超時、或者I/O處理完畢時,線程從新轉入就緒狀態。

6) 死亡狀態(Dead):線程執行完了或者因異常退出了run()方法,該線程結束生命週期。安全

2.2線程建立

image.png

2.2.1繼承Thread

建立對象:多線程

Thread() 
    分配新的 Thread 對象。 
Thread(Runnable target) 
    分配新的 Thread 對象。 
Thread(Runnable target, String name) 
    分配新的 Thread 對象。 
Thread(String name) 
    分配新的 Thread 對象。

經常使用方法:併發

long getId() 
    返回該線程的標識符。 
String getName() 
    返回該線程的名稱。 
void run() 
    若是該線程是使用獨立的 Runnable 運行對象構造的,則調用該 Runnable 對象的 run 方法;不然,該方法不執行任何操做並返回。 
void setName(String name) 
    改變線程名稱,使之與參數 name 相同。 
static void sleep(long millis) 
    在指定的毫秒數內讓當前正在執行的線程休眠(暫停執行), 
void start() 
    使該線程開始執行;Java 虛擬機調用該線程的 run 方法。 
void stop() 
    終止線程。

測試:app

package cn.tedu.thread;
        //測試  Thread類
        public class Test2_Thread {
            public static void main(String[] args) {
                //1,建立對象
                MyThread t = new MyThread();//新建狀態
                //2,開啓線程
        //      t.run();
                 t.setName("線程1");//修改線程名
                t.start();//可運行狀態
                //3,run()和start()有什麼區別?
                //run()和start()都能執行任務,可是run()執行時就只是一個普通的方法調用沒有多線程併發搶佔的效果
                //--模擬多線程--
                MyThread t2 = new MyThread();
        //        t2.run();
                 t.setName("線程2");//修改線程名
                t2.start();
                /*
                4,多線程的執行結果具備隨機性
                Thread-0---0
                Thread-1---0
                Thread-0---1
                Thread-1---1
                Thread-0---2
                Thread-1---2
                Thread-1---3*/
            }
        }
        //1,繼承Thread類,實現多線程編程
        class MyThread extends Thread {
            //2,在多線程編程裏,把全部的業務放入--重寫的run()裏--generate--override methods..
            //--需求:重複的打印10次線程名稱
            @Override
            public void run() {//運行狀態
                for (int i = 0; i < 10; i++) {//fori
                    System.out.println(super.getName()+"---"+i);//getName()--獲取線程名稱
                }
            }//結束狀態
        }

2.2.2實現Runnable接口

概述:若是本身的類已經extends另外一個類,就沒法多繼承,此時,能夠實現一個Runnable接口。ide

經常使用方法:性能

void run()
      使用實現接口 Runnable 的對象建立一個線程時,啓動該線程將致使在獨立執行的線程中調用對象的 run 方法。

測試:測試

package cn.tedu.thread;
//測試 Runnable接口
public class Test3_Runnable {
            public static void main(String[] args) {
                //1,建立目標對象
                MyRunnable target = new MyRunnable();
                //2,把要執行的目標 和Thread綁定關係
                Thread t = new Thread(target);
                t.setName("豬豬俠");
                //3,啓動線程
                t.start();

                //4,模擬多線程
                Thread t2 = new Thread(target);
                t2.setName("蜘蛛俠");
                t2.start();
        /*      5,多線程的隨機性
                Thread-0===0
                Thread-0===1
                Thread-1===0
                Thread-1===1
                Thread-1===2
                Thread-1===3
                Thread-1===4
                Thread-0===2
                Thread-0===3
        */
            }
        }
        //1,實現Runnable接口,實現多線程編程
        class MyRunnable implements Runnable{
            //2,若是有本身的業務,須要把業務 -- 放在重寫的run()裏
            //--需求:打印10次線程名稱
            @Override
            public void run() {
                for (int i = 0; i < 20; i++) {
                    //Thread.currentThread()--獲取正在執行任務的線程對象
                    //getName()--獲取線程的名字
                    //3,Thread.currentThread().getName() -- 獲取當前執行任務的線程對象的 名字
                    System.out.println( Thread.currentThread().getName() +"==="+i );
                }
            }
        }

2.2.3實現Callable接口

建立步驟:
1.建立Callable接口的實現類,並重寫call()方法,該方法做爲線程的執行體,並有返回值。
2.建立Callable實現類的實例,使用FutureTask類來包裝Callable對象,該FutureTask對象封裝了該Callable對象的call()方法的返回值。
3.建立Thread,調用start()方法。
4.但是有FutureTask.get()獲取call()的返回值。

public class TestCallable {
    public static void main(String[] args) throws InterruptedException, ExecutionException {
        CallableDemo callableDemo = new CallableDemo();
        FutureTask futureTask = new FutureTask(callableDemo); 
        new Thread(futureTask).start();//啓動線程對象,並啓動線程
        //Integer a = (Integer) futureTask.get(); //經過get()獲取返回值
        for (int j = 0; j < 10; j++) {
            System.out.println("main:"+j);
        }
    }
}
 
class CallableDemo implements Callable{
 
    @Override
    public Integer call() throws Exception {
        int i =0;
        for (; i < 10; i++) {
            System.out.println("線程:"+i);
        }
        return i;
    }
}

結果:

main:0
線程:0
main:1
線程:1
main:2
線程:2
main:3
線程:3
線程:4
線程:5
線程:6
線程:7
線程:8
線程:9
main:4
main:5
main:6
main:7
main:8
main:9

2.2.4建立線程池


線程池是池化思想的一種應用,都是經過複用對象,以減小因建立和釋放對象所帶來的資源消耗,進而來提高系統性能。

ThreadPoolExecutor、AbstractExecutorService、ExecutorService和Executor之間的關係:

  • Executor是一個頂層接口,在它裏面只聲明瞭一個方法execute(Runnable),返回值爲void,參數爲Runnable類型,從字面意思能夠理解,就是用來執行傳進去的任務的;
  • ExecutorService接口繼承了Executor接口,並聲明瞭一些方法:submit、invokeAll、invokeAny以及shutDown等;
  • 抽象類AbstractExecutorService實現了ExecutorService接口,基本實現了ExecutorService中聲明的全部方法;
  • ThreadPoolExecutor繼承了類AbstractExecutorService。

推薦使用ThreadPoolExecutor建立線程

// 建立線程池
ThreadPoolExecutor threadPool = new ThreadPoolExecutor(CORE_POOL_SIZE,
                                             MAXIMUM_POOL_SIZE,
                                             KEEP_ALIVE,
                                             TimeUnit.SECONDS,
                                             sPoolWorkQueue,
                                             sThreadFactory);
// 向線程池提交任務
threadPool.execute(new Runnable() {
    @Override
    public void run() {
        ... // 線程執行的任務
    }
});
// 關閉線程池
threadPool.shutdown(); // 設置線程池的狀態爲SHUTDOWN,而後中斷全部沒有正在執行任務的線程
threadPool.shutdownNow(); // 設置線程池的狀態爲 STOP,而後嘗試中止全部的正在執行或暫停任務的線程,並返回等待執行任務的列表

4種線程池的建立方法:
image.png

  • FixedThreadPoolSingleThreadExecutor:主要問題是堆積的請求處理隊列均採用LinkedBlockingQueue,可能會耗費很是大的內存,甚至OOM。
  • CachedThreadPoolScheduledThreadPool:主要問題是線程數最大數是Integer.MAX_VALUE,可能會建立數量很是多的線程,甚至OOM。

2.3線程通訊

線程通訊的兩種簡單模型,共享內存和消息傳遞。

2.3.1使用volatile關鍵字

使用volatile關鍵字定義全局變量,當變量發生變化時,線程可以感應並執行相應的業務。採用了共享內存的思想。

2.3.2使用Object類提供的wait(),notify()和notifyAll()

在synchronized修飾的同步方法或代碼塊中使用Object類提供的wait(),notify()和notifyAll()3個方法進行線程通訊。

1.wait():致使當前線程等待,直到其餘線程調用該同步監視器的notify()方法或notifyAll()方法來喚醒該線程。wait()釋放鎖。
2.notify():喚醒在此同步監視器上等待的單個線程。notify()不釋放鎖。
3.notifyAll():喚醒在此同步監視器上等待的全部線程。

3線程鎖

3.1悲觀鎖和樂觀鎖


悲觀鎖:仍是像它的名字同樣,對於併發間操做產生的線程安全問題持悲觀狀態,悲觀鎖認爲競爭老是會發生,所以每次對某資源進行操做時,都會持有一個獨佔的鎖,就像synchronized,無論三七二十一,直接上了鎖就操做資源了。

樂觀鎖:就像它的名字同樣,對於併發間操做產生的線程安全問題持樂觀狀態,樂觀鎖認爲競爭不老是會發生,所以它不須要持有鎖,將比較-替換這兩個動做做爲一個原子操做嘗試去修改內存中的變量,若是失敗則表示發生衝突,那麼就應該有相應的重試邏輯。

3.2兩種常見的鎖


synchronized 互斥鎖(悲觀鎖,有罪假設)

採用synchronized修飾符實現的同步機制叫作互斥鎖機制,它所得到的鎖叫作互斥鎖。每一個對象都有一個monitor(鎖標記),當線程擁有這個鎖標記時才能訪問這個資源,沒有鎖標記便進入鎖池。任何一個對象系統都會爲其建立一個互斥鎖,這個鎖是爲了分配給線程的,防止打斷原子操做。每一個對象的鎖只能分配給一個線程,所以叫作互斥鎖。

ReentrantReadWriteLock 讀寫鎖(樂觀鎖,無罪假設)

ReentrantLock是排他鎖,排他鎖在同一時刻僅有一個線程能夠進行訪問,實際上獨佔鎖是一種相對比較保守的鎖策略,在這種狀況下任何「讀/讀」、「讀/寫」、「寫/寫」操做都不能同時發生,這在必定程度上下降了吞吐量。然而讀操做之間不存在數據競爭問題,若是」讀/讀」操做可以以共享鎖的方式進行,那會進一步提高性能。所以引入了ReentrantReadWriteLock,顧名思義,ReentrantReadWriteLock是Reentrant(可重入)Read(讀)Write(寫)Lock(鎖),咱們下面稱它爲讀寫鎖。

讀寫鎖內部又分爲讀鎖和寫鎖,讀鎖能夠在沒有寫鎖的時候被多個線程同時持有,寫鎖是獨佔的。讀鎖和寫鎖分離從而提高程序性能,讀寫鎖主要應用於讀多寫少的場景。

3.3synchronized同步鎖

把共享資源,用鎖鎖起來,誰有鑰匙,誰去用共享資源,沒鑰匙的就排隊等待。

用法:
經過synchronized關鍵字實現同步鎖

用在方法上:synchronized public StringBuffer append(String str)
用在代碼塊上:synchronized( 鎖對象 ){ 須要同步的代碼;}

用在方法上,使用的鎖對象是默認的this.;
用在代碼塊上,鎖的對象是任意的。

4單例設計模式

4.1餓漢式

public class Test6_Singleton {
    public static void main(String[] args) {
       MySingle m = MySingle.getSingle();
       MySingle m2 = MySingle.getSingle();
       //==若是比較的是兩個引用類型的變量?比較的是地址值
       System.out.println(m==m2);
    }
}
//建立本身的單例程序
class MySingle {
    //1,私有化構造方法,不讓外界隨意new
    private MySingle() {}
    //2,在類的內部,提供一個已經建立好的對象
    //static是由於  靜態資源getSingle()只能調用靜態
    static private MySingle single = new MySingle();
    //3,對外提供一個全局訪問點
    //static 是由於,想要訪問get()能夠建立對象訪問,可是目前已經不容許建立對象了。只能經過類名調用,就得修飾。
    static public MySingle getSingle(){
       //把內部建立好的對象返回調用位置
       return single;
    }
}

4.2懶漢式

//面試重點:
//一、延遲加載思想:是指不會第一時間就把對象建立好來佔用內存,而是何時用何時建立
//二、線程安全問題:是指共享資源有線程併發的數據隱患,加同步鎖,鎖方法,也能夠鎖代碼塊
public class Test7_Single2 {
    public static void main(String[] args) {
       MySingleton my = MySingleton.getMy();
       MySingleton my2 = MySingleton.getMy();
       System.out.println(my==my2);
    }
}
// 建立類
class MySingleton {
    // 1,私有化構造方法 -- 控制外界new的權利
    private MySingleton() {
    }
    // 2,在類的內部建立好對象 -- 延遲加載!!
    static private MySingleton my;
//  static Object obj = new Object();
    // 3,提供全局訪問點
    // 問題:程序中有共享資源my,而且有多條語句(3條)操做了共享資源,此時,my共享資源必定會存在多線程編程的數據安全隱患
    // 解決方案就是加 同步鎖 。
    //若是用同步代碼塊須要肯定鎖的位置? 鎖的對象?因爲方法中的全部代碼都被同步了,能夠直接變成同步方法。
    synchronized static public MySingleton getMy() {
//     synchronized (obj) {//同步代碼塊:靜態區域內,不能是用this關鍵字?由於加載的前後順序問題
           if (my == null) {// 說明沒new過,保存的是默認值null
              my = new MySingleton();// 須要時纔會建立對象,不須要也不會提早就開始佔用內存。
           }
           return my;
//     }
    }
}
相關文章
相關標籤/搜索