Java 中线程的使用

devtools/2025/2/2 9:31:07/

文章目录

  • Java 线程
      • 1 进程
      • 2 线程
      • 3 线程的基本使用
        • (1)继承 Thread 类,重写 run 方法
        • (2)实现 Runnable 接口,重写 run 方法
        • (3)多线程的使用
        • (4)线程的理解
        • (5)继承 Thread 和 实现 Runnable 接口的比较
      • 4 线程的终止
      • 5 线程的常用方法
      • 6 用户线程和守护线程
      • 7 线程的生命周期(状态)
      • 8 线程的同步(synchornized)
      • 9 互斥锁
        • (1)对象互斥锁的作用
        • (2)synchronized 的 2 种用法
        • (3)锁的规则
        • (4)同步的局限性
        • (5) 实际应用示例(售票问题)
        • (6) 最佳实践
      • 10 死锁与释放锁
        • 10.1 死锁示例
        • 10.2 死锁产生条件:
        • 10.3 释放锁的操作
          • 1. 同步方法/代码块执行结束
          • 2. 遇到 `break` 或 `return`
          • 3. 未处理的异常或错误
          • 4. 调用 `wait()` 方法
        • 10.4 不释放锁的操作
          • 1. `Thread.sleep()` 或 `Thread.yield()`
          • 2. 调用 `suspend()`(已弃用)
        • 10.5 关键结论
        • 10.6 如何避免死锁

Java 线程

程序: 是为了完成特定任务,用某种语言编写的一组指令的集合。

1 进程

进程: 进程是指运行中的程序,比如我们使用 QQ,就启动了一个进程,操作系统就会为该进程分配内存空间。当我们使用迅雷,又启动了一个进程,操作系统将为迅雷分配新的内存空间。

进程是程序的一次执行过程,或是正在运行的一个程序。是动态过程:有它自身的产生、存在和消亡的过程。


2 线程

  1. 线程由进程创建的,是进程的一个实体。
  2. 一个进程可以有多个线程。
  3. 单线程: 同一个时刻,只允许执行一个线程。
  4. 多线程: 同一个时刻,可以执行多个线程,比如:一个qq进程里面,可以同时打开多个聊天窗口,一个迅雷进程,可以同时下载多个文件。
  5. 并发: 同一个时刻,多个任务交替执行,造成一种“貌似同时”的错觉,简单的说,单核cpu实现的多任务就是并发。
  6. 并行: 同一个时刻,多个任务同时执行,多核cpu可以实现并行。

3 线程的基本使用

  在 java 中,线程的使用有 2 种方法:

  1. 继承 Thread 类,重写 run 方法。
  2. 实现 Runnable 接口,重写 run 方法。
Thread__run__31">(1)继承 Thread 类,重写 run 方法

Thread 类的关系如下图:

image-20250128153808448

  Runnable 接口的源码为:

image-20250128153931039

创建线程的案例:

(1) 开启一个线程,该线程每隔 1 秒,在控制台输出 “Cat…Cat…”。

(2) 当输出 10 次时,结束该线程。

(3) 使用 JConsole 监控线程执行情况,并画出示意图。

java">public class Extend_thread {public static void main(String[] args) {Cat cat = new Cat();cat.start();int i = 0;while(true) {System.out.println("主线程" + ++i);try {Thread.sleep(1000);} catch (InterruptedException e) {throw new RuntimeException(e);}if(i >= 10) {break;}}}
}
class Cat extends Thread {@Overridepublic void run() {int count = 0;while(true) {System.out.println("Cat...Cat..." + (++count) +"\t线程名" +Thread.currentThread().getName());//休眠1秒try {Thread.sleep(1000);} catch (InterruptedException e) {throw new RuntimeException(e);}//输出到达10,退出if(count >= 10) {break;}}}
}

在Java中,start()方法是启动线程的入口点。当调用start()方法时,Java虚拟机(JVM)会执行以下步骤来启动线程:Thread 中的 start() 方法如下图:可以看到实际上是,start0() 方法担任了创建线程的工作。

image-20250128155627490

