并发编程之 CompletableFuture

在 Java 中,如果需要异步执行任务,可以使用线程来实现,但是我们希望线程执行完之后可以获得执行结果,怎么实现呢?

JDK 1.5 中,引入了 Future 的概念,它可以结合 Callable 接口来获得线程异步执行完成之后的返回值,但它在使用上存在一定的局限性。所以在 JDK1.8 中引入了 CompletableFuture 组件,在 Future 的基础上提供了更加丰富和完善的功能。

1. Future接口

Java5新加的一个接口,它提供了一种异步并行计算的功能,实现类<FutureTask>。如果主线程需要执行一个很耗时的计算任务,我们就可以通过 Future把这个任务放到异步线程中执行,主线程继续处理其他任务或者先行结束,再通过 Future获取计算结果。看一下类关系图:

1.1 使用

下面,通过代码来演示一下,如何使用:

class MyThread implements Callable<String> {
	@Override
	public String call() throws Exception {
		return "hello!";
	}
}
public static void main(String[] args) throws InterruptedException, ExecutionException {
	FutureTask<String> ft = new FutureTask<>(new MyThread());
	Thread t1 = new Thread(ft);
	t1.start();
	String s = ft.get(); // 获取异步线程的返回结果
	System.out.println(s);
}

运行结果:

获取到了异步线程的返回结果“hello”。

1.2 FutureTask 的优缺点

  • 优点

        结合线程池,可以大大提高程序的运行效率;

  • 缺点

① get() 方法阻塞线程,一旦调用不见不散,需要等待异步线程执行完毕,返回处理结果,违背了异步线程提高效率的初衷;

② 如果异步线程耗时较久,主线程又不想等待太长时间,可以通过设置等待时间【get("x",TimeUnit.xx)】进行处理,过时不候,即当达到了设置的等待时间,异步线程仍没有返回结果,直接抛出异常;

③ 等待超时抛出异常的方式,不太优雅,会报出太多的异常日志,可以使用 isDone()方式来告知异步线程的处理状态,当处理完成后,告知主线程处理完成,但是主线程一直轮询等待会耗费无谓的 CPU 资源;

 代码演示:

get()阻塞

class MyThread implements Callable<String> {

	@Override
	public String call() throws Exception {
		System.out.println("异步线程开始执行");
		TimeUnit.SECONDS.sleep(5);
		return "hello callable!!";
	}
}



public static void main(String[] args) throws Exception {
	long start = System.currentTimeMillis();
	FutureTask<String> futureTask = new FutureTask<>(new MyThread());
	Thread thread = new Thread(futureTask);
	thread.start();
	String s = futureTask.get();
	System.out.println("异步线程: " + thread.getName() + "返回的结果:" + s);
	System.out.println("====== 主线程执行结束 ========");
	long end = System.currentTimeMillis();
	System.out.println("========  共耗时 ============ " + (end-start) + "ms");
}

可以看到,当调用了异步线程获取返回结果的时候,影响了主线程的运行效率。

get(xx,xx) 等待超时,抛出异常

public static void main(String[] args) throws Exception {
	long start = System.currentTimeMillis();
	FutureTask<String> futureTask = new FutureTask<>(new MyThread());
	Thread thread = new Thread(futureTask);
	thread.start();
	// 主线程,只等待 3s,如果 3s没有响应,则抛出异常
	String s = futureTask.get(3,TimeUnit.SECONDS); 
	System.out.println("异步线程: " + thread.getName() + "返回的结果:" + s);
	System.out.println("====== 主线程执行结束 ========");
	long end = System.currentTimeMillis();
	System.out.println("========  共耗时 ============ " + (end-start) + "ms");
}

isDone()轮询,浪费无谓的 CPU 资源

public static void main(String[] args) throws Exception {
	long start = System.currentTimeMillis();
	FutureTask<String> futureTask = new FutureTask<>(new MyThread());
	Thread thread = new Thread(futureTask);
	thread.start();
	System.out.println("====== 主线程执行结束 ========");
	// isDone 轮询等待
	while (true) {
		if (futureTask.isDone()) {
			String s = futureTask.get();
			System.out.println("异步线程处理返回结果===== " + s);
			break;
		} else {
			System.out.println("异步线程正在处理~~");
		}
	}
	long end = System.currentTimeMillis();
	System.out.println("========  共耗时 ============ " + (end-start) + "ms");
}

 1.3 Future 总结

