Java多线程(三)高级concurrent包

背景
ReentrantLock类
  基本使用
​  基本特点
​  tryLock使用
​  总结
ReadWriteLock接口
​  特点
​  基本使用
​  适用条件
​  总结
Condition接口
​  基本使用
​  方法原理
  总结
Concurrent集合
​  基本使用
​  Blocking集合
​  总结
Atomic包
​  使用举例
​  总结
ExecutorService线程池
​  线程池
​  常用ExecutorService
​  ScheduledThreadPool
​    模式
​    思考
​  Timer
​  总结
Future
​  Callable<T>接口
​  Future接口
​  总结
CompletableFuture类
​  基本使用
​  优点
​  用法详解
​    1、创建对象
​    2、设置回调
​    3、主线程等待
​  多任务串行执行
​  多任务并行执行
​    anyOf
​    allOf
​  CompletableFuture方法命名规则
​  总结
Fork / Join
​  案例
​  总结

背景

前面已经提到,Java提供了synchronized/wait/notify等方法来解决多线程竞争和协调问题,但是编写多线程的同步依然比较困难,步骤很负责。在JDK1.5开始Java提供了一个高级的concurrent包来处理多线程问题

java.util.concurrent

ReentrantLock类

基本使用

使用ReentrantLock可以替代synchronized

class Counter {
  	// 创建ReentrantLock对象(实现了Lock接口)
  	final Lock lock = new ReentrantLock();
  	public void add() {
      	// 加锁,要写在try之前,因为可能会失败
      	lock.lock();
      	try {
          	n = n + 1;
        } finally {
          	// 释放锁。为了保证一定能释放锁,必须使用try...finally...
          	lock.unlock();
        }
    }
}
基本特点
  • 可重入锁,一个线程可多次获取同一个锁
  • lock()方法可获取锁
  • tryLock() 方法可尝试获取锁并可指定超时时间
tryLock使用
class Counter {
  	// 创建ReentrantLock对象
  	final Lock lock = new ReentrantLock();
  	public void add() {
      	// 加锁,并指定超时时间
      	if (lock.tryLock(1,TimeUnit.SECONDS)) {
          	try {
          			n = n + 1;
        		} finally {
          			// 释放锁。
          			lock.unlock();
        }
    }
}
总结
  • ReentrantLock可以替代synchronized
  • ReentrantLock获取锁更安全
  • 必须使用try… finally保证正确获取锁和释放锁

ReadWriteLock接口

特点
  • 只允许一个线程写入(其他线程既不能写入也不能读取)
  • 没有写入时,多个线程允许同时读(提高性能)
基本使用
class Counter {
  	// 创建ReadWriteLock接口的实现类对象
  	final ReadWriteLock lock = new ReentrantReadWriteLock();
  	// 获取读锁
  	final Lock rLock = lock.readLock();
  	// 获取写锁
  	final Lock wLock = lock.writeLock();
  	// 写方法
  	public void add() {
      	// 使用写锁来加锁
      	wLock.lock();
      	try {
          	value += 1;
        } finally {
          	wLock.unlock();
        }
    }
  	// 读方法
  	public void get() {
      	// 使用读锁来加锁
      	rLock.lock();
      	try {
          	return this.value;
        } finally {
          	rLock.unlock();
        }
    }
}
适用条件
  • 同一个实例,有大量线程读取,仅有少数线程修改(比如文章的评论)
总结

使用ReadWriteLock可以提高读取效率

  • ReadWriteLock只允许一个线程写入
  • ReadWriteLock允许多个线程同时读取
  • ReadWriteLock适合读多写少的场景

Condition接口

前面已经提到,使用ReentrantLock可以替代synchronized,但是不能直接实现wait和notify的功能,Java提供了一个Condition接口来配合ReentrantLock来实现wait和notify功能。

基本使用
class TaskQueue {
  	final Lock lock = new ReentrantLock();
  	// condition对象必须从ReentrantLock获取
  	final Condition condition = lock.newCondition();
		public String getTask() {
      	lock.lock();
      	try {
          	while (this.queue.isEmpty()) {
              	// 线程等待方法 await()
              	// 等同于synchronized的wait()方法
              	condition.await();
            }
          	return queue.remove();
        } finally {
          	lock.unlock();
        }
    }
  	
