Java-Day-22( 线程一:相关概念 + 继承 Thread 类 + 实现 Runnable 接口 + JConsole 监控 )

发布时间 2023-05-16 23:21:56作者: 朱呀朱~

Java-Day-22

线程相关概念

  • 程序:是为完成特定任务、用某种语言编写的一组指令的集合 ( 就是平常所写代码 )

  • 进程:运行中的程序,例如,打开一个软件就启动一个进程,操作系统就会给每个启动的软件分配一新的内存 ( 活动进程占用的物理内存 ) 空间

    • 进程是程序的一次执行过程,或是正在运行的一个程序。是动态过程:有其自身的产生、存在和消亡的过程 ( 任务管理器 )
  • 线程

    • 由进程创建的,是进程的一个实体
    • 一个进程可以拥有多个线程
      • 就像是一个网盘 ( 进程 ) 可以同时进行多个下载 ( 线程 )
  • 线程分类

    • 单线程:同一个时刻,只允许执行一个线程
    • 多线程:同一时刻可以指向多个线程 ( 如网盘的多个下载、QQ 的多个聊天框 )
  • 并发:同一时刻,多个任务交替执行,造成一种 “ 貌似同时 ” 的错觉 ( 实际就是多件事短时间迅速切换 )。简单来说,单核 cpu 实现的多任务就是并发

  • 并行:同一时刻,多个任务同时执行。多核 cpu 可以实现并行 ( 多个 cpu 各干各的 )

    • 并发和并行也可以同时,即有一个 cpu 并发多个交替,另一个仅执行一个,整体来看就是并行 ( " 右键点击此电脑 —> 管理 —> 设备管理器 —> 处理器 " 查看 cpu 数 )
    • cpu:中央处理器,是处理器的一种,一般情况就直接称之为处理器
    public static void main(String[] args) {
        Runtime runtime = Runtime.getRuntime();
        //        获取当前电脑的cpu数量,available:可获得的 Processors:处理器
        int cpuNums = runtime.availableProcessors();
        System.out.println("当前电脑cpu:" + cpuNums + "核");
    }
    

线程基本使用

  • 创建线程的两种方式

    • 继承 Thread 类,重写 run 方法
    • 实现 Runnable 接口,重写 run 方法

    image-20230515171406538

使用方式1:继承 Thread 类

  • 编写一个程序,开启一个线程,该线程每隔 1 秒在控制台输出 " bark ! 汪 ! "

  • 改进限制,当输出 8 次后就退出程序

    public class test {
        public static void main(String[] args) {
    //        创建 Dog 对象当作线程使用
            Dog dog = new Dog();
    //        启动线程,源码有执行 run()
            dog.start();
        }
    }
    
    // 当一个类继承了 Thread 类,该类就可以当作线程使用
    // 我们会重写 run 方法,写上自己的业务代码逻辑
    // run() 实际也是 Thread 重写的接口 Runnable 的方法
    class Dog extends Thread {
    
        int times = 0;
        @Override
        public void run() {
            while (true) { // 因为只执行了一次,想要多次输出的话就循环
    //        每隔一秒钟输出
                System.out.println("bark ! 汪 !" + (++times));
    //        让该线程休眠1秒
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                if (times == 8) {
                    break; // 到8次就 退出循环,线程也就退出
                }
            }
        }
    }
    

JConsole 监控线程执行情况

引出 ( 多线程 )

// main内修改
public static void main(String[] args) throws InterruptedException {
    //        创建 Dog 对象当作线程使用
    Dog dog = new Dog();
    //        启动线程,源码有执行 run()
    dog.start();
    //        当main线程启动一个子线程 Thread-0,主线程不会阻塞,而是会继续执行

    System.out.println("主线程" + Thread.currentThread().getName() + "正在执行"); // 名为 main
    for (int i = 0; i < 10; i++) {
        System.out.println("主线程 i = " + i);
        //            让主线程休眠
        Thread.sleep(1000);
        //            主main线程和Thread-0线程交替执行
    }
}