通过上面的代码演示,可以得知,Future对于结果的获取不是很友好,只能通过阻塞或者轮询的方式得到任务的结果。显然,这是不够完美的,那么有没有一种更完美的解决方案呢?答案是有的,也就是 JDK1.8 中的 CompletableFuture,完美的解决了这些问题。

2. CompletableFuture

2.1 Future 的优化思路

① 对于简单的业务场景,使用 Futrue 完成 OK;

② 对于复杂的,高并发的业务场景,我们需要有回调通知,当异步线程执行完毕后,主动通知主线程,而不是通过 isDone()轮询的方式去一直等待,那样大大的占用了CPU 资源;

③ 异步任务(Future + 线程池配置使用);

④ 多任务前后依赖组合处理,比如下一个任务,需要前一个任务的结果进行支撑,两个任务可以独立处理,但是又有依赖关系;

⑤ 对计算速度选最快等(并行的异步任务,哪个先完成,返回哪个结果);

2.2 CompletableFuture 对 Future 的优化

Future的 get() 方法在计算完成之前会一直处于阻塞状态;

isDone()方法容易耗费 CPU 资源;

对于真正的异步处理,我们希望是可以通过传入回调函数,在 Future结束时自动调用该回调函数,这样我们就不用等待结果;

阻塞的方式和异步编程的设计理念相违背,而轮询的方式会耗费无畏的 CPU 资源,因此,JDK1.8 设计出 CompletableFuture。它提供了一种观察者模式类似的机制,可以让任务执行完后通知监听的一方。

2.3 CompletableFuture API

CompletableFuture 扩展了 Futrue的所有功能,同时提供了其他更加强大的功能。

 先来看一下类图:

  •  四大核心构造方法

在官方的 API 文档中,是不推荐直接使用无参构造方法来进行创建实例的。

下面来看一下,推荐使用的四大核心构造方法:

共分为两类,一类是有返回值,一类是无返回值:

① runAsync  无返回值

② supplyAsync 有返回值

 Executor 参数说明: 如果没有指定 Executor 的方法,直接使用默认的 ForkJoinPool.commonPool()作为它的线程池执行异步代码;如果指定线程池,则使用我们自定义的或者特别指定的线程池执行异步代码。

代码演示:

无返回值

public static void main(String[] args) throws Exception {
	CompletableFuture<Void> completableFuture = CompletableFuture.runAsync(() -> {
		System.out.println(Thread.currentThread().getName());
		try {
			TimeUnit.SECONDS.sleep(1);
			System.out.println("task is over!!");
		} catch (InterruptedException e) {
			e.printStackTrace();
		}
	});
	Void unused = completableFuture.get();
	System.out.println(unused);
}

public static void main(String[] args) throws Exception {
	// 自定义线程池
	ExecutorService pool = Executors.newFixedThreadPool(3);
	CompletableFuture<Void> completableFuture = CompletableFuture.runAsync(() -> {
		System.out.println(Thread.currentThread().getName());
		try {
			TimeUnit.SECONDS.sleep(1);
			System.out.println("task is over!!");
		} catch (InterruptedException e) {
			e.printStackTrace();
		}
	},pool);
	Void unused = completableFuture.get();
	System.out.println(unused);
	// 使用完后进行关闭
	pool.shutdown();
}

有返回值

