天天看點

多線程大雜燴

死鎖:

(1)定義:在多線程中(兩個或以上線程),因為資源搶占而造成線程無限等待的問題。由于一個線程可以擁有多把鎖,但是一個鎖隻能被一個線程擁有。

(2)死鎖造成的四個因素(同時):

1.互斥條件(一個資源隻能被一個線程持有);

2.請求擁有條件(一個線程持有了一個資源之後,又請求另一個資源);

3.不可剝奪條件(一個資源被一個線程擁有之後,如果這個線程不釋放此資源,那麼其他線程不能強制獲得此資源);

4.環路等待條件(多個線程在擷取資源時形成了一個環形鍊);

(3)如何解決死鎖的問題?

從以下兩個條件入手,修改以下條件任意一個:

1.請求擁有條件

2.環路等待條件:

解決死鎖可以通過控制擷取鎖的順序來解決死鎖問題(破壞環路等待條件)。

線程等待:

sleep休眠缺陷:必須傳遞一個明确的結束時間。

線程的通訊機制:一個線程的動作可以讓另一個線程感覺到。

wait(線程休眠)/notify(線程喚醒)/nofifyAll(喚醒目前鎖對應的全部線程)

1.在使用以上方法時必須要加鎖;

wait為什麼要加鎖?

因為在進行wait操作時,會先釋放鎖;

為什麼要釋放鎖?

因為wait在不傳任何參數時,預設是永久等待,這樣會造成一個資源一直被占用。

2.加鎖對象和以上方法的加鎖對象必須保持一緻;

3.一組wait(休眠)/notify(喚醒)/nofifyAll(喚醒全部)必須加同一把鎖;

4.notifyAll隻能喚醒目前對象對應的線程;

sleep(0)和wait(0)的差別:

1.sleep是Thread的一個靜态方法;而wait是Object的方法;

2.sleep(0)立即觸發一次CPU資源的搶占,wait(0)代表永久的等待下去;

wait和sleep差別:

相同點:

都可以讓目前線程休眠;

都必須要處理一個Interrupt的異常;

不同點:

wait來自于Object中的一個方法,而sleep來自Thread類;

傳參不同,wait可以沒有參數,而sleep必須有一個大于0的參數;

wait必須要加鎖,sleep使用時不用加鎖;

wait使用時會釋放鎖,sleep沒有鎖;

wait預設不傳參的情況下,會進入WAITING狀态,sleep會進入TIMEDWAITING狀态;

為什麼wait會放到Object中而不是Thread?

wait必須要加鎖和釋放鎖,而鎖屬于對象級别,而非線程級别(一個線程線程擁有多把鎖),為了靈活起見(釋放哪把鎖),就把wait放在Object中。

LockSupport.park()方法——新版本休眠——WAITING——不用加鎖,不用處理異常

LockSupport.unpark(線程)方法——新版本喚醒休眠——可以控制線程喚醒順序,不能和notify等混用

LockSupport.parkUntil(休眠時間)與wait差別:

相同點:

1.都可以線程休眠;

2.都可以傳參不傳參,并且線程狀态一緻;

不同點:

1.wait必須要配合synchronized一起使用(加鎖),而LockSupport不用;

2.wait隻能喚醒全部和随機的一個線程,而LockSupport可以喚醒指定線程;

線程池:

1.線程的建立需要開辟記憶體資源:本地方法棧、Java虛拟機棧、程式計數器等線程私有變量的記憶體,頻繁的建立和銷毀會造成性能問題;

2.使用線程不能很好的管理任務和友好的拒絕任務;

定義:使用池化技術來管理和使用線程的技術就叫做線程池。

線程池的建立方式總共包含七種:

方式一:建立固定個數的線程池;Executors——當對目前代碼執行很了解,知道執行多少個任務

public static void main(String[] args) {
        //建立固定個數的線程池
      ExecutorService executorService=Executors.newFixedThreadPool(5);
      //執行一個任務
       /* executorService.execute(new Runnable() {
            @Override
            public void run() {
                System.out.println("線程名"+Thread.currentThread().getName());
            }
        });*/
       //執行多個任務
        for (int i = 0; i < 10; i++) {
            executorService.execute(new Runnable() {
                @Override
                public void run() {
                    System.out.println("線程名"+Thread.currentThread().getName());
                }
            });
        }

    }
           

建立了10個線程的線程池來執行兩個任務,實際建立了幾個線程?答案:2個

