一.原子类
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,只不过是参数不一样
我们在使用线程池还是根据业务自定义。