进程和线程

进程和线程是并发编程的两个基本的执行单元。在 Java 中,并发编程主要涉及线程。
一个计算机系统通常有许多活动的进程和线程。在给定的时间内,每个处理器只能有一个线程得到真正的运行。对于单核处理器来说,处理时间是通过时间切片来在进程和线程之间进行共享的。

1.进程

进程是资源分配的基本单位

进程的特征

  1. 一个进程就是一个执行中的程序,而每一个进程都有自己独立的一块内存空间,一组系统资源。在进程的概念中,每一个进程的内部数据和状态都是完全独立的。
  2. 创建并执行一个进程的系统开销是比较大的。
  3. 进程是程序的一次执行过程,是系统运行程序的基本单位。

2.线程

  • 线程是 CPU 调度的基本单位
  • 一个进程中可以有多个线程,它们共享进程资源。
  • 线程有时被称为轻量级进程。进程和线程都提供一个执行环境,但创建一个新的线程比创建一个新的进程需要更少的资源。

线程的特征

  1. 在Java中,程序通过流控制来执行程序流。程序中单个顺序的流控制称为线程。
  2. 多线程指的是在单个进程中可以同时运行多个不同的线程,执行不同的任务。多线程意味着一个程序的多行语句可以看上去几乎同时运行。

线程的状态

任何一个线程一般都具有5个状态,即创建、就绪、运行、阻塞、终止。
image.png

在给定时间点上,一个线程只能处于一种状态。
image.png

  1. New(新建):至今尚未启动的线程处于这种状态
  2. Runnable(可运行):正在Java虚拟机中执行的线程处于这种状态
  3. Blocked(阻塞):受阻塞并等待某个监视器锁的线程处于这种状态
  4. 等待状态(WAITING):当前线程需要等待其他线程做出一些的特定的动作(通知或中断)

    1. Waiting(无限期等待):无限期地等待另一个线程来执行某一个特定操作的线程处于这种状态 | 进入方法 | 退出方法 | | —- | —- | | 没有设置 Timeout 参数的 Object.wait() 方法 | Object.notify() / Object.notifyAll() | | 没有设置 Timeout 参数的 Thread.join() 方法 | 被调用的线程执行完毕 | | LockSupport.park() 方法 | LockSupport.unpark(Thread) |

    2. *Time_waiting(期限等待):等待另一个线程来执行取决于指定等待时间的操作的线程处于这种状态。

调用 Thread.sleep() 方法使线程进入限期等待状态时,常常用“使一个线程睡眠”进行描述。
调用 Object.wait() 方法使线程进入限期等待或者无限期等待时,常常用“挂起一个线程”进行描述。
睡眠和挂起是用来描述行为,而阻塞和等待用来描述状态

进入方法 退出方法
Thread.sleep() 方法 时间结束
设置了 Timeout 参数的 Object.wait() 方法 时间结束 / Object.notify() / Object.notifyAll()
设置了 Timeout 参数的 Thread.join() 方法 时间结束 / 被调用的线程执行完毕
LockSupport.parkNanos() 方法 LockSupport.unpark(Thread)
LockSupport.parkUntil() 方法 LockSupport.unpark(Thread)

阻塞和等待的区别在于:
阻塞是被动的,它是在等待获取一个排它锁。
等待是主动的,通过调用 Thread.sleep() 和 Object.wait() 等方法进入。

  1. Terminated(死亡):已退出的线程处于这种状态

3.进程与线程的区别

线程和进程最根本的区别在于:进程是资源分配的单位,线程是调度和执行的单位。

  • 资源
    进程是资源调度的基本单位。
    线程不占有任何资源。
  • 调度
    线程是独立调度的基本单位。
    同一进程中,线程的切换不会引起进程切换,但一个进程中线程切换到另一个进程中的线程时,就会引起进程切换了。
  • 系统开销
    进程的创建和撤销所付出的开销比线程的创建和撤销要大很多。
    类似的,进程切换的开销也比线程切换的开销要大很多,因为进程切换时,涉及到当前执行进程的 CPU 环境的保存新调度进程 CPU 环境的设置,而线程切换时只需要设置和保存少量的寄存器内容,开销很小。
  • 通信
    线程可以通过直接读写同一进程中的数据进行通信。
    进程之间的通信需要借助 IPC (Inter Process Communication)。

