并发工具类
CountDownLatch 计数器
CountDownLatch 是一个辅助工具类,它允许一个或多个线程等待一系列指定操作的
完成。CountDownLatch 以一个给定的数量初始化。countDown() 每被调用一次,这
一数量就减一。通过调用 await() 方法之一,线程可以阻塞等待这一数量到达零。
适用于等待多个任务一起执行完成。最后执行总任务流程。
比如之前分销系统,等待各个微服务统计结束后,再次统计所有服务获得的总收益比较方便。
CyclicBarrier栅栏
CyclicBarrier一个同步辅助类,它允许一组线程互相等待,直到到达某个公共屏障点
(common barrier point)。在涉及一组固定大小的线程的程序中,这些线程必须不时地
互相等待,此时 CyclicBarrier 很有用。因为该 barrier 在释放等待线程后可以重用,所
以称它为循环 的 barrier。
需要所有的子任务都完成时,才执行主任务,这个时候就可以选择使用CyclicBarrier。
当所有的线程都准备好后,一起执行,最后一个线程await后将唤醒所有线程
Phaser移相器
阶段器——多段奔跑,有点像月考,一天考好多门课程
register注册
deregister取消注册
核心方法:
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分类
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读写锁是以共享锁方式实现的,属于乐观锁
- StampedLock的写锁、悲观读锁,属于悲观锁。
- StampedLock的乐观读锁,属于乐观锁。
4.可重入锁
1.当一个线程想要获取本线程已经持有的锁时,不会被阻塞,而是能够再次获得这个锁,这就是重入锁。
- Synchornized是一种可重入锁,内部维护一个线程标志(谁持有锁),以及一个计数器。
- 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模式
简单来说,客户端请求之后,先返回一个应答结果,然后异步的去准备数据,客户端可
以先去处理其他事情,当需要最终结果的时候再来获取, 如果此时数据已经准备好,则
将真实数据返回;如果此时数据还没有准备好,则阻塞等待。
时序图原理
空心箭头表示异步执行
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
CompletionService特别实用
总分总模式
轻松完成master-work模式开发
这张图说明了CompletionService的使用模式
你可用它不断的提交任务(线程)给Executor处理
处理后的结果都会自动放入BlockedQueue
另外一个线程不断的从队列里取得处理结果
好处是,哪个任务先处理完就能先得到哪个结果
最后做汇总处理
从而轻松完成MasterWorker模式相同的功能
这张图说明了CompletionService的本质
就是线程池Executor加上阻塞队列BlockedQueue
线程池Executor用来处理任务
BlockedQueue用来获取每个线程的运行结果
基本用法:
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模式--分治思想
图解
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);
8.工厂模式(自研)
线程工厂