JAVA多線程之JUC總結

1.前置知識

1.1.線程與進程

進程是一個具備獨立功能的程序的一次動態執行的過程。是操作系統進行資源分配和調度的獨立單位。

早期的操作系統是沒有線程這個概念的。進程就是擁有資源的運行的最小單位,也是程序運行的最小單位。隨着計算機的發展。進程的上下文切換開銷較大

所以發明了一個新的概念“線程”。線程是程序執行中一個單一的順序執行流程。是程序執行的最小的單位,是處理器調度的分配的基本單位。

總結:

1.線程是執行的最小單位,進程是操作系統分配資源的最小單位

2.一個進程由一個線程或者多個線程組成,線程是一個進程中代碼的不同執行路線(多線程)

3.線程的調度和切換比進程快得多

進程可以類比爲QQ.exe,而線程則是QQ的聊天功能,視頻功能的執行過程

1.2並行與併發

並行是指當系統有多個cpu的時候,多個線程可以在不同的cpu上同時執行,兩個進程互不搶佔cpu資源。可以同時進行

併發是指在一個時間段中,有多個進程都處於已啓動到運行完畢的過程中,它們搶佔cpu資源。cpu將一個時間段劃分成爲一個個的時間片(時間區間),然後在這個時間片中進行進程的來回切換執行。

1.3線程的狀態

Thread.State
 
 
 
public enum State {
    /**
     * Thread state for a thread which has not yet started.
     */
    NEW,(新建)

    /**
     * Thread state for a runnable thread.  A thread in the runnable
     * state is executing in the Java virtual machine but it may
     * be waiting for other resources from the operating system
     * such as processor.
     */
    RUNNABLE,(準備就緒)

    /**
     * Thread state for a thread blocked waiting for a monitor lock.
     * A thread in the blocked state is waiting for a monitor lock
     * to enter a synchronized block/method or
     * reenter a synchronized block/method after calling
     * {@link Object#wait() Object.wait}.
     */
    BLOCKED,(阻塞)

    /**
     * Thread state for a waiting thread.
     * A thread is in the waiting state due to calling one of the
     * following methods:
     * <ul>
     *   <li>{@link Object#wait() Object.wait} with no timeout</li>
     *   <li>{@link #join() Thread.join} with no timeout</li>
     *   <li>{@link LockSupport#park() LockSupport.park}</li>
     * </ul>
     *
     * <p>A thread in the waiting state is waiting for another thread to
     * perform a particular action.
     *
     * For example, a thread that has called <tt>Object.wait()</tt>
     * on an object is waiting for another thread to call
     * <tt>Object.notify()</tt> or <tt>Object.notifyAll()</tt> on
     * that object. A thread that has called <tt>Thread.join()</tt>
     * is waiting for a specified thread to terminate.
     */
    WAITING,(不見不散)

    /**
     * Thread state for a waiting thread with a specified waiting time.
     * A thread is in the timed waiting state due to calling one of
     * the following methods with a specified positive waiting time:
     * <ul>
     *   <li>{@link #sleep Thread.sleep}</li>
     *   <li>{@link Object#wait(long) Object.wait} with timeout</li>
     *   <li>{@link #join(long) Thread.join} with timeout</li>
     *   <li>{@link LockSupport#parkNanos LockSupport.parkNanos}</li>
     *   <li>{@link LockSupport#parkUntil LockSupport.parkUntil}</li>
     * </ul>
     */
    TIMED_WAITING,(過時不候)

    /**
     * Thread state for a terminated thread.
     * The thread has completed execution.
     */
    TERMINATED;(終結)
}
 
 

2.Lock接口

2.1多線程編程模板

進行多線程編程的時候,在高內聚低耦合的情況下,編寫線程  操作  資源類。例子代碼在下

class Ticket {/*資源類*/
    private Lock lock = new ReentrantLock();/*可重入鎖*/
    private int number = 300;

    public void sale() {//操作(對外暴露的調用方法)
        try {
            lock.lock();
            if (number > 0) {
                System.out.println(Thread.currentThread().getName() + "賣出第" + number-- + "張票,還剩下" + number + "張票");
            }
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            lock.unlock();
        }

    }

}

public class SaleTicket {
    public static void main(String[] args) {
        Ticket ticket = new Ticket();
        //線程
        new Thread(() ->{ for (int i = 0; i < 300; i++) ticket.sale(); },"A").start();
        new Thread(() ->{ for (int i = 0; i < 300; i++) ticket.sale(); },"B").start();
        new Thread(() ->{ for (int i = 0; i < 300; i++) ticket.sale(); },"C").start();
    }
}

