基本概念

要了解多线程,需要先了解一下这些基本概念:
程序:由特定语言编写,完成某项功能的指令集合。
进程:程序的一次执行过程,或者是正在运行的程序;是一个动态的过程:有自身的产生、存在和消亡的过程。
线程:可以理解进程的执行单位,是操作系统能够进行运算调度的最小单位。它被包含在进程之中,是进程中的实际运作单位;
Java中,每个线程拥有独立的运行栈和程序计数器(pc)。

多线程:Java的main方法是一个线程,当你想main方法与别的方法同时执行时,就需要创建线程来执行对应的方法;
简而言之,多线程就是Java中多个线程共同运行,分别或共同完成特定的功能,充分利用CPU。
线程优先级:线程存在1-10的优先级。
线程的生命周期(几种状态)
新建 – 就绪 – 运行 – 阻塞 – 死亡

为什么需要多线程和多线程的优点

多线程在特定场景下可以提高系统的响应速度,完成一些高并发场景下的需求。

多线程的优点:
(1)提高应用程序的响应。图像化页面情况下可增强用户的体验;
(2)提高计算机系统CPU的利用率;
(3)改善程序结构;将既长又复杂的进程分为多个线程,独立运行,利于理解和修改。

Java创建多线程方式一:继承Thread类

  1. 创建一个继承于Thread类的子类
  2. 重写Thread类的run() –> 将需要此线程执行的操作声明在run()中
  3. 创建Thread类的子类的对象
  4. 通过此对象调用start()
1
2
3
4
5
6
7
8
9
10
11
12
13
public class Demo {
public static class MyThread extends Thread {
@Override
public void run() {
System.out.println("MyThread");
}
}

public static void main(String[] args) {
Thread myThread = new MyThread();
myThread.start();
}
}

注意要调用start()方法后,该线程才算启动!
我们在程序里面调用了start()方法后,虚拟机会先为我们创建一个线程,然后等到这个线程第一次得到时间片时再调用run()方法。
注意不可多次调用start()方法。在第一次调用start()方法后,再次调用start()方法会抛出IllegalThreadStateException异常;因为线程的生命周期是个不可循环的过程,一个线程对象结束了不能再次start。

Java创建多线程方式二:实现Runnale接口

  1. 创建一个实现了Runnable接口的类
  2. 该实现类去实现Runnable中的抽象方法:run(),将需要此线程执行的操作声明在run()中
  3. 创建实现类的对象
  4. 将此对象作为参数传递到Thread类的构造器中,创建Thread类的对象
  5. 通过Thread类的对象调用start()方法

先来看下Runnable接口:

1
2
3
4
@FunctionalInterface
public interface Runnable {
public abstract void run();
}

可以看到Runnable是一个函数式接口,这意味着我们可以使用Java 8的函数式编程来简化代码。
示例代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public class Demo {
public static class MyThread implements Runnable {
@Override
public void run() {
System.out.println("MyThread");
}
}

public static void main(String[] args) {
//方式一:
new Thread(new MyThread()).start();
//方式二: Java 8 函数式编程,可以省略MyThread类
new Thread(() -> {
System.out.println("MyThread,Java 8 匿名内部类");
}).start();
}
}

Java创建多线程方式三:实现Callable接口

  1. 创建一个实现了Callable接口的MyCallable类
  2. MyCallable实现类去实现Callable中的抽象方法:call(),将需要此线程执行的操作声明在call()中,并且有返回值
  3. 创建MyCallable实现类的对象;
  4. 使用FutureTask类来包装Callable对象,该FutureTask对象封装了该Callable对象的call()方法的返回值。
  5. 将FutureTask对象作为参数传递到Thread类的构造器中,然后直接调用start()方法
  6. 可以通过futureTask.get()方法,获取线程的返回值

示例代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
public class CallableTest {
public static void main(String[] args) {
MyCallable myCallable = new MyCallable();
FutureTask futureTask = new FutureTask(myCallable);
new Thread(futureTask).start();

try {
Object sum = futureTask.get();
System.out.println(sum);
} catch (InterruptedException e) {
e.printStackTrace();
} catch (ExecutionException e) {
e.printStackTrace();
}
}
}

class MyCallable implements Callable{

@Override
public Object call() throws Exception {
int sum = 0;
for (int i = 0; i <= 100; i++) {
if (i % 2 ==0){
System.out.println(i);
sum +=i;
}
Thread.sleep(100);
}
return sum;
}
}

输出结果如下:

1
2
3
4
5
0
2
4
6
...

Java创建多线程方式四:使用线程池(重要、推荐使用)

Java创建线程池总体上可以分为2类:
(1)一类是通过Executors创建的线程池;
(2)另一类是通过ThreadPoolExecutor创建的线程池。

