java多線程基礎

我們經常會聽到或看到有人說:java天生就是多線程的。但是爲什麼這麼說呢?可以參考這篇文章的解釋Java天生就是多線程語言
在你值執行一個main方法的時候其實jvm同時開啓了許多的輔助線程來保證你的程序正常運行,比如清除引用對象的線程,調用對象finalize方法的線程等等。

線程的一些基礎概念:

1. CPU核心數和線程的關係
通常,線程數與CPU核心數的關係是1:1,但是當微軟引入了超線程技術之後這個關係變爲了2:1
2. 時間片輪轉調度算法
既然線程數與CPU核心數最大比例也是2:1,那麼爲什麼經常會看到代碼裏面會開上幾十上百的線程呢?這就要涉及到一個CPU的調度機制了。

時間片輪轉調度是一種最古老,最簡單,最公平且使用最廣的算法。每個任務被分配一個時間段,稱作它的時間片,即該任務允許運行的時間。如果在時間片結束時任務還在運行,則CPU將被剝奪並分配給另一個任務。如果任務在時間片結束前阻塞或結束,則CPU當即進行切換。調度程序所要做的就是維護一張就緒任務列表,當任務用完它的時間片後,它被移到隊列的末尾。

在各個線程進行切換的時候需要花費一定的時間,這個時間叫做上下文切換時間,這也就意味着線程數並不是越多越好,因爲線程數越多,那麼CPU上下文切換也就越頻繁,這樣耗費在上下文切換上的時間也就越多。

3. 進程和線程的區別
進程簡單點來說就是計算機上執行的一次活動。而線程是程序執行的最小單位,在同一個進程下,線程共享全局資源。詳細解釋可以參考進程和線程的區別

4. 並行和併發
在實際開發中很多人會認爲並行和併發是一樣的,但實際上並行和併發是兩個不同的概念。
並行:並行是指計算機同時處理多任務的能力。
併發:併發是指計算機在單位時間內處理多任務的能力。
他們之間最大的區別就在於一個時間上,一個是同時,一個是單位時間內

java實現多線程的方式

1. 繼承Thread類

public class TestThread extends Thread{
    @Override
    public void run() {
        System.out.println("線程【"+Thread.currentThread().getName()+ "】執行了" );
    }
}

2. 實現Runnable接口


public class TestThread implements Runnable{
    @Override
    public void run() {
        System.out.println("線程【"+Thread.currentThread().getName()+ "】執行了" );
    }
}

3. 實現Callable接口
Callable接口實現的run方法與之前兩個是不一樣的,Callable是帶有返回值的。

public class TestThread implements Callable<String> {
    @Override
    public String call() {
        System.out.println("線程【"+Thread.currentThread().getName()+ "】執行了" );
        return "Success";
    }
}

在啓動Callable線程的時候也是有點不一樣的
前兩種的啓動方式

  TestThread ts = new TestThread();
        Thread t = new Thread(ts);
        t.setName("測試線程");
        t.start();

Callable的啓動方式

 TestThread ts = new TestThread();
        FutureTask task = new FutureTask(ts);
        Thread t = new Thread(task);
        t.setName("測試線程");
        t.start();
        try {
            System.out.println("執行結果===>"+task.get());
        } catch (InterruptedException e) {
            e.printStackTrace();
        } catch (ExecutionException e) {
            e.printStackTrace();
        }

我們看Thread的源碼可以知道Thread構造方法只能傳入Runnable接口,因此我們需要把Callable接口封裝爲Runnable類型,所以我們用FutureTask 進行包裝,因爲FutureTask 是實現了Runnable接口的。
在這裏插入圖片描述

安全的停止線程

有開始就有結束,怎麼樣才能讓Java裏的線程安全停止工作呢?
stop() ,resume(),suspend()
在早期的java版本中,java爲我們提供了stop(),resume(),suspend()等方法來終止當前線程,當然,該方法已經過時,因爲這個方法是一個不安全的方法,使用stop這個方法將終止所有未結束的方法,包括run方法。當一個線程停止時候,他會立即釋放所有他鎖住對象上的鎖。這會導致對象處於不一致的狀態。假如一個方法在將錢從一個賬戶轉移到另一個賬戶的過程中,在取款之後存款之前就停止了。那麼現在銀行對象就被破壞了。因爲鎖已經被釋放了。當線程想終止另一個線程的時候,它無法知道何時調用stop是安全的,何時會導致對象被破壞。所以這個方法被棄用了。你應該中斷一個線程而不是停止他。
而suspend()容易導致死鎖。

interrupt() 、 isInterrupted()、static方法interrupted()

