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

 

(尊重劳动成果,转载请注明出处:https://blog.csdn.net/qq_25827845/article/details/80672328冷血之心的博客)

系列文章:

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

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

前序:

      在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一起交流学习哦~

 

 

  • 4
    点赞
  • 11
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 3
    评论
评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

温柔狠角色

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值