JUC併發編程(二)

讀寫鎖

寫鎖:也叫獨佔鎖,一次只能被一個線程佔有。
讀鎖:也叫共享鎖,該鎖可以被多個線程佔有。
ReadWriteLock,即讀寫鎖,正如它的名字一樣,它包含了讀鎖和寫鎖,一個用於只讀操作,一個用於寫入操作,我們先來看看JDK文檔中對它的說明。

創建一個讀寫鎖對象:

ReadWriteLock readWriteLock = new ReentrantReadWriteLock();

加讀鎖和解讀鎖:

readWriteLock.readLock().lock(); 
readWriteLock.readLock().unlock();

加寫鎖和解寫鎖:

readWriteLock.writeLock().lock(); 
readWriteLock.writeLock().unlock();

數據讀寫時可以使用讀寫鎖來保證線程安全,示例代碼如下:

package com.wunian.juc.rwlock;

import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.locks.ReadWriteLock;
import java.util.concurrent.locks.ReentrantReadWriteLock;

/**
 * 獨佔鎖(寫鎖)一次只能被一個線程佔有
 * 共享鎖(讀鎖)一個鎖可以被多個線程佔有
 */
public class ReadWriteLockDemo {
    public static void main(String[] args) {
        MyCacheLock myCache=new MyCacheLock();
        //模擬線程
        //寫
        for (int i=1;i<=5;i++){
            final int tempInt=i;
            new Thread(()->{
                myCache.put(tempInt+"",tempInt+"");
            },String.valueOf(i)).start();
        }
        //讀
        for (int i=1;i<=5;i++){
            final int tempInt=i;
            new Thread(()->{
                myCache.get(tempInt+"");
            },String.valueOf(i)).start();
        }
    }
}

//加鎖後的讀寫操作
class MyCacheLock{

    private volatile Map<String,Object> map=new HashMap<>();

    //讀寫鎖
    private ReadWriteLock readWriteLock=new ReentrantReadWriteLock();

    //讀:可以被多個線程同時讀
    public void get(String key){
        //鎖一定要匹配,否則可能導致死鎖
        readWriteLock.readLock().lock();//讀鎖,被多個線程同時持有
        try {
            System.out.println(Thread.currentThread().getName()+"讀取"+key);
            Object o=map.get(key);
            System.out.println(Thread.currentThread().getName()+"讀取結果"+o);
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            readWriteLock.readLock().unlock();//解鎖
        }
    }

    //寫:應該保證原子性,不應該被打擾,寫線程寫入的過程中如果不加鎖,會被讀線程打擾
    public void put(String key,Object value){
        readWriteLock.writeLock().lock();//寫鎖,只能被一個線程佔有
        try {
            System.out.println(Thread.currentThread().getName()+"寫入"+key);
            map.put(key,value);
            System.out.println(Thread.currentThread().getName()+"寫入成功");
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            readWriteLock.writeLock().unlock();//解鎖
        }
    }
}

阻塞隊列

隊列:是一種先進先出的數據結構。
棧:是一種後進先出的數據結構。
阻塞隊列是一種隊列,我們先來看看JDK文檔中對它的說明。
在這裏插入圖片描述
阻塞隊列在什麼情況下一定會被阻塞?

  • 當隊列是滿的,如果還要往它裏面添加元素就會被阻塞。
  • 當隊列是空的,如果還要取它裏面的元素就會被阻塞。

什麼時候使用阻塞隊列?
當編寫多線程程序時,對於線程之間的通信,不需要關心喚醒的情況下可以使用阻塞隊列。
阻塞隊列是新知識嗎?
List、Set這些集合類我們都學過,阻塞隊列和它們是一樣的,我們可以來看一張集合類的關係圖。
在這裏插入圖片描述
由上圖可知,BlockingQueue是Queue的子類,而Queue與List、Set一樣都是Collection類的子類。
ArrayBlockingQueue是BlockingQueue的子類,它含有四組對元素的插入和獲取的API,我們可以用表格來對比一下。

