工作中需要用到的Java知识(多线程进阶篇)

上一次更新多线程的内容还是在今年三月份,当时发布的内容不完整,仅介绍了Thread和Runnable两种创建多线程的方式,但是其实这两种方式在我的工作中是没有使用到的,当时项目正上线也没有特意去关注,所以这次来填坑。

这篇文章主要是想来补充一下上次没有介绍完全的内容,再学习一下线程池,以及在工作中用到的创建多线程的方式。

实现Callable接口方式创建多线程

Callable属于JUC——java.util.concurrent包下的创建多线程的方式

使用Callable方式来创建多线程的步骤分为:

1.实现Callable接口,设置返回值类型

2.重写call方法

3.创建目标对象 ——为实现Callable接口的类对象,需要创建几个线程,就要创建几个对象

4.创建执行服务 —— 创建几个线程,线程个数就为几

ExecutorService 自定义执行服务名称 = Executors.newFixedThreadPool(线程个数)

5.提交执行 —— 创建一个目标对象,就要写一次提交执行的代码

Future<Boolean> 多线程执行结果 = 执行服务名称.submit(目标对象)

6.获取结果 —— 每有一个提交执行,就要获取一个结果

boolean r1 = 多线程执行结果.get()

7.关闭服务

执行服务名称.shutdownNow()

这种创建多线程的方式比较复杂,但是好处在于可以设置返回值,可以抛出异常(其他两种创建多线程方式则不允许抛出异常)。

使用FutureTask对象创建多线程 

1.我们需要创建一个Callable对象

Callable<返回值类型> callable = () -> {
    子任务内容;
    return 返回值;
}

2.然后创建一个FutureTask对象

FutureTask<返回值类型> task = new FutureTask(callable)

3.创建一个Thread类对象,将FutureTask对象作为参数,执行线程

Thread thread = new Thread(task)
//执行多线程
thread.start()

4.获取多线程返回值与异常

当我们调用了get方法后,主线程就会一直不停的询问子线程状态,是否执行结束,会等待子线程执行结束后才继续向下执行。

//线程正常执行完毕会正常返回,如果报错则会返回ExecutionException异常
//我们可以填写get()方法的参数,限定等待时长
//超过时长会抛出TimeoutException,我们可以捕获后处理此异常
task.get()

5.获取子线程产生的异常内容

我们通过第四步获取到了异常后,通过捕获ExecutionException异常,可以调用getCause()方法来获取子线程中产生的异常内容

CompletableFuture方式创建线程

CompletableFuture是Java8中提出的创建多线程的方式,也是我在工作中使用的创建多线程的方式

//查询当前电脑CPU有多少核心
Runtime.getRuntime().availableProcessors()

//设置JVM启动时的最大线程数
System.setProperty("java.util.concurrent.ForkJoinPool.common.parallelism", "最大线程个数")

//查看当前线程数
ForkJoinPool.commonPool().getPoolSize()

//查看最大线程数
ForkJoinPool.getCommonPoolParallelism()

CompletableFuture创建线程的方式就比较简单

//通过runAsync方法执行的代码,会自动跳到另一个线程中执行(无返回值)
CompletableFuture<返回值类型> thread = CompletableFuture.runAsync(() -> {
    方法体
    return 返回值
});


//通过supplyAsync方法执行的代码,会自动跳到另一个线程中执行(有返回值)
CompletableFuture<返回值类型> thread = CompletableFuture.supplyAsync(() -> {
    方法体
    return 返回值
});


//等待线程执行结束获取返回结果,此方法会抛出运行时异常,不需要强制捕获
thread.join();

//等待所有线程执行结束
CompletableFuture.allOf().join()

//通过thenCompose方法,当第一个线程结束后,再开启另一个线程
//线程2中可以使用线程1的返回值数据
CompletableFuture<返回值类型> thread = CompletableFuture.supplyAsync(() -> {
    方法体
    return 返回值
}).thenCompose(threadReturn -> CompletableFuture.supplyAsync(() -> {
    方法体
    return 返回值
}));


//通过thenCombine方法,当前两个线程执行结束后,在开启第三个线程
//线程3中可以使用线程1和线程2的返回值数据
CompletableFuture<返回值类型> thread = CompletableFuture.supplyAsync(() -> {
    方法体
    return 返回值1
}).thenCombine(CompletableFuture.supplyAsync(() -> {
    方法体
    return 返回值2
}), (返回值1, 返回值2) -> {
    方法体
    return 返回值3
});


//通过applyToEither方法,线程1跟线程2一起执行,哪个先执行完就将哪个结果返回
CompletableFuture<返回值类型> thread = CompletableFuture.supplyAsync(() -> {
    方法体
    return 返回值
}).applyToEither(threadReturn -> CompletableFuture.supplyAsync(() -> {
    方法体
    return 返回值
}), 最终的返回值参数 -> {方法体(可以进行数据处理,然后返回)});
//如果直接返回则为      返回值参数 -> 返回值参数  


//通过exceptionally方法,可以捕获线程出现的异常进行处理,后面可以接上其他线程操作
CompletableFuture<返回值类型> thread = CompletableFuture.supplyAsync(() -> {
    方法体
    return 返回值
}).exceptionally(e -> {
    异常处理
    return 异常状态的返回值
});

守护线程

线程分为用户线程和守护线程。

