java 多线程基础 万字详解(通俗易懂)

news2024/11/25 3:21:07

目录

一、前言

二、定义

        1.进程 : 

        2.线程 :  

        3.单线程与多线程 : 

        4.并发与并行 : 

三、线程的创建

        1.创建线程的两种基本方式 : 

                1° 继承Thread类,并重写run方法

                1.5° 多线程的执行机制(重要)

                2° 实现Runnable接口,并重写run方法

        2. 两种创建线程方式的比较 : 

四、线程的方法

        1. setName(String name) 和 getName() : 

        2.start() 和 run() : 

        3.setPriority(int newPriority) 和 getPriority() : 

        4.sleep(long millis) 和 interrupt() : 

        5.Δ代码演示 : 

五、线程的操作

        1.线程退出 :

        2. 线程插队 : 

        3.线程守护 : 

六、 线程的状态

        1.线程的7种状态:

        2.线程的生命周期图:(重要)

七、线程同步机制

        1.概述 : 

        2.使用 : 

                1° 同步代码块

                2° 同步方法

        3.案例 : 

八、关于“锁”

        1.互斥锁

        2.死锁 : 

        3.释放锁 : 

九、完结撒❀ 


一、前言

        大家好,本篇博文将会和大家分享关于“多线程”内容的基础部分。“多线程和高并发”作为java学习中一块非常难啃的骨头,本身难度很大,但是依然无法阻止我们向着“牛逼java人”迈进;up将来会在《java高级》专栏与大家分享“多线程和高并发”的相关内容。本篇博文主要面向基础阶段,但是,虽说是基础,该说的我们还是不会含糊。

        注意:①代码中的注释也很重要;不要眼高手低,行之明觉精察处即是知,自己动手跟着过一遍才算有收获;点击文章前面的目录或者侧边栏目录可以进行跳转。良工不示人以朴,所有文章都会适时改进。感谢阅读!


二、定义

        1.进程 : 

                进程是指程序的一次执行过程,或者说是正在运行的一个程序当我们启动了某个应用程序时,就启动了一个进程,操作系统就会为该进程分配内存空间。进程是一个动态过程,自身有产生,存在和消亡的过程。
                比方说,up现在打开了百度网盘,那么,当前百度网盘就是一个进程,我们可以在任务管理器的“进程”栏中看到它。如下图所示 :  

        2.线程 :  

                线程是由进程创建的,是进程的一个实体。一个进程可以拥有多个线程
                比方说,启动百度网盘后,就启动了一个进程,而在百度网盘中我们可以同时启动多个下载任务,这些下载任务就是百度网盘这个进程所创建的线程。如下图所示 : 

        3.单线程与多线程 : 

                单线程——同一时刻,只允许执行一个线程
                多线程——同一时刻,可以执行多个线程。eg : 百度网盘可以同时下载多个任务;谷歌浏览器可以同时打开多个页面;qq可以同时打开多个聊天窗口,等等。 

        4.并发与并行 : 

                并发——同一时间间隔内,多个任务在宏观上同时执行;单核CPU实现的多任务处理就是并发。实际上,单核CPU在同一时刻只能处理一道程序(一个任务),因此微观上单核CPU的多任务处理是通过“分时交替”来实现的;即,将某一时间间隔分成许多的时间片,然后在不同的时间片不停地切换正在处理的任务,宏观上给人的感觉就是“同时执行”了。

                并行——同一时刻可以完成两种或两种以上的任务的处理;并行需要相关硬件的支持,多核CPU可以实现并行。(一般来说,多核CPU的处理是并发和并行同时存在的)。


