多线程实现与管理

news2024/10/6 16:23:14

进程与线程

  • 进程 : 进程是操作系统进行资源分配的最小单位,每执行一个程序、一条命令操作系统都会启动一个进程,进程是一个程序的执行过程,当程序启动时,操作系统会把进程的代码加载到内存中,并为新进程分配一个唯一的PID、内存资源、设备等。

  • 线程:一个进程最少有一个线程去执行代码,一个进程启动后会创建一个主线程,CPU最小的调度单位是线程,每个线程会有自己独立的栈空间,但是会共享进程的内存空间。线程也有自己的TID。线程的创建也需要在进程内部进行。通常是通过编程语言提供的API(如Java的Thread类、C++的std::thread等)来创建线程。创建线程时,操作系统会在当前进程内部创建一个新的线程,并分配给该线程一定的资源。

简单来说进程是一个大单位,好比上学时的班级,而线程是这个班级的每个学生,每个学生都属于这个班级(线程属于某个进程),而每个学生可以独立的学习(学习进度不一样,学习成绩不一样) 好比每个线程执行获取到的CPU的时间片不一样,执行进度也不一样。每个学生有独立的座位,就好比每个线程都独立的栈空间。

  1. 线程是由操作系统创建并调度的资源。
  2. 线程之间的切换是CPU完成的,切换线程需要消耗大量CPU资源。
  3. 一个操作系统通常能调度的线程是有限的,可结合线程池使用。

Java多线程编程

一个java命令就会启动一个进程,例如 java -jar 、 java xxx ,而启动一个进程以后,JVM就会创建一个main线程,由这个main线程开始执行main方法的代码。

  • Java实现多线程的方式
  1. 继承Thread或者实现Runable接口
public class ThreadTest01 {

    public static void main(String[] args) {

        Thread thread = new MyThread();
        thread.start(); //调用start方法开启多线程

        Thread thread1 = new Thread(new MyThread2());
        thread1.start();

    }

}


class MyThread extends Thread{

    @Override
    public void run() {
        System.out.println(Thread.currentThread().getName());
    }
}

class MyThread2 implements Runnable{

    @Override
    public void run() {
        System.out.println(Thread.currentThread().getName());
    }
}

实现Runable的方式是在创建Thread类时当作参数传递进去的,在调用了run方法时,其实还是调用了Runable接口的run方法。

在这里插入图片描述

在启动线程时,不要直接调用run方法,直接调用run 方法不会开启新的线程,而是相当于仅仅是调用了一个普通方法,而start方法的签名如下:

    public synchronized void start() {
        /**
         * This method is not invoked for the main method thread or "system"
         * group threads created/set up by the VM. Any new functionality added
         * to this method in the future may have to also be added to the VM.
         *
         * A zero status value corresponds to state "NEW".
         */
        if (threadStatus != 0)
            throw new IllegalThreadStateException();

        /* Notify the group that this thread is about to be started
         * so that it can be added to the group's list of threads
         * and the group's unstarted count can be decremented. */
        group.add(this);

        boolean started = false;
        try {
            start0();
            started = true;
        } finally {
            try {
                if (!started) {
                    group.threadStartFailed(this);
                }
            } catch (Throwable ignore) {
                /* do nothing. If start0 threw a Throwable then
                  it will be passed up the call stack */
            }
        }
    }

    private native void start0();

而调用了start方法之后,方法内部又调用了start0方法,而start0方法是个native方法,表示此方法是JVM中的C++代码实现的,Java本身无法实现,这就是开启多线程的关键。 JVM需要等所有线程都运行完成以后才会退出。

  • 线程实例与线程类的方法
  1. interrupt :中断线程的方法
  2. join : 优先执行线程调用者的run方法
  3. setName :设置线程名称
  4. setDaemon : 设置为守护线程
  5. setPriority : 设置线程优先级
  6. Thread.sleep : 让线程休眠,进入TIMED_WAITING状态。

最常用的方法有上述几个,用一个程序来演示一下

public class ThreadTest01 {

