进程和线程

进程

进程是操作系统进行资源分配的最小单位。其中资源包括:CPU、内存空间、磁盘IO等,同一进程中的多条线程共享进程中的全部系统资源,而进程和进程之间是相互独立的。进程是具有一定独立功能的程序关于某个数据集合上的一次运行活动,进程是系统进行资源分配和调度的一个独立单位。
进程是程序在计算机上的一次执行活动。当运行一个程序,就启动了一个进程。程序是死的、静态的,进程是活的、动态的。进程可以分为:系统进程用户进程。凡是用于完成操作系统的各种功能的进程就是系统进程,它们就是处于运行状态下的操作系统本身,用户进程就是所有由用户启动的进程。
进程间通信的方式
1.管道(pipe)及有名管道(named pipe):管道可用于具有亲缘关系的父子进程间的通信,有名管道除了具有管道所具有的功能外,它还允许无亲缘关系进程间的通信。
2.信号(signal):信号是在软件层次上对中断机制的一种模拟,它是比较复杂的通信方式,用于通知进程有某事件发生,一个进程收到一个信号与处理器收到一个中断请求效果上可以说是一致的。
3.消息队列(message queue):消息队列是消息的链接表,它克服了上两种通信方式中信号量有限的缺点,具有写权限得进程可以按照一定得规则向消息队列中添加新信息;对消息队列有读权限得进程则可以从消息队列中读取信息。
4.共享内存(shared memory):可以说这是最有用的进程间通信方式。它使得多个进 程可以访问同一块内存空间,不同进程可以及时看到对方进程中对共享内存中数据得更新。这种方式需要依靠某种同步操作,如互斥锁和信号量等。
5.信号量(semaphore):主要作为进程之间及同一种进程的不同线程之间得同步和互斥手段。
6.套接字(socket):这是一种更为一般得进程间通信机制,它可用于网络中不同机器之间的进程间通信,应用非常广泛。

线程

线程有时被称为轻量级进程(Lightweight Process,LWP),是操作系统调度 (CPU调度)执行的最小单位。必须 依赖于进程而存在,线程是进程的一个实体,是CPU调度和分配的基本单位,它是比进程更小的、能独立运行的基本单位。线程自己基本上不拥有系统资源,只拥有一点在运行中必不可少的资源,如:程序计数器,栈。但是它可与同属一个进程的其他的线程共享所拥有的全部资源。
线程无处不在,任何一个程序都必须要创建线程,特别是java不管任何程序都必须启动一个main函数的主线程,javaWeb开发里面的定时任务、定时器、JSP和Servlet、异步消息处理机制,远程访问接口等,任何一个监听事件,onclick的触发事件等都离不开线程和并发的知识。
线程的同步互斥
线程同步是指线程之间所具有的一种制约关系,一个线程的执行依赖另一个线程的消息,当它没有得到另一个线程的消息时应等待,直到消息到达时才被唤醒。
线程互斥是指对于共享的进程系统资源,在各单个线程访问时的排它性。当有若干个线程都 要使用某一共享资源时,任何时刻最多只允许一个线程去使用,其它要使用该资源的线程必须等待,直到占用资源者释放该资源。线程互斥可以看成是一种特殊的线程同步。
四种线程同步互斥的控制方法
临界区:通过对多线程的串行化来访问公共资源或一段代码,速度快,适合控制数据访问。(在一段时间内只允许一个线程访问的资源就称为临界资源)。
互斥量:为协调共同对一个共享资源的单独访问而设计的。
信号量:为控制一个具有有限数量用户资源而设计。
事件:用来通知线程有一些事件已发生,从而启动后继任务的开始。

协程(纤程)

协程,英文Coroutines, 是一种基于线程之上,但又比线程更加轻量级的存在,协程不是被操作系统内核所管理,而完全是由程序所控制(也就是在用户态执行),具有对内核来说不可见的特性。这样带来的好处就是性能得到了很大的提升,不会像线程切换那样消耗资源。
image.png

  1. def A():
  2. print '1'
  3. print '2'
  4. print '3'
  5. def B():
  6. print 'x'
  7. print 'y'
  8. print 'z'

