JUC併發編程學習和筆記

JAVA 線程實現/創建方式

1、繼承Thread

public class MyThread extends Thread { 
 public void run() { 
 System.out.println("MyThread.run()"); 
 } 
} 
MyThread myThread1 = new MyThread(); 
myThread1.start(); 

2、實現Runnable接口

public class MyThread extends OtherClass implements Runnable { 
 public void run() { 
 System.out.println("MyThread.run()"); 
 } 
}
//啓動 MyThread,需要首先實例化一個 Thread,並傳入自己的 MyThread 實例:
MyThread myThread = new MyThread(); 
Thread thread = new Thread(myThread); 
thread.start(); 

3、實現callable接口

實現 Callable 接口。 相較於實現 Runnable 接口的方式,方法可以有返回值,並且可以拋出異常。執行 Callable 方式,

需要 FutureTask 實現類的支持,用於接收運算結果。 FutureTask 是 Future 接口的實現類

class ThreadTest implements Callable<Integer> {

    @Override
    public Integer call() throws Exception {
        return 666;
    }
}

class Demo {
    public static void main(String[] args) throws ExecutionException, InterruptedException {
        ThreadTest threadTest = new ThreadTest();
        FutureTask<Integer> result = new FutureTask<>(threadTest);
        new Thread(result).start();
        System.out.println(result.get());
    }
}

synchronized關鍵字

1、修飾代碼塊

鎖的是當前對象

public void run(){
    synchronized(this){
        //todo
    }
}
//只有一個對象,實現同步
SyncThread syncThread = new SyncThread();
Thread thread1 = new Thread(syncThread, "SyncThread1");
Thread thread2 = new Thread(syncThread, "SyncThread2");
thread1.start();
thread2.start();

//多個對象,不是同一把鎖,兩個線程互不干擾
SyncThread syncThread1 = new SyncThread();
SyncThread syncThread2 = new SyncThread();
Thread thread1 = new Thread(syncThread1, "SyncThread1");
Thread thread2 = new Thread(syncThread2, "SyncThread2");
thread1.start();
thread2.start();

這時創建了兩個SyncThread的對象syncThread1和syncThread2,線程thread1執行的是syncThread1對象中的synchronized代碼(run),而線程thread2執行的是syncThread2對象中的synchronized代碼(run);我們知道synchronized鎖定的是對象,這時會有兩把鎖分別鎖定syncThread1對象和syncThread2對象,而這兩把鎖是互不干擾的,不形成互斥,所以兩個線程可以同時執行。

確定需要鎖的對象寫法

public void method3(SomeObject obj)
{
   //obj 鎖定的對象
   synchronized(obj)
   {
      // todo
   }
}

不確定需要鎖的對象寫法

class Test implements Runnable
{
   private byte[] lock = new byte[0];  // 特殊的instance變量
   public void method()
   {
      synchronized(lock) {
         // todo 同步代碼塊
      }
   }
   public void run() {

   }
}

2、修飾非靜態方法

鎖定了整個方法時的內容

public synchronized void method()
{
   // todo
}

3、修飾靜態方法

我們知道靜態方法是屬於類的而不屬於對象的。同樣的,synchronized修飾的靜態方法鎖定的是這個類的所有對象

public synchronized static void method() {
   // todo
}

4、修飾類

class ClassName {
   public void method() {
      synchronized(ClassName.class) {
         // todo
      }
   }
}

synchronized教程地址:https://www.cnblogs.com/pingchuanxin/p/8473288.html

synchronized跟Lock鎖區別

1.首先synchronized是java內置關鍵字,在jvm層面,Lock是個java類;

2.synchronized無法判斷是否獲取鎖的狀態,Lock可以判斷是否獲取到鎖;

3.synchronized會自動釋放鎖(a 線程執行完同步代碼會釋放鎖 ;b 線程執行過程中發生異常會釋放鎖),Lock需在finally中手工釋放鎖(unlock()方法釋放鎖),否則容易造成線程死鎖;

4.用synchronized關鍵字的兩個線程1和線程2,如果當前線程1獲得鎖,線程2線程等待。如果線程1阻塞,線程2則會一直等待下去,而Lock鎖就不一定會等待下去,如果嘗試獲取不到鎖,線程可以不用一直等待就結束了;

5.synchronized的鎖可重入、不可中斷、非公平,而Lock鎖可重入、可判斷、可公平(兩者皆可)

6.Lock鎖適合大量同步的代碼的同步問題,synchronized鎖適合代碼少量的同步問題。

公平鎖與非公平鎖的區別
1、公平鎖 指在分配鎖前檢查是否有線程在排隊等待獲取該鎖,優先將鎖分配給排隊時間最長的線程
2、非公平鎖 指在分配鎖時不考慮線程排隊等待的情況,隨機分配鎖
3、公平鎖需要在多核的情況下維護一個鎖線程等待隊列,基於該隊列進行鎖的分配,因此效率比非公平鎖低很多

什麼是可重入鎖?
可重入鎖指的是在一個線程中可以多次獲取同一把鎖
比如:一個線程在執行一個帶鎖的方法,該方法中又調用了另一個需要相同鎖的方法,則該線程可以直接執行調用的方法,而無需重新獲得鎖;
在java中ReentrantLock和synchronized都是可重入鎖

Lock鎖

public interface Lock {
    void lock();
    void lockInterruptibly() throws InterruptedException;
    boolean tryLock();
    boolean tryLock(long time, TimeUnit unit) throws InterruptedException;
    void unlock();
    Condition newCondition();
}

unLock()方法是用來釋放鎖的

Lock lock = ...;
lock.lock();
try{
    //處理任務
}catch(Exception ex){
     
}finally{
    lock.unlock();   //釋放鎖
}

