【多线程与并发】线程

一、前言

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. 创建:

  1. 继承Thread类,并覆盖run()方法
  2. 实现Runnable接口,并覆盖run()方法
  3. 实现Callable接口,并覆盖call()方法

2. 启动:

  1. new Thread(…).start()
  2. 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提供了这六种线程状态

  1. NEW:顾名思义刚刚new出来还未start的线程所处的状态
  2. RUNNABLE:线程start之后所处的状态,可能正在执行,也可能等待CPU的调度
  3. BLOCKED:线程等待同步锁时所处的状态,例如等待进入synchronized方法块
  4. WAITING:类似于线程挂起的状态,是该线程主动让出执行机会,需要其他线程唤醒。调用如下三个方法会进入这个状态 ①Object.wait() ②Thread.join() ③LockSupport.park()
  5. 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
  6. TERMINATED:终止状态,线程执行完毕后退出所处的状态
    深入了解java虚拟机-状态转换

四、底层

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虚拟机-线程

五、参考

图片来源于深入理解java虚拟机
Go 为什么这么“快”
JVM之Java线程启动流程

猜你喜欢

转载自blog.csdn.net/hch814/article/details/106289606