通过本文,你将了解Java中线程的状态,如何进行状态切换,同时掌握线程池相关知识,了解线程池常用参数以及参数如何合理配置,还有JDK中常见的线程池介绍,最后将介绍volatile关键字,如何解决可见性问题、原子性问题
初始(NEW):新创建了一个线程对象,但还没有调用start()方法。
运行(RUNNABLE):Java线程中将就绪(ready)和运行中(running)两种状态笼统的称为“运行”。
线程对象创建后,其他线程(比如main线程)调用了该对象的start()方法。该状态的线程位于可运行线程池中,等待被线程调度选中,获取CPU的使用权,此时处于就绪状态(ready)。就绪状态的线程在获得CPU时间片后变为运行中状态(running)。
阻塞(BLOCKED):表示线程阻塞于锁。
等待(WAITING):进入该状态的线程需要等待其他线程做出一些特定动作(通知或中断)。
超时等待(TIMED_WAITING):该状态不同于WAITING,它可以在指定的时间后自行返回(等待态需要被唤醒,超时等待自己醒)。
PS:这个我感觉翻译成计时等待更加合理一点,听到超时等待容易想偏,以为是线程运行超时进入等待状态,其实并不是
终止(TERMINATED):表示该线程已经执行完毕。
线程状态 | 具体含义 |
---|---|
NEW |
一个尚未启动的线程的状态。也称之为初始状态、开始状态。线程刚被创建,但是并未启动。还没调用start方法。MyThread t = new MyThread()只有线程象,没有线程特征。 |
RUNNABLE |
当我们调用线程对象的start方法,那么此时线程对象进入了RUNNABLE状态。那么此时才是真正的在JVM进程中创建了一个线程,线程一经启动并不是立即得到执行,线程的运行与否要听令与CPU的调度,那么我们把这个中间状态称之为可执行状态(RUNNABLE)也就是说它具备执行的资格,但是并没有真正的执行起来而是在等待CPU的度。 |
BLOCKED |
当一个线程试图获取一个对象锁,而该对象锁被其他的线程持有,则该线程进入Blocked状态;当该线程持有锁时,该线程将变成Runnable状态。 |
WAITING |
一个正在等待的线程的状态。也称之为等待状态。造成线程等待的原因有两种,分别是调用Object.wait()、join()方法。处于等待状态的线程,正在等待其他线程去执行一个特定的操作。例如:因为wait()而等待的线程正在等待另一个线程去调用notify()或notifyAll();一个因为join()而等待的线程正在等待另一个线程结束。 |
TIMED_WAITING |
一个在限定时间内等待的线程的状态。也称之为限时等待状态。造成线程限时等待状态的原因有三种,分别是:Thread.sleep(long),Object.wait(long)、join(long)。 |
TERMINATED |
一个完全运行完成的线程的状态。也称之为终止状态、结束状态 |
状态被定义在了java.lang.Thread.State枚举类中,State枚举类的源码如下:
public class Thread {
public enum State {
/* 新建 */
NEW ,
/* 可运行状态 */
RUNNABLE ,
/* 阻塞状态 */
BLOCKED ,
/* 无限等待状态 */
WAITING ,
/* 计时等待 */
TIMED_WAITING ,
/* 终止 */
TERMINATED;
}
// 获取当前线程的状态
public State getState() {
return jdk.internal.misc.VM.toThreadState(threadStatus);
}
}
为了验证上面论述的状态即状态转换的正确性,也为了加深对线程状态转换的理解,下面通过三个案例演示线程间中的状态转换。
需求:编写一段代码,依次显示一个线程的这些状态:NEW -> RUNNABLE -> TIME_WAITING -> RUNNABLE -> TERMINATED
为了简化我们的开发,本次我们使用匿名内部类结合lambda表达式的方式使用多线程。
public class ThreadStateDemo01 {
public static void main(String[] args) throws InterruptedException {
Thread t = new Thread(() -> {
System.out.println("2.执行t.start()之后Thread-0的线程状态: " + Thread.currentThread().getState());
try {
Thread.sleep(100);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
// RUNNABLE(就绪态)
System.out.println("4.执行Thread.sleep()之后Thread-0的线程状态: " + Thread.currentThread().getState());
});
// NEW(新建态)
System.out.println("1.执行Thread.start()之前Thread-0的线程状态: " + t.getState());
// RUNNABLE(就绪态)
t.start();
// TIMED_WAITING(超时等待态)
// Thread-0线程休眠100ms,main线程休眠50ms,所以程序启动后的50ms左右的时候
// main线程是处于RUNNABLE(运行态),而Thread-0线程处于 TIMED_WAITING(超时等待态)
Thread.sleep(50);
System.out.println("3.执行Thread.sleep()之后Thread-0的线程状态: " + t.getState());
// TERMINATED(终止态)
// main线程休眠150ms,加上前面休眠的50ms,main线程总共休眠了200ms
// 而Thread-0只休眠了100ms,200ms时Thread-0已经执行完毕了
Thread.sleep(150);
System.out.println("5.执行完毕之后Thread-0线程的状态: " + t.getState());
}
}
需求:编写一段代码,依次显示一个线程的这些状态:NEW -> RUNNABLE -> WAITING -> RUNNABLE -> TERMINATED
package com.hhxy.demo11;
/**
* @author ghp
* @date 2023/6/14
* @title
* @description
*/
public class ThreadStateDemo02 {
public static void main(String[] args) throws InterruptedException {
// 定义一个对象,用来加锁和解锁
Object obj = new Object();
Thread t = new Thread(() -> {
// RUNNABLE(就绪态)
System.out.println("2.执行t.start()之后,Thread-0线程的状态" + Thread.currentThread().getState());
synchronized (obj) {
try {
// 让Thread-0休眠100ms
Thread.sleep(100);
obj.wait();
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
// RUNNABLE(就绪态)
// Thread-0被Thread-1唤醒
System.out.println("4.执行obj.notify()之后,Thread-0线程的状态" + Thread.currentThread().getState());
});
// NEW(新建态)
System.out.println("1.执行t.start()方法之前,Thread-0线程的状态" + t.getState());
t.start();
// WAITING(无限等待态)
// 此时Thread-0休眠100ms,而main线程休眠了150ms
// 当main线程休眠完毕,此时Thread-0已经执行了obj.wait(),进入了TIMED_WAITING
Thread.sleep(150);
System.out.println("3.执行obj.wait()后,Thread-0线程的状态" + t.getState());
// 创建一个新线程Thread-1,用于唤醒处于TIMED_WAITING态的Thread-0
new Thread(() -> {
synchronized (obj) {
obj.notify();
}
}).start();
// TERMINATED(终止态)
// 主线程休眠10ms,保障此时Thread-0已经运行完毕
Thread.sleep(10);
System.out.println("5.线程执行完毕后,Thread-0的状态" + t.getState());
}
}
需求:编写一段代码,依次显示一个线程的这些状态:NEW -> RUNNABLE -> BLOCKED -> RUNNABLE -> TERMINATED
public class ThreadStateDemo03 {
public static void main(String[] args) throws InterruptedException {
// 定义一个对象,用来加锁和解锁
Object obj = new Object();
// 创建一个线程,先抢占锁
new Thread(() -> {
synchronized (obj) {
try {
Thread.sleep(100); //第一个线程要持有锁100毫秒
obj.wait(); //然后通过wait()方法进行等待状态,并释放obj2的对象锁
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}).start();
Thread t = new Thread(() -> {
// RUNNABLE(就绪态)
System.out.println("2.执行t.start()之后,线程的状态: " + Thread.currentThread().getState());
synchronized (obj) {
try {
// 唤醒处于等待态中的Thread-0
obj.notify();
} catch (RuntimeException e) {
e.printStackTrace();
}
}
// RUNNABLE(就绪态)
System.out.println("4.执行obj.notify()后,Thread-1线程的状态: " + Thread.currentThread().getState());
});
// NEW(新建态)
System.out.println("1.执行t.start()之前,Thread-1线程的状态: " + t.getState());
t.start();
// BLOCKED(阻塞态)
// main线程等待50ms,确保Thread-1已经被执行了
// 此时由于Thread-1被执行,需要获取锁,锁被Thread-0抢占了,Thread-0休眠了100ms
// Thread-0无法释放锁,所以Thread-1无法获取锁,进入BLOCKED
Thread.sleep(50);
System.out.println("3.因为等待锁而阻塞时,Thread-1线程的状态: " + t.getState());
// TERMINATED(终止态)
// main线程休眠300ms,Thread-1早已执行完毕
Thread.sleep(300);
System.out.println("5.Thread-1线程线程执行完毕之后的状态: " + t.getState());
}
}
实现思路:
在【Java多线程快速入门】这篇文章中,我们已经学习了使用Executors
创建创建线程池,它创建的线程池是JDK提供的,现在我们来尝试着自己实现一个线程池吧(●’◡’●)
TreadPool:
package com.hhxy.demo12;
import java.util.concurrent.BlockingQueue;
import java.util.concurrent.LinkedBlockingQueue;
public class ThreadPool {
// 初始化线程个数
private static final int DEFAULT_POOL_SIZE = 2;
// 在该类中定义两个成员变量poolSize(线程池初始化线程的个数) , BlockingQueue(任务容器)
private int poolSize = DEFAULT_POOL_SIZE;
private BlockingQueue<Runnable> blockingQueue = new LinkedBlockingQueue<Runnable>();
// 无参构造方法
public ThreadPool() {
this.initThread();
}
// 有参构造方法,通过构造方法来创建两个线程对象(消费者线程),并且启动
public ThreadPool(int poolSize) {
if (poolSize > 0) {
this.poolSize = poolSize;
}
this.initThread();
}
// 初始化线程方法
public void initThread() {
for (int x = 0; x < poolSize; x++) {
new TaskThread("Thread-" + x).start();
}
}
// 提供一个方法(submit)向任务容器中添加任务
public void submit(Runnable runnable) {
try {
blockingQueue.put(runnable);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
// 使用内部类的方式去定义一个线程类
public class TaskThread extends Thread {
// 提供一个构造方法,用来初始化线程名称
public TaskThread(String name) {
super(name);
}
@Override
public void run() {
while (true) {
try {
// 两个消费者线程需要不断的从任务容器中获取任务,如果没有任务,则线程处于阻塞状态。
Runnable task = blockingQueue.take();
task.run();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
}
测试类:
public class ThreadPoolDemo01 {
public static void main(String[] args) {
// 创建线程池对象,无参构造方法创建
// ThreadPool threadPool = new ThreadPool();
// 有参构造,指定线程池中线程的数量为5
ThreadPool threadPool = new ThreadPool(5);
// 提交任务
for(int x = 0 ; x < 10 ; x++) {
threadPool.submit( () -> {
System.out.println(Thread.currentThread().getName() + "---->>>处理了任务");
});
}
}
}
获取线程池的方法
创建一个可缓存线程池,可灵活的去创建线程,并且灵活的回收线程,若无可回收,则新建线程
ExecutorService newCachedThreadPool()
初始化一个具有固定数量线程的线程池
ExecutorService newFixedThreadPool(int nThreads)
初始化一个具有一个线程的线程池(做完一个,再做一个,不停歇,直到做完,老黄牛性格)
ExecutorService newSingleThreadExecutor():
初始化一个具有一个线程的线程池,支持定时及周期性任务执行(按照固定的计划去执行线程,一个做完之后按照计划再做另一个)
ScheduledExecutorService newSingleThreadScheduledExecutor()
备注:这个方法返回的都是ExecutorService
类型的对象(ScheduledExecutorService
继承ExecutorService),而ExecutorService可以看做就是一个线程池,那么ExecutorService
线程池中常见的方法
提交任务方法
Future<?> submit(Runnable task)
关闭线程池的方法
void shutdown():
示例
示例一:演示newCachedThreadPool
方法所获取到的线程池的特点
public class ExecutorsDemo01 {
// 演示Executors中的newCachedThreadPool返回的线程池的特点
public static void main(String[] args) throws InterruptedException {
// 获取线程池对象
ExecutorService threadPool = Executors.newCachedThreadPool();
// 提交任务
threadPool.submit(() -> {
System.out.println( Thread.currentThread().getName() + "---执行了任务");
});
// Thread.sleep(100); // 主线程休眠100ms
// 提交任务
threadPool.submit(() -> {
System.out.println( Thread.currentThread().getName() + "---执行了任务");
});
// 不使用线程池了,还可以将线程池关闭
threadPool.shutdown();
}
}
控制台打印结果:
pool-1-thread-2---执行了任务
pool-1-thread-1---执行了任务
如果提交第一个任务后,让主线程休眠100ms,控制台打印结果:
pool-1-thread-1---执行了任务
pool-1-thread-1---执行了任务
案例二:演示newFixedThreadPool
方法所获取到的线程池的特点
public class ExecutorsDemo03 {
// 演示newFixedThreadPool方法所获取到的线程池的特点
public static void main(String[] args) {
// 获取线程池对象,初始化一个具有固定数量线程的线程池
ExecutorService threadPool = Executors.newFixedThreadPool(3); // 在该线程池中存在3个线程
// 提交任务
for(int x = 0 ; x < 5 ; x++) {
threadPool.submit( () -> {
System.out.println(Thread.currentThread().getName() + "----->>>执行了任务" );
});
}
// 关闭线程池
threadPool.shutdown();
}
}
控制台输出结果(通过控制台的输出结果,我们可以看到5个任务是通过3个线程进行执行的,说明此线程池中存在三个线程对象):
pool-1-thread-1----->>>执行了任务
pool-1-thread-2----->>>执行了任务
pool-1-thread-2----->>>执行了任务
pool-1-thread-2----->>>执行了任务
pool-1-thread-3----->>>执行了任务
示例三:演示newSingleThreadExecutor
方法所获取到的线程池的特点
public class ExecutorsDemo04 {
// 演示newSingleThreadExecutor方法所获取到的线程池的特点
public static void main(String[] args) {
// 获取线程池对象,初始化一个具有一个线程的线程池
ExecutorService threadPool = Executors.newSingleThreadExecutor();
// 提交任务
for(int x = 0 ; x < 5 ; x++) {
threadPool.submit(() -> {
System.out.println(Thread.currentThread().getName() + "----->>>执行了任务");
});
}
// 关闭线程池
threadPool.shutdown();
}
}
控制台打印结果(我们可以看到5个任务是通过1个线程进行执行的,说明此线程池中只存在一个线程对象):
pool-1-thread-1----->>>执行了任务
pool-1-thread-1----->>>执行了任务
pool-1-thread-1----->>>执行了任务
pool-1-thread-1----->>>执行了任务
pool-1-thread-1----->>>执行了任务
示例四:演示newSingleThreadScheduledExecutor
方法所获取到的线程池的特点(支持定时及周期性任务执行)
测试类1(演示定时执行):
public class ExecutorsDemo06 {
// 演示newSingleThreadScheduledExecutor方法所获取到的线程池的特点(支持定时及周期性任务执行)
public static void main(String[] args) {
// 获取线程池对象
ScheduledExecutorService threadPool = Executors.newSingleThreadScheduledExecutor();
// 提交任务,10s以后开始执行该任务
threadPool.schedule( () -> {
System.out.println(Thread.currentThread().getName() + "---->>>执行了该任务");
} , 10 , TimeUnit.SECONDS) ;
// 关闭线程池
threadPool.shutdown();
}
}
测试类2(演示周期执行):
public class ExecutorsDemo07 {
// 演示newSingleThreadScheduledExecutor方法所获取到的线程池的特点(支持定时及周期性任务执行)
public static void main(String[] args) {
// 获取线程池对象
ScheduledExecutorService threadPool = Executors.newSingleThreadScheduledExecutor();
// 提交任务,10s以后开始第一次执行该任务,然后每隔1秒执行一次
threadPool.scheduleAtFixedRate( () -> {
System.out.println(Thread.currentThread().getName() + "---->>>执行了该任务");
} , 10 ,1, TimeUnit.SECONDS) ;
}
}
ScheduledExecutorService中和定时以及周期性执行相关的方法:
/*
定时执行
command: 任务类对象
delay : 延迟多长时间开始执行任务, 任务提交到线程池以后我们需要等待多长时间开始执行这个任务
unit : 指定时间操作单元
*/
public ScheduledFuture<?> schedule(Runnable command,long delay, TimeUnit unit);
/*
周期性执行
command: 任务类对象
initialDelay: 延迟多长时间开始第一次该执行任务, 任务提交到线程池以后我们需要等待多长时间开始第一次执行这个任务
period: 下一次执行该任务所对应的时间间隔
unit: 指定时间操作单元
*/
public ScheduledFuture<?> scheduleAtFixedRate(Runnable command, long initialDelay, long period, TimeUnit unit);
刚才我们是通过Executors中的静态方法去创建线程池的,通过查看源代码我们发现,其底层都是通过ThreadPoolExecutor构建的。比如:newFixedThreadPool方法的源码
public static ExecutorService newFixedThreadPool(int nThreads) {
// 创建了ThreadPoolExecutor对象,然后直接返回
return new ThreadPoolExecutor(nThreads, nThreads,0L, TimeUnit.MILLISECONDS, new LinkedBlockingQueue<Runnable>());
}
那么也可以使用ThreadPoolExecutor去创建线程池。
ThreadPoolExecutor最完整的构造方法:
public ThreadPoolExecutor(int corePoolSize,
int maximumPoolSize,
long keepAliveTime,
TimeUnit unit,
BlockingQueue<Runnable> workQueue,
ThreadFactory threadFactory,
RejectedExecutionHandler handler)
参数说明:
corePoolSize
: 核心线程的最大值,不能小于0maximumPoolSize
:最大线程数,不能小于等于0,maximumPoolSize >= corePoolSizekeepAliveTime
: 空闲线程最大存活时间,不能小于0unit
: 时间单位workQueue
: 任务队列,不能为nullthreadFactory
: 创建线程工厂,不能为nullhandler
: 任务的拒绝策略,不能为null示例
public class ThreadPoolExecutorDemo01 {
// 演示基本使用
public static void main(String[] args) {
// 通过ThreadPoolExecutor创建一个线程池对象
ThreadPoolExecutor threadPoolExecutor = new ThreadPoolExecutor(1 , 3 , 60 , TimeUnit.SECONDS ,
new ArrayBlockingQueue<Runnable>(3) , Executors.defaultThreadFactory() , new ThreadPoolExecutor.AbortPolicy()) ;
/*
以上代码表示的意思是:核心线程池中的线程数量最大为1,
整个线程池中最多存在3个线程,空闲线程最大的存活时间为60,
时间单位为秒,阻塞队列使用的是有界阻塞队列容量为3,使用默认的线程工厂;以及默认的任务处理策略
*/
// 提交任务
threadPoolExecutor.submit( () -> {
System.out.println(Thread.currentThread().getName() + "------>>>执行了任务");
});
// 关闭线程池
threadPoolExecutor.shutdown();
}
}
当我们通过submit
方法向线程池中提交任务的时候,具体的工作流程如下:
举例说明:
假如有一个工厂,工厂里面有10个工人(正式员工),每个工人同时只能做一件任务。因此只要当10个工人中有工人是空闲的,来了任务就分配给空闲的工人做;当10个工人都有任务在做时,
如果还来了任务,就把任务进行排队等待;如果说新任务数目增长的速度远远大于工人做任务的速度,排队的任务已经满了,那么此时工厂主管可能会想补救措施,比如重新招4个临时工人进来;然后就将任务也分配
给这4个临时工人做;如果说着14个工人做任务的速度还是不够,此时工厂主管可能就要考虑不再接收新的任务或者抛弃排队中的一些任务了。当这14个工人当中有人空闲时,而新任务增长的速度
又比较缓慢,工厂主管可能就考虑辞掉4个临时工了,只保持原来的10个工人,毕竟请额外的工人是要花钱的。
这里的工厂可以看做成是一个线程池,每一个工人可以看做成是一个线程。其中10个正式员工,可以看做成是核心线程池中的线程,临时工就是非核心线程池中的线程。当临时工处于空闲状态
的时候,那么如果空闲的时间超过keepAliveTime
所指定的时间,那么就会被销毁。
示例
接下来我们就通过一段代码的断点测试,来演示一下线程池的工作原理。
public class ThreadPoolExecutorDemo01 {
public static void main(String[] args) {
/**
* 核心线程数量为1 , 最大线程池数量为3, 任务容器的容量为1 ,空闲线程的最大存在时间为20s
*/
ThreadPoolExecutor threadPoolExecutor = new ThreadPoolExecutor(1 , 3 , 20 , TimeUnit.SECONDS ,
new ArrayBlockingQueue<>(1) , Executors.defaultThreadFactory() , new ThreadPoolExecutor.AbortPolicy()) ;
// 提交3个任务,此时会产生一个核心线程,一个临时工线程,队列中会存在一个任务,20s后临时工线程被回收,核心线程不会被回收
for(int x = 0 ; x < 3 ; x++) {
threadPoolExecutor.submit(() -> { // 断点位置
System.out.println(Thread.currentThread().getName() + "---->> 执行了任务");
});
}
}
}
初次debug方式启动线程,查看变量值:
由于此时还没有提交任务,因此线程池中的线程数量为0,工作队列的任务数量也为0;提交一个任务:
再次查看各个值的变化
再次提交一个任务
再次查看各个值的变化
此时会把第二个任务存储到工作队列中,因此工作队列的值为1了。再次提交一个任务
再次查看各个值的变化
此时3个任务都以及提交完毕,断点跳过。经过20s以后,再次查看该进程中的线程。
我们发现非核心线程已经被线程池回收了。
RejectedExecutionHandler
是 JDK 提供的一个任务拒绝策略接口,它下面存在4个子类。
// 丢弃任务并抛出RejectedExecutionException异常。是默认的策略。
ThreadPoolExecutor.AbortPolicy
// 丢弃任务,但是不抛出异常 这是不推荐的做法。
ThreadPoolExecutor.DiscardPolicy
// 抛弃队列中等待最久的任务 然后把当前任务加入队列中。
ThreadPoolExecutor.DiscardOldestPolicy
// 调用任务的run()方法绕过线程池直接执行。
ThreadPoolExecutor.CallerRunsPolicy
注:线程池最大可执行的任务数 = 队列容量 + 最大线程数(核心线程数+临时线程数)
示例
示例一:演示ThreadPoolExecutor.AbortPolicy
任务处理策略
public class ThreadPoolExecutorDemo01 {
public static void main(String[] args) {
/**
* 核心线程数量为1 , 最大线程池数量为3, 任务容器的容量为1 ,空闲线程的最大存在时间为20s
*/
ThreadPoolExecutor threadPoolExecutor = new ThreadPoolExecutor(1 , 3 , 20 , TimeUnit.SECONDS ,
new ArrayBlockingQueue<>(1) , Executors.defaultThreadFactory() , new ThreadPoolExecutor.AbortPolicy()) ;
// 提交5个任务,而该线程池最多可以处理4个任务,当我们使用AbortPolicy这个任务处理策略的时候,就会抛出异常
for(int x = 0 ; x < 5 ; x++) {
threadPoolExecutor.submit(() -> {
System.out.println(Thread.currentThread().getName() + "---->> 执行了任务");
});
}
}
}
提交个任务,而线程池的最大容量为4,所以采用AbortPolicy,会直接抛出一个RejectedExecutionException`异常
示例二:演示ThreadPoolExecutor.DiscardPolicy
任务处理策略
package com.hhxy.demo12;
import java.util.concurrent.ArrayBlockingQueue;
import java.util.concurrent.Executors;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;
public class ThreadPoolExecutorDemo02 {
public static void main(String[] args) {
/**
* 核心线程数量为1 , 最大线程池数量为3, 任务容器的容量为1 ,空闲线程的最大存在时间为20s
*/
ThreadPoolExecutor threadPoolExecutor = new ThreadPoolExecutor(1, 3, 20, TimeUnit.SECONDS,
new ArrayBlockingQueue<>(1), Executors.defaultThreadFactory(), new ThreadPoolExecutor.DiscardPolicy());
// 提交5个任务,而该线程池最多可以处理4个任务,当我们使用DiscardPolicy这个任务处理策略的时候,控制台不会报错
for (int x = 0; x < 5; x++) {
final int y = x;
threadPoolExecutor.submit(() -> {
System.out.println(Thread.currentThread().getName() + "---->> 执行了任务" + y);
});
}
}
}
提交5个任务,丢弃了一个任务,并不会抛异常:
示例三:演示ThreadPoolExecutor.DiscardOldestPolicy
任务处理策略
public class ThreadPoolExecutorDemo02 {
public static void main(String[] args) {
/**
* 核心线程数量为1 , 最大线程池数量为3, 任务容器的容量为1 ,空闲线程的最大存在时间为20s
*/
ThreadPoolExecutor threadPoolExecutor;
threadPoolExecutor = new ThreadPoolExecutor(1 , 3 , 20 , TimeUnit.SECONDS ,
new ArrayBlockingQueue<>(1) , Executors.defaultThreadFactory() , new ThreadPoolExecutor.DiscardOldestPolicy());
// 提交5个任务
for(int x = 0 ; x < 5 ; x++) {
// 定义一个变量,来指定指定当前执行的任务;这个变量需要被final修饰
final int y = x ;
threadPoolExecutor.submit(() -> {
System.out.println(Thread.currentThread().getName() + "---->> 执行了任务" + y);
});
}
}
}
核心线程处理任务0,任务1进入阻塞队列,任务2和任务3被临时线程处理,提交任务4时,超过了线程池的最大可执行线程数,此时采用DiscardOldestPolicy
,直接将阻塞队列中等待最久的任务移除,所以任务1发生了丢失
示例四:演示ThreadPoolExecutor.CallerRunsPolicy
任务处理策略
public class ThreadPoolExecutorDemo04 {
public static void main(String[] args) {
/**
* 核心线程数量为1 , 最大线程池数量为3, 任务容器的容量为1 ,空闲线程的最大存在时间为20s
*/
ThreadPoolExecutor threadPoolExecutor;
threadPoolExecutor = new ThreadPoolExecutor(1 , 3 , 20 , TimeUnit.SECONDS ,
new ArrayBlockingQueue<>(1) , Executors.defaultThreadFactory() , new ThreadPoolExecutor.CallerRunsPolicy());
// 提交5个任务
for(int x = 0 ; x < 5 ; x++) {
threadPoolExecutor.submit(() -> {
System.out.println(Thread.currentThread().getName() + "---->> 执行了任务");
});
}
}
}
通过控制台的输出,我们可以看到次策略没有通过线程池中的线程执行任务,而是直接调用任务的run()
方法绕过线程池直接执行:
编写Thread类:
public class VolatileThread extends Thread {
// 定义成员变量
private boolean flag = false ;
public boolean isFlag() { return flag;}
@Override
public void run() {
// 线程休眠1秒
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
// 将flag的值更改为true
this.flag = true ;
System.out.println("flag=" + flag);
}
}
编写测试类:
package com.hhxy.demo013;
public class VolatileThreadDemo01 {
public static void main(String[] args) {
// 创建VolatileThread线程对象
VolatileThread volatileThread = new VolatileThread() ;
volatileThread.start();
// 在main线程中获取开启的线程中flag的值
while(true) {
System.out.println("main线程中获取开启的线程中flag的值为" + volatileThread.isFlag());
if (volatileThread.isFlag()){
System.out.println("main线程中获取开启的线程中flag的值为" + volatileThread.isFlag());
break;
}
}
}
}
控制台输出:
按照我们的分析,当我们把volatileThread线程启动起来以后,那么volatileThread线程开始执行。在volatileThread线程的run方法中,线程休眠1s,休眠一秒以后那么flag的值应该为true,此时我们在主线程中不停的获取flag的值。发现前面释放false,后面是true信息,那么这是为什么呢?要想知道原因,那么我们就需要学习一下JMM
关于JMM详情,可以参考这篇文章:
什么是JMM?
JMM(Java Memory Model)是Java内存模型的简称。它定义了Java程序中线程之间如何通过内存进行通信以及如何执行操作的规范。JMM规定了一组规则和语义,用于确保多线程环境下共享变量的可见性、有序性和原子性。JMM定义了主内存与工作内存的概念,并规定了线程与内存之间的交互操作,描述了Java程序中各种变量(线程共享变量)的访问规则,以及在 JVM 中将变量存储到内存和从内存中读取变量这样的底层细节。
JMM相关概念:
JMM规定了以下几个重要的特性:
此外:
所有的共享变量都存储于主内存(计算机的RAM)这里所说的变量指的是实例变量和类变量。不包含局部变量,因为局部变量是线程私有的,因此不存在竞争问题。
每一个线程还存在自己的工作内存,线程的工作内存,保留了被线程使用的变量的工作副本。
线程对变量的所有的操作(读,写)都必须在工作内存中完成,而不能直接读写主内存中的变量,不同线程之间也不能直接访问对方工作内存中的变量,线程间变量的值的传递需要通过主内存完成。
了解了一下JMM,那么接下来我们就来分析一下3.1中产生问题的原因
流程分析:
VolatileThread
对象后,类加载器会先初始化成员变量,将VolatileThread的flag
的值进行初始化,然后放入主内存中,此时flag的值是falsevolatileThread.isFlag()
获取VolatileThread对象中的flag属性的值时,main线程会从主存中将flag的值拷贝到自己的工作内存中,而由于此时VolatileThread的run方法进行了休眠,所以此时主存中flag的值并没有发生改变,仍然是false。这就是为什么3.1中为什么一开始全是输出false的原因(●ˇ∀ˇ●)以上就是对3.1程序执行流程的一个分析,那有什么问题避免我能够即时获取到VolatileThread中flag更新后的值呢,而不会因为VolatileThread线程运行时延迟,导致main线程不能即时获取到flag更新后的值。这里提供一个最为简单的方式:
package com.hhxy.demo013;
public class VolatileThreadDemo01 {
public static void main(String[] args) throws InterruptedException {
// 创建VolatileThread线程对象
VolatileThread volatileThread = new VolatileThread() ;
volatileThread.start();
// 在main线程中获取开启的线程中flag的值
while(true) {
// 让main线程休眠2s,保障main线程执行时,VolatileThread线程已经修改了flag的值
Thread.sleep(2000);
System.out.println("main线程中获取开启的线程中flag的值为" + volatileThread.isFlag());
if (volatileThread.isFlag()){
System.out.println("main线程中获取开启的线程中flag的值为" + volatileThread.isFlag());
break;
}
}
}
}
当然实际开发中,我们肯定不能通过让线程休眠来实现数据的即时同步,这显然是很不科学的,首先我们不确定调用线程的延迟时间,休眠时间过短达不到效果,休眠时间过长浪费时间,所以我们需要使用更为优秀的方法
package com.hhxy.demo013;
public class VolatileThreadDemo01 {
public static void main(String[] args) throws InterruptedException {
// 创建VolatileThread线程对象
VolatileThread volatileThread = new VolatileThread() ;
volatileThread.start();
// 在main线程中获取开启的线程中flag的值
while(true) {
if (volatileThread.isFlag()){
System.out.println("main线程中获取开启的线程中flag的值为" + volatileThread.isFlag());
break;
}
}
}
}
可以看到,如果不在 if 前面进行休眠,是无法进入while循环中的if中的;3.1中能够进入是??由于sout能够从主存刷新到工作内存中??;而后面又通过Thread.sleep休眠,等到flag刷新,也是能够进入if中的;而这里既没有延迟,也没有sout,所以就导致main线程不能将主存中更新的flag刷新到main的工作队列中,main线程每次获取flag都是从工作内存中获取的,此时没有更新,为false,所以无法进入if
备注:??。。。??中的话有待商榷,具体原因我也不是很懂,这是我的猜测
package com.hhxy.demo013;
public class VolatileThreadDemo01 {
public static void main(String[] args) throws InterruptedException {
// 创建VolatileThread线程对象
VolatileThread volatileThread = new VolatileThread() ;
volatileThread.start();
// 在main线程中获取开启的线程中flag的值
while(true) {
synchronized (volatileThread){
if (volatileThread.isFlag()){
System.out.println("main线程中获取开启的线程中flag的值为" + volatileThread.isFlag());
break;
}
}
}
}
}
工作原理说明
对上述代码加锁完毕以后,某一个线程支持该程序的过程如下:
VolatileThread类,给flag成员变量添加一个volatile关键字修饰
private volatile boolean flag = false;
package com.hhxy.demo013;
public class VolatileThreadDemo01 {
public static void main(String[] args) throws InterruptedException {
// 创建VolatileThread线程对象
VolatileThread volatileThread = new VolatileThread();
volatileThread.start();
// 在main线程中获取开启的线程中flag的值
while (true) {
if (volatileThread.isFlag()) {
System.out.println("main线程中获取开启的线程中flag的值为" + volatileThread.isFlag());
break;
}
}
}
}
可以看到不用加锁,也能进入if中:
工作原理说明
synchronized和volatile都可以保障共享变量的可见性,但两者的机制稍有不同,
简单理解:synchronized是通过内存屏障实现的,volatile是通过强制让线程和主内存交互实现的
两种实现方式的比较
关于volatile原子性测试直接看一下小节就好了
我们刚才说到了volatile在多线程环境下只保证了共享变量在多个线程间的可见性,但是不保证原子性。那么接下来我们就来做一个测试。测试的思想,就是使用volatile修饰count。
线程类
public class VolatileAtomicThread implements Runnable {
// 定义一个int类型的变量,并且使用volatile修饰
private volatile int count = 0 ;
@Override
public void run() {
// 对该变量进行++操作,100次
for(int x = 0 ; x < 100 ; x++) {
count++ ;
System.out.println("count =========>>>> " + count);
}
}
}
控制台输出结果(需要运行多次)
备注:这个存在随机性,如果运气好还是可以出现10000,多运行几次就会出现上面的结果
通过控制台结果的输出,我们可以看到程序还是会出现问题。因此也就证明volatile关键字是不保证原子性的。
volatile关键字不保证原子性操作,那么大家可能会存在一些疑问,volatile关键字在什么情况下进行使用呢?这里我们举两个基本的使用场景。
比如现在存在一个线程不断向控制台输出一段话"传智播客中国IT教育的标杆…",当这个线程执行5秒以后,将该线程结束。
PS:这个在3.4节就已经介绍过了
实现思路:定义一个boolean类型的变量,这个变量就相当于一个标志。当这个变量的值为true的时候,线程一直执行,10秒以后我们把这个变量的值更改为false,此时结束该线程的执行。
为了保证一个线程对这个变量的修改,另外一个线程立马可以看到,这个变量就需要通过volatile关键字进行修饰。
线程类
package com.hhxy.demo015;
public class VolatileUseThread implements Runnable {
// 定义标志变量
private volatile boolean flag = false ;
@Override
public void run() {
while(!flag) {
System.out.println("传智播客中国IT教育的标杆....");
}
}
// 关闭线程
public void shutdown() {
this.flag = true ;
}
}
测试类
public class VolatileUseThreadDemo01 {
public static void main(String[] args) throws InterruptedException {
// 创建线程任务类对象
VolatileUseThread volatileUseThread = new VolatileUseThread() ;
// 创建线程对象
Thread thread = new Thread(volatileUseThread);
// 启动线程
thread.start();
// 主线程休眠
TimeUnit.SECONDS.sleep(5);
// 关闭线程
volatileUseThread.shutdown();
}
}
观察控制台输出,volatileUseThread线程执行5秒以后程序结束。
volatile的另一种简单使用场景是:定期"发布"观察结果供程序内部使用。例如,假设有一种环境传感器能够感觉环境温度。一个后台线程可能会每隔几秒读取一次该传感器数据,并更新包
含这个volatile变量的值。然后,其他线程可以读取这个变量,从而随时能够看到最新的温度值。这种使用就是多个线程操作共享变量,但是是有一个线程对其进行写操作,其他的线程都是读。
我们可以设计一个程序,模拟上面的温度传感器案例。
实现步说明
定义一个温度传感器(TemperatureSensor)的类,在该类中定义两个成员变量(temperature(温度值),type(传感器的类型)),temperature变量需要被volatile修饰
定义一个读取温度传感器的线程的任务类(ReadTemperatureRunnable),该类需要定义一个TemperatureSensor类型的成员变量(该线程需要读取温度传感器的数据)
定义一个定时采集温度的线程任务类(GatherTemperatureRunnable),该类需要定义一个TemperatureSensor类型的成员变量(该线程需要将读到的温度设置给传感器)
创建测试类(TemperatureSensorDemo)
TemperatureSensor类
public class TemperatureSensor { // 温度传感器类
private volatile int temperature ; // 温度值
private String type ; // 传感器的类型
public int getTemperature() {
return temperature;
}
public void setTemperature(int temperature) {
this.temperature = temperature;
}
public String getType() {
return type;
}
public void setType(String type) {
this.type = type;
}
}
ReadTemperatureRunnable类
public class ReadTemperatureRunnable implements Runnable {
// 温度传感器
private TemperatureSensor temperatureSensor ;
public ReadTemperatureRunnable(TemperatureSensor temperatureSensor) {
this.temperatureSensor = temperatureSensor ;
}
@Override
public void run() {
// 不断的读取温度传感器中的数据
while(true) {
// 读取数据
System.out.println(Thread.currentThread().getName() + "---读取到的温度数据为------>>> " + temperatureSensor.getTemperature());
try {
// 让线程休眠100毫秒,便于观察
TimeUnit.MILLISECONDS.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
GatherTemperatureRunnable类
public class GatherTemperatureRunnable implements Runnable {
// 温度传感器
private TemperatureSensor temperatureSensor ;
public GatherTemperatureRunnable(TemperatureSensor temperatureSensor) {
this.temperatureSensor = temperatureSensor ;
}
@Override
public void run() {
// 定义一个变量,表示环境初始温度
int temperature = 23 ;
// 不断进行数据采集
while(true) {
// 将采集到的数据设置给温度传感器
System.out.println(Thread.currentThread().getName() + "-----采集到的数据为----->>> " + temperature);
temperatureSensor.setTemperature(temperature);
try {
// 线程休眠2秒,模拟每隔两秒采集一次数据
TimeUnit.SECONDS.sleep(2);
} catch (InterruptedException e) {
e.printStackTrace();
}
// 环境温度改变
temperature += 2 ;
}
}
}
测试类
public class TemperatureSensorDemo {
public static void main(String[] args) {
// 创建TemperatureSensor对象
TemperatureSensor temperatureSensor = new TemperatureSensor();
// 创建ReadTemperatureRunnable类对象
ReadTemperatureRunnable readTemperatureRunnable = new ReadTemperatureRunnable(temperatureSensor) ;
// 创建GatherTemperatureRunnable类对象
GatherTemperatureRunnable gatherTemperatureRunnable = new GatherTemperatureRunnable(temperatureSensor) ;
// 创建2个Thread对象,并启动; 这两个线程负责读取TemperatureSensor中的温度数据
for(int x = 0 ; x < 2 ; x++) {
new Thread(readTemperatureRunnable).start();
}
// 创建1个Thread对象,并启动,这个线程负责读取定时采集数据中的温度数据
Thread gatherThread = new Thread(gatherTemperatureRunnable);
gatherThread.setName("温度采集线程");
gatherThread.start();
}
}
控制台输出结果
通过控制台的输出,我们可以看到当温度采集线程刚采集到环境温度以后,那么此时两个温度读取线程就可以立即感知到环境温度的变化。
什么是原子性?
所谓的原子性是指在一次操作或者多次操作中,要么所有的操作全部都得到了执行并且不会受到任何因素的干扰而中断,要么所有的操作都不执行,多个操作是一个不可以分割的整体。
分析如下程序的执行结果
线程类
public class VolatileAtomicThread implements Runnable {
// 定义一个int类型的变量
private int count = 0 ;
@Override
public void run() {
// 对该变量进行++操作,100次
for(int x = 0 ; x < 100 ; x++) {
count++ ;
System.out.println("冰淇淋的个数 =========>>>> " + count);
}
}
}
测试类
public class VolatileAtomicThreadDemo {
public static void main(String[] args) {
// 创建VolatileAtomicThread对象
VolatileAtomicThread volatileAtomicThread = new VolatileAtomicThread() ;
// 开启100个线程对count进行++操作
for(int x = 0 ; x < 100 ; x++) {
new Thread(volatileAtomicThread).start();
}
}
}
程序分析:我们在主线程中通过for循环启动了100个线程,每一个线程都会对VolatileAtomicThread类中的count加100次。那么直接结果应该是10000。但是真正的执行结果和我们分析
的是否一样呢?运行程序(多运行几次),查看控制台输出结果
备注:这个存在随机性,如果运气好还是可以出现10000,多运行几次就会出现上面的结果
通过控制台的输出,我们可以看到最终count的结果可能并不是10000。接下来我们就来分析一下问题产生的原因。
以上问题主要是发生在count++操作上:
count++操作包含3个步骤:
count++操作不是一个原子性操作,也就是说在某一个时刻对某一个操作的执行,有可能被其他的线程打断。
产生问题的执行流程分析:
从上面我们可以看到,虽然线程A和线程B都执行了count+1操作,但是count值增加了一次,这就是多线程操作中存在的一个问题
要保障操作原子性,最简单的方式就算加一个互斥锁synchronized
,这种方式最为简单,但是需要注意加锁的范围,我们要锁的是对于共享变量操作的那一块,将他变成一个临界区,确保每一次只有一个线程访问
public class VolatileAtomicThread implements Runnable {
// 定义一个int类型的变量
private int count = 0 ;
// 定义一个Object类型的变量,该变量将作为同步代码块的锁
private Object obj = new Object();
@Override
public void run() {
// 对该变量进行++操作,100次
for(int x = 0 ; x < 100 ; x++) {
synchronized (obj){
count++ ;
System.out.println("冰淇淋的个数 =========>>>> " + count);
}
}
}
}
详情可以参考这篇博客:java原子类详解_yetaoii的博客-CSDN博客
我们直接将count变量的数据类型变为AtomicInteger
,从而保障操作的原子性
public class VolatileAtomicThread implements Runnable {
// 定义一个int类型的变量
private AtomicInteger atomicInteger = new AtomicInteger() ;
@Override
public void run() {
// 对该变量进行++操作,100次
for(int x = 0 ; x < 100 ; x++) {
synchronized (obj){
int i = atomicInteger.incrementAndGet();
System.out.println("冰淇淋的个数 =========>>>> " + i);
}
}
}
}
AtomicInteger底层通过自旋锁 + CAS算法来确保操作的原子性
具体流程如下:
上诉过程中线程1更新前的比较的过程就是CAS机制,更新失败重新更新的过程就是自选机制
一般用于替代HashMap在多线程场景下适用
这是一个线程计数器,用于阻塞主线程,一般是用于主线程等待所有子线程执行完毕才继续往下执行
CyclicBarrier(循环栅栏)和CountDownLatch(线程计数器)功能类似,也是用于协调多个线程的运行,但是使用上有一定的区别
Semaphore(信号量)用于线程间的同步和协调,它的作用是控制访问特定资源的线程数目。
Exchanger(线程交换器),用于线程间数据的交换