假设由协程执行,在执行A的过程中,可以随时中断,去执行B,B也可能在执行过程中中断再去执行A,结果可能是:1 2 x y 3 z。
协程的特点在于是一个线程执行,那和多线程比,协程有何优势?
线程的切换由操作系统调度,协程由用户自己进行调度,因此减少了上下文切换,提高了效率。
线程的默认stack大小是1M,而协程更轻量,接近1k。因此可以在相同的内存中开启更多的协程。
不需要多线程的锁机制:因为只有一个线程,也不存在同时写变量冲突,在协程中控制共享资源不加锁,只需要判断状态就好了,所以执行效率比多线程高很多。
注意: 协程适用于被阻塞的,且需要大量并发的场景(网络io)。不适合大量计算的场景。

上下文切换(Context Switch)

上下文切换是指CPU(中央处理单元)从一个进程或线程到另一个进程或线程的切换。
image.png上下文切换可以更详细地描述为内核(即操作系统的核心)对CPU上的进程(包括线程)执行以下活动:
1. 暂停一个进程的处理,并将该进程的CPU状态(即上下文)存储在内存中的某个地方;
2. 从内存中获取下一个进程的上下文,并在CPU的寄存器中恢复它;
3. 返回到程序计数器指示的位置(即返回到进程被中断的代码行)以恢复进程。
image.png上下文切换只能在内核模式下发生。内核模式是CPU的特权模式,其中只有内核运行,并提供对所有内存位置和所有其他系统资源的访问。其他程序(包括应用程序)最初在用户模式下运行,但它们可以通过系统调用运行部分内核代码。

用户态和内核态

内核模式(Kernel Mode)

在内核模式下,执行代码可以完全且不受限制地访问底层硬件。它可以执行任何CPU指令和引用任何内存地址。内核模式通常为操作系统的最低级别、最受信任的功能保留。内核模式下的崩溃是灾难性的,他们会让整个电脑瘫痪。

用户模式(User Mode)

在用户模式下,执行代码不能直接访问硬件或引用内存。在用户模式下运行的代码必须委托给系统api来访问硬件或内存。由于这种隔离提供的保护,用户模式下的崩溃总是可以恢复的。计算机上运行的大多数代码将在用户模式下执行。
windows系统用户模式和内核模式组件之间的通信如下
image.png
x86 CPU提供了四个保护环(protection rings):0、1、2和3。通常只使用0环(内核)和3环(用户)。
image.png
应用程序一般会在以下几种情况下切换到内核模式:
1. 系统调用。
2. 异常事件。当发生某些预先不可知的异常时,就会切换到内核态,以执行相关的异常事件。
3. 设备中断。在使用外围设备时,如外围设备完成了用户请求,就会向CPU发送一个中断信号,此时,CPU就会暂停执行原本的下一条指令,转去处理中断事件。此时,如果原来在用户态,则自然就会切换到内核态。

Java线程的实现方式

直接使用Thread类或继承Thread类

  1. 方式1:使用 Thread类或继承Thread
  2. // 创建线程对象 线程和任务在一起
  3. Thread t = new Thread() {
  4. public void run() {
  5. // 要执行的任务
  6. }
  7. };
  8. // 启动线程
  9. t.start();

继承Thread时候会调用run,run执行任务时候target.run(), target的本质是Runnable,所以Thread和Runnable其实是一种。
image.png

实现Runnable接口配合Thread,实现解耦的功能

该方式的目的是把线程和任务分开(实现解耦) Thread:代表线程 Runnable:可运行的任务

  1. Runnable runnable = new Runnable() {
  2. public void run(){
  3. // 要执行的任务
  4. }
  5. };
  6. // 线程和任务分开
  7. // 创建线程对象
  8. Thread t = new Thread(runnable);
  9. // 启动线程
  10. t.start();