線程池執行流程:當拿到一個任務之後,會判斷目前線程池裡面的線程數量是否達到最大值,如果沒有就建立新的線程執行任務;當任務來了之後,線程池的線程數量已經是最大值,并且沒有空閑線程,那麼會将任務放到任務隊列中等待執行;

線程池裡面的兩個重要任務:

1.線程;

2.任務隊列;

方式二:建立帶緩存的線程池;——當有大量短期任務的時候适用,根據任務的數量來建立對應的線程數;

public static void main(String[] args) {
        //建立帶緩存的線程池
      ExecutorService service=Executors.newCachedThreadPool();
      for(int i=0;i<10;++i){
          service.execute(new Runnable() {
              @Override
              public void run() {
                  System.out.println(Thread.currentThread().getName());
              }
          });
      }
    }
           

方式三:建立可執行定時任務的線程池;

public static void main(String[] args) {
        //建立可執行定時任務的線程池
        ScheduledExecutorService service=Executors.newScheduledThreadPool(10);
        //執行任務
        System.out.println(new Date());
        service.scheduleWithFixedDelay(new Runnable() {
            @Override
            public void run() {
                System.out.println(new Date());
            }
        },1,3, TimeUnit.SECONDS);
        System.out.println(new Date());
    }
           

參數:線程執行的任務、定時任務延遲多長時間開始執行、定時任務的執行頻率、配合參數2和參數3的時間機關。

schedule和scheduleWithFixedDelay的差別:

1.前者沒有延遲執行的時間設定;

2.前者隻執行一次;

scheduleAtFixedRate:開始時間是以上次任務的開始時間去記錄的,固定的頻率,穩定;

scheduleWithFixedDelay:任務的開始時間是以上次任務的結束時間去記錄的,适合用于任務不穩定的,不穩定;

方式四:建立單個執行定時任務的線程池;

單個線程的線程池有什麼意義?

1.無需頻繁的建立和銷毀線程;

2.可以更好的配置設定和管理以及存儲任務(任務隊列);

方式五:單個線程的線程池;

public static void main(String[] args) {
        //建立單個線程的線程池
      ExecutorService service=Executors.newSingleThreadExecutor();
      service.execute(new Runnable() {
          @Override
          public void run() {
              System.out.println(Thread.currentThread().getName());
          }
      });
      for(int i=0;i<10;++i){
          service.execute(new Runnable() {
              @Override
              public void run() {
                  System.out.println(Thread.currentThread().getName());
              }
          });
      }
    }
           

方式六:(jdk8+)根據目前的工作環境(CPU核心數、任務量),異步線程池

public static void main(String[] args) {
        //根據目前工作環境來建立
      ExecutorService service=Executors.newWorkStealingPool();
        for (int i = 0; i < 10; i++) {
            service.execute(new Runnable() {
                @Override
                public void run() {
                    System.out.println(Thread.currentThread().getName());
                }
            });
        }
        while(!service.isTerminated()){}//任務執行狀态
    }
           

同步與異步:

同步:按照某種規則按序執行的就叫同步(解決線程不安全的主要手段);

流程:main調用線程池-》線程池執行完之後關閉線程池-》main也會随之關閉;

異步:main調用異步線程池-》異步線程池背景執行,對于main線程來說,異步線程池已經執行完成,main會直接關閉線程;

Executors建立線程池的問題:

1.線程數量不可控;(線程的過度切換和搶占,造成執行速度會變慢)

2.任務數量不可控,當任務量比較大的時候會造成記憶體溢出異常(OOM)

方式七:原始建立方式:ThreadPoolExecutors

public static void main(String[] args) {
        //原始的建立線程池的方法
        ThreadPoolExecutor executor=new ThreadPoolExecutor(
                5,//核心線程數
                10,//最大線程數,大于等于核心線程數
                60,//臨時線程的最大生命周期long類型
                TimeUnit.SECONDS,//時間機關
                new LinkedBlockingDeque<>(1000)//任務隊列
        );
        /*for (int i = 0; i < 20; i++) {
            executor.execute(new Runnable() {
                @Override
                public void run() {
                    System.out.println(Thread.currentThread().getName());
                }
            });
        }*/
        ThreadFactory threadFactory=new ThreadFactory() {
            @Override
            public Thread newThread(Runnable r) {
                Thread thread=new Thread(r);
                thread.setName("線程666");
                return thread;
            }
        };
        ThreadPoolExecutor executor1=new ThreadPoolExecutor(
                5,
                10,
                60,
                TimeUnit.SECONDS,
                new LinkedBlockingQueue<>(1000),
                threadFactory
        );
        executor1.execute(new Runnable() {
            @Override
            public void run() {
                System.out.println(Thread.currentThread().getName());
            }
        });
    }
           