4.多进程与多线程

多进程的目的是提高 CPU 的使用率
多线程的目的是提高应用程序的使用率

多线程共享同一进程资源(堆内存和方法区),但是栈内存是独立的,JVM 中一个线程对应一个线程栈。多线程抢夺的是 CPU 资源,一个时间点上只有一个线程运行。

5.并发与并行

  • 并发:一个处理器同时处理多个任务,是逻辑上的同时发生
  • 并行:多个处理器或者多核处理器同时处理多个不同的任务,是物理上的同时发生

image.png

下图反映了一个包含 8 个操作的任务在一个有两核心的 CPU 中创建四个线程运行的情况。

假设每个核心有两个线程,那么每个CPU中两个线程会交替并发,两个CPU之间的操作会并行运算。

单就一个 CPU 而言两个线程可以解决线程阻塞造成的不流畅问题,其本身运行效率并没有提高, 多 CPU 的并行运算才真正解决了运行效率问题,这也正是并发和并行的区别。
image.png

线程对象

每个线程都与 Thread 类的一个实例相关联。有两种使用线程对象来创建并发应用程序的基本策略:

  • 为了直接控制线程的创建和管理,简单地初始化线程,应用程序每次需要启动一个异步任务。
  • 通过传递给应用程序任务给一个 executor,从而从应用程序的其他部分抽象出线程管理。

1.创建线程

  • 继承Thread类,重写run()方法

    1. public class MyThread extends Thread {
    2. private static MyThread myThread = new MyThread();
    3. private MyThread(){}
    4. public static MyThread getInstance(){
    5. return myThread;
    6. }
    7. @Override
    8. public void run() {
    9. System.out.println("MyThread run");
    10. }
    11. }
    12. public static void main(String[] args) {
    13. (MyThread.getInstance()).start();
    14. }
  • 实现 Runnable 接口,重写 run() 方法

    1. public class MyRunnable implements Runnable {
    2. private static class MyRunnableSingleton{
    3. private static final MyRunnable INSTANCE = new MyRunnable();
    4. }
    5. private MyRunnable(){}
    6. public static MyRunnable getInstance(){
    7. return MyRunnableSingleton.INSTANCE;
    8. }
    9. @Override
    10. public void run() {
    11. System.out.println("MyRunnable run");
    12. }
    13. }
    14. public static void main(String[] args) {
    15. (new Thread(MyRunnable.getInstance())).start();
    16. }
  • 实现 Callable 接口,重写 call() 方法

    1. class MyCallable implements Callable<String> {
    2. private static MyCallable myCallable;
    3. private MyCallable(){}
    4. public static synchronized MyCallable getInstance(){
    5. if (myCallable == null){
    6. myCallable = new MyCallable();
    7. }
    8. return myCallable;
    9. }
    10. @Override
    11. public String call() throws Exception {
    12. return "call";
    13. }
    14. }
    15. public static void main(String[] args) {
    16. MyCallable myCallable = MyCallable.getInstance();
    17. FutureTask futureTask = new FutureTask(myCallable);
    18. Thread thread = new Thread(futureTask);
    19. thread.start();
    20. }

启动线程
调用start() 方法

实现Runnable接口相对于继承Thread类来说,有几个显著优势

  • 避免单继承带来的局限
    Java 不支持多重继承,因此继承了 Thread 类就无法继承其它类,但是可以实现多个接口;
  • 代码能够被多个线程共享
  • 适合多个相同程序的线程处理同一资源的情况,把虚拟CPU(线程)同程序的代码、数据有效分离,较好地体现了面向对象的设计思想。
  • 使用接口实现的线程可以对线程进行复用(线程池中创建的线程就是实现 Runnable 接口的):
    Runnable 是轻量级的,重复 new 不会耗费太多资源。
    Thread 是重量级的,并且线程执行结束完后就销毁了,无法再次利用。