方法 會拋出異常 返回布爾值,不會拋出異常 延時等待 一直等待
插入 add() offer(e) offer(e,time) put()
取出 remove() poll() poll(time) take()
檢查 element() peek() - -

四組API的示例代碼如下:

package com.wunian.juc.queue;

import com.sun.scenario.effect.impl.sw.java.JSWBlend_SRC_OUTPeer;

import java.sql.Time;
import java.util.concurrent.ArrayBlockingQueue;
import java.util.concurrent.TimeUnit;

/**
 * 阻塞隊列
 */
public class BlockingQueueDemo {

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

        ArrayBlockingQueue blockingQueue=new ArrayBlockingQueue(3);
        //隊列已滿出現的四種情況:報錯、拋棄不報錯、一直等待、超時等待!
        //java.lang.IllegalStateException: Queue full
        blockingQueue.add("a");
        blockingQueue.add("b");
        blockingQueue.add("c");
        //blockingQueue.add("d");//會拋出隊列已滿的異常
        System.out.println(blockingQueue.element());//檢測第一個元素,輸出

//        System.out.println(blockingQueue.remove());
//        System.out.println(blockingQueue.remove());
//        System.out.println(blockingQueue.remove());

        //返回布爾值,不拋出異常
//        System.out.println(blockingQueue.offer("a"));//true
//        System.out.println(blockingQueue.offer("b"));//true
//        System.out.println(blockingQueue.offer("c"));//true
//        //嘗試等待三秒,三秒鐘後會失敗,返回false
//        //System.out.println(blockingQueue.offer("d", 3L,TimeUnit.SECONDS));
//        System.out.println(blockingQueue.peek());//檢測第一個元素,輸出
//
//        System.out.println("================================");
//        System.out.println(blockingQueue.poll());
//        System.out.println(blockingQueue.poll());
//        System.out.println(blockingQueue.poll());
//        //雖然是空的,還是會等待3秒,然後返回null
//        System.out.println(blockingQueue.poll(3L, TimeUnit.SECONDS));

//        blockingQueue.put("a");
//        blockingQueue.put("b");
//        blockingQueue.put("c");
//        //System.out.println("準備放入第四個元素");
//        //blockingQueue.put("d");//隊列滿了會一直等,並且會阻塞
//
//        System.out.println("===============================");
//        System.out.println(blockingQueue.take());
//        System.out.println(blockingQueue.take());
//        System.out.println(blockingQueue.take());
//        System.out.println(blockingQueue.take());//隊列空了會一直等,並且阻塞
    }
}

同步隊列

SynchronousQueue,即同步隊列,是一種特殊的阻塞隊列,因爲它只有一個容量,並且每進行一個put操作,就需要有一個take操作。
示例代碼如下:

package com.wunian.juc.queue;

import java.util.concurrent.BlockingQueue;
import java.util.concurrent.SynchronousQueue;
import java.util.concurrent.ThreadLocalRandom;
import java.util.concurrent.TimeUnit;

/**
 * 同步隊列
 * 只能存放一個值,一存一取(存一個必須取一個才能繼續存)
 */
public class SynchronuseQueueDemo {