守护线程是保护我们正常运行程序的线程,例如:gc 垃圾回收线程。

代码运行时虚拟机不必等待守护线程结束。

synchronized锁

synchronized 添加在方法作用域标识符后面,用来表示此方法为同步方法。

synchronized 默认锁的是this,如果不希望锁住this对象,则需要使用synchronized代码块实现

//synchronized 锁this例子
public synchronized 返回值类 方法名(){
}

//synchronized 锁对象的例子(锁上需要增删改查的对象)
synchronized (object) {
}

synchronized是隐式锁,出了作用域自动解锁。

synchronized锁比较笨重,JVM需要较长事件来进行线程的调度。

死锁

多个线程持有对方需要的资源互相还需要对方的资源,就会形成死锁。

像多线程、数据库层面容易发生死锁事件,我们应该去了解一些底层原理,明白代码是如何运行的,来避免死锁问题的发生。

sleep方法与wait方法的区别

这个区别在工作中不常用到,但是经常被用作面试的提问,所以在这里总结一下区别。

区别一:sleep是Thread类中的静态方法,在执行sleep方法时,不会释放此线程的锁,待睡眠结束后继续执行此线程wait是Object类中的方法,再执行wait方法时,会释放此线程的锁,等待其他线程调用notify方法或是指定时间以后重新回到线程队列等待执行。

区别二:sleep常用于增强多线程测试时出现问题的可能性,不涉及多线程的互相通信;wait方法涉及多线程的互相通信。

区别三:sleep方法可以放在任何地方使用,而wait方法只能在同步方法或同步方法块中使用。

线程中断

其实这部分我不是很想说了,因为平时在工作中不太用到,但是觉得还是比较重要的吧,所以来说一下线程中断。

我们在调用sleep方法或wait方法时,总是需要抛出InterruptedException(线程中断异常),因为在我们休眠等待的时候可能其他线程会调用我们的中断方法,唤醒我们重新让我们执行。

中断状态是线程的一个状态,当线程已经为中断状态时不会自动恢复。

方法名作用
interrupt()设置线程的中断状态
isInterrupt()判断线程是否为中断状态
interrupted()清除线程的中断状态

sleep方法也会清除线程的中断状态。

当我们调用sleep方法开始睡眠后,等待其他方法执行到interrupt方法的时候,就会自动唤醒调用方法的线程,继续向下执行。

线程池

现在接触到的池类概念一共有两个:线程池,数据库连接池。

先说一下这个设计的优点,为什么需要有池。

当我们创建线程,或是数据库连接的时候会十分消耗资源,所以使用池的概念可以将创建好的线程或数据库连接进行重复利用。使用时从池中拿出对象,使用完毕放回池中,避免频繁的创建销毁。

1.可以提高响应速度(避免了创建销毁浪费的时间)。

2.降低资源消耗。

3.便于对线程进行管理。

在阿里巴巴的编码规约中,建议我们创建线程池的时候通过ThreadPoolExecutor 的方式创建,这样的处理方式可以让写的同学更加明确线程池的运行规则,规避资源耗尽的风险。

那么该如何创建ThreadPoolExecutor线程池呢?

public ThreadPoolExecutor(int corePoolSize,                        //核心线程数
                              int maximumPoolSize,                 //最大线程数
                              long keepAliveTime,                  //最大超时时间
                              TimeUnit unit,                       //超时时间单位
                              BlockingQueue<Runnable> workQueue,   //缓冲阻塞队列,用于等待
                              ThreadFactory threadFactory,         //线程工厂
                              RejectedExecutionHandler handler)    //拒绝策略

在创建线程池时我们应该如何确定线程池的数量呢?

有两种策略:

1.CPU密集型,我们按照服务器的CPU核数来确定线程池中个数,这样可以保持CPU的效率最高。

// 获取CPU核数/处理器个数
Runtime.getRuntime().availableProcessors()

2.IO密集型,我们要自主判断程序中有多少个大型任务,大型任务十分耗费资源,我们可以根据大型任务的两倍数量来进行线程池的创建,这样可以避免程序进入阻塞状态。

拒绝策略参数一共有如下四种

//(默认拒绝策略)会抛出RejectedExecutionException
new ThreadPoolExecutor.AbortPolicy()

//重试添加当前的任务,他会自动重复调用 execute() 方法,直到成功
new ThreadPoolExecutor.CallerRunsPolicy()

//对拒绝任务不抛弃,而是抛弃队列里面等待最久的一个线程,然后把拒绝任务加到队列
new ThreadPoolExecutor.DiscardOldestPolicy()

//对拒绝任务直接无声抛弃,没有异常信息
new ThreadPoolExecutor.DiscardPolicy()

当一个多线程方法被添加到线程池时,线程池的运行策略

1.如果当前线程池中的线程数量小于corePoolSize,即使线程都是空闲状态,也要创建新的线程来处理添加的任务。

2.如果当前线程池中的线程数量大于等于corePoolSize,但缓冲阻塞队列未满,则任务会被放入阻塞队列中。

3.如果当前线程池中的线程数量大于corePoolSize,缓冲阻塞队列满了,且线程数量小于maximumPoolSize,则创建新的线程来处理添加的任务。

4.如果当前线程池中的线程数量大于corePoolSize,缓冲阻塞队列满了,且线程数量大于maximumPoolSize,则通过设置的拒绝策略来处理 添加的任务。

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值