二.線程的安全與通信

1:線程安全

一:什麼是線程安全性

當多個線程訪問某個類,不管運行時環境採用何種調度方式或者這些線程如何交替執行,並且在主調代碼中不需要任何額外的同步或協同,這個類都能表現出正確的行爲,那麼就稱這個類爲線程安全的.----《併發編程實戰》

二:什麼是線程不安全

多線程併發訪問時,得不到正確的結果.

從字節碼角度剖析線程不安全操作

舉例:

前置操作:

  1. javac -encoding UTF-8 UnsafeThread.java 編譯成.class
  2. javap -c UnsafeThread.class 進行反編譯,得到相應的字節碼指令
       0: getstatic     #2 //獲取指定類的靜態域,並將其押入棧頂
       3: iconst_1 //將int型1押入棧頂
       4: iadd //將棧頂兩個int型相加,將結果押入棧頂
       5: putstatic     #2 //爲指定類靜態域賦值
       8: return

例子中,產生線程不安全問題的原因:
num++ 不是原子性操作,被拆分成好幾個步驟,在多線程併發執行的情況下,因爲cpu調度,多線程快遞切換,有可能兩個同一時刻都讀取了同一個num值,之後對它進行+1操作,導致線程安全性.

2:鎖的分類

鎖的分類

  • 自旋鎖: 在獲取自旋鎖的時候,先必須先得到鎖,在沒有線程獲取到當前鎖的時候,就可以獲取到鎖直接執行鎖內的代碼,在有其他線程持有當前鎖的時候,則當前線程置爲掛起狀態,等待其他線程執行完成釋放鎖後再執行,該鎖一些特定情況下容易導致死鎖,例如在遞歸過程中,在第二次調用則會因無法獲取到鎖,進而導致進入死鎖狀態

  • 阻塞鎖: 例如java中的synchronize的wait,notify,notifyAll,當wait的時候,當前線程掛起,當notify,notifyAll獲得相應的信號時,當前線程就可以繼續執行,其繼續執行依據遵循liunx的線程任務調度機制

  • 重入鎖: 例如java中的synchronize修飾的方法,當前線程獲取到該鎖後,可以無限制調用該方法,其他線程需要等到該線程釋放鎖後才能得以執行

  • 讀寫鎖: 假如有兩個線程,同時執行寫,同一時刻只能有一個線程執行,一個寫一個讀,同一時刻只能有一個執行必須有先後,都是讀的話就沒有限制

  • 互斥鎖: 例如java中的synchronize,Lock鎖的持有者有且最多隻有一個,必須當鎖的持有者釋放之後才能夠被其他線程持有

  • 悲觀鎖: 例如java中的synchronize,Lock,按其名稱的理解,就是對於鎖內內容的訪問都視爲將會修改數據,從而限定每次鎖的持有者只能有一個

  • 樂觀鎖: 按其名稱的理解,對該鎖的持有者不作限定,同時其依舊遵從於讀寫鎖的原則設定

  • 公平鎖: 獲取鎖的持有,按照隊列順序依次獲取

  • 非公平鎖: 在獲取鎖的隊列中可進行插隊,隨機抽取隊列單元給於鎖等操作

  • 偏向鎖->輕量級鎖->重量級鎖: 依據當前的鎖的競爭激烈情況而進行相當的升級

  • 獨佔鎖: 例如synchronize,Lock,這也只是一種概念,獨佔鎖模式下,每次只能有一個線程能持有鎖

  • 共享鎖: 允許多個線程共享該鎖,但只能讀數據,不能修改數據


3.實現線程安全:synchronize,lock

一:synchronize使用解析:

  1. synchronize修飾普通方法 : 鎖住對象的實例,並不會鎖住該對象所在類的整個類.
public synchronized void create() {}
  1. synchronize修飾靜態方法 : 鎖住該對象所在類的整個類.
public static synchronized void create() {}
  1. synchronize修飾代碼塊 :
    1. 成員鎖:鎖住變量,並不會鎖住該對象所在類的整個類.
    2. 實例對象鎖: 鎖住對象的實例,並不會鎖住該對象所在類的整個類.
    3. 當前類的 class 對象鎖: 鎖住該對象所在類的整個類.