tryLock(long time, TimeUnit unit)和tryLock()嘗試獲取鎖

Lock lock = ...;
if(lock.tryLock()) {
     try{
         //處理任務
     }catch(Exception ex){
         
     }finally{
         lock.unlock();   //釋放鎖
     } 
}else {
    //如果不能獲取鎖,則直接做其他事情
}

lockInterruptibly中斷等待狀態的線程

當一個線程獲取了鎖之後,是不會被interrupt()方法中斷的。因爲本身在前面的文章中講過單獨調用interrupt()方法不能中斷正在運行過程中的線程,只能中斷阻塞過程中的線程。因此當通過lockInterruptibly()方法獲取某個鎖時,如果不能獲取到,只有進行等待的情況下,是可以響應中斷的。

中斷的線程會拋出InterruptedException異常

public void method() throws InterruptedException {
    lock.lockInterruptibly();
    try {  
     //.....
    }
    finally {
        lock.unlock();
    }  
}

ReentrantLock

ReentrantLock,意思是“可重入鎖”,關於可重入鎖的概念在下一節講述。ReentrantLock是唯一實現了Lock接口的類,並且ReentrantLock提供了更多的方法。

即Lock接口的方法由ReentrantLock調用

private Lock lock = new ReentrantLock(); 
lock.lock();//上鎖

ReadWriteLock

ReadWriteLock也是一個接口,在它裏面只定義了兩個方法:

public interface ReadWriteLock {
    /**
     * Returns the lock used for reading.
     *
     * @return the lock used for reading.
     */
    Lock readLock();
 
    /**
     * Returns the lock used for writing.
     *
     * @return the lock used for writing.
     */
    Lock writeLock();
}

一個用來獲取讀鎖,一個用來獲取寫鎖。也就是說將文件的讀寫操作分開,分成2個鎖來分配給線程,從而使得多個線程可以同時進行讀操作。下面的ReentrantReadWriteLock實現了ReadWriteLock接口。

ReentrantReadWriteLock

ReentrantReadWriteLock裏面提供了很多豐富的方法,不過最主要的有兩個方法:readLock()和writeLock()用來獲取讀鎖和寫鎖。

readLock可以多個線程同時執行

writeLock只有一個線程可以執行,其他線程需要等到釋放寫鎖才能執行

ReentrantLock教程地址:https://www.cnblogs.com/bsjl/p/7654618.html

生產者消費者問題

1. wait() / notify()方法

當緩衝區已滿時,生產者線程停止執行,放棄鎖,使自己處於等狀態,讓其他線程執行;
當緩衝區已空時,消費者線程停止執行,放棄鎖,使自己處於等狀態,讓其他線程執行。
當生產者向緩衝區放入一個產品時,向其他等待的線程發出可執行的通知,同時放棄鎖,使自己處於等待狀態;
當消費者從緩衝區取出一個產品時,向其他等待的線程發出可執行的通知,同時放棄鎖,使自己處於等待狀態。

/**
 * 倉庫
 */
public class Storage {
    private final int Max = 10;
    private LinkedList<Object> list = new LinkedList<>();

