好好学习,天天向上
本文已收录至我的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开发手册》中推荐使用的方式。
但是比较尴尬的是我现在水平不够,还不会线程池