三、线程的创建

        1.创建线程的两种基本方式 : 

                1° 继承Thread类,并重写run方法

                我们先来看一下Thread是什么意思,如下图所示 : 

                在java中,Thread表示线程,来看一下Thread类的类图,如下

                可以看到,Thread类其实是实现了Runnable接口,所以才有了两种创建线程的方式。
                对于第一种创建线程的方式,up以Thread_Demo1类为测试类,在源文件中定义Grape类去继承Thread类并重写run方法代码如下 : (注意看注释

package csdn.knowledge.thread;

/**
 * @author : Cyan_RA9
 * @version : 21.0
 */
public class Thread_Demo1 {
    public static void main(String[] args) {
        //创建线程对象
        Grape grape = new Grape();
        //启动线程!
        grape.start();
        /*
            通过start方法启动线程,默认会调用线程类中的run方法。
         */
    }
}

class Grape extends Thread {    /**当某个类继承了Thread类后,就可以当作线程使用*/
    private int times = 0;      //定义times变量,用于统计run方法被执行的次数。

    /*
        一般要重写run方法(本质是实现了Runnable接口中的抽象方法),以实现自己的业务需求。
     */
    @Override
    public void run() {
        while (true) {
            System.out.println("我被吃第" + ++times + "次了😭!");
            /*
                Thread类中的静态方法sleep可以让线程休眠指定时间,传入的实参以毫秒为单位。
                使用sleep方法时会出现编译期异常,可以使用Ctrl + Alt + t/T快捷键,
                然后选择使用try-catch包围。
             */
            try {
                Thread.sleep(500);      //让线程每执行一次run方法就休眠0.5秒
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            }

            if (times >= 11)            //执行11次后,跳出while循环
                break;
        }
    }
}

                运行效果:(如下GIF图) 

                1.5° 多线程的执行机制(重要)

                以上面代码为例, 当我们点击“Run 'Thread_Demo1.main()' "运行该程序时,就相当于启动了一个进程,而main函数作为程序的入口,它是当前进程所启动的第一个线程,作为主线程当main函数执行到grape.start()方法时,主线程就会启动一个子线程,即第二个线程
                注意:当第二个线程启动时,主线程不会受到影响而阻塞。也就是说,假设main函数中的"grape.start(); "语句后面还有其他的语句,则在start方法执行后会继续执行下去,不会等着启动的第二个线程完毕后才接着执行。因此,假设我们在"grape.start();"语句后面增加一些输出语句,我们就可以在控制台看到两个线程交替执行的画面。
                当然,进行代码演示前,先给大家来张流程图——如下 : 

                为了验证我们上面的说法,我们在上面Thread_Demo1类代码的基础上,添加一些新的东西——我们在main函数第一行语句打印出当前线程的名字(具体打印步骤在代码注释中),并在线程类run方法的第一行打印出第二个线程的名字;
                然后在main函数的"grape.start();" 语句后面再加上一个for循环语句,并且,为了模拟线程交替执行的效果,我们在for循环中使用也Thread.sleep()方法,令main线程也每0.5秒休眠一次

                代码如下 : 

package csdn.knowledge.thread;

public class Thread_Demo1 {
    public static void main(String[] args) throws InterruptedException {    //使用sleep方法会产生编译期异常
        //先来看看主线程叫啥名字吧,不会就叫main吧?
        /*
            通过Thread类的静态方法currentThread可以获取到当前线程对象,
            而通过getName方法,可以获得当前线程对象的名字。
         */
        System.out.println("第一个线程是:" + Thread.currentThread().getName());

        //创建线程对象
        Grape grape = new Grape();
        //启动线程!
        grape.start();
        /*
            通过start方法启动线程,默认会调用线程类中的run方法。
         */

        for (int i = 0; i < 5; ++i) {
            System.out.println("主线程正在执行中,这是👴第 " + (i+1) + " 次出现。");
            Thread.sleep(500);
        }
    }
}

class Grape extends Thread {    /**当某个类继承了Thread类后,就可以当作线程使用*/
    private int times = 0;      //定义times变量,用于统计run方法被执行的次数。

    /*
        一般要重写run方法(本质是实现了Runnable接口中的抽象方法),以实现自己的业务需求。
     */
    @Override
    public void run() {
        //那第二个线程默认名字是啥呢?
        System.out.println("第二个线程是:" + Thread.currentThread().getName());

        while (true) {
            System.out.println("我被吃第" + ++times + "次了😭!");
            /*
                Thread类中的静态方法sleep可以让线程休眠指定时间,传入的实参以毫秒为单位。
                使用sleep方法时会出现编译期异常,可以使用Ctrl + Alt + t/T快捷键,
                然后选择使用try-catch包围。
             */
            try {
                Thread.sleep(500);      //让线程每执行一次run方法就休眠0.5秒
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            }

            if (times >= 11)
                break;
        }
    }
}

                运行效果:(如下GIF图)

                我们不但可以清楚地看到主线程和Thread-0线程交替执行的情况,而且也可以看到主线程的确就叫“main”,start启动的第二个线程确实就叫“Thread-0”。更重要的是,当主线程挂掉后,Thread-0线程还行继续运行,并在控制台上不停地打印信息。
                注意:当主线程(main线程)挂掉后(执行完毕),由于Thread-0线程还在运行,所以当前进程并没有结束,而是直到所有线程都挂掉了,进程才退出的
                因此,我们可以得出结论——多线程中,不会因为main线程的退出就退出整个进程;而是当当前进程的所有线程都退出后,才退出当前进程

                PS : 

                这时候,可能就要有p小将(Personable小将,指风度翩翩的人)出来挑事儿问问题了——看你写得代码注释,既然start方法默认会调用run方法,那我直接调用run方法不就得嘞,为啥还要调用一下start方法捏,这不是脱裤子放屁😅?

                (ಥ _ ಥ)(/感动),p小将总算问出个正经问题了。
                好的,我们先来看看——如果将start方法替换为run方法,会是怎样的输出效果?如下GIF图演示

                可以看到,最明显的改变就是run方法中的内容与main方法中的内容不再是挨个轮流的输出了,即不再有线程交替执行的现象了。而是先执行完了run方法中的内容,才接着执行main函数中剩余的其他内容,即主线程出现了阻塞
                其实,打印出的线程名已经告诉了我们答案——第二个线程是:main。说明整个程序(进程)的执行过程中就只有main线程一个线程,压根没启动新的线程。这与start方法的特点有关。在start方法的源码中,我们可以找到一个start0方法,如下图所示 : 

                我们可以根据 Ctrl + b/B 快捷键来继续查看start0方法的源码,如下 : 

                可以看到,start0方法其实是Thread类中的一个native方法,native方法表示该方法是由C和C++实现的,由jvm来底层调用,程序员无法手动调用。而start0方法关系到了新线程的启动

                注意 : start()方法调用了start0()方法后,该线程并不一定会马上执行,只是将线程变成了可运行状态。具体什么时候执行取决于CPU,由CPU来统一调度。如下图所示 : 

                2° 实现Runnable接口,并重写run方法

                在面向对象专题我们说过,java是单继承机制,不允许同一类去继承多个类;在某些情况下,一个类已经继承了另一个类,这时如果我们想把这个类当作线程类来使用,显然无法使用第一种方法
                java的设计者们提供了第二种方式来创建线程——直接实现Runnable接口,并重写run方法。我们前面说过,第一种创建线程的方式(继承Thread类并重写run方法)根本上靠的是Runnable接口中的run方法,因为Thread类中的run方法其实就是实现了Runnable接口中的run方法如下图所示 : 

                所以,我们跳过Thread类,直接实现Runnable接口并重写run方法,理论上是完全可行的,start方法最终不就是要调用run方法么。
                好的,up就以Thread_Demo2类为测试类在源文件中另定义Apple类和Fruit类,并让Apple类先继承Fruit类,再实现Runnable接口并重写run方法如下图所示 : 

                接着,我们还是按部就班地在测试类main方法中创建线程类对象,并调用start方法;然而,你会发现你想骂娘了,如下图所示 : 

                我**(咋回事儿啊?),没错,不懂人情世故的IDEA居然说没有找到start方法!Crazy!
                当然,其实原因很简单😂。我们来看一下Runnable接口的方法类图,如下 : 

                噢,原来Runnable接口是个光棍儿啊(bushi)。 Runnable接口中不存在start方法,你仅仅是实现了Runnable接口,通过Apple类对象来调用start方法当然找不到。那就没有解决方案吗?

                当然有😎,这里我们要用到“代理模式”,什么意思呢?说得通俗点,就是说——start方法不是在Thread类中吗,那我们请Thread类出山,帮咱个忙不就行了。
                代码如下 : 

package csdn.knowledge.thread;

/**
 * @author : Cyan_RA9
 * @version : 21.0
 */
public class Thread_Demo2 {
    public static void main(String[] args) throws InterruptedException {
        System.out.println("第一个线程是:" + Thread.currentThread().getName());

        Apple apple = new Apple();
        //请Thread大仙出山帮个忙
        Thread thread = new Thread(apple);

        thread.start();

        for (int i = 0; i < 5; ++i) {
            System.out.println("主线程正在运行 " + (i + 1));
            Thread.sleep(500);
        }
    }
}

class Fruit {}
class Apple extends Fruit implements Runnable {
    int times = 0;

    @Override
    public void run() {
        System.out.println("第二个线程是:" + Thread.currentThread().getName());

        while (true) {
            System.out.println("Apple NB! " + (++times));

            try {
                Thread.sleep(500);
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            }

            if (times >= 11)
                break;
        }
    }
}

                运行效果:

                这里我们用到了一个Thread类的带参构造,如下 : 

                它可以接收一个Runnable接口类型的引用变量,那显然我们可以把Apple类对象丢给它,接口的多态么。用Thread类的带参构造把Apple类对象封装起来后,就可以使用thread引用来调用start方法了。Thread类在这里就相当于是一个代理类,帮助我们使用真正的线程类Apple。

        2. 两种创建线程方式的比较 : 

                从java的设计来看通过继承Thread类并重写run方法(第一种方式)或者 通过实现Runnable接口并重写run方法(第二种方式)这两种创建线程的方式在本质上没有区别最终都是要通过Thread类中的start方法来调用底层的start0方法,从而启动一个新的子线程,并执行子线程类中的run方法
                但是,通过实现Runnable接口并重写run方法(第二种方式)来创建线程,更加适合多个子线程共享一个资源的情况。比如说,我们可以将同一个线程类对象传递给多个代理,以使得多个线程处理同一个对象,如下所示 : 

//以下代码仅作为演示,无实际意义
public static void main(String[] args) {
    Test test = new Test();
    Thread thread_1 = new Thread(test);
    Thread thread_2 = new Thread(test);

    thread_1.start();
    thread_2.start();
}

class Test implements Runnable {
    @Override
    public void run() {
        //body
    }
}

                那么,只要我们在线程类的run方法中写上相应的业务代码,就可以实现“启动的多个子线程”共享一个资源的情景。
                除此之外,使用第二种方式还可以规避单继承机制的约束。因此,平时的实际开发中,建议大家用第二种方式——实现Runnable接口并重写run方法——来创建子线程


四、线程的方法

        1. setName(String name) 和 getName() : 

                setName方法可以设置当前线程的名称
                而getName方法可以返回当前线程的名称。若没有为当前线程设置名称,则返回默认名称。

        2.start() 和 run() : 

                start方法会启动新的线程,因为jvm会调用其底层的start0方法
                而线程对象的run方法仅仅是一个简单的方法调用,并不会启动新的线程

        3.setPriority(int newPriority) 和 getPriority() : 

                先来看一下Priority是什么意思,如下 : 

                setPriority方法可以更改当前线程的优先级
                而getPriority方法可以获取当前线程的优先级

                Thread类中提供了三种优先级别,我们可以查看源码,如下 : 

                MIN_PRIORITY——线程可以设置的最低优先级别,用int类型1表示
                NORM_PRIORITY——线程默认的优先级别,用int类型5表示
                MAX_PRIORITY——线程可以设置的最高优先级别,用int类型10表示

        4.sleep(long millis) 和 interrupt() : 

                sleep方法可以让当前线程休眠指定的毫秒数(暂停执行)
                而interrupt方法可以中断当前线程。由于interrupt方法并没有真正的结束当前线程,所以一般用来中断正在休眠的线程——即,对正在休眠的线程使用interrupt方法可以使其结束休眠,相当于“唤醒”正在休眠的线程

        5.Δ代码演示 : 

                up以Thread_Demo4类为演示类,代码如下 : (先看Cat类中的代码注释)

package csdn.knowledge.thread;

public class Thread_Demo4 {
    public static void main(String[] args) throws InterruptedException {
        Cat cat = new Cat();
        Thread thread = new Thread(cat);
        //启动线程!
        thread.start();

        //setName 和 getName
        System.out.println("old name = " + thread.getName());
        thread.setName("线程1号");
        System.out.println("new name = " + thread.getName());

        //setPriority 和 getPriority
        System.out.println("\nold priority = " + thread.getPriority());
        thread.setPriority(Thread.MIN_PRIORITY);
        System.out.println("new priority = " + thread.getPriority());

        //先让主线程 main休眠 5s,等待run方法第一个for循环执行完成。
        Thread.sleep(5 * 1000);

        /*
            子线程休眠20s太长了,不想等了,倒计时3秒直接唤醒正在休眠的子线程
         */
        for (int i = 3; i > 0; --i) {
            System.out.println("唤醒子线程倒计时:" + i + "~");
            Thread.sleep(1 * 1000);
        }

        //唤醒正在休眠的子线程!
        thread.interrupt();
    }
}

class Cat implements Runnable {

    @Override
    public void run() {
        //run方法中第一个for循环
        for (int i = 0; i < 5; i++) {
            System.out.println("我是一只猫~ (number." + (i + 1) + ")");
            //每执行一次for循环,令子线程休眠1秒(以展示出效果)
            try {
                Thread.sleep(1 * 1000);
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            }
        }

        try {
            //正常情况下,执行完run方法中第一个for循环后,令子线程休眠20秒。
            Thread.sleep(20 * 1000);
        } catch (InterruptedException e) {
            /*
                当执行interrupt方法中断子线程的休眠时,
                就会catch到一个InterruptedException类型的异常对象;
                我们可以在catch语句中写上自己的业务代码。
             */
            System.out.println("\n捕获到异常——" + e.getMessage());
            System.out.println(Thread.currentThread().getName() + "被唤醒!(被interrupt了)\n");
        }

        //run方法中第二个for循环
        for (int i = 0; i < 5; i++) {
            System.out.println("快乐的星猫~ (no." + (i + 1) + ")");
            try {
                Thread.sleep(500);
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            }
        }
    }
}

                运行效果:(如下GIF图) 


五、线程的操作

        1.线程退出 :

                线程的退出主要分为两种情况——
                当线程对应的任务执行完毕后,线程会自动退出
                可以使用“通知”的方式来退出线程,即通过使用变量控制run方法退出来停止线程

                up以Thread_Demo3类为测试类,在源文件下定义Car类,并使用实现Runnable接口重写run方法的方式来创建Car类的线程在Car类中定义控制变量,使主线程可以根据该控制变量来控制run方法的退出,从而退出子线程
                代码如下 : 

package csdn.knowledge.thread;

public class Thread_Demo3 {
    public static void main(String[] args) throws InterruptedException {
        System.out.println("主线程启动——" + Thread.currentThread().getName());

        //创建线程类对象
        Car car = new Car();
        //请Thread大仙出山
        Thread thread = new Thread(car);
        //启动子线程!
        thread.start();

        //令主线程休眠11秒,当主线程休眠结束后,终止run方法,子线程退出。
        Thread.sleep(11 * 1000);
        System.out.println("主线程已休眠11s,令子线程退出!");
        car.setLoop(false);
    }
}

class Car implements Runnable{
    //添加控制变量
    private boolean loop = true;
    private int times;
    @Override
    public void run() {
        System.out.println("子线程启动——" + Thread.currentThread().getName());
        //控制变量可以控制while循环何时结束
        while (loop) {
            System.out.println("道路千万条,安全第一条;行车不规范,亲人两行泪。* " + (++times));
            //每执行一次while循环,令子线程休眠0.5秒
            try {
                Thread.sleep(500);
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            }
        }

        System.out.println("子线程已退出!");
    }

    //提供控制变量loop的setter方法,使主线程可以随时通知子线程退出.
    public void setLoop(boolean loop) {
        this.loop = loop;
    }
}

                运行效果:(如下GIF图)

        2. 线程插队 : 

        yield() : 静态方法,可以实现线程的礼让。该方法可以让出CPU对当前线程的服务,使得CPU可以专注于执行其他的线程,但是礼让的具体效果取决于CPU的底层调度,一般在资源紧张的情况下更容易礼让成功,其中涉及到了操作系统的底层。所以,使用yield方法不一定礼让成功。

        join() : 该方法可以实现线程的插队。并且,一旦插队成功,就一定会优先将插队的线程的所有任务都执行完毕。PS : 想让谁插队,就调用谁的join方法。

                up以Thread_Demo5类为测试类,要达到如下效果——主线程输出滕王阁序的第一段和最后两段,中间部分全部由子线程输出;代码如下 : 

package csdn.knowledge.thread;

public class Thread_Demo5 {
    public static void main(String[] args) {
        Poem poem = new Poem();

        Thread thread = new Thread(poem);
        //启动子线程!
        thread.start();

        System.out.println("        豫章故郡,洪都新府。星分翼轸,地接衡庐。襟三江而带五湖,控蛮荆而引瓯越。\n" +
                "    物华天宝,龙光射牛斗之墟;人杰地灵,徐孺下陈蕃之榻。雄州雾列,俊采星驰。\n" +
                "    台隍枕夷夏之交,宾主尽东南之美。都督阎公之雅望,棨戟遥临;宇文新州之懿范,襜帷暂驻。\n" +
                "    十旬休假,胜友如云;千里逢迎,高朋满座。腾蛟起凤,孟学士之词宗;紫电青霜,王将军之武库。\n" +
                "    家君作宰,路出名区;童子何知,躬逢胜饯。\n");

        //让子线程插队,优先执行子线程的全部任务。
        try {
            thread.join();
        } catch (InterruptedException e) {
            throw new RuntimeException(e);
        }

        System.out.println("        勃,三尺微命,一介书生。无路请缨,等终军之弱冠;有怀投笔,慕宗悫之长风。\n" +
                "    舍簪笏于百龄,奉晨昏于万里。非谢家之宝树,接孟氏之芳邻。他日趋庭,叨陪鲤对;\n" +
                "    今兹捧袂,喜托龙门。杨意不逢,抚凌云而自惜;钟期既遇,奏流水以何惭?\n" +
                "\n" +
                "        呜呼!胜地不常,盛筵难再;兰亭已矣,梓泽丘墟。临别赠言,幸承恩于伟饯;\n" +
                "    登高作赋,是所望于群公。敢竭鄙怀,恭疏短引;一言均赋,四韵俱成。请洒潘江,各倾陆海云尔。\n");
    }
}

class Poem implements Runnable {
    @Override
    public void run() {
        System.out.println("        时维九月,序属三秋。潦水尽而寒潭清,烟光凝而暮山紫。俨骖騑于上路,访风景于崇阿。\n" +
                "    临帝子之长洲,得天人之旧馆。层峦耸翠,上出重霄;飞阁流丹,下临无地。\n" +
                "    鹤汀凫渚,穷岛屿之萦回;桂殿兰宫,即冈峦之体势。\n" +
                "  \n" +
                "        披绣闼,俯雕甍,山原旷其盈视,川泽纡其骇瞩。闾阎扑地,钟鸣鼎食之家;\n" +
                "    舸舰弥津,青雀黄龙之舳。云销雨霁,彩彻区明。落霞与孤鹜齐飞,秋水共长天一色。\n" +
                "    渔舟唱晚,响穷彭蠡之滨,雁阵惊寒,声断衡阳之浦。\n");

        //让子线程每执行完一条语句就休眠1s,以展示出效果。
        try {
            Thread.sleep(1 * 1000);
        } catch (InterruptedException e) {
            throw new RuntimeException(e);
        }

        System.out.println("        遥襟甫畅,逸兴遄飞。爽籁发而清风生,纤歌凝而白云遏。睢园绿竹,气凌彭泽之樽;\n" +
                "    邺水朱华,光照临川之笔。四美具,二难并。穷睇眄于中天,极娱游于暇日。\n" +
                "    天高地迥,觉宇宙之无穷;兴尽悲来,识盈虚之有数。望长安于日下,目吴会于云间。\n" +
                "    地势极而南溟深,天柱高而北辰远。关山难越,谁悲失路之人;萍水相逢,尽是他乡之客。\n" +
                "    怀帝阍而不见,奉宣室以何年?\n");

        try {
            Thread.sleep(1 * 1000);
        } catch (InterruptedException e) {
            throw new RuntimeException(e);
        }

        System.out.println("        嗟乎!时运不齐,命途多舛。冯唐易老,李广难封。屈贾谊于长沙,非无圣主;\n" +
                "    窜梁鸿于海曲,岂乏明时?所赖君子见机,达人知命。老当益壮,宁移白首之心?\n" +
                "    穷且益坚,不坠青云之志。酌贪泉而觉爽,处涸辙以犹欢。北海虽赊,扶摇可接;\n" +
                "    东隅已逝,桑榆非晚。孟尝高洁,空余报国之情;阮籍猖狂,岂效穷途之哭!\n");
    }
}

                运行效果:(如下GIF图)

                可以看到,子线程执行的run方法中,每输出一句都要休眠1秒钟。但是由于子线程插队,cpu必须将子线程的run方法执行完毕后才能继续回到主线程执行剩余部分

        3.线程守护 : 

                用户线程:也叫工作线程;退出方式有两种——①任务执行完毕后自动退出;②通过通知的方式退出
                守护线程:守护线程一般是为工作线程来服务的。守护线程存在的目的是——当其他所有的线程都退出后,守护线程才自动退出。常见的守护线程就是——java中的垃圾回收机制

                对于守护线程,你可以理解为——就是一个“兜底的”,比方说,一个宿舍六个人,规定谁最晚上床谁关灯,那最后留下的那个哥们,一定是在其他五个人均已上床后,它才去关灯,然后上床。此时,最后关灯的哥们就相当于是一个“守护者”😎。

                守护线程的英文是Demon,如下: 

                up以Thread_Demo6类为测试类,代码如下 : 

package csdn.knowledge.thread;

public class Thread_Demo6 {
    public static void main(String[] args) throws InterruptedException {
        Ye ye = new Ye();
        Thread thread = new Thread(ye);

        //设置ye为守护线程
        thread.setDaemon(true);
        //启动👴线程
        thread.start();

        //当for循环执行完毕后,主线程就会退出,此时没有其他线程运行了,因此守护线程👴也会退出。
        for (int i = 0; i < 11; ++i) {
            System.out.println("此处不留👴,自有👴光处!" + (i + 1));
            //每执行一次for循环,让主线程休眠1s,以展示出效果
            Thread.sleep(1000);
        }
    }
}

class Ye implements Runnable {
    @Override
    public void run() {
        int times = 0;

        /*
            一个死循环,如果不是守护线程的话,永远不会退出。
         */
        while (true) {
            System.out.println("卷死👴了! " + (++times));
            //每执行一次while循环,令子线程休眠0.5秒,不然太快了
            try {
                Thread.sleep(500);
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            }
        }
    }
}

                运行效果:(如下GIF图)


六、 线程的状态

        1.线程的7种状态:

                首先我们得明白,java语言的设计者——即java官方的标准到底定义了几种线程的状态。通过查看Thread类的源代码,我们可以在其中找到一个内部类State,如下图所示 : 

                显然State类是一个枚举类,java的设计者在该枚举类中给出了线程的状态,共6种。我们也可以在API文档中直接查看如下图所示 : 

                官方定义的线程的6种状态分别为——
                NEW(尚未启动);
                RUNNABLE(正在JVM中执行);
                BLOCKED(被阻塞);
                WAITING(等待其他线程执行特定任务);
                TIMED_WAITING(等待指定时间);
                TERMINATED(业已退出)

                这时候可能就要有p小将(Personable小将,指风度翩翩的人)出来bb问了:丫的牛头不对马嘴 !标题还写着7种状态,写着写着变成6种了,搁这儿阉割呢?
                诶,p佬先别急,这不,下面一张线程生命周期图就给大伙儿说明白了。

        2.线程的生命周期图:(重要)

                如下图所示 : 

                老规矩,我们一步一步来看——
                当我们通过new关键字创建一个新的线程对象后,线程就处于“NEW”的状态(新建状态)
                当start() 方法中的start0() 方法正式启动新的线程后,线程就进入了“RUNNABLE”的状态(可运行状态)"RUNNABLE"对应上图中间的粉色大矩形框.
                    因此, 如果我们细分的话, “RUNNABLE”状态又可分为两种具体的状态——Ready(就绪状态) Running(运行状态);其中,就绪状态Ready表示当前线程可以运行,但最终会不会被执行要取决于该线程会不会被调度器选中; 如果线程被选中执行,那线程就进入了运行状态Running
                    也就是说,如果我们把Ready和Running合并看作一个状态,也就是RUNNABLE(可运行状态),那线程就可以处于6种状态的一种,与java的官网文档相匹配;但如果我们细化,那就是我们上面标题说的“7种状态”
                线程的任务执行完毕后,线程就会进入“TERMINATED”状态(终止状态),可称为线程“死掉了”或者“挂掉了”。
                如果一个正处于RUNNABLE状态的线程,调用了一个锁,线程就可能进入“BLOCKED”状态(阻塞状态),直到获得锁后重新进入RUNNABLE状态。
               如果一个处于RUNNABLE状态的线程遇到了wait, join, park这些方法,就可能进入“WAITING”状态(等待执行状态)。比方说,我们上文中提到的join方法,当第二个线程通过join方法插队成功后,cpu就会优先去执行插队的线程,那么原先的第一个线程此时就处于WAITING状态,必须等待第二个线程执行完毕后,才能返回到第一个线程中继续执行。第一个线程也可以通过notify等方法重新回到RUNNABLE状态
                对于“TIMED_WAITING”状态(超时等待状态),最经典的就是我们上面一直在用的sleep方法,令指定线程休眠指定时间,时间结束后重新回到RUNNABLE状态
                对于RUNNABLE细分的两个状态Ready 和 Running,它们之间可以进行相互转化。比方说,可以将一个正在运行的线程挂起,使其进入就绪状态;或者对一个正在运行的线程使用yield方法进行礼让,使其进入就绪状态。其实,这张线程生命周期图也间接验证了我们之前提到的观点——使用yield方法不一定会礼让成功;因为yield之后只是进入就绪状态了,如果在资源充足的情况下,它可能重新被调度器选中并执行;而join方法则是直接将该线程置为了WAITING状态。

                up以Thread_Demo7类为测试类,代码如下 : 

package csdn.knowledge.thread;

public class Thread_Demo7 {
    public static void main(String[] args) throws InterruptedException {
        State state = new State();
        Thread thread = new Thread(state);
        /*
            getState方法可以获取指定线程当前的状态。
         */
        System.out.println("当前线程的状态——" + thread.getState());

        thread.start();
        /*
            只要线程当前的状态不为TERMINATED(终止状态),就不停输出它当前的状态。
         */
        while (thread.getState() != Thread.State.TERMINATED) {
            System.out.println("当前线程的状态——" + thread.getState());

            Thread.sleep(500);
        }

        System.out.println("当前线程的状态——" + thread.getState());
    }
}

class State implements Runnable {
    @Override
    public void run() {
        int times = 0;

        while (true) {
            System.out.println("现在是啥子状态噢——" + (++times));

            try {
                Thread.sleep(500);
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            }

            if (times >= 5)
                break;
        }
    }
}

                运行效果:(如下GIF图)


七、线程同步机制

        1.概述 : 

        在多线程编程中,一些敏感数据或者重要数据往往不允许被多个线程同时访问,因此会用到线程访问技术。通过线程同步机制,使得重要数据在同一时刻最多只能被一个线程访问,以保证数据的完整性。

        所谓“线程同步”,即——当已经有一个线程对内存中的某些数据进行操作时,其他线程都不能访问这个内存地址。直到该线程执行完毕任务,其他线程才能对该内存地址进行操作。

        2.使用 : 

                1° 同步代码块

        synchronized (对象){

                //body(需要被同步的代码写在这儿

        }

        Δ说明 : 

        使用“synchronized(对象)”的格式修饰代码块,称为“对象锁只有拿到了对象锁的线程才可以访问该同步代码块,访问完毕后,再将对象锁放回去。下一次线程如果想访问该同步代码块,需重复上述操作。

                2° 同步方法

        public synchronized void f(parameter list) {

                //body(需要被同步的代码写到这儿

        }

        Δ说明 : 

        synchronized关键字修饰方法,表示整个方法是一个同步方法在同一时刻,同步方法只能被最多一个线程访问。

        3.案例 : 

                利用线程同步机制解决经典的“超卖”问题。
                up以Thread_Demo8类为测试类,代码如下 : 

package csdn.knowledge.thread;

/**
 * 线程同步机制——
 * 解决经典“ 超卖 ”问题。
 */
public class Thread_Demo8 {
    public static void main(String[] args) {
        //采用实现Runnable接口的方式创建线程类对象
        Conductor conductor = new Conductor();

        //给出三个代理,相当于售票员共有三位,三个线程共享一个资源。
        Thread thread1 = new Thread(conductor);
        thread1.setName("售票员1号");
        Thread thread2 = new Thread(conductor);
        thread2.setName("售票员2号");
        Thread thread3 = new Thread(conductor);
        thread3.setName("售票员3号");

        //启动线程
        thread1.start();
        thread2.start();
        thread3.start();
    }
}

class Conductor implements Runnable {
    //所有售票员共售卖600张票
    private int sum = 600;
    //控制变量
    private boolean loop = true;

    /*
        1.因为所有线程默认都会调用run方法,所以,利用封装的思想相关业务代码封装到sell方法中。
        2.通过添加synchronized关键字,将sell方法设置为同步方法。
          同一时刻,仅允许有一个线程访问sell方法。
     */
    public synchronized void sell() {
        //如果sum == 0,说明票买完了,直接走人儿!
        if (sum <= 0) {
            loop = false;
            System.out.println("卖完了敖~");
            return;
        }

        //售票员使用机器卖票,每卖出一张就休眠0.01秒。
        try {
            Thread.sleep(10);
        } catch (InterruptedException e) {
            throw new RuntimeException(e);
        }

        //实现显示当前还剩多少张票没有卖出。
        System.out.println(Thread.currentThread().getName() + "卖出一张票," + "还剩 " + (--sum) + " 张票。");
    }

    @Override
    public void run() {
        while (loop) {
            sell();
        }
    }
}

                运行效果 : (如下GIF图) 

                GIF图演示中,最后打印出了三句“卖完了敖~”,说明三个线程是真正存在的,但是为什么实际卖票的就一个线程呢?说来惭愧😂,因为up设置的票数少了,CPU不屑于调用售票员2号和售票员3号;经过实测,在up电脑上,如果设置500张以上,就可以看到他们了,但是设置的票数太多的话,最后录出来的GIF图片太大了不能上传,也是没办法。
                当然,大家可以在自己电脑上试试,一般情况下出现其他售票员所需的票数越高,说明你电脑性能越好😂。休眠时间尽量设置的短一些更容易调用到其他线程。


八、关于“锁”

        1.互斥锁

        1° java中引入了“对象互斥锁”的概念,以保证共享数据操作的完整性。每个对象都对应于一个可称为“互斥锁”的标记,该标记用来保证同一时刻仅允许一个线程访问该对象
         当某个对象被synchronized关键字修饰时,表明该对象在任一时刻仅允许被一个线程访问。

        如果是非静态的同步方法,它的锁默认是this,也可以是其他对象要求是同一个对象);如果是静态的同步方法,它的锁为当前类本身

            对于非静态方法,比方说,对于上面卖票问题的sell方法,我们可以把同步方法转换为同步代码块,“(对象)”中放入this,如下所示 : 

    public /*synchronized*/ void sell() {
        //synchronized关键字修饰的同步代码块
        synchronized (this) {
            if (sum <= 0) {
                loop = false;
                System.out.println("卖完了敖~");
                return;
            }

            try {
                Thread.sleep(10);
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            }

            System.out.println(Thread.currentThread().getName() + "卖出一张票," + "还剩 " + (--sum) + " 张票。");
        }
    }

            我们也可以自己定义一个对象来使用,如下图所示:

            这么做也是可以的不会发生超卖现象。重点就在于——保证多个线程争夺同一个对象。 

            对于静态方法,同步的静态方法在形式上不变化;但若想在一个静态方法中定义一个同步代码块,需要将括号中的对象改为“类名.class”

        4° 线程同步机制的局限性——会降低程序的执行效率。为了尽可能减少这种效率的损耗,建议优先使用同步代码块而不是同步方法,因为同步代码块范围更小更精准,相对同步方法效率更高。

                up以Thread_Demo9类为测试类,代码如下 : 

