主頁 >  其他 > java多執行緒詳細理解

java多執行緒詳細理解

2021-02-19 14:49:18 其他

明確一點:多執行緒不是為了提高程式執行速度(性能甚至更低),而是提高應用程式的使用效率,

多執行緒的三大特性:原子性、可見性、有序性

一、創建執行緒

創建執行緒額的開銷:分配記憶體 --> 列入調度 --> 執行緒切換的時候還要執行記憶體換頁,CPU 的快取被清空,切換回來的時候還要重新從記憶體中讀取資訊(破壞了資料的區域性)

創建執行緒的三種方式

1.繼承Thread(重點),重寫run()方法,在main函式中,呼叫start()方法
2.實作Runnable介面(重點),重寫run()方法,在main函式中,呼叫start()方法
3.實作Callable介面(了解),重寫call()方法,在main函式中呼叫start()方法

1.繼承Thread類

package com.yang.demo01;
/*
繼承Thead類
重新run方法
在main中呼叫start開啟執行緒
 */
public class TestThread01 extends Thread{
    @Override
    public void run() {
        //run方法執行緒體,該執行緒要執行的操作
        for (int i = 0; i < 200; i++) {
            System.out.println("灰太狼");
        }
    }

    public static void main(String[] args) {
        //創建執行緒物件并呼叫start方法
        new TestThread01().start();
        for (int i = 0; i < 1000; i++) {
            System.out.println("美羊羊");
        }

    }
}

發現結果是亂序的(而且每次的運行結果都一樣),主執行緒和子執行緒是并行交替執行的,實際的運行是根據CPU的分配情況來決定的!

美羊羊
美羊羊
美羊羊
灰太狼
灰太狼
...
美羊羊

程式啟動運行main時候,java虛擬機啟動一個行程,主執行緒main在main()呼叫時候被創建,隨著呼叫start()方法,另外兩個執行緒也啟動了,這樣,整個應用就在多執行緒下運行,

要注意:start()方法的呼叫后并不是立即執行多執行緒代碼,而是使得該執行緒變為可運行態(Runnable),什么時候運行是由作業系統決定的!

而run()方法是多執行緒程式的一個約定,所有的多執行緒代碼都在run方法里面,

復雜一點的:

2.實作Runnbale介面

package com.yang.demo01;
/*
實作Runnbale介面
重寫run方法

 */
public class TeastRunnable implements Runnable{
    @Override
    public void run() {
        //run方法執行緒體
        for (int i = 0; i < 200; i++) {
            System.out.println("灰太狼");
        }
    }

    public static void main(String[] args) {
        //創建runnable介面的實作類物件
        TestThread01 testThread01 = new TestThread01();
        //創建執行緒物件,通過執行緒物件來開啟執行緒
        new Thread(testThread01).start();

        for (int i = 0; i < 1000; i++) {
            System.out.println("美羊羊");
        }
    }
}

結果也是交替的!

那這兩種方式哪種比較好?

繼承Thread類不適合資源共享,實作Runnable介面更具有靈活性,

繼承Thread:


public class FirstThread extends Thread {
    public void run() {
        for (int i = 1; i <= 10; i++) {
            System.out.println(getName() + " " + i);
        }
    }
    public static void main(String args[]) {
        for (int i = 1; i <= 5; i++) {
            System.out.println(Thread.currentThread().getName() + " " + i);
        }
        new FirstThread().start();
        new FirstThread().start();
    }
}

實作Runnable介面:

package com.yang.demo01;

public class Test01 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) {
        Test01 ticket = new Test01();
/*      Thread thread01 = new Thread(t1,"a");
        thread01.start();
        Thread thread02 = new Thread(t1,"b");
        thread02.start();*/
        new Thread(ticket,"喜洋洋").start();
        new Thread(ticket,"灰太狼").start();
        new Thread(ticket,"機器貓").start();

    }
}

對比    new FirstThread().start();
        new FirstThread().start();
和
		Test01 ticket = new Test01();
        new Thread(ticket,"喜洋洋").start();
        new Thread(ticket,"灰太狼").start();
        new Thread(ticket,"機器貓").start();

可以發現傳給Thread的是同一個Runnable物件,這也就是為什么說實作Runnable介面,可以資源共享,因為操作的都是同一個物件!而繼承Thread每次都要new一個新的物件,物件自然就不同了,并且,實作Runnable介面這種方式更體現了面向物件這種思維,new一個執行緒,執行緒里面傳一個物件,這個物件封裝了一系列操作,

所以總的來說,好處大致有四點

1.避免繼承的局限!因為一個類可以實作多個介面,
2.適合于資源的共享,
3.執行緒池只能放入實作Runable或callable類執行緒,不能直接放入繼承Thread的類,
4.增加程式的健壯性,代碼可以被多個執行緒共享,代碼和資料獨立

二、執行緒狀態

執行緒的五種狀態:
新建狀態、就緒狀態、運行狀態、阻塞狀態、死亡狀態 ,

在這里插入圖片描述
在這里插入圖片描述
詳細解釋:
1、新建狀態(New):新創建了一個執行緒物件,
2、就緒狀態(Runnable):執行緒物件創建后,其他執行緒呼叫了該物件的start()方法時進入就緒狀態,該狀態的執行緒位于可運行執行緒池中,變得可運行,等待獲取CPU的使用權,
3、運行狀態(Running):就緒狀態的執行緒獲取了CPU,執行程式代碼,
4、阻塞狀態(Blocked):阻塞狀態是執行緒因為某種原因放棄CPU使用權,暫時停止運行,直到執行緒進入就緒狀態,才有機會轉到運行狀態,阻塞的情況分三種:
(一)等待阻塞:運行的執行緒執行 wait()方法,JVM會把該執行緒放入等待池中,(wait會釋放持有的鎖)
(二)同步阻塞:運行的執行緒在獲取物件的 同步鎖 時,若該同步鎖被別的執行緒占用,則JVM會把該執行緒放入鎖池中,
(三)其他阻塞:運行的執行緒執行 sleep()或join()方法 ,或者發出了I/O請求時,JVM會把該執行緒置為阻塞狀態,當sleep()狀態超時、join()等待執行緒終止或者超時、或者I/O處理完畢時,執行緒重新轉入就緒狀態,(注意,sleep是不會釋放持有的鎖)
5、死亡狀態(Dead):執行緒執行完了或者因例外退出了run()方法,該執行緒結束生命周期,

