Java多线程编程学习总结(一)
(尊重劳动成果,转载请注明出处:https://blog.csdn.net/qq_25827845/article/details/80672328冷血之心的博客)
前序:
在2017年参加的大小校招面试过程中,本人也曾经死啃Java多线程编程,抱着一本书天天背诵各种理论知识,详情请见
Java多线程编程实战指南(核心篇)读书笔记(一) 等系列知识概念总结文章。说来惭愧呀,当时觉得自己对多线程也有点儿了解了,基本可以和面试官进行一定的沟通和交流了。然而实际工作中,当我遇到一个简单的多线程问题时,依然花费了大量的时间和精力才调试成功。突然觉得多线程当真是鬼神莫测,学习起来是一回事,coding起来是一回事,实际运行的时候又是一回事。鉴于此,我将总结一些与本人本次工作中使用到的多线程技术相关的知识点。
在本篇博客中,首先需要了解Java Executor框架,在JDK1.5中出现了java.util.concurrent.Executor接口,该接口对任务的执行进行了抽象,接口中只有execute一个方法,即:
void execute(Runnable command)
看一下api中的解释:
介绍:command参数代表需要执行的任务,Executor接口使得任务的提交和任务的执行解耦,调用方只需要执行Executor.execute方法即可以使得指定的任务command被执行,无需关心任务的具体执行细节。
缺点:Executor接口无法将执行任务的结果返回给调用方;Executor内部维护的工作者线程(真正执行任务的线程)并不能够被其主动停掉并且释放所占的资源。
有了缺点,就会有新的改进接口出现,是的,请看Executor接口的继承图:
在其子接口ExecutorService中,定义了submit方法,可以接受Callable接口或者Runnable接口表示的任务,并且可以返回相应的Future实例。通过Future,调用方可以获得线程的执行结果;该ExecutorService接口中,还定义了shutdown和shutdownNow方法来关闭相应的工作者线程。具体可见api方法示意:
submit方法:
关闭工作者线程的方法:
鉴于ExecutorService接口的submit方法可以接受Runnable和Callable接口的任务,我们先来比较下Runnable和Callable接口的区别。
相同点:
Callable和Runnable都是接口
Callable和Runnable都可以应用于Executors
不同点:
Callable要实现call方法,Runnable要实现run方法
call方法可以返回执行结果,run方法不能返回结果
call方法可以抛出checked exception,run方法不能抛异常
Runnable接口出现在JDK1.0,Callable接口出现在JDK1.5
再来看下Callable.java和Runnable.java的源码:
Callable.java
/*
* ORACLE PROPRIETARY/CONFIDENTIAL. Use is subject to license terms.
* Written by Doug Lea with assistance from members of JCP JSR-166
* Expert Group and released to the public domain, as explained at
* http://creativecommons.org/publicdomain/zero/1.0/
*/
package java.util.concurrent;
/**
* A task that returns a result and may throw an exception.
* Implementors define a single method with no arguments called
* {@code call}.
*
* <p>The {@code Callable} interface is similar to {@link
* java.lang.Runnable}, in that both are designed for classes whose
* instances are potentially executed by another thread. A
* {@code Runnable}, however, does not return a result and cannot
* throw a checked exception.
*
* <p>The {@link Executors} class contains utility methods to
* convert from other common forms to {@code Callable} classes.
*
* @see Executor
* @since 1.5
* @author Doug Lea
* @param <V> the result type of method {@code call}
*/
@FunctionalInterface
public interface Callable<V> {
/**
* Computes a result, or throws an exception if unable to do so.
*
* @return computed result
* @throws Exception if unable to compute a result
*/
V call() throws Exception;
}
Runnable.java
/*
* Copyright (c) 1994, 2013, Oracle and/or its affiliates. All rights reserved.
* ORACLE PROPRIETARY/CONFIDENTIAL. Use is subject to license terms.
*/
package java.lang;
/**
* The <code>Runnable</code> interface should be implemented by any
* class whose instances are intended to be executed by a thread. The
* class must define a method of no arguments called <code>run</code>.
* <p>
* This interface is designed to provide a common protocol for objects that
* wish to execute code while they are active. For example,
* <code>Runnable</code> is implemented by class <code>Thread</code>.
* Being active simply means that a thread has been started and has not
* yet been stopped.
* <p>
* In addition, <code>Runnable</code> provides the means for a class to be
* active while not subclassing <code>Thread</code>. A class that implements
* <code>Runnable</code> can run without subclassing <code>Thread</code>
* by instantiating a <code>Thread</code> instance and passing itself in
* as the target. In most cases, the <code>Runnable</code> interface should
* be used if you are only planning to override the <code>run()</code>
* method and no other <code>Thread</code> methods.
* This is important because classes should not be subclassed
* unless the programmer intends on modifying or enhancing the fundamental
* behavior of the class.
*
* @author Arthur van Hoff
* @see java.lang.Thread
* @see java.util.concurrent.Callable
* @since JDK1.0
*/
@FunctionalInterface
public interface Runnable {
/**
* When an object implementing interface <code>Runnable</code> is used
* to create a thread, starting the thread causes the object's
* <code>run</code> method to be called in that separately executing
* thread.
* <p>
* The general contract of the method <code>run</code> is that it may
* take any action whatsoever.
*
* @see java.lang.Thread#run()
*/
public abstract void run();
}
下边依次给出自定义的Task类,实现Callable和Runnable接口:
import java.util.concurrent.Callable;
public class CallableTask implements Callable<String> {
@Override
public String call() throws Exception {
return "";
}
}
public class RunnableTask implements Runnable {
@Override
public void run() {
// you can do something
}
}
也就是说,我们可以将自己的执行逻辑(一个任务Task)放入run或者call方法中,将该Task通过executorservice.submit(Task)来执行。好的,让我们回到正文,接着介绍ExecutorService接口的实现类:ThreadPoolExecutor,这个才是本文的主角。先看继承图如下:
THreadPoolExecutor是ExecutorService的默认实现类。这个类是一个线程池,我们只需要调用该类对象的submit或者execute方法,并且传入相应的RunnableTask或者CallableTask即好。
那么我们如何创建线程池呢?
我们先来看看ThreadPoolExecutor的构造函数:
解释下构造函数中涉及到的重要参数:
corePoolSize:线程池中的核心线程数
maximumPoolSize:线程池中允许的最大线程数
(ThreadPoolExecutor 将根据 corePoolSize和 maximumPoolSize设置的边界自动调整池大小。当新任务在方法 execute(java.lang.Runnable) 中提交时,如果运行的线程少于 corePoolSize,则创建新线程来处理请求,即使其他辅助线程是空闲的。如果运行的线程多于 corePoolSize 而少于 maximumPoolSize,则仅当队列满时才创建新线程。)
keepAliveTime:当线程数大于核心线程数时,终止多余的空闲线程等待新任务的最长时间
unit:该参数表示keepAliveTime的时间单位
workQueue:用于表示任务的队列
线程池可以解决两个不同问题:由于减少了每个任务调用的开销,它们通常可以在执行大量异步任务时提供增强的性能,并且还可以提供绑定和管理资源(包括执行任务集时使用的线程)的方法。每个 ThreadPoolExecutor 还维护着一些基本的统计数据,如完成的任务数。
尽管我们可以通过调整构造函数中的值来创建一个线程池,但是,我们强烈建议应该使用较为方便的 Executors 工厂方法 Executors.newCachedThreadPool()(*线程池,可以进行自动线程回收)、Executors.newFixedThreadPool(int)(固定大小线程池)和 Executors.newSingleThreadExecutor()(单个后台线程),它们均为大多数使用场景预定义了设置。
如果你了解Array和Arrays,Collection和Collections的关系,那么你一定会猜到Java提供了Executor框架的同时也提供了工具类Executors,使用该工具类可以非常方便的创建不同类型的线程池,我们通过ThreadPoolThread的构造函数中的核心线程数以及最大线程数来说明下。
Executors.newCachedThreadPool():*线程池,将 maximumPoolSize 设置为基本的*值(如 Integer.MAX_VALUE),则允许池适应任意数量的并发任务。来一个创建一个线程,适合用来执行大量耗时较短且提交频率较高的任务。
Executors.newFixedThreadPool(int):固定大小线程池,设置的 corePoolSize 和 maximumPoolSize 相同,则创建了固定大小的线程池。当线程池大小达到核心线程池大小,就不会增加也不会减小工作者线程的固定大小的线程池。
Executors.newSingleThreadExecutor( ):便于实现单(多)生产者-消费者模式。
接下来,我们说一下参数workQueue,也就是任务队列,既然是队列,那么对于任务来说,肯定会存在一个排队策略。
- 如果运行的线程少于 corePoolSize,则 Executor 始终首选添加新的线程,而不进行排队。
- 如果运行的线程等于或多于 corePoolSize,则 Executor 始终首选将请求加入队列,而不添加新的线程。
- 如果无法将请求加入队列,则创建新的线程,除非创建此线程超出 maximumPoolSize,在这种情况下,任务将被拒绝。
---------------------------------------------------------------------------------------
我是华丽的分割线,好了基础概念先说到这里,接下来,我们将进入Demo案例环节。
---------------------------------------------------------------------------------------
Demo1:演示创建线程池,创建线程执行任务Task,实现简单的多线程。
package pak2;
public class RunnableTask implements Runnable {
String name;
public RunnableTask(String name){
this.name = name;
}
@Override
public void run() {
// you can do something
System.out.println(name);
}
}
package pak2;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
public class Main {
private static ExecutorService executors= Executors.newCachedThreadPool();
public static void main (String[] args){
List<String> list = new ArrayList<String>();
list.add("name1");
list.add("name2");
list.add("name3");
list.add("name4");
list.add("name5");
list.add("name6");
list.add("name7");
list.add("name8");
int num = list.size();
System.out.println(System.currentTimeMillis());
for (int i = 0; i < list.size(); i++) {
test(list.get(i));
}
System.out.println("This is end of process...");
}
private static void test(String s) {
RunnableTask runnableTask = new RunnableTask(s);
executors.execute(runnableTask);
}
}
执行结果如下:
由结果可以看的出来,在for循环中确实开了许多个线程,并且主线程main和各个子线程谁先执行结束具有不确定性。
当然了,我们也可以将test方法中的execute方法变为submit方法,执行效果不变。
Demo2:展示获取各个线程的执行结果
我们知道,在ExecutorService接口中,submit方法可以接收Callable或者Runnable接口的任务,并且可以返回一个Future实例,这样客户端(也就是线程调用者)可以获得线程任务的执行结果。
Future实例代表该异步计算的结果,它提供了检查计算是否完成的方法,以等待计算的完成,并获取计算的结果。计算完成后只能使用 get 方法来获取结果。Future提供了以下五种方法:
但是我们常用的还是get( )方法。
我们先定义一个CallableTask任务,可以返回结果。
package pak3;
import java.util.concurrent.Callable;
import java.util.concurrent.CountDownLatch;
public class CallableTask implements Callable<String> {
public String name ;
public CallableTask(String name) {
this.name = name;
}
@Override
public String call() throws Exception {
if(name.equals("name5")){
try {
Thread.sleep(3000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
if(name.equals("name2")){
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
System.out.println("name="+name);
return name;
}
}
package pak3;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.*;
public class Main2 {
private static ExecutorService executors= Executors.newCachedThreadPool();
public static void main (String[] args) throws ExecutionException, InterruptedException {
List<String> list = new ArrayList<String>();
list.add("name1");
list.add("name2");
list.add("name3");
list.add("name4");
list.add("name5");
list.add("name6");
list.add("name7");
list.add("name8");
System.out.println(System.currentTimeMillis());
List<Future<String>> resList = new ArrayList<Future<String>>();
for (int i = 0; i < list.size(); i++) {
Future<String> future = test(list.get(i));
// 将future实例存入list
resList.add(future);
}
// 此时,我们已经开了若干个线程,并且获得了各个线程返回的Future实例
for (int i = 0; i < resList.size(); i++) {
// 此处可以获得各个线程的执行返回结果,并且可以对结果进行保存等其他操作
System.out.println(resList.get(i).get()); // 做为演示,此处只是对结果进行了输出
}
System.out.println("This is end of process...");
}
private static Future<String> test(String s) {
CallableTask callableTask = new CallableTask(s);
// 异步计算返回一个Future实例
Future<String> future = executors.submit(callableTask);
return future;
}
}
执行结果如下所示:
执行结果不确定,每个子线程的顺序不一定,但是由于future.get( )方法是一个阻塞方法,所以各个线程的返回结果一定是按顺序得到的。
我们对Main进行修改,如下所示:
package pak3;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.*;
public class Main2 {
private static ExecutorService executors= Executors.newCachedThreadPool();
public static void main (String[] args) throws ExecutionException, InterruptedException {
List<String> list = new ArrayList<String>();
list.add("name1");
list.add("name2");
list.add("name3");
list.add("name4");
list.add("name5");
list.add("name6");
list.add("name7");
list.add("name8");
int num = list.size();
System.out.println(System.currentTimeMillis());
List<Future<String>> resList = new ArrayList<Future<String>>();
for (int i = 0; i < list.size(); i++) {
test(list.get(i));
}
System.out.println(System.currentTimeMillis());
System.out.println("This is end of process...");
}
private static void test(String s) throws ExecutionException, InterruptedException {
CallableTask callableTask = new CallableTask(s);
Future<String> future = executors.submit(callableTask);
// 得到future实例后立马调用get方法
System.out.println(future.get());
}
}
执行结果如下:
哈哈,惊不惊喜,意不意外,我们的程序变成了单线程执行。这是一种错误的用法哈~
我们想想 ,submit方法对于CallableTask和RunnableTask均可以得到future实例,但是Runnable接口的run方法是不能够有返回值的,那么future.get( )会返回什么呢?接着看代码:
public class RunnableTask implements Runnable {
public String name ;
public RunnableTask(String name) {
this.name = name;
}
@Override
public void run() {
if(name.equals("name5")){
try {
Thread.sleep(6000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
System.out.println("name="+name);
}
}
package pak3;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;
public class Main {
private static ExecutorService executors= Executors.newCachedThreadPool();
public static void main (String[] args) throws ExecutionException, InterruptedException {
List<String> list = new ArrayList<String>();
list.add("name1");
list.add("name2");
list.add("name3");
list.add("name4");
list.add("name5");
list.add("name6");
list.add("name7");
list.add("name8");
System.out.println(System.currentTimeMillis());
List<Future<?>> resList = new ArrayList<>();
for (int i = 0; i < list.size(); i++) {
Future<?> future = test(list.get(i));
resList.add(future);
}
for (int i = 0; i < resList.size(); i++) {
System.out.println(resList.get(i).get());
}
System.out.println(System.currentTimeMillis());
System.out.println("This is end of process...");
}
private static Future<?> test(String s) {
RunnableTask runnableTask = new RunnableTask(s);
Future<?> future = executors.submit(runnableTask);
return future;
}
}
执行结果如下:
可以看的出来,当submit中执行的Runnable方法时,在线程任务成功执行完毕之后future.get()会返回null,表示执行成功。
说到了线程池,可能我们会希望多个线程可以在全部执行结束之后,我们再根据子线程的执行结果来接着执行主线程的处理逻辑。这个时候,我们就用到了CountDownLatch。
CountDownLatch:
一个同步辅助类,在完成一组正在其他线程中执行的操作之前,它允许一个或多个线程一直等待。用给定的计数 初始化 CountDownLatch。由于调用了 countDown() 方法,所以在当前计数到达零之前,await 方法会一直受阻塞。之后,会释放所有等待的线程,await 的所有后续调用都将立即返回。这种现象只出现一次——计数无法被重置。如果需要重置计数,请考虑使用 CyclicBarrier(译为栅栏,也可以实现多线程之间的等待,此处不做介绍,感兴趣的同学可以自行学习)。
接下来,我们给出使用CountDownLatch实现多线程等待的功能的Demo:
package pak1;
import java.util.concurrent.Callable;
import java.util.concurrent.CountDownLatch;
public class CallableTask implements Callable<String> {
public String name ;
public CountDownLatch countDownLatch;
public CallableTask(String name, CountDownLatch countDownLatch) {
this.name = name;
this.countDownLatch = countDownLatch;
}
@Override
public String call() throws Exception {
if(name.equals("name5")){
try {
Thread.sleep(3000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
if(name.equals("name2")){
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
System.out.println("name="+name);
// countDown减小1
countDownLatch.countDown();
return name;
}
}
package pak1;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.*;
public class Main2 {
private static ExecutorService executors= Executors.newCachedThreadPool();
public static void main (String[] args) throws ExecutionException, InterruptedException {
List<String> list = new ArrayList<String>();
list.add("name1");
list.add("name2");
list.add("name3");
list.add("name4");
list.add("name5");
list.add("name6");
list.add("name7");
list.add("name8");
int num = list.size();
// 创建CountDownLatch
CountDownLatch countDownLatch = new CountDownLatch(num);
System.out.println(System.currentTimeMillis());
List<Future<String>> resList = new ArrayList<Future<String>>();
for (int i = 0; i < list.size(); i++) {
test(list.get(i),countDownLatch);
}
System.out.println(System.currentTimeMillis());
try {
// 这是一个阻塞方法,只有倒计数减小为0才会通过
countDownLatch.await();
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(System.currentTimeMillis());
System.out.println("This is end of process...");
}
private static void test(String s,CountDownLatch countDownLatch) throws ExecutionException, InterruptedException {
CallableTask callableTask = new CallableTask(s,countDownLatch);
executors.submit(callableTask);
}
}
执行结果如下所示:
可以看的出,只要有任何一个子线程没有执行完成,即countDwon内部的计数器还不为0,那么将一直阻塞主线程,直到某一时刻,子线程全部执行完毕,接着执行主线程。
自此,我们学习了多线程编程的一些简单知识,也是我最近工作中遇到的一些问题的简单总结,希望可以帮助更多的初学者,纸上得来终觉浅,还是的写Demo加深印象。
如果对你有帮助,记得点赞哦~欢迎大家关注我的博客,我会持续更新后续学习笔记,如果有什么问题,可以进群366533258一起交流学习哦~