java多线程(线程池)

线程池

​ 管理线程的资源池,可以复用线程,不用频繁的创建新线程,节省线程开销的资源损耗,节省资源,提高响应速度。主要概念:核心线程、阻塞队列、非核心线程、空闲时间、饱和策略。

线程池执行过程

Created with Raphaël 2.3.0 提交任务 核心线程池是否已满? 阻塞队列(:任务队列)是否已满? 线程池是否已满(是否可以创建非核心线程)? 饱和策略 创建非核心线程执行任务 任务添加到阻塞队列 创建核心线程执行任务 yes no yes no yes no

大于设定的空闲时间的非核心线程会被销毁释放。

JUC线程管理

ThreadPoolExecutor

《阿里巴巴java开发手册》中指出了线程资源必须通过线程池提供,不允许在应用中自行显示的创建线程,这样一方面是线程的创建更加规范,可以合理控制开辟线程的数量;另一方面线程的细节管理交给线程池处理,优化了资源的开销。而线程池不允许使用Executors去创建,而要通过ThreadPoolExecutor方式,这一方面是由于jdk中Executors框架虽然提供了如newFixedThreadPool()、newSingleThreadExecutor()、newCachedThreadPool()等创建线程池的方法,但都有其局限性,不够灵活;另外由于前面几种方法内部也是通过ThreadPoolExecutor方式实现,使用ThreadPoolExecutor有助于大家明确线程池的运行规则,创建符合自己的业务场景需要的线程池,避免资源耗尽的风险。

ThreadPoolExecutor的构造方法:

public ThreadPoolExecutor(int corePoolSize,
                              int maximumPoolSize,
                              long keepAliveTime,
                              TimeUnit unit,
                              BlockingQueue<Runnable> workQueue,
                              ThreadFactory threadFactory,
                              RejectedExecutionHandler handler) {
        if (corePoolSize < 0 ||
            maximumPoolSize <= 0 ||
            maximumPoolSize < corePoolSize ||
            keepAliveTime < 0)
            throw new IllegalArgumentException();
        if (workQueue == null || threadFactory == null || handler == null)
            throw new NullPointerException();
        this.acc = System.getSecurityManager() == null ?
                null :
                AccessController.getContext();
        this.corePoolSize = corePoolSize;
        this.maximumPoolSize = maximumPoolSize;
        this.workQueue = workQueue;
        this.keepAliveTime = unit.toNanos(keepAliveTime);
        this.threadFactory = threadFactory;
        this.handler = handler;
    }

参数含义:
corePoolSize:指定了线程池中的线程数量,它的数量决定了添加的任务是开辟新的线程去执行,还是放到workQueue任务队列中去;
maximumPoolSize:指定了线程池中的最大线程数量,这个参数会根据你使用的workQueue任务队列的类型,决定线程池会开辟的最大线程数量;
keepAliveTime:当线程池中空闲线程数量超过corePoolSize时,多余的线程会在多长时间内被销毁;
unit:keepAliveTime的单位
workQueue:任务队列,被添加到线程池中,但尚未被执行的任务;它一般分为直接提交队列、有界任务队列、无界任务队列、优先任务队列几种;
threadFactory:线程工厂,用于创建线程,一般用默认即可;
handler:拒绝策略;当任务太多来不及处理时,如何拒绝任务;

四种类型的阻塞队列:

  1. 直接提交队列(SynchronousQueue队列)
    使用SynchronousQueue队列,提交的任务不会被保存,总是会马上提交执行。如果用于执行任务的线程数量小于maximumPoolSize,则尝试创建新的进程,如果达到maximumPoolSize设置的最大值,则根据你设置的handler执行拒绝策略。

  2. 有界的任务队列(ArrayBlockingQueue)
    使用ArrayBlockingQueue有界任务队列,若有新的任务需要执行时,线程池会创建新的线程,直到创建的线程数量达到corePoolSize时,则会将新的任务加入到等待队列中。若等待队列已满,即超过ArrayBlockingQueue初始化的容量,则继续创建线程,直到线程数量达到maximumPoolSize设置的最大线程数量,若大于maximumPoolSize,则执行拒绝策略。

  3. 无界的任务队列(LinkedBlockingQueue)
    使用无界任务队列,线程池的任务队列可以无限制的添加新的任务,而线程池创建的最大线程数量就是你corePoolSize设置的数量,也就是说在这种情况下maximumPoolSize这个参数是无效的,哪怕你的任务队列中缓存了很多未执行的任务,当线程池的线程数达到corePoolSize后,就不会再增加了;若后续有新的任务加入,则直接进入队列等待,当使用这种任务队列模式时,一定要注意你任务提交与处理之间的协调与控制,不然会出现队列中的任务由于无法及时处理导致一直增长,直到最后资源耗尽的问题.

  4. 优先任务队列(PriorityBlockingQueue)
    PriorityBlockingQueue它其实是一个特殊的无界队列,它其中无论添加了多少个任务,线程池创建的线程数也不会超过corePoolSize的数量,只不过其他队列一般是按照先进先出的规则处理任务,而PriorityBlockingQueue队列可以自定义规则根据任务的优先级顺序先后执行。
    待执行的任务实现Comparable接口,返回的值越小,表示优先级越高。