2.2傳統Synchroinezd實現線程的通信

首先需要明白兩個概念:

1.等待池:當線程A調用某個對象的wait方法後,那麼線程A就會釋放鎖並進入該對象的等待池。在等待池中的線程是不會去競爭鎖的。

2.鎖池:當某個線程調用該對象的notifyAll()方法時,處於該對象等待池中的所有對象都會進入鎖池,鎖池中的線程會去競爭鎖。

在線程通信的時候 編寫代碼口訣  判斷 幹活 通知

多線程交互中,必須防止多線程的虛假喚醒,也即(判斷只能用while,不能用if,根本原因是if之後,不會再進行判斷)

下方的例子中如果使用if判斷,噹噹前number值是1時,喚醒線程可能會喚醒執行add方法的線程,而且add方法的線程不會再執行判斷,所以會增加爲2。導致錯誤

例子代碼實現對一個變量進行加一減一。

 

class AirConditioner{
    private int number = 0;
    public synchronized void add(){
        //判斷(滿足條件跳出循環)
        while (number != 0) {
            try {
                this.wait();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
        //幹活(執行業務邏輯)
        number ++;
        System.out.println(Thread.currentThread().getName() + number);

        //通知(喚醒其他線程)
        this.notifyAll();
    }
    public synchronized void dec(){
        //判斷
        while (number == 0) {
            try {
                this.wait();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
        //幹活
        number --;
        System.out.println(Thread.currentThread().getName() + number);

        //通知
        this.notifyAll();
    }
}
public class ThreadWaitNotifyDemo {
    public static void main(String[] args) {
        AirConditioner conditioner = new AirConditioner();
        new Thread(()->{
            for (int i = 0; i < 10; i++) {
                conditioner.add();
            }

        },"同學A").start();
        new Thread(()->{
            for (int i = 0; i < 10; i++) {
                conditioner.dec();
            }
        },"同學B").start();
        new Thread(()->{
            for (int i = 0; i < 10; i++) {
                conditioner.add();
            }

        },"同學C").start();
        new Thread(()->{
            for (int i = 0; i < 10; i++) {
                conditioner.dec();
            }
        },"同學D").start();
    }
}

2.3Lock實現線程的通信

首先看下圖

使用Lock後,synchronized中的wait和notify被await和signal方法代替了。

而下面代碼的Condition又是什麼呢?還記得剛剛講的等待池和鎖池的概念嗎。他們有異曲同工之妙,但是Condition的功能更加強大。

 

class AirConditioner2 {
    private int number = 0;
    private Lock lock = new ReentrantLock();
    private Condition condition = lock.newCondition();

    public void add() {
        lock.lock();
        try {
            //判斷
            while (number != 0) {
                condition.await();
            }
            //幹活
            number++;
            System.out.println(Thread.currentThread().getName() + number);
            //通知
            condition.signalAll();
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            lock.unlock();
        }
    }

    public void dec() {
        lock.lock();
        try {
            //判斷
            while (number == 0){
                condition.await();
            }
            //幹活
            number --;
            System.out.println(Thread.currentThread().getName() + number);
            //通知
            condition.signalAll();
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            lock.unlock();
        }
    }
}

public class ThreadWaitNotifyDemo2 {
    public static void main(String[] args) {
        AirConditioner2 conditioner = new AirConditioner2();
        new Thread(() -> {
            for (int i = 0; i < 10; i++) {
                conditioner.add();
            }

        }, "同學A").start();
        new Thread(() -> {
            for (int i = 0; i < 10; i++) {
                conditioner.dec();
            }
        }, "同學B").start();
        new Thread(() -> {
            for (int i = 0; i < 10; i++) {
                conditioner.add();
            }

        }, "同學C").start();
        new Thread(() -> {
            for (int i = 0; i < 10; i++) {
                conditioner.dec();
            }
        }, "同學D").start();
    }
}

2.4Lock實現精準喚醒線程

第一個問題:如何保證順序

第二個問題:如何保證喚醒的線程 

解決:第一個問題使用一個標誌位,定義一個變量來充當標誌位,判斷時使用該變量來判斷屬於哪一個線程

          第二個問題可以使用Condition來實現精準喚醒。condition都有自己的等待隊列。當await的時候,就將該線程放入自己的等待隊列中。當使用signal()的時候喚醒當前condition隊列中的線程

/* 多個線程之間按順序調用,實現A->B->C
* AA打印五次,BB打印十次,CC打印十五次
* 接着
* AA打印五次,BB打印十次,CC打印十五次
來十輪*/ 

class ShareResource{
    private int number = 1;//A:1 B:2 C:3 標誌位
    private Lock lock = new ReentrantLock();
    private Condition condition1 = lock.newCondition();
    private Condition condition2 = lock.newCondition();
    private Condition condition3 = lock.newCondition();
    /*設置三個condition是爲了更加精確提升效率,每個condition都有自己的等待隊列。當await的時候,就將該線程放入自己的等待隊列中。
    可以用當前condition.singal方法進行精確喚醒*/
    public void print5(){
        lock.lock();
        try {
            //判斷
            while (number != 1){
                condition1.await();
            }
            //幹活
            for (int i = 0; i < 5; i++) {
                System.out.println(Thread.currentThread().getName() + "\t" + i);
            }
            //通知
            number = 2;
            condition2.signal();
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            lock.unlock();
        }
    }

    public void print10(){
        lock.lock();
        try {
            //判斷
            while (number != 2){
                condition2.await();
            }
            //幹活
            for (int i = 0; i < 10; i++) {
                System.out.println(Thread.currentThread().getName() + "\t" + i);
            }
            //通知
            number = 3;
            condition3.signal();
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            lock.unlock();
        }
    }

    public void print15(){
        lock.lock();
        try {
            //判斷
            while (number != 3){
                condition3.await();
            }
            //幹活
            for (int i = 0; i < 15; i++) {
                System.out.println(Thread.currentThread().getName() + "\t" + i);
            }
            //通知
            number = 1;
            condition1.signal();
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            lock.unlock();
        }
    }
}
public class ZZ01ThreadOrderAccess {

    public static void main(String[] args) {
        ShareResource shareResource = new ShareResource();
        new Thread(()->{
            for (int i = 0; i <10 ; i++) {
                shareResource.print5();
            }
        },"A").start();
        new Thread(()->{
            for (int i = 0; i <10 ; i++) {
                shareResource.print15();
            }
        },"C").start();
        new Thread(()->{
            for (int i = 0; i <10 ; i++) {
                shareResource.print10();
            }
        },"B").start();

    }
}

總結
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鎖適合代碼少量的同步問題。





3.線程安全的集合類

3.1CopyOnWriteArrayList

先看看我們平時使用的ArrayList是如何在多線程環境報異常的

public static void listNotSafe1() {
        //List<String> list = Collections.synchronizedList(new ArrayList<>());
        List<String> list = new ArrayList<>();
        for (int i = 0; i < 30; i++) {
            new Thread(() -> {
                list.add(UUID.randomUUID().toString().substring(0, 8));
                System.out.println(list);
            }, String.valueOf(i)).start();
        }
    }

上述代碼執行過程中會出現java.util.ConcurrentModificationException(併發修改異常)

導致此異常的原因是什麼呢?

看看報錯信息是迭代器出現了異常

private class Itr implements Iterator<E> {
        int cursor;       //下一個要訪問元素的索引值
        int lastRet = -1; //上一個訪問元素的索引值
        int expectedModCount = modCount;//修改次數的期望值,modCount表示ArrayList被修改的次數

        Itr() {}

        public boolean hasNext() {
            return cursor != size;
        }

        @SuppressWarnings("unchecked")
        public E next() {
            checkForComodification();
            int i = cursor;
            if (i >= size)
                throw new NoSuchElementException();
            Object[] elementData = ArrayList.this.elementData;
            if (i >= elementData.length)
                throw new ConcurrentModificationException();
            cursor = i + 1;
            return (E) elementData[lastRet = i];
        }

        public void remove() {
            if (lastRet < 0)
                throw new IllegalStateException();
            checkForComodification();

            try {
                ArrayList.this.remove(lastRet);
                cursor = lastRet;
                lastRet = -1;
                expectedModCount = modCount;
            } catch (IndexOutOfBoundsException ex) {
                throw new ConcurrentModificationException();
            }
        }

        @Override
        @SuppressWarnings("unchecked")
        public void forEachRemaining(Consumer<? super E> consumer) {
            Objects.requireNonNull(consumer);
            final int size = ArrayList.this.size;
            int i = cursor;
            if (i >= size) {
                return;
            }
            final Object[] elementData = ArrayList.this.elementData;
            if (i >= elementData.length) {
                throw new ConcurrentModificationException();
            }
            while (i != size && modCount == expectedModCount) {
                consumer.accept((E) elementData[i++]);
            }
            // update once at end of iteration to reduce heap write traffic
            cursor = i;
            lastRet = i - 1;
            checkForComodification();
        }

        final void checkForComodification() {
            //當對list的次改次數不等於期望的修改次數後,就會產生這個異常,比如多線程擴容的情況下
            if (modCount != expectedModCount)
                throw new ConcurrentModificationException();
        }
    }

我們來看看線程安全的集合類

使用CopyOnWriteArrayList可以實現

public static void listNotSafe() {
        //List<String> list = Collections.synchronizedList(new ArrayList<>());
        List<String> list = new CopyOnWriteArrayList<>();
        for (int i = 0; i < 30; i++) {
            new Thread(() -> {
                list.add(UUID.randomUUID().toString().substring(0, 8));
                System.out.println(list);
            }, String.valueOf(i)).start();
        }
    }

看看CopyOnWriteArrayList是如何實現併發修改的(保證高併發的讀,和寫的時候數據一致性)。看看添加元素的源碼

CopyOnWriteArrayList是一個寫時複製的思想。當向容器中添加元素的時候,並不會向原容器中直接添加,而是複製出一個新的數組,將原有容器的元素添加到新的數組

Object[] newElements 中,並且長度爲原有容器長度+1,然後向新的容器添加元素,最後setArray(newElements),將原容器的引用指向新的容器。

這樣做的好處是,可以進行併發的讀,而不需要加鎖。只有對元素進行增刪時纔會進行加鎖操作。所以CopyOnWriteArrayList也會一種讀寫分離的思想,讀和寫都是不同的容器

3.2CopyOnWriteArraySet

說句題外話。HashSet底層是HashMap。那麼value值是什麼呢?

value值是一個Object對象。爲什麼設置爲Object,而不是null呢。null佔用空間小,而且效率更高,究竟是爲什麼呢?

我們先來看看HashSet的remove方法

public boolean remove(Object o) {
        return map.remove(o)==PRESENT;
    }

可以看到它調用的是HashMap的remove方法,來吧。看看HashMap的remove方法

public V remove(Object key) {
        Node<K,V> e;
        return (e = removeNode(hash(key), key, null, false, true)) == null ?
            null : e.value;
    }

看到這應該就明白了,當刪除某一個數據時。會返回被刪除數據的value。如果HashSet的value設置爲null的話。刪除數據永遠返回null。從而無法判斷是否刪除成功。

CopyOnWriteArraySet又是如何把保證線程安全的呢?

看看它的構造方法

public class CopyOnWriteArraySet<E> extends AbstractSet<E>
        implements java.io.Serializable {
    private static final long serialVersionUID = 5457747651344034263L;

    private final CopyOnWriteArrayList<E> al;

    /**
     * Creates an empty set.
     */
    public CopyOnWriteArraySet() {
        al = new CopyOnWriteArrayList<E>();
    }
}

可以看到,它的底層也是使用的CopyOnWriteArrayList。接下來就不用多說了吧

3.3ConcurrentHashMap

請關注後續帖子

4.Callable接口

既然有了Runnable接口爲什麼還需要Callable接口呢?

來看看吧

class MyThread implements Runnable{

    @Override
    public void run() {

    }
}
class MyThread2 implements Callable<Integer> {


    @Override
    public Integer call() throws Exception {
        System.out.println("***********come in");
        return 1024;
    }
}

從上述代碼段可以看出run方法沒有返回值,且不能拋出異常,而call方法有返回值,而且能拋出異常。是否感覺很強大呢?

那麼如何傳入Callable呢?看他的繼承樹,它和Runnable接口有沒有任何關係

我們可以先看看Runnable接口

它有一個實現類。是FutureTask。而FutureTask剛好可以傳入一個Callable。

好了。這下我們知道怎麼寫代碼了

public class ZZ04CallableDemo {
    public static void main(String[] args)
            throws ExecutionException, InterruptedException {
        MyThread2 thread2 = new MyThread2();
        FutureTask futureTask = new FutureTask(thread2);
        new Thread(futureTask,"a").start();
        System.out.println(futureTask.get());
    }
}

這下就可以傳入了。

那麼Callable究竟是爲了解決什麼樣的問題而存在呢?

重點就是返回值,可以獲取在不同的計算場景中獲取他們的返回值,成功或失敗等

以上述代碼爲例

當在主線程中執行比較耗時的任務時,又不想阻塞主線程。可以將這些任務交給Callable在後臺完成。當主線程將來需要時,就可以通過Future對象獲得後臺作業的計算結果或者執行狀態。

一般FutureTask多用於耗時的計算,主線程可以在完成自己的任務後,再去獲取結果。

重點:僅在計算完成時才能檢索結果;如果計算尚未完成,則阻塞 get 方法。一旦計算完成, 就不能再重新開始或取消計算。get方法而獲取結果只有在計算完成時獲取,否則會一直阻塞直到任務轉入完成狀態, 然後會返回結果或者拋出異常。

也就是說調用get方法時,如果get方法未執行完畢,那麼主線程也會被阻塞。所以說get方法最好是放在最後執行。

5.常用工具類

5.1CountDownLatch

計數器

例子代碼:教室裏有6個學生,必須等6個學生全部離開教室,教室門纔可以關閉

public class ZZ05CountDownLatchDemo {
    public static void main(String[] args) throws InterruptedException {
        CountDownLatch countDownLatch = new CountDownLatch(6);
        for (int i = 0; i <6 ; i++) {
            new Thread(()->{
                System.out.println(Thread.currentThread().getName()+"離開教室");
                countDownLatch.countDown();
            },String.valueOf(i)).start();
        }
        countDownLatch.await();
        System.out.println(Thread.currentThread().getName()+"班長關門走人");
    }
}

CountDownLatch主要有兩個方法:

countDown():其它線程調用countDown方法會將計數器減1(調用countDown方法的線程不會阻塞)

await():當一個或多個線程調用await方法時,這些線程會阻塞。當計數器的值變爲0時,因await方法阻塞的線程會被喚醒,繼續執行。

 5.2CyclicBarrier

可循環使用的屏障

例子代碼:當阻塞線程達到7個,纔可以召喚神龍

public class ZZ06CycliBarrierDemo {
    public static void main(String[] args) throws BrokenBarrierException, InterruptedException {
        CyclicBarrier cyclicBarrier = new CyclicBarrier(7,()->{
            {
                System.out.println("召喚神龍");
            };
        });
        for (int i = 0; i < 7; i++) {
            final int tempInt = 1;
            new Thread(()->{

                System.out.println(Thread.currentThread().getName()+"收集到第" + tempInt +  "顆龍珠");
                try {
                    cyclicBarrier.await();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                } catch (BrokenBarrierException e) {
                    e.printStackTrace();
                }
            },String.valueOf(i)).start();

        }

    }
}

 CyclicBarrier內部有一個方法

await():當前線程進入阻塞狀態(進入屏障)

只有當最後一個線程到達屏障點。匿名內部類的方法和所有被屏障攔截的線程纔會繼續執行。

5.3Semaphore

信號量

例子代碼:6輛車搶佔3個車位

public class ZZ07SemaphoreDemo {
    public static void main(String[] args) {
        Semaphore semaphore = new Semaphore(3);//模擬資源類,有三個空車位
        for (int i = 0; i < 6; i++) {
            new Thread(() -> {
                try {
                    semaphore.acquire();
                    System.out.println(Thread.currentThread().getName() + "搶佔到了車位");
                    //暫停一會兒線程
                    TimeUnit.SECONDS.sleep(3);
                    System.out.println(Thread.currentThread().getName() + "離開了車位");
                } catch (Exception e) {
                    e.printStackTrace();
                }finally {
                    semaphore.release();
                }

            }, String.valueOf(i)).start();
        }

    }
}

Semphore內部有兩個方法

acquire():獲取,當線程調用此方法時,要麼成功獲取信號量(信號量-1),要麼一直阻塞,直到有線程釋放信號量或者超時

release():釋放,當線程調用此方法時,會釋放信號量(信號量+1)。然後喚醒阻塞的線程。

Semphore的使用主要有兩個目的,一個是設置多線程的互斥使用,一個是控制併發量

6.ReadWriteLock

在傳統的高併發情況下,不管讀和寫都會加鎖。其他線程只能等到獲取到鎖後才能去執行自己的操作。這樣的話如果只是讀取操作,也會導致效率變慢

所以誕生了讀寫鎖。只有讀操作時爲了滿足高併發只是加共享鎖,此時讀操作允許多個線程同時訪問。當某一個線程拿到寫鎖時,此時會加排它鎖。除此線程外所有線程都不能讀寫

總而言之就是  讀-讀 可以共存  讀-寫 不可以共存  寫-寫 不可以共存

class Cache {
    private volatile Map<String, Object> map = new HashMap<>();
    private ReadWriteLock readWriteLock = new ReentrantReadWriteLock();

    public void put(String key, Object value) {
        readWriteLock.writeLock().lock();
        System.out.println(Thread.currentThread().getName() + "寫入數據"+ value );
        try {
            TimeUnit.MICROSECONDS.sleep(300);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        map.put(key, value);
        System.out.println(Thread.currentThread().getName() + "寫入完成");
        readWriteLock.writeLock().unlock();
    }

    public void get(String key) {
        readWriteLock.readLock().lock();
        System.out.println(Thread.currentThread().getName() + "讀取了數據");
        try {
            TimeUnit.MICROSECONDS.sleep(300);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        Object o = map.get(key);
        System.out.println(Thread.currentThread().getName() + "讀取完成" + o);
        readWriteLock.readLock().unlock();
    }
}

public class ZZ08ReadOnWriteLockDemo {
    public static void main(String[] args) {
        Cache cache = new Cache();
        for (int i = 0; i < 5; i++) {
            final int tempInt = i;
            new Thread(() -> {
                cache.put(tempInt+"",tempInt + "");

            }, String.valueOf(i)).start();
        }
        for (int i = 0; i < 5; i++) {
            final int tempInt = i;
            new Thread(() -> {
                cache.get(tempInt+"");
            }, String.valueOf(i)).start();
        }
    }
}

底層源碼請關注後續AQS解讀

7.BlockingQueue(阻塞隊列)

阻塞隊列是用於在高併發情況下不得不阻塞線程的時候,如何管理線程和資源的一種手段

 

Thread1向阻塞隊列中添加元素,Thread2從隊列中取走元素

當隊列是空的,從對列中取出元素的操作將會被阻塞

當隊列是滿的,向對列中添加元素的操作將會被阻塞

試圖從空的隊列中獲取元素的線程將會被阻塞,直到其他線程往空的隊列插入新的元素

試圖向已滿的隊列中添加新元素的線程將會被阻塞,直到其他線程從隊列中移除一個或多個元素或者完全清空,使隊列變得空閒起來並後續新增

 

阻塞隊列的優勢:

不需要關心什麼時候需要阻塞線程,什麼時候需要喚醒線程,因爲一切都由BlockingQueue包辦了。

首先看看阻塞隊列的繼承樹

 

ArrayBlockQuque:由數組結構組成的有界的阻塞隊列

LinkedBlockingQueue:由鏈表結構組成的有界(界限爲Integer.MAX_VALUE)的阻塞隊列

SynchronousQueue:不存儲元素的阻塞隊列。也就是單個元素的隊列

LinkedBlockingDeque:由鏈表結構組成的有界(界限爲Integer.MAX_VALUE)的雙向阻塞隊列

具體的方法見下

public class ZZ09BlockingQueueDemo {

    public static void main(String[] args) throws InterruptedException {
        BlockingQueue<String> blockingQueue= new ArrayBlockingQueue<>(3);
        /*此組add方法與remove,超出隊列界限會拋出異常
        System.out.println(blockingQueue.add("a"));
        System.out.println(blockingQueue.add("b"));
        System.out.println(blockingQueue.add("c"));
        blockingQueue.remove();
        blockingQueue.remove();
        blockingQueue.remove();
        blockingQueue.remove();
        System.out.println(blockingQueue.add("d"));*/
        /*System.out.println(blockingQueue.add("a"));
        System.out.println(blockingQueue.add("b"));
        System.out.println(blockingQueue.element());//返回隊列第一個元素*/

        /*此組offer與poll方法超出隊列界限不會拋出異常只返回false和null
        System.out.println(blockingQueue.offer("a"));
        System.out.println(blockingQueue.offer("b"));
        System.out.println(blockingQueue.offer("c"));
        System.out.println(blockingQueue.offer("x"));
        System.out.println(blockingQueue.poll());
        System.out.println(blockingQueue.poll());
        System.out.println(blockingQueue.poll());
        System.out.println(blockingQueue.poll());*/

        /*put,當隊列沒有滿,可以往裏添加,滿了後續的put操作會被阻塞*/
       /*blockingQueue.put("a");
        blockingQueue.put("b");
        blockingQueue.put("c");
        blockingQueue.put("d");
        System.out.println(blockingQueue.take());

        System.out.println(blockingQueue.take());
        System.out.println(blockingQueue.take());
        System.out.println(blockingQueue.take());*/

       /* System.out.println(blockingQueue.offer("a"));
        System.out.println(blockingQueue.offer("a"));
        System.out.println(blockingQueue.offer("a"));
        System.out.println(blockingQueue.offer("b", 3L, TimeUnit.SECONDS));*/
    }
}

 8.線程池

什麼是線程池?

  線程池是一種多線程處理任務的一種方式。處理過程中將任務添加到隊列(也就是我們上一講的阻塞隊列中),然後這些線程自動啓動這些任務

線程池的主要工作就是控制運行的線程數量,處理過程中將任務放入阻塞隊列,然後在線程創建後去執行這些任務,如果線程數量超過了最大數量,超出數量的線程排隊等候,等待其他線程執行完畢,再從隊列中取出任務來執行

主要特點:

  線程複用

  控制最大併發數

  管理線程。

優勢:

  1.重複利用已經創建的線程,降低線程創建和銷燬所帶來的開銷

  2.任務可以不需要等待線程創建就可以執行,從而提高響應速度

  3.提高線程的可管理性,使用線程池可以進行集中的監控,分配和調優。

看看Java中線程池的繼承樹

Executors類似於Collections,它作爲線程池的工具類爲我們提供了很多強大的功能

例子代碼如下:

public class ZZ11MyThreadPoolDemo {
    public static void main(String[] args) {
        /*
        Executors 輔助工具類,類似於collections
        * */
       /* ExecutorService threadPool = Executors.newFixedThreadPool(5);//一池五個線程,類似於銀行有五個受理窗口
        ExecutorService threadPool = Executors.newSingleThreadExecutor();*///一個池子一個線程,類似於銀行有一個受理窗口
        //一池子N個工作線程,類似於銀行有N個受理窗口。相當於前兩個線程池的結合體,
        //執行很多任務時可以使用,線程池根據任務需要進行創建線程,但是在先前的線程可用的時候重用他們,在不可用的時候新建線程(擴容)
        //ExecutorService threadPool = Executors.newCachedThreadPool();

        //自定義線程池
        ExecutorService threadPool = new ThreadPoolExecutor
                (2, 5, 2L, TimeUnit.SECONDS,
                        new LinkedBlockingQueue<>(3),
                        Executors.defaultThreadFactory(),
                        new ThreadPoolExecutor.DiscardOldestPolicy());
        try {

            /*模擬十個顧客過來辦理業務,目前池子只有五個工作人員提供服務*/
            for (int i = 0; i < 9; i++) {
                threadPool.execute(() -> {
                    System.out.println(Thread.currentThread().getName() + "辦理業務");

                });
            }
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            threadPool.shutdown();
        }
    }
}

我們看看它們是如何創建線程池的

public static ExecutorService newFixedThreadPool(int nThreads) {
        return new ThreadPoolExecutor(nThreads, nThreads,
                                      0L, TimeUnit.MILLISECONDS,
                                      new LinkedBlockingQueue<Runnable>());
    }
public ThreadPoolExecutor(int corePoolSize,
                              int maximumPoolSize,
                              long keepAliveTime,
                              TimeUnit unit,
                              BlockingQueue<Runnable> workQueue) {
        this(corePoolSize, maximumPoolSize, keepAliveTime, unit, workQueue,
             Executors.defaultThreadFactory(), defaultHandler);
    }

 可以看到在源碼中,ThreadPoolExecutor中有七個參數

int corePoolSize 常駐核心線程數

int maximumPoolSize 線程池中能容納同時執行的最大線程數

long keepAliveTime 多餘空閒線程的存活時間,如果達到,線程會被銷燬

TimeUnit unit  keepAliveTime的單位

BlockingQueue<Runnable> workQueue 任務隊列,被提交但是尚未被執行的任務

ThreadFactory threadFactory 創建線程的工廠

RejectedExecutionHandler handler 拒絕策略,當隊列滿了,並且工作線程大於等於線程池的最大線程數時如何來拒絕請求執行的runnable的策略

 

 

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