Java_22_多线程02

news2024/12/23 13:22:42

多线程

线程通信

  1. 线程通信:多个线程因为在同一个进程中,所以互相通信比较容易的。

  2. 线程通信的经典模型:生产者与消费者问题。
    生产者负责生成商品,消费者负责消费商品。
    生产不能过剩,消费不能没有。(即时生产,即时消费)

  3. 模拟一个案例:
    小明和小红有一个共同账户:共享资源
    他们有3个爸爸(亲爸,岳父,干爹)给他们存钱。

  4. 模型:小明和小红去取钱,如果有钱就取出,然后等待自己,唤醒他们3个爸爸们来存钱
    他们的爸爸们来存钱,如果发现有钱就不存,没钱就存钱,然后等待自己,唤醒孩子们来取钱。
    做整存整取:10000元。

  5. 分析:
    生产者线程:亲爸,岳父,干爹
    消费者线程:小明,小红
    共享资源:账户对象。

  6. 注意:线程通信一定是多个线程在操作同一个资源才需要进行通信。
    线程通信必须先保证线程安全,否则毫无意义,代码也会报错!

  7. 线程通信的核心方法:
    public void wait(): 让当前线程进入到等待状态 此方法必须锁对象调用.
    public void notify() : 唤醒当前锁对象上等待状态的某个线程 此方法必须锁对象调用
    public void notifyAll() : 唤醒当前锁对象上等待状态的全部线程 此方法必须锁对象调用
    小结:
    是一种等待唤醒机制。
    必须是在同一个共享资源才需要通信,而且必须保证线程安全

主函数

public class ThreadConnection {
    public static void main(String[] args){
        //1.创建共享账户
        Account acc = new Account();
        //2.创建线程对象
        Runnable Little_Ming = new DrawThread(acc,0);
        new Thread(Little_Ming,"Little_Ming").start();
        Runnable Little_Hong = new DrawThread(acc,0);
        new Thread(Little_Hong,"Little_Hong").start();

        Runnable TrueDad = new SaveThread(acc);
        new Thread(TrueDad,"True_Dad").start();
        Runnable Gandad = new SaveThread(acc);
        new Thread(TrueDad,"Gan_Dad").start();
        Runnable GrandDad = new SaveThread(acc);
        new Thread(TrueDad,"Grand_Dad").start();
    }
}
/**
 * 取钱线程类
 */
public class DrawThread implements Runnable{
    private Account acc;
    private double money;
    public DrawThread(Account acc,double money){
        this.acc = acc;
        this.money = money;
    }
    @Override
    public void run() {
        //小明 小红取钱
        while (true) {
            try {
                Thread.sleep(3000);
            } catch (Exception e) {
                e.printStackTrace();
            }
            acc.drawMoney(10000);
        }
    }
}

/**
 * 存钱的线程类
 */
public class SaveThread implements Runnable{
    private Account acc ; // 定义了一个账户类型的成员变量接收取款的账户对象!
    public SaveThread(Account acc){
        this.acc = acc ;
    }
    @Override
    public void run() {
        while(true){
            try {
                Thread.sleep(4000);
                acc.saveMoney(10000);
            } catch (Exception e) {
                e.printStackTrace();
            }
        }
    }
}

//账户类
public class Account {
    private String cardID;
    private double money;

    public Account() {
    }

    public synchronized void drawMoney(double money) {
        try {
            //谁取
            String name = Thread.currentThread().getName();
            //余额是否足够
            if(this.money >= money){
                //2.开始拿钱
                this.money -= money;
                System.out.println(name + "取走了" + money + ",剩余" + this.money);
                //3.取钱后没钱,等待自己唤醒别人
                this.notifyAll();//唤醒别人
                this.wait(); //自己等待
            }
        }catch (Exception e){
            e.printStackTrace();
        }
    }
    public synchronized void saveMoney(double money) {
        try {
            //谁来存钱
            String name = Thread.currentThread().getName();
            //1.判断余额
            if(this.money <= 0){
                //没钱
                this.money += money;
                System.out.println(name + "来存了" + money);
            }
            //存完钱或者有钱,唤醒别人等待自己
            this.notifyAll();
            this.wait();
        }catch (Exception e){
            e.printStackTrace();
        }
    }
}

利用this.notifyAll();this.wait();轮流唤醒其他用户操作,达到线程通信的目的!!

线程状态

在这里插入图片描述

在这里插入图片描述

线性池

  1. 线程池:其实就是一个容纳多个线程的容器,其中的线程可以反复的使用,
    省去了频繁创建和销毁线程对象的操作,无需反复创建线程而消耗过多资源。

  2. 为什么要用线程池:
    合理利用线程池能够带来三个好处

    1. 降低资源消耗。
      – 减少了创建和销毁线程的次数,每个工作线程都可以被重复利用,可执行多个任务。

    2. 提高响应速度
      – 不需要频繁的创建线程,如果有线程可以直接用,不会出现系统僵死!

    3. 提高线程的可管理性(线程池可以约束系统最多只能有多少个线程,不会因为线程过多而死机)

  3. 线程池的核心思想:线程复用,同一个线程可以被重复使用,来处理多个任务。

在这里插入图片描述

线程池的创建

  1. 线程池在Java中的代表类:ExecutorService(接口)。

  2. Java在Executors类下提供了一个静态方法得到一个线程池的对象:

    1. public static ExecutorService newFixedThreadPool(int nThreads): 创建一个线程池返回。

    2. ExecutorService提交线程任务对象执行的方法:

      1. Future<?> submit(Runnable task):提交一个Runnable的任务对象给线程池执行。
  3. 小结:
    pools.shutdown(); 等待任务执行完毕以后才会关闭线程池
    pools.shutdownNow(); 立即关闭线程池的代码,无论任务是否执行完毕!
    线程池中的线程可以被复用,线程用完以后可以继续去执行其他任务。

Runnable接口做线程池

public class ThreadPoolsDemo01 {
    public static void main(String[] args){
        //1.创建线程池,指定线程数量固定为3
        ExecutorService pools = Executors.newFixedThreadPool(3);
        //添加线程任务让线程处理
        Runnable tar = new MyRunnable();

        pools.submit(tar);//第一次提交任务,此时线程池创建新线程
        pools.submit(tar);//第二次提交任务,此时线程池创建新线程
        pools.submit(tar);//第三次提交任务,此时线程池创建新线程
        pools.submit(tar);//第四次提交任务,复用之前的线程

        pools.shutdown(); //等待任务执行完毕后关闭线程池
        //pools.shutdownNow(); //立即关闭线程池代码,无论任务是否执行完毕!
    }
}
class MyRunnable implements Runnable{
    @Override
    public void run(){
        for(int i = 0;i < 5;i ++) {
            System.out.println(Thread.currentThread().getName() + "==>" + i);
        }
    }
}

Callable接口做线程池

  1. 线程池在Java中的代表类:ExecutorService(接口)。

  2. Java在Executors类下提供了一个静态方法得到一个线程池的对象:
    1.public static ExecutorService newFixedThreadPool(int nThreads):创建一个线程池返回。

  3. ExecutorService提交线程任务对象执行的方法:
    1.Future<?> submit(Runnable task):提交一个Runnable的任务对象给线程池执行。 1.Future<?> submit(Callable task):提交一个Runnable的任务对象给线程池执行。

  4. 小结:
    Callable做线程池的任务,可以得到它执行的结果!!

public class ThreadPoolsDemo02 {
    public static void main(String[] args) throws ExecutionException, InterruptedException {
        ExecutorService pools = Executors.newFixedThreadPool(3);

        //2.提交Callable的任务对象后返回一个未来任务对象!
        Future<String> t1 = pools.submit(new MyCollable(100));
        Future<String> t2 = pools.submit(new MyCollable(200));
        Future<String> t3 = pools.submit(new MyCollable(300));
        Future<String> t4 = pools.submit(new MyCollable(400));

        //3.获取线程池执行的任务的结果
        try{
            String rs1 = t1.get();
            String rs2 = t2.get();
            String rs3 = t3.get();
            String rs4 = t4.get();
            
            System.out.println(rs1);
            System.out.println(rs2);
            System.out.println(rs3);
            System.out.println(rs4);
        }catch (Exception e){
            e.printStackTrace();
        }


    }
}
class MyCollable implements Callable<String>{
    //需求:使用线程池,计算1-100,1-200,1-300的和返回
    private int n;
    public MyCollable(int n){
        this.n = n;
    }
    @Override
    public String call() throws Exception{
        int sum = 0;
        for(int i = 1;i <= n;i ++) sum += i;
        return Thread.currentThread().getName() + "执行的结果为" + sum;
    }
}

死锁

  1. 死锁是这样一种情形:多个线程同时被阻塞,它们中的一个或者全部都在等待某个资源被释放。
    由于线程被无限期地阻塞,因此程序不可能正常终止。

  2. 客户(占用资金,等待经销商的货品资源) 经销商(占用货品资源,等待客户的资金)

  3. java 死锁产生的四个必要条件:

    1. 互斥使用,即当资源被一个线程使用(占有)时,别的线程不能使用。
    2. 不可抢占,资源请求者不能强制从资源占有者手中夺取资源,资源只能由资源占有者主动释放。
    3. 请求和保持,即当资源请求者在请求其他的资源的同时保持对原有资源的占有。
    4. 循环等待,即存在一个等待循环队列:p1要p2的资源,p2要p1的资源。这样就形成了一个等待环路

当上述四个条件都成立的时候,便形成死锁。当然,死锁的情况下如果打破上述任何一个条件,
便可让死锁消失

  1. 小结:
    死锁是多个线程满足上述四个条件才会形成,死锁需要尽量避免。
    死锁一般存在资源的嵌套请求
public class ThreadDeadDemo01 {
    //定义资源对象
    public static Object resources01 = new Object();
    public static Object resources02 = new Object();
    public static void main(String[] args){
        //死锁至少两个线程
        new Thread(new Runnable() {
            @Override
            public void run() {
                synchronized (resources01){
                    System.out.println("线程1占用资源1,请求资源2");
                    try{
                        Thread.sleep(1000);
                    }catch (Exception e){
                        e.printStackTrace();
                    }
                    synchronized (resources02){
                        System.out.println("线程1成功占用资源2");
                    }
                }
            }
        }).start();
        new Thread(new Runnable() {
            @Override
            public void run() {
                synchronized (resources02){
                    System.out.println("线程2占用资源2,请求资源1");
                    try{
                        Thread.sleep(1000);
                    }catch (Exception e){
                        e.printStackTrace();
                    }
                    synchronized (resources01){
                        System.out.println("线程2成功占用资源1");
                    }
                }
            }
        }).start();
    }
}

volatile关键字

解决:并发编程下变量不可见性问题

  1. 引入:

    1. 问题:线程修改了某个成员变量的值,但是在主线程中读取到的还是之前的值
      修改后的值无法读取到。
    2. 原因:按照JMM模型,所有的成员变量和静态变量都存在于主内存中,主内存中的变量可以被多个线程共享。
      每个线程都存在一个专属于自己的工作内存,工作内存一开始存储的是成员变量的副本。
      所以线程很多时候都是直接访问自己工作内存中的该变量,其他线程对主内存变量值的修改将不可见!!
  2. 解决此问题:
    希望所有线程对于主内存的成员变量修改,其他线程是可见的。

    1. 加锁:可以实现其他线程对变量修改的可见性。
      某一个线程进入synchronized代码块前后,执行过程入如下:

      1. 线程获得锁
      2. 清空工作内存
      3. 从主内存拷贝共享变量最新的值到工作内存成为副本
    2. 可以给成员变量加上一个volatile关键字,立即就实现了成员变量多线程修改的可见性。

  3. 小结:

    1. 可以给成员变量加上一个volatile关键字,当一个线程修改了这个成员变量的值,其他线程可以立即看到修改后的值并使用!
    2. volatile与synchronized的区别。
      - volatile只能修饰实例变量和静态变量,而synchronized可以修饰方法,以及代码块。
      - volatile保证数据的可见性,但是不保证原子性(多线程进行写操作,不保证线程安全);
      而synchronized是一种排他(互斥)的机制,可保证原子性(线程安全)
public class VolatileDemo01 extends Thread {
    private boolean flag = false;
    @Override
    public void run(){

        try {
            Thread.sleep(1000);
        } catch (Exception e) {
            e.printStackTrace();
        }
        //线程中修改变量
        flag = true;
        System.out.println("flag = " + flag);
    }
    public boolean isFlag() {
        return flag;
    }
    public void setFlag(boolean flag) {
        this.flag = flag;
    }
}
class VisibilityDemo{
    public static void main(String[] args){
        //1.启动子线程,修改flag的变量成true
        VolatileDemo01 var = new VolatileDemo01();
        var.start();
        //2.主线程
        while(true){
            if(var.isFlag()) System.out.println("主线程进入执行~~");
        }
    }
}

运行结果:

flag = true
子线程修改了值,主线程没用读到!!

在这里插入图片描述

不可见性解决方案

加锁

可以实现其他线程对变量修改的可见性。
某一个线程进入synchronized代码块前后,执行过程入如下:
1. 线程获得锁
2. 清空工作内存
3. 从主内存拷贝共享变量最新的值到工作内存成为副本

class VisibilityDemo01{
    public static void main(String[] args){
        //1.启动子线程,修改flag为true
        VolatileDemo02 var = new VolatileDemo02();
        var.start();
        //2.主线程
        while(true){
            //加锁会清空工作内存,读取主内存中的最新值到工作内存中来
            synchronized (VisibilityDemo01.class){
                if(var.isFlag()) System.out.println("主线程进入执行~~");
            }
        }
    }
}

Volatile关键字修饰

工作原理:一旦修改,主内存通知工作内存变量已修改,原值已失效!!再去主内存加载最新值。
在这里插入图片描述

private volatile boolean flag = false;

Volatile修饰变量的原子性研究

  1. 概述:所谓的原子性是指在一次操作或者多次操作中,所有的操作全部都得到了执行并且不会受到任何因素的干扰。最终结果要保证线程安全。

  2. 小结:在多线程环境下,volatile关键字可以保证共享数据的可见性,
    但是并不能保证对数据操作的原子性(在多线程环境下volatile修饰的变量也是线程不安全的)。

  3. volatile的使用场景

    1. 开关控制
      利用可见性特点,控制某一段代码执行或者关闭。
    2. 多个线程操作共享变量,但是是有一个线程对其进行写操作,其他的线程都是读。此时加上更好,其他线程可以立即读取到最新值。
      volatile不能保证变量操作的原子性(安全性)。
public class VoatileDemo03 {
    public static void main(String[] args){
        Runnable tar = new MyRunnable();
        for(int i = 1;i <= 100;i ++)
            //启动100次线程,执行100次任务
            new Thread(tar).start();
    }

}
class MyRunnable implements Runnable{
    private volatile int count = 0;
    @Override
    public void run(){
        for(int i = 1;i <=100;i ++) {
            count ++;
            System.out.println("Count ==>" + count);
        }
    }
}

运行结果:有时不准确

加锁实现线程安全

加锁机制性能很差

class MyRunnable01 implements Runnable{
    private int icount = 0;
    @Override
    public void run() {
        synchronized ("Safty") {
            for (int i = 1; i <= 100; i++) {
                icount++;
                System.out.println("iCount ==>" + icount);
            }
        }
    }
}

原子类保证原子性操作

  1. 如何保证变量访问的原子性呢?

    1. 加锁实现线程安全。
      – 虽然安全性得到了保证,但是性能不好!!
    2. 基于CAS方式的原子类。
      1. Java已经提供了一些本身即可实现原子性(线程安全)的类。
      2. 概述:java从JDK1.5开始提供了java.util.concurrent.atomic包(简称Atomic包),这个包中的原子操作类提供了一种用法简单 ,性能高效,线程安全地更新一个变量的方式。
      3. 操作整型的原子类
        public AtomicInteger(): 初始化一个默认值为0的原子型Integer
        public AtomicInteger(int initialValue): 初始化一个指定值的原子型Integer
        int get(): 获取值
        int getAndIncrement(): 以原子方式将当前值加1,注意,这里返回的是自增前的值。
        int incrementAndGet(): 以原子方式将当前值加1,注意,这里返回的是自增后的值。
        int addAndGet(int data): 以原子方式将输入的数值与实例中的值(AtomicInteger里的value)相加,并返回结果。
        int getAndSet(int value): 以原子方式设置为newValue的值,并返回旧值。
  2. CAS与Synchronized总结:

    1. CAS和Synchronized都可以保证多线程环境下共享数据的安全性。那么他们两者有什么区别?
      1. Synchronized是从悲观的角度出发:
        总是假设最坏的情况,每次去拿数据的时候都认为别人会修改,所以每次在拿数据的时候都会上锁,这样别人想拿这个数据就会阻塞直到它拿到锁
        共享资源每次只给一个线程使用,其它线程阻塞,用完后再把资源转让给其它线程)。因此Synchronized我们也将其称之为悲观锁。jdk中的ReentrantLock也是一种悲观锁。性能较差!!
      2. CAS是从乐观的角度出发:
        总是假设最好的情况,每次去拿数据的时候都认为别人不会修改,所以不会上锁,但是在更新的时候会判断一下在此期间别人有没有去更新这个数据。
        CAS这种机制我们也可以将其称之为乐观锁。综合性能较好!
public class VolatileDemo05 {
    public static void main(String[] args){
        Runnable var = new MyRunnable();
        for(int i = 1;i <= 100;i ++){
            new Thread(var).start();
        }
    }
}
class MyRunnable02 implements Runnable{
    //创建一个Integer更新的原子类AtomicInteger.初始值为0 取代int count
    private AtomicInteger atomicInteger = new AtomicInteger();
    @Override
    public void run() {
        for (int i = 1; i <= 100; i++) {
        //atomicInteger.incrementAndGet() 先加再取
            System.out.println("iCount ==>" + atomicInteger.incrementAndGet());
        }
    }
}
原子类CAS机制

CAS:Compare And Swap(比较再交换)
总是假设最好的情况,每次去拿数据的时候都认为别人不会修改,所以不会上锁,但是在更新的时候会判断一下在此期间别人有没有去更新这个数据。

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

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

相关文章

创建一个vite项目,一个命令创建

1. 在一个文件夹下打开cmd命令窗口并输入命令&#xff1a; npm init vuelatest //注意&#xff0c;此命令安装的是vue最新的依赖包&#xff0c;步骤也许跟以下有点点区别&#xff0c;不过问题不大 2. 接着询问你是否需要安装以下这些包&#xff0c;这些都是需要的&#xff0c…

华为OD机试真题 Java 实现【最多获得的短信条数】【2023Q1 100分】,附详细解题思路

目录 专栏导读一、题目描述二、输入描述三、输出描述四、解题思路五、Java算法源码六、效果展示 华为OD机试 2023B卷题库疯狂收录中&#xff0c;刷题点这里 专栏导读 本专栏收录于《华为OD机试&#xff08;JAVA&#xff09;真题&#xff08;A卷B卷&#xff09;》。 刷的越多&…

Pytorch深度学习------torchvision中dataset数据集的使用(CIFAR10)

文章目录 一、什么是TorchVision二、以torchvision.datasets子模块下的CIFAR10数据集为例1、CIFAR10数据集参数2、代码中使用 一、什么是TorchVision torchvision是pytorch的一个图形库&#xff0c;用来处理图像&#xff0c;主要用来构建计算机视觉模型。 从下面的官网截图可以…

力扣1114.按序打印-----题目解析

题目描述 解析&#xff1a; class Foo {public int a 0;public Foo() {}public void first(Runnable printFirst) throws InterruptedException {// printFirst.run() outputs "first". Do not change or remove this line.printFirst.run();a;}public void second…

【多任务编程-线程通信】

进程/线程通信的方式 某些应用程序中&#xff0c;进程/进程和线程/线程之间不可避免的进行通信&#xff0c;进行消息传递&#xff0c;数据共享等 同一进程的线程之间通信方式包括Windows中常用Event, Message等。 不同进程之间的通信可以利用Event, FileMapping(内存共享), W…

unity进阶--xml的使用学习笔记

文章目录 xml实例解析方法一解析方法二 xml-path创建xml文档 xml实例 解析方法一 解析方法二 xml-path 创建xml文档

测试用例实战

测试用例实战 三角形判断 三角形测试用例设计 测试用例编写 先做正向数据&#xff0c;再做反向数据。 只要有一条边长为0&#xff0c;那就是不符合要求&#xff0c;不需要再进行判断&#xff0c;重复。 四边形 四边形测试用例

Javascript程序异常处理

什么是异常&#xff0c;异常就是我们在编写Javascript程序时出现的一些错误&#xff0c;并会在控制台中抛出这个错误&#xff0c;出现异常其实并不是一件坏事&#xff0c;相对的呢它可以提醒我们开发人员哪里出现了错误&#xff0c;方便我们后续的修改&#xff0c;能让我们的代…

JRebel+XRebel热部署插件激活支持IDEA2023.1

JRebel是一款JVM插件&#xff0c;它使得Java代码修改后不用重启系统&#xff0c;立即生效。IDEA上原生是不支持热部署的&#xff0c;一般更新了 Java 文件后要手动重启 Tomcat 服务器&#xff0c;修改才能生效&#xff1b;所以推荐使用 JRebel 插件进行热部署。 在填入Team UR…

字符串类QString

字符串类QString 构造函数数据操作字符串查找和判断遍历查看字节数类型转换字符串格式 Qt中不仅支持C, C中的字符串类型, 而且还在框架中定义了专属的字符串类型。 Cchar*Cstd::string, char*QtQByteArray, QString QByteArray QString和QByteArray的函数很多都是相似的。。…

Feign远程调用如何携带form url

这是一个需要携带参数在form url上的请求&#xff0c;正常调用方式是这样的 响应&#xff1a; 在Feign中&#xff0c;应该怎么调用呢?? 定义OpenFeignClient接口 FeignClient(value "client-service", url "http://127.0.0.1/api") public interface…

Acwing.897 最长公共子序列(动态规划)

题目 给定两个长度分别为N和M的字符串A和B&#xff0c;求既是A的子序列又是B的子序列的字符串长度最长是多少。 输入格式 第一行包含两个整数N和M。 第二行包含一个长度为N的字符串&#xff0c;表示字符串A。 第三行包含一个长度为M的字符串&#xff0c;表示字符串B。字符串…

RunnerGo相比较JMeter有哪些优势

当谈到性能测试需求时&#xff0c;JMeter和RunnerGo都提供了丰富的功能&#xff0c;包括测试场景设置、执行性能测试和性能测试结果分析。然而&#xff0c;这两工具在结构方面存在一些区别。以下是对它们进行比较的另一种角度&#xff1a; 模块化设计&#xff1a; JMeter采用…

16K个大语言模型的进化树;81个在线可玩的AI游戏;AI提示工程的终极指南;音频Transformers课程 | ShowMeAI日报

&#x1f440;日报&周刊合集 | &#x1f3a1;生产力工具与行业应用大全 | &#x1f9e1; 点赞关注评论拜托啦&#xff01; &#x1f916; LLM 进化树升级版&#xff01;清晰展示 15821 个大语言模型的关系 这张进化图来自于论文 「On the Origin of LLMs: An Evolutionary …

基于压缩和差分算法的嵌入式平台远程更新设计与分析

传统的嵌入式远程更新方案普遍采用整包更新方式&#xff0c;这种方式更新数据量大&#xff0c;占用网络带宽时间长&#xff0c;同时也增加了设备的功耗。 针对这些问题&#xff0c;提出了以减少更新数据量为核心的两种远程更新方案。这两种方案分别使用LZ77压缩和BSDiff差分算…

【Linux】passwd

passwd 与 root 不同的是&#xff0c;一般帐号在更改密码时需要先输入自己的旧密码 &#xff08;亦即 current 那一行&#xff09;&#xff0c;然后再输入新密码 &#xff08;New 那一行&#xff09;。 要注意的是&#xff0c;密码的规范是非常严格的&#xff0c;尤其新的 dist…

《互联网引流:如何吸引更多用户并实现商业目标?》

互联网引流是当今数字时代中&#xff0c;企业和个人在互联网上推广自己产品、服务或内容的一种重要手段。随着互联网的普及和发展&#xff0c;越来越多的企业意识到了利用互联网引流来吸引潜在用户的重要性。 互联网引流的目标是通过各种策略和手段&#xff0c;将潜在用户转化…

Android 在程序运行时申请权限——以自动拨打电话为例

Android 6.0及以上系统在使用危险权限时必须进行运行时权限处理。 main_activity.xml <?xml version"1.0" encoding"utf-8"?> <LinearLayout xmlns:android"http://schemas.android.com/apk/res/android"xmlns:app"http://sche…

WordPress集成Argon主题[CentOS7]

下载 Argon主题 https://gitcode.net/mirrors/solstice23/argon-theme/-/releases/v1.3.5?spm1033.2243.3001.5876配置并安装Argon主题 mkdir -p /home/wwwroot/default/wordpress/wp-content/uploads/ cd /home/wwwroot/default/wordpress/wp-content/uploads/# 上传并解压…

echarts自定义tooltip,给tooltip增加百分号%

1.formatter为回调函数&#xff1a; 支持返回 HTML 字符串或者创建的 DOM 实例。 (params: Object|Array, ticket: string, callback: (ticket: string, html: string)) > string | HTMLElement | HTMLElement[] 在 trigger 为 ‘axis’ 的时候&#xff0c;或者 tooltip 被…