public static void main(String[] args) throws Exception {
	// 自定义线程池
	ExecutorService pool = Executors.newFixedThreadPool(3);
	CompletableFuture<String> completableFuture = CompletableFuture.supplyAsync(() -> {
		System.out.println(Thread.currentThread().getName());
		try {
			TimeUnit.SECONDS.sleep(1);
		} catch (InterruptedException e) {
			e.printStackTrace();
		}
		return "hello supplyAsync";
	},pool);
	String result = completableFuture.get();
	System.out.println(result);
	// 使用完后进行关闭
	pool.shutdown();

前面说了,CompletableFuture 是 Future的增强版,减少了阻塞和轮询,通过上面的代码,发现和 Future 并没有什么区别,那它是怎么做到的呢?来看下面的代码:

public static void main(String[] args) throws Exception {
	ExecutorService pool = Executors.newFixedThreadPool(3);
	CompletableFuture.supplyAsync(() -> {
		System.out.println(Thread.currentThread().getName());
		try {
			TimeUnit.SECONDS.sleep(1);
		} catch (InterruptedException e) {
			e.printStackTrace();
		}
		// 产生一个 随机数
		int i = ThreadLocalRandom.current().nextInt(10);
		return i;
	},pool).whenComplete((v,e) -> {
		// v: 上一步的返回结果,e: 异常信息
		// 正常执行完毕,没有异常
		if (e == null) {
			System.out.println("异步线程执行完毕,进行回调======== " + v);
		}
	}).exceptionally(e -> {
		e.printStackTrace();
		System.out.println("异步线程出现异常了============ " + e.getCause() + "\t" + e.getMessage());
		return null;
	});
	System.out.println(Thread.currentThread().getName() + " 主线程执行处理其他业务去了 ===========");
	// 注意:使用默认的线程池 主线程不要 立刻结束,否则 CompletableFuture默认使用的线程池会立刻关闭
	TimeUnit.SECONDS.sleep(3);
	// 使用我们自己的线程池
	pool.shutdown();
}

通过上面的代码,可以很明显的看出 CompletableFuture灵活了很多。

异步任务结束时,会自动回调某个对象的方法;

主线程设置好回调后,不再关系异步任务的执行,异步任务之间可以顺序执行;

异步任务出错时,会自动回调某个对象的方法;

join 和 get 对比

两者都是获取异步线程的返回值,区别在于:

get 在编译期间会抛出异常,需要进行声明式抛出,允许被中断,抛出 InterruptedException 异常;

join 在编译期间不会抛出异常,在运行时进行处理,不允许被中断;

 allOf() 和 anyOf()

allOf: 接收多个 CompletableFuture 无返回值任务,当所有的任务执行结束后,返回一个新的 CompleatbleFuture 对象;

anyOf: 接收多个带有返回值的任务,当任何一个任务执行完成后,返回一个新的CompleatbleFuture 对象;

public static void main(String[] args) throws ExecutionException, InterruptedException, TimeoutException {
	ExecutorService threadPool = Executors.newFixedThreadPool(3);
	CompletableFuture<Integer> futureA = CompletableFuture.supplyAsync(() -> {
		try {
			TimeUnit.SECONDS.sleep(1L);
		} catch (InterruptedException e) {
			e.printStackTrace();
		}
		return 10;
	}, threadPool);
	CompletableFuture<Integer> futureB = CompletableFuture.supplyAsync(() -> {
		try {
			TimeUnit.SECONDS.sleep(2L);
		} catch (InterruptedException e) {
			e.printStackTrace();
		}
		return 20;
	}, threadPool);
	System.out.println(CompletableFuture.anyOf(futureA, futureB).join());
	threadPool.shutdown();

 

2.4 经典案例实战

电商比价需求分析:

① 同一款产品,同时搜索出同款产品 在各大电商平台的售价;

② 同一款产品,同时搜索出本产品在同一个电商平台下,各个入驻卖家售价是多少;

 解决方案:

①中规中矩, 一个线程一步一步的去查;

②万箭齐发,异步线程,同时分开去查;

代码演示:

public class CompletableFutureTest {

	static List<NetMall> netMalls = Arrays.asList(
			new NetMall("JD"),
			new NetMall("taobao"),
			new NetMall("dangdang")
	);

	public static void main(String[] args) {
		long start = System.currentTimeMillis();
		List<String> strings = getPrice(netMalls, "MySQL");
		strings.stream().forEach(item -> System.out.println(item));
		long end = System.currentTimeMillis();
		System.out.println(" ======== 共耗时 ===== " +  (end - start));
	}

	public static List<String> getPrice(List<NetMall> malls, String productName) {
		return malls.stream()
				.map(item ->
						String.format("《" + productName + "》" + " in %s price is %.2f", item.getNetMallName(), item.calcPrice(productName)))
				.collect(Collectors.toList());
	}
}

class NetMall {

	@Getter
	private String netMallName;

	public NetMall(String netMallName) {
		this.netMallName = netMallName;
	}

	public double calcPrice(String productName) {
		try {
			TimeUnit.SECONDS.sleep(1);
		} catch (InterruptedException e) {
			e.printStackTrace();
		}
		return ThreadLocalRandom.current().nextDouble(5) *2 + productName.charAt(0);
	}
}

 第一种解决方案,满足了我们的需求,但是,不尽人意,此时,需要对功能进行性能优化,使用第二种解决方案进行优化:

public static void main(String[] args) {
	long start = System.currentTimeMillis();
	// 使用异步方式进行处理
	List<String> strings = getPriceByAsync(netMalls, "MySQL");
	strings.stream().forEach(item -> System.out.println(item));
	long end = System.currentTimeMillis();
	System.out.println(" ======== 共耗时 ===== " +  (end - start));
}


/**
 * 异步处理
 * @param malls
 * @param productName
 * @return
 */
public static List<String> getPriceByAsync(List<NetMall> malls, String productName) {
	return malls.stream()
			.map(item ->
					CompletableFuture.supplyAsync(
							() -> String.format("《" + productName + "》" + " in %s price is %.2f", item.getNetMallName(), item.calcPrice(productName))))
			.collect(Collectors.toList())
			.stream()
			.map(cf -> cf.join()).collect(Collectors.toList());
}

 通过优化,发现性能上升显著,由原来的 3s 到现在的 1s,非常棒!当然,此案例中只有 3 家电商平台,在现实的业务场景中,远远不止这么几家,当集合的数量越大的时候,性能提升的越明显。

2.5 CompletableFuture 常用方法

API内容较多,这里简单分为五组进行描述

① 获得结果和触发计算;

② 对计算结果进行处理;

③ 对计算结果进行消费;

④ 对计算速度进行选用;

⑤ 对计算结果进行合并;

 获得结果:get() / get(x,TimeUtil.xx) / join() / getNow("xxx")

	public static void main(String[] args) throws ExecutionException, InterruptedException, TimeoutException {
		CompletableFuture<String> supplyAsync = CompletableFuture.supplyAsync(() -> {
			try {
				TimeUnit.SECONDS.sleep(2);
			} catch (InterruptedException e) {
				e.printStackTrace();
			}
			return "123";
		});
//		// 线程阻塞,不见不散
//		String result = supplyAsync.get();
//		// 与 get 的区别在于不会在编译期间显示的声明异常;
//		String res = supplyAsync.join();
//		// 给定一个等待时间,在等待时间内,不返回结果,则抛出异常
//		String r = supplyAsync.get(1L, TimeUnit.SECONDS);
        TimeUnit.SECONDS.sleep(3L);
		// 在调用返回结果的时候,如果线程没有处理完,则使用给定的默认值,不会阻塞线程
		String defaultValue = supplyAsync.getNow("返回默认值");
	}
}

触发计算: completa(T value) 是否打断 get方法,立即获取括号值

	public static void main(String[] args) throws ExecutionException, InterruptedException, TimeoutException {
		CompletableFuture<String> supplyAsync = CompletableFuture.supplyAsync(() -> {
			try {
				TimeUnit.SECONDS.sleep(2);
			} catch (InterruptedException e) {
				e.printStackTrace();
			}
			return "123";
		});
//		TimeUnit.SECONDS.sleep(3L);
		// 如果异步线程没有执行完毕,则直接打断,获取给定的值
		System.out.println(supplyAsync.complete("aaa"));
		System.out.println(supplyAsync.join());
	}
}

