Java-Day-22( 线程一:相关概念 + 继承 Thread 类 + 实现 Runnable 接口 + JConsole 监控 + 售票比较 )
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 方法
使用方式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 统一调度 ( 想细懂会涉及到操作系统 )
- 即真正实现多线程效果的是 start() 里的 start0(),而不是 run()
使用方式2:实现 Runnable 接口
-
由于 java 是单继承的,所以在某些情况下此类可能以及继承了某个父类,此时想再继承 Thread 类方法创建线程明细不可能,
- 所以提供了另一个创建线程的方法:用 Runnable 接口来创建线程
-
请编写程序,该程序可以每隔一秒钟在控制台进行输出,输出十次后自动退出 ( 使用实现 Runnable 接口的方式实现 )
public class test1 { public static void main(String[] args) { 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) { 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) { // 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
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· TypeScript + Deepseek 打造卜卦网站:技术与玄学的结合
· 阿里巴巴 QwQ-32B真的超越了 DeepSeek R-1吗?
· 如何调用 DeepSeek 的自然语言处理 API 接口并集成到在线客服系统
· 【译】Visual Studio 中新的强大生产力特性
· 2025年我用 Compose 写了一个 Todo App