package csdn.knowledge.thread;

public class Thread_Demo9 {
    public static void main(String[] args) {
        /*
            为两个线程分别赋予不同的key值。
         */
        Lock lock1 = new Lock(true);
        Lock lock2 = new Lock(false);

        lock1.setName("A线程——大头姐姐");
        lock2.setName("B线程——小头哥哥");

        lock1.start();
        lock2.start();
    }
}

class Lock extends Thread {
    /*
        使用static修饰Object类型的对象,以保证多个线程共享相同的对象。
     */
    static private Object o1 = new Object();
    static private Object o2 = new Object();
    private boolean key;

    public Lock(boolean key) {
        this.key = key;
    }

    /*
        if语句和else语句中分别设置两个嵌套同步代码块,两把对象锁分别是o1和o2;
        只不过,if语句中是先抢o1对象,而else语句中是先抢o2对象。
        那么只要A,B两个线程的初始key值不同,就极容易发生死锁——A线程和B线程都卡在第一层同步代码块里出不来了!
     */
    @Override
    public void run() {
        if (key) {
            synchronized (o1) {
                System.out.println(Thread.currentThread().getName() + "拿到了o1对象锁!");
                synchronized (o2) {
                    System.out.println("先拿到了o1,现在拿到了o2");
                }
            }
        } else {
            synchronized (o2) {
                System.out.println(Thread.currentThread().getName() + "拿到了o2对象锁!");
                synchronized (o1) {
                    System.out.println("先拿到o2,现在拿到了o1");
                }
            }
        }
    }
}

                运行结果:(卡着就不动了,程序永远不会自动停止)

        2.死锁 : 

        线程的死锁是指——当多个线程都占用了对方的资源,但不肯相让,既不能释放资源也无法获取资源,就会导致死锁

        比方说,有两个线程A和B,A线程想获取B的资源,而B线程想获取A的资源;但是A线程和B线程又分别占据了A对应的资源和B对应的资源,此时A线程和B线程既无法释放资源也无法进一步获取资源,始终处于“BOLCKED”状态,造成死锁。
        因此,编程中,我们应该尽量避免“死锁”的发生

        3.释放锁 : 

        1° 常见释放锁的四种情况——

                当前线程对应的同步方法或者同步代码块执行完毕,线程自动退出

                人话 : 拉完了,完事儿就出来了😋。

                当前线程对应的同步方法或者同步代码块,在执行过程中遇到了break或者return

                人话 : 拉一半儿呢,老师👩‍💼点名了,夹断,出来。

                当前线程对应的同步方法或者同步代码块,在执行过程中遇到了未处理的Error或者Exception

                人话 : 拉着正酣😙呢,突然发现“彻硕儿”是坏的,拉出去的全掉楼下了,吓得赶紧出来😱。

                当前线程对应的同步方法或者同步代码块,在执行过程中遇到了线程对象的wait方法,线程暂停执行,并释放锁

                人话 : 蹲的腿都麻了还挤不出二两💩来,先出去补两斤地瓜,含章可贞,以时发也。


        2° 常见不释放锁的两种情况——

                线程执行同步代码块或者同步方法时,程序调用了Thread.sleep(...)方法或者Thread.yield()方法,只是暂停当前线程的执行,并不会释放锁。对于前者,会使当前线程进入TIMED_WAITING状态,对于后者,会使当前线程进入Ready状态

                人话 : 前者——刚拉出一块儿大的,实在累的不彳亍了,先一屁股坐下去睡会儿😴,管他是什么触觉;后者——边拉边背诵“孔融让梨”的故事,幡然醒悟,不管是拉了多少还是正在往下坠,直接起身👍。

                线程执行同步代码块或者同步方法时,其他线程调用了该线程的suspend方法,将该线程挂起,但挂起后依然不会释放锁。PS : 不推荐使用suspend方法和resume方法,因为已经过时。

                人话 : 你被打上了思想🧠钢印,始终认为自己想去拉,于是便占着茅坑不拉💩。

