Java多線程詳解


多線程概述

  • 線程簡介
  • 線程實現(重點)
  • 線程狀態
  • 線程同步(重點)
  • 線程通信問題
  • 高級主題

線程、進程、多線程

  • 多任務

    image

    • 現實生活中太多這樣同時做多件事情的例子了,看起來是多個任務在做,其實本質上我們的大腦在同一時間依舊只做一件事情
  • 多線程

    image

    • 原來是一條路,慢慢因為車太多,道路堵塞,效率極低。為了提高使用效率,能夠充分利用道路,於是加了多個車道。從此媽媽再也不用擔心道路堵塞了
    • 說說大家的多線程例子(生活、游戲、編程)
  • 普通方法調用和多線程

    image

  • 程序.進程.線程

    image

Process和Thread

  • 說起來進程,就不得不說下程序。程序是指令和數據的有序集合,其本身沒有任何運行的含義,是一個靜態的概念

  • 而進程則是執行程序的一次執行過程,它是一個動態的概念。是系統資源分配的單位

  • 通常在一個進程中可以包含若干個線程,當然一個進程中至少有一個線程,不然沒有存在的意義。線程是CPU調度和執行的單位

    注意:很多多線程是模擬出來的,真正的多線程是指有多個CPU,即多核,如服務器。如果是模擬出來的多線程,即在一個CPU的情況下,在同一時間點,CPU只能執行一個代碼,因為切換的很快,所以就有同時執行的錯覺

本章核心概念

  • 線程就是獨立的執行路徑
  • 在程序運行時,即使沒有自己創建線程,后台也會有多個線程,如主線程,gc線程
  • main()稱之為主線程,為系統入口,用於執行整個程序
  • 在一個進程中,如果開辟了多個線程,線程的運行由調度器安排調度,調度器是與操作系統緊密相關的,先后順序是不能人為干預的
  • 對同一份資源操作時,會存在資源搶奪問題,需要加入並發控制
  • 線程會帶來額外的開銷,如CPU調度時間,並發控制開銷
  • 每個線程在自己的工作內存交互,內存控制不當會造成數據不一致

線程創建

  • 三種創建方式
    • 繼承Thread類(重點)
    • 實現Runnable接口(重點)
    • 實現Callable接口(現階段了解)

Thread

  • 自定義線程類繼承Thread類
  • 重寫run()方法,編寫線程執行體
  • 創建線程對象,調用start()方法啟動線程
  • 線程不一定立即執行,CPU安排調度

學習提示:查看JDK幫助文檔

package tread;
//創建線程方式一:繼承Thread類,重寫run()方法,調用start開啟線程
public class TestThread extends Thread {
    //線程入口點
    @Override
    public void run() {
        //run方法線程體
        for (int i = 0; i < 20; i++) {
            System.out.println("我在看代碼===========");
        }
    }

    //main線程,主線程
    public static void main(String[] args) {
        //創建線程對象
        TestThread thread1 = new TestThread();
        //調用start方法,開啟線程
        //thread1.start();//我在學習多線程循環和我在看代碼循環同時執行,交替執行
        thread1.run();//我在看代碼循環走完才執行我在學習多線程循環

        for (int i = 0; i < 2000; i++) {
            System.out.println("我在學習多線程=========="+i);
        }
    }
}
#注意:線程開啟不一定立即執行,由CPU調度

網圖下載

package tread;

import org.apache.commons.io.FileUtils;

import java.io.File;
import java.io.IOException;
import java.net.URL;

//練習thread,實現多線程同步下載圖片
public class TestThread2 extends Thread{
    private String url;//網絡圖片地址
    private String name;//保存的文件名

    public TestThread2(String url,String name){
        this.url=url;
        this.name=name;
    }

    //下載圖片線程的執行體
    @Override
    public void run() {
        WebDownloader webDownloader = new WebDownloader();
        webDownloader.downLoader(url,name);
        System.out.println("下載了文件名為" + name);
    }

    public static void main(String[] args) {
        TestThread2 t1 = new TestThread2(
                "https://gimg2.baidu.com/image_search/src=http%3A%2F%2F01.minipic.eastday.com%2F20171211%2F20171211185710_66c75b95e5421bbc73d255836976d21d_5.jpeg&refer=http%3A%2F%2F01.minipic.eastday.com&app=2002&size=f9999,10000&q=a80&n=0&g=0n&fmt=jpeg?sec=1619060788&t=749e179f08ffeb44b9b6a502b68ed70a",
                "寶馬跑車.jpg");
        TestThread2 t2 = new TestThread2(
                "https://gimg2.baidu.com/image_search/src=http%3A%2F%2Fattach.bbs.miui.com%2Fforum%2F201205%2F03%2F01400598djmyeczcskh2yr.jpg&refer=http%3A%2F%2Fattach.bbs.miui.com&app=2002&size=f9999,10000&q=a80&n=0&g=0n&fmt=jpeg?sec=1619061019&t=65367c12edf8aeb3224916794edfd831",
                "阿狸.jpg");
        TestThread2 t3= new TestThread2(
                "https://gimg2.baidu.com/image_search/src=http%3A%2F%2Faliyunzixunbucket.oss-cn-beijing.aliyuncs.com%2Fjpg%2Fcd46effe3def00805ef90dd5b20b1b22.jpg%3Fx-oss-process%3Dimage%2Fresize%2Cp_100%2Fauto-orient%2C1%2Fquality%2Cq_90%2Fformat%2Cjpg%2Fwatermark%2Cimage_eXVuY2VzaGk%3D%2Ct_100&refer=http%3A%2F%2Faliyunzixunbucket.oss-cn-beijing.aliyuncs.com&app=2002&size=f9999,10000&q=a80&n=0&g=0n&fmt=jpeg?sec=1619061081&t=a099d4bad9b5c8c4203427411c765365",
                "狼牙.jpg");
        //線程不是按順序執行的,是同時執行的
        t1.start();
        t2.start();
        t3.start();
    }
}