  	public void addTask(String name) {
      	lock.lock();
      	try {
          	this.queue.add(name);
          	// 唤醒方法 signal/signalAll
          	// 等同于synchronized的notify/notifyAll
          	condition.signalAll();
        } finally {
          	lock.unlock();
        }
    }
}
方法原理

Condition中的await / signal / signalAll原理和 synchronized中的wait / notifu/ notifyAll一致

  • await()会释放当前锁,进入等待状态
  • signal()会唤醒某个等待线程
  • signalAll()会唤醒所有等待线程
  • 唤醒线程从await()返回后需要重新获得锁
总结
  • Condition可以替代wait / notify
  • Condition对象必须从ReentrantLock对象获取
  • ReentrantLock+Condition可以替代synchronized + wait / notify

Concurrent集合

前面的案例中,从一个队列中读取数据时,如果没有数据则需要等待,这种情况下的队列被称为Blocking Queue。Java提供了线程安全的Blocking Queue来简化开发。

基本使用
class WorkerThread extends Thread {
  	BlockingQueue<String> taskQueue;
  
  	public WorkerThread(BlockingQueue<String> taskQueue) {
      	this.taskQueue = taskQueue;
    }
  	@Override
  	public void run() {
      	while (!isInterrupted()) {
          	String name;
          	try {
              	// 从队列中取数据,如果没有数据就会进行等待
              	name = taskQueue.take();
            } catch (InterruptedException e) {
              	break;
            }
          	String result = "Hello, " + name + "!";
          	System.out.println(result);
        }
    }
}
public class Main {
  	public static void main(String[] args) throws Exception { 
      	BlockingQueue<String> taskQueue = new ArrayBlockingQueue<>(100);
      	WorkerThread worker = new WorkerThread(taskQueue);
      	worker.start();
      	taskQueue.put("Alice");
      	Thread.sleep(1000);
      	taskQueue.put("Bob");
      	Thread.sleep(1000);
      	taskQueue.put("Tim");
      	Thread.sleep(1000);
      	worker.interrupt();
      	worker.join();
      	System.out.println("END");
    }
}
Blocking集合

在这里插入图片描述

总结

使用concurrent提供的Blocking集合可以简化多线程编程

  • 多线程同时访问Blocking集合是安全的
  • 尽量使用JDK提供的concurrent集合,避免自己编写同步代码

Atomic包

java.util.concurrent.atomic提供了一组原子类型操作:

  • AtomicInteger
    • int addAndGet(int delta)
    • int incrementAndGet()
    • int get()
    • int compareAndSet(int expect, int update)

Atomic类可以实现

  • 无锁(lock-free)实现的线程安全(thread-safe)访问
使用举例
class IdGenerator {
  	AtomicLong var = new AtomicLong(0);
		/*
			多线程安全的ID序列生成器
		*/
  	public long getNextId() {
      	// 加1并返回值
      	return var.incrementAndGet();
    }
}
总结

使用java.util.atomic提供的原子操作可以简化多线程编程:

