前段时间,有朋友跟我说,能否写一些关于JUC的教程文章。本来呢,JUC也有在我的专栏计划之内,只是一直都还没空轮到他,那么既然有这样的一个契机,那就把JUC计划提前吧。那么今天就重点来初步认识一下什么是JUC,以及一些基本的JUC相关基础知识。
关于JUC,建议配合Java API来学习(本文使用JAVA8)。Java API下载直达链接:https://download.csdn.net/download/p793049488/87743633
JUC(java.util .concurrent),是JDK内置的一个用来处理并发(concurrent)的工具包。从JDK1.5开始就已经出现,该包中增加了很多使用在并发编程中常用的工具类和接口,包括线程池、原子类、锁、并发容器等。这些工具类和接口能够简化多线程编程的复杂度,提高程序的并发性能和可靠性。
其中包含了一些我们常见的工具类,如
这些后面都会一一说到。
而JUC主要包含了三个模块:
前面我们提到JUC是一个用来处理并发编程问题的工具包。那么什么是并发?相对于并发,很多时候人们更多听到的应该是并行,那么并行和并发有什么区别?
这里不得不请出我们的金牌教师C老师(ChatGPT)来给大家讲述一下:
简单总结一下就是:
举个简单的例子:
假设你需要做一份午餐,你可以同时准备饭、菜、汤等多个食材,然后交替进行烹饪和加工,这就是并发。
如果你有一个烤箱和一个煤气灶,你可以同时在烤箱里烤面包,同时在煤气灶上煮汤,这就是并行。
以下是线程和进程的主要区别:
总的来说,
进程是程序资源调度的基本单位。
线程是CPU执行的基本单位。
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.");
}
}
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.");
}
}
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) "创建成功";
}
}
Thread中有两个方法,start()和run()。我们使用多线程并发时,应该使用start()方法,而不是run()方法。
start()用于启动一个线程,并在线程中执行run方法。一个线程只能start一次。
run()用于在本线程内执行,只是普通类的一个方法,可以被重复调用多次。如果在主线程中调用run(),那么就失去了并发的意义。
从上面的代码中我们可以看出,要实现一个多线程编程。有以下几个步骤:
那么既然Runnable或Callable已经能够创建出一个子线程,那么为什么还需要new Thread,调用它的start()呢?
通过查看Thread的源码可知,Thread本身其实是对Runnable的扩展:
而Thread扩展了一系列线程的操作方法,如start(),stop(),yeild()......
而Runnable只是一个函数式接口而已,注意他只是个接口,而且他只有一个方法:run()。
而官方注释也明确告诉大家,Runnable应该由任何类来实现,该接口旨在为希望在活动状态下执行代码的对象提供公共协议。而大多数情况下,run()方法应该交由子类进行重写。
所以,Thread只是Runnable的一个实现,扩展了一系列方法操作线程的方法。我的理解是Runnable的存在,是为了更方便提供子类对线程操作的扩展。对于面向对象编程来说,这一类的扩展是很有必要的。网络上很多说“Runnable更容易可以实现多个线程间的资源共享,而Thread不可以”,这句话见仁见智,Runnable接口的存在,可以让你自由的定义很多可被重复使用的线程实现类,符合面向对象的思想。
这个问题几乎是面试JUC基础中必问的一个题目。既然Runnable能够实现子线程的操作,也符合面向对象思想,那么为什么还需要Callable。而new Thread构造函数还不支持传入一个Callable,那Callable的意义在哪里呢?
答案就是:存在即合理。
先来看下Callable源码:
从源码可以看出Callable和Runnable的区别是:
而事实证明确实如此,不仅源码这么说,官方文档也这么说:
当我们有需要某线程的执行状态,或需要对该线程的异常进行自定义处理,或需要获取多线程的反馈结果的时候。我们就需要用到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";
}
}
返回结果:
Java语言定义了6中线程状态。任意一个时间点,一个线程有且只有一种状态,并且可以通过特定方法切换不同状态。
状态的转换关系如下图:
自从多处理器问世以来,并发编程一直都是提高系统响应速率和吞吐率的最佳方式。但是相应也提高了编程的复杂度,相比单线程而言,多线程更加充满了未知性。一旦并发问题出现后,有时候没有特定的场景是根本无法复现的。因此我们更加需要巩固多线程的基础,才能从容应对多线程带来的一系列未知性问题。JUC基础学习第一篇就到这里吧,介绍一些常见的多线程知识为后面的学习铺垫。一天进步一点点。