JDK併發包-(下).md

三.併發工具
從以下四個方面來學習:執行器,鎖與原子操作,流編程,Fork/Join框架;
3.1.執行器
從兩個方面來學習,首先學習執行器的基本概念,其次學習與執行器相關的接口Callable和Future。
(1)用於啓動並控制線程的執行
(2)核心接口爲Executor,包含一個execute(Runnable)用於指定被執行的線程
(3)ExecutorService接口用於控制線程和管理線程
(4)預定義瞭如下執行器:ThreadPoolExecutor/ScheduledThreadPoolExecutor/ForkJoinPool
(5)Callable<V>:表示具有返回值的線程
(6)V:表示返回值類型
(7)call():執行任務
(8)Future<V>:表示Callable的返回值
(9)V:返回值類型
(10)get():獲取返回值
import java.util.concurrent.Callable;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;
 
public class ExecDemo {
public static void main(String[] args) throws Exception{
ExecutorService es = Executors.newFixedThreadPool(2);//調用newFixedThreadPool()方法:該方法返回一個固定線程數量的線程池。該線程池中的線程數量始終不變。當有一個新的任務提交時,線程池若有空閒線程,則立即執行。若沒有,則新的任務會被暫存在一個任務隊列中,待有線程空閒時,便處理在任務隊列中的任務;
Future<Integer> r1 = es.submit(new MC(1,100));//Future<Integer>意思是返回值類型爲Integer;調用ExecutorService中的submit();
Future<Integer> r2 = es.submit(new MC(100,10000));
System.out.println(r1.get() +":"+r2.get());//在java語言中,需要有連接字符;
es.shutdown();//調用ExecutorService中的shutdown();
}
}
 
 
class MC implements Callable<Integer>{//MC這個類實現了Callable接口,並且確定返回值的類型;
private int begin, end;
public MC(int begin,int end){//創建構造函數,傳遞兩個私有變量;
this.begin= begin;//調用本類的中的變量;
this.end = end;
}
public Integer call() throws Exception{//利用call()方法來計算從begin到end方法的和;
int sum = 0;
for(int i= begin;i <end;i++)
sum+=i;
return sum;
}
}
3.2.鎖與原子操作
鎖具有以下基本的用途:
原子操作:
import java.util.concurrent.atomic.AtomicInteger;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
 
public class LockDemo {
public static void main(String[] args){
new MT().start();
new MT().start();
new MT().start();
}
}
 
class Data{
static int i = 0;
//爲了使其i++和打印輸出i的值爲原子操作,而使用一些方法來代替加鎖操作;
static Lock lock = new ReentrantLock();
static AtomicInteger ai = new AtomicInteger(0);//採用原子操作的方法,不需要再進行加鎖和解鎖;
//重入鎖的性能遠遠好於synchronized,使用重入鎖保護臨界資源i,確保多線程對i操作的安全性;
//重入鎖可以手動加鎖,解鎖,這就說明重入鎖的邏輯控制遠遠好於synchronized。
//static synchronized void operate(){}
static void operate(){
System.out.println(ai.incrementAndGet());//獲取原子操作,來代替lock鎖機制;
//lock.lock();//申請鎖;
//i++;//原子性是指一個操作是不可中斷的。即使是在多個線程一起執行的時候,一個操作一旦開始,就不會被其他線程干擾;此時爲了保證操作不被中斷而採用一些加鎖的機制來實現;
//System.out.println(i);
//lock.unlock();//釋放鎖;
}
}
 
 
class MT extends Thread{
public void run(){
while(true){
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
Data.operate();
}
}
}
3.3.流編程
嚴格來說,流編程其實不屬於java併發工具包中的內容,但是在java併發編程中,或多或少的會利用到流編程的一些內容。
流的基本知識,編程模型,基本操作;
流的基本知識
流的編程模型
import java.awt.List;
import java.util.ArrayList;
//import java.util.Optional;
 
