什么是中断线程&等待线程?

        在上一篇中我们探讨了进程&线程的区别,我们算是正式接触了线程,在Java中我们主要研究多线程相关的知识,那么这一篇小玉将讲述线程的更多知识点,玉粉们可以根据目录看自己想看的.....那么在阅读这篇文章之前希望大家时时默念一句话:线程的调度是随机的无序的,线程的调度是随机的无序的,线程的调度是随机的无序的.......

什么是中断线程&等待线程?_第1张图片


目录

1.线程的中断 

*变量捕获 

*Java中自带的标志位

2.线程的等待 

*关于寄存器和内存


1.线程的中断 

        我们学会了如何创建一个线程,那么如何中断线程呢?这里我们需要引入一个概念叫:"标志位". 例如一下代码:

public class ThreadDemo6 {
    public static boolean isQuit = false;

    public static void main(String[] args) {
        Thread t = new Thread(()->{
           while (!isQuit){
               System.out.println("hello t ");
               try {
                   Thread.sleep(1000);
               } catch (InterruptedException e) {
                   e.printStackTrace();
               }
           }
            System.out.println("线程中断");
        });

        t.start();

        try {
            Thread.sleep(4000);//主线程休眠4s,然后修改成员变量isQuit的值
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        isQuit = true;

    }
}

结果:

什么是中断线程&等待线程?_第2张图片

这里小玉希望大家思考一个问题:我们的变量isQuit是作为成员变量来作用的,那如果我们让它当局部变量可以吗?  例如:

什么是中断线程&等待线程?_第3张图片

*变量捕获 

这里大家要注意一件事:变量捕获!!!


我们将鼠标放在红线处可以发现:
什么是中断线程&等待线程?_第4张图片

译为:lambda表达式中使用的变量应为final或实际上为final 

lambda表达式虽然可以访问到外面的局部变量,但是必须是final或者实际上为final!! 
什么叫实际上为final?  那就是我们虽然定义了一个变量,但是我们不尝试修改它的值,但是我们在这里例子中,最后将isQuit变成了true(35行),那就不再是实际上是final了.......

那Java中有没有更方便的东西,让我们不用自己创建标志位?
答案是有的!我们可以调用  Thread.currentThread().isInterrupted(),这是Java中自带的标志位,例如:

public class ThreadDemo7 {
    public static void main(String[] args) {
        Thread t = new Thread(()->{
           while (!Thread.currentThread().isInterrupted()){
               System.out.println("hello t");
               try {
                   Thread.sleep(1000);
               } catch (InterruptedException e) {
                   e.printStackTrace();
               }
           }
        });
        t.start();

        try {
            Thread.sleep(3000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        t.interrupt();
    }
}

(其中Thread.currentThread()就是获取当前线程,谁调用就是谁,也可以写作:t)
在最后我们调用了一次  t.interrupt();  就相当于把标志位更改了(标志位默认是false).

结果: 

什么是中断线程&等待线程?_第5张图片

我们发现不对啊???? 预期结果应该是,打印三次hello t,之后就结束.但是我们发现三次打印完之后,抛异常了,完了之后还继续打印.....

*Java中自带的标志位

玉粉们注意:这里打印的异常,正是我们在Thread.sleep(1000);时候surround的异常-----中断异常.我们在调用t.interrupt()的时候,发生了一系列的连锁反应:

1.设置原来的标志位FALSE改为TRUE.
2.唤醒sleep()

我们当然希望,调用它的时候,帮我们更改标志位,那第一个反应是我们预期的,那么在进行第二个反应的时候,它会唤醒sleep(),但是sleep()被唤醒之后,它会将标志位重新清空! 也就是将已经被设置好的true标志位清空为FALSE!  这对于whlie循环来说标志位是false取非之后还是true,仍然进入循环.所以才有我们看到的结果.

那么如何修改代码,呈现出我们预期的结果呢?

只需要加上一个break,让循环跳出即可:

public class ThreadDemo7 {
    public static void main(String[] args) {
        Thread t = new Thread(()->{
           while (!Thread.currentThread().isInterrupted()){
               System.out.println("hello t");
               try {
                   Thread.sleep(1000);
               } catch (InterruptedException e) {
                   e.printStackTrace();
                   break;
               }

           }
        });
        t.start();

        try {
            Thread.sleep(3000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        t.interrupt();
    }
}

结果(符合预期版):

什么是中断线程&等待线程?_第6张图片

这里小玉需要提醒一些糊涂的玉粉:
1.我们这里使用的创建线程的方式是lambda表达式,这里表达式()->{} ,大括号的内容就是现成的入口方法,所以我们不用谢run()方法了.
2.注意这里加上break的位置,是在打印异常调用栈的下面,因为我们希望在sleep被interrupt唤醒并且抛异常的时候 ,这时候sleep会清空标志位,我们在这里break就可以跳出循环了.

sleep()方法为什么要清空标志位?

因为线程这时候被interrupt()方法告知自己不需要做更多工作了,可以终止了.sleep()就起到一个收尾机会的作用:当领导告诉你可以下班了,你说好的好的,等我把这个代码写完就是下班....而不是直接拔插销走人...... 这样的话我们就可以对线程进行更好的控制:不是立即终止线程而是等我看看还有没有任务要做我再终止.... 所以interrupt()方法是通知,不是命令~~~

2.线程的等待 

        还记得我开始的时候让你们默念的一句话吗?线程的调度是随机的无序的........好的接下来请牢牢记住这句话!

由于线程的调度是随机的无序的,所以我们对于线程执行任务的结果是不清楚,不可控的.但是在某些特定情况下,我们希望,线程按照我们期望的顺序执行,另外我认为还有一个很重要的原因那就是,当时我们说线程&进程的区别是:线程共用进程的资源(内存和文件描述附表).内存可以共用也是线程不安全的重要原因之一!

如何让线程之间相互有礼貌的等待对方执行完,那么就要用到join()方法了:

public class ThreadDemo9 {
    public static void main(String[] args) {
        Thread t = new Thread(()->{
            System.out.println("hello t");
        });
        t.start();

        System.out.println("hello main");
    }

}

结果:

什么是中断线程&等待线程?_第7张图片

大家多运行几次可以发现:虽然我们说线程之间是随机调度的,是无序的,但是无论大家运行十遍二十遍还是三十遍都会发现main线程好像比t线程先执行.因为任性从它作为主线程。在创建一个新线程的时候是需要有开销的,所以大多数情况下它都是会比新线程要更慢的执行的,但是我们不排除。如果电脑卡了或者说一些额外的因素也有可能....

接下来我们加入join() 方法:

什么是中断线程&等待线程?_第8张图片

注意是在启动方法之后加入,那么我们能看到结果一定是先打印hello t ,无论运行多少次都是hello t先于hello main.


我们需要注意一点就是,在主线程中调用这个方法是让主线程等待新线程执行.
那么除了这两个线程有等待的关系之外其他线程不受影响正常随机调度只有这两个线程有等待关系.
 

那么join() 方法的作用就当一个线程没有执行完成的时候,另一个线程进入阻塞(BLOCKING)状态,等待它执行完毕在继续执行.

我们再用一段代码案例来演示:

class Counter{
    public static int count;
    public void add(){
        count++;
    }
    public int getCount(){
        return count;
    }
}
public class ThreadDemo8 {
    public static void main(String[] args) throws InterruptedException {
        Counter counter = new Counter();
        Thread t1 = new Thread(()->{
            for (int i = 0; i < 50000; i++) {
                counter.add();
            }
        });

        Thread t2 = new Thread(()->{
            for (int i = 0; i < 50000; i++) {
                counter.add();
            }
        });

        t1.start();
        t2.start();
        t1.join();
        t2.join();
        System.out.println(counter.getCount());
    }
}

这段代码是让t1 t2两个线程对count这个变量各自增5w次,那么加上join之后我们预期输出10w.来看结果:

什么是中断线程&等待线程?_第9张图片

多次运行结果来看:似乎每次的值都不一样,但是都没有达到10万。看起来更像是一个随机值(5w,10w).我们明明已经加入了join()方法,两个线程难道不等对方增加吗??

*关于寄存器和内存

 这里我们要明确一点内存和寄存器这两个概念,变量都是在内存上的,当我们修改这个变量的时候,由于访问寄存器的速度比访问内存速度快了好几个数量级,【内存大而慢,寄存器小而快,看到过一个非常恰当的比喻:书架是外存,办公桌是内存,桌上的随身记是寄存器】所以我们会将变量先从内存读取到寄存器,然后在寄存器上进行操作,最后再写回内存.我们看图理解吧:

什么是中断线程&等待线程?_第10张图片

        什么由图我们可以看到?每一步对于变量的修改都会分成三步: 那就是加载->修改->写回。 那么现在带入到我们多线程的思想里,我们有两个线程在同时对一个变量进行修改,那么就会产生如下图解:先来看线程t1如何操作的:

什么是中断线程&等待线程?_第11张图片

那么线程t2也是如此:

什么是中断线程&等待线程?_第12张图片

但是我们需要再默念一遍,我让你们牢记的话,那就是线程之间的调度是随机的,无序的。那么我们来看看两个线程在无序调度状态下是发生怎样的混乱。 

当两个线程的load add save"听话"的情况下是这样的: 也只有在这两种情况下他们不会发生线程安全问题无论怎样调度他们都是能各自自增五万次.

 什么是中断线程&等待线程?_第13张图片

不听话的情况下: 

什么是中断线程&等待线程?_第14张图片

那么当他们的三部曲被调度拆开的时候,他们读取的内存变量就会发生变化,有可能会产生无效覆写。 我们拿不听话情况的第一种距离,我们将两个线程的load,add,save,进行编号统共是6步:

什么是中断线程&等待线程?_第15张图片

什么是中断线程&等待线程?_第16张图片

我们注意在第二次load的时候,线程一还没有修改,更没有将修改完的变量写回,所以他们load的都是同一个值。那么在经历过第一遍操作的时候两次修改,实际上只修改了一次。就会产生无效覆写。 其他的情况亦是如此.所以我们将这样的情况称之为------线程不安全.


        好了小玉先讲这么多,接下来我们将进入线程安全问题的讨论,以后也是很重要的部分,在面试中也会经常问到,我叫各位读者玉粉,没问题吧?希望小玉的文章真的帮到你,请多多鼓励,小玉会创作出更优质的内容!

什么是中断线程&等待线程?_第17张图片

你可能感兴趣的:(开发语言,java)