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

发布时间:2023-05-05 09:28:51

 

1、前言

一段时间前,一个朋友告诉我,你能写一些关于JUC的教程文章吗。最初,JUC也在我的专栏计划中,但没有时间轮到他了,所以既然有这样的机会,那就提前计划JUC。所以今天我将专注于初步了解什么是JUC,以及一些与JUC相关的基本知识。

关于JUC,建议与Java合作 API学习(本文使用JAVA8)。

2、JUC是什么?

JUC(java.util .concurrent),JDK内置用于处理并发(concurrent)的工具包。自JDK1.5以来,包中增加了许多常用于并发编程的工具和接口,包括线程池、原子、锁、并发容器等。这些工具类和接口可以简化多线程编程的复杂性,提高程序的并发性和可靠性。

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

它包含了一些我们常见的工具,比如

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

以后会一一提到这些。

3、并行和并发

正如我们前面提到的,JUC是一个处理并发编程问题的工具包。那么什么是并发呢?与并发相比,人们经常听到更多的并发,那么并发和并发有什么区别呢?

我们必须邀请我们的金牌教师C老师(ChatGPT)给大家讲一下:

【JUC基础】01. 初步认识JUC_开发语言_02

简单总结一下:

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

举个简单的例子:

假设你需要做午餐,你可以同时准备食物、蔬菜、汤和其他成分,然后交替烹饪和加工,这是并发的。

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

4、进程和线程
  • 进程(process),指操作系统中正在运行的程序的例子,它有自己独立的空间和资源,包括内存、文件、网络等。一个过程可以由一个或多个线程组成。如果您打开计算机的任务管理器,您可以看到每个正在运行的详细列表。

【JUC基础】01. 初步认识JUC_多线程_03

  • 线程(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.");    }}
5.2、Runnnablele实现
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.");    }}
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) “创造成功”;            }}
5.4、小结

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

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

run()用于在本线程中执行只是一种普通的方法,可以多次重复调用。如果Run()被调用到主线程中,它将失去并发的意义。

6、和Runnable

从以上代码可以看出,要实现多线程编程。有以下步骤:

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

既然Runnnable或Callable已经能够创建一个子线程,为什么需要neww? Thread,调用它的start()怎么样?

从查看Thread的源码可以看出,Thread本身实际上是对Runnable的扩展,而Thread则扩展了start()等一系列线程操作方法,stop(),yeild()...

【JUC基础】01. 初步认识JUC_开发语言_05

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

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

官方注释还明确表示,Runnable应由任何类别实现,旨在为希望在活动中执行代码的对象提供公共协议。在大多数情况下,run()方法应由子类重写。

因此,Thread只是Runnable的实现,扩展了一系列方法操作线程的方法。我的理解是Runnable的存在,为了更方便地提供子类对线程操作的扩展。这种扩展对于面向对象编程是非常必要的。网上很多人说“Runnable更容易实现多线程之间的资源共享,而Thread不能”。这句话有不同的看法。Runnable接口的存在可以让你自由定义许多可重复使用的线程实现类别,这符合面向对象的想法。

7、Runnable和Callable

这个问题几乎是JUC面试中必不可少的问题。既然Runnable可以实现子线程的操作,也符合面向对象的思想,为什么还需要Calable?new Thread构造函数还不支持一个Callable的引入,那么Callable有什么意义呢?

答案是:存在是合理的。

先来看看Callable源码:

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

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

  1. Runnable返回值为void,callable返回值为泛型。
  2. Runnable的默认内置方法是Run,Callable的默认方法是call。
  3. Runnable默认无异常抛掷,Callable有异常抛掷。

事实证明,不仅源码这么说,官方文件也这么说:

【JUC基础】01. 初步认识JUC_多线程_08

当我们需要执行一个线程的状态,或者定制线程的异常,或者获得多线程的反馈结果时。我们需要使用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";    }}

返回结果:

【JUC基础】01. 初步认识JUC_多线程_09

8、线程状态

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

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

状态转换关系如下图所示:

【JUC基础】01. 初步认识JUC_开发语言_10

9、总结

并发编程自多处理器问世以来,一直是提高系统响应速率和吞吐率的最佳途径。然而,编程的复杂性也相应提高。与单线程相比,多线程更加未知。一旦出现并发问题,有时没有特定的场景是无法复制的。因此,为了从容应对多线程带来的一系列未知问题,我们需要巩固多线程的基础。这是JUC基础学习的第一篇文章,介绍一些常见的多线程知识,为以后的学习铺平道路。一天进步一点。

上一篇 Flux 和Mono之间的区别
下一篇 Junit4&Junit5对比

文章素材均来源于网络,如有侵权,请联系管理员删除。

标签: Java教程Java基础Java编程技巧面试题Java面试题