三、執行緒狀態的常用方法

1. 如何讓執行緒停止?
可以使用一個標志位,進行終止變數,當flag=false時,則執行緒停止,

為什么不能使用JDK提供的stop()和suspend()方法?
答:stop() 會解除由執行緒獲取的所有鎖定,是不安全的,
suspend()方法容易發生死鎖,因為呼叫 suspend()的時候, 目標執行緒會停下來, 但卻仍然持有在這之前獲得的鎖定,

package com.yang;

public class Stop implements Runnable{
    //設定一個標志位
    private boolean flag = true;

    @Override
    public void run() {
        int i = 0;
        while (flag){
            System.out.println("run Thread "+ i++);
        }
    }

    //自己設定一個stop方法來轉換標志位,停止執行緒
    public void stop(){
        this.flag = false;
    }

    public static void main(String[] args) {
        Stop stop = new Stop();
        new Thread(stop).start();

        for (int i = 0; i<1000; i++){
            System.out.println("main執行緒 "+ i);;
            if (i == 900){
                //呼叫stop方法切換標志位,讓執行緒停止
                stop.stop();
                System.out.println("子執行緒停止");
            }
        }
    }
}

2. 執行緒休眠:sleep()

指定當前行程阻塞的毫秒數,sleep存在一個InterruptedException例外,當sleep時間到達后,執行緒進入就緒狀態,一般run()里面都要加一個sleep(),要注意:每個物件都有一個鎖,而sleep并不會釋放鎖!

sleep可以用來做倒計時,

package com.yang;
//模擬倒計時
public class Sleep  {

    public static void tenCount() throws InterruptedException {
        int nums = 10;
        while (true){
            Thread.sleep(1000);  //休眠一秒
            System.out.println(nums--);
            if (nums <= 0){
                break;
            }
        }
    }
    
}

還可以用來獲取定時系統時間:

package com.yang;

import java.text.SimpleDateFormat;
import java.util.Date;

//模擬倒計時
public class Sleep  {
    public static void main(String[] args) {
        Date time = new Date(System.currentTimeMillis());// 獲取系統時間
        while (true){
            try {
                Thread.sleep(2000);  //休眠兩秒
                System.out.println(new SimpleDateFormat("HH:mm:ss").format(time)); //日期格式化,輸出時間
                time = new Date(System.currentTimeMillis());// 更新時間
            } catch (InterruptedException e) {
                e.printStackTrace();
            }

        }
    }

結果:

2021-02-16 22:23:01
2021-02-16 22:23:03
2021-02-16 22:23:05
2021-02-16 22:23:07
2021-02-16 22:23:09
2021-02-16 22:23:11
...

3. 執行緒禮讓:yield()

讓當前正在執行的執行緒暫停,但不會讓執行緒轉到等待/睡眠/阻塞狀態!只是讓執行緒從運行狀態轉為就緒狀態,把執行機會讓給相同或者更高優先級的執行緒,讓CPU重新調度,禮讓不一定能成功,還是要看CPU,也就是說,實際中無法保證yield()達到讓步目的,因為讓步的執行緒還有可能被執行緒調度程式再次選中,

package com.yang;

public class Yield implements Runnable{

    @Override
    public void run() {
        System.out.println(Thread.currentThread().getName()+"執行緒開始執行");
        Thread.yield();  //禮讓
        System.out.println(Thread.currentThread().getName()+"執行緒停止執行");
    }

    public static void main(String[] args) {
        Yield yield = new Yield();
        new Thread(yield,"a").start();
        new Thread(yield,"b").start();
    }
}

如果不加禮讓時:

a執行緒開始執行
a執行緒停止執行
b執行緒開始執行
b執行緒停止執行

加了禮讓時:

//禮讓失敗
a執行緒開始執行
a執行緒停止執行
b執行緒開始執行
b執行緒停止執行
//禮讓成功
a執行緒開始執行
b執行緒停止執行
a執行緒開始執行
b執行緒停止執行

sleep()和yield()的區別:

  1. 狀態,sleep()使當前執行緒進入阻塞狀態,所以執行sleep()的執行緒在指定的時間內肯定不會被執行;yield()只是使當前執行緒重新回到可執行狀態,所以執行yield()的執行緒有可能在進入到可執行狀態后馬上又被執行,

  2. 時間,sleep 方法使當前運行中的執行緒睡眠一段時間,進入阻塞狀態,這段時間的長短是由程式設定的,yield 方法使當前執行緒讓出 CPU 占有權,但讓出的時間是不可設定的,實際上,yield()方法對應了如下操作:先檢測當前是否有相同優先級的執行緒處于同可運行狀態,如有,則把 CPU 的占有權交給此執行緒,否則,繼續運行原來的執行緒,所以yield()方法稱為“退讓”,它把運行機會讓給了同等優先級的其他執行緒,

