主頁 > 後端開發 > Java基礎回顧-多執行緒

Java基礎回顧-多執行緒

2021-04-15 07:25:28 後端開發

目錄
  • 多執行緒
    • 并發
    • 并行
    • 執行緒與行程
      • 行程:
      • 執行緒:
      • 多執行緒好處:
    • 執行緒調度
      • 分時調度
      • 搶占式調度
    • 主執行緒
    • 創建執行緒類(創建多執行緒)
    • Thread類中的常用方法
      • 獲取執行緒名稱
      • 設定執行緒的名稱
      • sleep類
    • 創建多執行緒的第二種方式-Runable介面(推薦使用)
    • 匿名內部類的方式實作執行緒的創建
    • 執行緒安全問題
    • 解決執行緒安全問題
      • 執行緒同步
        • 執行緒同步方式1:同步代碼塊
          • 該同步技術的原理
        • 執行緒同步方式2:同步方法
          • 該同步技術的原理
          • 還有一種靜態同步方法
        • 執行緒同步方式3:Lock鎖機制

多執行緒

并發

指兩個或多個時間再同一個時間段內發生(在多個任務中交替執行,一個一個的執行)

比喻:一個人吃兩個饅頭,一個一個吃

并行

指兩個或多個時間再同一個時刻發生(同時發生)(多個任務同時執行)

比喻:兩個人吃兩個饅頭,同時吃~


所以說,并行的速度更快(但是由于計算機處理事務特別快,所以當事務不是很多的時候,二者速度在我們人來感受起來,相差并不大)


執行緒與行程

行程:

指一個記憶體中正在運行的應用程式,即電腦當中每一個app都算一個行程~

每打開一個app(應用程式),就會進入記憶體產生一個行程~

執行緒:

先了解CPU(中央處理器):指揮電腦中的軟體和硬體干活兒~

CPU的分類:

  1. AMD-----
  2. Inter-----Core(核心) i7 8866 四核八執行緒(8執行緒表示可以同時執行8個任務)
  3. 以電腦管家為例:每次使用電腦管家里的功能進行首頁體檢、病毒查殺、垃圾清理、電腦加速的時候,每一次都會開辟一條執行路徑,這個路徑就叫執行緒~

執行緒屬于行程

是行程中的一個執行單元,負責程式的執行

假如CPU是單核心單執行緒CPU,它只能同時執行一個執行緒,如果要執行電腦管家,它就會在多個任務之間,高速的切換,輪流執行多個執行緒~

由于速度很快,就看起來像同時執行~(效率較低)

假如CPU是4核心8執行緒CPU,它就能同時執行8個執行緒,如果要執行電腦管家,它就會是8個執行緒在多個任務之間,高速的切換,那么它的速度就會是單核心單執行緒CPU的8倍(每個任務被執行到的幾率都被提高8倍)!

由于速度很快,就看起來像同時執行~(效率較高)


多執行緒好處:

  1. 效率較高
  2. 多個執行緒之間互不影響

執行緒調度


分時調度