九、完结撒❀ 

        🆗,以上就是我们“多线程”基础部分的全部内容。不出意外,多线程专题将是我们javaSE基础的最后一个专题了。之后up会将java基础,java进阶,java高级等内容总结到一块儿,进行一个汇总。感谢阅读!

        System.out.println("END-----------------------------------------------------------------------------"); 

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/425144.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

【C++】继承---下(子类默认成员函数、虚继承对象模型的详解等)

前言&#xff1a; 上篇文章我们一起初步了解了继承的概念和使用&#xff0c;本章我们回家新一步深入探讨继承更深层次的内容。 前文回顾——>继承---上 目录 &#xff08;一&#xff09;派生类的默认成员函数 &#xff08;1&#xff09;6个默认成员函数 &#xff08;…

Pytorch全连接神经网络实现手写数字识别

问题Mnist手写数字识别数据集作为一个常见数据集&#xff0c;包含10个类别&#xff0c;在此次深度学习的过程中&#xff0c;我们通过pytorch提供的库函数&#xff0c;运用全连接神经网络实现手写数字的识别方法设置参数input_size 784hidden_size 500output_size 10num_epoc…

JavaScript对象类型之function

目录 一、Function 定义函数 调用函数 默认参数 匿名函数 箭头函数 二、函数是对象 三、函数作用域 四、闭包 五、let、var与作用域 一、Function 定义函数 function 函数名(参数) {// 函数体return 结果; } 例如&#xff1a; function add(a, b) {return a b; …

应届生通过Java培训班转行IT有前途吗?

借用邓小平同志曾说过的一句话&#xff1a;科学技术是第一生产力。IT行业作为科技行业中的一员&#xff0c;不管是在自身的发展&#xff0c;还是支持其他行业的发展中都扮演了不可或缺的角色&#xff0c;“互联网”是社会发展的趋势&#xff0c;前途是无限的。而计算机语言是目…

dolphinscheduler之hivecli 任务

hivecli 任务 Hivecli任务说明 dolphinscheduler的hivecli任务是专门执行hivesql的任务类型。其中子类型分为FROM_SCRIPT和FROM_FILE。 FROM_SCRIPT 执行的脚本可以直接在文本框中编写 执行的底层采用-e参数执行 hive -e "show databases;show tables"FROM_FILE…

建造者模式解读

目录 话题引进 传统方式解决盖房需求 传统方式的问题分析 建造者模式基本介绍 基本介绍 四个角色 原理类图 ​编辑 应用实例 改进代码 建造者模式在 JDK 的应用和源码分析 建造者模式的注意事项和细节 抽象工厂模式 VS 建造者模式 话题引进 1) 需要建房子&#xff1a;…