使用有返回值的Callable

  1. class CallableTask implements Callable<Integer> {
  2. @Override
  3. public Integer call() throws Exception {
  4. return new Random().nextInt();
  5. }
  6. }
  7. //创建线程池
  8. ExecutorService service = Executors.newFixedThreadPool(10);
  9. //提交任务,并用 Future提交返回结果
  10. Future<Integer> future = service.submit(new CallableTask());

Callable需要借用线程池,判断线程池中的线程,需要调用ThreadFactory#newThread,也是new一个Thread返回一个线程,本质也是Thread,最终都是调start方法再启动线程。

使用 lambda

  1. new Thread(() > System.out.println(Thread.currentThread().getName())).start();

注:本质上Java中实现线程只有一种方式,都是通过new Thread()创建线程,调用Thread#start启动线程最终都会调用Thread#run方法

线程创建和启动流程总结

1.使用new Thread()创建一个线程,然后调用start()方法进行java层面的线程启动;
2.调用本地方法start0(),去调用jvm中的JVM_StartThread方法进行线程创建和启动;
3.调用new JavaThread(&thread_entry,sz)进行线程的创建,并根据不同的操作系统平台调用对应的os::create_thread方法进行线程创建;
4.新创建的线程状态为Initialized(初始状态),调用了sync->wait()的方法进行等待,等到被唤醒才继续执行thread->run();
5.调用Thread::start(native_thread)方法进行线程启动,此时将线程状态设置为RUNNABLE,接着调用os::start_thread(thread),根据不同的操作系统选择不同的线程启动方式;
6.线程启动之后状态设置为RUNNABLE,并唤醒第4步中等待的线程,接着执行thread->run()的方法;
7.JavaThread::run()方法会回调第1步new Thread中复写的run()方法。

Java线程的调度机制

线程调度是指系统为线程分配处理器使用权的过程,主要调度方式分两种,分别是协同式线程调度和抢占式线程调度

协同式线程调度

线程执行时间由线程本身来控制,线程把自己的工作执行完之后,要主动通知系统切换到另外一个线程上。最大好处是实现简单,且切换操作对线程自己是可知的,没啥线程同步问题。坏处是线程执行时间不可控制,如果一个线程有问题,可能一直阻塞在那里。

抢占式线程调度(Java线程使用)

每个线程将由系统来分配执行时间,线程的切换不由线程本身来决定(Java中, Thread.yield()可以让出执行时间,但无法获取执行时间)。线程执行时间系统可控,也不会有一个线程导致整个进程阻塞。
希望系统能给某些线程多分配一些时间,给一些线程少分配一些时间,可以通过设置线程优先级来完成。Java语言一共10个级别的线程优先级(Thread.MIN_PRIORITY=1至 Thread.MAX_PRIORITY=10,默认是5),在两线程同时处于ready状态时,优先级越高的线程越容易被系统选择执行。但优先级并不是很靠谱,因为Java线程是通过映射到系统的原生线程上来实现的,所以线程调度最终还是取决于操作系统

线程生命周期

操作系统层面

操作系统层面的线程生命周期基本上可以用下图这个“五态模型”来描述。这五态分别是:初始状态(new)、可运行状态(调用了start)、运行状态、休眠状态和终止状态。
image.png
1. 初始状态,指的是线程已经被创建,但是还不允许分配CPU执行。这个状态属于编程语言特有的,不过这里所谓的被创建,仅仅是在编程语言层面被创建,而在操作系统层面,真正的线程还没有创建。
2. 可运行状态,指的是线程可以分配CPU执行。在这种状态下,真正的操作系统线程已经被成功创建了,所以可以分配CPU执行。
3. 当有空闲的CPU时,操作系统会将其分配给一个处于可运行状态的线程,被分配到CPU的线程的状态就转换成了运行状态。
4. 运行状态的线程如果调用一个阻塞的API(例如以阻塞方式读文件)或者等待某个事件 (例如条件变量),那么线程的状态就会转换到休眠状态,同时释放CPU使用权,休眠状态的线程永远没有机会获得CPU使用权。当等待的事件出现了,线程就会从休眠状态转换到可运行状态。
5. 线程执行完或者出现异常就会进入终止状态,终止状态的线程不会切换到其他任何状态,进入终止状态也就意味着线程的生命周期结束了。
注:这五种状态在不同编程语言里会有简化合并。例如,C 语言的POSIX Threads规范,就把初始状态和可运行状态合并了;Java语言里则把可运行状态和运行状态合并了,这两个状态在操作系统调度层面有用,而JVM层面不关心这两个状态,因为JVM把线程调度交给操作系统处理了。

