一、前言
1. 并发 ≠ 并行
并发 (concurrency) 和 并行 ( parallelism) 是不同的。
在单个 CPU 核上,线程通过时间片或者让出控制权来实现任务切换,达到 “同时” 运行多个任务的目的,这就是所谓的并发。但实际上任何时刻都只有一个任务被执行,其他任务通过某种算法来排队。
多核 CPU 可以让同一进程内的 “多个线程” 做到真正意义上的同时运行,这才是并行。
2. 程序、进程、线程、协程
程序:是一个静态的概念,但可以被动态地执行。例如静静地躺在你硬盘中的QQ.exe
进程:进程是系统进行资源分配的基本单位,有独立的内存空间。是一个动态的概念,一个程序可以对应多个进程。
线程:线程是 CPU 调度和分派的基本单位,线程依附于进程存在,每个线程会共享父进程的资源。是一个进程的不同执行路径。一个进程可以对应多个线程。可以想象成线程执行指令的过程像一根线在穿梭。
协程:协程是一种用户态的轻量级线程,协程的调度完全由用户控制,协程间切换只需要保存任务的上下文,没有内核的开销。很遗憾的是目前Java还没有提供语言级别的协程支持
3. 线程上下文切换
由于中断处理,多任务处理,用户态切换等原因会导致 CPU 从一个线程切换到另一个线程,切换过程需要保存当前进程的状态并恢复另一个进程的状态。
上下文切换的代价是高昂的,因为在核心上交换线程会花费很多时间。上下文切换的延迟取决于不同的因素,大概在在 50 到 100 纳秒之间。考虑到硬件平均在每个核心上每纳秒执行 12 条指令,那么一次上下文切换可能会花费 600 到 1200 条指令的延迟时间。实际上,上下文切换占用了大量程序执行指令的时间。
如果存在跨核上下文切换(Cross-Core Context Switch),可能会导致 CPU 缓存失效(CPU 从缓存访问数据的成本大约 3 到 40 个时钟周期,从主存访问数据的成本大约 100 到 300 个时钟周期),这种场景的切换成本会更加昂贵。
4. 为何需要多线程
既然线程上下文切换会消耗资源和时间,那为何还需要使用多线程?一般来说io消耗的代价远高于线程切换。而目前大多数公司提供的服务是io密集型,不会大量占用cpu资源,单线程的进程cpu在执行io操作时会阻塞,该进程无法响应其他请求。例如一个用户请求服务器的用户数据,服务器需要去数据库取数据,此时该进程等待数据库网络io,无法接受另一个用户的请求。
二、线程的创建和启动
1. 创建:
- 继承Thread类,并覆盖run()方法
- 实现Runnable接口,并覆盖run()方法
- 实现Callable接口,并覆盖call()方法
2. 启动:
- new Thread(…).start()
- threadPool.execute(…)
三、线程的状态
/**
* A thread state. A thread can be in one of the following states:
* <ul>
* <li>{@link #NEW}<br>
* A thread that has not yet started is in this state.
* </li>
* <li>{@link #RUNNABLE}<br>
* A thread executing in the Java virtual machine is in this state.
* </li>
* <li>{@link #BLOCKED}<br>
* A thread that is blocked waiting for a monitor lock
* is in this state.
* </li>
* <li>{@link #WAITING}<br>
* A thread that is waiting indefinitely for another thread to
* perform a particular action is in this state.
* </li>
* <li>{@link #TIMED_WAITING}<br>
* A thread that is waiting for another thread to perform an action
* for up to a specified waiting time is in this state.
* </li>
* <li>{@link #TERMINATED}<br>
* A thread that has exited is in this state.
* </li>
* </ul>
*
* <p>
* A thread can be in only one state at a given point in time.
* These states are virtual machine states which do not reflect
* any operating system thread states.
*
* @since 1.5
* @see #getState
*/
public enum State {
NEW,
RUNNABLE,
BLOCKED,
WAITING,
TIMED_WAITING,
TERMINATED;
}
以1.8为例,JDK提供了这六种线程状态
- NEW:顾名思义刚刚new出来还未start的线程所处的状态
- RUNNABLE:线程start之后所处的状态,可能正在执行,也可能等待CPU的调度
- BLOCKED:线程等待同步锁时所处的状态,例如等待进入synchronized方法块
- WAITING:类似于线程挂起的状态,是该线程主动让出执行机会,需要其他线程唤醒。调用如下三个方法会进入这个状态 ①Object.wait() ②Thread.join() ③LockSupport.park()
- TIMED_WAITING:有限期等待,调用如下五个方法会进入该状态。①Object.wait(long timeout) ②Thread.sleep(long millis) ③Thread.join(long millis) ④LockSupport.parkNanos(Object blocker, long nanos) ⑤LockSupport.parkUntil(Object blocker, long deadline) 其中前三个方法会抛出InterruptedException
- TERMINATED:终止状态,线程执行完毕后退出所处的状态
四、底层
1. JVM
在Java代码中new出Thread后并不意味着内核中创建出了一个线程,它仅仅代表着jvm中创建一个线程对象。只有当调用start方法后内核中的线程才会真正被创建。下面的start0是关键方法
//Thread.java
public synchronized void start() {
//省略...
boolean started = false;
try {
start0();
started = true;
} finally {
//...
}
}
private native void start0();
该本地方法经过一系列调用链会执行到如下的c++方法(具体如何调用可以查看参考文章链接)
// hotspot/src/os/linux/vm/os_linux.cpp
bool os::create_thread(Thread* thread, ThreadType thr_type,
size_t req_stack_size) {
//...
pthread_t tid;
//第一个参数是线程id,第二个参数是属性,第三个参数是调用的方法,第四个参数是调用方法入参
int ret = pthread_create(&tid, &attr, (void* (*)(void*)) thread_native_entry, thread);
//...
return true;
}
pthread_create是linux提供的一个c函数,定义在pthread.h中。thread_native_entry经过一系列调用最终执行的下面方法。vmSymbols::run_method_name():即java中的"run"方法;
// hotspot/src/share/vm/prims/jvm.cpp
static void thread_entry(JavaThread* thread, TRAPS) {
HandleMark hm(THREAD);
Handle obj(THREAD, thread->threadObj());
JavaValue result(T_VOID);
JavaCalls::call_virtual(&result,
obj,
KlassHandle(THREAD, SystemDictionary::Thread_klass()),
vmSymbols::run_method_name(),
vmSymbols::void_method_signature(),
THREAD);
}
2. 操作系统
下面通过一个小例子看下线程在OS层面的实现。
public class ThreadTest {
public static void main(String[] args) throws InterruptedException {
Thread t = new Thread(() -> System.out.println("hello thread"));
t.start();
t.join();
}
}
运行一下这个小程序,并追踪一下系统调用strace -ff -o syscall.out java ThreadTest
该命令会对每个线程输出一个文件,文件中记录着每一个系统调用。ll一下发现有11个文件生成,而不是2个,可以得出jvm在执行代码时自身也会创建出多个线程,整个java进程在运行时所产生的内核线程要大于你在java代码中编写的。那么究竟哪个才是我们代码中的那个线程呢?
[hch@instance-mfw2qss3] tt$ ll
total 356
drwxrwxr-x 2 hch hch 4096 May 23 00:43 .
drwxr-xr-x 5 hch hch 4096 May 22 20:33 ..
-rw-rw-r-- 1 hch hch 13641 May 22 21:12 syscall.out.22187
-rw-rw-r-- 1 hch hch 230173 May 22 21:12 syscall.out.22188
-rw-rw-r-- 1 hch hch 1585 May 22 21:12 syscall.out.22189
-rw-rw-r-- 1 hch hch 1006 May 22 21:12 syscall.out.22190
-rw-rw-r-- 1 hch hch 1139 May 22 21:12 syscall.out.22191
-rw-rw-r-- 1 hch hch 1297 May 22 21:12 syscall.out.22192
-rw-rw-r-- 1 hch hch 40622 May 22 21:12 syscall.out.22193
-rw-rw-r-- 1 hch hch 27136 May 22 21:12 syscall.out.22194
-rw-rw-r-- 1 hch hch 1007 May 22 21:12 syscall.out.22195
-rw-rw-r-- 1 hch hch 1873 May 22 21:12 syscall.out.22196
-rw-rw-r-- 1 hch hch 1268 May 22 21:12 syscall.out.22197
-rw-rw-r-- 1 hch hch 1151 May 22 21:12 ThreadTest.class
-rw-rw-r-- 1 hch hch 218 May 22 21:12 ThreadTest.java
[hch@instance-mfw2qss3] tt$ grep -rn "hello thread" .
./syscall.out.22197:13:write(1, "hello thread", 12) = 12
./ThreadTest.java:3: Thread t = new Thread(() -> System.out.println("hello thread"));
Binary file ./ThreadTest.class matches
[hch@instance-mfw2qss3] tt$ grep -rn 22197 .
./syscall.out.22188:3140:clone(child_stack=0x7fc4355a8fb0, flags=CLONE_VM|CLONE_FS|CLONE_FILES|CLONE_SIGHAND|CLONE_THREAD|CLONE_SYSVSEM|CLONE_SETTLS|CLONE_PARENT_SETTID|CLONE_CHILD_CLEARTID, parent_tidptr=0x7fc4355a99d0, tls=0x7fc4355a9700, child_tidptr=0x7fc4355a99d0) = 22197
./syscall.out.22197:2:gettid() = 22197
./syscall.out.22197:8:sched_getaffinity(22197, 32, [0]) = 32
./syscall.out.22197:9:sched_getaffinity(22197, 32, [0]) = 32
从以上结果可以看出22188号线程是我们编写的主线程,22197号线程是我们在主线程中创建出来的另一个线程。最终pthread_create中执行的系统调用为clone函数,该函数生成了一个轻量级进程(LWP),并且与内核线(KLT)程是一对一的关系。
clone(child_stack=0x7fc4355a8fb0,flags=CLONE_VM|CLONE_FS|CLONE_FILES|CLONE_SIGHAND|CLONE_THREAD|CLONE_SYSVSEM|CLONE_SETTLS|CLONE_PARENT_SETTID|CLONE_CHILD_CLEARTID, parent_tidptr=0x7fc4355a99d0, tls=0x7fc4355a9700, child_tidptr=0x7fc4355a99d0) = 22197
五、参考
图片来源于深入理解java虚拟机
Go 为什么这么“快”
JVM之Java线程启动流程