⛺️ 欢迎关注:👍点赞 👂🏽留言 😍收藏 💞 💞 💞
🚀 前言
之前我们在这篇博客 【Java多线程】:理解线程创建、特性及后台进程 里面已经讲了多线程的基础内容了,现在就要面对多线程的最大问题了,让我们来看看吧
1. 线程的生命周期及状态转换
在Java中,任何对象都有生命周期,线程也不例外,它也有自己的生命周期。
- 当 Thread 对象创建完成时,线程的生命周期便开始了
- 当run()方法中的代码正常执行完毕或者线程抛出一个未捕获的异常(Exception)或者错误(Error)时,线程的生命周期便会结束。
- 在线程的整个生命周期中,线程可能处于不同的状态
- 例如,线程在刚刚创建完成时处于新建状态,线程在执行任务时处于运行状态
- 在线程的整个生命周期中,其基本状态一共有6种,分别是新建(New)状态、可运行(Runnable)状态、锁阻塞(Blocked)状态、无限等待(Waiting)状态、计时等待(Timed_Waiting)状态和被终止(Teminated)状态,线程的不同状态表明了线程当前正在进行的活动。
public class ThreadState {
public static void main(String[] args) {
for (Thread.State state : Thread.State.values()) {
System.out.println(state);
}
}
}
接下来针对线程生命周期中的6种基本状态分别进行详细讲解
(1)新建状态
- 创建一个线程对象后,该线程对象就处于新建状态
- 此时还没调用start()方法启动线程,和其他Java对象一样,仅仅由JVM为其分配了内存,没有表现出任何线程的动态特征
(2)可运行状态
- 可运行状态也称为就绪状态。当线程对象调用了 start() 方法后就进入就绪状态
- 处于就绪状态的线程位于线程队列中,此时它只是具备了运行的条件,要获得CPU的使用权并开始运行,还需要等待系统的调度
(3)锁阻塞状态
- 如果处于可运行状态的线程获得了CPU 的使用权,并开始执行 run() 方法中的线程执行体,则线程处于运行状态。
- 一个线程启动后,它可能不会一直处于运行状态:
- 当一个线程试图获取一个对象锁,而该对象锁被其他的线程持有时,则该线程进入锁阻塞状态;
- 当该线程持有锁的时候,该线程将变成可运行状态。
(4)无限等待状态
- 一个线程在等待另一个线程执行一个(唤醒)动作时,该线程进入无限等待状态
- 线程进入这个状态后是不能自动唤醒的,必须等待另一个线程调用 notify() 或者notifyAll() 方法才能够唤醒。
(5)计时等待状态
- 计时等待状态是具有指定等待时间的线程状态
- 线程由于调用了计时等待的方法(包括 Thread.sleep()、Object.wait(),Thread.join()、LockSupport.parkNanos()、LockSupport.parkUntil() ),并且指定了等待时间,就处于计时等待状态。这一状态将一直保持到超时或者接收到唤醒通知。
(6)被终止状态
- 被终止状态是终止运行的线程的状态
- 线程因为 run() 方法正常退出而死亡,或者因为没有捕获的异常终止了 run() 方法而结束执行。
注意:在程序中,通过一些操作,可以使线程在不同状态之间转换
- 线程状态转化如下:
2. 线程操作的相关方法
🔥 程序中的多个线程是并发执行的,某个线程若想执行,就必须获得CPU的使用权。Java 虚拟机会按照特定的机制为程序中的每个线程分配CPU的使用权,这种机制被称作线程的调度。
在计算机中,线程调度有两种模型,分别是 分时调度模型 和 抢占式调度模型
- 分时调度模型:是指让所有的线程轮流获得CPU的使用权,并且平均分配每个线程占用CPU的时间片
- 抢占式调度模型:是指让线程池中优先级高的线程优先占用CPU;而对于优先级相同的线程,随机选择一个线程使其占用CPU,当它失去了CPU的使用权后,再随机选择其他线程获取 CPU 的使用权
- Java 虚拟机默认采用抢占式调度模型。通常情况下程序员不需要关心计算机使用的是哪种调度模型,但在某些特定的需求下需要改变这种模式,由程序自身控制CPU的调度。本节将围绕线程调度的相关知识进行详细讲解。
2.1 线程的优先级 -- priority
🐸 在应用程序中,如果要对线程进行调度,最直接的方式就是设置线程的优先级
- 优先级越高的线程获得CPU使用权的机会越大,而优先级越低的线程获得CPU使用权的机会越小。
线程的优先级用1~10的整数表示,数字越大,优先级越高。除了可以直接使用数字表示线程的优先级,还可以使用Thread 类中提供的 3 个静态常量表示线程的优先级
优先级常量 | 功能描述 |
static int MAX_PRIORITY | 表示线程的最高优先级,值为 10 |
static int MIN_PRIORITY | 表示线程的最低优先级,值为 1 |
static int NORM_PRIORITY | 表示线程的默认优先级,值为 5 |
程序在运行期间,处于就绪状态的每个线程都有自己的优先级。
例如:主线程具有普通优先级。然而线程的优先级不是固定不变的,可以通过调用Thread 类的 setPriority(in newPriority)方法进行设置,该方法中的参数 newPriority 接收的是1~10 的整数或者 Thread类 的 3 个静态常量
下面演示不同优先级的两个线程在程序中的运行情况
class MaxPriority implements Runnable{
public void run(){
for(int i = 0; i < 3; i++){
System.out.println(Thread.currentThread().getName() + "正在输出: " + i);
}
}
}
class MinPriority implements Runnable{
public void run(){
for(int i = 0; i < 3; i++){
System.out.println(Thread.currentThread().getName() + "正在输出: " + i);
}
}
}
public class thread_two {
public static void main(String[] args) {
// 创建两个线程
Thread minPriority = new Thread(new MinPriority(), "优先级较低的线程");
Thread maxPriority = new Thread(new MaxPriority(), "优先级较高的线程");
minPriority.setPriority(Thread.MIN_PRIORITY);
maxPriority.setPriority(Thread.MAX_PRIORITY);
// 开启两个线程
maxPriority.start();
minPriority.start();
}
}
// 运行输出
优先级较高的线程正在输出: 0
优先级较低的线程正在输出: 0
优先级较低的线程正在输出: 1
优先级较低的线程正在输出: 2
优先级较高的线程正在输出: 1
优先级较高的线程正在输出: 2
- 从上面可以看出,优先级较高的maxPriority线程先。运行,它运行完毕后,优先级较低的minPriority 线程才开始运行。所以优先级越高的线程获取CPU时间片的机会越大。
需要注意的是,虽然Java 提供了线程优先级,但是这些优先级需要操作系统的支持。不同的操作系统对优先级的支持是不一样的,操作系统中的线程优先级不会和Java中线程优先级一一对应。
- 因此,在设计多线程应用程序时,其功能的实现一定不能依赖于线程的优先级,而只能把线程优先级作为一种提高程序效率的手段。
2.2 线程休眠 -- sleep
🦋 线程休眠 指让当前线程暂停执行,从运行状态进入阻塞状态,将CPU资源让给其他线程的一种调度方式,可以调用线程的操作方法 sleep()实现线程休眠,sleep() 方法是 java.lang.Thread 类中定义的静态方法
- 使用 sleep() 方法时需要指定当前线程体眠的时间,传入一个 long类型的数据作为休眠时间,单位为毫秒,并且任意一个线程的实例化对象都可以调用该方法。
如下:
public static void main(String[] args) {
Thread t = new Thread(() ->{
while (true) {
System.out.println("Hello thread");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
});
t.start();
while (true) {
System.out.println("Hello main");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
注意:sleep() 是静态方法,只能控制当前正在运行的线程休眠,而不能控制其他线程休眠。当休眠结束之后,线程就会返回 就绪状态,而不是立即开始执行,
2.3 线程插队 -- join
🦌 线程插队 指将某个线程插入当前线程中,由两个线程交替执行变成两个线程顺序执行,即一个线程执行完毕之后再执行第二个线程,可以通过调用线程对象的 join() 方法实现线程插队。
假设有两个线程——线程甲和线程乙。
- 线程甲在执行到某个时间点的时候调用线程乙的 join() 方法,则表示从当前时间点开始CPU资源被线程乙独占,线程甲进入阻塞状态;
- 直到线程乙执行完毕,线程甲才进入就绪状态,等待获取CPU资源后进入运行状态继续执行。
下面通过一个案例演示 join()方法在程序中的使用,如下:
class JoinRunnable implements Runnable{
public void run(){
for(int i = 1; i <= 3; i++){
System.out.println(Thread.currentThread().getName() + "输出:" + i);
}
}
}
public class thread_join {
public static void main(String[] args) throws InterruptedException{
Thread thread = new Thread(new JoinRunnable(), "Thead");
thread.start();
for(int i =1; i <= 5; i++) {
System.out.println(Thread.currentThread().getName() + "输出: " + i);
if (i == 2) thread.join();
}
}
}
// 输出:
main输出: 1
main输出: 2
Thead输出:1
Thead输出:2
Thead输出:3
main输出: 3
main输出: 4
main输出: 5
Thread 类不仅提供了无参数的线程插队方法 join() ,还提供了带有时间参数的线程插队方法 join(long millis)
- 当执行带有时间参数的 join(long millis) 方法进行线程插队时,必须等待插入的线程指定时间过后才会继续执行其他线程。
同样是完成线程合并的操作,join() 和 join(long millis) 还是有区别的。
- join() 表示在被调用线程执行完成之后才能执行其他线程
- join(long millis) 则表示被调用线程执行 milis 毫秒之后,无论是否执行完毕,其他线程都可以和它争夺CPU资源。
方法 说明 | 功能 |
public void join() x | 等待线程结束 |
public void join(long millis) | 等待线程结束,最多等 millis 毫秒 |
public void join(long millis, int nanos) | 同理,但可以更高精度 |
package NoteBook;
class JoinRunnable implements Runnable{
public void run(){
for(int i = 1; i <= 3; i++){
try {
Thread.currentThread().sleep(1000);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
System.out.println(Thread.currentThread().getName() + "输出:" + i);
}
}
}
public class thread_join {
public static void main(String[] args) throws InterruptedException{
Thread thread = new Thread(new JoinRunnable(), "Thead");
thread.start();
for(int i =1; i <= 5; i++) {
System.out.println(Thread.currentThread().getName() + "输出: " + i);
if (i == 2) thread.join(2000);
}
}
}
// 输出
main输出: 1
main输出: 2
Thead输出:1
main输出: 3
Thead输出:2
main输出: 4
main输出: 5
Thead输出:3
在上面可以看到,当main线程执行到 i = 2 时,thread线程插队,优先于main线程执行。
- thread线程 插队是通过调用 join(3000) 方法实现的。
- 从插队开始 thread线程 独占CPU资源,执行 2000ms之后,main线程继续与thread线程抢占资源。因为 thread线程 每次执行会休眠 1000ms
- 所以看到的结果是在执行了两次 thread线程 之后,main线程再次进人就绪状态,抢占CPU资源。
2.4 线程让步 -- yield
🦁 线程让步 是指在某个特定的时间点,让线程暂停抢占CPU资源的行为,即从运行状态或就绪状态转到阻塞状态,从而将CPU资源让给其他线程使用。
- 这相当于现实生活中地铁排队进站,轮到你进站时,你让其他人先进了,把这次进站的机会让给其他人。
- 但是这并不意味着你放弃排队,你只是在某个时间点做了一次让步,过了这个时间点,你依然要进行排队。
- 线程的让步也是如此:
- 假如线程甲和线程乙在交替执行
- 在某个时间点线程甲做出让步,让线程乙占用了CPU资源,执行其业务逻辑
- 线程乙执行完毕之后,线程甲会再次进人就绪状态,争夺CPU资源
下面通过一个案例演示yield()方法在程序来实现线程让步,如下:
class YieldThread extends Thread{
public YieldThread(String name){
super(name);
}
public void run() {
for(int i = 1; i < 5; i++){
System.out.println(Thread.currentThread().getName() + "---" + i);
if(i == 2){
System.out.print("线程让步: ");
Thread.yield();
}
}
}
}
public class thread_yield {
public static void main(String[] args) {
Thread thread1 = new YieldThread("thread1");
Thread thread2 = new YieldThread("thread2");
thread1.start();
thread2.start();
}
}
// 运行结果如下:(答案不固定)
thread1---1
thread1---2
线程让步: thread2---1
thread2---2
线程让步: thread1---3
thread2---3
thread2---4
thread1---4
- 从上面可以看出,当线程 thread1 输出2以后,会做出让步,线程 thread2 获得执行权;同样,线程thread2输出2以后,也会做出让步,线程thread1获得执行权。
小提示:yield()方法的弊端
- 通过 yield() 方法可以实现线程让步,让当前正在运行的线程失去CPU使用权,让系统的调度器重新调度一次
- 由于Java虚拟机默认采用抢占式调度模型,所有线程都会再次抢占CPU资源使用权,所以在执行线程让步后并不能保证立即执行其他线程,CPU可能会有一段空余时间。
2.5 线程中断 -- interuppt
这里介绍的线程中断是指在线程执行过程中通过手动操作停止该线程
- 例如,当用户在执行一次操作时,因为网络问题导致延迟,则对应的线程对象就一直处于运行状态。如果用户希望结束这个操作,即终止该线程,就要使用线程中断机制了。
在Java中执行线程中断有如下两个常用方法:
public void interrupt()。
public boolean isInterrupted()
- 当一个线程对象调用 interrupt() 方法时,表示中断当前线程对象。每个线程对象都通过一个标志位来判断当前是否为中断状态。
- isInterrupted() 方法 就是用来获取当前线程对象的标志位的。
- 该方法有true和false两个返回值。
- true表示清除了标志位,当前线程对象已经中断
- false表示没有清除标志位,当前对象没有中断
- 当一个线程对象处于不同的状态时,中断机制也是不同的
- 该方法有true和false两个返回值。
下面通过案例来演示不同生命周期状态下的线程中断
public class thread_interrupt {
public static void main(String[] args){
// 情况一:实例化线程,但是未启动
Thread t = new Thread();
t.interrupt();
System.out.println("未启动线程是否中断 --- " + t.isInterrupted());
// 情况二:线程运行
Thread thread = new Thread(new Runnable() {
@Override
public void run() {
for(int i = 0; i < 5; i++){
if(i == 2) {
Thread.currentThread().interrupt();
System.out.println("thread 线程是否中断 ---" + Thread.currentThread().isInterrupted());
}
}
}
});
thread.start();
}
}
// 运行
未启动线程是否中断 --- true
thread 线程是否中断 ---true
2.6 补充
🥝 Thread 的常见构造方法
方法 | 说明 |
Thread() | 创建线程对象 |
Thread(Runnable target) | 使用 Runnable 对象创建线程对象 |
Thread(String name) | 创建线程对象,并命名 |
Thread(Runnable target, String name) | 使用 Runnable 对象创建线程对象,并命名 |
【了解】Thread(ThreadGroup group, Runnable target) |
线程可以被用来分组管理,分好的组即为线程组,这 个目前我们了解即可 |
Thread t1 = new Thread();
Thread t2 = new Thread(new MyRunnable());
Thread t3 = new Thread("这是我的名字");
Thread t4 = new Thread(new MyRunnable(), "这是我的名字");
🥝 Thread 的常见构造属性
有些属性之前已经讲过,我们这里只是做个小结
属性 | 获取方法 |
ID | getId() |
名称 | getName() |
状态 | getState() |
优先级 | getPriority() |
是否后台线程 | isDaemon() |
是否存活 | isAlive() |
是否被中断 | isInterrupted() |
- ID 是线程的唯一标识,不同线程不会重复
- 名称是各种调试工具用到
- 状态表示线程当前所处的一个情况
- 优先级高的线程理论上来说更容易被调度到
- 关于后台线程,需要记住一点:JVM会在一个进程的所有非后台线程结束后,才会结束运行。
- 是否存活,即简单的理解,为 run 方法是否运行结束了
3. 线程同步
💫 之前讲过,多线程的并发执行可以提高程序的效率。但是,当多个线程访问共享资源时,也会引发一些安全问题。
- 例如,当统计一个班的学生数目时,如果有学生进进出出,则很难统计正确。为了解决这样的问题,需要实现多线程的同步,即限制共享资源在同一时刻只能被一个线程访问。本节将详细讲解线程同步的相关知识。
3.1 线程安全
还记得我们之前学的 join嘛,下面我们来演示一个例子,如下:
public class Test {
private static int cnt = 0;
public static void main(String[] args) throws InterruptedException{
Object locker = new Object();
Thread t1 = new Thread(() ->{
// 对 count 变量自增 5w 次
for(int i = 0; i < 50000; i++) cnt++;
});
Thread t2 = new Thread(() ->{
// 对 count 变量自增 5w 次
for(int i = 0; i < 50000; i++) cnt++;
});
t1.start();
t2.start();
// 如果没有这个 join,肯定不行,线程还没有自增完,就开始打印,就会导致打印的 cnt 为0
t1.join(); //线程随机调度,如果将 t1.join 放在 start 之后,就会使得调度混乱
t2.join();
// 预期结果应该是 10 w
System.out.println("cnt最后自增的结果是: " + cnt);
}
}
运行结果如下:
最后结果并不是我们想象的 10 w,因此就会用到 synchronized 的关键字,我们来学习一下吧
3.2 同步代码块
🐅 通过上面可以了解到,线程安全问题其实就是由多个线程同时处理共享资源所导致的。
- 要想解决上面的线程安全问题,必须保证在任何时刻都只能有一个线程访问共享资源
- 为了实现多个线程处理同一个资源,在Java中提供了同步机制
- 当多个线程使用同一个共享资源时,可以将处理共享资源的代码放在一个使用synchronized关键字修饰的代码块中,这个代码块被称作同步代码块。
使用synchronized关键字创建同步代码块的语法格式如下:
synchronized(lock){
处理共享资源的代码块
}
解释:
- 在上面的代码中,lock是一个锁对象,它是同步代码块的关键,相当于为同步代码加锁
- 当某个线程执行同步代码块时,其他线程将无法执行同步代码块,进入阻塞状态
- 当前线程执行完同步代码块后,再与其他线程重新抢夺CPU的执行权,抢到CPU执行权的线程将进入同步代码块,执行其中的代码。
- 以此循环往复,直到共享资源被处理完为止
需要加锁来保证为 10 w,因此代码修改如下:
注意:
- 同步代码块中的锁对象可以是任意类型的对象,但多个线程共享的锁对象必须是同一个。
- “任意”:说的是共享锁对象的类型。
- 锁对象的创建代码不能放到 run()方法中,否则每个线程运行到 run() 方法时都会创建一个新对象,这样,每个线程都会有一个不同的锁,每个锁都有自己的标志位,线程之间便不能产生同步的效果。
3.3 同步方法
🐻 同步代码块可以有效解决线程安全问题,当把共享资源的操作放在同步代码块中时,便为这些操作加了同步锁。
synchronized 关键字除了修饰代码块,同样可以修饰方法,被synchronized 关键字修饰的方法称为同步方法。
- 同步方法和同步代码块一样,在同一时刻只允许一个线程调用同步方法
synchronized 关键字修饰方法的语法格式如下:
synchronized 返回值类型 方法名([参数列表]) {}
代码修改如下:
class Counter{
public int count;
synchronized public void increase(){
count++;
}
public void increase2(){ //上下方法等价,上面是下面方法的简化
synchronized (this){
count++;
}
}
}
public class Demo14 {
public static void main(String[] args) throws InterruptedException{
Counter counter = new Counter();
Thread t1 = new Thread(() ->{
for(int i = 0; i < 50000; i++){
counter.increase();
}
});
Thread t2 = new Thread(() ->{
for(int i = 0; i < 50000; i++){
counter.increase();
}
});
t1.start();
t2.start();
t1.join();
t2.join();
System.out.println(counter.count);
}
}
这个时候也实现了和同步代码块一样的效果。
多学一招:同步方法的锁
- 读者可能会有这样的疑问:同步代码块的锁是自己定义的任意类型的对象,那么同步方法是否也存在锁?如果有,它的锁是什么呢?答案是肯定的,同步方法也有锁,它的锁就是调用该方法的当前对象,也就是this指向的对象。这样做的好处是,同步方法被所有线程所共享,方法所属的对象相对于所有线程来说是唯一的,从而保证了锁的唯一性。当一个线程执行同步方法时,其他线程就不能进入该方法中,直到当前线程执行完同步方法为止,从而达到线程同步的效果。
- 有时候需要同步的方法是静态方法,静态方法不需要创建对象就可以直接用“类名.方法名()”的方式调用。这时候读者就会有一个疑问:如果不创建对象,静态同步方法的锁就不会是this,那么静态同步方法的锁是什么?Java中静态方法的锁是该方法所在类的class对象,class对象在装载该类时自动创建,该对象可以直接用“类名.class”的方式获取,
- 同步代码块和同步方法解决多线程问题有好处也有弊端。同步解决了多个线程同时访问共享数据时的线程安全问题,只要加上同一个锁,在同一时间内只能有一个线程执行。但是线程在执行同步代码时每次都会判断锁的状态,非常消耗系统资源,效率较低。
3.4 死锁问题
有这样一个场景:一个中国人和一个美国人在一起吃饭,美国人拿了中国人的筷子,中国人拿了美国人的刀叉,两个人开始争执不休:
- 中国人:“你先给我筷子,我再给你刀叉!”
- 美国人:“你先给我刀叉,我再给你筷子!”
结果可想而知,两个人都吃不成饭。
这个例子中的中国人和美国人相当于不同的线程,筷子和刀又就相当于锁。两个线程在运行时都在等待对方的锁,这样便造成了程序的停滞,这种现象称为 死锁
public class DeadLocker {
private static Object locker1 = new Object();
private static Object locker2 = new Object();
// 死锁案例:
public static void main(String[] args) {
Thread t1 = new Thread(() ->{
synchronized (locker1){
// 此处的 sleep 很重要,要确保 t1 和 t2 都分别拿到一把锁之后再进行后续操作
try{
Thread.sleep(1000);
} catch (InterruptedException e){
e.printStackTrace();
}
synchronized (locker2){
System.out.println("t1 加锁成功");
}
}
});
Thread t2 = new Thread(() ->{
synchronized (locker2){
try{
Thread.sleep(1000);
} catch (InterruptedException e){
e.printStackTrace();
}
synchronized (locker1){
System.out.println("t2 加锁成功");
}
}
});
t1.start();
t2.start();
}
}
运行输出如下:
原因:两个线程都需要对方占用的资源,但是都无法释放自己的锁,于是两个线程都处于挂起状态,因此造成了上面看到的死锁
解决办法如下:
public class DeadLocker {
private static Object locker1 = new Object();
private static Object locker2 = new Object();
// 解决死锁的方法:
// 方式一:不构成嵌套即可
public static void main(String[] args) {
Thread t1 = new Thread(() ->{
synchronized (locker1){
// 此处的 sleep 很重要,要确保 t1 和 t2 都分别拿到一把锁之后再进行后续操作
try{
Thread.sleep(1000);
} catch (InterruptedException e){
e.printStackTrace();
}
}
synchronized (locker2){
System.out.println("t1 加锁成功");
}
});
Thread t2 = new Thread(() ->{
synchronized (locker2){
try{
Thread.sleep(1000);
} catch (InterruptedException e){
e.printStackTrace();
}
}
synchronized (locker1){
System.out.println("t2 加锁成功");
}
});
t1.start();
t2.start();
}
//方式二:约定先用 locker1,后用locker2
public static void main3(String[] args) {
Thread t1 = new Thread(() ->{
synchronized (locker1){
// 此处的 sleep 很重要,要确保 t1 和 t2 都分别拿到一把锁之后再进行后续操作
try{
Thread.sleep(1000);
} catch (InterruptedException e){
e.printStackTrace();
}
synchronized (locker2){
System.out.println("t1 加锁成功");
}
}
});
Thread t2 = new Thread(() ->{
synchronized (locker1){
try{
Thread.sleep(1000);
} catch (InterruptedException e){
e.printStackTrace();
}
synchronized (locker2){
System.out.println("t2 加锁成功");
}
}
});
t1.start();
t2.start();
}
}
解决死锁的方法主要有以下几种:
- 避免死锁:通过破坏死锁的必要条件之一来预防死锁
- 互斥条件:允许进程同时访问某些资源,但并非所有资源都可以被同时访问,因此这种方法在实际应用中可能有限。
- ”不可抢占条件:允许进程强行从占有者那里夺取某些资源,即当一个进程申请不到新的资源时,必须释放所占有的资源,之后再重新申请。
- 循环等待条件:通过避免存在一个进程等待序列(如P1等待P2,P2等待P3,….,Pn等待P1),可以有效避免死锁。
- 检测与解除死锁:允许系统进入死锁状态,但通过检测和解除来解决
- 检测死锁:通过检测系统状态,识别出死锁的进程
- 解除死锁:通过终止或回滚死锁进程来解除死锁。
3.5 重入锁
🦋 重入锁(ReentrantLock)的作用类似于synchronized 关键字,synchronized 关键字是通过Java虚拟机实现的,而重入锁通过JDK实现。
- 重入锁是指可以给同一个资源添加多个锁,并且释放锁的方式与synchronized也不同。
- synchronized的锁在线程执行完毕之后自动释放。
- ReentrantLock的锁必须手动释放。
重入锁的使用格式如下:
private ReentrantLock reentrantLock = new ReentrantLock();
//加锁
reentrantLock.lock();
//需要锁的数据
//释放锁
修改后的代码如下:
package NoteBook;
import java.util.concurrent.locks.ReentrantLock;
class Counter {
public int count;
// 使用 ReentrantLock 代替 synchronized
private final ReentrantLock lock = new ReentrantLock();
public void increase() {
// 使用 lock 进行加锁
lock.lock();
try {
count++;
} finally {
lock.unlock(); // 保证释放锁
}
}
// 静态方法使用 ReentrantLock 来代替 synchronized
private static final ReentrantLock staticLock = new ReentrantLock();
public static void increase3() {
// 使用静态锁来替代 synchronized
staticLock.lock();
try {
// 静态方法的同步内容
} finally {
staticLock.unlock(); // 保证释放静态锁
}
}
public static void increase4() {
// 使用锁住 Counter.class 对象来代替 synchronized
staticLock.lock();
try {
// 静态方法的同步内容
} finally {
staticLock.unlock(); // 保证释放锁
}
}
}
public class thread_reentrant {
public static void main(String[] args) throws InterruptedException {
Counter counter = new Counter();
// 创建两个线程,分别执行 increase 方法
Thread t1 = new Thread(() -> {
for (int i = 0; i < 50000; i++) {
counter.increase();
}
});
Thread t2 = new Thread(() -> {
for (int i = 0; i < 50000; i++) {
counter.increase();
}
});
t1.start();
t2.start();
t1.join();
t2.join();
System.out.println(counter.count); // 输出最终结果
}
}
从上面可以看出, 运行结果 和 使用 synchronized 结果一致。如果需要在此基础添加几把锁,只需要调用 lock() 方法即可。 需要注意的是,使用重入锁是加了几把锁就必须释放几把锁,否则会使线程处于阻塞状态
为什么 ReentrantLock 更灵活?
- ReentrantLock 是可重入的,这意味着同一线程可以多次获得同一个锁而不会发生死锁。
- ReentrantLock 提供了更丰富的功能,比如尝试锁定 (tryLock()),锁定带有超时的 (lock(long time, TimeUnit unit)) 等,这对于一些复杂的并发控制逻辑是非常有用的。
- 与 synchronized 相比,ReentrantLock 更适用于需要在多个线程之间共享更复杂的控制逻辑时。
结论:
通过将 synchronized 替换为 ReentrantLock,我们实现了与原来代码相同的线程安全效果,只不过是显式控制加锁和释放锁的过程。这样做可以提高代码的可读性和灵活性,尤其在复杂并发操作中,ReentrantLock 提供了更多控制选项。