Java核心基础篇(二)——多线程

2022/8/2 1:26:21

本文主要是介绍Java核心基础篇(二)——多线程,对大家解决编程问题具有一定的参考价值,需要的程序猿们随着小编来一起学习吧!

一、线程池

1、线程池的作用
(1)通过线程池可以实现对已创建线程的复用,减少资源的消耗;
(2)控制并发的数量;
(3)以及对线程进行统一的管理。

2、线程池的分类
(1)newCachedThreadPool(可缓存线程池):核心线程数为0,总线程数是最大整数,当需要执行很多短时任务时它的服用率比较高,会显著提升性能。并且线程空闲60s就会被回收,
所以如果没有任务,它并不会占用很多资源。
适用场景:比较适合任务量大但耗时少的任务。任务队列采用的是同步队列SynchronousQueue。
(2)newFixedThreadPool(指定工作线程数量的线程池):核心线程数等于最大线程数。也就是该线程池中没有非核心线程。
适用场景:由于该线程池线程数固定,且不被回收,线程与线程池的生命周期同步,所以适用于任务量比较固定但耗时长的任务。任务队列采用了无界的阻塞队列LinkedBlockingQueue。
(3)newSingleThreadExecutor(单线程化的线程池):只有一个核心线程,所有任务按照先来先执行的顺序执行。
适用场景:这类线程池适用于多个任务顺序(FIFO)执行的场景。其任务队列是LinkedBlockingQueue。
(4)newScheduledThreadPool(定长线程池):这个线程池指定了核心线程的数量,线程总数可以创建整数的最大数个。由于空闲时间固定为0,只要线程工作完,立马就会被回。
该线程池支持定时以及周期性任务执行。
适用场景:综合来说,这类线程池适用于执行定时任务和具体固定周期的重复任务。任务队列采用的DelayedWorkQueue。

3、线程池的重要参数
(1)(int)corePoolSize:核心线程最大数量。线程池中有两类线程,分别为核心线程和非核心线程。核心线程默认情况下会一直存在于线程池中,即使这个核心线程什么都不干,
而非核心线程如果长时间的闲置,就会被销毁。
(2)(int)maximumPoolSize:线程总数最大值。等于核心线程数+非核心线程数。
(3)(long)keepAliveTime:非核心线程的闲置超时时间。非核心线程闲置时间超过此值就会被销毁。
(4)(TimeUnit)unit:keepAliveTime的单位,一般为MILLSECONDS(1毫秒)、SECONDS(1秒)。
(5)(BlockingQueue)workQueue:阻塞队列,主要有以下四种=>
1)LinkedBlockingQueue
链式阻塞队列,默认大小:Interger.MAX_VALUE,可以指定大小。
2)ArrayBlockingQueue
数组阻塞队列,需要指定大小。
3)SynchronousQueue
同步队列,内部容量为0,每个put操作都必须等待一个take操作。反之亦然。
4)DelayQueue
延迟队列,队列中的元素之后当其指定的延迟时间到了,才能从队列中获取到该元素。
(6)(ThreadFactory)threadFactory:线程工厂。建线程的工厂,用于批量创建线程,如果不指定,会新建一个默认的线程工厂。
(7)(RejectedExecutionHandler)handler:拒绝策略。拒绝处理策略,当无法创建新线程去处理任务并且阻塞队列已满时就会采用拒绝处理策略。
1)AbortPolicy:为线程池默认的拒绝策略,该策略直接抛异常处理。
2)DiscardPolicy:直接抛弃不处理。
3)DiscardOldestPolicy:丢弃队列中最老的任务。
4)CallerRunsPolicy:将任务分配给当前执行execute方法的线程来处理。

4、线程池的执行流程
(1)提交任务后,首先会进行当前工作线程数与核心线程数的比较,如果当前工作线程数小于核心线程数,表明有空闲的核心线程,则直接调用 addWorker() 方法
创建一个核心线程去执行任务;
(2)如果工作线程数大于核心线程数,即线程池核心线程数已满,则新任务会被添加到阻塞队列中等待执行,当然,添加队列之前也会进行队列是否已满的判断;
(3)如果线程池里面存活的线程数已经等于核心线程数了,表明没有空闲的非核心线程,且阻塞队列已经满了,再会去判断当前线程数是否已经达到最大线程数 maximumPoolSize,
如果没有达到,则会调用 addWorker() 方法创建一个非核心线程去执行任务;
(4)如果当前线程的数量已经达到了最大线程数时,当有新的任务提交过来时,会执行拒绝策略。
总结:总的来说就是优先核心线程、阻塞队列次之,最后非核心线程。addWorker() 执行过程中会反复使用 runStateOf() 和 workerCountOf() 来获取线程池的状态
和工作线程的数量。

