首页 > 技术文章 > Java并发学习(一):进程和线程

robod 2020-07-25 16:12 原文

好好学习,天天向上


本文已收录至我的Github仓库DayDayUP:github.com/RobodLee/DayDayUP,欢迎Star,更多文章请前往:目录导航

前言

俗话说得好“一人拾柴火不旺,众人拾柴火焰高”,一个人的力量毕竟是有限的,想要把一件事情做好,免不了需要一帮人齐心协力。同样的道理,一个复杂程序里面不会只有一个线程在工作,必然是很多个线程在一起工作。那么,这篇文章作为Java并发学习系列的第一篇,就来聊一聊Java并发的基础知识:进程和线程。

进程和线程概念

进程

什么是进程呢?进程是计算机中的程序关于某数据集合上的一次运行活动,是系统进行资源分配和调度的基本单位,是操作系统结构的基础。简单的说进程就是一个正在运行的程序,打开任务管理器,可以看到有很多的进程。

比如写文章用的Typora,写程序用的IDEA等,这些都是进程。

线程

那么线程又是什么呢?线程是操作系统能够进行运算调度的最小单位。每个进程里面都包含了一个或多个线程,比如我正在运行的IDEA里面就包含了58个线程。

举个栗子:平时大家应该会用视频软件看视频吧,那么播放视频,下载视频,上传视频,弹幕功能等,这些都是一个个单独线程,这些线程之间可以互相协作,也可以互不干扰的运行。

线程和CPU之间的关系

线程作为操作系统的最小调度单位,同一时间只能运行在一个CPU核心上。现在的操作系统都是分时操作系统,一个线程不会一直占用一个CPU核心,而是等待操作系统的任务调度器分配CPU时间片给一个线程,这个线程才能运行,否则只能在一边等着。就像网吧一样,每台机子就相当于一个CPU核心,上网的人就是线程。每个人不管时间多少都是时间限制的,下机后就给另外的人上,没有机子的人就在一边等着别人下机。不过网吧上网时间是由你自行决定的,而线程的运行时间是操作系统分配的。不过一个CPU时间片特别短,所以你感觉所有的线程都是一起运行的,其实是存在线程切换的。

并行和并发

既然要学习Java并发,那么首先得知道并行和并发的概念吧。

并行

  • 并行(parallel)是同一时间动手做(doing)多件事情的能力

举个栗子:中午了,大家都在食堂打饭,现在有两个窗口,前面排了两队人,每个打饭阿姨同一时间只要应付一个同学,这就是并行

并发

  • 并发(concurrent)是同一时间应对(dealing with)多件事情的能力

还是打饭的例子,现在只有一个阿姨在打饭,但前面还是排了两队人,这样的话,阿姨一次要处理两个同学的打饭请求,这就是并发

Java线程

多线程的好处

前面介绍过,线程是操作系统进行调度的最小单位,一个线程同一时间只能运行在CPU的一个核心上,我们都知道,现在的CPU都是多核的。假如我们的程序只有一个线程,那么只会用到CPU的一个核心,那我们搞多核CPU不就浪费了吗,所以就需要多线程去提高程序的运行效率。打个比方,现在有个软件开发的任务,需要前端,后端,美工,但现在只有一个人身兼多职,其它几个人在边上喝茶,就相当于单线程运行,开发效率肯定不高。要是这几个人分工明确,齐心合作,开发效率自然就上去了。这就是多线程带来的好处。

创建和运行线程

1. 继承Thread ,重写run() 方法

第一种创建线程的方式是新建一个类继承自 Thread ,然后重写父类的 run() 方法,并在里面编写代码即可:

class MyThread extends Thread {

    @Override
    public void run() {
        System.out.println("创建了线程");
    }
}

启动线程的方式很简单,只要创建MyThread类的实例,然后调用start()方法就行了:

new MyThread().start();

其实也可以简单一点,直接使用匿名类的方式,原理都是一样的:

new Thread() {
    @Override
    public void run() {
        System.out.println("创建了线程");
    }
}.start();

2. 实现Runnable接口

第一种方式的耦合性比较高,一般采用实现Runnable接口的方式去创建线程:

class MyThread implements Runnable {

    @Override
    public void run() {
        System.out.println("创建了线程");
    }
}

启动线程的方式是创建Thread类的实例,接收一个Runnable参数,因为MyThread类实现了Runnable接口,所以把MyThread类的实例当作参数传入Thread类的构造方法,最后调用Thread类的start()方法就可以了。

new Thread(new MyThread()).start();

3. Callable+FutureTask

前面两种方式创建的线程都没有返回值,要想线程有返回值就可以使用第三种方式去创建线程:

FutureTask<Integer> task = new FutureTask<Integer>(new Callable<Integer>() {
    @Override
    public Integer call() throws Exception {
        return 12345;
    }
});
new Thread(task).start();
  • 让一个类实现Callable接口,重写里面的call方法,泛型就是返回值的类型,可以是任意数据类型。这里使用了匿名类的写法。
  • 创建一个FutureTask类的实例,将Callable对象作为参数传入其构造方法。
  • new一个Thread类的实例,将FutureTask对象当作参数传入,调用其start()方法启动线程。

这时候线程就成功创建并启动了,那怎么获取返回值呢?直接调用FutureTask的get()方法就可以了。

task.get();

这样就可以获取到返回值了,不过这里有个需要注意的地方,当调用get()方法的时候,线程就会阻塞在这里,直到成功获取返回值。我们可以来写段代码测试一下是不是这样:

FutureTask<Integer> task = new FutureTask<Integer>(new Callable<Integer>() {
    @Override
    public Integer call() throws Exception {
        Thread.sleep(2000);
        return 12345;
    }
});
new Thread(task).start();
long start = System.nanoTime();
task.get();     //线程会阻塞在这里
long end = System.nanoTime();
long time = (end-start)/1000000;
System.out.println("耗费的时间为:"+time + "毫秒");

运行一下,看看结果:

从结果中可以看出,get()方法确实会阻塞线程。

4. 线程池

最后一种方式就是借助线程池,这也是《阿里巴巴Java开发手册》中推荐使用的方式。

但是比较尴尬的是我现在水平不够,还不会线程池

推荐阅读