# Java Demo **Repository Path**: lzr12356789/java-demo ## Basic Information - **Project Name**: Java Demo - **Description**: No description available - **Primary Language**: Unknown - **License**: Not specified - **Default Branch**: master - **Homepage**: None - **GVP Project**: No ## Statistics - **Stars**: 0 - **Forks**: 0 - **Created**: 2025-02-27 - **Last Updated**: 2025-04-08 ## Categories & Tags **Categories**: Uncategorized **Tags**: None ## README # 多线程 # 一. 线程的概念 1. #### 程序 进程与线程的区分: ##### 程序(program): ​ 为完成特定任务,用某种语言编写的一组指令的集合,即指一段静态的代码,静态地对象 ##### 进程(process): ​ 程序的一次执行过程,或者是正在内存中运行的应用程序,如:运行中的QQ,网易云等 ​ 每一个进程都有一个独立的内存空间,系统运行的一个程序即是一个进程从创建 运行到消亡的过程(生命周期) ​ 程序是静态的,进程是动态的 ​ 进程作为操作系统调度和分配资源的最小单位(亦是系统运行程序的基本单位),系统在运行时会为每个进程分配不同 的内存区域 ##### 线程(thread): ​ 进程可进一步细化为线程,是程序内部的一条执行路径,一个程序至少有一个线程 ​ 一个进程同一时间若并行执行多个线程,就是支持多线程的 2. #### 线程调度策略: ##### 分时调度: ​ 所有线程 轮流使用 CPU的使用权,并且平均分配每个线程占用CPU的时间 ##### 抢占式调度: ​ 让优先级高的线程以较大的概率优先使用CPU,如果优先级相同,那么就会随机选择一个(线程随机性) Java使用的是抢占 式 # 二. 线程的创建 1. #### 线程的创建方法一: 继承Thread类 ##### 1.1 步骤: ​ 1.创建一个类继承于Thread类的子类 ​ 2. 重写Thread类的run()方法 ---->将此线程将要执行的操作放在此方法体里 ​ 3. 创建当前Thread子类的对象 ​ 4. 通过当前对象调用start()方法 ##### 1.2 例题: ​ 1. 创建一个分线程1,用于遍历100以内的偶数 ```java // 1. 创建一个继承于Thread类的子类 public class MyThread extends Thread { // 2. 重写Thread类的run()方法,实现线程的功能 @Override public void run() { for (int i = 0; i <= 100; i++) { if (i % 2 == 0) { System.out.println(i); } } } } ``` ```java public class EvenNumber { public static void main(String[] args) { // 3. 创建一个Thread类的子类的对象 MyThread t = new MyThread(); // 4. 通过Thread类的对象调用start() t.start(); } } ``` ​ start()作用: 1. 启动线程 2. 调用当前线程的run() 练习1:创建两个分线程,其中一个线程遍历100以内的偶数,另一个遍历100以内的奇数 2. #### 线程创建的第二种方法: 实现 Runnable 接口: ##### 2.1 步骤: ​ 1. 创建一个实现 Runnable 接口的类 ​ 2. 实现接口中的 run() --->此线程要执行的操作,声明到此方法体内 ​ 3. 创建当前实现类的对象 ​ 4. 将此对象作为参数传到 Thread 类的构造器中,创建这个类的实例 ​ 5. 通过 Thread 的实例,调用 start(); 3. #### 对比两种方式: ##### 共同点: ​ 1. 要想启用线程,使用的都是 Thread 类中定义的run(); ​ 2. 创建的线程的对象,都是 Thread 类或其子类的实例 ##### 不同点: ​ 一个是类的继承,一个是接口的实现 (建议使用 Runnable 接口的方式) ​ Runnable 方式的好处: ​ 1. 避免了类的单继承的局限性 ​ 2. 更适合来处理共享数据的问题 ​ 3. 实现了代码和数据的分离 ##### 联系: ​ public class Thread implements Runnable (代理模式) # 三 线程的常用方法与线程的生命周期 ### 一 线程的常用结构 #### 1. 线程中的构造器: ​ public Thread() : 分配一个新的线程对象 ​ public Thread(String name) : 分配一个指定名字的新的线程对象 ​ public Thread(Runnable target) : 指定创建线程的目标对象,它实现了Runnable 接口中的run()方法 ​ public Thread(Runnable target,String name) : 分配一个带指定目标的新线程对象,并指定名字 #### 2. 线程中常用的方法: ​ start() : 1. 启动线程 2. 调用run() ​ run(): 将线程要执行的操作,声明在 run() 中 ​ currentThread(): 获取当前执行代码的线程 ​ getName(): 获取线程名 ​ setName: 设置线程名 ​ sleep(long millis): 静态方法,调用时,可使当前线程睡眠指定的毫秒数 ​ yield(): 一旦执行此方法,就会释放CUP的执行权 ​ join(): 在线程 a 中通过线程 b 调用join(),意味着线程a会进入阻塞状态,直到线程结束,才会结束阻塞,继续执行线程a ​ isAlive(): 判断线程是否存活,返回布尔值 ​ 过时方法: ​ stop():强行结束一个线程的执行,直接进入死亡状态。不建议使用 ​ void suspend()/void resume():可能造成死锁,所以也不建议使用 #### 3. 线程的优先级: ​ getPriority():获取线程的优先级 ​ setPriority(): 设置线程的优先级,范围[1到10] ###### Thread类内部声明的三个常量: ​ MAX_PRIORITY(10):最高优先级 ​ MIN_PRIORITY(1):最低优先级 ​ NORM_PRIORITY(5):普通优先级,默认情况下main线程具有普通优先级。 #### 二. 线程的生命周期: ###### 1.JDK1.5之前的生命周期 ​ ![](E:\JAVA Project\Multithreading\img\线程的声明周期(JDK1.5之前).png) 2. ###### JDK1.5之后: ![](E:\JAVA Project\Multithreading\img\线程的声明周期(JDK1.5之后).png) # 四 线程安全问题与线程的同步机制 1. #### 多线程卖票出现的线程安全问题: ```java public class SellTickets extends Thread { static int tickets = 100; @Override public void run() { while (true) { if (tickets > 0){ System.out.println(Thread.currentThread().getName() + "正在出售第" + tickets + "张票"); tickets--; }else{ break; } } } } ``` ```JAVA public class test { public static void main(String[] args) { SellTickets t1 = new SellTickets(); SellTickets t2 = new SellTickets(); SellTickets t3 = new SellTickets(); t1.setName("窗口1"); t2.setName("窗口2"); t3.setName("窗口3"); t1.start(); t2.start(); t3.start(); } } ``` ​ 出现了重票和错票 2. #### 什么原因导致的? ​ 线程1在操作 tickets 的时候,尚未结束的情况下,其他线程就参与进来了,对 tickets 进行操作 3. #### 如何解决这个问题? ​ 必须保证一个线程 a 在操作 tickets 的过程中,其他线程必须等待,直到线程 a 操作结束之后,其他线程才能参与 4. #### JAVA是如何解决线程安全问题的? ​ 使用线程的同步机制 ​ 方式1: 同步代码块 ​ synchronized (同步监视器) { ​ 需要被同步的代码 ​ } ​ 说明: ​ 需要同步的代码: 就是需要共享数据的代码 ​ 共享的数据: 即多个线程需要操作的数据,比如 tickets ​ 需要被同步的代码: 在被 synchronized () 包裹以后,就使得一个线程在操作这些代码的过程中,其他线程必须等待 ​ 同步监视器: 俗称 锁 ,哪个线程获取了锁,哪个线程就能执行需要被同步的代码 ​ 同步监视器: 可以使用任何一个类的对象充当, 但是多个线程必须共用一个同步监视器 ​ 注意: 在实现 Runnable 接口的方式中,同步监视器可以考虑使用 this ,而在继承 Thread 类的方式中,同步监视器慎用 this ,考虑使用: 当前类.class ```Java /** * ClassName: SellTickets * Description: * 售票, 使用同步代码块解决线程安全问题 * * @Author L_Z_R * @Create 2025/03/04 19:24 * @Version 1.0 */ public class SellTickets extends Thread { static int tickets = 100; @Override public void run() { while (true) { synchronized (SellTickets.class) { if (tickets > 0) { System.out.println(Thread.currentThread().getName() + "正在出售第" + tickets + "张票"); tickets--; } else { break; } } } } } ``` ```Java /** * ClassName: test * Description: * * @Author L_Z_R * @Create 2025/03/04 19:21 * @Version 1.0 */ public class test { public static void main(String[] args) { SellTickets t1 = new SellTickets(); SellTickets t2 = new SellTickets(); SellTickets t3 = new SellTickets(); t1.setName("窗口1"); t2.setName("窗口2"); t3.setName("窗口3"); t1.start(); t2.start(); t3.start(); } } ``` ​ 方式2: 同步方法: ​ 说明: ​ 如果操作共享数据的的代码完整的声明在了一个方法中,那么我们可以将此方法声明为同步方法 ​ 非静态的同步方法,默认同步监视器就是 this ​ 静态的同步方法,默认同步监视器就是当前类本身 ```Java // 1. 创建一个实现 Runnable 接口的类 class EvenNumberPrint implements Runnable { static int tickets = 100; static boolean flag = true; @Override public void run() { while (flag) { show(); } @Override public void run() { while (flag) { show(); } } public synchronized void show(){ try { Thread.sleep(10); } catch (InterruptedException e) { e.printStackTrace(); } if (tickets > 0) { System.out.println(Thread.currentThread().getName() + "正在出售第" + tickets + "张票"); tickets--; } else { flag = false; } } } public class EvenNumber { public static void main(String[] args) { // 3. 创建当前实现类的对象 EvenNumberPrint evenNumberPrint = new EvenNumberPrint(); // 4. 将此对象作为参数传到 Thread 类的构造器中,创建这个类的实例 Thread t = new Thread(evenNumberPrint); // 5. 通过 Thread 的实例,调用 start(); t.start(); } } ``` #### 5.synchronized 的好处: ​ 解决了线程的安全问题 ​ 弊端: ​ 在操作共享数据时,多线程其实是串行执行的,意味着性能就低一些. #### 6.线程同步可能带来的问题:死锁 ##### 1. 什么是死锁? ​ 不同的线程分别占用对方需要的同步资源不放弃,都在等待对方放弃自己需要的同步资源,就形成了线程的死锁。 ​ 我们编写程序时,要避免出现死锁 ##### 2. 诱发死锁的原因? ​ 互斥条件 ​ 占用且等待 ​ 不可抢夺(或不可抢占) ​ 循环等待 ​ 以上4个条件,同时具备就会触发死锁 ##### 3. 如何解决死锁? ​ 死锁一旦出现,基本很难人为干预,只能尽量规避,可以考虑打破以上的4种诱发条件 ​ 针对条件1:互斥条件基本上无法被破坏。因为线程需要通过互斥解决安全问题 ​ 针对条件2:可以考虑一次性申请所有所需的资源,这样就不存在等待的问题 ​ 针对条件3:占用部分资源的线程在进一步申请其他资源时,如果申请不到,就主动释放掉已经占用的资源 ​ 针对条件4:可以将资源改为线性顺序。申请资源时,先申过序号较小的,这样避免循环等待问题 7. #### lock锁的使用: ##### 1.步骤: ```Java public class SellTickets extends Thread { static int tickets = 100; // 1.创建lock的实例,需要确保多个线程共用一个lock实例,需要考虑将声明为 static final private static final ReentrantLock lock = new ReentrantLock(); @Override public void run() { while (true) { try { // 2.执行lock()方法,锁定共享资源的调用 lock.lock(); if (tickets > 0) { System.out.println(Thread.currentThread().getName() + "正在出售第" + tickets + "张票"); tickets--; } else { break; } } finally { // 3.unlock()释放共享资源的锁定 lock.unlock(); } } } } ``` ##### 2. 面试题: ​ synchronized 同步的方式与 lock 的对比: ​ synchronized : 不管是同步代码块,还是同步方法,都需要结束一对{}之后,释放对同步监视器的调用 ​ lock: 是通过两个方法控制需要被同步的代码,要更灵活一些 ​ lock 作为接口,提供了多种实现类,适合更多的复杂的场景,效率更高 ​ # 五. 线程的通信 1. #### 线程间通信的理解: ​ 当我们需要多个线程来共同完成一件任务,并且希望他们有规律的去执行,那么多线程之间就需要一些通信机制,可以协调他们工作, 以此实现多线程共同操作一份数据 2. #### 涉及到三个方法的使用: ​ wait(): 线程一旦执行此方法,就会进入等待状态,同时会释放同步监视器的调用 ​ notify(): 一旦执行此方法,那么就会唤醒被 wait() 的线程中优先级最高的那个线程 ​ (如果被 wait() 的多个线程优先级相同,则随机唤醒一个) , 被唤醒的线程从当初被 wait() 的位置继续执行 ​ notifyAll(): 一旦执行此方法,那么就会唤醒所有被 wait() 的线程 3. #### 注意点: ​ 此三个方法的使用,必须是在同步代码块或同步方法中 ​ 此三个方法的调用者,必须是同步方法的同步监视器,否则会报异常 ​ 此三个方法声明在Object类当中 4. wait() 和 sleep() 的区别? ​ 相同点: 一旦执行,当前线程都会进入阻塞状态 ​ 不同点: ​ 声明的位置: ​ wait(): 声明在 Object类中 ​ sleep(): 声明在 Thread 类中,静态的 ​ 使用的场景不同: ​ wait(): 只能在同步代码块,或同步方法中 ​ sleep(): 可以在任何需要使用的场景 ​ 使用在同步方法或代码块中: ​ wait(): 会释放同步监视器 ​ sleep(): 不会释放同步监视器 ​ 结束阻塞状态的方式: ​ wait(): 到达指定时间自动结束,或则被 notify() 唤醒结束阻塞 ​ sleep(): 到达指定时间自动结束