更好的中斷線程的方式應該是採用 interrupt() 方法來中斷線程。這裏需要注意的是interrupt()方法僅僅是將線程的中斷標誌位置爲true,並不是真正的停止了當前線程,因此我們需要在線程中使用isInterrupted()方法去判斷標誌位的狀態來手動中斷線程。static方法interrupted()是判斷當前線程有沒有被中斷並且將中斷標誌位改爲false。
特殊情況: 當線程的代碼中拋出了InterruptedException 的時候 中斷標誌位會被置爲false,如果確實是需要中斷線程,需要我們自己在catch語句塊裏再次調用interrupt()。

線程的生命週期

在這裏插入圖片描述

1. 創建線程對象
當我們創建一個實現了多線程的對象並使用Thread類去構造這個對象的時候就進行了第一步:創建線程對象
2. 準備就緒
調用我們創建好的對象的start()方法的時候,線程進入準備就緒狀態
3. 執行run方法
線程準備就緒之後會進入下一步去執行我們事先的run方法
4. 阻塞
在線程執行的過程中肯會因爲各種情況需要暫停一會兒,這時候我們可以調用某些方法使線程處於阻塞狀態
5. 結束
當run方法執行完成後線程也就結束了

線程之間的共享

關於線程之間共享的概念解釋這裏參考一片我覺得寫得非常好的博客,但是原作者找不到了。共享變量在線程間的可見性

1. 線程的加鎖方式
線程的加鎖方式在【共享變量在線程間的可見性】這篇博客裏面也有很詳細的解釋

2.ThreadLocal

ThreadLocal的實例代表了一個線程局部的變量,每條線程都只能看到自己的值,並不會意識到其它的線程中也存在該變量。
它採用採用空間來換取時間的方式,解決多線程中相同變量的訪問衝突問題。

ThreadLocal 的底層實現是基於map實現的,key就是當前線程對象

線程間的協作

1、等待-通知
在開發中可能會遇到這樣一種情況:我們會使用多個線程去處理大量的任務,每個任務的處理時間都是不固定的,而這些任務在主線程中已經定義好,這時候就會使用wait-notify機制來實現,等待子線程處理完成之後喚醒主線程進行下一批任務的執行。
通常等待-通知會有一個代碼的標準範式:
等待和通知的標準範式
對於等待方:
1、 獲取對象的鎖;
2、 循環裏判斷條件是否滿足,不滿足調用wait方法,
3、 條件滿足執行業務邏輯
對於通知方來:
1、 獲取對象的鎖;
2、 改變條件
3、 通知所有等待在對象的線程

示例代碼:
這裏面總的任務書是25個,最大允許的線程數是10個,每個線程中使用sleep() 來代表執行業務代碼需要耗費的時間。每當所有子線程執行完之後就會喚醒主線程分匹配下一批任務。

package com.example.test.thread;

import java.util.concurrent.atomic.AtomicInteger;