注意:一个线程只能 start 一次,若多次 start,会出现java.lang.IllegalException

2.实现处理线程的返回值

方式一:主线程等待法

如果未获取到值,则主线程等待,一直到获取值为止。

  1. public class CycleWait implements Runnable{
  2. private String value;
  3. @Override
  4. public void run() {
  5. try {
  6. Thread.currentThread().sleep(5000);
  7. } catch (InterruptedException e) {
  8. e.printStackTrace();
  9. }
  10. value = "we have data now";
  11. }
  12. public static void main(String[] args) throws InterruptedException {
  13. CycleWait cycleWait = new CycleWait();
  14. Thread t = new Thread(cycleWait);
  15. t.start();
  16. //TODO:如果未获取到值,则主线程等待,一直到获取值为止。
  17. while (cycleWait.value == null){ //主线程等待法
  18. Thread.sleep(1000);
  19. }
  20. System.out.println("value:"+cycleWait.value);
  21. //输出结果:
  22. //value:we have data now
  23. }
  24. }

方式二:join()

使用 Thread 的 join() 阻塞当前线程以等待子线程处理完毕

  1. public class CycleWait2 implements Runnable{
  2. private String value;
  3. @Override
  4. public void run() {
  5. try {
  6. Thread.currentThread().sleep(5000);
  7. } catch (InterruptedException e) {
  8. e.printStackTrace();
  9. }
  10. value = "we have data now";
  11. }
  12. public static void main(String[] args) throws InterruptedException {
  13. CycleWait2 cycleWait = new CycleWait2();
  14. Thread t = new Thread(cycleWait);
  15. t.start();
  16. //TODO:使用 Thread 的 join 方法阻塞当前线程,等待子线程执行完毕
  17. t.join(); //阻塞当前主线程,直到 t 线程执行完毕
  18. System.out.println("value:"+cycleWait.value);
  19. //输出结果:
  20. //value:we have data now
  21. }
  22. }

方式三:Callable + FutureTask

实现 Callablee 接口,使用 FutureTask 接收 call() 方法返回的数据。

  1. public class CycleWait3 implements Callable<String>{
  2. private String value;
  3. @Override
  4. public String call() throws Exception {
  5. try {
  6. Thread.currentThread().sleep(5000);
  7. } catch (InterruptedException e) {
  8. e.printStackTrace();
  9. }
  10. value = "we have data now";
  11. return value;
  12. }
  13. public static void main(String[] args) throws InterruptedException, ExecutionException {
  14. CycleWait3 cycleWait = new CycleWait3();
  15. FutureTask ft = new FutureTask(cycleWait);
  16. Thread t = new Thread(ft);
  17. t.start();
  18. while (!ft.isDone()){
  19. Thread.sleep(1000);
  20. }
  21. // FutureTask 的 get() 方法会一直阻塞,一直会等待任务执行完毕,才返回。
  22. System.out.println("value:"+ft.get());
  23. }

方式四:线程池 + Future

线程池执行任务,返回的结果使用 Future 接收。

  1. public class CycleWait4 implements Callable<String>{
  2. private String value;
  3. @Override
  4. public String call() throws Exception {
  5. try {
  6. Thread.currentThread().sleep(5000);
  7. } catch (InterruptedException e) {
  8. e.printStackTrace();
  9. }
  10. value = "we have data now";
  11. return value;
  12. }
  13. public static void main(String[] args) throws InterruptedException, ExecutionException {
  14. ExecutorService service = Executors.newCachedThreadPool();
  15. CycleWait4 cycleWait = new CycleWait4();
  16. //将任务提交给线程池处理
  17. Future<String> future = service.submit(cycleWait);
  18. while (!future.isDone()){
  19. Thread.sleep(1000);
  20. }
  21. System.out.println("value:"+future.get());
  22. //关闭线程池
  23. service.shutdown();
  24. }
  25. }