我是一名很普通的双非大三学生。接下来的几个月内,我将坚持写博客,输出知识的同时巩固自己的基础,记录自己的成长和锻炼自己,备战2021暑期实习面试!奥利给!!
多线程也是面试中必问的点,是必备的基础技能。
Thread(线程)的概念
首先我们要知道线程的概念是什么,在继续往下说,大家可以先闭上眼回忆一下:
线程(Thread)是并发编程的基础,也是程序执行的最小单元,它依托进程而存在。一个进程中可以包含多个线> 程,多线程可以共享一块内存空间和一组系统资源,因此线程之间的切换更加节省资源、更加轻量化。
线程的状态有哪些?
线程的状态在 JDK 1.5 之后以枚举的方式被定义在 Thread 的源码中,它总共包含以下 6 个状态:
public enum State {
NEW,
RUNNABLE,
BLOCKED,
WAITING,
TIMED_WAITING,
TERMINATED;
}
六种状态分别为:
- NEW,新建状态,线程被创建出来,但尚未启动时的线程状态;
- RUNNABLE,就绪状态,表示可以运行的线程状态,它可能正在运行,或者是在排队等待操作系统给它分配 CPU 资源;
- 比如Thread.start方法就是将线程从NEW状态 转换成 RUNNABLE 状态。
- BLOCKED,阻塞等待锁的线程状态,表示处于阻塞状态的线程正在等待监视器锁
- 比如等待执行 synchronized 代码块或者使用 synchronized 标记的方法。
- WAITING,等待状态,一个处于等待状态的线程正在等待另一个线程执行某个特定的动作。
- 比如,一个线程调用了
Object.wait()
方法,那它就在等待另一个线程调用Object.notify()
或Object.notifyAll()
方法。
- 比如,一个线程调用了
- TIMED_WAITING,计时等待状态,和上者类似,只是多了一个超时时间。
- 比如调用了有超时时间设置的方法
Object.wait(long timeout)
和Thread.join(long timeout)
等这些方法时,它才会进入此状态;
- 比如调用了有超时时间设置的方法
- TERMINATED,终止状态,表示线程已经执行完成。
但是这 6 种状态并不是线程所有的状态,只是在 Java 源码中列举出的 6 种状态, Java 线程的处理方法都是围绕这 6 种状态的。
线程的工作模式
线程的工作模式:
- 首先要new Thread()创建线程,此时线程的状态是NEW,表示线程创建成功但没有运行。
- 然后再调用线程的
start()
方法,此时线程就从 NEW(新建)状态变成了 RUNNABLE(就绪)状态。 - 此时线程会判断要执行的方法中有没有
synchronized
修饰的代码块或方法,如果有并且其他线程也在使用此锁,那么线程就会变为 BLOCKED(阻塞等待)状态,当其他线程使用完此锁之后,线程会继续执行剩余的方法。 - 当在遇到
Object#wait、Thread#join、LockSupport#park
这些方法时,线程会变为 WAITING(等待状态)状态,如果是带了超时时间的等待方法,那么线程会进入 TIMED_WAITING(计时等待)状态 - 当有其他线程执行了 notify() 或 notifyAll() 方法之后,线程被唤醒继续执行剩余的业务方法,直到方法执行完成为止,此时整个线程的流程就执行完了。
执行流程如下图所示:
这里要区分开BLOCKED 和 WAITING 的区别:
虽然二者都有等待的含义,但还是区别很大的,首先它们状态形成的调用方法不同,其次BLOCKED可以理解为当前线程还处于活跃状态,只是在阻塞等待其他线程使用完某个锁资源;而WAITING则是因为自身调用了Object.wait()
、Thread.join()
、LockSupport.park()
而进入等待状态,只能等待其他线程执行某个特定的动作才能被继续唤醒,比如当线程因为调用了Object.wait()
而进入WAITING状态之后,则需要等待另一个线程执行Object.notify()、Object.notifyAll()
才能被唤醒。
线程的使用
创建线程的方式
有幸听过一节马哥的公开课小马哥慕课直播,感兴趣的可以看看,视频很不错,上和下加一起有六个小时在第12分钟的时候,有说到其实创建线程的方式从本质上说,只有一种,那就是Thread.run()
,因为不管你是继承Thread,还是实现Runnable接口,我们可以看源码:
public Thread(Runnable target) {
init(null, target, "Thread-" + nextThreadNum(), 0);
}
public void run() {
if (target != null) {
target.run();
}
}
运行线程的方式有两种,一种当用Runnable接口时间,thread.run会判断target在决定调用谁的run,而线程thread实际是重写了该run方法。怎么用就不我例举了吧。
说到这里,可能有同学就疑惑了,明明我平常写代码调的是start()
不是run()
啊?这里看这篇文章java中多线程执行时,为何调用的是start()方法而不是run()方法我在补充两点:
- start()方法不能被多次调用,多次调用会抛出
java.lang.IllegalStateException
;而run()方法可以进行多次调用,因为它只是一个普通的方法。
线程常见的方法
线程的优先级
优先级代表线程执行的机会的大小,优先级高的可能先执行,低的可能后执行,在 Java 源码中,优先级从低到高分别是 1 到 10,线程默认 new 出来的优先级都是 5,源码如下:
// 最低优先级
public final static int MIN_PRIORITY = 1;
// 普通优先级,也是默认的
public final static int NORM_PRIORITY = 5;
// 最大优先级
public final static int MAX_PRIORITY = 10;
在程序中我们可以通过 Thread.setPriority() 来设置优先级
join方法
在一个线程中调用其他线程对象的join()方法时 ,这时候当前线程会让出执行权给其他线程,直到 其他线程执行完或者过了超时时间之后再继续执行当前线程,join() 源码如下:
public final synchronized void join(long millis)
throws InterruptedException {
long base = System.currentTimeMillis();
long now = 0;
if (millis < 0) {
throw new IllegalArgumentException("timeout value is negative");
}
if (millis == 0) {
// 其他线程好了之后,当前线程的状态是 TERMINATED,isAlive 返回 false
// NEW false
// RUNNABLE true
while (isAlive()) {
// 等待其他线程,一直等待
wait(0);
}
} else {
while (isAlive()) {
long delay = millis - now;
if (delay <= 0) {
break;
}
// 等待一定的时间,如果在 delay 时间内,等待的线程仍没有结束,放弃等待
wait(delay);
now = System.currentTimeMillis() - base;
}
}
}
从源码中可以看出 join() 方法底层还是通过 wait() 方法来实现的。假设我们现在有一道题,保证三个线程的有序执行,让你去实现,想想用join怎么实现?
Thread thread1 = new Thread(() -> {
System.out.println("t1执行");
}, "t1");
Thread thread2 = new Thread(() -> {
System.out.println("t2执行");
}, "t2");
Thread thread3 = new Thread(() -> {
System.out.println("t3执行");
}, "t3");
thread1.start();
thread1.join();
thread2.start();
thread2.join();
thread3.start();
thread3.join();
在想想,在看过join的源码后,你还有什么方法可以保证线程的有序执行?这里在沾两个方法:
Thread thread1 = new Thread(() -> {
System.out.println("t1执行");
}, "t1");
Thread thread2 = new Thread(() -> {
System.out.println("t2执行");
}, "t2");
Thread thread3 = new Thread(() -> {
System.out.println("t3执行");
}, "t3");
thread1.start();
while (thread1.isAlive()) {
}
thread2.start();
while (thread2.isAlive()) {
}
thread3.start();
while (thread3.isAlive()) {
}
Thread thread1 = new Thread(() -> {
System.out.println(Thread.currentThread().getName() + "执行");
}, "t1");
Thread thread2 = new Thread(() -> {
System.out.println(Thread.currentThread().getName() + "执行");
}, "t2");
Thread thread3 = new Thread(() -> {
System.out.println(Thread.currentThread().getName() + "执行");
}, "t3");
threadStartAndWait(thread1);
threadStartAndWait(thread2);
threadStartAndWait(thread3);
public static void threadStartAndWait(Thread thread) {
System.out.println(Thread.currentThread().getName());
if (Thread.State.NEW.equals(thread.getState())) {
thread.start();
}
// Java Thread 对象和实际 JVM 执行的OS Thread 不是相同对象
// JVM Thread 回调 Java Thread.run() 方法
// 同时 Thread 提供一些 native 方法获取 JVM Thread 状态
// 当 JVM Thread 执行完之后,自动就notify()了
while (thread.isAlive()) { // thread 特殊的object
// 当线程Thread isAlive() == false 时,thread.wait() 操作会被自动释放
synchronized (thread) {
try {
//阻塞的是这个对象所在的线程(通常是主线程)
thread.wait(); // 到底是谁在通知Thread -> thread.notify();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
yield方法
yield() 为本地方法,源码如下:
// 当前线程做出让步,放弃当前 cpu,让线程重新选择 cpu,避免线程过度使用 cpu
// 让步不是不执行,也有可能重新选中自己
public static native void yield();
yield()
方法表示给线程调度器一个当前线程愿意出让 CPU 使用权的暗示,但是线程调度器可能会忽略这个暗示。
例如下面这段代码:
public static void main(String[] args) throws InterruptedException {
Runnable runnable = () -> {
for (int i = 0; i < 10; i++) {
System.out.println("线程:" + Thread.currentThread().getName() + " I:" + i);
if (i == 5) {
Thread.yield();
}
}
};
Thread t1 = new Thread(runnable, "T1");
Thread t2 = new Thread(runnable, "T2");
t1.start();
t2.start();
}
当你多次执行这段代码的时候,会发现有时候执行结果会不一样,这是因为如上所说,它是不稳定的。
sleep方法
sleep 也是本地 (native) 方法,意思是当前线程会沉睡多久,但是沉睡时不会释放锁资源,所以沉睡时,其它线程是无法得到锁的。
interrupt(),interrupted() 和 isInterrupted() 的区别
interrupt():将调用该方法的对象所表示的线程标记一个停止标记,并不是真的停止该线程。
interrupted():获取当前线程的中断状态,并且会清除线程的状态标记。是一个是静态方法。
isInterrupted():获取调用该方法的对象所表示的线程的中断状态,不会清除线程的状态标记。是一个实例方法。
简单的中断案例:t1先执行,但是sleep不释放锁资源,在这期间t2等候两秒钟还没拿到锁就中断
public class Test {
static ReentrantLock lock = new ReentrantLock();
public static void main(String[] args) throws InterruptedException {
Thread t1 = new Thread(() -> {
testsync();
},"t1");
Thread t2 = new Thread(() -> {
testsync();
},"t2");
t1.start();
TimeUnit.SECONDS.sleep(2);
t2.start();
TimeUnit.SECONDS.sleep(2);
System.out.println("main");
/**
* 如果t2两秒钟还拿不到就中断
*/
t2.interrupt();
}
public static void testsync(){
try {
/**
* lockInterruptibly 和 lock的区别,前者会直接抛出异常可以响应中断,后者则不可以
*/
lock.lockInterruptibly();
System.out.println(Thread.currentThread().getName());
TimeUnit.SECONDS.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
lock.unlock();
}
}
}
总结
这篇文章简单介绍了线程的状态以及线程的执行流程,还介绍了BLOCKED(阻塞等待)和WAITING(等待)的区别,start()方法和run()方法的区别,以及thread的几个常见方法
看完这篇文章你能回答出这些问题了吗?
- BLOCKED(阻塞等待)和 WAITING(等待)有什么区别?
- start() 方法和 run() 方法有什么区别?
- 线程的优先级有什么用?该如何设置?
- 线程的常用方法有哪些?
- 怎么中断线程?