线程池的饱和策略有四种:

  1. AbortPolicy: 抛出一个异常,,阻止系统正常工作,默认值;
  2. DiscardPolicy:新提交的任务直接抛弃;
  3. DiscardOldestPolicy:丢弃队列里最老的任务,将当前这个任务继续提交给线程池
  4. CallerRunsPolicy:交给线程池调用所在的线程进行处理,即将某些任务退回给调用者。该策略会把任务队列中的任务放在调用者线程当中运行

通过线程工厂自定义线程创建:

线程池中线程就是通过ThreadPoolExecutor中的ThreadFactory,线程工厂创建的。那么通过自定义ThreadFactory,可以按需要对线程池中创建的线程进行一些特殊的设置,如命名、优先级等,下面代码我们通过ThreadFactory对线程池中创建的线程进行记录与命名

public class ThreadPool {
    private static ExecutorService pool;
    public static void main( String[] args )
    {
        //自定义线程工厂
        pool = new ThreadPoolExecutor(2, 4, 1000, TimeUnit.MILLISECONDS, new ArrayBlockingQueue<Runnable>(5),
                new ThreadFactory() {
            public Thread newThread(Runnable r) {
                System.out.println("线程"+r.hashCode()+"创建");
                //线程命名
                Thread th = new Thread(r,"threadPool"+r.hashCode());
                return th;
            }
        }, new ThreadPoolExecutor.CallerRunsPolicy());
          
        for(int i=0;i<10;i++) {
            pool.execute(new ThreadTask());
        }    
    }
}

public class ThreadTask implements Runnable{    
    public void run() {
        //输出执行线程的名称
        System.out.println("ThreadName:"+Thread.currentThread().getName());
    }
}

ThreadPoolExecutor扩展:
ThreadPoolExecutor扩展主要是围绕beforeExecute()、afterExecute()和terminated()三个接口实现的。

  1. beforeExecute:线程池中任务运行前执行
  2. afterExecute:线程池中任务运行完毕后执行
  3. terminated:线程池退出后执行

线程吃线程数量的设置没有一个明确的指标,根据实际情况,结合下面这个公式即可

			/**
             * Nthreads=CPU数量
             * Ucpu=目标CPU的使用率,0<=Ucpu<=1
             * W/C=任务等待时间与任务计算时间的比率
             */
            Nthreads = Ncpu*Ucpu*(1+W/C)

参考文章:ThreadPoolExecutor

线程池实现线程复用的原理

  • 线程池持有一个Worker的内部类,改内部类实现Runable,其run方法(runWorker)中是一个while循环,会从任务队列里获取任务,实现一个线程执行多个任务,实际上是将任务和线程分离。
  • 提交任务时,当线程数小于核心线程数时,创建worker内部类,执行任务;当核心线程池满时,任务添加到阻塞队列,当队列满时,创建非核心线程执行任务。
    参考文章

Future获取多线程的执行结果

    继承Thread、实现Runnable接口的多线程实现方式,无法获取到多线程执行的结果。通过实现Callable,返回Future的方式,可以获取到多线程的执行结果。Future表示一个可能还没有完成的异步任务的结果。

Future类图结构

future

  • get()方法可以当任务结束后返回一个结果,如果调用时,工作还没有结束,则会阻塞线程,直到任务执行完毕

  • get(long timeout,TimeUnit unit)做多等待timeout的时间就会返回结果

  • cancel(boolean mayInterruptIfRunning)方法可以用来停止一个任务,如果任务可以停止(通过mayInterruptIfRunning来进行判断),则可以返回true,如果任务已经完成或者已经停止,或者这个任务无法停止,则会返回false.

  • isDone()方法判断当前方法是否完成

  • isCancel()方法判断当前方法是否取消