剑指 Offer (第 2 版)

&#xff08;简单&#xff09;剑指 Offer 03. 数组中重复的数字 找出数组中重复的数字。 在一个长度为 n 的数组 nums 里的所有数字都在 0&#xff5e;n-1 的范围内。数组中某些数字是重复的&#xff0c;但不知道有几个数字重复了&#xff0c;也不知道每个数字重复了几次。请…

Python实现采集某二手房源数据并做数据可视化展示

目录环境介绍&#xff1a;模块使用:实现爬虫思路&#xff1a;代码环境介绍&#xff1a; Python 3.8Pycharm 模块使用: requests >>> pip install requests 数据请求模块 parsel >>> pip install parsel 数据解析模块 csv 内置模块 实现爬虫思路&#x…

如何搭建自己的V Rising自建服务器,以及常见的V Rising服务器问题解决方案

V rising官方服务器经常无法连接&#xff0c;无法和小伙伴玩耍&#xff1b;如何搭建自己的V rising服务器呢&#xff1f;还可以修改掉落倍率&#xff0c;加快游戏进度&#xff0c;搭建自己的私人服务器。 前言 最近V rising这个游戏很火呀&#xff0c;迫不及待地和小伙伴一起…

基于粒子群优化算法的面向综合能源园区的三方市场主体非合作交易方法(Matlab代码实现)

