2w字 + 41张图带你参透并发编程!(三)

简介: 在计算机最早期的时候,没有操作系统,执行程序只需要一种方式,那就是从头到尾依次执行。任何资源都会为这个程序服务,在计算机使用某些资源时,其他资源就会空闲,就会存在 浪费资源 的情况。

函数性并行


函数性并行模型是最近才提出的一种并发模型,它的基本思路是使用函数调用来实现。消息的传递就相当于是函数的调用。传递给函数的参数都会被拷贝,因此在函数之外的任何实体都无法操纵函数内的数据。这使得函数执行类似于原子操作。每个函数调用都可以独立于任何其他函数调用执行。

当每个函数调用独立执行时,每个函数都可以在单独的 CPU 上执行。这也就是说,函数式并行并行相当于是各个 CPU 单独执行各自的任务。

JDK 1.7 中的 ForkAndJoinPool 类就实现了函数性并行的功能。Java 8 提出了 stream 的概念,使用并行流也能够实现大量集合的迭代。

函数性并行的难点是要知道函数的调用流程以及哪些 CPU 执行了哪些函数,跨 CPU 函数调用会带来额外的开销。

我们之前说过,线程就是进程中的一条顺序流,在 Java 中,每一条 Java 线程就像是 JVM 的一条顺序流,就像是虚拟 CPU 一样来执行代码。Java 中的 main() 方法是一条特殊的线程,JVM 创建的 main 线程是一条主执行线程,在 Java 中,方法都是由 main 方法发起的。在 main 方法中,你照样可以创建其他的线程(执行顺序流),这些线程可以和 main 方法共同执行应用代码。

Java 线程也是一种对象,它和其他对象一样。Java 中的 Thread 表示线程,Thread 是 java.lang.Thread 类或其子类的实例。那么下面我们就来一起探讨一下在 Java 中如何创建和启动线程。



创建并启动线程


在 Java 中,创建线程的方式主要有三种

  • 通过继承 Thread 类来创建线程
  • 通过实现 Runnable 接口来创建线程
  • 通过 CallableFuture 来创建线程

下面我们分别探讨一下这几种创建方式

继承 Thread 类来创建线程

第一种方式是继承 Thread 类来创建线程,如下示例

public class TJavaThread extends Thread{
    static int count;
    @Override
    public synchronized void run() {
        for(int i = 0;i < 10000;i++){
            count++;
        }
    }
    public static void main(String[] args) throws InterruptedException {
        TJavaThread tJavaThread = new TJavaThread();
        tJavaThread.start();
        tJavaThread.join();
        System.out.println("count = " + count);
    }
}

线程的主要创建步骤如下

  • 定义一个线程类使其继承 Thread 类,并重写其中的 run 方法,run 方法内部就是线程要完成的任务,因此 run 方法也被称为 执行体
  • 创建了 Thread 的子类,上面代码中的子类是 TJavaThread
  • 启动方法需要注意,并不是直接调用 run 方法来启动线程,而是使用 start 方法来启动线程。当然 run 方法可以调用,这样的话就会变成普通方法调用,而不是新创建一个线程来调用了。
public static void main(String[] args) throws InterruptedException {
  TJavaThread tJavaThread = new TJavaThread();
  tJavaThread.run();
  System.out.println("count = " + count);
}

这样的话,整个 main 方法只有一条执行线程也就是 main 线程,由两条执行线程变为一条执行线程

微信图片_20220414220504.png

Thread 构造器只需要一个 Runnable 对象,调用 Thread 对象的 start() 方法为该线程执行必须的初始化操作,然后调用 Runnable 的 run 方法,以便在这个线程中启动任务。我们上面使用了线程的 join 方法,它用来等待线程的执行结束,如果我们不加 join 方法,它就不会等待 tJavaThread 的执行完毕,输出的结果可能就不是 10000

微信图片_20220414220508.png

可以看到,在 run  方法还没有结束前,run 就被返回了。也就是说,程序不会等到 run 方法执行完毕就会执行下面的指令。

使用继承方式创建线程的优势:编写比较简单;可以使用 this 关键字直接指向当前线程,而无需使用 Thread.currentThread() 来获取当前线程。

使用继承方式创建线程的劣势:在 Java 中,只允许单继承(拒绝肛精说使用内部类可以实现多继承)的原则,所以使用继承的方式,子类就不能再继承其他类。

使用 Runnable 接口来创建线程

相对的,还可以使用 Runnable 接口来创建线程,如下示例

public class TJavaThreadUseImplements implements Runnable{
    static int count;
    @Override
    public synchronized void run() {
        for(int i = 0;i < 10000;i++){
            count++;
        }
    }
    public static void main(String[] args) throws InterruptedException {
        new Thread(new TJavaThreadUseImplements()).start();
        System.out.println("count = " + count);
    }
}

线程的主要创建步骤如下

  • 首先定义 Runnable 接口,并重写 Runnable 接口的 run 方法,run 方法的方法体同样是该线程的线程执行体。
  • 创建线程实例,可以使用上面代码这种简单的方式创建,也可以通过 new 出线程的实例来创建,如下所示
TJavaThreadUseImplements tJavaThreadUseImplements = new TJavaThreadUseImplements();
new Thread(tJavaThreadUseImplements).start();
  • 再调用线程对象的 start 方法来启动该线程。

