一 线程状态转换图
Java中线程从创建到最终消失,要经历以下几个状态,
NEW ,线程还没启动
RUNNABLE 线程运行在jvm中,但是在等待cpu
BLOCKED 线程在等待monitor lock
WAITING, 一个线程等待另一个线程的特定操作结果,一般是由于调用了如下方法wait(),join(),park()方法,没有time
一个线程在这个状态由于等待时间,一般是由于线程调用了下面方法
Thread.sleep()
Object.wait with timeout
Thread.join with timeout
LockSupport.parkNanos
LockSupport.parkUntil
TIMED_WAITING,
线程执行结束
TERMINATED;
}
- 阻塞和等待的区别
阻塞: 当一个线程试图获取对象锁(非java.util.concurrent库的锁,即synchronized),而该锁被其他线程持有,则这个线程进入阻塞状态。它的特点是使用简单,由JVM调度器来决定唤醒自己,而不需要由另一个线程显示唤醒自己,不响应中断。
等待:当一个线程等待另一个线程通知调度器一个条件时,该线程进入等待状态。它的特点是需要等待另一个线程显示的唤醒自己,实现灵活,语义丰富,可响应中断。例如调用:Object.wait(),Thread.join(),以及等待Lock,或Condition.
这个地方sychronized和JUC里面的lock都实现锁的功能,但线程进入状态是不一样的。synchronzied会让线程进入阻塞态,而JUC里的lock是用LockSupport.park()/unpart()来实现阻塞/唤醒的,会让线程进入等待状态,这里虽然等待锁进入的状态不一样,但是被唤醒后又进入runnable态,从行为效果来看又是一样的。
二 线程切换
对于单核CPU来说(对于多核CPU,此处就理解为一个核),CPU在一个时刻只能运行一个线程,当在运行一个线程的过程中转去运行另外一个线程,这个叫做线程切换(对于进程也是类似)。
由于可能当前线程的任务并没有执行完毕,所以在切换时需要保存线程的运行状态,以便下次重新切换回来时能够继续切换之前的状态运行。举个简单的例子:比如一个线程A正在读取一个文件的内容,正读到文件的一半,此时需要暂停线程A,转去执行线程B,当再次切换回来执行线程A的时候,我们不希望线程A又从文件的开头来读取。 因此需要记录线程A的运行状态,那么会记录哪些数据呢?因为下次恢复时需要知道在这之前当前线程已经执行到哪条指令了,所以需要记录程序计数器的值,另外比如说线程正在进行某个计算的时候被挂起了,那么下次继续执行的时候需要知道之前挂起时变量的值时多少,因此需要记录CPU寄存器的状态。所以一般来说,线程上下文切换过程中会记录程序计数器、CPU寄存器状态等数据。
说简单点的:对于线程的上下文切换实际上就是 存储和恢复CPU状态的过程,它使得线程执行能够从中断点恢复执行。
虽然多线程可以使得任务执行的效率得到提升,但是由于在线程切换时同样会带来一定的开销代价,并且多个线程会导致系统资源占用的增加,所以在进行多线程编程时要注意这些因素。
三 Thread 类API
- start() 这个方法使线程开始执行,由jvm调用这个线程的run方法,这个方法使得产生两个线程,一个当前线程(运行start的),一个运行run的线程。
public synchronized void start() {
/**
* This method is not invoked for the main method thread or "system"
* group threads created/set up by the VM. Any new functionality added
* to this method in the future may have to also be added to the VM.
*
* A zero status value corresponds to state "NEW".
*/
if (threadStatus != 0)
throw new IllegalThreadStateException();
/* Notify the group that this thread is about to be started
* so that it can be added to the group's list of threads
* and the group's unstarted count can be decremented. */
group.add(this);
boolean started = false;
try {
start0();
started = true;
} finally {
try {
if (!started) {
group.threadStartFailed(this);
}
} catch (Throwable ignore) {
/* do nothing. If start0 threw a Throwable then
it will be passed up the call stack */
}
}
}
- run()
run()方法是不需要用户来调用的,当通过start方法启动一个线程之后,当该线程获得了CPU执行时间,便进入run方法体去执行具体的任务。注意,继承Thread类必须重写run方法,在run方法中定义具体要执行的任务。
- sleep()
sleep使得线程休眠,交出cpu的执行权,sleep方法不会释放锁,也就是说如果当前线程持有锁,即使调用sleep方法,其他线程也无法访问这个对象。
- setDaemon(boolean on),将此线程标记为 守护线程。 当运行的线程都是守护进程线程时,Java虚拟机将退出。 在启动线程前调用这个方法。
引用场景:为了维护连接发送心跳包的线程。java种的gc线程也是守护线程,
- yield()
调用yield方法会让当前线程交出cpu权限,让cpu去执行其他线程,但是可能cpu还是会执行这个线程,yield方法只是让其他线程有获取cpu执行时间的机会。和sleep方法一样,不会释放锁。
这里yield方法并不会让线程进入阻塞状态,而是让线程重回runnalble状态,它只需要重新获取cpu执行时间,这一点和sleep方法不一样的。
- join()
jion()实际上是利用 了wait()。只不过它不用等到notify/notifyAll,它的结束条件是
(1) 等待时间到(2)目标线程已经运行完(通过isAlive()来判断)
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");
}
//时间为0,就要一直等待目标线程跑完
if (millis == 0) {
while (isAlive()) {
wait(0);
}
} else { //如果目标线程没运行完,就按照时间等待
while (isAlive()) {
long delay = millis - now;
if (delay <= 0) {
break;
}
wait(delay);
now = System.currentTimeMillis() - base;
}
}
}
-
interrupt()
此操作会将线程的中断标志位置位,至于线程作何动作那要看线程了- 如果线程sleep(),wait(),join()等处于阻塞状态, 那么线程会定时检查中断状态位如果发现中断状态位为true,那么这些阻塞方法会抛出InterruptedException异常,并且在抛出异常后会立即将线程中断状态位清除,即重新设置为false。抛出异常是为了线程从阻塞状态醒过来,并在结束线程前让程序员有足够的时间来处理中断请求。
- 如果线程正在运行,争用synchronized,lock()等,那么是不可中断的,他们会忽略
可以通过以下三种方式来判断中断:
1)isInterrupted()
此方法只会读取线程的中断标志位,并不会重置。
2)interrupted()
此方法读取线程的中断标志位,并会重置。
3)throw InterruptException
抛出该异常的同时,会重置中断标志位。