【JAVA进化论】LV5-6:java并发包-juc的简单介绍

一、线程池

1.1:简单介绍

前面简单提了一嘴线程池,回想下我们前几节内容提到的线程创建:

代码块1
1
2
Thread t1 = new Thread();
t1.start(); //毫无意义的一个线程被创建并触发了,这个线程不会执行任何逻辑

这段简单的代码,会在运行的时候向操作系统申请开启一个线程,当我们利用这个线程把我们的业务代码运行完,线程就会被销毁,再次会议一下线程的生命周期那张图。

但是线程是稀缺资源,当我们的程序里存在大量开启-关闭线程这种操作时,对于系统本身是一种浪费,其次,开启线程是有性能开销的,所以有没有一种方法,可以让线程创建之后,就不再死亡,而是用的时候直接去一个地方拿,用完了也不再销毁它,而是重新放回去,等待别的地方使用呢?

线程池诞生了!

跟它的名字一样,它是个用来存放线程对象的池子,最开始就会往里面放一定量的线程,别人需要使用线程的时候,它便从池子里取到一个闲置的线程用来给别人使用,别人用完了,就回收这个线程,接下来给别人用。

1.2:初始化线程池

简单记一下,有下面三种快速初始化线程池对象的方法:

  • Executors.newCachedThreadPool():无限线程池。
  • Executors.newFixedThreadPool(nThreads):创建固定大小的线程池。
  • Executors.newSingleThreadExecutor():创建单个线程的线程池。

我们目前用newCachedThreadPool的情况比较多,它可以无限创建线程,每个线程被创建后都可以活60s的时间,期间可以被反复使用。

例子:

代码块2
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
public class Test {

//全局声明一个线程池
private static ExecutorService threadPool = Executors.newCachedThreadPool();

public static void main(String[] args) {
//直接启动一个线程来运行async方法
Thread t = new Thread(Test::async);
t.start();

//使用线程池提供的线程运行async方法
threadPool.execute(Test::async); //利用execute方法触发线程运行
}

public static void async() {
System.out.println("线程名为" + Thread.currentThread().getName() + "正在运行...");
}
}

二、ReentrantLock

2.1:简介

前面讲了使用synchronized同步块来解决多线程下操作同一块资源的安全性问题,现在,来认识juc下同样可以实现同步控制的锁:ReentrantLock

ReentrantLock实现了Lock接口,除此之外,相比synchronized只支持非公平锁的特性,它支持灵活配置锁竞争的公平非公平模式,什么是锁竞争的公平与非公平呢?我们继续来拿之前的流程图举例:

图1

2.2:用法

代码块3
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
public class Word {

//全局声明一个重入锁作为成员变量,这样,使用这个变量控制的同步块就跟原先的对象锁功能相同,只在同一个对象下生效
private ReentrantLock lock = new ReentrantLock(false); //参数代表启用公平或者非公平的锁竞争模式,我们这里设置为非公平

//声明为static的情况下,跟之前的类锁相似,只要属于这个类,这个锁则全生效
private static ReentrantLock lockAll = new ReentrantLock(false);

//对象锁,相同对象被多个线程访问才会生效
public void A() {
lock.lock();
try {
System.out.println("A方法被调用");
} finally {
lock.unlock(); //释放锁,为了保证锁释放一定会被执行,一般放到finally块里
}
}

//类锁,只要是Word对象,被多个线程访问时都会上锁
public void B() {
lockAll.lock();
try {
System.out.println("B方法被调用");
} finally {
lock.unlock();
}
}

}

2.3:await、signal、signalAll

同样的ReentrantLock也支持线程通信,我们可以利用ReentrantLock来改写下前面的那个阻塞队列:

代码块4
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
//阻塞队列类
public class BlockQueue {

private List<String> msgs = new LinkedList<>();

private ReentrantLock lock = new ReentrantLock(false);
//不同于synchronized可以利用对象里的wait和notify,想要利用ReentrantLock实现线程通信,需要使用lock对象生成一个condition对象
private Condition condition = lock.newCondition();

public void put(String msg) {
lock.lock(); //将synchronized同步块换成lock的方式进行
try {
msgs.add(msg); //存放进
condition.signalAll(); //效果等于notifyAll
} finally {
lock.unlock();
}
}

public String get() {
lock.lock();
try {
if (msgs.size() == 0) {
try {
condition.await(); //效果等于wait
} catch (InterruptedException e) {
e.printStackTrace();
}
}
return msgs.remove(msgs.size() - 1);
} finally {
lock.unlock();
}
}
}

运行效果跟之前是一样的。

三、利用线程池的Future模式做到跟join一样的效果

还是前面优化大首页的例子,我们不在将Dao层的代码列出来,我们仅看service层的异步实现,对比下不同:

代码块5
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
// 异步获取,通过多线程优化性能
public WebModule getWebModuleMsgSimpleAsync() throws InterruptedException {

WebModule webModule = new WebModule();

Thread topTask = new Thread(() -> webModule.setTop(moduleDao.getTop()));
Thread leftTask = new Thread(() -> webModule.setLeft(moduleDao.getLeft()));
Thread rightTask = new Thread(() -> webModule.setRight(moduleDao.getRight()));
Thread userTask = new Thread(() -> webModule.setUser(moduleDao.getUser()));

//触发各个异步任务
topTask.start();
leftTask.start();
rightTask.start();
userTask.start();

//等待所有的任务均执行完毕
topTask.join();
leftTask.join();
rightTask.join();
userTask.join();

return webModule;
}

这是我们使用普通方式创建线程,然后使用join来完成的异步任务调度,结合最开始说的有关线程池的优点来看这个流程,会发现我们一下子就创建了4个线程,这是有开销的,那么线程池是否也能完成类似这种操作呢?

代码块6
1
2
3
4
5
6
7
8
9
10
11
12
13
//线程池Future模式完成异步操作
public WebModule getWebModuleMsgAsync() throws ExecutionException, InterruptedException {
Future<String> top = threadPool.submit(moduleDao::getTop);
Future<String> left = threadPool.submit(moduleDao::getLeft);
Future<String> right = threadPool.submit(moduleDao::getRight);
Future<String> user = threadPool.submit(moduleDao::getUser);
WebModule webModule = new WebModule();
webModule.setTop(top.get());
webModule.setLeft(left.get());
webModule.setRight(right.get());
webModule.setUser(user.get());
return webModule;
}

上面的代码就是我们利用线程池改造后的代码块5,它的运行效果是和代码块5一致的。

四、异步任务编排:CompletableFuture

有时候,我们需要拿到某个方法的结果后再去拿着这个结果去调用别的方法,这时就不能盲目使用join或者线程池来做异步,juc提供了更高级的类:CompletableFuture

详细用法请查看附件:利用CompletableFuture优化程序的执行效率