    public static void main(String[] args) {
        //特殊的阻塞隊列
        BlockingQueue<String> blockingQueue=new SynchronousQueue<>();
        //存
        new Thread(()->{
            try {
                System.out.println(Thread.currentThread().getName()+" put  a");
                blockingQueue.put("a");
                System.out.println(Thread.currentThread().getName()+" put  b");
                blockingQueue.put("b");
                System.out.println(Thread.currentThread().getName()+" put  c");
                blockingQueue.put("c");
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        },"A").start();
        //取
        new Thread(()->{
            try {
                TimeUnit.SECONDS.sleep(3);
                System.out.println(Thread.currentThread().getName()+" "+blockingQueue.take());
                TimeUnit.SECONDS.sleep(3);
                System.out.println(Thread.currentThread().getName()+" "+blockingQueue.take());
                TimeUnit.SECONDS.sleep(3);
                System.out.println(Thread.currentThread().getName()+" "+blockingQueue.take());
            } catch (InterruptedException e) {
                e.printStackTrace();
            }

        },"A").start();
    }
}

線程池

線程池是一種多線程處理形式,處理過程中將任務添加到隊列,然後在創建線程後自動啓動這些任務。
爲什麼要使用線程池?
實現線程複用,線程複用可以提高線程的使用效率,保證內核的充分利用,防止過分調度。
線程池的三大方法
創建只有一個線程的線程池:

ExecutorService pool= Executors.newSingleThreadExecutor();

創建固定線程數的線程池:

ExecutorService  pool=Executors.newFixedThreadPool(3);

創建可伸縮的線程池:

ExecutorService pool=Executors.newCachedThreadPool();

線程池的七大參數
我們先看看上面三大方法的源碼:

//可伸縮的線程池
new ThreadPoolExecutor(0, Integer.MAX_VALUE, // 約等於21億
                                      60L, TimeUnit.SECONDS,
                                      new SynchronousQueue<Runnable>());
//固定線程數的線程池
new ThreadPoolExecutor(nThreads, nThreads,
                                      0L, TimeUnit.MILLISECONDS,
                                      new LinkedBlockingQueue<Runnable>());
//單線程的線程池
new ThreadPoolExecutor(1, 1, 0L, TimeUnit.MILLISECONDS,
                                    new LinkedBlockingQueue<Runnable>())

由以上代碼可以知道,三大方法的底層還是創建的ThreadPoolExecutor對象,這也就是爲什麼阿里巴巴開發手冊要求不能使用三大方法,而必須直接通過創建ThreadPoolExecutor對象來創建線程池的原因。
我們再來看看定義了七大參數的構造器源碼:

public ThreadPoolExecutor(int corePoolSize, // 核心池線程數大小 (常用)
                              int maximumPoolSize,  // 最大的線程數大小 (常用)
                              // 超時等待時間,超過一定時間會把核心線程數以外的閒置線程關閉(常用)
                              long keepAliveTime, 
                              TimeUnit unit, // 時間單位 (常用)
                              BlockingQueue<Runnable> workQueue, // 阻塞隊列(常用)
                              ThreadFactory threadFactory, // 線程工廠
                              RejectedExecutionHandler handler // 拒絕策略(常用)) {
    if (corePoolSize < 0 ||
        maximumPoolSize <= 0 ||
        maximumPoolSize < corePoolSize ||
        keepAliveTime < 0)
        throw new IllegalArgumentException();
    if (workQueue == null || threadFactory == null || handler == null)
        throw new NullPointerException();
    this.acc = System.getSecurityManager() == null ?
        null :
    AccessController.getContext();
    this.corePoolSize = corePoolSize;
    this.maximumPoolSize = maximumPoolSize;
    this.workQueue = workQueue;
    this.keepAliveTime = unit.toNanos(keepAliveTime);
    this.threadFactory = threadFactory;
    this.handler = handler;
}

由以上代碼可知,七大參數爲:

  • int corePoolSize:核心池線程數,即線程池固定開啓的線程數量。
  • int maximumPoolSize:最大線程數,當核心池線程全部都在處理任務,阻塞隊列中的任務也已經滿了,一旦增加了新任務,線程池會開啓一個新的線程來處理,開啓的線程數量最多爲最大線程數,超過這個數量,將執行拒絕策略。
  • long keepAliveTime:超時等待時間,超過這個時間會將閒置的線程關閉,最後只保留核心池線程數量的線程。
  • TimeUnit unit:keepAliveTime參數的時間單位。
  • BlockingQueue< Runnable > workQueue:阻塞隊列,用來存放等待線程執行的任務。
  • ThreadFactory threadFactory:線程工廠,默認值:Executors.defaultThreadFactory()
  • RejectedExecutionHandler handler:拒絕策略,當線程池中執行的線程數量達到最大線程數,新增的任務將根據拒絕策略進行處理。

除了ThreadFactory這個參數使用的是系統默認的線程工廠外,其它六個參數都是必須掌握的。
四大拒絕策略
七大參數中的RejectedExecutionHandler參數,指的就是拒絕策略,源碼中爲我們提供了四大拒絕策略。
在這裏插入圖片描述

