• 多线程编程的基础用法


    前言

    在学习Java中的多线程编程之前,需要对进程、线程的基本知识有初步的了解,不懂的同学可以看我之前总结的博客

    初始多线程

    深入理解多线程

    多线程与锁

    在Java标准库中,提供了一个Thread类,用来表示/操作线程

    1、创建线程的方式

    第一种方式: 自定义一个类,继承Thread类,重写run方法

    //创建子类,继承父类,重写run方法
    class MyThread extends Thread{
        @Override
        public void run(){
            System.out.println("hello,thread");
        }
    }
    
    public class Demo1 {
        public static void main(String[] args) throws InterruptedException {
            Thread t = new MyThread();
            //这里调用了start才是真正在系统中创建了线程,然后开始执行run操作
            t.start();
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15

    在这里插入图片描述

    run方法描述了线程内部需要执行哪些代码,run方法中的逻辑,是在新创建出来的线程中,被执行的代码。

    new MyThread()并不是真正的创建线程,当调用start()方法后,才会在操作系统中创建一个线程,并且执行run操作,在调用start()方法之前,系统中是没有创建出线程的

    第二种方式: 创建一个类,实现Runnable接口,再创建Runnable实例传给Thread实例

    //Runnable 就是在描述一个“任务”
    class MyRunnable implements Runnable{
        @Override
        public void run(){
            System.out.println("hello,thread");
        }
    }
    
    
    public class Demo2 {
        public static void main(String[] args) {
            Thread t = new Thread(new MyRunnable());
            t.start();
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15

    在这里插入图片描述

    通过Runnable来描述任务的内容,进一步再把描述好的任务交给Thread实例

    第三种方式: 使用匿名内部类的方式创建

    public class Demo3 {
        public static void main(String[] args) {
            Thread t = new Thread() {
                @Override
                public void run() {
                    System.out.println("hello,thread");
                }
            };
            t.start();
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11

    在这里插入图片描述

    创建一个匿名内部类,继承自Thread类,重写run方法,同时再new出这个匿名内部类的实例

    第四种方式: 创建Runnable匿名内部类的实例,作为参数传给Thread

    public class Demo5 {
        public static void main(String[] args) {
            Thread t = new Thread(new Runnable() {
                @Override
                public void run() {
                    System.out.println("hello thread");
                }
            });
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10

    在这里插入图片描述

    new的Runnable,针对这个创建的匿名内部类,同时new出Runnable实例传给Thread的构造方法

    和第三种方式中的代码对比,通常认为第四种方式的写法更好一点,它能够做到让线程和线程执行的任务,更好的进行解耦。Runnable单纯的只是描述了一个任务,至于这个任务是要通过一个进程来执行,还是线程来执行,还是线程池来执行,还是协程来执行,Runnable本身并不关心Runnable里面的代码也不关心

    第五种方式: 使用lambda表达式,是第四种方式的延伸

    public class Demo6 {
        public static void main(String[] args) {
            //利用lambda表达式创建线程
            Thread t = new Thread(()->{
                System.out.println("hello thread");
            });
            t.start();
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9

    在这里插入图片描述

    2、通过代码比较多线程的优势

    都说多线程快,那我们就简单证明一下,串行执行两个变量从0增加到10亿和并发执行两个变量从0增加到10亿,看看哪一个更快

    串行执行:

    public class Demo7 {
        private static final long count = 10_0000_0000;
    
        public static void serial() {
            //记录程序执行时间
            long begin = System.currentTimeMillis();
            long a = 0;
            for(int i = 0; i < count; ++i) {
                a++;
            }
    
            long b = 0;
            for(int i = 0; i < count; ++i) {
                b++;
            }
            //记录结束时间
            long end = System.currentTimeMillis();
            System.out.println("serial()消耗时间: " + (end - begin) + "ms");
        }
    
        public static void main(String[] args) throws InterruptedException {
            serial();
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24

    在这里插入图片描述

    执行5次,消耗的时间大致都在750ms上下

    并发执行:

    public class Demo7 {
        private static final long count = 10_0000_0000;
        
        public static void concurrency() throws InterruptedException {
            long begin = System.currentTimeMillis();
            Thread t1 = new Thread(()->{
               long a = 0;
               for(int i = 0; i <count; ++i) {
                   a++;
               }
            });
            t1.start();
    
            Thread t2 = new Thread(()->{
               long b = 0;
               for(int i = 0; i < count; ++i) {
                   b++;
               }
            });
            t2.start();
            //让main线程等待t1和t2执行完了再记录结束时间
            t1.join();//让main线程等待t1执行结束
            t2.join();//让main线程等待t2执行结束
            long end = System.currentTimeMillis();
            System.out.println("concurrency()消耗时间: " + (end - begin) + "ms");
        }
    
        public static void main(String[] args) throws InterruptedException {
            concurrency();
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31

    在这里插入图片描述

    执行5次,消耗的时间大致都在460ms上下

    并发执行的效率提升了将近50%,但是并不是说一个线程600多ms,两个线程就400多ms。这两个线程在底层到底是并行还是并发,是不确定的,真正并行执行的时候,效率才会提升

    多线程不是万能的,不是用了多线程,效率就一定高,还得看具体的应用场景

    3、Thread类常见的构造方法和属性

    常见的构造方法

    方法说明
    Thread()创建线程对象
    Thread(Runnable target)使用 Runnable 对象创建线程对象
    Thread(String name)创建线程对象,并命名
    Thread(Runnable target, String name)使用 Runnable 对象创建线程对象,并命名
    Thread(ThreadGroup group,Runnable target)线程可以被用来分组管理,分好的组即为线程组

    常见的属性

    属性获取方法
    IDgetId()
    名称getName()
    状态getState()
    优先级getPriority()
    是否后台线程isDaemon()
    是否存活isAlive()
    是否被中断isInterrupted()
    • ID 是线程的唯一标识,不同线程不会重复

    • 名称是各种调试工具用到

    • 状态表示线程当前所处的一个情况

    • 优先级高的线程理论上来说更容易被调度到

    • 关于后台线程,创建的是前台线程,main执行完毕后,进程也不能退出,得等到线程执行完毕后,整个进程才结束,如果是后台线程,main执行完毕后,整个进程就直接退出,线程会被强行终止。需要记住一点:JVM会在一个进程的所有非后台线程结束后,才会结束运行

    • 是否存活,操作系统中对应的线程是否正在运行。Thread对象的生命周期和内核中对应的线程的生命周期并不完全一致,创建出线程对象之后,在调用start之前,系统中是没有对应的线程的。在run方法执行完毕后,系统中的线程就被销毁了,但线程这个对象可能还在

    • 线程的中断问题,中断也就是让一个线程停下来

    通过以下代码,来打印线程的各种属性:

    public class Demo32 {
        public static void main(String[] args) {
            Thread thread = new Thread(() -> {
                for (int i = 0; i < 10; i++) {
                    try {
                        System.out.println(Thread.currentThread().getName() + ": 我还 活着");
                        Thread.sleep(1000);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
                System.out.println(Thread.currentThread().getName() + ": 我即将死去");
            });
            System.out.println(Thread.currentThread().getName() + ": ID: " + thread.getId());
            System.out.println(Thread.currentThread().getName() + ": 名称: " + thread.getName());
            System.out.println(Thread.currentThread().getName() + ": 状态: " + thread.getState());
            System.out.println(Thread.currentThread().getName() + ": 优先级: " + thread.getPriority());
            System.out.println(Thread.currentThread().getName() + ": 后台线程: " + thread.isDaemon());
            System.out.println(Thread.currentThread().getName() + ": 活着: " + thread.isAlive());
            System.out.println(Thread.currentThread().getName() + ": 被中断: " + thread.isInterrupted());
            thread.start();
            while (thread.isAlive()) {
            }
            System.out.println(Thread.currentThread().getName() + ": 状态: " + thread.getState());
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26

    在这里插入图片描述

    4、启动线程

    启动线程就是线程实例调用start()方法,前面已经使用过,不多赘述,这里主要讲一下start和run的区别

    start和run的区别

    public class Demo9 {
        public static void main(String[] args) throws InterruptedException {
            Thread t = new Thread(() -> {
                while (true) {
                    System.out.println("hello thread");
                    try {
                        Thread.sleep(1000);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
            });
            t.start();
            
            while(true) {
                System.out.println("hello main");
                Thread.sleep(1000);
            }
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20

    在这里插入图片描述
    两个线程并发,交替打印

    如果将t.start()换成t.run(),则会出现不一样的结果

    在这里插入图片描述

    结果只会打印"hello thread"

    run方法只是一个普通的方法,在main线程中调用run,其实并没有创建的新的线程
    这个循环仍然是在main线程中执行
    既然是在一个线程中执行,代码就得从前到后按顺序执行
    先运行第一个循环,再运行第二个循环,但一个循环会一直进行下去

    调用 start 方法, 才真的在操作系统的底层创建出一个线程

    5、中断线程

    线程停下来的关键,就是让线程对应的run方法执行完毕(还有一个特殊的线程,那就是main线程,对于main来说,main方法执行完毕后,线程就完了)

    1.)可以手动的设置一个标志位(自己创建的变量,boolean),来控制线程是否要执行结束

    public class Demo10 {
        private static boolean isQuit = false;
    
        public static void main(String[] args) throws InterruptedException {
            Thread t = new Thread(()->{
                while (!isQuit) {
                    System.out.println("hello thread");
                    try {
                        Thread.sleep(1000);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
            });
    
            t.start();
    
            //把isQuit设为true,t线程的循环就结束了,再进一步执行run,线程就结束了
            Thread.sleep(5000);
            isQuit = true;
            System.out.println("线程t终止");
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23

    注意:main线程和t线程在同一个进程地址空间,因此,main线程修改的isQuit和t线程判定的isQuit是同一个值

    2.)上述代码不够严谨,更好的做法是使用Thread中内置的一个标志位来进行判定
    可以通过Thread.interrupted()和Thread.currentThread.isinterrupted()获得这个标志位
    前者是一个静态方法,后者是一个实例方法
    推荐使用后者,因为一个代码中的线程可能有很多个,随时哪个线程都可能会终止。
    Thread.interrupted()判定的标志位是Thread的static成员(一个程序中只有一个标志位)
    Thread.currentThread.isinterrupted()判定的标志位是Thread的普通成员,每个线程实例都有自己的标志位

    public class Demo11 {
        public static void main(String[] args) {
            Thread t = new Thread(()->{
                while (true) {
                    System.out.println("hello thread");
                    try {
                        Thread.sleep(1000);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                        //当触发异常后,立即退出循环
                        break;
                    } finally {
                        System.out.println("这是收尾工作");
                    }
                }
            });
            t.start();
    
    
            try {
                Thread.sleep(5000);
            } catch (InterruptedException e) {
                e.printStackTrace();
    
            }
    
            //在主线程中调用 interrupt 方法,来中断这个线程
            //t.interrupt()的意思就是让t中断!!
            //如果调用这个方法,可能会产生两种情况
            //1.如果t线程处在就绪状态,就是设置线程的标志位为true
            //2.如果t线程处在阻塞状态(sleep休眠),就会触发一个interruptedException,此时设置标志位就不能起到及时唤醒的作用
            t.interrupt();
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34

    在这里插入图片描述

    6、等待线程

    有时,我们需要等待一个线程完成它的工作后,才能进行自己的下一步工作。因此,我们需要一个方法明确等待线程的结束。

    public class Demo12 {
        public static void main(String[] args) {
            Thread t = new Thread(()->{
               for(int i = 0; i < 5; ++i) {
                   System.out.println("hello,thread");
                   try {
                       Thread.sleep(1000);
                   } catch (InterruptedException e) {
                       e.printStackTrace();
                   }
               }
            });
            t.start();
    
            //在主线程中可以使用一个等待操作,来等待t线程的执行结束
            try {
                t.join(5000);//最多等待5秒
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
    
            System.out.println("main等待完毕");
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24

    在这里插入图片描述

    首先,调用t.join()这个方法的线程是main线程,针对t这个线程对象调用的,此时就是让main等待t
    调用join之后,main线程就会进入阻塞状态(暂时无法在cpu上执行),直到t线程执行完毕,main线程才会被唤醒,继续向下执行。
    通过线程等待,一定程度上的干预了线程的执行顺序

    join()方法默认情况下,是死等(不见不散),如果t线程一直不执行完毕,main线程就一直等下去
    如果给join()方法添加参数,例如join(5000),表示main线程最多等5秒,如果在5秒之内,t线程执行完毕,main线程就不会再等。如果t线程5秒之后还在执行,main线程最多等待5秒就不再等待了

    7、获取当前线程的引用

    public static Thread currentThread()就能够获取到当前线程的引用(Thread实例的引用)。
    哪个线程调用的这个方法,就获取到的是哪个线程的实例

    public class Demo13 {
        public static void main(String[] args) {
                Thread t1 = new Thread(){
                @Override
                public void run() {
                    //获取当前线程的实例
                    //这个代码是通过继承Thread的方式来创建线程
                    //此时run方法中,直接通过this,拿到的就是当前的Thread的实例
                    System.out.println(Thread.currentThread().getName());
                    System.out.println(this.getName());
                }
            };
            t1.start();
    
            Thread t2 = new Thread(new Runnable() {
                @Override
                public void run() {
                    //此处的this不是指向Thread,而是指向Runnable,而Runnable只是一个单纯的任务,没有name属性的
                    //System.out.println(this.getName());
                    //要想拿到线程的名字,只能通过Thread.currentThread().getName()
                    //lambda表达式效果同Rannable
                    System.out.println(Thread.currentThread().getName());
                }
            });
            t2.start();
    
            //这个线程是在main线程中调用的,因此拿到的是main这个线程的实例
            System.out.println(Thread.currentThread().getName());
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30

    在这里插入图片描述

    注意:通过Runnable去构造Thread对象,在run方法中,this指向的是Runnable,而不是Thread对象。使用lambda去构造Thread对象和Runnable是一样的

  • 相关阅读:
    CentOS 7.6安装JDK8过程(通过官网下载压缩包方式)
    Spring-MVC的文件上传下载,及插件的使用(让项目开发更节省时间)
    针对 SAP 的增强现实技术
    对象解构 的几种方法
    基于Vue+Element UI+Node+MongoDB的医院门诊预约挂号系统
    企业微信自建应用 网页授权登录 获取用户信息
    王道机试C++第8章递归与分治 Day35和蓝桥杯两道真题程序
    五、2023.10.1.C++stl.5
    目标检测:cocoeval中的evaluateImg,accumulate函数解析
    Apache POI
  • 原文地址:https://blog.csdn.net/qq_56044032/article/details/127843960