背景
多线程入门知识挺多的,尤其是JMM需要学习。JVM内存模型也需要学习。很多概念需要搞明白,比如进程,线程,协程,守护线程,用户线程,线程上下文切换,共享变量,局部变量,用户空间,内核空间,单例,多例等等。线程间通信表达什么意思。线程间如何交换数据。业务逻辑和线程的隔离(Runnable, Thread)。线程的生命周期。
核心概念理解
- 进程
- 比如Java程序一旦起来,其实就是Java进程,就会分配相应的内存结构。也有可能有多个进程,比如一些守护进程(system.gc)。
- 在同一个JVM上可以跑很多个Java应用程序,其实就是Java进程,而且这些进程间是相互独立的。
- 线程 在《深入理解计算机系统》中有详细的描述
- 在HotSpot VM线程模型中,Java线程(java.lang.Thread)被一对一映射为本地操作系统线程。Java线程启动时会创建一个本地操作系统线程;当该Java线程终止时,这个操作系统线程也会被回收。操作系统会调度所有线程并将它们分配给可用的CPU。线程是程序执行的基本单位,是CPU时间片调度的基本单位。
- 协程
- Kotlin编程语言中有协程的概念。协程开销小,无抢占式的调度。协程也是线程。协程调用切换时,将寄存器上下文和栈保存到其他地方,在切回来的时候,恢复先前保存的寄存器上下文和栈,直接操作栈则基本没有内核切换的开销,可以不加锁的访问全局变量。所以上下文切换非常快。
- Java编程语言是没有协程概念的,但是也是可以支持的。使用quasar.jar包。
- Golang编程语言中的goroutine就是轻量级的线程。与创建线程相比,创建成本和开销都很小,每个goroutine的堆栈只有几kb。
-
守护线程
守护线程有一定的保护、协助和辅助的含义。它是建立子用户线程之上的。如果用户线程全部销毁了,那么守护线程也会被销毁。Java中一个守护线程GC线程。 -
用户线程
当Java应用程序启动后,Java进程存活于JVM中。应用程序通过java.lang.Thread类创建的线程就是用户线程。比如Executor框架中的线程池中的线程就是用户线程。 -
线程上下文切换
操作系统采用时间片的方式对线程进行调度。同一个核心每个时刻只有一个线程在执行,多颗核心,同时就会有多个线程执行。当线程切换的时候,当前线程的状态就会保存到寄存器中,如果线程抢占到资源的时候,在恢复线程状态,并执行。 -
共享变量
共享变量是需要考量线程安全的,比如JVM内存模型中的堆上内存,这个堆上分配的引用类型就是共享变量,同时可能会被多个线程访问,所以需要我们保证线程安全。 -
局部变量
局部变量是线程安全的,能够为每个线程创建一份。 -
用户空间
像Java进程一样,当启动Java应用程序后,会给Java进程就是用户空间。在这个用户空间中有分配的内存大小,有创建的用户线程。 -
内核空间
是操作系统的概念。区别用户空间。 -
单例
单例是线程不安全的,每个线程都会访问到同一个变量。如果单例的属性都是finall修饰的,那么是线程安全。如果不是线程安全的话,需要使用保证线程安全的知识。比如ThreadLocal, volatile, synchronized,final的运用。 -
多例
多例是线程安全的,是为每个线程都会创建一个实例。相当于一个局部变量一样。 -
轻量级的线程
- 少一些时间片的抢占。
- 少一些上下文的切换。
- 执行栈直接去寄存器中拿操作指令,而不关心其他操作,比如锁之类的。
- Java应用程序启动线程说明
- 当非守护线程main启动的时候,后台会启动很多其他守护线程,比如Reference Handler句柄引用的守护线程,Finalizer垃圾回收的守护线程,Signal Dispatcher 信号的守护线程比如kill -9 给操作系统发送一个信号。
- 当我们另启一个自定义线程叫Custom-Thread,这个线程跟main线程是独立的。
- 当main线程中的逻辑执行完成后,main线程就销毁了。Custom-Thread线程正在执行。通过线程sleep时间进行调节。
- 而当Custom-Thread线程执行完成后,Custom-Thread线程就被销毁了,main线程依然在执行。通过sleep时间进行调节。
- main是入口线程,在这个main线程中启动几个自定义线程,但是main的逻辑执行完成后,main线程就被销毁掉。然后其他线程继续执行。之所以能够继续执行是因为这个Java进程还在。为什么进程还在呢?是因为main在启动过程中还有启动了很多守护线程或守护进程。一旦线程全部执行完成了,则这些守护线程也没有存在的意义了,也会被销毁掉。
- 为什么要使用多线程?
- 现代计算机几乎都是多核的。使用多线程可以避免硬件资源的浪费。
- 在一个功能中。我可以启动一个线程去读文件,在启动一个线程去发出HTTP请求。解耦业务逻辑,提高程序执行速度。
- 核心目的就是:加快程序执行速度。
- 并发与并行
- 并发,描述的是系统中同时有多少线程在运行比如100w个线程之类的,计算机依然是需要在线程间进行上下文切换,对应CPU核来说,它依然是同一个时间点只有一个线程被执行。
- 并行,描述的是系统中的线程是同时运行。如果要支持同时运行,则我们的CPU必须是多核心的,不然永远也无法并行。哪怕我们的CPU使用了超线程技术,它依然只能在同一个时间点,也只能执行一个线程,另一个线程还是被挂起的。
- 线程的生命周期
- new Thread(“MyThread“),创建一个线程。
- 调用start()后,线程存活在JVM中,但是线程并不会立即执行。而只是具备可执行的状态。runnable
- CPU(Dispatch)分配时间片执行。
- running
- blocked
- terminated
- 在CPU进行线程切换的时候,就是在running和runnable之间状态不停切换。任何一个状态都有可能变成死亡状态。是JVM调用run()方法。main线程去启动其他线程。如果是在main中直接调用线程的run方法的话,那么是不会有新的线程出现的,所以调用start方法才是启动一个新的线程。
- 如何理解run()方法?
- 其实Thread使用了模板方法设计模式,并预留出run()方法。因为Thread并不知道用户的具体实现逻辑是怎样的,用户需要自己继承Thread并且重写run方法。
- 并且这个模板方法是必须定义为final类型的,这样子类是不能复写这个方法。
- 源码底层有native方法采用C++写的,start0()。
- 线程其他基础信息
- 只要Java涉及到的线程,都只有一个Thread类。Thread类实现了Runnable接口。
- 无论是线程池还是Java并发包下面的所有的一些关于线程的实现,都是需要使用Thread类。
- 在实践的应用。Netty中NioEventLoop类就持有Thread类的引用。
- 也就说,当说到线程,一定是指的java.lang.Thread。
小结
- Java应用程序的main函数是一个线程,是被JVM启动的时候调用,线程的名字叫main。
- 实现一个线程,必须创建Thread实例, override run 方法,并且调用start方法。
- 在JVM启动后,实际上有多个线程,但是至少有一个非守护线程。
- 当调用一个线程start方法的时候,此时至少有两个线程,一个是启动线程的线程,还有一个是执行run方法的线程,当线程被执行了start()方法后。
- 线程的生命周期分为:new, runnable, running, block, terminate。熟悉状态之间的转换条件。