文章目录
锁与线程
文章目录 |
---|
本文来自《Java高并发核心编程(2)》的学习笔记 |
JAVA入门中_说好不能打脸_CSDN博客-系统架构,javaer,系统间通信技术领域博主 |
一、进程/线程的基本介绍
进程
什么是进程?进程就是程序的一次启动执行
![](https://csdn-pic-1301850093.cos.ap-guangzhou.myqcloud.com/csdn-pic/并发编程-进程结构.png)
线程
什么是线程?线程是进程的代码片段,一个进程可以有一个或多个线程,各个线程之间共享进程的内存空间、系统资源
![](https://csdn-pic-1301850093.cos.ap-guangzhou.myqcloud.com/csdn-pic/并发编程-线程结构.png)
1 线程的调度与时间片
线程的调度模型目前主要分为两种:分时调度模型和抢占式调度模型。
(1)分时调度模型:系统平均分配CPU的时间片,所有线程轮流占用CPU,即在时间片调度的分配上所有线程“人人平等”
(2)抢占式调度模型:系统按照线程优先级分配CPU时间片。优先级高的线程优先分配CPU时间片,如果所有就绪线程的优先级相同,那么会随机选择一个,优先级高的线程获取的CPU时间片相对多一些
由于目前大部分操作系统都是使用抢占式调度模型进行线程调度,Java的线程管理和调度是委托给操作系统完成的,与之相对应,Java的线程调度也是使用抢占式调度模型,因此Java的线程都有优先级。
2 优先级
线程运行的优先级,Java中使用抢占式调度模型进行线程调度。priority实例属性的优先级越高,线程获得CPU时间片的机会就越多
方法1:public final int getPriority(),获取线程优先级
方法2:public final void setPriority(int priority),设置线程优先级
3 生命周期
主要是Thread内部的枚举类public enum State
public enum State {
NEW,
RUNNABLE,
BLOCKED,
WAITING,
TIMED_WAITING,
TERMINATED;
}
新建、可执行、阻塞、等待、限时等待、终止
进程与线程的区别
- 通俗的讲,进程是大于线程的,一个进程相当于多个线程的组成,一个进程最少有一个线程,那就是它本身
- 线程是CPU调度的最小单位
- 线程之间资源共享,进程之间相互独立
- 上下文切换的速度来说,线程更轻量、更快
二、线程的使用
2.1 Thread类的介绍
在Thread类中,通常有这些操作
-
线程ID
-
名称
-
优先级
Java线程的最大优先级值为10,最小值为1,默认值为5
-
守护线程
什么是守护线程呢?
守护线程是在进程运行时提供某种后台服务的线程,比如垃圾回收(GC)线程
-
状态
新建 | 就绪、运行 | 阻塞 | 等待 | 计时等待 | 结束
-
启动和运行
-
获取当前线程
2.2 创建线程的方法
Thread
public class ThreadDemo extends Thread {
@Override
public void run() {
System.out.println("ThreadDemo 多线程测试");
System.out.println("线程名称 " + currentThread().getName());
System.out.println("ID " + currentThread().getId());
System.out.println("状态 " + currentThread().getState());
System.out.println("优先级 " + currentThread().getPriority());
}
}
-----------------------------------
public class Demo01 {
public static void main(String[] args) {
System.out.println("main 主线程运行=======");
new ThreadDemo().start();
}
}
Runnable
- 方法一:匿名类
new Thread(new Runnable() {
@Override
public void run() {
System.out.println("Runnable 运行");
}
}).start();
- 方法二:函数式编程
new Thread(() -> {
System.out.println("Runnable 运行");
}).start();
使用实现Runnable接口
这种方式是存在优缺点的
优点
(1)可以避免由于Java单继承带来的局限性。如果异步逻辑所在类已经继承了一个基类,就没有办法再继承Thread类
(2)逻辑和数据更好分离。通过实现Runnable接口实现多线程能更好地做到多个线程并发地完成同一个任务
缺点
(1)所创建的类并不是线程类,而是线程的target执行目标类,需要将其实例作为参数传入线程类的构造器,才能创建真正的线程
(2)如果访问当前线程的属性(甚至控制当前线程),不能直接访问Thread的实例方法,必须通过Thread.currentThread()获取当前线程实例,才能访问和控制当前线程
Callable
前面已经介绍了继承Thread类或者实现Runnable接口这两种方式来创建线程类,但是这两种方式有一个共同的缺陷:不能获取异步执行的结果。
这是一个比较大的问题,很多场景都需要获取异步执行的结果,通过Runnable无法实现,是因为它的run()方法不支持返回值。
为了解决异步执行的结果问题,Java语言在1.5版本之后提供了一种新的多线程创建方法:通过Callable接口和FutureTask类相结合创建线程
实现Callable接口的案例
class CallableDemo implements Callable{
@Override
public Object call() throws Exception {
System.out.println("有返回值 并且受检异常");
return null;
}
}
Future
问题:Callable实例能否和Runnable实例一样,作为Thread线程实例的target来使用呢?答案是不行。Thread的target属性的类型为Runnable,而Callable接口与Runnable接口之间没有任何继承关系,并且二者唯一的方法在名字上也不同。显而易见,Callable接口实例没有办法作为Thread线程实例的target来使用。既然如此,那么该如何使用Callable接口创建线程呢?
答案就是Future
Thread中的target是什么?
Allocates a new Thread object. This constructor has the same effect as Thread (null, target, gname), where gname is a newly generated name. Automatically generated names are of the form “Thread-”+n, where n is an integer.
Params:
target – the object whose run method is invoked when this thread is started. If null, this classes run method does nothing.分配一个新的Thread对象。这个构造函数的效果与Thread (null, target, gname)相同,其中gname是一个新生成的名称。自动生成的名称形式为“Thread-”+n,其中n是整数。
参数:
Target - 线程启动时调用其run方法的对象。如果为空,这个类运行方法什么也不做。public Thread(Runnable target) { this(null, target, "Thread-" + nextThreadNum(), 0); }
Future是一个泛型接口,用于异步的计算提供了检查计算是否完成、等待计算完成以及检索计算结果的方法
public interface Future<V> {
boolean cancel(boolean mayInterruptIfRunning);
// 取消任务的执行。参数指定是否立即中断任务执行,或者等等任务结束
boolean cancel(boolean mayInterruptIfRunning);
// 任务是否已经取消,任务正常完成前将其取消,则返回 true
boolean isCancelled();
// 任务是否已经完成。需要注意的是如果任务正常终止、异常或取消,都将返回true
boolean isDone();
// 等待任务执行结束,然后获得V类型的结果
V get() throws InterruptedException, ExecutionException;
// 参数timeout指定超时时间,uint指定时间的单位
V get(long timeout, TimeUnit unit)
throws InterruptedException, ExecutionException, TimeoutException;
}
FutureTask类是实现Runnable的Future实现
利用线程池
使用方式一:Executors(官方不推荐),原因后面
// 开辟线程池,放入10个线程数
ExecutorService Service = Executors.newFixedThreadPool(10);
// 适用于 Runnable,将Runnable实现类对象放入
Service.execute(new NumberThread());
Service.execute(new NumberThread2());
// 适用于 Callable
// Service.submit();
// 关闭线程池
Service.shutdown();
使用方式二:ThreadPoolExecutor(官方推荐
)
将在下章节对这两种方式进行说明
总结
2.3 为什么Executors被禁止使用
为什么阿里巴巴禁止使用 Executors 创建线程池?_singwhatiwanna-CSDN博客
阿里巴巴开发手册为什么禁止使用 Executors
去创建线程池
原因就是 newFixedThreadPool()
和 newSingleThreadExecutor()
两个方法允许请求的最大队列长度是 Integer.MAX_VALUE
,可能会出现任务堆积,出现OOM。
newCachedThreadPool()
允许创建的线程数量为 Integer.MAX_VALUE
,可能会创建大量的线程,导致发生OOM。
它建议使用ThreadPoolExecutor
方式去创建线程池,通过上面的分析我们也知道了其实Executors
三种创建线程池的方式最终就是通过ThreadPoolExecutor
来创建的,只不过有些参数我们无法控制,如果通过ThreadPoolExecutor
的构造器去创建,我们就可以根据实际需求控制线程池需要的任何参数,避免发生OOM异常
2.4 线程的API操作
![](https://csdn-pic-1301850093.cos.ap-guangzhou.myqcloud.com/csdn-pic/并发编程-线程的API操作-1.png)
2.5 线程间的通信
线程是程序调度的最小单位,有自己的栈空间,线程之间是共享内存空间的(抛开ThrealLocal),一个进程下的各个线程相互协作通信
线程之间主要还是依靠**wait();及notify();**实现相互之间的工作协助
知识点:
- wait() / notify() 方法原理
- 通过各类同步对象定义线程状态
- 需要在synchronized同步块的内部使用wait和notify
重点在于 wait() / notify() 方法原理 以及它们之间的通信要点
三、线程池的深入不出
3.1 ThreadPoolExecutor
构造参数说明
这个类 extends 于 AbstractExecutorService,下面针对它的有参构造参数进行说明
序号 | 名称 | 类型 | 含义 |
---|---|---|---|
1 | corePoolSize | int | 核心线程池大小 |
2 | maximumPoolSize | int | 最大线程池大小 |
3 | keepAliveTime | long | 线程最大空闲时间 |
4 | unit | TimeUnit | 时间单位 |
5 | workQueue | BlockingQueue | 线程等待队列 |
6 | threadFactory | ThreadFactory | 线程创建工厂 |
7 | handler | RejectedExecutionHandler | 拒绝策略 |
提交任务方式
-
方式一:调用execute()方法
Execute()方法只能接收Runnable类型的参数
-
方式二:调用submit()方法
submit()方法可以接收Callable、Runnable两种类型的参数
这个提交任务的方式就是说,你造了个线程池,然后用他对象的方法丢一个线程进去,这个方法就是我们说的提交任务的方式
阻塞队列
Java中的阻塞队列(BlockingQueue)与普通队列相比有一个重要的特点:在阻塞队列为空时会阻塞当前线程的元素获取操作。具体来说,在一个线程从一个空的阻塞队列中获取元素时线程会被阻塞,直到阻塞队列中有了元素;当队列中有元素后,被阻塞的线程会自动被唤醒(唤醒过程不需要用户程序干预)。
Java线程池使用BlockingQueue实例暂时接收到的异步任务,BlockingQueue是JUC包的一个超级接口,下面有很多比较常用的实现类
钩子方法
ThreadPoolExecutor threadPoolExecutor
= new ThreadPoolExecutor(
corePoolSize, maximumPoolSize, keepAliveTime,
timeUnit, workQueue
){
@Override
protected void beforeExecute(Thread t, Runnable r) {
System.out.println("任务执行前");
}
@Override
protected void afterExecute(Runnable r, Throwable t) {
System.out.println("任务执行后");
}
@Override
protected void terminated() {
System.out.println("线程池终止时");
}
};
拒绝策略
在线程池的任务缓存队列为有界队列(有容量限制的队列)的时候,如果队列满了,提交任务到线程池的时候就会被拒绝
总体来说,任务被拒绝有两种情况
(1)线程池已经被关闭
(2)工作队列已满且maximumPoolSize已满
无论以上哪种情况任务被拒绝,线程池都会调用RejectedExecutionHandler
实例的rejectedExecution方法。RejectedExecutionHandler是拒绝策略的接口
JUC为该接口提供了以下几种实现
- AbortPolicy:拒绝策略
- DiscardPolicy:抛弃策略
- DiscardOldestPolicy:抛弃最老任务策略
- CallerRunsPolicy:调用者执行策略
- 自定义策略
继承 RejectedExecutionHandler 接口实现 rejectedExecution 方法即可完成自定义策略
3.2 线程池的任务调度流程
![](https://csdn-pic-1301850093.cos.ap-guangzhou.myqcloud.com/csdn-pic/并发编程-线程池任务调度流程.png)
3.3 线程池工厂
首先看看接口
![](https://csdn-pic-1301850093.cos.ap-guangzhou.myqcloud.com/csdn-pic/并发编程-线程池工厂的接口-1.png)
里面只有一个方法,那就是造一个线程
线程池的工厂方法有四种实现,如下
我们还可以按需定制自己的线程池工厂
Java并发编程:Java的四种线程池的使用,以及自定义线程工厂 - 鄙人薛某 - 博客园 (cnblogs.com)
3.4 线程池生命周期/状态
// runState is stored in the high-order bits
private static final int RUNNING = -1 << COUNT_BITS;
private static