Java 中线程的使用
文章目录
- Java 线程
- 1 进程
- 2 线程
- 3 线程的基本使用
- (1)继承 Thread 类,重写 run 方法
- (2)实现 Runnable 接口,重写 run 方法
- (3)多线程的使用
- (4)线程的理解
- (5)继承 Thread 和 实现 Runnable 接口的比较
- 4 线程的终止
- 5 线程的常用方法
- 6 用户线程和守护线程
- 1. 用户线程(User Thread)
- 2. 守护线程(Daemon Thread)
- 3 主要区别对比
- 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 线程
- 线程由进程创建的,是进程的一个实体。
- 一个进程可以有多个线程。
- 单线程: 同一个时刻,只允许执行一个线程。
- 多线程: 同一个时刻,可以执行多个线程,比如:一个qq进程里面,可以同时打开多个聊天窗口,一个迅雷进程,可以同时下载多个文件。
- 并发: 同一个时刻,多个任务交替执行,造成一种“貌似同时”的错觉,简单的说,单核cpu实现的多任务就是并发。
- 并行: 同一个时刻,多个任务同时执行,多核cpu可以实现并行。
3 线程的基本使用
在 java 中,线程的使用有 2 种方法:
- 继承 Thread 类,重写 run 方法。
- 实现 Runnable 接口,重写 run 方法。
(1)继承 Thread 类,重写 run 方法
Thread 类的关系如下图:
Runnable 接口的源码为:
创建线程的案例:
(1) 开启一个线程,该线程每隔 1 秒,在控制台输出 “Cat…Cat…”。
(2) 当输出 10 次时,结束该线程。
(3) 使用 JConsole 监控线程执行情况,并画出示意图。
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 {
@Override
public 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() 方法担任了创建线程的工作。
- 创建线程对象:首先,JVM会创建一个线程对象。这个对象与线程的执行环境相关联,包括线程的堆栈、程序计数器等。
- 调用线程的
run()
方法:线程对象创建完成后,JVM会调用线程的run()
方法。run()
方法是线程执行的具体逻辑。在这个例子中,Cat
类继承了Thread
类,并重写了run()
方法。因此,当cat.start()
被调用时,Cat
类的run()
方法将被执行。 - 线程执行:
run()
方法中的代码将开始执行。在这个例子中,run()
方法中的代码会循环打印信息,并在每次循环后休眠1秒。当计数器count
达到10时,循环结束,线程执行完毕。 - 线程结束:当
run()
方法执行完毕后,线程将结束。JVM会清理与该线程相关的资源,如堆栈等。
执行结果如下:可以看到,其它线程的执行,并不会影响主线程(main方法)的执行。
注意: start() 方法调用 start0() 方法后,该线程并不一定会立马执行,只是将线程变成了可运行状态。具体什么时候执行,取决于 CPU ,由 CPU 统一调度。
(2)实现 Runnable 接口,重写 run 方法
java 是单继承的,在某些情况下,一个类可能已经继承了某个父类,这时再用继承 Thread 类的方法来创建线程显然就不行了。所以,引入了实现 Runnable 接口来创建线程。
案例实现:
(1) 开启一个线程,该线程每隔 1 秒,在控制台输出 “Dog…Dog…”。
(2) 当输出 10 次时,结束该线程。
(3) 使用 实现 Runnable 接口的方式实现。
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 {
@Override
public 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
类。以下是详细的执行流程:
- 创建线程对象
- 当
thread.start()
被调用时,JVM会创建一个线程对象。 - 这个线程对象与线程的执行环境相关联,包括线程的堆栈、程序计数器等。
- 调用线程的
run()
方法
- JVM会调用线程的
run()
方法。在这个例子中,Dog
类实现了Runnable
接口,并重写了run()
方法。 - 当
thread.start()
被调用时,JVM会调用Dog
类的run()
方法。
- 线程执行
run()
方法中的代码开始执行。- 在
run()
方法中,count
从0开始,每次循环增加1。 - 每次循环打印信息,并调用
Thread.sleep(1000)
休眠1秒。 - 当
count
达到10时,循环结束。
- 线程结束
run()
方法执行完毕后,线程结束。- JVM清理与该线程相关的资源,如堆栈等。
(3)多线程的使用
请编写一个程序,创建2个线程,一个线程每隔1秒输出 “hi”,输出10次,一个线程每隔1秒输出“nihao”,输出5次退出。
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 {
@Override
public 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)线程的理解
我们可以通过创建不同的线程,去解决不同的问题,提高执行的效率。
(5)继承 Thread 和 实现 Runnable 接口的比较
- 从 java 的设计来看,通过继承 Thread 或者实现 Runnable 接口来创建线程本质上没有区别,从 jdk 文档来看,Thread 类本身就实现了 Runnabel 接口。
- 实现 Runnable 接口方式更加适合多个线程共享一个资源的情况,并且避免了单继承的限制。
4 线程的终止
- 当线程完成任务后,会自动退出。
- 可以通过使用变量来控制 run 方法退出,来停止线程,即 通知方式
案例:启动一个线程,要求在 main 线程中去停止线程 thread01
在 Thread01 中声明一个 boolean 值 loop,用于控制 run 方法的执行与退出。然后我们在 main 线程控制 loop 的值即可实现。
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;
}
@Override
public 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 线程的常用方法
-
setName
:设置线程名称,使之与参数 name 相同 -
getName
:返回该线程的名称 -
start
:使该线程开始执行;java 虚拟机底层调用该线程的start0()
方法start
底层会创建新的线程,调用run
,run
就是一个简单的方法调用,不会启动新线程。 -
run
:调用线程对象 run 方法。 -
setPriority
:更改线程的优先级。在Java中,
Thread
类定义了以下线程优先级的常量等级:Thread.MIN_PRIORITY
- 值为 1
- 表示最低优先级。
Thread.NORM_PRIORITY
- 值为 5
- 表示默认优先级。
Thread.MAX_PRIORITY
- 值为 10
- 表示最高优先级。
-
getPriority
:获取线程的优先级。 -
sleep
:在指定的毫秒内让当前正在执行的线程休眠(暂停执行)。 -
interrupt
:中断线程,但是没有真正的结束线程,所以一般用于中断正在休眠的线程。 -
yield
:线程的礼让。让出 cpu ,让其他线程执行,但礼让的时间不确定,所以也不一定礼让成功。 -
join
:线程的插队。插队的线程一旦插队成功,则肯定先执行完,插入的线程所有的任务。(哪个线程要插队,就哪个线程调用 join 方法)
注意: 线程调度依赖于操作系统,优先级和礼让不一定总是有效。
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); // 设置优先级为最高
// 启动线程1
thread1.start();
// 主线程休眠500ms,确保thread1先运行
try {
Thread.sleep(500);
} catch (InterruptedException e) {
e.printStackTrace();
}
// 启动线程2
thread2.start();
// 使用join方法,让主线程等待thread1和thread2执行完毕
try {
thread1.join();
thread2.join();
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("Main thread finished.");
}
}
class MyRunnable implements Runnable {
@Override
public 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方法,让出CPU
if (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 用户线程和守护线程
1. 用户线程(User Thread)
- 定义:用户线程是程序中的核心线程,用于执行主要的业务逻辑。默认情况下,Java 线程都是用户线程。
- 特点:
- 阻止 JVM 退出:只要存在任何一个用户线程未结束,JVM 就不会终止。
- 生命周期独立:用户线程会一直运行直到任务完成,或主动调用
Thread.stop()
(已废弃)等终止方法。 - 典型用途:处理用户请求、计算任务等需要确保完整性的操作。
2. 守护线程(Daemon Thread)
- 定义:守护线程是为其他线程提供支持的辅助线程,其生命周期依赖于用户线程。
- 特点:
- 不阻止 JVM 退出:当所有用户线程结束时,JVM 会立即终止所有守护线程(无论是否完成任务)。
- 设置方法:通过
thread.setDaemon(true)
设置,需在start()
前调用,否则抛出IllegalThreadStateException
。 - 典型用途:垃圾回收(GC)、心跳检测、后台日志处理等非关键任务。
3 主要区别对比
对比项 | 用户线程 | 守护线程 |
---|---|---|
JVM 退出条件 | 所有用户线程结束后,JVM 才会退出。 | 不阻止 JVM 退出,随用户线程结束而终止。 |
默认类型 | 新线程默认是用户线程。 | 需显式调用 setDaemon(true) 设置。 |
资源处理风险 | 确保任务完成,适合关键操作(如写文件)。 | 可能被强制终止,需避免依赖 finally 块或未完成操作。 |
优先级与调度 | 优先级由开发者设置,无系统差异。 | 与用户线程调度机制相同,但可能被提前终止。 |
异常处理影响 | 未捕获异常导致线程终止,不影响其他线程。 | 异常终止同样不影响其他线程,但可能因 JVM 退出而被忽略。 |
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()前调用,
// 否则抛出 IllegalThreadStateException
thread.setDaemon(true);
thread.start();
//休眠5秒
Thread.sleep(5000);
System.out.println("main线程退出");
}
}
class Thread01 implements Runnable {
@Override
public void run() {
while(true) {
//休眠 1 秒
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
System.out.println("你好...");
}
}
}
可以看到,我们设置的守护线程是无限循环,可是它在main线程退出时,终止了。因为在这个程序中,只有一个用户线程 ——main 线程。
注意:守护线程中的操作(如 I/O 写入)需谨慎,避免因 JVM 突然终止导致数据不一致。
比如,如果守护线程正在写文件,而JVM因为用户线程结束而退出,那么写操作可能没有完成,导致数据丢失。所以守护线程需要小心处理资源操作,确保在JVM退出时不会留下不完整的状态。
7 线程的生命周期(状态)
在Java中,线程的状态由Thread.State
枚举类表示,共有以下6种状态:
- NEW(新建)
- 线程刚被创建,尚未启动。
- RUNNABLE(可运行)
- 线程已启动,正在运行或等待CPU资源。
- BLOCKED(阻塞)
- 线程因等待监视器锁(如
synchronized
)而被阻塞。
- 线程因等待监视器锁(如
- WAITING(等待)
- 线程无限期等待其他线程的特定操作(如
Object.wait()
或Thread.join()
)。
- 线程无限期等待其他线程的特定操作(如
- TIMED_WAITING(计时等待)
- 线程在指定时间内等待(如
Thread.sleep()
或Object.wait(long timeout)
)。
- 线程在指定时间内等待(如
- TERMINATED(终止)
- 线程已完成执行。
这些状态反映了线程在其生命周期中的不同阶段。
在下图中,RUNNABLE(可运行) 状态又可分为:就绪态和运行态。
8 线程的同步(synchornized)
我们通过一个售票的例子来讲解Java中synchronized
关键字的作用。假设有三个售票窗口,每个窗口每次卖1张票,总票数为10张。使用三个线程模拟三个窗口的售票过程。
问题描述:
如果不使用synchronized
关键字,多个线程可能同时访问共享资源(票数),导致数据不一致(如超卖)。
代码示例:
public class TicketSale implements Runnable {
private int tickets = 10; // 总票数
@Override
public 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();
}
}
结果如下图:
问题:
运行上述代码时,可能会出现以下问题:
- 超卖:多个线程同时检查
tickets > 0
(即,可以同时进入sellTicket()
方法),导致卖出的票数超过实际票数。 - 重复卖票:多个线程同时执行
tickets--
,导致同一张票被多次卖出。
使用synchronized
解决问题:
synchronized
关键字可以确保同一时刻只有一个线程访问共享资源(售票方法),从而避免数据不一致。
修改后的代码:
将sellTicket()
方法用synchronized
修饰:
public class TicketSale implements Runnable {
private int tickets = 10; // 总票数
@Override
public 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();
}
}
结果如下图:
解释:
synchronized
的作用:- 当一个线程进入
sellTicket()
方法时,其他线程必须等待,直到当前线程执行完毕。 - 这确保了同一时刻只有一个线程可以修改
tickets
变量,避免了数据竞争。
- 当一个线程进入
- 线程安全:
- 使用
synchronized
后,不会出现超卖或重复卖票的问题。
- 使用
总结:
synchronized
关键字用于实现线程同步,确保多个线程在访问共享资源时的数据一致性。在售票例子中,它保证了每个窗口按顺序售票,避免了数据混乱。
9 互斥锁
先说如下的结论:
- java 语言中,引入了对象互斥锁的概念,来保证数据操作的完整性。
- 每个对象都对应一个可称为互斥锁的标记,这个标记用来保证在任一时刻,只能有一个线程访问该对象。
- 关键字 synchronized 来与对象的互斥锁联系。当某个对象用 synchronized 修饰时,表明该对象在任一时刻只能由一个线程访问。
- 也可以在代码块上写 synchronized,同步代码块, 显式指定锁对象。
- 同步的局限性:导致程序的执行效率要降低。
- 同步方法(非静态的)的锁可以是 this,也可以是其他对象(要求是同一个对象)
- 同步方法(静态的)的锁为当前类本身。
(1)对象互斥锁的作用
Java通过对象互斥锁保证共享数据的完整性。每个对象都隐含一个锁(称为监视器锁/Monitor),确保同一时刻只有一个线程能访问被锁保护的代码或资源。
核心目标:防止多线程并发修改导致数据不一致。
(2)synchronized 的 2 种用法
-
同步方法:
-
非静态方法:锁是当前对象实例(
this
)。public synchronized void method() { ... }
-
静态方法:锁是当前类的
Class
对象(如TicketSale.class
)。public static synchronized void method() { ... }
-
-
同步代码块:
显式指定锁对象(可以是任意对象,但需保证多线程共享同一锁)。//lockObject表示任意对象, //使用时我们需要输入具体的对象,比如 this synchronized (lockObject) { // 需要同步的代码 } //锁是当前对象实例 this synchronized (this) { // 需要同步的代码 } //锁为当前类的 class 对象 synchronized (TicketSale.class) { // 需要同步的代码 }
(3)锁的规则
-
静态同步方法:
锁是类的Class
对象(全局唯一),与非静态方法的锁无关。// 静态方法的锁是 TicketSale.class public static synchronized void staticMethod() { ... }
-
非静态同步方法:
锁是this
对象,但也可以手动指定其他对象(需保证所有线程使用同一对象锁)。//锁对象需声明为 final,防止被重新赋值导致锁失效 private final Object lock = new Object(); public void method() { synchronized (lock) { ... } // 锁为自定义对象 }
分析可知:如果多个线程使用不同的对象实例(即每个线程的 Runnable
是独立创建的),那么每个实例中的 lock
对象是不同的,此时 synchronized(lock)
无法实现线程同步,因为它们依赖的是不同的锁对象。
假设以下代码中,每个线程都使用独立的 TicketSale
实例:
public class TicketSale implements Runnable {
private final Object lock = new Object(); // 每个实例的 lock 是独立的
private static int tickets = 10; // 假设票池是静态共享的
@Override
public 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
,确保所有实例共享同一把锁。
public class TicketSale implements Runnable {
private static int tickets = 10;
// 静态锁对象(全局唯一)
private static final Object LOCK = new Object();
@Override
public void run() {
synchronized (LOCK) { // 所有线程共享 LOCK
if (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
对象作为锁(等效于静态锁)。
public class TicketSale implements Runnable {
private static int tickets = 10;
@Override
public 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
修饰售票方法或代码块,确保每次只有一个线程执行售票逻辑。private synchronized void sellTicket() { if (tickets > 0) { System.out.println("卖出第 " + tickets-- + " 张票"); } }
结果:避免超卖(如第0张票)或重复卖票(同一票号被多个线程卖出)。
(6) 最佳实践
- 最小化同步范围:尽量用同步代码块代替同步方法,减少锁的持有时间。
- 避免锁嵌套:防止死锁(如线程A持有锁1请求锁2,线程B持有锁2请求锁1)。
- 优先使用线程安全类:如
ConcurrentHashMap
代替手动同步的HashMap
。
10 死锁与释放锁
10.1 死锁示例
死锁是指两个或多个线程互相持有对方需要的锁,且无法释放自己的锁,导致所有线程永久阻塞。以下是一个经典死锁案例:
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 死锁产生条件:
- 互斥:资源(锁)只能被一个线程持有。
- 请求与保持:线程持有至少一个锁,并请求其他线程持有的锁。
- 不可剥夺:线程不会主动释放已持有的锁。
- 循环等待:线程之间形成等待环路。
10.3 释放锁的操作
以下操作会释放锁,允许其他线程获取锁:
1. 同步方法/代码块执行结束
public synchronized void method() {
// 同步代码...
} // 方法执行完毕,自动释放锁
2. 遇到 break
或 return
synchronized (lock) {
if (condition) {
return; // 提前返回,释放锁
}
// 其他代码...
}
3. 未处理的异常或错误
synchronized (lock) {
throw new RuntimeException("异常发生"); // 抛出异常,释放锁
}
4. 调用 wait()
方法
synchronized (lock) {
lock.wait(); // 释放锁,线程进入等待状态
}
wait()
会释放当前锁,直到其他线程调用notify()
/notifyAll()
。
10.4 不释放锁的操作
以下操作不会释放锁,线程仍持有锁:
1. Thread.sleep()
或 Thread.yield()
synchronized (lock) {
Thread.sleep(1000); // 线程休眠,但锁仍被持有
}
- 线程暂停执行,但不会释放锁。
2. 调用 suspend()
(已弃用)
synchronized (lock) {
Thread.currentThread().suspend(); // 线程挂起,但锁仍被持有
}
- 注意:
suspend()
和resume()
已废弃,可能导致死锁和不稳定。
10.5 关键结论
场景 | 是否释放锁 | 原因 |
---|---|---|
同步代码执行完毕 | 是 | 正常流程结束 |
return /break | 是 | 提前退出代码块 |
未处理的异常 | 是 | 异常终止代码块执行 |
wait() | 是 | 主动释放锁并进入等待队列 |
sleep() /yield() | 否 | 线程暂停,但锁仍被持有 |
suspend() | 否 | 线程挂起,但锁未被释放(已弃用,避免使用) |
10.6 如何避免死锁
- 避免嵌套锁:尽量减少同步代码块中的锁嵌套。
- 按固定顺序获取锁:所有线程按相同顺序请求锁(如先
lockA
后lockB
)。 - 使用超时机制:通过
tryLock(timeout)
避免无限等待。 - 避免长时间持有锁:缩小同步代码块的范围。