【JUC基础】01. 初步认识JUC

简介: 前段时间,有朋友跟我说,能否写一些关于JUC的教程文章。本来呢,JUC也有在我的专栏计划之内,只是一直都还没空轮到他,那么既然有这样的一个契机,那就把JUC计划提前吧。那么今天就重点来初步认识一下什么是JUC,以及一些基本的JUC相关基础知识。

 目录

1、前言

2、什么是JUC

3、并行和并发

4、进程和线程

5、如何创建子线程

5.1、继承Thread

5.4、小结

6、Thread和Runnable

7、Runnable和Callable

8、线程状态

9、总结


1、前言

前段时间,有朋友跟我说,能否写一些关于JUC的教程文章。本来呢,JUC也有在我的专栏计划之内,只是一直都还没空轮到他,那么既然有这样的一个契机,那就把JUC计划提前吧。那么今天就重点来初步认识一下什么是JUC,以及一些基本的JUC相关基础知识。

关于JUC,建议配合Java API来学习(本文使用JAVA8)。Java API下载直达链接:https://download.csdn.net/download/p793049488/87743633

2、什么是JUC

JUC(java.util .concurrent),是JDK内置的一个用来处理并发(concurrent)的工具包。从JDK1.5开始就已经出现,该包中增加了很多使用在并发编程中常用的工具类和接口,包括线程池、原子类、锁、并发容器等。这些工具类和接口能够简化多线程编程的复杂度,提高程序的并发性能和可靠性。

image.png