CompletableFuture

参考文章:CompletableFuture

  • 创建异步任务的四个方法:
public static CompletableFuture<Void> runAsync(Runnable runnable)	//不支持返回值的异步任务创建,使用默认的ForkJoinPool.commonPool() 的线程池
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)
  • 计算结果完成的回调方法:
public CompletableFuture<T> whenComplete(BiConsumer<? super T,? super Throwable> action)//使用原有任务的线程执行回调方法
public CompletableFuture<T> whenCompleteAsync(BiConsumer<? super T,? super Throwable> action) //回调方法继续交由线程池执行
public CompletableFuture<T> whenCompleteAsync(BiConsumer<? super T,? super Throwable> action, Executor executor)
public CompletableFuture<T> exceptionally(Function<Throwable,? extends T> fn) //异常时执行回调方法
  • 两个线程有依赖关系时,使用thenApply把这两个线程串行化
public <U> CompletableFuture<U> thenApply(Function<? super T,? extends U> fn) //T上一个线程的返回类型,U当前线程的返回类型
public <U> CompletableFuture<U> thenApplyAsync(Function<? super T,? extends U> fn)
public <U> CompletableFuture<U> thenApplyAsync(Function<? super T,? extends U> fn, Executor executor)

//example
private static void thenApply() throws Exception {
    CompletableFuture<Long> future = CompletableFuture.supplyAsync(new Supplier<Long>() {
        @Override
        public Long get() {
            long result = new Random().nextInt(100);
            System.out.println("result1="+result);
            return result;
        }
    }).thenApply(new Function<Long, Long>() {
        @Override
        public Long apply(Long t) {
            long result = t*5;
            System.out.println("result2="+result);
            return result;
        }
    });
    
    long result = future.get();
    System.out.println(result);
}
  • 任务执行完成的方法handle,与thenApply处理方式类似,可以处理正常和异常结束的情况,thenApply只能处理正常结束的情况:
public <U> CompletionStage<U> handle(BiFunction<? super T, Throwable, ? extends U> fn);
public <U> CompletionStage<U> handleAsync(BiFunction<? super T, Throwable, ? extends U> fn);
public <U> CompletionStage<U> handleAsync(BiFunction<? super T, Throwable, ? extends U> fn,Executor executor);
  • thenAccept 消费处理结果,依赖任务的执行结果
public CompletionStage<Void> thenAccept(Consumer<? super T> action);
public CompletionStage<Void> thenAcceptAsync(Consumer<? super T> action);
public CompletionStage<Void> thenAcceptAsync(Consumer<? super T> action,Executor executor);

thenRun 方法,不关心任务的结果,只要任务完成就开始执行

public CompletionStage<Void> thenRun(Runnable action);
public CompletionStage<Void> thenRunAsync(Runnable action);
public CompletionStage<Void> thenRunAsync(Runnable action,Executor executor);
  • thenCombine 合并任务,将调用方与第一个future的结果合并
  • thenAcceptBoth任务合并后的任务的后续处理
  • applyToEither 方法,两个任务对执行的快的那个任务的结果做处理
  • acceptEither 方法,两个任务对执行的快的那个任务的结果做处理
  • runAfterEither 方法,两个任务任何一个返回了就执行
  • runAfterBoth ,两个任务都完成了执行
  • thenCompose,两个任务的流水线操作,第一个任务的结果用于第二个任务

java并发包JUC

juc参考文档
juc包含的主要功能
多线程并发包

tools

  1. CountDownLatch 闭锁,必须等指定的线程数完成后,主线程才能继续向下执行。countDownLatch参考文章
    * 初始化:CountDownLatch latch = new CountDownLatch(3);
    * 计数器减一:latch.countDown();
    * 主线程阻塞等待其他线程完成(计数器降到0,阻塞解除):latch.await();

  2. CyclicBarrier 循环栅栏,当指定数量的线程到达栅栏时,解除线程阻塞,未达到指定数量时,子线程阻塞。CyclicBarrier参考文章
    * 初始化指定数量的栅栏和满足条件的处理逻辑:CyclicBarrier barrier = new CyclicBarrier(2,() -> {System.out.println(“[” + Thread.currentThread().getName() + “]” + “task1and2 finish…”);});
    * 多个子线程等待栅栏条件触发(有2个await时,自动执行栅栏逻辑): barrier.await();

  3. Exchanger 交换器,两个线程的数据交换,多个偶数线程时,随机配对。线程需同时(指定时间范围内)到达。Exchanger参考文章
    * 初始化:Exchanger exchanger = new Exchanger<>()
    * 交换数据:Object data = exchanger.exchange(data); //两个线程会互相拿到对方的数据