public class StreamDemo {
public static void main(String[] args){
//流是對一組數據來進行操作的;數據通常以數組和集合形式出現;
List<String> ls = new ArrayList<>();
ls.add("abc");
ls.add("def");
ls.add("ghi");
ls.add("jkl");
ls.add("mno");
ls.add("pqr");
ls.add("stu");
ls.add("vwx");
/*Optional<String> max = ls.stream().max(String::compareTo);
//假設要求流的最大值,指定max(compareTo)對數據進行比較;max是一個終端流,返回值類型爲optional;
System.out.println(max.get());//獲取流中最大的值;*/
ls.stream().sorted().forEach(e -> System.out.println(e));//調用sorted()方法對流進行排序,forEach是終端操作,sorted是中間操作,
System.out.println(ls.stream().distinct().count());//輸出這個流中不重複元素的數量;
}
 
}
流的基本操作:過濾,排序,縮減,映射,收集,迭代
3.4.Fork/Join框架
從三個方面來學習Fork/Join框架中的主要類,分而治之策略,Fork/Join框架案例
"分而治之"一直是一個非常有效地處理大數據的方法。著名的MapReduce也是採取了分而治之的思想。簡單來說,就是如果你要處理1000個數據,但是你並不具備處理1000個數據的能力,那麼你可以只處理其中的10個,然而分階段處理100次,將100次的結果進行合成,那就是最終你想要的對原始1000個數據的處理結果。
何謂Fork/Join框架,在Linux平臺上,函數用fork()用來創建子進程,使得系統進程可以多一個執行分支。在java中也沿用了相似的命名方式。
Join()方法在線程的生命週期中,表示等待的意思,意思是說使用fork()後系統又多了一個執行分支(線程),所以需要等待這個執行分支執行完畢,纔有可能得到最終的結果,因此join()就表示等待。
注意:在實際的使用中,若毫無顧忌的使用fork()來開啓線程進行處理,那麼就很有可能導致系統開啓過多的線程而嚴重影響性能。所以,在JDK中,給出了一個ForkJoinPool線程池,對於fork()方法並不着急開啓線程,而是提交給ForkJoinPool線程池進行處理,以節省系統資源。使用Fork/Join進行數據處理時的總體結構如下圖所示:

 
                                        Fork/Join執行邏輯
a.Fork/Join框架中的主要類
b.Fork/Join框架的分而治之策略
import java.util.ArrayList;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.ForkJoinPool;
import java.util.concurrent.ForkJoinTask;
 