对计算结果进行处理:thenApply / handle  计算结果存在依赖关系,两个线程串行化,区别在于两者对异常的处理稍有不同,下面通过代码来进行演示。

	public static void main(String[] args) throws ExecutionException, InterruptedException, TimeoutException {
		ExecutorService threadPool = Executors.newFixedThreadPool(3);
		CompletableFuture<Integer> future = CompletableFuture.supplyAsync(() -> {
			System.out.println(Thread.currentThread().getName() + "111");
			try {
				TimeUnit.SECONDS.sleep(1L);
			} catch (InterruptedException e) {
				e.printStackTrace();
			}
			return 1;
		}, threadPool).thenApply(f -> {
            // 故意抛出一个异常
            int i= f/0;
			System.out.println(Thread.currentThread().getName() + "222");
			return f + 2;
		}).thenApply(f -> {
			System.out.println(Thread.currentThread().getName() + "333");
			return f + 3;
		}).exceptionally(e -> {
			e.printStackTrace();
			System.out.println(e.getMessage());
			return null;
		});
		System.out.println(future.join());
		threadPool.shutdown();
	}
}

正常运行下

thenApply 对异常的处理 ,出现异常,直接终止

 handle 正常的运行下,和 thenApply 一样,主要来看一下异常的处理:

	public static void main(String[] args) throws ExecutionException, InterruptedException, TimeoutException {
		ExecutorService threadPool = Executors.newFixedThreadPool(3);
		CompletableFuture<Integer> future = CompletableFuture.supplyAsync(() -> {
			System.out.println(Thread.currentThread().getName() + "111");
			try {
				TimeUnit.SECONDS.sleep(1L);
			} catch (InterruptedException e) {
				e.printStackTrace();
			}
			return 1;
		}, threadPool).handle((f,e) -> {
			// 故意抛出一个异常
			int i= f/0;
			System.out.println(Thread.currentThread().getName() + "222");
			return f + 2;
		}).handle((f,e) -> {
			System.out.println(Thread.currentThread().getName() + "333");
			return f + 3;
		}).exceptionally(e -> {
			e.printStackTrace();
			System.out.println(e.getMessage());
			return null;
		});
		System.out.println(future.join());
		threadPool.shutdown();
	}
}

 通过异常来看,handle 有异常的时候,会跳过,继续往下执行。