//成員鎖
public Object create(Object o) {
    synchronized(o) {
        // 操作
    }
}
//對象鎖
synchronized(this) {
    for (int j = 0; j < 100; j++) {
        i++;
    }
}
// class 類鎖
synchronized(AccountingSync.class) {
    for (int j = 0; j < 100; j++) {
        i++;
    }
}

總結:如果作用的是非靜態的方法或者代碼塊,它取得的鎖是一個對象,如果作用的是靜態代碼塊,它取得的鎖是這個類,這個類中所有對象是同一把鎖

二:Lock與synchronized的區別

操作不同點:

  • lock獲取鎖與釋放鎖的過程都需要手動
  • synchronize都是自動的

原理機制不同點:

  • Lock 依賴於CPU指令,java代碼
  • synchronize依賴於jvm

4.線程間的通信

一:wait,notify,notifyAll注意點

public  void text(Object o){
        synchronized(o){
            try {o.wait();} catch (Exception e) {}
            try {o.notify();} catch (Exception e) {}
            try {o.notifyAll();} catch (Exception e) {}
        }
    }
  1. wait,notify,notifyAll必須在synchronized的代碼塊中 2. o.notify() 只能喚醒被Obect類型修飾的o對象的wait,假如換做o2的object則不不可以,o2.notify()只能o2的wait
何時使用
  1. 需要保證線程安全就只使用關鍵字synchronized即可
  2. 在多個線程中需要交互,例如線程1,執行到某段邏輯之後,需要先執行線程2的代碼,類似這樣的情況下就可以使用wait,notify,notifyAll
wait跟sleep的區別

出處不同:

  1. wait來自於Object
  2. sleep來自於Thread

鎖的持有不同:

  1. wait執行後會將當前線程掛起,並且會釋放鎖,而其他的線程可以執行同步代碼塊
  2. sleep執行後會將當前線程設定爲阻塞狀態且不會釋放所,其他線程無法執行同步代碼塊
notify跟notifyAll的區別
  • nofity隨機喚醒一個被同一對象修飾限定的等待的線程
  • notifyAll喚醒所有被同一對象修飾限定的等待的線程

二:join()方法:

上面有講到


三:ThreadLocal的使用

ThreadLocal介紹:

在java類中,創建的被private修飾的成員變量,被當前類所使用,且成員變量的生命週期隨從於當前類,隨着類的創建而存在,隨着類的消失而消失;
而ThreadLocal理解上和java類中的成員變量是類似的,ThreadLocal歸於線程,隨着線程的創建調用ThreadLocal而存在,隨着線程的消失而消失

ThreadLocal使用:

ThreadLocal<Object> threadLocal = new ThreadLocal<Object>() {
            @Override//未set()get()方法會返回該初始值
            protected Object initialValue() {return null;}
        };
        threadLocal.set("1");//存儲操作
        threadLocal.get();//獲取操作
        threadLocal.remove();//移除操作

ThreadLocal注意事項:

需要注意的一點是ThreadLocal不像list集合等可以進行多組數據的存儲,ThreadLocal只能保存任意類型的一組數據


四:Condition的使用

Condition介紹:

從使用上來說,Condition就是Lock和synchronize功能的結合體,其包含了Lock的功能,並且還能給進行await和signal(也就是在synchronize中的notify功能)

Condition具體使用如下:

public static void main(String[] args) {
         Lock lock = new ReentrantLock();
         Condition boyCondition = lock.newCondition();
         Condition girlCondition = lock.newCondition();

        Thread boy = new Thread(() -> {
            //保障girl線程先跑起來
            try {Thread.sleep(100);} catch (InterruptedException e) {}
            lock.lock();
            System.out.println("1.我已經在咖啡廳等你一個小時了");
            girlCondition.signal();

            try {boyCondition.await();} catch (InterruptedException e) {}
            System.out.println("3.不等了我要去打王者了");
            girlCondition.signal();

            try {boyCondition.await();} catch (InterruptedException e) {}
            System.out.println("5.你化完給我拍個照片算了,不當面看了");
            System.out.println("6.自此,這個男孩失去了女票,男孩痛苦的自問:王者這打到了一百星又能怎麼樣呢???從此男孩一蹶不振,整日沉迷遊戲,勉爲其難的接受衆多女粉絲的追捧......");

            lock.unlock();
        });
        boy.start();

        Thread girl = new Thread(() -> {
            lock.lock();
//            System.out.println("半小時後女孩打開手機");
            try {girlCondition.await();} catch (InterruptedException e) {}
            System.out.println("2.我在化妝請再等一小時");
            boyCondition.signal();

            try {girlCondition.await();} catch (InterruptedException e) {}
            System.out.println("4.那我這一個小時白畫了,不行!");
            boyCondition.signal();

            lock.unlock();
        });
        girl.start();

    }
