线程的创建、中断及通信
本文已被收录至 Github: github.com/JavaLiuTong…. 想要获取更多的干货文章,关注公众号:不会说话的刘同学
线程的三种创建方式
继承Thread
我们在代码中想要简单的启动一个线程,最熟悉的方式就是继承 Thread ,这个类是JDK提供的一个线程类,这个类实现了 Runnable 接口,我们可以来看看 Thread 比较常用的几个方法
首先是其构造方法
构造方法我们只需要重点关注前面两个就可以了,一个是无参构造,一个是传入 Runnable 的实现类
其他比较常用的方法:
public synchronized void start() : 启动线程
public void run():线程具体执行的方法,需要在继承的类中重写
public static native void sleep(long millis) :线程休眠,需要传入休眠的时间,该方法会暂停线程执行直到线程休眠时间结束
public static void sleep(long millis, int nanos):跟上一个方法一样,只不过多传了个时间机制
public void interrupt():给线程打上一个中断标识
public boolean isInterrupted():判断线程是否被打上中断标识
public final synchronized void setName(String name):给线程设置一个名称
public final void join():让主线程等待子线程执行结束
public static native void yield():表示当前线程愿意让出CPU使用权
方法具体的使用我这里就不一一讲解了,我们直接来看看代码的实现细节
示例代码:
// 自定义类继承 Thread
public class Thread01 extends Thread{
@Override
public void run() {
System.out.println("Hello World");
}
}
public static void main(String[] args) throws Exception {
Thread01 thread01 = new Thread01();
// 启动线程
thread01.start();
}
实现Runnable接口
Runnable 内部提供了一个 run 抽象方法,上面我也说过 Thread 实现了 Runnable 接口
因为 Runnable 没有给我们提供启动线程的方法,因此我们需要借助 Thread 类来启动
代码示例如下:
// 自定义类实现 Runnable 接口
public class Thread02 implements Runnable {
@Override
public void run() {
System.out.println("Hello World");
}
}
public static void main(String[] args) throws Exception {
Thread02 thread02 = new Thread02();
// 借助 Thread 类来启动
Thread thread = new Thread(thread02);
// 启动线程
thread.start();
}
实现Callable接口
上面的两种方式创建线程有一个缺点,但是无法获取到线程执行的结果,我们可以通过实现 Callable 接口从而获得线程执行的结果
同样 Callable 也只提供了一个方法,同时接口接收一个泛型作为返回的结果参数
Callable 也没有提供启动线程的方法,因此我们也需要借助 Thread 类来启动
但是我们可以看到 Thread 除了 Runnable 传参的构造方法之外,并没有提供 Callable 传参的构造方法,因此我们无法直接通过 Thread 来启动,需要一个媒介,这个媒介就是 FutureTask,其实细心点我们可以发现,Callable 和 FutureTask 都是 JUC 并发包下的
FutureTask 继承了 Runnable 接口,因此 FutureTask 本身就是 Runnable 的一个实现
FutureTask 也提供了两个构造方法,一个是 Callable 传参,一个是 Runnable传参
因此我们可以将 Callable 的实现包装成一个 FutureTask,然后再通过 Thread 来启动
示例代码:
// 自定义 Callable 实现
public class Thread03 implements Callable<String> {
@Override
public String call() throws Exception {
return "这是一个线程执行的结果.....";
}
}
public static void main(String[] args) throws Exception {
Thread03 thread03 = new Thread03();
// 包装成 FutureTask
FutureTask futureTask=new FutureTask(thread03);
// 借助 Thread 类启动
Thread thread = new Thread(futureTask);
thread.start();
// 获取执行的结果
System.out.println(futureTask.get());
}
线程的生命周期
就跟Spring的Bean一样,线程也有生命周期
Java的线程生命周期一共有六种状态:
NEW(初始化状态)
RUNNABLE(可运行/运行状态)
BLOCKED(阻塞状态)
WAITING(无时限等待)
TIMED_WAITING(有时限等待)
TERMINATED(终止状态)
其中 BLOCKED、WAITING、TIMED_WAITING 这三种状态我们可以归结为一种状态,即休眠状态,因为这三种状态只要是其中的一种,那么这个线程就会永远的没有CPU的使用权
我们再来看看线程状态之间是如何进行转换的
1、从 NEW 到 RUNNABLE 状态
Java 刚创建出来的 Thread 对象就是NEW状态,此时JVM为其分配内存,并初始化其成员变量的值,线程并不会执行线程执行体
当调用 start() 方法,就会从 NEW 转换到 RUNNABLE 状态
2、RUNNABLE 到 BLOCKED 状态
当线程调用 synchronized 修饰的方法或代码块的时候,其他的线程就只能等待,而等待的线程就会从 RUNNABLE 转换到 BLOCKED 状态
当等待的线程获得 synchronized 隐式锁时,又会从 BLOCKED 转换到 RUNNABLE 状态
3、RUNNABLE 到 WAITING 状态
有三种场景会触发这两个状态的转换
第一种场景,获取synchronized 隐式锁的线程,调用无参数的 Object.wait() 方法
第二种场景,调用无参数的 Thread.join() 方法,如果有个线程 ThreadA ,当调用 ThreadA.join() 的时候,执行这条语句的线程就会等待 ThreadA 执行完,而等待的过程中,状态就会从 RUNNABLE 转换到 WAITING,当 ThreadA 执行完的之后,等待的线程就会从 WAITING 转换到 RUNNABLE
第三种场景,调用 LockSupport.park() 方法,当线程调用这个方法的时候,状态就会从 RUNNABLE 状态转换到 WAITING ,调用 LockSupport.unpark() 可唤醒目标线程,目标线程状态又会从 WAITING 状态到 RUNNABLE 状态
4、RUNNABLE 到 TIMED_WAITING 状态
这种状态的转换有五种场景
第一种场景,调用带超时参数的 Thread.sleep(long millis) 方法
第二种场景,获得 synchronized 隐式锁的线程,调用带超时参数的 Object.wait(long timeout) 方法
第三种场景,调用带超时参数的 Thread.join(long millis) 方法
第四种场景,调用带超时参数的 LockSupport.parkNanos(Object blocker, long deadline) 方法
第五种场景,调用带超时参数的 LockSupport.parkUntil(long deadline) 方法
TIMED_WAITING 和 WAITING 状态的区别就在于多了个超时参数
5、 RUNNABLE 和 WAITING 状态的区别
线程执行完 run() 方法后,会自动转换到 TERMINATED 状态,当然如果执行 run() 方法的 时候异常抛出,也会导致线程终止
线程的中断策略
我们有时候可能会终止线程的运行,比如某些中间件,在正常关闭的时候可能需要终止某些正在运行的线程
Thread 提供了一个 stop() 方法,这个方法会直接终止线程,不给任何喘息的机会
但是这么暴力的终止线程也会存在一个问题,就是如果线程正在执行某些任务,恰好这个线程持有 ReentrantLock 锁 ,一旦我们通过暴力的形式终止线程,线程并不会自动调用 ReentrantLock 的 unlock() 去释放锁,那么这个锁将永远得不到释放,因此 JDK 官方是不建议使用 stop 方法来终止线程,另外这个方法也被标注为过时了
Thread 给我们提供了一个 interrupt() 方法, 这个方法相对来说就优雅的多了
当调用这个方法的之后,并不会终止线程,而是会给这个线程打上一个终止标记,标记这是个终止的线程,后续处理的时候,我们只需要调用 isInterrupted() 方法判断该线程是否是终止线程,如果是的话我们再做终止处理,同时我们也可以无视这个终止标记
除了调用 isInterrupted() 主动检测外,另外一种就是通过异常来检测
我们如果调用 wait()、join()、sleep() 方法都会抛出一个 InterruptedException 异常,这个异常的触发条件就是:其他线程调用了该线程的 interrupt() 方法
线程之间的通信
线程与线程之间不完全是互相独立的,也需要某种通信机制来达到互相协助的目的
Object 给我们提供了 wait()、notifyAll()、notify() 三个方法,线程调用 wait() 方法会进入到等待,调用 notifyAll()、notify() 会唤醒等待的线程
notifyAll()、notify() 区别在于 :
notify()唤醒正在等待此对象监视器的单个线程。 如果有多个线程在等待,则选择其中一个随机唤醒(由调度器决定),唤醒的线程享有公平竞争资源的权利
notifyAll()唤醒正在等待此对象监视器的所有线程,唤醒的所有线程公平竞争资源
我们来通过一个代码示例来具体说明下
public class Thread01 extends Thread{
private Object lock;
public Thread01(Object lock){
this.lock = lock;
}
@Override
public void run() {
synchronized(lock){
try {
// 调用 wait 方法,线程进入等待
lock.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(super.getName()+"被唤醒...");
}
}
}
public static void main(String[] args) throws Exception {
Object lock = new Object();
Thread01 thread01 = new Thread01(lock);
thread01.setName("线程1");
// 启动线程后,线程会被阻塞等待
thread01.start();
Thread.sleep(3000);
synchronized (lock){
// 唤醒正在等待的线程
lock.notify();
}
}
具体的细节我在注释里说明了,感兴趣的话,可以自己运行下这个代码
这种通信机制有缺点因此很少使用,JUC 并发包提供了 LockSupport 类,这个类提供了 part()和unpark()两个方法,分别对应线程的阻塞和唤醒,一般会使用这种方式做线程的通信
结束语
码字不易,还希望多多点赞、收藏支持下
转载自:https://juejin.cn/post/7187755864739037239