欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页

Java多线程编程学习总结(一)

程序员文章站 2022-05-04 17:54:18
...

(尊重劳动成果,转载请注明出处: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中的解释:

Java多线程编程学习总结(一)

介绍:command参数代表需要执行的任务,Executor接口使得任务的提交和任务的执行解耦,调用方只需要执行Executor.execute方法即可以使得指定的任务command被执行,无需关心任务的具体执行细节。

缺点:Executor接口无法将执行任务的结果返回给调用方;Executor内部维护的工作者线程(真正执行任务的线程)并不能够被其主动停掉并且释放所占的资源。

有了缺点,就会有新的改进接口出现,是的,请看Executor接口的继承图:

Java多线程编程学习总结(一)

在其子接口ExecutorService中,定义了submit方法,可以接受Callable接口或者Runnable接口表示的任务,并且可以返回相应的Future实例。通过Future,调用方可以获得线程的执行结果;该ExecutorService接口中,还定义了shutdown和shutdownNow方法来关闭相应的工作者线程。具体可见api方法示意:

submit方法:

Java多线程编程学习总结(一)

Java多线程编程学习总结(一)

关闭工作者线程的方法:

Java多线程编程学习总结(一)

鉴于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,这个才是本文的主角。先看继承图如下:

Java多线程编程学习总结(一)

THreadPoolExecutor是ExecutorService的默认实现类。这个类是一个线程池,我们只需要调用该类对象的submit或者execute方法,并且传入相应的RunnableTask或者CallableTask即好。

那么我们如何创建线程池呢?

我们先来看看ThreadPoolExecutor的构造函数:

Java多线程编程学习总结(一)

解释下构造函数中涉及到的重要参数:

    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);
    }
}

执行结果如下:

Java多线程编程学习总结(一)

由结果可以看的出来,在for循环中确实开了许多个线程,并且主线程main和各个子线程谁先执行结束具有不确定性。

当然了,我们也可以将test方法中的execute方法变为submit方法,执行效果不变。

Demo2:展示获取各个线程的执行结果

我们知道,在ExecutorService接口中,submit方法可以接收Callable或者Runnable接口的任务,并且可以返回一个Future实例,这样客户端(也就是线程调用者)可以获得线程任务的执行结果。

Future实例代表该异步计算的结果,它提供了检查计算是否完成的方法,以等待计算的完成,并获取计算的结果。计算完成后只能使用 get 方法来获取结果。Future提供了以下五种方法:

Java多线程编程学习总结(一)

但是我们常用的还是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;
    }
}

执行结果如下所示:

Java多线程编程学习总结(一)Java多线程编程学习总结(一)

执行结果不确定,每个子线程的顺序不一定,但是由于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());
    }
}

执行结果如下:

Java多线程编程学习总结(一)

哈哈,惊不惊喜,意不意外,我们的程序变成了单线程执行。这是一种错误的用法哈~

我们想想 ,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;
    }
}

执行结果如下:

Java多线程编程学习总结(一)

可以看的出来,当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);
    }
}

执行结果如下所示:

Java多线程编程学习总结(一)

可以看的出,只要有任何一个子线程没有执行完成,即countDwon内部的计数器还不为0,那么将一直阻塞主线程,直到某一时刻,子线程全部执行完毕,接着执行主线程。


自此,我们学习了多线程编程的一些简单知识,也是我最近工作中遇到的一些问题的简单总结,希望可以帮助更多的初学者,纸上得来终觉浅,还是的写Demo加深印象。


如果对你有帮助,记得点赞哦~欢迎大家关注我的博客,我会持续更新后续学习笔记,如果有什么问题,可以进群366533258一起交流学习哦~



相关标签: u