//下載器
class WebDownloader{
    //下載方法
    public void downLoader(String url,String name){
        try {
            FileUtils.copyURLToFile(new URL(url),new File(name));
        } catch (IOException e) {
            e.printStackTrace();
            System.out.println("IO異常,downloader方法出現問題");
        }
    }
}

實現Runnable

  • 定義MyRunnable類實現Runnable接口
  • 實現run()方法,編寫線程執行體
  • 創建線程對象,調用start()方法啟動線程
  • 推薦使用Runnable對象,因為Java單繼承的局限性
package tread;

//創建線程方式2:實現Runnable接口,重寫run方法,執行線程需要丟入Runnable實現類,調用start方法
public class TestRunnable1 implements Runnable{
    @Override
    public void run() {
        //run方法線程體
        for (int i = 0; i < 200; i++) {
            System.out.println("我在看代碼========="+i);
        }
    }

    public static void main(String[] args) {
        //創建runnbale接口的實現類對象
        TestRunnable1 testRunnable1 = new TestRunnable1();
        //創建線程對象,通過線程對象來開啟我們的線程,(代理類對象)
        /*Thread thread = new Thread(testRunnable1);
        thread.start();*/
        new Thread(testRunnable1).start();

        for (int i = 0; i < 2000; i++) {
            System.out.println("我在學習多線程===" + i);
        }
    }
}

小結:

  • 繼承Thread類

    • 子類繼承Thread類具備多線程能力
    • 啟動線程:子類對象.start()
    • 不建議使用:避免OOP單繼承局限性
  • 實現Runnable接口

    • 實現接口Runnable具有多線程能力

    • 啟動線程:傳入目標對象+Thread對象.start()

    • 推薦使用:避免單繼承局限性,靈活方便,方便同一個對象被多個線程使用

      //一份資源
      TestRunnable1 testRunnable1 = new TestRunnable1();
      //多個代理
      new Thread(testRunnable1,"小明").start();
      new Thread(testRunnable1,"小紅").start();
      new Thread(testRunnable1,"老師").start();
      

初始並發問題

package tread;

//多個線程同時操作同一個對象
//買火車票的例子

//發現問題:多個線程操作同一個資源時,線程不安全,數據紊亂
public class TestThread4 implements Runnable{
    //票數
    private int ticketNums =10;

    @Override
    public void run() {
        while (true){
            if (ticketNums<=0){
                break;
            }
            //模擬延時
            try {
                Thread.sleep(200);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }

            System.out.println(Thread.currentThread().
                    getName() +
                    "-->拿到了第" +
                    ticketNums-- +
                    "票");
        }
    }

    public static void main(String[] args) {
        TestThread4 ticket = new TestThread4();
        new Thread(ticket,"小明").start();
        new Thread(ticket,"小張").start();
        new Thread(ticket,"小紅").start();
    }
}