ThreadPoolExecutor虽然是最原始的创建线程池的方式,它包含了 7 个参数可供设置,但是也是推荐使用的。
在阿里巴巴开发手册中,明确强制线程池不允许使用 Executors 去创建,而是通过 ThreadPoolExecutor 的方式,因为这样的处理方式可以让大家更加明确线程池的运行规则,规避资源耗尽的风险。
说明:Executors 返回的线程池对象的弊端如下:
(1) FixedThreadPool 和 SingleThreadPool:
允许的请求队列长度为 Integer.MAX_VALUE,可能会堆积大量的请求,从而导致 OOM。
(2) CachedThreadPool:
允许的创建线程数量为 Integer.MAX_VALUE,可能会创建大量的线程,从而导致 OOM。

那么怎么通过ThreadPoolExecutor去创建线程池呢?有人说直接创建对应的对象即可,那么创建对象的时候需要传什么参数进去呢?这些参数是什么意思呢?下面我们先来看看ThreadPoolExecutor最全的一个构造器的源码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
public ThreadPoolExecutor(int corePoolSize,
int maximumPoolSize,
long keepAliveTime,
TimeUnit unit,
BlockingQueue<Runnable> workQueue,
ThreadFactory threadFactory,
RejectedExecutionHandler handler) {
if (corePoolSize < 0 ||
maximumPoolSize <= 0 ||
maximumPoolSize < corePoolSize ||
keepAliveTime < 0)
throw new IllegalArgumentException();
if (workQueue == null || threadFactory == null || handler == null)
throw new NullPointerException();
this.acc = System.getSecurityManager() == null ?
null :
AccessController.getContext();
this.corePoolSize = corePoolSize;
this.maximumPoolSize = maximumPoolSize;
this.workQueue = workQueue;
this.keepAliveTime = unit.toNanos(keepAliveTime);
this.threadFactory = threadFactory;
this.handler = handler;
}

可以看到,ThreadPoolExecutor构造器的参数长达7个之多,我们需要完全了解这些参数的意义,才可以创建好自己想要的线程池:

corePoolSize:the number of threads to keep in the pool, even if they are idle, unless {@code allowCoreThreadTimeOut} is set (核心线程数大小:不管它们创建以后是不是空闲的。线程池需要保持 corePoolSize 数量的线程,除非设置了 allowCoreThreadTimeOut;就是线程池中始终存活的线程数)

maximumPoolSize:the maximum number of threads to allow in the pool。(最大线程数:线程池中最多允许创建 maximumPoolSize 个线程。)

keepAliveTime:when the number of threads is greater than the core, this is the maximum time that excess idle threads will wait for new tasks before terminating。(存活时间:如果经过 keepAliveTime 时间后,超过核心线程数的线程还没有接受到新的任务,那就回收。)

unit:the time unit for the {@code keepAliveTime} argument (keepAliveTime 的时间单位。)

workQueue:the queue to use for holding tasks before they are executed. This queue will hold only the {@code Runnable} tasks submitted by the {@code execute} method。(存放待执行任务的队列:当提交的任务数超过核心线程数大小后,再提交的任务就存放在这里。它仅仅用来存放被 execute 方法提交的 Runnable 任务。所以这里就不要翻译为工作队列了。)

threadFactory:the factory to use when the executor creates a new thread。(线程工程:用来创建线程工厂。比如这里面可以自定义线程名称,当进行虚拟机栈分析时,看着名字就知道这个线程是哪里来的。)

handler :the handler to use when execution is blocked because the thread bounds and queue capacities are reached。(拒绝策略:当队列里面放满了任务、最大线程数的线程都在工作时,这时继续提交的任务线程池就处理不了,应该执行怎么样的拒绝策略。)

workQueue包含以下 7 种类型:

ArrayBlockingQueue:一个由数组结构组成的有界阻塞队列。
LinkedBlockingQueue:一个由链表结构组成的有界阻塞队列。
SynchronousQueue:一个不存储元素的阻塞队列,即直接提交给线程不保持它们。
PriorityBlockingQueue:一个支持优先级排序的无界阻塞队列。
DelayQueue:一个使用优先级队列实现的无界阻塞队列,只有在延迟期满时才能从中提取元素。
LinkedTransferQueue:一个由链表结构组成的无界阻塞队列。与SynchronousQueue类似,还含有非阻塞方法。
LinkedBlockingDeque:一个由链表结构组成的双向阻塞队列。
较常用的是 LinkedBlockingQueueSynchronous,线程池的排队策略与 BlockingQueue 有关。

handler拒绝策略,拒绝处理任务时的策略,系统提供了 4 种可选:

