• Java EE|多线程基本操作


    一、一个简单的线程程序及运行

    在写这样一个代码之前,我们需要对Thread类有一个简单/感性的认识

    我们已经知道进程是OS进行资源分配的基本单位,线程是OS进行调度的基本单位。也就是说其实进程和线程都是基于操作系统的概念。又因为app的运行需要OS进行协助,所以,app中必须要有针对进程和线程的处理,而OS中也必须有关于对app发送过来的这些信息进程接收和加工,从而启动硬件,完成我们的任务。

    简而言之,就是说OS和app之间需要有一个类似API一样的东西进行联系。但其实并不是一个就可以。因为我们经常会有这样的需求:不同的OS上运行相同的app;相同的OS上运行不同的app。

    因此,实际上,我们的OS会提供一套这样的类库,每个app又会提供一套用于连接OS的类库,不同的OS上对应的app类库不同。

    举个例子:

    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-GcIhP2et-1669768479691)(F:\typora插图\image-20221128125953291.png)]

    Thread 类是 JVM 用来管理线程的一个类。每个线程都有一个唯一的 Thread 对象与之关 联。

    另外,Thread类是一个实现了Runnable接口的类,其中这个线程跑起来就是靠的这个接口中run方法。但是这个跑是在后台跑,我们并不能看到明显的效果,因此为了看到效果,我们需要重写run方法。这里我们采用的是继承Thread类重写run方法。

    方法被调用了线程才能跑起来,怎么调用呢?这里用到的了Thread类对象的start方法。具体我会在后边讨论。也可以说只有start方法被调用了,这个线程才算创建成功了。

    复盘一下,怎么样才算创建好一个线程呢?

    ①MyThread类继承Thread类,重写run方法②调用start方法

    下边我们就来,写一下程序。

    class MyThread extends Thread{
        @Override
        public void run() {
            for(int i=0;i<100;i++){
                System.out.println("子线程:hello world");
            }
        }
    }
    public class ThreadDemo1 {
        public static void main(String[] args) {
            Thread t1=new MyThread();
            Thread t2=new MyThread();
    
            t1.start();
            t2.start();
    
            for(int i=0;i<100;i++){
                System.out.println("主线程:hello");
            }
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21

    (不完全运行截图)

    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-aHApEXQw-1669768479693)(F:\typora插图\image-20221128115426831.png)]

    除了程序运行结果,我们还可以通过jdk提供的线程的观察工具——jconsole.exe,观察线程。

    具体使用方法是:

    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-6cYrRmL6-1669768479694)(F:\typora插图\image-20221129092415580.png)]

    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-MmxsxXOX-1669768479695)(F:\typora插图\image-20221129090456698.png)]

    当然除了上述方法我们还可以通过下边这种方式进行观察:

    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-DX8Xytkx-1669768479696)(F:\typora插图\image-20221129093105948.png)]

    此时,我们的一个简单的线程程序就写好了。

    下边,我们来讨论几个问题:

    1.主线程&子线程

    1.什么是主线程?什么是子线程?

    当一个程序启动时,就有一个进程被OS创建,同时进程中的某个线程也立即运行。这个线程就叫做主线程。主线程就是用的main线程标识的。如果你通过jconsole观察没有发现,可能就是你的主线程没有阻塞,已经执行完了。

    子线程就是由其他线程所创建的线程。标号默认是从0开始标号的。

    2.主线程的重要性

    (1)是产生其他子线程的线程

    (2)它通常需要最后完成执行

    补充:

    main方法执行完了,主线程也就完了;同理,run方法完了,子线程也就完了。

    这里边的t1、t2、t3都是子进程、main是主进程。

    2.主线程和子线程的执行顺序

    由于线程抢占式执行的特点,使得线程调度也具有随机性。即使代码时固定的。

    因此,不同线程的执行顺序是不可预估的,具有随机性。

    这是由OS内核实现决定的。

    3.多线程程序和普通程序有什么区别

    • 每个线程都是一个独立的执行流
    • 多个线程之间是“并发”执行的。
    • 代码固定,执行顺序不一定固定

    虽然我们这里说是创建一个线程,但其实这个程序整体本质上还是一个多线程程序,main是一个主线程,创建的子线程包裹在主线程下。


    二、线程的创建

    上边我们已经写了一个简单的多线程程序,但线程的创建方法远不止一种。基本上有三种,另外还有两种基于此的拓展。

    方法一:继承Thread类

    class MyThread extends Thread{
        @Override
        public void run() {
            for(int i=0;i<100;i++){
                System.out.println("子线程:hello world");
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }
    }
    public class ThreadDemo1 {
        public static void main(String[] args) {
            Thread t1=new MyThread();
            t1.start();
            for(int i=0;i<100;i++){
                System.out.println("主线程:hello");
            }
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22

    方法二:实现Runnable接口

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

    方法三:匿名内部类创建Thread子类对象

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

    方法四:匿名内部类创建Runnable子类对象

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

    方法五:lambda表达式创建Runnable子类对象【用的比较多】

    public class ThreadDemo5 {
        public static void main(String[] args) {
            Thread t=new Thread(()-> System.out.println("hello world"));
            t.start();
            System.out.println("hello");
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7

    三、线程类——Thread详解

    每个执行流,需要有一个对象来描述,而 Thread 类的对象 就是用来描述一个线程执行流的,JVM 会将这些 Thread 对象组织起来,用于线程调度,线程管理。

    是java.lang包下的==>不需要导包

    常见构造方法

    方法说明
    Thread()创建对象
    Thread(Runnable target)使用Runnable对象创建线程对象
    Thread(String name)创建线程对象并命名
    Thread(Runnable target,String name)使用Runnable对象创建线程对象并命名
    Thread(ThreadGroup group,Runnable target)创建到指定线程组下

    假如我们起名字mythread,那么通过jconsole观察到的线程名就是mythread,而我们在代码中的t是代码中的变量名

    常见几个属性

    常见属性有id、名称、状态、优先级,但是由于都是私有类型的,所以必须要有对应方法才能访问到,除此以外还需判断几个特性比如是否存在,是否是后台线程,是否被中断。

    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-evYZYUAu-1669768479696)(F:\typora插图\image-20221128134356160.png)]

    属性获取方法
    idgetId()
    名称getName()
    状态getState()
    优先级getPriority()
    是否是后台程序isDaemon()
    是否存活isAlive()
    是否被中断isInterrupted

    其中:

    • id,线程的唯一标识,不同线程id不同。只能get,不能set.

    • 名称,就是我们在构造方法中起的名字,在各种调试工具会用到。可set可get。get通过getName()方法得到,set通过Thread构造方法设置。

    • 状态,表示当前线程所处的状态,直接下一篇会详细讨论(java中线程的状态会比OS原生状态更丰富一些)

    • 补充:Thread类中的静态方法:
      ①currentThread:获取当前执行线程的引用。
      一般来讲,状态的获取有两种方法:一是通过Thread类静态方法currentThread获得当前线程的引用,然后使用getState方法调用’Thread.currentThread.getState()';二是通过线程对象变量直接调用getState这个方法t.getState。再或者,如果是使用的继承Thread类定义的线程类,这个时候还会有this.getState()这种用法。这个时候我们需要特别注意什么时候打印的是当前线程实例(run方法),什么时候打印的是main线程(静态代码块、构造方法)。
      ②sleep:让当前线程休眠/暂停指定时间,主动放弃cpu资源竞争
      ③yeild:与sleep相似,但是放弃时间随机
      ④interrupted:测试当前线程是否中断
      当然这几个静态方法其实还有很多需要注意的地方,我们这里只是简单了解,等用到了再进行补充/总结。
    • 优先级高的线程理论上 更容易被调度到。但基本上没啥用,影响因素太多。

    • 后台进程:也就是守护线程。JVM会在一个进程的所有非后台进程结束后,才会结束运行

      我们这里结合前台线程解释后台线程。

      前台线程,会组织进程结束,前台线程没做完,进程完不了;

      后台线程,不会组织进程结束,后台线程工作没做完,进程也可以结束。

      [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-UNRf6OFv-1669768479697)(F:\typora插图\image-20221128143715204.png)]

      易错:线程(都是死循环)调度过程中的交替打印和线程是前台和后台没有关系。是前台和是后台只跟setDaemon的设置有关。

      思考:JVM进程什么退出?

      1.所有的前台线程全部退出

      2.与主线程无关,主线程的退出不影响

    • 是否存活:判断OS中线程是否存活也就是run方法是否运行结束

      t的回收:没有被引用,就会被GC回收。

      GC:(垃圾回收( Garbage Collection )是一种自动管理内存的机制)

      [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-oniz2T4Y-1669768479698)(F:\typora插图\image-20221128145629476.png)]

      [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-bAkcHQDI-1669768479698)(F:\typora插图\image-20221128150725440.png)]

    • 线程中断问题,下边会详细讨论

    线程的启动——start()

    调用start方法,才会真正在OS底层创建出一个线程。

    run方法和start方法功能对比:

    run方法:描述了线程要做的工作

    start方法:真正在OS内核中创建了一个线程,并且让这个线程调用run方法

    线程的中断

    此处中断的含义:让当前线程停止执行。【不要和OS中的中断弄混】

    注意:中断的意思不是让线程立即就停止,而是给线程发送一个通知,你要停止了,但是否真的停止,取决于线程具体的实现

    线程的中断常见的有两种方式

    (1)通过共享的标记来进行沟通

    public class ThreadDemo6 {
        public static boolean flag=true;
    
        public static void main(String[] args) {
            Thread t=new Thread(()-> {
                while(flag){
                    System.out.println("hello world");
                }
            });
            t.start();
            //在主线程中可以随时通过flag变量的取值,来操作t线程是否结束
            flag=false;
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14

    (2)调用interrupt方法进行通知【用的更多些】

    这里是用的Thread自带的标志位进行中断。这个东西可以唤醒上边的sleep

    public class ThreadDemo7 {
        public static boolean flag=true;
    
        public static void main(String[] args) {
            Thread t=new Thread(()-> {
                while(!Thread.currentThread().isInterrupted()){
                    System.out.println("hello world");
                }
            });
            t.start();
            t.interrupt();
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13

    currentThread方法:是Thread类的静态方法,通过这个方法可以获得当前线程,哪个线程调用这个方法,就得到哪个线程的引用,类似于this引用。

    isInterrupted方法:是在t.run中被调用的,这里获取的线程就是t线程。结果为true就是表示被终止,为false就是表示没有被终止(需要继续执行)。

    interrupt方法:就是终止线程。这里t.interrupt()就是终止t线程。这里是在main方法中,也就是主线程通知子线程,它需要中断了,

    方法说明
    public void interrupt()中断对象关联的线程,如果线程正在阻塞/sleep,就以异常方式通知,否则设置标志位,变成true
    public static boolean interrupted()(少)判断当前线程的中断标志位是否设置,调用后清除标志位
    public boolean isinterrupted()判断对象关联的线程的标志位是否设置,调用后不清楚标志位

    思考:“如果线程正在阻塞,就以异常方式通知,否则设置标志位,变成true”这句话怎么理解呢?

    1. 此时interrupt触发sleep内部的异常,导致sleep提前返回。

    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-pMm1oWrJ-1669768479698)(F:\typora插图\image-20221128154633191.png)]

    1. sleep被唤醒之后,又会把标志位设回false

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

      [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-d02J15vP-1669768479699)(F:\typora插图\image-20221128155307369.png)]


    特别注意:sleep存在时的线程中断

    interrupt做的事情:

    1. 线程内部的标志位会设置成true
    2. 线程若在sleep,会出发异常,把sleep唤醒

    sleep被唤醒之后做的事情:

    ​ 把刚刚设置的标志位,再设回false(清空标志位)

    效果:当sleep的异常被catch完后,循环还要继续执行

    这也就说明了,我们上边为什么说是通知它终止,而不是它一定终止。除此以外,sleep被唤醒之后,还有其他处理方式。下边我们来总结一下。

    (1)忽略中断请求

    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-xeRwXVy0-1669768479699)(F:\typora插图\image-20221128160804926.png)]

    (2)立即响应中断请求【加了break】

    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-HnM4m5IV-1669768479700)(F:\typora插图\image-20221128160832793.png)]

    (3)稍后进行中断【中间加了其他代码(任何)】

    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-FemsuNGD-1669768479701)(F:\typora插图\image-20221128160933166.png)]

    也就是说我们可以通过清除标志位之后的操作来决定线程t如何对待我们的中断请求。

    与之类似的是,像wait、join等造成代码“暂停”的方法都会有类似的清除标志位的设定。


    线程的等待——join()

    线程的等待就是等待一个线程结束。

    由于抢占式执行,我们无法判定两个线程谁先开始,但是我们可以通过调用join方法决定谁先结束。

    方法说明
    public void join()等待线程结束。如果仍未结束,那么就等待;反之立即返回
    public void join(long miles)等待线程结束,最多等miles秒
    public void join(long miles,int nanos)第二个的plus版本,体现在精度更高上

    其中这里的nanos是纳秒单位

    下边我们针对第一个join方法来举个例子:

    public class ThreadDemo8 {
        public static void main(String[] args) {
            Thread t=new Thread(()-> {
                    System.out.println("hello world");
                    try {
                        Thread.sleep(1000);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
            });
            t.start();
            System.out.println("join之前");
            try {
                //此时的join就是让当前的main线程等待t线程执行完再,继续执行
                //此时main线程走到这里就停止了,我们称为它被阻塞了
                //此时一定达到一个目的————t线程先于main线程结束
                System.out.println();
                t.join();
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            }
            System.out.println("join之后");
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24

    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-YerqEJP7-1669768479702)(F:\typora插图\image-20221128163344101.png)]

    上述情况是,执行join时,t仍未结束,那么main线程就会阻塞;

    但如果join不阻塞,main线程不会阻塞,会立即返回。

    相对于第一种,第二种使用的更多。第一种没有参数,相当于“死等”。第二种是指定一个最长等待时间,超过一定时间,不再等。

    而第三种,是第二种的plus版,其中nano是纳的意思,也就是说多少毫秒多少纳秒更加精准罢了。

    线程引用的获取

    方法说明
    public static Thread currentThread()返回当前线程的引用

    类似this引用,比较“智能”。哪个线程调用,就是哪个线程的引用。

    线程的休眠

    方法说明
    public static void sleep(long miles) throws InterruptedException休眠当前线程miles毫秒
    public static void sleep(long miles,int nanos) throws InterruptedException可以更高精度休眠

    四、多线程编程效率举例

    一般而言,程序分成使用cpu密集即需要大量运算和io密集即读写文件密集。

    我们之前一直在说多线程编程优于多进程编程,但是那只是理论上,我们并没有直观的感受。

    下边我们通过单线程和多线程实现对a和b分别自增100w次效率对比 ,来直观感受一下。

    public class ThreadDemo9 {
        public static void main(String[] args) {
            //serial();//串行耗时
            concurrency();//并行耗时
        }
        public static void serial(){
            long start=System.currentTimeMillis();
            long a=0;
            long b=0;
            for (long i = 0; i <100_0000_0000L; i++) {
                a++;
            }
            for (long i = 0; i <100_0000_0000L; i++) {
                b++;
            }
            long end=System.currentTimeMillis();
            System.out.println("执行时间:"+(end-start)+"ms");
        }
        public static void concurrency(){
            Thread t1=new Thread(()-> {
                long a=0;
                for (long i = 0; i <100_0000_0000L; i++) {
                    a++;
                }
            });
            Thread t2=new Thread(()-> {
                long b=0;
                for (long i = 0; i <100_0000_0000L; i++) {
                    b++;
                }
            });
            long start=System.currentTimeMillis();
            t1.start();
            t2.start();
            try {
                t1.join();
                t2.join();
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            }
            long end=System.currentTimeMillis();
            System.out.println("执行时间:"+(end-start)+"ms");
        }
    }
    
    • 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
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44

    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-FhUnxu4s-1669768479703)(F:\typora插图\image-20221128172121448.png)]

    接下来,我们讨论几个问题:

    1. 为什么两个线程的结果不是一个线程耗时的一半?

      线程的切换需要时间、不能保证两个线程总是并行执行,即使是cpu是多核的。

    2. 为什么要join?

      线程调度是随机的,如果不join,有可能a和b都还没有完成全部的自增任务,就已经结束了。

    3. start和end的定义时机特别注意

    参考

    Thread类静态方法参考

  • 相关阅读:
    OD华为机试 15
    干货分享:有哪些好用的ocr图片文字识别软件?
    C#运算符和流程控制语句
    【Web】Java反序列化之再看CC1--LazyMap
    【OpenCV 例程200篇】216. 绘制多段线和多边形
    Nginx配置文件
    IDEA02:配置SQL Server2019数据库
    阿里P8大佬,耗时72小时整理的Docker实战笔记,你值得拥有
    Python中将列表拆分为大小为N的块
    基于Spring、SpringMVC、MyBatis毕业生就业信息管理系统
  • 原文地址:https://blog.csdn.net/moteandsunlight/article/details/128108491