// while循环内修改输出部分
System.out.println("bark ! 汪 !" + (++times) + "。线程名为:" + Thread.currentThread().getName());
  • 输出为:( 进程 —> main 线程 — ( start ) —> Thread-0 线程 )

    主线程main正在执行
    bark ! 汪 !1。线程名为:Thread-0
    主线程 i = 0
    主线程 i = 1
    bark ! 汪 !2。线程名为:Thread-0
    主线程 i = 2

    ......

  • main 与 Thread-0 交替执行 ( 根据电脑可能规律不同,但都是交替执行:当 main 线程启动一个子线程 Thread-0,主线程不会阻塞,而是会继续执行 )

jconsole 使用:

  • 给 main 和 Thread-0 以足够多的循环 ( 例如 main 循环 60 次,Thread-0 循环 80 次 )

  • 运行后进程开启,打开 Terminal 输入 jconsole 回车,弹窗点击代码所在 java 文件进行连接 ( 后 PID 为进程号 ),如出现,点击不安全的连接,打开导航栏的线程,发现左下角显示线程中有 main 和 Thread-0,并随着 60 次已到 main 线程退出消失只剩下 Thread-0,80 次已有后,连接断开线程结束,Thread-0 不再消失,因为断开不再刷新,弹窗连接断开

    • 可见虽然 main 线程 start 出了 Thread-0 线程,但是 main 线程结束不会妨碍其子线程 ( Thread-0 ) 的继续,进程也是等到所有线程都结束后才会结束断开
  • 若是非 main 内 dog.start();,而是直接调用 dog.run() ( class dog 内重写的方法 ),会发现

    • 此时只是简单执行一个 run(),并没有真正启动一个线程
    • 输出就是非交替进行,而是把 run() 循环输出结束后再执行 main 内的循环输出语句 ( 输出被阻塞 )
    • 输出可见:线程 Thread-0 并没有创建,仍都是 main 线程
  • 源码

    public synchronized void start() {
        // ...
        start0;
        // ...
    }
    
    private native void start0();
    // start0 是一个本地方法,是 JVM 调用,底层是c/c++实现
    
    • 即真正实现多线程效果的是 start() 里的 start0(),而不是 run()
      • 内部 start0() 被调用后,该线程并不一定会立马执行,只是将线程变成了可运行状态。具体什么时候执行,取决于 CPU,由 CPU 统一调度 ( 想细懂会涉及到操作系统 )

使用方式2:实现 Runnable 接口

  • 由于 java 是单继承的,所以在某些情况下此类可能以及继承了某个父类,此时想再继承 Thread 类方法创建线程明细不可能,

    • 所以提供了另一个创建线程的方法:用 Runnable 接口来创建线程
  • 请编写程序,该程序可以每隔一秒钟在控制台进行输出,输出十次后自动退出 ( 使用实现 Runnable 接口的方式实现 )

    public class test1 {
        public static void main(String[] args) throws InterruptedException {
            Dog dog = new Dog();
    //        dog.start(); 无此方法,接口只有run(),但
    //        dog.run(); 但这样直接调用还是指普通方法,并无线程创建
    
    //        所以使用以下编码方式进行线程的创建 
            Thread thread = new Thread(dog);
            thread.start();
        }
    }
    
    class Dog implements Runnable {
    
        int count = 0;
        @Override
        public void run() {
            while (true) {
                System.out.println("输出:啊呜~~" + (++count) + "。线程名为:" + Thread.currentThread().getName());
    //        输出后让线程休眠1秒
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                if (count == 10) {
                    break;
                }
            }
        }
    }
    
    • 此处是使用了 ( 静态 ) 代理的设计模式

    • 简单模拟代理模式:

      class ThreadProxy implements Runnable { // Proxy(代理)
      //    这里用 ThreadProxy 当作 Thread 来进行模拟
      
          private Runnable target = null; // 属性,类型为 Runnable,接收implements的对象
          @Override
          public void run() {
              if (target != null){
                  target.run();
              }
          }
      
          public ThreadProxy(Runnable target) {
              this.target = target;
          }
      
          public void start() {
              start0();
          }
          public void start0() {
              run();
          }
      }
      
      • thread.start():代理用 target 接收对象,执行 start() —> start0() —> 代理内的 run() —> 动态绑定对象重写的 run()
  • 多线程练习 ( 多个工作就启动线程,来让多个线程执行多个工作 )

    • 编写一个程序,创建两个线程,一个线程每隔一秒输出 ” hello,world “,输出十次,退出,一个线程每隔一秒输出 ” hi “,输出五次退出

      public class test1 {
          public static void main(String[] args) throws InterruptedException {
              T1 t1 = new T1();
              T2 t2 = new T2();
              Thread thread1 = new Thread(t1);
              Thread thread2 = new Thread(t2);
              thread1.start();
              thread2.start();
          }
      }
      
      // 创建两个线程:T1、T2
      class T1 implements Runnable {
          int count = 0;
          @Override
          public void run() {
              while (true) {
                  System.out.println("hello,world" + (++count));
                  try {
                      Thread.sleep(1000);
                  } catch (InterruptedException e) {
                      e.printStackTrace();
                  }
                  if (count == 10) {
                      break;
                  }
              }
          }
      }
      class T2 implements Runnable {
          int count = 0;
          @Override
          public void run() {
              while (true) {
                  System.out.println("hi" + (++count));
                  try {
                      Thread.sleep(1000);
                  } catch (InterruptedException e) {
                      e.printStackTrace();
                  }
                  if (count == 5) {
                      break;
                  }
              }
          }
      }
      

