3 Java并发编程—工具2

原子类

— 性能好,基本不会出现死锁问题(但可能出现饥饿和活锁问题,因为自旋会反复重试)

场景:单一共享变量原理:CAS指令(3个参数,分别为变量的内存地址A,用于比较的值B和共享变量的新值C。当B和内存中的值一样,把变量换成C)

比如下面的代码,类似CAS指令,只有当目前 count 的值和期望值 expect 相等时,才会将 count 更新为 newValue。

class SimulatedCAS{
  int count;
  synchronized int cas(
    int expect, int newValue){
    // 读目前 count 的值
    int curValue = count;
    // 比较目前 count 值是否 == 期望值
    if(curValue == expect){
      // 如果是,则更新 count 的值
      count = newValue;
    }
    // 返回写入前的值
    return curValue;
  }
}

使用方式:CAS+自旋(就是一直循环尝试)

【如果获取的变量值和内存中的值一样,这个变量就没有被修改过,然后调用CAS方法,传入获取的变量值和新值,进行更新】

【如果失败的话,重新获取变量的最新值,再计算要改变的值,然后继续CAS方法来更新】

class SimulatedCAS{
  volatile int count;
  // 实现 count+=1
  addOne(){
    do {
        // 注意要获取共享变量的新值
      newValue = count+1; //①
    }while(count !=
      cas(count,newValue) //②
  }
  // 模拟实现 CAS,仅用来帮助理解
  synchronized int cas(
    int expect, int newValue){
    // 读目前 count 的值
    int curValue = count;
    // 比较目前 count 值是否 == 期望值
    if(curValue == expect){
      // 如果是,则更新 count 的值
      count= newValue;
    }
    // 返回写入前的值
    return curValue;
  }
}

CAS使用示例:

do {
  // 获取当前值
  oldV = xxxx;
  // 根据当前值计算新值
  newV = ...oldV...
}while(!compareAndSet(oldV,newV);

ABA问题:

就是有一个线程T2把变量的值从A改成B,然后另一个线程T3把B改成A,本线程T1获取的是T2改变前的值,调用CAS进行更新的时候,判定通过,但是原来的值已经被修改过,会覆盖掉T2,T3的更改【特别是对象的更新,这个时候加个递增的版本号就好啦】

  • 原子化的基本数据类型:相关实现有 AtomicBoolean、AtomicInteger 和 AtomicLong

    • 常用方法

    • getAndIncrement() // 原子化 i++
      getAndDecrement() // 原子化的 i--
      incrementAndGet() // 原子化的 ++i
      decrementAndGet() // 原子化的 --i
      // 当前值 +=delta,返回 += 前的值
      getAndAdd(delta)
      // 当前值 +=delta,返回 += 后的值
      addAndGet(delta)
      //CAS 操作,返回是否成功
      compareAndSet(expect, update)
      // 以下四个方法
      // 新值可以通过传入 func 函数来计算
      getAndUpdate(func)
      updateAndGet(func)
      getAndAccumulate(x,func)
      accumulateAndGet(x,func)
      
  • 原子化的对象引用类型:相关实现有 AtomicReference、AtomicStampedReference 和 AtomicMarkableReference AtomicStampedReference 和 AtomicMarkableReference 这两个原子类可以解决 ABA 问题

  • 原子化数组 :AtomicIntegerArray、AtomicLongArray 和 AtomicReferenceArray

  • 原子化对象属性更新器:AtomicIntegerFieldUpdater、AtomicLongFieldUpdater 和 AtomicReferenceFieldUpdater

    • 创建更新器的方法如下:【对象属性必须是 volatile 类型的,只有这样才能保证可见性】

    • public static <U>
      AtomicXXXFieldUpdater<U>
      newUpdater(Class<U> tclass,
         String fieldName)
      
  • 原子化的累加器:DoubleAccumulator、DoubleAdder、LongAccumulator 和 LongAdder

    • 这四个类仅仅用来执行累加操作,相比原子化的基本数据类型,速度更快,但是不支持 compareAndSet() 方法。如果你仅仅需要累加操作,使用原子化的累加器性能会更好。
线程池

本质其实不是池化资源,它没有方法去获取和释放线程的方法线程池的本质是生产者消费者模式,生产者是调用者,消费者是线程池,线程池指向调用者的任务

额外注意:CompletableFuture 和 parallel stream 用的是全局的 ForkJoinPool 线程池最好用自定义的,不然如果有io比较慢的任务过来,很容易造成其他任务一直不被执行

线程池构造函数

ThreadPoolExecutor(
  int corePoolSize,
  int maximumPoolSize,
  long keepAliveTime,
  TimeUnit unit,
  BlockingQueue<Runnable> workQueue,
  ThreadFactory threadFactory,        // 通过这个参数你可以自定义如何创建线程,例如你可以给线程指定一个有意义的名字
  RejectedExecutionHandler handler)

线程池参数:

  • corePoolSize:表示线程池保有的最小线程数。有些项目很闲,但是也不能把人都撤了,至少要留 corePoolSize 个人坚守阵地。
  • maximumPoolSize:表示线程池创建的最大线程数。当项目很忙时,就需要加人,但是也不能无限制地加,最多就加到 maximumPoolSize 个人。当项目闲下来时,就要撤人了,最多能撤到 corePoolSize 个人。
  • keepAliveTime & unit:上面提到项目根据忙闲来增减人员,那在编程世界里,如何定义忙和闲呢?很简单,一个线程如果在一段时间内,都没有执行任务,说明很闲,keepAliveTime 和 unit 就是用来定义这个“一段时间”的参数。也就是说,如果一个线程空闲了keepAliveTime & unit这么久,而且线程池的线程数大于 corePoolSize ,那么这个空闲的线程就要被回收了。
  • workQueue:工作队列,和上面示例代码的工作队列同义。
  • threadFactory:通过这个参数你可以自定义如何创建线程,例如你可以给线程指定一个有意义的名字。
  • handler:通过这个参数你可以自定义任务的拒绝策略。如果线程池中所有的线程都在忙碌,并且工作队列也满了(前提是工作队列是有界队列),那么此时提交任务,线程池就会拒绝接收。至于拒绝的策略,你可以通过 handler 这个参数来指定。ThreadPoolExecutor 已经提供了以下 4 种策略。
    • CallerRunsPolicy:提交任务的线程自己去执行该任务。
    • AbortPolicy:默认的拒绝策略,会 throws RejectedExecutionException。
    • DiscardPolicy:直接丢弃任务,没有任何异常抛出。
    • DiscardOldestPolicy:丢弃最老的任务,其实就是把最早进入工作队列的任务丢弃,然后把新任务加入到工作队列。
    • 【如果是一些不重要任务,可以选择直接丢弃。但是如果为重要任务,可以采用降级处理,例如将任务信息插入数据库或者消息队列,启用一个专门用作补偿的线程池去进行补偿。】

Java 在 1.6 版本还增加了 allowCoreThreadTimeOut(boolean value) 方法,它可以让所有线程都支持超时,这意味着如果项目很闲,就会将项目组的成员都撤走。

多线程和线程池的使用—线程间的编排

/**
* 启动方式+定义线程
* 1)、继承Thread
*      Threade1 thread new Threade1()j
*      thread.start();//启动线程
*
*          *      public static class Thread01 extends Thread {
*          *         @Override
*          *         public void run() {
*          *             System.out.println("当前线程:" + Thread.currentThread().getId());
*          *             int i = 10 / 2;
*          *             System.out.println("运行结果:" + i);
*          *         }
*          *     }
*
* 2)、实现Runnable:接口
*      Runable01 runable01 new Runable01();
*      new Thread(runabLee1).start();
*
*          *     public static class Runable01 implements Runnable {
*          *         @Override
*          *         public void run() {
*          *             System.out.println("当前线程:" + Thread.currentThread().getId());
*          *             int i = 10 / 2;
*          *             System.out.println("运行结果:" + i);
*          *         }
*          *     }
*
* 3)、实现Callable接口+FutureTask(可以拿到返回结果,可以处理异常)
*      FutureTask<Integer>futureTask new FutureTask<>(new CaLLable01())j
*      new Thread(futureTask).start();
*      //阻塞等待整个线程执行完成,获取返回结果
*      Integer integer =futureTask.get();
*
*          *     public static class Callable01 implements Callable<Integer> {
*          *         @Override
*          *         public Integer call() throws Exception {
*          *             System.out.println("当前线程:" + Thread.currentThread().getId());
*          *             int i = 10 / 2;
*          *             System.out.println("运行结果:" + i);
*          *             return i;
*          *         }
*          *     }
*
* 4)、线程池  我们以后在业务代码里面,以上三种启动线程的方式都不用。【将所有的多线程异步任务都交给线程池执行】
*      定义一个线程池【有很多种线程池】
*          public static ExecutorService service = Executors.newFixedThreadPool(10);
*
*         // 异步任务提交给线程池执行
*         // 没有返回结果 [可以提交 runnable或者callable]
*         service.execute(new Runnable01());
*         // 有返回结果
*         service.submit(new Runnable01());
*
* 区别:
*      1、2不能得到返回值。3可以获取返回值
*      1、2、3都不能控制资源
*      4可以控制资源,性能稳定
*/

线程池使用示例+工作流程

* //自建线程池,
* ThreadPoolExecutor executor = new ThreadPoolExecutor(
*                 5,
*                 200,
*                 10,
*                 TimeUnit.SECONDS,
*                 new LinkedBlockingDeque<>(100000),  //默认是Integer的最大值,直接new内存不够
*                 Executors.defaultThreadFactory(),
*                 new ThreadPoolExecutor.AbortPolicy()); // 如果不想抛弃,默认运行,使用CallerRunsPolicy

* 工作顺序:
* 1)、线程池创建,准备好core数量的核心线程,准备接受任务
* 1.1、core满了,就将再进来的任务放入阻塞队列中。空闲的core就会自己去阻塞队列获取任务执行
* 1,2、阻塞队列满了,就直接开新线程执行,最大只能开到max指定的数量
* 1,3、max满了就用RejectedExecutionHandler拒绝任务
* 1.4、max都执行完成,有很多空闲.在指定的时间keepAliveTime以后,释放 当前线程数-core这些线程
*
* 面试题:
* 一个线程池core7,max20,queue50,100并发进来怎么分配的;
* 7个会立即得到执行,50个会进入队列,再开13个进行执行。剩下的30个就使用拒绝策略。|
*
* Executors里面常用的线程池:
* Executors.newCachedThreadPool()      core是8,所有都可回收
* Executors.newFixedThreadPool()       固定大小,core=max; 都不可回收
* Executors.newScheduLedThreadPool()   定时任务的线程池
* Executors.newSingleThreadExecutor()  单线程的线程池,后台从队列里面获取任务,挨个执行

线程池使用—注解

