要怎样提升性能
提升性能的方法有很多种 大到从架构上优化 小到在代码上优化 今天要写的就是在代码上去优化性能 对于我来说 优化的第一反应自然是多线程啦 只要能充分利用CPU 免去不必要的阻塞等待 性能一般都不会差 所以今天就在一个查询电影余票数与价格的小案例基础上进行一步一步的优化 废话不多说直接上实战
实战
首先准备好基础设施 创建一个Film类
/**
* 电影
* Created by YiMing on 2017/7/5.
*/
public class Film {
private String name;
public Film(String name) {
this.name = name;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
}
创建一个FilmService类 用于模拟调用远程查询余票与价格
/**
* Created by YiMing on 2017/7/5.
*/
public class FilmService {
}
在FilmService类中增加查询余票数与价格的方法
/**
* 获取余票数
*
* @return
*/
public Integer getTicketCount(Film film) {
// 模拟调用远程获取余票数接口花费时间
delay();
// 随机返回0~50张余票
return (int) (Math.random() * 51);
}
/**
* 获取票价
*
* @return
*/
public Integer getTicketPrice(Film film) {
// 模拟调用远程获取票价接口花费时间
delay();
// 随机返回30~80的价格
return (int) (Math.random() * 51) + 30;
}
/**
* 模拟花费时间
*
* @return
*/
private static void delay() {
try {
Thread.sleep(1000l);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
好 基本类和方法都写好了 那么开始实战
需求1: 查询电影
变形金刚5
的余票数与票价
实现:
Film film = new Film("变形金刚5");
long start = System.currentTimeMillis();
int ticketCount = filmService.getTicketCount(film);
int ticketPrice = filmService.getTicketPrice(film);
System.out.println(film.getName() + "的票价为:" + ticketPrice + ",目前剩余" + ticketCount + "张票");
System.out.println("耗时:" + (System.currentTimeMillis() - start));
打印得出结果:
变形金刚5的票价为:33,目前剩余45张票
耗时:2001
来分析一下 首先调用getTicketCount()方法需要1秒的计算时间(delay模拟),调用getTicketPrice()方法也需要1秒的时间 加起来一共耗时2秒 所以打印出的结果在意料之中 那怎么去优化呢 刚才说过一点 那就是免去不必要的阻塞 如果我在查询余票数的时候 不去等待余票数的结果 而是立刻去查询票价 这样是不是就可以省去了一部分阻塞等待的时间? 理论上行得通 那么来尝试一下 将同步查询票价与余票数的接口改为异步的API
实现如下:
/**
* 异步获取余票数
*
* @return
*/
public CompletableFuture<Integer> getTicketCountAsync(Film film) {
return CompletableFuture.supplyAsync(() -> getTicketCount(film));
}
/**
* 异步获取电影价格
*
* @return
*/
public CompletableFuture<Integer> getTicketPriceAsync(Film film) {
return CompletableFuture.supplyAsync(() -> getTicketPrice(film));
}
接下来在客户端进行调用测试:
Film film = new Film("变形金刚5");
long start = System.currentTimeMillis();
Future<Integer> ticketCount = filmService.getTicketCountAsync(film);
Future<Integer> ticketPrice = filmService.getTicketPriceAsync(film);
System.out.println(film.getName() + "的票价为:" + ticketPrice.get() + ",目前剩余" + ticketCount.get() + "张票");
System.out.println("耗时:" + (System.currentTimeMillis() - start));
打印结果:
变形金刚5的票价为:39,目前剩余50张票
耗时:1150
果然如此! 省去了将近一半的时间 如果在企业级开发中能提升1秒的查询性能 那可以说是相当可观的 接下来再看需求2
需求2: 查询电影列表的所有票价
实现:
/**
* 获取电影价格列表
*
* @return
*/
public List<Integer> getTicketPriceList(List<Film> films) {
return films.stream().map(f -> getTicketPrice(f)).collect(toList());
}
在测试之前我们先估测一下 以同步的方式去调用接口 每个接口耗时1秒 那么查询8个电影 至少耗时8秒 是否如此呢 往下走
客户端调用测试:
// 初始化包含8个电影的列表
List<Film> films = IntStream.rangeClosed(1, 8).boxed().map(x -> "电影" + x).map(Film::new).collect(toList());
long start = System.currentTimeMillis();
List<Integer> prices = filmService.getTicketPriceList(films);
System.out.println(prices);
System.out.println("耗时:" + (System.currentTimeMillis() - start));
打印结果:
[78, 55, 49, 49, 74, 40, 71, 53]
耗时:8004
结果在意料之中 那么怎么去优化呢
首先尝试使用并行流的方式 充分利用CPU的所有核
/**
* 并行获取电影价格列表
*
* @return
*/
public List<Integer> getTicketPriceListParallel(List<Film> films) {
return films.stream().parallel().map(f -> getTicketPrice(f)).collect(toList());
}
客户端调用:
long start = System.currentTimeMillis();
List<Integer> prices = filmService.getTicketPriceListParallel(films);
System.out.println(prices);
System.out.println("耗时:" + (System.currentTimeMillis() - start));
打印结果:
[44, 31, 33, 40, 53, 55, 61, 80]
耗时:1118
从8秒变成1秒 可以说是相当666了 是不是很激动
那么接下来做一个小小的改动
// 初始化包含9个电影的列表
List<Film> films = IntStream.rangeClosed(1, 9).boxed().map(x -> "电影" + x).map(Film::new).collect(toList());
把电影列表的数量增加到9
再来看看打印结果:
[63, 78, 33, 63, 53, 32, 56, 44, 43]
耗时:2140
时间竟然翻倍了 这样的优化结果显然不是最优选择 在追求最优之前 先思考一下为什么仅仅增加了一个电影 查询时间就翻倍了呢
事实上这跟处理器的核数有关 通过
Runtime.getRuntime().availableProcessors()
这个方法可以得知处理器的核数 我的是8核 默认的线程池是8个线程 所以使用并行的方式时 如果有8个电影 正好可以并行运行在8个线程上 但如果多了一个电影 也就没有多余的线程去计算 因此最后那个电影只能等待某个线程处理完毕之后才能进行计算 所以前8个电影耗时1秒多 最后一个耗时1秒多 一共就是2秒多了 如果因此去修改默认的线程池 并不是一个理想的办法 所以要尝试其他的方式进行进一步优化
在实战1中采用了异步的方式 那接下来尝试异步 是否能带来惊喜
/**
* 异步获取电影价格列表
*
* @return
*/
public List<Integer> getTicketPriceListAsync(List<Film> films) {
return films.stream().map(f -> CompletableFuture.supplyAsync(() -> getTicketPrice(f))).collect(toList())
.stream().map(CompletableFuture::join).collect(toList());
}
客户端调用:
List<Film> films = IntStream.rangeClosed(1, 8).boxed().map(x -> "电影" + x).map(Film::new).collect(toList());
long start = System.currentTimeMillis();
List<Integer> prices = filmService.getTicketPriceListAsync(films);
System.out.println(prices);
System.out.println("耗时:" + (System.currentTimeMillis() - start));
打印结果:
[40, 78, 72, 50, 80, 78, 50, 38]
耗时:2125
似乎与并行流的方式相差不多 尝试着将电影数量修改成20 或者其他 也仍然相差不多 究其原因都一样:它们内部采用的是同样的通用线程池 默认都使用固定数目的线程 具体线程数取决于Runtime. getRuntime().availableProcessors()的返回值 所以为了打破这个“魔咒” 决定定制一个新的线程池执行器
Executor executor = Executors.newFixedThreadPool(Math.min(films.size(), 80), Thread::new);
线程池内的线程数暂时以电影列表的数量为准 也给了个最高上限80 接下来使用定制的线程池进行查询
/**
* 使用线程池异步获取电影价格列表
*
* @return
*/
public List<Integer> getTicketPriceListAsyncExecutor(List<Film> films) {
return films.stream().map(f -> CompletableFuture.supplyAsync(() -> getTicketPrice(f), executor))
.collect(toList()).stream().map(CompletableFuture::join).collect(toList());
}
客户端走一个:
// 初始化包含20个电影的列表
List<Film> films = IntStream.rangeClosed(1, 20).boxed().map(x -> "电影" + x).map(Film::new).collect(toList());
long start = System.currentTimeMillis();
List<Integer> prices = filmService.getTicketPriceListAsyncExecutor(films);
System.out.println(prices);
System.out.println("耗时:" + (System.currentTimeMillis() - start));
打印结果:
[74, 34, 58, 38, 79, 68, 68, 46, 42, 51, 47, 69, 65, 64, 79, 51, 61, 65, 48, 56]
耗时:1000
结果相当激动人心啊 查询20部电影的票价 只花了1秒的时间 如果采用最原始的同步的方式 至少要20秒以上 这可以说是一种质变了!
需求3: 查询电影列表的所有余票数与票价
为方便展示 先创建一个FilmItem类 用于展示票数与价格的信息
/**
* Created by YiMing on 2017/7/5.
*/
public class FilmItem {
private Integer ticketCount;
private Integer ticketPrice;
public FilmItem(Integer ticketCount, Integer ticketPrice) {
this.ticketCount = ticketCount;
this.ticketPrice = ticketPrice;
}
public FilmItem() {
}
public Integer getTicketCount() {
return ticketCount;
}
public void setTicketCount(Integer ticketCount) {
this.ticketCount = ticketCount;
}
public Integer getTicketPrice() {
return ticketPrice;
}
public void setTicketPrice(Integer ticketPrice) {
this.ticketPrice = ticketPrice;
}
@Override
public String toString() {
return "余票数为:" + this.ticketCount + " 价格为:" + this.ticketPrice;
}
}
编写同步获取票数与价格的方法
/**
* 获取电影价格列表
*
* @return
*/
public List<FilmItem> getFilmItemList(List<Film> films) {
return films.stream().map(f -> new FilmItem(getTicketCount(f), getTicketPrice(f))).collect(toList());
}
客户端调用
List<Film> films = IntStream.rangeClosed(1, 8).boxed().map(x -> "电影" + x).map(Film::new).collect(toList());
long start = System.currentTimeMillis();
List<FilmItem> filmItems = filmService.getFilmItemList(films);
System.out.println(filmItems);
System.out.println("耗时:" + (System.currentTimeMillis() - start));
此时查询每部电影会调用查询余票数与价格两个接口 每个接口耗时1秒 也就是说每部电影耗时约2秒+ 8部至少16秒+ 接下来看结果:
[余票数为:50 价格为:78, 余票数为:28 价格为:49, 余票数为:27 价格为:32, 余票数为:1 价格为:68, 余票数为:4 价格为:77, 余票数为:18 价格为:80, 余票数为:36 价格为:46, 余票数为:37 价格为:51]
耗时:16105
确实如此 有过前两次的优化经验 那么这次 自然也是手到擒来了!
优化查询方法:
/**
* 异步获取电影价格与余票数列表
*
* @return
*/
public List<FilmItem> getFilmItemListAsync(List<Film> films) {
return films.stream()
.map(f -> CompletableFuture.supplyAsync(() -> getTicketCount(f), executor)
.thenCombine(CompletableFuture.supplyAsync(() -> getTicketPrice(f), executor), FilmItem::new))
.collect(toList()).stream().map(CompletableFuture::join).collect(toList());
}
这一次我们同时查询40部电影的票价与余票数信息 来感受一下优化的成果!
List<Film> films = IntStream.rangeClosed(1, 40).boxed().map(x -> "电影" + x).map(Film::new).collect(toList());
long start = System.currentTimeMillis();
List<FilmItem> filmItems = filmService.getFilmItemListAsync(films);
System.out.println(filmItems);
System.out.println("耗时:" + (System.currentTimeMillis() - start));
打印结果:
[余票数为:11 价格为:38, 余票数为:12 价格为:78, 余票数为:35 价格为:37, 余票数为:9 价格为:68,
余票数为:32 价格为:67, 余票数为:40 价格为:73, 余票数为:18 价格为:42, 余票数为:9 价格为:50,
余票数为:21 价格为:50, 余票数为:2 价格为:69, 余票数为:34 价格为:71, 余票数为:25 价格为:43,
余票数为:22 价格为:50, 余票数为:11 价格为:64, 余票数为:13 价格为:33, 余票数为:18 价格为:49,
余票数为:41 价格为:69, 余票数为:49 价格为:34, 余票数为:40 价格为:48, 余票数为:47 价格为:58,
余票数为:49 价格为:32, 余票数为:15 价格为:32, 余票数为:33 价格为:64, 余票数为:37 价格为:59,
余票数为:2 价格为:65, 余票数为:43 价格为:47, 余票数为:19 价格为:59, 余票数为:38 价格为:72,
余票数为:49 价格为:32, 余票数为:21 价格为:50, 余票数为:22 价格为:41, 余票数为:46 价格为:77,
余票数为:15 价格为:52, 余票数为:32 价格为:73, 余票数为:2 价格为:33, 余票数为:47 价格为:69,
余票数为:1 价格为:38, 余票数为:1 价格为:31, 余票数为:19 价格为:60, 余票数为:3 价格为:73]
耗时:1028
同时查询40部电影的票价与余票数信息耗时1秒 这酸爽?
最后
今天的博客就到这里 虽然用多线程异步的方式可以优化性能 但是也要注意不能肆无忌惮的去创建线程 今天的案例创建那么多线程只是为了起到演示作用 在正式使用上 要注意线程的数量 多使用线程池 以免造成不必要的资源浪费 下次再写一篇关于异步回调+事件驱动的方式 在很多情况下要比多线程异步更优! 最后也欢迎大家和我分享自己的编程经验 本人能力有限 如果有写的不对或是不妥的地方也欢迎大家积极指出来 共同学习 共同进步。