如果说编写正确的程序很难,那么编写正确的并发程序更是难上加难,相比Java技术栈中的其他知识点,Java 多线程并发编程的学习门槛较高,导致很多人望而却步。但无论是职场面试,还是高并发/高流量系统的实现,都离不开并发编程,于是能够真正掌握并发编程的人成为了市场迫切需求的人才。
线程是进程中的一个执行路径,线程本身是不会独立存在的,而必须存在于某一个进程中,那么什么是进程呢?你打开的微信 App 就是一个进程、打开的手淘 App 是一个进程,打开的记事本也是一个进程。学过操作系统的朋友应该都知道,进程是代码+数据集合,是代码在数据集合上的运行活动。在操作系统中,进程是资源分配和调度的基本单位,也就是说操作系统是以进程为单位进行资源分配的,但是 CPU 这个资源却比较特殊,CPU 的分配是以线程为单位的,这是因为具体占用 CPU 运行的是进程中的线程。
在 Java 的世界中,当你在 IDE 里面启动一个 main
函数时候,其实就是开启了一个 JVM 进程,在 Mac 下你可以使用 ps -eaf |grep java
查看进程的存在,在这个进程中其实是存在好多线程,大家可以使用 jstack pid 查看,例如下面是一个查看结果:
上图红框部分是线程堆栈里面 main
函数所在的线程,这个线程目前阻塞到 Thread.sleep
这个函数。
上图红色框里的是一个 gc 线程,这也说明了一个进程中是存在多个线程的,每个线程只是进程的一个执行单元。
下面我们使用一张图来解释进程与线程的关系:
如上图所示,Thread1 到 ThreadN 这 N 个线程共享进程的堆和方法区资源,但是每个线程有自己的程序计数器(PC)和栈(stack)区域。
-
其中 PC 计数器本质上是一块内存区域,用来记录线程当前要执行的指令地址,CPU 一般是使用时间片轮转方式让线程轮询占用的,因此当前线程 CPU 时间片用完后,要让出 CPU,这时 PC 计数器就会记录下当前线程下次要执行的命令的地址,等下次轮到该线程占有 CPU 执行时,就从 PC 计数器获取自己将要执行的命令的地址继续执行。
-
每个线程有自己的栈资源,用于存储该线程的局部变量,这些局部变量是该线程私有的,其它线程是访问不了的,比如当一个线程调用下面的
test
方法的时候,在test
方法体内创建的局部变量a
、b
就是在该线程的栈中分配的:void test() { int a = 5; int b = 6; }
另外,栈还用来存放线程的调用栈帧,那么什么是栈帧呢?如下代码,当我们调用
test()
方法时,就会把当前方法的一些信息封装为栈帧压入到栈顶,栈顶的栈帧就是活跃的test
方法。当执行到say()
方法时就会在栈顶新加一个关于say()
方法的栈帧,这时候say()
方法所在栈帧就是活跃栈帧。当say()
方法执行完毕后,say()
方法所在的栈顶帧就会出栈,这时候栈顶活跃帧就是test
方法的了。
void test(int x,int y) { int a = 5; int b = 6; say(); }
-
堆(heap)是一个进程中最大的一块内存,是进程创建时候创建的,堆是被进程中的所有线程共享的。堆里面主要存放使用
new
操作创建的对象实例,如下操作就是在堆上创建了一个ArrayList
对象,这里需要注意的是,指向堆对象的list
变量本身是在线程的栈上保存的,只是list
指向了堆上的ArrayList
的地址。void test(int x,int y) { List<String> list = new ArrayList<String>(); }
-
方法区(method area)用来存放 JVM 加载的类信息、常量、静态变量等信息,也是线程共享的。
在 Java 中创建线程有两种方式:
- 实现
Runable()
接口
public class TestRxJava {
private static final String THREAD_NUM = "sub-thread";
public static void main(String[] args) {
Thread thread = new Thread(new Runnable() {
public void run() {
System.out.println("--" + Thread.currentThread().getName() + "--");
try {
Thread.sleep(200000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}, THREAD_NUM);
thread.start();
System.out.println("--" + Thread.currentThread().getName() + "--");
try {
Thread.sleep(200000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
以上代码里创建了一个线程 thread,并且指定了该线程的名字为 sub-thread
(注:创建线程时候给每个线程指定业务相关的名称,有利于问题排查),并调用 thread 的 start
方法启动了该线程,线程内部首先打印一行,然后调用 sleep
方法挂起自己。运行该代码就会创建一个 JVM 进程,该进程里面包含 main
函数所在的线程和我们自己创建的 thread 线程,当然不止这两个线程,运行后会输出:
–sub-thread–
–main–
这时候你使用 jstack pid 查看线程堆栈会发现:
由以上可知,main
函数所在线程阻塞了 sleep
方法。
由以上可知,我们创建的 sub-thread
线程也阻塞到了 sleep
方法。
- 继承
Thread
类并重写run
方法
public class Test {
private static final String THREAD_NUM = "sub-thread";
static class MyThread extends Thread {
public MyThread(String name) {
super(name);
}
@Override
public void run() {
System.out.println("--" + Thread.currentThread().getName() + "--");
try {
Thread.sleep(200000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
public static void main(String[] args) {
MyThread thread = new MyThread(THREAD_NUM);
thread.start();
System.out.println("--" + Thread.currentThread().getName() + "--");
try {
Thread.sleep(200000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
在 Java 中线程被分为了两大类:daemon 线程和 User 线程。默认我们创建的线程都是 User 线程,在 Java 中当进程中不存在任何 User 线程时候 jvm 就会退出:
public class TestRxJava {
private static final String THREAD_NUM = "sub-thread";
public static void main(String[] args) {
Thread thread = new Thread(new Runnable() {
public void run() {
System.out.println("--" + Thread.currentThread().getName() + "--");
try {
Thread.sleep(200000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}, THREAD_NUM);
thread.start();
System.out.println("--" + Thread.currentThread().getName() + "--");
}
}
如上代码中 sub-thread
线程和 main
函数所在线程就是 User 线程,运行上面代码,你会发现 jvm 进程会一直存在,直到 sub-thread
线程执行完毕。并且执行 jstack pid 查看线程堆栈,会发现 main
函数所在线程已经不存在了,但是 sub-thread
线程还是存在的,这说明 main
函数退出后,jvm 函数并没退出。
Java 中创建 daemon 线程:
public class TestRxJava {
private static final String THREAD_NUM = "sub-thread";
public static void main(String[] args) {
Thread thread = new Thread(new Runnable() {
public void run() {
System.out.println("--" + Thread.currentThread().getName() + "--");
try {
Thread.sleep(200000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}, THREAD_NUM);
thread.setDaemon(true);
thread.start();
System.out.println("--" + Thread.currentThread().getName() + "--");
}
}
创建 daemon 线程只需要设置 thread.setDaemon(true)
,这时候你再运行上面的代码,会发现 jvm 进程直接退出了,这是因为这里 main
函数所在线程是唯一的 User 线程,当 main
函数运行完毕后,当前进程就不存在 User 线程,所以 jvm 进程就退出了。