Java 实现多线程
1、继承Thread类
优点:简单易用,可以直接操作线程。
访问当前线程,无需使用Thread.currentThread()方法,直接使用this即可获得当前线程
缺点:扩展性较差,继承Thread类后,不能再继承其他类。
public class TreadDemo extends Thread {
//保证多线程中,原子性
private AtomicInteger count;
public TreadDemo() {
}
public TreadDemo(AtomicInteger count) {
this.count = count;
}
@Override
public void run() {
System.out.println(this.getName() + " : " + count + 1);
System.out.println(Thread.currentThread().getName() + " : " + count + 1);
}
public static void main(String[] args) {
TreadDemo treadDemo1 = new TreadDemo(new AtomicInteger(1));
treadDemo1.setName("treadDemo1");
treadDemo1.start();
TreadDemo treadDemo2 = new TreadDemo(new AtomicInteger(2));
treadDemo2.setName("treadDemo2");
treadDemo2.start();
}
}
2、实现Runnable接口
特点:run方法没有返回值。
run方法无法抛出异常。
优点:可以实现多个线程共享同一个目标对象,适合多个相同线程处理同一份资源的情况。
缺点:不能直接操作线程,需要借助Thread类或线程池来运行。
访问当前线程,则必须使用Thread.currentThread()方法
public class RunableDemo implements Runnable {
@Override
public void run() {
for (int i = 0; i < 5; i++) {
System.out.println(Thread.currentThread().getName() + " - " + i);
try {
Thread.sleep(1000); // 模拟耗时操作
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
public static void main(String[] args) {
Thread tread = new Thread(new RunableDemo());
tread.setName("RunableDemo thread");
tread.start();
}
}
3、实现Callable接口
特点:call方法必须有返回值。
call方法可以抛出checked exception。
优点:可以获取线程的执行结果,支持返回值。
缺点:相比Runnable接口,实现相对复杂。
访问当前线程,则必须使用Thread.currentThread()方法
public class CallableDemo implements Callable {
/**
* 抛出异常,会导致剩下的线程中断不执行
* @return
* @throws Exception
*/
@Override
public String call() throws Exception {
for (int i = 0; i < 8; i++) {
System.out.println(Thread.currentThread().getName() + " - " + i);
Thread.sleep(1000); // 模拟耗时操作
if (i == 6) {
int a = 0;
throw new Exception("Exception");
}
}
return "任务完成";
}
public static void main(String[] args) {
CallableDemo callableDemo = new CallableDemo();
FutureTask<String> futureTask = new FutureTask(callableDemo);
Thread thread = new Thread(futureTask);
tread.setName("CallableDemo thread");
thread.start();
try {
// 获取Callable返回结果
String result = futureTask.get();
System.out.println(result + ".........");
} catch (Exception e) {
e.printStackTrace();
}
}
}
4、线程池
优点:提高了线程的利用率,降低了资源消耗,提高了响应速度,线程可管理。
缺点:需要合理配置线程池参数,避免出现线程过多或过少的情况。
/**
* 一个项目可以有多个线程池
* 原因是:不同类型的任务可能有不同的执行时间、优先级、依赖关系等,如果放到同一个线程池中,就可能会出现以下几种情况:
*
* 如果一个任务执行时间过长,或者出现异常,那么它就会占用线程池中的一个线程,导致其他任务无法及时得到执行,影响系统的吞吐量和响应时间。
* 如果一个任务的优先级较低,或者不是很重要,那么它就可能抢占线程池中的一个线程,导致其他任务无法及时得到执行,影响系统的可用性和正确性。
* 如果一个任务依赖于另一个任务的结果,或者需要等待另一个任务的完成,那么它就可能造成线程池中的一个线程被阻塞,导致其他任务无法及时得到执行,甚至导致死锁的问题
*/
public class ThreadPoolDemo {
/**
* 线程池
*/
private static ExecutorService executor = initDefaultExecutor();
/**
* 统一的获取线程池对象方法
*/
public static ExecutorService getExecutor() {
return executor;
}
private static final int DEFAULT_THREAD_SIZE = 5;
private static final int DEFAULT_QUEUE_SIZE = 1024 * 3;
private static ExecutorService initDefaultExecutor() {
int cpuCout = Runtime.getRuntime().availableProcessors();
System.out.println("CPU COUNT: " + cpuCout);
return new ThreadPoolExecutor(DEFAULT_THREAD_SIZE, // 最小核心线程数
DEFAULT_THREAD_SIZE,// 最大线程数,当队列满时,能创建的最大线程数
300, TimeUnit.SECONDS,// 空闲线程超过核心线程时,回收该线程的最大等待时间
new ArrayBlockingQueue<>(DEFAULT_QUEUE_SIZE),// 阻塞队列大小,当核心线程使用满时,新的线程会放进队列
new CustomizableThreadFactory("ThreadPoolDemo"),// 自定义线程名
new ThreadPoolExecutor.CallerRunsPolicy());// 线程执行的拒绝策略
}
}
最核心的三个参数:
corePoolSize:核心线程数(能即时处理的线程)
CPU密集型任务(线程池数 = CPU核心数 N + 1),太多了就阻塞等待了,但是最好也不要设置为N,设置为 N + 1 的目的是:防止线程偶发的缺页中断,或者其它原因导致的任务暂停带来的影响。(类似内存中进行排序就是CPU密集型任务)
I/O密集型任务(线程池数 = CPU 核心数 N * 2),I/O并不会消耗过多CPU资源,所以设置为2N是没有问题的。(类似磁盘IO、网络传输、文件读取就是IO密集型任务)
maximumPoolSize:最大线程数(当线程数超过核心线程数,并且阻塞队列也满了,就会扩充线程池大小为 maximumPoolSize,如果线程池的数量已经达到了最大线程数,又来了一个线程,就会使用拒绝策略来阻止它)
workQueue: 队列可以用来保存暂时无法处理的任务(线程数如果超过核心线程了,新来的任务就会被放入队列)
其它常见参数:
keepAliveTime:线程数量大于 corePoolSize时,没有新的任务提交,核心线程外的线程不会立即销毁,而是会等待,直到等待时间超过了 keepAliveTime 才会被回收销毁;
unit:keepAliveTime 参数的时间单位
threadFactory:executor 创建线程时用到,使用默认即可
handler:拒绝策略(阻塞队列满了,线程池扩容了扩大为最大线程数了,且线程已经占满了线程池,再来一个线程,就会使用拒绝策略进行拒绝)
拒绝策略:
AbortPolicy:抛异常
CallerRunsPolicy:把当前运行 excute() 方法的线程拿来处理当前任务(但是会影响整体性能)
DiscardPolicy:不处理任务直接丢弃
DiscradOldestPolicy:丢弃最早的未处理的任务请求。
线程池创建的两种方式
通过 ThreadPoolExecutor 构造函数创建(推荐)
通过 Executor 框架的工具类 Executors 来创建(不推荐)
Executors 创建线程池? 它内部的实现会导致一些问题:
FixedThreadPool 和 SingleThreadExecutor:使用无界的阻塞队列LinkedBlockingQueue 作为任务队列(最大数量是Integer.MAX_VALUE),可能导致大量堆积的任务,导致OOM;
CachedThreadPool:使用同步队列,每次线程不够就新创建一个,而不是放入任务队列中,这也可能导致创建大量的线程,导致OOM;
ScheduledThreadPool 和 SingleThreadScheduledExecutor:使用无界的延迟阻塞队列DelayedWorkQueue,任务队列最大长度为 Integer.MAX_VALUE,可能堆积大量请求,导致OOM。
线程池队列类型
无界队列
代表实现:LinkedBlockingQueue(当不指定容量或指定容量为Integer.MAX_VALUE时)
特点:队列容量理论上是无限的,但受限于JVM的内存。当内存耗尽时,会抛出OutOfMemoryError。
适用场景:适用于任务量非常大,且任务执行时间较长,生产者生成任务的速度不会超过消费者处理任务的速度的场景。但需注意内存溢出的风险。
有界队列
代表实现:ArrayBlockingQueue、LinkedBlockingQueue(指定具体容量)
特点:队列有一个固定的容量限制,当队列满时,尝试添加新任务的操作会被阻塞,直到队列中有空间可用。
适用场景:适用于需要控制任务数量,防止资源耗尽的场景。通过调整队列大小和线程池大小,可以灵活控制任务的并发执行。
直接提交队列(SynchronousQueue)同步队列
特点:这种队列实际上并不存储任何元素,每个插入操作必须等待另一个线程的相应删除操作(也就是:要添加新任务必须得有空闲的线程才能添加),反之亦然。即一个线程尝试向队列中添加元素时,必须有另一个线程正在等待接收这个元素。
适用场景:适用于任务处理时间较短,且生产者和消费者速度大致匹配的场景。它可以有效减少任务在队列中的等待时间,提高系统的响应速度。
优先级队列
代表实现:PriorityBlockingQueue
特点:队列中的元素会根据其优先级进行排序,优先级高的元素会先被取出执行。
适用场景:适用于需要按照任务优先级顺序执行的场景。通过调整任务的优先级,可以确保重要任务得到优先处理。
在选择线程池中的队列时,需要根据具体的应用场景和需求来决定:
1、任务类型和特点:
如果任务处理时间较长,且任务量不确定,可以选择无界队列;如果任务量较大且需要控制并发数,可以选择有界队列。
如果低耗时、高并发的场景,适合配置较小的核心线程数和较大的队列;如果高耗时、高并发的场景,适合配置较大的核 心线程数和较小的队列;队列大小设置合理,就不需要走最大线程数造成额外开销,所以配置线程池的最佳方式是核心线程数搭配队列大小;线程池拒绝策略尽量以默认为主;
2、系统资源:考虑系统的内存和CPU资源。无界队列虽然可以处理大量任务,但存在内存溢出的风险;有界队列可以避免内存溢出,但需要合理设置队列大小和线程池大小。
3、性能要求:如果要求系统响应速度快,且任务处理时间较短,可以选择直接提交队列或优先级队列。
4、任务优先级:如果任务有明确的优先级要求,可以选择优先级队列。
线程池中的队列选择需要根据实际情况进行权衡和决策。在设计和配置线程池时,应充分考虑任务类型、系统资源和性能要求等因素。
execute() vs submit()
execute():没有返回值
submit():返回一个 Future 对象,可以异步监控任务完成情况、取消任务、获取任务的最终结果;可以通过 get() 方法获取返回值,get() 方法会阻塞直到任务完成,使用 get(long timeout, TimeUnit unit),如果timeout时间内没有完成,就会抛出异常
shutdown() vs shutdownNow()
shutdown():关闭线程池,状态变成 SHUTDOWN,不会再接受新任务,队列的任务会执行完毕;
shutdownNow():关闭线程池,状态变成 STOP,终止所有任务,停止处理排队的任务返回正在等待执行的List
isTerminated() vs isShutdown()
isShutdown: 调用 shutdown()方法后返回 true
isTerminated():调用shutdown() 方法后,并且所有提交的任务完成后返回 true
sleep() vs wait()
语法使用不同
wait 方法必须配合 synchronized 一起使用,不然在运行时就会抛出 IllegalMonitorStateException 的异常
sleep 可以单独使用,无需配合 synchronized 一起使用。
所属类不同
wait 方法属于 Object 类的方法
sleep 属于 Thread 类的方法
唤醒方式不同
sleep 方法必须要传递一个超时时间的参数,且过了超时时间之后,线程会自动唤醒。
wait 方法可以不传递任何参数,不传递任何参数时表示永久休眠,直到另一个线程调用了 notify 或 notifyAll 之后,休眠的线程才能被唤醒。即sleep 方法具有主动唤醒功能,而不传递任何参数的 wait 方法只能被动的被唤醒。
释放锁资源不同
wait 方法会主动的释放锁。
sleep 方法不会释放锁。
线程进入状态不同
sleep 方法线程会进入 TIMED_WAITING 有时限等待状态。
wait 方法,线程会进入 WAITING 无时限等待状态。
get() vs join()
异常处理:get()方法抛出的是ExecutionException,而join()方法抛出的是CompletionException。CompletionException是ExecutionException的子类,它提供了更多的信息,例如原始异常的类型和消息。
中断处理:get()方法在被中断时会抛出InterruptedException,而join()方法不会。如果你需要处理中断,应该使用get()方法。
使用场景:join()方法通常用于CompletableFuture链式调用中,因为它抛出的CompletionException可以被链式调用中的exceptionally方法捕获和处理。而get()方法通常用于需要捕获中断异常的场景。
小知识
耗时任务会占用线程池,导致线程池崩溃或者程序假死
耗时任务(网络请求、文件读写)可以采用 CompletableFuture 等其它异步操作方式处理,避免线程池的线程被阻塞。
线程池 和 ThreadLocal 共用 导致的脏数据(因为线程可以处理不同任务,导致 ThreadLocal 变量被重用,导致 ThreadLocal 并不是正确的值,变成脏数据),解决方案:使用 TransmittableThreadLocal,它在使用线程池会池化复用线程的执行组件的情况下,提供 ThreadLocal值的传递功能,解决异步执行时上下文传递的问题。
使用场景:
1:快速响应用户请求
用户查询商品详情页,会涉及查询商品关联的一系列信息如价格、优惠、库存、基础信息等,站在用户体验的角度,希望商详页的响应时间越短越好,此时可以考虑使用线程池并发地查询价格、优惠、库存等信息,再聚合结果返回,降低接口总rt。这种线程池用途追求的是最快响应速度,所以可以考虑不设置队列去缓冲并发任务,而是尽可能设置更大的corePoolSize和maxPoolSize;
2:快速处理批量任务
项目中在对接渠道同步商品供给时,需要查询大量的商品数据并同步给渠道,此时可以考虑使用线程池快速处理批量任务。这种线程池用途关注的是如何使用有限的机器资源,尽可能地在单位时间内处理更多的任务,提升系统吞吐量,所以需要设置阻塞队列缓冲任务,并根据任务场景调整合适的corePoolSize;
3、尽量在可以延迟且不是特别重要的场景下使用
4、线程池不要混用,特定业务记得隔离
public class PersonService {
private Random random = new Random();
private String[] names = {"黄某人", "负债程序猿", "谭sir", "郭德纲", "蔡徐鸡", "蔡徐老母鸡", "李狗蛋", "铁蛋", "赵铁柱"};
private String[] addrs = {"二仙桥", "成华大道", "春熙路", "锦里", "宽窄巷子", "双子塔", "天府大道", "软件园", "熊猫大道", "交子大道"};
private String[] companys = {"京东", "腾讯", "百度", "小米", "米哈游", "网易", "字节跳动", "美团", "蚂蚁", "完美世界"};
/**
* 1、建造者模式的意义
* 在构建时,对外部隐藏内部细节,将构建过程和部件都可以进行扩展,使他们的耦合程度降低。
*
* 2、使用场景
* (1)相同的方法,执行不同的顺序,产生不同的事件结果
* (2)多个部件或零件,都可以装配到一个对象中,但是产生的结果又不一样
* (3)产品类非常复杂,或者产品类的调用顺序不同产生了不同的作用
* (4)当初始化一个对象特别复杂,参数很多,很多参数都有默认值时。
*
* @return
*/
//@Builder 建造者模式
private Person getPerson() {
Person person = Person.builder()
.name(names[random.nextInt(names.length)])
.phone(18800000000L + random.nextInt(88888888))
.salary(new BigDecimal(random.nextInt(99999)))
.company(companys[random.nextInt(companys.length)])
.ifSingle(random.nextInt(2))
.sex(random.nextInt(2))
.address("四川省成都市" + addrs[random.nextInt(addrs.length)])
.createUser(names[random.nextInt(names.length)]).build();
return person;
}
public List<Person> getPersonList(int count) {
List<Person> persons = new ArrayList<>(count);
for (int i = 0; i < count; i++) {
persons.add(getPerson());
}
return persons;
}
}
/**
* CompletableFuture多任务异步,获取返回值,汇总结果,返回前端,减少RT
* 有几个方法比较关键:
* supplyAsync(): 异步处理任务,有返回值
* whenComplete():任务完成后触发,该方法有返回值。两个参数,第一个参数是任务的返回值,第二个参数是异常。
* allOf():就是所有任务都完成时触发。可以配合get()一起使用
*/
public class BusiDemo {
Logger logger = LoggerFactory.getLogger(BusiDemo.class);
public static ExecutorService pool = ThreadPoolDemo.getExecutor();
private static PersonService personService = new PersonService();
/**
* 获取CPU数量
*
* @param args
*/
public static void main(String[] args) {
new BusiDemo().getPersonList();
}
public void getPersonList() {
//线程安全的list,适合写多读少的场景
List<Person> resultList = Collections.synchronizedList(new ArrayList<>(60));
CompletableFuture<List<Person>> completableFuture1 = CompletableFuture.supplyAsync(
() -> personService.getPersonList(2), pool)
.whenComplete((result, throwable) -> {
//任务完成时执行。用list存放任务的返回值
if (result != null) {
//int a = 1/0;
resultList.addAll(result);
}
//触发异常
if (throwable != null) {
logger.error("completableFuture1 error:{}", throwable);
}
});
CompletableFuture<List<Person>> completableFuture2 = CompletableFuture.supplyAsync(
() -> personService.getPersonList(30), pool)
.whenComplete((result, throwable) -> {
if (result != null) {
resultList.addAll(result);
}
if (throwable != null) {
logger.error("completableFuture2 error:{}", throwable);
}
});
List<CompletableFuture<List<Person>>> futureList = new ArrayList<>();
futureList.add(completableFuture1);
futureList.add(completableFuture2);
try {
//多个任务
CompletableFuture[] futureArray = futureList.toArray(new CompletableFuture[0]);
//将多个任务,汇总成一个任务,总共耗时不超时2秒
CompletableFuture.allOf(futureArray).get(10, TimeUnit.SECONDS);
} catch (Exception e) {
logger.error("CompletableFuture.allOf Exception error.", e);
}
List<Person> list = new ArrayList<>(resultList);
list.forEach(System.out::println);
System.out.println("list size: " + list.size());
//pool.shutdown();
}
}
@Builder
@NoArgsConstructor
@AllArgsConstructor
@Data
public class Person {
private Long id;
private String name;//姓名
private Long phone;//电话
private BigDecimal salary;//薪水
private String company;//公司
private Integer ifSingle;//是否单身
private Integer sex;//性别
private String address;//住址
private LocalDateTime createTime;
private String createUser;
}