并发笔记(八)JUC原子类以及线程池(Executors)

一.原子类

JUC中提供了针对一个变量的修改的简单原子操作,提供了原子类,相对于我们自己采用锁的方式实现来说,原子类的性能更好。

1.1原子类的底层实现原理理论:volatile+(循环的CAS)

CAS大致流程:每次修改的时候都会拿着之前读到的内容当做期望值,和当前值进行比较,如果说两者相等,那么可以将修改后的值进行赋值。

分析:
首先通过volatile保证可见性,也就是每次修改之后对于其他线程是可见的。

但是我又想到之前学的内容,volatile虽然可以保证可见性,但是存在竞态条件,那么肯定这个CompareAndSet是原子操作的。那么这个CAS是怎么保证原子性的,随便找一个原子类,看内部的源码,都是调用Unsafe类的方法,其内部提供的方法又是native的,网上看了一些资料,需要翻到jdk的源码。

在这里插入图片描述
大致能看懂,LOCK_IF_MP(mp) cmpxchg… 也就是多处理器的时候需要使用CPU的lock指令,实现原子性的,所以说是无锁应该是针对java层来说的,但是底层由CPU直接lock指令的性能比java层来触发要好的多,单处理器(单线程的)由于没有同一时刻的多线程,通过可见性以及cmpxchg就可以保证线程的安全性。

1.2 原子化的基本数据类型

JUC提供的原子类,分为五个类别:原子化的基本数据类型原子化的对象引用类型原子化数组原子化对象属性更新器原子化的累加器图片来源
在这里插入图片描述

原子化的基本数据类型: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

相对于上面的原子化的基本数据类型引用数据类型存在ABA的问题,解决ABA的问题方式通过添加自增的版本号,那么A->B->A的过程通过版本号进行记录。

AtomicStampedReference方法:


boolean compareAndSet(
  V expectedReference,//期望值
  V newReference,//更新值
  int expectedStamp,//期望的版本号
  int newStamp) // 新的版本号

AtomicMarkableReference 的实现机制则更简单,将版本号简化成了一个 Boolean 值,方法签名如下:


boolean compareAndSet(
  V expectedReference,
  V newReference,
  boolean expectedMark,
  boolean newMark)
原子化数组:AtomicIntegerArray、AtomicLongArray 和 AtomicReferenceArray

原子化地更新数组里面的每一个元素。这些类提供的方法和原子化的基本数据类型的区别仅仅是:每个方法多了一个数组的索引参数

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

创建更新器的方法:


public static <U> AtomicXXXFieldUpdater<U> 
	newUpdater(Class<U> tclass,String fieldName)

需要注意的是,对象属性必须是 volatile 类型的,只有这样才能保证可见性,如果对象属性不是 volatile 类型的,newUpdater() 方法会抛出 IllegalArgumentException 这个运行时异常。

原子化的累加器:DoubleAccumulator、DoubleAdder、LongAccumulator 和 LongAdder

累加器仅支持累加的操作,相对于原子化的基本数据类型,累加的操作速度更快。

二.线程池

2.1线程池的基础工作原理

线程对象在JAVA中是一个重量级对象,因为java中的线程不仅仅是在堆上申请一个空间,需要调用操作系统的API,然后操作系统还需要为线程分配一系列的资源,所以在开发的过程中我们要避免频繁的创建线程和销毁线程,合理的使用资源,所以采用池化技术。

但是线程池的池化技术,和我们不同的池化技术不一样,普通的池化模型对外提供acquire,release方法,但是如果线程池按照这个模型去封装的话,对于使用方很不友好,如果达到最大线程数怎么办,等待的任务如何存放等等。最好是在进一步的封装,把线程的的获取和释放也封装进去,对外暴露统一的入口(放任务)。类似于生产者-消费者模型,调用方为生产者,线程池为消费者。

简化版的线程池实现代码:便于理解

代码来源


//简化的线程池,仅用来说明工作原理
class MyThreadPool{
  //利用阻塞队列实现生产者-消费者模式
  BlockingQueue<Runnable> workQueue;
  //保存内部工作线程
  List<WorkerThread> threads 
    = new ArrayList<>();
  // 构造方法
  MyThreadPool(int poolSize, 
    BlockingQueue<Runnable> workQueue){
    this.workQueue = workQueue;
    // 创建工作线程
    for(int idx=0; idx<poolSize; idx++){
      WorkerThread work = new WorkerThread();
      work.start();
      threads.add(work);
    }
  }
  // 提交任务
  void execute(Runnable command){
    workQueue.put(command);
  }
  // 工作线程负责消费任务,并执行任务
  class WorkerThread extends Thread{
    public void run() {
      //循环取任务并执行
      while(true){ ①
        Runnable task = workQueue.take();
        task.run();
      } 
    }
  }  
}

/** 下面是使用示例 **/
// 创建有界阻塞队列
BlockingQueue<Runnable> workQueue = 
  new LinkedBlockingQueue<>(2);
// 创建线程池  
MyThreadPool pool = new MyThreadPool(
  10, workQueue);
// 提交任务  
pool.execute(()->{
    System.out.println("hello");
});
2.2 ThreadPoolExecutor
public 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:丢弃最老的任务,其实就是把最早进入工作队列的任务丢弃,然后把新任务加入到工作队列。

一般都需要我们手动的设置threadFactory,定义业务线程名,方便定位问题,和实现handler拒绝策略(需要保存任务,可以存储到redis或者是消息中间件中)

之前的笔记中记录了合理线程数设置的公式:

		核数  X (1+io耗时/cpu耗时)
2.3JDK中提供的直接使用的四种线程池

1.SingleThreadPool:只有一个线程的线程池
2.FixedThreadPool:固定数量的线程池
3.CachePool:有弹性的线程池,来一个任务启动一个,只要没有闲的就会启动新的
4.ScheduledPool:定时任务来执行的线程池

四个线程池底层就是:ThreadPoolExcutor,只不过是参数不一样

我们在使用线程池还是根据业务自定义。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值