JAVA多线程实现的四种方式:
Java多线程实现方式主要有四种:继承Thread类、实现Runnable接口、实现Callable接口通过FutureTask包装器来创建Thread线程、使用ExecutorService、Callable、Future实现有返回结果的多线程。
其中前两种方式线程执行完后都没有返回值,后两种是带返回值的。
Thread类本质上是实现了Runnable接口的一个实例,代表一个线程的实例。启动线程的唯一方法就是通过Thread类的start()实例方法。start()方法是一个native方法,它将启动一个新线程,并执行run()方法。这种方式实现多线程很简单,通过自己的类直接extend Thread,并复写run()方法,就可以启动新线程并执行自己定义的run()方法。
public class MyThread extends Thread{
@Override
public void run() {
System.out.println("Thread");
}
}
如果自己的类已经extends另一个类,就无法直接extends Thread,此时,可以实现一个Runnable接口,如下:
public class RunnableThread implements Runnable{
@Override
public void run() {
System.out.println("runnable");
}
}
其实我们的Thread类也是实现了Runnable接口的,所以第一二种方法他们的本质是一样的去实现了Runnable接口方法。
public class Thread implements Runnable {
...// 省略
}
public class CallableThread implements Callable {
@Override
public Object call() throws Exception {
return "Callable";
}
}
启动线程:
public static void main(String[] args) throws ExecutionException, InterruptedException {
MyThread myThread = new MyThread();
myThread.start();
RunnableThread runnableThread = new RunnableThread();
new Thread(runnableThread).start();
CallableThread callableThread = new CallableThread();
FutureTask oneTask = new FutureTask(callableThread);
new Thread(oneTask).start();
System.out.println(oneTask.get());;
Lock lock = new ReentrantLock();
}
ExecutorService、Callable、Future三个接口实际上都是属于Executor框架。返回结果的线程是在JDK1.5中引入的新特征,有了这种特征就不需要再为了得到返回值而大费周折了。而且自己实现了也可能漏洞百出。
可返回值的任务必须实现Callable接口。类似的,无返回值的任务必须实现Runnable接口。
执行Callable任务后,可以获取一个Future的对象,在该对象上调用get就可以获取到Callable任务返回的Object了。
注意:get方法是阻塞的,即:线程无返回结果,get方法会一直等待。
再结合线程池接口ExecutorService就可以实现传说中有返回结果的多线程了。
下面提供了一个完整的有返回结果的多线程测试例子,在JDK1.5下验证过没问题可以直接使用。代码如下:
@Configuration
@EnableAsync
public class ThreadPool {
@Value("${threadPool.core-pool-size}")
private int corePoolSize;
@Value("${threadPool.max-pool-size}")
private int maxPoolSize;
@Value("${threadPool.keep-alive-time}")
private int keepAliveTime;
@Bean
public ThreadPoolExecutor getExecutor() {
ThreadPoolExecutor executor = new ThreadPoolExecutor(
corePoolSize,
maxPoolSize,
keepAliveTime,
TimeUnit.MILLISECONDS,
new ArrayBlockingQueue<Runnable>(300));
return executor;
}
}
@Autowired
ThreadPoolExecutor executor;
public void execute() throws InterruptedException {
//线程计数器
CountDownLatch threadSignal = new CountDownLatch(5);
//线程池执行方法
Future future = executor.submit(() -> {
// 填充需要执行的方法
});
try {
// 尝试获取执行的一个返回值(如果需要用到返回值的话)
// future.get();
} catch (Exception e) {
//抛异常或者处理异常
}
//等待所有线程执行完成
threadSignal.await();
}
关于Java中线程的生命周期,首先看一下下面这张较为经典的图:
上图中基本上囊括了Java中多线程各重要知识点。掌握了上图中的各知识点,Java中的多线程也就基本上掌握了。主要包括:
Java线程具有五中基本状态
新建状态(New):当线程对象对创建后,即进入了新建状态,如:Thread t = new MyThread();
就绪状态(Runnable):当调用线程对象的start()方法(t.start();),线程即进入就绪状态。处于就绪状态的线程,只是说明此线程已经做好了准备,随时等待CPU调度执行,并不是说执行了t.start()此线程立即就会执行;
运行状态(Running):当CPU开始调度处于就绪状态的线程时,此时线程才得以真正执行,即进入到运行状态。注:就 绪状态是进入到运行状态的唯一入口,也就是说,线程要想进入运行状态执行,首先必须处于就绪状态中;
阻塞状态(Blocked):处于运行状态中的线程由于某种原因,暂时放弃对CPU的使用权,停止执行,此时进入阻塞状态,直到其进入到就绪状态,才 有机会再次被CPU调用以进入到运行状态。根据阻塞产生的原因不同,阻塞状态又可以分为三种:
1.等待阻塞:运行状态中的线程执行wait()方法,使本线程进入到等待阻塞状态;
2.同步阻塞 – 线程在获取synchronized同步锁失败(因为锁被其它线程所占用),它会进入同步阻塞状态;
3.其他阻塞 – 通过调用线程的sleep()或join()或发出了I/O请求时,线程会进入到阻塞状态。当sleep()状态超时、join()等待线程终止或者超时、或者I/O处理完毕时,线程重新转入就绪状态。
死亡状态(Dead):线程执行完了或者因异常退出了run()方法,该线程结束生命周期。
线程池和数据库连接池其实是差不多的,因为某些操作是十分消耗系统的性能和时间的,各种创建连接,断开连接这样的操作,为了避免这些问题,线程池和数据库连接池也就随之诞生了,在我们启动程序的时候他就已经准备好了一定数量的线程可以直接提供给我们使用,用完后又放回去可以供其他方法调用,这样就大大节省了系统对这些新建和销毁的性能消耗。
阿里的《Java 开发手册》中是这样规定线程池的:
线程池不允许使用 Executors 去创建,而是通过 ThreadPoolExecutor 的方式,这样的处理方式让写的读者更加明确线程池的运行规则,规避资源耗尽的风险。
Executors 返回的线程池对象的弊端如下:
FixedThreadPool 和 SingleThreadPool:允许的请求队列长度为 Integer.MAX_VALUE,可能会堆积大量的请求,从而导致 OOM。
CachedThreadPool 和 ScheduledThreadPool:允许的创建线程数量为 Integer.MAX_VALUE,可能会创建大量的线程,从而导致 OOM。
且Executors的创建线程池方法底层都是通过 ThreadPoolExecutor 实现的。
ThreadPoolExcutor的核心参数
public ThreadPoolExecutor(int corePoolSize, //常驻核心线程数
int maximumPoolSize, //任务最多时最大线程数
long keepAliveTime, //核心线程之外线程的存活时间
TimeUnit unit, //存活时间的单位
BlockingQueue<Runnable> workQueue, //线程池执行的任务队列
ThreadFactory threadFactory, //线程的创建工厂
RejectedExecutionHandler handler) //线程池的拒绝策略
{
}
第 1 个参数:corePoolSize 表示线程池的常驻核心线程数。如果设置为 0,则表示在没有任何任务时,销毁线程池;如果大于 0,即使没有任务时也会保证线程池的线程数量等于此值。
第 2 个参数:maximumPoolSize 表示线程池在任务最多时,最大可以创建的线程数。此值必须大于 0,也必须大于等于 corePoolSize.
第 3 个参数:keepAliveTime 表示线程的存活时间,当线程池空闲时并且超过了此时间,多余的线程就会销毁,直到线程池中的线程数量销毁的等于 corePoolSize 为止,如果 maximumPoolSize 等于 corePoolSize,那么线程池在空闲的时候也不会销毁任何线程。
第 4 个参数:unit 表示存活时间的单位,它是配合 keepAliveTime 参数共同使用的。
第 5 个参数:workQueue 表示线程池执行的任务队列,当线程池的所有线程都在处理任务时,如果来了新任务就会缓存到此任务队列中排队等待执行。
第 6 个参数:threadFactory 表示线程的创建工厂,此参数一般用的比较少,我们通常在创建线程池时不指定此参数,它会使用默认的线程创建工厂的方法来创建线程。可以通过实现 ThreadFactory 接口来自定义一个线程工厂,这样就可以自定义线程的名称或线程执行的优先级了。
第 7 个参数:RejectedExecutionHandler 表示指定线程池的拒绝策略,当线程池的任务已经在缓存队列 workQueue 中存储满了之后,并且不能创建新的线程来执行此任务时,就会用到此拒绝策略,它属于一种限流保护的机制。Java 自带的拒绝策略有 4 种:
AbortPolicy,终止策略,线程池会抛出异常并终止执行,它是默认的拒绝策略;
CallerRunsPolicy,把任务交给当前线程来执行;
DiscardPolicy,忽略此任务(最新的任务);
DiscardOldestPolicy,忽略最早的任务(最先加入队列的任务)。
@Configuration
@EnableAsync
public class ThreadPool {
@Value("${threadPool.core-pool-size}")
private int corePoolSize;
@Value("${threadPool.max-pool-size}")
private int maxPoolSize;
@Value("${threadPool.keep-alive-time}")
private int keepAliveTime;
@Bean
public ThreadPoolExecutor getExecutor() {
ThreadPoolExecutor executor = new ThreadPoolExecutor(
corePoolSize,
maxPoolSize,
keepAliveTime,
TimeUnit.MILLISECONDS,
new ArrayBlockingQueue<Runnable>(300));
return executor;
}
}
简单的使用:
// 注入线程池
@Autowired
ThreadPoolExecutor executor;
public void execute() throws InterruptedException {
//线程计数器
CountDownLatch threadSignal = new CountDownLatch(5);
//线程池执行方法
Future future = executor.submit(() -> {
// 填充需要执行的方法
});
try {
// 尝试获取执行的一个返回值(如果需要用到返回值的话)
future.get();
} catch (Exception e) {
//抛异常或者处理异常
}
//等待所有线程执行完成
threadSignal.await();
}