关于Java的多线程实现

news2024/11/19 3:45:59

多线程介绍

进程:进程指正在运行的程序。确切的来说,当一个程序进入内存运行,即变成一个进程,进程是处于运行过程中的程序,并且具有一定独立功能。

进程

线程:线程是进程中的一个执行单元,负责当前进程中程序的执行,一个进程中至少有一个线程。一个进程中是可以有多个线程的,这个应用程序也可以称之为多线程程序。

简而言之:一个程序运行后至少有一个进程,一个进程中可以包含多个线程

线程

什么是多线程呢?即就是一个程序中有多个线程在同时执行。

通过下图来区别单线程程序与多线程程序的不同:

  • 单线程程序:即,若有多个任务只能依次执行。当上一个任务执行结束后,下一个任务开始执行。如,去网吧上网,网吧只能让一个人上网,当这个人下机后,下一个人才能上网。
  • 多线程程序:即,若有多个任务可以同时执行。如,去网吧上网,网吧能够让多个人同时上网。

多线程

程序运行原理

  • 分时调度:所有线程轮流使用 CPU 的使用权,平均分配每个线程占用 CPU 的时间。
  • 抢占式调度:优先让优先级高的线程使用 CPU,如果线程的优先级相同,那么会随机选择一个(线程随机性),Java使用的为抢占式调度。

抢占、优先级

抢占式调度详解

大部分操作系统都支持多进程并发运行,现在的操作系统几乎都支持同时运行多个程序。比如:现在我们上课一边使用编辑器,一边使用录屏软件,同时还开着画图板,dos窗口等软件。此时,这些程序是在同时运行,”感觉这些软件好像在同一时刻运行着

抢占式调度

实际上,CPU(中央处理器)使用抢占式调度模式在多个线程间进行着高速的切换。对于CPU的一个核而言,某个时刻,只能执行一个线程,而 CPU的在多个线程间切换速度相对我们的感觉要快,看上去就是在同一时刻运行。

其实,多线程程序并不能提高程序的运行速度,但能够提高程序运行效率,让CPU的使用率更高。

线程的状态

线程可以处于下列状态之一:

  • NEW:至今尚未启动的线程处于这种状态
  • RUNNABLE:正在 Java 虚拟机中执行的线程处于这种状态
  • BLOCKED:受阻塞并等待某个监视器锁的线程处于这种状态
  • WAITING:无限期地等待另一个线程来执行某一特定操作的线程处于这种状态
    TIMED_WATING:等待另一个线程来执行取决于指定等待时间的操作的线程处于这种状态。TERHINATED已退出的线程处于这秘状态

在给定时间点上,一个线程只能处于一种状态。这些状态是虚拟机状态,它们并没有反映所有操作系统线程状态

线程的状态

主线程

当我们在dos命令行中输入java空格类名回车后,启动JVM,并且加载对应的class文件。虚拟机并会从main方法开始执行我们的程序代码,一直把main方法的代码执行结束。如果在执行过程遇到循环时间比较长的代码,那么在循环之后的其他代码是不会被马上执行的。如下代码演示:

class Demo{
	String name;
	Demo(String name){
		this.name = name;
	}
	void show()	{
		for (int i=1;i<=10000 ;i++ )		{
			System.out.println("name="+name+",i="+i);
		}
	}
}
class TestDemo {
	public static void main(String[] args) 	{
	    Demo d = new Demo("小强");
         Demo d2 = new Demo("旺财");
		d.show();
		d2.show();
		System.out.println("Hello World!");
	}
}

若在上述代码中show方法中的循环执行次数很多,这时在d.show();下面的代码是不会马上执行的,并且在dos窗口会看到不停的输出name=小强,i=值,这样的语句。为什么会这样呢?

原因是:jvm启动后,必然有一个执行路径(线程)从main方法开始的,一直执行到main方法结束,这个线程在java中称之为主线程。当程序的主线程执行时,如果遇到了循环而导致程序在指定位置停留时间过长,则无法马上执行下面的程序,需要等待循环结束后能够执行。

那么,能否实现一个主线程负责执行其中一个循环,再由另一个线程负责其他代码的执行,最终实现多部分代码同时执行的效果?

能够实现同时执行,通过Java中的多线程技术来解决该问题

Thread类

如何创建线程呢?通过API中搜索,查到Thread类。通过阅读Thread类中的描述。Thread是程序中的执行线程。Java 虚拟机允许应用程序并发地运行多个执行线程。

Thread类

构造方法

常用方法

创建新执行线程有两种方法。

  1. 将类声明为 Thread 的子类。该子类应重写 Thread 类的 run 方法。创建对象,开启线程。run方法相当于其他线程的main方法。
  2. 声明一个实现 Runnable 接口的类。该类然后实现 run 方法。然后创建Runnable的子类对象,传入到某个线程的构造方法中,开启线程。

创建线程方式一继承Thread类

创建线程的步骤:

  1. 定义一个类继承Thread
  2. 重写run方法
  3. 创建子类对象,就是创建线程对象
  4. 调用start方法,开启线程并让线程执行,同时还会告诉jvm去调用run方法
/**
 * 这是自定义线程类,继承Thread类
 * 重写run方法
 */
public class SubThread extends Thread {
    /**
     * 重写run方法,完成该线程执行的逻辑
     */
    @Override
    public void run() {
        for (int i=0; i<50; i++) {
            System.out.println(getName()+"循环第"+i+"次");
        }
    }
}
/**
 * 测试类,创建和启动一个线程
 *  创建Theard子类对象
 *  子类对象调用方法start(),让线程程序执行JVM调用线程中的run
 */
public class ThreadDemo {
    public static void main(String[] args) {
        //创建自定义线程对象
        SubThread st = new SubThread();
        //开启新线程
        st.start();

        //在主方法中执行for循环
        for (int i=0; i<50; i++) {
            System.out.println("main循环第"+i+"次");
        }
    }
}

线程对象调用 run方法和调用start方法区别:

线程对象调用run方法不开启线程。仅是对象调用方法。线程对象调用start开启线程,并让jvm调用run方法在开启的线程中执行。

线程随机性

1、我们为什么要继承Thread类,并调用其的start方法才能开启线程呢?

那是因为Thread类用来描述线程,具备线程应该有功能。

2、为什么不直接创建Thread类的对象呢?

Thread t1 = new Thread();
t1.start();

这样做没有错,但是该start调用的是Thread类中的run方法,而这个run方法没有做什么事情,更重要的是这个run方法中并没有定义我们需要让线程执行的代码。对于之前所讲的主线程,它的任务定义在main函数中。自定义线程需要执行的任务都定义在run方法中。Thread类run方法中的任务并不是我们所需要的,只有重写这个run方法。既然Thread类已经定义了线程任务的编写位置(run方法),那么只要在编写位置(run方法)中定义任务代码即可。所以进行了重写run方法动作。

多线程执行时,在栈内存中,其实每一个执行线程都有一片自己所属的栈内存空间。进行方法的压栈和弹栈,当执行线程的任务结束了,线程自动在栈内存中释放了,但是当所有的执行线程都结束了,那么进程就结束了。

多线程的内存图解