    @Test
    public void test1() throws Exception {
        Thread thread = new MyThread();
        thread.setName("t1");
        thread.setPriority(6); //设置线程的优先级
        thread.start(); //调用start方法开启多线程
        for (int i = 0; i < 100; i++) {
            System.out.println(Thread.currentThread().getName() + " ===> " + i);
            Thread.sleep(100);
            if (i >= 60){
//                thread.join();//优先让t1线程执行完成
                thread.interrupt();//中断t1线程的执行、注意、只能是中断t1线程,而无法中断其他已经运行的线程

            }
        }

    }

}


class MyThread extends Thread {

    @SneakyThrows
    @Override
    public void run() {
        for (int i = 0; i < 100; i++) {
            System.out.println(Thread.currentThread().getName() + " ===> " + i);
            Thread.sleep(100);
        }
    }
}

setDaemon方法:设置线程为守护线程,当所有非守护线程执行完成后,守护线程自动中断,典型的守护线程应用有 JVM 中的 GC垃圾回收线程。

  • 线程安全问题

线程安全问题的本质就是多线程在对同一份数据进行读写时,与期望的逻辑不相符,由于CPU在执行多线程时,是来回切换执行的,这种操作极有可能导致线程安全问题。

这里的同一份数据是表示能够通过变量名或者类名引用到的某个基本数据类型或者应用数据类型,例如静态变量,多个线程共享一个引用变量等。也就是说,只要多个线程能到某个引用或者基本数据类型,就可能会产生线程安全问题。

public class ThreadTest02 {

    public static void main(String[] args) throws InterruptedException {

        T1 t1 = new T1();
        t1.start();
        T2 t2 = new T2();
        t2.start();

        //先阻塞main线程
        t1.join();
        t2.join();

        System.out.println(Counter.count);

    }

}

class Counter{

    public static int count = 0;

}

class T1 extends Thread{

    @Override
    public void run() {
        for (int i = 0; i < 1000; i++) {
            Counter.count++;
        }
    }
}

class T2 extends Thread{

    @Override
    public void run() {
        for (int i = 0; i < 1000; i++) {
            Counter.count--;
        }
    }
}

在上述这个案例中 ,T1线程 + 1000次,而 t2 线程 - 1000 次,最后的结果理应还是0 .
在这里插入图片描述
但是多运行几次,就会发现结果大概率不是0,这就是线程安全问题。

如何解决线程安全问题? — 加锁

  • 加锁实现线程安全

加锁的意义就是保证同一时刻的方法或者代码块,只会有一个线程执行。将上述代码进行如下改造

public class ThreadTest02 {

    public static void main(String[] args) throws InterruptedException {

        T1 t1 = new T1();
        t1.start();
        T2 t2 = new T2();
        t2.start();

        //先阻塞main线程
        t1.join();
        t2.join();

        System.out.println(Counter.count);

    }

}

class Counter{

    public static int count = 0;

    public static synchronized void add(){
        count++;
    }

    public static synchronized void dec(){
        count--;
    }

}

class T1 extends Thread{

    @Override
    public void run() {
        for (int i = 0; i < 1000; i++) {
            Counter.add();
        }
    }
}

class T2 extends Thread{

    @Override
    public void run() {
        for (int i = 0; i < 1000; i++) {
            Counter.dec();
        }
    }
}

在Counter类上的两个方法签名上分别加了synchronized 关键字,表示这个方法是一个同步方法,任意时刻只会有一个线程进入此方法去执行。加上了synchronized 关键字以后,保证了add和dec方法各自都可以原子性的执行1000次,所以 无论运行多少次,最终结果都会是0.

synchronized 关键字如果是写在了静态方法上,锁的是当前类的class对象,如果写在了实例方法上,则锁的是当前实例对象,如果线程安全是以对象为单位的,则不同可以用对象锁,如果线程安全是以类为单位的,则可以用类锁。

    public static  void add(){
        synchronized (new Object()){
            count++;
        }
    }

上述的 synchronized 锁的对象是有严重的线程安全问题的,因为每次锁的都是一个新创建出来的新对象,这个对象是刚创建出来的,对象头中的锁信息没有,则每次来一个线程都可以进入方法执行。

同时,synchronized 是一个可重入锁,看下面这个代码块,add方法是一个同步方法,在add方法内部又调用了dec方法,但是dec方法也是需要加锁的,此时只有进入了add方法线程可以进入dec方法,因为都是用的一把锁,这就是可重入锁,像是进入了add方法,但不能进入dec方法的就是不可重入锁。

    public static synchronized void add(){
            count++;
            dec();
    }

    public static synchronized void dec(){
        count--;
    }
  • 线程之间的通信

