从零开始的多线程生活——02
上一期的末尾我们聊到了interrupted线程中断,由于篇幅有限我只是简单一提,这一期我将详细为大家介绍讲解一下关于线程中断的知识。
提到线程中断,isinterrupted方法相当重要,通过该方法返回的boolean值可以判断线程中断与否,而主动中断线程则是用interrupt方法,那么请看下一段代码。
package thread.csdn;
public class Demo10 {
public static void main(String[] args) throws InterruptedException {
Thread t=new Thread(()->{
while (Thread.currentThread().isInterrupted()!=true){
System.out.println("hello thread");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
throw new RuntimeException(e);
//break;
}
}
});
t.start();
Thread.sleep(5000);
System.out.println("main线程尝试终止t线程");
t.interrupt();
}
}
这个程序所执行的逻辑是,t线程通过isinterrupted方法判断是否还要继续执行run的逻辑,在主线程中等待五秒后尝试对t线程执行终止操作。这串代码的运行结果在上期末尾我就提过,是无法达到预期效果的,sleep会报一个异常,具体如下。

关于这个异常,上期我也简单提到过,这是因为主线程中的interrupt方法在中断线程的同时也极大概率会唤醒sleep,为什么说是极大概率呢?因为细分一下,t线程中执行的逻辑其实有两个,第一是休眠,第二是打印"hello thread",休眠操作占据1000ms,但输出逻辑或许也就是占据1ms甚至更少,所以当主线程中执行interrupt时大概率会碰到哪个操作自然就不言而喻了。
那么如何解决报错的问题?说到这就要去分析抛异常的原因,在t线程里调用sleep语句我们采用的是try-catch捕获异常,但是这个catch语句捕获异常的逻辑就略微有点微妙,不知道你是否注意到,我们捕获异常的方式是抛出另一个异常,那现在你肯定明白为什么上述代码会抛异常了吧。如何解决呢?是不是我们把catch里的逻辑改掉就可以了,比如我采用的方法就是break,当sleep按照原计划抛出InterruptedEception异常时会被catch捕获,此时再往下执行break就是跳出整个t线程的入口函数了,自然t线程会终止达到了我们预期的效果,代码比较容易,就是把上边抛异常改成break就可以了,这里就不再贴一遍代码了,仅演示一下效果。

不知道看到这里你是否产生了一点疑问,先前我们提过interrupt方法中断线程时还做了一个操作,就是把boolean值设为true,那按照正常逻辑,假设catch语句里什么也不写,下一步进入循环判断条件的环节,也并不符合循环执行的条件,也就是说即使不加这个break也能达到我们想要的效果。逻辑上讲合情合理但事实果真如此吗?


很显然,线程并没有终止,那这又是什么原因?其实吧,能暗箱操作的原因就那么几个,或许你猜也能猜到这又是sleep在从中作梗,当interrupt中断线程时的确会把isinterrupted的标志位boolean值置为true,但sleep也会被提前唤醒,提前唤醒的sleep会把isinterrupted的标志位再次置为false,因此循环的条件得到满足,就会继续进行。