线程的五种状态

线程状态参考文章
线程状态切换
new : 线程对象被实例化(new)
runable: 线程实例调用start启动,分为等待cpu分配资源(ready)、或正在运行(running)
blocked: 线程等待其他线程释放锁,synchronized 。
waiting:当前线程发起等待(不限期),如Object.wait()。另一个线程通过notify可以唤醒
time_waiting: 有超时时间的等待,和waiting类似,由当前线程发起。Object.wait(time)
terminated: 终止,线程执行完成

阻塞线程的四种方式对比

Thread.sleep;Object.wait;Condition.await;LockSupport.park;
阻塞线程的四种方式
参考文章

  • RetraintLock结合Condition,做多线程同步
    给RetraintLock添加Condition条件,线程开始时加锁,线程内的所有任务完成后释放锁,中间当不满足当前线程的执行条件时,通过condition的await方法,是当前线程进入waiting状态并释放锁;当前线程任务执行完成后,通过condition的signal方法,唤醒满足条件的线程执行对应任务。

三个线程交替打印1-100数字-condition实现模式
三个线程交替打印1-100数字-原子变量AtomicInteger来做多线程同步

  • CountDownLatch闭锁,通过初始化信号量的值,在线程内做countdown,监听线程阻塞等待countDownLatch降为0后,继续后续任务,可以等多个并发线程都执行完成后,再执行后续的逻辑
    countDownLatch使用

Synchronized 关键字式同步锁

synchronized核心组件

  • synchronized是非公平锁,线程进入时,会首先通过自旋尝试获取锁,获取到则直接成owner,也可以竞争成为onDeck(准备线程);获取不到,则进入ContentionList(竞争队列),contentionList会有频繁的cas写入,Owner unlock时,会将contentionList的一部分线程迁移到EntryLIst,并指定entryLIst的某个线程进入OnDesk(准备线程),Owner被wait后进入waitSet,被notify唤醒后,重新进入到EntryList;Owner释放锁后,Ondesk尝试竞争锁成为Owner.
  • waitSet/Contentionlist/EntryList中的线程都处于阻塞状态,该状态是有操作系统的内核函数实现的。线程的切换需要操作系统从内核态到用户态的切换,上下文切换。
  • 每个对象都有monitor对象,加锁是在竞争monitor对象,通过指令实现,需要调用操作系统相关接口,是低效的。1.6有很多优化,如适应自旋、锁消除、锁粗化、轻量级锁及偏向锁等,效率有了本质的提高。

reentrantlock api级的可重入锁

  • reentrantlock是实现了lock接口中方法的锁,除了能支持synchronized的所有操作外,还支持公平锁、可中断锁、可轮询锁等。(非公平锁:随机、就近原则分配锁,更高的性能;公平锁:按提出请求的顺序获取锁)
  • lock 提供了丰富的api,如获取等待线程数、总数、添加条件对象(Condition,可通过Condition.await释放锁,可通过signal唤醒)、获取锁、尝试获取锁等。Condittion signal可以唤醒指定的线程;Object notify随机唤醒线程
  • 有lock,必须保证最后(finally)有unlock释放锁
  • 可重入锁,外层获取到锁后,内层也可以获取到锁,reentrantlock和synchronized都是可重入锁
  • aqs //todo

Semaphore 信号量

  • 是一种基于计数的信号量,可以设定阈值。可以用来构建一些数据池、资源池(如数据库链接)。基本能实现reentrantlock的所有功能