//輸出結果:
1.我已經在咖啡廳等你一個小時了
2.我在化妝請再等一小時
3.不等了我要去打王者了
4.那我這一個小時白畫了,不行!
5.你化完給我拍個照片算了,不當面看了
6.自此,這個男孩失去了女票,男孩痛苦的自問:王者這打到了一百星又能怎麼樣呢???從此男孩一蹶不振,整日沉迷遊戲,勉爲其難的接受衆多女粉絲的追捧......


五(工具類):CountDownLatch

  • await():將當前現在置爲等待的狀態
  • countDown():計數器減一;當減到0的是時候就會去調用執行被await的線程
  • 舉例 : 王者榮耀裏集結隊友打大龍
public static void main(String[] args) {
            CountDownLatch countDownLatch = new CountDownLatch(4);
            new Thread(()->{
                try {
                    System.out.println("currentThreadName:  "+Thread.currentThread().getName()+"    "+"打野:對面已團滅快過來打大龍~");
                    countDownLatch.await();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                System.out.println("currentThreadName:  "+Thread.currentThread().getName()+"    "+"大龍被擊殺!!!");
                System.out.println("currentThreadName:  "+Thread.currentThread().getName()+"    "+"說時遲那時快,對面復活的魯班一個飛過來的導彈把大龍搶走了");
            }).start();
        String[] Arr = new String[]{"射手","法師","輔助","上單"};
            for (int i = 0; i < 4; i++) {
                int finalI = i;
                new Thread(()->{
                    try {
                        System.out.println("currentThreadName:  "+Thread.currentThread().getName()+"    "+Arr[finalI]+"  :我來了");
                        Thread.sleep(finalI * 300l);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    } finally {
                        countDownLatch.countDown();
                    }
                }).start();
            }
        }
//輸出結果
currentThreadName:  Thread-0    打野:對面已團滅快過來打大龍~
currentThreadName:  Thread-1    射手  :我來了
currentThreadName:  Thread-2    法師  :我來了
currentThreadName:  Thread-3    輔助  :我來了
currentThreadName:  Thread-4    上單  :我來了
currentThreadName:  Thread-0    大龍被擊殺!!!
currentThreadName:  Thread-0    說時遲那時快,對面復活的魯班一個飛過來的導彈把大龍搶走了

上面的例子可以看的到new CountDownLatch(4)限定了需要countDown()的次數,在循環四次調用了四次countDown()後,才繼續執行countDownLatch.await();後面代碼,才擊殺大龍


(工具類)CyclicBarrier–柵欄

CyclicBarrier介紹:

  • 用於一組線程,兩個線程到達指定狀態後再一起執行,假如只有一條線程那該線程就會一直處於等待狀態
  • 更形象的解釋就是 : 如同晚上十二點約女朋友去看電影,先一起到酒店門口集合,等都到了酒店門口,再一起去看電影;假如是單數,是一個單身狗,就算在酒店門口站到天荒地老也等不到女朋友晚上十二點一起看電影的

跟countDownLatch的區別:

  1. CountDownLatch : 用於當前線程執行到await後掛起,然後等待其他線程執行完任務之後,再繼續執行且不可重複使用
  2. CyclicBarrier : 用於一組線程,兩個線程到達指定狀態後再一起執行,假如只有一條線程那該線程就會一直處於等待狀態 可重用的
 public static void main(String[] args) {
        CyclicBarrier cyclicBarrier = new CyclicBarrier(6);

        for (int i = 0; i < 6; i++) {
            int finalI = i;
            new Thread(() -> {
                try {
                    Thread.sleep(finalI * 500L);
                    System.out.println(Thread.currentThread().getName() + "準備就緒");
                    cyclicBarrier.await();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                } catch (BrokenBarrierException e) {
                    e.printStackTrace();
                }

                System.out.println("開始賽跑");
            }).start();
        }
    }

(工具類)Semaphore信號量的使用

Semaphore介紹:

控制併發數量

 public static void main(String[] args) {
        Semaphore semaphore = new Semaphore(2);
        for (int i = 0; i < 20; i++) {
            new Thread(()->{
                try {
                    //每次只會創建兩個線程執行,當十秒後再創建兩條,依次循環
                    semaphore.acquire();
                    System.out.println(Thread.currentThread().getName() + "666");
                    Thread.sleep(1000L);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                } finally {
                    semaphore.release();
                }
            }).start();
        }
    }

(工具類)Exchanger

Exchanger介紹:

  • 兩個線程之間交換數據
  • 它和CyclicBarrier類似,用於一組線程,兩個線程到達指定狀態後再一起執行,假如只有一條線程那該線程就會一直處於等待狀態
public static void main(String[] args) {
        Exchanger<String> stringExchanger = new Exchanger<>();

        String roleADC = "ADC";
        String rolesubsequent = "輔助";
        String dog = "我掛機...";

        new Thread(() -> {
            System.out.println(Thread.currentThread().getName() + "    :一號玩家最初想玩:" + roleADC);
            try {
                String exchange = stringExchanger.exchange(roleADC);
                System.out.println(Thread.currentThread().getName() + "    :交換後一號玩家玩:" + exchange);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }, "線程1").start();

        new Thread(() -> {
            System.out.println(Thread.currentThread().getName() + "    :二號玩家最初想玩:" + rolesubsequent);
            try {
                String exchange = stringExchanger.exchange(rolesubsequent);
                System.out.println(Thread.currentThread().getName() + "    :交換後二號玩家玩:" + exchange);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }, "線程2").start();

        new Thread(() -> {
            System.out.println(Thread.currentThread().getName() + "    :掛機玩家:" + dog);
            try {
                String exchange = stringExchanger.exchange(dog);
                System.out.println(Thread.currentThread().getName() + "    :掛機後:" + exchange);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }, "線程3").start();
    }
// 輸出結果:

線程1    :一號玩家最初想玩:我玩ADC
線程2    :二號玩家最初想玩:我玩輔助
線程2    :交換後二號玩家玩我玩ADC
線程1    :交換後一號玩家玩:我玩輔助
線程3    :掛機玩家:我掛機...
  • 由此可見Exchanger必須成對使用,掛機玩家就再也沒有出現過

5.單例與線程安全

一:單例與線程安全

public class HungerSingleton {

    private static HungerSingleton ourInstance = new HungerSingleton();

    public static HungerSingleton getInstance() {
        return ourInstance;
    }

    private HungerSingleton() {
    }
}

注意:

  • 餓漢式單例,在類加載的時候,就已經進行實例化,假如該實例對象內存佔用高,實例化後長期並未使用這就會導致浪費內存.

二:懶漢式單例

/**
 * 懶漢式單例
 * 在需要的時候再實例化
 */
public class LazySingleton {

    private static volatile LazySingleton lazySingleton = null;

    private LazySingleton() {

    }

    public static LazySingleton getInstance() {
        //判斷實例是否爲空,爲空則實例化
        if (null == lazySingleton) {
            //模擬實例化時耗時的操作
            try {
                Thread.sleep(1000L);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            synchronized (LazySingleton.class) {
                if (null == lazySingleton) {
                
                //正常的加載順序是: 1. 分配對象的內存空間 2. 加載對象 3. 將對象指向分配的內存空間
                // 重排序的情況下將是: 1,3,2 當A線程執行完第一步操作後,因爲指令重排序,先執行第三步驟,則這個時候對象不爲null,這時,B線程線程進行調用if (null == lazySingleton)時候就會得到不爲null
                //所以lazySingleton必須要加volatile 關鍵字修飾
                    lazySingleton = new LazySingleton();
                }
            }
        }
        //否則直接返回
        return lazySingleton;
    }
}

三:枚舉方式單例

public enum  EnumSingletion {
    INSTANCE;
    private Object singletion;
    //JVM保證整個方法只被調用一次
    EnumSingletion(){
        singletion = new Object();
    }

    public Object getSingletion() {
        return singletion;
    }
}
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章