Java并发之多线程

Java并发之多线程

什么是线程?

通常我们在使用桌面操作系统的时候,说的都是XXX进程。比如我们启动一个Java程序,那操作系统中就会新建一个Java进程。那线程是什么呢?线程是比进程更加轻量级的调度单位,在现代操作系统中,线程就是最小的调度单位,又被称为“轻量级进程”。

在一个进程中是可以创建多个线程的,这些线程拥有自己的虚拟机栈,本地方法栈,程序计数器。如下图JVM的运行时内存划分中绿色的部分,就是线程私有的。CPU在多个线程中高速切换,让用户感觉像是在同时执行。总结来说,操作系统中可以同时执行多个任务,每个任务就是进程;进程可以同时执行多个任务,每个任务就是线程。

有些初次学习Java的同学可能会很疑惑,好像在日常的开发中,很少用到多线程啊?其实多线程就伴随着我们的日常开发,举个栗子,如果只用单线程,那么在SpringMVC中,前端每发起一个HTTP请求,那么后端接口就会进入阻塞,等待这个线程执行完成,后面的请求才能继续执行。这样的情况下效率将会非常低下。之所以SpringMVC能同时处理多个请求,当然是使用了多线程。

其实Java程序天生就是多线程程序,让我们来看一段简单的Java代码:

1
2
3
4
5
6
7
8
9
10
11
12
public static void main(String[] args) {
//获取Java线程管理的MXBean
ThreadMXBean threadMXBean = ManagementFactory.getThreadMXBean();
//仅获取线程和堆栈信息
ThreadInfo[] threadInfos = threadMXBean.dumpAllThreads(false, false);
//遍历线程信息,仅打印线程ID和线程名称信息
for (ThreadInfo threadInfo : threadInfos) {
System.out.println("[" + threadInfo.getThreadId() + "]" + threadInfo.getThreadName());
}
//打印当前线程名字
System.out.println(Thread.currentThread().getName());
}

结果如下(不同版本的JDK可能不同):

1
2
3
4
5
6
[5]Monitor Ctrl-Break
[4]Signal Dispatcher
[3]Finalizer
[2]Reference Handler
[1]main
ThreadId:1 ThreadName:main

可以看出,我们仅仅跑了一个main方法,但是却有多个其他线程在同时执行。

为什么使用多线程?

  1. 发挥多处理器核心的优势
    现在的计算机核心数量已经越来越多,单核的计算机几乎已经不存在了。一个程序可以作为一个进程来运行,程序运行过程中可以创建多个线程,而一个线程在同一时刻只能运行在一个处理器核心上。如果是单线程程序,那么同一时间只能有一个进程的一个线程运行,即时有再多的核心,也无法发挥出多核处理器的优势。如果使用多线程,可以在不同的核心上运行不同的计算逻辑,将会显著的提升性能。
  2. 提升响应时间
    在有一些业务逻辑中,会涉及到复杂的流程,比如创建一个用户,要初始化很多数据,用户信息,用户菜单等等。用户在使用这个功能的时候,如果要等到所有流程执行完才能看到返回成功,那么很多用户是不能忍受这么长时间等待的。这时候就可以利用多线程,异步地去执行某些用户不关心的操作,尽快返回结果,提升用户体验。
  3. 合理利用系统资源
    进程在系统中是相互分隔的,而线程之间隔离程度比进程小,而且线程可以共享内存,进程公有数据,相互之间很容易就能实现通信。同时,创建线程的代价比进程要小很多,而且多线程执行效率也比多进程更高更节省系统资源。

多线程的好处不仅仅是这些,正是因为多线程带来的诸多好多,Java在语言内就内置了多线程支持,Java为多线程提供了良好的变成模型,让开发者能够专注对于问题的解决,为所遇到的问题建立合适的模型,而不是绞尽脑汁去思考如何将程序多线程化。

Java多线程的创建

在Java中有三种方式来实现多线程,但是都离不开Thread这个类,所有的线程对象都必须是Thread类或其子类的实例。每个线程都是执行一段程序流,Java使用线程执行体来代表这段程序流。

  • 继承Thread类创建线程
    步骤如下:
  1. 定义一个类继承Thread,并且重写其run()方法,run()方法就是我们所说的线程执行体。
  2. 创建Thread子类的实例,就相当于创建了线程对象。
  3. 调用实例的start()方法来启动线程。

具体代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
public class ThreadTest {
public static void main(String[] args) throws Exception {
for (int i = 0; i < 5; i++) {
MyThread thread = new MyThread("MyThread-" + i);
thread.start();
}
}
}

class MyThread extends Thread {
public MyThread(String name) {
super(name);
}

@Override
public void run() {
//这里可以直接使用getName()方法获取线程的名称,该方法是Thread类的实例方法
System.out.println(this.getName() + ":created success");
}
}

结果如下:

1
2
3
4
5
MyThread-0:created success
MyThread-1:created success
MyThread-2:created success
MyThread-3:created success
MyThread-4:created success

  • 实现Runnable接口来创建线程
  1. 定义Runnable接口的实现类,并重写该类的run()方法,该run()方法的方法体同样是该线程的线程执行体。
  2. 创建Runnable实现类的实例,并且以此实例作为Thread类的target来创建Thread对象,这个Thread对象才是真正的线程对象。

我们可以查看Thread的构造函数

1
2
3
public Thread(Runnable target, String name) {
init(null, target, name, 0);
}

具体代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
public class ThreadTest {
public static void main(String[] args) throws Exception {
MyThread myThread;
for (int i = 0; i < 5; i++) {
myThread = new MyThread();
new Thread(myThread, "MyThread-").start();
}
}
}

class MyThread implements Runnable {

@Override
public void run() {
//这里必须使用Thread.currentThread()方法来获取当前线程
System.out.println(Thread.currentThread().getName() + ":created success");
}
}

执行结果同上

  • 实现Callable接口创建线程

在上面的两种实现方式,都是在日常开发中经常见到的方式,但是从Java5开始,提供了Callable接口,它提供了一个call()方法来作为线程执行体,但不同的是call()方法比run()方法更加强大。
call()方法可以有返回值,同时call()方法可以声明抛出异常。

Callable不能直接作为Thread的target,因为他不是Runnable的子接口,所以Java提供了一个FutureTask实现类,该实现类同时实现了Future接口和Runnable接口,Future接口代表了call()方法的返回值。使用Callable的步骤如下:

  1. 创建Callable接口的实现类,实现call()方法,再创建该类的实例。
  2. 使用FutureTask来包装Callable对象,FutureTask封装了Callable对象的call()方法的返回值。
  3. 使用FutureTask的对象作为Thread对象的target来启动新线程。
  4. 调用FutureTask对象的get()方法来实现线程类,并启动新线程。

具体代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
public class ThreadTest {
public static void main(String[] args) throws Exception {
FutureTask<Integer> task;
for (int i = 0; i < 5; i++) {
task = new FutureTask<>(new MyThread());
String name = "MyThread-" + i;
new Thread(task, name).start();
System.out.println(name + " return:" + task.get());
}
}
}

class MyThread implements Callable<Integer> {

@Override
public Integer call() throws Exception {
Integer i = new Random().nextInt(10);
System.out.println(Thread.currentThread().getName() + ":created success");
return i;
}
}

结果如下:

1
2
3
4
5
6
7
8
9
10
MyThread-0:created success
MyThread-0 return:8
MyThread-1:created success
MyThread-1 return:9
MyThread-2:created success
MyThread-2 return:1
MyThread-3:created success
MyThread-3 return:3
MyThread-4:created success
MyThread-4 return:7

Runnable和Callable在JDK1.8之后已经变成了函数式接口,可以使用lambda表达式来创建他们的对象,会使代码更加的简洁。通过上述三种方式都可以实现多线程,实现Runnable和Callable接口的方式基本上相似,只是Callable的功能更加强大一些。在实际开发中可以根据自己的需求进行选择。

线程的生命周期

在知道了怎么创建线程之后,我们还需要搞清楚线程的生命周期。线程需要经历新建(new),就绪(Runnable),运行(Running),阻塞(Blocked)和死亡(Dead)这5种状态。下面这张图描述了线程生命周期各个状态的转换:

  • 在我们通过new创建了一个线程的实例过后,该线程就处于新建状态,此时这个线程对象就和其他Java对象一样,JVM为其分配内存,初始化成员变量的值。
  • 调用线程对象的start()方法之后,线程进入就绪状态,Java虚拟机会为其创建栈帧和程序计数器,但是这个状态的线程也并没有开始运行,只是表明这个线程已经可以开始运行了,具体运行时间要看JVM的调度。这里千万要注意,启动线程要使用start()方法,而不是run()方法,使用start()方法启动系统会把run()方法当做线程执行体来执行,但是如果使用run()方法,相当于会立即执行run()方法,线程对象也只是一个普通对象,不会把run()方法包装成线程执行体来执行。
  • 处于就绪状态的线程如果获取了CPU,那么就会进入运行状态,在这个状态的线程可能会调用sleep()方法进入阻塞,也可能调用yield()方法再次进入就绪状态,也可能完整地执行完成后进入死亡状态。如果线程进入死亡状态,就不能再次调用start()方法来启动它了,否则会抛出IllegalThreadStateExcetion异常。
  • 处于阻塞状态的线程在sleep()时间结束、线程调用的阻塞式IO方法已经返回、线程成功获取锁、被notify()方法唤醒或者调用resume()方法之后会重新进入就绪状态。

结束前

以上内容都是个人学习的总结,后面可能会补充更多,如果有错误,请指出。

参考:
  • 《Java并发编程的艺术》
  • 《疯狂Java讲义》