多线程
为什么要使用多线程
- 异步执行
- 利用多
CPU
资源实现真正意义上的并行执行
多线程的本质是合理的利用多核心CPU
资源来实现线程的并行处理,来实现同一个进程内的多个任务的并行执行,同时基于线程本身的异步执行特性,提升任务处理效率。
java
中使用多线程的方式
继承Thread
类
package com.example.demo;
public class ThreadDemo extends Thread {
@Override
public void run() {
System.out.println("Current thread: " + Thread.currentThread().getName());
}
public static void main(String[] args) {
new ThreadDemo().start();
}
}
实现Runnable
接口
package com.example.demo;
public class RunnableDemo implements Runnable{
@Override
public void run() {
System.out.println("Current thread: " + Thread.currentThread().getName());
}
public static void main(String[] args) {
new Thread(new RunnableDemo()).start();
}
}
实现Callable
接口
package com.example.demo;
import java.util.concurrent.*;
public class CallableDemo implements Callable<String> {
@Override
public String call() throws Exception {
System.out.println("Current thread: " + Thread.currentThread().getName());
return "Hello Thread";
}
public static void main(String[] args) throws ExecutionException, InterruptedException {
ExecutorService executorService = Executors.newFixedThreadPool(1);
Future<String> submit = executorService.submit(new CallableDemo());
// submit.get是一个阻塞方法
System.out.println(Thread.currentThread().getName()+"-"+submit.get());
}
}
线程的生命周期
java
线程从创建到销毁,一共经历6中状态
-
NEW
:初始状态,线程被构建,但还没有调用start
方法 -
RUNNABLED
:运行状态,java
线程把操作系统中的就绪和运行两种状态统一称为运行中
,start之后会等到系统调度就是就绪状态,并不是立刻开始运行。 -
BLOCKED
:阻塞状态,表示线程进入等待状态,也就是线程因为某种原因放弃了CPU
使用权,阻塞也分为几种情况 -
WAITING
:等待状态 -
TIME_WAITING
:超时等待状态,超时以后自动返回 TERMINATED
: 终止状态,表示当前线程执行完毕
Thread.join
的使用及原理
Thread.join
的作用就是保证线程执行结果的可见性。
package com.example.demo;
public class ThreadJoinDemo {
private static Integer x = 0;
private static Integer i = 0;
public static void main(String[] args) throws InterruptedException {
Thread t1 = new Thread(() -> {
i = 1;
x = 2;
}, "t1");
Thread t2 = new Thread(() -> {
i = x + 2;
}, "t2");
t1.start();
// t1 线程的执行结果对于 t2 是可见的,join 使得 t1 线程比 t2 线程先运行并使主线程处于阻塞状态
t1.join();
t2.start();
Thread.sleep(1000);
System.out.println("result x=" + x);
System.out.println("result i=" + i);
}
}
join
是通过wait
来实现阻塞,并在线程执行终止的时候,来唤醒所有被阻塞的线程。
/**
* Waits at most {@code millis} milliseconds for this thread to
* die. A timeout of {@code 0} means to wait forever.
*
* <p> This implementation uses a loop of {@code this.wait} calls
* conditioned on {@code this.isAlive}. As a thread terminates the
* {@code this.notifyAll} method is invoked. It is recommended that
* applications not use {@code wait}, {@code notify}, or
* {@code notifyAll} on {@code Thread} instances.
*
* @param millis
* the time to wait in milliseconds
*
* @throws IllegalArgumentException
* if the value of {@code millis} is negative
*
* @throws InterruptedException
* if any thread has interrupted the current thread. The
* <i>interrupted status</i> of the current thread is
* cleared when this exception is thrown.
*/
// 最多等待几毫秒让该线程终止。millis为 0 意味着永远等待
public final synchronized void join(long millis) throws InterruptedException {
long base = System.currentTimeMillis();
long now = 0;
if (millis < 0) {
throw new IllegalArgumentException("timeout value is negative");
}
if (millis == 0) {
// 查看当前线程是否存活,如果存活则继续等待,当线程终止时,将调用 this.notifyAll 方法
while (isAlive()) {
wait(0);
}
} else {
while (isAlive()) {
long delay = millis - now;
// 如果超时,跳出循环,结束当前线程
if (delay <= 0) {
break;
}
// 否则就继续等待
wait(delay);
now = System.currentTimeMillis() - base;
}
}
}
Thread.sleep
的作用
使线程暂停执行一段时间,直到等待的时间结束才恢复执行或在这段时间内被中断
package com.example.demo;
public class TreadSleepDemo {
public static void main(String[] args) {
new Thread(()->{
try {
System.out.println("begin: "+System.currentTimeMillis());
Thread.sleep(2000);
System.out.println("end: "+System.currentTimeMillis());
} catch (InterruptedException e) {
e.printStackTrace();
}
}).start();
}
}
工作流程
- 挂起线程并修改其运行状态
- 使用
sleep
提供的参数来设置一个定时器 - 当时间结束的时候,定时器会触发,内核收到中断后,修改线程的运行状态,例如:线程被标志位就绪,进入就绪队列等待调度。
问题思考
- 假设现在是
2022-04-01 12:00:00.000
,如果我调用一下Thread.Sleep(1000)
,在2022-04-01 12:00:01.000
的时候,这个线程会不会被唤醒?Thread.Sleep(1000)
表示当前线程不参与接下来一秒内的 CPU 竞争,但也并不代表 1s 过后,当前线程就会获得执行,可能 CPU正在忙,也可能有优先级更高的其它线程,所以当前线程并不一定能够背准时唤醒。 Thread.Sleep(0)
的意义
在未来的 0 毫秒不参与 cpu 的竞争,让渡出 cpu 的使用权,这会触发 cpu 使用权的重新分配,在这个重新分配的过程中,当前线程也有可能重新获取 cpu 的使用权限。
线程的调度算法
操作系统中,CPU竞争有很多种策略。Unix系统使用的是时间片算法,而Windows则属于抢占式的。
wait
和notify
的使用
如何实现一个线程修改了一个对象的值,而另一个线程感知到了变化,然后进行响应的操作。
通过双线程互相唤醒,来实现一个生产者消费者模式。
生产者
package com.example.demo.mode;
import java.util.Queue;
public class Producer implements Runnable {
private Queue<String> bags;
private Integer size;
public Producer(Queue<String> queue, Integer size) {
this.bags = queue;
this.size = size;
}
@Override
public void run() {
int i=0;
while (true){
i++;
synchronized (bags){
while (bags.size()==size){
System.out.println("bags 已经满了");
// 阻塞
try {
bags.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("生产出-bag"+i);
bags.add("bag"+i);
// 唤醒处于阻塞状态下的消费者
bags.notifyAll();
}
}
}
}
消费者
package com.example.demo.mode;
import java.util.Queue;
public class Consumer implements Runnable {
private Queue<String> bags;
private Integer size;
public Consumer(Queue<String> queue, Integer size) {
this.bags = queue;
this.size = size;
}
@Override
public void run() {
int i=0;
while (true){
i++;
synchronized (bags){
if(bags.isEmpty()){
System.out.println("bags 已经空了");
// 阻塞等待
try {
bags.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
String bag = bags.remove();
System.out.println("消费者消费了-"+bag);
// 唤醒处于阻塞状态下的生产者
bags.notifyAll();
}
}
}
}
测试代码。
package com.example.demo.mode;
import java.util.LinkedList;
import java.util.Queue;
public class WaitNotifyDemo {
public static void main(String[] args) {
Queue<String> queue = new LinkedList<>();
int size = 10;
new Thread(new Producer(queue,size)).start();
new Thread(new Consumer(queue,size)).start();
}
}
思考,为什么要使用synchronized
?
- 从上面的生产者消费者的案例来看,
wait
和notify
本质上其实是一种条件竞争,至少来说,wait
和notify
方法一定是互斥存在的,既然要实现互斥,那么synchronized
就是一个很好的解决方法 -
wait
和notify
是用于实现多个线程之间通信的,而通信比如会存在一个通信载体。那么synchronized
就是两者实现通信的载体,即两者必须要在同一个锁的范围内。
如何正确的终止一个线程
thread.stop() 已经是废弃的方法,不再推荐使用,stop 的方法类似于 bash 中的
kill -9
,比较粗暴,会直接中断正在执行中的业务。
终止一个线程,唯一的方法就是让 run
方法执行结束。
package com.example.demo;
public class StopDemo {
public static void main(String[] args) {
new Thread(()->{
// 正常情况下,这个死循环是不会被终止的
while (true){
System.out.println("持续运行");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}).start();
}
}
正常情况下,这个死循环是不会被终止的,我们可以考虑通过一个信号量来控制什么时候停止while
循环。代码如下:
package com.example.demo;
import java.util.concurrent.TimeUnit;
public class StopDemo {
// 声明一个共享变量,用来控制 while 循环终止的时机
static volatile boolean stop = false;
public static void main(String[] args) throws InterruptedException {
new Thread(()->{
while (!stop){
System.out.println("持续运行");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}).start();
TimeUnit.SECONDS.sleep(2);
stop=true;
}
}
interrupt 方法
当其他线程通过调用当前线程的 interrupt
方法,表示向当前线程打个招呼,告诉他可以中断线程的执行了,至于什么时候中断,取决于当前线程自己。
package com.example.demo;
import java.util.concurrent.TimeUnit;
public class StopDemo {
public static void main(String[] args) throws InterruptedException {
Thread t1 = new Thread(() -> {
// 判断中断标识
while (!Thread.currentThread().isInterrupted()) {
System.out.println("持续运行");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
});
t1.start();
TimeUnit.SECONDS.sleep(2);
t1.interrupt(); // 通知中断 (友好)
}
}
这个原理和上面通过信号来终止线程的方式是一样的,在 jvm
中维护了一个interrupt
的变量,该变量可以通过 Thread.currentThread().isInterrupted()
来获取(默认值 false
),并可以通过 interrupt
方法来改变 interrupt
的值。
interrupt
可以中断正在运行 while
循环或者阻塞状态下的线程。如果线程处在阻塞(Thread.sleep 、wait、join等)状态下,interrupt
会唤醒线程并抛出InterruptedException
异常,来终止 run 方法的执行。