class CountTask<Long> implements RecursiveTask<Long>{//CountTask繼承RecursiveTask,可以攜帶返回值,設置返回值類型爲long;
private static final int THRESHOLD = 10000;//定義的這個值作爲閥值,一個臨界值;
private long start;//定義的起始值;
private long end;//定義的末尾值;
public CountTask(long start,long end){
this.start= start;
this.end = end;
}
public Long compute(){//compute()方法是計算sum的返回值;
long sum = 0;
boolean canCompute = (end - start)<THRESHOLD;//canCompute的值小於閥值,那麼可以 繼續向下執行;
if(canCompute){
for(long i=start;i<=end;i++){
sum+=i;
}//上面是針對任務比較小的時候,可以利用此來解決;
}else{
//分成100個小任務;
long step=(start+end)/100;
ArrayList<CountTask> subTasks = new ArrayList<CountTask>();
long pos = start;
for(int i =0;i<100;i++){
long lastOne = pos+step;
if(lastOne>end) lastOne = end;
CountTask subTask = new CountTask(pos,lastOne);
pos+=step+1;
subTasks.add(subTask);
subTask.fork();
}
for(CountTask t:subTasks){
sum+=t.join();
}
}
return sum;
}
public static void main(String[] args){
ForkJoinPool forkJoinPool = new ForkJoinPool();//由於毫無顧忌的使用fork()開啓線程,很有可能導致系統開啓過多的線程而嚴重影響性能;
//利用ForkJoinPool線程池來解決這種問題;對於fork()並不急着開啓線程,而是提交給ForkJoinPool線程池來處理,來節省系統資源;
CountTask task = new CountTask(0,200000L);//構造一個計算1到200000求和的任務;
ForkJoinTask<Long> result = forkJoinPool.submit(task);//將任務提交給線程池,線程池會返回一個攜帶結果的任務
try{
long res = result.get();//調用get()方法進行獲取結果,可能會拋出異常,可以利用try...catch結構處理異常,也可以利用throws()方法處理異常;
//如果在執行get()方法時,任務沒有結束,那麼主線程就會在get()方法時等待。
System.out.println("sum="+res);
}catch(ExecutionException e){
e.printStackTrace();
}
}
}
四.線程複用:線程池
多線程在一定程度上以最大限度地發揮現代多核處理器的計算能力,但是如果不加控制和管理的隨意使用線程,對系統的性能反而會產生不利的影響;
java Executor框架
其中,ThreadPoolExecutor表示一個線程池。Executor類則扮演着線程池工廠的角色,通過Executor可以取得擁有特定功能的線程池。
Executor框架提供了各種類型的線程池,主要有以下工廠方法:
public static ExecutorService newFixedThreadpool(int nThreads)
public static ExecutorService newSingleThreadExecutor()
public static ExecutorService newCachedThreadPool()
public static ScheduleExcutorService newSingleThreadScheduledExcutor
public static ScheduleExcutorService newSingleThreadScheduledExcutor(int corePoolSize)
newFixedThreadPool()方法:該方法返回一個固定線程數量的線程池。該線程池中的數量始終不變。當有一個新的任務提交時,線程池中 若有空閒線程,則立即執行。若沒有,則新的任務會暫存在一個任務隊列中,待有線程空閒時,便處理在任務隊列中的任務。
newSingleThreadExecutor()方法:該方法返回一個只有一個線程的線程池。若多餘一個任務被提交到該線程池,任務會被保存在一個任務隊列中,待線程空閒,按先入先出的順序執行隊列中的任務。
newCachedThreadPool():該方法返回一個可根據實際情況的調整線程數量的線程池。線程池的線程數量不確定,但若有空閒線程可以複用,則會優先使用可複用的線程。若所有線程均在工作,又有新的任務提交,則會創建新的線程處理任務。所有線程在當前任務在當前任務執行完畢後,將返回線程池進行復用。
newSingleThreadScheduleExecutor()方法:該方法返回一個ScheduledExecutorService對象,線程池大小爲1。ScheduledExecutorService接口在ExecutorService接口之上擴展了在給定時間執行某任務的功能,如在某個固定的延時之後執行,或者週期性執行某個任務。
newScheduleThreadPool()方法:該方法返回一個ScheduleExccutorService對象,但線程池可以指定線程數量;
值得注意的一個方法是newScheduleThreadPool()。它返回一個ScheduleExecutorService對象可以根據時間需要對線程進行調度,它的一些主要方法如下:
public ScheduleFuture<?>  schedule(Runnable ,long delay,TimeUnit unit);
public ScheduleFuture<?>  scheduleAtFixedRate(Runnable command, long initialDelay,long period,TimeUnit unit);
public ScheduleFuture<?> scheduleWithFixedDelay(Runnable command, long initialDelay,long Delay,TimeUnit unit);
與其他幾個線程池不同,ScheduleExecutorService並不一定會立即安排執行任務,它是起到了計劃任務的作用。它會在指定的時間,對任務進行調度。
然而這幾種方法略有不同,方法schedule()會在給定時間,對任務進行一次調度。方法scheduleAtFixedRate()和 scheduleWithFixedDelay()會對任務進行週期性的調度,區別在於,對於FixedRate方式來說,任務調度的頻率是一定的。它是以上一個任務開始執行時間爲起點。之後的period時間,調度下一次任務,而FixDelay則是在上一個任務結束後,再經歷delay時間進行任務調度。
對於核心的幾個線程池,不論是newFixedThreadPool()方法,newSingleThreadExecutor還是newCachedThreadPool()方法,在創建線程是各有千秋,但其內部實現均使用了ThreadPoolExecutor類的封裝。
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章