  • ThreadPoolExecutor.AbortPolicy(): 拋出異常,丟棄任務。
  • ThreadPoolExecutor.DiscardPolicy():不拋出異常,丟棄任務。
  • ThreadPoolExecutor.DiscardOldestPolicy(): 嘗試獲取任務,不一定執行。
  • ThreadPoolExecutor.CallerRunsPolicy():哪來的去哪裏找對應的線程執行。

最大線程數應該如何設置?

  • CPU密集型:根據CPU的處理器數量來決定,這樣能夠保證最大效率。
    獲取電腦CPU核數:Runtime.getRuntime().availableProcessors();
  • IO密集型:例如有50個線程都是進程操作大IO資源,比較耗時,這時就要考慮最大線程數一定要大於這個常用IO的任務數,即最大線程數要大於50。
    最終代碼如下:
package com.wunian.juc.threadpool;

import java.util.concurrent.*;

/**
 * 線程池
 */
public class ThreadPoolDemo {

    public static void main(String[] args) {
        ExecutorService pool=new ThreadPoolExecutor(
                2,//核心線程數大小(常用)
                //最大線程數大小(常用)
                Runtime.getRuntime().availableProcessors(),//獲取電腦CPU核數
                //超時等待時間(常用,超過一定時間會把核心線程數以外的閒置線程關閉)
                3L,
                //時間單位(常用)
                TimeUnit.SECONDS,
                //阻塞隊列(常用)
                new LinkedBlockingDeque<>(3),
                //線程工廠
                Executors.defaultThreadFactory(),
                //拒絕策略(常用,4種)
                new ThreadPoolExecutor.CallerRunsPolicy()
        );

        try {
            //線程池的使用方式
            for(int i=1;i<=100;i++){
                pool.execute(()->{
                    System.out.println(Thread.currentThread().getName()+" ok");

                });
            }
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            //使用完畢後需要關閉!
            pool.shutdown();
        }
    }
}

四個函數式接口

函數式接口在java.util.function包下面,所有的函數式接口都可以用來簡化編程模型,都可以使用lambda表達式簡化。
必須掌握的四個函數式接口

  • Function:有一個輸入參數,有一個輸出參數。
  • Consumer:有一個輸入參數,沒有輸出參數。
  • Supplier:沒有輸入參數,只有輸出參數。
  • Predicate:有一個輸入參數,判斷是否正確

lambda表達式語法格式可以簡單概括爲:(參數)->{方法體},示例代碼如下:

package com.wunian.juc.function;

import java.util.function.Consumer;
import java.util.function.Function;
import java.util.function.Predicate;
import java.util.function.Supplier;

/**
 * 函數式接口是現在必須掌握且精通的
 *所有的函數式接口都可以使用lambda表達式簡化
 * lambda表達式是java8必須掌握的
 *
 */
public class FunctionInterfaceDemo {

