Java Spring多线程
开启一个线程
https://blog.csdn.net/qq_44715943/article/details/116714584
1 继承java.lang.Thread类
- 编写一个类,直接 继承 java.lang.Thread,重写 run方法,run方法为这个线程要做的事情。
- 创建线程对象, new继承线程的类。
- 启动线程,调用线程对象的 start() 方法。
public class ThreadTest02 {
public static void main(String[] args) {
MyThread t = new MyThread();
// 启动线程
//t.run(); // 不会启动线程,不会分配新的分支栈。(这种方式就是单线程。)
t.start();
// 这里的代码还是运行在主线程中。
for(int i = 0; i < 1000; i++){
System.out.println("主线程--->" + i);
}
}
}
class MyThread extends Thread {
@Override
public void run() {
// 编写程序,这段程序运行在分支线程中(分支栈)。
for(int i = 0; i < 1000; i++){
System.out.println("分支线程--->" + i);
}
}
}
注意:
t.run()
不会启动线程,只是普通的调用方法而已。不会分配新的分支栈。(这种方式就是单线程。)t.start()
方法的作用是:启动一个分支线程,在JVM中开辟一个新的栈空间,这段代码的任务只是为了开启一个新的栈空间,只要新的栈空间开出来,start()方法就结束了。线程就启动成功了。启动成功的线程会自动调用run方法。 run方法在分支栈的栈底部(压栈)main方法在主栈的栈底部,run和main是平级的。
2 实现java.lang.Runnable接口
- 编写一个类,实现 java.lang.Runnable 接口,实现run方法,run方法为这个线程要做的事情(同1)。
- 创建线程对象,new线程类传入可运行的类/接口。
- 启动线程呢,调用线程对象的 start() 方法(同1)。
public class ThreadTest03 {
public static void main(String[] args) {
Thread t = new Thread(new MyRunnable());
// 启动线程
t.start();
for(int i = 0; i < 100; i++){
System.out.println("主线程--->" + i);
}
}
}
// 这并不是一个线程类,是一个可运行的类。它还不是一个线程。
class MyRunnable implements Runnable {
@Override
public void run() {
for(int i = 0; i < 100; i++){
System.out.println("分支线程--->" + i);
}
}
}
优势:
- 实现的方式没有类的单继承性的局限性:一个类实现了接口,它还可以去继承其它的类,更灵活。
- 实现的方式更适合来处理多个线程有共享数据的情况
3 实现Callable接口
- 创建一个实现Callable的实现类
- 实现call方法,将此线程需要执行的操作声明在call()中
- 创建Callable接口实现类的对象
- 将此Callable接口实现类的对象作为传递到FutureTask构造器中,创建FutureTask的对象
- 将FutureTask的对象作为参数传递到Thread类的构造器中,创建Thread对象,并调用start()
- 获取Callable中call方法的返回值
import java.util.concurrent.Callable;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.FutureTask;
//1.创建一个实现Callable的实现类
class NumThread implements Callable {
//2.实现call方法,将此线程需要执行的操作声明在call()中
@Override
public Object call() throws Exception {
int sum = 0;
//把100以内的偶数相加
for (int i = 1; i <= 100; i++) {
if (i % 2 == 0) {
System.out.println(i);
sum += i;
}
}
return sum;
}
}
public class ThreadNew {
public static void main(String[] args) {
//3.创建Callable接口实现类的对象
NumThread numThread = new NumThread();
//4.将此Callable接口实现类的对象作为传递到FutureTask构造器中,创建FutureTask的对象
FutureTask futureTask = new FutureTask(numThread);
//5.将FutureTask的对象作为参数传递到Thread类的构造器中,创建Thread对象,并调用start()
new Thread(futureTask).start();
try {
//6.获取Callable中call方法的返回值
//get()返回值即为FutureTask构造器参数Callable实现类重写的call()的返回值。
Object sum = futureTask.get();
System.out.println("总和为:" + sum);
} catch (InterruptedException e) {
e.printStackTrace();
} catch (ExecutionException e) {
e.printStackTrace();
}
}
}
优势:
- call()可以有返回值的
- call()可以抛出异常,被外面的操作捕获,获取异常的信息
- Callable是支持泛型的
4 实现线程池ThreadPoolExecutor
- 创建好实现了Runnable接口的类或实现Callable的实现类
- 实现run或call方法
- 创建线程池
- 调用线程池的execute方法执行某个线程,参数是之前实现Runnable或Callable接口的对象
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.ThreadPoolExecutor;
class NumberThread implements Runnable {
@Override
public void run() {
//遍历100以内的偶数
for (int i = 0; i <= 100; i++) {
if (i % 2 == 0) {
System.out.println(Thread.currentThread().getName() + ": " + i);
}
}
}
}
class NumberThread1 implements Runnable {
@Override
public void run() {
//遍历100以内的奇数
for (int i = 0; i <= 100; i++) {
if (i % 2 != 0) {
System.out.println(Thread.currentThread().getName() + ": " + i);
}
}
}
}
public class ThreadPool {
public static void main(String[] args) {
//1. 提供指定线程数量的线程池
ExecutorService service = Executors.newFixedThreadPool(10);
ThreadPoolExecutor service1 = (ThreadPoolExecutor) service;
//自定义线程池的属性
// service1.setCorePoolSize(15);
// service1.setKeepAliveTime();
//2. 执行指定的线程的操作。需要提供实现Runnable接口或Callable接口实现类的对象
service.execute(new NumberThread());//适用于Runnable
service.execute(new NumberThread1());//适用于Runnable
// service.submit(Callable callable);//适合使用于Callable
//3. 关闭连接池
service.shutdown();
}
}
操作线程
线程的状态
在Java程序中,一个线程对象只能调用一次start()方法启动新线程,并在新线程中执行run()方法。一旦run()方法执行完毕,线程就结束了。因此,Java线程的状态有以下几种:
New:新创建的线程,尚未执行;
Runnable:运行中的线程,正在执行run()方法的Java代码;
Blocked:运行中的线程,因为某些操作被阻塞而挂起;
Waiting:运行中的线程,因为某些操作在等待中;
Timed Waiting:运行中的线程,因为执行sleep()方法正在计时等待;
Terminated:线程已终止,因为run()方法执行完毕。
1 等待线程 join()
一个线程还可以等待另一个线程直到其运行结束。例如,main线程在启动t线程后,可以通过t.join()
等待t线程结束后再继续运行:
public class Main {
public static void main(String[] args) throws InterruptedException {
Thread t = new Thread(() -> {
System.out.println("hello");
});
System.out.println("start");
t.start();
t.join();
System.out.println("end");
}
}
如果t线程已经结束,对实例t调用join()
会立刻返回。此外,join(long)
的重载方法也可以指定一个等待时间,超过等待时间后就不再继续等待。
2 中断线程 interrupt()
中断一个线程非常简单,只需要在其他线程中对目标线程调用interrupt()方法,目标线程需要反复检测自身状态是否是interrupted状态,如果是,就立刻结束运行。
public class Main {
public static void main(String[] args) throws InterruptedException {
Thread t = new MyThread();
t.start();
Thread.sleep(1); // 暂停1毫秒
t.interrupt(); // 中断t线程
t.join(); // 等待t线程结束
System.out.println("end");
}
}
class MyThread extends Thread {
public void run() {
int n = 0;
while (! isInterrupted()) {
n ++;
System.out.println(n + " hello!");
}
}
}
main线程通过调用t.interrupt()方法中断t线程,但是要注意,interrupt()方法仅仅向t线程发出了“中断请求”,至于t线程是否能立刻响应,要看具体代码。而t线程的while循环会检测isInterrupted(),所以上述代码能正确响应interrupt()请求,使得自身立刻结束运行run()方法。
3 守护线程(Daemon Thread)
如果有一个线程没有退出,JVM进程就不会退出。但是有一种线程的目的就是无限循环,例如,一个定时触发任务的线程。然而这类线程经常没有负责人来负责结束它们。但是,当其他线程结束时,JVM进程又必须要结束,怎么办?
守护线程是指为其他线程服务的线程。在JVM中,所有非守护线程都执行完毕后,无论有没有守护线程,虚拟机都会自动退出。
因此,JVM退出时,不必关心守护线程是否已结束。
Thread t = new MyThread();
t.setDaemon(true);
t.start();
注意:守护线程不能持有任何需要关闭的资源,例如打开文件等,因为虚拟机退出时,守护线程没有任何机会来关闭文件,这会导致数据丢失。
4 线程同步 synchronized
两个线程同时对一个int变量进行操作,一个加10000次,一个减10000次,最后结果应该是0,但是,每次运行,结果实际上都是不一样的。
这是因为对变量进行读取和写入时,结果要正确,必须保证是原子操作。原子操作是指不能被中断的一个或一系列操作。
n = n + 1;
看上去是一行语句,实际上对应了3条指令:
ILOAD
IADD
ISTORE
JVM规范定义了几种原子操作:
-基本类型赋值,例如:int n = m;
-引用类型赋值,例如:List list = anotherList。
这说明多线程模型下,要保证逻辑正确,对共享变量进行读写时,必须保证一组指令以原子方式执行:即某一个线程执行时,其他线程必须等待:
通过加锁和解锁的操作,就能保证3条指令总是在一个线程执行期间,不会有其他线程会进入此指令区间。即使在执行期线程被操作系统中断执行,其他线程也会因为无法获得锁导致无法进入此指令区间。只有执行线程将锁释放后,其他线程才有机会获得锁并执行。这种加锁和解锁之间的代码块我们称之为临界区
(Critical Section),任何时候临界区最多只有一个线程能执行。
一段代码的原子性就是通过加锁和解锁实现的。Java程序使用synchronized
关键字对一个对象进行加锁:
public class Main {
public static void main(String[] args) throws Exception {
var add = new AddThread();
var dec = new DecThread();
add.start();
dec.start();
add.join();
dec.join();
System.out.println(Counter.count);
}
}
class Counter {
public static final Object lock = new Object(); 2. 共享实例作为锁
public static int count = 0; 1. 共享修改变量
}
class AddThread extends Thread {
public void run() {
for (int i=0; i<10000; i++) {
synchronized(Counter.lock) { // 3. 获取锁
Counter.count += 1;
} // 无论有无异常,都会在此释放锁
}
}
}
class DecThread extends Thread {
public void run() {
for (int i=0; i<10000; i++) {
synchronized(Counter.lock) { // 3. 获取锁
Counter.count -= 1;
} // 无论有无异常,都会在此释放锁
}
}
}
如何使用synchronized:
- 找出修改共享变量的线程代码块;
- 选择一个共享实例作为锁,使用同一个实例作为锁;可以锁住的对象是this,即当前实例,这又使得创建多个Counter实例的时候,它们之间互不影响,可以并发执行
- 使用synchronized(lockObject) { … }。
在使用synchronized的时候,不必担心抛出异常。因为无论是否有异常,都会在synchronized结束处正确释放锁。
Java线程池
如果我们不采用线程池,为每一个请求都创建一个线程的话:
- 管理线程的生命周期开销非常高。管理这些线程的生命周期会明显增加 CPU 的执行时间,会消耗大量计算资源。
- 线程间上下文切换造成大量资源浪费。
- 降低系统资源消耗,通过重用已存在的线程,降低线程创建和销毁造成的消耗
- 程序稳定性会受到影响。我们知道,创建线程的数量存在一个限制,这个限制将随着平台的不同而不同,并且受多个因素制约,包括jvm的启动参数、Thread构造函数中请求的栈大小,以及底层操作的限制等。如果超过了这个限制,那么很可能抛出OutOfMemoryError异常,这对于运行中的应用来说是非常危险的。
Java自JDK1.5 提供了自己的多线程框架,称为 Executor
框架.
- Executor:java线程池框架的最上层父接口,在Executor这个接口中只有一个execute方法,该方法的作用是向线程池提交任务并执行。
- ThreadPoolExecutor:这是Java线程池最核心的一个类,该类继承自AbstractExecutorService,主要功能是创建线程池,给任务分配线程资源,执行任务。
- ScheduledExecutorSerivce 和 ScheduledThreadPoolExecutor 提供了另一种线程池:
延迟执行和周期性执行
的线程池。 - Executors:这是一个静态工厂类,该类定义了一系列静态工厂方法,通过这些工厂方法可以
返回各种不同的线程池
。 - ExecutorService:该接口继承自Executor接口,添加了shutdown、shutdownAll、submit、invokeAll等一系列对线程的操作方法,该接口比较重要,在使用线程池框架的时候,经常用到该接口。
- AbstractExecutorService:这是一个抽象类,实现ExecuotrService接口
Executors 线程池类型
SingleThreadExecutor
: 此线程池 Executor 只有一个线程。它用于以顺序方式的形式执行任务。如果此线程在执行任务时因异常而挂掉,则会创建一个新线程来替换此线程,后续任务将在新线程中执行。
ExecutorService executorService = Executors.newSingleThreadExecutor()
FixedThreadPool(n)
: 它是一个拥有固定数量线程的线程池,创建一个核心线程和最大线程均为固定值的线程池。提交给 Executor 的任务由固定的 n 个线程执行,如果有更多的任务,它们存储在无长度限制的等待队列 LinkedBlockingQueue 里。这个数字 n 通常跟底层处理器支持的线程总数有关。
ExecutorService executorService = Executors.newFixedThreadPool(4);
如果是CPU密集型任务,就需要尽量压榨CPU,参考值可以设为 NCPU+1 如果是IO密集型任务,参考值可以设置为2*NCPU
注意⚠️:
由于FixedThreadPool采用无界的等待队列,一旦空闲线程被用尽,就会向队列中加入任务,这时一旦任务进入速度远高于线程处理能力,就有出现 OOM 的可能。
-
CachedThreadPool
: 该线程池主要用于执行大量短期并行任务、响应时间要求高、数据量可控的场景。与固定线程池不同,此线程池的线程数不受限制。如果所有的线程都在忙于执行任务并且又有新的任务到来了,这个线程池将创建一个新的线程并将其提交到 Executor。只要其中一个线程变为空闲,它就会执行新的任务。 如果一个线程有 60 秒的时间都是空闲的,它们将被结束生命周期并从缓存中删除。创建一个核心线程数为0、最大线程数为整型最大值的线程池。注意⚠️:
但是,如果管理得不合理,或者任务不是很短的,则线程池将包含大量的活动线程。这可能导致资源紊乱并因此导致性能下降。由于其不限制创建线程的个数,故若数据量不可控,会造成程序 OOM
ExecutorService executorService = Executors.newCachedThreadPool();
ScheduledExecutor
:当我们有一个需要定期运行的任务或者我们希望延迟某个任务时,就会使用此类型的 executor。
ScheduledExecutorService scheduledExecService = Executors.newScheduledThreadPool(1);
scheduledExecService.scheduleAtFixedRate(Runnable command, long initialDelay, long period, TimeUnit unit)
scheduledExecService.scheduleWithFixedDelay(Runnable command, long initialDelay, long period, TimeUnit unit)
Future与线程池
由于提交给Executor 的任务是异步的,需要有一个对象来接收Executor 的处理结果:
Future<String> result = executorService.submit(callableTask);
调用者可以继续执行主程序,当需要提交任务的结果时,他可以在这个 Future对象上调用.get()
方法来获取。如果任务完成,结果将立即返回给调用者,否则调用者将被阻塞,直到 Executor 完成此操作的执行并计算出结果。
如果调用者不能无限期地等待任务执行的结果,那么这个等待时间也可以设置为定时地。可以通过 Future.get(long timeout,TimeUnit unit)
方法实现,如果在规定的时间范围内没有返回结果,则抛出 TimeoutException。调用者可以处理此异常并继续执行该程序。
如果在执行任务时出现异常,则对 get 方法的调用将抛出一个ExecutionException。对于 Future.get()方法返回的结果,一个重要的事情是,只有提交的任务实现了java.util.concurrent.Callable接口时才返回 Future。如果任务实现了Runnable接口,那么一旦任务完成,对 .get() 方法的调用将返回 null。
另一点是 Future.cancel(boolean mayInterruptIfRunning) 方法。此方法用于取消已提交任务的执行。如果任务已在执行,则 Executor 将尝试在mayInterruptIfRunning 标志为 true 时中断任务执行。
创建Callable任务:
public class Task implements Callable<String> {
private String message;
public Task(String message) {
this.message = message;
}
@Override
public String call() throws Exception {
return "Hello " + message + "!";
}
}
异步执行任务:
public class ExecutorExample {
public static void main(String[] args) {
// 1. 实例化了 Task 类
Task task = new Task("World");
// 2. 创建了一个具有4个线程数的 FixedThreadPool Executors
// 3. 提交给 Executors 执行
ExecutorService executorService = Executors.newFixedThreadPool(4);
// 4. 结果由 Future 对象返回
Future<String> result = executorService.submit(task);
try {
System.out.println(result.get());
} catch (InterruptedException | ExecutionException e) {
System.out.println("Error occured while executing the submitted task");
e.printStackTrace();
}
// 调用 executorService 对象上的 shutdown 来终止所有线程并将资源返回给 OS
executorService.shutdown();
}
}
shutdown()
方法等待 Executor 完成当前提交的任务。 但是,如果要求是立即关闭 Executor 而不等待,那么我们可以使用 shutdownNow()
方法。
或者创建Runnable任务:
public class Task implements Runnable{
private String message;
public Task(String message) {
this.message = message;
}
public void run() {
// 由于Runnable任务没有返回值,不能得到任务执行结果。需要在任务内完成所有操作(打印)
System.out.println("Hello " + message + "!");
}
}
线程间通信
1 公共变量
用一个running标志位来标识线程是否应该继续运行,在外部线程中,通过把HelloThread.running置为false,就可以让线程结束
public class Main {
public static void main(String[] args) throws InterruptedException {
HelloThread t = new HelloThread();
t.start();
Thread.sleep(1);
t.running = false; // 标志位置为false
}
}
class HelloThread extends Thread {
public volatile boolean running = true;
public void run() {
int n = 0;
while (running) {
n ++;
System.out.println(n + " hello!");
}
System.out.println("end!");
}
}
volatile
为什么要对线程间共享的变量用关键字volatile
声明?这涉及到Java的内存模型。在Java虚拟机中,变量的值保存在主内存中,但是,当线程访问变量时,它会先获取一个副本,并保存在自己的工作内存中。如果线程修改了变量的值,虚拟机会在某个时刻把修改后的值回写到主内存,但是,这个时间是不确定的!
这会导致如果一个线程更新了某个变量,另一个线程读取的值可能还是更新前的。例如,主内存的变量a = true,线程1执行a = false时,它在此刻仅仅是把变量a的副本变成了false,主内存的变量a还是true,在JVM把修改后的a回写到主内存之前,其他线程读取到的a的值仍然是true,这就造成了多线程之间共享的变量不一致。
因此,volatile关键字的目的是告诉虚拟机:
- 每次访问变量时,总是获取主内存的最新值;
- 每次修改变量后,立刻回写到主内存。
volatile关键字解决的是可见性问题:当一个线程修改了某个共享变量的值,其他线程能够立刻看到修改后的值。