&#x1f4a5;&#x1f4a5;&#x1f49e;&#x1f49e;欢迎来到本博客❤️❤️&#x1f4a5;&#x1f4a5; &#x1f3c6;博主优势&#xff1a;&#x1f31e;&#x1f31e;&#x1f31e;博客内容尽量做到思维缜密&#xff0c;逻辑清晰&#xff0c;为了方便读者。 ⛳️座右铭&a…

【JSP学习笔记】4.JSP 隐式对象及客户端请求

前言 本章介绍JSP的隐式对象及客户端请求。 JSP 隐式对象 JSP隐式对象是JSP容器为每个页面提供的Java对象&#xff0c;开发者可以直接使用它们而不用显式声明。JSP隐式对象也被称为预定义变量。 JSP所支持的九大隐式对象&#xff1a; 对象描述requestHttpServletRequest 接…

一文吃透Arthas常用命令!

Arthas 常用命令 简介 Arthas 是Alibaba开源的Java诊断工具&#xff0c;动态跟踪Java代码&#xff1b;实时监控JVM状态&#xff0c;可以在不中断程序执行的情况下轻松完成JVM相关问题排查工作 。支持JDK 6&#xff0c;支持Linux/Mac/Windows。这个工具真的很好用&#xff0c;…

【C++】模板进阶--非类型模板参数模板特化及分离编译

