前言
相信大家多多少少都对线程有一些认知,也在平时的工作中使用过多线程。但我最近在越发的想要了解线程的前世今生,为什么它成为了八股文中浓墨重彩的一笔。所以今天就从这个角度来学习一下,什么TM叫多线程。
一个进程的诞生
在讨论线程前不得不提到的就是进程,因为在写这篇文章前,我也在网上查阅了大量的进程、线程的概念。感觉越看越糊涂,最后也对自己已有的认知产生了怀疑。所以这里再描述一下进程的概念,巩固自己的认知。
首先,做个思想实验。抛开具体的计算机原理,我们只要知道CPU是有序执行加载到内存中的指令的。假设现在磁盘上有个程序叫QQ音乐,这个程序里只有20条指令,CPU需要20秒的时间去执行完这些指令。那么在这20秒内,计算机无法做任何其他事情,这不是玩呢吗?我写博客的时候就不能听音乐,听音乐的时候就不能写博客?
所以,我们希望CPU能够同时执行多个程序,或者看起来同时执行多个程序。假设我又打开了新浪微博,我想一边刷微博一边听音乐,新浪微博这个程序有30条指令,我让CPU先执行一秒钟QQ音乐再执行一秒钟新浪微博。这样看上去就好像同时在执行多个程序。
好了,到此我们的思想实验结束。那么这个实验跟进程有什么关系呢?我的理解是:处理QQ音乐这20条指令的过程就是一个进程,处理新浪微博这30条指令的过程就是另外一个进程。
再给一个进程的抽象概念:进程是静态的程序的动态的执行过程。 到这里我大概对进程有了一个感性的认识。这就足矣,不再过多讨论。
一个线程的诞生
借助阮一峰老师举的例子,假定有一座工厂,工厂的电力有限每次只能给一个车间供电。也就是说一个车间开工的时候,其他车间都必须停工。这样进程就好比工厂的车间。
一个车间可以有很多工人,他们协同完成同一个任务。线程就好比车间里的工人。一个进程可以包括多个线程。
很简单,我们从这个例子中就有了一个比较整体的认知,到底什么是进程,什么是线程。
Java线程实现
有了以上的铺垫,我们就可以来学习一下java中线程的相关知识了。JDK中提供了Thread类和Runnable接口来让我们实现自己的“线程”类。
- 继承
Thread类,并重写run方法; - 实现
Runnable接口的run方法; 继承Thread类:
public class Demo {
public static class MyThread extends Thread {
@Override
public void run() {
System.out.println("MyThread");
}
}
public static void main(String[] args) {
Thread myThread = new MyThread();
myThread.start();
}
}
当调用start()方法之后,线程才算成功启动。
实现Runnable接口:
public class Demo {
public static class MyThread implements Runnable {
@Override
public void run() {
System.out.println("MyThread");
}
}
public static void main(String[] args) {
new Thread(new MyThread()).start();
// Java 8 函数式编程,可以省略MyThread类
new Thread(() -> {
System.out.println("Java 8 匿名内部类");
}).start();
}
}
Java线程状态
java线程中有6个状态:
// Thread.State 源码
public enum State {
NEW,
RUNNABLE,
BLOCKED,
WAITING,
TIMED_WAITING,
TERMINATED;
}
- 处于NEW状态的线程此时尚未启动。这里的尚未启动指的是还没调用Thread实例的start()方法。
- RUNNABLE表示当前线程正在运行中。处于RUNNABLE状态的线程在Java虚拟机中运行,也有可能在等待CPU分配资源。
- BLOCKED表示阻塞状态。处于BLOCKED状态的线程正等待锁的释放以进入同步区。
- WAITING表示等待状态。处于等待状态的线程变成RUNNABLE状态需要其他线程唤醒。
- TIMED_WAITING表示超时等待状态。线程等待一个具体的时间,时间到后会被自动唤醒。
- TERMINATED终止状态。此时线程已执行完毕。
线程状态的转换
BLOCKED状态与RUNNABLE状态的转换
上面说过:处于BLOCKED状态的线程是在等待锁的释放。假如有a和b两个线程,a线程提前获得了锁且暂未释放锁,此时b线程就处于BLOCKED状态,我们先来看个示例:
@Test
public void blockedTest() {
Thread a = new Thread(new Runnable() {
@Override
public void run() {
testMethod();
}
}, "a");
Thread b = new Thread(new Runnable() {
@Override
public void run() {
testMethod();
}
}, "b");
a.start();
b.start();
System.out.println(a.getName() + ":" + a.getState()); // 输出?
System.out.println(b.getName() + ":" + b.getState()); // 输出?
}
// 同步方法争夺锁
private synchronized void testMethod() {
try {
Thread.sleep(2000L);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
可能有人会觉得a线程的状态一定是TIMED_WAITING,b线程的状态一定是BLOCKED状态。其实不然,在调用blockedTest()方法时,我们不要忽略还有一个main线程,main线程只保证a、b两个线程调用start()方法(转化为RUNNABLE状态),其他的就充满了不确定。可能打印出的两个线程状态都是RUNNABLE状态,可能其中某个线程是BLOCKED状态。
我们再来调整一下代码:
public void blockedTest() throws InterruptedException {
······
a.start();
Thread.sleep(1000L); // 需要注意这里main线程休眠了1000毫秒,而testMethod()里休眠了2000毫秒
b.start();
System.out.println(a.getName() + ":" + a.getState()); // 输出?
System.out.println(b.getName() + ":" + b.getState()); // 输出?
}
在这个例子中两个线程的状态转换如下
- a的状态转换过程:RUNNABLE(
a.start()) -> TIMED_WATING(Thread.sleep())->RUNABLE(sleep()时间到)->BLOCKED(未抢到锁) -> TERMINATED - b的状态转换过程:RUNNABLE(
b.start()) -> BLOCKED(未抢到锁) ->TERMINATED 斜体表示可能出现的状态, 大家可以在自己的电脑上多试几次看看输出。同样,这里的输出也可能有多钟结果。
WAITING状态与RUNNABLE状态的转换
根据转换图我们知道有3个方法可以使线程从RUNNABLE状态转为WAITING状态。我们主要介绍下Object.wait() 和Thread.join() 。
Object.wait()
调用wait()方法前线程必须持有对象的锁。
线程调用wait()方法时,会释放当前的锁,直到有其他线程调用notify()/notifyAll()方法唤醒等待锁的线程。
需要注意的是,其他线程调用notify()方法只会唤醒单个等待锁的线程,如有有多个线程都在等待这个锁的话不一定会唤醒到之前调用wait()方法的线程。
同样,调用notifyAll()方法唤醒所有等待锁的线程之后,也不一定会马上把时间片分给刚才放弃锁的那个线程,具体要看系统的调度。
Thread.join()
调用join()方法,会一直等待这个线程执行完毕(转换为TERMINATED状态)。
我们再把上面的例子线程启动那里改变一下:
public void blockedTest() {
······
a.start();
a.join();
b.start();
System.out.println(a.getName() + ":" + a.getState()); // 输出 TERMINATED
System.out.println(b.getName() + ":" + b.getState());
}
要是没有调用join方法,main线程不管a线程是否执行完毕都会继续往下走。
a线程启动之后马上调用了join方法,这里main线程就会等到a线程执行完毕,所以这里a线程打印的状态固定是TERMINATED。
至于b线程的状态,有可能打印RUNNABLE(尚未进入同步方法),也有可能打印TIMED_WAITING(进入了同步方法)。
TIMED_WAITING与RUNNABLE状态转换
TIMED_WAITING与WAITING状态类似,只是TIMED_WAITING状态等待的时间是指定的。具体过程就不再赘述。
结语
以上差不多就是java线程的基础概念了,为了能够简单轻松的了解线程的相关知识,没有对实现原理做过多深入的理解。以此达到先了解再深入的循序渐进的学习方法。