java线程层面 Thread#State

Java语言中线程共有六种状态,分别是:
image.png
image.png
注:上述Java线程中的BLOCKED、WAITING、TIMED_WAITING在操作系统层面上是一种状态(即休眠状态)。也就是说只要Java线程处于这三种状态之一,那么这个线程就永远没有CPU的使用权。
image.png
image.png

Thread/Object常用方法

Thread方法:

sleep

调用sleep会让当前线程从Running进入TIMED_WAITING状态,不会释放对象锁;
其它线程可以使用interrupt方法打断正在睡眠的线程,这时sleep方法会抛出InterruptedException,并且会清除中断标志;
睡眠结束后的线程未必会立刻得到执行;
sleep当传入参数为0时,和yield相同。

  1. public class SyncWaitTest {
  2. private Object lock = new Object();
  3. public void test() {
  4. log.debug(Thread.currentThread().getName()+" start");
  5. synchronized (lock){
  6. log.debug(Thread.currentThread().getName()+" execute");
  7. try {
  8. //Thread.sleep(5000); //不释放锁
  9. lock.wait(5000); //释放锁
  10. } catch (InterruptedException e) {
  11. e.printStackTrace();
  12. }
  13. log.debug(Thread.currentThread().getName()+" end");
  14. }
  15. }
  16. public static void main(String[] args) {
  17. SyncWaitTest test = new SyncWaitTest();
  18. for(int i=0;i<2;i++){ //开启2个线程
  19. new Thread(new Runnable() {
  20. @Override
  21. public void run() {
  22. test.test(); //同时执行test方法
  23. }
  24. },"thread"+i).start();
  25. }
  26. }
  27. }

yield

yield会释放CPU资源,让当前线程从Running进入Runnable状态,让优先级更高(至少是相同)的线程获得执行机会,不会释放对象锁;
假设当前进程只有main线程,当调用yield之后,main线程会继续运行,因为没有比它优先级更高的线程;
具体的实现依赖于操作系统的任务调度器。

join

等待调用join方法的线程结束之后,程序再继续执行,一般用于等待异步线程执行完结果之后才能继续运行的场景。
Object方法:

wait

调用该方法的线程进入WAITING状态,只有等待另外线程的通知或被中断才会返回。需要注意,调用wait()方法后,会释放对象的锁。

wait(long)

超时等待一段时间,这里的参数时间是毫秒,也就是等待长达n毫秒,如果没有通知就超时返回。

wait(long,int)

对于超时时间更细粒度的控制,可以达到纳秒。

notify

通知一个在对象上等待的线程,使其从wait方法返回,而返回的前提是该线程获取到了对象的锁,没有获得到锁的线程重新进入WAITING状态。

notifyAll

通知所有等待在该对象上的线程。

如何正确优雅的停止线程?

stop(废弃)

stop()方法已经被jdk废弃,原因就是stop()方法太过于暴力,强行把执行到一半的线程终止。stop会释放对象锁,可能会造成数据不一致。

中断机制