文章目录一、非类型模板参数二、模板的特化1.模板特化的概念2.函数模板的特化3.类模板的特化3.1 全特化3.2 偏特化4.类模板特化应用示例三、模板的分离编译四、模板总结一、非类型模板参数 模板参数分为类型形参与非类型形参&#xff0c;其中&#xff0c;类型形参即出现在模板…

MBD-PMSM闭环控制模型(FOC算法)

目录 前面 Speed_and_Position_Estimator 获取HALL信号 HALL状态更新 计算转速 位置判断 ADC相电流/总线电流电压 获取AD值 计算实际值 低速高速切换 SlowLoopControl FastLoopControl 最后 前面 前面分析了BLDC的开环与闭环&#xff0c;接下来分析PMSM或者说FOC…

MySQL 异步复制、半同步复制、增强半同步复制(史上最全)

背景&#xff1a;来自于小伙伴问题 小伙伴的难题&#xff1a; mysql主从同步的时候&#xff0c;半同步和增强半同步是怎样的一个概念&#xff0c;我看网上说的有点不明不白的&#xff0c;也没找到合适的解释。 这里尼恩给大家做一下系统化、体系化的梳理。也一并把这个题目以…

【每天学习一点点】RocketMQ的架构、写数据、高效的数据查询索引、负载均衡

