• 并发编程精通


    并发工具类

    CountDownLatch 计数器

    CountDownLatch 是一个辅助工具类,它允许一个或多个线程等待一系列指定操作的

    完成。CountDownLatch 以一个给定的数量初始化。countDown() 每被调用一次,这

    一数量就减一。通过调用 await() 方法之一,线程可以阻塞等待这一数量到达零。

    适用于等待多个任务一起执行完成。最后执行总任务流程。

    比如之前分销系统,等待各个微服务统计结束后,再次统计所有服务获得的总收益比较方便。

    CyclicBarrier栅栏

    CyclicBarrier一个同步辅助类,它允许一组线程互相等待,直到到达某个公共屏障点

    (common barrier point)。在涉及一组固定大小的线程的程序中,这些线程必须不时地

    互相等待,此时 CyclicBarrier 很有用。因为该 barrier 在释放等待线程后可以重用,所

    以称它为循环 的 barrier。

    需要所有的子任务都完成时,才执行主任务,这个时候就可以选择使用CyclicBarrier。

    当所有的线程都准备好后,一起执行,最后一个线程await后将唤醒所有线程

    Phaser移相器

    阶段器——多段奔跑,有点像月考,一天考好多门课程

    register注册

    deregister取消注册

    image-20211227163031372

    核心方法:

    Phaser phaser = new Phaser(5);
    phaser.arriveAndAwaitAdvance();到达
    phaser.arriveAndDeregister();当前线程任务结束,不参与后续执行,取消登记
    

    phase计数,初始为0;当一个阶段所有线程arrive时,会将phase+1,这个动作称为advance,当计数器达到integer.MAX_VALUE时,会被重置为0,开始下一个循环。

    在advice时,会触发onadvice方法的执行

    Semaphore许可证

    Semaphore一个计数信号量。信号量维护了一个许可集合; 通过acquire()和release()

    来获取和释放访问许可证。只有通过acquire获取了许可证的线程才能执行,否则阻塞。

    通过release释放许可证其他线程才能进行获取。

    核心方法:

    acquire()
    release()
    

    公平性:

    公平性:没有办法保证线程能够公平地可从信号量中获得许可。也就是说,无法担保

    掉第一个调用 acquire() 的线程会是第一个获得一个许可的线程。如果第一个线程在等

    待一个许可时发生阻塞,而第二个线程前来索要一个许可的时候刚好有一个许可被释

    放出来,那么它就可能会在第一个线程之前获得许可。如果你想要强制公平,

    Semaphore 类有一个具有一个布尔类型的参数的构造子,通过这个参数以告知

    Semaphore 是否要强制公平。强制公平会影响到并发性能,所以除非你确实需要它否

    则不要启用它。

    默认为false
    final Semaphore semp = new Semaphore(2);
    以下设置为true
    final Semaphore semp = new Semaphore(2,true);
    

    维护公平需要更多代价,非公平可以增加效率

    Exchanger 线程数据交换

    Exchanger 类表示一种两个线程可以进行互相交换对象的会和点。

    只能用于两个线程之间,并且两个线程必须都到达汇合点才会进行数据交换

    ReentrantLock重入锁

    ReentrantLock可以用来替代Synchronized,在需要同步的代码块加上锁,最后一定要

    释放锁,否则其他线程永远进不来。

    Condition

    可以使用Condition来替换wait和notify来进行线程间的通讯,Condition只针对某一把锁

    一个Lock可以创建多个Condition,更加灵活,相比较原来的notifyall减小了notifyall的颗粒度

    condition.await();  // Object wait,释放锁
    condition.signal();		//Object notify
    

    公平性

    ReentrantLock的构造函数可以如传入一个boolean参数,用来指定公平/非公平模式,

    默认是false非公平的。非公平的效率更高。

    ReentrantReadWriteLock读写锁

    ReentrantReadWriteLock读写锁,采用读写分离机制,高并发下读多写少时性能优于ReentrantLock

    读读共享,写写互斥,读写互斥

    ReentrantReadWriteLock.readLock()
    ReentrantReadWriteLock.writeLock()
    

    stampLock邮戳锁

    写锁,悲观读锁,乐观读锁

    stampLock中引入了一个stamp(邮戳)的概念。它代表线程获取到锁的版本,每一把锁都有一个唯一的stamp

    写锁writeLock,是排它锁,也叫独占锁,相同时间只能有一个线程获取锁,其他线程请求读锁和写锁都会被阻塞

    功能类似于ReentrantReadWriteLock.writeLock

    区别是stampLock的写锁是不可重入锁。当前没有线程持有读锁或写锁的时候才可以获得获取到该锁

    long stamp1 = sl1.tryWriteLock();
    System.out.println("get StampedLock.tryWriteLock lock1,stamp="+stamp1);
    long stamp2 = sl1.tryWriteLock();
    System.out.println("can not get StampedLock.tryWriteLock lock1,stamp="+stamp2);
    long stamp3 = sl1.writeLock();
    tryWriteLock尝试获取锁,获取不到也不阻塞继续执行
    stamp为0表示获取锁失败
    
    writeLock()要和unwriteLock()一起使用
    

    乐观读锁

    long stamp = sl.tryOptimisticRead();
    StampedLock.validate(stamp)
    以上两者一定要紧紧挨着使用,否则在获取和验证之间很可能数据被修改。如果期间锁发生变化,则返回false
    乐观读锁本质上并未加锁,
    

    悲观读锁

    是一个共享锁,没有线程占用写锁的情况下,多个线程可以同时获取读锁。

    读锁可以多次获取,但每次读锁的stamp都不同

    为什么叫悲观锁?

    认为数据是极有可能被修改的,所以使用数据之前都需要先加锁。

    读写读多写少的场景十分常用

    LockSupport(重要)

    1、LockSupport的底层采用Unsafe类来实现,他是其他同步类的阻塞与唤醒的基础。

    2、park与unpark需要成对适用,parkUntil与parkNanos可以单独适用

    3、先调用unpark再调用park会导致unpark失效

    4、线程中断interrupte会导致park失效并且不抛异常

    5、例如blocker可以对堆栈进行追踪,官方推荐,例如结合jstack进行使用

    AQS原理思想(不熟练)

    1、抽象队列同步器简称AQS,它同步器的基础组件,JUC种锁的底层实现均依赖于AQS,开发不需要使用。。

    2、采用FIFO的双向队列实现,队列元素为Node(静态内部类),Node内的thread变量用于存储进入队列的线程。

    3、Node节点内部的SHARED用来标记该线程是获取共享资源时被阻塞挂起后放入AQS队列的,

    EXCLUSIVE用来标记线程是获取独占资源时被挂起后放入AQS队列的。waitStatus记录当前线程等待状态可以为CANCELLED(线程被取消了)、SIGNAL(线程需要被唤醒)、CONDITION(线程在条件队列里面等待)、PROPAGATE(释放共享资源时需要通知其他节点);prev记录当前节点的前驱节点,next记录当前节点的后继节点。

    4、在AQS中维持了一个单一的状态信息state,可以通过getState、setState、compareAndSetState函数修改其值。对于ReentrantLock的实现来说,state可以用来表示当前线程获取锁的可重入次数;对于读写锁ReentrantReadWriteLock来说,state的高16位表示读状态,也就是获取该读锁的次数,低16位表示获取到写锁的线程的可重入次数;对于semaphore来说,state用来表示当前可用信号的个数;对干CountDownlatch来说,state用来表示计数器当前的值。

    AQS分类

    image-20211228135127420

    1.乐观锁/悲观锁-StampedLock.tryOptimisticRead

    \1. 乐观锁和悲观锁的概念来自于数据库

    \2. 悲观锁对数据被修改持悲观态度,认为数据很容易就会被其他线程修改,所以在处理数据之前先加锁

    处理完毕释放锁。

    3.乐观锁对数据被修改持乐观态度,认为数据一般情况下不会被其他线程修改,所以在处理数据之前不

    会加锁,而是在数据进行更新时进行冲突检测

    \4. 对于数据库的悲观锁就是排它锁,在处理数据之前,先尝试给记录加排它锁,如果成功则继续处理,

    如果失败则挂起或抛出异常,直到数据处理完毕释放锁。

    5.对于数据库的乐观锁所典型的就是CAS方式更新,例如∶update name='kevin'where id=1 and

    name='kevin0',在更新数据的时候校验这个值是否发生了变化,类似于CAS的操作。

    2.公平锁/非公平锁--ReentrantLock

    1.据线程获取锁的抢占机制,锁可以分为公平锁和非公平锁,最早请求锁的线程将最早获取到锁。而非

    公平锁则先请求不一定先得。JUC中的ReentrantLock提供了公平和非公平锁特性。2.公平锁∶ ReentrantLock pairLock = new ReentrantLock(true)

    3.非公平锁∶ReentrantLock pairLock=new ReentrantLock(false),如果构造函数不传递参数,则默认

    是非公平锁。

    4.非必要情况下使用非公平锁,公平锁存在性能开销

    3.排它锁/共享锁--ReentrantLock,ReadWriteLock

    1.只能被单个线程所持有的锁是独占锁,可以被多个线程持有的锁是共享锁。

    2.ReentrantLock就是以独占方式实现的,属于悲观锁

    3.ReadWriteLock读写锁是以共享锁方式实现的,属于乐观锁

    1. StampedLock的写锁、悲观读锁,属于悲观锁。
    2. StampedLock的乐观读锁,属于乐观锁。

    4.可重入锁

    1.当一个线程想要获取本线程已经持有的锁时,不会被阻塞,而是能够再次获得这个锁,这就是重入锁。

    1. Synchornized是一种可重入锁,内部维护一个线程标志(谁持有锁),以及一个计数器。
    2. ReentrantLock也是一种可重入锁

    4.ReadWriteLock、StampedLock的读锁也是可重入锁

    5.自旋锁-先不阻塞

    \1. 当获取锁的时候如果发现锁已经被其他线程占有,则不阻塞自己,也不释放CPU使用权,而是尝试多

    次获取,如果尝试了指定次数之后仍然没有获得锁,再阻塞线程。

    \2. 自旋锁认为锁不会被长时间持有,使用CPU时间来换取线程上下文切换的开销,从而提高性能。但是

    可能会浪费CPU资源。

    3.-XX∶PreBlockSpin=n可以设置自旋次数(已经成为了历史),在Jdk7u40时被删除了,其实在jkd6的时

    候就已经无效了,现在HotSpotVM采用的是adaptive spinning(自适应自旋),虚拟机会根据情况来对每个线程使用不同的自旋次数。

    乐观认为数据不会被占用很长时间,减少线程唤醒切换上下文的消耗。

    底层就是死循环cas方式去更新就是自旋锁的原理

    高性能随机数ThreadLocalRandom

    Random存在性能缺陷,主要原因是要不断的计算新的种子更新原种子,使用CAS方法。高并发的情况下会造成大量的线程自旋,而只有一个线程会更新成功。

    Random使用自旋操作去更新同一个随机种子

    ThreadLocalRandom采用ThreadLocal的机制,每一个线程都是用自己的种子去进行计算下一个种子,规避CAS在并发下的问题。性能至少提升3倍以上,原来需要3分钟,现在1分钟不到

    protected int next(int bits) {
        long oldseed, nextseed;
        AtomicLong seed = this.seed;
        do {
            oldseed = seed.get();
            nextseed = (oldseed * multiplier + addend) & mask;
        } while (!seed.compareAndSet(oldseed, nextseed));
        return (int)(nextseed >>> (48 - bits));
    }
    
    final long nextSeed() {
        Thread t; long r; // read and update per-thread seed
        UNSAFE.putLong(t = Thread.currentThread(), SEED,
                       r = UNSAFE.getLong(t, SEED) + GAMMA);
        return r;
    }
    
    ThreadLocalRandom.current().nextInt(50);
    

    高性能累加器LongAddr

    AtomicLong存在性能瓶颈,由于使用CAS方法。高并发的情况下会造成大量的线程自旋,而只有一个线程会更新成功,浪费CPU资源。

    LongAdder的思想是将单一的原子变量拆分为多个变量,从而降低高并发下的资源争抢。

    没有incrementAndGet、decrementAndGet这种方法,只有单独的increment、longValue这种方法,如果组合使用则需要自己做同步控制,否则无法保证原子性。

    LongAddr本质上是一种空间换时间的策略,累加器家族还有以下3种

    java.util.concurrent.atomic.DoubleAdder

    java.util.concurrent.atomic.LongAccumulator

    java.util.concurrent.atomic.DoubleAccumulator

    LongAdder是LongAccumulator的特例,DoubleAdder是DoubleAccumulator的特列Accumulator的特点是可以设置初始值、自定义累加算法

    四种线程池

    0.基础

    cachedThreadPool.execute(task);只能提交没有返回值的线程
    submit都可以,有返回值的线程callable和普通的task
    
    
    

    1.newCachedThreadPool

    newCachedThreadPool 具有缓存性质的线程池,线程最大空闲时间60s,线程可重复利用(缓存特性),没有最大线程数限制。任务耗时端,数量大。

    //core 核心线程数	当前池的最小线程数
    //max  最大线程数	当前线程池的最大线程数
    //timeout 超时时间   线程空闲时间超时就销毁
    //queue    等待队列  必须是阻塞队列   BlockingQueue<Runnable> workQueue
    
    public static ExecutorService newCachedThreadPool() {
        return new ThreadPoolExecutor(0, Integer.MAX_VALUE,
                                      60L, TimeUnit.SECONDS,
                                      new SynchronousQueue<Runnable>());
    这里使用的队列是无容阻塞队列,就是不允许等待
    //core 核心线程数	当前池的最小线程数	0
    //max  最大线程数	当前线程池的最大线程数  max
    //timeout 超时时间   线程空闲时间超时就销毁  60s
    //queue    等待队列  		SynchronousQueue无容量队列	
    

    2.newFixedThreadPool (推荐)

    newFixedThreadPool 具有固定数量的线程池,核心线程数等于最大线程数,线程最

    大空闲时间为0,执行完毕即销毁,超出最大线程数进行等待。高并发下控制性能。

    public static ExecutorService newFixedThreadPool(int nThreads) {
        return new ThreadPoolExecutor(nThreads, nThreads,
                                      0L, TimeUnit.MILLISECONDS,
                                      new LinkedBlockingQueue<Runnable>());
    }
    //core 核心线程数	当前池的最小线程数	自定义
    //max  最大线程数	当前线程池的最大线程数  max==自定义
    //timeout 超时时间   线程空闲时间超时就销毁  0 执行结束就销毁
    //queue    等待队列  必须是阻塞队列		LinkedBlockingQueue无界阻塞队列	
    

    线程数量建议

    System.out.println(Runtime.getRuntime().availableProcessors());
    一般是CPU核数我的是16
    

    3.newScheduledThreadPool

    newScheduledThreadPool 具有时间调度特性的线程池,必须初始化核心线程数,

    底层使用DelayedWorkQueue实现延迟特性。

    public ScheduledThreadPoolExecutor(int corePoolSize) {
        super(corePoolSize, Integer.MAX_VALUE, 0, NANOSECONDS,
              new DelayedWorkQueue());
    }
    //core 核心线程数	当前池的最小线程数	自定义
    //max  最大线程数	当前线程池的最大线程数  max
    //timeout 超时时间   线程空闲时间超时就销毁  0 执行结束就销毁
    //queue    等待队列  必须是阻塞队列		DelayedWorkQueue延迟阻塞队列	
    
    延时执行指定任务
    Runnable task = new Runnable() {}
    ScheduledFuture<?> schedule = scheduledThreadPool.schedule(task, i, TimeUnit.SECONDS);
    scheduledThreadPool.shutdown();
    
        public ScheduledFuture<?> schedule(Runnable command,
                                           long delay, TimeUnit unit);
        command		放入任务
        delay		延迟多久执行任务    
    	unit		时间单位
    
    以固定频率执行任务
    public ScheduledFuture<?> scheduleAtFixedRate(Runnable command,
                                                  long initialDelay,
                                                  long period,
                                                  TimeUnit unit);
     	command			执行任务
     	initialDelay	首次执行在多久之后
     	period			每隔多久执行一次  任务实际执行多久不管,时间到了就启用一个线程执行任务
     	unit			时间单位
     
     如果period时间少于任务本身执行的时间,任务会延期!  固定频率低于任务执行时间会有问题
    
    以固定延迟时间执行任务  两个任务之间的空闲时间相等
    public ScheduledFuture<?> scheduleWithFixedDelay(Runnable command,
                                                     long initialDelay,
                                                     long delay,
                                                     TimeUnit unit);
                                                      	command			执行任务
     	initialDelay	首次执行在多久之后
     	delay			延迟多久执行
     	unit			时间单位
    
    scheduleWithFixedDelay.cancel(true);
    停止定时任务  
    false   等任务执行完毕再停止
    true 	立即停止
    scheduledThreadPool.shutdown();
    关闭线程池
    
    

    4.newSingleThreadExecutor

    newSingleThreadExecutor 核心线程数与最大线程数均为1,用于不需要并发,需要顺序执行

        public static ExecutorService newSingleThreadExecutor() {
            return new FinalizableDelegatedExecutorService
                (new ThreadPoolExecutor(1, 1,
                                        0L, TimeUnit.MILLISECONDS,
                                        new LinkedBlockingQueue<Runnable>()));
        }
     	corePoolSize			核心线程=1
     	maximumPoolSize			最大线程=1
     	keepAliveTime			线程空闲时间0,用完就销毁
     	TimeUnit				
     	LinkedBlockingQueue<Runnable> workQueue    无界 阻塞队列
     	
     	关闭线程池,所有线程池通用
     	singleThreadExecutor.shutdown();
    

    自定义线程池

    ThreadPoolExecutor

    四种线程池都是通过Executors类创建的, 底层创建的都是

    ThreadPoolExecutor类, 可以构建自己需要的线程类。

    拒绝策略

    ThreadPoolExecutor executor = new ThreadPoolExecutor(3,3,0,TimeUnit.SECONDS,new LinkedBlockingQueue<Runnable>(1),new MyPolicy());
    
    自定义线程如上,前三个线程会执行任务,第四个线程等待,第五个线程就需要拒绝了。
        public ThreadPoolExecutor(int corePoolSize,
                                  int maximumPoolSize,
                                  long keepAliveTime,
                                  TimeUnit unit,
                                  BlockingQueue<Runnable> workQueue,
                                  RejectedExecutionHandler handler) {
            this(corePoolSize, maximumPoolSize, keepAliveTime, unit, workQueue,
                 Executors.defaultThreadFactory(), handler);
        }
    
    拒绝策略:
    		//AbortPolicy			抛出异常,不影响其他线程运行
    		//CallerRunsPolicy		调用当前任务
    		//DiscardOldestPolicy	丢弃最老的任务
    		//DiscardPolicy			直接丢弃,什么也不做
    
    
    
    默认策略:
        private static final RejectedExecutionHandler defaultHandler =
            new AbortPolicy();
    
    
    

    自定义拒绝策略

    class MyPolicy implements RejectedExecutionHandler{
       @Override
       public void rejectedExecution(Runnable r, ThreadPoolExecutor executor) {
          System.out.println(r+"被拒绝执行"+System.currentTimeMillis());
       }
    }
    

    线程池常用操作

    ThreadFactory

    我们通常使用线程池的submit方法将任务提交到线程池内执行。

    如果此时线程池内有空闲的线程,则会立即执行该任务,如果没有则需要根据线程池的类型选择等待,

    或者新建线程。

    所以线程池内的线程并不是线程池对象初始化(new)的时候就创建好的。而是当有任务被提交进来之

    后才创建的,而创建线程的过程是无法干预的。

    如果我们想在每个线程创建时记录一些日志,或者推送一些消息那怎么做?

    使用ThreadFactory

    第一步: 编写ThreadFactory接口的实现类

    第二步: 创建线程池时传入ThreadFactory对象

    线程池内异常优雅处理

    方式1:线程内部抓取异常

    static class Runner1 implements Runnable {
        public void run() {
            try {
                P.l(Thread.currentThread().getName()+" run");
                int i = 10/0;
            }catch(Exception e){
                P.l(Thread.currentThread().getName()+" exception record:"+e.getMessage());
            }
        }
    }
    

    方式2:ThreadFactory

    重写uncaughtException方法+线程池提交方法使用execute方法,用submit无效!!

    static class ThreadFactory3 implements ThreadFactory{
    
        @Override
        public Thread newThread(Runnable r) {
            Thread thread = new Thread(r);
            thread.setName("KEVIN "+System.currentTimeMillis());
            thread.setUncaughtExceptionHandler(new Thread.UncaughtExceptionHandler() {
                @Override
                public void uncaughtException(Thread t, Throwable e) {
                    P.l(t.getName()+" exception " + e.getMessage());
                }
            });
            return thread;
        }
        
        cachedThreadPool.execute(task);只能提交没有返回值的线程
    

    关闭线程池--shutdown

    shutdown shutdownNow

    shutdown让线程池内的任务继续执行完毕,但是不允许新的任务提交

    shutdown方法不阻塞, 等所有线程执行完毕后,销毁线程

    shutdown之后提交的任务会抛出RejectedExecutionException异常,代表拒绝接收

    shutdownNow之后提交的任务会抛出RejectedExecutionException异常,代表拒绝接收

    shutdownNow之后会引发sleep、join、wait方法的InterruptedException异常

    如果任务中没有触发InterruptedException的条件,则任务会继续运行直到结束

    线程池状态

    • isShutdown用来判断线程池是否已经关闭

    • isTerminated任务全部执行完毕,并且线程池已经关闭,才会返回true

    • awaitTermination 阻塞,直到所有任务在关闭请求后完成执行,或发生超时,或当前线程中断(以先发生者为准)。

    允许核心线程超时策略

     核心线程也允许销毁, allowsCoreThreadTimeOut就用来做这个事

     设置控制核心线程是否可能超时的策略,如果在保持活动时间内没有任务到达,则该策略将在新任

    务到达时根据需要被替换。

     如果为false,则不会由于缺少传入任务而终止核心线程。

     如果为true,则应用于非核心线程的相同保持活动策略也适用于核心线程。

     为避免连续更换线程,设置为true时保持活动时间必须大于零。

     通常应该在池被激活之前调用此方法。

    默认关闭核心线程也允许销毁配置
    P.l("是否允许核心线程超时:"+pool.allowsCoreThreadTimeOut());
    

    核心线程预启动策略

     默认情况下,核心线程只有在任务提交的时候才会创建

     而预启动策略,可以让核心线程提前启动,从而增强最初提交的线程运行性能

     prestartCoreThread启动1个核心线程,覆盖仅在执行新任务时启动核心线程的默认策略。如果所

    有核心线程都已启动,则此方法将返回false。

     prestartAllCoreThreads启动所有核心线程。覆盖仅在执行新任务时启动核心线程的默认策略。如

    果核心线程全部启动后再次调用,则会返回0

    P.l("启动核心线程数量为:" +executor.prestartCoreThread());//启动1个
    P.l("启动核心线程数量为:" +executor.prestartAllCoreThreads());//启动所有核心线程
    

    建议把这个程序放在项目初始化的时候去做

    线程及线程池切面

     在线程执行前、执行后增加切面,在线程池关闭时执行某段程序。

     需要实现自己的线程池类,并覆写beforeExecute、afterExecute、terminated(终止时执行)方法

    一般用来记日志

    移除线程池中的任务

     使用remove方法

     已经正在运行中的任务不可以删除

     execute方法提交的,未运行的任务可以删除

    submit****方法提交的,未运行任务就不可以删除,小心采坑!

    P.l("线程池中的任务数:"+executor.getTaskCount());
    

    获取各种线程池状态数据

    可以获取线程池的各种动态和静态数据,用于程序控制。

     返回核心线程数getCorePoolSize

     返回当前线程池中的线程数getPoolSize

     返回最大允许的线程数getMaximumPoolSize

     返回池中同时存在的最大线程数getLargestPoolSize

     返回预定执行的任务总和getTaskCount

     返回当前线程池已经完成的任务数getCompletedTaskCount

     返回正在执行任务的线程的大致数目getActiveCount

     返回线程池空闲时间getKeepAliveTime

    P.l("返回核心线程数getCorePoolSize:"+executor.getCorePoolSize());
    P.l("返回当前线程池中的线程数getPoolSize:"+executor.getPoolSize());
    P.l("返回最大允许的线程数getMaximumPoolSize:"+executor.getMaximumPoolSize());
    P.l("返回池中同时存在的最大线程数getLargestPoolSize:"+executor.getLargestPoolSize());
    P.l("预定执行的任务总和getTaskCount:"+executor.getTaskCount());
    P.l("当前线程池已经完成的任务数getCompletedTaskCount:"+executor.getCompletedTaskCount());
    P.l("正在执行任务的线程的大致数目getActiveCount:"+executor.getActiveCount());
    P.l("空闲时间getKeepAliveTime:"+executor.getKeepAliveTime(TimeUnit.SECONDS));
    P.l("------------------------------------");
    Thread.sleep(1100);
    P.l("当前线程池已经完成的任务数getCompletedTaskCount:"+executor.getCompletedTaskCount());
    Thread.sleep(1100);
    P.l("当前线程池已经完成的任务数getCompletedTaskCount:"+executor.getCompletedTaskCount());
    executor.shutdown();
    P.l("当前线程池已经完成的任务数getCompletedTaskCount:"+executor.getCompletedTaskCount());
    

    并发设计模式

    1.单例模式

    饿汉模式

    类加载的时候,就进行对象的创建,系统开销较大,但是不存在线程安全

    * 饿汉模式-单例对象构建方法
    * (类加载的时候,就进行对象的创建,系统开销较大,影响性能,所以多数采用饿汉模式,在使用时才真正的创建单例对象)
    方式一:饿汉模式:性能最差
    class Singleton1 {
    	private static Singleton1 singleton = new Singleton1(); // 建立对象
    	
    问题:每次类加载的时候,就进行对象的创建,性能开销大
    

    懒汉模式

    多数采用饿汉模式,在使用时才真正的创建单例对象,但是存在线程安全

    懒汉模式

    线程不安全

    方式二:懒汉模式-性能不安全
    class Singleton2 {
    	private static Singleton2 singleton = null; // 不建立对象
    /*synchronized 可以解决线程安全问题,但是存在性能问题,即使singleton!=null也需要先获得锁*/
    public synchronized static Singleton2 getInstance() {
    	if (singleton == null) { // 先判断是否为空
    		try {
    			Thread.sleep(1000);
    			System.out.println("构建这个对象可能耗时很长...");
    		} catch (InterruptedException e) {
    			e.printStackTrace();
    		}
    		singleton = new Singleton2(); // 懒汉式做法
    	}
    	return singleton;
    }
    

    问题:每次线程都要走同步代码块性能 不高

    线程安全

    方式三:懒汉模式-性能安全
    class Singleton3 {
    	private static Singleton3 singleton = null; // 不建立对象
    	private Singleton3() {
    	}
    	/*synchronized 代码块进行双重检查,可以提高性能*/
    	public static Singleton3 getInstance() {
    		if (singleton == null) { // 先判断是否为空
    			synchronized (Singleton3.class) {
    				//此处必须进行双重判断,否则依然存在线程安全问题
    				if(singleton == null){
    					try {
    						Thread.sleep(1000);
    						System.out.println("构建这个对象可能耗时很长...");
    					} catch (InterruptedException e) {
    						e.printStackTrace();
    					}
    					singleton = new Singleton3(); // 懒汉式做法
    				}
    			}
    		}
    		return singleton;
    	}
    
    这里的synchronize只有开头几个线程会进来,此时singleton为null。
    为什么要进行双重判断?
    因为同步代码块的存在,多个线程程序刚启动的时候会一起进来,1个线程进入代码块执行方法,结束后,其他线程执行方法前要判断这个对象是否为null,这里的判断是判断是否一初始化实例。
    第一次是判断单例是否存在,准备使用
    第二次是判断单例是否存在,准备创建
    

    静态内部类单例

    兼具懒汉模式和饿汉模式的优点

    class Singleton4 {
       private static class InnerSingletion {
          private static Singleton4 single = new Singleton4();
       }
       	public static Singleton4 getInstance(){
    		return InnerSingletion.single;
    	}
    	
    
    类第一次被使用的时候才会初始化。
    

    2.Future模式

    简单来说,客户端请求之后,先返回一个应答结果,然后异步的去准备数据,客户端可

    以先去处理其他事情,当需要最终结果的时候再来获取, 如果此时数据已经准备好,则

    将真实数据返回;如果此时数据还没有准备好,则阻塞等待。

    时序图原理

    image-20211229093506901

    空心箭头表示异步执行

    JDK的Future

     JDK的Concurrent包提供了Futrue模式的实现,可以直接使用。

     使用Futrue模式需要实现Callable接口,并使用FutureTask进行封装,

    使用线程池进行提交。

    自定义future类,重写call方法
    public class JdkFuture implements Callable<String>{
    
    	public JdkFuture(String para){
    		this.para = para;
    	}
    	/**
    	 * 这里是真实的业务逻辑,其执行可能很慢
    	 */
    	@Override
    	public String call() throws Exception {
    		//模拟执行耗时
    		Thread.sleep(5000);
    		String result = this.para + "处理完成";
    		return result;
    	}
    

    3.生产者消费者模式

    Producer-Consumer称为生产者消费者模式,是消息队列中间件的核心实现模式,ActiveMQ、RocketMQ、Kafka、RabbitMQ。

    优点:

    精分业务,系统解耦,两边的系统可以不同,互相不影响!

    4.Master-worker模式

     Master-Worker模式是一种将串行任务并行化的方案,被分解的子任务在系统中可以被

    并行处理,同时,如果有需要,Master进程不需要等待所有子任务都完成计算,就可

    以根据已有的部分结果集计算最终结果集。

     客户端将所有任务提交给Master,Master分配Worker去并发处理任务,并将每一个任

    务的处理结果返回给Master,所有的任务处理完毕后,由Master进行结果汇总再返回给Client

    image-20211229101406912

    CompletionService特别实用

    总分总模式

    轻松完成master-work模式开发

    image-20211229104544134

    这张图说明了CompletionService的使用模式

    你可用它不断的提交任务(线程)给Executor处理

    处理后的结果都会自动放入BlockedQueue

    另外一个线程不断的从队列里取得处理结果

    好处是,哪个任务先处理完就能先得到哪个结果

    最后做汇总处理

    从而轻松完成MasterWorker模式相同的功能

    image-20211229104731848

    这张图说明了CompletionService的本质

    就是线程池Executor加上阻塞队列BlockedQueue

    线程池Executor用来处理任务

    BlockedQueue用来获取每个线程的运行结果

    image-20211229104750303

    基本用法:
    1.传入线程池
    //创建线程池
    ThreadPoolExecutor executor = new ThreadPoolExecutor(3, 10, 5, TimeUnit.SECONDS, new LinkedBlockingDeque<Runnable>());
    //创建CompletionService, 需要借助Executor来实现
    CompletionService cs = new ExecutorCompletionService(executor);
    
    for (int i = 0; i < 3; i++) {
        //阻塞-take获取已经处理结束的结果, 最先处理结束的,最先得到
        Future f = cs.take();
        //非阻塞-poll, 获取不到则返回null
        //Future f = cs.poll();
        //阻塞固定时间-到时间获取不到则返回null
        //Future f = cs.poll(2,TimeUnit.SECONDS);
        System.out.println("result("+i+")="+f.get());
    }
    
    2.获取结果对象
    Future f = cs.take();
    获取结果
    f.get();
    

    5.ForkJoin模式--分治思想

    图解

    image-20211229111531518

    image-20211229113611697

    public class ForkJoinDemo0 {
        public static void main(String[] args) throws InterruptedException {
            //创建ForkJoinPool
            ForkJoinPool pool = new ForkJoinPool();
            //提交任务RecursiveAction0递归action
            pool.submit(new RecursiveAction0(0, 9));
            Thread.sleep(Integer.MAX_VALUE);
        }
    }
    
    //重写compute方法
        @Override
        protected void compute() {
        
                if (endValue - beginValue > 2) {
                //计算中间值
                int middelNum = (beginValue + endValue) / 2;
                //左计算
                RecursiveAction0 leftAction = new RecursiveAction0(beginValue, middelNum);
                //右计算
                RecursiveAction0 rightAction = new RecursiveAction0(middelNum + 1, endValue);
                //拆解
                this.invokeAll(leftAction, rightAction);
    

    image-20211229121345998

    8.工厂模式(自研)

    线程工厂

  • 相关阅读:
    进程间的通讯(IPC)方式
    进程间通信IPC之--共享内存
    TMDS协议
    HDMI接口与协议
    HDMI的CEC是如何控制外围互联设备的
    SVN并行开发管理策略
    关于 javascript event flow 的一个bug
    H面试程序(15): 冒泡排序法
    android应用如何启动另外一个apk应用
    [置顶] 一份诚恳的互联网找工作总结和感想(附:怎样花两年时间去面试一个人)
  • 原文地址:https://www.cnblogs.com/yslu/p/16250484.html
Copyright © 2020-2023  润新知