  1. 创建线程对象:首先,JVM会创建一个线程对象。这个对象与线程的执行环境相关联,包括线程的堆栈、程序计数器等。
  2. 调用线程的run()方法:线程对象创建完成后,JVM会调用线程的run()方法。run()方法是线程执行的具体逻辑。在这个例子中,Cat类继承了Thread类,并重写了run()方法。因此,当cat.start()被调用时,Cat类的run()方法将被执行。
  3. 线程执行run()方法中的代码将开始执行。在这个例子中,run()方法中的代码会循环打印信息,并在每次循环后休眠1秒。当计数器count达到10时,循环结束,线程执行完毕。
  4. 线程结束:当run()方法执行完毕后,线程将结束。JVM会清理与该线程相关的资源,如堆栈等。

  执行结果如下:可以看到,其它线程的执行,并不会影响主线程(main方法)的执行。

image-20250128154606257

注意: start() 方法调用 start0() 方法后,该线程并不一定会立马执行,只是将线程变成了可运行状态。具体什么时候执行,取决于 CPU ,由 CPU 统一调度。


(2)实现 Runnable 接口,重写 run 方法

java 是单继承的,在某些情况下,一个类可能已经继承了某个父类,这时再用继承 Thread 类的方法来创建线程显然就不行了。所以,引入了实现 Runnable 接口来创建线程。

案例实现:

(1) 开启一个线程,该线程每隔 1 秒,在控制台输出 “Dog…Dog…”。

(2) 当输出 10 次时,结束该线程。

(3) 使用 实现 Runnable 接口的方式实现。

java">package com.xbf.thread;public class DaiLi {public static void main(String[] args) {Dog dog = new Dog();Thread thread = new Thread(dog);thread.start();int i = 0;while(true) {System.out.println("主线程" + ++i);try {Thread.sleep(1000);} catch (InterruptedException e) {throw new RuntimeException(e);}if(i >= 10) {break;}}}
}
class Animal {}
class Dog extends Animal implements Runnable {@Overridepublic void run() {int count = 0;while(true) {System.out.println("Dog...Dog..." + (++count));try {Thread.sleep(1000);} catch (InterruptedException e) {throw new RuntimeException(e);}if(count >= 10) {break;}}}
}

在这个例子中,start()方法的执行流程与之前类似,但有一些关键的区别,因为这次是通过实现Runnable接口来创建线程,而不是通过继承Thread类。以下是详细的执行流程:

  1. 创建线程对象
  • thread.start()被调用时,JVM会创建一个线程对象。
  • 这个线程对象与线程的执行环境相关联,包括线程的堆栈、程序计数器等。
  1. 调用线程的run()方法
  • JVM会调用线程的run()方法。在这个例子中,Dog类实现了Runnable接口,并重写了run()方法。
  • thread.start()被调用时,JVM会调用Dog类的run()方法。
  1. 线程执行
  • run()方法中的代码开始执行。
  • run()方法中,count从0开始,每次循环增加1。
  • 每次循环打印信息,并调用Thread.sleep(1000)休眠1秒。
  • count达到10时,循环结束。
  1. 线程结束
  • run()方法执行完毕后,线程结束。
  • JVM清理与该线程相关的资源,如堆栈等。

image-20250128162522626


(3)多线程的使用

  请编写一个程序,创建2个线程,一个线程每隔1秒输出 “hi”,输出10次,一个线程每隔1秒输出“nihao”,输出5次退出。

java">package com.xbf.thread;public class ThreadNums {public static void main(String[] args) {T1 t1 = new T1();T1 t2 = new T1();Thread thread1 = new Thread(t1);Thread thread2 = new Thread(t2);thread1.start();thread2.start();}
}
class T1 implements Runnable {@Overridepublic void run() {int count = 0;while(true) {System.out.println("hi..." + (++count));try {Thread.sleep(1000);} catch (InterruptedException e) {throw new RuntimeException(e);}if(count >= 10) {break;}}}
}

(4)线程的理解

image-20250128164231113

  我们可以通过创建不同的线程,去解决不同的问题,提高执行的效率。