其中包含了一些我们常见的工具类,如

    • Callable
      • ExecutorService
        • ThreadFactory
          • ConcurrentHashMap
          • ......

          这些后面都会一一说到。

          3、并行和并发

          前面我们提到JUC是一个用来处理并发编程问题的工具包。那么什么是并发?相对于并发,很多时候人们更多听到的应该是并行,那么并行和并发有什么区别?

          这里不得不请出我们的金牌教师C老师(ChatGPT)来给大家讲述一下:

          image.png

          简单总结一下就是:

            • 并行:同一时刻,任务同时进行。强调“同时”。
            • 并发:利用等待某些事情完成的时间,交替完成其他的事情。不一定是同时。更强调“交替”。

            举个简单的例子:

            假设你需要做一份午餐,你可以同时准备饭、菜、汤等多个食材,然后交替进行烹饪和加工,这就是并发。

            如果你有一个烤箱和一个煤气灶,你可以同时在烤箱里烤面包,同时在煤气灶上煮汤,这就是并行。

            4、进程和线程

              • 进程(process),是指操作系统中一个正在运行的程序的实例,它有自己的独立空间和资源,包括内存、文件、网络等。一个进程可以由一个或多个线程组成。如打开电脑的任务管理器,就能够看到每个正在运行的详情列表。

              image.png

                • 线程(thread),是指操作系统中调度执行的最小单位,是进程中的一个执行单元。一个进程中的多个线程可以共享进程的资源,如内存、文件等。

                以下是线程和进程的主要区别:

                  1. 资源占用:一个进程占用独立的系统资源,包括内存、文件、网络等,而线程是在进程内运行的,多个线程可以共享进程的资源,减少资源占用。
                  2. 切换开销:线程切换的开销要比进程小,因为线程是在进程内部调度的,而进程切换则需要保存和恢复进程的状态,这个过程比线程切换开销更大。
                  3. 通信方式:在同一进程内的线程可以通过共享内存等方式进行通信,而不同进程之间的通信则需要使用进程间通信机制,如管道、消息队列等。
                  4. 执行独立性:进程之间是独立的,一个进程的崩溃不会影响其他进程的执行,而线程之间共享进程的资源,一个线程的崩溃可能会导致整个进程崩溃。
                  5. 系统开销:由于进程拥有自己独立的资源,进程间切换需要更多的系统开销,而线程共享进程的资源,切换开销更小。

                  总的来说,

                  进程是程序资源调度的基本单位。

                  线程是CPU执行的基本单位。

                  5、如何创建子线程

                  5.1、继承Thread

                  package com.github.fastdev;
                  public class Main {
                      public static void main(String[] args) {
                          new MyThread1("我是继承Thread的线程").start();
                      }
                  }
                  class MyThread1 extends Thread {
                      private String name;
                      public MyThread1(String name) {
                          this.name = name;
                      }
                      public void run() {
                          System.out.println("Thread-1 " + name + " is running.");
                      }
                  }

                  image.gif

                  5.2、实现Runnable

                  package com.github.fastdev;
                  public class Main {
                      public static void main(String[] args) {
                          new Thread(new MyThread2("我是实现Runnable的线程")).start();
                      }
                  }
                  class MyThread2 implements Runnable {
                      private String name;
                      public MyThread2(String name) {
                          this.name = name;
                      }
                      public void run() {
                          System.out.println("Thread-2 " + name + " is running.");
                      }
                  }

                  image.gif

                  5.3、实现Callable

                  package com.github.fastdev;
                  import java.util.concurrent.Callable;
                  import java.util.concurrent.ExecutorService;
                  import java.util.concurrent.Executors;
                  public class Main {
                      public static void main(String[] args) {
                          // 由于new Thread构造函数无法接收callable。这里使用线程池的方式调用
                          ExecutorService executor = Executors.newFixedThreadPool(1);
                          executor.submit(new MyThread3("我是实现Callable的线程"));
                      }
                  }
                  class MyThread3<String> implements Callable<String> {
                      private String name;
                      public MyThread3(String name) {
                          this.name = name;
                      }
                      @Override    
                      public String call() throws Exception {
                          System.out.println("Thread-3 " + name + " is running.");
                          return (String) "创建成功";
                      }
                  }

                  image.gif

                  5.4、小结

                  Thread中有两个方法,start()和run()。我们使用多线程并发时,应该使用start()方法,而不是run()方法。

                  start()用于启动一个线程,并在线程中执行run方法。一个线程只能start一次。

                  run()用于在本线程内执行,只是普通类的一个方法,可以被重复调用多次。如果在主线程中调用run(),那么就失去了并发的意义。

                  6、Thread和Runnable

                  从上面的代码中我们可以看出,要实现一个多线程编程。有以下几个步骤:

                    1. 创建子线程,选择5.1-5.3三种方式之一创建即可。
                    2. new Thread(),将执行线程传到Thread构造函数中。
                    3. 调用start()方法。

                    那么既然Runnable或Callable已经能够创建出一个子线程,那么为什么还需要new Thread,调用它的start()呢?

                    通过查看Thread的源码可知,Thread本身其实是对Runnable的扩展:

                    image.png

                    而Thread扩展了一系列线程的操作方法,如start(),stop(),yeild()......

                    image.png

                    而Runnable只是一个函数式接口而已,注意他只是个接口,而且他只有一个方法:run()。

                    image.png

                    而官方注释也明确告诉大家,Runnable应该由任何类来实现,该接口旨在为希望在活动状态下执行代码的对象提供公共协议。而大多数情况下,run()方法应该交由子类进行重写。

                    所以,Thread只是Runnable的一个实现,扩展了一系列方法操作线程的方法。我的理解是Runnable的存在,是为了更方便提供子类对线程操作的扩展。对于面向对象编程来说,这一类的扩展是很有必要的。网络上很多说“Runnable更容易可以实现多个线程间的资源共享,而Thread不可以”,这句话见仁见智,Runnable接口的存在,可以让你自由的定义很多可被重复使用的线程实现类,符合面向对象的思想。

                    7、Runnable和Callable

                    这个问题几乎是面试JUC基础中必问的一个题目。既然Runnable能够实现子线程的操作,也符合面向对象思想,那么为什么还需要Callable。而new Thread构造函数还不支持传入一个Callable,那Callable的意义在哪里呢?

                    答案就是:存在即合理。

                    先来看下Callable源码:

                    image.png

                    从源码可以看出Callable和Runnable的区别是:

                      1. Runnable返回值是void,Callable返回值是一个泛型。
                      2. Runnable的默认内置方法是run,Callable默认方法是call。
                      3. Runnable默认没有抛异常,Callable有抛异常。

                      而事实证明确实如此,不仅源码这么说,官方文档也这么说:

                      image.png

                      当我们有需要某线程的执行状态,或需要对该线程的异常进行自定义处理,或需要获取多线程的反馈结果的时候。我们就需要用到Callable。

                      代码示例:

                      package com.github.fastdev;
                      import java.util.concurrent.*;
                      import java.lang.String;
                      public class Main {
                          public static void main(String[] args) throws ExecutionException, InterruptedException {
                              Future<String> future = executor.submit(new MyThread3("我是实现Callable的线程"));
                              System.out.println("线程返回结果:" + future.get());
                          }
                      }
                      class MyThread3 implements Callable<java.lang.String> {
                          private String name;
                          public MyThread3(String name) {
                              this.name = name;
                          }
                          @Override    
                          public String call() throws Exception {
                              System.out.println("Thread-3 " + name + " is running.");
                              return "ok";
                          }
                      }

                      image.gif

                      返回结果:

                      image.png

                      8、线程状态

                      Java语言定义了6中线程状态。任意一个时间点,一个线程有且只有一种状态,并且可以通过特定方法切换不同状态。

                        1. 新建(New):创建后尚未启用
                        2. 运行(Runnable):包括了Running和Ready,此状态的线程由可能正在执行,也有可能正在等待操作系统分配执行时间
                        3. 无限期等待(Waiting):该状态线程不会被分配执行时间,要等待被显式唤醒。以下几种情况下线程会处于该状态:
                          1. 没设置Timeout参数的Object::wait()方法;
                          2. 没设置Timeout参数的Object::join()方法;
                          3. LockSupport::park()方法
                            1. 限期等待(Timed Waiting):该状态线程不会被分配执行时间,不过无需等待被其他线程显式唤醒,在一定时间后由系统自动唤醒。以下几种情况下线程会处于该状态:
                              1. Thread::sleep()方法。
                              2. 设置了Timeout参数的Object::wait()方法。
                              3. 设置了Timeout参数的Thread::join()方法。
                              4. LockSupport::parkNanos()方法。
                              5. LockSupport::parkUntil()方法。
                                1. 阻塞(Blocked):线程被阻塞了。其中由阻塞状态和等待状态。
                                  1. 阻塞状态:在等待获取到一个排他锁,这个时间将在另一个线程放弃这个锁的时候发生;
                                  2. 等待状态:等待一段时间,或者唤醒动作的发生。在程序等待进入同步区域的时候,线程将进入这种状态。
                                    1. 结束(Terminated):种植线程状态,线程结束运行。

                                    状态的转换关系如下图:

                                    image.png

                                    9、总结

                                    自从多处理器问世以来,并发编程一直都是提高系统响应速率和吞吐率的最佳方式。但是相应也提高了编程的复杂度,相比单线程而言,多线程更加充满了未知性。一旦并发问题出现后,有时候没有特定的场景是根本无法复现的。因此我们更加需要巩固多线程的基础,才能从容应对多线程带来的一系列未知性问题。JUC基础学习第一篇就到这里吧,介绍一些常见的多线程知识为后面的学习铺垫。一天进步一点点。

                                    相关文章
                                    |
                                    7月前
                                    |
                                    安全 Java 编译器
                                    高并发编程之什么是 JUC
                                    高并发编程之什么是 JUC
                                    63 1
                                    |
                                    4月前
                                    |
                                    安全 Java
                                    JUC(3)
                                    这篇文章讨论了Java集合类在高并发情况下的不安全性,并介绍了使用CopyOnWriteArrayList、Vector、ConcurrentHashMap等线程安全集合来解决这些问题的方法。
                                    JUC(3)
                                    |
                                    6月前
                                    |
                                    安全 算法 Java
                                    |
                                    7月前
                                    |
                                    安全 Java 程序员
                                    Java多线程基础-17:简单介绍一下JUC中的 ReentrantLock
                                    ReentrantLock是Java并发包中的可重入互斥锁,与`synchronized`类似但更灵活。
                                    62 0
                                    |
                                    安全 Java 调度
                                    JUC并发编程(上)
                                    JUC并发编程(上)
                                    75 0
                                    |
                                    并行计算 Java 应用服务中间件
                                    JUC并发编程超详细详解篇(一)
                                    JUC并发编程超详细详解篇
                                    1673 1
                                    JUC并发编程超详细详解篇(一)
                                    |
                                    存储 缓存 监控
                                    JUC并发编程(下)
                                    JUC并发编程(下)
                                    42 0
                                    |
                                    Java 编译器 调度
                                    JUC是什么?
                                    JUC是什么?
                                    |
                                    安全 Java 数据安全/隐私保护
                                    JUC基础(四)—— 并发集合
                                    JUC基础(四)—— 并发集合
                                    133 0
                                    |
                                    Java
                                    JUC
                                    JUC
                                    110 0