三.并发工具
从以下四个方面来学习:
执行器,
锁与原子操作,
流编程,
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类的封装。