java的并发容器和线程池

199 阅读5分钟

一、 1.首先我们知道Java的并发实现的根本都是实现Runnable的接口,那么在java8时代中我们可以使用新的completableFuture技术可以使得多线程并发编程更加优雅。

代码如下:

import java.util.concurrent.atomic.AtomicInteger;

public class Register {
  private static AtomicInteger count = new AtomicInteger(0);

  // 注册学号
  public Student regId(Student student) {
    //这里我们所创建的Student类里面只有name和id两个成员变量即可。
    student.setId(count.incrementAndGet());
    return student;
  }
}

上面是Register类,里面实现了乐观锁,我们再进行学号的并行注册。代码实现如下:

import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.CompletableFuture;

public class StudentIDTest {
  public static void main(String[] args) {
    // 构建学生集合
    List<Student> studentList = new ArrayList<>();
    for (int i = 1; i <= 10; i++) {
      Student s = new Student();
      s.setName("学生" + i);
      studentList.add(s);
    }

    Register reg = new Register();

    studentList.forEach(s -> {
      CompletableFuture.supplyAsync(
          // 每个学生都注册学号
          () -> reg.regId(s)
        )
        // 学号注册完毕后,打印欢迎消息
        .thenAccept(student -> {
          System.out.println("你好 " + student.getName() + ", 欢迎来到春蕾中学大家庭");
        });
    });

    System.out.println("mission complate");
  }
}

上面我们可以看到是在Lambda表达式里面实现了completableFuture.supplyAsync()方法,里面的对象则是jdk内置实现了Runnable接口,这个方法则就是多线程编程,下一个thenAccept()方法参数则就是上一个方法的返回结果,本质上也是多线程编程,而且它也经常位于任务链的末端。不要将所有大任务都集中在supplyAsync里面执行。

2.我们现在如果想在注册学生学号之间再加个步骤要怎么办呢,直接看代码吧:

CompletableFuture.supplyAsync(() -> reg.regId(s))
   //中间若是有另外的任务则也是通过thenApply()方法进行执行。
  .thenApply(student -> {
    return dis.assignClasses(student);
  })
  .thenAccept(student -> {
     System.out.println("姓名:" + student.getName() + ",学号:" + student.getId() + ",班级号:" + student.getClassId());
  });

3.其中supplyAsync是静态方法,里面的thenApply和thenAccept和都是返回的completableFuture实例对象,故返回值为:

CompletableFuture<Void> cf = CompletableFuture.supplyAsync(() -> reg.regId(s))
  .thenApply(student -> {
    return dis.assignClasses(student);
  })
  .thenAccept(student -> {
     System.out.println("姓名:" + student.getName() + ",学号:" + student.getId() + ",班级号:" + student.getClassId());
  });
  ---------------------------------------------------------------------------.sout
  //下面的任务链末端不是thenAccept()方法,故不是没有返回值,所以返回值类型不能设置为Void了。
CompletableFuture<Student> cf = CompletableFuture.supplyAsync(() -> reg.regId(s))
  .thenApply(student -> {
    return dis.assignClasses(student);
  });

4.那么如果执行的任务量太多,会造成main()方法线程任务时间不匹配,导致主方法先结束而线程任务未结束的情况发生,这种就需要返回值来处理了,代码:

List<CompletableFuture> cfs = new ArrayList<>();
studentList.forEach(s -> {
  CompletableFuture<Void> cf = CompletableFuture.supplyAsync(() -> reg.regId(s))
    .thenApply(student -> {
        return dis.assignClasses(student);
    }).thenAccept(student -> {
        System.out.println("姓名:" + student.getName() + ",学号:" + student.getId() + ",班级号:" + student.getClassId());
    });

  cfs.add(cf);
});

try {
  // 等待所有的线程执行完毕
  CompletableFuture.allOf(cfs.toArray(new CompletableFuture[] {})).get();
  //上面的allof()方法也是静态方法,但是里面的参数类型必须为数组类型,况且后面的get()方法直接导致了main()方法等待,因此也直接解决了主方法和线程任务异步的问题。
} catch (Exception e) {
  e.printStackTrace();
}