获取线程名称

开启的线程都会有自己的独立运行栈内存,那么这些运行的线程的名字是什么呢?该如何获取呢?既然是线程的名字,按照面向对象的特点,是哪个对象的属性和谁的功能,那么我们就去找那个对象就可以了。查阅Thread类的API文档发现有个方法是获取当前正在运行的线程对象。还有个方法是获取当前线程对象的名称。

API说明

/**
 * 这是自定义线程类,继承Thread类
 * 重写run方法
 *
 * 获取线程名字的方法 String getName()
 */
public class SubThread extends Thread {
    /**
     * 重写run方法,完成该线程执行的逻辑
     */
    @Override
    public void run() {
        System.out.println(super.getName());
    }
}
/**
 * 测试类,创建和启动一个线程
 * 每个线程都有自己的名字
 *  运行方法main主线程,默认名字就是"main"
 *  其他新建的子线程,默认名字是"Thread-x"
 *
 * JVM开启主线程,运行方法main,主线程也是线程必然也是继承于Thread类
 * Theard类当中,静态方法static Thread currentThread()返回正在执行的线程对象
 */
public class ThreadDemo {
    public static void main(String[] args) {
        //创建自定义线程对象
        SubThread st = new SubThread();
        //开启新线程
        st.start();

        //通用获取线程名称的方式
        System.out.println(Thread.currentThread().getName());
    }
}

设置线程名称

/**
 * 这是自定义线程类,继承Thread类
 * 重写run方法
 *
 * 设置线程名字的方法 setName(String name)
 */
public class SubThread extends Thread {
    public SubThread(){}
    public SubThread(String threadName){
        setName(threadName);
    }

    /**
     * 重写run方法,完成该线程执行的逻辑
     */
    @Override
    public void run() {
        System.out.println(super.getName());
    }
}
/**
 * 测试类,创建和启动一个线程
 * 每个线程都有自己的名字
 *  运行方法main主线程,默认名字就是"main"
 *  其他新建的子线程,默认名字是"Thread-x"
 *
 * JVM开启主线程,运行方法main,主线程也是线程必然也是继承于Thread类
 * Theard类当中,静态方法static Thread currentThread()返回正在执行的线程对象
 */
public class ThreadDemo {
    public static void main(String[] args) {
        //创建自定义线程对象
        SubThread st = new SubThread("new-thread-01");
        //开启新线程
        st.start();

        //通用获取线程名称的方式
        System.out.println(Thread.currentThread().getName());
    }
}

创建线程方式—实现Runnable接口

创建线程的另一种方法是声明实现 Runnable 接口的类。该类然后实现 run 方法。然后创建Runnable的子类对象,传入到某个线程的构造方法中,开启线程。

为何要实现Runnable接口,Runable是啥玩意呢?

查看Runnable接口说明文档:Runnable接口用来指定每个线程要执行的任务。包含了一个 run 的无参数抽象方法,需要由接口实现类重写该方法。

Runnable接口

构造方法

常用方法

创建线程的步骤:

  1. 定义类实现Runnable接口;
  2. 覆盖接口中的run方法;
  3. 创建Thread类的对象;
  4. 将Runnable接口的子类对象作为参数传递给Thread类的构造函数;
  5. 调用Thread类的start方法开启线程;
/**
 * 实现线程程序的另一种方式,接口实现
 * 实现接口Runnable,重写run方法
 */
public class SubRunnable implements Runnable{
    @Override
    public void run() {
        for(int i=0; i<5; i++) {
            System.out.println(i);
        }
    }
}
/**
 * 测试类,创建和启动一个线程
 * 创建Thread类对象,在构造方法当中传递Runnable接口实现类
 * 调用Thread类方法start()启动
 */
public class ThreadDemo {
    public static void main(String[] args) {
        SubRunnable sr = new SubRunnable();
        new Thread(sr).start();
    }
}

为什么需要定一个类去实现Runnable接口呢?继承Thread类和实现Runnable接口有啥区别呢?

实现Runnable接口,避免了继承Thread类的单继承局限性。覆盖Runnable接口中的run方法,将线程任务代码定义到run方法中。创建Thread类的对象,只有创建Thread类的对象才可以创建线程。线程任务已被封装到Runnable接口的run方法中,而这个run方法所属于Runnable接口的子类对象,所以将这个子类对象作为参数传递给Thread的构造函数,这样,线程对象创建时就可以明确要运行的线程的任务。

实现Runnable的好处:

第二种方式实现Runnable接口避免了单继承的局限性,所以较为常用。实现Runnable接口的方式,更加的符合面向对象,线程分为两部分,一部分线程对象,一部分线程任务。继承Thread类,线程对象和线程任务耦合在一起。一旦创建Thread类的子类对象,既是线程对象,有又有线程任务。实现runnable接口,将线程任务单独分离出来封装成对象,类型就是Runnable接口类型。Runnable接口对线程对象和线程任务进行解耦。

线程的匿名内部类使用

使用线程的内匿名内部类方式,可以方便的实现每个线程执行不同的线程任务操作

/**
 * 使用匿名内部类,实现多线程程序
 * 前提:继承或者接口的实现
 * new 父类或者接口(){
 *     重写抽象的方法
 * }
 */
public class ThreadDemo{
    public static void main(String[] args) {
        //继承方式 XXX extends Thread{public void run(){ ... }}
        new Thread(){
            @Override
            public void run() {
                System.out.println(getName());
            }
        }.start();

        //实现接口 XXX implements Runnable(){public void run(){ ... }}
        new Thread(new Runnable() {
            @Override
            public void run() {
                System.out.println(Thread.currentThread().getName());
            }
        }){}.start();
    }
}

线程池

线程池概念

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

线程池原理

我们详细的解释一下为什么要使用线程池?

在java中,如果每个请求到达就创建一个新线程,开销是相当大的。在实际使用中,创建和销毁线程花费的时间和消耗的系统资源都相当大,甚至可能要比在处理实际的用户请求的时间和资源要多的多。除了创建和销毁线程的开销之外,活动的线程也需要消耗系统资源。如果在一个jvm里创建太多的线程,可能会使系统由于过度消耗内存或“切换过度”而导致系统资源不足。为了防止资源不足,需要采取一些办法来限制任何给定时刻处理的请求数目,尽可能减少创建和销毁线程的次数,特别是一些资源耗费比较大的线程的创建和销毁,尽量利用已有对象来进行服务。

线程池主要用来解决线程生命周期开销问题和资源不足问题。通过对多个任务重复使用线程,线程创建的开销就被分摊到了多个任务上了,而且由于在请求到达时线程已经存在,所以消除了线程创建所带来的延迟。这样,就可以立即为请求服务,使用应用程序响应更快。另外,通过适当的调整线程中的线程数目可以防止出现资源不足的情况。

使用线程池方式–Runnable接口

通常,线程池都是通过线程池工厂创建,再调用线程池中的方法获取线程,再通过线程去执行任务方法。

  • Executors:线程池创建工厂类
    • public static ExecutorService newFixedThreadPool(int nThreads):返回线程池对象
  • ExecutorService:线程池类
    • Future<?> submit(Runnable task):获取线程池中的某一个线程对象,并执行
  • Future接口:用来记录线程任务执行完毕后产生的结果。线程池创建与使用