Java没有提供一种安全、直接的方法来停止某个线程,而是提供了中断机制。中断机制是一种协作机制,也就是说通过中断并不能直接终止另一个线程,而需要被中断的线程自己处理。被中断的线程拥有完全的自主权,它既可以选择立即停止,也可以选择一段时间后停止,也可以选择压根不停止。
interrupt():将线程的中断标志位设置为true,不会停止线程;
isInterrupted(): 判断当前线程的中断标志位是否为true,不会清除中断标志位 ;
Thread.interrupted():判断当前线程的中断标志位是否为true,并清除中断标志位,重置为fasle。

  1. public class ThreadInterruptTest {
  2. static int i = 0;
  3. public static void main(String[] args) {
  4. System.out.println("begin");
  5. Thread t1 = new Thread(new Runnable() {
  6. @Override
  7. public void run() {
  8. while (true) {
  9. i++;
  10. System.out.println(i);
  11. //Thread.interrupted() 清除中断标志位
  12. //Thread.currentThread().isInterrupted() 不会清除中断标志位
  13. if (Thread.currentThread().isInterrupted() ) {
  14. System.out.println("=========");
  15. }
  16. if(i==10){
  17. break;
  18. }
  19. }
  20. }
  21. });
  22. t1.start();
  23. //不会停止线程t1,只会设置一个中断标志位flag=true
  24. t1.interrupt();
  25. }
  26. }

利用中断机制优雅的停止线程

  1. public class StopThread implements Runnable {
  2. @Override
  3. public void run() {
  4. int count = 0;
  5. while (!Thread.currentThread().isInterrupted() && count < 1000) {
  6. System.out.println("count = " + count++);
  7. try {
  8. //处于休眠中(sleep,wait)的线程被中断,线程是可以感受到中断信号的,并且会抛出
  9. //一个InterruptedException异常,同时清除中断信号,将中断标记位设置成false
  10. Thread.sleep(1);
  11. } catch (InterruptedException e) { //sleep会抛出中断异常
  12. e.printStackTrace();
  13. //重新设置线程中断状态为true
  14. Thread.currentThread().interrupt();
  15. }
  16. }
  17. System.out.println("线程停止: stop thread");
  18. }
  19. public static void main(String[] args) throws InterruptedException {
  20. Thread thread = new Thread(new StopThread());
  21. thread.start();
  22. Thread.sleep(5);
  23. thread.interrupt();
  24. }
  25. }

注:使用中断机制时一定要注意是否存在中断标志位被清除的情况
sleep可以被中断 抛出中断异常:sleep interrupted,清除中断标志位
wait可以被中断 抛出中断异常:InterruptedException,清除中断标志位
业务场景:
1.一个线程处理一些业务,当满足一定的条件让它停止,就可以用中断机制。
2.如果在while中使用中断标志位,线程中使用了sleep,需要在sleep的catch方法中重新设置中断标志位 Thread.currentThread().interrupt()。
3.AQS中会有很多中断处理

Java线程间通信

Volatile

volatile有两大特性,一是可见性,二是有序性(禁止指令重排序),其中可见性就是可以让线程之间进行通信。

  1. public class VolatileDemo {
  2. private static volatile boolean flag = true;
  3. public static void main(String[] args) {
  4. new Thread(new Runnable() {
  5. @Override
  6. public void run() {
  7. while (true){
  8. if (flag){
  9. System.out.println("trun on");
  10. flag = false;
  11. }
  12. }
  13. }
  14. }).start();
  15. new Thread(new Runnable() {
  16. @Override
  17. public void run() {
  18. while (true){
  19. if (!flag){
  20. System.out.println("trun off");
  21. flag = true;
  22. }
  23. }
  24. }
  25. }).start();
  26. }
  27. }

等待唤醒(等待通知)机制

等待唤醒机制可以基于wait和notify方法来实现,在一个线程内调用该线程锁对象的wait方法,线程将进入等待队列进行等待直到被唤醒。

  1. public class WaitDemo {
  2. private static Object lock = new Object();
  3. private static boolean flag = true;
  4. public static void main(String[] args) {
  5. new Thread(new Runnable() {
  6. @Override
  7. public void run() {
  8. synchronized (lock){
  9. while (flag){
  10. try {
  11. System.out.println("wait start .......");
  12. //等待
  13. lock.wait();
  14. } catch (InterruptedException e) {
  15. e.printStackTrace();
  16. }
  17. }
  18. System.out.println("wait end ....... ");
  19. }
  20. }
  21. }).start();
  22. new Thread(new Runnable() {
  23. @Override
  24. public void run() {
  25. if (flag){
  26. synchronized (lock){
  27. if (flag){
  28. try {
  29. Thread.sleep(2000);
  30. } catch (InterruptedException e) {
  31. e.printStackTrace();
  32. }
  33. //通知
  34. lock.notifyAll();
  35. System.out.println("notify .......");
  36. flag = false;
  37. }
  38. }
  39. }
  40. }
  41. }).start();
  42. }
  43. }