线程间的通信主要是有3个:

  1. wait : 让执行了wait方法的线程进入等待状态,同时释放已经获取的锁,进入了wait状态的线程不参与锁的竞争。
  2. notify :唤醒一个当前锁对象调用了wait方法的线程。
  3. notifyAll : 唤醒所有当前锁对象调用了wait方法的线程。

上述三个方法有以下共同点:

  • 必须是在同步方法内调用,也就是synchronized 方法或者synchronized 代码块中调用
  • 调用的对象必须的同步的锁对象 也就是 synchronized 锁的对象。

假设有有一个存取队列的场景,有A、B两个线程,一个线程去队列中存数据,另一个线程取队列中的数据,但是取数据的线程不知道存的线程什么时候放,如果使用while去一直监听的话,这样会造成系统资源的浪费,更好的一种做法就是取线程如果获取不到数据,则进入等待状态,待存线程存入数据后 再通过取线程去获取。

public class ThreadTest03 {

    public static void main(String[] args) {

        MyQueue myQueue = new MyQueue();

        Thread thread = new Thread(() -> {

            while (true){
                //存数据
                myQueue.addTask();
                try {
                    Thread.sleep(20);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }

            }
        });

     new Thread(() -> {
            while (true) {
                myQueue.getTask();
            }
        }).start();

        new Thread(() -> {
            while (true) {
                myQueue.getTask();
            }
        }).start();

        new Thread(() -> {
            while (true) {
                myQueue.getTask();
            }
        }).start();

        new Thread(() -> {
            while (true) {
                myQueue.getTask();
            }
        }).start();

        new Thread(() -> {
            while (true) {
                myQueue.getTask();
            }
        }).start();

        thread.start();

    }

}

class MyQueue{

    private Queue<String> queue = new ArrayDeque<>();

    private Object lock = new Object();

    public void addTask(){
        synchronized (lock){
            String s = UUID.randomUUID().toString();
            queue.add(s);
            //唤醒所有处于wait状态的线程,线程被唤醒之后,参与锁的竞争
            lock.notifyAll();
        }
    }

