基于这样的事实,线程不是一直并行执行的,比如当某个线程需要很长时间的读写操作,这个时候则需要线程调度,即线程间切换;
一、start()
启动一个新线程,在新的线程中运行run方法中的代码,start方法只是让线程进入就绪状态,里面代码不一定立刻执行(比如CPU的时间片还没分给它)。每个线程对象的start方法只能调用一次,否则会报IllegalThreadStateException,直接在main方法中不创建线程,而是创建run方法再调用只是在调用普通方法,而不是开启了另一个线程,故启动线程一定要用start方法,再由新的线程去执行run()方法。
输出结果:
可以看出start之间仅仅是存在一个新进程,start之后才是真正开启了一个线程。
二、sleep()
- 调用sleep会让当前线程从Running进入Timed Waiting状态(阻塞)
- 其它线程可以使用interrupt方法打断正在睡眠的线程(叫醒),这sleep方法会抛出InterruptedException
- 睡眠结束后的线程未必会立刻得到执行,要等待cpu调度分配时间片给线程时该线程才会执行
- 建议用TimeUnit的sleep代替Thread的sleep来获得更好可读性
1. 线程进入sleep状态
当刚开启线程t1线程时,线程处于Runnable状态,即为可运行但是还没运行状态(就绪状态),当继续执行主线程时候,线程t1开始执行,主线程休眠0.5秒,t1线程休眠2秒,当主线程休眠结束后,线程t1仍在休眠,所以打印出的状态是TIMED_WAITING,注意sleep方法写在哪个线程中,就对哪个线程休眠.
2. 使用interrupt打断sleep
3. sleep可读性
与Thread.sleep()方法等价,TimeUnit.SECONDS.sleep()方法也是让当前线程睡眠,只不过精度单位是秒级,当然也可以指定分钟级,
即TimeUnit.MINUTES.sleep();
4. sleep应用
有些应用场景需要While(True)循环,比如保持服务器端程序一直运转,这个时候则需要在线程中加入sleep()方法防止CPU空转
public class Test2 {
public static void main(String[] args) {
Thread task = new Thread(){
@Override
public void run() {
while (true){
try {
Thread.sleep(500);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
};
task.start();
}
}
三、yield()
调用yield会让当前线程从Running进入Runnable就绪状态,然后调度执行其它线程,
具体的实现依赖于操作系统的任务调度器,有可能出现想让出CPU但没有让出去的情况.
yield和sleep的区别: sleep后线程是进入阻塞状态,相当于除了CPU之外仍有条件未满足,比如sleep就是睡眠时间还没达到.而yield后线程是进入就绪状态,即只差cpu资源就进入运行态,所以就绪态是可以分配时间片的,也可以立马被cpu调度,这是两者最大的区别.
四、线程优先级:setPriority()
- 线程优先级会提示(hint)调度器优先调度该进程,但它仅仅是一个提示,调度器可以忽略它。
- 如果CPU比较忙,那么优先级高的线程会获得更多的时间片,但CPU空闲时,优先级几乎没作用;
如果创建两个线程,线程1之中正常运行,线程2中使用yield方法将线程空出cpu使用权(变成就绪态,但仍然可以被CPU调度,且是否调度由操作系统决定),可以看到CPU整体上是向着线程1偏离,即线程1会更多时间使用CPU。
弹幕里出现了这样的问题:多核cpu为什么不可以并行执行线程?还要这样用CPU来回切换线程执行?
答:CPU核心数一共就那么多,如果CPU只跑一个线程,那么可能确实这个进程的线程能够并行地运行,但主机有那么多的进程,CPU要在进程之间来回调度(注意进程只能并发不能并行,除非多处理器CPU),再加上进程内线程间的运行进度肯定不一样(比如某个进程执行io操作),这样即说明进程和线程的调度是很复杂的,每个时候的情况都不同。所以说线程一般情况下肯定是不能并行执行,仍然需要CPU在进程间来回调度。
<br />
给线程设置优先级,setPriority():
此方法给线程设置优先级, 即CPU有更大概率调度高优先级的线程,注意设置优先级要在启动线程(start)之前完成。
五、join()
下列代码最后输出r=0,分析:
- 因为主线程和线程t1是并行执行的,t1线程需要1秒之后才能算出r=10;
而主线程一开始就要打印r的结果,所以只能打印出r=0;
public class Test2 {
static int r = 0;
public static void main(String[] args) {
test1();
}
public static void test1(){
Thread t1 = new Thread(()->{
try {
Thread.sleep(1);
r=10;
} catch (InterruptedException e) {
e.printStackTrace();
}
});
t1.start();
System.out.println(r);
}
}
故引入join()方法:等待线程结束,会等待调用join方法的线程对象执行完毕。实际上是与本线程与调用join方法的线程同步,即等待执行完成。
上述例子改成如下,则r=10:
public class Test2 {
static int r = 0;
public static void main(String[] args) {
test1();
}
public static void test1(){
Thread t1 = new Thread(()->{
try {
Thread.sleep(1);
r=10;
} catch (InterruptedException e) {
e.printStackTrace();
}
});
t1.start();
t1.join();//主线程到join这儿即卡住,必须等待t1线程执行完毕后,才可以继续执行;
System.out.println(r);
}
}
join方法-同步应用:
从调用者角度看,如果当前线程需要等待某一线程执行结束,则称为同步,否则称为异步。
public class Test2 {
static int r1=0;
static int r2=0;
public static void main(String[] args) throws InterruptedException {
test1();
}
public static void test1() throws InterruptedException {
Thread t1= new Thread(()->{
try {
Thread.sleep(1);
r1=10;
} catch (InterruptedException e) {
e.printStackTrace();
}
});
Thread t2 = new Thread(()->{
try {
Thread.sleep(2);
r2=10;
} catch (InterruptedException e) {
e.printStackTrace();
}
});
long start = System.currentTimeMillis();
t1.start();
t2.start();
//等待t1和t2执行完毕
t1.join();
t2.join();
long end = System.currentTimeMillis();
System.out.println(end-start);
}
}
这里t1.join()和t2.join()是在主线程调用,所以主线程要等待两个分线程结束后再执行.
还是有老生常谈的疑问,注意多核情况下线程1和线程2是并行执行的(理想状态下,实际上应该也是并行的,因为CPU分给了当前进程,那么核就可以分给线程并行执行,没什么问题),所以时间差是2秒而不是3秒.
注意:如果是单核CPU,总的运行时间差也是2秒,因为当CPU分给t1线程后,t1线程会执行sleep()即进入阻塞态,这时候CPU会立马切换到t2线程执行,当t2调用sleep方法时,cpu此时空转,因为主线程要等待分线程执行,此时CPU没有可执行的线程,当线程t2睡眠结束后,r1和r2再赋值完成,所以时间总共是2秒钟。、
join方法-限时同步:
可以设置join方法等待的时间,如果超过等待的时间线程仍未结束,则不再等待该线程执行完毕,继续运行当前线程。
public class Test2 {
static int r2=0;
public static void main(String[] args) throws InterruptedException {
test1();
}
public static void test1() throws InterruptedException {
Thread t1= new Thread(()->{
try {
Thread.sleep(2000);
r2=10;
} catch (InterruptedException e) {
e.printStackTrace();
}
});
long start = System.currentTimeMillis();
t1.start();
//主线程等待t1线程执行1.5秒,如果1.5秒后线程仍未结束,则不再等待,转而继续执行当前主线程。
t1.join(1500);
long end = System.currentTimeMillis();
System.out.println(end-start);
}
}
如果join设置的等待时间大于线程实际执行时间,不会按照最大时间等待,而是按照实际时间运行,例如:
import javax.swing.table.TableRowSorter;
public class Test2 {
static int r2=0;
public static void main(String[] args) throws InterruptedException {
test1();
}
public static void test1() throws InterruptedException {
Thread t1= new Thread(()->{
try {
Thread.sleep(2000);
r2=10;
} catch (InterruptedException e) {
e.printStackTrace();
}
});
long start = System.currentTimeMillis();
t1.start();
//t1线程2秒执行完毕,主线程不会等待3秒,而是当t1线程执行结束后,主线程立马不再等待,开始执行
t1.join(3000);
long end = System.currentTimeMillis();
System.out.println(end-start);
}
}
六、interrupt()
打断标记:
每个线程拥有一个打断标记,如果标记为真代表该线程在被打断,但什么时候打断由该线程自己控制,如果标记为假,表示该线程处于未被打断状态。注意以下两种打断情况的区别,陷入【WAITING】和【TIMED-WAITING】状态的线程被打断会抛出打断异常,直接进入catch块处理后事,而正常运行状态下的线程会将打断标志置为真,至于怎样中断由线程内部自己实现。
打断阻塞:
这里特指打断进入Waiting/Time_waiting等状态的阻塞线程,如打断执行sleep、wait、join的线程。
当打断此种线程时,清空打断标记,将打断标记置为FALSE值,同时抛出InterruptedException。
打断正常:
打断正常运行的线程,不会清空打断标记,打断标记实际是一个打断信号,将打断标记置为TRUE,告诉被打断线程,你要被打断了,但实际不会执行打断操作。
所以当打断正常执行的线程时,并不会立刻终止被打断线程的执行,而是交由被打断线程去判断应该什么时候终止自己,线程仍然处于正常执行状态。
例如这种情况,打断后程序仍会继续执行,并不会打断线程t1的执行,只是将线程的打断标志置了TRUE;
import javax.swing.table.TableRowSorter;
import java.security.KeyStore;
public class Test2{
public static void main(String[] args) throws InterruptedException {
Thread t1 = new Thread(()->{
while(true){
System.out.println("1111");
}
},"t1");
t1.start();
//注意这里主线程要睡眠,一会儿否则进程t还没开始,主线程就要去打断线程,这并不会执行任何操作
Thread.sleep(1000);
t1.interrupt();
}
}
这种情况,在t1线程内部通过判断打断标记的真假,自己决定什么时候中断线程;
public class Test2{
public static void main(String[] args) throws InterruptedException {
Thread t1 = new Thread(()->{
while(true){
boolean interrupted = Thread.currentThread().isInterrupted();
if (interrupted){
System.out.println("被打断了,退出循环");
//跳出循环,即t1线程运行结束。
break;
}
}
},"t1");
t1.start();
//注意这里主线程要睡眠,一会儿否则进程t还没开始,主线程就要去打断线程,这并不会执行任何操作
Thread.sleep(1000);
t1.interrupt();
}
}
设计模式-两阶段终止:
两阶段终止模式(Two Phase Termination):
在一个线程T1中如何优雅终止线程T2?这里的【优雅】指的是给T2一个料理后事的机会。
1.错误思路
使用线程对象的stop()方法停止线程
stop方法会真正直接杀死线程,如果这时线程锁住了共享资源,那么当它被杀死后就再也没有机会释放锁,其它线程将永远无法获取锁;
使用System.exit(int)方法停止线程
目的仅仅是停止一个线程,但这种做法会让整个进程都停止运行;(**自爆卡车,全军覆没**)
2.两阶段终止模式
首先是一个while(true)循环,根据打断标记判断是否被打断,如果打断标记是TRUE,则开始料理后事,释放一些相关共享资源等,然后结束循环,线程结束。如果打断标记是FALSE,表示目前还没有被打断,则进入睡眠状态2秒,若在sleep时无异常,那么睡眠后继续执行监控,在执行监控记录过程中如果被打断,会将打断标记记为TRUE,在下次循环的时候跳出循环,若在sleep时有异常,则抛出InterruptedException,因为InterruptedException会将打断标记置为FALSE值,这里需要重新设置打断标记为TRUE,以在下次循环的时候能够跳出循环。
两阶段终止代码实现:
注意细节,如果在情况1被打断,此时线程在sleep,即阻塞状态,那么打断后会抛出异常进入catch块,需要重新设置打断标记;
如果在情况2以及其它点被打断,那么会默认将打断标记置为TRUE,下次循环的时候则会跳出循环。
public class Test3 {
public static void main(String[] args) throws InterruptedException {
TwoPhaseTermination tpt = new TwoPhaseTermination();
//创建线程,监控该线程状态,
tpt.start();
Thread.sleep(3500);
//停止线程,但优雅地停止,选择interrupt线程;
tpt.stop();
}
}
//监控类
class TwoPhaseTermination{
private Thread monitor;
//启动监控线程
public void start(){
//java基础,调用start方法,这里的monitor即指当前对象的monitor属性,与this.monitor等价
monitor = new Thread(()->{
while(true){
Thread current = Thread.currentThread();
if(current.isInterrupted()){
System.out.println("料理后事");
break;
}
try {
Thread.sleep(1000);//打断情况1
System.out.println("执行监控记录");//打断情况2
} catch (InterruptedException e) {
e.printStackTrace();
/*
只要是在sleep阶段被打断,即情况1时候被打断,会进入catch块
所以要重新设置打断标记为真
*/
//重新设置打断标记为真
current.interrupt();
}
}
});
monitor.start();
}
// 停止监控线程
public void stop(){
monitor.interrupt();
}
}
isInterrupted()和interrupted()方法区别:
isInterrupted:判断当前线程是否会被打断(实际上就是判断打断标记是TRUE还是FALSE),并不会清除打断标记。
interrupted:不仅判断当前线程是否会被打断,但会清除打断标记。
打断park线程:
LockSupport.park()方法:当前线程调用park方法会被挂起,可以被interrupt方法打断,打断后线程可以正常执行,相当于是正常运行的线程被interrupt,只是将打断标记置为TRUE。
当前线程如果打断标记为假,则park方法生效,如果打断标记为真再调用park方法,则线程会一直执行无法停下。
注意挂起状态可认为是暂时被淘汰出内存的状态,和阻塞状态要区分开。
七、主线程与守护线程
默认情况下,JAVA进程需要等待所有线程都运行结束,才会结束(这句话约等于废话,进程是指令序列,而线程是分指令序列,线程不执行完毕进程一定不可能结束),有一种特殊的线程叫做守护线程,只要其它非守护线程运行结束了,即使守护线程的代码没有执行完,也会强制结束。
示例,主线程可以先结束,其它副线程还在继续运行,这样是可行的:
public class Test4 {
public static void main(String[] args) throws InterruptedException {
Thread t1 =new Thread(()->{
while(true){
if(Thread.currentThread().isInterrupted()){
break;
}
}
},"t1");
t1.start();
Thread.sleep(1000);
System.out.println("结束");
}
}
可以看到主线程已经执行完毕,t1线程仍在空转,被打断才会执行完毕。
可以将t1线程通过setDaemon()方法设置为守护线程,这样当主线程运行结束后,线程t1也会被强制结束。
代码如下:
public class Test4 {
public static void main(String[] args) throws InterruptedException {
Thread t1 =new Thread(()->{
while(true){
if(Thread.currentThread().isInterrupted()){
break;
}
}
},"t1");
t1.setDaemon(true);
t1.start();
Thread.sleep(1000);
System.out.println("结束");
}
}
setDaemon方法一定是在线程启动之前设置,即start方法之前。程序正常结束,可以看到t1线程确实被强制结束。
注意:
- GC线程就是一种守护线程,当其它功能线程已经执行完毕后,GC线程无用处则会自动结束;
- Tomcat中的Acceptor和Poller线程都是守护线程,所以Tomcat接收到shutdown命令后,不会等待它们处理完当前请求;