线程在使用实现 Runnable 的同时也能实现其他接口,非常适合多个相同线程来处理同一份资源的情况,体现了面向对象的思想。

使用 Runnable 实现的劣势是编程稍微繁琐,如果要访问当前线程,则必须使用 Thread.currentThread() 方法。

使用 Callable 接口来创建线程

Runnable 接口执行的是独立的任务,Runnable 接口不会产生任何返回值,如果你希望在任务完成后能够返回一个值的话,那么你可以实现 Callable 接口而不是 Runnable 接口。Java SE5 引入了 Callable 接口,它的示例如下

public class CallableTask implements Callable {
    static int count;
    public CallableTask(int count){
        this.count = count;
    }
    @Override
    public Object call() {
        return count;
    }
    public static void main(String[] args) throws ExecutionException, InterruptedException {
        FutureTask<Integer> task = new FutureTask((Callable<Integer>) () -> {
            for(int i = 0;i < 1000;i++){
                count++;
            }
            return count;
        });
        Thread thread = new Thread(task);
        thread.start();
        Integer total = task.get();
        System.out.println("total = " + total);
    }
}

我想,使用 Callable 接口的好处你已经知道了吧,既能够实现多个接口,也能够得到执行结果的返回值。Callable 和 Runnable 接口还是有一些区别的,主要区别如下

  • Callable 执行的任务有返回值,而 Runnable 执行的任务没有返回值
  • Callable(重写)的方法是 call 方法,而 Runnable(重写)的方法是 run 方法。
  • call 方法可以抛出异常,而 Runnable 方法不能抛出异常


相关文章
|
存储 缓存 监控
|
存储 消息中间件 缓存
四万字爆肝总结java多线程所有知识点(史上最全总结)
全文从多线程的实现方式、线程的状态、线程的方法、线程的同步、线程的通讯、等角度对多线程的基础知识进行总结
485 1
四万字爆肝总结java多线程所有知识点(史上最全总结)
|
消息中间件 缓存 监控
7000字+24张图带你彻底弄懂线程池(1)
7000字+24张图带你彻底弄懂线程池(1)
|
存储 监控 Java
7000字+24张图带你彻底弄懂线程池(2)
7000字+24张图带你彻底弄懂线程池(2)
|
编译器 C++
【C++】—— 类和对象(中)一张图带你搞清楚6个默认成员函数+万字总结 复习全靠它(3)
【C++】—— 类和对象(中)一张图带你搞清楚6个默认成员函数+万字总结 复习全靠它(3)
79 0
【C++】—— 类和对象(中)一张图带你搞清楚6个默认成员函数+万字总结 复习全靠它(3)
|
存储 安全 Java
Java多线程基础——两万字详解
进程简单来说就是正在运行的程序,是可以通过双击执行的.exe文件,打开我们电脑的任务管理器,可以看到我们的电脑正在执行的进程,目前我们的电脑都是多进程模式。
121 0
Java多线程基础——两万字详解
|
编译器 C++
【C++】—— 类和对象(中)一张图带你搞清楚6个默认成员函数+万字总结 复习全靠它(2)
【C++】—— 类和对象(中)一张图带你搞清楚6个默认成员函数+万字总结 复习全靠它(2)
93 0
【C++】—— 类和对象(中)一张图带你搞清楚6个默认成员函数+万字总结 复习全靠它(2)
|
编译器 C++
【C++】—— 类和对象(中)一张图带你搞清楚6个默认成员函数+万字总结 复习全靠它(1)
【C++】—— 类和对象(中)一张图带你搞清楚6个默认成员函数+万字总结 复习全靠它(1)
107 0
【C++】—— 类和对象(中)一张图带你搞清楚6个默认成员函数+万字总结 复习全靠它(1)
|
消息中间件 SQL 安全
25 张图,1.4 w字!彻底搞懂分布式事务原理(三)
本文提纲如下: 0. 前言 1. 单数据源事务 & 多数据源事务 2. 常见分布式事务解决方案 2.1. 分布式事务模型 2.2. 二将军问题和幂等性 2.3. 两阶段提交(2PC) & 三阶段提交(3PC)方案 2.4. TCC 方案 2.5. 事务状态表方案 2.6. 基于消息中间件的最终一致性事务方案 3. Seata in AT mode 的实现 3.1. Seata in AT mode 工作流程概述 3.2. Seata in AT mode 工作流程详述 4. 结束语
291 0
25 张图,1.4 w字!彻底搞懂分布式事务原理(三)
|
机器学习/深度学习 人工智能 算法
畅快!5000字通俗讲透决策树基本原理
在当今这个人工智能时代,似乎人人都或多或少听过机器学习算法;而在众多机器学习算法中,决策树则无疑是最重要的经典算法之一。这里,称其最重要的经典算法是因为以此为基础,诞生了一大批集成算法,包括Random Forest、Adaboost、GBDT、xgboost,lightgbm,其中xgboost和lightgbm更是当先炙手可热的大赛算法;而又称其为之一,则是出于严谨和低调。实际上,决策树算法也是个人最喜爱的算法之一(另一个是Naive Bayes),不仅出于其算法思想直观易懂(相较于SVM而言,简直好太多),更在于其较好的效果和巧妙的设计。似乎每个算法从业人员都会开一讲决策树专题,那么今天
302 0
畅快!5000字通俗讲透决策树基本原理