在java中,很多时候我们忽略的基本的知识,这是很致命的,只有搞懂Thread的基础知识,才能进一步探索:reentrantLock,AQS等。
1:Thread的线程状态到底有几种?
很多博文写的并不准确,我的答案是6种:(注意:没有所谓的就绪状态(prepare),这个状态是很多人在推断多线程执行过程自己yy的,臆想的)
public enum State {
/**
* Thread state for a thread which has not yet started.
尚未启动的线程的线程状态。
*/
NEW,
/**
* Thread state for a runnable thread. 可运行线程的线程状态。
*/
RUNNABLE,
/**
* A thread in the blocked state is waiting for a monitor lock
* to enter a synchronized block/method or
* reenter a synchronized block/method after calling
* 等待获取锁的状态,或者等待获取重入锁的状态
*/
BLOCKED,
/**
* Thread state for a waiting thread. 线程等待状态
调用以下方法都可以使一个线程进入等待状态 -》等待后必须通过notify(),notifyAll()才能唤醒,唤醒后才能重新去尝试获取CPU的执行权
* <ul>
* <li>{@link Object#wait() Object.wait} with no timeout</li>
* <li>{@link #join() Thread.join} with no timeout</li>
* <li>{@link LockSupport#park() LockSupport.park}</li>
* </ul>
*/
WAITING,
/**
* Thread state for a waiting thread with a specified waiting time.
* 有期限的等待,超时等待。以下方法:
* sleep Thread.sleep
* Object#wait(long)
* join(long) Thread.join
* LockSupport#parkNanos LockSupport.parkNanos
* LockSupport#parkUntil LockSupport.parkUntil
*/
TIMED_WAITING,
/**
* Thread state for a terminated thread.
* 线程结束
*/
TERMINATED;
}
以上源码就解释了什么是阻塞,等待,但是没有挂起,为什么没有挂起状态,因为线程挂起这种操作已经过时,不建议使用了,这里不做过多讨论,有人愿意研究的,请自行搜索:线程挂起。不要搜跟阻塞,等待的区别,很多都说错了。
2:到底如何区分阻塞,等待?
一切回归原始:假设一个场景:多个线程任务执行在同一个单核cpu上,在我们获取临界资源的时候,为了线程安全,是要加锁的,假设锁是synchronized,那么同一时刻,只有一个线程任务能获取锁资源,那么其它的线程就进入了:blocked状态,等待获取锁的状态,如上代码BLOCKED, 此时,这些没有获取到锁的线程有一些条件:1:他们都是可以随时被CPU上下文切换获取到执行权的。2:他们虽然可以获取CPU执行的时间片,但是他们无法获取锁。所以被阻塞在这里了。
基于以上两点,那么他们处于阻塞状态。而第一个获取到锁的线程,是runnable状态。
如果有大量线程为了竞争同一把锁(同一临界资源)而发生大面积阻塞,就会形成线程饥饿。
那什么是等待?还是上面的场景,加入有一个线程(第一个线程)获取到了锁,那么其它任何线程尝试获取锁都会失败,一旦失败,调用方法让他们进入等待的话,他们就不能再获取到CPU执行权了,跟上面所说的阻塞特征的第一点冲突,另外,他们也不会一直尝试去获取锁了,因为他们拿不到CPU执行权,相当于一个瘫痪的人没有CPU的帮扶,是无法站起来的。 那么他们如何才能站起来,继续有机会获取CPU执行权,进而获取锁呢? --唤醒-》notify。 所以,假如一个线程是wait状态已经,如果没有任何线程去唤醒它,那它永远是死的,如果大量的线程一旦永无机会被唤醒的话,将会占用大量内存。
引申思考:lockSupport的park()与unpark()详解:
翻看源码我们可以知道:lockSupport类里面的方法都是两个一起出现的,一个有参,一个无参的。(重载),那么为什么呢?
public static void park(Object blocker); // 暂停当前线程
public static void parkNanos(Object blocker, long nanos); // 暂停当前线程,不过有超时时间的限制
public static void parkUntil(Object blocker, long deadline); // 暂停当前线程,直到某个时间
public static void park(); // 无期限暂停当前线程
public static void parkNanos(long nanos); // 暂停当前线程,不过有超时时间的限制
public static void parkUntil(long deadline); // 暂停当前线程,直到某个时间
public static void unpark(Thread thread); // 恢复当前线程
public static Object getBlocker(Thread t);
有参函数都有一个blocker,这个blocker是用来记录线程被阻塞时被谁阻塞的。用于线程监控和分析工具来定位原因的。我们分析dump文件的时候,如果有这个blocker就可以定位到具体那个地方,而没有就不一样。
与wait()和notify()、notifyAll() 区别:
(1)wait和notify都是Object中的方法,在调用这两个方法前必须先获得锁对象,但是park不需要获取某个对象的锁就可以让此线程进入等待状态。
(2)notify只能随机选择一个线程唤醒,无法唤醒指定的线程,unpark却可以唤醒一个指定的线程。
(3)根据第一条wait在获取到锁对象以后,再执行,再自动释放锁的。而park如果在锁内部(即同步代码块内)进行执行,不会释放锁,因为它跟sleep一样,不涉及锁。验证如下:
public class ThreadWaitDemo {
private static final Object obj = new Object();
public static void main(String[] args) throws InterruptedException {
Thread t01 = new Thread(() -> {
synchronized (obj) {
System.out.println("运行中: Thread.currentThread().getName() = " + Thread.currentThread().getName() + "获取到了锁");
LockSupport.parkNanos(9000000000L);//9秒
System.out.println("t01线程运行结束,释放锁----");
}
}, "t01");
t01.start();
TimeUnit.SECONDS.sleep(1);
System.out.println("t01.getState() = " + t01.getState());
System.out.println("main线程尝试去获取锁----------");
synchronized (obj) {
System.out.println("main线程获取到锁");
}
System.out.println("结束运行: Thread.currentThread().getName() = " + Thread.currentThread().getName());
}
运行结果:
运行中: Thread.currentThread().getName() = t01获取到了锁
t01.getState() = TIMED_WAITING
main线程尝试去获取锁----------
t01线程运行结束,释放锁----
main线程获取到锁
结束运行: Thread.currentThread().getName() = main
所以,通过以上代码验证,我们知道在同步代码块,也就是在锁中使用locksupport是不合理的,应尽量避免这样的操作,以免产生线程永远无法唤醒的bug。
3:wait() 和 join() 的区别?
从第一段博文我们知道,这两个方法可以让线程进入wait状态,他们的区别?基于以上两点,查看API,不难学会如何使用。这里不啰嗦讲解,不懂的朋友可以去查。
这里主要说:join方法的底层执行? 看如测试用例: ->
两个红色的框分别说明了sleep方法前后test01的状态,这个不是重点,只是一个验证而已。
重要的是test02的状态,调用join方法后,test02变成了WAITING状态,为什么?
看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) {
while (isAlive()) {
//注意这里:join方法的底层还是调用了wait()
wait(0);
}
} else {
while (isAlive()) {
long delay = millis - now;
if (delay <= 0) {
break;
}
wait(delay);
now = System.currentTimeMillis() - base;
}
}
}
join方法加了锁,锁的是谁?test01,谁持有锁?test02,搞清楚这两个地方后,那么:
我们可以看出:是test02线程在调用 join() -> wait(), 所以test02会进入WAITING状态。直到test01执行完,自动调用Thread.exist()方法,在exist()方法中,test02会被唤醒。具体细节如何,请自行百度exist()即可。