并发编程-线程
线程的实现方式
1. 继承Thread类创建线程
public class Thread implements Runnable
public class MyThread extends Thread {
public void run() {
System.out.println("MyThread.run()"); }
}
MyThread myThread1 = new MyThread();
MyThread myThread2 = new MyThread();
myThread1.start();
myThread2.start();
2. 实现Runnable接口创建线程
如果自己的类已经extends另一个类,就无法直接extends Thread,此时,可以实现一个Runnable接口
public class MyThread extends OtherClass implements Runnable { public void run() { System.out.println("MyThread.run()"); } }
3. 实现Callable接口通过FutureTask包装器来创建Thread线程
有的时候,我们可能需要让一步执行的线程在执行完成以后,提供一个返回值给到当前的主线程,主线程需要依赖这个值进行后续的逻辑处理,那么这个时候,就需要用到带返回值的线程了。Java中提供了这样的实现方式
package com.example.springbootdemo.duobingfa; import java.util.concurrent.Callable; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; import java.util.concurrent.Future; public class CallableDemo implements Callable<String> { public static void main(String[] args) throws Exception{ ExecutorService executorService= Executors.newFixedThreadPool(3); CallableDemo callableDemo=new CallableDemo(); for(int i = 0; i < 3; i++){ Future<String> future=executorService.submit(callableDemo); System.out.println(future.get()); } executorService.shutdown(); } @Override public String call() throws Exception { int a=1; int b=2; System.out.println(Thread.currentThread().getName() +" : " + (a+b)); return "执行结果:"+(a+b); } }
结果:
pool-1-thread-1 : 3
执行结果:3
pool-1-thread-2 : 3
执行结果:3
pool-1-thread-3 : 3
执行结果:3
get()方法的阻塞性
任务是在调用submit方法时就开始执行了,如果在调用get()方法时,任务已经执行完毕,那么就不会造成阻塞。任务还没有执行完,所以会一直等到任务完成,形成了阻塞。
多线程的实际应用场景
其实大家在工作中应该很少有场景能够应用多线程了,因为基于业务开发来说,很多使用异步的场景我们都通过分布式消息队列来做了。但并不是说多线程就不会被用到,你们如果有看一些框架的源码,会发现线程的使用无处不在。
线程的生命周期
线程一共有6种状态(NEW、RUNNABLE、BLOCKED、WAITING、TIME_WAITING、TERMINATED)
NEW:初始状态,线程被构建,但是还没有调用start方法
RUNNABLED:运行状态,JAVA线程把操作系统中的就绪和运行两种状态统一称为“运行中”
BLOCKED:阻塞状态,表示线程进入等待状态,也就是线程因为某种原因放弃了CPU使用权,阻塞也分为几种情况
等待阻塞:运行的线程执行wait方法,jvm会把当前线程放入到等待队列
同步阻塞:运行的线程在获取对象的同步锁时,若该同步锁被其他线程锁占用了,那么jvm会把当前的线程放入到锁池中
其他阻塞:运行的线程执行Thread.sleep或者t.join方法,或者发出了I/O请求时,JVM会把当前线程设置为阻塞状态,当 sleep结束、join线程终止、io处理完毕则线程恢复
TIME_WAITING:超时等待状态,超时以后自动返回
TERMINATED:终止状态,表示当前线程执行完毕
启动一个线程前,最好为这个线程设置线程名称,因为这样在使用jstack分析程序或者进行问题排查时,就会给开发人员提供一些提示
显示线程的状态
➢ 运行示例,打开终端或者命令提示符,键入“jps”,(JDK1.5提供的一个显示当前所有java进程pid的命令)
➢ 根据上一步骤获得的pid,继续输入jstack pid(jstack是java虚拟机自带的一种堆栈跟踪工具。jstack用于打印出给定的java进程ID或core file或远程调试服务的Java堆栈信息)
通过上面的分析,我们了解到了线程的生命周期,现在在整个生命周期中并不是固定的处于某个状态,而是随着代码的执行在不同的状态之间进行切换
如何终止一个线程?
线程的终止,并不是简单的调用stop命令去。虽然api仍然可以调用,但是和其他的线程控制方法如suspend、resume一样都是过期了的不建议使用,就拿stop来说,stop方法在结束一个线程时并不会保证线程的资源正常释放,因此会导致程序可能出现一些不确定的状态。
要优雅的去中断一个线程,在线程中提供了一个interrupt方法
interrupt方法
当其他线程通过调用当前线程的interrupt方法,表示向当前线程打个招呼,告诉他可以中断线程的执行了,至于什么时候中断,取决于当前线程自己。
线程通过检查自身是否被中断来进行响应,可以通过isInterrupted()来判断是否被中断。
通过下面这个例子,来实现了线程终止的逻辑
public class InterruptDemo { private static int i; public static void main(String[] args) throws InterruptedException { Thread thread=new Thread(()->{ while(!Thread.currentThread().isInterrupted()){ //默认情况下isInterrupted返回false、通过thread.interrupt变成了true i++; } System.out.println("Num:"+i); },"interruptDemo"); thread.start(); TimeUnit.SECONDS.sleep(1); thread.interrupt(); //加和不加的效果 } }
运行结果:
Num:445519809
Process finished with exit code 0
这种通过标识位或者中断操作的方式能够使线程在终止时有机会去清理资源,而不是武断地将线程停止,因此这种终止线程的做法显得更加安全和优雅。
Thread.interrupted
上面的案例中,通过interrupt,设置了一个标识告诉线程可以终止了,线程中还提供了静态方法Thread.interrupted()对设置中断标识的线程复位。比如在上面的案例中,外面的线程调用thread.interrupt来设置中断标识,而在线程里面,又通过Thread.interrupted把线程的标识又进行了复位
其他的线程复位
除了通过Thread.interrupted方法对线程中断标识进行复位以外,还有一种被动复位的场景,就是对抛出InterruptedException异常的方法,在InterruptedException抛出之前,JVM会先把线程的中断标识位清除,然后才会抛出InterruptedException,这个时候如果调用isInterrupted方法,将会返回false
为什么要复位
Thread.interrupted()是属于当前线程的,是当前线程对外界中断信号的一个响应,表示自己已经得到了中断信号,但不会立刻中断自己,具体什么时候中断由自己决定,让外界知道在自身中断前,他的中断状态仍然是false,这就是复位的原因。
这里给大家普及一个知识点,为什么Object.wait、Thread.sleep和Thread.join都会抛出InterruptedException?你会发现这几个方法有一个共同点,都是属于阻塞的方法
而阻塞方法的释放会取决于一些外部的事件,但是阻塞方法可能因为等不到外部的触发事件而导致无法终止,所以它允许一个线程请求自己来停止它正在做的事情。当一个方法抛出InterruptedException时,它是在告诉调用者如果执行该方法的线程被中断,它会尝试停止正在做的事情并且通过抛出InterruptedException表示提前返回。
所以,这个异常的意思是表示一个阻塞被其他线程中断了。然后,由于线程调用了interrupt()中断方法,那么
Object.wait、Thread.sleep等被阻塞的线程被唤醒以后会通过is_interrupted方法判断中断标识的状态变化,如果发现中断标识为true,则先清除中断标识,然后抛出InterruptedException
需要注意的是,InterruptedException异常的抛出并不意味着线程必须终止,而是提醒当前线程有中断的操作发生,至于接下来怎么处理取决于线程本身,比如
1. 直接捕获异常不做任何处理
2. 将异常往外抛出
3. 停止当前线程,并打印异常信息