  • 线程池配置类
import java.util.concurrent.Executors;
import java.util.concurrent.LinkedBlockingDeque;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;

@EnableConfigurationProperties(ThreadPoolConfigProperties.class)
@Configuration
public class MyThreadConfig {

    @Bean
    public ThreadPoolExecutor executor(ThreadPoolConfigProperties pool) {
        return new ThreadPoolExecutor(
                pool.getCoreSize(),
                pool.getMaxSize(),
                pool.getKeepAliveTime(),
                TimeUnit.SECONDS,
                new LinkedBlockingDeque<>(100000),
                Executors.defaultThreadFactory(),
                new ThreadPoolExecutor.AbortPolicy()
        );
    }
}
  • 配置类,绑定配置文件
import lombok.Data;
import org.springframework.boot.context.properties.ConfigurationProperties;

@ConfigurationProperties(prefix = "gulimall.thread")
// @Component
@Data
public class ThreadPoolConfigProperties {

    private Integer coreSize;
    private Integer maxSize;
    private Integer keepAliveTime;
}
  • 配置文件
#配置线程池
gulimall.thread.coreSize=20
gulimall.thread.maxSize=200
gulimall.thread.keepAliveTime=10

注意事项:

  1. 不要轻易使用Java提供的Executors,因为里面的线程池很多都是无界队列或者是无限多线程,很容易OOM
  2. 要用好默认拒绝策略
  3. 线程池中,若执行任务失败,会报运行时错误,但是编译器不要求强制catch【如果不捕获,执行失败的时候不会收到通知,就以为是执行成功的,很危险】
最好是捕获所有异常
try {
  // 业务逻辑
} catch (RuntimeException x) {
  // 按需处理
} catch (Throwable x) {
  // 按需处理
}
使用FutureTask进行调度 并获取线程执行结果
public class Test6 {
    public static void main( String[] args ) {
        ThreadPoolExecutor poolExecutor = new ThreadPoolExecutor (
                5,
                10,
                1,
                TimeUnit.MINUTES,
                new LinkedBlockingQueue<> (),
                Executors.defaultThreadFactory (),
                new ThreadPoolExecutor.AbortPolicy ()
        );

        FutureTask ft2 = new FutureTask (new Callable () {
            @Override
            public String call() throws Exception {
                System.out.println ("洗茶壶");
                TimeUnit.SECONDS.sleep (1);

                System.out.println ("洗茶杯");
                TimeUnit.SECONDS.sleep (2);

                System.out.println ("拿茶叶");
                TimeUnit.SECONDS.sleep (1);

                return "龙井";
            }
        });

        FutureTask ft1 = new FutureTask (new Callable () {
            @Override
            public String call() throws Exception {
                System.out.println ("烧开水");
                TimeUnit.SECONDS.sleep (15);

                String chaye = (String) ft2.get ();
                System.out.println ("拿到茶叶:"+chaye);
                System.out.println ("泡茶");

                return "上茶:"+chaye;
            }
        });

        System.out.println ("洗水壶");
        try {
            TimeUnit.SECONDS.sleep (1);
        } catch (InterruptedException e) {
            e.printStackTrace ();
        }


        poolExecutor.execute (ft1);
        poolExecutor.execute (ft2);
        
        //Thread thread1 = new Thread (ft1);
        //thread1.start ();
        //
        //Thread thread2 = new Thread (ft2);
        //thread2.start ();


        try {
            System.out.println (ft1.get ());
        } catch (InterruptedException e) {
            e.printStackTrace ();
        } catch (ExecutionException e) {
            e.printStackTrace ();
        }
    }
}
CompletableFuture异步编程

在复杂场景下,可以很方便清晰地按照各个线程的关系顺序异步执行

注意:默认情况下 CompletableFuture 会使用公共的 ForkJoinPool 线程池。如果所有 CompletableFuture 共享一个线程池,那么一旦有任务执行一些很慢的 I/O 操作,就会导致线程池中所有线程都阻塞在 I/O 操作上,从而造成线程饥饿,进而影响整个系统的性能。所以,强烈建议你要根据不同的业务类型创建不同的线程池,以避免互相干扰。

CompletionService

可以批量执行异步任务,里面有一个阻塞队列,任务的future按得到结果的先后顺序放里面阻塞队列

其中一种应用场景:可以从3个地图数据源获取数据,返回最快的数据,返回后取消所有任务

对于简单的并行任务,你可以通过“线程池 +Future”的方案来解决;如果任务之间有聚合关系,无论是 AND 聚合还是 OR 聚合,都可以通过 CompletableFuture 来解决;而批量的并行任务,则可以通过 CompletionService 来解决。

Fork-Join

使用分治思想的线程池,可以把任务分成子任务处理,然后再合并子任务的结果,最后汇总得到总结果

神器呀!!!对于一些可以分治的算法可以多线程处理,速度提高很多(比如归并排序)

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

沛权

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值