  3. 優先級,sleep()方法在給其他執行緒運行機會時不考慮執行緒的優先級,低優先級仍有機會被調度,而yield()方法只會給相同優先級或更高優先級的執行緒運行的機會,如果較高優先級的執行緒沒有呼叫 sleep 方法,又沒有受到 I\O 阻塞,那么,較低優先級執行緒只能等待所有較高優先級的執行緒運行結束,才有機會運行,

4. 等待執行緒終止:join()

join()的作用是:“等待該執行緒終止”,待該執行緒執行完成后,才能執行其他執行緒,否則其余執行緒只能阻塞,這里需要理解的就是該執行緒是指的主執行緒等待子執行緒的終止,也就是在子執行緒呼叫了join()方法后面的代碼,只有等到子執行緒結束了才能執行,可以理解為插隊!和sleep一樣,也會拋出InterruptedException例外,

package com.yang;

public class Join implements Runnable {
    @Override
    public void run() {
        for (int i = 0; i < 5; i++) {
            System.out.println("vip執行緒"+i);
        }
    }

    public static void main(String[] args) throws InterruptedException {
        Join join = new Join();
        Thread thread = new Thread(join);
        thread.start();

        //主執行緒
        for (int i = 0; i <10 ; i++) {
            if (i == 4){
                thread.join(); //讓vip執行緒插隊
            }
            System.out.println("main"+i);
        }
    }
}

結果:

main0
main1
main2
main3
vip執行緒0
vip執行緒1
vip執行緒2
vip執行緒3
vip執行緒4
main4
main5
main6
main7
main8
main9
Process finished with exit code 0

為什么要用join方法?或者說什么時候用join方法?

在很多情況下,主執行緒生成并起動了子執行緒,如果子執行緒里要進行大量的耗時的運算,主執行緒往往將于子執行緒之前結束,但是如果主執行緒需要用到子執行緒的處理結果,也就是主執行緒需要等待子執行緒執行完成之后再結束,這個時候就要用到join()方法了,

5. 執行緒通信中的方法:wait()和notify()

Obj.wait(),與Obj.notify()必須要與synchronized(Obj)一起使用,也就是wait,與notify是針對已經獲取了Obj鎖進行操作,它們都屬于Object類,

?5.1 強迫一個執行緒等待:wait()
??當一個執行緒執行到wait()方法時,它就進入到一個和該物件相關的等待池中,同時失去(釋放)了物件的鎖(暫時失去鎖,wait(long timeout)超時時間到后會返還物件鎖);其他執行緒可以訪問;
使用wait之后,必須使用notify或者notifyAll來喚醒當前等待池中的執行緒,
也就是說,使用wait后,執行緒在獲取物件鎖后,主動釋放物件鎖,同時本執行緒休眠,直到有其它執行緒呼叫物件的notify()喚醒該執行緒,才能繼續獲取物件鎖,并繼續執行

?5.2 喚醒一個執行緒:notify()
??與wait()對應,notify()是釋放自身物件鎖,喚醒下一個等待執行緒,但有一點需要注意的是notify()呼叫后,并不是馬上就釋放物件鎖的,而是在相應的同步代碼塊執行結束,自動釋放鎖后,JVM會在wait()物件鎖的執行緒中隨機選取一執行緒,賦予其物件鎖,喚醒執行緒,繼續執行,這樣就提供了在執行緒間同步、喚醒的操作,

案例:建立三個執行緒,A執行緒列印10次A,B執行緒列印10次B,C執行緒列印10次C,要求執行緒同時運行,交替列印10次ABC,

package com.yang.state;

public class wait implements Runnable {