  • AtomicInteger / AtomicLong / AtomicIntegerArray 等
  • 原子操作实现了无锁的线程安全
  • 适用于计数器, 累加器等

ExecutorService线程池

创建线程会消耗系统资源,而且频繁创建和销毁线程需要消耗大量时间,如果能够复用线程将大大提高运行效率,降低资源消耗,因此线程池应运而生。

线程池
  • 线程池维护若干个线程,处于等待状态
  • 如果有新任务,就分配一个空闲线程执行
  • 如果所有线程都处于忙碌状态,新任务放入队列等待

JDK提供了ExecutorService接口表示线程池

ExecutorService executor = Executors.newFixedThreadPool(4);// 固定大小的线程池
executor.submit(task1);
executor.submit(task2);
executor.submit(task3);
...
常用ExecutorService:
  • FixedThreadPool:线程数固定
  • CachedThreadPool:线程数根据任务数动态调整
  • SingleThreadExecutor:仅单线程执行,只创建一个线程
// 固定线程数的线程池
ExecutorService executor1 = Executors.newFixedThreadPool(10);
// 线程数根据任务数量动态调整的线程池
ExecutorService executor2 = Executors.newCachedThreadPool();
// 单线程的线程池
ExecutorService executor3 = Executors.newSingleThreadExecutor();

虽然CachedThreadPool不可可以设置固定的线程数,但是查看其源码

public static ExecutorService newCachedThreadPool() {
        return new ThreadPoolExecutor(0, Integer.MAX_VALUE,
                                      60L, TimeUnit.SECONDS,
                                      new SynchronousQueue<Runnable>());
}

其中ThreadPoolExecutor方法的第二个参数为线程数量,因此可以通过创建这个对象来创建固定线程数量的线程池

ExecutorService executor = new ThreadPoolExecutor(0, 5,
                                      60L, TimeUnit.SECONDS,
                                      new SynchronousQueue<Runnable>());
ScheduledThreadPool

可以定期反复执行一个任务的线程池

模式
  • Fixed Rate 定期多久执行一次任务,不管任务执行多久
// 创建一个定期执行的线程池,并指定维持的线程数量
ScheduledExecutorService executor = Executors.newScheduledThreadPool(3);
// 参数一:执行的任务
// 参数二:多长时间后开始执行任务
// 参数三:每隔多久执行一次任务
// 参数四:时间单位
// 1秒后开始执行任务,而且每3秒执行一次
executor.scheduleAtFixedRate(new Thread(), 1, 3, TimeUnit.SECONDS);

  • Fixed Delay 一次任务结束后间隔多久执行下一次任务
// 创建一个定期执行的线程池,并指定维持的线程数量
ScheduledExecutorService executor = Executors.newScheduledThreadPool(3);
// 参数一:执行的任务
// 参数二:多长时间后开始执行任务
// 参数三:两次任务的间隔
// 参数四:时间单位
// 1秒后开始执行任务,上一次任务结束后3秒执行下一次任务
executor.scheduleWithFixedDelay(new Task("002"), 1, 3, TimeUnit.SECONDS);


思考
  • FixedRate模式下,如果任务执行时间过长,后续任务会不会并发执行?(不会)
  • 如果任务抛出了异常,后续任务是否继续执行?(会)
Timer

java.util.Timer

  • 一个Timer对应一个Thread
  • 必须在主线程结束时调用Timer.cancel()
总结
  • JDK提供了ExecutorService实现了线程池功能
  • 线程池内部维护一组线程,可以高效执行大量小任务
  • Executors提供了静态方法创建不同类型的ExecutorService
  • 必须调用shutdown()关闭ExecutorService
  • ScheduledThreadPool可以定期调度多个任务

Future接口

Callable<T>接口

Callable和Runnable相似,但是Runnable没有返回值,Callable有返回值,所以实现Callable时需要指定范型,并重写call()方法指定返回值类型

// Callable接口源码
@FunctionalInterface
public interface Callable<V> {
    /**
     * Computes a result, or throws an exception if unable to do so.
     *
     * @return computed result
     * @throws Exception if unable to compute a result
     */
    V call() throws Exception;
}
// 实现Callable接口
class Task implements Callable<String> {
  	@Override
  	public String call() throws Exception {
      	return "Hello";
    }
}
Future接口

表示一个任务未来可能会返回的结果

  • get() 获取执行结果
  • get(long timeout, TimeUnit unit) 指定最大等待结果时间
  • cancel(boolean mayInterruptIfRunning) 中断异步任务的执行
  • isDone() 判断异步任务是否完成
Callable<String> Task = new Task();
ExecutorService executor = Executors.newFixedThreadPool(4);
// 接收任务执行结果对象
Future<String> future = executor.submit(task);
// 获取具体的返回值,如果任务还没结束会阻塞,一直到任务执行结束返回结果
String result = future.get();

Runnable VS Callable
在这里插入图片描述

总结
  • 提交Callable任务,可以获得一个Future对象
  • 可以用Furure在将来某个时刻获取结果

CompletableFuture类

使用Future获取异步执行结果的方法:

  • get():阻塞方法
  • isDone():判断任务是否完成来轮询

以上两种方式效率都比较低,JDK提供了一个CompletableFuture类,可以通过设置回调方法的形式在任务结束后来获取结果。

基本使用
// 创建CompletableFuture对象,并指定范型(结果)类型
CompletableFuture<String> cf = ...
// thenAccept方法设置任务正常运行完成后的操作
cf.thenAccept(new Consumer<String>() {
  	@Override
  	public void accept(String s) {
    		System.out.println("异步任务正常运行结果 : " + s);
  	}
});
// exceptionally方法设置任务发生异常的操作
cf.exceptionally(new Function<Throwable, String>() {
  	@Override
   	public String apply(Throwable throwable) {
     		System.out.println("运行发生异常 : " + throwable.getLocalizedMessage());
      	return null;
   	}
});
/*************************  分割线  ******************************/
// JDK1.8函数式编程写法(了解)
cf.thenAccept((result) -> {
  	System.out.println("异步任务正常运行结果 : " + s);
});
cf.exceptionally((t) -> {
  	System.out.println("运行发生异常");
  	return null;
});
优点
  • 异步任务结束时,会自动回调某个对象的方法
  • 异步任务出错时,会自动回调某个对象的方法
  • 主线程设置好回调后,不再关心异步任务的执行
用法详解
1、创建对象
CompletableFuture<String> cf = CompletableFuture.supplyAsync(new ASupplier());
// Supplier接口源码
@FunctionalInterface
public interface Supplier<T> {
    /**
     * Gets a result.
     *
     * @return a result
     */
    T get();
}

通过CompletableFuture的supplyAsync方法创建对象,需要传入一个Supplier实例对象,可以理解为任务对象,重写get()方法执行具体任务。

2、设置回调
// thenAccept方法设置任务正常运行完成后的操作
cf.thenAccept(new Consumer<String>() {
  	@Override
  	public void accept(String s) {
    		System.out.println("异步任务正常运行结果 : " + s);
  	}
});
// exceptionally方法设置任务发生异常的操作
cf.exceptionally(new Function<Throwable, String>() {
  	@Override
   	public String apply(Throwable throwable) {
     		System.out.println("运行发生异常 : " + throwable.getLocalizedMessage());
      	return null;
   	}
});

3、主线程等待
// 注意:主线程结束时默认使用的Executor会关闭,所以要使用join方法等待任务执行完毕
cf.join();
多任务串行执行
// 实例1对象
CompletableFuture<String> cf1 = CompletableFuture.supplyAsync(Supplier1);
// cf1通过thenApplyAsync方法获取实例2对象
CompletableFuture<Float> cf2 = cf1.thenApplyAsync(Supplier2);
// cf2通过thenApplyAsync方法获取实例3对象
CompletableFuture<Integer> cf3 = cf2.thenApplyAsync(Supplier3);
// 通过实例3获取结果
cf3.thenAccept(实例3运行结果操作);
cf3.exceptionally(实例3运行异常操作);

// 多个任务都执行完后再获取结果:cf1执行完 --> cf2执行完 --> cf3执行完 --> 获取结果
多任务并行执行
anyOf

多个任务中只要有一个任务结束就获取结果

// 创建两个任务实例
CompletableFuture<String> cf1 = CompletableFuture.supplyAsync(Supplier1);
CompletableFuture<Float> cf2 = CompletableFuture.supplyAsync(Supplier2);
// 通过anyOf转化为新的实例,注意修改范型类型
CompletableFuture<Object> cf3 = CompletableFuture.anyOf(cf1, cf2);
// 通过新的实例获取结果
cf3.thenAccept(运行结果操作);
cf3.join();
allOf

多个任务全部执行结束后才会获取结果

// 创建两个任务实例
CompletableFuture<String> cf1 = CompletableFuture.supplyAsync(Supplier1);
CompletableFuture<Float> cf2 = CompletableFuture.supplyAsync(Supplier2);
// 通过allOf转化为新的实例,范型类型只能是Void
CompletableFuture<Void> cf3 = CompletableFuture.allOf(cf1, cf2);
// 通过新的实例获取结果
cf3.thenAccept(运行结果操作);
cf3.join();
CompletableFuture方法命名规则
  • xxx():表示继续在已有的线程中执行
  • xxxAsync():用Executor的新线程执行
总结

CompletableFuture对象可以指定异步处理流程:

  • thenAccept() 处理正常结果
  • exceptionally() 处理异常结果
  • thenApplyAsync() 用于串行化另一个CompletableFuture对象
  • anyOf / allOf 用于并行化多个CompletableFuture

Fork / Join

Fork/Join框架是Java7提供的一个用于并行执行任务的框架, 是一个把大任务分割成若干个小任务,最终汇总每个小任务结果后得到大任务结果的框架。使用工作窃取(work-stealing)算法,主要用于实现“分而治之”。
案例
// 任务类必须继承自RecursiveTask(有返回值) / RecursiveAction(没有返回值)
class SumTask extends RecursiveTask<Long> {
  // 定义一个阀值,用于判断是否要进行拆分
	static final int THRESHOLD = 500;
	long[] array;
	int start;
	int end;

	SumTask(long[] array, int start, int end) {
		this.array = array;
		this.start = start;
		this.end = end;
	}

  // 执行任务方法
	@Override
	protected Long compute() {
		if (end - start <= THRESHOLD) {
			// 如果任务足够小,直接计算:
			long sum = 0;
			for (int i = start; i < end; i++) {
				sum += this.array[i];
				try {
					Thread.sleep(2);
				} catch (InterruptedException e) {
				}
			}
			return sum;
		}
		// 任务太大,一分为二:
		int middle = (end + start) / 2;
		System.out.println(String.format("split %d~%d ==> %d~%d, %d~%d", start, end, start, middle, middle, end));
    // 分成两个小任务
		SumTask subtask1 = new SumTask(this.array, start, middle);
		SumTask subtask2 = new SumTask(this.array, middle, end);
    // 并行执行两个任务
		invokeAll(subtask1, subtask2);
    // 分别获取结果
		Long subresult1 = subtask1.join();
		Long subresult2 = subtask2.join();
    // 得出最终结果
		Long result = subresult1 + subresult2;
		System.out.println("result = " + subresult1 + " + " + subresult2 + " ==> " + result);
		return result;
	}
}

public class ForkJoinTaskSample {
	public static void main(String[] args) throws Exception {
		// 创建1000个随机数组成的数组:
		long[] array = new long[1000];
		long expectedSum = 0;
		for (int i = 0; i < array.length; i++) {
			array[i] = random();
			expectedSum += array[i];
		}
		System.out.println("Expected sum: " + expectedSum);
		// fork/join:
		ForkJoinTask<Long> task = new SumTask(array, 0, array.length);
		long startTime = System.currentTimeMillis();
    // 执行任务
		Long result = ForkJoinPool.commonPool().invoke(task);
		long endTime = System.currentTimeMillis();
		System.out.println("Fork/join sum: " + result + " in " + (endTime - startTime) + " ms.");
	}
	static Random random = new Random(0);
	static long random() {
		return random.nextInt(10000);
	}
}
总结
  • Fork / Join是一种基于“分治”的算法:
  • 分解任务 + 合并结果
  • ForkJoinPool线程池可以把一个大任务分拆成小任务并行执行
  • 任务类必须继承自RecursiveTask(有返回值) / RecursiveAction(无返回值)
  • 使用Fork / Join模式可以进行并行计算提高效率
  • 1
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

Bright1st

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值