对计算结果进行消费: thenAccept 接收任务的处理结果,并消费处理,无返回结果

	public static void main(String[] args) throws ExecutionException, InterruptedException, TimeoutException {
		ExecutorService threadPool = Executors.newFixedThreadPool(3);
		CompletableFuture.supplyAsync(() -> {
			System.out.println(Thread.currentThread().getName() + "111");
			try {
				TimeUnit.SECONDS.sleep(1L);
			} catch (InterruptedException e) {
				e.printStackTrace();
			}
			return 1;
		}, threadPool).handle((f,e) -> {
			System.out.println(Thread.currentThread().getName() + "222");
			return f + 2;
		}).thenAccept(System.out::println);
		threadPool.shutdown();
	}
}

 thenRun / thenAccept / thenApply  三者的执行顺序

API 接口执行顺序
thenRun任务 A执行完毕执行 B,并且 B不需要 A的就结果
thenAccept任务 A执行完毕执行 B,B需要 A的结果,但是任务 B无返回值
thenApply任务 A执行完毕执行 B,B需要 A的结果,同时任务 B 有返回值

关于异步线程的线程池

1、没有传入自定义线程池, 都用默认线程池ForkJoinPool;

2、传入了一个自定义线程池,如果你执行第一个任务的时候,传入了一个自定义线程池:  

        调用thenRun方法执行第二个任务时,则第二个任务和第一个任务是共用同一个线程池。
        调用thenRunAsync执行第二个任务时,则第一个任务使用的是你自己传入的线程池,第二个任务使用的是ForkJoin线程池

3、备注

        有可能处理太快,系统优化切换原则,直接使用main线程处理
        其它如:thenAccept和thenAcceptAsync,thenApply和thenApplyAsync等,它们之间的区别也是同理

对计算速度选用: applyToEither  谁先完成,用谁的结果。

	public static void main(String[] args) throws ExecutionException, InterruptedException, TimeoutException {
		ExecutorService threadPool = Executors.newFixedThreadPool(3);
		CompletableFuture<String> A = CompletableFuture.supplyAsync(() -> {
			try {
				TimeUnit.SECONDS.sleep(1L);
			} catch (InterruptedException e) {
				e.printStackTrace();
			}
			return "A";
		}, threadPool);
		CompletableFuture<String> B = CompletableFuture.supplyAsync(() -> {
			try {
				TimeUnit.SECONDS.sleep(2L);
			} catch (InterruptedException e) {
				e.printStackTrace();
			}
			return "B";
		}, threadPool);
		CompletableFuture<String> future = A.applyToEither(B, f -> "Get is " + f);
		System.out.println(future.join());
		threadPool.shutdown();
	}
}

 对结果集合并: thenCombine  两个 completionStage任务都完成后,最终将江哥任务的结果一起交给 thenCombine 来处理,先完成的等待后完成的

	public static void main(String[] args) throws ExecutionException, InterruptedException, TimeoutException {
		ExecutorService threadPool = Executors.newFixedThreadPool(3);
		CompletableFuture<Integer> futureA = CompletableFuture.supplyAsync(() -> {
			try {
				TimeUnit.SECONDS.sleep(1L);
			} catch (InterruptedException e) {
				e.printStackTrace();
			}
			return 10;
		}, threadPool);
		CompletableFuture<Integer> futureB = CompletableFuture.supplyAsync(() -> {
			try {
				TimeUnit.SECONDS.sleep(2L);
			} catch (InterruptedException e) {
				e.printStackTrace();
			}
			return 20;
		}, threadPool);
		CompletableFuture<Object> result = futureA.thenCombine(futureB, (x, y) -> x+y);
		System.out.println("两线程结果集合并结果为:" + result.join());
		threadPool.shutdown();
	}
}

CompletableFuture 还有其他很多功能,小伙伴们可以去看一下其他相关的 API 接口,这里不做过多描述。

