JUC并发编程学习与实践

news2024/10/7 7:30:39

文章目录

  • 学习资料
  • 创建和运行线程
    • 方法一:直接使用Thread
    • 方法二:使用Runnable配合Thread
    • 方法三:FutureTask配合Thread
  • 线程的常见方法
    • start与run
    • sleep与yield
      • 线程的优先级
    • join方法详解
    • interrupt线程打断
      • interrupt线程打断后,线程不会终止运行
      • 两阶段终止模式
      • isInterrupted与interrupted
      • LockSupport.park()阻塞线程
    • 不推荐的方法
    • 主线程与守护线程
      • 其它线程
      • 守护线程
    • 线程的状态
      • 从操作系统层面分
      • 从Java API层面分
        • 示例代码
  • 共享模型之管程
    • 共享带来的问题
      • Java的体现
      • 临界区 Critical Section
      • 竞态条件 Race Condition
    • synchronized解决方案
      • 应用之互斥
      • synchronized
        • 语法
        • 解决
      • synchronized加在方法上
    • 变量的线程安全分析
      • 成员变量和静态变量是否线程安全?
      • 局部变量是否线程安全?
    • Monitor(锁)
    • 轻量级锁
    • 锁膨胀
    • 自旋优化
    • 偏向锁
      • 偏向状态
      • 撤销-调用对象hashCode
      • 撤销-其他线程使用对象
      • 批量重偏向
      • 批量撤销
    • Wait notify
      • 原理
      • API介绍
      • Wait notify的正确姿势
    • 模式
      • 同步模式之保护性暂停
        • 定义
        • 代码示例
      • 异步模式之生产者/消费者
        • 代码示例
    • Park & Unpark
      • 基本使用
      • 特点
      • 原理
    • 线程状态转换
      • 情况1 NEW --> RUNNABLE
      • 情况2 RUNNABLE <--> WAITING
      • 情况3 RUNNABLE < -- > WAITING
      • 情况4 RUNNABLE < -- > WAITING
      • 情况5 RUNNABLE < -- > TIMED_WAITING
      • 情况6 RUNNABLE < -- > TIMED_WAITING
      • 情况7 RUNNABLE < -- > TIMED_WAITING
      • 情况8 RUNNABLE < -- > TIMED_WAITING
      • 情况9 RUNNALE < -- > BLOCKED
      • 情况10 RUNNABLE < -- > TERMINATED
    • 活跃性
      • 死锁
      • 定位死锁


学习资料

【黑马程序员深入学习Java并发编程,JUC并发编程全套教程-哔哩哔哩】
【阿里巴巴Java开发手册】

创建和运行线程

方法一:直接使用Thread

package com.xz;

public class ThreadTest1 {

    public static void main(String[] args) {
        Thread thread = new Thread(){
            @Override
            public void run() {
                System.out.println("线程执行");
            }
        };
        thread.start();
    }
}

方法二:使用Runnable配合Thread

package com.xz;

public class ThreadTest2 {

    public static void main(String[] args) {
        Runnable runnable = () -> System.out.println("线程执行");
        Thread thread = new Thread(runnable);
        thread.start();
    }
}

方法三:FutureTask配合Thread

package com.xz;

import java.util.concurrent.ExecutionException;
import java.util.concurrent.FutureTask;

public class ThreadTest3 {

    public static void main(String[] args) throws ExecutionException, InterruptedException {
        FutureTask<Integer> task = new FutureTask<>(() -> {
            System.out.println("线程执行");
            return 100;
        });
        Thread thread = new Thread(task);
        thread.start();
        System.out.println(task.get());// 阻塞,等待FutureTask结果返回
    }
}

线程的常见方法

start与run

调用start()方法是启动一个新的线程。

调用run()方法相当于在当前线程中,调用run()方法,而不是再新启动一个线程。

start()方法不能被调用两次,否则会报错。

sleep与yield

调用sleep会让当前线程从Running运行状态进入Timed Waiting定时等待状态

其它线程可以使用interrupt方法打断正在睡眠的线程,这时sleep方法会抛出InterruptedException

睡眠结束后的线程未必会立刻得到执行。

Thread.sleep()在哪个线程中被调用,哪个线程就获得休眠。

建议用TimeUnit的sleep代替Thread的sleep来获得更好的可读性。
TimeUnit提供多种时间单位。
在这里插入图片描述
如下代码,效果相同的情况下,TimeUnit的可读性更好。
在这里插入图片描述
TimeUnit的sleep内部,其实调用的也是Thread的sleep方法。
在这里插入图片描述

调用yield会让当前线程从Running运行状态进入Runnable就绪状态,然后调度执行其它优先级的线程。如果这时没有同优先级的线程,那么不能保证让当前线程暂停的效果。

具体的实现依赖于操作系统的任务调度器。

线程的优先级

线程优先级会提示(hint)调度器优先调度该线程,但它仅仅是一个提示,调度器可以忽略它。

如果cpu比较忙,那么优先级高的线程会获得更多的时间片,但cpu闲时,优先级几乎没作用。

join方法详解

package com.xz;

import java.util.concurrent.TimeUnit;

public class JoinTest {

    private static Integer r = 0;

    public static void main(String[] args) throws InterruptedException {
        System.out.println("开始");
        Thread t1 = new Thread(()->{
            System.out.println("线程开始");
            try {
                TimeUnit.MILLISECONDS.sleep(1);
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            }
            System.out.println("线程结束");
            r = 10;
        });
        t1.start();
        System.out.println("结果为:"+r);
        System.out.println("结束");
    }
}

输出结果

开始
线程开始
线程结束
结果为:0
结束

分析

因为主线程和线程t1是并行执行的,t1线程需要1毫秒后才能算出r=10
而主线程一开始就要打印r的结果,所以只能打印r=0

解决办法

用join,加在t1.start()之后即可,因为join方法的定义是等待该线程执行直到终止

package com.xz;

import java.util.concurrent.TimeUnit;

public class JoinTest {

    private static Integer r = 0;

    public static void main(String[] args) throws InterruptedException {
        System.out.println("开始");
        Thread t1 = new Thread(()->{
            System.out.println("线程开始");
            try {
                TimeUnit.MILLISECONDS.sleep(1);
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            }
            System.out.println("线程结束");
            r = 10;
        });
        t1.start();
        t1.join();
        System.out.println("结果为:"+r);
        System.out.println("结束");
    }
}

输出结果

开始
线程开始
线程结束
结果为:10
结束

join(最大等待毫秒)如果join加了参数,则意味着最多等待多少毫秒,超过这个时间则停止等待,如果再不超过这个时间的时候,线程结束了,则join立马成功退出,而不会强行等待到最大等待毫秒。

interrupt线程打断

package com.xz;

import java.util.concurrent.TimeUnit;

public class InterruptTest {

    public static void main(String[] args) throws InterruptedException {
        Thread t1 = new Thread(()->{
            try {
                TimeUnit.SECONDS.sleep(5);
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            }
        });
        t1.start();
        t1.interrupt();
        System.out.println("获取打断标记:"+t1.isInterrupted());
        TimeUnit.SECONDS.sleep(1);
        System.out.println("如果sleep、wait、join后,再次获取打断标记则为假:"+t1.isInterrupted());
    }
}

运行结果,强行打断会抛出异常

Exception in thread "Thread-0" java.lang.RuntimeException: java.lang.InterruptedException: sleep interrupted
	at com.xz.InterruptTest.lambda$main$0(InterruptTest.java:12)
	at java.base/java.lang.Thread.run(Thread.java:833)
Caused by: java.lang.InterruptedException: sleep interrupted
	at java.base/java.lang.Thread.sleep(Native Method)
	at java.base/java.lang.Thread.sleep(Thread.java:337)
	at java.base/java.util.concurrent.TimeUnit.sleep(TimeUnit.java:446)
	at com.xz.InterruptTest.lambda$main$0(InterruptTest.java:10)
	... 1 more
获取打断标记:true
如果sleep、wait、join后,再次获取打断标记则为假:false

interrupt线程打断后,线程不会终止运行

interrupt线程打断后,线程不会终止运行,需要开发者自行判断,如下,先通过Thread.currentThread()获取当前线程,然后通过isInterrupted()方法获取打断标记,进行判断程序是否终止运行。

package com.xz;

public class InterruptTest {

    public static void main(String[] args) {
        Thread t1 = new Thread(()->{
            while (true) {
               boolean interrupted = Thread.currentThread().isInterrupted();
               if(interrupted){
                   break;
               }
            }
        },"t1");
        t1.start();
        t1.interrupt();
    }
}

两阶段终止模式

在这里插入图片描述

package com.xz;

import java.util.concurrent.TimeUnit;

public class TwoPhaseTerminationTest {

    public static void main(String[] args) {
        try {
            TwoPhaseTermination twoPhaseTermination = new TwoPhaseTermination();
            twoPhaseTermination.start();
            TimeUnit.SECONDS.sleep(5);
            twoPhaseTermination.stop();
        }catch (Exception e){
            e.printStackTrace();
        }
    }

}

class TwoPhaseTermination {
    private Thread monitor;

    // 开启线程
    public void start() {
        monitor = new Thread(() -> {
            System.out.println("进入线程");
            while (true) {
                Thread current = Thread.currentThread();
                if(current.isInterrupted()){
                    System.out.println("料理后事");
                    break;
                }
                try {
                    TimeUnit.SECONDS.sleep(1);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                    current.interrupt();// 异常并不能影响程序执行,并且会重置线程打断状态,需要再次设置打断状态,否则会不断的执行
                }
                System.out.println("执行监控记录");
            }
        });
        monitor.start();
    }

    // 关闭线程
    public void stop() {
        monitor.interrupt();
    }
}

执行结果

进入线程
执行监控记录
执行监控记录
执行监控记录
执行监控记录
java.lang.InterruptedException: sleep interrupted
	at java.base/java.lang.Thread.sleep(Native Method)
	at java.base/java.lang.Thread.sleep(Thread.java:337)
	at java.base/java.util.concurrent.TimeUnit.sleep(TimeUnit.java:446)
	at com.xz.TwoPhaseTermination.lambda$start$0(TwoPhaseTerminationTest.java:34)
	at java.base/java.lang.Thread.run(Thread.java:833)
执行监控记录
料理后事

isInterrupted与interrupted

isInterrupted判断是否被打断,不会清除打断标记。在这里插入图片描述

interrupted判断当前线程是否被打断,会清除打断标记。在这里插入图片描述

LockSupport.park()阻塞线程

当打断状态为true时,LockSupport.park()失效,只有当打断状态为false时,LockSupport.park()才会终止执行。

package com.xz;

import java.util.concurrent.locks.LockSupport;

public class LockSupportParkTest {

    public static void main(String[] args) {
        Thread t1 = new Thread(() -> {
            System.out.println("打断状态:"+Thread.currentThread().isInterrupted());
            LockSupport.park();
            System.out.println("打断状态:"+Thread.currentThread().interrupted()+",并重置为false");
            LockSupport.park();
            System.out.println("打断状态:"+Thread.currentThread().isInterrupted());
        });
        t1.start();
        t1.interrupt();
    }
}

执行结果

打断状态:true
打断状态:true,并重置为false

不推荐的方法

还有一些不推荐使用的方法,这些方法已经过时,容易破坏同步代码块,造成线程死锁。
stop()停止线程运行
suspend()挂起(暂停)线程运行
resume()恢复线程运行

主线程与守护线程

默认情况下,java程序需要等待所有线程都运行结束,才会结束。有一种特殊的线程叫做守护线程,只要其它非守护线程运行结束了,即时守护线程的代码没有执行完,也会强制结束。

其它线程

package com.xz;

import java.util.concurrent.TimeUnit;

public class DaemonTest {
    public static void main(String[] args) {
        Thread t1 = new Thread(() -> {
            try {
                TimeUnit.SECONDS.sleep(100); //睡眠100秒
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            }
        });
        t1.start();
        System.out.println("方法结束");
    }
}

执行结束,线程并没有退出,而是继续执行自己的代码。
在这里插入图片描述

守护线程

通过setDaemon(true);设置为守护线程。

package com.xz;

import java.util.concurrent.TimeUnit;

public class DaemonTest {
    public static void main(String[] args) {
        Thread t1 = new Thread(() -> {
            try {
                TimeUnit.SECONDS.sleep(100); //睡眠100秒
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            }
        });
        t1.setDaemon(true);// 设置为守护线程
        t1.start();
        System.out.println("方法结束");
    }
}

不管守护线程是否执行结束,其它线程结束时,守护线程直接结束。
在这里插入图片描述

注意
垃圾回收线程就是一种守护线程。
Tomcat中的Acceptor和Poller线程都是守护线程,所以Tomcat接收到shutdown命令后,不会等待它们处理完当前请求。

线程的状态

从操作系统层面分

从操作系统层面来描述的话,总共分为五种状态。
在这里插入图片描述

【初始状态】仅是在语言层面创建了线程对象,还未与操作系统线程关联。

【可运行状态】(就绪状态)指该线程已经被创建(与操作系统线程关联),可以由 CPU 调度执行。

【运行状态】指获取了CPU时间片运行中的状态,当CPU 时间片用完,会从【运行状态】 转换至【可运行状态】,会导致线程的上下文切换。

【阻塞状态】如果调用了阻塞API,如 BIO 读写文件,这时该线程实际不会用到CPU,会导致线程上下文切换,进入【阻塞状态】。等 BIO操作完毕,会由操作系统唤醒阻塞的线程,转换至【可运行状态】与【可运行状态】的区别是,对【阻塞状态】的线程来说只要它们一直不唤醒,调度器就一直不会考虑调度它们。

【终止状态】表示线程已经执行完毕,生命周期已经结束,不会再转换为其他状态。

从Java API层面分

从Java API层面来描述的话,根据Thread.State枚举,分为六种状态。
在这里插入图片描述

NEW 线程刚被创建,但是还没有调用start()方法。

RUNNABLE 当调用了start()方法之后,注意,JavaAPI层面的 RUNNABLE 状态涵盖了操作系统层面的【可运行状态】、【运行状态】和【阻塞状态】(由于 BIO 导致的线程阻塞,在Java 里无法区分,仍然认为是可运行)。

BLOCKED,WAITING,TIMED_WAITING 都是Java API层面对【阻塞状态】的细分,后面会在状态转换一节详述。

TERMINATED 当线程代码运行结束。

示例代码
package com.xz;

import java.util.concurrent.TimeUnit;

public class StateTest {

    public static void main(String[] args) {
        Thread t1 = new Thread(() -> {
            System.out.println("running...");
        },"t1");

        Thread t2 = new Thread(() -> {
            while (true) {

            }
        },"t2");
        t2.start();

        Thread t3 = new Thread(() -> {
            System.out.println("running...");
        },"t3");
        t3.start();

        Thread t4 = new Thread(() -> {
            synchronized (StateTest.class) {
                try {
                    TimeUnit.SECONDS.sleep(10000);
                } catch (InterruptedException e) {
                    throw new RuntimeException(e);
                }
            }
        },"t4");
        t4.start();

        Thread t5 = new Thread(() -> {
            try {
                t2.join();
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            }
        },"t5");
        t5.start();

        Thread t6 = new Thread(() -> {
            synchronized (StateTest.class) {
                try {
                    TimeUnit.SECONDS.sleep(10000);
                } catch (InterruptedException e) {
                    throw new RuntimeException(e);
                }
            }
        },"t6");
        t6.start();

        System.out.println("t1 state:"+t1.getState());
        System.out.println("t2 state:"+t2.getState());
        System.out.println("t3 state:"+t3.getState());
        System.out.println("t4 state:"+t4.getState());
        System.out.println("t5 state:"+t5.getState());
        System.out.println("t6 state:"+t6.getState());
    }
}

运行结果

running...
t1 state:NEW
t2 state:RUNNABLE
t3 state:TERMINATED
t4 state:TIMED_WAITING
t5 state:WAITING
t6 state:BLOCKED

共享模型之管程

共享带来的问题

Java的体现

两个线程对初始值为0的静态变量一个做自增,一个做自减,各做5000次,结果是0吗?

static int counter = 0;

    public static void main(String[] args) throws InterruptedException {
        Thread t1 = new Thread(() -> {
            for(int i=0; i<5000; i++){
                counter ++;
            }
        },"t1");

        Thread t2 = new Thread(() -> {
            for(int i=0; i<5000; i++){
                counter --;
            }
        },"t2");

        t1.start();
        t2.start();
        t1.join();
        t2.join();
        System.out.println("counter="+counter);
    }

多次运行结果

counter=-1809
counter=271
counter=-2332
counter=-2849
counter=-1071

临界区 Critical Section

一段代码块内如果存在对共享资源的多线程读写操作,称这段代码块为临界区

例如,下面代码中的临界区


    static int counter = 0;

    static void increment()
    // 临界区
    {
        counter++;
    }

    static void decrement()
    // 临界区
    {
        counter--;
    }

竞态条件 Race Condition

多个线程在临界区内执行,由于代码的执行序列不同而导致结果也无法预测,称之为发生了竞态条件

synchronized解决方案

应用之互斥

为了避免临界区的竞态条件发生,有多种手段可以达到目的。
1、阻塞式的解决方案:synchronized、Lock
2、非阻塞式的解决方案:原子变量

本次使用阻塞式的解决方案:synchronized,来解决上述问题,即俗称的【对象锁】,它采用互斥的方式让同一时刻至多只有一个线程能持有对象锁,其他线程再想获取这个【对象锁】时就会阻塞住。这样就能保证拥有锁的线程可以安全的执行临界区内的代码,不用担心线程上下文切换。

注意
虽然java中互斥和同步都可以采用synchronized关键字来完成,但它们还是有区别的:
1、互斥是保证临界区的竞态条件发生,同一时刻只能有一个线程执行临界区的代码。
2、同步是由于线程执行的先后、顺序不同、需要一个线程等待其它线程运行到某个点。

synchronized

同一时刻只能有一个线程获得锁,假设线程1获得了此锁,那么线程2想获得锁的时候只能等待,线程1执行完毕后,会唤起线程2,此时线程2才能获得此锁,只能一个线程执行完毕后,另一个线程才能开始执行。并且只有synchronized锁中的是同一个对象,才具有互斥性。 为确保锁中的是同一个对象,建议在对象上加final关键词,确保对象不可变。

语法
synchronized(对象)
{
	临界区
}
解决

    static int counter = 0;
    static final Object romm = new Object();

    public static void main(String[] args) throws InterruptedException {
        Thread t1 = new Thread(() -> {
            for(int i=0; i<5000; i++){
                synchronized (romm) {
                    counter ++;
                }
            }
        },"t1");

        Thread t2 = new Thread(() -> {
            for(int i=0; i<5000; i++){
                synchronized (romm) {
                    counter --;
                }
            }
        },"t2");

        t1.start();
        t2.start();
        t1.join();
        t2.join();
        System.out.println("counter="+counter);
    }

多次运行结果

counter=0
counter=0
counter=0
counter=0
counter=0

在这里插入图片描述
在这里插入图片描述

synchronized加在方法上

class SynchronizedTest {

    public synchronized void test(){
        
    }
}

等价于

class SynchronizedTest {

    public void test(){
        synchronized (this) {
            
        }
    }
}
class SynchronizedTest {

    public synchronized static void test(){
        
    }
}

等价于

class SynchronizedTest {

    public static void test(){
        synchronized (SynchronizedTest.class) {

        }
    }
}

变量的线程安全分析

成员变量和静态变量是否线程安全?

如果它们没有共享,则线程安全。

如果它们被共享了,根据它们的状态是否能够改变,又分两种情况。
1、如果只有读操作,则线程安全。
2、如果有读写操作,则这段代码是临界区,需要考虑线程安全。

局部变量是否线程安全?

局部变量是线程安全的。

但局部变量引用的对象则未必。
1、如果该对象没有逃离方法的作用范围,它是线程安全的。
2、如果该对象逃离方法的作用范围,需要考虑线程安全。

Monitor(锁)

Monitor被翻译为监视器管程

每个Java对象都可以关联一个Monitor对象,如果使用synchronized给对象上锁(重量锁)之后,该对象头的Mark Word中就被设置指向Monitor对象的指针。

Monitor结构如下
在这里插入图片描述
1、刚开始 Monitor 中 Owner 为 null。

2、当Thread-2 执行 synchronized(obj)就会将 Monitor 的所有者 Owner 置为 Thread-2,Monitor中只能有一个Owner。

3、在 Thread-2 上锁的过程中,如果 Thread-3,Thread-4,Thread-5 也来执行 synchronized(obj),就会进入EntryList BLOCKED。

4、Thread-2 执行完同步代码块的内容,然后唤醒 EntryList 中等待的线程来竞争锁,竞争的时是非公平的。

5、图中 WaitSet 中的 Thread-0,Thread-1 是之前获得过锁,但条件不满足进入 WAITING 状态的线程,后面讲 wait-notify 时会分析。

注意:
1、synchronized必须是进入同一个对象的monitor才有上述效果。
2、不加synchronized的对象不会关联监视器,不遵从以上规则。

轻量级锁

轻量级锁的使用场景:如果一个对象虽然有多线程访问,但多线程访问的时间可能是错开的(也就是没有竞争),那么可以使用轻量级锁来优化。

轻量级锁对使用者是透明的,即语法仍然是使用synchronized。

假设有两个方法


    static final Object obj = new Object();

    public static void method1(){
        synchronized (obj) {
            // 同步块 A
            method2();
        }
    }

    public static void method2(){
        synchronized (obj) {
            // 同步块 B
        }
    }

创建锁记录(Lock Record)对象,每个线程的栈帧都会包含一个锁记录的结构,内部可以存储锁定对象的Mark Word。
在这里插入图片描述

让锁记录中Object reference指向锁对象,并尝试用cas替换Object的Mark Word,将Mark Word的值存入锁记录。
在这里插入图片描述

如果cas替换成功,对象头中存储了锁记录地址和状态00,表示由该线程给对象加锁,这时图示如下。
在这里插入图片描述

如果cas失败,有两种情况
1、如果其它线程已经持有了该Object的轻量级锁,这时表明有竞争,进入锁膨胀过程。
2、如果自己执行了synchronized锁重入,那么再添加一条Lock Record作为重入的计数。
在这里插入图片描述

当退出synchronized代码块(解锁时)如果有取值为null的锁记录,表示有重入,这时重置锁记录,表示重入计数减一。
在这里插入图片描述
当退出synchronized代码块(解锁时)锁记录的值不为null,这时使用cas将Mark Word的值恢复给对象头。
成功,则解锁成功。
失败,说明轻量级锁进行了锁膨胀或已经升级为重量级锁,进入重量级锁解锁流程。

锁膨胀

如果再尝试加轻量级锁的过程中,CAS操作无法成功,这时一种情况就是有其它线程为此对象加上了轻量级锁(有竞争),这时需要进行锁膨胀,将轻量级锁变为重量级锁。

	static final Object obj = new Object();

    public static void method1(){
        synchronized (obj) {
            // 同步块
        }
    }

当Thread-1进行轻量级加锁时,Thread-0已经对该对象加了轻量级锁。
在这里插入图片描述
这时Thread-1加轻量级锁失败,进入锁膨胀流程。
1、即Object对象申请Monitor锁,让Object指向重量级锁地址。
2、然后自己进入Monitor的EntryList BLOCKED。
在这里插入图片描述
当Thread-0退出同步块解锁时,使用cas将Mark Word的值恢复给对象头,失败。这时会进入重量级解锁流程,即按照Monitor地址找到Monitor对象,设置Owner为null,唤醒EntryList中的BLOCKED线程。

自旋优化

重量级锁竞争的时候,还可以使用自旋来进行优化,如果当前线程自旋成功(即这时候持锁线程已经退出了同步块,释放了锁),这时当前线程就可以避免阻塞。

自旋重试成功的情况
在这里插入图片描述

自旋重试失败的情况
在这里插入图片描述
在Java 6之后自旋锁是自适应的,比如对象刚刚的一次自旋操作成功过,那么认为这次自旋成功的可能性会高,就多自旋几次,反之,就少自旋甚至不自旋,总之,比较智能。

自旋占用CPU时间,单核CPU自旋就是浪费,多核CPU自旋才能发挥优势。

Java 7之后不能控制是否开启自旋功能。

偏向锁

轻量级锁在没有竞争时(就自己这个线程),每次重入仍需要执行CAS操作。

Java 6中引入了偏向锁来做进一步优化:只有第一次使用CAS将线程ID设置到对象的Mark Word头,之后发现这个线程ID是自己的就表示没有竞争,不用重新CAS。以后只要不发生竞争,这个对象就归该线程所有。

例如:

    static final Object obj = new Object();

    public static void m1(){
        synchronized (obj) {
            // 同步块 A
            m2();
        }
    }

    public static void m2(){
        synchronized (obj) {
            // 同步块 B
            m3();
        }
    }

    public static void m3(){
        synchronized (obj) {
            // 同步块 C
        }
    }

在这里插入图片描述

在这里插入图片描述

偏向状态

在这里插入图片描述
一个对象创建时:

如果开启了偏向锁(默认开启),那么对象创建后,markword值为0x05即最后3位为101,这时它的thread、epoch、age都为0。

偏向锁是默认是延迟的,不会在程序启动时立即生效,如果想避免延迟,可以加VM参数 -XX:BiasedLockingStartupDelay=0来禁用延迟。

如果没有开启偏向锁,那么对象创建后,markword值为0x01即最后3位为001,这时它的hashcode、age都为0,第一次用到hashcode时才会赋值。

添加VM参数-XX:-UseBiasedLocking禁用偏向锁。

撤销-调用对象hashCode

调用了对象的hashCode,但偏向锁的对象MarkWord中存储的线程id,如果调用hashCode会导致偏向锁被撤销。
1、轻量级锁在锁记录中记录hashCode。
2、重量级锁会在Monitor中记录hashCode。

在调用hashCode后使用偏向锁,记得去掉-XX:-UseBiasedLocking

撤销-其他线程使用对象

当有其他线程使用偏向锁对象时,会将偏向锁升级为轻量级锁。

批量重偏向

如果对象虽然被多个线程访问,但没有竞争,这时偏向了线程T1的对象仍有机会重新偏向T2,重偏向会重置对象的Thread ID。

当撤销偏向锁阈值超过20次后,jvm会这样觉得,我是不是偏向错了呢?于是会给这些对象加锁时重新偏向至加锁线程。

批量撤销

当撤销偏向锁阈值超过40次后,jvm会觉得,自己确实偏向错了,根本就不该偏向。于是整个类的所有对象都会变为不可偏向的,新建的对象也是不可偏向的。

Wait notify

原理

在这里插入图片描述
Owner 线程发现条件不满足,调用wait方法,即可进入WaitSet变为WAITING状态。

BLOCKED和WAITING的线程都处于阻塞状态,不占用CPU时间片。

BLOCKED线程会在Owner线程释放锁时唤醒。

WAITING线程会在Owner线程调用notify或notifyAll时唤醒,但唤醒后并不意味着立即获得锁,仍需进入EntryList重新竞争。

API介绍

obj.wait()让进入object监视器的线程到waitSet等待。

obj.notify()在object上正在waitSet等待的线程中挑一个唤醒。

obj.notifyAll()让object上正在waitSet等待的线程全部唤醒。

它们都是线程之间进行协作的手段,都属于Object对象的方法。必须获得此对象的锁,才能调用这几个方法。

package com.xz;

import java.util.concurrent.TimeUnit;

public class TestWaitNotify {

    final static Object obj = new Object();

    public static void main(String[] args) throws InterruptedException {
        new Thread(() -> {
            synchronized (obj) {
                System.out.println("执行t1线程");
                try {
                    obj.wait();// 让线程在obj上一直等待下去
                } catch (InterruptedException e) {
                    throw new RuntimeException(e);
                }
                System.out.println("t1其他代码");
            }
        },"t1").start();

        new Thread(() -> {
            synchronized (obj) {
                System.out.println("执行t2线程");
                try {
                    obj.wait();// 让线程在obj上一直等待下去
                } catch (InterruptedException e) {
                    throw new RuntimeException(e);
                }
                System.out.println("t2其他代码");
            }
        },"t2").start();

        TimeUnit.SECONDS.sleep(2);
        System.out.println("唤起obj上其它线程");
        synchronized (obj) {
            obj.notify();// 唤醒obj上一个线程
            // obj.notifyAll();// 唤醒obj上所有等待线程
        }
    }
}

obj.notify();运行结果

执行t1线程
执行t2线程
唤起obj上其它线程
t1其他代码

obj.notifyAll();运行结果

执行t1线程
执行t2线程
唤起obj上其它线程
t1其他代码
t2其他代码

wait()方法会释放对象的锁,进入WaitSet等待区,从而让其他线程就机会获得对象的锁。无限制等待,直到notify()为止。

wait(long n)有时限的等待,到n毫秒后结束等待,或是被notify。

Wait notify的正确姿势

Sleep(long n)和wait(long n)的区别
1、sleep是Thread方法,而wait是Object的方法。
2、sleep不需要强制和synchronized配合使用,但wait需要和synchronized一起使用。
3、sleep在睡眠的同时,不会释放对象锁的,但wait在等待的时候会释放对象锁。
4、他们的状态都是TIME_WAITING。

synchronized (lock) {
   while (条件不成立) {
        lock.wait();
    }
    // 干活
}

// 另一个线程
synchronized (lock) {
    lock.notifyAll();
}

模式

同步模式之保护性暂停

定义

即Guarded Suspension,用在一个线程等待另一个线程的执行结果。

要点:
1、有一个结果需要从一个线程传递到另一个线程,让他们关联同一个GuardedObject。
2、如果结果不断从一个线程到另一个线程那么可以使用消息队列(见生产者/消费者)。
3、JDK中,join的实现、Future的实现,采用的就是此模式。
4、因为要等待另一方的结果,因此归类到同步模式。

在这里插入图片描述

代码示例
package com.xz;

import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.net.HttpURLConnection;
import java.net.URL;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.List;

public class GuardedObjectTest {

    public static void main(String[] args) {
        GuardedObject guardedObject = new GuardedObject();
        new Thread(() -> {
            System.out.println("等待结果");
            List<String> list = (List<String>) guardedObject.get(10000);
            if(list!=null){
                System.out.println("结果大小:"+list.size());
            }else{
                System.out.println("超时结束");
            }
        },"t1").start();

        new Thread(() -> {
            try {
                System.out.println("开始下载");
                guardedObject.complete(download());
            } catch (IOException e) {
                throw new RuntimeException(e);
            }
        },"t2").start();
    }

    // 模拟下载
    public static List<String> download() throws IOException {
        HttpURLConnection conn = (HttpURLConnection) new URL("https://www.baidu.com/").openConnection();
        List<String> lines = new ArrayList<>();
        BufferedReader reader = new BufferedReader(new InputStreamReader(conn.getInputStream(), StandardCharsets.UTF_8));
        String line;
        while ((line = reader.readLine()) != null) {
            lines.add(line);
        }
        return lines;
    }
}

class GuardedObject {
    // 结果
    private Object response;

    /**
     * 获取结果
     * @param timeout 最大等待时间:毫秒
     * @return
     */
    public Object get(long timeout){
        synchronized (this) {
            // 开始时间
            long begin = System.currentTimeMillis();
            // 经历时间
            long passedTime = 0;
            while (response == null) {
                long waitTime = timeout - passedTime;// 线程最大等待时间,防止虚假唤醒,不能每次都等待相同的时间
                // 经历时间超过了最大等待时间,退出循环
                if(passedTime >= timeout){
                    break;
                }
                try {
                    this.wait(waitTime);
                } catch (InterruptedException e) {
                    throw new RuntimeException(e);
                }
                // 求得经历时间
                passedTime = System.currentTimeMillis() - begin;
            }
            return response;
        }
    }

    /**
     * 赋值
     * @param response
     */
    public void complete(Object response){
        synchronized (this) {
            // 给结果成员变量赋值
            this.response = response;
            this.notifyAll();
        }
    }
}

执行结果

等待结果
开始下载
结果大小:3

异步模式之生产者/消费者

1、与前面的保护性暂停中的GuardObject不同,不需要产生结果和消费结果的线程 一 一 对应。
2、消费队列可以用来平衡生产和消费的线程资源。
3、生产者仅负责结果数据,不关心数据该如何处理,而消费者专心处理结果数据。
4、消息队列是有容量限制的,满时不会再加入数据,空时不会再消耗数据。
5、JDK中各种阻塞队列,采用的就是这种模式。
在这里插入图片描述

代码示例
package com.xz;

import java.util.LinkedList;

public class MessageTestMQ {

    public static void main(String[] args) {
        MessageQueue messageQueue = new MessageQueue(2);
        for(int i=0; i<3; i++){
            int id = i;
            new Thread(() -> {
                messageQueue.put(new Message(id,"值"+id));
            },"生产者").start();
        }

        new Thread(() -> {
            while (true) {
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    throw new RuntimeException(e);
                }
                messageQueue.take();
            }
        },"消费者").start();
    }
}

// 消息队列类,java线程之间通信
class MessageQueue {

    // 消息的队列集合
    private LinkedList<Message> list = new LinkedList<>();

    // 队列容量
    private int capcity;

    public MessageQueue(int capcity) {
        this.capcity = capcity;
    }

    // 获取消息
    public Message take(){
        // 检查队列是否为空
        synchronized (list) {
            while (list.isEmpty()) {
                try {
                    System.out.println("队列为空,消费者线程等待");
                    list.wait();
                } catch (InterruptedException e) {
                    throw new RuntimeException(e);
                }
            }
            // 从队列的头部获取消息并返回
            Message message = list.removeFirst();
            System.out.println("已消费消息:"+message.toString());
            list.notifyAll();
            return message;
        }
    }

    // 存入消息
    public void put(Message message){
        synchronized (list) {
            // 检查队列是否已满
            while (list.size() == capcity) {
                try {
                    System.out.println("队列已满,生产者线程等待");
                    list.wait();
                } catch (InterruptedException e) {
                    throw new RuntimeException(e);
                }
            }
            // 将消息加入队列尾部
            list.addLast(message);
            System.out.println("已生产消息:"+message.toString());
            list.notifyAll();
        }
    }
}

// 声明final,不能有子类,不会存在被子类覆盖父类的方法,线程安全更加稳固
final class Message {
    private int id;
    private Object value;

    // 仅创建构造器,不创建set方法,仅创建的时候设置值,整个类不可变,线程安全
    public Message(int id, Object value) {
        this.id = id;
        this.value = value;
    }

    public int getId() {
        return id;
    }

    public Object getValue() {
        return value;
    }

    @Override
    public String toString() {
        return "Message{" +
                "id=" + id +
                ", value=" + value +
                '}';
    }
}

执行结果

已生产消息:Message{id=1, value=1}
已生产消息:Message{id=0, value=0}
队列已满,生产者线程等待
已消费消息:Message{id=1, value=1}
已生产消息:Message{id=2, value=2}
已消费消息:Message{id=0, value=0}
已消费消息:Message{id=2, value=2}
队列为空,消费者线程等待

Park & Unpark

基本使用

它们是LockSupport类中的方法

// 暂停当前线程
LockSupport.park();
// 恢复某个线程的运行
LockSupport.unpark(暂停线程对象);

先park再unpark,如果出现先unpark再park就会导致失效,从而径直向下执行。

package com.xz;

import java.util.concurrent.locks.LockSupport;

public class TestParkUnPark {

    public static void main(String[] args) throws InterruptedException {
        Thread t1 = new Thread(() -> {
            System.out.println("start...");
            try {
                Thread.sleep(2000);
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            }
            System.out.println("park...");
            LockSupport.park();
            System.out.println("resume...");
        });
        t1.start();

        Thread.sleep(1000);
        System.out.println("unpark...");
        LockSupport.unpark(t1);
    }
}

执行结果

start...
unpark...
park...
resume...

特点

与Object的wait & notify相比

wait,notify和notifyAll必须配合Object Monitor一起使用,而unpark不必。

park & unpark是以线程为单位来【阻塞】和【唤醒】线程,而notify只能随机唤醒一个等待线程,notifyAll是唤醒所有等待线程,就不那么【精确】。

park & unpark可以先unpark,而wait & notify不能先notify。

原理

每个线程都有自己的一个Parker对象,由三部分组成_counter,_cond和_mutex。
在这里插入图片描述
1、当前线程调用Unsafe.park()方法。

2、检查_counter,本情况为0,这时,获得_mutex互斥锁。

3、线程进入_cond条件变量阻塞。

4、设置_counter = 0。
在这里插入图片描述
1、调用Unsafe.unpark(Thread_0)方法,设置_counter为1。

2、唤醒_cond条件变量中的Thread_0.

3、Thread_0恢复运行。

4、设置_counter为0。

线程状态转换

在这里插入图片描述

情况1 NEW --> RUNNABLE

当调用t.start()方法时,由NEW --> RUNNABLE

情况2 RUNNABLE <–> WAITING

t线程用synchronized(obj)获取了对象锁后

调用obj.wait()方法时,t线程从RUNNABLE --> WAITING

调用obj.notify(),obj.notifyAll(),t.interrupt()时:
1、竞争锁成功,t线程从WAITING --> RUNNABLE。
2、竞争锁失败,t线程从WAITING --> BLOCKED

情况3 RUNNABLE < – > WAITING

当前线程调用t.join()方法时,当前线程从RUNNABLE – > WAITING,注意是当前线程t线程对象的监视器上等待。

t线程运行结束,或调用了当前线程的interrupt()时,当前线程从WAITING – > RUNNABLE。

情况4 RUNNABLE < – > WAITING

当前线程调用LockSupport.park()方法会让当前线程从RUNNABLE --> WAITING。

调用LockSupport.unpark(目标线程)或调用线程的interrupt()会让目标线程从WAITING – > RUNNABLE。

情况5 RUNNABLE < – > TIMED_WAITING

t线程用synchronized(obj)获取了对象锁后

调用obj.wait(long n)方法时,t线程从RUNNABLE – > TIMED_WAITING
t线程等待时间超过了n毫秒,或调用obj.notify(),obj.notifyAll(),t.interrupt()时:
1、竞争锁成功,t线程从TIMED_WAITING – > RUNNABLE。
2、竞争锁失败,t线程从TIMED_WAITING – > BLOCKED。

情况6 RUNNABLE < – > TIMED_WAITING

当前线程调用t.join(long n)方法时,当前线程从RUNNABLE – TIMED_WAITING,注意当前线程t线程对象的监视器上等待。

当前线程等待超过了n毫秒,或t线程运行结束,或调用了当前线程的interrupt时,当前线程从TIMED_WATING – > RUNNABLE。

情况7 RUNNABLE < – > TIMED_WAITING

当前线程调用Thread.sleep(long n),当前线程从RUNNABLE - > TIMED_WAITING。

当前线程等待时间超过了n毫秒,当前线程从TIMED_WAITING – > RUNNABLE。

情况8 RUNNABLE < – > TIMED_WAITING

当前线程调用LockSupport.parkNanos(long nanos)或LockSupport.parkUntil(long millis)时,当前线程从RUNNABLE – > TIMED_WAITING。

调用LockSupport.unpark(目标线程)或调用了线程的interrupt(),或是等待超时,会让目标线程从TIMED_WAITING --> RUNNABLE。

情况9 RUNNALE < – > BLOCKED

t线程用synchronized(obj)获取了对象锁时如果竞争失败,从RUNNABLE – > BLOCKED。

持obj锁线程的同步代码块执行完毕,会唤醒该对象上所有BLOCKED的线程重新竞争,如果其中**
t线程**竞争成功,从BLOCKED – > RUNNABLE,其他失败的线程仍然BLOCKED。

情况10 RUNNABLE < – > TERMINATED

当前线程所有代码运行完毕,进入TERMINATED。

活跃性

死锁

有这样的情况:一个线程需要同时获取多把锁,这时就容易发生死锁。

t1线程获得A对象锁,接下来想获取B对象的锁
t2线程获得B对象锁,接下来想获取A对象的锁

例如:

package com.xz;

public class TestDeadLock {

    public static void main(String[] args) {
        test1();
    }

    private static void test1(){
        Object A = new Object();
        Object B = new Object();
        Thread t1 = new Thread(()->{
            synchronized (A) {
                System.out.println("lock A");
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    throw new RuntimeException(e);
                }
                synchronized (B) {
                    System.out.println("lock B");
                    System.out.println("操作...");
                }
            }
        },"t1");

        Thread t2 = new Thread(()->{
            synchronized (B) {
                System.out.println("lock B");
                try {
                    Thread.sleep(500);
                } catch (InterruptedException e) {
                    throw new RuntimeException(e);
                }
                synchronized (A) {
                    System.out.println("lock A");
                    System.out.println("操作...");
                }
            }
        },"t2");
        t1.start();
        t2.start();
    }
}

定位死锁

检测死锁可以使用jconsole工具

启动TestDeadLock死锁进程,用jconsole追踪TestDeadLock进程
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

或者使用jps定位进程id,再用jstack定位死锁
在这里插入图片描述
在这里插入图片描述

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/1462625.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

【成都游戏业:千游研发之都的发展与机遇】

成都游戏业&#xff1a; 千游研发之都的发展与机遇 作为我国西部游戏产业的龙头&#xff0c;成都这座城市正在高速发展&#xff0c;目标是崛起成为千亿级游戏研发之都。多年来&#xff0c;在政策扶持、人才汇聚以及文化底蕴等助力下&#xff0c;成都游戏业已经形成完整的产业链…

SAP MIGO发货过账的时候批次库存确定:事务码MBC1进行激活即可

事务码&#xff1a; MBC1 ~ MBC3 使用MBC1按照工厂层级进行激活 接下来MIGO发货过账的时候就可以使用批次库存确定了&#xff0c;点击下图中圈出来的库存确定按钮

Spring AOP 实现原理详解之 JDK 动态代理

目录 一. 前言 二. JDK 代理的示例 2.1. 不需要 Maven 依赖 2.2. 定义实体 2.3. 被代理的类和接口 2.4. JDK 代理类 2.5. 使用代理 三. JDK 代理的流程 3.1. ProxyGenerator 生成代码 3.2. 从生成的 Proxy 代码看执行流程 四. Spring AOP 中 JDK代理的实现 4.1. Sp…

【虚拟仿真】Unity3D中实现3DUI,并且实现Button、InputField、Toggle等事件绑定

推荐阅读 CSDN主页GitHub开源地址Unity3D插件分享简书地址大家好,我是佛系工程师☆恬静的小魔龙☆,不定时更新Unity开发技巧,觉得有用记得一键三连哦。 一、前言 最近在项目中需要用到3DUI的展示,之前一般会用TextMeshPro进行展示: 但是,后面又需要添加按钮、Toggle等…

[开源协议] 什么是MIT协议及其使用场景

什么是MIT协议? MIT协议是一种开放源代码软件授权协议&#xff0c;全称为Massachusetts Institute of Technology License。该协议允许自由地使用、复制、修改、合并、发布、分发、再授权和销售软件及其副本的任何部分。MIT协议要求在软件的所有副本中包含版权声明和许可声明…

Web基础②nginx搭建与配置

目录 一.Nginx概述 1.定义 2.Nginx模块作用 &#xff08;1&#xff09;main模块 &#xff08;2&#xff09;stream服务模块 &#xff08;3&#xff09;邮件服务模块 &#xff08;4&#xff09;第三方模块 &#xff08;5&#xff09;events模块 &#xff08;6&#xff…

WordPress如何将后台右上角管理员头像去除并调整注销位置及启用注销确认功能?

WordPress后台默认情况下右上角可以看到管理员昵称和头像&#xff0c;将鼠标移动到该昵称上还会出现一个下拉菜单&#xff0c;点击下拉菜单中的“注销”无需我们再次确认就会自动退出。 现在我想将WordPress后台右上角的管理员头像和管理员昵称子菜单去除&#xff0c;并将“注销…

探索亚马逊自养号测评的实际效果与使用感受

自养号在亚马逊测评中的应用给了我们一种全新的体验。通过使用亚马逊自养号&#xff0c;我们发现了许多令人满意的优势&#xff0c;这些优势不仅提升了我们的测评效率&#xff0c;还增加了我们的信誉度。 首先&#xff0c;自养号的质量可控性给了我们极大的信心。我们可以自行…

基于springboot+vue的高校学科竞赛系统(前后端分离)

博主主页&#xff1a;猫头鹰源码 博主简介&#xff1a;Java领域优质创作者、CSDN博客专家、阿里云专家博主、公司架构师、全网粉丝5万、专注Java技术领域和毕业设计项目实战&#xff0c;欢迎高校老师\讲师\同行交流合作 ​主要内容&#xff1a;毕业设计(Javaweb项目|小程序|Pyt…

Redis之缓存穿透问题解决方案实践SpringBoot3+Docker

文章目录 一、介绍二、方案介绍三、Redis Docker部署四、SpringBoot3 Base代码1. 依赖配置2. 基本代码 五、缓存优化代码1. 校验机制2. 布隆过滤器3. 逻辑优化 一、介绍 当一种请求&#xff0c;总是能越过缓存&#xff0c;调用数据库&#xff0c;就是缓存穿透。 比如当请求一…

利用LaTex批量将eps转pdf、png转eps、eps转png、eps转svg

1、eps转pdf 直接使用epstopdf命令&#xff08;texlive、mitex自带&#xff09;。 在cmd中进入到eps矢量图片的目录&#xff0c;使用下面的命令&#xff1a; for %f in (*.eps) do epstopdf "%f" 下面是plt保存eps代码&#xff1a; import matplotlib.pyplot as…

挑战6万月薪【三】Purple Pi OH开发板带你7天入门OpenHarmony!

现在为止&#xff0c;我们已经完成了Purple Pi OH主板的串口调试和部分配件的连接&#xff0c;接下来&#xff0c;让我们趁热打铁&#xff0c;完成剩余配件的连接&#xff01; 注&#xff1a;配件连接前请断开主板所有供电&#xff0c;避免敏感电路损坏&#xff01; 一. 接口…

计算机网络-局域网

文章目录 局域网局域网拓扑结构以太网以太网传输介质以太网时隙提高传统以太网带宽的途径以太网帧格式 局域网协议IEEE 802参考模型IEEE802.2协议LLC帧格式及其控制字段LLC提供的三种服务 IEEE 802.3协议IEEE 802.4协议IEEE 802.5协议 高速局域网100M以太网千兆以太网万兆以太网…

ubuntu22.04@Jetson Orin Nano之CSI IMX219安装

ubuntu22.04Jetson Orin Nano之CSI IMX219安装 1. 源由2. 安装2.1 硬件安装2.2 软件配置2.3 新增摄像头 3. 效果4. 参考资料 1. 源由 折腾半天时间&#xff0c;捣鼓这个套装摄像头(IMX219)的安装&#xff0c;死活就是没有这个设备。世界总是这么小&#xff0c;看看遇到问题的大…

吴恩达机器学习全课程笔记第三篇

目录 前言 P42-P48 神经元和大脑 神经网络中的层 更复杂的神经网络 前向传播&#xff08;做出预测&#xff09; P49-P53 代码中的推理 构建一个神经网络 P54-P60 矩阵乘法 TensorFlow框架实现神经网络 前言 这是吴恩达机器学习笔记的第三篇&#xff0c;第二篇笔记…

C#知识点-16(计算器插件开发、事件、递归、XML)

计算器插件开发 1、Calculator.cs using System; using System.Collections.Generic; using System.Linq; using System.Text; using System.Threading.Tasks;namespace Calculator_DLL {//用来明确所有插件开发人员的开发规范public abstract class Calculator{public int N…

2024作品集设计指南:3个你需要知道的趋势!

如果你想在面试中获得额外的分数&#xff0c;你的首要任务是仔细准备一个个人作品集。作品集是展示设计师个人能力的载体。一个优秀的作品集不仅可以向面试官展示你的设计技巧&#xff0c;还可以通过将作品集与设计趋势“融合”来体现你对市场的关注。“设计技能市场思维”的作…

Linux环境下查看磁盘层级占用空间的解决方案

大家好,我是爱编程的喵喵。双985硕士毕业,现担任全栈工程师一职,热衷于将数据思维应用到工作与生活中。从事机器学习以及相关的前后端开发工作。曾在阿里云、科大讯飞、CCF等比赛获得多次Top名次。现为CSDN博客专家、人工智能领域优质创作者。喜欢通过博客创作的方式对所学的…

Java基于SpringBoot的社区医院信息管理系统

博主介绍&#xff1a;✌程序员徐师兄、7年大厂程序员经历。全网粉丝12w、csdn博客专家、掘金/华为云/阿里云/InfoQ等平台优质作者、专注于Java技术领域和毕业项目实战✌ &#x1f345;文末获取源码联系&#x1f345; &#x1f447;&#x1f3fb; 精彩专栏推荐订阅&#x1f447;…

iMazing3终极iPhone数据设备管理软件

iMazing是一款功能丰富的iOS设备管理软件&#xff0c;具备多种实用功能&#xff0c;以下是它的主要功能的详细介绍&#xff1a; iMazing3Mac-最新绿色安装包下载如下&#xff1a; https://wm.makeding.com/iclk/?zoneid49816 iMazing3Win-最新绿色安装包下载如下&#xff1…