5、线程池的使用
(1)ExecutorService threadPool=Executors.上述的四种线程池创建方法,这几种线程池创建方法本质是对new ThreadPoolExecutor进行了不同传参;
(2)ExecutorService threadPool=new ThreadPoolExecutor(不同参数),手动创建线程池;
(3)通过threadPool.submit(线程任务)或threadPool.execute(线程任务)来提交任务进行处理。使用submit可以执行有返回值的任务或者是无返回值的任务;
而execute只能执行不带返回值的任务。

6、线程池的最大线程数的设置考量
(1)CPU密集型:CPU核数+1个线程
(2)IO密集型:① CPU核数*2;② CPU核数/1-阻塞系数。(阻塞系数:0.8~0.9)

 

二、同步

1、同步方法
(1)即有synchronized关键字修饰的方法。  
(2)由于java的每个对象都有一个内置锁,当用此关键字修饰方法时,内置锁会保护整个方法。在调用该方法前,需要获得内置锁,否则就处于阻塞状态。
(3)synchronized关键字也可以修饰静态方法,此时如果调用该静态方法,将会锁住整个类

2、同步代码块
(1)即有synchronized关键字修饰的语句块。synchronized(object){ //同步内容 }  
(2)被该关键字修饰的语句块会自动被加上内置锁,从而实现同步.
(3)同步是一种高开销的操作,因此应该尽量减少同步的内容。通常没有必要同步整个方法,使用synchronized代码块同步关键代码即可。 

3、使用特殊域变量volatile实现线程同步(不建议使用)
(1)volatile关键字为域变量的访问提供了一种免锁机制
(2)使用volatile修饰域相当于告诉虚拟机该域可能会被其他线程更新
(3)volatile修饰的变量/对象,每次修改后会让cpu通知其他线程,使得其他线程使用该变量/对象时从主内存中获取
(4)因此每次使用该域就要重新计算,而不是使用寄存器中的值。每个线程对应的线程栈(虚拟机栈)都会具有自己的副本 
(5)volatile不会提供任何原子操作,它也不能用来修饰final类型的变量 

4、使用重入锁ReentrantLock实现线程同步
(1)在JavaSE5.0中新增了一个java.util.concurrent包来支持同步。 
(2)ReentrantLock类是可重入、互斥、实现了Lock接口的锁, 
(3)它与使用synchronized方法和快具有相同的基本行为和语义,并且扩展了其能力
(4)使用:ReentrantLock():创建一个ReentrantLock实例;lock():获得锁;unlock():释放锁 

5、使用局部变量实现线程同步
(1)如果使用ThreadLocal管理变量,则每一个使用该变量的线程都获得该变量的副本 
(2)副本之间相互独立,这样每一个线程都可以随意修改自己的变量副本,而不会对其他线程产生影响
(3)常用方法:
ThreadLocal() : 创建一个线程本地变量 
initialValue() : 返回此线程局部变量的当前线程的"初始值" 
set(T value) : 将此线程局部变量的当前线程副本中的值设置为value
get() : 返回此线程局部变量的当前线程副本中的值
remove():该方法用来移除当前线程中变量的副本,目的是为了减少内存的占用

6、使用阻塞队列实现线程同步
(1)LinkedBlockingQueue 类常用方法 
LinkedBlockingQueue() : 创建一个容量为Integer.MAX_VALUE的LinkedBlockingQueue 
put(E e) : 在队尾添加一个元素,如果队列满则阻塞 
size() : 返回队列中的元素个数 
take() : 移除并返回队头元素,如果队列空则阻塞 

7、使用原子变量实现线程同步
(1)在java的util.concurrent.atomic包中提供了创建了原子类型变量的工具类,
使用该类可以简化线程同步;
(2)其中AtomicInteger 表可以用原子方式更新int的值,可用在应用程序中(如以原子方式增加的计数器),
但不能用于替换Integer;可扩展Number,允许那些处理机遇数字类的工具和实用工具进行统一访问;
(3)AtomicInteger类常用方法:
AtomicInteger(int initialValue) : 创建具有给定初始值的新的AtomicInteger
addAddGet(int dalta) : 以原子方式将给定值与当前值相加
get() : 获取当前值

 

三、异步

1、方法一:手动使用ThreadPoolExecutor去创建线程池

2、方法二:基于注解@Async("自定义线程池名")使用Spring封装的ThreadPoolTaskExecutor线程池
springboot创建线程池,Spring提供的对ThreadPoolExecutor封装的线程池ThreadPoolTaskExecutor,直接使用注解启用。
Async相当于是方法级别的线程,本身没有自定义线程池更加灵活
相当于是每进来一个请求就开启一个线程,超过核心线程数小于最大线程数放入队列,队列满了,继续创建线程直至达到最大线程数。

方法二的使用步骤:
(1)自定义线程池==>在xml配置bean
<!-- 用于支持多线程执行任务的线程池配置 -->
    <bean id="threadPool"
        class="org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor">
        <!-- 核心线程数 10 -->
        <property name="corePoolSize">
            <value>${plugs.task.pool.corePoolSize}</value>
        </property>
        <!-- 最大线程数 -->
        <property name="maxPoolSize">
            <value>${plugs.task.pool.maxPoolSize}</value>
        </property>
        <!-- 队列最大长度 >=mainExecutor.maxSize -->
        <property name="queueCapacity">
            <value>${plugs.task.pool.queueCapacity}</value>
        </property>
        <!-- 线程池维护线程所允许的空闲时间 -->
        <property name="keepAliveSeconds">
            <value>${plugs.task.pool.keepAliveSeconds}</value>
        </property>
        <!-- 线程池对拒绝任务(无线程可用)的处理策略   -->
        <property name="rejectedExecutionHandler">
            <bean class="java.util.concurrent.ThreadPoolExecutor$CallerRunsPolicy" />
        </property>
    </bean>
    
(2)自定义任务类以及需要执行任务的方法
@Configuration      //任务类实例需要交由spring管理,也可以是@Component
@EnableAsync        //使@Async注解生效,如果项目上下文已经使用过这个注解了,就不需要再使用了
@EnableScheduling   //使@Scheduling生效,如果项目上下文已经使用过这个注解了,就不需要再使用了
public class RmRollBackTimeTask {
   
    @Async("threadPool")    //使用自定义线程池
//触发方式一(s使用定时触发):定时触发从线程池里复用线程去异步执行rollBackTask方法;
还有一种方法是:通过使用类RmRollBackTimeTask的bean去手动触发rollBackTask方法。
    @Scheduled(cron = "${plugs.task.log.yearreport.timer:0 30 1 1 * ?}")  
    public void rollBackTask() {
       //任务执行体
       
    }
}

3、方法三:使用JDK1.8的CompletableFuture去作异步处理
CompletableFuture<String> future = CompletableFuture.supplyAsync(()->{
            return "hello world";
  });
System.out.println(future.get());  //阻塞的获取结果  ''helllo world"

//CompletableFuture提供的四个异步处理静态方法
public static CompletableFuture<Void>   runAsync(Runnable runnable)
public static CompletableFuture<Void>   runAsync(Runnable runnable, Executor executor)
public static <U> CompletableFuture<U>  supplyAsync(Supplier<U> supplier)
public static <U> CompletableFuture<U>  supplyAsync(Supplier<U> supplier, Executor executor)
//Asynsc表示异步,而supplyAsync与runAsync不同在与前者异步返回一个结果,后者是void.第二个函数第二个参数表示是用我们自己创建的线程池,
否则采用默认的ForkJoinPool.commonPool()作为它的线程池.其中Supplier是一个函数式接口,代表是一个生成者的意思,传入0个参数,返回一个结果.

 



这篇关于Java核心基础篇(二)——多线程的文章就介绍到这儿,希望我们推荐的文章对大家有所帮助,也希望大家多多支持为之网!


扫一扫关注最新编程教程