可见,java中线程终止并不是一个强制性的逻辑,不是说main让t终止就终止,最后还是要看t线程自己的逻辑。这也为我们提供了很多思考,当sleep的逻辑提前唤醒后,你是选择线程立即终止、亦或是不终止再或者执行一段逻辑后再终止完全取决于自己,这就为我们提供了很大的操作空间。
那关于线程中断的话题就聊到这里吧。
接下来,我们聊一下线程等待的问题。
大家都知道,cpu是随机调度多线程的,就拿之前几期的代码案例来讲,在t线程打印和在主线程中打印的顺序完全是随机的,这就是操作系统中多线程并行执行的特点,那我就想让一个线程执行完了再执行下一个线程可以吗?当然是没问题的,这就是我们的线程等待问题。
package thread.csdn;
public class Demo11 {
public static void main(String[] args) throws InterruptedException {
Thread t=new Thread(()->{
for (int i = 0; i < 10; i++) {
System.out.println("hello thread");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
});
t.start();
t.join();
System.out.println("t线程执行完毕");
}
}

可见,线程等待用到的是join函数,在上边代码逻辑中,我们是在main线程里执行t.join(),所实现的效果是main线程等待t线程结束后再执行自己的剩余逻辑。我通过jconsole向大家演示一下确实存在等待这个效果。

但是,大家仔细想一想,这样的效果真的好吗?这里t线程的逻辑我也就是进行了10次循环,假如循环次数更多,或者就是一个死循环,那main线程得到什么时候才可以执行自己的逻辑?在t线程结束之前,main线程一直都是阻塞等待。
所以join函数还有一个带参数的版本,我们可以自行指定它的等待时间。
![]()
这就是等待3000ms的意思,到了3000ms还没有结束t线程逻辑main也不会再等了。到了指定时间,就接着并发执行main线程剩余的内容,如果t线程先于等待指定的时间结束,那main线程也不会再等下去,直接就开始执行自己剩余逻辑。
关于这个带参数版本,还有另一种写法。
![]()
这个是把等待时间精确到纳秒,也就是3000ms+500ns的等待时间,但是在日常编程是用不到的,因为随便一条语句的时间开销都是ms级别(尤其是线程本身的调度开销),所以精确到纳秒也无济于事或者说效果十分有限。
再回到sleep,来看这段简单的代码sleep(1000),虽然我这里写的是休眠1000ms,但实际的休眠时间有可能会比1000略多,这里休眠的逻辑是说当执行到这一步,该线程主动放弃cpu资源,并允许其他线程占有该资源完成自己的逻辑,等到1000ms之后并不是说立即拿回cpu资源,而是意味着自己允许被操作系统调度了,然而从这一时刻到cpu真正被分配到该线程的过程也是需要时间的,这就导致了实际休眠时间会略多于1000ms。
关于sleep的特殊写法,sleep(0),这种写法不是休眠零毫秒的意思,而是该线程立即放弃cpu资源,让给其他线程调用。
再来说说线程状态,之前我们跟大家也介绍过线程状态,分别有就绪态和阻塞态,准确来说,这是站在cpu的角度上观察的,而在java中线程也是对操作系统线程的封装,那么自然Java也对线程状态进行了封装,在了解这几个封装后的状态前,我先来讲讲如何获取线程状态。

通过调用这个getState函数,获取线程状态。那么接下来我将为大家介绍6种线程状态。
1.NEW状态,顾名思义,刚刚创建好线程还没有开始执行,对应到代码中,就是还没有调用start。
package thread.csdn;
public class Demo12 {
public static void main(String[] args) {
Thread t=new Thread(()->{
System.out.println("hello thread");
});
System.out.println("调用start函数前:"+t.getState());
t.start();
}
}

在这里补充点,虽然我们这里讲的是多线程,但其实你调用run函数出来的也是NEW状态,因为我们说的是线程状态这和你是不是多线程变成没啥关系。


2.TERMINATED:线程结束,但是Thread对象还在。

![]()
注意,不论是线程中断调用interrupt还是正常结束,线程状态都是TERMINATED,看我下段代码。
package thread.csdn;
public class Demo13 {
public static void main(String[] args) throws InterruptedException {
Thread t=new Thread(()->{
while(!Thread.currentThread().isInterrupted()){
System.out.println("hello thread");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
// throw new RuntimeException(e);
break;
}
}
});
t.start();
Thread.sleep(3000);
t.interrupt();
System.out.println(t.getState());
}
}

3.RUNNABLE:其实就是就绪状态,代表线程可以随时工作,具体分为两种情况,第一是正在cpu上执行,第二是随时可以去cpu上执行,也就是说当你线程new好了之后就会进入这个状态。
4.TIMED_WAITING:指定时间的阻塞,注意这里是阻塞态,但也并不是一直阻塞,而是到了指定时间就停止阻塞。
package thread.csdn;
public class Demo15 {
public static void main(String[] args) throws InterruptedException {
Thread t=new Thread(()->{
while(true){
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
});
t.start();
Thread.sleep(1000);
System.out.println(t.getState());
}
}
![]()
除了sleep以外,带参数的join也是TIMED_WAITING状态。


5.WAITING:跟TIMED_WAITING状态都是阻塞状态,只不过WAITING状态是死等,没有超时时间的阻塞等待。


6.BLOCKED:这种状态也是一种阻塞状态,比较特殊,这是由加锁引起的阻塞,等到后面介绍到锁再去详细讲解。

总结一下这六种线程状态之间关系形如此图。
接下来,开始多线程中的重点部分,即线程安全问题。
package thread.csdn;
public class Demo16 {
private static int count=0;
public static void main(String[] args) throws InterruptedException {
Thread t1=new Thread(()->{
for (int i = 0; i < 50000; i++) {
count++;
}
});
Thread t2=new Thread(()->{
for (int i = 0; i < 50000; i++) {
count++;
}
});
t1.start();
t2.start();
t1.join();
t2.join();
System.out.println(count);
}
}
首先,看这串代码,要实现的逻辑是t1、t2两个线程分别对count自增50000次,然后main线程等待这俩线程结束后再结束线程,按理来说最后拿到的count应该是100000,但事实并非如此。
![]()
![]()
还有不少就不列举了,显然这并不是一个固定的数。那么为什么会出现这样的情况呢?这种情况就称为线程不安全。解决这个问题的方法也很简单,直接把并行改成串行就可以了。

![]()
现在我来解释一下这个问题出现的原因。
其实count++自增逻辑可以分为三步,第一是从内存中读取count,放到寄存器中;第二是把寄存器中存储的值实现+1操作;最后是把寄存器中的值写回内存。
但是,当一个线程执行每一步间隙的时候,都会触发cpu的随机调度,调度到另一个线程去执行同样的三步,我就举一种情况,假设t1你完成了第一步第二步,然后cpu调度到t2,t2去完成这三步,当t2把内存count加载到寄存器时,这是还没有加过的数据,显然t1已经完成自增了,所以这并不合理,而当这三步执行完t1再去把寄存器的值写入内存时写入的只是自增一次的结果,很显然这是错误的。
由于篇幅有限,第二讲就到这里结束了,欲知后事如何,且听下回分解。
AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。
更多推荐



所有评论(0)