    public static void main(String[] args) {

        /*Function<String,Integer> function=new Function<String,Integer>(){

            @Override
            public Integer apply(String s) {
                return s.length();
            }
        };*/

        //Function  lambda表達式格式 (參數)->{方法體}
        Function<String,Integer> function=(str)->{return str.length();};
        System.out.println(function.apply("1234565"));

        /*Predicate<String> predicate=new Predicate<String>() {
            @Override
            public boolean test(String s) {
                return s.isEmpty();
            }
        };*/

        //Predicate lambda表達式格式 (參數)->{方法體}
        Predicate<String> predicate=str->{return str.isEmpty();};
        System.out.println(predicate.test("qqq"));

        /*Supplier<String> supplier=new Supplier<String>() {
            @Override
            public String get() {
                return "hello word";
            }
        };*/

        //Supplier lambda表達式格式 (參數)->{方法體}
        Supplier<String> supplier=()->{return "hello juc";};
        System.out.println(supplier.get());

        /*Consumer<String> consumer=new Consumer<String>() {
            @Override
            public void accept(String s) {

                System.out.println(s);

            }
        };*/

        //Consumer lambda表達式格式 (參數)->{方法體}
        Consumer<String> consumer=(s)->{
            System.out.println(s);
        };
        consumer.accept("hello consumer");
    }
}

Stream流式計算

數據庫和集合都是用來存數據的,我們可以把計算和處理數據交給Stream。先來看看JDK文檔中對它的說明。
在這裏插入圖片描述
比如現在有一個集合,需要按條件用戶篩選以下條件:
1、id 爲偶數
2、年齡大於24
3、用戶名大寫 映射
4、用戶名倒排序
5、輸出一個用戶
並且只能用一行代碼完成!
使用Stream流式計算,這個問題很好處理,代碼如下:

package com.wunian.juc.stream;

import java.util.Arrays;
import java.util.List;

/**
 * 流式計算
 * 數據庫、集合:存數據的
 *計算和處理數據交給Stream
 */
public class StreamDemo {
    public static void main(String[] args) {
        User u1=new User(1,"a",23);
        User u2=new User(2,"b",20);
        User u3=new User(3,"c",26);
        User u4=new User(4,"d",30);
        User u5 =new User(5,"e",21);
        //存儲
        List<User> users= Arrays.asList(u1,u2,u3,u4,u5);
        //計算等操作交給流
        //forEach(消費者類型接口)
        users.stream()
                .filter(u->{return u.getId()%2==0;})
                .filter(u->{return u.getAge()>24;})
                .map(u->{return u.getName().toUpperCase();})
                .sorted((o1,o2)->{return o2.compareTo(o1);})
                .limit(1)
                .forEach(System.out::println);
    }
}

ForkJoin分支合併

ForkJoin採用了分治算法思想,在必要的情況下,將一個大任務,進行拆分(fork) 成若干個子任務(拆到不能再拆,這裏就是指我們制定的拆分的臨界值),再將一個個小任務的結果進行join彙總。
MapReduce:input->split->map->reduce->output
主要就是兩步:
1、任務拆分
2、結果合併
在這裏插入圖片描述
前提
使用ForkJoin的前提是在大數據量的情況下,如果數據量很小,ForkJoin的效率還不如不用來的快。
ForkJoin的工作原理
假如兩個CPU上有不同的任務,這時候B已經執行完,A還有任務等待執行,這時候B就會將A隊尾的任務偷過來,加入自己的隊列中,這叫做工作竊取,ForkJoin的底層維護的是一個雙端隊列
好處:處理效率高。
壞處:可能產生資源爭奪。
在這裏插入圖片描述
我們可以使用求和計算測試一下ForkJoin,這就需要用到RecursiveTask類了,先來看看它的JDK文檔。
在這裏插入圖片描述
先來創建一個類繼承RecursiveTask,重寫其compute方法,代碼如下:

package com.wunian.juc.forkjoin;

import java.util.concurrent.ForkJoinTask;
import java.util.concurrent.RecursiveTask;

/**
 * 求和計算
 */
public class ForkJoinDemo  extends RecursiveTask<Long> {

    private Long start;
    private Long end;
    private static final Long tempLong=10000L;//臨界值,只要超過了這個值,ForkJoin效率就會更高

