线程和进程的区别

认识多线程



一、认识线程

1. 线程是什么?

   一个线程就是一个“执行流”,每个线程之间都可以按照顺序执行自己的代码。多个线程之间 “同时”执行着多份代码。

  一个可执行的.exe文件运行起来就是一个进程。一个进程可以有一个线程,或者多个线程。每个线程都是一个独立的执行流。多个线程之间,是并发执行的。多个线程可能是在一个 CPU 核心上同时运行,也可能是在多个 CPU 核心上,通过快速调度的方式,并发执行。操作系统真正调度的是线程,而不是进程。
举个例子:

2. 为什么要有多线程?

1. 首先,“并发变成” 成为了 “刚需”。

  • 单核 CPU 的发展遇到了瓶颈。想要提高算力,就需要多核 CPU 。而并发编程能更充分利用多核 CPU 资源。
  • 有些任务场景需要 “等待IO”,为了让等待 IO 的时间能够去做一些其他的工作,也需要用到并发编程。

2. 虽然多进程也能实现并发编程,但是线程比进程更轻量。

  • 创建线程比创建进程更快
  • 销毁线程比销毁进程更快
  • 调度线程比调度进程更快

3.为了能够在多线程的基础之上,进一步提高开发的效率,又出现了“线程池(ThreadPool)” 和 “协程(Coroutine)”。

3.多线程具体是什么?

  一个可执行的 (.exe)文件运行起来就是一个进程。同一个进程,内部可以并发的完成多组任务。其中,每一个任务就是一个线程。

例如:
  微信 是一个程序,视频聊天时,同时使用网络传输、视频录制、声音录制等功能。

4.进程和线程的区别

  • 进程包含线程,每一个进程至少有一个线程,即主线程。

  • 进程是操作系统进行资源分配的最小单位,线程是操作系统调度执行的最小单位。

  • 进程之间具有独立性,一个进程挂了,不会影响到别的进程;同一个进程的多个线程之间,一个线程挂了,可能会把整个进程都带走,从而影响到其他的线程。

  • 进程有自己独立的内存空间和文件描述符表。进程和进程之间不共享内存空间;同一个进程的多个线程之间,共享一份地址空间和文件描述符表。

  • 共享地址空间是指,一个线定义的变量,在另一个线程中也可以使用。
  • 共享文件描述符表,一个线程打开的文件,在另一个线程中也可以继续使用。

  只有在进程启动,创建第一个线程的时候,需要花成本去申请系统资源。一旦进程(第一个线程)创建完毕,此后后续在创建线程的,就不需要再申请资源了,或者说申请的资源会少很多,这样创建/销毁的效率就会提高不少。也就是线程之间可以实现资源的复用。

二、线程的创建