继承 Thread vs 实现 Runnable

  • 从 java 的设计来看,通过继承 Thread 或者实现 Runnable 接口来创建线程本质无区别,因为 Thread 类本身就实现了 Runnable 接口

  • 实现 Runnable 接口方式更加适合多个线程共享一个资源的情况,并且避免了单继承的限制 ( 建议使用 Runnable 接口 )

    T1 t1 = new T1("hello~");
    Thread thread1 = new Thread(t1);
    Thread thread2 = new Thread(t1);
    // 两个线程同时都执行一个对象T1
    thread1.start();
    thread2.start();
    
  • 编程模拟三个售票窗口,售存票 100 张,分别使用继承 Thread 和实现 Runnable 方式,并加以分析

    public class SellTicket {
        public static void main(String[] args) throws InterruptedException {
    //        SellTicket01 sellTicket01 = new SellTicket01();
    //        SellTicket01 sellTicket02 = new SellTicket01();
    //        SellTicket01 sellTicket03 = new SellTicket01();
    //
    ////        可能会出现负数票超卖的问题
    //        sellTicket01.start(); // 买票启动,1、2、3窗口共用ticketnum资源
    //        sellTicket02.start();
    //        sellTicket03.start();
    
    
    //          仍可能会出现负数票超卖的问题
            SellTicket02 sellTicket02 = new SellTicket02();
    //        使用实现接口的方式来售票
            new Thread(sellTicket02).start(); // 第一个窗口(简写方式)
            new Thread(sellTicket02).start(); // 第二个窗口
            new Thread(sellTicket02).start(); // 第三个窗口
        }
    }
    
    // 使用 Thread 方式
    class SellTicket01 extends Thread {
    //    让多个线程共享售票数,要 new 多个对象,所以要用static静态
        private static int ticketNum = 100;
        @Override
        public void run() {
            while (true) {
    
                if (ticketNum <= 0) {
                    System.out.println("售票结束...");
                    break;
                }
    
    //            休眠50ms,模拟缓冲
                try {
                    Thread.sleep(50);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
    
                System.out.println("窗口 " + Thread.currentThread().getName() + " 出售了一张票 "
                + "剩余票数 = " + (--ticketNum));
            }
    //        会出现负数票超卖的问题,因为可能窗口一判断符合后,还没到减减部分,下一个窗口也挤着判断进入了执行
        }
    }
    
    // 实现 Runnable 接口方式
    class SellTicket02 implements Runnable {
    //    没有静态的必要了
        private int ticketNum = 100;
        @Override
        public void run() {
            while (true) {
    
                if (ticketNum <= 0) {
                    System.out.println("售票结束...");
                    break;
                }
    
    //            休眠50ms,模拟缓冲
                try {
                    Thread.sleep(50);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
    
                System.out.println("窗口 " + Thread.currentThread().getName() + " 出售了一张票 "
                        + "剩余票数 = " + (--ticketNum));
            }
    //        会出现负数票超卖的问题,因为可能窗口一判断符合后,还没到减减部分,下一个窗口也挤着判断进入了执行
        }
    }
    
    • 两者都有超卖的可能性,要想解决需要用到后面将学的 Synchronized