1、前言
一段时间前,一个朋友告诉我,你能写一些关于JUC的教程文章吗。最初,JUC也在我的专栏计划中,但没有时间轮到他了,所以既然有这样的机会,那就提前计划JUC。所以今天我将专注于初步了解什么是JUC,以及一些与JUC相关的基本知识。
关于JUC,建议与Java合作 API学习(本文使用JAVA8)。
2、JUC是什么?JUC(java.util .concurrent),JDK内置用于处理并发(concurrent)的工具包。自JDK1.5以来,包中增加了许多常用于并发编程的工具和接口,包括线程池、原子、锁、并发容器等。这些工具类和接口可以简化多线程编程的复杂性,提高程序的并发性和可靠性。
它包含了一些我们常见的工具,比如
- Callable
- ExecutorService
- ThreadFactory
- ConcurrentHashMap
- ...
以后会一一提到这些。
3、并行和并发正如我们前面提到的,JUC是一个处理并发编程问题的工具包。那么什么是并发呢?与并发相比,人们经常听到更多的并发,那么并发和并发有什么区别呢?
我们必须邀请我们的金牌教师C老师(ChatGPT)给大家讲一下:
简单总结一下:
- 并行:同时完成任务。强调“同时”。
- 并发:利用等待某些事情完成的时间,交替完成其他事情。不一定是同时的。更强调“交替”。
举个简单的例子:
假设你需要做午餐,你可以同时准备食物、蔬菜、汤和其他成分,然后交替烹饪和加工,这是并发的。
如果你有一个烤箱和一个煤气炉,你可以同时在烤箱里烤面包,在煤气炉上煮汤,这是平行的。
4、进程和线程- 进程(process),指操作系统中正在运行的程序的例子,它有自己独立的空间和资源,包括内存、文件、网络等。一个过程可以由一个或多个线程组成。如果您打开计算机的任务管理器,您可以看到每个正在运行的详细列表。
- 线程(thread),是指操作系统中调度执行的最小单位,是过程中的执行单位。一个过程中的多个线程可以共享内存、文件等过程资源。
以下是线程与过程的主要区别:
- 资源占用:一个过程占用独立的系统资源,包括内存、文件、网络等,线程在过程中运行,多个线程可以共享过程资源,减少资源占用。
- 切换费用:线程切换费用小于过程,因为线程是在过程内部调度的,而过程切换需要保存和恢复过程状态,比线程切换费用大。
- 通信方式:同一过程中的线程可以通过共享内存进行通信,而不同过程之间的通信需要使用过程间通信机制,如管道、消息队列等。
- 执行独立性:过程是独立的,一个过程的崩溃不会影响其他过程的执行,而线程之间的资源共享可能会导致整个过程的崩溃。
- 系统费用:由于流程有自己独立的资源,流程间切换需要更多的系统费用,而线程共享流程的资源,切换费用较小。
总的来说,
流程是程序资源调度的基本单位。
线程是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从以上代码可以看出,要实现多线程编程。有以下步骤:
- 创建子线程,选择5.1-5.3三种创建方式之一。
- new Thread()将执行线程传输到Thread构造函数中。
- 调用start()方法。
既然Runnnable或Callable已经能够创建一个子线程,为什么需要neww? Thread,调用它的start()怎么样?
从查看Thread的源码可以看出,Thread本身实际上是对Runnable的扩展,而Thread则扩展了start()等一系列线程操作方法,stop(),yeild()...
Runnable只是一个函数接口,注意他只是一个接口,他只有一种方法:run()。
官方注释还明确表示,Runnable应由任何类别实现,旨在为希望在活动中执行代码的对象提供公共协议。在大多数情况下,run()方法应由子类重写。
因此,Thread只是Runnable的实现,扩展了一系列方法操作线程的方法。我的理解是Runnable的存在,为了更方便地提供子类对线程操作的扩展。这种扩展对于面向对象编程是非常必要的。网上很多人说“Runnable更容易实现多线程之间的资源共享,而Thread不能”。这句话有不同的看法。Runnable接口的存在可以让你自由定义许多可重复使用的线程实现类别,这符合面向对象的想法。
7、Runnable和Callable这个问题几乎是JUC面试中必不可少的问题。既然Runnable可以实现子线程的操作,也符合面向对象的思想,为什么还需要Calable?new Thread构造函数还不支持一个Callable的引入,那么Callable有什么意义呢?
答案是:存在是合理的。
先来看看Callable源码:
Callable和Runnable的区别从源码上可以看出:
- Runnable返回值为void,callable返回值为泛型。
- Runnable的默认内置方法是Run,Callable的默认方法是call。
- Runnable默认无异常抛掷,Callable有异常抛掷。
事实证明,不仅源码这么说,官方文件也这么说:
当我们需要执行一个线程的状态,或者定制线程的异常,或者获得多线程的反馈结果时。我们需要使用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"; }}
返回结果:
8、线程状态Java语言定义了6中线程状态。在任何时间点,一个线程只有一个状态,并且可以通过特定的方法切换到不同的状态。
- 新建(New):创建后尚未启用
- 运行(Runnable):包括Running和Ready,这个状态的线程可能正在执行,或者等待操作系统分配执行时间
- 无限期等待(Waiting):执行时间不会分配到这个状态线程,需要等待被显式唤醒。在以下情况下,线程将处于这种状态:
- Object没有设置timeout参数::wait()方法;
- Object没有设置timeout参数::join()方法;
- LockSupport::park()方法
- 限期等待(Timed Waiting):该状态线程不会分配执行时间,但不需要等待被其他线程显式唤醒,系统会在一定时间后自动唤醒。在以下情况下,线程将处于此状态:
- Thread::sleep()方法。
- Object设置Timeout参数::wait()方法。
- Thread设置Timeout参数::join()方法。
- LockSupport::parkNanos()方法。
- LockSupport::parkUntil()方法。
- 阻塞(Blocked):线程被堵塞。这是阻塞状态和等待状态。
- 堵塞状态:等待获得排他锁,此时将发生在另一线程放弃锁时;
- 等待状态:等待一段时间,或唤醒动作的发生。当程序等待进入同步区域时,线程将进入此状态。
- 结束(Terminated):种植线程状态,线程结束运行。
状态转换关系如下图所示:
9、总结并发编程自多处理器问世以来,一直是提高系统响应速率和吞吐率的最佳途径。然而,编程的复杂性也相应提高。与单线程相比,多线程更加未知。一旦出现并发问题,有时没有特定的场景是无法复制的。因此,为了从容应对多线程带来的一系列未知问题,我们需要巩固多线程的基础。这是JUC基础学习的第一篇文章,介绍一些常见的多线程知识,为以后的学习铺平道路。一天进步一点。