需要额外强调的是:springboot的服务端是常驻程序,不会像main()方法那样执行完毕就退出了。

二、我之前说过,通过直接调用线程类来传入Runnable对象来实现线程的话是非常消耗CPU的,看代码:

public class StudentIDTest {
    public static void main(String[] args) {
for (int i = 1; i <= 4; i++) {
    Student s = new Student();
    s.setName("学生" + i);
    Register register = new Register(s);
    Thread thread = new Thread(register);
    thread.start();
}
    }
}

上面的代码可以看出来,我们得创建非常多的对象,大量的消耗内存和CPU,不光如此,Java的垃圾回收机制还会自动销毁对象,销毁对象还会消耗额外的资源,所以我们的目的是要得到复用的线程,即Thread对象,而Java恰好有线程池的技术。

线程池的创建一共有以下几步,废话不多说,直接看代码:

import org.apache.commons.lang3.concurrent.BasicThreadFactory;

import java.util.concurrent.*;

public class StudentIDTest {

  // 创建线程工厂实例
  private static final ThreadFactory namedThreadFactory = new BasicThreadFactory.Builder()//这个Builder()是一个构造函数。
    .namingPattern("studentReg-pool-%d")//这个就是线程池的命名。
    .daemon(true)//使线程池为守护线程,主方法结束后jvm则会等待守护线程运行完才结束。
    .build();

  // 创建线程等待队列实例
  //里面的构造参数表示等待线程中的个数。
  private static final BlockingQueue<Runnable> workQueue = new LinkedBlockingQueue<Runnable>(1024);

  // 创建线程池实例
  private static final ThreadPoolExecutor EXECUTOR_SERVICE = new ThreadPoolExecutor(
20,//线程池初始化主线程数量
200,//线程池最大线程数量
30,//线程池中的线程数超过主线程数(即第一个参数),若在规定时间内未使用则回收,如果填0则立即回收。
TimeUnit.SECONDS,//第三个构造函数参数的时间单位,此处指的是秒。
workQueue,//线程工厂,已经创建好了。
namedThreadFactory,//线程等待实例,已经创建好了。
new ThreadPoolExecutor.AbortPolicy()//这个指的是当任务超出队列容量时采用AbortPolicy策略处理。
      );

  public static void main(String[] args) {

  }
}

另外,上面不要忘记添加相关依赖:

<dependency>
  <groupId>org.apache.commons</groupId>
  <artifactId>commons-lang3</artifactId>
  <version>3.10</version>
</dependency>

上面的线程池创建成功后则可以直接调用线程池运行了。

public class StudentIDTest {
  public static void main(String[] args) {
    // 构建学生集合
    for (int i = 1; i <= 2000; i++) {
Student s = new Student();
s.setName("学生" + i);
Register register = new Register(s);
// 传入 Runnable 对象,运行任务,执行线程池实例的execute()方法。
EXECUTOR_SERVICE.execute(register);
    }
  }
}

注意:线程池还是没有解决掉main()方法和线程运行的时间问题,即可能线程还没运行完任务,但是主方法已经结束了,解决方法就是可以使Thread.sleep()方法里面的参数时间尽可能调长一点,这样可以使得jvm可以等待一段时间结束。

其次,我们上面所说的并发容器completableFuture实际上内部也是使用的默认线程池,故我们也可以使用自己创建的线程池,例如:

CompletableFuture.supplyAsync(
    () -> reg.regId(s),
    EXECUTOR_SERVICE//线程池
  )

从上面可以得到,线程池其实并不利于编排,只是直接将实现了Runnable()接口的对象扔到了线程池中运行罢了,所以总结可得:基础多线程——>线程池——>并发容器。

最后,忘提醒了,SpringBoot使常驻服务端,不会出现等待多线程的问题,故使用框架时没必要考虑这部分问题了。