AbortPolicy:拒绝并抛出异常。
CallerRunsPolicy:使用当前调用的线程来执行此任务。
DiscardOldestPolicy:抛弃队列头部(最旧)的一个任务,并执行当前任务。
DiscardPolicy:忽略并抛弃当前任务。
默认策略为 AbortPolicy

ThreadPoolExecutor线程池的参数我们都了解完了,是不是有点迷糊呢?那么我们来通过图例了解一下ThreadPoolExecutor线程池的执行流程,加深对参数、在实际使用中的理解:
线程池执行流程

当线程数小于核心线程数时,添加工作线程并执行。
当线程数大于等于核心线程数,且任务队列未满时,将任务放入任务队列。
当线程数大于等于核心线程数,且任务队列已满:
若线程数小于最大线程数,创建线程;
若线程数等于最大线程数,默认情况下抛出异常,拒绝任务。

下面来演示一下ThreadPoolExecutor线程池代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
public static void main(String[] args) {
int corePoolSize = 3;
int maximumPoolSize = 10;
long keepAliveTime = 100L;
TimeUnit unit = TimeUnit.MILLISECONDS;
BlockingQueue<Runnable> workQueue = new ArrayBlockingQueue<Runnable>(1);
ThreadFactory threadFactory = r -> {
Thread t = new Thread(r);
t.setName("测试应用---" + t);
return t;
};
ThreadPoolExecutor.DiscardPolicy discardPolicy = new ThreadPoolExecutor.DiscardPolicy();

ThreadPoolExecutor threadPoolExecutor = new ThreadPoolExecutor(corePoolSize, maximumPoolSize, keepAliveTime, unit, workQueue, threadFactory, discardPolicy);

for (int i = 0; i < 10; i++) {
threadPoolExecutor.execute(() -> {
for (int j = 0; j < 10; j++) {
System.out.println(j + ">>>>" + Thread.currentThread().getName());
}
});
}
}

 输出结果部分如下:

1
2
3
4
5
6
7
8
9
10
11
...
0>>>>测试应用---Thread[Thread-4,5,main]
1>>>>测试应用---Thread[Thread-4,5,main]
2>>>>测试应用---Thread[Thread-4,5,main]
3>>>>测试应用---Thread[Thread-4,5,main]
4>>>>测试应用---Thread[Thread-4,5,main]
0>>>>测试应用---Thread[Thread-3,5,main]
1>>>>测试应用---Thread[Thread-3,5,main]
2>>>>测试应用---Thread[Thread-3,5,main]
3>>>>测试应用---Thread[Thread-3,5,main]
...

Thread类的几个常用方法

currentThread():静态方法,返回对当前正在执行的线程对象的引用;
start():开始执行线程的方法,java虚拟机会调用线程内的run()方法;
yield():yield在英语里有放弃的意思,同样,这里的yield()指的是当前线程愿意让出对当前处理器的占用。这里需要注意的是,就算当前线程调用了yield()方法,程序在调度的时候,也还有可能继续运行这个线程的;
sleep():静态方法,使当前线程睡眠一段时间;
join():使当前线程等待另一个线程执行完毕之后再继续执行,内部调用的是Object类的wait方法实现的;
run():通常需要重写thread类中的此方法,将创建的线程要执行的操作写在此方法中;
getName():获取线程名字;
setName():设置线程名字;
stop():已过时;强制结束线程的生命周期;
isAlive():判断当前线程是否存活。

线程的生命周期

线程的生命周期

Thread类和Runnable接口方式的比较

对于这两种方法,在开发中优先选择:实现Runnable接口的方式;
原因:

  1. Runnable接口实现的方式没有类的单继承性的限制;
  2. Runnable接口实现的方式更适合来处理多个线程由共享数据的情况(继承方式的话共享数据需要声明为static,有时候还需要额外的处理)。

所以,我们通常优先使用“实现Runnable接口”这种方式来自定义线程类。

Runnable 和 Callable

Runnable自 Java 1.0 以来一直存在,但Callable仅在 Java 1.5 中引入,目的就是为了来处理Runnable不支持的用例。**Runnable 接口**不会返回结果或抛出检查异常,但是 Callable 接口可以。所以,如果任务不需要返回结果或抛出异常推荐使用 Runnable 接口,这样代码看起来会更加简洁。

execute 和 submit

  • execute()方法用于提交不需要返回值的任务,所以无法判断任务是否被线程池执行成功与否;
  • submit()方法用于提交需要返回值的任务。线程池会返回一个 Future 类型的对象,通过这个 Future 对象可以判断任务是否执行成功,并且可以通过 Futureget()方法来获取返回值,get()方法会阻塞当前线程直到任务完成,而使用 get(long timeout,TimeUnit unit)方法的话,如果在 timeout 时间内任务还没有执行完,就会抛出 java.util.concurrent.TimeoutException