目录
2.2 并发(Concurrency)和并行(Parallelism)
一、 进程和多线程简介
1.1 进程和线程
进程和线程的对比这一知识点由于过于基础,所以在面试中很少碰到,但是极有可能会在笔试题中碰到。常见的提问形式是这样的:“什么是线程和进程?,请简要描述线程与进程的关系、区别及优缺点? ”。
1.2 何为进程?
进程是程序的一次执行过程,是系统运行程序的基本单位,因此进程是动态的。系统运行一个程序即是一个进程从创建,运行到消亡的过程。如下图所示,在 windows 中通过查看任务管理器的方式,我们就可以清楚看到 window 当前运行的进程(.exe文件的运行)。
1.3 何为线程?
线程与进程相似,但线程是一个比进程更小的执行单位。一个进程在其执行的过程中可以产生多个线程。与进程不同的是同类的多个线程共享同一块内存空间和一组系统资源,所以系统在产生一个线程,或是在各个线程之间作切换工作时,负担要比进程小得多,也正因为如此,线程也被称为轻量级进程
1.4 何为多线程?
多线程就是多个线程同时运行或交替运行。单核CPU的话是顺序执行,也就是交替运行。多核CPU的话,因为每个CPU有自己的运算器,所以在多个CPU中可以同时运行。
1.5 为什么多线程是必要的?
个人觉得可以用一句话概括:开发高并发系统的基础,利用好多线程机制可以大大提高系统整体的并发能力以及性能。
1.6 为什么提倡多线程而不是多进程?
线程就是轻量级进程,是程序执行的最小单位。使用多线程而不是用多进程去进行并发程序的设计,是因为线程间的切换和调度的成本远远小于进程。
二 、几个重要的概念
2.1 同步和异步
同步和异步通常用来形容一次方法调用。同步方法调用一旦开始,调用者必须等到方法调用返回后,才能继续后续的行为。异步方法调用更像一个消息传递,一旦开始,方法调用就会立即返回,调用者可以继续后续的操作。
关于异步目前比较经典以及常用的实现方式就是消息队列:在不使用消息队列服务器的时候,用户的请求数据直接写入数据库,在高并发的情况下数据库压力剧增,使得响应速度变慢。但是在使用消息队列之后,用户的请求数据发送给消息队列之后立即 返回,再由消息队列的消费者进程从消息队列中获取数据,异步写入数据库。由于消息队列服务器处理速度快于数据库(消息队列也比数据库有更好的伸缩性),因此响应速度得到大幅改善。
2.2 并发(Concurrency)和并行(Parallelism)
并发和并行是两个非常容易被混淆的概念。它们都可以表示两个或者多个任务一起执行,但是偏重点有些不同。并发偏重于多个任务交替执行,而多个任务之间有可能还是串行的。而并行是真正意义上的“同时执行”。
多线程在单核CPU的话是顺序执行,也就是交替运行(并发)。多核CPU的话,因为每个CPU有自己的运算器,所以在多个CPU中可以同时运行(并行)。
2.3 高并发
高并发(High Concurrency)是互联网分布式系统架构设计中必须考虑的因素之一,它通常是指,通过设计保证系统能够同时并行处理很多请求。
高并发相关常用的一些指标有响应时间(Response Time),吞吐量(Throughput),每秒查询率QPS(Query Per Second),并发用户数等。
2.4 临界区
临界区用来表示一种公共资源或者说是共享数据,可以被多个线程使用。但是每一次,只能有一个线程使用它,一旦临界区资源被占用,其他线程要想使用这个资源,就必须等待。在并行程序中,临界区资源是保护的对象。
2.5 阻塞和非阻塞
非阻塞指在不能立刻得到结果之前,该函数不会阻塞当前线程,而会立刻返回,而阻塞与之相反。
三、线程的实现方式
多线程的形式上实现方式主要有两种,一种是继承Thread类,一种是实现Runnable接口。本质上实现方式都是来实现线程任务,然后启动线程执行线程任务(这里的线程任务实际上就是run方法)。这里所说的6种,实际上都是在以上两种的基础上的一些变形。
3.1 集成Thread类方式
万物皆对象,那么线程也是对象,对象就应该能够抽取其公共特性封装成为类,使用类可以实例化多个对象,那么实现线程的第一种方式就是继承Thread类的方式。继承Thread类是最简单的一种实现线程的方式,通过jdk给我们提供的Thread类,重写Thread类的run方法即可,那么当线程启动的时候,就会执行run方法体的内容。代码如下:
/**
* 继承Thread类的方式创建线程
*/
public class ThreadDemo01 extends Thread{
public ThreadDemo01(String name) {
// 设置当前线程的名字
this.setName(name);
}
public static void main(String[] args) {
// 注意这里,要调用start方法才能启动线程,不能调用run方法
new ThreadDemo01("MyThread01").start();
// 创建多个线程实例,同时执行
new ThreadDemo01("MyThread02").start();
// 主线程的任务,为了演示多个线程一起执行
for(int i=0;i<10;i++){
// 输出线程的名字,与主线程名称相区分
printThreadInfo();
try {
Thread.sleep(1000);// 线程休眠一秒
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
@Override
public void run() {
for (int i = 0; i < 10; i++) {
printThreadInfo();
// 线程休眠一秒
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
/**
* 输出当前线程的信息
*/
private static void printThreadInfo() {
System.out.println("当前运行的线程名为: " + Thread.currentThread().getName());
}
}
运行结果:
这里要注意,在启动线程的时候,我们并不是调用线程类的run方法,而是调用了线程类的start方法。那么我们能不能调用run方法呢?答案是肯定的,因为run方法是一个public声明的方法,因此我们是可以调用的,但是如果我们调用了run方法,那么这个方法将会作为一个普通的方法被调用,并不会开启线程。这里实际上是采用了设计模式中的模板方法模式,Thread类作为模板,而run方法是在变化的因此放到子类。
3.2 实现runnable接口方式
实现Runnable接口也是一种常见的创建线程的方式。使用接口的方式可以让我们的程序降低耦合度。Runnable接口中仅仅定义了一个方法,就是run。
其实Runnable就是一个线程任务,线程任务和线程的控制分离,这也就是上面所说的解耦。我们要实现一个线程,可以借助Thread类,Thread类要执行的任务就可以由实现了Runnable接口的类来处理。 这就是Runnable的精髓之所在!
使用Runnable实现上面的例子步骤如下:
- 定义一个类实现Runnable接口,作为线程任务类
- 重写run方法,并实现方法体,方法体的代码就是线程所执行的代码
- 定义一个可以运行的类,并在main方法中创建线程任务类
- 创建Thread类,并将线程任务类做为Thread类的构造方法传入
- 启动线程
class CreateThreadDemo02 implements Runnable {
@Override
public void run() {
for (int i = 0; i < 30; i++) {
printThreadInfo();
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
/**
* 输出当前线程的信息
*/
private static void printThreadInfo() {
System.out.println("当前运行的线程名为: " + Thread.currentThread().getName());
}
}
public class ThreadDemo02 {
public static void main(String[] args) {
//1.
CreateThreadDemo02 t2 = new CreateThreadDemo02();
Thread thread = new Thread(t2);
thread.start();
for (int i = 0; i < 30; i++) {
printThreadInfo();
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
/**
* 输出当前线程的信息
*/
private static void printThreadInfo() {
System.out.println("当前运行的线程名为: " + Thread.currentThread().getName());
}
}
运行结果:
线程任务和线程的控制分离,那么一个线程任务可以提交给多个线程来执行。这是很有用的,比如车站的售票窗口,每个窗口可以看做是一个线程,他们每个窗口做的事情都是一样的,也就是售票。这样我们程序在模拟现实的时候就可以定义一个售票任务,让多个窗口同时执行这一个任务。那么如果要改动任务执行计划,只要修改线程任务类,所有的线程就都会按照修改后的来执行。相比较继承Thread类的方式来创建线程的方式,实现Runnable接口是更为常用的(因为runnable容易扩展,符合Java的面向对象编程;单继承多接口)。
3.3 匿名内部类方式
这并不是一种新的实现线程的方式,只是另外的一种写法。比如有些情况我们的线程就想执行一次,以后就用不到了。那么像上面两种方式都还要再定义一个类,显得比较麻烦,我们就可以通过匿名内部类的方式来实现。使用内部类实现依然有两种,分别是继承Thread类和实现Runnable接口。代码如下:
/**
* 使用匿名内部类创建线程
*
*/
public class ThreadDemo03 {
public static void main(String[] args) {
Thread thread = new Thread(new Runnable() {
@Override
public void run() {
for (int i = 0; i < 30; i++) {
printThreadInfo();
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
});
thread.start();
for (int i = 0; i < 30; i++) {
printThreadInfo();
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
/**
* 输出当前线程的信息
*/
private static void printThreadInfo() {
System.out.println("当前运行的线程名为: " + Thread.currentThread().getName());
}
}
运行结果:
3.4 lambda方式
这里就是为了简化内部类的编写,简化了大量的模板代码,显得更加简洁。如果读者看不明白,可以读完内部类方式之后,回过来再看这段代码。
public class ThreadDemo04 {
public static void main(String[] args) {
// 使用lambda的形式实例化线程任务类
Runnable task = ()->{
for(int i=0;i<10;i++){
printThreadInfo(); // 输出线程的名字
}
};
// 创建线程对象,并将线程任务类作为构造方法参数传入
new Thread(task).start();
// 主线程的任务,为了演示多个线程一起执行
for(int i=0;i<10;i++){
printThreadInfo();
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
/**
* 输出当前线程的信息
*/
private static void printThreadInfo() {
System.out.println("当前运行的线程名为:"+Thread.currentThread().getName());
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
运行结果:
3.5 实现callable接口方式
我们发现上面提到的不管是继承Thread类还是实现Runnable接口,发现有两个问题,第一个是无法抛出更多的异常,第二个是线程执行完毕之后并无法获得线程的返回值。那么下面的这种实现方式就可以完成我们的需求。这种方式的实现就是我们后面要详细介绍的Future模式,只是在jdk5的时候,官方给我们提供了可用的API,我们可以直接使用。但是使用这种方式创建线程比上面两种方式要复杂一些,步骤如下。
- 创建一个类实现Callable接口,实现call方法。这个接口类似于Runnable接口,但比Runnable接口更加强大,增加了异常和返回值。
- 创建一个FutureTask,指定Callable对象,做为线程任务。
- 创建线程,指定线程任务。
- 启动线程
代码如下
/**
*一、实现 Callable 接口。 相较于实现 Runnable 接口的方式,方法可以有返回值,并且可以抛出异常。
*二、执行 Callable 方式,需要 FutureTask 实现类的支持,用于接收运算结果。 FutureTask 是 Future 接口的实现类
*/
public class ThreadDemo05 implements Callable<String>{
public static void main(String[] args) {
ThreadDemo05 threadDemo05 = new ThreadDemo05();
//1.执行 Callable 方式,需要 FutureTask 实现类的支持,用于接收运算结果。
FutureTask<String> result = new FutureTask<String>(threadDemo05);
new Thread(result).start();
//2.接收线程运算后的结果
try {
for(int i=0;i<10;i++){
printThreadInfo();
Thread.sleep(300);
}
//FutureTask 可用于 闭锁 类似于CountDownLatch的作用,在所有的线程没有执行完成之后这里是不会执行的
String info = result.get();
System.out.println(info);
} catch (Exception e) {
e.printStackTrace();
}
}
@Override
public String call() throws Exception {
for(int i=0;i<10;i++){
printThreadInfo();
Thread.sleep(300);
}
String txt = "Hello ";
return txt+"world!!!";
}
/**
* 输出当前线程的信息
*/
private static void printThreadInfo() {
System.out.println("当前运行的线程名为: " + Thread.currentThread().getName());
}
}
运行结果:
*综上例子可以看到: Callable 和 Runnable接口的区别
- Callable规定的方法是call(),而Runnable规定的方法是run().
- Callable的任务执行后可返回值,而Runnable的任务是不能返回值的。
- call()方法可抛出异常,而run()方法是不能抛出异常的。
- 运行Callable任务可拿到一个Future对象, Future表示异步计算的结果。
Callable提供了检查计算是否完成的方法,以等待计算的完成,并检索计算的结果。
通过Future对象可了解任务执行情况,可取消任务的执行,还可获取任务执行的结果。
Callable是类似于Runnable的接口,实现Callable接口的类和实现Runnable的类都是可被其它线程执行的任务。
3.6 线程池方式
我们知道,线程和数据库连接这些资源都是非常宝贵的资源。那么每次需要的时候创建,不需要的时候销毁,是非常浪费资源的。那么我们就可以使用缓存的策略,也就是使用线程池。当然了,线程池也不需要我们来实现,jdk的官方也给我们提供了API。
public class ThreadDemo06 {
public static void main(String[] args) {
// 创建一个线程池
ExecutorService cachedThreadPool = Executors.newFixedThreadPool(10);
cachedThreadPool.execute(new Runnable() {//提交线程任务,并执行
@Override
public void run() {
for(int i=0;i<10;i++){
printThreadInfo();
try {
Thread.sleep(300);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
});
// 关闭线程池
cachedThreadPool.shutdown();
for(int i=0;i<10;i++){
printThreadInfo();
try {
Thread.sleep(300);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
/**
* 输出当前线程的信息
*/
private static void printThreadInfo() {
System.out.println("当前运行的线程名为: " + Thread.currentThread().getName());
}
}
运行结果: