导航
- 导读
- 基础:多线程写法
- 方式一:Runnable
- 方式二:Callable
- 方式三:线程池的简单使用
- 例1. 三种JDK内置线程池的简单使用
- 例2. newFixedThreadPool+Future
- 例3. cachedThreadPool+CountDownLatch
- 进阶:多线程基本原理入门
- 从FutureTask的实现开始
- 认识阻塞和线程通信
导读
小白很着急,多线程的原理可以以后慢慢探索,但是工作要先完成,希望这篇文章有所帮助。
基础:多线程写法
方式一:Runnable
开启一个线程最简单的写法:
new Thread(() -> {
businessTask(); // 业务代码
}).start();
这里用lambda表达式代替了匿名函数,如果不用lambda:
new Thread(new Runnable() {
@Override
public void run() {
businessTask(); // 业务代码
}
}).start();
再换种写法:
Runnable runnable1 = new Runnable(
@Override
public void run() {
businessTask();
}
);
new Thread(runnable1).start();
总结:这种方式分为两步,第一步用Runnable将业务逻辑包装起来,第二步交给Thread执行。
方式二:Callable
如果使用上面的方法,我们发现如果线程有返回结果是没办法获取的。这个时候就要用到jdk提供的一套API:future/callable。
new Thread(new Callable() {
@Override
public String call() throw Exception {
String result = businessTask(); // 假设业务逻辑返回结果是一个String
return result;
}
}).start();
如何得到线程执行的返回:
Callable callable1 = new Callable(
@Override
public String call() throw Exception {
String result = businessTask();
return result;
}
);
FutureTask<String> futureTsk = new FutureTask<>(callble1);
new Thread(futureTsk).start();
System.out.println(futureTsk.get()) // get方法返回callable1的返回值
总结:结合代码,Callable和Runnable有以下 的区别和联系:
- Callable方式可以使用get方法获取返回值,还可以抛异常,但是Runnable不行;
- Callable对象不能直接丢给Thread执行,而是要先包装成一个FutureTask对象。
方式三:线程池的简单使用
上面两种方法都是开启一个线程,如果要开启较多的线程,就要用到线程池了。下面展示线程池使用的三个简单例子:
例1. 三种JDK内置线程池的简单使用
// 三种JDK内置线程池的创建和简单使用
public class ThreadPoolTest {
private static ExecutorService cachedThreadPool = Executors.newCachedThreadPool();
private static ExecutorService fixedThreadPool = Executors.newFixedThreadPool(5);
private static ExecutorService singleThreadPool = Executors.newSingleThreadExecutor();
public void test(){
for (int i = 0; i < 50; i++) {
int finalI = i;
// 使用不通的线程池执行50次简单任务:打印一句话并sleep 100ms
cachedThreadPool.execute(()->{
System.out.println(Thread.currentThread().getName() + "--" + finalI);
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
});
}
System.out.println("main方法线程执行结束");
}
}
例2. newFixedThreadPool+Future
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
@Service
public Class BusinessTask{
static ExecutorService threadPool= Executors.newFixedThreadPool(10); // 开启包含10个线程的线程池
// 第一个线程任务,返回类型String
Future<String> callable1 = threadPool.submit(() -> {
String result = businessTask1();
return result;
});
// 第二个线程任务,返回类型JSONObject
Future<JSONObject> callable2 = threadPool.submit(() -> {
JSONObject resObj = businessTask2();
return resObj ;
});
// 第三、四...十个线程任务
...
// 获取线程执行结果(放在try块中执行)
System.out.println(callable1.get());
System.out.println(callable2.get());
}
例3. cachedThreadPool+CountDownLatch
实战中往往单独写一个初始化线程池单例的工具类:
public class CachedThreadPoolProvider {
private static CachedThreadPoolProvider instance;
private ExecutorService cachedThreadPool = Executors.newCachedThreadPool();
public ExecutorService getCachedThreadPool() {
return cachedThreadPool;
}
public static CachedThreadPoolProvider getInstance() {
// 双重锁检测
if (instance == null) {
synchronized (CachedThreadPoolProvider.class){
if (instance == null) {
instance = new CachedThreadPoolProvider();
}
}
}
return instance;
}
}
业务代码:
@Service
public Class BusinessTask{
private ExecutorService cachedThreadPool;
@PostConstruct
public void init(){
cachedThreadPool = CachedThreadPoolProvider.getInstance().getCachedThreadPool();
}
// 假设bussinessTaskNum是个待执行的业务编号列表
CountDownLatch cdl = new CountDownLatch(bussinessTaskNum.size());
for (String bussinessTask: bussinessTaskNum) {
cachedThreadPool.execute(() -> {
try {
// 业务代码...
executeBussinessTask(bussinessTask);
} catch (Exception e) {
logError(e);
} finally {
cdl.countDown();
}
});
}
try {
// 阻塞,等待全部任务执行完
cdl.await();
} catch (Exception e) {
Cat.logError(e);
}
}
总结 :
- 使用线程池使代码更简洁了
- CachedThreadPool和FixedThreadPool是java提供的两种线程池。关于线程池不是本文重点,这里不再赘述可自行百度或参考本人的另一篇笔记:JAVA线程池学习小结和源码初探
- 线程池一般只需要初始化一次,因此上面例2中使用单独一个工具类来初始化线程池是一种常见操作。
- 线程池经常会和CountDownLatch搭配使用。
进阶:多线程基本原理入门
从FutureTask的实现开始
先看一下java里的Runnable是什么样的:
不会8就这?一个接口+一个抽象方法就完了?我上我也行。下面本公子亲自模仿一个Runnable的实现——FutureTask(下面栗子中命名为dustinFutureTask)。
public class dustinFutureTask<T> implements Runnable{
Callable<T> callable; // callable里包装了业务逻辑
T result; // 业务逻辑返回值
String state = "NEW"; // 当前线程task状态,用户get方法中的阻塞功能
LinkedBlockingQueue<Thread> waiters = new LinkedBlockingQueue<>(); // 线程列表,便于后续线程通信
public dustinFutureTask(Callable<T> callable){
this.callable = callable;
}
@Override
public void run(){
// run方法是java多线程调用,或者说执行业务逻辑的唯一方法
try{
T result = callable.call();
}catch(Exception e){
...
}finally{
state = "END";
}
// “生产者”生产出返回结果后,通知“消费者”。
// 解除所有线程的阻塞
while(true){
Thread waiter = waiters.poll();
if(waiter == null){
break;
}
LockSupport.unpark(waiter);
}
}
// get方法中会包含阻塞(等待结果返回)的逻辑
public T get(){
// 阻塞逻辑:如果task没有结束,等待task结束。此时将外部主线程状态置为[WAITING]
while(!"END".equals(state)){
waiters.add(Thread.currentThread());
LockSupport.park(Thread.currentThread()); // 阻塞当前线程
}
}
}
几个值得注意的地方总结 :
- 为什么要实现Runnable?很好理解,java的多线程Thread只接受Runnable类型;
- T是泛型,简单理解下,就是“泛指”多线程run方法返回值的类型;
- 关于构造函数的实现。想一下什么时候出现过:
FutureTask<String> futureTsk = new FutureTask<>(callble1)
; - 多线程执行业务逻辑的时候,Thread调用run()方法,而run()内部则调用了callable的call()方法;
- Callable vs Runnable。区别在于前者有返回值、可以抛异常,上面也提到过此处不再赘言。除此之外在FutureTask的实现中,它们有个重要联系:Runnable的run方法会调用Callable对象,执行其call()方法。
- 关于get()方法内部的阻塞:get方法需要等待callable.call()方法的返回结果,因此必须有阻塞逻辑。详细内容接下来讲。
- 最后是线程池的创建和使用方式,本例中采取了
Executors.newXXXThreadPool()
的方式,既原始又有隐患。实际情况中我们一般使用ThreadPoolExecutor
的构造方法创建和使用线程池,这里不再多说。
认识阻塞和线程通信
Obviously,get方法需要等待callable的call方法的返回结果,因此需要有一个阻塞的逻辑。通过代码看到,futureTask设置了一个属性"state",当callable的call方法有返回结果的时候,更新这个state(这里引申出一个线程状态的问题,可自行百度java线程的6种状态)。
但是说起来容易实现起来并不简单。我是个小白,直观想到了while循环:如果state没更新就一直循环!后果就是老板看到直呼外行,如果没返回岂不是成了死循环?专业的方法是怎么做的呢?线程通信。
讲到线程通信,首先要理清一个问题。执行callable的call方法的线程,和调用get方法获取返回结果的线程并不是同一个线程!回想下:调用callable.call方法(也就是调用futureTask对象run方法)的线程是新开的子线程(记为thread-0),而调用get方法的是外部主线程(记为thread-main)。
先讲线程的阻塞。代码中get()方法的注释也写的比较详细了,阻塞一个线程有很多种方法,这里选择了:LockSupport.park(Thread.currentThread())
。其中LockSupport.park方法会将当前线程置为【WAITING】状态(不止这一种方法可自行百度)。
再谈线程的通信。这里的线程通信很简单:来看下run方法,注释也写到了,当run方法有了返回结果之后,使用LockSupport.unpark(waiter)
解除所有线程的阻塞状态,这就是线程的通信。注意一点,之前也提到了,执行run方法和执行get方法的是不同线程,于是借助了waiters这个中间变量(其实就是把不同线程都保存在一个公共列表中)。
最后提一句,这其实就是个经典的生产者-消费者模型。谁是生产者谁是消费者我就不多说了8。