多線程-基礎-1

線程安全:多個線程同時訪問這個類時,始終返回正常的行爲,稱爲線程安全。

1、Synchronized

1.1 多個線程一個鎖

/**同步:synchronized
 *    同步的概念就是共享,我們要牢記“共享”,如果不是共享的資源就沒必要進行同步
 * 異步:asynchronized
 *    異步就是獨立,相互之間不受任何制約。就好像我們學習http的時候,在頁面發起ajax請求
 *  我們還可以繼續瀏覽頁面或者操作頁面的其他內容,二者之間沒有任何關係。
 *
 * 同步的目的就是爲了線程安全,其實對於線程安全來說,需要滿足兩個特性
 *   原子性(同步)
 *   可見性
 */

1.1.1 示例代碼:

public class MyObject {
    public synchronized void method1(){
        try {
            System.out.println(Thread.currentThread().getName());
            Thread.sleep(4000);
        }catch (InterruptedException e){
            e.printStackTrace();
        }
    }
    public   void method2(){
        try {
            System.out.println(Thread.currentThread().getName());
            Thread.sleep(1000);
        }catch (InterruptedException e){
            e.printStackTrace();
        }
    }

    /**
     * 一個實例對象,他的方法加鎖了,則這個鎖是加在實例對象上的。
     *  ①:同一個對象在訪問被synchronized修飾的代碼塊(方法)時,需要等待鎖釋放。(public synchronized void method1(),public synchronized void method2(),)
     *  ②:若同一個對象訪問未被synchronized修飾的代碼塊(方法)時,無鎖。(public synchronized void method1(),public void method2())
     * @param args
     */
    public static void main(String[] args) {
        MyObject t1 = new MyObject();
        new Thread(() -> t1.method1(),"t1").start();
        new Thread(() -> t1.method2(),"t2").start();
    }
}

    /**
     * 一個實例對象,他的方法加鎖了,則這個鎖是加在實例對象上的。
     *  ①:同一個對象在訪問被synchronized修飾的代碼塊(方法)時,需要等待鎖釋放。(public synchronized void method1(),public synchronized void method2(),)
     *  ②:若同一個對象訪問未被synchronized修飾的代碼塊(方法)時,無鎖。(public synchronized void method1(),public void method2())
     * @param args
     */
    public static void main(String[] args) {
        MyObject t1 = new MyObject();
        new Thread(() -> t1.method1(),"t1").start();
        new Thread(() -> t1.method2(),"t2").start();
    }
}

1.1.2 實例說明:

 /**
 *實例總結:
 *    A線程先持有object對象的Lock鎖,B線程如果在這個時候調用對象中的同步
 *  (synchronized)方法則需要等待,也就是同步
 *    A線程先持有object對象的Lock鎖,B線程可以異步的方式調用對象中的非
 * (synchronized)修飾的方法。
 */

 

1.2 多個線程多個鎖

/**
 * 多個線程多個鎖:多個線程,每個線程都可以拿到自己指定的鎖,分別獲得鎖之後,執行synchronized方法體的內容。
 */

1.2.1 示例代碼:

public class MutiThread {
    /**
     * 全局的屬性
     */
    private static int num = 0;
    public static synchronized void printNum(String tag){
        try {
           if (tag.equals("a")) {
               num = 100;
               System.out.println("tag a ,set num over ! ");
               Thread.sleep(4000);
           }else {
               num =  200;
               System.out.println("tag b, set num over !");
               Thread.sleep(100);
           }
            System.out.println("tag "+ tag + ", num = " + num);
          } catch (InterruptedException e) {
              e.printStackTrace();
      }
    }

 /**
     * ①:兩個線程獲取的是兩個對象的鎖,互不影響,不會有同步效果【public synchronized void printNum(String tag)】
     * ②:多個對象之間如果想要共用一把鎖,需要類級別的鎖(方法被static修飾)【public static synchronized void printNum(String tag)】
     * 主方法
     * @param args
     */
    public static void main(String[] args) {
        //定義兩個不同的對象
        MutiThread m1 = new MutiThread();
        MutiThread m2 = new MutiThread();
        new Thread(()-> m1.printNum("a")).start();
        new Thread(()-> m2.printNum("b")).start();
    }
}
​

 

1.2.2 執行結果:

tag a ,set num over ! 
tag b, set num over !
tag b, num = 200
tag a, num = 200
​
Process finished with exit code 0

1.2.3 實例總結:

​
/**
 * 實例總結:
 *       ①:關鍵字synchronized取得的鎖都是對象鎖,而不是把一段代碼(方法)當作鎖。
 *   所以示例代碼中那個線程先執行synchronized關鍵字的方法,那個線程就持有該方法
 *   所屬對象的鎖(Lock),兩個對象,線程獲得獲得的就是兩個不同對象的鎖,他們互不影響。
 *       ②:有一種情況則是相同的鎖,即在靜態方法上加synchronized關鍵字,表示鎖定.class類,
 *   類一級別的鎖(獨佔.class類)。
 */

1.3 synchronized 鎖對象選擇:

1.3.1 儘量不要使用字符串常量作爲鎖對象,容易出現死循環

public class StringLock {
    public void method1(){
        String lock = new String("就這樣吧");
        synchronized ("就這樣吧")
        //synchronized (lock)
                           {
            try {
                while (true) {
                    System.out.println("當前線程:" + Thread.currentThread().getName() + "開始");
                    Thread.sleep(1000);
                    System.out.println("當前線程:" + Thread.currentThread().getName() + "結束");
                }
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
​
    public static void main(String[] args) {
       final StringLock stringLock = new StringLock();
        new Thread(() -> stringLock.method1(), "t1").start();
        new Thread(() -> stringLock.method1(), "t2").start();
    }
}
當前線程:t1開始
當前線程:t1結束
當前線程:t1開始
......

1.3.2 String作爲鎖對象,被重新賦值之後,對象引用發生變化,導致鎖變化。

**
 * 字符串常量在內部發生變化,引用對象發生變化【System.out.println(lock.getBytes());】,引用的不是同一個對象,所以兩個線程同時進入了
 */
public class ChangeLock {
    private String lock = "lock";
    private void method(){
        synchronized (lock) {
            try {
                System.out.println("當前線程:"+ Thread.currentThread().getName() +"開始");
                System.out.println(lock.getBytes());
                lock = "change lock";
                Thread.sleep(2000);
                System.out.println("當前線程: "+ Thread.currentThread().getName() +"結束");
            }catch (InterruptedException e){
                e.printStackTrace();
            }
        }
    }
    public static void main(String[] args) {
        ChangeLock changeLock = new ChangeLock();
        new Thread(() -> changeLock.method(), "t1").start();
        new Thread(() -> changeLock.method(), "t2").start();
    }
}
當前線程:t1開始
[B@8193e5
當前線程: t1結束
當前線程:t2開始
[B@25fef1
當前線程: t2結束

1.3.1 對象作爲鎖對象,即使對象內部屬性發生變化,則引用的是同一對象。

/**
 * 對象內部屬性發生變化,則引用的是同一對象
 */
public class ModifyObject {
    public String name;
    public int age;
​
    private synchronized void changeAttribute(String name ,int age){
        try {
            System.out.println("當前線程:"+ Thread.currentThread().getName() +"開始");
            this.name = name;
            this.age = age;
            Thread.sleep(2000);
            System.out.println("當前線程: "+ Thread.currentThread().getName() +"結束");
        }catch (InterruptedException e){
            e.printStackTrace();
        }
    }
    public static void main(String[] args) {
        ModifyObject modifyObject = new ModifyObject();
        new Thread(() -> modifyObject.changeAttribute("張三", 18), modifyObject.toString()).start();
        new Thread(() -> modifyObject.changeAttribute("李四",  25), modifyObject.toString()).start();
    }
}
當前線程:com.qiulin.study.thread.day01.ModifyObject@91c18f開始
當前線程: com.qiulin.study.thread.day01.ModifyObject@91c18f結束
當前線程:com.qiulin.study.thread.day01.ModifyObject@91c18f開始
當前線程: com.qiulin.study.thread.day01.ModifyObject@91c18f結束
​

1.4 鎖重入:

1.4.1 方法裏面調用被synchronized修飾的方法。

/**
 * synchorized鎖重入
 */
public class SyncDubbo1 {
​
    public synchronized void  method1(){
        System.out.println("method1.....");
        method2();
    }
    public synchronized void method2(){
        System.out.println("method2.....");
        method3();
    }
    public synchronized void method3(){
        System.out.println("method3.....");
    }
    
    public static void main(String[] args) {
        SyncDubbo1 t1 = new SyncDubbo1();
        new Thread(() -> t1.method1()).start();
    }
}
method1.....
method2.....
method3.....

1.4.2 父子類

static class Parent{
        public int i = 10;
        public synchronized void operationSup(){
            try {
                i--;
                System.out.println("Parent print i = "+ i);
                Thread.sleep(100);
            }catch (InterruptedException e){
                e.printStackTrace();
            }
        }
    }
​
    static class Child extends Parent{
        public synchronized void operationSub(){
            try {
                while (i>0){
                    i--;
                    System.out.println("Child print i =" + i);
                    Thread.sleep(100);
                    this.operationSup();
                }
            }catch (InterruptedException e){
                e.printStackTrace();
            }
        }
    }
​
    public static void main(String[] args) {
        Child child = new Child();
        new Thread(() -> child.operationSub()).start();
    }
Child print i =2
Parent print i = 1
Child print i =0
Parent print i = -1

2. Volatile 關鍵字的主要作用是使變量在多個線程之間可見

/**
* 在java 中,每一個線程都會有一塊內存區,其中存放着所有線程共享的主內存中的變量的拷貝。
* 當線程執行時,他在自己的工作內存中操作這些變量。爲了存取一個共享的變量,
* 一個線程通常先獲取鎖並去清除它的內存工作區,把這些共享變量從所有線程的共享內存區中
* 正確的裝入到他自己的工作內存中,當線程解鎖時該工作內存中變量的值寫回到共享內存中。
*
*一個線程可以執行的操作有使用(use)、賦值(assign)、裝載(load)、存儲(store)、
* 鎖定(lock)、解鎖(unlock)。
* 而主內存可以執行的操作有讀(read)、寫(write)、鎖定(lock)、解鎖(unlock),
* 每個操作都是原子的。
*
* volatile的作用就是強制線程到主內存(共享內存)裏去讀取變量,而不去線程工作內存區裏去讀取,
* 從而實現了多個線程之間的變量可見,也就是滿足線程安全的可見性。
*/
public class VoliteRunThread  {
    private  boolean isRunning = true;
​
    private void setRunning(boolean isRunning) {
        this.isRunning = isRunning;
    }
​
    public void method() {
        System.out.println("進入run方法...");
        try {
            while (isRunning == true) {
                System.out.println("線程開始工作...");
                Thread.sleep(200);
            }
            System.out.println("線程結束");
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
​
    }
​
    public static void main(String[] args) throws InterruptedException {
        VoliteRunThread runThread = new VoliteRunThread();
        new  Thread(() -> runThread.method() ).start();
        Thread.sleep(1000);
        new  Thread(() -> runThread.setRunning(false)).start();
    }
}
進入run方法...
線程開始工作...
線程開始工作...
線程開始工作...
線程開始工作...
線程開始工作...
線程結束

3. 線程間通信

/**
 * 線程之間的通信:線程是操作系統中獨立的個體,但這些個體如果 不經過特殊的處理就不能
 * 成爲一個整體,線程間的通信就是成爲整體的必用方式之一。當線程存在通信指揮,系統間的交互性
 * 會更強大,在提高CPU利用率的同時還會使開發人員對線程任務在處理的過程中進行
 * 有效的把控與監督。
 * <p>
 * 使用wait/notify方法實現線程間的通信。(注意這兩個方法都是object的類的方法,換句話說
 * java 爲所有的對象都提供這兩個方法。)
 * <p>
 * 1. wait 和 notify 必須配合synchronized 關鍵字使用
 * 2. wait釋放鎖,notify方法不釋放鎖。
 */
3.1 volatile 變量修飾,while空輪詢檢測

public class ListAdd1 {
    private volatile List list = new ArrayList();
​
    public void listAdd() {
        list.add("我最帥");
    }
​
    public int getListSize() {
        return list.size();
    }
​
    /**
     * 1.線程間通信,不足:while 空輪詢,浪費資源
     *
     * @param args
     */
    public static void main(String[] args) {
​
        ListAdd2 obj = new ListAdd2();
        new Thread(() -> {
            for (int i = 0; i < 5; i++) {
                try {
                    System.out.println(Thread.currentThread().getName() + "添加一個元素...");
                    Thread.sleep(100);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                obj.listAdd();
            }
        }, "t1").start();
​
​
        new Thread(() -> {
            while (true) {
                if (obj.getListSize() == 2) {
                    System.out.println(Thread.currentThread().getName() + "中斷了....");
                    throw new RuntimeException("線程中斷....");
                }
            }
        }, "t2").start();
    }
}
t1添加一個元素...
t1添加一個元素...
t1添加一個元素...
t2中斷了....
Exception in thread "t2" java.lang.RuntimeException: 線程中斷....
    at com.qiulin.study.thread.day02.ListAdd1.lambda$main$1(ListAdd1.java:54)
    at java.lang.Thread.run(Thread.java:745)
t1添加一個元素...
t1添加一個元素...

3.1.2 使用wait /notify 方式實現【缺點:鎖被佔據,做不到實時通信(必須等到方法執行完畢,鎖釋放後,線程2才中斷)】

/**
 * 使用wait /notify  方式實現
 * <p>
 * 缺點:鎖被佔據,做不到實時通信【必須等到方法執行完畢,鎖釋放後,線程2才中斷】
 */
public class ListAdd2 {
    private volatile List list = new ArrayList();
​
    public void listAdd() {
        list.add("我最帥");
    }
​
    public int getListSize() {
        return list.size();
    }
​
    public static void main(String[] args) {
        ListAdd1 list2 = new ListAdd1();
        Object lock = new Object();
​
        new Thread(() -> {
            try {
                synchronized (lock) {
                    if (list2.getListSize() != 5) {
                        lock.wait();
                    }
                    System.out.println(Thread.currentThread().getName() + "中斷了....");
                    throw new RuntimeException("線程中斷....");
                }
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }, "t2").start();
​
        new Thread(() -> {
            try {
                synchronized (lock) {
                    for (int i = 0; i < 10; i++) {
                        list2.listAdd();
                        System.out.println(Thread.currentThread().getName() + "添加一個元素...");
                        Thread.sleep(100);
                        if (list2.getListSize() == 5) {
                            System.out.println("發出通知....");
                            lock.notify();
                        }
                    }
                }
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }, "t1").start();
​
    }
}

3.1.3 CountDownLatch【內部以計數器控制】CountDownLatch的兩個關鍵方法 await 和 countDown 的定義,其實CountDownLatch只是簡單的利用了 AQS 的 state 屬性(表示鎖可重入的次數),CountDownLatch 的內部類 sync 重寫了 AQS 的 tryAcquireShared,CountDownLatch 的 tryAcquireShared 方法的定義是:

public int tryAcquireShared(int acquires) {
    return getState() == 0? 1 : -1;
}

state的初始值就是初始化 CountDownLatch 時的計數器,在 sync 調用 AQS 的 acquireSharedInterruptibly的時候會判斷 tryAcquireShared(int acquires) 是否大於 0,如果小於 0,會將線程掛起。

/**
 * 實時通信  CountDownLatch
 */
public class ListAdd3 {
    private volatile List list = new ArrayList();
​
    public void listAdd() {
        list.add("我最帥");
    }
​
    public int getListSize() {
        return list.size();
    }
​
    public static void main(String[] args) {
        ListAdd3 list2 = new ListAdd3();
        CountDownLatch downLatch = new CountDownLatch(1);//計數器減爲0時,阻塞結束
​
        new Thread(() -> {
            try {
                if (list2.getListSize() != 2) {
                   downLatch.await();
                }
                System.out.println(Thread.currentThread().getName() + "中斷了....");
                throw new RuntimeException("線程中斷....");
​
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }, "t2").start();
        
        new Thread(() -> {
            try {
                for (int i = 0; i < 5; i++) {
                    list2.listAdd();
                    System.out.println(Thread.currentThread().getName() + "添加一個元素...");
                    Thread.sleep(100);
                    if (list2.getListSize() == 2) {
                        System.out.println("發出通知....");
                        downLatch.countDown();
                    }
                }
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }, "t1").start();
​
    }
}
t1添加一個元素...
t1添加一個元素...
發出通知....
t1添加一個元素...
t2中斷了....
Exception in thread "t2" java.lang.RuntimeException: 線程中斷....
    at com.qiulin.study.thread.day02.ListAdd3.lambda$main$0(ListAdd3.java:31)
    at java.lang.Thread.run(Thread.java:745)
t1添加一個元素...
t1添加一個元素...

4. 使用wait 和notify 實現一個阻塞隊列

 
/**
 * 使用wait 和notify 實現一個阻塞隊列
 *
 * 1.需要一個承裝元素的集合
 */
public class MyQueue {
    // 1.需要一個承裝元素的集合
    private final LinkedList<Object> list = new LinkedList<>();
​
    //2.需要一個計數器
    private AtomicInteger count = new AtomicInteger();
​
    //3. 需要制定上限和下限
    private int minSize = 0;
    //上限可以自定義指定
    private int maxSize ;
​
    //4.構造方法
    public MyQueue(int size){
        this.maxSize = size;
    }
​
    //5.初始化一個對象,用於加鎖
    private final  Object lock = new Object();
​
    //put(將對象加入到隊列)
    public void put(Object obj){
        synchronized (lock){
            while (maxSize == count.get()){  //當前容器已經滿了
                try {
                    lock.wait();
                }catch (InterruptedException e){
                    e.printStackTrace();
                }
            }
            //1.加入元素
            list.add(obj);
            //2.計數器累加
            count.incrementAndGet();
            System.out.println("新加入的元素"+obj);
            //3.通知其他線程【容器滿載的情況】
            lock.notify();
        }
    }
​
    //get(從隊列中獲取對象)
    public  Object take(){
        Object result = null;
        synchronized (lock){
            while (count.get() == this.minSize ){
                try {
                    lock.wait();
                }catch (InterruptedException e){
                    e.printStackTrace();
                }
            }
                //1.移除元素
                result = list.removeFirst();
                System.out.println("移除的元素爲:"+result);
                //2.計數器遞減
                count.decrementAndGet();
                //3.喚醒其他線程【容器爲空的時候】
                lock.notify();
​
        }
        return result;
    }
​
    public int getSize(){
        return list.size();
    }
​
    public static void main(String[] args) {
        MyQueue myQueue = new MyQueue(5);
        myQueue.put("a");
        myQueue.put("b");
        myQueue.put("c");
        myQueue.put("d");
        myQueue.put("e");
        System.out.println("當前隊列長度:"+ myQueue.getSize());
​
        new Thread(() -> myQueue.put("f"),"t1").start();
        new Thread(() ->myQueue.take(),"t2").start();
    }
}
新加入的元素a
新加入的元素b
新加入的元素c
新加入的元素d
新加入的元素e
當前隊列長度:5
移除的元素爲:a
新加入的元素f

5、ThreadLocal

/**
 * ThreadLocal:線程局部變量,是一種多線程併發訪問變量的解決方案。與其synchronized
 * 等加鎖的方式不同,ThreadLocal完全不提供鎖,而使用空間換時間的手段,
 * 爲每個線程提供獨立副本,以保證線程安全。
 *
 * 從性能上說,ThreadLocal不具有絕對的優勢,在併發不是很高的情況下,加鎖的性能
 * 會更好,但是作爲一套與鎖完全無關的解決方案,在併發量或者競爭激烈的額場景,使用
 * ThreadLocal可以在一定程度上減少鎖競爭。
 *
 * 加鎖:以時間換空間
 * ThreadLocal:以空間換時間
 */
public class ThreadLocalDemo {
    private static  ThreadLocal<String> threadLocal = new ThreadLocal<>();
​
    public void setValue(String value){
        threadLocal.set(value);
    }
​
    public String getValue(){
       return threadLocal.get();
    }
​
    public static void main(String[] args) {
        ThreadLocalDemo localDemo = new ThreadLocalDemo();
​
        new Thread(() ->{
            localDemo.setValue("張三");
            System.out.println("當前線程"+Thread.currentThread().getName()+",ThreadLocal值爲:"+localDemo.getValue());
        },"t1").start();
​
        new Thread(() ->{
            System.out.println("當前線程"+Thread.currentThread().getName()+",ThreadLocal值爲:"+localDemo.getValue());
        },"t2").start();
    }
}
結果:
當前線程t1,ThreadLocal值爲:張三
當前線程t2,ThreadLocal值爲:null
​

6、單例模式

/**
 * 單例模式:最常見的額就是飢餓模式和懶漢模式
 * 飢餓模式:直接實例化對象,類加載的時候創建出來
 * 懶漢模式:在調方法時進行實例化對象
 *
 * 在多線程模式中,考慮到性能和線程安全問題,我們一般選擇下面兩種
 * 比較經典的單例模式,在性能提高的同時,又保證了線程安全。
 *
 * ① dubble check instance 雙重空值判斷
 * ② static inner class 內部類
 */

6.1 、懶漢模式

/**
 * 懶漢模式:在調用方法的時候被創建出來
 */
public class LazySingleton {
    private static LazySingleton instance = null;
    public LazySingleton(){
​
    }
    public static LazySingleton getInstance(){
        if (instance == null){
            instance = new LazySingleton();
        }
        return instance;
    }
}

​6.2、 餓漢模式

/**
 * 餓漢模式:在類被加載時創建
 */
public class HungrySingleton {
    private static HungrySingleton instance = new HungrySingleton();
    private HungrySingleton(){
​
    }
    public static HungrySingleton getInstance(){
        return instance;
    }
}

6.3、雙重空值判斷

  • /**
     * 雙重檢測
     */
    public class DubbleSingleton {
        private static DubbleSingleton  instance;
    ​
        public static DubbleSingleton getInstance(){
            if (instance == null){
                try {
                    //模擬初始化對象準備的時間
                    Thread.sleep(3000);
                }catch (InterruptedException e){
                    e.printStackTrace();
                }
                synchronized (DubbleSingleton.class){
                   /* if (instance == null) {
                        instance = new DubbleSingleton();
                    }*/
                    instance = new DubbleSingleton();
            }
            }
            return instance;
        }
    ​
        public static void main(String[] args) {
            new Thread(()->{
                System.out.println(DubbleSingleton.getInstance().hashCode());
            },"t1").start();
    ​
            new Thread(()->{
                System.out.println(DubbleSingleton.getInstance().hashCode());
            },"t2").start();
        }
        
    26103236
    21414242

    ​6.4、靜態內部類

/**
 * 靜態內部類實現單例
 */
public class InnerSingleton {
    private static class Singleton{
        private static InnerSingleton instance = new InnerSingleton();
    }
    public static InnerSingleton getInstance(){
        return Singleton.instance;
    }
}

​7、同步容器類

        同步容器類都是線程安全的,但是在某些場景下可能需要加鎖來保護符合操作。複合類操作如:迭代(反覆訪問某個元素,遍歷完容器中所有的元素)、跳轉(根據指定的順序找到當前元素的下一個元素)、以及條件運算。這些複合操作在多線程併發地修改容器時,可能會表現出意外的行爲,最經典的是ConcurrentModificationException,原因是當容器迭代的過程中,被併發的修改了內容,這是由於早期迭代器設計的時候並沒有考慮併發修改的問題。

        同步類容器:如Vector、HashTable。這些容器的同步功能其實都是JDK的Collections.synchronized***等工廠方法去創建實現的。其底層的機制無非就是用傳統的synchronized關鍵字對每個共用的方法都進行同步,使得每次只能有一個線程訪問容器的狀態。這很明顯不滿足高併發的需求,在保證縣城安全的同時,也必須要保證性能。

7.1、將非線程安全的容器轉換成線程安全的容器,只需要在容器外面使用Collections.sysnchronized***包裝即可。返回的容器爲同步類容器。

 HashMap<Integer,String> map = new HashMap();
  Map<Integer, String> synchronizedMap = Collections.synchronizedMap(map);

8、併發類容器

        Jdk5.0之後提供了許多種併發類容器來替代同步類容器從而改善性能。同步類容器的狀態都是串行化的。他們雖然實現了線程安全,但是嚴重降低了併發性,在多線程環境下,嚴重降低了應用程序的吞吐量。

        併發類容器是專門針對併發設計的,使用ConcurrentHashMap來代替HashTable,而且在ConcurrentHashMap中,添加了一些常見的複合操作的支持。以及使用了CopyOnWriteArrayList代替Vecor,併發的CopyonWriteArraySet,以及併發的Queue,ConcurrentLinkedQueue和LinkedBlockQueue,前者是高性能的隊列,後者是阻塞形式的隊列。還有ArrayBlockingQueue、PriortyQueue、SynchronousQueue等。

8.1、ConcurrentMap 接口有兩個重要的實現:

8.1.1、ConcurrentHashMap

        ConcurrentHashMap內部使用段(Segment)來表示這些不同的部分,每個段其實就是一個小的HashTable,他們有自己的鎖。只要多個修改操作發生在不同的段上,它們就可以併發進行。把一個整體分成了16個段(Segment)。也就是最高支持16個線程併發修改操作。這也是在多線程場景時減小鎖的力度從而降低鎖競爭的一種方案。並且代碼中大多共享變量使用Volatile關鍵字聲明,目的是第一時間獲取修改的內容。

8.1.2、ConcurrentSkipListMap(支持併發排序功能,彌補ConcurrentHashMap),類似於TreeMap。

8.2、Copy-On-Write容器,可以在非常多的併發場景中用到。(讀多寫少場景合適,寫需要copy,耗時)

Copy-On-Write容器,其實是一種用於程序設計中的優化策略。

8.2.1、CopyOnWriteArrayList

8.2.2、CopyOnWriteArraySet

        CopyOnWrite容器即寫時複製的容器。我們再往容器中添加一個元素時,不能直接往容器裏添加,而是先將容器進行Copy,複製出一個新的容器,然後新的容器再添加元素,添加完元素之後,再將原容器的引用指向新的容器。這樣做的好處是我們可以對CopyOnWrite容器進行併發的讀,而不需要加鎖,因爲當前容器不會添加任何元素。所以CopyOnWrite容器也是一種讀寫分離的思想,讀和寫在不同的容器。

  • 注意:*在多個寫(add,remove)的時候,其實方法裏面是有重入鎖保證了數據的一致性。*

9、併發Queue

9.1、ConcurrentLinkedQueue:高性能隊列

        適用於高併發場景,通過無鎖的方式,實現了高併發狀態下的高性能,通常ConcurrentLinkedQueue性能好於BlockingQueue。它是一個基於鏈接節點的無界線程安全的隊列。遵循先進先出原則。隊列不允許null元素。

9.1.1 重要方法:

add()/offer() 添加元素,在ConcurrentLinkedQueue中,這兩個方法沒有任何區別。

poll()/peek() 取出元素,poll()會隊列中刪除元素,peek()不會刪除。

9.2、BlockingQueue:阻塞隊列

9.2.1 ArrayBlockQueue

        基於數組的阻塞隊列實現,在ArrayBlockingQueue內部,維護了一個定長的數組,以便緩存隊列中的數據對象,其內部沒實現讀寫分離,也就意味着生產和消費不能完全並行,長度是需要定義的,可以制定先進先出或者先進後出,也叫有界隊列。

9.2.2 LinkedBlockingQueue

        基於鏈表的阻塞隊列,通ArrayBlockingQueue類似,其內部也維持着一個數據緩衝隊列(該隊列由一個鏈表構成),LinkedBlockingQueue之所以能夠高效的併發處理數據,是因爲其內部實現採用分離鎖(讀寫分離兩個鎖),從而實現生產者和消費者操作的完全並行運行。他是一個無界隊列。

9.2.3 SynchronousQueue

        一種沒有緩衝的隊列,生產者產生的數據直接被消費者獲取並消費。SynchronousQueue 它是一個對於元素來說空了才能存入,存在才能取出的隊列,只保留一個元素在queue裏。

/** 
注意1:它一種阻塞隊列,其中每個 put 必須等待一個 take,反之亦然。
    同步隊列沒有任何內部容量,甚至連一個隊列的容量都沒有。
注意2:它是線程安全的,是阻塞的。
注意3:不允許使用 null 元素。
注意4:公平排序策略是指調用put的線程之間,或take的線程之間。
公平排序策略可以查考ArrayBlockingQueue中的公平策略。
注意5:SynchronousQueue的以下方法很有趣:
    * iterator() 永遠返回空,因爲裏面沒東西。
    * peek() 永遠返回null。
    * put() 往queue放進去一個element以後就一直wait直到有其他thread進來把這個element取走。
    * offer() 往queue裏放一個element後立即返回,如果碰巧這個element被另一個thread取走了,offer方法返回true,認爲offer成功;否則返回false。
    * offer(2000, TimeUnit.SECONDS) 往queue裏放一個element但是等待指定的時間後才返回,返回的邏輯和offer()方法一樣。
    * take() 取出並且remove掉queue裏的element(認爲是在queue裏的。。。),取不到東西他會一直等。
    * poll() 取出並且remove掉queue裏的element(認爲是在queue裏的。。。),只有到碰巧另外一個線程正在往queue裏offer數據或者put數據的時候,該方法纔會取到東西。否則立即返回null。
    * poll(2000, TimeUnit.SECONDS) 等待指定的時間然後取出並且remove掉queue裏的element,其實就是再等其他的thread來往裏塞。
    * isEmpty()永遠是true。
    * remainingCapacity() 永遠是0。
    * remove()和removeAll() 永遠是false。
 **/
SynchronousQueue<String> synchronousQueue = new SynchronousQueue<>();
​
            new Thread(()->{
                try {
                    for (int i=0 ; i< 5; i++){
                        String s = UUID.randomUUID().toString();
                        synchronousQueue.put(s);
                        System.out.println("生產一個數據:" + s);
                        Thread.sleep(1000);
                    }
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }).start();
​
​
            new Thread(() -> {
                while (true) {
                    try {
                        Thread.sleep(500);
                        System.out.println("消費數據:" + synchronousQueue.take());
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
            }).start();
​
​
消費數據:cb02c171-775d-4389-a5d4-b24358e66dff
生產一個數據:cb02c171-775d-4389-a5d4-b24358e66dff
生產一個數據:e89fed09-a57c-4a15-9433-cd5eef0be956
消費數據:e89fed09-a57c-4a15-9433-cd5eef0be956
生產一個數據:ea6725a7-de26-4ec6-abab-2f2181a0c921
消費數據:ea6725a7-de26-4ec6-abab-2f2181a0c921
生產一個數據:35a6eef6-805f-4422-ad2f-f876f35f10c8
消費數據:35a6eef6-805f-4422-ad2f-f876f35f10c8
生產一個數據:ac74809f-647e-421a-9191-28e726311d82
消費數據:ac74809f-647e-421a-9191-28e726311d82

9.3.4 PriorityBlockingQueue

        基於優先級的阻塞隊列(優先級的判斷通過構造函數傳入的Compator對象來決定,也就是說傳入的對象必須實現Comparable接口),在實現PriorityBlockingQueue時,內部控制線程同步的鎖採用的是公平鎖,他是一個無界隊列。

/**
*注意:隊列添加元素時,先將元素添加到數組末尾,在取出元素時,採用“上冒”的方式將該元素儘量往上冒。即:在添加的時候元素沒有排序,在取出的時候,元素按照類似冒泡的方式進行排序。
*/
public static void main(String[] args) {
        PriorityBlockingQueue<Task> queue = new PriorityBlockingQueue<>();
​
​
        queue.add(new Task(1, "task1"));
        queue.add(new Task(3, "task3"));
        queue.add(new Task(2, "task2"));
        queue.add(new Task(4,"task4"));
        System.out.println(queue.toString());
        System.out.println("取出第一個元素:"+queue.poll());
        System.out.println(queue.toString());
    }
​
​
    static class Task implements Comparable<Task>{
      private int id;
      private String name;
​
        public int getId() {
            return id;
        }
​
        public void setId(int id) {
            this.id = id;
        }
​
        public String getName() {
            return name;
        }
​
        public void setName(String name) {
            this.name = name;
        }
​
        public Task(int id, String name) {
            this.id = id;
            this.name = name;
        }
​
        @Override
        public int compareTo(Task task) {
            return this.id>task.id?1 :(this.id ==  task.id ? 0 : -1);
        }
​
        @Override
        public String toString() {
            return "Task{" +
                    "id=" + id +
                    ", name='" + name + '\'' +
                    '}';
        }
    }
    
[Task{id=1, name='task1'}, Task{id=3, name='task3'}, Task{id=2, name='task2'}, Task{id=4, name='task4'}]
取出第一個元素:Task{id=1, name='task1'}
[Task{id=2, name='task2'}, Task{id=3, name='task3'}, Task{id=4, name='task4'}]

9.3.5 DalayQueue

       帶有延時時間的Queue,其中的元素只有當其指定的延時時間到了,才能夠從隊列中獲取到該元素。DelayQueue中的元素必須實現Delayed接口,DelayedQueue是一個沒有大小限制的隊列,應用場景很多,如:對緩存超時的數據進行移除、任務超時處理、空閒連接的關閉等。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章