    private String name;
    private Object prev;
    private Object self;
    wait(String name,Object prev, Object self){
        this.name = name;
        this.prev = prev;
        this.self = self;
    }
    @Override
    public void run() {
        int count = 10;
        while (count > 0) {
            synchronized (prev) {
                synchronized (self) {
                    System.out.print(name);
                    count--;
                    self.notify(); //釋放自身物件(self)鎖,喚醒下一個等待執行緒
                }
                try {
                    prev.wait(); //釋放prev物件鎖,終止當前執行緒,等待回圈結束后再次被喚醒
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }
    }

    public static void main(String[] args) throws InterruptedException {

        //Object物件
        Object a = new Object();
        Object b = new Object();
        Object c = new Object();

        wait pa = new wait("A", c, a);
        wait pb = new wait("B", a, b);
        wait pc = new wait("C", b, c);

        new Thread(pa).start();
        Thread.sleep(1000);  //確保按A、B、C順序執行
        new Thread(pb).start();
        Thread.sleep(1000);
        new Thread(pc).start();
        Thread.sleep(1000);
    }
}

結果:
在這里插入圖片描述

?該問題為三執行緒間的同步喚醒操作,主要的目的就是ThreadA->ThreadB->ThreadC->ThreadA回圈執行三個執行緒,為了控制執行緒執行的順序,那么就必須要確定喚醒、等待的順序,所以每一個執行緒必須同時持有兩個物件鎖,才能繼續執行,一個物件鎖是prev,就是前一個執行緒所持有的物件鎖,還有一個就是自身物件鎖,主要的思想就是,為了控制執行的順序,必須要先持有prev鎖,也就前一個執行緒要釋放自身物件鎖,再去申請自身物件鎖,兩者兼備時列印,之后首先呼叫self.notify()釋放自身物件鎖,喚醒下一個等待執行緒,再呼叫prev.wait()釋放prev物件鎖,終止當前執行緒,等待回圈結束后再次被喚醒,運行上述代碼,可以發現三個執行緒回圈列印ABC,共10次,程式運行的主要程序就是A執行緒最先運行,持有C,A物件鎖,后釋放A,C鎖,喚醒B,執行緒B等待A鎖,再申請B鎖,后列印B,再釋放B,A鎖,喚醒C,執行緒C等待B鎖,再申請C鎖,后列印C,再釋放C,B鎖,喚醒A,

面試題:sleep()和wait()的區別?

答:1. sleep()睡眠時,保持物件鎖,仍然占有該鎖;而wait()睡眠時,釋放物件鎖, 
  2. sleep()是Thread類的方法,wait()是Object的方法
  3.wait()只能在同步方法或者同步代碼塊里面使用,而sleep()可以在任何地方使用
  4.sleep只有睡夠時間才能醒,wait可以隨時喚醒

四、執行緒優先級

Java執行緒有優先級,優先級高的執行緒會獲得較多的運行機會,

Java執行緒的優先級用整數表示,取值范圍是1~10,

Thread.MIN_PRIORITY=1     執行緒可以具有的最低優先級
Thread.MAX_PRIORITY=10    執行緒可以具有的最高優先級
Thread.NORM_PRIORITY=5    分配給執行緒的默認優先級,取值為5

可以使用getPriority()和setPriority(int xxx)來改變和獲取優先級,

package com.yang;

public class Priority implements Runnable{
    @Override
    public void run() {
        //查看主執行緒的默認優先級
        System.out.println(Thread.currentThread().getName()+"-->"+Thread.currentThread().getPriority());
    }

    public static void main(String[] args) {

        System.out.println(Thread.currentThread().getName()+"-->"+Thread.currentThread().getPriority());
        Priority priority = new Priority();
        Thread thread1 = new Thread(priority);
        Thread thread2 = new Thread(priority);
        Thread thread3 = new Thread(priority);

        //不設定優先級
        thread1.start();
        //先設定優先級再啟動
        thread2.setPriority(1);
        thread2.start();

        thread3.setPriority(7);
        thread3.start();
    }
}

結果:

main-->5
Thread-2-->7
Thread-0-->5
Thread-1-->1

Process finished with exit code 0

但是要注意,優先級低只意味著獲得調度的概率低!并不是一定按著優先級的高低來呼叫的!這都是看CPU的調度,

main-->5
Thread-0-->5
Thread-2-->6
Thread-1-->1

Process finished with exit code 0

五、不同執行緒解釋

(了解即可,重點就一個用戶執行緒和守護行程的區別)

主執行緒:JVM呼叫程式main()所產生的執行緒,

當前執行緒:這個是容易混淆的概念,一般指通過Thread.currentThread()來獲取的行程,

守護(daemon)執行緒:指為其他執行緒提供服務的執行緒,也稱為后臺執行緒,JVM的垃圾回收執行緒(gc)就是一個守護執行緒,可以通過isDaemon()和setDaemon()方法來判斷和設定一個執行緒是否為守護執行緒, 執行緒分為 用戶執行緒守護執行緒 ,他們的區別在于:是否等待主執行緒依賴于主執行緒結束而結束,

用戶執行緒執行緒:正常的執行緒都是用戶執行緒,

六、執行緒同步

1.概念

處理多執行緒問題時,多個執行緒訪問同一個物件(并發問題),而且某些執行緒還想修改這個物件,這個時候我們就需要執行緒同步,執行緒同步其實是一種等待機制,多個需要同時訪問此物件的執行緒進入這個物件的等待池形成佇列,等待前面的執行緒使用完畢之后,下一個執行緒再使用,
而執行緒同步的形成條件就是:佇列+鎖,比如上排隊上廁所,進去之后,只能把廁所門鎖上,才能保證里面安全,否則后面排隊的全都進去的話,,就不安全了,,也就是說,為了解決沖突問題,在訪問時加入鎖機制,當一個執行緒獲得物件的排他鎖,獨占資源時,其他執行緒必須等待,等這個執行緒使用之后釋放鎖,所以這也導致了以下問題:

(1).一個執行緒持有鎖時會導致其他需要此鎖的執行緒掛起,

(2).在多執行緒競爭下,加鎖和釋放鎖會導致比較多的背景關系切換和調度演示,引起性能問題(這是必然的,就像你進廁所之后把門鎖了,別人只能等著,雖然安全了,但是比起一起進廁所,性能低了),

(3).如果一個優先級高的執行緒等待一個優先級低的執行緒釋放鎖,會導致優先級倒置,引起性能問題,

不安全案例:買票

package com.yang.syn;

public class BuyTickets implements Runnable {

    private int tickets = 10;
    boolean flag = true;
    
    public static void main(String[] args) {
        BuyTickets tickets = new BuyTickets();
        new Thread(tickets,"喜羊羊").start();
        new Thread(tickets,"灰太狼").start();
        new Thread(tickets,"村長").start();
    }
    
    @Override
    public void run() {
        while (flag){
            buy();
        }
    }
    
    //買票方法
    private void buy(){
        //判斷是否有票
        if (tickets <= 0){
            flag = false;
            return;
        }
        try {
            Thread.sleep(100);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println(Thread.currentThread().getName()+"買到第"+tickets--+"張票");
    }
}

發現出現了票重復的現象,比如有兩個第九張票,,,這就是執行緒不安全!就需要用到同步技術,

灰太狼買到第9張票
喜羊羊買到第10張票
村長買到第10張票
喜羊羊買到第8張票
灰太狼買到第7張票
村長買到第6張票
村長買到第5張票
灰太狼買到第4張票
喜羊羊買到第5張票
灰太狼買到第3張票
村長買到第2張票
喜羊羊買到第3張票
村長買到第1張票
灰太狼買到第1張票
喜羊羊買到第1張票

Process finished with exit code 0

2、同步方法和同步塊:

synchronized關鍵字有兩種用法:synchronized方法和synchronized塊,

(1).synchronized方法:

public synchronized void method(int args){}

??synchronized方法控制對物件的訪問,每個物件對應一把鎖,synchronized方法必須獲得呼叫該方法的物件的鎖才能執行,而且一旦執行,就獨占該鎖,直至方法運行結束釋放鎖,后面被阻塞的執行緒才能獲得這個鎖繼續執行,在某個物件實體內,synchronized 方法可以防止多個執行緒同時訪問這個物件的synchronized方法,也就是說,如果一個物件有多個synchronized方法,只要一個執行緒訪問了其中的一個synchronized方法,其它執行緒就不能同時訪問這個物件中任何一個synchronized方法,(牢記:鎖的是物件,無論synchronized關鍵字加在方法上還是物件上,)

同步方法有鎖嗎?
有鎖,是本類的物件參考,this,

PS:這里還有個面試題:當一個執行緒進入一個物件的synchronized方法A之后,其它執行緒是否可進入此物件的synchronized方法B?

答:不能,其它執行緒只能訪問該物件的非同步方法,試圖進入B方法的執行緒就只能在等鎖池(注意不是等待池)中等待物件的鎖,

解決買票問題,只需要在方法中加一個synchronized關鍵字就好,就實作了佇列+鎖

package com.yang.syn;

public class BuyTickets implements Runnable {

    private int tickets = 10;
    boolean flag = true;

    public static void main(String[] args) {
        BuyTickets tickets = new BuyTickets();
        new Thread(tickets,"喜羊羊").start();
        new Thread(tickets,"灰太狼").start();
        new Thread(tickets,"村長").start();
    }

    @Override
    public void run() {
        while (flag){
            buy();
        }
    }

    //synchronized 同步方法,鎖的是物件本身,或者說是呼叫這個同步方法的物件,即this,
    private synchronized void buy(){
        //判斷是否有票
        if (tickets <= 0){
            flag = false;
            return;
        }
        try {
            Thread.sleep(100);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println(Thread.currentThread().getName()+"買到第"+tickets--+"張票");
    }
}

結果:

喜羊羊買到第10張票
村長買到第9張票
灰太狼買到第8張票
村長買到第7張票
喜羊羊買到第6張票
村長買到第5張票
灰太狼買到第4張票
村長買到第3張票
村長買到第2張票
村長買到第1張票

?如果將synchronized作用于靜態(static) 函式時,取得的鎖是當前呼叫這個方法的物件所屬的類(Class,而不再是由這個Class產生的某個具體物件了),即 本類類名.class

(2).synchronized塊:

synchronized(Obj){/*區塊*/}

Obj稱為同步監視器:

  • Obj可以是任何物件,但是推薦使用共享資源作為同步監視器
  • 同步方法中無序指定同步監視器,因為同步方法的同步監視器就是this,即這個物件本身或者是class(反射),

同步監視器執行程序:

  • 第一個執行緒訪問,鎖定同步監視器,執行其中代碼
  • 第二個執行緒訪問,發現同步監視器被鎖定,無法訪問
  • 第一個執行緒訪問完畢,解鎖同步監視器
  • 第二個執行緒訪問,發現同步監視器沒有鎖,鎖定并訪問,

PS:這里也有個面試題:在監視器(Monitor)內部,是如何做執行緒同步的?程式應該做哪種級別的同步?

答:在 java 虛擬機中, 每個監視器和一個物件參考相關聯, 為了實作監視器的互斥功能, 每個物件都對應著一把鎖. 一旦方法或者代碼塊被 synchronized 修飾, 那么這個部分就放入了監視器的監視區域, 確保一次只能有一個執行緒執行該部分的代碼, 執行緒在獲取鎖之前不允許執行該部分的代碼 , java 提供了顯式監視器( Lock )和隱式監視器( synchronized )兩種鎖方案,程式員可以使用顯示鎖或隱式鎖實作互斥,wait notify notifyall condition 實作協作,

實體:執行緒不安全的集合

package com.yang.syn;

import java.util.ArrayList;
import java.util.List;
//執行緒不安全的集合
public class UnsafeList {
    public static void main(String[] args) {

        List<Object> list = new ArrayList<>();
        for (int i = 0; i < 1000 ; i++) {
            //開啟執行緒
            new Thread(()->{
                //向集合中添加執行緒名字
                list.add(Thread.currentThread().getName());
            }).start();
            try {
                Thread.sleep(3000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            //查看集合大小
            System.out.println(list.size());
        }
    }
}

正常情況來說,集合中應該有1000個執行緒名字,但是事實是,每次運行結果都不一樣,這是因為不同執行緒可能在同一時刻將名字添加到了同一位置,出現了覆寫,這也是執行緒不安全的,
(其實java中有一個JUC安全型別的集合,叫CopyOnWriteArrayList()

解決:同步代碼塊

package com.yang.syn;

import java.util.ArrayList;
import java.util.List;
//執行緒不安全的集合
public class UnsafeList {
    public static void main(String[] args) {

        List<Object> list = new ArrayList<>();
        for (int i = 0; i < 10000 ; i++) {
            //開啟執行緒
            new Thread(()->{
                //增加同步代碼塊,把list鎖住(鎖的一般是需要變化的量,即需要增刪改的物件)
                synchronized (list){
                    //向集合中添加執行緒名字
                    list.add(Thread.currentThread().getName());
                }

            }).start();

        }
        try {
            Thread.sleep(1000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        //查看集合大小
        System.out.println(list.size());
    }
}

此時鎖就是list這個物件,誰拿到這個鎖誰就可以運行它所控制的那段代碼,當有一個明確的物件作為鎖時,就可以這樣寫程式,

同步方法和同步代碼塊的區別是什么?

答:同步方法默認用this或者當前類class物件作為鎖;
??同步代碼塊可以選擇以什么來加鎖,比同步方法要更細顆粒度,我們可以選擇只同步會發生同步問題的部分代碼而不是整個方法,

3.死鎖

package com.yang.syn;

public class DeadLock extends Thread{
    //構造方法
    DeadLock(int choice){
        this.choice=choice;
    }

    //用static來保證只有一份資源
    static A a = new A();
    static B b = new B();

    int choice;

    public static void main(String[] args) {
        //創建執行緒
        DeadLock deadLock1 = new DeadLock(0);
        DeadLock deadLock2 = new DeadLock(1);
        deadLock1.start();
        deadLock2.start();

    }
    @Override
    public void run() {
            if (choice == 0){
                synchronized (a){   //獲得a的鎖
                    System.out.println(Thread.currentThread().getName()+"if...lockA");
                    try {
                        Thread.sleep(1000);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    synchronized (b){  //一秒后獲得b的鎖
                        System.out.println(Thread.currentThread().getName()+"if...LockB");
                    }
                }
            }else {
                synchronized (b){  //獲得b的鎖
                    System.out.println(Thread.currentThread().getName()+"else..LockB");
                    try {
                        Thread.sleep(1000);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    synchronized (a){  //一秒后獲得a的鎖
                        System.out.println(Thread.currentThread().getName()+"else..LockA");
                    }
                }
            }
        }

}

class A{

}
class B{

}

結果:卡死,陷入死鎖,
在這里插入圖片描述
這是因為a和b兩個物件鎖時唯一性的,即當一個執行緒走if陳述句獲得a鎖后,else中的a鎖也沒有了,,b鎖同理,所以會陷入死鎖,

死鎖的前提:必須是多執行緒,而且出現同步嵌套!如果不把同步代碼塊進行嵌套,就不會死鎖,比如:

package com.yang.syn;

public class DeadLock extends Thread{
    //構造方法
    DeadLock(int choice){
        this.choice=choice;
    }

    //用static來保證只有一份資源
    static A a = new A();
    static B b = new B();

    int choice;

    public static void main(String[] args) {
        //創建執行緒
        DeadLock deadLock1 = new DeadLock(0);
        DeadLock deadLock2 = new DeadLock(1);
        deadLock1.start();
        deadLock2.start();

    }
    @Override
    public void run() {
            if (choice == 0){
                synchronized (a){   //獲得a的鎖
                    System.out.println(Thread.currentThread().getName()+"if...lockA");
                    try {
                        Thread.sleep(1000);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
                //不嵌套在a的同步代碼塊中
                synchronized (b){  //一秒后獲得b的鎖
                    System.out.println(Thread.currentThread().getName()+"if...LockB");
                }
            }else {
                synchronized (b){  //獲得b的鎖
                    System.out.println(Thread.currentThread().getName()+"else..LockB");
                    try {
                        Thread.sleep(1000);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }

                }
                //不嵌套在b的同步代碼塊中
                synchronized (a){  //一秒后獲得a的鎖
                    System.out.println(Thread.currentThread().getName()+"else..LockA");
                }
            }
        }

}

//
class A{

}
class B{

}

4.Lock鎖

JDK5引入ReentrantLock(可重入鎖)類,顯示的定義鎖!

package com.yang.lock;

import com.yang.syn.BuyTickets;

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

public class TestLock implements Runnable{

    private int tickets = 10;
    boolean flag = true;

    public static void main(String[] args) {
        BuyTickets tickets = new BuyTickets();
        new Thread(tickets,"喜羊羊").start();
        new Thread(tickets,"灰太狼").start();
        new Thread(tickets,"村長").start();
    }

    //定義lock鎖
    private final Lock lock = new ReentrantLock();

    @Override
    public void run() {
        while (flag){

            try {
                lock.lock(); //加鎖
                if (tickets <= 0){
                    flag = false;
                    return;
                }
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                System.out.println(Thread.currentThread().getName()+"買到第"+tickets--+"張票");
            }finally {
                //解鎖
                lock.unlock();
            }
        }
    }
}

synchronized和Lock對比:

  • Lock是顯式鎖(手動開啟和關閉鎖),synchronized是隱式鎖,出了作用域自動釋放
  • Lock只代碼塊鎖,synchonized有代碼塊鎖和方法鎖
  • 使用Lock鎖,JVM將花費較少的時間來調度執行緒,性能更好

(但是最長用的其實是synchronized),

以上,就是能實作執行緒同步的所有方法,即:
?(1)同步方法:synchronized關鍵字修飾的方法
?(2)同步代碼塊:synchronized修飾的代碼塊
?(3)顯示同步鎖:ReentrantLock(可重入鎖)類中的Lock
?(4)特殊域變數(volatile)實作執行緒同步(不太清楚這個,應該不常用)

總結:
(1)、執行緒同步的目的是為了防止多個執行緒訪問一個資源時對資源的破壞,

( 2)、執行緒同步方法是通過鎖來實作,每個物件都有切僅有一個鎖,這個鎖與一個特定的物件關聯,執行緒一旦獲取了物件鎖,其他訪問該物件的執行緒就無法再訪問該物件的其他非同步方法,

(3)、對于靜態同步方法,鎖是針對這個類的,鎖物件是該類的Class物件,靜態和非靜態方法的鎖互不干預,一個執行緒獲得鎖,當在一個同步方法中訪問另外物件上的同步方法時,會獲取這兩個物件鎖,
(4)、當多個執行緒等待一個物件鎖時,沒有獲取到鎖的執行緒將發生阻塞,

七、執行緒池

在經常創建和銷毀、使用量特別大的資源,比如并發情況下得執行緒,對性能影響很大,這個時候就可以使用執行緒池,提前創建好多個執行緒,放入執行緒池中,使用時直接獲取,使用完就放回池中,這樣可以避免頻繁的創建銷毀、實作重復利用,
好處是:
1、提高回應速度
2、降低資源消耗
3、便于執行緒管理

但是用執行緒池構建的應用程式容易遭受任何其它多執行緒應用程式容易遭受的所有并發風險,諸如同步錯誤和死鎖,它還容易遭受特定于執行緒池的少數其它風險,諸如與池有關的死鎖、資源不足執行緒泄漏

JDK5起提供了執行緒池相關的API:ExecutorService和Executors

  • ExecutorService是Java提供的用于管理執行緒池的類,該類的兩個作用:控制執行緒數量和重用執行緒
  • Executors是工具類、執行緒池的工廠類,用于創建并回傳不同型別的執行緒池,

四種常見的執行緒池(回傳值都是ExecutorService):

1.Executors.newCacheThreadPool():可快取執行緒池,先查看池中有沒有以前建立的執行緒,如果有,就直接使用,如果沒有,就建一個新的執行緒加入池中,快取型池子通常用于執行一些生存期很短的異步型任務

2.Executors.newFixedThreadPool(int n): 創建一個指定作業執行緒數量的執行緒池,每當提交一個任務就創建一個作業執行緒,如果作業執行緒數量達到執行緒池初始的最大數,則將提交的任務存入到池佇列中,

3.Executors.newScheduledThreadPool(int n):單執行緒化的Executor,即只創建唯一的作業者執行緒執行任務,保證所有任務按照指定順序(FIFO, LIFO, 優先級)執行,如果這個執行緒例外結束,會有另一個取代它,保證順序執行,

4.Executors.newScheduleThreadPool: 創建一個定長的執行緒池,而且支持定時的以及周期性的任務執行,支持定時及周期性任務執行,

示例:

package com.yang.Pool;

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

public class TestPool  implements Runnable{
    public static void main(String[] args) {
        //創建執行緒池
        ExecutorService service = Executors.newFixedThreadPool(10);
        //執行
        service.execute(new TestPool());
        service.execute(new TestPool());
        service.execute(new TestPool());
        service.execute(new TestPool());

        //關閉連接
        service.shutdown();

    }
    @Override
    public void run() {
        System.out.println(Thread.currentThread().getName());
    }
}

結果:
在這里插入圖片描述

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

標籤:其他

上一篇:Postman API自動化測驗操作手冊1

下一篇:Apache 中間件漏洞(換行決議)詳解

標籤雲
其他(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)

熱門瀏覽
  • 網閘典型架構簡述

    網閘架構一般分為兩種:三主機的三系統架構網閘和雙主機的2+1架構網閘。 三主機架構分別為內端機、外端機和仲裁機。三機無論從軟體和硬體上均各自獨立。首先從硬體上來看,三機都用各自獨立的主板、記憶體及存盤設備。從軟體上來看,三機有各自獨立的作業系統。這樣能達到完全的三機獨立。對于“2+1”系統,“2”分為 ......

    uj5u.com 2020-09-10 02:00:44 more
  • 如何從xshell上傳檔案到centos linux虛擬機里

    如何從xshell上傳檔案到centos linux虛擬機里及:虛擬機CentOs下執行 yum -y install lrzsz命令,出現錯誤:鏡像無法找到軟體包 前言 一、安裝lrzsz步驟 二、上傳檔案 三、遇到的問題及解決方案 總結 前言 提示:其實很簡單,往虛擬機上安裝一個上傳檔案的工具 ......

    uj5u.com 2020-09-10 02:00:47 more
  • 一、SQLMAP入門

    一、SQLMAP入門 1、判斷是否存在注入 sqlmap.py -u 網址/id=1 id=1不可缺少。當注入點后面的引數大于兩個時。需要加雙引號, sqlmap.py -u "網址/id=1&uid=1" 2、判斷文本中的請求是否存在注入 從文本中加載http請求,SQLMAP可以從一個文本檔案中 ......

    uj5u.com 2020-09-10 02:00:50 more
  • Metasploit 簡單使用教程

    metasploit 簡單使用教程 浩先生, 2020-08-28 16:18:25 分類專欄: kail 網路安全 linux 文章標簽: linux資訊安全 編輯 著作權 metasploit 使用教程 前言 一、Metasploit是什么? 二、準備作業 三、具體步驟 前言 Msfconsole ......

    uj5u.com 2020-09-10 02:00:53 more
  • 游戲逆向之驅動層與用戶層通訊

    驅動層代碼: #pragma once #include <ntifs.h> #define add_code CTL_CODE(FILE_DEVICE_UNKNOWN,0x800,METHOD_BUFFERED,FILE_ANY_ACCESS) /* 更多游戲逆向視頻www.yxfzedu.com ......

    uj5u.com 2020-09-10 02:00:56 more
  • 北斗電力時鐘(北斗授時服務器)讓網路資料更精準

    北斗電力時鐘(北斗授時服務器)讓網路資料更精準 北斗電力時鐘(北斗授時服務器)讓網路資料更精準 京準電子科技官微——ahjzsz 近幾年,資訊技術的得了快速發展,互聯網在逐漸普及,其在人們生活和生產中都得到了廣泛應用,并且取得了不錯的應用效果。計算機網路資訊在電力系統中的應用,一方面使電力系統的運行 ......

    uj5u.com 2020-09-10 02:01:03 more
  • 【CTF】CTFHub 技能樹 彩蛋 writeup

    ?碎碎念 CTFHub:https://www.ctfhub.com/ 筆者入門CTF時時剛開始刷的是bugku的舊平臺,后來才有了CTFHub。 感覺不論是網頁UI設計,還是題目質量,賽事跟蹤,工具軟體都做得很不錯。 而且因為獨到的金幣制度的確讓人有一種想去刷題賺金幣的感覺。 個人還是非常喜歡這個 ......

    uj5u.com 2020-09-10 02:04:05 more
  • 02windows基礎操作

    我學到了一下幾點 Windows系統目錄結構與滲透的作用 常見Windows的服務詳解 Windows埠詳解 常用的Windows注冊表詳解 hacker DOS命令詳解(net user / type /md /rd/ dir /cd /net use copy、批處理 等) 利用dos命令制作 ......

    uj5u.com 2020-09-10 02:04:18 more
  • 03.Linux基礎操作

    我學到了以下幾點 01Linux系統介紹02系統安裝,密碼啊破解03Linux常用命令04LAMP 01LINUX windows: win03 8 12 16 19 配置不繁瑣 Linux:redhat,centos(紅帽社區版),Ubuntu server,suse unix:金融機構,證券,銀 ......

    uj5u.com 2020-09-10 02:04:30 more
  • 05HTML

    01HTML介紹 02頭部標簽講解03基礎標簽講解04表單標簽講解 HTML前段語言 js1.了解代碼2.根據代碼 懂得挖掘漏洞 (POST注入/XSS漏洞上傳)3.黑帽seo 白帽seo 客戶網站被黑帽植入劫持代碼如何處理4.熟悉html表單 <html><head><title>TDK標題,描述 ......

    uj5u.com 2020-09-10 02:04:36 more
最新发布
  • 2023年最新微信小程式抓包教程

    01 開門見山 隔一個月發一篇文章,不過分。 首先回顧一下《微信系結手機號資料庫被脫庫事件》,我也是第一時間得知了這個訊息,然后跟蹤了整件事情的經過。下面是這起事件的相關截圖以及近日流出的一萬條資料樣本: 個人認為這件事也沒什么,還不如關注一下之前45億快遞資料查詢渠道疑似在近日復活的訊息。 訊息是 ......

    uj5u.com 2023-04-20 08:48:24 more
  • web3 產品介紹:metamask 錢包 使用最多的瀏覽器插件錢包

    Metamask錢包是一種基于區塊鏈技術的數字貨幣錢包,它允許用戶在安全、便捷的環境下管理自己的加密資產。Metamask錢包是以太坊生態系統中最流行的錢包之一,它具有易于使用、安全性高和功能強大等優點。 本文將詳細介紹Metamask錢包的功能和使用方法。 一、 Metamask錢包的功能 數字資 ......

    uj5u.com 2023-04-20 08:47:46 more
  • vulnhub_Earth

    前言 靶機地址->>>vulnhub_Earth 攻擊機ip:192.168.20.121 靶機ip:192.168.20.122 參考文章 https://www.cnblogs.com/Jing-X/archive/2022/04/03/16097695.html https://www.cnb ......

    uj5u.com 2023-04-20 07:46:20 more
  • 從4k到42k,軟體測驗工程師的漲薪史,給我看哭了

    清明節一過,盲猜大家已經無心上班,在數著日子準備過五一,但一想到銀行卡里的余額……瞬間心情就不美麗了。最近,2023年高校畢業生就業調查顯示,本科畢業月平均起薪為5825元。調查一出,便有很多同學表示自己又被平均了。看著這一資料,不免讓人想到前不久中國青年報的一項調查:近六成大學生認為畢業10年內會 ......

    uj5u.com 2023-04-20 07:44:00 more
  • 最新版本 Stable Diffusion 開源 AI 繪畫工具之中文自動提詞篇

    🎈 標簽生成器 由于輸入正向提示詞 prompt 和反向提示詞 negative prompt 都是使用英文,所以對學習母語的我們非常不友好 使用網址:https://tinygeeker.github.io/p/ai-prompt-generator 這個網址是為了讓大家在使用 AI 繪畫的時候 ......

    uj5u.com 2023-04-20 07:43:36 more
  • 漫談前端自動化測驗演進之路及測驗工具分析

    隨著前端技術的不斷發展和應用程式的日益復雜,前端自動化測驗也在不斷演進。隨著 Web 應用程式變得越來越復雜,自動化測驗的需求也越來越高。如今,自動化測驗已經成為 Web 應用程式開發程序中不可或缺的一部分,它們可以幫助開發人員更快地發現和修復錯誤,提高應用程式的性能和可靠性。 ......

    uj5u.com 2023-04-20 07:43:16 more
  • CANN開發實踐:4個DVPP記憶體問題的典型案例解讀

    摘要:由于DVPP媒體資料處理功能對存放輸入、輸出資料的記憶體有更高的要求(例如,記憶體首地址128位元組對齊),因此需呼叫專用的記憶體申請介面,那么本期就分享幾個關于DVPP記憶體問題的典型案例,并給出原因分析及解決方法。 本文分享自華為云社區《FAQ_DVPP記憶體問題案例》,作者:昇騰CANN。 DVPP ......

    uj5u.com 2023-04-20 07:43:03 more
  • msf學習

    msf學習 以kali自帶的msf為例 一、msf核心模塊與功能 msf模塊都放在/usr/share/metasploit-framework/modules目錄下 1、auxiliary 輔助模塊,輔助滲透(埠掃描、登錄密碼爆破、漏洞驗證等) 2、encoders 編碼器模塊,主要包含各種編碼 ......

    uj5u.com 2023-04-20 07:42:59 more
  • Halcon軟體安裝與界面簡介

    1. 下載Halcon17版本到到本地 2. 雙擊安裝包后 3. 步驟如下 1.2 Halcon軟體安裝 界面分為四大塊 1. Halcon的五個助手 1) 影像采集助手:與相機連接,設定相機引數,采集影像 2) 標定助手:九點標定或是其它的標定,生成標定檔案及內參外參,可以將像素單位轉換為長度單位 ......

    uj5u.com 2023-04-20 07:42:17 more
  • 在MacOS下使用Unity3D開發游戲

    第一次發博客,先發一下我的游戲開發環境吧。 去年2月份買了一臺MacBookPro2021 M1pro(以下簡稱mbp),這一年來一直在用mbp開發游戲。我大致分享一下我的開發工具以及使用體驗。 1、Unity 官網鏈接: https://unity.cn/releases 我一般使用的Apple ......

    uj5u.com 2023-04-20 07:40:19 more