一、 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使常驻服务端,不会出现等待多线程的问题,故使用框架时没必要考虑这部分问题了。