使用线程池中线程对象的步骤:

  1. 创建线程池对象
  2. 创建Runnable接口子类对象
  3. 提交Runnable接口子类对象
  4. 关闭线程池
/**
 * JDK5的新特性 线程池技术
 * 实现一个线程池,使用util包下的concurrent工厂类Executors中的静态方法创建线程对象,并设定线程的个数
 *  static ExecutorService newFixedThreadPool(int 个数) 返回线程池对象
 *  返回的是ExecutorService接口的实现类(线程池的对象)
 *  接口实现类对象,调用方法submit(Runnable r)提交一个线程执行的任务
 */
public class ThreadPoolDemo {
    public static void main(String[] args) {
        //调用工厂类的静态方法,创建线程池对象
        //返回的是线程池对象,即ExecutorService接口
        ExecutorService es = Executors.newFixedThreadPool(2);
        //调用接口实现类对象es中的方法submit提交执行任务
        //将Runnable的实现类对象传递即可
        es.submit(
                new Runnable() {
                    @Override
                    public void run() {
                        System.out.println(Thread.currentThread().getName()+"正在执行新任务");
                    }
                }
        );

        //销毁线程池的方法(一般不会使用)
        es.shutdown();
    }
}

使用线程池方式—Callable接口

  • Callable接口:与Runnable接口功能相似,用来指定线程的任务。其中的call()方法,用来返回线程任务执行完毕后的结果,call方法可抛出异常。
  • ExecutorService:线程池类
    • Future submit(Callable task):获取线程池中的某一个线程对象,并执行线程中的call()方法
  • Future接口:用来记录线程任务执行完毕后产生的结果。线程池创建与使用

使用线程池中线程对象的步骤:

  1. 创建线程池对象
  2. 创建Callable接口子类对象
  3. 提交Callable接口子类对象
  4. 关闭线程池
import java.util.concurrent.*;

/**
 * JDK5的新特性 线程池技术 实现Callable接口
 * 实现步骤:
 *  Executor工厂类静态方法newFixedThreadPool创建线程池对象
 *  线程池实现对象ExecutorService接口实现类,调用submit方法提交线程
 *  submit(Callable c)
 */
public class ThreadPoolDemo {
    public static void main(String[] args) throws ExecutionException, InterruptedException {
        ExecutorService es = Executors.newFixedThreadPool(2);
        //提交线程任务的方法submit将返回一个Future接口的实现类
        Future<String> f = es.submit(
                /**
                 * Callable接口实现类,作为线程提交任务出现
                 * 使用方法返回值
                 */
                new Callable<String>() {
                    @Override
                    public String call() throws Exception {
                        return Thread.currentThread().getName()+"正在执行新任务";
                    }
                }
        );

        //获取返回值
        System.out.println(f.get());
    }
}

案例:多线程实现异步计算

import java.util.concurrent.Callable;

/**
 * Callable接口的实现类
 */
public class CountDemo implements Callable<Integer> {
    private int a;
    private int b;

    public CountDemo(int a, int b) {
        this.a = a;
        this.b = b;
    }

    @Override
    public Integer call() throws Exception {
        int result = 0;
        for (int i=a; i<=b; i++){
            result=result+i;
        }
        return result;
    }
}
import java.util.concurrent.*;

/**
 * 使用多线程技术进行求和计算
 * 要求两个线程处理,一个线程计算1...100的和,另一个计算101...200的和
 */
public class ThreadPoolDemo {

    public static void main(String[] args) throws ExecutionException, InterruptedException {
        ExecutorService es = Executors.newFixedThreadPool(2);

        //线程一:计算1...100的和
        Future<Integer> result1 = es.submit(new CountDemo(1,100));
        //线程二:计算101...200的和
        Future<Integer> result2 = es.submit(new CountDemo(101,200));

        int finalResult = result1.get() + result2.get();
        System.out.println("最终结果为:"+ finalResult);
    }
}

自定义线程池:ThreadPoolExecutor

内置的线程池, 不推荐生产使用:
Executors.newCachedThreadPool();
Executors.newFixedThreadPool(10);
Executors.newScheduledThreadPool(10);
Executors.newSingleThreadExecutor();

上述的方法不推荐在生产当中使用,在生产场景下必须使用ThreadPoolExecutor构建线程池,这样可以明确线程池的运行规则,创建符合自己业务场景需要的线程池,避免资源耗尽的风险。以下是一个简单的示例:

构造函数

构造方法

  • corePoolSize:指定了线程池中的线程数量,它的数量决定了添加的任务是开辟新的线程去执行,还是放到workQueue任务队列中去。
  • maximumPoolSize:指定了线程池中的最大线程数量,这个参数会根据你使用的workQueue任务队列的类型,决定线程池会开辟的最大线程数量。
  • keepAliveTime:当线程池中空闲线程数量超过corePoolSize时,多余的线程会在多长时间内被摧毁。
  • unit:keepAliveTime的单位。
  • workQueue:任务队列,被添加到线程池中,但尚未被执行的任务;分为直接提交队列,有界任务队列,无界任务队列,优先任务队列。
  • threadFactory:线程工厂,用于创建线程。
  • handler:拒绝策略,当任务太多来不及处理的时候,会用这种策略拒绝任务。

corePoolSize和maximumPoolSize

ThreadPoolExecutor executorPool = new ThreadPoolExecutor(
                5,	//corePoolSize
                10,	//maximumPoolSize
                3, 	//keepAliveTime
                TimeUnit.SECONDS,	//unit
                new ArrayBlockingQueue<Runnable>(50)
        );

上边代码意思是有5个核心线程数,10个最大线程数,任务队列是50个线程。

在运行时,JVM首先为前5个新任务创建新线程,此时再来任务就放入任务队列中,直到任务队列已放满,此时再来新任务,JVM就会创建新线程,直到此时线程池中达到10个线程了就停止创建,即达到了最大线程数,此时再来新任务就会使用配置的拒绝策略新任务的提交。

workQueue任务队列

分为直接提交队列,有界任务队列,无界任务队列,优先任务队列。

直接提交队列

设置SynchronousQueue队列,SynchronousQueue是一个特殊的BlockingEueue,它没有容量,每执行一个插入操作就会阻塞,需要再执行一个删除操作才能被唤醒,反之每一个删除操作也都要等待对应的插入操作。

import java.util.concurrent.*;
public class ThreadPool {
    private static ExecutorService pool;

    public static void main(String[] args) {
        pool = new ThreadPoolExecutor(1, 2,
                1000,
                TimeUnit.MICROSECONDS,
                new SynchronousQueue<Runnable>(),
                Executors.defaultThreadFactory(),
                new ThreadPoolExecutor.AbortPolicy());

        for (int i = 0; i < 3; i++) {
            pool.execute(
                    new Runnable() {
                        @Override
                        public void run() {
                            System.out.println(Thread.currentThread().getName());
                        }
                    }
            );

        }

        pool.shutdown();
    }
}