    // 生產
    public void produce() {
        synchronized (list) {
            while (list.size() + 1 > Max) {
                System.out.println("【生產者" + Thread.currentThread().getName()
                        + "】倉庫已滿");
                try {
                    list.wait();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
            list.add(new Object());
            System.out.println("【生產者" + Thread.currentThread().getName()
                    + "】生產一個產品,現庫存" + list.size());
            list.notifyAll();
        }
    }

    // 消費
    public void consume() {
        synchronized (list) {
            while (list.size() == 0) {
                System.out.println("【消費者" + Thread.currentThread().getName()
                        + "】倉庫爲空");
                try {
                    list.wait();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
            list.remove();
            System.out.println("【消費者" + Thread.currentThread().getName()
                    + "】消費一個產品,現庫存" + list.size());
            list.notifyAll();
        }
    }
}

/**
 * 生產者
 */
public class Producer implements Runnable {

    private Storage storage;

    public Producer() {
    }

    public Producer(Storage storage) {
        this.storage = storage;
    }

    @Override
    public void run() {
        while (true) {
            try {
                Thread.sleep(1000);
                storage.produce();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
}

/**
 * 消費者
 */
public class Consumer implements Runnable {

    private Storage storage;

    public Consumer() {
    }

    public Consumer(Storage storage) {
        this.storage = storage;
    }

    @Override
    public void run() {
        while (true) {
            try {
                Thread.sleep(3000);
                storage.consume();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
}

/**
 * 測試類
 */
public class test {
    public static void main(String[] args) {
        Storage storage = new Storage();
        Thread p1 = new Thread(new Producer(storage));
        Thread p2 = new Thread(new Producer(storage));
        Thread p3 = new Thread(new Producer(storage));
        Thread c1 = new Thread(new Consumer(storage));
        Thread c2 = new Thread(new Consumer(storage));
        Thread c3 = new Thread(new Consumer(storage));
        p1.start();
        p2.start();
        p3.start();
        c1.start();
        c2.start();
        c3.start();
    }
}

2. await() / signal()方法

在JDK5中,用ReentrantLock和Condition可以實現等待/通知模型,具有更大的靈活性。通過在Lock對象上調用newCondition()方法,將條件變量和一個鎖對象進行綁定,進而控制併發程序訪問競爭資源的安全。

  1. await() :造成當前線程在接到信號或被中斷之前一直處於等待狀態。
  2. await(long time, TimeUnit unit) :造成當前線程在接到信號、被中斷或到達指定等待時間之前一直處於等待狀態。
  3. awaitNanos(long nanosTimeout) :造成當前線程在接到信號、被中斷或到達指定等待時間之前一直處於等待狀態。返回值表示剩餘時間,如果在nanosTimesout之前喚醒,那麼返回值 = nanosTimeout - 消耗時間,如果返回值 <= 0 ,則可以認定它已經超時了。
  4. awaitUninterruptibly() :造成當前線程在接到信號之前一直處於等待狀態。【注意:該方法對中斷不敏感】。
  5. awaitUntil(Date deadline) :造成當前線程在接到信號、被中斷或到達指定最後期限之前一直處於等待狀態。如果沒有到指定時間就被通知,則返回true,否則表示到了指定時間,返回返回false。
  6. signal():喚醒一個等待線程。該線程從等待方法返回前必須獲得與Condition相關的鎖。
  7. signal()All:喚醒所有等待線程。能夠從等待方法返回的線程必須獲得與Condition相關的鎖。

在這裏只需改動Storage類

package test1;

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

public class Storage {

    // 倉庫最大存儲量
    private final int MAX_SIZE = 10;
    // 倉庫存儲的載體
    private LinkedList<Object> list = new LinkedList<Object>();
    // 鎖
    private final Lock lock = new ReentrantLock();
    // 倉庫滿的條件變量
    private final Condition full = lock.newCondition();
    // 倉庫空的條件變量
    private final Condition empty = lock.newCondition();

    public void produce()
    {
        // 獲得鎖
        lock.lock();
        while (list.size() + 1 > MAX_SIZE) {
            System.out.println("【生產者" + Thread.currentThread().getName()
                    + "】倉庫已滿");
            try {
                full.await();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
        list.add(new Object());
        System.out.println("【生產者" + Thread.currentThread().getName()
                + "】生產一個產品,現庫存" + list.size());

        empty.signalAll();
        lock.unlock();
    }

    public void consume()
    {
        // 獲得鎖
        lock.lock();
        while (list.size() == 0) {
            System.out.println("【消費者" + Thread.currentThread().getName()
                    + "】倉庫爲空");
            try {
                empty.await();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
        list.remove();
        System.out.println("【消費者" + Thread.currentThread().getName()
                + "】消費一個產品,現庫存" + list.size());

        full.signalAll();
        lock.unlock();
    }
}

3. BlockingQueue阻塞隊列方法

BlockingQueue是JDK5.0的新增內容,它是一個已經在內部實現了同步的隊列,實現方式採用的是我們第2種await() / signal()方法。它可以在生成對象時指定容量大小,用於阻塞操作的是put()和take()方法。
put()方法:類似於我們上面的生產者線程,容量達到最大時,自動阻塞。
take()方法:類似於我們上面的消費者線程,容量爲0時,自動阻塞。

import java.util.concurrent.LinkedBlockingQueue;

public class Storage {

    // 倉庫存儲的載體
    private LinkedBlockingQueue<Object> list = new LinkedBlockingQueue<>(10);

    public void produce() {
        try{
            list.put(new Object());
            System.out.println("【生產者" + Thread.currentThread().getName()
                    + "】生產一個產品,現庫存" + list.size());
        } catch (InterruptedException e){
            e.printStackTrace();
        }
    }

    public void consume() {
        try{
            list.take();
            System.out.println("【消費者" + Thread.currentThread().getName()
                    + "】消費了一個產品,現庫存" + list.size());
        } catch (InterruptedException e){
            e.printStackTrace();
        }
    }
}

可能會出現put()或take()和System.out.println()輸出不匹配的情況,是由於它們之間沒有同步造成的。BlockingQueue可以放心使用,這可不是它的問題,只是在它和別的對象之間的同步有問題。

【生產者Thread-0】生產一個產品,現庫存3
【生產者Thread-2】生產一個產品,現庫存3
【生產者Thread-1】生產一個產品,現庫存3
【生產者Thread-0】生產一個產品,現庫存4
【生產者Thread-1】生產一個產品,現庫存6
【生產者Thread-2】生產一個產品,現庫存5
【消費者Thread-3】消費了一個產品,現庫存4
【消費者Thread-5】消費了一個產品,現庫存4
【消費者Thread-4】消費了一個產品,現庫存3

ArrayBlockingQueue與LinkedBlockingQueue

區別:https://www.jianshu.com/p/5b85c1794351

使用場景:https://blog.csdn.net/jiangguilong2000/article/details/11617529

4. Semaphore信號量

Semaphore是一種基於計數的信號量。它可以設定一個閾值,基於此,多個線程競爭獲取許可信號,做完自己的申請後歸還,超過閾值後,線程申請許可信號將會被阻塞。Semaphore可以用來構建一些對象池,資源池之類的,比如數據庫連接池,我們也可以創建計數爲1的Semaphore,將其作爲一種類似互斥鎖的機制,這也叫二元信號量,表示兩種互斥狀態。計數爲0的Semaphore是可以release的,然後就可以acquire(即一開始使線程阻塞從而完成其他執行。)

主要方法:

  • void acquire() :從信號量獲取一個許可,如果無可用許可前將一直阻塞等待,
  • void acquire(int permits) :獲取指定數目的許可,如果無可用許可前也將會一直阻塞等待
  • boolean tryAcquire():從信號量嘗試獲取一個許可,如果無可用許可,直接返回false,不會阻塞
  • boolean tryAcquire(int permits): 嘗試獲取指定數目的許可,如果無可用許可直接返回false
  • boolean tryAcquire(int permits, long timeout, TimeUnit unit): 在指定的時間內嘗試從信號量中獲取許可,如果在指定的時間內獲取成功,返回true,否則返回false
  • void release(): 釋放一個許可,別忘了在finally中使用,注意:多次調用該方法,會使信號量的許可數增加,達到動態擴展的效果,如:初始permits爲1, 調用了兩次release,最大許可會改變爲2
  • int availablePermits(): 獲取當前信號量可用的許可
public class Storage {

    // 倉庫存儲的載體
    private LinkedList<Object> list = new LinkedList<Object>();
    // 倉庫的最大容量
    final Semaphore notFull = new Semaphore(10);
    // 將線程掛起,等待其他來觸發
    final Semaphore notEmpty = new Semaphore(0);
    // 互斥鎖,只有一個線程可以得到許可
    final Semaphore mutex = new Semaphore(1);

    public void produce() {
        try {
            // 獲取許可
            notFull.acquire();
            mutex.acquire();
            list.add(new Object());
            System.out.println("【生產者" + Thread.currentThread().getName()
                    + "】生產一個產品,現庫存" + list.size());
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            // 釋放
            mutex.release();
            notEmpty.release();//初始0多次釋放後數值會增加
        }
    }

    public void consume() {
        try {
            // 獲取許可
            notEmpty.acquire();
            mutex.acquire();
            list.remove();
            System.out.println("【消費者" + Thread.currentThread().getName()
                    + "】消費一個產品,現庫存" + list.size());
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            // 釋放
            mutex.release();
            notFull.release();
        }
    }
}

5. 管道

具體實現查看教程

生產者消費者問題教程地址:https://blog.csdn.net/ldx19980108/article/details/81707751

Java中的鎖分類

  • 公平鎖/非公平鎖
  • 可重入鎖
  • 獨享鎖/共享鎖
  • 互斥鎖/讀寫鎖
  • 樂觀鎖/悲觀鎖
  • 分段鎖
  • 偏向鎖/輕量級鎖/重量級鎖
  • 自旋鎖

上面是很多鎖的名詞,這些分類並不是全是指鎖的狀態,有的指鎖的特性,有的指鎖的設計,下面總結的內容是對每個鎖的名詞進行一定的解釋。

詳細介紹:https://www.cnblogs.com/qifengshi/p/6831055.html

CopyOnWriteArrayList 

一、CopyOnWriteArrayList介紹
①、CopyOnWriteArrayList,寫數組的拷貝,支持高效率併發且是線程安全的,讀操作無鎖的ArrayList。所有可變操作都是通過對底層數組進行一次新的複製來實現。
②、CopyOnWriteArrayList適合使用在讀操作遠遠大於寫操作的場景裏,比如緩存。它不存在擴容的概念,每次寫操作都要複製一個副本,在副本的基礎上修改後改變Array引用。CopyOnWriteArrayList中寫操作需要大面積複製數組,所以性能肯定很差。
③、CopyOnWriteArrayList 合適讀多寫少的場景,不過這類慎用 ,因爲誰也沒法保證CopyOnWriteArrayList 到底要放置多少數據,萬一數據稍微有點多,每次add/set都要重新複製數組,這個代價實在太高昂了。在高性能的互聯網應用中,這種操作分分鐘引起故障。

二、CopyOnWriteArrayList 有幾個缺點:
1、由於寫操作的時候,需要拷貝數組,會消耗內存,如果原數組的內容比較多的情況下,可能導致young gc或者full gc。
(1、young gc :年輕代(Young Generation):對象被創建時,內存的分配首先發生在年輕代(大對象可以直接被創建在年老代),大部分的對象在創建後很快就不再使用,因此很快變得不可達,於是被年輕代的GC機制清理掉(IBM的研究表明,98%的對象都是很快消亡的),這個GC機制被稱爲Minor GC或叫Young GC。
2、年老代(Old Generation):對象如果在年輕代存活了足夠長的時間而沒有被清理掉(即在幾次Young GC後存活了下來),則會被複制到年老代,年老代的空間一般比年輕代大,能存放更多的對象,在年老代上發生的GC次數也比年輕代少。當年老代內存不足時,將執行Major GC,也叫 Full GC

2、不能用於實時讀的場景,像拷貝數組、新增元素都需要時間,所以調用一個set操作後,讀取到數據可能還是舊的,雖然CopyOnWriteArrayList 能做到最終一致性,但是還是沒法滿足實時性要求;

三、CopyOnWriteArrayList的一些方法
1、add(E e) :將指定元素添加到此列表的尾部,返回值爲boolean。
2、add(int index, E element) : 在此列表的指定位置上插入指定元素。
3、clear():從此列表移除所有元素。
4、contains(Object o) :如果此列表包含指定的元素,則返回 true。
5、equals(Object o) :比較指定對象與此列表的相等性。
6、get(int index) : 返回列表中指定位置的元素。
7、hashCode() : 返回此列表的哈希碼值。
8、indexOf(E e, int index) : 返回第一次出現的指定元素在此列表中的索引,從 index 開始向前搜索,如果沒有找到該元素,則返回 -1。
9、indexOf(Object o) :返回此列表中第一次出現的指定元素的索引;如果此列表不包含該元素,則返回 -1。
10、isEmpty() :如果此列表不包含任何元素,則返回 true。
11、iterator() :返回以恰當順序在此列表元素上進行迭代的迭代器,返回值爲 Iterator。
12、lastIndexOf(E e, int index) :返回最後一次出現的指定元素在此列表中的索引,從 index 開始向後搜索,如果沒有找到該元素,則返回 -1。
13、lastIndexOf(Object o) : 返回此列表中最後出現的指定元素的索引;如果列表不包含此元素,則返回 -1。
14、remove(int index) :移除此列表指定位置上的元素。
15、remove(Object o) :從此列表移除第一次出現的指定元素(如果存在),返回值爲 boolean。
16、set(int index, E element) :用指定的元素替代此列表指定位置上的元素。
17、size() :返回此列表中的元素數。
18、subList(int fromIndex, int toIndex) :返回此列表中 fromIndex(包括)和 toIndex(不包括)之間部分的視圖,返回值爲 List 。

總結
CopyOnWriteArrayList這是一個ArrayList的線程安全的變體,其原理大概可以通俗的理解爲:初始化的時候只有一個容器,很長一段時間,這個容器數據、數量等沒有發生變化的時候,大家(多個線程),都是讀取(假設這段時間裏只發生讀取的操作)同一個容器中的數據,所以這樣大家讀到的數據都是唯一、一致、安全的,但是後來有人往裏面增加了一個數據,這個時候CopyOnWriteArrayList 底層實現添加的原理是先copy出一個容器(可以簡稱副本),再往新的容器裏添加這個新的數據,最後把新的容器的引用地址賦值給了之前那個舊的的容器地址,但是在添加這個數據的期間,其他線程如果要去讀取數據,仍然是讀取到舊的容器裏的數據。

使用講解:https://www.cnblogs.com/fsmly/p/11298782.html

原理優缺點:https://www.cnblogs.com/yangfei629/p/11530968.html

CopyOnWriteArrayset

一、CopyOnWriteArraySet介紹
它是線程安全的無序的集合,可以將它理解成線程安全的HashSet,有意思的是,CopyOnWriteArraySet和HashSet雖然都繼承於共同的父類都繼承於共同的父類;但是,HashSet是通過“散列表(HashMap)”實現的,而CopyOnWriteArraySet則是通過“動態數組(CopyOnWriteArrayList)”實現的,並不是散列表。和CopyOnWriteArrayList類似,CopyOnWriteArraySet具有以下特性:
1、它最適合於具有以下特徵的應用程序:Set 大小通常保持很小,只讀操作遠多於可變操作,需要在遍歷期間防止線程間的衝突。
2、它是線程安全的。
3、因爲通常需要複製整個基礎數組,所以可變操作(add()、set() 和remove() 等等)的開銷很大。
4、迭代器支持hasNext(), next()等不可變操作,但不支持可變remove()等操作。
5、使用迭代器進行遍歷的速度很快,並且不會與其他線程發生衝突。在構造迭代器時,迭代器依賴於不變的數組快照。

二、CopyOnWriteArraySet的一些方法
1、add(E e) :如果指定元素並不存在於此 set 中,則添加它,返回值爲Boolean。
2、clear():移除此 set 中的所有元素。
3、contains(Object o) : 如果此 set 包含指定元素,則返回 true。
4、equals(Object o) :比較指定對象與此 set 的相等性,返回值爲 boolean 。
5、isEmpty() :如果此 set 不包含任何元素,則返回 true。
6、iterator() :返回按照元素添加順序在此 set 中包含的元素上進行迭代的迭代器,返回值爲 Iterator。
7、remove(Object o) :如果指定元素存在於此 set 中,則將其移除,返回值爲 boolean 。
8、size() :返回此 set 中的元素數目。
9、Object[] toArray() : 返回一個包含此 set 所有元素的數組。

使用教程:https://www.cnblogs.com/xiaolovewei/p/9142046.html

使用教程2:https://blog.csdn.net/weixin_42146366/article/details/88019292

ConcurrentHashMap 

HashMap 是 Java Collection Framework 的重要成員,也是Map族(如下圖所示)中我們最爲常用的一種。不過遺憾的是,HashMap不是線程安全的。也就是說,在多線程環境下,操作HashMap會導致各種各樣的線程安全問題,比如在HashMap擴容重哈希時出現的死循環問題,髒讀問題等。HashMap的這一缺點往往會造成諸多不便,雖然在併發場景下HashTable和由同步包裝器包裝的HashMap(Collections.synchronizedMap(Map<K,V> m) )可以代替HashMap,但是它們都是通過使用一個全局的鎖來同步不同線程間的併發訪問,因此會帶來不可忽視的性能問題。慶幸的是,JDK爲我們解決了這個問題,它爲HashMap提供了一個線程安全的高效版本 —— ConcurrentHashMap。在ConcurrentHashMap中,無論是讀操作還是寫操作都能保證很高的性能:在進行讀操作時(幾乎)不需要加鎖,而在寫操作時通過鎖分段技術只對所操作的段加鎖而不影響客戶端對其它段的訪問。特別地,在理想狀態下,ConcurrentHashMap 可以支持 16 個線程執行併發寫操作(如果併發級別設爲16),及任意數量線程的讀操作。

使用教程:https://blog.csdn.net/cx897459376/article/details/106427587/

CountDownLatch

1、類介紹

一個同步輔助類,在完成一組正在其他線程中執行的操作之前,它允許一個或多個線程一直等待。用給定的計數 初始化 CountDownLatch。由於調用了 countDown() 方法,所以在當前計數到達零之前,await 方法會一直受阻塞。之後,會釋放所有等待的線程,await 的所有後續調用都將立即返回。這種現象只出現一次——計數無法被重置。 一個線程(或者多個), 等待另外N個線程完成某個事情之後才能執行

示例

public class CountDownLatchTest {
    private static final int RUNNER_COUNT = 10;

    public static void main(String[] args) throws InterruptedException {
        final CountDownLatch begin = new CountDownLatch(1);
        final CountDownLatch end = new CountDownLatch(RUNNER_COUNT);
        //創建線程池
        final ExecutorService exec = Executors.newFixedThreadPool(10);

        for (int i = 0; i < RUNNER_COUNT; i++) {
            final int NO = i + 1;
            Runnable run = new Runnable() {
                @Override
                public void run() {
                    try {
                        // 線程阻塞,直到計數爲0的時候喚醒;可以響應線程中斷退出阻塞
                        begin.await();
                        Thread.sleep((long) (Math.random() * 10000));
                        System.out.println("No." + NO + " arrived");
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    } finally {
                        // 計數-1
                        end.countDown();
                    }
                }
            };
            exec.submit(run);
        }
        System.out.println("Game Start ...");
        // 計數-1
        begin.countDown();
        // 線程阻塞,直到計數爲0的時候喚醒;可以響應線程中斷退出阻塞
        end.await();
        // 線程阻塞一段時間,如果計數依然不是0,則返回false;否則返回true
        // end.await(30, TimeUnit.SECONDS);
        System.out.println("Game Over.");
        // 啓動有序關閉
        exec.shutdown();
    }
}

CyclicBarrier

柵欄類似於閉鎖,它能阻塞一組線程直到某個事件的發生。柵欄與閉鎖的關鍵區別在於,所有的線程必須同時到達柵欄位置,才能繼續執行。閉鎖用於等待事件,而柵欄用於等待其他線程。

CyclicBarrier可以使一定數量的線程反覆地在柵欄位置處彙集。當線程到達柵欄位置時將調用await方法,這個方法將阻塞直到所有線程都到達柵欄位置。如果所有線程都到達柵欄位置,那麼柵欄將打開,此時所有的線程都將被釋放,而柵欄將被重置以便下次使用。                                                                                  理解:設定一個值(線程的數量),每個線程啓動後進入await方法,當所有線程都運行後,去到鎖的線程纔會繼續執行,否則處於堵塞狀態

教程地址:https://blog.csdn.net/qq_38293564/article/details/80558157

SynchronousQueue

SynchronousQueue是無界的,是一種無緩衝的等待隊列,但是由於該Queue本身的特性,在某次添加元素後必須等待其他線程取走後才能繼續添加;可以認爲SynchronousQueue是一個緩存值爲1的阻塞隊列,但是 isEmpty()方法永遠返回是true,remainingCapacity() 方法永遠返回是0,remove()和removeAll() 方法永遠返回是false,iterator()方法永遠返回空,peek()方法永遠返回null。

教程地址:https://www.cnblogs.com/hongdada/p/6147834.html

Java自定義線程池和七個參數

 public ThreadPoolExecutor(int corePoolSize,
                              int maximumPoolSize,
                              long keepAliveTime,
                              TimeUnit unit,
                              BlockingQueue<Runnable> workQueue,
                              ThreadFactory threadFactory,
                              RejectedExecutionHandler handler) {
                              //todo}

一、corePoolSize 線程池核心線程大小                                                                                                                                                                                                 二、maximumPoolSize 線程池最大線程數量                                                                                                                                                                                        三、keepAliveTime 空閒線程存活時間                                                                                                                                                                                                 四、unit 空閒線程存活時間單位                                                                                                                                                                                                          五、workQueue 工作隊列                                                                                                                                                                                                                     六、threadFactory 線程工廠                                                                                                                                                                                                                 七、handler 拒絕策略                                                                                                                                                                                                                           教程地址:https://blog.csdn.net/weixin_38938840/article/details/104774426

函數式接口

 函數式接口(Functional Interface)就是一個有且僅有一個抽象方法,但是可以有多個非抽象方法的接口。                                                                                     函數式接口可以被隱式轉換爲 lambda 表達式。                                                                                                                                                                                 如定義了一個函數式接口如下:

@FunctionalInterface
interface GreetingService 
{
    void sayMessage(String message);
}

那麼就可以使用Lambda表達式來表示該接口的一個實現(注:JAVA 8 之前一般是用匿名類實現的):                                                                                               

GreetingService greetService1 = message -> System.out.println("Hello " + message);
greetService1.sayMessage("666")

 消費型接口

 接口唯一的抽象方法是:

public interface Consumer<T> {
    void accept(T T);
}

 這是一個單參數,無返回值的方法,參數是泛型類。這個接口被稱爲消費型接口,因爲沒有返回值,接口裏面幹了什麼和調用方沒什麼關係。

這種單參數無返回值的接口我們可以這麼用Lambda表達式:

Consumer consumer = (a) -> System.out.println("this is " + a);
consumer.accept("123");

 供給型接口                                                                            

 接口唯一的抽象方法是:

public interface Consumer<T> {
    T accept();
}

這是一個無參數,有返回值的方法,返回值類型是泛型類。這個接口被稱作供給型接口。

這種無參數有返回值的方法我們可以這麼用:

Consumer<String> consumer=()-> "666";
String str = consumer.accept();
System.out.println(str);

Steam流式算法

Stream 流處理,首先要澄清的是 java8 中的 Stream 與 I/O 流 InputStream 和 OutputStream 是完全不同的概念。
Stream 機制是針對集合迭代器的增強。流允許你用聲明式的方式處理數據集合(通過查詢語句來表達,而不是臨時編寫一個實現)

教程地址:https://www.cnblogs.com/gaopengfirst/p/10813803.html

擴展------Java雙冒號(::)運算符的使用

以下是Java 8中方法引用的一些語法:

靜態方法引用(static method)語法:classname::methodname 例如:Person::getAge
對象的實例方法引用語法:instancename::methodname 例如:System.out::println
對象的超類方法引用語法: super::methodname
類構造器引用語法: classname::new 例如:ArrayList::new
數組構造器引用語法: typename[]::new 例如: String[]:new
如果上的語法太枯燥,那就通過一些例子來加強對它的理解:

教程地址:https://blog.csdn.net/zhoufanyang_china/article/details/87798829

ForkJoin

fork/join框架相當於一個map/reduce的過程,先將一個大的任務分解成幾個小模塊,再將幾個小模塊繼續分解成子模塊,直到達到可以處理的閾值,最後再將各個子模塊的結果進行彙總。                                                                                                                                                                                                               ForkJoinPool: 線程池最大的特點就是分叉(fork)合併(join)模式,將一個大任務拆分成多個小任務,並行執行,再結合工作竊取算法提高整體的執行效率,充分利用CPU資源。 

//異步執行給定任務的排列,無返回值
ForkJoinPool.execute()
//執行給定的任務,在完成後返回其結果
ForkJoinPool.invoke()
//提交一個ForkJoinTask來執行,有返回值
ForkJoinPool.submit()

ForkJoinTask: 運行在ForkJoinPool的一個任務抽象,可以理解爲類線程但是比線程輕量的實體,在ForkJoinPool中運行的少量ForkJoinWorkerThread可以持有大量的ForkJoinTask和它的子任務,同時也是一個輕量的Future,使用時應避免較長阻塞或IO。

繼承子類:

RecursiveAction:遞歸無返回值的ForkJoinTask子類;

RecursiveTask<T>:遞歸有返回值的ForkJoinTask子類;核心方法:

fork():在當前線程運行的線程池中創建一個子任務;

join():模塊子任務完成的時候返回任務結果;

invoke():執行任務,也可以實時等待最終執行結果;

使用流程:

1、任務類繼承RecursiveTask<T>或者RecursiveAction,這裏只是區分要不要返回值

2、使用ForkJoinPool執行任務

累加示例

package test3;

import java.util.concurrent.RecursiveTask;

/**
 * 累加任務
 */
public class ForkJoinTest extends RecursiveTask<Long> {

    private Long star;
    private Long end;
    private Long temp;

    public ForkJoinTest(Long star, Long end, Long temp) {
        this.star = star;
        this.end = end;
        //臨界值
        this.temp = temp;
    }

    @Override
    protected Long compute() {
        //沒有超出臨界值
        if ((end - star) < temp) {
            Long sum = 0L;
            for (Long i = star; i <= end; i++) {
                sum += i;
            }
            return sum;
        } else {
            //中間值
            Long middle = (star + end) / 2;
            //本質還是遞歸
            ForkJoinTest FJ1 = new ForkJoinTest(star, middle, temp);
            FJ1.fork();
            ForkJoinTest FJ2 = new ForkJoinTest(middle + 1, end, temp);
            FJ2.fork();
            return FJ1.join() + FJ2.join();
        }
    }
}
public class test {
    public static void main(String[] args) throws ExecutionException, InterruptedException {
        ForkJoinPool forkJoinPool = new ForkJoinPool();
        ForkJoinTest forkJoinTest = new ForkJoinTest(0L, 10_0000_0000L, 10L);
        ForkJoinTask<Long> submit = forkJoinPool.submit(forkJoinTest);
        Long l = submit.get();
        System.out.println(l);
    }
}

異步回調

runAsync方法不支持返回值。

示例

public class test {
    public static void main(String[] args) throws ExecutionException, InterruptedException {
        //異步調用無返回值
        CompletableFuture<Void> completableFuture = CompletableFuture.runAsync(() -> {
            //正常打印
            System.out.println("進入異步任務");
            try {
                TimeUnit.SECONDS.sleep(3);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            //調用completableFuture.get()纔會打印
            System.out.println("延時打印");
        });
        System.out.println("主線程任務");
        //獲取阻塞執行結果
        completableFuture.get();
    }
}

supplyAsync可以支持返回值。

public class test2 {
    public static void main(String[] args) throws ExecutionException, InterruptedException {
        CompletableFuture<Integer> completableFuture = CompletableFuture.supplyAsync(() -> {
            System.out.println("進入了異步任務");
//            int i = 10 / 0;
            return 1024;
        });

        // 成功的回調
        completableFuture.whenComplete((t, u) -> {
            // t 正常的返回結果
            System.out.println("t=" + t);
            // u 錯誤的返回信息
            System.out.println("u=" + u);

        }).exceptionally((e) -> {  // 失敗回調
            System.out.println(e.getMessage());
            return 404;
        }).get();
    }
}

Java內存模型- JMM(Java Memory Model)

JMM規定了所有的變量都存儲在主內存(Main Memory)中。每個線程還有自己的工作內存(Working Memory),線程的工作內存中保存了該線程使用到的變量是主內存的副本拷貝,線程對變量的所有操作(讀取、賦值等)都必須在工作內存中進行,而不能直接讀寫主內存中的變量。不同的線程之間也無法直接訪問對方工作內存中的變量,線程之間值的傳遞都需要通過主內存來完成。

可見性:指當多個線程訪問同一個變量時,一個線程修改了這個變量的值,其他線程能夠立即看得到修改的值。

下面的示例將變量i加上volatile修飾後,新線程會跳出循環

不可見性問題:


 

示例:運行後新建的線程會一直運行,如果循環內有其他操作,線程會跳出循環,因爲i的值已經被修改?

public class test3 {
    private static int i = 0;

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

        new Thread(() -> {
            while (i == 0) {
            }
        }).start();
        TimeUnit.SECONDS.sleep(1);
        i = 1;
        System.out.println("main線程i的值:" + i);
    }
}

關於主內存與工作內存之間的具體交互協議,即一個變量如何從主內存拷貝到工作內存、如何從工作內存同步到主內存之間的實現細節,Java內存模型定義了以下八種操作來完成:

  • lock(鎖定):作用於主內存的變量,把一個變量標識爲一條線程獨佔狀態。
  • unlock(解鎖):作用於主內存變量,把一個處於鎖定狀態的變量釋放出來,釋放後的變量纔可以被其他線程鎖定。
  • read(讀取):作用於主內存變量,把一個變量值從主內存傳輸到線程的工作內存中,以便隨後的load動作使用
  • load(載入):作用於工作內存的變量,它把read操作從主內存中得到的變量值放入工作內存的變量副本中。
  • use(使用):作用於工作內存的變量,把工作內存中的一個變量值傳遞給執行引擎,每當虛擬機遇到一個需要使用變量的值的字節碼指令時將會執行這個操作。
  • assign(賦值):作用於工作內存的變量,它把一個從執行引擎接收到的值賦值給工作內存的變量,每當虛擬機遇到一個給變量賦值的字節碼指令時執行這個操作。
  • store(存儲):作用於工作內存的變量,把工作內存中的一個變量的值傳送到主內存中,以便隨後的write的操作。
  • write(寫入):作用於主內存的變量,它把store操作從工作內存中一個變量的值傳送到主內存的變量中。

Java內存模型還規定了在執行上述八種基本操作時,必須滿足如下規則:

  • 如果要把一個變量從主內存中複製到工作內存,就需要按順尋地執行read和load操作, 如果把變量從工作內存中同步回主內存中,就要按順序地執行store和write操作。但Java內存模型只要求上述操作必須按順序執行,而沒有保證必須是連續執行。
  • 不允許read和load、store和write操作之一單獨出現
  • 不允許一個線程丟棄它的最近assign的操作,即變量在工作內存中改變了之後必須同步到主內存中。
  • 不允許一個線程無原因地(沒有發生過任何assign操作)把數據從工作內存同步回主內存中。
  • 一個新的變量只能在主內存中誕生,不允許在工作內存中直接使用一個未被初始化(load或assign)的變量。即就是對一個變量實施use和store操作之前,必須先執行過了assign和load操作。
  • 一個變量在同一時刻只允許一條線程對其進行lock操作,但lock操作可以被同一條線程重複執行多次,多次執行lock後,只有執行相同次數的unlock操作,變量纔會被解鎖。lock和unlock必須成對出現
  • 如果對一個變量執行lock操作,將會清空工作內存中此變量的值,在執行引擎使用這個變量前需要重新執行load或assign操作初始化變量的值
  • 如果一個變量事先沒有被lock操作鎖定,則不允許對它執行unlock操作;也不允許去unlock一個被其他線程鎖定的變量。
  • 對一個變量執行unlock操作之前,必須先把此變量同步到主內存中(執行store和write操作)。

教程地址:https://zhuanlan.zhihu.com/p/29881777

volatile

volatile是Java提供的一種輕量級的同步機制。Java 語言包含兩種內在的同步機制:同步塊(或方法)和 volatile 變量,相比於synchronized(synchronized通常稱爲重量級鎖),volatile更輕量級,因爲它不會引起線程上下文的切換和調度。但是volatile 變量的同步性較差(有時它更簡單並且開銷更低),而且其使用也更容易出錯。

特性

1.保證可見性,不保證原子性

2.禁止指令重排

教程地址:https://blog.csdn.net/u012723673/article/details/80682208

CAS的概念

CAS,全稱Compare And Swap(比較與交換),解決多線程並行情況下使用鎖造成性能損耗的一種機制。                                                                                    CAS(V, A, B),V爲內存地址、A爲預期原值,B爲新值。如果內存地址的值與預期原值相匹配,那麼將該位置值更新爲新值。否則,說明已經被其他線程更新,處理器不做任何操作;無論哪種情況,它都會在 CAS 指令之前返回該位置的值。而我們可以使用自旋鎖,循環CAS,重新讀取該變量再嘗試再次修改該變量,也可以放棄操作。

爲什麼需要CAS機制呢?我們先從一個錯誤現象談起。我們經常使用volatile關鍵字修飾某一個變量,表明這個變量是全局共享的一個變量,同時具有了可見性和有序性。但是卻沒有原子性。比如說一個常見的操作a++。這個操作其實可以細分成三個步驟:

(1)從內存中讀取a

(2)對a進行加1操作

(3)將a的值重新寫入內存中

在單線程狀態下這個操作沒有一點問題,但是在多線程中就會出現各種各樣的問題了。因爲可能一個線程對a進行了加1操作,還沒來得及寫入內存,其他的線程就讀取了舊值。造成了線程的不安全現象。

Volatile關鍵字可以保證線程間對於共享變量的可見性可有序性,可以防止CPU的指令重排序(DCL單例),但是無法保證操作的原子性,所以jdk1.5之後引入CAS利用CPU原語保證線程操作的院子性。

CAS操作由處理器提供支持,是一種原語。原語是操作系統或計算機網絡用語範疇。是由若干條指令組成的,用於完成一定功能的一個過程,具有不可分割性,即原語的執行必須是連續的,在執行過程中不允許被中斷。如 Intel 處理器,比較並交換通過指令的 cmpxchg 系列實現。

AtomicReference

AtomicReference和AtomicInteger非常類似,不同之處就在於AtomicInteger是對整數的封裝,底層採用的是compareAndSwapInt實現CAS,比較的是數值是否相等,而AtomicReference則對應普通的對象引用,底層使用的是compareAndSwapObject實現CAS,比較的是兩個對象的地址是否相等。也就是它可以保證你在修改對象引用時的線程安全性。

順便說一下:引用類型的賦值是原子的。雖然虛擬機規範中說64位操作可以不是原子性的,可以分爲兩個32位的原子操作,但是目前商用的虛擬機幾乎都實現了64位原子操作。

public class Test6 {
    public static void main(String[] args) throws InterruptedException {
        User user1 = new User("ykk", 18);
        User user2 = new User("czz", 19);
        User user3 = new User("yxx", 2);
        AtomicReference<User> AR = new AtomicReference<>();

        AR.set(user1);
        System.out.println("初始值:" + AR.get().toString());

        AR.compareAndSet(user1, user2);
        System.out.println("如果當前值==爲預期值,則將值設置爲給定的更新值。" + AR.get().toString());

        User u = AR.getAndSet(user3);
        System.out.println("將原子設置爲給定值並返回舊值。" + u.toString());
        System.out.println("新的值:" + AR.get().toString());
        
    }

    static class User {
        private String name;
        private Integer age;

        public User(String name, Integer age) {
            this.name = name;
            this.age = age;
        }

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

}

 

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