LockSupport

LockSupport是JDK中用来实现线程阻塞和唤醒的工具,线程调用park则等待“许可”,调用unpark则为指定线程提供“许可”。使用它可以在任何场合使线程阻塞,可以指定任何线程进行唤醒,并且不用担心阻塞和唤醒操作的顺序,但要注意连续多次唤醒的效果和一次唤醒是一样的。

public class LockSupportTest {

    public static void main(String[] args) {
        Thread parkThread = new Thread(new ParkThread());
        parkThread.start();
        System.out.println("唤醒parkThread");
        //为指定线程parkThread提供“许可”
        LockSupport.unpark(parkThread);
    }

    static class ParkThread implements Runnable{
        @Override
        public void run() {
            System.out.println("ParkThread开始执行");
            // 等待“许可”
            LockSupport.park();
            System.out.println("ParkThread执行完成");
        }
    }
}

管道输入输出流

管道输入/输出流和普通的文件输入/输出流或者网络输入/输出流不同之处在于,它主要用于线程之间的数据传输,而传输的媒介为内存。管道输入/输出流主要包括了如下4种具体实现: PipedOutputStream、PipedInputStream、PipedReader和PipedWriter,前两种面向字节, 而后两种面向字符。

public class PipedTest {
    public static void main(String[] args) throws Exception {
        PipedWriter out = new PipedWriter();
        PipedReader in = new PipedReader();
        // 将输出流和输入流进行连接,否则在使用时会抛出IOException
        out.connect(in);

        Thread printThread = new Thread(new Print(in), "PrintThread");
        printThread.start();

        int receive = 0;
        try {
            while ((receive = System.in.read()) != -1) {
                out.write(receive);
            }
        } finally {
            out.close();
        }
    }

    static class Print implements Runnable {
        private PipedReader in;

        public Print(PipedReader in) {
            this.in = in;
        }

        @Override
        public void run() {
            int receive = 0;
            try {
                while ((receive = in.read()) != -1) {
                    System.out.print((char) receive);
                }
            } catch (IOException ex) {
            }
        }
    }
}

Thread.join

join可以理解成是线程合并,当在一个线程调用另一个线程的join方法时,当前线程阻塞等待被调用join方法的线程执行完毕才能继续执行,所以join的好处能够保证线程的执行顺序,但是如果调用线程的join方法其实已经失去了并行的意义,虽然存在多个线程,但是本质上还是串行的,最后join的实现其实是基于等待通知机制的。

问题一:CAS涉及到用户模式到内核模式的切换吗?

没有,切换内核态是调用了ring0提供的接口,cas只是调用了cpu的指令

问题二:为什么说创建Java线程的方式本质上只有一种?Java线程和go语言的协程有什么区别?

本质上Java中实现线程只有一种方式,都是通过new Thread()创建线程,调用Thread#start启动线程最终都会调用Thread#run方法。
Java线程属于内核级线程,线程的创建、撤消、切换都由内核实现。协程,是一种基于线程之上,但又比线程更加轻量级的存在,协程不是被操作系统内核所管理,而完全是由程序所控制(也就是在用户态执行),具有对内核来说不可见的特性。

问题三:Java线程执行为什么不能直接调用run()方法,而要调用start()方法?

参考线程的创建流程
new一个Thread,线程进入了新建状态。调用start()方法,会启动一个线程并使线程进入了就绪状态,当分配到时间片后就可以开始运行了。start()会执行线程的相应准备工作,然后自动执行run()方法的内容,这是真正的多线程工作。而直接执行run()方法,会把run方法当成一个main线程下的普通方法去执行,并不会在某个线程中执行它,所以这并不是多线程工作。