------输出结果---------
pool-1-thread-2
pool-1-thread-1
Exception in thread "main" java.util.concurrent.RejectedExecutionException: Task com.if010.thread.ThreadPool$1@5e2de80c rejected from java.util.concurrent.ThreadPoolExecutor@1d44bcfa[Running, pool size = 2, active threads = 1, queued tasks = 0, completed tasks = 0]
	at java.util.concurrent.ThreadPoolExecutor$AbortPolicy.rejectedExecution(ThreadPoolExecutor.java:2063)
	at java.util.concurrent.ThreadPoolExecutor.reject(ThreadPoolExecutor.java:830)
	at java.util.concurrent.ThreadPoolExecutor.execute(ThreadPoolExecutor.java:1379)
	at com.if010.thread.ThreadPool.main(ThreadPool.java:16)

从输出结果当中看到,当任务队列为SynchronousQueue时,创建的线程数量大于maximumPoolSize时,直接执行了拒绝策略抛出异常。

使用SynchronousQueue队列,提交的任务不会被保存,会马上提交执行。如果用于执行任务的线程数量小于maximumPoolSize时,则尝试创建新的线程,如果达到maximumPoolSize设置的最大值,则会根据设置的handler执行拒绝策略。

有界任务队列

使用ArrayBlockingQueue实现

import java.util.concurrent.*;
public class ThreadPool {
    private static ExecutorService pool;

    public static void main(String[] args) {
        pool = new ThreadPoolExecutor(1, 2,
                1000,
                TimeUnit.MICROSECONDS,
                new ArrayBlockingQueue<Runnable>(10),
                Executors.defaultThreadFactory(),
                new ThreadPoolExecutor.AbortPolicy());

        for (int i = 0; i < 3; i++) {
            pool.execute(
                    new Runnable() {
                        @Override
                        public void run() {
                            System.out.println(Thread.currentThread().getName());
                        }
                    }
            );

        }

        pool.shutdown();
    }
}

------输出结果-------
pool-1-thread-1
pool-1-thread-1
pool-1-thread-1

使用ArrayBlockingQueue有界任务队列,当有新的任务需要执行的时候,线程池会创建新的线程,直到创建的线程数量达到corePoolSize时,会将新的任务加入到等待队列中。当等待队列满了的时候,也会继续创建线程,直到线程数量达到maximumPoolSize设置的最大线程数量时,则会执行拒绝策略。在这种情况下,线程数量的上限与有界任务队列的状态有直接的关系,如果有界队列初始容量较大或者没有达到超负荷的状态,线程数将一直维持在corePoolSize以下,反之当任务队列满了时,则会以maximumPoolSize为最大线程数上限。

无界任务队列

使用LinkedBlockingQueue实现。

import java.util.concurrent.*;
public class ThreadPool {
    private static ExecutorService pool;

    public static void main(String[] args) {
        pool = new ThreadPoolExecutor(1, 2,
                1000,
                TimeUnit.MICROSECONDS,
                new LinkedBlockingDeque<Runnable>(),
                Executors.defaultThreadFactory(),
                new ThreadPoolExecutor.AbortPolicy());

        for (int i = 0; i < 3; i++) {
            pool.execute(
                    new Runnable() {
                        @Override
                        public void run() {
                            System.out.println(Thread.currentThread().getName());
                        }
                    }
            );

        }

        pool.shutdown();
    }
}

------输出结果-------
pool-1-thread-1
pool-1-thread-1
pool-1-thread-1

使用无界任务队列,线程池的任务队列可以无限制的添加新任务,而线程池创建的最大线程数量就是corePoolSize设置的数量,也就是说此时maximumPoolSize参数是无效的。当有新的任务加入时,则会直接进入等待队列,所以你一定要注意任务提交与处理直接的协调,要防止等待队列中的任务由于无法及时处理而一直增长,导致资源耗尽。

优先任务队列

使用PriorityBlockingQueue实现。

import java.util.concurrent.*;
public class ThreadPool {
    private static ExecutorService pool;

    public static void main(String[] args) {
        pool = new ThreadPoolExecutor(1, 2,
                1000,
                TimeUnit.MICROSECONDS,
                new PriorityBlockingQueue<Runnable>(),
                Executors.defaultThreadFactory(),
                new ThreadPoolExecutor.AbortPolicy());

        for (int i = 0; i < 10; i++) {
            pool.execute(new ThreadTask(i));
        }

        pool.shutdown();
    }
}

class ThreadTask implements Runnable, Comparable<ThreadTask> {
    private int priority;

    public int getPriority() {
        return priority;
    }

    public void setPriority(int priority) {
        this.priority = priority;
    }

    public ThreadTask() {
    }

    public ThreadTask(int priority) {
        this.priority = priority;
    }

    //当前对象与其它对象比较,当优先级大时返回-1,优先级小时返回1
    //priority值越小优先级越高
    @Override
    public int compareTo(ThreadTask o) {
        return this.priority > o.priority ? -1 : 1;
    }