    public  void getTask(){
        synchronized (lock){
            while (queue.isEmpty()){
                try {
                    System.out.println(Thread.currentThread().getName() + "进入等待状态");
                    lock.wait();//这里必须使用lock对象去调用wait
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
            String poll = queue.poll();
            System.out.println(Thread.currentThread().getName() + " 获取到的数据是 ===> " + poll);
        }
    }

}

这里要值得注意的是,当调用了notifyAll方法后唤醒了等待状态的线程以后,这些线程需要再次获得锁,才能够去执行剩余的代码。

  • 谈一谈死锁

现在有 t1、t2两个线程 同时有 A B 两把锁,t1线程的代码执行顺序是先获取A锁 再获取B锁,而t2 线程的代码执行顺序是先获取B锁 再获取A锁,如下代码:

public class ThreadTest04 {

    public static void main(String[] args) {

        Object lock1 = new Object();
        Object lock2 = new Object();

        new Thread(() -> {

            synchronized (lock1){
                System.out.println("A线程 获取到了lock1");
                try {
                    Thread.sleep(10);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                synchronized (lock2){
                    System.out.println("A线程 获取到了lock2");
                }
            }

        }).start();

        new Thread(() -> {

            synchronized (lock2){
                System.out.println("B线程 获取到了lock2");
                synchronized (lock1){
                    System.out.println("B线程 获取到了lock1");
                }
            }

        }).start();

    }

}

此时程序的控制台输出如下:

在这里插入图片描述
死锁一旦发生,除非通过借助外力的方式终止,否则程序本身是无法停止的。

并发编程

  • ReentrantLock

ReentrantLock 相比于 synchronized关键字实现同步,提供了更灵活阻塞等待的控制,synchronized 在其他线程获取不到锁时,是一直处于阻塞的状态的,而ReentrantLock 提供了获取不到锁的超时机制。

class Counter01 {

    private int nums = 0;

    private ReentrantLock lock = new ReentrantLock();

    public void add() throws InterruptedException {
        if ( lock.tryLock(3, TimeUnit.SECONDS)){
            try {
                System.out.println(Thread.currentThread().getName() + " 获取到了锁");
            }finally {
                lock.unlock();
            }
        }else {
            //没有获取到锁
            System.out.println(Thread.currentThread().getName() + " 没有获取到锁,放弃执行");
        }
    }
    
    public void  dec(){
        try {
            lock.lock();
            System.out.println(Thread.currentThread().getName() + " 获取到了锁");
        }finally {
            lock.unlock();
        }
        
        
    }

}

ReentrantLock 锁 不同于synchronized,后者是Java语言层面提供的支持,当代码执行完成或者出现异常后,JVM会自动释放锁,而ReentrantLock 不行,必须使用try + finally 最后手动释放锁。

  • ReentrantLock 支持的 wait 与 notify

如果使用了ReentrantLock 锁,如何实现 synchronized锁的 wait 、notify 、notifyAll ?

    private ReentrantLock lock = new ReentrantLock();

    private Condition condition = lock.newCondition();
    
    public void condition() throws InterruptedException {
        
        condition.await();//相当于wait
        condition.signal(); //相当于 notify
        condition.signalAll(); //相当于 notiflAll
    }

使用ReentrantLock提供的newCondition API 来分别代替 wait notify notiflAll 。

  • 读写锁

在之前的队列存储案例中,多个线程可以分别取和存,但是存取方法使用了同一把锁,这就导致,两个方法在任意时刻只能执行其中的某一个方法,而如果有一种场景是读可以多线程,但写的时候不能进行读,同时也只会有一个线程允许写,其他写线程和读线程必须等待,这种适用于读多写少的场景就是读写锁。

public class ThreadTest06 {

    public static void main(String[] args) {

        Article article = new Article();

        new Thread(() -> {
            while (true){
                article.update();
                try {
                    Thread.sleep(3000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }).start();

        for (int i = 0; i < 3; i++) {
            new Thread(() -> {
               while (true){
                   article.getArticle();
                   try {
                       Thread.sleep(500);
                   } catch (InterruptedException e) {
                       e.printStackTrace();
                   }
               }
            }).start();
        }

    }

}

class Article{

    private ReadWriteLock readWriteLock = new ReentrantReadWriteLock();

    private Lock readLock = readWriteLock.readLock();

    private Lock writeLock = readWriteLock.writeLock();

    private String content = "";

    public void update(){
        try {
            writeLock.lock();
            System.out.println(Thread.currentThread().getName() + "正在更新数据............");
            Thread.sleep(5000);
            content = UUID.randomUUID().toString();
            System.out.println(Thread.currentThread().getName() + "数据更新完成 !!!");
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            writeLock.unlock();
        }
    }

    public void getArticle(){
        try {
            readLock.lock();
            System.out.println(Thread.currentThread().getName() + "获取到的数据是:" + content );
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            readLock.unlock();

        }

    }

}

上述代码示例可以比较好的说明读写锁的问题。要注意的一个地方就是如果把 Thread.sleep写在了方法内部,则 Thread.sleep在执行的时候是不会释放锁的,如果写线程不释放锁,则读线程也进不去,此时如果写线程一直while true的话就会造成大量的写请求。

 while (true){
                article.update();
             }

而如果在while true的外部加一个Thread.sleep 此时就会让线程休眠,而且休眠的时间也不会占用锁,那么读线程就可以获取到锁。

  • 信号量 Semaphore

信号量的作用是允许可以灵活的控制某个方法在任意时刻最多有多少个线程可以访问。

public class ThreadTest07 {

    public static void main(String[] args) {

        MySemaphore mySemaphore = new MySemaphore();
        for (int i = 0; i < 50; i++) {
            new Thread(() -> {
                try {
                    mySemaphore.test();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }).start();
        }

    }

}


class MySemaphore {

    private Semaphore semaphore = new Semaphore(5);

    public void test() throws InterruptedException {
        try {
            semaphore.acquire();
            System.out.println(Thread.currentThread().getName() + " 进入执行 !");
            Thread.sleep(2000);
        } finally {
            semaphore.release();
        }

    }

}

使用JDK提供的 Semaphore 类可以很好的实现信号量线程控制。

  • JDK线程池

在实际项目开发中,很少会直接创建线程,因为频繁的创建线程以及销毁线程会造成系统资源的浪费,一般都会结合池化思想,使用线程池来处理多任务。类似池化思想的还有 数据库连接池、Http请求池、Socket IO池等等。
线程池的总体设计思想就是 当接收到任务时,判断是否还有空余线程,如果有空余线程,则直接执行、如果没有,则判断队列满没满,如果满了,则执行拒绝策略,如果没满,则加入队列。

public class ThreadTest08 {

    public static void main(String[] args) throws InterruptedException {

        //创建固定数量的线程池
        ExecutorService executorService = Executors.newFixedThreadPool(5);
        for (int i = 0; i < 50; i++) {
            executorService.submit(() -> {
                System.out.println(Thread.currentThread().getName());
            });
        }

        //
        ExecutorService executorService1 = Executors.newCachedThreadPool();
        for (int i = 0; i < 50; i++) {
            executorService1.submit(() -> {
                System.out.println(Thread.currentThread().getName());
            });
        }

        ExecutorService executorService2 = Executors.newSingleThreadExecutor();
        for (int i = 0; i < 50; i++) {
            executorService2.submit(() -> {
                System.out.println(Thread.currentThread().getName());
            });
        }

        ScheduledExecutorService executorService3 = Executors.newScheduledThreadPool(10);
        for (int i = 0; i < 50; i++) {

            executorService3.scheduleAtFixedRate(() -> {
                System.out.println(Thread.currentThread().getName());
            },3, 3,TimeUnit.SECONDS);

            executorService3.scheduleWithFixedDelay(() -> {
                System.out.println(Thread.currentThread().getName());
            },3, 3,TimeUnit.SECONDS);

        }

        Thread.sleep(200);

    }

}

线程池的类型主要有以下几种

  1. Executors.newFixedThreadPool 创建固定数量的线程池
  2. Executors.newCachedThreadPool 创建动态数量的线程池
  3. Executors.newSingleThreadExecutor 创建单个任务的线程池,同一时刻只能执行一个任务
  4. Executors.newScheduledThreadPool 创建定时任务类型的线程池,定时任务主要有两种类型FixedRate、FixedDelay,例如同样是每隔3s执行一次,FixedRate 不包含任务的执行时间在内,而 FixedDelay 是包含任务的执行时间在内的。
  • Future、Callable
    使用Future以及Callale实现有返回值的多线程
public class TheadTest09 {

    public static void main(String[] args) throws ExecutionException, InterruptedException {

        ExecutorService executorService = Executors.newFixedThreadPool(5);

        Future<String> future = executorService.submit(() -> UUID.randomUUID().toString());

        String s = future.get();//可能会阻塞

        System.out.println("线程返回值是:" + s);

        executorService.shutdown();

    }

}
  • CompletableFuture 异步编排
    CompletableFuture 是 JDK1.8 新增的一个异步任务编排解决方案,可以结合线程池实现多任务并发等。
    CompletableFuture API的命名特点:

runxxx:处理无返回值的异步任务
supplyxxx:处理有返回值的异步任务
thenAccept:处理正常结果
exceptional:处理异常结果
thenApplyAsync:用于串行化另一个CompletableFuture
anyOf()和allOf:用于并行化多个CompletableFuture

简而言之,一个CompletableFuture 对象就表示一个异步任务或者是具有异步任务处理的能力。

public class ThreadTest10 {

    public static void sleep(int mills) {
        try {
            Thread.sleep(mills);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }

    @Test
    public void test01() throws Exception {
        CompletableFuture<String> future = CompletableFuture.supplyAsync(() -> {
            return UUID.randomUUID().toString();
        });

        sleep(3000);

        future.thenAccept((val) -> {
            System.out.println("异步任务的返回值是:" + val);
        });

        future.exceptionally((ex) -> {
            System.out.println("异步任务的异常信息:" + ex);
            return null;
        });

        sleep(10000);
    }

    @Test
    public void test2() throws Exception {
        CompletableFuture<String> f1 = CompletableFuture.supplyAsync(() -> {
            sleep(3000);
            return UUID.randomUUID().toString();
        });

        CompletableFuture<String> f2 = CompletableFuture.supplyAsync(() -> {
            sleep(1000);
            return UUID.randomUUID().toString();
        });

        //组合API,监听任意一个任务成功即可
        CompletableFuture<Object> data = CompletableFuture.anyOf(f1, f2);
        data.thenAccept((val) -> {
            System.out.println("返回结果:" + val);
        });

        Thread.sleep(10000);

    }

    public static void main(String[] args) throws InterruptedException {
        CompletableFuture<String> f1 = CompletableFuture.supplyAsync(() -> {
            sleep(3000);
            return UUID.randomUUID().toString();
        });

        CompletableFuture<String> f2 = CompletableFuture.supplyAsync(() -> {
            sleep(1000);
            return UUID.randomUUID().toString();
        });

        //组合API,监听任意一个任务成功即可
        CompletableFuture<Void> f3 = CompletableFuture.allOf(f1, f2);
        
        f3.thenAccept((val) -> {
            System.out.println("返回结果:" + val);
        });

        Thread.sleep(10000);

    }

}
  • ForkJoin
    Java 7开始引入了一种新的Fork/Join线程池,它可以执行一种特殊的任务:把一个大任务拆成多个小任务并行执行。每个小任务开启线程独立计算,然后计算结合向上递归,最终计算出整个大任务的结果。

案例:使用ForkJoin 对数组进行分段求和。

public class ForkJoinTest {

    public static void main(String[] args) {
        int size = 1000000;
        Random random = new Random();
        int[] nums = new int[size];
        for (int i = 0; i < size; i++) {
            nums[i] = random.nextInt(100);
        }

        long l = System.currentTimeMillis();

        int expected = 0;
        for (int i = 0; i < nums.length; i++) {
            expected = expected + nums[i];
        }
        long end = System.currentTimeMillis();
        System.out.println("ms " +  (end - l) + " result " + expected);

        //采用分治思想 将size大小的数组拆分为 1000 一组
        SumTaskArr sumTaskArr = new SumTaskArr(nums, 0, size);
        Long invoke = ForkJoinPool.commonPool().invoke(sumTaskArr);
        System.out.println(invoke);
    }

}

/**
 * 相同思想的实现还有
 */
class SumTaskArr extends RecursiveTask<Long>{

    private int[] arr;

    //开始下标
    private int start;

    //结束下标
    private int end;

    private int threashold = 1000;

    public SumTaskArr(int[] arr,int start,int end){
        this.arr = arr;
        this.start = start;
        this.end = end;
    }

    /**
     * 每个任务的返回结果,这里只需要处理每个任务即可,返回后Root任务自动累加
     * @return
     */
    @Override
    protected Long compute() {
        if ((end - start) > threashold){
            try {
                Thread.sleep(100);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            int middle = (start + end) / 2; //继续拆分
            SumTaskArr childtask1 = new SumTaskArr(arr, start, middle);
            SumTaskArr childtask2 = new SumTaskArr(arr, middle, end);
            invokeAll(childtask1,childtask2);
            Long join = childtask1.join();
            Long join1 = childtask2.join();
            System.out.println(Thread.currentThread() + "  ==> " + (join + join1));
            return join + join1;
        }else {
            //如果不大于阈值,则直接计算
            long ex = 0;
            for (int i = start; i < end; i++) {
                ex = ex + arr[i];
            }
            return ex;
        }
    }
}

ForkJoin 采用的思想叫分治思想,当处理一个大任务比较困难的时候 把任务拆分成多个小任务做,此类思想的算法实现还有归并排序以及快速排序。这个案例最关键的是这三行代码

 			invokeAll(childtask1,childtask2);
            Long join = childtask1.join();
            Long join1 = childtask2.join();

invokeAll 表示 继续开启新线程执行childtask1、childtask2 的compute代码,但是注意 childtask1.join(); 的作用就是优先执行完 childtask1的代码,但是不影响 childtask1、childtask2 并发执行,每个新的线程都会阻塞在开启的子线程上,知道最后的线程完成计算任务并返回。

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

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

相关文章

Python爬虫——scrapy_日志信息以及日志级别

日志级别&#xff08;由高到低&#xff09; CRITICAL&#xff1a; 严重错误 ERROR&#xff1a; 一般错误 WARNING&#xff1a; 警告 INFO&#xff1a; 一般警告 DEBUG&#xff1a; 调试信息 默认的日志等级是DEBUG 只要出现了DEBUG或者DEBUG以上等级的日志&#xff0c;那么这些…

开集输出和开漏输出

​​​​​​ 首先指明一下以下8中GPIO输入输出模式&#xff1a; GPIO_Mode_AIN 模拟输入&#xff1b; GPIO_Mode_IN_FLOATING 浮空输入&#xff1b; GPIO_Mode_IPD 下拉输入&#xff1b; GPIO_Mode…

JVM面试题-2

1、有哪几种垃圾回收器&#xff0c;各自的优缺点是什么&#xff1f; 垃圾回收器主要分为以下几种&#xff1a;Serial、ParNew、Parallel Scavenge、Serial Old、Parallel Old、CMS、G1&#xff1b; Serial:单线程的收集器&#xff0c;收集垃圾时&#xff0c;必须stop the worl…

RPM包的概念以及制作过程

RPM包的概念以及制作过程 1. 软件包管理工具的背景介绍2. RPM&#xff08;Red-Hat Package Manager&#xff09;2.1 rpm包的命名规范2.2 rpm的基础命令2.3 安装与卸载 3. RPM包的制作3.1 源码包的制作3.2 .spec配置文件的构建3.3 rpmbuild命令编译验证 4. 软件仓库制作4.1 安装…

QChart:数据可视化(用图像形式显示数据内容)

1、数据可视化的图形有&#xff1a;柱状/线状/条形/面积/饼/点图、仪表盘、走势图&#xff0c;弦图、金字塔、预测曲线图、关系图、数学公式图、行政地图、GIS地图等。 2、在QT Creator的主页面&#xff0c;点击 欢迎》示例》右侧输入框 输入Chart&#xff0c;即可查看到QChar…

鲁棒优化入门(5)—Matlab+Yalmip求解鲁棒优化编程实战

之前的博客&#xff1a;鲁棒优化入门&#xff08;二&#xff09;——基于matlabyalmip求解鲁棒优化问题 去年发布了使用Yalmip工具箱求解鲁棒优化问题的博客之后&#xff0c;陆陆续续有朋友问我相关的问题&#xff0c;有人形容从学习这篇博客到求解论文中的鲁棒优化问题&#x…

redis--主从复制

redis主从复制 Redis 主从复制是一种用于实现数据复制和数据备份的机制&#xff0c;它允许将一个 Redis 服务器的数据复制到其他 Redis 服务器上。主从复制在 Redis 中通常用于构建高可用性架构、读写分离以及数据分析等场景。 主从复制的角色 主服务器&#xff08;Master&a…

互斥锁、自旋锁、读写锁和文件锁

互斥锁 互斥锁&#xff08;mutex&#xff09;又叫互斥量&#xff0c;从本质上说是一把锁&#xff0c;在访问共享资源之前对互斥锁进行上锁&#xff0c;在访问完成后释放互斥锁&#xff08;解锁&#xff09;&#xff1b;对互斥锁进行上锁之后&#xff0c;任何其它试图再次对互斥…

python中文热词统计demo

背景 老人家不识字&#xff0c;在城市生活不便&#xff0c;喜欢去基督教堂&#xff0c;但是听不懂&#xff0c;也难以和姊妹们(老头老太太们)交流。于是想教他识字&#xff0c;从哪里教起呢&#xff0c;不如从 《圣经》的常用字词开始吧&#xff0c;于是花了几分钟把《圣经》热…

LeetCode 周赛上分之旅 #40 结合特征压缩的数位 DP 问题

⭐️ 本文已收录到 AndroidFamily&#xff0c;技术和职场问题&#xff0c;请关注公众号 [彭旭锐] 和 BaguTree Pro 知识星球提问。 学习数据结构与算法的关键在于掌握问题背后的算法思维框架&#xff0c;你的思考越抽象&#xff0c;它能覆盖的问题域就越广&#xff0c;理解难度…

构建 NodeJS 影院微服务并使用 docker 部署它(02/4)

一、说明 构建一个微服务的电影网站&#xff0c;需要Docker、NodeJS、MongoDB&#xff0c;这样的案例您见过吗&#xff1f;如果对此有兴趣&#xff0c;您就继续往下看吧。 图片取自网络 — 封面由我制作 这是✌️“构建 NodeJS 影院微服务”系列的第二篇文章。 二、对第一部分的…

8.3.tensorRT高级(3)封装系列-tensor封装,索引计算,内存标记及自动复制

目录 前言1. Tensor封装总结 前言 杜老师推出的 tensorRT从零起步高性能部署 课程&#xff0c;之前有看过一遍&#xff0c;但是没有做笔记&#xff0c;很多东西也忘了。这次重新撸一遍&#xff0c;顺便记记笔记。 本次课程学习 tensorRT 高级-tensor封装&#xff0c;索引计算&a…

cuOSD(CUDA On-Screen Display Library)库的学习

目录 前言1. cuOSD1.1 Description1.2 Getting started1.3 For Python Interface1.4 Demo1.5 Performance Table 2. cuOSD案例2.1 环境配置2.2 simple案例2.3 segment案例2.4 segment2案例2.5 polyline案例2.6 comp案例2.7 perf案例 3. cuOSD浅析3.1 simple_draw函数 4. 补充知…

MacBook上有Face ID的梦想还没破灭,但是为什么迟迟不来呢

苹果公司详细介绍了Face ID与Touch ID相比的优势&#xff0c;尤其是在安全方面。因此&#xff0c;令人惊讶的是&#xff0c;该功能还没有进入MacBook&#xff0c;尤其是在显示方面。值得庆幸的是&#xff0c;一项新专利表明&#xff0c;在某个时候&#xff0c;这可能是一种可能…

《修图大杀器》PS beta 25.0最新版安装(无需魔法)和Draggan(拖拽式修图)安装

个人网站&#xff1a;https://tianfeng.space 文章目录 psbeta下载安装1.注册2.安装ps beta2.安装神经网络滤镜3.使用 Draggan下载安装 psbeta下载安装 链接&#xff1a;https://pan.baidu.com/s/1XbxSAFoXh0HDz6YbkrAzDg 提取码&#xff1a;e8pn 1.注册 https://account.a…

C++--深入类和对象(下)

续接上篇&#xff0c;接着来谈我们的类和对象的深入的知识&#xff0c;话不多说&#xff0c;我们即刻出发...... 目录 1.友元 1.1友元函数 输出流运算符的重载 1.2友元类 2.再谈构造函数 2.1构造函数体赋值和初始化列表 构造函数体赋值为何不能叫做初始化&#xff1f; …

一个模型解决所有类别的异常检测

文章目录 一、内容说明二、相关链接三、概述四、摘要1、现有方法存在的问题2、方案3、效果 五、作者的实验六、如何训练自己的数据1、数据准备2、修改配置文件3、代码优化修改4、模型训练与测试 七、结束 一、内容说明 在我接触的缺陷检测项目中&#xff0c;检测缺陷有两种方法…

TMS320C54X 的软件编程

1、DSP 编程工具与流程 DSP 的设计目标是进行数字信号处理&#xff0c;在硬件设计的基础上选择好一定的优化算法并 通过编程在 DSP 芯片上实现是 DSP 技术的核心内容。对 DSP 进行编程&#xff0c;目前最有效的语言 工具仍是 DSP 汇编语言&#xff0c;同时为方便用户用高级语言…

每日一题之数值的整数次方

数值的整数次方 描述&#xff1a; 实现函数 double Power(double base, int exponent)&#xff0c;求 base 的 exponent 次方。 注意&#xff1a; 1.保证base和exponent不同时为0。 2.不得使用库函数&#xff0c;同时不需要考虑大数问题 3.有特殊判题&#xff0c;不用考虑小数…

mysql+jdbc+servlet+java实现的学生在校疫情信息打卡系统

摘 要 I Abstract II 主 要 符 号 表 i 1 绪论 1 1.1 研究背景 1 1.2 研究目的与意义 2 1.3 国内外的研究情况 2 1.4 研究内容 2 2 系统的开发方法和关键技术 4 2.1 开发方法 4 2.1.1 结构化开发方法 4 2.1.2 面向对象方法 4 2.2 开发技术 4 2.2.1 小程序开发MINA框架 4 2.2.2 …