其他概念

  • 共享锁与独占锁
    1. 独占锁:只能有一个线程能获取锁。如reentrantLock。是一种悲观保守的加锁策略
    2. 共享锁:允许多个线程同时获取锁,乐观锁,放宽了加锁策略。如ReadWriteLock,允许多个线程进行多操作,读写互斥,写写互斥。aqs的内部类Node 定义了两个常量 SHARED 和 EXCLUSIVE,标识等待线程的锁获取方式。

  • 四种状态的锁:

    1. 无锁状态:
    2. 偏向锁:锁不仅仅不存在多线程竞争,而且锁由同一个线程获取和释放。这种情况下,需要消除消除线程锁重入(轻量级锁cas)的开销,看起来就像这个线程得到偏护。轻量级锁需要依赖多次cas原子操作,而偏向锁只是在置换ThreadId时存在一次cas操作。如果出现多线程竞争,则必须撤销偏向锁
    3. 轻量级锁:在没有多线程竞争的情况下(如果有多线程竞争,则升级到重量级锁),减少重量级锁带来的性能消耗。适用于多线程交替执行某个同步代码块时,如果同一时间有两个以上线程竞争,则升级未重量级锁。
    4. 重量级锁:依赖于操作系统的命令来实现的锁,线程切换需要用户态到内核态的转换,成本非常高。未优化的synchronized就是这种。

分段锁

ConcurrentHashMap 对segment加锁,就是分段锁理念非常好的实践。

cas

  • 比较并交换,新值、旧值、内存值

AQS

  • 抽象的队列同步器,多线程访问共享资源的同步器框架。ReentrantLock/Semaphore/CountDownLatch都依赖它
  • 包含一个voilate int state(代表共享资源)和fifo线程等待队列(多线程争用共享资源被阻塞时进入此队列)
  • 定义了两种资源共享模式,Exclusive独占模式、Share共享模式。独占模式下实现 tryAcquire-tryRelease,共享模式下实现
    tryAcquireShared-tryReleaseShared
  • 一般的锁只实现独占或共享模式的一种,但ReentrantReadWriteLock两种都会实现,共享读和独占写,共享资源(int state)的高16位标识读锁,低16位标识写锁
  • 5
    点赞
  • 8
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
Java多线程线程池是一种重用线程的机制,它可以管理和调度多个线程来执行任务。使用线程池可以提高程序的性能和效率,同时也可以避免线程的频繁创建和销毁。 在Java中,可以使用线程池类ExecutorService和Executors来创建和管理线程池。具体步骤如下: 1. 创建一个线程池对象:可以使用Executors提供的静态方法创建不同类型的线程池,比如newFixedThreadPool固定大小线程池、newCachedThreadPool缓存线程池等。 2. 向线程池提交任务:使用submit方法向线程池提交需要执行的任务,也可以使用execute方法提交任务。任务可以是实现了Runnable接口或Callable接口的类,也可以是Lambda表达式。 3. 线程池执行任务:线程池会根据线程池的规模和任务的数量来调度和执行任务,多个任务会并发执行。 4. 关闭线程池:当不再需要线程池时,可以调用线程池的shutdown方法来关闭线程池,确保所有的任务都被执行完毕。 使用线程池的好处有: 1. 提高性能:线程池可以重用线程,避免线程频繁创建和销毁的开销,提高程序的性能。 2. 提供线程管理和调度:线程池可以管理和调度线程,根据线程池的规模和任务的数量来调度和执行任务。 3. 控制资源的使用:线程池可以限制并发线程的数量,避免过度占用系统资源。 在Java开发中,使用线程池是一种推荐的多线程编程方式,也是阿里巴巴在其《Java开发手册》中强制规定的做法。 Java线程的创建是依赖于系统内核的,通过JVM调用系统库创建内核线程,内核线程与Java Thread是1:1的映射关系。<span class="em">1</span><span class="em">2</span><span class="em">3</span> #### 引用[.reference_title] - *1* *2* [Java多线程(六):线程池详解](https://blog.csdn.net/m0_59140023/article/details/124436385)[target="_blank" data-report-click={"spm":"1018.2226.3001.9630","extra":{"utm_source":"vip_chatgpt_common_search_pc_result","utm_medium":"distribute.pc_search_result.none-task-cask-2~all~insert_cask~default-1-null.142^v93^chatsearchT3_1"}}] [.reference_item style="max-width: 50%"] - *3* [java多线程线程池](https://blog.csdn.net/qq_29996285/article/details/118955325)[target="_blank" data-report-click={"spm":"1018.2226.3001.9630","extra":{"utm_source":"vip_chatgpt_common_search_pc_result","utm_medium":"distribute.pc_search_result.none-task-cask-2~all~insert_cask~default-1-null.142^v93^chatsearchT3_1"}}] [.reference_item style="max-width: 50%"] [ .reference_list ]
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值