    public ForkJoinDemo(Long start, Long end) {
        this.start = start;
        this.end = end;
    }
    //計算方法
    @Override
    protected Long compute() {
        if((end-start)<=tempLong){
            Long sum=0L;
            for(Long i=start;i<=end;i++){
                sum+=i;
            }
            return sum;
        }else{//超過臨界值,用第二種方式
            long middle=(end+start)/2;
            //ForkJoin實際上是通過遞歸來實現
            ForkJoinDemo right=new ForkJoinDemo(start,middle);
            right.fork();//壓入線程隊列
            ForkJoinDemo left=new ForkJoinDemo(middle+1,end);
            left.fork();//壓入線程隊列
            //獲得結果join,會阻塞等待結果
            return right.join()+left.join();
        }
    }
}

創建一個測試類,分別測試普通方法、ForkJoin方法、並行流計算方法的計算效率,代碼如下:

package com.wunian.juc.forkjoin;

import java.util.concurrent.ExecutionException;
import java.util.concurrent.ForkJoinPool;
import java.util.concurrent.ForkJoinTask;
import java.util.stream.LongStream;

/**
 * 測試forkjoin
 */
public class MyTest {

    public static void main(String[] args) throws ExecutionException, InterruptedException {
        //test1();//6345
        //test2();//13476
        test3();//902
    }

    //普通方法
    public  static void test1() {
        long sum=0L;
        long start=System.currentTimeMillis();
        for(Long i=0L;i<=10_0000_0000L;i++){
            sum+=i;
        }
        long end=System.currentTimeMillis();
        System.out.println("times:"+(end-start)+" rs=>:"+sum);
    }

    //forkjoin(計算大數據量時纔有效果,數據較小時可能還不如普通方法快)
    public  static void test2() throws ExecutionException, InterruptedException {
        long start=System.currentTimeMillis();
        ForkJoinPool forkJoinPool=new ForkJoinPool();
        ForkJoinDemo forkJoinWork=new ForkJoinDemo(0L,10_0000_0000L);
        ForkJoinTask<Long> submit=forkJoinPool.submit(forkJoinWork);
        Long sum=submit.get();
        long end=System.currentTimeMillis();
        System.out.println("times:"+(end-start)+" rs=>:"+sum);
    }

    //並行流計算
    public  static void test3() {
        long sum=0L;
        long start=System.currentTimeMillis();  
 sum=LongStream.rangeClosed(0L,10_0000_0000L).parallel().reduce(0,Long::sum);//並行計算

        long end=System.currentTimeMillis();
        System.out.println("times:"+(end-start)+" rs=>:"+sum);
    }
}

最後的測試結果表明,在計算數據量較大時,並行流計算方法是最快的,ForkJoin方法次之,普通方法最慢。在計算數據量很小時,ForkJoin方法和並行流計算方法反而比普通方法慢,再次驗證了使用ForkJoin的前提是在大數據量的情況下的這個結論。

異步回調

以往我們常常會使用callable來進行異步調用,但是callable沒有返回值。與之相比,Future可以有返回值,也可以沒有返回值。我們可以使用Future的子類completableFuture來測試一下,代碼如下:

package com.wunian.juc.future;

import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.TimeUnit;

/**
 * 異步回調  callable沒有返回值,使用Future
 *
 */
public class CompletableFutureDemo {

    public static void main(String[] args) throws ExecutionException, InterruptedException {
        //沒有返回值
       /* CompletableFuture<Void> completableFuture=CompletableFuture.runAsync(()->{
            try {
                TimeUnit.SECONDS.sleep(2);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println(Thread.currentThread().getName()+"沒有返回值!");
        });
        System.out.println("111111");
        completableFuture.get();*/

       //有返回值
        CompletableFuture<Integer> completableFuture=CompletableFuture.supplyAsync(()->{
            System.out.println(Thread.currentThread().getName()+"=>supply Async!");
            int i=10/0;
            return  1024;
        });
        System.out.println(completableFuture.whenComplete((t,u)->{
            System.out.println("t=>"+t);//正確結果
            System.out.println("u=>"+u);//錯誤信息
        }).exceptionally(e->{//失敗,如果錯誤就返回錯誤的結果
            System.out.println("e:"+e.getMessage());
            return 500;
        }).get());
    }
}

由以上代碼我們可以知道,CompletableFuture不但有返回值,並且當異步調用過程中如果出現異常,連錯誤信息也會返回,這樣就可以很方便的做一些異常處理,顯然這點要比callable強大很多。

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