为什么使用多线程?这应该学习多线程提到了一个关键问题。多线程技术的使用原因也就是多线程的优点:
- 提高资源利用:多核处理器的发展是多线程技术发扬光大的前提。硬件的提升使得人们对软件的要求也慢慢的提高。而多线程的应用能提高CPU的利用率。
- 响应时间更快。响应时间越快,用户体验越好。降低响应时间,也就是提高了吞吐量,使系统更加健壮。
知道了为什么使用多线程之后,那么怎么使用多线程,了解一个线程的“生老病死”的过程就成了迫在眉睫的一步。
线程的生命周期
线程的执行也就是也就是线程各个生命阶段的转换。线程并不是Java特有的概念,在其他语言像C等,都有线程的概念,而关于线程的生命周期的部分都免不了一套通用的生命周期中演变出来。现在来看看线程通用的生命周期。
通用的线程生命周期基本上可以用下图这个“五态模型”来描述。这五态分别是:初始状态、可运行状态、运行状态、休眠状态和终止状态。
这“五态模型”的详细情况如下所示。
初始状态,指的是线程已经被创建,但是还不允许分配 CPU 执行。这个状态属于编程语言特有的,不过这里所谓的被创建,仅仅是在编程语言层面被创建,而在操作系统层面,真正的线程还没有创建。
可运行状态,指的是线程可以分配 CPU 执行。在这种状态下,真正的操作系统线程已经被成功创建了,所以可以分配 CPU 执行。
当有空闲的 CPU 时,操作系统会将其分配给一个处于可运行状态的线程,被分配到 CPU 的线程的状态就转换成了运行状态。
运行状态的线程如果调用一个阻塞的 API(例如以阻塞方式读文件)或者等待某个事件(例如条件变量),那么线程的状态就会转换到休眠状态,同时释放 CPU 使用权,休眠状态的线程永远没有机会获得 CPU 使用权。当等待的事件出现了,线程就会从休眠状态转换到可运行状态。
线程执行完或者出现异常就会进入终止状态,终止状态的线程不会切换到其他任何状态,进入终止状态也就意味着线程的生命周期结束了。
而Java在这基础上又有了丰富和完善,来看看Java线程的生命周期:
Java 语言中线程共有六种状态,分别是:
NEW(初始化状态)
RUNNABLE(可运行 / 运行状态)
BLOCKED(阻塞状态)
WAITING(无时限等待)
TIMED_WAITING(有时限等待)
TERMINATED(终止状态)
在Java 线程中的 BLOCKED、WAITING、TIMED_WAITING 是一种状态,即前面我们提到的休眠状态。也就是说只要 Java 线程处于这三种状态之一,那么这个线程就永远没有 CPU 的使用权。
下面将用线程的api使用方法来结合线程中的各个状态以及线程间状态的转换更好的展示出如何使用多线程技术
NEW(初始化状态)实现一个线程
初始状态,指的是线程已经被创建,但是还不允许分配 CPU 执行。这个状态属于编程语言特有的,不过这里所谓的被创建,仅仅是在编程语言层面被创建,而在操作系统层面,真正的线程还没有创建。而常用的创建线程的方式有以下四种方式:
- 继承Thread类,重写run方法,调用start方法
package cn.com.threadtx.mythred;
public class Test1 {
public static void main(String[] args) {
MyThread1 myThread1 = new MyThread1();
myThread1.start();
}
}
package cn.com.threadtx.mythred;
public class MyThread1 extends Thread {
@Override
public void run() {
System.out.print("extends Thread ");
}
}
优缺点:继承Thread类和实现Runnable方法启动线程都是使用start方法,然后JVM虚拟机将此线程放到就绪队列中,如果有处理机可用,则执行run方法。api调用丰富
- 实现Runnable接口
package cn.com.threadtx.mythred;
public class Test2 {
public static void main(String[] args) {
Thread myThread2 = new Thread(new MyThread2());
myThread2.start();
}
}
package cn.com.threadtx.mythred;
public class MyThread2 implements Runnable {
@Override
public void run() {
System.out.print("MyThread2 implements Runnable ");
}
}
优缺点:实现Runnable接口可以避免Java单继承特性而带来的局限;增强程序的健壮性,代码能够被多个线程共享,代码与数据是独立的;适合多个相同程序代码的线程去处理同一资源的情况。
- 实现Callable接口通过FutureTask包装器来创建Thread线程
package cn.com.threadtx.mythred;
import java.util.concurrent.Callable;
import java.util.concurrent.FutureTask;
public class Test3 {
public static void main(String[] args) {
Callable<String> thread3 = new MyThread3();
FutureTask<String> ft = new FutureTask<String>(thread3);
Thread thread = new Thread(ft);
thread.start();
}
}
package cn.com.threadtx.mythred;
import java.util.concurrent.Callable;
public class MyThread3 implements Callable<String> {
@Override
public String call() {
System.out.print("MyThread3 implements Callable<String> ");
return "MyThread3 implements Callable<String> ";
}
}
优缺点:实现Callable接口要实现call方法,并且线程执行完毕后会有返回值。其他的两种都是重写run方法,没有返回值
- 使用ExecutorService、Callable、Future实现有返回结果的线程,线程池的使用
class MyThread4 implements Runnable {
@Override
public void run() {
System.out.println(Thread.currentThread().getName());
}
}
class MyThread2 implements Callable<Integer> {
@Override
public Integer call() throws Exception {
System.out.println(Thread.currentThread().getName());
return 0;
}
}
public class Demo03 {
public static void main(String[] args) {
ExecutorService executorService = Executors.newFixedThreadPool(5);
for(int i=0;i<2;i++) {
executorService.execute(new MyThread());
FutureTask<Integer> ft = new FutureTask<Integer>(new MyThread2());
//输出
executorService.submit(ft);
}
executorService.shutdown();
}
}
优缺点:
第一:降低资源消耗。通过重复利用已创建的线程降低线程创建和销毁造成的消耗。 第二:提高响应速度。当任务到达时,任务可以不需要的等到线程创建就能立即执行。 第三:提高线程的可管理性。线程是稀缺资源,如果无限制的创建,不仅会消耗系统资源,还会降低系统的稳定性,使用线程池可以进行统一的分配,调优和监控。
值得注意的是thread的start方法和run方法并不一个场景,start只是通知CPU线程已经进去就绪状态,run方法才是真正的启动线程,也就是由NEW(初始化状态)变成RUNNABLE(可运行 / 运行状态)。
线程的等待( RUNNABLE 与 WAITING 的状态转换,RUNNABLE 与 BLOCKED 的状态转换,RUNNABLE 与 TIMED_WAITING 的状态转换)
RUNNABLE 与 WAITING 的状态转换
总体来说,有三种场景会触发这种转换。 第一种场景,获得 synchronized 隐式锁的线程,调用无参数的 Object.wait() 方法。其中,wait() 方法我们在上一篇讲解管程的时候已经深入介绍过了,这里就不再赘述。 第二种场景,调用无参数的 Thread.join() 方法。其中的 join() 是一种线程同步方法,例如有一个线程对象 thread A,当调用 A.join() 的时候,执行这条语句的线程会等待 thread A 执行完,而等待中的这个线程,其状态会从 RUNNABLE 转换到 WAITING。当线程 thread A 执行完,原来等待它的线程又会从 WAITING 状态转换到 RUNNABLE。 第三种场景,调用 LockSupport.park() 方法。其中的 LockSupport 对象,也许你有点陌生,其实 Java 并发包中的锁,都是基于它实现的。调用 LockSupport.park() 方法,当前线程会阻塞,线程的状态会从 RUNNABLE 转换到 WAITING。调用 LockSupport.unpark(Thread thread) 可唤醒目标线程,目标线程的状态又会从 WAITING 状态转换到 RUNNABLE。
RUNNABLE 与 BLOCKED 的状态转换
只有一种场景会触发这种转换,就是线程等待 synchronized 的隐式锁。synchronized 修饰的方法、代码块同一时刻只允许一个线程执行,其他线程只能等待,这种情况下,等待的线程就会从 RUNNABLE 转换到 BLOCKED 状态。而当等待的线程获得 synchronized 隐式锁时,就又会从BLOCKED 转换到 RUNNABLE 状态。
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) 方法。
如何优雅的停止一个线程(从 RUNNABLE 到 TERMINATED 状态)
线程执行完 run() 方法后,会自动转换到 TERMINATED 状态,当然如果执行 run() 方法的时候异常抛出,也会导致线程终止。有时候我们需要强制中断 run() 方法的执行,例如 run() 方法访问一个很慢的网络,我们等不下去了,想终止怎么办呢?Java 的 Thread 类里面倒是有个 stop() 方法,不过已经标记为 @Deprecated,所以不建议使用了。正确的姿势其实是调用 interrupt() 方法。
stop和interrupt的区别:
stop() 方法会真的杀死线程,不给线程喘息的机会,如果线程持有 ReentrantLock 锁,被 stop() 的线程并不会自动调用 ReentrantLock 的 unlock() 去释放锁,那其他线程就再也没机会获得 ReentrantLock 锁,这实在是太危险了。所以该方法就不建议使用了,类似的方法还有 suspend() 和 resume() 方法,这两个方法同样也都不建议使用了,所以这里也就不多介绍了。
而 interrupt() 方法就温柔多了,interrupt() 方法仅仅是通知线程,线程有机会执行一些后续操作,同时也可以无视这个通知。被 interrupt 的线程,是怎么收到通知的呢?一种是异常,另一种是主动检测。