先上总结:
1.使用实现多线程有四种方式:①继承Thread类;②实现Runnable接口;③使用Callable和FutureTask实现有返回值的多线程;④使用ExecutorService和Executors工具类实现线程池(如果需要线程的返回值,需要在线程中实现Callable和Future接口)
2.继承Thread类的优点:简单,且只需要实现父类的run方法即可(start方法中含有run方法,会创建一个新的线程,而run是执行当前线程)。缺点是:Java的单继承,如果对象已经继承了其他的类则不能使用该方法。且不能获取线程的返回值
3.实现Runnable接口优点:简单,实现Runnable接口必须实现run方法。缺点:创建一个线程就必须创建一个Runnable的实现类,且不能获取线程的返CallabTask优点:可以获取多线程的返回值。缺点:每个多线程都需要创建一个Callable的实现类
4.线程池ExecutorService和工具类Executors优点:可以根据实际情况创建线程数量,且只需要创建一个线程池即可,也能够通过Callable和Future接口得到线程的返回值,程序的执行时间与线程的数量紧密相关。缺点:需要手动销毁该线程池(调用shutdown方法)。
尽量不要使用 继承Thread类 和 实现Runnable接口;尽量使用线程池。否则项目导出都是线程。
在上代码:
package com.swain.programmingpearls.thread;
import java.util.concurrent.Callable;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.FutureTask;
/**
* thread 的几种实现
*/
public class threadTest {
public static void main (String[] args) {
//继承thread
ExtendsThread extendsThread = new ExtendsThread();
extendsThread.start();
//实现runnable
Thread runThread = new Thread(new AchieveRunnable());
runThread.start();
//调用callable 可以有返回值 可以捕获异常
Callable<String> tc = new TestCallable();
FutureTask<String> task = new FutureTask<String>(tc);
new Thread(task).start();
try {
System.out.println(task.get());//获取返回值
} catch (InterruptedException | ExecutionException e) {
e.printStackTrace();
}
//runable 匿名内部类方式
new Thread(new Runnable() {
@Override
public void run() {
System.out.println("实现Runnable 匿名内部类方式:" + Thread.currentThread().getName());
}
}).start();
//runnable Lamda表达式
new Thread(()->{
for (int i = 0; i < 5; i++) {
System.out.println("Lamda表达式:" + i);
}
}).start();
System.out.println("主线程");
//创建线程池
ExecutorService executorService = Executors.newFixedThreadPool(2);
for(int i = 0; i<5; i++)
{
AchieveRunnable achieveRunnable = new AchieveRunnable();
try {Thread.sleep(1000);} catch (InterruptedException e) {}
executorService.execute(achieveRunnable);
}
//关闭线程池
executorService.shutdown();
}
/**
* 继承thread类
*/
public static class ExtendsThread extends Thread {
public void run(){
System.out.println("方法一 继承Thread线程:" + Thread.currentThread().getName());
}
}
/**
* 实现runnable
*/
public static class AchieveRunnable implements Runnable {
@Override
public void run() {
System.out.println("方法二 实现Runnable:" + Thread.currentThread().getName());
}
}
/**
* 通过Callable和FutureTask创建线程
*/
public static class TestCallable implements Callable<String> {
@Override
public String call() throws Exception {
System.out.println("方法三 实现callable:" + Thread.currentThread().getName());
return "我是callable的返回";
}
}
}
运行结果:
方法一 继承Thread线程:Thread-0
方法二 实现Runnable:Thread-1
方法三 实现callable:Thread-2
我是callable的返回
实现Runnable 匿名内部类方式:Thread-3
主线程
Lamda表达式:0
Lamda表达式:1
Lamda表达式:2
Lamda表达式:3
Lamda表达式:4
方法二 实现Runnable:pool-1-thread-1
方法二 实现Runnable:pool-1-thread-2
方法二 实现Runnable:pool-1-thread-1
方法二 实现Runnable:pool-1-thread-2
方法二 实现Runnable:pool-1-thread-1
关于Concurrent包
concurrent包是在AQS的基础上搭建起来的,AQS提供了一种实现阻塞锁和一系列依赖FIFO等待队列的同步器的框架。
线程池参数
我们常用的主要有newSingleThreadExecutor、newFixedThreadPool、newCachedThreadPool、调度等,使用Executors工厂类创建。
newSingleThreadExecutor可以用于快速创建一个异步线程,非常方便。而newCachedThreadPool永远不要用在高并发的线上环境,它用的是无界队列对任务进行缓冲,可能会挤爆你的内存。
我习惯性自定义ThreadPoolExecutor,也就是参数最全的那个。
假如我的任务可以预估,corePoolSize,maximumPoolSize一般都设成一样大的,然后存活时间设的特别的长。可以避免线程频繁创建、关闭的开销。I/O密集型和CPU密集型的应用线程开的大小是不一样的,一般I/O密集型的应用线程就可以开的多一些。
threadFactory我一般也会定义一个,主要是给线程们起一个名字。这样,在使用jstack等一些工具的时候,能够直观的看到我所创建的线程。
监控
高并发下的线程池,最好能够监控起来。可以使用日志、存储等方式保存下来,对后续的问题排查帮助很大。
通常,可以通过继承ThreadPoolExecutor,覆盖beforeExecute、afterExecute、terminated方法,达到对线程行为的控制和监控。
阻塞队列
阻塞队列会对当前的线程进行阻塞。当队列中有元素后,被阻塞的线程会自动被唤醒,这极大的提高的编码的灵活性,非常方便。在并发编程中,一般推荐使用阻塞队列,这样实现可以尽量地避免程序出现意外的错误。阻塞队列使用最经典的场景就是socket数据的读取、解析,读数据的线程不断将数据放入队列,解析线程不断从队列取数据进行处理。
ArrayBlockingQueue对访问者的调用默认是不公平的,我们可以通过设置构造方法参数将其改成公平阻塞队列。
LinkedBlockingQueue队列的默认最大长度为Integer.MAX_VALUE,这在用做线程池队列的时候,会比较危险。
SynchronousQueue是一个不存储元素的阻塞队列。每一个put操作必须等待一个take操作,否则不能继续添加元素。队列本身不存储任何元素,吞吐量非常高。对于提交的任务,如果有空闲线程,则使用空闲线程来处理;否则新建一个线程来处理任务”。它更像是一个管道,在一些通讯框架中(比如rpc),通常用来快速处理某个请求,应用较为广泛。
DelayQueue是一个支持延时获取元素的无界阻塞队列。放入DelayQueue的对象需要实现Delayed接口,主要是提供一个延迟的时间,以及用于延迟队列内部比较排序。这种方式通常能够比大多数非阻塞的while循环更加节省cpu资源。
另外还有PriorityBlockingQueue和LinkedTransferQueue等,根据字面意思就能猜测它的用途。在线程池的构造参数中,我们使用的队列,一定要注意其特性和边界。比如,即使是最简单的newFixedThreadPool,在某些场景下,也是不安全的,因为它使用了无界队列。
CountDownLatch
假如有一堆接口A-Y,每个接口的耗时最大是200ms,最小是100ms。
我的一个服务,需要提供一个接口Z,调用A-Y接口对结果进行聚合。接口的调用没有顺序需求,接口Z如何在300ms内返回这些数据?
此类问题典型的还有赛马问题,只有通过并行计算才能完成问题。归结起来可以分为两类:
- 实现任务的并行性
- 开始执行前等待n个线程完成任务
CountDownLatch是通过一个计数器来实现的,计数器的初始值为线程的数量。每当一个线程完成了自己的任务后,计数器的值就会减1。当计数器值到达0时,它表示所有的线程已经完成了任务,然后在闭锁上等待的线程就可以恢复执行任务。
CyclicBarrier与其类似,可以实现同样的功能。不过在日常的工作中,使用CountDownLatch会更频繁一些。
信号量
Semaphore虽然有一些应用场景,但大部分属于炫技,在编码中应该尽量少用。
信号量可以实现限流的功能,但它只是常用限流方式的一种。其他两种是漏桶算法、令牌桶算法
Lock && Condition
在Java中,对于Lock和Condition可以理解为对传统的synchronized和wait/notify机制的替代。concurrent包中的许多阻塞队列,就是使用Condition实现的。
End
不管是wait、notify,还是同步关键字或者锁,能不用就不用,因为它们会引发程序的复杂性。最好的方式,是直接使用concurrent包所提供的机制,来规避一些编码方面的问题。