1.自定义 MyThread 继承 Thread 创建线程

  • 自定义一个子类 MyThread ,子类继承 Java 标准库的 Thread 类,重写 run( )方法
  • 通过向上转型的方式,创建子类对象。
  • 启动线程
       //1.创建子类
    class MyThread extends Thread {
        //2.重写方法
        @Override
        public void run() {
            while(true){
                System.out.println("hello MyThread!");
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }
    }
    public class ThreadDemo {
        public static void main(String[] args) {
            //3.通过向上转型实例化子类对象

            Thread t = new MyThread();
            //4.启动线程
            t.start();
            while(true){
                System.out.println("hello main!");
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }


        }
    }

在这里插入图片描述

  • run(),表示创建的线程需要执行的任务。
  • start(),通过 start()启动线程,让创建好的线程执行指定的任务。

   注意,main()是一个线程,此时执行程序可以看见,频幕上出现了 "hello MyThread!“和"hello main!” 的交替打印,说明 t.start()启动了另外一个执行流,这个新的执行流(新的线程)执行输出 “hello MyThread!”,此时 idea是一个进程,运行的程序属于idea里的java进程,这两个线程是属于java进程的两个线程。并且这两个线程是同时进行的。

  为什么说这两个线程是同时进行的呢?仔细看可以发现,这时两个线程中,都分别存在一个死循环,如果这两个线程不是同时进行的话,那么在频幕上不可能出现两个语句交替打印,而是死循环打印其中的一个语句。

2.自定义 MyRnunable 实现 Runnable 接口

  • 自定义一个 MyRunnable 的子类,实现Runnable 接口,并重写Runnable的 run( )方法。
  • 创建 MyRunnable 的对象
  • 创建 Thread 的对象,并将 MyRunnable 的对象作为参数。
  • 启动线程

class MyRunnable implements Runnable {
    @Override
    public void run() {
        while (true) {
            System.out.println("MyRunnable!");
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }


    }
}

public class RunnableDemo {
    public static void main(String[] args) {
        MyRunnable myRunnable = new MyRunnable();
        Thread thread = new Thread(myRunnable);
        thread.start();

        while (true) {
            System.out.println("main!");
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }

    }
}

3.继承 Thread ,使用匿名内部类创建线程

public class ThreadDemo2 {
    public static void main(String[] args) {
        Thread thread = new Thread() {
            @Override
            public void run() {
                while (true) {
                    System.out.println("匿名内部类");
                    try {
                        Thread.sleep(1000);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
            }
        };

        thread.start();

        while (true) {
            System.out.println("main");
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
}

4.实现 Runnable 接口,使用匿名内部类创建线程

public class RunnableDemo2 {
    public static void main(String[] args) {
        Thread thread = new Thread(new Runnable() {
            @Override
            public void run() {
                while (true) {
                    System.out.println("MyRunnable!");
                    try {
                        Thread.sleep(1000);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
            }
        });

        thread.start();

        while (true) {
            System.out.println("main!");
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }

        }
    }
}

5.使用 Callable 接口创建线程

import java.util.concurrent.Callable;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.FutureTask;

public class CallableDemo {
    public static void main(String[] args) throws ExecutionException, InterruptedException {

        //创建任务
        Callable<Integer> callable = new Callable<Integer>() {
            @Override
            public Integer call() throws Exception {
                int sum = 0;
                for (int i = 1; i <= 1000; i++) {
                    sum += i;
                }
                return sum;
            }
        };

        //Thread 不能直接传 callable,需要再包装一层
        FutureTask<Integer> futureTask = new FutureTask<>(callable);

        //创建线程,让线程来完成这个任务
        Thread t = new Thread(futureTask);
        t.start();
        System.out.println(futureTask.get());
    }

}

6.使用 lambda 表达式创建线程

public class LambdaDemo {
    public static void main(String[] args) {
        Thread thread = new Thread(() -> {
            while (true) {
                System.out.println("MyRunnable!");
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        });

        thread.start();

        while (true) {
            System.out.println("main");
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
}

注意事项:
  lambda 表达式的使用,需要遵守变量捕获规则。捕获的变量必须是 final 修饰的或者“实际 final(变量没有使用 final 修饰,但是在代码执行的过程中,没有对变量就行修改)”

三、Thread 类及常见的方法

1.Thread 类的构造方法

方法说明
Thread()创建线程对象
Thread ( Runnable target )使用 Runnale 对象创建线程对象
Thread ( String name)创建线程对象,并命名
Thread(Runnable target, String name)使用 Runnable 对象创建线程对象,并命名
Thread(ThreadGroup group,Runnable target)线程可以被用来分组管理,分好的组即为线程组,这个目前我们了解即可

2.Thread 类的常见属性

属性获取方法
IDgetId()
名称getName()
状态getState()
优先级getPriority()
是否是后台线程isDaemon()
是否存活isAlive()
是否被中断isInterrupted()
  • ID 是线程的唯一标识,不同的线程不会重复
  • 名称是为了方便调试的时候观察线程用到的
  • 状态标识当前线程所处的一个情况
  • 优先级高的线程理论上来说更容易被调度到
  • 后台进程,JVM 会在一个进程的所有非后台进程结束后,才会结束运行。换言之,前台线程,就是指会影响 JVM 进程结束的线程,前台线程不结束,JVM 的进程就不会结束。
  • 是否存活,标识当前线程的 run() 是否运行结束

四、线程中断

  通过特定的方式,让一个正在执行的线程停止运行。可以通过设定结束标志位,判断是否需要中断线程。

   例如,当你在和你的朋友正在聊天的时候,正巧这个时候你的女朋友发来视频邀请,这个时候你不得不终止和你朋友的交流,然后接通你女朋友的视频通话。

1.自定义结束标志位

设定一个结束标志位 isQuit = false,使用 isQuit 作为条件循环的判定条件。

public class ThreadDemo {

    public static boolean isQuit = false;

    public static void main(String[] args) throws InterruptedException {
        Thread t = new Thread(() -> {
            while (!isQuit) {
                System.out.println("hello t");
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
            System.out.println("线程终止");
        });
        t.start();
        Thread.sleep(3000);
        isQuit = true;
    }
}

  上述代码的预期效果是这样的,我们设立了一个结束标志位 isQuit,在主线程 main 中创建一个线程 t 并通过start 启动线程,主线程 main 在创建线程之后睡眠 3 秒钟,在这 3 秒钟内,每隔 1 秒钟,t 线程在控制台上输出一句 “hello t”,3 秒钟后主线程 main 睡眠结束,通过修改 isQuit = true,t 线程再进行循环条件判定的时候为 false,此时不进入循环,线程 t 终止,并在控制台上输出 “线程终止”。

执行结果如下:
在这里插入图片描述

 如果将isQuit设置在 main() 方法内,会有结果会怎么样呢?

在这里插入图片描述
此时我们会发现,程序报错。为什么会出现这样的结果呢?

  • 在上面的 lambda 表达式我们说过,lambda 表达式对变量捕获的要求是,变量必须是 final 修饰的或者是“实际 final(没用 final 修饰,但是在变量的整个生命周期中,变量不能做任何的修改)”,因此在使用自定义的结束标志位的时候,标志位的声明一定得满足变量捕获的规则,因此我们通常是将结束标志位设置为成员变量。

2.Thread 提供的结束标志位, isInterrupted( )

通过调用 Thread 类提供的结束标志位,判定线程是否需要终止。


public class ThreadDemo {

    public static void main(String[] args) {

        Thread t = new Thread(()->{
           while(!Thread.currentThread().isInterrupted()){
               System.out.println("hello t");
               try {
                   Thread.sleep(1000);
               } catch (InterruptedException e) {
                   e.printStackTrace();
               }
           }
        });

        t.start();
        try {
            Thread.sleep(3000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        t.interrupt();

    }
}
  • Thread.currentThread()的作用,获取当前线程实例对象
  • isInterrupted( ),Thread对象自带的结束标志位, 有 true和 false 两种状态
  • interrupt( ),将线程 t 内部的结束标志位改成 true ,默认是 false

  上述代码的预期是通过主线程 main 创建一个新的线程 t,创建线程后主线程睡眠 3 秒,线程 t 不受影响继续,每隔 1 秒钟在 控制台输出一句 “ hello t!”,3 秒钟后主线程睡眠结束,将结束标志位修改为 true ,使线程 t 在就行循环判定的结果为 false,终止线程。

结果展示:
在这里插入图片描述

  执行代码后,我们发现执行结果和预期效果大相径庭,我们发现,经过 3 秒后,main 线程将结束标志位修改为 true 后,t 线程并没有结束,还是抛出异常后继续执行,为什么会出现这样的情况?

首先我们必须了解 sleep() 和 interrupt() 的工作原理:

interrupt()的作用:

  • 将结束标志位设置为 true
  • 如果当前线程正在阻塞中(比如正在执行 sleep),此时就会把阻塞状态唤醒,通过抛出异常的方式让 sleep 立即结束。

sleep的工作原理:

  • 当代码正常执行到 sleep 时,线程会进行睡眠,当 sleep 的过程中,如果结束标志位被修改为 true,sleep 会被通过抛出异常的方式强制唤醒,唤醒后,sleep 会把结束标志位 isInterrupted() 清空(true -> false),这就导致线程 t 在进行循环判定的结果仍为真 ,线程 t 会继续执行。

那么有没有解决的办法,通过修改上述的代码逻辑,达到预期的效果呢?

   显然强制将 sleep 唤醒后,会抛出异常,再进行循环条件的判定,我们只需要在抛出异常后,手动添加 break ,循环就可以结束,就可以实现线程 t 的终止了。

在这里插入图片描述
运行结果展示:

在这里插入图片描述

  • 如果主线程执行 interrupt() 后,标志位由 false 改成了 true ,此时线程 t 正好执行打印语句,那么 sleep 还会将 结束标志为 interrupted 改成 false 么?

  执行 interrupt() 后, 虽然此时线程 t 正在执行打印语句,但是当打印语句执行结束后,就会执行到 sleep ,线程 t 进入睡眠,但是因为结束标志位已经修改为 true,线程会立即将 sleep 唤醒,并抛出异常,然后将结束标志位 interrupted() 清空(true -> false),如果此时的代码在抛出异常语句后有 break 语句,那么线程 t 就会终止,如果没有,那么线程 t 继续执行。也就是说:

  • 当结束标志位 位 true 的时候,不论是 sleep 准备执行,或者是已经执行到了一半的情况,都会触发两件事,一是立即抛出异常;而是清空结束标志位(true -> false)。此时如果需要结束线程,需要程序员自己收到到 catch 中添加 break 语句结束线程。
  • 为什么 sleep 结束后要清空结束标志位?

目的就是为了让线程自身能够对于线程何时结束,有一个更明确的控制。

五、等待一个线程

  因为线程的执行是并发的,线程一旦启动之后,我们是不能确定哪个线程会先执行结束的。在多线程环境下实现业务逻辑,有时候我们需要等待一个线程完成它的工作后,才能进行自己的下一步工作。例如,张三看上了一双鞋子,但是,临近月底了,钱包空了,工资又没发。如果张三想要买这双鞋子,只能等到月底,确定老板李四发了工资之后,才能有足够的钱购买他喜欢的鞋子。
我们先看一下代码:

public class JoinDemo {
    public static void main(String[] args) {
        Thread t = new Thread(() -> {
            System.out.println("hello t!");
        });

        t.start();

        System.out.println("hello main!");
    }
}

结果展示:
在这里插入图片描述
  我们可以观察到,此时 main 线程先于 t 线程执行结束。如果我们需要让 t 线程先结束,main 线程再结束,此时我们就可以用 join( ),让main 线程等待线程 t 执行结束后再执行。

   虽然上述的代码的执行结果是先输出的 “hello main!”,再输出的 “hello t!”,但是实际上这两个代码谁先执行结束,程序员是无法确定的。大部分时候都是先输出 “hello main!” ,因为线程的创建是需要消耗资源的,但是不排除特定的情况下,主线程 “hello mian!” 没有立即执行到。作为开发人员,这样的不确定性,不是我们想要的结果,因此我们有时候需要明确规定线程的结束顺序,此时就可以使用线程等待来实现。

public class JoinDemo {
    public static void main(String[] args) throws InterruptedException {
        Thread t = new Thread(() -> {
            System.out.println("hello t!");
        });

        t.start();
        t.join();
        System.out.println("hello main!");
    }
}

结果展示:
在这里插入图片描述

join() 的使用方法:
   在需要等待的线程中调用 join(),join() 的调用对象是被等待的线程。例如:在上述的例子中,需要 main 线程等待 t 线程线程先结束,因此需要在 main 线程中调用 join() ,被等待的对象是 t 线程,因此join() 的调用对象就是 t ,因此只需要在 main 线程中,t.start() 的代码后面添加代码 t.join()。

上述的代码实现有两种情况:

  • 第一种情况,main 线程调用 t.join( ) 的时候,如果 t 线程还在运行,此时 main 线程阻塞,直到 t 线程执行结束(t 线程的 run() 方法执行结束),main 线程才从阻塞状态解除,继续往下执行。
  • 第二种情况,main 线程调用 t.join() 的时候,t 线程已经执行结束了,此时 join()方法就不会造成main 线程阻塞,main 线程会继续往下执行。

join( )有两个版本:

  • 无参的 join( )方法,效果是 “死等”
  • 有参的 join(超时时间),效果是:如果阻塞的时间超过了超时时间,线程会继续往下执行

六、线程的状态

状态含义
NEW系统中的线程还没有创建出来,只是有个 Thread 的对象
TERMINATED系统中的线程已经执行完了,但是Thread 的对象还在
RUNNABLE就绪状态,正在 CPU 上执行,或者准备好随时可以去 CPU 上执行
TIMED_WAITING指定时间等待,调用 sleep()
BLOCKED表示等待锁出现的状态
WAITING等待 wait() 出现的状态

进程状态的转换:
在这里插入图片描述

1.NEW 状态

public class JoinDemo {
    public static void main(String[] args) throws InterruptedException {
        Thread t = new Thread(() -> {
            System.out.println("hello t!");
        });

        System.out.println(t.getState());
        t.start();
        t.join();
        System.out.println("hello main!");
    }
}

结果展示:在这里插入图片描述

2.TERMINATED 状态

public class JoinDemo {
    public static void main(String[] args) throws InterruptedException {
        Thread t = new Thread(() -> {
            System.out.println("hello t!");
        });

        t.start();
        t.join();
        System.out.println(t.getState());
        System.out.println("hello main!");
    }
}

结果展示:在这里插入图片描述

  t 线程执行结束后,main 线程还没有结束,此时的 Thread 的对象还没有被回收,因此Thread 的对象 t 还在,但是 t 线程已经执行结束,因此此时的状态就是 TERMINATED。

3. RUNNABLE 状态

public class JoinDemo {
    public static void main(String[] args) throws InterruptedException {
        Thread t = new Thread(() -> {
            System.out.println("hello t!");
        });

        t.start();
        System.out.println(t.getState());
        t.join();
        System.out.println("hello main!");
    }
}

结果展示:
在这里插入图片描述

  此时的线程 t 刚启动,还处在运行的阶段,因此此时的状态为 RUNNABLE。

4. TIMED_WAITING 状态

package com.demo;
public class JoinDemo {
    public static void main(String[] args) throws InterruptedException {
        Thread t = new Thread(() -> {
            try {
                System.out.println("hello t!");
                Thread.sleep(2000);

            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        });
        t.start();
        Thread.sleep(1000);
        System.out.println(t.getState());
        t.join();
        System.out.println("hello main!");
    }
}

结果展示:
在这里插入图片描述

提示:这里对文章进行总结:

例如:以上就是今天要讲的内容,本文仅仅简单介绍了pandas的使用,而pandas提供了大量能使我们快速便捷地处理数据的函数和方法

  • 3
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 3
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值