Rocket一、学习目标二、RocketMQ的架构运行图2.1、NameServer2.1.1 为什么需要NameServer2.1.1.1 不可以没有nameserver吗&#xff1f;2.1.2 NameServer需要单独部署吗2.1.3 Nameserver可以动态注册和注销Broker、Topic和Consume 是什么意思2.1.4 可以使用nacos的配置中心替代N…

成本与体验的“非零和博弈”

随着移动互联网和智能终端的普及&#xff0c;越来越多的海内外互联网企业开始发力短视频业务。在短视频用户全球化&#xff0c;短视频产品及内容消费井喷式增长的今天&#xff0c;用户开始逐渐对体验有了越来越高的要求。为了更清晰更流畅地播放&#xff0c;用户播放成本也随着…

TensorFlow GPU不可用,WSL2安装

这个帖子写给23年刚买电脑、系统是win11&#xff0c;tensorflow版本是2.10以上的兄弟们。不符合的可以去看其他答案了。 这是以我三天来的安装经历来写的&#xff0c;希望能给后来的兄弟们减少时间的浪费。 win11&#xff0c;安装的tensorflow的版本都是2.12的&#xff0c;但…

(二)Cmd Markdown 编辑阅读器的使用效果 | 以 Cmd Markdown 编辑阅读器为例

Cmd Markdown 编辑阅读器使用指南 &#xff08;一&#xff09;Cmd Markdown 编辑阅读器的使用示例 | 以 Cmd Markdown 编辑阅读器为例&#xff08;二&#xff09;Cmd Markdown 编辑阅读器的使用效果 | 以 Cmd Markdown 编辑阅读器为例 在 Cmd Markdown 编辑阅读器&#xff08; …

C语言实现Allan方差计算

Allan方差专有概念解释 1.量化噪声 量化噪声是一切量化操作所固有的噪声,只要进行数字量化编码采样,传感器输出的理想值与量化值之间就必然会存在微小的差别,量化噪声代表了传感器检测的最小分辨率水平。 2.角度随机游走 角度随机游走是宽带角速率白噪声积分的结果,即陀螺…