public class TestWaitNotifly {
    Object lock = new Object();
    private static int maxThreadNum = 10;
    private static AtomicInteger count = new AtomicInteger(0);
    public static void main(String[] args){
        TestWaitNotifly t = new TestWaitNotifly();
        t.doTask();

    }
    private  void doTask(){
        int totalTaskNum = 25;
        int executedNum = 0;
        int threadNum=maxThreadNum;
        while(true) {
           
            if(executedNum+maxThreadNum>totalTaskNum){
                threadNum = totalTaskNum-executedNum;
            }
            if(totalTaskNum<maxThreadNum){
                threadNum = totalTaskNum;
            }
            for (int i = 0; i < threadNum; i++) {
                MyThread myThread = new MyThread(totalTaskNum, lock);
                Thread thread = new Thread(myThread);
                thread.start();
            }
            executedNum = executedNum + threadNum;
            try {
                    synchronized (lock) {
                        System.out.println("已經執行線程====>" + executedNum);
                         if(executedNum>=totalTaskNum){
                			break;
            			}else{
							lock.wait();
						}
                    }
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
        System.out.println("線程執行完成");
    }
    class MyThread implements Runnable{
        private int totalTaskNum;
        private Object lock;
        public MyThread(int totalTaskNum,Object lock){
            this.totalTaskNum = totalTaskNum;
            this.lock = lock;
        }

        @Override
        public void run() {
            try {
                count.addAndGet(1);
                long time= (long) (Math.random()*1000);
                Thread.sleep(time);
              //  System.out.println("休眠線程====>"+Thread.currentThread().getId());
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            
            synchronized (lock){
                if(count.get()%maxThreadNum==0||this.totalTaskNum == count.get()){
                    System.out.println("任務執行完成,喚醒主線程"+count.get());
                    lock.notifyAll();
                }
            }
        }
    }
}

2、等待超時
等待超時這種情況常常發生在網絡請求,獲取數據庫連接資源等場景下,下面我們就來模擬一下這種情況
一版來說等待超時模式也會有一個標準範式:
假設 等待時間時長爲T,當前時間now+T以後超時

long overtime = now+T;
long remain = T;//等待的持續時間
while(result不滿足條件&& remain>0){
wait(remain);
remain = overtime – now;//等待剩下的持續時間
}
return result;

示例代碼:

package com.example.test.thread;

import java.util.LinkedList;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.atomic.AtomicInteger;

public class TestWaitOverTime {
    //初始化10個連接
    static MyPool pool  = new MyPool(10);
    // 控制器:控制main線程將會等待所有獲取連接線程結束後才能繼續執行
    static CountDownLatch end;
    public static void main(String[] args) throws InterruptedException {
        // 線程數量
        int threadCount = 20;
        end = new CountDownLatch(threadCount);
        int count = 20;//每個線程的操作次數
        AtomicInteger getNum = new AtomicInteger();//計數器:統計可以拿到連接的線程
        AtomicInteger failNum = new AtomicInteger();//計數器:統計沒有拿到連接的線程
        for (int i = 0; i < threadCount; i++) {
            Thread thread = new Thread(new TestThread(count, getNum, failNum));
            thread.start();
        }
        end.await();
        System.out.println("總共嘗試了: " + (threadCount * count)+" 次獲取");
        System.out.println("成功拿到連接的次數:  " + getNum+" 次");
        System.out.println("獲取連接失敗的次數: " + failNum+" 次");
    }
    static class TestThread implements  Runnable{
        int allCount;
        AtomicInteger getNum;
        AtomicInteger failNum;
        public TestThread( int allCount,AtomicInteger getNum, AtomicInteger failNum){
            this.allCount=allCount;
            this.getNum=getNum;
            this.failNum=failNum;
        }
        @Override
        public void run() {
            while(allCount>0) {
                try {
                    //最大允許超時1000ms
                    String conn = pool.getConn(1000);
                    if (conn != null) {
                        try {
                            //隨機休眠一段時間,模仿操作
                            Thread.currentThread().sleep((long) (((int) Math.random() * 100) + 80));

                        } catch (InterruptedException e) {
                            e.printStackTrace();
                        } finally {
                            getNum.incrementAndGet();
                            pool.releaseConn(conn);
                        }
                    } else {
                        failNum.incrementAndGet();
                        System.out.println(Thread.currentThread().getId() + "   等待超時!");

                    }
                } finally {
                    allCount--;
                }
            }
            end.countDown();
        }
    }

}
/**
 * 模擬數據庫連接池
 */
class MyPool{
    //數據庫池的容器
    private static LinkedList<String> pool = new LinkedList<>();

    /**
     * 初始化連接池
     * @param initialSize
     */
    public MyPool(int initialSize){
        if(initialSize>0){
            for(int i=0;i<initialSize;i++) {
                pool.addLast(new String("Connection"+i));
            }
        }
    }

    /**
     * 在時間maxTime之內如果獲取不到連接,則返回null
     * @param maxTime
     * @return
     */
    public  String getConn(long maxTime){
        synchronized(pool) {
            try {
                String rtn = null;
                //maxTime <0表示不限制時間,一直獲取
                if (maxTime < 0) {
                    while (pool.isEmpty()) {
                        pool.wait();
                    }
                } else {
                    //超時時間點:當前時間+允許等待的時間
                    long overtime = System.currentTimeMillis() + maxTime;
                    //剩餘可等待的時間
                    long remain = maxTime;
                    //當pool爲空以及可以等待時間>0的時候纔去等待
                    while (pool.isEmpty() && remain > 0) {
                        pool.wait();
                        //剩餘可以等待的 時間=超時時間點-當前時間
                        remain = overtime - System.currentTimeMillis();
                    }
                    //等待完畢
                    if (!pool.isEmpty()) {
                        rtn = pool.removeFirst();
                    }
                    return rtn;
                }
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
        return null;
    }

    /**
     * 使用完連接後放回
     * @param conn
     */
    public  void releaseConn(String conn){
        synchronized (pool) {
            if (conn != null) {
                pool.addLast(conn);
                pool.notifyAll();
            }
        }
    }
}


3、join()方法
線程A,執行了線程B的join方法,線程A必須要等待B執行完成了以後,線程A才能繼續自己的工作

4、yield() 、sleep()、wait()、notify()等方法對鎖的影響

  1. 線程在執行yield()以後,持有的鎖是不釋放的
  2. sleep()方法被調用以後,持有的鎖是不釋放的
  3. 調動方法之前,必須要持有鎖。調用了wait()方法以後,鎖就會被釋放,當wait方法返回的時候,線程會重新持有鎖
  4. 調動方法之前,必須要持有鎖,調用notify()方法本身不會釋放鎖的
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章