Java学习篇(四)—— Java 多线程
如何创建一个线程?
Java创建线程有两种方法,这里对三种方法做一个梳理,方便理解。
实现Runnable接口和run()方法
Java的接口就是一种协议,约定了想要被统一管理的类要遵循的协议。在Java中,线程是由Thread
类创建和管理的,但线程需要执行具体的任务——也就是我们写的代码逻辑。而Runnable
就是一个“任务的标准形式”:
public class MyTask implements Runnable {
@Override
public void run() {
System.out.println("线程正在运行:" + Thread.currentThread().getName());
}
}
public class Main {
public static void main(String[] args) {
Runnable task = new MyTask(); // 创建任务
Thread thread = new Thread(task); // 把任务交给线程
thread.start(); // 启动线程(调用 run())
}
}
Runnable
的意义是:让任何类都可以被线程调用,Java线程调用的代码入口必须是run()
方法。
所以用这个方法创建的对象,都需要交给Thread
对象去管理,thread
则会调用run()
方法。
更常见的做法是使用内部匿名类:
Thread thread = new Thread(new Runnable() {
public void run() {
System.out.println("匿名线程运行中!");
}
});
thread.start();
继承Thread来创建线程
public class MyThread extends Thread {
public void run() {
System.out.println("我是继承 Thread 的线程");
}
}
另一个方法是继承Thread
类,重写了它的run()
方法。这种方法与实现Runnable
接口的区别就是:实现Runnable
接口是在实现一个任务,将任务交给线程管理者执行和控制,而继承Thread
方法则是直接定义了这个线程管理者执行的任务。由于Java是单继承的,所以继承的方法缺点也很明显,灵活性较低。
继承Thread类的方式,如果想要执行一个任务,必须新建一个线程,执行完成后还要销毁,开销非常大;而实现Runnable接口只需要新建任务,可以做到同一个线程执行多个任务,大大减小了线程创建、销毁的资源浪费。
Thread类详解
为什么创建线程可以有两种方式,原因在于Thread
本身就是实现了Runnable
接口的一个类,因此它也必须实现run()
方法,那么继承Thread
类的子类也可以重写run()
方法。
Java线程状态
Java中线程可以有如下6中状态:NEW 新创建,RUNNABLE 可运行,BLOCKED 阻塞,WAITING 等待,TIMED WAITING 计时等待,TERMINATED 终止。
在现代JVM中,Java的线程本质上就是操作系统线程(Native Thread),并且是一一对应的关系,操作系统的线程状态有五种,分别是:初始状态(NEW),可运行状态(READY),运行状态(RUNNING),等待状态(WAITING),终止状态 (TERMINATED)。
初始状态(NEW):这个状态其实是存疑的,操作系统程在调用
pthread_create()
后,已经在内核中被创建完成,拥有了自己的资源和调度实体,所以更类似于Java的new + start()
,Java的NEW状态是指,线程在JVM层已存在,但是操作系统线程尚未分配、未创建,因此也没有占用OS资源(比如线程栈空间、线程ID)。可运行状态(READY):线程资源已经创建,但是没有分配时间片,没有运行,对应Java的RUNNABLE可运行状态。
运行状态(RUNNING):分配时间片,正在运行,对应的也是Java的RUNNABLE可运行状态
等待状态(WAITING):对操作系统而言,等待有两个原因:被其它优先级更高的任务打断、没有获取资源。而Java则将等待分为了三种状态:BLOCKED 阻塞,WAITING 等待,TIMED WAITING 计时等待,当访问的资源被上锁时,进入阻塞状态;被I/O等中断时,进入等待状态;也可以计时等待,超时自动唤醒,进入阻塞状态或者可运行状态。
终止状态 (TERMINATED):进程运行结束/线程运行结束进入终止态,对应Java的终止态。
Thread类
源码详见参考资料一,源码中可以看到,当使用Runnable
实现类实例化Thread
对象时,会使this.target = traget
,在之后的run()
里调用target
的run()
方法。
public Thread() {
init(null, null, "Thread-" + nextThreadNum(), 0);
}
public Thread(Runnable target) {
init(null, target, "Thread-" + nextThreadNum(), 0);
}
@Override
public void run() {
if (target != null) {
target.run();
}
}
线程池
推荐使用Runnable
创建线程任务的关键原因是,线程资源很宝贵,为一个任务创建一个线程非常浪费线程资源,一个线程可以完成不只一个任务,所以为任务创建一个任务队列,再创建一个线程池,管理一组线程池来执行任务队列里的任务,能充分的利用线程资源,同时提高响应速度。
Excutor框架
Excutor接口
void execute(Runnable command);
Executor executor = new Executor() {
public void execute(Runnable command) {
new Thread(command).start(); // 每次都开新线程执行任务
}
};
Excutor
接口里只有一个核心方法,用来执行任务,它不在乎任务到底是怎么执行的,由实现这个接口的类自己定义,最简单的就是来一个任务申请一个线程,但是这个显然不合理,我们使用线程池就是为了管理线程。
ExecutorService接口
ExecutorService
接口继承了Executor
接口,但是Executor
只能执行任务,无法控制任务的结果、取消、超时,不具备线程池的管理能力。ExecutorService
在Executor
基础上新增了很多重要能力:
提交任务:
submit()
方法能提交Runnable
或Callable
。生命周期管理:
shutdown()
、isShutdown()
、awaitTermination()
等方法,关闭线程池。获取结果:
submit()
返回Future
,可获取返回值或取消任务。批量提交:
invokeAll()
、invokeAny()
,提交多个任务。
public interface ExecutorService extends Executor {
void shutdown(); // 平滑关闭线程池
List<Runnable> shutdownNow(); // 立即停止线程池
boolean isShutdown();
boolean isTerminated();
boolean awaitTermination(long timeout, TimeUnit unit);
<T> Future<T> submit(Callable<T> task); // 提交有返回值的任务
<T> Future<T> submit(Runnable task, T result); // 提交无返回值的任务
Future<?> submit(Runnable task); // 提交无返回值的任务
<T> List<Future<T>> invokeAll(Collection<? extends Callable<T>> tasks);
<T> T invokeAny(Collection<? extends Callable<T>> tasks);
}
ExecutorService
只是一个接口,规定了任务可以提交,可以关闭线程池,对于有返回值的任务可以获取返回值Future
,具体的实现需要类来定义。
ThreadPoolExecutor实现类
ThreadPoolExecutor
是Java并发编程中最核心的线程池实现类,实现了ExecutorService
接口,所有通过Executors
工具类创建的线程池底层其实都是它。主要负责:1. 管理线程的复用(减少线程创建/销毁开销)。2.控制并发线程的数量。3.管理任务队列。4.定义线程池饱和时的策略。
ExecutorService
接口只定义了线程的提交,提交的线程具体怎么管理和执行,是由ThreadPoolExcutor
定义的。
下面是ThreadPoolExecutor
的构造函数:
public ThreadPoolExecutor(
int corePoolSize, // 核心线程数,常驻线程数(长期保留,即使空闲)
int maximumPoolSize, // 最大线程数,任务太多时会临时扩充
long keepAliveTime, // 当线程数大于核心线程数时,多余的空闲线程存活的最长时间
TimeUnit unit, // 时间单位(秒、毫秒等)
BlockingQueue<Runnable> workQueue, // 等待执行的任务队列(如 LinkedBlockingQueue)
ThreadFactory threadFactory, // 生成线程的方式,默认即可
RejectedExecutionHandler handler // 拒绝策略
)
假设你提交一个任务:
executor.execute(() -> { /* 一段任务代码 */ });
它的流程如下:
当前线程数 < corePoolSize:立刻调用
addWorker()
创建一个新线程执行任务。线程数 >= corePoolSize 且队列没满:任务加入队列等待。
队列满了 且 当前线程数 < maximumPoolSize:创建新线程处理任务。
队列满了 且 当前线程数 >= maximumPoolSize:执行拒绝策略(默认抛异常)。
拒绝策略和任务队列详见参考资料四
线程池不会主动把任务交给空闲线程,而是让线程从队列中拉任务,任务必须先入队,空闲线程才会处理。
常见对比
Runnable 和 Callable
Runnable自 Java 1.0 以来一直存在,但Callable仅在 Java 1.5 中引入,目的就是为了来处理Runnable不支持的用例。Runnable 接口不会返回结果或抛出检查异常,但是 Callable 接口可以。所以,如果任务不需要返回结果或抛出异常推荐使用 Runnable 接口,这样代码看起来会更加简洁。
工具类 Executors 可以实现将 Runnable 对象转换成 Callable 对象。(Executors.callable(Runnable task) 或 Executors.callable(Runnable task, Object result))。
@FunctionalInterface
public interface Runnable {
/**
* 被线程执行,没有返回值也无法抛出异常
*/
public abstract void run();
}
@FunctionalInterface
public interface Callable<V> {
/**
* 计算结果,或在无法这样做时抛出异常。
* @return 计算得出的结果
* @throws 如果无法计算结果,则抛出异常
*/
V call() throws Exception;
}
Callable
用到了泛型机制,可以定义返回类型。
参考资料
Java学习篇(四)—— Java 多线程的更多相关文章
- Java学习笔记四:Java的八种基本数据类型
Java的八种基本数据类型 Java语言提供了八种基本类型.六种数字类型(四个整数型,两个浮点型),一种字符类型,还有一种布尔型. Java基本类型共有八种,基本类型可以分为三类,字符类型char,布 ...
- Java 学习笔记 (四) Java 语句优化
这个问题是从headfirst java看到的. 需求: 一个移动电话用的java通讯簿管理系统,要求最有效率的内存使用方法. 下面两段程序的优缺点,哪个占用内存更少. 第一段: Contact[]c ...
- java提高篇(四)-----理解java的三大特性之多态
面向对象编程有三大特性:封装.继承.多态. 封装隐藏了类的内部实现机制,可以在不影响使用的情况下改变类的内部结构,同时也保护了数据.对外界而已它的内部细节是隐藏的,暴露给外界的只是它的访问方法. 继承 ...
- Java总结篇:Java多线程
Java总结篇系列:Java多线程 多线程作为Java中很重要的一个知识点,在此还是有必要总结一下的. 一.线程的生命周期及五种基本状态 关于Java中线程的生命周期,首先看一下下面这张较为经典的图: ...
- java提高篇-----理解java的三大特性之封装
在<Think in java>中有这样一句话:复用代码是Java众多引人注目的功能之一.但要想成为极具革命性的语言,仅仅能够复制代码并对加以改变是不够的,它还必须能够做更多的事情.在这句 ...
- 从.Net到Java学习第四篇——spring boot+redis
从.Net到Java学习系列目录 “学习java已经十天,有时也怀念当初.net的经典,让这语言将你我相连,怀念你......”接上一篇,本篇使用到的框架redis.FastJSON. 环境准备 安装 ...
- java学习(四)
学号 20189214 <Java程序设计>第四周学习总结 教材学习内容总结 枚举 枚举是JDK1.5版本新增的特性(泛型.For-each等如今被广泛应用的特性也是由JDK1.5时所新增 ...
- JAVA学习第四十七课 — IO流(一):文件的读写
输入流和输出流相对于内存 将外部设备的数据读取到内存中:输入 将内存中的数据写入外部设备中:输出 IO流经常使用基类 字节流的抽象基类:InputStream,OutputStream 字符的抽象基类 ...
- Java学习十四
学习内容: 1.Junit 2.maven安装配置环境 一.Junit实例演示步骤 1.引入jar包 junit包需要引入hamcrest-core包,否则会报错 2.测试如下代码 package c ...
- Java学习笔记四:三目运算符与字符串连接符等
一 .三目运算符与自增自减 GitHub代码练习地址:https://github.com/Neo-ML/JavaPractice/blob/master/OperPrac02.java 条件运算符由 ...
随机推荐
- 学习unigui【21】unistringGrid的标题栏动态增加
var Column: TUniGridColumn; begin Column := TUniGridColumn(unstrngrd_summary.Columns.Add); Column.Ti ...
- 一句话秒建公网站!AI边缘计算颠覆传统开发
一句话就能让 AI 搭建一个公网可访问的完整网站: 短短几秒钟内,AI 便能完成所有构建操作: 这或许是目前全球最简便的建站方案: 本文使用的 AI 工具为腾讯云的 EdgeOne Pages MCP ...
- MySQL的并发问题的解决方案
怎么解决脏读.不可重复读.幻读这些问题呢?其实有两种可选的解决方案 方案一.读操作利用MVCC(多版本并发控制),写操作进行加锁. 所谓的MVCC,就是生成一个ReadView,通过ReadView找 ...
- JDK的SPI有什么缺陷?dubbo做了什么改进?
JDK的SPI机制的缺点 ⽂件中的所有类都会被加载且被实例化.这样也就导致获取某个实现类的方式不够灵活,只能通过 Iterator 形式获取,不能根据某个参数来获取对应的实现类.如果不想用某些实现类, ...
- CTF_RSA解密学习
CTF_RSA解密学习 00X00 .先看了一边李永乐老师的视频 https://www.bilibili.com/video/av26639065/ 00X01.对称.非对称算法了解 对称算法,加解 ...
- idea格式化代码快捷键
Ctrl+Alt+L Ctrl+Shift+Alt+L
- 【工具】you-get + ffmpeg|视频下载+音频提取
一.原理: you-get下载,ffmpeg音视频分离. 这两个都是命令行工具. you-get安装(无python环境请参考python详细安装教程): pip3 install --upgrade ...
- centos7部署keepalived
yum install keepalived -y 修改/etc/keepalived.conf配置文件,达到高可用状态 vim /etc/keepalived/keepalived.conf ! C ...
- 王炸!SpringBoot+MCP 让你的系统秒变AI小助手
王炸!SpringBoot+MCP 让你的系统秒变AI小助手 感觉本篇对你有帮助可以关注一下我的微信公众号(深入浅出谈java),会不定期更新知识和面试资料.技巧!!! 一.MCP 是什么? MCP( ...
- echart的使用心得
前言:由于本人在最近的公司中接触了一些与数据可视化有关的项目,所以特意花了一些时间去学习了echarts,以下是我个人在使用与学习echarts的一些心得体会. 1.首先我们需要知道的是什么是Echa ...