前言
自从面试头条被虐之后,我发现自己对多线程的学习确实不到位,所以最近重新学习一下Java多线程的知识,通过本篇文章记录一下。
点击这里查看面试头条的惨状?
正文
▶ 线程与进程
多线程是很多语言的重要特性,在开发中无处不在,不管是网络编程还是服务器端开发,都能看到它的身影。那么啥是线程呢?
线程
线程(Thread)是操作系统能够进行运算调度的最小单位。它被包含在进程之中,是进程中的实际运作单位。一条线程指的是进程中一个单一顺序的控制流,一个进程中可以并发多个线程,每条线程并行执行不同的任务。(来自百度百科)
提到线程就不得不提一下进程:
进程
进程(Process)是计算机中的程序关于某数据集合上的一次运行活动,是系统进行资源分配和调度的基本单位,是操作系统结构的基础。在早期面向进程设计的计算机结构中,进程是程序的基本执行实体;在当代面向线程设计的计算机结构中,进程是线程的容器。程序是指令、数据及其组织形式的描述,进程是程序的实体。(来自百度百科)
在描述它们的关系之前,我先打个比方,马路大家都见过吧,下图是我从网上找的两张图片:
(图片来自网络)
可以看出,一条马路可以存在一个车流(道),也可以存在多个车流(道),这就类似进程与线程之间,一个进程可以存在单个线程,也可以存在多个线程,它们之间的联系如下所示:
▶ 线程与进程的区别
线程 | 进程 | |
---|---|---|
单位 | CPU独立调度和分派的基本单位 | 资源分配和调度的基本单位 |
资源 | 共享进程的资源 | 拥有操作系统的资源 |
占用内存 | 少,切换迅速开销小 | 多,切换较慢开销大 |
CPU利用率 | 高 | 低 |
其它 | 是进程中的实体,一个线程必须有一个父进程 | 是线程的容器,一个进程包含一个或多个线程 |
▶ Java中如何创建一个线程?
① 扩展(继承)Thread类,重写run()方法,并调用start()启动
示例代码如下所示:
class StartThread extends Thread{
@Override
public void run() {
for (int i = 0; i < 10; i++) {
System.out.println("线程1");
}
}
}
public class Test {
public static void main(String[] args) {
new StartThread().start();
for (int i = 0; i < 10; i++) {
System.out.println("Main线程");
}
}
}
运行结果如下(结果不唯一):
Main线程
Main线程
线程1
线程1
线程1
线程1
线程1
线程1
线程1
线程1
线程1
线程1
Main线程
Main线程
Main线程
Main线程
Main线程
Main线程
Main线程
Main线程
这里有必要说明一下,程序的运行结果不是唯一的,可能先打印所有的“Main线程”再打印所有的“线程1”,因为这跟操作系统的调度有关系,由操作系统决定什么时候调度线程1,什么时候调度Main线程。
② 实现Runnable接口,重写run()方法,并通过代理Thread来调用start()启动
示例代码如下所示:
class StartThread2 implements Runnable{
@Override
public void run() {
for (int i = 0; i < 10; i++) {
System.out.println("线程1");
}
}
}
public class Test2 {
public static void main(String[] args) {
new Thread(new StartThread2()).start();
for (int i = 0; i < 10; i++) {
System.out.println("Main线程");
}
}
}
运行结果如下:
Main线程
Main线程
Main线程
Main线程
线程1
线程1
线程1
线程1
Main线程
Main线程
Main线程
Main线程
Main线程
Main线程
线程1
线程1
线程1
线程1
线程1
线程1
当然,这里的运行结果也是不唯一的,如果想呈现出上面的效果,建议多运行几次或者把for里面的次数改大一点。
这里推荐第二种方法来创建一个线程,主要原因如下
- Java无法同时继承多个类,使用第一种方法创建线程会存在单继承的局限,但是Java可以同时实现多个接口,这样在实现Runnable接口的同时还可以继承其它类的特性;
- 可以很容易实现资源的共享
一般呢常见的创建线程方式就是上面两种,如果你回答出下面这种的话,说不定对你的面试有加分。
③ 实现Callable接口,重写call()方法,通过代理Thread来接收FutureTask对象并调用start()启动
Callable类似Runnable,但是Runnable的run()方法无法返回值,也无法抛出异常,而Callable则相反,不仅可以返回任意类型的结果,也可以抛出异常。
示例代码如下所示:
public class Test3 {
public static void main(String[] args) {
// 通过FutureTask接收执行CallableDemo的结果
FutureTask<Long> result = new FutureTask<>(new CallableDemo());
new Thread(result).start();
try {
// 获取线程返回的结果
Long timeMillis = result.get();
System.out.println("CallableDemo中的时间戳:" + timeMillis);
} catch (InterruptedException | ExecutionException e) {
e.printStackTrace();
}
}
}
class CallableDemo implements Callable<Long> {
@Override
public Long call() throws Exception {
return System.currentTimeMillis();
}
}
运行结果如下所示:
CallableDemo中的时间戳:1564529593835
案例中通过实现Callable接口,返回运行线程的当前时间给result,最后通过result.get()获取结果。
▶ 线程的生命周期
当线程被创建并启动以后,它既不是一启动就进入了执行状态,也不是一直处于执行状态,在线程的生命周期中,它要经过新建(New)、就绪(Runnable)、运行(Running)、阻塞(Blocked)和死亡(Dead)。(来自疯狂Java讲义)
线程进入阻塞状态的时机
- 线程调用sleep()方法主动放弃所占用的处理器资源。
- 线程调用了一个阻塞式IO方法,在该方法返回之前,该线程被阻塞。
- 线程试图获得一个同步监视器,但该同步监视器正被其它线程所持有。
- 线程在等待某个通知(notify)。
- 程序调用了线程的suspend()方法将该线程挂起。但该方法容易导致死锁,尽量避免使用。
线程进入就绪状态的时机
- 调用sleep()方法的线程经过了指定时间。
- 线程调用的阻塞式IO方法已经返回。
- 线程成功地获得了试图取得的同步监视器。
- 线程正在等待某个通知时,其它线程发出了一个通知。
- 处于挂起状态的线程被调用了resume()恢复方法。
线程的状态转换图
注意:
进入阻塞状态的线程只能转换为就绪状态,无法直接转换为运行状态;
进入死亡状态的线程无法“复活”,该线程不可再次作为线程执行。
start()和run()方法的区别是什么?
既然讲到了线程的状态,那就顺便提一下这俩方法的区别吧。
当一个线程通过new关键字创建之后,该线程就处于新建状态,此时仅由虚拟机为其分配了内存,但是该线程的执行体并不会执行。
调用start()方法来启动线程时,该线程就处于就绪状态,此时该线程可以跟其他线程一起运行。
直接调用run()方法的话,就跟普通的方法没什么区别了,会按照代码顺序进行运行,同时也就没有多线程的效果了。
如何结束一个线程?
在一般情况下,线程中的run()或者call()执行完成之后线程就已经结束(死亡)了,但是我们可能会出现突然终止线程的场景,比如关闭正在执行的定时器(闹钟)线程,这时候就不可能等线程执行完所有的代码了,因为一般都是使用while(true)不断判断时间,到达指定时间则执行任务。
在这种情况下,我们可以通过一个标志来结束线程,示例代码如下所示:
public class Test4 {
public static void main(String[] args) {
Job job = new Job();
new Thread(job).start();
try {
// 5秒后结束job线程
Thread.sleep(5000);
job.finish();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
class Job implements Runnable {
// 结束标志,默认为false
boolean stop = false;
@Override
public void run() {
// 模拟倒计时10秒
int i = 10;
while(!stop) {
System.out.println("还剩" + i + "秒结束。。");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
if (i-- == 0)
break;
}
System.out.println("倒计时结束!");
}
// 结束线程
public void finish() {
this.stop = true;
}
}
运行结果如下所示:
还剩10秒结束。。
还剩9秒结束。。
还剩8秒结束。。
还剩7秒结束。。
还剩6秒结束。。
倒计时结束!
原本job线程自动结束的话,会出现下面的结果:
还剩10秒结束。。
还剩9秒结束。。
还剩8秒结束。。
还剩7秒结束。。
还剩6秒结束。。
还剩5秒结束。。
还剩4秒结束。。
还剩3秒结束。。
还剩2秒结束。。
还剩1秒结束。。
还剩0秒结束。。
倒计时结束!
但是因为在main线程中,等待5秒之后,就会启动job的finish()方法,导致job中的标志变量stop变为true
this.stop = true;
这时候,run()方法中就无法一直循环下去了,因为!stop为false,退出循环
while(!stop) {
运行完System.out.println(“倒计时结束!”); 这句代码之后,线程已经被我们强制结束了。
▶ 线程的优先级问题
默认线程的优先级是5,可以通过setPriority()方法来设置线程的优先级,线程的优先级支持以下三种类型
- MIN_PRIORITY:代表最小优先级,对应的值为1
- NORM_PRIORITY:代表标准优先级,对应的值为5
- MAX_PRIORITY:代表最大优先级,对应的值为10
下面的代码演示了6个线程,其中3个为最大优先级,2个为最小优先级,1个默认优先级:
public class Test5 {
public static void main(String[] args) {
MyPriority myPriority1 = new MyPriority();
Thread t0 = new Thread(myPriority1);
Thread t1 = new Thread(myPriority1);
Thread t2 = new Thread(myPriority1);
Thread t3 = new Thread(myPriority1);
Thread t4 = new Thread(myPriority1);
Thread t5 = new Thread(myPriority1);
t0.setPriority(Thread.NORM_PRIORITY);
t1.setPriority(Thread.MAX_PRIORITY);
t2.setPriority(Thread.MAX_PRIORITY);
t3.setPriority(Thread.MAX_PRIORITY);
t4.setPriority(Thread.MIN_PRIORITY);
t5.setPriority(Thread.MIN_PRIORITY);
t0.start();
t1.start();
t2.start();
t3.start();
t4.start();
t5.start();
}
}
class MyPriority implements Runnable {
@Override
public void run() {
System.out.println(Thread.currentThread().getName() + "-> 优先级:" + Thread.currentThread().getPriority());
}
}
运行结果如下:
Thread-1-> 优先级:10
Thread-3-> 优先级:10
Thread-0-> 优先级:5
Thread-2-> 优先级:10
Thread-5-> 优先级:1
Thread-4-> 优先级:1
这里的运行结果也不是唯一的,有个问题,线程2的优先级尽管是最大的,但是默认优先级线程0却比线程2先运行,我们多运行几次:
Thread-1-> 优先级:10
Thread-5-> 优先级:1
Thread-4-> 优先级:1
Thread-2-> 优先级:10
Thread-3-> 优先级:10
Thread-0-> 优先级:5
Thread-0-> 优先级:5
Thread-5-> 优先级:1
Thread-4-> 优先级:1
Thread-3-> 优先级:10
Thread-2-> 优先级:10
Thread-1-> 优先级:10
Thread-1-> 优先级:10
Thread-0-> 优先级:5
Thread-2-> 优先级:10
Thread-3-> 优先级:10
Thread-4-> 优先级:1
Thread-5-> 优先级:1
Thread-0-> 优先级:5
Thread-3-> 优先级:10
Thread-1-> 优先级:10
Thread-2-> 优先级:10
Thread-5-> 优先级:1
Thread-4-> 优先级:1
其实这里的优先级不一定会优先运行,存在一个概率问题,优先级高的线程比优先级低的更有机会先运行,同理优先级低的线程也有小概率比优先级高的先运行,取决于操作系统的调度。
设置线程的优先级时,一定要在start()方法前设置,否则会无效。
▶ 啥是守护线程(后台线程)?
守护线程的任务就是为其它线程提供服务的,比如JVM的垃圾回收线程就是典型的守护线程。
它的特征:如果所有的用户线程(非后台线程或者叫前台线程)都死亡,守护线程也会自动死亡。
默认的线程为用户线程,JVM必须等待所有的用户线程执行完毕才停止,JVM的停止跟守护线程无关,守护线程没执行完毕的话,JVM也可以停止。
可以通过使用setDaemon(true)方法将线程设置为守护线程。
下面的代码演示了God线程守护Person线程的过程:
public class Test6 {
public static void main(String[] args) {
God god = new God();
Person person = new Person();
Thread thread = new Thread(god);
// 如果下面的代码不注释的话,god会一直执行,因为god属于用户线程,JVM必须等待所有的用户线程执行完毕才会停止
// 如果注释掉的话,那么person线程执行完毕之后,JVM也就停止了,不用等待god
thread.setDaemon(true);
thread.start();
new Thread(person).start();
}
}
class Person implements Runnable {
@Override
public void run() {
// 假设人可以活100年
for (int i = 0; i < 365 * 100; i++) {
System.out.println("Live");
}
System.out.println("Die");
}
}
class God implements Runnable {
@Override
public void run() {
// 上帝一直守护人类
while (true) {
System.out.println("守护人类");
}
}
}
如果将God线程设置为守护线程的话,那么在Person线程死亡的时候,God线程会结束无限循环,打印出如下所示的结果:
Live
Live
Live
Live
Live
守护人类
守护人类
守护人类
守护人类
守护人类
Live
Live
Live
Live
Live
守护人类
守护人类
守护人类
守护人类
守护人类
…(这里省略了很多重复的打印)
Die
守护人类
如果将God线程设置为用户线程的话,那么在Person线程死亡的时候,God线程还会持续运行,会打印出如下所示的结果:
Live
Live
Live
Live
Live
守护人类
守护人类
守护人类
守护人类
守护人类
Live
Live
Live
Live
Live
守护人类
守护人类
守护人类
守护人类
守护人类
…(这里省略了很多重复的打印)
Die
守护人类
守护人类
守护人类
守护人类
守护人类
…(后面就是无限打印“守护人类”)
在打印出“Die”之后,God线程还会一直打印,也就是说JVM不会停止,因为还有用户线程没有执行完。
▶ 控制线程的方法
sleep()
sleep()会让当前线程暂停一段时间,并进入阻塞状态,此时在睡眠时间段内,该线程不会获得执行的机会,即使系统中没有其他可执行的线程,处于sleep()中的线程也不会执行,因此sleep()方法常用来暂停程序的执行。
有一点需要注意,sleep()方法执行的时候,是“抱着锁”睡觉的,即sleep()方法执行时,不会释放对象的锁。
yield()
yield()方法跟sleep()方法有点类似,都可以让当前的线程暂停,但是yield()会将线程进入就绪状态,当前线程暂停一会,让系统的线程调度器重新调度一次;而sleep()会让线程进程阻塞状态,时间结束之后进入就绪状态。
sleep()与yield()具体的区别
- sleep()方法暂停当前线程后,会给其他线程执行机会,不理会其他线程的优先级;但yield()方法只会给优先级相同或优先级更高的线程执行机会。
- sleep()方法会将线程转入阻塞状态,直到经过阻塞时间才会转入就绪状态;而yield()不会将线程转入阻塞状态,它只是强制当前线程进入就绪状态。因此完全有可能某个线程调用yield()方法暂停之后,立即获得处理器资源被执行。
- sleep()方法声明抛出了InterruptedException异常,所以调用sleep()方法时要么捕捉该异常要么显式声明抛出异常;而yield()方法则没有声明抛出任何异常。
- sleep()方法比yield()方法有更好的可移植性,通常不建议使用yield()方法来控制并发线程的执行。
join()
首先join()是一个实例方法,是一个让线程等待另一个线程完成的方法。当在某个程序执行流中调用其他线程的join()方法时,调用线程将被阻塞,直到被join()方法加入的join线程执行完为止。通俗的讲,就是谁调用join,谁就插队!
在如下的代码中,展示了线程A插队main线程的过程,main先启动,当计数器计算到10的时候,线程A插队,只有线程A执行完毕才能让main线程执行:
public class Test7 {
public static void main(String[] args) throws InterruptedException {
for (int i = 0; i < 20; i++) {
if (i == 10) {
// 线程A插队
MyThread threadA = new MyThread("线程A");
threadA.start();
threadA.join();
}
System.out.println(Thread.currentThread().getName() + ",正在执行" + i + "次。");
}
}
}
class MyThread extends Thread {
public MyThread(String name) {
super(name);
}
@Override
public void run() {
for (int i = 0; i < 20; i++) {
System.out.println(Thread.currentThread().getName() + ",正在执行" + i + "次。");
}
}
}
运行结果如下所示:
main,正在执行0次。
main,正在执行1次。
main,正在执行2次。
main,正在执行3次。
main,正在执行4次。
main,正在执行5次。
main,正在执行6次。
main,正在执行7次。
main,正在执行8次。
main,正在执行9次。
线程A,正在执行0次。
线程A,正在执行1次。
线程A,正在执行2次。
线程A,正在执行3次。
线程A,正在执行4次。
线程A,正在执行5次。
线程A,正在执行6次。
线程A,正在执行7次。
线程A,正在执行8次。
线程A,正在执行9次。
线程A,正在执行10次。
线程A,正在执行11次。
线程A,正在执行12次。
线程A,正在执行13次。
线程A,正在执行14次。
线程A,正在执行15次。
线程A,正在执行16次。
线程A,正在执行17次。
线程A,正在执行18次。
线程A,正在执行19次。
main,正在执行10次。
main,正在执行11次。
main,正在执行12次。
main,正在执行13次。
main,正在执行14次。
main,正在执行15次。
main,正在执行16次。
main,正在执行17次。
main,正在执行18次。
main,正在执行19次。
从结果可以看出,当计数器到达20时,线程A插队main线程,此时main线程处于阻塞状态,只有线程A执行结束之后main才继续执行。
总结
以上就是对入门Java多线程的小结(好像确实比较长,也感谢各位能够看到这里),下节将会带来更加深入的内容,包括同步、JUC等知识点。
一起加油吧!!!