0


【Java成王之路】EE初阶第二篇:(多线程2)

前言

接上一篇内容......

中断线程~

中断线程就是让线程结束~(让内核里面的CPU被销毁)

让线程结束的关键,就是让线程对应的入口方法,执行完毕.

入口方法:

1.继承Thread重写的run方法

2.实现Runnable重写run方法

3.lambda

这些都算是它的入口方法,只要入口方法执行完了,线程就随机结束了

像这种情况:

//Thread是 Java 标准库中描述一个关于线程的类
//常用的方法就是自己定义一个类继承 Thread
//重写 Thread 中的 run 方法. run方法就是表示线程要执行的具体任务(代码).
class MyThread extends Thread{
    @Override
    public void run() {
        System.out.println("hello,Thread");
    }
}
public class Test {
    public static void main(String[] args) {
        Thread thread = new MyThread();
        //stat 方法,就会在操作系统中真的创建一个线程出来,(内核中搞个PCB,加入到双链表中)
        //这个新的线程,就会执行 run中所描述的代码
        thread.start();
        //thread.run()

    }
}

像这种情况

像这种情况,只要run执行完,线程就随之结束了

更多情况下,线程不一定这么快就能执行完run方法:

public class TestDome1 {
    public static void main(String[] args) {
        Thread t = new Thread(new Runnable() {
            @Override
            public void run() {
                while (true){
                    //打印当前线程的名字
                    //Thread.currentThread()这个静态方法,获取到当前线程实例
                    //哪个线程调用这个方法,就能获取到当前线程实例
                    System.out.println(Thread.currentThread().getName());
                    try {
                        Thread.sleep(1000);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }

            }
        },"myThread");
        t.start();

        //在这里也打印一下这个线程的属性
        System.out.println("id: " + t.getId() );
        System.out.println("name: " + t.getName());
        System.out.println("state: " + t.getState());
        System.out.println("priority: " + t.getPriority());
        System.out.println("isDaemon" + t.isDaemon());
        System.out.println("isInterrupted: "+t.isInterrupted());
        System.out.println("isAlive " + t.isAlive());
    }

}

如果run里面带的是一个死循环,此时这个线程就会一直持续运行,直到整个进程结束.

实际开发中,并不希望线程run就是一个死循环,更希望能够控制这个线程,按照咱们的需要随时结束.

为了实现这样的效果,就有一些办法:

1.简单粗暴的办法,就是使用一个boolean变量来作为循环结束标记

public class TestDome {
    private static boolean flag = true;
    public static void main(String[] args) throws InterruptedException {
        Thread t = new Thread(){
            @Override
            public void run() {
                while (flag){
                    System.out.println("线程运行中.....");
                    try {
                        Thread.sleep(10000);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
                System.out.println("线程结束!");
            }
        };
        t.start();

        //主循环中也等待个三秒
        Thread.sleep(3000);

        //三秒之后,就把flag 改成 false
        flag = false;
    }
}

2.刚才是使用程序猿自己定义的变量作为循环标记,还可以使用标准库里内置的标记

获取线程内置的标记位:线程的isinterupted()(打断的意思)判定当前线程是不是应该结束循环

修改线程的内置标记位:Thread.interrupt()来修改这个标记位

public class TestDome2 {
    public static void main(String[] args) {
        Thread t = new Thread(){
            @Override
            public void run() {
                //默认情况下isInterrupted(打断的意思)这个方法的值为false(默认是一个未被打断的状态,所以说要取反)
                while (!Thread.currentThread().isInterrupted()){
                    System.out.println("线程运行中...");
                    try {
                        Thread.sleep(1000);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                        //在这里再加个break 就可以保证循环能结束了
                        break;
                    }
                }
            }
        };
        t.start();

        //在主线程中,通过t.interrupt()方法来设置这个标记位
        try {
            Thread.sleep(3000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        //这个操作就是把Thread.currentThread().isInterrupted()给设置成true
        t.interrupt();
    }
}

打印结果:

当我们三秒之后,insterrupt方法貌似并没有修改这个标记位......循环看起来再继续

同时这里还有个异常:

就是这个异常:

interrupt方法并没有修改这个标记位,而是直接抛了个异常

这里的interrupt方法可能有两种行为:

1.如果当前线程正在运行,此时就会修改Thread.currentThread().isInterrupted()标记位为true

2.如果当前线程正在sleep/wait/等待锁.......此时就会触发interruptedException

在这里加上break就可以保证循环结束了

isInterrupted()这个是Thread的实例方法

和这个方法还有一个类似的

interrupted()这个也是Thread的类方法(static)

都能去判断当前的标记位是不是被设置上了

使用两者的区别:

使用这个静态方法,会自动清楚标记位

例如:调用interrupt()方法,把标记位设为true,就应该结束循环

当调用静态的interrupted()来判定标记位的时候,就会返回true,同时会把标记位再改回false,下次再调用interrupted()就返回false

如果是调用非静态的isinterrupted()来判定标记位,也会返回true.同时不会对标记位进行修改.后面再调用isInterrupted()的时候就仍然返回true

线程等待 ~

线程和线程之间,调度顺序是完全不确定的(取决于操作系统调度器自身的实现)

但是有的时候,希望这里的顺序是可控的

此时,线程等待就是一种办法.

这里的线程等待,主要就是控制线程结束的先后顺序~

一种常见的逻辑:有一个t1线程,创建t2,t3,t4.让这三个新的线程分别执行一些任务,然后t1线程最后在这里汇总结果~~

这样的场景我们就需要t1结束时机必须比t2,t3,t4都迟

比如现在有一个线程实例:

Thread t = new Thread()

当我们调用 t.join() 的时候,执行到这个代码,此时调用这个代码的线程就会阻塞等待,代码就不继续往下走了.具体说就是操作系统短时间内不会把这个线程调度到CPU上了

以往的代码,只要代码一写好跑起来,此时就会快速的按照顺序执行...

但是join就会触发阻塞等待

当我们执行start()方法的时候,就会立刻创建出一个新的线程来.

同时main这个线程也立刻往下执行,就执行到t.join

执行到t.join的时候就发现,当前t线程还是在运行中的.....

只要t在运行中,join方法就会一直阻塞等待,一直等到t线程执行结束(run方法执行完了)

public class TestDome3 {
    public static void main(String[] args) throws InterruptedException {
        Thread t = new Thread(){
            @Override
            public void run() {
                int count = 0;
                while (count < 5){
                   count++;
                    System.out.println("线程运行中");
                    try {
                        Thread.sleep(1000);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
            }
        };
        t.start();

        //此处的join就会阻塞等待
        System.out.println("join开始执行");
        t.join();
        System.out.println("join执行结束");
    }
}

查看结果:

假设调用join的时候,t线程已经结束了,会咋样?

public class TestDome3 {
    public static void main(String[] args) throws InterruptedException {
        Thread t = new Thread(){
            @Override
            public void run() {
                int count = 0;
                while (count < 5){
                   count++;
                    System.out.println("线程运行中");
                    try {
                        Thread.sleep(1000);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
            }
        };
        t.start();
        Thread.sleep(7000);

        //此处的join就会阻塞等待
        System.out.println("join开始执行");
        t.join();
        System.out.println("join执行结束");
    }
}

打印结果:

join方法:

join无参数版本:相当于死等.

join有参数版本,参数就是最大等待时间.

在实际开发中,使用死等操作,往往是比较危险的

典型的就是网络编程中

发了一个请求,希望得到一个回应~~

由于种种原因,回应没有到达.....

此时我们的程序就不应该一直死等

获取当前线程引用

getCurrentThread() 能够获取到当前线程对应的Thread实例的引用

public class TestDome4 {
    public static void main(String[] args) {
        Thread t = new Thread(){
            @Override
            public void run() {
                System.out.println(Thread.currentThread().getId());
                System.out.println(this.getId());
            }
        };
        //在这个代码中,Thread.currentThread()得到的就是t这个引用,也就相当于是在run中直接使用this
        t.start();
    }
}

打印结果:

在这个代码中看起来就好像是this 和 Tread.currentThread没啥区别

但是实际上,没区别的前提,是使用这种继承Thread,重写run的方式创建线程,才是没区别

如果当前是通过Runnable或者lambda的方式,就不行了

这个代码中this指向的是Runnable实例,而不是Thread实例了.此时也就没有getId这样的方法了

休眠当前线程

sleep这个方法,本质上是把线程PCB给从就绪队列,移动到了阻塞队列~

操作系统管理线程

1.描述:PCB

2.组织:双向链表(其实不仅仅是一个)

线程的状态

用于辅助系统对于线程进行调度这样的属性

public class TestDome5 {
    public static void main(String[] args) throws InterruptedException {
        Thread t = new Thread(){
            @Override
            public void run() {
                while (!Thread.currentThread().isInterrupted()){

                }
            }
        };
        System.out.println(t.getId() + ":" + t.getState());
        t.start();
        System.out.println(t.getId() + ":" + t.getState());
        Thread.sleep(1000);
        t.interrupt();
        System.out.println(t.getId() + ":" + t.getState());
    }
}

打印结果:

**NEW:**当前已经把Thread对象创建出来了,但是内核里面的PCB还没创建出来(意思是当前只是把任务给它创建出来,但是没有任务去具体执行

**RUNNABLE:**当前的PCB也创建出来了,同时这个PCB随时待命(就绪)这个线程可能是正在CPU上运行,也可能是在就绪队列中排队...

public class TestDome5 {
    public static void main(String[] args) throws InterruptedException {
        Thread t = new Thread(){
            @Override
            public void run() {
                while (!Thread.currentThread().isInterrupted()){
                    try {
                        Thread.sleep(1000);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                        break;
                    }

                }
            }
        };
        System.out.println(t.getId() + ":" + t.getState());

        t.start();
        Thread.sleep(1000);
        System.out.println(t.getId() + ":" + t.getState());
        Thread.sleep(1000);
        t.interrupt();
        Thread.sleep(1000);
        System.out.println(t.getId() + ":" + t.getState());
    }
}

打印结果:

**TIME_WAITING:**表示当前的PCB在阻塞队列中等待~

这样的一个等待是一个"带有结束时间"的等待

这个操作就会触发这个状态~~

**TERMINATED:**这个状态就表示当前PCB已经结束了.Thread对象还在,此时调用获取状态,得到的就是这个状态

**WAITING:**线程中如果调用了wait方法,也会阻塞等待.此时处在WAITING状态.(死等)除非是其他线程唤醒了该线程

**BLOCKED:**线程中尝试进行加锁,结果发现加锁已经被其他线程占用了.此时该线程也会阻塞等待.这个等待就会在其他线程释放锁之后,被唤醒

yield();这个方法并不会经常用到.效果是让线程主动让出CPU,但是不改变线程状态

这个操作咱们在Java很少会使用

当前这个几个状态,都是Java的Thread类的状态和操作系统内部PCB里面的状态的值并不完全一致的

线程安全

代码举例:

预期让count变量能够自增十万次

public class TestDome6 {
    static class Counter{
        public int count = 0;
        public void increase(){
            count ++;
        }
    }
    static Counter counter = new Counter();
    public static void main(String[] args) {
        //此处创建两个线程,分别针对 count 自增5w次
        Thread t1 = new Thread(){
            @Override
            public void run() {
                for (int i = 0; i < 50000; i++) {
                    counter.increase();

                }
            }
        };
        t1.start();

        Thread t2 = new Thread(){
            @Override
            public void run() {
                for (int i = 0; i <50000 ; i++) {
                    counter.increase();

                }
            }
        };
        t2.start();
        try {
            t1.join();
            t2.join();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println(counter.count);

    }
}

多次打印结果:

预期能够自增10w次

但实际上自增的次数,无法确定,每次运行结果都不一样

写代码最害怕的就是"不确定'

显然,这个代码我们视为一个"bug"

为啥会产生这个情况

大概率是和并发执行相关

由于多线程并发执行,导致代码中出现了BUG,这样的情况就称为"线程不安全"

分析程序执行的过程~

count++的详细过程;分成三个步骤~~

2.执行++操作(就是在CPU里面把0给它变成1)

3.把CPU的值写回到内存中~

此时我们这1次++就完成了

如果是两个线程并发的执行count++,就容易出现问题~~

我们来看一下两个线程并发执行的一个情况

我们给前面三个步骤分别起名为A,B,C

假设两个线程分别在不同的CPU上执行~~

产生线程不安全的原因:

1.线程之间是抢占式执行的(根本原因,线程不安全的万恶之源)

抢占式执行,导致两个线程的里面操作的先后顺序无法确定

这样的随机性,就是导致线程安全问题的根本存在

我们无力改变,这是操作系统内核实现的

2.多个线程修改同一个变量(和代码的写法密切相关)

一个线程修改同一个变量,没有线程问题!!不涉及并发,结果就是确定的

多个线程读取同一个变量,也没有线程安全问题!!读只是单纯的把数据从内存放到CPU中~,不管怎么读,内存的数据始终不变~~

多个线程修改不同的变量,也没有线程安全问题!!其实就认为就类似于第一个情况,一个线程修改一个变量

所以为了规避线程安全问题,就可以尝试变换代码的组织形式,达到一个线程只改变一个变量.....

有的场景下能这样变换,但是有的场景下不能这样变换

3.原子性~

像++这样的操作,本质上是三个步骤,是一个"非原子"的操作

像 = (赋值)操作,本质上就是一个步骤,认为是一个"原子"的操作

像当前,咱们的++操作本身不是原子的,可以通过加锁的方式,把这个操作编程原子的~~

4.内存可见性~

和原子性有点类似

例如:

线程1,读取变量

线程2,对变量自增

这样读,线程1就读到的是修改之前的值

这样读,线程1读到的就是修改之后值

像上面的两种情况都是OK的

1

3

4

如果我们的线程2更复杂了是循环的进行自增

比如说是按照这样一个方式来执行

线程2循环执行很多次自增

很多次自增就会涉及到很多次A和C

CPU执行B操作的速度比执行A和C要快1w倍~

这个时候线程2为了能够算的更快,于是就偷懒了~~

线程2这里面就不会每次自增都去A和C了

而是变成这个样子:

为了提高程序的整体效率,于是线程2就会把中间的一些A和C操作省略掉

这个省略操作是编译器(javac)和JVM(java)总和配合达成的效果~~

如果只是单线程下,这样的优化,没有任何副作用和

如果是多线程下,另外一个线程也尝试读取/修改这个数据

此时就出大事了

按照刚才的画法:

预期线程1读到的数据应该是1

但是实际上由于这样的优化可能导致线程1读到的是0了

如果把中间的A和C操作都省略掉,之后最后有一个C操作

这个时候A读到的其实还是内存中原来的值

这里的三次B操作都是在CPU内部来完成的,并没有把这样的结果写回到内存中

于是线程1再来去读内存就发现读到是旧的结果

1

至于这样的问题如何解决?

volatile关键字,就是用来解决这个问题的

我们下一篇继续.......


本文转载自: https://blog.csdn.net/m0_64397675/article/details/125243869
版权归原作者 K稳重 所有, 如有侵权,请联系我们删除。

“【Java成王之路】EE初阶第二篇:(多线程2)”的评论:

还没有评论