    @Override
    public void run() {
        try {
            //阻塞线程,使后续任务进入缓存队列
            Thread.sleep(1000);
            System.out.println("当前线程优先级:" + this.priority + ",线程名字:" + Thread.currentThread().getName());
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}

------输出结果-------
当前线程优先级:0,线程名字:pool-1-thread-1
当前线程优先级:9,线程名字:pool-1-thread-1
当前线程优先级:8,线程名字:pool-1-thread-1
当前线程优先级:7,线程名字:pool-1-thread-1
当前线程优先级:6,线程名字:pool-1-thread-1
当前线程优先级:5,线程名字:pool-1-thread-1
当前线程优先级:4,线程名字:pool-1-thread-1
当前线程优先级:3,线程名字:pool-1-thread-1
当前线程优先级:2,线程名字:pool-1-thread-1
当前线程优先级:1,线程名字:pool-1-thread-1

除了第一个任务直接创建线程执行外,其它的任务都被放入了优先任务队列,按照优先级进行重新排序执行,且线程池的线程数一直为corePoolSize,也就是一个,说明此时maximumPoolSize设置无效。

也就是说PriorityBlockingQueue是一个特殊的无界队列,无论添加了多少个任务,线程池创建的线程数也不会超过corePoolSize设置的数量。

threadFactory

线程池中线程是通过ThreadPoolExecutor中的ThreadFactory线程工厂创建。通过自定义ThreadFactory可以按需要对线程池中创建的线程进行一些特殊设置,比如命名,优先级。

import java.util.concurrent.*;
public class ThreadPool {
    private static ExecutorService pool;

    public static void main(String[] args) {
        pool = new ThreadPoolExecutor(2, 4,
                1000,
                TimeUnit.MICROSECONDS,
                new ArrayBlockingQueue<Runnable>(5),
                new ThreadFactory() {
                    @Override
                    public Thread newThread(Runnable r) {
                        System.out.println("线程 " + r.hashCode() + " 创建");
                        //线程命名
                        Thread th = new Thread(r, "线程名字 " + r.hashCode());
                        return th;
                    }
                },
                new ThreadPoolExecutor.CallerRunsPolicy());

        for (int i = 0; i < 10; i++) {
            pool.execute(
                    new Runnable() {
                        @Override
                        public void run() {
                            //输出执行线程的名称
                            System.out.println("执行中的线程名字:" + Thread.currentThread().getName());
                        }
                    }
            );
        }

        pool.shutdown();
    }
}

------输出结果-------
线程 1627674070 创建
线程 1360875712 创建
执行中的线程名字:线程名字 1627674070
线程 1625635731 创建
执行中的线程名字:线程名字 1360875712
执行中的线程名字:线程名字 1627674070
执行中的线程名字:线程名字 1360875712
执行中的线程名字:线程名字 1627674070
执行中的线程名字:线程名字 1627674070
执行中的线程名字:线程名字 1625635731
执行中的线程名字:线程名字 1360875712
执行中的线程名字:线程名字 1625635731
执行中的线程名字:线程名字 1627674070

handler

为防止资源被耗尽,任务队列都会选择创建有界任务队列,但是这种模式下如果出现任务队列已满并且线程池创建的线程数已达到最大线程数时,就需要指定ThreadPoolExecutor的RejectedExecutionHandler参数提供拒绝策略,来处理线程池超载情况。

ThreadPoolExecutor自带的拒绝策略如下:

  1. AbortPolicy策略:该策略直接抛出异常,阻止系统正常工作
  2. CallerRunsPolicy策略:如果线程池的线程数量达到上限,该策略会把任务队列中的任务放在调用者线程中运行
  3. DiscardOledestPolicy策略:该策略会丢弃任务队列中最老的一个任务,也就是当前任务队列中最先被添加进去的
  4. DiscardPolicy策略:该策略会丢弃无法处理的任务,不予任何处理

以上内置的策略都实现了RejectedExecutionHandler接口,当然也可以自定义拒绝策略

import java.util.concurrent.*;
public class ThreadPool {
    private static ExecutorService pool;

    public static void main(String[] args) {
        pool = new ThreadPoolExecutor(1, 2,
                1000,
                TimeUnit.MICROSECONDS,
                new ArrayBlockingQueue<Runnable>(5),
                Executors.defaultThreadFactory(),
                new RejectedExecutionHandler() {
                    @Override
                    public void rejectedExecution(Runnable r, ThreadPoolExecutor executor) {
                        System.out.println(r.toString() + "执行了拒绝策略");
                    }
                });

        for (int i = 0; i < 10; i++) {
            pool.execute(
                    new Runnable() {
                        @Override
                        public void run() {
                            try {
                                //让线程阻塞,使后续任务进入后续队列
                                Thread.sleep(1000);
                                //输出执行线程的名称
                                System.out.println("执行中的线程名字:" + Thread.currentThread().getName());
                            } catch (InterruptedException e) {
                                e.printStackTrace();
                            }
                        }
                    }
            );
        }

        pool.shutdown();
    }
}

------输出结果-------
com.if010.thread.ThreadPool$2@60e53b93执行了拒绝策略
com.if010.thread.ThreadPool$2@5e2de80c执行了拒绝策略
com.if010.thread.ThreadPool$2@1d44bcfa执行了拒绝策略
执行中的线程名字:pool-1-thread-1
执行中的线程名字:pool-1-thread-2
执行中的线程名字:pool-1-thread-1
执行中的线程名字:pool-1-thread-2
执行中的线程名字:pool-1-thread-2
执行中的线程名字:pool-1-thread-1
执行中的线程名字:pool-1-thread-2

当任务加入了休眠阻塞,执行需要花费一定时间,导致会有一定的任务被丢弃,从而执行自定义的拒绝策略。

ThreadPoolExecutor扩展

  • beforeExecute:线程池中的任务运行前执行
  • afterExecute:线程池中的任务运行完毕后执行
  • terminated:线程池退出后执行

通过这三个接口,可以监控每个任务的开始和结束时间

import java.util.concurrent.*;
public class ThreadPool {
    private static ExecutorService pool;

    public static void main(String[] args) {
        pool = new ThreadPoolExecutor(2, 4,
                1000,
                TimeUnit.MICROSECONDS,
                new ArrayBlockingQueue<Runnable>(5),
                new ThreadFactory() {
                    @Override
                    public Thread newThread(Runnable r) {
                        System.out.println("线程 " + r.hashCode() + " 创建");
                        //线程命名
                        Thread th = new Thread(r, "线程 " + r.hashCode());
                        return th;
                    }
                },
                new ThreadPoolExecutor.CallerRunsPolicy()) {
            @Override
            protected void beforeExecute(Thread t, Runnable r) {
                System.out.println("准备执行 " + ((ThreadTask) r).getTaskName());
            }

            @Override
            protected void afterExecute(Runnable r, Throwable t) {
                System.out.println("执行完毕 " + ((ThreadTask) r).getTaskName());
            }

            @Override
            protected void terminated() {
                System.out.println("线程池退出");
            }
        };

        for (int i = 0; i < 10; i++) {
            pool.execute(new ThreadTask("Task" + i));
        }

        pool.shutdown();
    }
}

class ThreadTask implements Runnable {
    private String taskName;

    public String getTaskName() {
        return taskName;
    }

    public void setTaskName(String taskName) {
        this.taskName = taskName;
    }

    public ThreadTask(String taskName) {
        this.taskName = taskName;
    }

    @Override
    public void run() {
        //输出执行线程的名称
        System.out.println("任务名称:" + this.getTaskName() + " 线程名称" + Thread.currentThread().getName());
    }
}

------输出结果-------
线程 1627674070 创建
线程 1360875712 创建
准备执行 Task0
任务名称:Task0 线程名称线程 1627674070
线程 1625635731 创建
准备执行 Task1
执行完毕 Task0
任务名称:Task1 线程名称线程 1360875712
准备执行 Task2
线程 1580066828 创建
任务名称:Task2 线程名称线程 1627674070
执行完毕 Task2
执行完毕 Task1
准备执行 Task8
任务名称:Task8 线程名称线程 1580066828
执行完毕 Task8
准备执行 Task5
任务名称:Task5 线程名称线程 1580066828
执行完毕 Task5
准备执行 Task7
任务名称:Task7 线程名称线程 1625635731
执行完毕 Task7
准备执行 Task6
准备执行 Task4
准备执行 Task3
任务名称:Task3 线程名称线程 1360875712
任务名称:Task4 线程名称线程 1627674070
执行完毕 Task4
任务名称:Task6 线程名称线程 1580066828
执行完毕 Task6
准备执行 Task9
任务名称:Task9 线程名称线程 1625635731
执行完毕 Task9
执行完毕 Task3
线程池退出

对于这三个方法的重写,可以对线程池中线程的运行状态进行监控,在其执行前后打印相关信息。使用shutdown方法可以比较安全的关闭线程池,当调用该方法后,线程池不再接受后续添加的任务,但是此时线程池不会马上退出,而是等到添加到线程池中的任务都已经完成处理后,才会退出。

线程分配流程

线程分配流程

线程安全

如果有多个线程在同时运行,而这些线程可能会同时运行这段代码。程序每次运行结果和单线程运行的结果是一样的,而且其他的变量的值也和预期的是一样的,就是线程安全的。

我们通过一个案例,演示线程的安全问题

电影院要卖票,我们模拟电影院的卖票过程。假设要播放的电影是 “功夫熊猫3”,本次电影的座位共10个(本场电影只能卖10张票)。

/**
 * 模拟电影院的售票窗口,实现多个窗口同时卖"功夫熊猫3"这场电影票(多个窗口一起卖这10张票)
 * 窗口,线程对象来模拟
 * 票,Runnable接口子类来模拟
 */
public class MovieTestDemo {
    public static void main(String[] args) {
        //创建Runnable接口实现类对象
        Runnable t = new Tickets();
        //创建3个Thread类对象,传递Runnable接口实现类
        Thread t0 = new Thread(t);
        Thread t1 = new Thread(t);
        Thread t2 = new Thread(t);

        t0.start();
        t1.start();
        t2.start();
    }
}

class Tickets implements Runnable{
    //定义出10张票
    private int tickets = 10;

    @Override
    public void run() {
        while (true){
            //对票数进行判断,大于0则可以出售,变量--操作
            if (tickets > 0) {
                try {
                    Thread.sleep(10);
                } catch (InterruptedException e) {
                    throw new RuntimeException(e);
                }
                System.out.println(Thread.currentThread().getName() + " 出售第" + tickets-- + "张票");
            } else {
                System.out.println("售票结束");
                break;
            }
        }
    }
}

---------输出结果------
Thread-1 出售第10张票
Thread-0 出售第10张票
Thread-2 出售第9张票
Thread-0 出售第8张票
Thread-1 出售第7张票
Thread-2 出售第6张票
Thread-1 出售第5张票
Thread-0 出售第4张票
Thread-2 出售第3张票
Thread-1 出售第2张票
Thread-0 出售第1张票
售票结束
Thread-2 出售第0张票
售票结束
Thread-1 出售第-1张票
售票结束

运行结果发现:上面程序出现了问题

  • 票出现了重复的票
  • 错误的票 0、-1

其实,线程安全问题都是由全局变量及静态变量引起的。若每个线程中对全局变量、静态变量只有读操作,而无写操作,一般来说,这个全局变量是线程安全的;若有多个线程同时执行写操作,一般都需要考虑线程同步,否则的话就可能影响线程安全。

线程安全处理Synchronized

java中提供了线程同步机制,它能够解决上述的线程安全问题。

线程同步的方式有两种:

  1. 同步代码块
  2. 同步方法

同步代码块执行原理图

同步代码块

同步代码块: 在代码块声明上 加上synchronized

synchronized (锁对象) {
	可能会产生线程安全问题的代码
}

同步代码块中的锁对象可以是任意的对象;但多个线程时,要使用同一个锁对象才能够保证线程安全。

使用同步代码块,对电影院卖票案例中Ticket类进行如下代码修改:

/**
 * 模拟电影院的售票窗口,实现多个窗口同时卖"功夫熊猫3"这场电影票(多个窗口一起卖这10张票)
 * 窗口,线程对象来模拟
 * 票,Runnable接口子类来模拟
 */
public class MovieTestDemo {
    public static void main(String[] args) {
        //创建Runnable接口实现类对象
        Runnable t = new Tickets();
        //创建3个Thread类对象,传递Runnable接口实现类
        Thread t0 = new Thread(t);
        Thread t1 = new Thread(t);
        Thread t2 = new Thread(t);

        t0.start();
        t1.start();
        t2.start();
    }
}

class Tickets implements Runnable{
    //定义出10张票
    private int tickets = 10;
    Object lock = new Object();

    @Override
    public void run() {
        while (true){
            synchronized (lock) {
                //对票数进行判断,大于0则可以出售,变量--操作
                if (tickets > 0) {
                    try {
                        Thread.sleep(10);
                    } catch (InterruptedException e) {
                        throw new RuntimeException(e);
                    }
                    System.out.println(Thread.currentThread().getName() + " 出售第" + tickets-- + "张票");
                } else {
                    System.out.println("售票结束");
                    break;
                }
            }
        }
    }
}

同步方法

同步方法:在方法声明上加上synchronized

public synchronized void method(){
   	//可能会产生线程安全问题的代码
}

同步方法中的锁对象是 this

public void method(){
    synchronized(this) {
        //可能会产生线程安全问题的代码
    }
}

使用同步方法,对电影院卖票案例中Ticket类进行如下代码修改:

/**
 * 模拟电影院的售票窗口,实现多个窗口同时卖"功夫熊猫3"这场电影票(多个窗口一起卖这10张票)
 * 窗口,线程对象来模拟
 * 票,Runnable接口子类来模拟
 */
public class MovieTestDemo {
    public static void main(String[] args) {
        //创建Runnable接口实现类对象
        Runnable t = new Tickets();
        //创建3个Thread类对象,传递Runnable接口实现类
        Thread t0 = new Thread(t);
        Thread t1 = new Thread(t);
        Thread t2 = new Thread(t);

        t0.start();
        t1.start();
        t2.start();
    }
}

class Tickets implements Runnable{
    //定义出10张票
    private int tickets = 10;
  
    @Override
    public void run() {
        while (true){
            if (buyTickets() == false){
                System.out.println("售票结束");
                return;
            }
        }
    }

    public synchronized boolean buyTickets(){
        if (tickets > 0) {
            try {
                Thread.sleep(10);
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            }
            System.out.println(Thread.currentThread().getName() + " 出售第" + tickets-- + "张票");
            return true;
        } else {
            return false;
        }
    }
}

静态同步方法: 在方法声明上加上static synchronized,

public static synchronized void method(){
    //可能会产生线程安全问题的代码
}

静态同步方法中的锁对象是 类名.class

public static void method(){
    synchronized(MovieTestDemo.class) {
        //可能会产生线程安全问题的代码
    }
}

死锁

同步锁使用的弊端:当线程任务中出现了多个同步(多个锁)时,如果同步中嵌套了其他的同步。这时容易引发一种现象:程序出现无限等待,这种现象我们称为死锁。这种情况能避免就避免掉。

线程死锁的原理

synchronzied(A){
    synchronized(B){
        //肯能产生线程安全问题的代码
    }
}

实现死锁:

public class TestLock {
    public static void main(String[] args) {
        LockDemo lockDemo = new LockDemo();
        Thread t0 = new Thread(lockDemo);
        Thread t1 = new Thread(lockDemo);

        t0.start();t1.start();
    }
}

class LockDemo implements Runnable{
    private int i=0;
    @Override
    public void run() {
        while (true){
            if (i%2 == 0){
                //先拿A锁,再那B锁
                synchronized (LockA.class){
                    System.out.println(Thread.currentThread().getName()+"号玩家在 if 已拿到 A 锁");
                    synchronized (LockB.class){
                        System.out.println(Thread.currentThread().getName()+"号玩家在 if 已拿到 B 锁");
                    }
                }
            } else {
                //先拿B锁,再那A锁
                synchronized (LockB.class){
                    System.out.println(Thread.currentThread().getName()+"号玩家在 else 已拿到 B 锁");
                    synchronized (LockA.class){
                        System.out.println(Thread.currentThread().getName()+"号玩家在 else 已拿到 A 锁");
                    }
                }
            }
            i++;
        }
    }
}

class LockA{
    private LockA(){}
    public static final LockA locka = new LockA();
}

class LockB{
    private LockB(){}
    public static final LockB lockb = new LockB();
}

Lock接口

查阅API,查阅Lock接口描述,Lock 实现提供了比使用 synchronized 方法和语句可获得的更广泛的锁定操作。

Lock接口中的常用方法

Lock提供了一个更加面对对象的锁,在该锁中提供了更多的操作锁的功能。

我们使用Lock接口,以及其中的lock()方法和unlock()方法替代同步,对电影院卖票案例中Ticket类进行如下代码修改:

import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;

/**
 * 使用JDK1.5之后的接口Lock,替换同步代码块,实现线程的安全性
 * Lock接口方法:
 *  lock() 获取锁
 *  unlock() 释放锁
 * 实现类ReentrantLock
 */
public class MovieTestDemo {
    public static void main(String[] args) {
        //创建Runnable接口实现类对象
        Runnable t = new Tickets();
        //创建3个Thread类对象,传递Runnable接口实现类
        Thread t0 = new Thread(t);
        Thread t1 = new Thread(t);
        Thread t2 = new Thread(t);

        t0.start();
        t1.start();
        t2.start();
    }
}

class Tickets implements Runnable{
    //定义出售的票源
    private int tickets = 10;

    //在类成员位置,创建Lock接口实现类对象
    private Lock lock = new ReentrantLock();

    @Override
    public void run() {
        while (true){
            //调用Lock接口方法lock获取锁
            lock.lock();
            try {
                if (tickets > 0) {
                    try {
                        Thread.sleep(10);
                    } catch (InterruptedException e) {
                        throw new RuntimeException(e);
                    }
                    System.out.println(Thread.currentThread().getName() + " 出售第" + tickets-- + "张票");
                } else {
                    System.out.println("售票结束");
                    return;
                }
            } catch (Exception e){

            } finally {
                //释放锁,调用Lock接口方法unlock
                lock.unlock();
            }
        }
    }
}

等待唤醒机制

在开始讲解等待唤醒机制之前,有必要搞清一个概念——线程之间的通信:多个线程在处理同一个资源,但是处理的动作(线程的任务)却不相同。通过一定的手段使各个线程能有效的利用资源。而这种手段即—— 等待唤醒机制。

等待唤醒机制所涉及到的方法:

  • wait() :等待,将正在执行的线程释放其执行资格 和 执行权,并存储到线程池中。
  • notify():唤醒,唤醒线程池中被wait()的线程,一次唤醒一个,而且是任意的。
  • notifyAll(): 唤醒全部:可以将线程池中的所有wait() 线程都唤醒。

其实,所谓唤醒的意思就是让 线程池中的线程具备执行资格。必须注意的是,这些方法都是在 同步中才有效。同时这些方法在使用时必须标明所属锁,这样才可以明确出这些方法操作的到底是哪个锁上的线程。

仔细查看JavaAPI之后,发现这些方法 并不定义在 Thread中,也没定义在Runnable接口中,却被定义在了Object类中,为什么这些操作线程的方法定义在Object类中?

因为这些方法在使用时,必须要标明所属的锁,而锁又可以是任意对象。能被任意对象调用的方法一定定义在Object类中。

方法

接下里,我们先从一个简单的示例入手:

线程通信示例

如上图说示,输入线程向Resource中输入name ,sex , 输出线程从资源中输出,先要完成的任务是:

  1. 当input发现Resource中没有数据时,开始输入,输入完成后,叫output来输出。如果发现有数据,就wait();
  2. 当output发现Resource中没有数据时,就wait(); 当发现有数据时,就输出,然后,叫醒input来输入数据。

下面代码,模拟等待唤醒机制的实现:

/**
 * 定义资源类,有两个成员变量name,sex
 * 同时有两个线程,对资源中的变量操作
 * 线程一对name,age赋值
 * 线程二对name,age输出
 */
public class Resource {
    public String name;
    public String sex;
    public boolean flag = false;
}

/**
 * 输入的线程,对资源对象Resource中的成员变量赋值
 */
class Input implements Runnable{
    private Resource r;

    public Input(Resource r) {
        this.r = r;
    }

    @Override
    public void run() {
        int i = 0;
        while (true) {
            synchronized (r) {
                if (r.flag){
                    try {
                        r.wait();
                    } catch (InterruptedException e) {
                        throw new RuntimeException(e);
                    }
                } else {
                    if (i % 2 == 0) {
                        r.name = "小王";
                        r.sex = "男";
                    } else {
                        r.name = "小林";
                        r.sex = "女";
                    }
                    i++;
                    r.flag = true;
                    r.notify();
                }
            }
        }
    }
}

/**
 * 输出线程,对Resource的成员变量进行打印输出
 */
class Output implements Runnable{
    private Resource r;

    public Output(Resource r) {
        this.r = r;
    }

    @Override
    public void run() {
        while (true){
            synchronized (r) {
                if (r.flag) {
                    System.out.println(r.name + "\t" + r.sex);
                    r.flag = false;
                    r.notify();
                } else {
                    try {
                        r.wait();
                    } catch (InterruptedException e) {
                        throw new RuntimeException(e);
                    }
                }
            }
        }
    }
}

/**
 * 开启输入线程和输出线程
 */
class TestDome{
    public static void main(String[] args) {
        Resource r = new Resource();
        Input in = new Input(r);
        Output out = new Output(r);

        Thread t_in = new Thread(in);
        Thread t_out = new Thread(out);

        t_in.start();t_out.start();
    }
}

这里要注意的是wait和notify需要使用锁对象进行调用,否则会报以下错误:

Exception in thread "Thread-1" java.lang.IllegalMonitorStateException
	at java.lang.Object.wait(Native Method)
	at java.lang.Object.wait(Object.java:502)
	at com.if010.thread.Output.run(Resource.java:73)
	at java.lang.Thread.run(Thread.java:750)

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

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

相关文章

大数据课程D11——hadoop的Ganglia

文章作者邮箱&#xff1a;yugongshiyesina.cn 地址&#xff1a;广东惠州 ▲ 本章节目的 ⚪ 了解Ganglia的概念&#xff1b; ⚪ 掌握Ganglia的安装操作&#xff1b; ⚪ 掌握Ganglia的监控Flume操作&#xff1b; 一、概述 1. Ganglia是UC Berkeley发起的一个开源…

JVM基础篇-程序计数器

程序计数器 定义 Program Counter Register 程序计数器&#xff08;寄存器&#xff09; 作用:记住下一条jvm指令的执行地址特点 是线程私有的:每个线程都有自己的程序计数器不会存在内存溢出(规定) 作用 左侧:jvm指令 右侧:java代码 0: getstatic #20 // PrintSt…

三维点云与深度图相互转换

点云转深度图 一、效果二、实现原理与代码2.1 获取点云边界2.2 确定图像大小2.3 稀疏点图像填充2.4 完整代码三、由深度图转换回点云信息丢失问题3.1 深度图转点云3.2 深度图转点云代码3.3 多视角的深度图融合一、效果 对点云进行转换,z向表示深度,转换效果如下 二、实现…

Docker安装配置启动Oracle11g容器解决ORA-12541:TNS: 无监听程序连接第三方客户端

Windows下安装可参考我这篇&#xff1a;win11&win7下安装oracle11g数据库全过程 一、下载与启动 前提&#xff1a;需要安装配置好docker(设置镜像源、配置阿里云加速)等&#xff0c;可参考我这篇 基于CentOS7安装配置docker与docker-compose 。 Docker容器相关操作可参考…

【自动化运维】playbook剧本

目录 一、Ansible 的脚本 playbook 剧本1.1playbooks的组成 二、剧本编写实验2.1定义、引用变量2.2使用远程主机sudo切换用户2.3whenn条件判断2.4迭代 三、Templates 模板四、Tags模板 一、Ansible 的脚本 playbook 剧本 1.1playbooks的组成 &#xff08;1&#xff09;Tasks&…

Diffusion扩散模型学习2——Stable Diffusion结构解析-以文本生成图像(文生图,txt2img)为例

Diffusion扩散模型学习2——Stable Diffusion结构解析-以文本生成图像&#xff08;文生图&#xff0c;txt2img&#xff09;为例 学习前言源码下载地址网络构建一、什么是Stable Diffusion&#xff08;SD&#xff09;二、Stable Diffusion的组成三、生成流程1、文本编码2、采样流…

Python自动化测试----生成测试报告

如何才能让用例自动运行完之后&#xff0c;生成一张直观可看易懂的测试报告呢&#xff1f; 对于自动化测试有兴趣的朋友可以观看这个视频&#xff1a; 【整整200集】超超超详细的Python接口自动化测试进阶教程&#xff0c;真实模拟企业项目实战&#xff01;&#xff01; 小编使…

【Ap模块EM】09- 什么是Manifest?

先直观感受一下下面的这个服务定义: -fidl文字描述版本: arxml版本: 了解Manifest之前,我们了解一下AutoSAR是怎么开发的? AUTOSAR方法论 AUTOSAR 提供了一种开发方法论,该方法描述了从抽象系统定义一直到最终 EUC 可执行文件的流程步骤,并包含设计步骤和工作产品列…

读取application-dev.properties的中文乱码【bug】

读取application-dev.properties的中文编码【bug】 2023-7-30 22:37:46 版权 禁止其他平台发布时删除以下此话 本文首次发布于CSDN平台 作者是CSDN日星月云 博客主页是https://blog.csdn.net/qq_51625007 禁止其他平台发布时删除以上此话 bug 读取application-dev.propert…

详解Unity中的Nav Mesh|导航寻路系统 (一)

前言 在类RTS、RPG游戏中&#xff0c;都会提供自动寻路功能&#xff0c;当玩家下达指令后&#xff0c;NPC就会自动计算到达目标的路径&#xff0c;实现这种功能的方式有很多种&#xff0c;其中Unity本身也自带了一种导航寻路系统&#xff0c;该系统会将游戏场景中复杂的对象烘…

STM32入门学习之外部中断

1.STM32的IO口可以作为外部中断输入口。本文通过按键按下作为外部中断的输入&#xff0c;点亮LED灯。在STM32的19个外部中断中&#xff0c;0-15为外部IO口的中断输入口。STM32的引脚分别对应着0-15的外部中断线。比如&#xff0c;外部中断线0对应着GPIOA.0-GPIOG.0&#xff0c;…

SpaceX 摊上事?未获环境许可,星舰发射系统涉嫌违规排放污染物

马斯克“寄予厚望”的星舰SpaceX最近可能摊上事了&#xff0c;疑似未申请环境许可&#xff0c;星舰发射系统涉嫌违规排放污染物。 据报导&#xff0c;SpaceX可能在火焰偏转器系统方面存在环境许可问题&#xff0c;其在火箭发射时&#xff0c;可能涉及违反相关环境法规。 最近&a…

深入学习 Redis - 基于 Spring Data Redis 操作 Redis

目录 一、前置工作 1.1、引入 Spring Data Redis 依赖 1.2、编写配置文件 二、Spring Data Redis 2.1、前置知识 2.2、演示 Demo 一、前置工作 1.1、引入 Spring Data Redis 依赖 1.2、编写配置文件 spring:redis:host: 127.0.0.1port: 8888二、Spring Data Redis 2.1、…

yolo系列笔记(v4-v5)

YOLOv4 YOLOv4网络详解_哔哩哔哩_bilibili 网络结构&#xff0c;在Yolov3的Darknet的基础上增加了CSP结构。 CSP的优点&#xff1a; 加强CNN的学习能力 去除计算瓶颈。 减少显存的消耗。 结构为&#xff1a; 、 其实还是类似与残差网络的结构&#xff0c;保留下采样之前…

标准IO_格式化IO之printf函数

目录 1.可变参数原理 1.1 函数参数入栈原理 1.2 可变参数如何实现&#xff1f; 1.2.1 可变参数实现原理 1.2.2 固定参数有什么用&#xff1f; 1.2.3 va_start,va_arg,va_end如何使用&#xff1f; 2.printf函数实现原理 2.1 printf函数流程 2.2 printf函数格式解析原理…

WebSocket协议解析

文章目录 概要一、WS原理1.1、帧格式 二、WS实战2.1、客户端发起协议升级请求2.2、服务端响应协议升级2.3、核心事件2.4、心跳保活 三、总结 概要 项目中的IM系统是基于WebSocket做的&#xff0c;所以这里聊一下。 说到WS&#xff0c;不得不提HTTP,HTTP是基于TCP&#xff0c;面…

Mycat分片函数详解

Mycat新一代Mysql分布式集群,大数据处理中间件,中国第一开源软件 Checkout项目 可以用eclipse的svn插件来进行项目检出,也可以用Tortoise SVN等工具检出,由于maven(M2)中的buildnumber-maven-plugin 中的SVNkit最高支持1.7的SVN仓库,因此当你用Tortoise SVN 1.8的工具或版…

聊聊原子弹之父:奥本海默

最近诺兰的电影奥本海默即将热映,其改编自Kai Bird和 Martin J. Sherwin的 2005 年Pulitzer Prize 获奖小说:“American Prometheus: The Triumph and Tragedy of J. Robert Oppenheimer”。这本小说作者研究奥本海默25年,才得以成形,可见奥神本人身上的故事曲折和传奇。 …

MP的开发流程-2

RESTful的实现等级 0级&#xff1a;传统的RPC&#xff0c;基于SOAP的WS&#xff0c;调用的服务名&#xff0c;参数放在HTTP协议的body里面&#xff0c;同时必须以POST方式提交&#xff0c;问题在于你必须清楚的知道所有服务&#xff0c;子服务&#xff0c;及其参数的信息&…

SpringBoot环境标识设置及nacos匹配配置

本地环境标识设置 本地父类maven配置 可以看到相关的分类&#xff0c;设置环境标识主要需要用到profiles; <profiles><profile><id>dev</id><properties><!-- 环境标识&#xff0c;需要与配置文件的名称相对应 --><profiles.active&…