所有執行緒輪流使用CPU,每個執行緒使用CPU的時間平均分~(AA制


搶占式調度

把執行緒分優先級,優先級高的執行緒優先使用CPU(如果優先級一樣,CPU就隨機選一個執行緒)(VIP制

CPU都是在多個行程的多個執行緒之間進行高速切換,誰的優先級高,呢么被執行的機會就大一些~


主執行緒

指執行主方法(main方法)的執行緒~


單執行緒程式:Java程式中只有一個執行緒

程式從main方法開始,從上到下,按順序執行

Person.java

public class Person {
    private String name;

    public void run(){
        //輸出10次name
        for (int i = 1; i <= 10; i++) {
            System.out.println("第" + i +"次name:"+name+i);
        }
    }

    public Person() {
    }

    public Person(String name) {
        this.name = name;
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    @Override
    public String toString() {
        return "Person{" +
                "name='" + name + '\'' +
                '}';
    }
}

PersonTest.java

public class PersonTest {
    public static void main(String[] args) {
        Person person1 = new Person("牛牛");
        Person person2 = new Person("niniu");
        person1.run();
        person2.run();
    }
}

運行結果:

這就是一個單執行緒程式~??

假如在牛牛的run()方法niuniu的run()方法之間加一個例外(這里我用運算例外:除法分母不能為零的例外),會發現,niuniu就不列印了,如下圖所示:

public class PersonTest {
    public static void main(String[] args) {
        Person person1 = new Person("牛牛");
        Person person2 = new Person("niniu");
        
        person1.run();
        
        //加入一個運算例外
        System.out.println(1/0);
        
        person2.run();
    }
}

運行結果:


由于單執行緒有弊端,所以我們學習創建多執行緒!??

創建執行緒類(創建多執行緒)

Java使用java.lang.Thread類代表執行緒

Java虛擬機允許應用程式并發的運行多個執行執行緒-------->并發:同時發生

創建新執行執行緒有兩種方法

  • 一種是將類宣告為Thread的子類------------------->該子類應該重寫Thread類的run方法,接著該子類就可以分配并啟動該子類的實體,

即創建一個自定義的子類去繼承Thread類,然后重寫里面的run();方法,然后在測驗類中new剛剛創建的Thread的子類物件,并使用它去呼叫start();方法,如下代碼和運行結果:

代碼:

ChildreThread.java

public class ChildreThread extends Thread{
    private String name;

    //構造器
    public ChildreThread(String name) {
        this.name = name;
    }

    //重寫run方法
    public void run(){
        //輸出100次name
        for (int i = 1; i <= 100; i++) {
            System.out.println("第" + i +"次name:"+name+i);
        }
    }
}

ChildreThreadTest.java

public class ChildreThreadTest {
    public static void main(String[] args) {
        ChildreThread childreThread2 = new ChildreThread("start創建的多執行緒");
        childreThread2.start();//start創建的多執行緒


        //輸出100次"主執行緒"
        for (int i = 1; i <= 100; i++) {
            System.out.println("第" + i +"次主執行緒");
        }
    }


}

運行結果:

發現兩個執行緒在互相爭搶運行~成功!??

產生多執行緒的原理:

本來main函式可以自上而下執行一條main執行緒,但是中途出現了一個“childreThread2.start();//start創建的多執行緒”,于是main方法這條路徑上就出現了一條新的執行緒路徑(這條路徑指向繼承Thread的子類中的run();方法),于是乎,CPU就會對這兩條執行緒路徑進行隨機選擇,也可以說是這兩條執行緒路徑在爭搶CPU的使用權,所以就出現了主執行緒和新創建的執行緒交替執行的畫面~??

多執行緒的記憶體圖解:

優點:

每一個執行緒都在一個新的堆疊里,所以他們互不干擾~??

  • 另一種是實作宣告實作Runnable介面的類------>略

Thread類中的常用方法

  • public String getName();--------->獲取當前執行緒名;
  • public void start();--------->導致該執行緒開始執行;Java虛擬機呼叫此執行緒的run方法;
  • public void run();--------->此執行緒要執行的任務在此處定義代碼;
  • public static void sleep(long millis);--------->使當前正在執行的執行緒以指定的毫秒數暫停(暫時停止執行);
  • public static Thread currentThread();--------->回傳對當前正在執行的執行緒物件的參考;

獲取執行緒名稱

public String getName();--------->獲取當前執行緒名:

MyThread.java

/*獲取執行緒的名稱:
*       1.使用Thread類中的getName()方法
*               String getName() 回傳該執行緒的名稱
*       2.可以先獲取到當前正在執行的執行緒,使用執行緒中的方法getName()獲取執行緒的名稱
*               static Thread currentThread() 回傳對當前正在執行的執行緒物件的參考,
* */

//第一步:定義一個Thread類的子類
public class MyThread extends Thread{
    //第二步:重寫Thread類中的run方法,設定執行緒任務


    @Override
    public void run() {
        //獲取執行緒的名稱
        String name = getName();
        System.out.println(name);
    }
}

MyThreadTest.java

public class MyThreadTest {
    public static void main(String[] args) {
        //創建子類物件
        MyThread myThread = new MyThread();
        //呼叫start方法開啟一個新執行緒
        myThread.start();//創建的新執行緒會去執行它自己重寫的run方法,即MyThread類中的run方法
    }
}

運行結果:

如果再創建一個新執行緒:

public class MyThreadTest {
    public static void main(String[] args) {
        //創建子類物件
        MyThread myThread = new MyThread();
        //呼叫start方法開啟一個新執行緒
        myThread.start();//創建的新執行緒會去執行它自己重寫的run方法,即MyThread類中的run方法
        //如果再創建一個新執行緒
        new MyThread().start();//新執行緒的名稱就會是Thread-1
        //如果又創建一個新執行緒
        new MyThread().start();//新執行緒的名稱就會是Thread-2
        //以此類推.....還會有Thread-3,Thread-4,Thread-5,Thread-6...Thread-n
        new MyThread().start();
        new MyThread().start();
        new MyThread().start();
        new MyThread().start();
        new MyThread().start();
        new MyThread().start();

    }
}

運行結果:

在重寫的run方法中,換另外一種方式獲取執行緒名,即使用Thread類中的currentThread()方法,直接就可以獲取到當前正在執行的執行緒,然后使用getName方法得到執行緒的名稱!

代碼如下:

MyThread.java

/*獲取執行緒的名稱:
*       1.使用Thread類中的getName()方法
*               String getName() 回傳該執行緒的名稱
*       2.可以先獲取到當前正在執行的執行緒,使用執行緒中的方法getName()獲取執行緒的名稱
*               static Thread currentThread() 回傳對當前正在執行的執行緒物件的參考,
* */

//第一步:定義一個Thread類的子類
public class MyThread extends Thread{
    //第二步:重寫Thread類中的run方法,設定執行緒任務


    @Override
    public void run() {
        //獲取執行緒的名稱
//        String name = getName();
//        System.out.println(name);
        Thread thread = Thread.currentThread();//currentThread()或取當前執行緒
        System.out.println(thread+"------這是執行緒");//列印當前執行緒
        String threadName = thread.getName();//getName()獲取執行緒名稱
        System.out.println(threadName+"---------------------這是執行緒名稱");//列印上一步獲取到的執行緒名稱
    }
}

MyThreadTest.java

public class MyThreadTest {
    public static void main(String[] args) {
        //創建子類物件
        MyThread myThread = new MyThread();
        //呼叫start方法開啟一個新執行緒
        myThread.start();//創建的新執行緒會去執行它自己重寫的run方法,即MyThread類中的run方法
        //如果再創建一個新執行緒
        new MyThread().start();//新執行緒的名稱就會是Thread-1
        //如果又創建一個新執行緒
        new MyThread().start();//新執行緒的名稱就會是Thread-2
        //以此類推.....還會有Thread-3,Thread-4,Thread-5,Thread-6...Thread-n
        new MyThread().start();
        new MyThread().start();
        new MyThread().start();
        new MyThread().start();
        new MyThread().start();
        new MyThread().start();

    }
}

運行結果:

代碼優化:

使用一行代碼(鏈式編程):

MyThread.java

/*獲取執行緒的名稱:
*       1.使用Thread類中的getName()方法
*               String getName() 回傳該執行緒的名稱
*       2.可以先獲取到當前正在執行的執行緒,使用執行緒中的方法getName()獲取執行緒的名稱
*               static Thread currentThread() 回傳對當前正在執行的執行緒物件的參考,
* */

//第一步:定義一個Thread類的子類
public class MyThread extends Thread{
    //第二步:重寫Thread類中的run方法,設定執行緒任務


    @Override
    public void run() {
        //獲取執行緒的名稱
//        String name = getName();
//        System.out.println(name);
/*        Thread thread = Thread.currentThread();//currentThread()或取當前執行緒
        System.out.println(thread+"------這是執行緒");//列印當前執行緒
        String threadName = thread.getName();//getName()獲取執行緒名稱
        System.out.println(threadName+"---------------------這是執行緒名稱");//列印上一步獲取到的執行緒名稱*/

        System.out.println(Thread.currentThread()+"------這是執行緒!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!");
        System.out.println(Thread.currentThread().getName()+"---------------------這是執行緒名稱");
    }
}

MyThreadTest.java

public class MyThreadTest {
    public static void main(String[] args) {
        //創建子類物件
        MyThread myThread = new MyThread();
        //呼叫start方法開啟一個新執行緒
        myThread.start();//創建的新執行緒會去執行它自己重寫的run方法,即MyThread類中的run方法
        //如果再創建一個新執行緒
        new MyThread().start();//新執行緒的名稱就會是Thread-1
        //如果又創建一個新執行緒
        new MyThread().start();//新執行緒的名稱就會是Thread-2
        //以此類推.....還會有Thread-3,Thread-4,Thread-5,Thread-6...Thread-n
        new MyThread().start();
        new MyThread().start();
        new MyThread().start();
        new MyThread().start();
        new MyThread().start();
        new MyThread().start();

    }
}

運行結果:

獲取主執行緒名稱:

public class MyThreadTest {
    public static void main(String[] args) {
        //創建子類物件
        MyThread myThread = new MyThread();
        //呼叫start方法開啟一個新執行緒
        myThread.start();//創建的新執行緒會去執行它自己重寫的run方法,即MyThread類中的run方法
        //如果再創建一個新執行緒
        new MyThread().start();//新執行緒的名稱就會是Thread-1
        //如果又創建一個新執行緒
        new MyThread().start();//新執行緒的名稱就會是Thread-2
        //以此類推.....還會有Thread-3,Thread-4,Thread-5,Thread-6...Thread-n
        new MyThread().start();
        new MyThread().start();
        new MyThread().start();

        //獲取主執行緒名稱
        System.out.println(Thread.currentThread().getName());

    }
}

運行結果:


設定執行緒的名稱

兩種方式:

  1. 使用Thread類中的setName(自定義執行緒名)方法

  1. .創建一個帶引數的構造方法,引數傳遞執行緒的名稱;然后呼叫父類的帶引數的構造方法,把執行緒名稱傳遞個父類,讓父類Thread給子執行緒起一個名字!

  • 使用Thread類中的setName(自定義執行緒名)方法

MyThread.java

/*設定執行緒的名稱:
*       1.使用Thread類中的setName()方法
*                void setName(String name) 改變執行緒名稱,使之與引數 name 相同,
*       2.創建一個帶引數的構造方法,引數傳遞執行緒的名稱;
*         然后呼叫父類的帶引數的構造方法,把執行緒名稱傳遞個父類,讓父類Thread給子執行緒起一個名字!
* */

public class MyThread extends Thread{

    //方式2
    public MyThread() {
    }

    public MyThread(String name) {
        super(name);//將name傳遞給父類
    }

    @Override
    public void run() {
        //獲取并列印執行緒名稱
        System.out.println(Thread.currentThread().getName());
    }
}

MyThreadTest.java

public class MyThreadTest {
    public static void main(String[] args) {
        //new一個子類物件,開啟新執行緒
        MyThread myThread = new MyThread();
        //先自定義一下執行緒名稱,然后再開啟新執行緒
        myThread.setName("niu_niu");
        myThread.start();

        //new一個子類物件并直接傳入自定義執行緒名稱引數,開啟新執行緒
        MyThread myThread1 = new MyThread("傳入自定義引數更改執行緒名稱");
        myThread1.start();

        //獲取主執行緒名稱
        System.out.println(Thread.currentThread().getName());

    }
}

運行結果:


sleep類

public static void sleep(long millis);--------->使當前正在執行的執行緒以指定的毫秒數暫停(暫時停止執行);

public class MyThreadTest {
    public static void main(String[] args) {
        //模擬秒表
        for (int i = 0; i <= 60; i++) {
            System.out.println(i);
            //使用Thread類的sleep方法讓程式睡眠一秒鐘
            try {
                Thread.sleep(1000);//靜態方法通過類名直接呼叫,括號里單位為毫秒,1000毫秒==1秒
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
}

運行結果:(每過一秒列印一個數字)


創建多執行緒的第二種方式-Runable介面(推薦使用)

撰寫java.lang.Runable介面的實作類,并重寫run()方法,該run()方法的方法體同樣是該執行緒的執行緒執行體,

但是!java.lang.Runable介面里沒有點start()方法,那么要怎么開啟執行緒呢?

不急,這么來就行-------->先創建一個java.lang.Runable介面的實作類的實作類物件,然后把實作類物件當作引數傳到Thread類里面,最后再去點start();這樣就開啟了一個新執行緒了~??

public class MyThread implements Runnable{

    @Override
    public void run() {
        String name = Thread.currentThread().getName();
        System.out.println("新執行緒名:"+name);
    }
}
public class MyThreadTest {

    public static void main(String[] args) {
        for (int i = 1; i <= 10; i++) {
            MyThread myThread = new MyThread();
            Thread thread = new Thread(myThread);
            thread.start();
    }


        //輸出100次"主執行緒"
        for (int i = 1; i <= 10; i++) {
            System.out.println("第" + i +"次主執行緒");
        }
    }
}

運行結果:


實作Runable介面創建多執行緒的好處:

  • 避免了單繼承的局限性----------------一個類只能繼承一個類(一個人只能有一個親爹!),類繼承了Thread類就不能繼承其他的類,用Runable介面創建多執行緒之后,子類還可以繼承其他的類,實作其他的而介面!
  • 增強了程式的擴展性,降低了程式的耦合性(解耦)----------實作Runable介面的方式,把設定執行緒任務和開啟新執行緒進行了分離(解耦)實作類中,重寫的run方法就是用來設定執行緒要執行的任務;而創建Threrad類物件,呼叫start()方法,就是用來開啟新執行緒;這樣我們可以傳給Thread類物件不同的引數,就可以控制執行緒執行不同的任務了~

匿名內部類的方式實作執行緒的創建

格式:

new 父類/介面(){

? 重寫父類/介面中的方法;

};

AnonymousInnerClass.java

public class AnonymousInnerClass {
    public static void main(String[] args) {
        //匿名內部類:執行緒的父類是Thread的方式
        new Thread(){
            @Override
            public void run() {
                System.out.println("新執行緒1:"+Thread.currentThread().getName());
            }
        }.start();
        //匿名內部類:執行緒的介面是Runable的方式
        Runnable runnable = new Runnable() {

            @Override
            public void run() {
                System.out.println("新執行緒2:" + Thread.currentThread().getName());
            }
        };
        new Thread(runnable).start();
    }
}

運行結果:


執行緒安全問題

/**
 * 實作多執行緒賣票案例
 */
public class MovieTicket implements Runnable{
    //首先定義一個共享票源
    private int ticket = 100;

    //設定執行緒任務:賣票
    @Override
    public void run() {
        //使用死回圈讓賣票操作一直重復
        while (true){
            //先判斷電影票是否存在
            if (ticket>0){

                //我在這里為了使程式在實驗的票數較少的情況下,出現重復賣票的幾率增加,特意添加一個睡眠陳述句
                try {
                    Thread.sleep(100);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }

                //有票,(票遞減)===>ticket--
                System.out.println(Thread.currentThread().getName()+"號視窗正在賣第"+ticket+"張票");
                ticket--;
            }
        }

    }
}
import junit.framework.TestCase;

public class MovieTicketTest extends TestCase {
    public static void main(String[] args) {
        //new一個Runable的實作類物件--->MovieTicket
        MovieTicket movieTicket = new MovieTicket();
        //用一個MovieTicket往三個執行緒的Thread里傳,模擬電影院3個賣票視窗一起賣100張電影票的效果
        Thread thread1 = new Thread(movieTicket);
        Thread thread2 = new Thread(movieTicket);
        Thread thread3 = new Thread(movieTicket);
        //開啟執行緒
        thread1.start();
        thread2.start();
        thread3.start();
    }
}

運行結果:

出現了重復的票和不存在的票!!!!!!!!!!

即出現了執行緒安全的問題!!!!!!!!!!


出現執行緒安全問題的原理


解決執行緒安全問題

執行緒同步

執行緒同步方式1:同步代碼塊

synchronized關鍵字可以用于方法中的某個區塊中,表示只對這個區塊中的資源進行互斥訪問,

格式:

synchroniuzed(同步鎖){
    需要同步操作的代碼(可能會出現執行緒安全問題的代碼)(訪問了共享資料的代碼)
}

什么是同步鎖?

物件的同步鎖只是一個概念,可以想象為在物件上標記了一個鎖,

? 1.鎖物件可以是任意型別,

? 2.必須保證多個執行緒物件要使用同一把鎖,

? 3.鎖物件的作用:把同步代碼塊”鎖住“,只讓一個執行緒在代碼塊中執行

注意:在任何時候,最多允許一個執行緒擁有同步鎖,誰拿到同步鎖,誰就進入代碼塊,此時其他的執行緒只能在外等候(BLOCKED)

/**
 * 實作多執行緒賣票案例
 */
public class MovieTicket implements Runnable{
    //首先定義一個共享票源
    private int ticket = 100;

    //創建一個鎖物件(在run方法外面創建鎖物件是因為,如果在run方法里面創建鎖物件,那么每次開啟一條執行緒就會創建一個鎖隨想,這樣就不能保證鎖物件唯一了)
    Object obj = new Object();

    //設定執行緒任務:賣票
    @Override
    public void run() {
        //使用死回圈讓賣票操作一直重復
        while (true){
            //創建同步代碼塊
            synchronized (obj){
                //先判斷電影票是否存在
                if (ticket>0){

                    //我在這里為了使程式在實驗的票數較少的情況下,出現重復賣票的幾率增加,特意添加一個睡眠陳述句
                    try {
                        Thread.sleep(100);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }

                    //有票,(票遞減)===>ticket--
                    System.out.println(Thread.currentThread().getName()+"號視窗正在賣第"+ticket+"張票");
                    ticket--;
                }
            }
        }
    }
}
import junit.framework.TestCase;

public class MovieTicketTest extends TestCase {
    public static void main(String[] args) {
        //new一個Runable的實作類物件--->MovieTicket
        MovieTicket movieTicket = new MovieTicket();
        //用一個MovieTicket往三個執行緒的Thread里傳,模擬電影院3個賣票視窗一起賣100張電影票的效果
        Thread thread1 = new Thread(movieTicket);
        Thread thread2 = new Thread(movieTicket);
        Thread thread3 = new Thread(movieTicket);
        //開啟執行緒
        thread1.start();
        thread2.start();
        thread3.start();
    }
}

運行結果:沒有出現重復票和不存在的票了,說明synchronized關鍵字有效解決了執行緒安全問題??????

該同步技術的原理

原理:使用了鎖物件,這個鎖物件又叫同步鎖或物件鎖,也叫物件監視器

個人理解:

當一個執行緒搶奪到CPU的執行權后,執行run方法時,會碰到synchronized代碼塊;

此時該執行緒就會檢查synchronized代碼塊是否有鎖物件,如果有,則會獲取到鎖物件,接著就執行同步代碼塊;

與此同時,另一個執行緒雖然也搶奪到了CPU的執行權,但是鎖物件此時已將被占用,所以其他搶奪到CPU使用權的執行緒就無法獲得鎖物件,這就導致沒有獲得鎖物件的執行緒無法進入同步代碼塊;

無法獲得鎖物件的執行緒就會進入阻塞狀態,只能等待那個已經獲取到鎖物件的執行緒執行完畢后把鎖物件釋放出來

于是乎,當多執行緒執行時,大家都去爭CPU并且還去爭著獲取唯一的鎖物件,當某一個執行緒獲取到鎖物件的時候,其它執行緒就會進入阻塞狀態,就只能干巴巴的等著鎖物件被釋放出來(獲取到鎖物件的執行緒執行完畢后就會把鎖物件釋放出來),然后大家再去搶奪被釋放出來的、唯一的鎖物件??????

這樣就達到了同步代碼塊每次只能由某一個執行緒執行一次,這樣模擬賣票程式就不會出現多個視窗賣同一張票或賣不存在的票的情況了~

執行緒同步方式2:同步方法

格式:

public synchroniuzed void method(){
    需要同步操作的代碼(可能會出現執行緒安全問題的代碼)(訪問了共享資料的代碼)
}

MovieTicket.java

/**
 * 實作多執行緒賣票案例
 */
public class MovieTicket implements Runnable{
    //首先定義一個共享票源
    private int ticket = 100;

    //創建一個鎖物件(在run方法外面創建鎖物件是因為,如果在run方法里面創建鎖物件,那么每次開啟一條執行緒就會創建一個鎖隨想,這樣就不能保證鎖物件唯一了)
    Object obj = new Object();

    //設定執行緒任務:賣票
    @Override
    public void run() {
        //使用死回圈讓賣票操作一直重復
        while (true){
            sellTickets();
        }
    }
    //定義一個同步方法sellTickets(賣票)
    public synchronized void sellTickets(){
        //先判斷電影票是否存在
        if (ticket>0){

            //我在這里為了使程式在實驗的票數較少的情況下,出現重復賣票的幾率增加,特意添加一個睡眠陳述句
            try {
                Thread.sleep(100);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }

            //有票,(票遞減)===>ticket--
            System.out.println(Thread.currentThread().getName()+"號視窗正在賣第"+ticket+"張票");
            ticket--;
        }
    }
}

MovieTicketTest.java

import junit.framework.TestCase;

public class MovieTicketTest extends TestCase {
    public static void main(String[] args) {
        //new一個Runable的實作類物件--->MovieTicket
        MovieTicket movieTicket = new MovieTicket();
        //用一個MovieTicket往三個執行緒的Thread里傳,模擬電影院3個賣票視窗一起賣100張電影票的效果
        Thread thread1 = new Thread(movieTicket);
        Thread thread2 = new Thread(movieTicket);
        Thread thread3 = new Thread(movieTicket);
        //開啟執行緒
        thread1.start();
        thread2.start();
        thread3.start();
    }
}

運行結果:

該同步技術的原理

原理:同步方法也會把方法內部的代碼鎖住,只讓一個執行緒執行,它的鎖物件就是執行緒的實作類物件

還有一種靜態同步方法
/**
 * 實作多執行緒賣票案例
 */
public class MovieTicket implements Runnable{
    //首先定義一個共享票源
    private static int ticket = 100;//靜態變數

    //創建一個鎖物件(在run方法外面創建鎖物件是因為,如果在run方法里面創建鎖物件,那么每次開啟一條執行緒就會創建一個鎖隨想,這樣就不能保證鎖物件唯一了)
    Object obj = new Object();

    //設定執行緒任務:賣票
    @Override
    public void run() {
        //使用死回圈讓賣票操作一直重復
        while (true){
            sellTicketsStatic();
        }
    }
    //定義一個同步方法sellTickets(賣票)
    public static synchronized void sellTicketsStatic(){//靜態方法
        //先判斷電影票是否存在
        if (ticket>0){

            //我在這里為了使程式在實驗的票數較少的情況下,出現重復賣票的幾率增加,特意添加一個睡眠陳述句
            try {
                Thread.sleep(100);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }

            //有票,(票遞減)===>ticket--
            System.out.println(Thread.currentThread().getName()+"號視窗正在賣第"+ticket+"張票");
            ticket--;
        }
    }
}
import junit.framework.TestCase;

public class MovieTicketTest extends TestCase {
    public static void main(String[] args) {
        //new一個Runable的實作類物件--->MovieTicket
        MovieTicket movieTicket = new MovieTicket();
        //用一個MovieTicket往三個執行緒的Thread里傳,模擬電影院3個賣票視窗一起賣100張電影票的效果
        Thread thread1 = new Thread(movieTicket);
        Thread thread2 = new Thread(movieTicket);
        Thread thread3 = new Thread(movieTicket);
        //開啟執行緒
        thread1.start();
        thread2.start();
        thread3.start();
    }
}

運行結果:

靜態同步方法的鎖物件是實作類物件.class;(比如MovieTicket.classs)

執行緒同步方式3:Lock鎖機制

Lock 實作提供了比使用 synchronized 方法和陳述句可獲得的更廣泛的鎖定操作,此實作允許更靈活的結構,可以具有差別很大的屬性,可以支持多個相關的 Condition 物件,

Lock介面中的常用方法:

方法摘要
void lock() 獲取鎖,
void lockInterruptibly() 如果當前執行緒未被中斷,則獲取鎖,
Condition newCondition() 回傳系結到此 Lock 實體的新 Condition 實體,
boolean tryLock() 僅在呼叫時鎖為空閑狀態才獲取該鎖,
boolean [tryLock](../../../../java/util/concurrent/locks/Lock.html#tryLock(long, java.util.concurrent.TimeUnit))(long time, TimeUnit unit) 如果鎖在給定的等待時間內空閑,并且當前執行緒未被中斷,則獲取鎖,
void unlock() 釋放鎖,

lock()獲取鎖unlock()釋放鎖使用步驟:3步

  1. 在成員位置創建一個ReentrantLock物件
  2. 在可能出現執行緒安全問題的代碼前面呼叫lock()方法獲取鎖
  3. 在可能出現執行緒安全問題的代碼后面呼叫unlock()方法釋放鎖

代碼如下:

import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;

/**
 * 實作多執行緒賣票案例
 */
public class MovieTicket implements Runnable{
    //首先定義一個共享票源
    private int ticket = 100;

    //在成員位置創建一個`ReentrantLock`物件
    Lock lock = new ReentrantLock();


    //設定執行緒任務:賣票
    @Override
    public void run() {
        //使用死回圈讓賣票操作一直重復
        while (true){
            //在可能出現執行緒安全問題的代碼前面呼叫`lock()`方法獲取鎖
            lock.lock();
            //先判斷電影票是否存在
            if (ticket>0){

                //我在這里為了使程式在實驗的票數較少的情況下,出現重復賣票的幾率增加,特意添加一個睡眠陳述句
                try {
                    Thread.sleep(100);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }

                //有票,(票遞減)===>ticket--
                System.out.println(Thread.currentThread().getName()+"號視窗正在賣第"+ticket+"張票");
                ticket--;
            }
            //在可能出現執行緒安全問題的代碼后面呼叫`unlock()`方法釋放鎖
            lock.unlock();
        }
    }
}

結果略,

上述代碼提高效率寫法:

import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;

/**
 * 實作多執行緒賣票案例
 */
public class MovieTicket implements Runnable{
    //首先定義一個共享票源
    private int ticket = 100;

    //在成員位置創建一個`ReentrantLock`物件
    Lock lock = new ReentrantLock();


    //設定執行緒任務:賣票
    @Override
    public void run() {
        //使用死回圈讓賣票操作一直重復
        while (true){
            //在可能出現執行緒安全問題的代碼前面呼叫`lock()`方法獲取鎖
            lock.lock();
            //先判斷電影票是否存在
            if (ticket>0){

                //我在這里為了使程式在實驗的票數較少的情況下,出現重復賣票的幾率增加,特意添加一個睡眠陳述句
                try {
                    Thread.sleep(100);
                    //有票,(票遞減)===>ticket--
                    System.out.println(Thread.currentThread().getName()+"號視窗正在賣第"+ticket+"張票");
                    ticket--;
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }finally {
                    //在可能出現執行緒安全問題的代碼后面呼叫`unlock()`方法釋放鎖
                    lock.unlock();//這樣寫,無論程式是否例外都會釋放鎖,提高程式效率
                }
            }
        }
    }
}

轉載請註明出處,本文鏈接:https://www.uj5u.com/houduan/276081.html

標籤:其他

上一篇:死磕Spring之AOP篇 - Spring AOP常見面試題

下一篇:python如何在函式中使用全域變數

標籤雲
其他(157675) Python(38076) JavaScript(25376) Java(17977) C(15215) 區塊鏈(8255) C#(7972) AI(7469) 爪哇(7425) MySQL(7132) html(6777) 基礎類(6313) sql(6102) 熊猫(6058) PHP(5869) 数组(5741) R(5409) Linux(5327) 反应(5209) 腳本語言(PerlPython)(5129) 非技術區(4971) Android(4554) 数据框(4311) css(4259) 节点.js(4032) C語言(3288) json(3245) 列表(3129) 扑(3119) C++語言(3117) 安卓(2998) 打字稿(2995) VBA(2789) Java相關(2746) 疑難問題(2699) 细绳(2522) 單片機工控(2479) iOS(2429) ASP.NET(2402) MongoDB(2323) 麻木的(2285) 正则表达式(2254) 字典(2211) 循环(2198) 迅速(2185) 擅长(2169) 镖(2155) 功能(1967) .NET技术(1958) Web開發(1951) python-3.x(1918) HtmlCss(1915) 弹簧靴(1913) C++(1909) xml(1889) PostgreSQL(1872) .NETCore(1853) 谷歌表格(1846) Unity3D(1843) for循环(1842)

熱門瀏覽
  • 【C++】Microsoft C++、C 和匯編程式檔案

    ......

    uj5u.com 2020-09-10 00:57:23 more
  • 例外宣告

    相比于斷言適用于排除邏輯上不可能存在的狀態,例外通常是用于邏輯上可能發生的錯誤。 例外宣告 Item 1:當函式不可能拋出例外或不能接受拋出例外時,使用noexcept 理由 如果不打算拋出例外的話,程式就會認為無法處理這種錯誤,并且應當盡早終止,如此可以有效地阻止例外的傳播與擴散。 示例 //不可 ......

    uj5u.com 2020-09-10 00:57:27 more
  • Codeforces 1400E Clear the Multiset(貪心 + 分治)

    鏈接:https://codeforces.com/problemset/problem/1400/E 來源:Codeforces 思路:給你一個陣列,現在你可以進行兩種操作,操作1:將一段沒有 0 的區間進行減一的操作,操作2:將 i 位置上的元素歸零。最終問:將這個陣列的全部元素歸零后操作的最少 ......

    uj5u.com 2020-09-10 00:57:30 more
  • UVA11610 【Reverse Prime】

    本人看到此題沒有翻譯,就附帶了一個自己的翻譯版本 思考 這一題,它的第一個要求是找出所有 $7$ 位反向質數及其質因數的個數。 我們應該需要質數篩篩選1~$10^{7}$的所有數,這里就不慢慢介紹了。但是,重讀題,我們突然發現反向質數都是 $7$ 位,而將它反過來后的數字卻是 $6$ 位數,這就說明 ......

    uj5u.com 2020-09-10 00:57:36 more
  • 統計區間素數數量

    1 #pragma GCC optimize(2) 2 #include <bits/stdc++.h> 3 using namespace std; 4 bool isprime[1000000010]; 5 vector<int> prime; 6 inline int getlist(int ......

    uj5u.com 2020-09-10 00:57:47 more
  • C/C++編程筆記:C++中的 const 變數詳解,教你正確認識const用法

    1、C中的const 1、區域const變數存放在堆疊區中,會分配記憶體(也就是說可以通過地址間接修改變數的值)。測驗代碼如下: 運行結果: 2、全域const變數存放在只讀資料段(不能通過地址修改,會發生寫入錯誤), 默認為外部聯編,可以給其他源檔案使用(需要用extern關鍵字修飾) 運行結果: ......

    uj5u.com 2020-09-10 00:58:04 more
  • 【C++犯錯記錄】VS2019 MFC添加資源不懂如何修改資源宏ID

    1. 首先在資源視圖中,添加資源 2. 點擊新添加的資源,復制自動生成的ID 3. 在解決方案資源管理器中找到Resource.h檔案,編輯,使用整個專案搜索和替換的方式快速替換 宏宣告 4. Ctrl+Shift+F 全域搜索,點擊查找全部,然后逐個替換 5. 為什么使用搜索替換而不使用屬性視窗直 ......

    uj5u.com 2020-09-10 00:59:11 more
  • 【C++犯錯記錄】VS2019 MFC不懂的批量添加資源

    1. 打開資源頭檔案Resource.h,在其中預先定義好宏 ID(不清楚其實ID值應該設定多少,可以先新建一個相同的資源項,再在這個資源的ID值的基礎上遞增即可) 2. 在資源視圖中選中專案資源,按F7編輯資源檔案,按 ID 型別 相對路徑的形式添加 資源。(別忘了先把檔案拷貝到專案中的res檔案 ......

    uj5u.com 2020-09-10 01:00:19 more
  • C/C++編程筆記:關于C++的參考型別,專供新手入門使用

    今天要講的是C++中我最喜歡的一個用法——參考,也叫別名。 參考就是給一個變數名取一個變數名,方便我們間接地使用這個變數。我們可以給一個變數創建N個參考,這N + 1個變數共享了同一塊記憶體區域。(參考型別的變數會占用記憶體空間,占用的記憶體空間的大小和指標型別的大小是相同的。雖然參考是一個物件的別名,但 ......

    uj5u.com 2020-09-10 01:00:22 more
  • 【C/C++編程筆記】從頭開始學習C ++:初學者完整指南

    眾所周知,C ++的學習曲線陡峭,但是花時間學習這種語言將為您的職業帶來奇跡,并使您與其他開發人員區分開。您會更輕松地學習新語言,形成真正的解決問題的技能,并在編程的基礎上打下堅實的基礎。 C ++將幫助您養成良好的編程習慣(即清晰一致的編碼風格,在撰寫代碼時注釋代碼,并限制類內部的可見性),并且由 ......

    uj5u.com 2020-09-10 01:00:41 more
最新发布
  • Rust中的智能指標:Box<T> Rc<T> Arc<T> Cell<T> RefCell<T> Weak

    Rust中的智能指標是什么 智能指標(smart pointers)是一類資料結構,是擁有資料所有權和額外功能的指標。是指標的進一步發展 指標(pointer)是一個包含記憶體地址的變數的通用概念。這個地址參考,或 ” 指向”(points at)一些其 他資料 。參考以 & 符號為標志并借用了他們所 ......

    uj5u.com 2023-04-20 07:24:10 more
  • Java的值傳遞和參考傳遞

    值傳遞不會改變本身,參考傳遞(如果傳遞的值需要實體化到堆里)如果發生修改了會改變本身。 1.基本資料型別都是值傳遞 package com.example.basic; public class Test { public static void main(String[] args) { int ......

    uj5u.com 2023-04-20 07:24:04 more
  • [2]SpinalHDL教程——Scala簡單入門

    第一個 Scala 程式 shell里面輸入 $ scala scala> 1 + 1 res0: Int = 2 scala> println("Hello World!") Hello World! 檔案形式 object HelloWorld { /* 這是我的第一個 Scala 程式 * 以 ......

    uj5u.com 2023-04-20 07:23:58 more
  • 理解函式指標和回呼函式

    理解 函式指標 指向函式的指標。比如: 理解函式指標的偽代碼 void (*p)(int type, char *data); // 定義一個函式指標p void func(int type, char *data); // 宣告一個函式func p = func; // 將指標p指向函式func ......

    uj5u.com 2023-04-20 07:23:52 more
  • Django筆記二十五之資料庫函式之日期函式

    本文首發于公眾號:Hunter后端 原文鏈接:Django筆記二十五之資料庫函式之日期函式 日期函式主要介紹兩個大類,Extract() 和 Trunc() Extract() 函式作用是提取日期,比如我們可以提取一個日期欄位的年份,月份,日等資料 Trunc() 的作用則是截取,比如 2022-0 ......

    uj5u.com 2023-04-20 07:23:45 more
  • 一天吃透JVM面試八股文

    什么是JVM? JVM,全稱Java Virtual Machine(Java虛擬機),是通過在實際的計算機上仿真模擬各種計算機功能來實作的。由一套位元組碼指令集、一組暫存器、一個堆疊、一個垃圾回收堆和一個存盤方法域等組成。JVM屏蔽了與作業系統平臺相關的資訊,使得Java程式只需要生成在Java虛擬機 ......

    uj5u.com 2023-04-20 07:23:31 more
  • 使用Java接入小程式訂閱訊息!

    更新完微信服務號的模板訊息之后,我又趕緊把微信小程式的訂閱訊息給實作了!之前我一直以為微信小程式也是要企業才能申請,沒想到小程式個人就能申請。 訊息推送平臺🔥推送下發【郵件】【短信】【微信服務號】【微信小程式】【企業微信】【釘釘】等訊息型別。 https://gitee.com/zhongfuch ......

    uj5u.com 2023-04-20 07:22:59 more
  • java -- 緩沖流、轉換流、序列化流

    緩沖流 緩沖流, 也叫高效流, 按照資料型別分類: 位元組緩沖流:BufferedInputStream,BufferedOutputStream 字符緩沖流:BufferedReader,BufferedWriter 緩沖流的基本原理,是在創建流物件時,會創建一個內置的默認大小的緩沖區陣列,通過緩沖 ......

    uj5u.com 2023-04-20 07:22:49 more
  • Java-SpringBoot-Range請求頭設定實作視頻分段傳輸

    老實說,人太懶了,現在基本都不喜歡寫筆記了,但是網上有關Range請求頭的文章都太水了 下面是抄的一段StackOverflow的代碼...自己大修改過的,寫的注釋挺全的,應該直接看得懂,就不解釋了 寫的不好...只是希望能給視頻網站開發的新手一點點幫助吧. 業務場景:視頻分段傳輸、視頻多段傳輸(理 ......

    uj5u.com 2023-04-20 07:22:42 more
  • Windows 10開發教程_編程入門自學教程_菜鳥教程-免費教程分享

    教程簡介 Windows 10開發入門教程 - 從簡單的步驟了解Windows 10開發,從基本到高級概念,包括簡介,UWP,第一個應用程式,商店,XAML控制元件,資料系結,XAML性能,自適應設計,自適應UI,自適應代碼,檔案管理,SQLite資料庫,應用程式到應用程式通信,應用程式本地化,應用程式 ......

    uj5u.com 2023-04-20 07:22:35 more