通过这篇文章,相信大家已经对 Java8 中的 CompletableFuture 有了深入掌握。

在日常的开发工作中,希望能合理运用到项目上,提升服务的性能!

  • 0
    点赞
  • 8
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
├─第一阶段 │      源码+ppt.rar │      高并发编程第一阶段01讲、课程大纲及主要内容介绍.wmv │      高并发编程第一阶段02讲、简单介绍什么是线程.wmv │      高并发编程第一阶段03讲、创建并启动线程.mp4 │      高并发编程第一阶段04讲、线程生命周期以及start方法源码剖析.mp4 │      高并发编程第一阶段05讲、采用多线程方式模拟银行排队叫号.mp4 │      高并发编程第一阶段06讲、用Runnable接口将线程的逻辑执行单元从控制中抽取出来.mp4 │      高并发编程第一阶段07讲、策略模式在Thread和Runnable中的应用分析.mp4 │      高并发编程第一阶段08讲、构造Thread对象你也许不知道的几件事.mp4 │      高并发编程第一阶段09讲、多线程与JVM内存结构的关系,虚拟机栈实验.mp4 │      高并发编程第一阶段10讲、Thread构造函数StackSize详细讲解.mp4 │      高并发编程第一阶段11讲、Thread构造函数StackSize详细讲解-续.mp4 │      高并发编程第一阶段12讲、Daemon线程的创建以及使用场景分析.mp4 │      高并发编程第一阶段13讲、线程ID,优先级讲解.mp4 │      高并发编程第一阶段14讲、Thread的join方法详细介绍,结合一个典型案例.mp4 │      高并发编程第一阶段15讲、Thread中断Interrupt方法详细讲解.mp4 │      高并发编程第一阶段16讲、采用优雅的方式结束线程生命周期.mp4 │      高并发编程第一阶段17讲、Thread API综合实战,编写ThreadService实现暴力结束线程的综合实战.mp4 │      高并发编程第一阶段18讲、数据同步的引入与Synchronized的简单介绍.mp4 │      高并发编程第一阶段19讲、结合jconsole,jstack以及汇编指令认识synchronized关键字.mp4 │      高并发编程第一阶段20讲、同步代码块以及同步方法之间的区别和关系.mp4 │      高并发编程第一阶段21讲、通过实验分析This锁的存在.mp4 │      高并发编程第一阶段22讲、通过实验分析Class锁的存在.mp4 │      高并发编程第一阶段23讲、多线程死锁分析,案例介绍.mp4 │      高并发编程第一阶段24讲、线程间通信快速入门,使用wait和notify进行线程间的数据通信.mp4 │      高并发编程第一阶段25讲、多Produce多Consume之间的通讯导致出现程序假死的原因分析.mp4 │      高并发编程第一阶段26讲、多线程下的生产者消费者模型,以及详细介绍notifyAll方法.mp4 │      高并发编程第一阶段27讲、wait和sleep的本质区别是什么,深入分析(面试常见问题).mp4 │      高并发编程第一阶段28讲、线程生产者消费者的综合实战结合Java8语法.mp4 │      高并发编程第一阶段29讲、如何实现一个自己的显式锁Lock精讲上.mp4 │      高并发编程第一阶段30讲、如何实现一个自己的显式锁Lock精讲下(让锁具备超时功能).mp4 │      高并发编程第一阶段31讲、如何给你的应用程序注入钩子程序,Linux下演示.mp4 │      高并发编程第一阶段32讲、如何捕获线程运行期间的异常.mp4 │      高并发编程第一阶段33讲、ThreadGroup API介绍之一.mp4 │      高并发编程第一阶段34讲、ThreadGroup API介绍之二.mp4 │      高并发编程第一阶段35讲、线程池原理与自定义线程池.mp4 │      高并发编程第一阶段36讲、自定义个简单的线程池并且测试.mp4 │      高并发编程第一阶段37讲、给线程池增加拒绝策略以及停止方法.mp4 │      高并发编程第一阶段38讲、给线程池增加自动扩充线程数量,以及闲时自动回收的功能.mp4 │      高并发编程第一阶段39讲、课程结束,内容回顾,下季内容预告.mp4 │ ├─第二阶段 │       Java并发编程.png │       ppt+源码.rar │       高并发编程第二阶段01讲、课程大纲及主要内容介绍.wmv │       高并发编程第二阶段02讲、介绍四种Singleton方式的优缺点在多线程情况下.wmv │       高并发编程第二阶段03讲、介绍三种高效优雅的Singleto

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值