案例:龜兔賽跑

  • 要求:

    1. 首先來個賽道距離,然后要離終點越來越近
    2. 判斷比賽是否結束
    3. 打印出勝利者
    4. 龜兔賽跑開始
    5. 故事中是烏龜贏的,兔子需要睡覺,所以我們來模擬兔子睡覺
    6. 終於烏龜贏得比賽
    package tread;
    
    public class Race implements Runnable {
    
        //勝利者
        private static String winner;
        @Override
        public void run() {
            for (int i = 0; i <= 100; i++) {
                //模擬兔子休息
                if (Thread.currentThread().getName().equals("兔子")&&i%10==0){
                    try {
                        Thread.sleep(1);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
                //判斷比賽是否結束
                boolean flag=gameOver(i);
                //如果比賽結束,就停止程序
                if (flag){
                    break;
                }
                System.out.println(Thread.currentThread().getName() + "-->跑了 " + i + " 步");
            }
        }
    
        //判斷是否完成比賽
        private boolean gameOver(int steps){
            //判斷是否有勝利者
            if (winner!=null){//已經有勝利者
                return true;
            }else {
                if (steps>=100){
                    winner =Thread.currentThread().getName();
                    System.out.println("winner is " + winner);
                    return true;
                }
            }
            return false;
        }
    
        public static void main(String[] args) {
            Race race = new Race();
            new Thread(race,"兔子").start();
            new Thread(race,"烏龜").start();
        }
    }
    

實現Callable接口(了解即可)

  1. 實現Callable接口,需要返回值類型

  2. 重寫call方法,需要拋出異常

  3. 創建目標對象

  4. 創建執行任務:ExecutorService ser = Executors.newFixedThreadPool(1);

  5. 提交執行任務:Future result1 = ser.submit(t1);

  6. 獲取結果:Boolean r1 = result1.get();

  7. 關閉服務器:ser.shutdownNow();

    演示:利用Callable改造下載圖片案例

    package tread.demo2;
    
    import org.apache.commons.io.FileUtils;
    import tread.TestThread2;
    
    import java.io.File;
    import java.io.IOException;
    import java.net.URL;
    import java.util.concurrent.*;
    
    //創建方式3:實現Callable接口
    /*
      好處:
      1.可以定義返回值
      2.可以拋出異常
     */
    public class TestCallable implements Callable<Boolean> {
        private String url;//網絡圖片地址
        private String name;//保存的文件名
    
        public TestCallable(String url,String name){
            this.url=url;
            this.name=name;
        }
    
        //下載圖片線程的執行體
        @Override
        public Boolean call() {
            WebDownloader webDownloader = new WebDownloader();
            webDownloader.downLoader(url,name);
            System.out.println("下載了文件名為" + name);
            return true;
        }
    
        public static void main(String[] args) {
            TestCallable t1 = new TestCallable(
                    "https://gimg2.baidu.com/image_search/src=http%3A%2F%2F01.minipic.eastday.com%2F20171211%2F20171211185710_66c75b95e5421bbc73d255836976d21d_5.jpeg&refer=http%3A%2F%2F01.minipic.eastday.com&app=2002&size=f9999,10000&q=a80&n=0&g=0n&fmt=jpeg?sec=1619060788&t=749e179f08ffeb44b9b6a502b68ed70a",
                    "寶馬跑車.jpg");
            TestCallable t2 = new TestCallable(
                    "https://gimg2.baidu.com/image_search/src=http%3A%2F%2Fattach.bbs.miui.com%2Fforum%2F201205%2F03%2F01400598djmyeczcskh2yr.jpg&refer=http%3A%2F%2Fattach.bbs.miui.com&app=2002&size=f9999,10000&q=a80&n=0&g=0n&fmt=jpeg?sec=1619061019&t=65367c12edf8aeb3224916794edfd831",
                    "阿狸.jpg");
            TestCallable t3= new TestCallable(
                    "https://gimg2.baidu.com/image_search/src=http%3A%2F%2Faliyunzixunbucket.oss-cn-beijing.aliyuncs.com%2Fjpg%2Fcd46effe3def00805ef90dd5b20b1b22.jpg%3Fx-oss-process%3Dimage%2Fresize%2Cp_100%2Fauto-orient%2C1%2Fquality%2Cq_90%2Fformat%2Cjpg%2Fwatermark%2Cimage_eXVuY2VzaGk%3D%2Ct_100&refer=http%3A%2F%2Faliyunzixunbucket.oss-cn-beijing.aliyuncs.com&app=2002&size=f9999,10000&q=a80&n=0&g=0n&fmt=jpeg?sec=1619061081&t=a099d4bad9b5c8c4203427411c765365",
                    "狼牙.jpg");
            //線程不是按順序執行的,是同時執行的
            //創建執行任務:
            ExecutorService ser = Executors.newFixedThreadPool(3);//線程池,放入3個線程
            // 提交執行任務:
            Future<Boolean> r1 = ser.submit(t1);
            Future<Boolean> r2 = ser.submit(t2);
            Future<Boolean> r3 = ser.submit(t3);
            //獲取結果:
            try {
                Boolean rs1 = r1.get();
                Boolean rs2 = r2.get();
                Boolean rs3 = r3.get();
                System.out.println(rs1);
            } catch (InterruptedException e) {
                e.printStackTrace();
            } catch (ExecutionException e) {
                e.printStackTrace();
            }
            //關閉服務器:
            ser.shutdownNow();
        }
    }
    
    //下載器
    class WebDownloader{
        //下載方法
        public void downLoader(String url,String name){
            try {
                FileUtils.copyURLToFile(new URL(url),new File(name));
            } catch (IOException e) {
                e.printStackTrace();
                System.out.println("IO異常,downloader方法出現問題");
            }
        }
    }
    
    

靜態代理模式

  • 結婚案例

    • 你:真實角色
    • 婚慶公司:代理你,幫你處理結婚的事
    • 結婚:都實現結婚接口即可

    演示:實現靜態代理對比Thread

    package tread;
    /*
    靜態代理模式總結:
        真實對象和代理對象都要實現同一個接口
        代理對象要代理真實角色
    好處:
        代理對象可以做好多真實對象做不了的事情
        真實對象專注自己的事情
     */
    public class StaticProxy {
        public static void main(String[] args) {
            You you = new You();//真實結婚對象
            new Thread(()->System.out.println("我愛你")).start();
    
    
            //you.HappyMarry();//之前調用方式
            /*WeddingCompany weddingCompany = new WeddingCompany(you);//代理方式,交給婚慶公司調用
            weddingCompany.HappyMarry();*/
            new WeddingCompany(you).HappyMarry();
        }
    }
    //結婚類
    interface Marry{
        void HappyMarry();
    }
    
    //真實角色,你去結婚
    class You implements Marry{
        @Override
        public void HappyMarry() {
            System.out.println("小王要結婚了,超級開心");
        }
    }
    
    //婚慶:代理角色,幫助你結婚
    class WeddingCompany implements Marry{
        //代理誰———>真實目標角色
        private Marry target;
    
        public WeddingCompany(Marry target) {
            this.target = target;
        }
    
        @Override
        public void HappyMarry() {
            before();
            this.target.HappyMarry();//這就是真實對象
           after();
        }
    
        private void before() {
            System.out.println("結婚之前,布置現場");
        }
    
        private void after() {
            System.out.println("結婚之后,收尾款");
        }
    }
    

Lambda表達式

image

  • λ 希臘字母表中排序第十一位的字母,英文名稱為Lambda

  • 避免匿名內部類定義過多

  • 其實質屬於函數式編程的概念

    (params)->expression[表達式]
    (params)->statement[語句]
    (params)->{statements}
    
    a->System.out.println("i like lambda-->" + a);
    new Thread(()->System.out.println("多線程學習。。。")).start();
    
    
  • 為什么要使用lambda表達式

    • 避免匿名內部類定義過多
    • 可以讓你的代碼看起來很簡潔
    • 去掉一堆沒有意義的代碼,只留下核心的邏輯
  • 也許你會說,我看了Lambda表達式,不但不覺得簡潔,反而覺得更亂,看不懂了。那是因為我們還沒習慣,用的多了,看習慣了,就好了

  • 理解Functional Interface(函數式接口)是學習Java8 lambda表達式的關鍵所在

  • 函數式接口的定義:

    • 任何接口,如果只包含唯一一個抽象方法,那么它就是一個函數式接口

      public interface Runnable{
          public abstract void run();
      }
      
    • 對於函數式接口,我們可以通過lambda表達式來創建該接口的對象

案例一:

package tread.lambda;

/**
 * 推導lambda表達式
 * 實現類的一步一步簡化
 * jdk1.8新性能
 */
public class TestLambda1 {
    //3.靜態內部類
    static class Like2 implements ILike{
        @Override
        public void lambda() {
            System.out.println("i like lambda2");
        }
    }
    public static void main(String[] args) {
        ILike like = new Like();
        like.lambda();

        like = new Like2();
        like.lambda();

        //4.局部內部類
        class Like3 implements ILike{
            @Override
            public void lambda() {
                System.out.println("i like lambda3");
            }
        }
        like = new Like3();
        like.lambda();

        //5.匿名內部類:沒有類的名稱,必須借助接口或父類
        like=new ILike() {
            @Override
            public void lambda() {
                System.out.println("i like lambda4");
            }
        };
        like.lambda();

        //6.用lambda簡化
        like=()-> {
            System.out.println("i like lambda5");
        };
        like.lambda();
    }
}

//1.定義一個函數式接口
interface ILike{
    void lambda();
}

//2.實現類
class Like implements ILike{
    @Override
    public void lambda() {
        System.out.println("i like lambda");
    }
}

案例二:

package tread.lambda;

/**
 * 簡化lambda表達式
 * 總結:
 *  lambda表達式只能有一行代碼的情況下才能簡化成一行,如果有多行,那么就用代碼塊包裹;
 *  前提是接口為函數式接口
 *  多個參數也可以去掉參數類型,要去就全去,必須加上括號
 */
public class TestLambda2 {
    public static void main(String[] args) {
        Ilove love=null;
        
        love=(int a)->{
            System.out.println("i love you-->" + a);
        };
        love.love(520);
        //簡化1:參數類型    主要使用
        love=(a)->{
            System.out.println("i love you-->" + a);
        };
        love.love(521);
        //簡化2:簡化括號
        love=a->{
            System.out.println("i love you-->" + a);
        };
        love.love(522);
        //簡化3:去掉花括號
        love=a->System.out.println("i love you-->" + a);
        love.love(523);
    }
}
interface Ilove{
    void love(int a);
}

線程五大狀態

image

image

線程方法

方法 說明
setPriority(int newPriority) 更改線程的優先級
static void sleep(long millis) 在指定的毫秒數內讓當前正在執行的線程休眠
void join() 等待該線程終止
static void yield() 暫停當前正在執行的線程對象,並執行其他線程
void interrupt() 中斷線程,別用這個方式
boolean isAlive() 測試線程是否處於活動狀態

停止線程

  • 不推薦使用JDK提供的stop()、destroy()方法。【已廢棄】
  • 推薦線程自己停下來
  • 建議使用一個標志位進行終止變量當flag=false,則終止線程運行
package tread;

/**
 * 測試停止線程
 *     1.建議線程正常停止-->利用次數,不建議死循環
 *     2.建議使用標志位-->設置一個標志位
 *     3.不要使用stop或destroy等過時或者JDK不建議使用的方法
 */
public class TestStop implements Runnable{
    //1.線程中定義線程體使用的標識
    private boolean flag=true;
    @Override
    public void run() {
        int i=0;
        while (flag){
            System.out.println("run...Thread"+i++);
        }
    }
    //2.設置一個公開的方法停止線程,轉換標志位
    public void stop(){
        this.flag=false;
    }

    public static void main(String[] args) {
        TestStop testStop = new TestStop();
        new Thread(testStop).start();
        for (int i = 0; i < 1000; i++) {
            System.out.println("main" + i);
            if (i==900){
                //調用stop方法切換標志位,讓線程停止
                testStop.stop();
                System.out.println("線程停止了");
            }
        }
    }

    /* private boolean flag=true;
    //1.線程中定義線程體使用的標識
    @Override
    public void run() {
        //2.線程體使用該標識
        while (flag){
            System.out.println("run...Thread");
        }
    }
    //3.對外提供方法改變標識
    public void stop(){
        this.flag=false;
    }*/
}

線程的禮讓-yield

  • 禮讓線程,讓當前正在執行的線程暫停,但不阻塞

  • 將線程從運行狀態轉為就緒狀態

  • 讓CPU重新調度,禮讓不一定成功!看CPU心情

    package tread;
    
    /**
     * 測試禮讓線程
     *    1.禮讓不一定成功,看CPU心情
     *    2.如有ab兩個線程,CPU將資源分配給a,那么a處於運行狀態,
     *      b處於就緒狀態禮讓為讓a釋放資源回到就緒狀態,讓CPU重
     *      新給ab分配資源
     */
    public class TestYield {
        public static void main(String[] args) {
            MyYield myYield = new MyYield();
            new Thread(myYield,"a").start();
            new Thread(myYield,"b").start();
            /*禮讓成功:
                a線程開始執行
                b線程開始執行
                a線程結束了
                b線程結束了
              禮讓失敗:
                a線程開始執行
                a線程結束了
                b線程開始執行
                b線程結束了
             */
        }
    }
    
    class MyYield implements Runnable{
    
        @Override
        public void run() {
            System.out.println(Thread.currentThread().getName() + "線程開始執行");
            Thread.yield();//線程禮讓
            System.out.println(Thread.currentThread().getName() + "線程結束了");
        }
    }
    

線程強制執行-join

  • Join合並線程,待此線程執行完成后,再執行其他線程,其他線程阻塞

  • 可以想成插隊

    package tread;
    
    /**
     * 測試join接口
     *     想象成插隊
     */
    public class TestJoin implements Runnable{
        public static void main(String[] args) throws InterruptedException {
            //啟動我們的線程
            TestJoin testJoin = new TestJoin();
            Thread thread = new Thread(testJoin);
            thread.start();
            //主線程
            for (int i = 0; i < 100; i++) {
                if (i==50){
                    thread.join();//當main執行到第50次時,thread線程強制執行,main線程阻塞
                }
                System.out.println("main。。。" + i);
            }
        }
    
        @Override
        public void run() {
            for (int i = 0; i < 3000; i++) {
                System.out.println("join......" + i);
            }
        }
    }
    

觀測線程狀態

  • Thread.State

    線程狀態。線程可以處於一下(JDK幫助文檔)狀態之一:

    • NEW

      尚未啟動的線程處於此狀態(新生狀態)

    • RUNNABLE

      在Java虛擬機中執行的線程處於此狀態(運行狀態)

    • BLOCKED

      被阻塞等待監視器鎖定的線程處於此狀態(阻塞狀態)

    • WAITING

      正在等待另一個線程執行特定動作的線程處於此狀態(阻塞狀態)

    • TIMED_WAITING

      正在等待另一個線程執行動作達到指定等待時間的線程處於此狀態(阻塞狀態)

    • TERMINATED

      已退出的線程處於此狀態(死亡狀態)

一個線程可以在給定時間點處於一個狀態。這些狀態是不反映任何操作系統線程狀態的虛擬機狀態

package tread;
//觀察測試線程的狀態
public class TestState {
    public static void main(String[] args) throws InterruptedException {
        Thread thread = new Thread(()->{
            for (int i = 0; i < 5; i++) {
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
            System.out.println("////");
        });
        //觀察狀態
        Thread.State state = thread.getState();
        System.out.println(state);//NEW
        //觀察啟動后的狀態
        thread.start();//啟動線程
        state  = thread.getState();
        System.out.println(state);//Run

        while (state!=Thread.State.TERMINATED){//只要線程不停止,就一直輸出狀態
            Thread.sleep(100);
            state=thread.getState();//更行線程狀態
            System.out.println(state);//打印線程狀態
        }
        
        //thread.start();//這里線程無法啟動,因為線程不能啟動兩次。死亡以后的線程無法再次啟動
    }
}

線程優先級

  • Java提供一個線程調度器來監控程序中啟動后進入就緒狀態的所有線程,線程調度器按照優先級決定應該調度那個線程來執行
  • 線程的優先級用數字表示,范圍從1~10
    • Thread.MIN_PRIORITY=1;
    • Thread.MAX_PRIORITY=10;
    • Thread.NORM_PRIORITY=5;
  • 使用以下方式改變或獲取優先級
    • getPriority() setPriority(int xxx)
  • 優先級的設定建議在start()調度前
  • 優先級低只是意味着獲得調度的概率低。並不是優先級低就不會被調用了。這都是看CPU的調度
package tread;

/**
 * 測試線程優先級
 *    線程優先級不一定高的就先跑,但是大多數時候優先級高的先跑
 */
public class TestPriority {
    public static void main(String[] args) {
        System.out.println(Thread.currentThread().getName() + "--->" + Thread.currentThread().getPriority());
        MyPriority myPriority = new MyPriority();
        Thread t1 = new Thread(myPriority);
        Thread t2 = new Thread(myPriority);
        Thread t3 = new Thread(myPriority);
        Thread t4 = new Thread(myPriority);
        Thread t5 = new Thread(myPriority);
        Thread t6 = new Thread(myPriority);
        //先設置優先級,再啟動
        t1.start();

        t2.setPriority(1);
        t2.start();

        t3.setPriority(4);
        t3.start();

        t4.setPriority(Thread.MAX_PRIORITY);//MAX_PRIORITY==10
        t4.start();

        /*報錯
        t5.setPriority(-1);
        t5.start();

        t6.setPriority(11);
        t6.start();*/
    }
}
class MyPriority implements Runnable{

    @Override
    public void run() {
        System.out.println(Thread.currentThread().getName() + "--->" + Thread.currentThread().getPriority());
    }
}

守護(daemon)線程

  • 線程分為用戶線程和守護線程
  • 虛擬機必須確保用戶線程(main)執行完畢
  • 虛擬機不用等待守護線程(gc—垃圾回收機制)執行完畢
    • 如:后台記錄操作日志,監控內存,垃圾回收等待..
ackage tread;

/**
 * 測試守護線程
 * 上帝守護你
 */
public class TestDaemon {
    public static void main(String[] args) {
        God god = new God();
        You2 you2 = new You2();
        Thread thread = new Thread(god);
        thread.setDaemon(true);//默認是false表示是用戶線程,正常的線程都是用戶線程。。。
        thread.start();//上帝守護線程啟動

        new Thread(you2).start();//你 用戶線程啟動
    }
}
//上帝
class God implements Runnable{

    @Override
    public void run() {
        while (true){
            System.out.println("上帝保護你");
        }
    }
}
//你
class You2 implements Runnable{

    @Override
    public void run() {
        for (int i = 0; i < 36500; i++) {
            System.out.println("你一生都開心的活着"+i);
        }
        System.out.println("good bye! world!===============================================");
    }
}

線程同步

多個線程操作同一個資源

並發

  • 並發:同一個對象被多個線程同時操作(之前代碼TestThread4)

image

線程同步

  • 現實生活中,我們會遇到“同一個資源,多個人都想使用”的問題,比如,食堂排隊打飯,每個人都想吃飯,最天然的解決辦法就是排隊,一個個來

    image

  • 處理多線程問題時,多個線程訪問同一個對象,並且某些線程還想修改這個對象。這時候我們就需要線程同步。線程同步其實就是一種等待機制,多個需要同時訪問此對象的線程進入這個對象的等待池形成隊列,等待前面線程使用完畢,下一個線程再使用

  • 由於同意進程的多個線程共享同一塊存儲空間,在帶來方便的同時,也帶來了訪問沖突問題,為了保證數據在方法中被訪問時的正確性,在訪問時加入鎖機制synchronized,當一個線程獲得對象的排它鎖,獨占資源,其他線程必須等待,使用后釋放鎖即可,存在以下問題:

    • 一個線程持有鎖會導致其他所有需要此鎖的線程掛起
    • 在多線程競爭下,加鎖,釋放鎖會導致比較多的上下文切換和調度延時,引起性能問題
    • 如果一個優先級高的線程等待一個優先級低的線程釋放鎖會導致優先級倒置,引起性能問題
    • 安全性好,性能低;安全性差,性能高

隊列和鎖

形成安全的線程同步的條件:隊列+鎖

三大不安全案例

  • 案例一:不安全的買票

    package tread.syn;
    
    /**
     * 不安全的買票
     * 線程不安全,出現了負數
     */
    public class UnsafeBuyTicket {
        public static void main(String[] args) {
            BuyTicket station = new BuyTicket();
            new Thread(station,"小王").start();
            new Thread(station,"小李").start();
            new Thread(station,"小張").start();
        }
    }
    
    class BuyTicket implements Runnable{
        //票
        private int ticketNums=10;
        boolean flag=true;//外部停止方式
        @Override
        public void run() {
            //買票
            while (flag){
                try {
                    buy();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }
    
        private void buy() throws InterruptedException {
            //判斷是否有票
            if (ticketNums<=0){
                flag=false;
                return;
            }
            //模擬延時
            Thread.sleep(100);
            //買票
            System.out.println(Thread.currentThread().getName() + "買到了" + ticketNums--);
        }
    }
    
  • 案例二:不安全的取錢

    package tread.syn;
    
    /**
     * 不安全的取錢
     * 兩個人去銀行取錢,同一賬戶
     * 出現了余額負數
     */
    public class UnsafeBank {
        public static void main(String[] args) {
            Account account = new Account(100, "結婚基金");
            Drawing you = new Drawing(account, 50, "你");
            Drawing girlFriend = new Drawing(account, 100, "妻子");
            you.start();
            girlFriend.start();
        }
    }
    
    //賬戶
    class Account{
         int money;//余額
         String name;//卡名
    
        public Account(int money, String name) {
            this.money = money;
            this.name = name;
        }
    }
    
    //銀行:模擬取款
    class Drawing extends Thread{
        Account account;//賬戶
        int drawingMoney;//取了多少錢
        int nowMoney;//現在手里還有多少錢
    
        //構造方法
        public Drawing(Account account,int drawingMoney,String name){
            super(name);
            this.account=account;
            this.drawingMoney=drawingMoney;
        }
        //取錢
        @Override
        public void run() {
            //判斷有沒有錢
            if (account.money-drawingMoney<0){
                System.out.println(Thread.currentThread().getName() + "錢不夠了,取不了");
            }
            //sleep可以放大問題的發生性
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            //卡內余額=余額-你取的錢
            account.money=account.money-drawingMoney;
            //你手里的錢
            nowMoney=nowMoney+drawingMoney;
    
            System.out.println(account.name + "余額為:" + account.money);
            //Thread.currentThread().getName()=this.getName()
            System.out.println(this.getName()+"手里的錢"+nowMoney);
        }
    }
    
  • 案例三:不安全的集合

    package tread.syn;
    
    import java.util.ArrayList;
    
    /**
     * 線程不安全的集合
     */
    public class UnsafeList {
        public static void main(String[] args) {
            ArrayList<String> list = new ArrayList<>();
            for (int i = 0; i < 10000; i++) {
                new Thread(()->{
                    list.add(Thread.currentThread().getName());
                }).start();
            }
            try {
                Thread.sleep(3000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println(list.size());//理想情況為10000
        }
    }
    

同步方法及同步塊

同步方法

  • 由於我們可以通過private關鍵字來保證數據對象只能被方法訪問,所以我們只需要針對方法提出一套機制,這套機制就是synchronized關鍵字,它包括兩種用法:synchronized方法和synchronized塊

    public synchronized void method(int args){
        
    }
    
  • synchronized方法控制對"對象"的訪問,每個對象對應一把鎖,每個synchronized方法都必須獲得調用該方法的對象的鎖才能執行,否則線程會阻塞,方法一旦執行,就獨占該鎖,直到該方法返回才釋放鎖,后面被阻塞的線程才能獲得這個鎖,繼續執行

    # 缺陷:若將一個大的方法申明為synchronized將會影響效率
    
  • 方法里面需要修改的內容才需要鎖,鎖的太多,浪費資源

同步塊

  • 同步塊:synchronized(Obj){}
  • Obj稱之為同步監視器
    • Obj可以是任何對象,但是推薦使用共享資源作為同步監視器
    • 同步方法中無需指定同步監視器,因為同步方法的同步監視器就是this,就是這個對象本身,或者事class(反射中講解)
  • 同步監視器的執行過程
    1. 第一個線程訪問,鎖定同步監視器,執行其中代碼
    2. 第二個線程訪問,發現同步監視器被鎖定,無法訪問
    3. 第一個線程訪問完畢,解鎖同步監視器
    4. 第二個線程訪問,發現同步監視器沒有鎖,然后鎖定並訪問

CopyOnWriteArrayList

package tread;

import java.util.concurrent.CopyOnWriteArrayList;

/**
 * 測試JUC中安全類型的集合
 */
public class TestJUC {
    public static void main(String[] args) {
        CopyOnWriteArrayList<String> list = new CopyOnWriteArrayList<String>();
        for (int i = 0; i < 10000; i++) {
            new Thread(()->{
                list.add(Thread.currentThread().getName());
            }).start();
        }
        try {
            Thread.sleep(3000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println(list.size());
    }
}

死鎖

  • 多個線程各自占有一些共享資源,並且相互等待其他線程占有的資源才能運行,而導致兩個或者多個線程都在等待對方釋放資源,都停止執行的情形。某一個同步塊同時擁有"兩個以上對象的鎖"時,就可能會發生"死鎖"的問題

  • 產生死鎖的四個必要條件:

    • 互斥條件:一個資源每次只能被一個進程使用
    • 請求與保持條件:一個進程因請求資源而阻塞時,對已獲得的資源保持不放
    • 不剝奪條件:進程以獲得的資源,在未使用完之前,不能強行剝奪
    • 循環等待條件:若干進程之間形成一種頭尾相接的循環等待資源關系

    上面列出了死鎖的四個必要條件,我們只要想辦法破其中的任意一個或多個條件就可以避免死鎖發生

Lock鎖

  • 從JDK1.5開始,Java提供了更強大的線程同步機制——通過顯式定義同步鎖對象來實現同步。同步鎖使用Lock對象充當

  • java.util.concurrent.locks.Lock接口是控制多個線程對共享資源進行訪問的工具。鎖提供了對共享資源的獨占訪問,每次只能有一個線程對Lock對象加鎖,線程開始訪問共享資源之前應先獲得Lock對象

  • ReentrantLock(可重入鎖)類實現了Lock,它擁有與synchronized相同的並發性和內存語義,在實現線程安全的控制中,比較常用的是ReentrantLock,可以顯式加鎖、釋放鎖

  • 代碼

    class A{
        private final ReentrantLock lock=new ReentrantLock();
        public void m(){
            lock.lock();
            try {
                //保證線程安全的代碼;
            }finally {
                lock.unlock();
                //如果同步代碼有異常,要將unlock()寫入finally語句塊
            }
        }
    }
    

使用案例:

package tread.syn.gaoji;

import java.util.concurrent.locks.ReentrantLock;

/**
 * 測試Lock鎖
 */
public class TestLock {
    public static void main(String[] args) {
        TestLock2 testLock2 = new TestLock2();
        new Thread(testLock2,"小明").start();
        new Thread(testLock2,"小紅").start();
        new Thread(testLock2,"小張").start();
    }
}
class TestLock2 implements Runnable{
    int ticketNums=10;
    //定義lock鎖
    private final ReentrantLock lock=new ReentrantLock();
    @Override
    public void run() {
        while (true){
            try {
                lock.lock();//加鎖
                if (ticketNums>0){
                    try {
                        Thread.sleep(1000);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    System.out.println(Thread.currentThread().getName()+"買了第"+ticketNums--);
                }else {
                    break;
                }
            }finally {
                //解鎖
                lock.unlock();
            }

        }
    }
}

synchronized與Lock的對比

  • Lock是顯式鎖(手動開啟和關閉鎖,別忘記關閉鎖);synchronized是隱式鎖,出了作用域自動釋放
  • Lock只有代碼塊鎖,synchronized有代碼塊鎖和方法鎖
  • 使用Lock鎖,JVM將花費較少的時間來調度線程,性能更好。並且具有更好的擴展性(提供更多的子類)
  • 優先使用順序:
    • Lock > 同步代碼塊(已經進入了方法體,分配相應資源) > 同步方法(在方法體之外)

線程協作

生產消費者問題

  • 這是一個問題,不是23種模式之一

線程通信

  • 應用場景:生產者和消費者問題

    • 假設倉庫中只能存放意見產品,生產者將生產出來的產品放入倉庫,消費者將倉庫中產品取走消費

    • 如果倉庫中沒有產品,則生產者將產品放入倉庫,否則停止生產並等待,直到倉庫中的產品被消費者取走為止

    • 如果長褲中放有產品,則消費者可以將產品取走消費,否則停止消費並等待,直到倉庫中再次放入產品為止

      image

  • 分析

    這是一個線程同步問題,生產者和消費者共享同一個資源,並且生產者和消費者之間相互依賴,互為條件

    • 對於生產者,沒有生產產品之前,要通知消費者等待,而生產了產品之后,又需要馬上通知消費者消費

    • 對於消費者,在消費之后,要通知生產者已經結束消費,需要生產新產品以供消費

    • 在生產者消費問題中,僅有synchronized是不夠的

      • synchronized可阻止並發更新同一個共享資源,實現同步

      • synchronized不能用來實現不同線程之間的消息傳遞(通信)

  • Java提供了幾個方法解決線程之間的通信問題

    方法名 作用
    wait() 表示線程一致等待,直到其他線程通知,與sleep不同,會釋放鎖
    wait(long timeout) 指定等待的毫秒數
    notify() 喚醒一個處於等待狀態的線程
    notifyAll() 喚醒同一個對象上所有調用wait()方法的線程,優先級別高的線程優先調度

注意:均是Object類的方法,都只能在同步方法或者同步代碼塊中使用,否則會拋出異常IllegalMonitorStateException

  • 解決方式一

    並發協作模型"生產者/消費者模式"--->管程法

    • 生產者:負責生產數據的模塊(可能是方法,對象,線程,進程)
    • 消費者:負責處理數據的模塊(可能是方法,對象,線程,進程)
    • 緩沖區:消費者不能直接使用生產者的數據,他們之間有個"緩沖區"

    生產者將生產好的數據放入緩沖區,消費者從緩沖區拿出數據

    package tread.syn.gaoji;
    
    /**
     * 測試生產者消費者模式-->利用緩沖區解決:管程法
     * 生產者,消費者,產品,緩沖區
     */
    public class TestPC {
        public static void main(String[] args) {
            SynContainer container = new SynContainer();
            new Productor(container).start();
            new Consumer(container).start();
        }
    }
    //生產者
    class Productor extends Thread{
        SynContainer container;
    
        public Productor(SynContainer container) {
            this.container = container;
        }
        //生產
        @Override
        public void run() {
            for (int i = 0; i < 100; i++) {
                container.push(new Chicken(i));
                System.out.println("生產了" + i + "只雞");
            }
        }
    }
    //消費者
    class Consumer extends Thread{
        SynContainer container;
    
        public Consumer(SynContainer container) {
            this.container = container;
        }
        //消費
        @Override
        public void run() {
            for (int i = 0; i < 100; i++) {
                System.out.println("消費了——>第" + container.pop().id + "只雞");
            }
        }
    }
    //產品
    class Chicken{
        int id;//產品編號
    
        public Chicken(int id) {
            this.id=id;
        }
    }
    //緩存區
    class SynContainer{
        //需要一個容器大小
        Chicken[] chickens=new Chicken[10];
        //容器計數器
        int count=0;
        //生產者放入產品
        public synchronized void push(Chicken chicken){
            //如果容器滿了,就需要等待消費者消費
            if (count==chickens.length){
                //通知消費者消費,生產者等待
                try {
                    this.wait();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
            //如果容器沒滿,我們就需要丟入產品
            chickens[count]=chicken;
            count++;
            //可以通知消費者消費了
            this.notifyAll();
    
        }
        //消費者消費產品
        public synchronized Chicken pop(){
            //判斷能否消費
            if (count==0){
                //等待生產者生產,消費者等待
                try {
                    this.wait();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
            //如果可以消費
            count--;
            Chicken chicken=chickens[count];
            //吃完了,通知生產者生產
            this.notifyAll();
            return chicken;
    
        }
    }
    
  • 解決方式二

    • 並發協作模型"生產者/消費者模式"--->信號燈法
    package tread.syn.gaoji;
    
    /**
     *測試生產者消費者問題2:信號燈法,標志位解決
     */
    public class TestPC2 {
        public static void main(String[] args) {
            TV tv = new TV();
            new Player(tv).start();
            new Watcher(tv).start();
        }
    }
    //生產者--?演員
    class Player extends Thread{
        TV tv;
        public Player(TV tv){
            this.tv=tv;
        }
    
        @Override
        public void run() {
            for (int i = 0; i < 20; i++) {
                if (i%2==0){
                    this.tv.play("快樂大本營播放中");
                }else {
                    this.tv.play("抖音:記錄美好生活");
                }
            }
        }
    }
    //消費者-->觀眾
    class Watcher extends Thread{
        TV tv;
        public Watcher(TV tv){
            this.tv=tv;
        }
    
        @Override
        public void run() {
            for (int i = 0; i < 20; i++) {
               tv.watch();
            }
        }
    }
    //產品-->節目
    class TV{
        //演員表演,觀眾等待 T
        //觀眾觀看,演員等待 F
        String voice;//表演的節目
        boolean flag=true;//標志位
        //表演
        public synchronized void play(String voice){
            if (!flag){
                try {
                    this.wait();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
            System.out.println("演員表演了" + voice);
            //通知觀眾觀看
            this.notify();//通知喚醒
            this.voice=voice;
            this.flag=!this.flag;
        }
        //觀看
        public synchronized void watch(){
            if (flag){
                try {
                    this.wait();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
            System.out.println("觀看了:" + voice);
            //通知演員表演
            this.notifyAll();
            this.flag=!this.flag;
        }
    }
    

線程池

  • 背景:經常創建和銷毀、使用量特別大的資源,比如並發情況下的線程,對性能影響很大
  • 思路:提前創建好多個線程,放入線程池中,使用時直接獲取,使用完放回池中。可以避免頻繁創建銷毀、實現重復利用。類似生活中的公共交通工具
  • 好處:
    • 提高響應速度(減少了創建新線程的時間)
    • 降低資源消耗(重復利用線程池中線程,不需要每次都創建)
    • 便於線程管理(...)
      • corePoolSize:核心池的大小
      • maximumPoolSize:最大線程數
      • keepAliveTime:線程沒有任務時最多保持多長時間后會終止
  • JDK5.0起提供了線程池相關API:ExecutorService和Executors
  • ExecutorService:真正的線程池接口。常見子類ThreadPoolExecutor
    • void execute(Runnable command):執行任務/命令,沒有返回值,一般用來執行Runnable
    • Future submit(Callable task):執行任務,有返回值,一般用來執行Callable
    • void shutdown():關閉連接池
  • Executors:工具類、線程池的工廠類,用於創建並返回不同類型的線程池
package tread.syn.gaoji;

import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

/**
 * 測試線程池
 */
public class TestPool {
    public static void main(String[] args) {
        //1.創建服務,創建線程池
        //newFixedThreadPool 參數為:線程池大小
        ExecutorService service = Executors.newFixedThreadPool(10);
        //執行
        service.execute(new MyThread());
        service.execute(new MyThread());
        service.execute(new MyThread());
        service.execute(new MyThread());
        //2.關閉連接
        service.shutdown();
    }
}
class MyThread implements Runnable{
    @Override
    public void run() {
        System.out.println(Thread.currentThread().getName());
    }
}

總結

package tread.syn.gaoji;

import java.util.concurrent.Callable;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.FutureTask;

/**
 * 回顧總結線程的創建
 */
public class ThreadNew {
    public static void main(String[] args) {
        new MyThread1().start();
        new Thread(new MyThread2()).start();
        FutureTask<Integer> futureTask = new FutureTask<Integer>(new MyThread3());
        new Thread(futureTask).start();
        try {
            Integer integer = futureTask.get();
            System.out.println(integer);
        } catch (InterruptedException e) {
            e.printStackTrace();
        } catch (ExecutionException e) {
            e.printStackTrace();
        }
    }
}
//1.繼承Thread類
class MyThread1 extends Thread{
    @Override
    public void run() {
        System.out.println("MyThread1");
    }
}
//2.實現Runnable接口
class MyThread2 implements Runnable{
    @Override
    public void run() {
        System.out.println("MyThread2");
    }
}
//2.實現Runnable接口
class MyThread3 implements Callable<Integer> {
    @Override
    public Integer call() throws Exception {
        System.out.println("MyThread3");
        return 100;
    }
}

注:本文是我在再次溫習多線程的過程中記下的筆記。謝謝大家的閱讀。如有不足之處,請留言,QQ:825888114!


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM