Java 多线程进阶

news2024/11/18 22:33:09

1 方法执行与进程执行

    @GetMapping("/demo1")
    public void demo1(){
        //方法调用
        new ThreadTest1("run1").run();
        //线程调用
        new ThreadTest1("run2").start();
    }

 下断点调试信息,可以看到run()方法当前线程是“main@1”

继续运行到run里面,当前线程仍然是“main@1” 

 接下来看一下start()方法运行的线程结果,可以看到是两个线程

 单线程的状态:

2 线程池

2.1 了解线程池

以线程的方式运行:

以线程池的方式运行,会发现线程池所需要的时间很短 

 1)Executor框架

Executor框架是 Java5 之后引进的,在 Java 5 之后,通过 Executor 来启动线程比使用 Thread 的 start 方法更好。Executor 框架不仅包括了线程池的管理,还提供了线程工厂、队列以及拒绝策略等,Executor 框架让并发编程变得更加简单。

 Executors如下:

Excuter主要有三大部分组成:

(1)任务:实现了Runnable接口或Callable接口的对象。Runnable任务不返回结果,而Callable任务可以返回结果。

(2)任务的执行(Excute,执行器):执行器是任务的执行引擎,它负责执行任务并管理任务的执行等。也就是把任务分派给多个线程的执行机制,包括Executor接口及继承自Executor接口的ExecutorService接口。

(3)异步计算的结果。包括Future接口及实现了Future接口的FutureTask类。

继续使用以下代码测试运行速度:

public class ThreadPools1 {

    public static void main(String args[]){
        ExecutorService executorService1 = Executors.newCachedThreadPool();

        ExecutorService executorService2 = Executors.newSingleThreadExecutor();

        ExecutorService executorService3 = Executors.newFixedThreadPool(10);

        for (int i=1; i <= 100; i++){
            executorService1.execute(new Mytask(i));
        }
    }
}

class Mytask implements Runnable{
    int i = 0;

    public Mytask(int i){
        this.i = i;
    }


    @Override
    public void run() {
        System.out.println(Thread.currentThread().getName() + "做第" + i + "个项目!");

        try{
            Thread.sleep(1000);
        }catch (Exception e){
            e.printStackTrace();
        }
    }
}

Executors.newCachedThreadPool() 运行结果

去掉Thread.sleep(1000)后的运行结果:

1)newCachedThreadPool的底层主要是调用了ThreadPoolExecutor,传入的参数如下: 

SynchronousQueue队列决定了每次只能一个进程完成任务后,在进行下一个

所以当执行任务时间很短时,会出现线程复用。

2)newSingleThreadExecutor传入的参数如下

3)newFixedThreadPool(10)传入的参数如下

底层都是ThreadPoolExecutor,所以接下来介绍一下ThreadPoolExecutor:

Java.util.concurrent.ThreadPoolExecutor 类就是 Java 对线程池的默认实现,开发中推荐使用ThreadPoolExecutor 而不是Excurors。

线程池的流程走向是受线程池的状态来决定的:

注意: ThreadPoolExecutor 就是通过将一个 32 位的 int 类型变量分割为两段,高 3 位用来表示线程池的当前生命周期状态,低 29 位就拿来表示线程池的当前线程数量,从而做到用一个变量值来维护两份数据,这个变量值就是 ctl。

  private final AtomicInteger ctl = new AtomicInteger(ctlOf(RUNNING, 0));  

  //通过按位取反 + 按位与运算,将 c 的高3位保留,舍弃低29位,从而得到线程池状态
  private static int runStateOf(int c)     { return c & ~CAPACITY; }
		
  //通过按位与运算,将 c 的高3位舍弃,保留低29位,从而得到工作线程数
  private static int workerCountOf(int c)  { return c & CAPACITY; }

ThreadPoolExecutor的参数很多,

    public ThreadPoolExecutor(

                              int corePoolSize,        //线程池核心线程大小
                              int maximumPoolSize, //线程池最大线程数量
                              long keepAliveTime, //空闲线程存活时间
                              TimeUnit unit, //空间线程存活时间单位
                              BlockingQueue<Runnable> workQueue, //工作队列
                              ThreadFactory threadFactory, //线程工厂
                              RejectedExecutionHandler handler //拒绝工厂

                               )

工作队列:

①ArrayBlockingQueue

        基于数组的有界阻塞队列,按FIFO排序。当线程池中线程数量达到corePoolSize后,再有新任务进来,则会将任务放入该队列的队尾,等待被调度。如果队列已经是满的,则创建一个新线程,如果线程数量已经达到maxPoolSize,则会执行拒绝策略。

②LinkedBlockingQuene

        基于链表的无界阻塞队列(其实最大容量为Interger.MAX),按照FIFO排序。、当线程池中线程数量达到corePoolSize后,再有新任务进来,会一直存入该队列,而不会去创建新线程直到maxPoolSize。

③SynchronousQuene

        一个不缓存任务的阻塞队列,生产者放入一个任务必须等到消费者取出这个任务。也就是说新任务进来时,不会缓存,而是直接被调度执行该任务,如果没有可用线程,则创建新线程,如果线程数量达到maxPoolSize,则执行拒绝策略。

④PriorityBlockingQueue

        具有优先级的无界阻塞队列,优先级通过参数Comparator实现。

2.2 推荐的使用方式

阿里不提倡使用Executors去创建线程池,推荐直接使用ThreadPoolExecutor去自定义线程,从而自动控制合理的参数。

1)执行优先级与提交执行优先级

执行优先级 : 核心线程>非核心线程>队列

提交优先级 : 核心线程>队列>非核心线程

  • 核心线程:线程池中固定含有的线程数目。也就是ThreadPoolExecutor方法中corePoolSize参数设置的数值。
  • 非核心线程:线程池中非固定含有的线程数目。如果ThreadPoolExecutor方法中maximumPoolSize参数的值大于corePoolSize则会有非核心线程出现,否则则没有。
  • 工作队列:用来保存等待任务的队列。主要是通过ThreadPoolExecutor方法中workQueue参数来配置,workQueue有好几种队列模式,像是同步队列,非同步队列等等,这个是通过传参类型来判定。

2)执行方法execute

 public void execute(Runnable command) {
        if (command == null)
            throw new NullPointerException();
        int c = ctl.get();
        if (workerCountOf(c) < corePoolSize) {
            if (addWorker(command, true))
                return;
            c = ctl.get();
        }
        if (isRunning(c) && workQueue.offer(command)) {
            int recheck = ctl.get();
            if (! isRunning(recheck) && remove(command))
                reject(command);
            else if (workerCountOf(recheck) == 0)
                addWorker(null, false);
        }
        else if (!addWorker(command, false))
            reject(command);
    }

如果核心线程10个,最大线程数20,等待队列10,在执行30个线程的时候。
会先执行任务1-10,然后执行任务21-30,最后执行任务11-20.
首先任务1-10会直接给核心线程执行,核心线程满了之后,任务会被放到等待队列;等待队列满了(11-20);就会把任务放到非核心线程处并创建非核心线程(21-30),这样放到非核心线程处的任务反而先执行了。如果连非核心线程处也满了,就执行拒绝策略。

 3)提交方法submit

4)拒绝策略

AbortPolicy

ThreadPoolExecutor 默认策略

直接抛出java.util.concurrent.RejectedExecutionException异常

DiscardPolicy放弃当前任务,并且不会抛出任何异常
DiscardOldestPolicy会将队列中最早添加的元素移除,再尝试添加,如果失败则按该策略不断重试
CallerRunsPolicy由调用线程(提交任务的线程)处理该任务,如果调用线程是主线程,那么主线程会调用执行器中的execute方法来执行改任务

3 CPU多核并发

多核并发缓存架构

3.1 JMM内存模型

(JAVA多线程内存模型)

主内存

所有线程创建的实例对象都存放在内存中,不管该实例对象是成员变量,还是局部变量,类信息、常量、静态变量都是放在主内存中,属于所有线程共享区域,所以存在线程之间安全问题。

工作内存

主要是存储局部变量(存储着主内存中变量的副本),每个线程只能在自己的工作内存中操作变量副本,对其他线程是不可见的。就算两个线程同时执行同一段代码,也是都在自己的工作内存中对变量进行操作。由于线程的工作内存是私有,所以线程之间是不可见的,同时也是线程安全。

通过一个demo进行研究:

运行结果:

通过内存模型可以知道,线程1不一定知道initFlag的值已经改变。

3.2 volatile关键字

当然,如果想要可以打印success,需要使用volatile关键字:

volatile的实现原理:

1)MESI缓存一致性协议:多个cpu从主内存读取同一个数据到各自的高速缓存,当其中某个cpu修改了缓存里的数据,该数据会马上同步回主内存,其它cpu通过总线嗅探机制可以感知到数据的变化从而将自己缓存里的数据失效。

2)Volatile缓存可见性实现原理底层实现主要是通过汇编lock前缀指令,它会锁定这块内存区域的缓存并回写到主内存,此操作被称为“缓存锁定”,MESI缓存一致性协议机制会阻止同时修改被两个以上处理器缓存的内存区域数据。一个处理器的缓存值通过总线回写到内存会导致其他处理器相应的缓存失效。 

3.3 原子操作

java内存模型定义了8种原子操作如下:

  • read (读取) : 从主内存读取数据
  • load(载入) :将主内存读取到的数据写入工作内存
  • use(使用) : 从工作内存读取数据来计算
  • assign (赋值) :将计算好的值重新赋值到工作内存中
  • store (存储) : 将工作内存数据写入主内存
  • write(写入) :将store过去的变量值赋值给主内存中的变量
  • lock(锁定) :将主内存变量加锁,标识为线程独占状态
  • unlock (解锁) :将主内存变量解锁,解锁后其他线程可以锁定该变量

并发编程三大特性:可见性,原子性,有序性。

Volatile保证可见性与有序性,但是不保证原子性;

synchronized 可以保证可见性,原子性,有序性。

最后运行结果 num < 10000

某一个CPU改变num值后,只有继续写入缓存,主内存,才会被其他CPU嗅探到。当多个CPU同时改变num值后,同时准备往主内存写,必然会出现先来后到问题,此时会产生影响。先写入主内存的被其他CPU嗅探后,会将其他CPU的值失效掉,其他CPU重新去读取主内存的值时,已经不是最开始的值了,而是被改过之后的值,从而数据不再准确。

synchronized 底层实际上通过JVM来实现的,同一时间只能有一个线程去执行synchronized 中的代码块,从而解决原子性问题。

3.4有序性与指令重排序

JMM内存模型具有先天的有序性,每个线程都有自己的工作内存。

通过一个代码示例来认识有序性:

1)为什么a b同时为1了?(指令重排序)

指令重排序:在不影响单线程程序执行结果的前提下,计算机为了最大限度的发挥机器
性能,会对机器指令重排序优化。

第一阶段,编译器重排序,就是在编译过程中,编译器根据上下文分析对指令进行重排序,目的是减少CPU和内存的交互,重排序之后尽可能保证CPU从寄存器或缓存行中读取数据。

第二阶段,处理器重排序,处理器重排序分为两个部分。

  • 并行指令集重排序,这是处理器优化的一种,处理器可以改变指令的执行顺序。
  • 内存系统重排序,这是处理器引入Store Buffer缓冲区延时写入产生的指令执行顺序不一致的问题。

补充:

        在处理器内核中一般会有多个执行单元,CPU在每个时钟周期内只能执行单条指令,也就是说只有一个执行单元在工作,其他执行单元处于空闲状态;在引入并行指令集之后,CPU在一个时钟周期内可以同时分配多条指令在不同的执行单元中执行。

        通过提前执行其他可执行指令来填补CPU的时间空隙,然后在结束时重新排序运算结果,从而实现指令顺序执行的运行结果。

2)重排序遵循as-if-serial和happens-before原则

as-if-serial原则:

不管怎么重排序(编译器和处理器为了提高并行度), (单线程)程序的执行结果不能被改变[编译器、runtime和处理器都必须遵守as-if-serial语义。
为了遵守as-if-serial语义,编译器和处理器不会对存在数据依赖关系的操作做重排席,因为这种重排席会改变执行结果。但是,如果操作之间不存在数据依赖关系,这些操作就可能被编译器和处理器重排序。

happens-before原则:
1.程序顺序原则: 即在一个线程内必须保证语义串行性,也就是说按照代码顺序执行。

2.锁规则: 解锁(unlock)操作必然发生在后续的同一个锁的加锁(lok)之前,也就是说,如果对于一个锁解锁后,再加锁,那么加锁的动作必须在解锁动作之后(同一个锁)。

3.volatile规则: volatile变量的写,先发生于读,这保证了volatile变量的可见性,简单的理解就是,volatile变量在每次被线程访问时,都强迫从主内存中读该变量的值,而当该变量发生变化时,又会强迫将最新的值刷新到主内存,任何时刻,不同的线程总是能够看到该变量的最新值。

4.线程启动规则: 线程的stat()方法先于它的每一个动作,即如果线程A在执行线程B的start方法之前修改了共享变量的值,那么当线程B执行start方法时,线程A对共享变量的修改对线程B可见。

5.传递性: A先于B,B先于C 那么A必然先于C。

6,线程终止规则: 线程的所有操作先于线程的终结,Thread.oin()方法的作用是等待当前执行的线程终上。假设在线程B终止之前,修改了共享变量,线程A从线程B的ioin方法成功返回后,线程B对共享变量的修改将对线程A可见。

3.5 内存屏障

内存屏障就是Java语言与CPU的约定代码标志, 告诉CPU看到这种代码不要重排序,且此处代码前后需要严格的数据一致性。

JVM规范定义的内存屏障:

规定volatile需要添加内存屏障,看一下底层源码实现:

3.7  双重检测锁DLC

    //单例模式   + 
    private volatile static LazySingletonDCL lazySingleton = null;

    public static LazySingletonDCL getLazySingleton() {
        // 第⼀重检查是否为 null
        if (lazySingleton == null) {
            // 使⽤ synchronized 加锁
            synchronized (LazySingletonDCL.class) {
                // 第二重检查是否为 null
                if (lazySingleton == null) {
                    lazySingleton = new LazySingletonDCL();
                }
            }
        }
        return lazySingleton;
    }

两次⾮空判断,锁指的是 synchronized 加锁,为什么要进⾏双重判断,其实很简单,第⼀重判断,如果实例已经存在,那么就不再需要进⾏同步操作,⽽是直接返回这个实例,如果没有创建,才会进⼊同步块,同步块的⽬的与之前相同,⽬的是为了防⽌有多个线程同时调⽤时,导致⽣成多个实例,有了同步块,每次只能有⼀个线程调⽤访问同步块内容,当第⼀个抢到锁的调⽤获取了实例之后,这个实例就会被创建,之后的所有调⽤都不会进⼊同步块,直接在第⼀重判断就返回单例。

4 JAVA锁机制

4.1 synchronized 同步锁

1)对象锁

非静态方法使用 synchronized 修饰的写法,修饰实例方法时,锁定的是当前对象:

    public synchronized void test(){
        // TODO
    }

    //等同于
    public void test(){
        synchronized (this) {
            // TODO
        }
    }

某个线程得到了对象锁之后,该对象的其他同步方法是锁定的,其他线程是无法访问的。

2)类锁

类锁需要 synchronized 来修饰静态 static 方法:

    public static synchronized void test(){
        // TODO
    }

    //等同于
    public static void test(){
        synchronized (TestSynchronized.class) {
            // TODO
        }
    }

类锁和对象锁其实是一样的,由于静态方法是类所有对象共用的,所以进行同步后,该静态方法的锁也是所有对象唯一的。每次只能有一个线程来访问对象的该非静态同步方法。

4.2 CAS(比较并交换)

volatile值有可见性和禁止指令重拍(有序性),无法保证原子性。jdk1.5后产生了CAS利用CPU原语(不可分割,连续不中断)保证现场操作原子性。

Java 提供常用的原子操作类有:

  • 基本类型:AtomicBoolean,AtomicInteger,AtomicLong;
  • 引用类型:AtomicReference,AtomicMarkableReference,AtomicStampedReference;
  • 数组类型:AtomicIntegerArray,AtomicLongArray,AtomicReferenceArray;
  • 字段类型:AtomicReferenceFieldUpdater,AtomicIntegerFieldUpdater,AtomicLongFieldUpdater。

AtomicInteger.incrementAndGet()就是CAS机制的实现,比较并交换,实现逻辑如下:

CAS的原理:CAS算法有三个操作数,通过内存中的值(V)、预期原始值(A)、修改后的新值。
(1)如果内存中的值和预期原始值相等, 就将修改后的新值保存到内存中。
(2)如果内存中的值和预期原始值不相等,说明共享数据已经被修改,放弃已经所做的操作,然后重新执行刚才的操作,直到重试成功。

对比两个锁:

(1)synchronized(悲观锁,影响其他线程)

(2)cas(乐观锁,不阻塞其他线程)

ABA问题就是进程1将A值改成B值,然后在进行比较并交换之前,进程2将A值改成C值又改回A值,那么进程1进行比较并交换是可以成功的,最终结果是正常的,但其实进程2也做操作了。

可以使用Version思维进行解决,每一次获取原来的值都附一个版本号进行标识。从Java1.5开始JDK的atomic包里提供了一个类AtomicStampedReference来解决ABA问题。这个类的compareAndSet方法作用是首先检查当前引用是否等于预期引用,并且当前标志是否等于预期标志,如 全部相等,则以原子方式将该引用和该标志的值设置为给定的更新值。

三个引用类型:

  • AtomicReference 不保证 ABA 问题;
  • AtomicMarkableReference 可以解决 ABA 问题,它会记录原数据是否被更新过;
  • AtomicStampedReference 也可以解决 ABA 问题,它不仅会记录原数据是否被更新过,还能记录被更新过多少次。

注意:CAS使用的时机
        线程数较少、等待时间短可以采用自旋锁进行CAS尝试拿锁,较于synchronized高效。

4.3 偏向锁

在Java SE 1.6中,锁一共有4种状态,级别从低到高依次是:无锁状态、偏向锁状态、轻量级锁状态和重量级锁状态。

核心原理

  • 如果不存在线程竞争的一个线程获得了锁,那么锁就进入偏向状态, 此时 Mark Word 的结构变为偏向锁结构:锁对象的锁标志位(lock)被改为 01,偏向标志位 (biased_lock)被改为 1,然后线程的 ID 记录在锁对象的 Mark Word 中(使用 CAS 操作完成)。
  • 以后该线程获取锁的时,判断一下线程 ID 和标志位,就可以直接进入同步块,连 CAS 操作都不需要,这样就省去了大量有关锁申请的操作,从而也就提供程序的性能。

补充:

HotSpot中,Java的对象内存模型分为三部分,分别为对象头、实例数据和对齐填充。而对象头中分为两部分,一部分是“Mark Word”(存储对象自身的运行时数据,32bit或64bit,可复用);另一部分是指向它的类的元数据指针。

因为synchronized是Java对象的内置锁,所以其优化策略(即偏向锁等)的信息都包含在Mark Word中:

偏向锁膨胀:

看一下打印结果: 

1)偏向锁

2)释放偏向锁

 Mark Word头中标记不变,(偏向这个线程)如果同线程再来启用这部分代码块,只需判断线程ID是否是同一个,然后进行偏向。

3)轻量级锁 

4)重量级锁

过程总结: 

轻量级锁:

线程在执行同步块之前,JVM会先在当前线程的栈桢中创建用于存储锁记录的空间,并将对象头中的Mark Word复制到锁记录中。然后线程尝试使用CAS将对象头中的Mark Word替换为指向锁记录的指针。如果成功,当前线程获得锁,如果失败,表示其他线程竞争锁,当前线程便尝试使用自旋来获取锁。

重量级锁:

重量级锁是依赖对象内部的monitor锁来实现。当系统检查到锁是重量级锁之后,会把等待想要获得锁的线程进行阻塞,被阻塞的线程不会消耗cup。但是阻塞或者唤醒一个线程时,都需要操作系统来帮忙,需要从用户态转换到内核态,而转换状态是需要消耗很多时间。

4.4 AQS机制

场景:有四个线程由于业务需求需要同时占用某资源,但该资源在同一个时刻只能被其中唯一线程所独占。那么此时应该如何标识该资源已经被独占,同时剩余无法获取该资源的线程又该何去何从呢?

AQS (AbstractQueuedSynchronizer)是一个集同步状态管理、线程阻塞、线程释放及队列管理功能与一身的同步框架。其核心思想是当多个线程竞争资源时会将未成功竞争到资源的线程构造为 Node 节点放置到一个双向 FIFO 队列中。被放入到该队列中的线程会保持阻塞直至被前驱节点唤醒。

1)同步状态

private volatile int state;

查看AQS源码可以知道,当 state 值为 1 的时候表示当前锁已经被某线程占用,除非等占用的锁的线程释放锁后将 state 置为 0,否则其它线程无法获取该锁。这里的 state 变量用 volatile 关键字保证其在多线程之间的可见性。

2)队列

Node类:

​ 

waitStatus:当前节点状态,该字段共有5种取值:

  • CANCELLED = 1。节点引用线程由于等待超时或被打断时的状态。
  • SIGNAL = -1。后继节点线程需要被唤醒时的当前节点状态。当队列中加入后继节点被挂起(block)时,其前驱节点会被设置为SIGNAL状态,表示该节点需要被唤醒。
  • CONDITION = -2。当节点线程进入condition队列时的状态。(见ConditionObject)
  • PROPAGATE = -3。仅在释放共享锁releaseShared时对头节点使用。(见共享锁分析)
  • 0。节点初始化时的状态。

3)资源竞争策略

public final void acquire(int arg) {
    if (!tryAcquire(arg) &&
        acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
        selfInterrupt();
}

acquire核心为tryAcquire、addWaiter和acquireQueued三个函数,其中tryAcquire需具体类实现。 每当线程调用acquire时都首先会调用tryAcquire,失败后才会挂载到队列,因此acquire实现默认为非公平锁。

  • addWaiter将线程包装为独占节点,尾插式加入到队列中,如队列为空,则会添加一个空的头节点。值得注意的是addWaiter中的enq方法,通过CAS+自旋的方式处理尾节点添加冲突。
  • acquireQueue在线程节点加入队列后判断是否可再次尝试获取资源,如不能获取则将其前驱节点标志为SIGNAL状态(表示其需要被unpark唤醒)后,则通过park进入阻塞状态。

4)资源释放

release流程较为简单,尝试释放成功后,即从头结点开始唤醒其后继节点,如后继节点被取消,则转为从尾部开始找阻塞的节点将其唤醒。阻塞节点被唤醒后,即进入acquireQueued中的for(;;)循环开始新一轮的资源竞争。

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

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

相关文章

练习六-使用Questasim来用verilog使用function函数

[TOC](使用Questasim来用verilog使用function函数 1&#xff0c;verilog中使用函数function2&#xff0c;RTL代码3&#xff0c;测试代码4&#xff0c;输出波形 1&#xff0c;verilog中使用函数function 目的&#xff1a; &#xff08;1&#xff09;了解函数的定义和在模块设计中…

问题解决:Ubuntu18.04下nvcc -V指令可用,/usr/local/下却没有cuda文件夹,原因分析及卸载方法

问题描述 今天要运行一个程序&#xff0c;需要CUDA版本高于10.0&#xff0c;我的电脑无法运行&#xff0c;于是开始检查 首先使用nvidia-smi与nvcc -V指令 能够看出来&#xff0c;当前显卡驱动适合的CUDA版本为12.1&#xff0c;而本机安装的版本是9.1.85&#xff0c;那么就需…

使用Navicat将SQL server数据库导入mysql数据库

使用Navicat将SQL server数据库导入mysql数据库 1、使用Navicat Premium打开MySql数据库&#xff0c;然后新建一个数据库名&#xff08;该数据库名称为需要从SqlServer数据库导过来的名称&#xff0c;mysql只有小写&#xff0c;不影响&#xff09; 比如需要将SqlServer数据库…

亚马逊澳大利亚纽扣电池UL4200A认证标准和要求

近来&#xff0c; 与纽扣电池相关的死亡和严重伤害越来越多。 美国消费品安全委员会(CPSC)的工作人员正在参与与消费品电池相关的自愿标准活动&#xff0c;包括UL 4200A -采用锂技术的纽扣或纽扣电池的产品安全标准。 一、适用产品有&#xff1a; 1. 安装有直径 ≤32mm&#x…

在ITSM中,实施变更管理的重要因素!

在ITSM管理中&#xff0c;变更管理是不可或缺的一步。在当今快速变革的商业环境中&#xff0c;组织需要不断地进行变更以适应市场的需求和竞争的压力。 然而&#xff0c;引入变更并成功地实施变更并不容易。变更管理是一种系统化和结构化的方法&#xff0c;旨在确保变更顺利进…

如何利用 AI 写一本书并实现被动收入

如何每个月写一本能赚 5000 美元的书&#xff1f;不少人不知道如何在一周内写作和出版一本书 这里有个教程教你如何利用 AI 写一本书并实现被动收入 [收藏起来以备后用] 推出书友智能写作工具&#xff1a;Bookwiz 不用花几年时间独自写作一本小说&#xff0c;人工智能可以作…

获取1688店铺所有商品、店铺列表api

返回数据格式&#xff1a; 请求链接 {"user": [],"items": {"item": [{"num_iid": "738354436678","title": "国产正品i13 promax全网通5G安卓智能手机源头厂家批发手机","pic_url": "…

【操作系统】文件系统的逻辑结构与目录结构

文章目录 文件的概念定义属性基本操作 文件的结构文件的逻辑结构文件的目录结构文件控制块&#xff08;FCB&#xff09;索引节点目录结构 文件的概念 定义 在操作系统中&#xff0c;文件被定义为&#xff1a;以计算机硬盘为载体的存储在计算机上的信息集合。 属性 描述文件…

JVM的垃圾收集算法

1.算法的分类 1.1标记清除算法 第一步&#xff1a;标记&#xff08;找出内存中需要回收的对象&#xff0c;并且把它们标记出来&#xff09; 根据可达性算法&#xff0c;标记的是存活的对象&#xff0c;然后将其他的空间进行回收 第二步&#xff1a;清除&#xff08;清除掉被…

企业域名邮箱申请流程指南:轻松搭建高效的企业邮箱系统

对于企业和个人来说拥有自己的域名和邮箱是展示形象和开展业务的重要工具&#xff0c;很多初学者可能对企业域名邮箱申请流程感到迷惑。企业域名邮箱申请流程分两步申请域名和创建邮箱&#xff0c;本文将详细介绍这两个步骤&#xff0c;帮助大家更好地理解和操作。 一、申请域名…

2024年最新FLStudio21破解版注册机百度网盘免费下载安装激活教程

高考完啦&#xff0c;你也迎来了人生中最长的暑假&#xff0c;现在你总不能以没时间为借口了&#xff0c;是时候学点属于自己的才艺了。还有3个月&#xff0c;你就会迎来开学迎新晚会这个校园中最受瞩目的活动。在这个特殊的时刻&#xff0c;如果你想以独特的方式展现自己&…

亚马逊出口电热毯日本PSE认证需要什么资料解析

电热毯出口日本需要办理PSE认证&#xff0c;电热毯&#xff0c;又名电褥&#xff0c;是一种接触式电暖器具。 PSE认证介绍是日本强制性认证&#xff0c;包含安全及EMI&#xff0c;用以证明电子电气等产品符合日期电气用品安全法或国际IEC标准的要求。日本电气用品安全法规定&am…

关于“研发效能冷思考”的冷思考 | IDCF

作者&#xff1a;冬哥 来源&#xff1a;DevOps 关于研发效能的冷思考 Robin&#xff08;茹炳晟老师&#xff09;在1.7号“冬哥有话说”的最后&#xff0c;提出关于研发效能的三个冷思考&#xff0c;核心问题是&#xff1a;研发效能的提升&#xff0c;是否真的让我们的工作和生活…

Chrome和chromedriver版本不匹配导致的UI自动化测试无法运行的问题

今天&#xff0c;遇到一个小问题&#xff0c;本来跑的好好UI自动化测试脚本突然不好使了&#xff0c;期初怀疑是页面元素有调整导致脚本出现异常无法正常执行&#xff0c;经排查后发现近期页面没有任何调整。 这下头大了&#xff0c;啥也没改&#xff0c;怎么好好的脚本不能跑…

环保回收信息展示预约小程序的效果如何

人们每天在线上的时间非常多&#xff0c;他们会通过线上寻找信息&#xff0c;而环保回收企业也在通过线上寻找客户&#xff0c;但受限于平台限制&#xff0c;无论引流获客还是营销互动、或是数据分析及全面管理方面都面对难题&#xff0c;其中微信/百度/快手/抖音/支付宝/快手等…

线性表但是是Java中数组实用使用

线性表定义&#xff1a; 由n (n≥0)个数据特性相同的元素构成的有限序列称为线性表&#xff0c;(n0)的时候被称为空表。 线性表的顺序表示 线性表的顺序存储又被称为顺序表 优点 无需为表示表中元素之间的逻辑关系而增加额外的存储空间可以随意读取任意位置的元素 缺点 插入…

Redis(位图Bitmap和位域Bitfield)

位图&#xff1a; 位图是字符串类型的扩展。 Redis中的位图是一种特殊的数据结构&#xff0c;用于表示一系列位的集合。它可以存储大量的布尔值数据&#xff0c;每个位代表一个布尔值&#xff08;0或1&#xff09;&#xff0c;并且可以对这些位进行各种位运算操作。位图通常用…

Python二级 每周练习题27

如果你感觉有收获&#xff0c;欢迎给我微信扫打赏码 ———— 以激励我输出更多优质内容 练习一: 用户输入一个半径r&#xff0c;求该半径下的圆的面积s与周长c。要求如下&#xff1a; &#xff08;1&#xff09;输出的面积与周长都保留俩位小数&#xff1b; &#xff08;2&a…

时尚女童千金小套装,爱了爱了

自带氛围感的小套装简直不要太好看 红色吸睛又喜庆&#xff0c;显白不挑人穿 谁穿都好看系列&#xff01;&#xff01; 厚实的苏两毛羊毛 保暖性能真的非常好 超柔软的手感和触感 显得很高级穿上也很舒适 一整个小公主style,时尚气质感拉满 里面的连衣裙单穿也很nice …

【CLion】CLion的cmake-build-release/debug位置变更

[CLion] CLion的cmake-build-debug和cmake-build-release位置变更 CLion IDE的cmake-build-release/debug位置是根目录下&#xff0c;虽然可以ignore这个文件&#xff0c;但是还是有点看起来不爽&#xff0c;按照这个方式就可以&#xff0c;干掉他&#xff0c;更改方式如下。 …