Thread___Runnable__238">(5)继承 Thread 和 实现 Runnable 接口的比较
  1. java 的设计来看,通过继承 Thread 或者实现 Runnable 接口来创建线程本质上没有区别,从 jdk 文档来看,Thread 类本身就实现了 Runnabel 接口。
  2. 实现 Runnable 接口方式更加适合多个线程共享一个资源的情况,并且避免了单继承的限制。

4 线程的终止

  1. 当线程完成任务后,会自动退出。
  2. 可以通过使用变量来控制 run 方法退出,来停止线程,即 通知方式

案例:启动一个线程,要求在 main 线程中去停止线程 thread01

  在 Thread01 中声明一个 boolean 值 loop,用于控制 run 方法的执行与退出。然后我们在 main 线程控制 loop 的值即可实现。

java">public class Example1 {public static void main(String[] args) {Thread01 thread01 = new Thread01();Thread thread = new Thread(thread01);thread.start();try {Thread.sleep(20000);} catch (InterruptedException e) {throw new RuntimeException(e);}thread01.setLoop(false);}
}class Thread01 implements Runnable{private boolean loop = true;public void setLoop(boolean loop) {this.loop = loop;}@Overridepublic void run() {int count = 0;while(loop) {System.out.println(Thread.currentThread().getName() +"-" + (++count));try {Thread.sleep(500);} catch (InterruptedException e) {throw new RuntimeException(e);}}}
}

5 线程的常用方法

  1. setName :设置线程名称,使之与参数 name 相同

  2. getName :返回该线程的名称

  3. start :使该线程开始执行;java 虚拟机底层调用该线程的 start0() 方法

    start 底层会创建新的线程,调用 runrun 就是一个简单的方法调用,不会启动新线程。

  4. run :调用线程对象 run 方法。

  5. setPriority :更改线程的优先级。

    在Java中,Thread类定义了以下线程优先级的常量等级:

    1. Thread.MIN_PRIORITY
      • 值为 1
      • 表示最低优先级。
    2. Thread.NORM_PRIORITY
      • 值为 5
      • 表示默认优先级。
    3. Thread.MAX_PRIORITY
      • 值为 10
      • 表示最高优先级。
  6. getPriority :获取线程的优先级。

  7. sleep :在指定的毫秒内让当前正在执行的线程休眠(暂停执行)。

  8. interrupt :中断线程,但是没有真正的结束线程,所以一般用于中断正在休眠的线程。

  9. yield :线程的礼让。让出 cpu ,让其他线程执行,但礼让的时间不确定,所以也不一定礼让成功。

  10. join :线程的插队。插队的线程一旦插队成功,则肯定先执行完,插入的线程所有的任务。(哪个线程要插队,就哪个线程调用 join 方法)

注意: 线程调度依赖于操作系统,优先级和礼让不一定总是有效。

java">public class ThreadDemo {public static void main(String[] args) {// 创建一个线程并设置名称Thread thread1 = new Thread(new MyRunnable(), "Thread-1");thread1.setPriority(Thread.NORM_PRIORITY); // 设置优先级为默认值// 创建另一个线程并设置名称Thread thread2 = new Thread(new MyRunnable(), "Thread-2");thread2.setPriority(Thread.MAX_PRIORITY); // 设置优先级为最高// 启动线程1thread1.start();// 主线程休眠500ms,确保thread1先运行try {Thread.sleep(500);} catch (InterruptedException e) {e.printStackTrace();}// 启动线程2thread2.start();// 使用join方法,让主线程等待thread1和thread2执行完毕try {thread1.join();thread2.join();} catch (InterruptedException e) {e.printStackTrace();}System.out.println("Main thread finished.");}
}class MyRunnable implements Runnable {@Overridepublic void run() {// 获取当前线程的名称System.out.println(Thread.currentThread().getName() + " is running with priority: " + Thread.currentThread().getPriority());// 模拟线程执行任务for (int i = 1; i <= 5; i++) {System.out.println(Thread.currentThread().getName() + " - Count: " + i);// 使用yield方法,让出CPUif (i == 3) {System.out.println(Thread.currentThread().getName() + " is yielding...");Thread.yield();}// 模拟线程休眠if (i == 4) {try {System.out.println(Thread.currentThread().getName() + " is going to sleep...");Thread.sleep(1000); // 休眠1秒} catch (InterruptedException e) {System.out.println(Thread.currentThread().getName() + " was interrupted while sleeping.");}}}System.out.println(Thread.currentThread().getName() + " has finished.");}
}

6 用户线程和守护线程

Thread_406">1. 用户线程(User Thread
  • 定义:用户线程是程序中的核心线程,用于执行主要的业务逻辑。默认情况下,Java 线程都是用户线程。
  • 特点
    • 阻止 JVM 退出:只要存在任何一个用户线程未结束,JVM 就不会终止。
    • 生命周期独立:用户线程会一直运行直到任务完成,或主动调用 Thread.stop()(已废弃)等终止方法。
    • 典型用途:处理用户请求、计算任务等需要确保完整性的操作。

Thread_416">2. 守护线程(Daemon Thread
  • 定义守护线程是为其他线程提供支持的辅助线程,其生命周期依赖于用户线程。
  • 特点
    • 不阻止 JVM 退出:当所有用户线程结束时,JVM 会立即终止所有守护线程(无论是否完成任务)。
    • 设置方法:通过 thread.setDaemon(true) 设置,需在 start() 前调用,否则抛出 IllegalThreadStateException
    • 典型用途:垃圾回收(GC)、心跳检测、后台日志处理等非关键任务。

3 主要区别对比
对比项用户线程守护线程
JVM 退出条件所有用户线程结束后,JVM 才会退出。不阻止 JVM 退出,随用户线程结束而终止。
默认类型新线程默认是用户线程。需显式调用 setDaemon(true) 设置。
资源处理风险确保任务完成,适合关键操作(如写文件)。可能被强制终止,需避免依赖 finally 块或未完成操作。
优先级与调度优先级由开发者设置,无系统差异。与用户线程调度机制相同,但可能被提前终止。
异常处理影响未捕获异常导致线程终止,不影响其他线程。异常终止同样不影响其他线程,但可能因 JVM 退出而被忽略。
java">package com.xbf.thread.daemon;public class DaemonDemo {public static void main(String[] args) throws InterruptedException {Thread thread = new Thread(new Thread01());// thread.setDaemon(true) 设置,// 需在 start()前调用,// 否则抛出 IllegalThreadStateExceptionthread.setDaemon(true);thread.start();//休眠5秒Thread.sleep(5000);System.out.println("main线程退出");}
}
class Thread01 implements Runnable {@Overridepublic void run() {while(true) {//休眠 1 秒try {Thread.sleep(1000);} catch (InterruptedException e) {throw new RuntimeException(e);}System.out.println("你好...");}}
}

  可以看到,我们设置的守护线程是无限循环,可是它在main线程退出时,终止了。因为在这个程序中,只有一个用户线程 ——main 线程。

image-20250129162204595

注意守护线程中的操作(如 I/O 写入)需谨慎,避免因 JVM 突然终止导致数据不一致。

比如,如果守护线程正在写文件,而JVM因为用户线程结束而退出,那么写操作可能没有完成,导致数据丢失。所以守护线程需要小心处理资源操作,确保在JVM退出时不会留下不完整的状态。


7 线程的生命周期(状态)

在Java中,线程的状态由Thread.State枚举类表示,共有以下6种状态:

  1. NEW(新建)
    • 线程刚被创建,尚未启动。
  2. RUNNABLE(可运行)
    • 线程已启动,正在运行或等待CPU资源。
  3. BLOCKED(阻塞)
    • 线程因等待监视器锁(如synchronized)而被阻塞。
  4. WAITING(等待)
    • 线程无限期等待其他线程的特定操作(如Object.wait()Thread.join())。
  5. TIMED_WAITING(计时等待)
    • 线程在指定时间内等待(如Thread.sleep()Object.wait(long timeout))。
  6. TERMINATED(终止)
    • 线程已完成执行。

这些状态反映了线程在其生命周期中的不同阶段。

在下图中,RUNNABLE(可运行) 状态又可分为:就绪态和运行态。

image-20250129171134038


8 线程的同步(synchornized)

我们通过一个售票的例子来讲解Java中synchronized关键字的作用。假设有三个售票窗口,每个窗口每次卖1张票,总票数为10张。使用三个线程模拟三个窗口的售票过程。

问题描述:

如果不使用synchronized关键字,多个线程可能同时访问共享资源(票数),导致数据不一致(如超卖)。

代码示例:

java">public class TicketSale implements Runnable {private int tickets = 10; // 总票数@Overridepublic void run() {while (tickets > 0) {sellTicket(); // 售票}}// 售票方法private void sellTicket() {if (tickets > 0) {try {Thread.sleep(100); // 模拟售票时间} catch (InterruptedException e) {e.printStackTrace();}System.out.println(Thread.currentThread().getName() + " 卖出了第 " + tickets-- + " 张票");}}public static void main(String[] args) {TicketSale ticketSale = new TicketSale();// 创建三个线程模拟三个窗口Thread window1 = new Thread(ticketSale, "窗口1");Thread window2 = new Thread(ticketSale, "窗口2");Thread window3 = new Thread(ticketSale, "窗口3");// 启动线程window1.start();window2.start();window3.start();}
}

结果如下图:

image-20250129182750665

问题:

运行上述代码时,可能会出现以下问题:

  1. 超卖:多个线程同时检查tickets > 0 (即,可以同时进入 sellTicket() 方法),导致卖出的票数超过实际票数。
  2. 重复卖票:多个线程同时执行tickets-- ,导致同一张票被多次卖出。

使用synchronized解决问题:

synchronized关键字可以确保同一时刻只有一个线程访问共享资源(售票方法),从而避免数据不一致。

修改后的代码:

sellTicket()方法用synchronized修饰:

java">public class TicketSale implements Runnable {private int tickets = 10; // 总票数@Overridepublic void run() {while (tickets > 0) {sellTicket(); // 售票}}// 使用 synchronized 修饰售票方法private synchronized void sellTicket() {if (tickets > 0) {try {Thread.sleep(100); // 模拟售票时间} catch (InterruptedException e) {e.printStackTrace();}System.out.println(Thread.currentThread().getName() + " 卖出了第 " + tickets-- + " 张票");}}public static void main(String[] args) {TicketSale ticketSale = new TicketSale();// 创建三个线程模拟三个窗口Thread window1 = new Thread(ticketSale, "窗口1");Thread window2 = new Thread(ticketSale, "窗口2");Thread window3 = new Thread(ticketSale, "窗口3");// 启动线程window1.start();window2.start();window3.start();}
}

结果如下图:

image-20250129182859436

解释:

  1. synchronized的作用
    • 当一个线程进入sellTicket()方法时,其他线程必须等待,直到当前线程执行完毕。
    • 这确保了同一时刻只有一个线程可以修改tickets变量,避免了数据竞争。
  2. 线程安全
    • 使用synchronized后,不会出现超卖或重复卖票的问题。

总结:

synchronized关键字用于实现线程同步,确保多个线程在访问共享资源时的数据一致性。在售票例子中,它保证了每个窗口按顺序售票,避免了数据混乱。


9 互斥锁

先说如下的结论:

  1. java 语言中,引入了对象互斥锁的概念,来保证数据操作的完整性。
  2. 每个对象都对应一个可称为互斥锁的标记,这个标记用来保证在任一时刻,只能有一个线程访问该对象。
  3. 关键字 synchronized 来与对象的互斥锁联系。当某个对象用 synchronized 修饰时,表明该对象在任一时刻只能由一个线程访问。
  4. 也可以在代码块上写 synchronized,同步代码块, 显式指定锁对象。
  5. 同步的局限性:导致程序的执行效率要降低。
  6. 同步方法(非静态的)的锁可以是 this,也可以是其他对象(要求是同一个对象)
  7. 同步方法(静态的)的锁为当前类本身。

(1)对象互斥锁的作用

Java通过对象互斥锁保证共享数据的完整性。每个对象都隐含一个锁(称为监视器锁/Monitor),确保同一时刻只有一个线程能访问被锁保护的代码或资源。
核心目标:防止多线程并发修改导致数据不一致。


(2)synchronized 的 2 种用法
  • 同步方法

    • 非静态方法:锁是当前对象实例(this)。

      java">public synchronized void method() { ... }
      
    • 静态方法:锁是当前类的Class对象(如TicketSale.class)。

      java">public static synchronized void method() { ... }
      
  • 同步代码块
    显式指定锁对象(可以是任意对象,但需保证多线程共享同一锁)。

    java">//lockObject表示任意对象,
    //使用时我们需要输入具体的对象,比如 this 
    synchronized (lockObject) { // 需要同步的代码 
    }
    //锁是当前对象实例 this
    synchronized (this) { // 需要同步的代码 
    }
    //锁为当前类的 class 对象
    synchronized (TicketSale.class) { // 需要同步的代码 
    }
    

(3)锁的规则
  • 静态同步方法
    锁是类的Class对象(全局唯一),与非静态方法的锁无关。

    java">// 静态方法的锁是 TicketSale.class
    public static synchronized void staticMethod() { ... }
    
  • 非静态同步方法
    锁是this对象,但也可以手动指定其他对象(需保证所有线程使用同一对象锁)。

    java">//锁对象需声明为 final,防止被重新赋值导致锁失效
    private final Object lock = new Object();
    public void method() {synchronized (lock) { ... }  // 锁为自定义对象
    }
    

  分析可知:如果多个线程使用不同的对象实例(即每个线程的 Runnable 是独立创建的),那么每个实例中的 lock 对象是不同的,此时 synchronized(lock) 无法实现线程同步,因为它们依赖的是不同的锁对象。

假设以下代码中,每个线程都使用独立的 TicketSale 实例:

java">public class TicketSale implements Runnable {private final Object lock = new Object(); // 每个实例的 lock 是独立的private static int tickets = 10; // 假设票池是静态共享的@Overridepublic void run() {synchronized (lock) { // 锁是实例内的独立对象if (tickets > 0) {System.out.println(Thread.currentThread().getName() + " 卖出第 " + tickets-- + " 张票");}}}public static void main(String[] args) {// 每个线程使用独立的 TicketSale 实例new Thread(new TicketSale(), "窗口1").start();new Thread(new TicketSale(), "窗口2").start();new Thread(new TicketSale(), "窗口3").start();}
}

问题分析

  • 每个 TicketSale 实例的 lock 对象是独立的(不同实例的 lock 不同)。
  • synchronized(lock) 本质是让线程竞争各自的 lock 对象,无法实现跨实例的互斥
  • 最终结果:多个线程可能同时修改 tickets,导致超卖或重复卖票。

解决方案

若需在多实例场景下实现线程同步,需确保所有线程共享同一个锁对象。以下是两种常见方法:

方法 1:使用静态锁对象

将锁对象声明为 static,确保所有实例共享同一把锁。

java">public class TicketSale implements Runnable {private static int tickets = 10;// 静态锁对象(全局唯一)private static final Object LOCK = new Object(); @Overridepublic void run() {synchronized (LOCK) { // 所有线程共享 LOCKif (tickets > 0) {System.out.println(Thread.currentThread().getName() + " 卖出第 " + tickets-- + " 张票");}}}public static void main(String[] args) {// 即使每个线程使用独立实例,锁 LOCK 仍是全局共享的new Thread(new TicketSale(), "窗口1").start();new Thread(new TicketSale(), "窗口2").start();new Thread(new TicketSale(), "窗口3").start();}
}

关键点

  • 静态变量 LOCK 属于类级别,所有实例共享。
  • 无论创建多少实例,所有线程竞争的是同一把锁。

方法 2:使用类级锁

直接使用类的 Class 对象作为锁(等效于静态锁)。

java">public class TicketSale implements Runnable {private static int tickets = 10;@Overridepublic void run() {synchronized (TicketSale.class) { // 类级锁if (tickets > 0) {System.out.println(Thread.currentThread().getName() + " 卖出第 " + tickets-- + " 张票");}}}public static void main(String[] args) {new Thread(new TicketSale(), "窗口1").start();new Thread(new TicketSale(), "窗口2").start();new Thread(new TicketSale(), "窗口3").start();}
}

关键点

  • TicketSale.class 是 JVM 中唯一的 Class 对象,所有实例共享。
  • 与静态锁对象 LOCK 等效,但更简洁。

对比总结

方案锁对象适用场景优点缺点
静态锁对象static final LOCK需要显式控制锁对象灵活性高,可自定义锁对象需额外声明静态变量
类级锁ClassName.class快速实现全局同步代码简洁,无需声明额外变量锁粒度较粗,可能影响性能

最终结论

  • 若多线程操作的是不同实例,但需要保护共享资源(如静态变量),必须使用全局唯一的锁(静态锁或类级锁)。
  • 若多线程操作的是同一实例,使用实例级锁(synchronized(this) 或实例内的 lock)即可。

(4)同步的局限性
  • 性能降低
    锁的获取和释放会引入线程阻塞和上下文切换,降低程序效率。
  • 死锁风险
    若多个线程互相持有对方需要的锁且不释放,会导致死锁

(5) 实际应用示例(售票问题)
  • 问题:多个窗口(线程)卖票时,需保证票数操作的原子性。

  • 解决:用synchronized修饰售票方法或代码块,确保每次只有一个线程执行售票逻辑。

    java">private synchronized void sellTicket() { if (tickets > 0) {System.out.println("卖出第 " + tickets-- + " 张票");}
    }
    

    结果:避免超卖(如第0张票)或重复卖票(同一票号被多个线程卖出)。


(6) 最佳实践
  • 最小化同步范围:尽量用同步代码块代替同步方法,减少锁的持有时间。
  • 避免锁嵌套:防止死锁(如线程A持有锁1请求锁2,线程B持有锁2请求锁1)。
  • 优先使用线程安全类:如ConcurrentHashMap代替手动同步的HashMap

10 死锁与释放锁

10.1 死锁示例

死锁是指两个或多个线程互相持有对方需要的锁,且无法释放自己的锁,导致所有线程永久阻塞。以下是一个经典死锁案例:

java">public class DeadLockDemo {private static final Object lockA = new Object();private static final Object lockB = new Object();public static void main(String[] args) {new Thread(() -> {synchronized (lockA) {System.out.println("线程1 持有 lockA");try {Thread.sleep(100); // 模拟操作耗时} catch (InterruptedException e) {e.printStackTrace();}synchronized (lockB) { // 试图获取 lockB(但已被线程2持有)System.out.println("线程1 获取 lockB");}}}, "线程1").start();new Thread(() -> {synchronized (lockB) {System.out.println("线程2 持有 lockB");synchronized (lockA) { // 试图获取 lockA(但已被线程1持有)System.out.println("线程2 获取 lockA");}}}, "线程2").start();}
}

10.2 死锁产生条件:
  1. 互斥:资源(锁)只能被一个线程持有。
  2. 请求与保持:线程持有至少一个锁,并请求其他线程持有的锁。
  3. 不可剥夺:线程不会主动释放已持有的锁。
  4. 循环等待:线程之间形成等待环路。

10.3 释放锁的操作

以下操作会释放锁,允许其他线程获取锁:

1. 同步方法/代码块执行结束
java">public synchronized void method() {// 同步代码...
} // 方法执行完毕,自动释放锁
2. 遇到 breakreturn
java">synchronized (lock) {if (condition) {return; // 提前返回,释放锁}// 其他代码...
}
3. 未处理的异常或错误
java">synchronized (lock) {throw new RuntimeException("异常发生"); // 抛出异常,释放锁
}
4. 调用 wait() 方法
java">synchronized (lock) {lock.wait(); // 释放锁,线程进入等待状态
}
  • wait() 会释放当前锁,直到其他线程调用 notify()/notifyAll()

10.4 不释放锁的操作

以下操作不会释放锁,线程仍持有锁:

Threadsleep__Threadyield_956">1. Thread.sleep()Thread.yield()
java">synchronized (lock) {Thread.sleep(1000); // 线程休眠,但锁仍被持有
}
  • 线程暂停执行,但不会释放锁。
2. 调用 suspend()(已弃用)
java">synchronized (lock) {Thread.currentThread().suspend(); // 线程挂起,但锁仍被持有
}
  • 注意suspend()resume() 已废弃,可能导致死锁和不稳定。

10.5 关键结论
场景是否释放锁原因
同步代码执行完毕正常流程结束
return/break提前退出代码块
未处理的异常异常终止代码块执行
wait()主动释放锁并进入等待队列
sleep()/yield()线程暂停,但锁仍被持有
suspend()线程挂起,但锁未被释放(已弃用,避免使用)

10.6 如何避免死锁
  1. 避免嵌套锁:尽量减少同步代码块中的锁嵌套。
  2. 按固定顺序获取锁:所有线程按相同顺序请求锁(如先 lockAlockB)。
  3. 使用超时机制:通过 tryLock(timeout) 避免无限等待。
  4. 避免长时间持有锁:缩小同步代码块的范围。


http://www.ppmy.cn/devtools/155401.html

相关文章

Hypium+python鸿蒙原生自动化安装配置

Hypiumpython自动化搭建 文章目录 Python安装pip源配置HDC安装Hypium安装DevEco Testing Hypium插件安装及使用方法​​​​​插件安装工程创建区域 Python安装 推荐从官网获取3.10版本&#xff0c;其他版本可能出现兼容性问题 Python下载地址 下载64/32bitwindows安装文件&am…

C#,入门教程(13)——字符(char)及字符串(string)的基础知识

上一篇&#xff1a; C#&#xff0c;入门教程(12)——数组及数组使用的基础知识https://blog.csdn.net/beijinghorn/article/details/123918227 字符串的使用与操作是必需掌握得滚瓜烂熟的编程技能之一&#xff01;&#xff01;&#xff01;&#xff01;&#xff01; C#语言实…

Ubuntu下的Doxygen+VScode实现C/C++接口文档自动生成

Ubuntu下的DoxygenVScode实现C/C接口文档自动生成 Chapter1 Ubuntu下的DoxygenVScode实现C/C接口文档自动生成1、 Doxygen简介1. 安装Doxygen1&#xff09;方法一&#xff1a;2&#xff09;方法二&#xff1a;2. doxygen注释自动生成插件3. doxygen注释基本语法4. doxygen的生成…

AI学习指南Ollama篇-使用Ollama构建自己的私有化知识库

一、引言 (一)背景介绍 随着企业对数据隐私和效率的重视,私有化知识库的需求日益增长。私有化知识库不仅可以保护企业数据的安全性,还能提供高效的知识管理和问答系统,提升企业内部的工作效率和创新能力。 (二)Ollama和AnythingLLM的结合 Ollama和AnythingLLM的结合…

玉米苗和杂草识别分割数据集labelme格式1997张3类别

数据集格式&#xff1a;labelme格式(不包含mask文件&#xff0c;仅仅包含jpg图片和对应的json文件) 图片数量(jpg文件个数)&#xff1a;1997 标注数量(json文件个数)&#xff1a;1997 标注类别数&#xff1a;3 标注类别名称:["corn","weed","Bean…

SpringBoot Web开发(SpringMVC)

SpringBoot Web开发&#xff08;SpringMVC) MVC 核心组件和调用流程 Spring MVC与许多其他Web框架一样&#xff0c;是围绕前端控制器模式设计的&#xff0c;其中中央 Servlet DispatcherServlet 做整体请求处理调度&#xff01; . 除了DispatcherServletSpringMVC还会提供其他…

Easy系列PLC尺寸测量功能块ST代码(激光微距仪应用)

激光微距仪可以测量短距离内的产品尺寸,产品规格书的测量 精度可以到0.001mm。具体需要看不同的型号。 1、激光微距仪 2、尺寸测量应用 下面我们以测量高度为例子,设计一个高度测量功能块,同时给出测量数据和合格不合格指标。 3、高度测量功能块 4、复位完成信号 5、功能…

在RHEL 8.10上安装开源工业物联网解决方案Thingsboard 3.9

在RHEL/CentOS/Rocky/AlmaLinux/Oracle Linux 8单节点上安装 备注&#xff1a; 适用于单节点 是否支持欧拉&#xff1f;&#xff1f;&#xff1f; 前提条件 本指南描述了如何在RHEL/CentOS 7/8上安装ThingsBoard。硬件要求取决于所选的数据库和连接到系统的设备数量。要在单…