當任務數量高于核心線程數,并且任務隊列未放滿,則會先将多出來的任務放在任務隊列中;

當任務數量高于核心線程數,并且任務隊列放滿了,那麼此時判斷最大線程數是否大于目前線程池中執行的線程數,大于的話,就開辟臨時線程,否則拒絕;

線程池建立線程是懶加載,在有任務的時候才建立對應任務數的線程去執行任務;

拒絕政策:JDK4種拒絕政策+自定義拒絕政策

1.預設拒絕政策:

2.調用調用者的線程來執行任務:

3.忽略政策;(忽略新任務)

4.忽略老任務;

5.自定義拒絕政策;

new RejectedExecutionHandler() {
                    @Override
                    public void rejectedExecution(Runnable r, ThreadPoolExecutor executor) {
                        System.out.println("執行了自定義的拒絕政策");
                    }
                }
           

線程池的兩種執行方式:

1.execute,執行Runnable,無傳回值;

2.submit 執行 (Runnable或者Callable);

execute如果出現記憶體溢出,則會列印異常,submit不會;

關閉線程池:

shutdown與shutdownNow

1.shutdown:拒絕新任務,等待線程池中的任務執行完之後,再停止線程池;

2.showdownNow:拒絕執行新任務,不會等待任務隊列中的任務執行完成,就關閉線程池;

線程池的狀态:(線程池狀态不等于線程狀态)

running、shutdown(調用shutdown)、stop(調用shutdownNow之後)、tidying(當pool是空的時候,銷毀的前置狀态)、terminated(銷毀狀态)

線程池的狀态隻是給開發者使用的,客戶機不可見;

ThreadLocal:線程級别的私有變量;

1.set:将私有變量放入線程中;

2.get:擷取線程中的變量;

3.remove:從線程中移除變量(不移除:髒讀、OOM);

4.initialValue:初始化ThreadLocal;

5.withInitial:初始化;

什麼情況下不會執行initialValue?為什麼?

(1)在進行了set操作之後不會執行initialValue;

(2)原因:ThreadLocal是懶加載的,當調用了get方法之後,才會嘗試執行initialValue方法,但在調用之前,會先嘗試擷取ThreadLocal的值,如果确實擷取不到,再執行初始化,執行完初始化後,這個初始化方法不會再執行;

ThreadLocal的使用場景:

1.解決線程安全問題;

2.實作線程級别的資料傳遞;

ThreadLocal的缺點:

1.不可繼承(父線程的東西,子線程通路不到);

如何解決不可繼承性?

2.髒讀:在一個線程中讀取到不屬于自己的資料;

線程使用ThreadLocal不會出現髒讀,線上程池裡使用ThreadLocal會出現髒讀,因為線程池會進行線程複用;

解決辦法:

1.避免使用靜态屬性(靜态屬性線上程池中會被複用);

2.使用remove,移除變量;

3.記憶體溢出問題:(最常出現的問題)

當一個線程執行完,不會釋放這個線程所占用的記憶體,或者記憶體釋放不及時的情況都叫做記憶體溢出;

ThreadLocal使用:

1.set:将私有變量設定到線程;

2.get:從線程中擷取私有變量;

3.remove:将線程中的私有變量移除;

4.initialValue:初始化;

5.withIitialValue:初始化;

使用場景:

1.解決線程安全問題;

2.線程級别的資料傳遞;

缺點:

1.不能實作父子線程之間的資料傳遞;

2.髒資料:ThreadLocal+線程池;

3.記憶體溢出(原因:線程池是長生命周期的,而線程是執行完任務線程就結束了(線程相關的資源都會釋放掉),但是線程池是長生命周期的,線程池中又有線程,線程裡面有ThreadLocalMap,Entry數組中存的是key和value,value是強引用,不會被釋放);

HashMap與ThreadLocalMap處理哈希沖突的差別?

前者使用連結清單法,後者使用開放尋址法;開放尋址法的特點和使用場景是資料量比較少的情況下性能更好;而HashMap裡面存儲的資料通常情況下資料量比較多,是以采用連結清單法更合适;

為什麼将ThreadLocal中的key設定為弱引用?

為了最大程度的避免OOM;

解決記憶體溢出問題:使用remove;