threadpool - 线程池
理解为计划经济,资源总量被控制,减少不熟练劳动力带来的过渡开销问题
应用场景
服务器接收大量的请求的时候
实际开发中需要创建5个以上的线程,就可以使用线程池来进行管理
创建和停止
添加线程规则
1.如果线程小于corePoolSize,即使其他工作线程处于空闲状态,也会创建一个新的线程来运行新任务
2.如果线程数等于或者大于corePoolSize但少于maximumPoolSize,则将任务放入队列
3.如果队列已经满,并且线程数小于maxPoolSize,则创建一个显得线程来运行任务
4.如果队列满了,并且线程数大于或等于maxPoolSize,则拒绝该任务
增减线程的特点
1.通过设置corePoolSize和maximumPoolSize相同,就可以创建固定大小的线程池
2.线程池希望保持较少的线程数,并且只有在负载变得很大时才增加它
3.通过设置maximumPoolSize.MAX_VALUE,可以允许线程池容纳任意数量的并发任务
4.是只有在队列填满时次啊创建多于corePoolSize的线程,所以如果你使用的是无界队列(LinkedBlockingQueue),那么线程数就不会超过corePoolSize
构造函数的参数 6个
- corePoolSize (添加线程规则,增减线程的特点)
- maxPoolSize (添加线程规则,增减线程的特点)
- keepAliveTime (如果线程池当前的线程数多于corePoolSize,那么如果多余的线程空闲时间超过KeepAliveTime,他们就会被终止)
- workQueue (添加线程规则,增减线程的特点)
- ThreadFactory (新的线程是由ThreadFactory创建的,默认使用的Executors.defaultThreadFactory(),已经够用了,创建出来的线程都在同一个线程组,优先级为5,非守护线程[可以设置:线程名,线程组,优先级,是否为守护线程])
- handler 拒绝策略
workQueue :
直接交换 SynchronousQueue (任务不多,只是通过队列进行简单的中转,存不下任务的,maxPoolSize要设置的大一点,没有队列缓冲,防止频繁创建新的线程)
无界队列 LinkedBlockingQueue (处理的速度一定要大于任务提交的速度,否则会内存浪费或者OOM异常)
有界队列 ArrayBlockingQueue (maxPoolSize就会更有意义,当对列满了之后,就会常见新的线程)
JDK自带的线程池
newFixedThreadPool corePoolSize=maxPoolSize keepAliveTime=0L(因为不会有线程被回收)时间单位为ms LinkedBlockingQueue - 会产生请求堆积(大多数请求的执行时间过长或者请求暴增),造成占用大量的内存,可能会造成OOM
newSimgleThreadPool corePoolSize=maxPoolSize=1 keepAliveTime=0L(因为不会有线程被回收)时间单位为ms LinkedBlockingQueue
newCacheThreadPool corePoolSize=0, maxPoolSize=Integer.MAX_VALUE,keepAliveTime=60L, TimeUnit.SECONDS, new SynchronousQueue(),可回收,无缓存队列,因为maxPoolSize=Integer.MAX_VALUE,会创建非常多的线程,造成OOM
newScheduledThreadPool corePoolSize, Integer.MAX_VALUE, 0, NANOSECONDS,new DelayedWorkQueue()
1.8加入的 workStealingPool 常用与递归执行的场景,子任务的线程池,窃取(子任务队列独立,子任务队列里面的任务可能会被他的其他空闲线程去执行),不保证执行顺序
手动创建还是自动创建
手动
线程池里的线程数量设定为多少比较合适
CPU密集型(加密,计算hash等):最佳线程数为CPU核心数的1-2倍左右
耗时IO型:(读写数据库,文件,网络读写等):最佳线程数一般会大于CPU核心线程数很多倍,以JVM线程监控显示繁忙情况为依据,保证线程空闲时间能接的上,参考Brain Goetz推荐的计算方法
线程数=CPU核心数*(1+平均等待时间/平均工作时间)
压测进行估计
停止线程池的正确方法
shutdown : 会把存量的任务(正在执行和队列里的)执行完,新的任务都会被拒绝 RejectedExecutionException
isShutdown : 是不是调用了shutdown,用来后提交新任务前的判断
isTerminated : 返回是不是线程池已经完全终止了
awaitTermination : 测试一段时间内线程是否会完全停止的方法
shutdownNow : 立刻把线程池关闭到,返回待执行的任务列表(这个参数用运用好)
任务太多,怎么拒绝
拒绝时机
1 当Executor关闭时,提交新任务会被拒绝
2 以及当Executor对最大线程和工作队列容量使用有限边界并且已经饱和时
4种拒绝策略
AbortPolicy 直接抛出异常RejectedExecutionException
DiscardPolicy 直接把新提交的任务进行丢弃
DiscardOldestPolicy 丢弃最老的任务
CallerRunsPolicy 把新的任务给你提交的线程来跑
钩子方法
每个任务执行前后
日志,统计
实现原理
线程池管理器 管理线程池的
工作线程
任务队列
任务借口(Task)
家族关系
Executor(顶层接口) <———— ExecutorService(扩展管理接口) <---- AbstractExecutoeService <———— ThreadPoolExecutor
Executors(创建线程池的工具类)
重要概念
线程池实现复用任务的原理 - 相同线程执行不同任务(runwork()-> 调用run)
线程池状态
RUNNING: 接收新任务并处理排队任务
SHUTDOWN: 不接受新任务,但处理排队任务
STOP: 不接受新任务,也不处理排队任务,并中断正在进行的任务
TIDYING: 中文是简洁,所有任务都已经终止了,workerCount为0时,线程会跳转到TIDYING状态,并将运行terminate()钩子方法
TERNINATED: terminate()运行完成
注意点
避免任务堆积
避免线程过度增加
排除线程泄漏
ps:
当解析文件的时候是会CPU飙升,采取的办法是每个线程休眠几ms,防止CPU异常飙升(多线程池时候应该如何考虑,不让CPU/IO全部跑满,也就是CPU/IO资源的合理分配)
手写一个线程池
概念的总结是基于源码的,要能在源码中进行复现
threadlocal
使用场景
1. 每一个线程需要一个独享的对象(通常是工具类,典型需要使用的类有SimpleDateFormate和Random)
每个Thread内有自己的实例副本,不共享
2. 每个线程内需要保存全局变量(例如在拦截器中获取用户信息),可以让不同方法直接使用,避免参数传递的麻烦
解决同一个对象作为参数层层传递,代码冗余不易维护
每个线程内要保存全局变量,可以让不同的方法直接使用,避免直接参数传递带来的麻烦
用ThreadLocal保存一些业务内容(用户权限信息,从用户系统获取到的用户名,user ID等)
这些信息在同一个线程内是相同的,但是在不同的线程使用的业务内容是不相同的
当前用户信息需要被线程内所有方法共享
使用UserMap,需要保证多线程问题,可以使用synchronized,也可以使用ConcurrentHashMap,但是无论用什么,都会对性能有影响
强调的是同一个请求内(同一个线程内)不同方法的共享
不需要重写initalValue()方法,但是必须手动调用set方法
作用
1. 让某个需要用到的对象在线程间隔离(每个线程都拥有自己的独立对象)
2. 在任何方法中都可以轻松获取到该对象
选用时机
根据共享对象的生成时机不同,选择initialValue或set来保存对象
initialValue
在ThreadLocal第一次get的时候就把对象给初始化出来了,对象的初始化时机由我们控制
set
需要保存到ThreadLocal里的对象生成设时机不由我们随意控制,例如拦截去生成的用户信息,用ThreadLocal.set直接放到我们的ThreadLocal中去,以便后续使用
好处
达到线程安全
不需要加锁,提高执行效率
更高效的利用内存,节省开销,相比于每个任务都新建一个SimpleDataFormat,显然用ThreadLocal可以节省内存和开销
免去传参麻烦,降低代码耦合度,更优雅
原理
Thread -(1对1)-> ThreadLocalMap -(1对多)-> ThreadLocal
主要方法
initialValue()
1.该方法会返回当前线程对应的初始值,这是一个延迟加载的犯法,只要在调用get的时候,才会触发
2.当线程第一次调用get方法访问变量时,将调用此方法,除非线程先前调用了set方法,在这种情况下,不会为线程调用本initialValue方法
3.每个线程最多调用一次此方法,但如果已经调用remove()方法后,再调用get(),则可以再次调用此方法
4.如果不重写本方法,这个方法会返回null.一般使用匿名内部类的方法来重写initialValue()方法,以便在后续使用中可以初始化副本对象
set()
为这个线程设置一个新值
get()
得到这个线程对应的value,如果是首次调用get(),则会调用 initialize来得到这个值
先取出当前线程ThreadLocalMap,然后调用map.getEntry方法.把本ThreadLocal的引用作为参数传入,取出map中属于本ThreadLocal的value值
这个map以及map中的key和value都是保存在线程中的,而不是保存在ThreadLocal中
remove()
删除这个线程对应的值,单个threadLocal
核心组成
ThreadLocalMap
ThreadLocalMap类是每个线程Thread类里面的变量,开麦你最重要的是一个键值对数组Entry[] table,可以认为是一个map,键值对
键: 这个ThreadLocal 值: 实际需要的成员变量,比如user或者simpleDateFormat对象
冲突: HashMap
ThreadLocalMap这里采用的是线性探测法,也就是如果发生冲突,就继续找下一个位置空位置,而不是用链表拉链,就找下一个空位置进行填补
注意点
内存泄漏
对象没用,但是占用的内存却不能被回收
ThreadLocalMap 的 kv
k 是使用弱引用(如果这个对象只是被弱引用关联,没有任何强关联,那么这个对象就可以被垃圾回收器进行回收的)的方式进行赋值的
value 是强引用(定义的),会导致存在内存泄漏的可能性
出现场景
OOM:
正常情况下,当线程终止,保存在ThreadLocal里的value会被垃圾回收,因为没有任务强占用了
但是线程始终不终止,那么value就不能被回收,因为有以下的调用链: Thread -> ThreadLocalMap -> Entry(key为null) -> Value
JDK会扫描key为null的Entry,并把对应的Value设置为null,这样value对象就可以被回收了 // HELP THE GC
但是如果一个ThreadLocal不被使用了,set,remove,rehash方法不会被调用,线程又不会停止,就会导致value的内存泄漏
规约:
最后要调用remove方法,防止内存泄漏问题发生
NPE:
在get前要先进行set吗,为什么会有空指针异常
控指针异常是在拆箱装箱时候会产生的,和ThreadLocal无关
共享对象:
如果在每个线程中ThreadLocal.set()进去的东西本来就是多线程共享的同一个对象,比如static对象,那么多个线程的ThreadLocal.get()
取得的还是这个共享对象本身,还是有并发访问安全问题
不要强行使用:
如果对象很少的情况下就没必要用ThreadLocal来解决问题
框架中已经有了就不需要自己去创造
Spring中,如果可以使用RequestContextHolder,那么就不用自己维护ThreadLocal,因为自己会忘记调用remove(),造成内存泄漏
Spring中的实例分析
DateTimeContextHolder类,看到里面用了ThreadLocal
RequestContextHolder类,请求信息
每次HTTP请求都对应一个线程,线程之间相互隔离,这就是ThreadLocal的典型场景
ps:
有时候好的设计即可把复杂的问题解决
threadlocal的设计就是 set赋值和初始值进行赋值的写法
两个场景殊途同归,入口不同,但是结果都是相同的
文件被占用考虑是什么情形,为什么360可以对文件进行检测占用和强制删除
lock - 锁
简介
锁是一种工具,用于对共享资源的访问
Lock并不是用来替代synchronized的,而是当使用synchronized不适合或者不足以满足要求的时候,来提供高级功能的
Lock接口的实现类最常见的就是ReentrantLock
通常情况下Lock只允许一个线程来访问这个共享资源的,不过有时候,一些特殊的实现也允许并发访问,比如ReadWriteLock里面的ReadLock
synchronized不太够用原因
1. 效率低:锁的释放情况少,试图获取锁时不能设定超时时间,不能中断一个正在试图获得锁的线程
2. 不够灵活,(读写锁更加灵活),加锁和释放锁的时机单一,每个锁仅有单一的条件(某个对象),可能是不够的
3. 无法知道是否成功获取到了锁
Lock主要方法介绍
lock()
就是最普通的获取锁,如果锁已被其他线程获取,则进行等待
Lock不会像synchronized一样在异常时候自动释放锁
因此最佳实践是,在finally中释放锁,以保证发生异常时锁一定会被释放
lock方法不能被中断,这会带来很大的隐患,一旦陷入死锁,lock就会陷入永久等待
tryLock()
用来尝试获取锁,如果当前所没有被其他线程占用,则获取成功,则返回true,否则返回false,代表获取锁失败
相比于lock,这样的方法显然功能更加强大了,我们可以根据是否能获取锁来决定后续程序的行为
该方法会立即返回,即便在拿不到锁时不会一直在那等
tryLock(long time,TimeUnit unit)
超时就放弃
lockInterruptibly()
相当于tryLock(long time,TimeUnit unit),把超时时间设置为无限,在等待锁的过程中,线程可以被中断
unlock()
一定要配合try-finally使用,先写
可见性保证
happens-before
Lock加解锁和synchronized有同样的内存语义,也就是说,下一个线程加锁后可以看到所有前一个线程解锁发生的所有操作
Lock,synchronized都是有这个能力的
锁的分类
从不同的角度来看待锁的分类
-- 乐观锁(非互斥同步锁)和悲观锁(互斥同步锁)
互斥同步锁的劣势
阻塞和唤醒带来的性能劣势
永久阻塞:如果持有锁的线程被永久阻塞,比如遇到了无限循环,死锁等活跃性问题,那么等待该线程释放锁的那几个悲催的线程的线程,将永远得不到执行
优先级反转
-
乐观锁的实现一般都是用CAS算法实现的
乐观锁的核心就是执行完之后进行比较共享资源是否被修改了(ABA问题)
-
Java中悲观锁的实现是synchronized和Lock相关的类
乐观锁的典型例子就是原子类和并发容器,git
- 开销对比
悲观锁的原始开销高于乐观锁,但是特点是一劳永逸,临界区持锁时间就算越来越差,也不会对互斥锁的开销造成影响
相反,虽然乐观锁一开始开销比悲观锁小,但是如果自旋时间很长或者不停重试,那么消耗的资源也会越来越多
- 使用场景
悲观锁: 适合并发写入多的情况,适用于临界区持续时间比较长的情形,悲观锁可以避免大量的无用自旋等消耗,典型情况:
1.临界区有IO操作
2.临界区代码复杂或者循环量大
3.临界区竞争非常激烈
乐观锁:
适合并发写入少,大部分是读取的场景,不加锁的能让读取性能大幅度提高
-- 可重入锁和非可重入锁,ReentrantLock
字符串打印
电影院
- 可重入特性
可重入锁又叫做递归锁,同一个线程同一把锁不需要重新获取
- 好处
避免死锁
提升封装性
- 源码对比
可重入锁ReentrantLock及非可重入ThreadPoolExecutor的Worker类(具体的在AQS里面进行补充)
- 其他方法
isHeldByCurrentThread 可以看出是否被当前线程持有
getQueueLength可以返回当前正在等待这把锁的队列有多长
一般这两个方法是开发和调试时候使用,上线后用的不多
-- 公平锁和非公平锁
公平指的是按照线程请求的顺序,来分配锁,非公平指的是,不完全按照请求的顺序,在一定情况下,可以插队
(非公平锁特不提倡进行插队,这里的非公平指的是在合适的时机进行插队,而不是盲目的进行插队)
非公平锁是为了提高效率,避免唤醒线程带来的空档期
ReentrantLock公平的时候,就需要把创建时的参数设置为true
-特例
针对tryLock()方法,它是很猛的,不遵守设定的公平的规则
例如,当有线程执行tryLock()的时候,一旦有线程释放了锁,那么这个正在tryLock的线程就能获取到锁,即使它之前已经有其他在等待队列里的
- 对比
公平锁,每个线程都可以执行,劣势比较慢,吞吐量小
非公平锁,更快,吞吐量大,有可能会产生线程饥饿,也就是某些线程在长时间内,始终不能执行
-- 共享锁和排它锁
排他锁,又称独占锁,独享锁
共享锁,又称读锁,获取共享锁后,可以查看但无法修改和删除数据,其他线程此时也可以获取到共享锁,也可以查看但无法修改和删除数据
共享锁和排它锁的典型是读写锁ReentrantReadWriteLock,其中读锁是共享锁,写锁是独享锁
- 作用
读没必要加锁,写才是需要加锁的,为了提高程序执行的效率
- 规则
要么是一个或多个线程同时读,要么是一个线程有写锁,但是两者不会同时出现(要么多读,要么一写)
- 理解
可以理解为一把锁的两种方式进行锁定,也可以理解为两把锁
- 读写锁的交互方式
选择规则,
读线程插队,提高效率
升降级
- 读锁插队策略
公平不允许插队
策略1:
读可以插队,效率高
容易让想要获取写锁的线程造成饥饿
策略2:
把其插入到队列里,等待写锁执行完
非公平写锁可以随时插队,读仅在等待队列头不是想获取写锁线程的时候可以插队
- 升降级
写降级和读升级 - 支持锁的降级不支持升级,为什么不支持锁的升级:死锁(可以自定义保证同一时间只有一个升级就可以实现锁的升级)
-- 自旋锁和阻塞锁
让当前线程稍微等一下,当前线程进行自旋,如果在自旋完成后前面锁定同步资源的线程已经释放了锁,那么当前线程就可以不必要阻塞而是直接获取同步资源,避免线程切换带来的开销
阻塞锁在没拿到锁的情况下,就会直接把线程阻塞,直到被唤醒
- 缺点
如果锁占用的时间比较长,那么自旋锁的线程就会白白浪费处理器资源
- 实现
java1.5的concurrent的atmoic包下的类基本就是自旋锁进行实现的
自旋锁的实现是CAS
AtomicInteger中调用unsafe进行自增操作的源码中的do-while循环就是一个自旋操作,如果修改过程中其他线程竞争导致没修改成功,就在while里死循环,直至修改成功
- 使用场景
自旋锁一般用于多核的服务器,在并发度不是特别高的情况下,比阻塞锁的效率高
自旋锁适用于临界区比较短小的情况,否则如果临界区很大(线程一旦拿到锁,很久才会释放),那也不合适的
-- 可中断锁
synchronized是不可中断锁,Lock是可中断锁
--- 锁优化
JVM对锁的优化
自旋锁和自适应,在一定时间内就转为阻塞锁,不会一直进行自旋
锁消除,没必要加锁的,私有方法内的就会帮你去掉锁
锁粗化,前后相邻的代码块使用的是同一个锁对象,把这几个合成一个较大的,就不需要反复的申请和释放锁了
代码级别的优化
缩小同步代码块
尽量不要锁住方法 (锁代码块来代替方法)
减少请求锁的次数 - 比如写数据库,利用中间件,然后从中间件中获取数据统一写入到数据库中
避免人为制造热点 - 比如hashMap中的size(),自己维护一个计数,复杂度降为O(1)
锁中不用包含锁
选择合适的锁类型或合适的工具类
ps:
核心就是算法,所以理解算法,可以给问题一个解决思路,也是开发的核心
atomic - 原子类
不可分割
一个操作是不可中断的,即便是多线程的情况下也可以保证
java.util.concurrent.atomic
- 作用
原子类和锁类似,不过原子类相比于锁,有一定优势:
粒度更细,变量级别的锁
使用原子类的效率比使用锁的效率高,除了高度竞争的情况
- Atomic 基本类型
AtomicInteger
AtomicLong
AtomicBoolean
- API
get() 获取当前的值
getAndSet(int newValue) 获取当前的值,并设置新的值
getAndIncrement() 获取当前的值,并且自增
getAndDecrement() 获取当前的值,并且减
getAndAdd(int data) 获取当前的值,并且加上预期的值
compareAndSet(int expect, int update) 如果输入的数值等于预期值,则以原子方式将该值设置为输入值(update)
- Atomic 数组类型
AtomicIntegerArray
AtomicLongArray
AtomicReferenceArray
当有多个变量要批量操作的时候,用一个数组来存
- Atomic 引用类型
AtomicReference
- 普通变量升级为Atomic功能的变量
AtomicIntegerFieldUpdater
- 使用场景
不是你进行声明的,可以进行升级
偶尔需要一个原子的get-set操作
- 注意点
可见范围(不能被private修饰)
不能被static修饰
- 累加器
Adder
Accumulator
- Adder产生特性
Java8引入的,相对比较新的
高并发下LongAdder比AtomicLong效率高,不过本质是空间换时间
竞争激烈的时候,LongAdder把不同线程对应下到不同的Cell上进行修改,降低了冲突的概率,是多段锁的概念,提高了并发性
(因为AtomicLong每一次加法,都要flush和refresh,导致很耗费资源)
(LongAdder,每一个线程有一个自己的计数器,仅用来在自己的线程内计数,这样一来不会和其他线程的计数器干扰)
- 原理
LongAdder引入了分段累加的概念,内部有一个base变量和一个Cell[]数组共同参与计数
base变量,竞争不激烈的情况下,直接累加到该变量上
Cell[]数组,竞争激烈,各个线程分散累加到自己的槽Cell[i]中(空间)
sum()源码,先加base,再加Cell
- 使用场景
在低竞争下,AtomicLong和LongAdder这两个类具有相似的特征,但是在竞争激烈的情况下,LongAdder的预期吞吐量要高得多,但是消耗更多的空间
LongAdder适合的场景是统计求和计数的场景,而且LongAdder基本只提供了add方法,而AtomicLong还具有cas方法
- Accumulator
- 使用场景
多线程并行计算,比for循环的优势大
不能有计算顺序,也不能有业务执行顺序
CAS(cas) - 各种锁的底层原理
并发,CPU的特殊指令,不会出现线程安全问题
CAS有三个操作数,内存值V,预期值A,要修改的值B,并且仅当预期值A和内存值V相同时,才将内存值改成B,否则什么都不做,最后返回现在的V值
- 应用场景
乐观锁
并发容器
原子类
AtomicInteger加载Unsafe工具,用来直接操作内存数据
用Unsafe来是实现底层操作
Unsafe是CAS的核心类,Java无法直接访问底层操作系统,而是通过本地native方法来访问,不过尽管如此,JVM还是开了一个后门,JDK有一个类Unsafe,它提供了硬件级别的原子操作
valueOffSet表示的是变量值在内存中的偏移地址,因为Unsafe就是根据内存便宜地址获取数据的原值的,这样我们就能通过unsafe来实现CAS了
用valatile修饰value字段,保证可见性
native - Unsafe-compareAndSwapInt
Atomic::cmpxchg(x,addr,e) 实现了底层原子性的比较替换,x是即将更新的值,参数e是原内存的值,至此,最终完成了CAS的全过程
- 缺点
ABA问题(重结果忽略过程)
解决ABA问题,可以借鉴版本号来解决
自旋时间过长
用到了死循环,可能会有较长时间的CPU占用
final - 不变应万变
不变性
如果对象在被创建之后,状态就不能被修改,那么它就是不可变的
不可变对象
就是内部元素都不可以被外部访问修改到,具有不可变性的对象一定是线程安全的,不需要对他进行额外的措施,也能保证线程安全
- final 的作用
final在早期是实现内嵌调用的,就是被一个类的方法拷到该方法下
(其实现在是为了进行清晰的标识,JVM已经可以自动的进行final化的处理了)
现在,类防止被继承,方法防止被重写,变量防止被修改
天生是线程安全的,而不需要额外的同步开销
- final 的3种用法
修饰变量
被final修饰的变量,意味着值不能被修改,如果变量是对象,那么对象的引用不能变,但是对象自身的内容依旧可以改变
- 类中的final属性
赋值时机(必须选择一种进行赋值)
- 声明变量的等号右边直接赋值
- 在构造函数中赋值 - 类似于现在的Spring的Bean构造器方式注入
- 在类的初始化代码块中赋值(不常用) { }
- 类中的static final属性
赋值时机(必须选择一种进行赋值)
- 声明变量的等号右边直接赋值
- 还可以用static修饰的代码块进行赋值
- 方法中的final变量
- 不规定赋值时机,但是要求在使用前必须及进行赋值,这和方法中的非final变量的要求也是一样的
修饰方法
- 修饰构造方法
构造方法不允许我们用final修饰
不可被重写,这个和static方法是一样的
但是static可以在子类中有相同的静态方法定义,原因是static与类绑定,不是动态绑定的
修饰类
不可被继承,String
总结
final修饰对象时,只是对象的引用不可变,而对象本身的属性是可以变化的
final使用规则,良好的编码习惯
不变性和final的关系
不变性不意味着,简单的用final修饰就是不可变了
对基本数据类型,确实被final修饰就具有不可变性了
但是对于对象,需要该对象保证在自身创建后,状态永远不可变才可以
如何利用final实现对象不可变
把所有的属性都声明为final?
内部有final修饰的引用,引用还存在有不是被final修饰的属性,就不满足了
一个属性是对象类型的不可变对象
private final Set xxx = new HashSet<>();
满足以下条件才是不可变的
对象创建后,其状态不可被修改
所有属性都是final修饰的
对象创建过程中没有发生溢出
栈封闭
把变量写在线程内部
例子
面试题 宏替换 编译器优化
ps:
赋值时机的设置合理性,如果你初始化不赋值,后续进行赋值,就从null变成了你的更改,就违反了final不变原则了
collections - 常见的并发容器
ConcurrentHashMap,CopyOnWriteArrayList,阻塞队列
- 过时的同步容器
Vector和Hashtable
Vector使用类似于List 所有方法都是synchronized 修饰的
Hashtable使用类似于Map 所有方法都是synchronized 修饰的
缺点:
并发性能差
- 常见的容器变同步容器
ArrayList和HashMap
两个类不是线程安全的
Collections.synchronizedList(new ArrayList())
Collections.synchronizedMap(new HashMap())
可以转成线程安全的
使用同步代码块的方式来实现的
缺点:
性能还是较差
- 常见的并发容器
ConcurrentHashMap 和 CopyOnWriteArrayList
- Map
实现
(map 是根据key来进行设计的,所以对key的操作,都要灵活使用它的API)
HashMap -> LinkedHashMap(顺序是插入的顺序)
Hashtable(已经不使用了)
SortedMap -> NavigableMap -> TreeMap(可以排序,默认是升序)
为什么HashMap是线程不安全的?
同时put碰撞会导致数据丢失
同时put会导致数据丢失
死循环造成的CPU100% ?
- 主要存在于JAVA7中
https://coolshell.cn/articles/9606.html
在多线程扩容的时候可能会导致循环链表,产生CPU100%,这个问题的本质是HashMap没有用对地方,HashMap不支持多线程环境
ConcurrentHashMap1.7实现
Java7中的ConcurrentHashMap最外层是多个 segment,每个 segment 的底层数据结构与HashMap类似,仍然是数组和链表组成的拉链法
每一个segment独立上 ReentrantLock 锁,每个segment之间互不影响,提供了并发效率
ConcurrentHashMap默认有16个Segments,所以最多可以同时支持16个线程并发写,(操作分别分布在不同的Segment上),这个默认值在初始化的时候射为止其他值,但是一旦初始化,是不可以扩容的
ConcurrentHashMap1.8实现(对1.7版本的完全重写 代码增加5000行)
putVal流程
判断key value不为空
计算hash值
根据对应位置节点的类型,来赋值,或者helptransfer,或者增长链表,或者给红黑树增加节点
检查满足阈值就"红黑树化"
返回oldVal
get流程
计算hash值
找到对应的位置,根据情况进行:
直接取值
红黑树里找值
遍历链表取值
返回找到的结果
核心:借鉴了1.8Map的实现,使用node来代替segment,扩容使用CAS和链表转红黑树或转移节点(降低了算法的复杂度)
为什么要把1.7的结构改成1.8的结构?
数据结构(提升并发度)
Hash碰撞(对1.7的拉链法进行升级)
保证并发安全(1.7是分段锁,1.8是CAS+synchronized)
查询复杂度(On Ologn)
为什么超过8转成红黑树?
默认是链表的形式,所占用的内存更少
基本不会遇到链表转成红黑树的情况,冲突为8次是千万分之一的概率(泊松分布)
错误使用ConcurrentHashMap也会造成线程不安全的情况?
组合操作并不保证线程安全的
用replace方法和while(true)进行优化,安全性保证
putIfAbsent()相当于检查key是否存在再进行加操作
实际案例
司机答题打乱题目,造成题目乱序的问题
- CopyOnWriteArrayList
- 介绍
代替Vector和SynchronizedList,就和ConcurrentHashMap代替SynchronizedMap的原因一样
Vector和SynchronizedList锁的粒度太大了,并发效率相对比较低,并且迭代时无法编辑
Copy-On-Write并发容器还包括CopyOnWriteArraySet,用来替代同步Set
- 适用场景
读操作可以尽可能得快,而写即使慢一点也没有太大关系
读多写少:黑名单,每日更新;监听器,迭代操作远多于修改操作
- 读写规则
回想读写锁,除了读读其他都是互斥的
读写锁规则的升级:读取是完全不用加锁的,并且更厉害的是,写入也不会阻塞读取操作,只有写入和写入之间需要进行同步等待
解决传统ArrayList在迭代时候不能修改的场景,改和迭代不冲突,迭代的还是原来的值
- 实现原理
创建新副本,读写分离
"不可变"原理,旧的不可变
迭代的时候,数据可能已经过期,迭代的时候,取决于迭代对象的创建时间,不取决于开始迭代的时间
- 缺点
数据一致性问题:CopyOnWrite容器只能保证数据的最终一致性,不能保证数据的实时一致性。所以如果你希望写入的数据,马上就能读到,马上能读到,请不要使用CopyOnWrite容器。
内存占用问题:因为CopyOnWrite的是写的复制机制,所以在进行写操作的时候,内存里会同时驻扎两个对象的内存。
- 源码分析
用ReentrantLock加锁
复制一份出来
- 并发队列 Queue: 阻塞队列(重点)
- 简介
在线程中传递数据:生产者消费者模式
考虑锁等线程安全问题的重任从“你”转移到了"队列"上
- 重要并发队列关系图(todo 这个需要再进行探究下)
Queue
SynchronousQueue(BlockingQueue)
ConcurrentLinkedQueue
BlockingQueue
ArrayListBlockingQueue
PriorityBlockingQueue
LinkedBlockingQueue
- 阻塞队列 BlockingQueue
简介
阻塞队列是具有阻塞功能的队列,所以它首先是一个队列,其次是阻塞功能
通常,阻塞队列的一端是给生产者放数据用,另一端给消费者拿数据用。阻塞队列是线程安全的,所以生产者和消费者都可以是多线程的
核心方法
take()方法:获取并移除队列的头结点,如果执行take的时候,队列里无数据,则阻塞,直到队列里有数据
put()方法:插入元素。但是如果队列已满,那么就无法继续插入,则阻塞,直到队列里有了空闲空间
是否有界(容量有多大)
这是一个非常重要的属性,无界队列意味着里面可能可以容纳非常多,(Integer.MAX_VALUE,约为2的31次方,是一个非常大的数字,可以近似认为是无限容量)
阻塞队列和线程池的关系(阻塞队列是线程池的重要组成部分)
重要方法
put,take
add,remove,element
offer,poll,peek
重要实现
- 阻塞并发队列
ArrayBlockingQueue
有界
指定容量
公平:可以指定是否需要保证公平,如果想保证公平的话,那么等待了最长时间的线程会优先被处理,不过这会同事带来一定的性能损耗
生产者消费者的设计方式
LinkedBlockingQueue
无界
容量 Integer.MAX_VALUE
内部结构 Node 两把锁 分析put方法
PriorityBlokingQueue
支持优先级
自然排序(而不是先进先出)
无界队列(可以进行扩容)
PriorityQueue的线程安全的版本
SynchronousQueue
容量为0
因为SynchronousQueue不需要去持有元素,他所做的就是直接传递(direct handoff)
效率很高
没有peek等函数,因为不进行存储,也没iterate方法
是一个极好的用来直接传递的并发数据结构
Executors.newCachedThreadPool()使用的阻塞队列
DelayQueue
延迟队列,根据延迟时间进行排序
元素需要实现Delayed接口,规定排序规则
- 非阻塞并发队列
ConcurrentLinkedQueue
使用链表作为数据结构
使用CAS非阻塞算法实现线程安全
offer方法的CAS思想,内有p.casNext方法,用了UNSAFE。compareAndSwapObject
适合用在对性能要求比较高的并发场景,用的相对比较少一点
如何选择适合自己的队列
边界
是否有边界
容量特别大,近似为无边界
容量不够,会进行自动扩容
空间
ArrayBlockingQueue比LinkedBlockingQueue内存更加整齐
吞吐量
LinkedBlockingQueue优于ArrayBlockingQueue,因为锁的粒度更加细致
其他特殊场景
SynchronousQueue用于不存储,直接交换的场景
DelayQueue延迟排序场景
PriorityBlokingQueue自定义优先级的队列
- 并发容器总结
java.util.concurrent包提供的容器,分为3类:Concurrent*,CopyOnWrite*,Blocking*
Concurrent*的特点是大部分通过CAS实现并发,而CopyOnWrite*则是通过复制一份原数据来实现,Blocking通过AQS实现
ps: 调试技巧
修改JDK版本
多线程配合 Thread - make default
process - 并发流程控制
- 大致了解
- Semaphore
作用:信号量,可以通过控制"许可证"的数量,来保证线程之间的配合
说明:线程只有在拿到"许可证"后才能继续运行,相比于其他的同步器,更灵活
- CyclicBarrier
作用:线程会等待,直到足够多的线程达到了事先规定的数目,达到触发条件,就可以进行下一步的动作
说明:适用于线程之间相互等待处理结果就绪的场景
- Phaser
作用:和CyclicBarrier类似,但是计数器可变
说明:Java7加入的
- CountDownLatch
作用:和CyclicBarrier类似,数量减到0时,触发动作
说明:不可以重复使用
- Exchanger
作用:让两个线程在合适时交换对象
场景:当两个线程工作在一个类的不同实例上时,用于交换数据
- Condition
作用:可以控制线程的"等待"和"唤醒"
说明:是Object.wait()的升级版
- CountDownLatch
倒数门闩
流程
等待,倒数为0,才进行执行
主要方法
CountDownLatch(int count):仅有一个构造函数,参数count为需要倒数的数值
await():调用await()方法的线程会被挂起,它会等待直到count值为0才继续执行
countDown():讲count值减1,直到值为0,等待的线程会被唤醒
用法
用法一:一等多,一个线程等其他线程都执行完再开始执行
用法二:多等一,多个线程等待某一个线程的信号,同时开始执行
用法三:组合两个用法,进行全过程的描述
扩展用法:多等多,多个线程等多个线程完成执行后,再同时执行
例子
拼团,人满发车
同一时间起跑
压测
注意:
CountDownLatch是不能够重用的,如果需要重新计数,可以考虑使用CyclicBarrier或者创建新的CountDownLatch实例
- Semaphore
概念
信号量
可以用来限制或管理数量有限资源的使用情况
许可证的概念
使用流程
1.初始化Semaphore并制定许可证的数量
2.在需要被现在的代码前加acquire()或者acquireUninterruptibly()方法
3.在任务结束后,调用release()来释放许可证
主要方法
new Semaphore(int permits,boolean fair):这里可以设置是否要使用公平策略,如果传入true,那么Semaphore会把之前等待的线程放到FIFO队列里,以便于当有了新的许可证,可以分发给之前等了最长时间的线程
acquire()响应可以中断
acquireUninterruptibly()不可以响应中断
tryAcquire():看看现在有没有空闲的许可证,如果有的话就获取,如果没有的话也没关系,我不必陷入阻塞,我可以去做别的事,过一会再来查看许可证的空闲情况
tryAcquire(timeout):和tryAcquire()一样,但是多了一个超时时间,比如"在3秒内获取不到许可证,我就去做别的事"
release()
用法
保护线程池中同时执行的线程数
acquire()可以指定一个方法拿到的许可证数量,从而进行权重,acquire几个,release就要对应的释放几个
信号量特殊用法
1.一个方法拿走权重的数量来进行控制方法是否会执行,从而规定方法的执行先后顺序和执行顺序权重
2.除了控制临界区最多同时有N个线程访问外,另一个作用是可以实现"条件等待",例如线程1需要在线程2完成准备工作后才能开始工作,那么就线程1acquire(),而线程2完成任务后release(),这样的话,相当于是轻量级的CountDownLatch
注意点
获取和释放的许可证数量必须一致, 如果不一致,会造成程序卡死,写程序的规范
注意在初始化Semaphore的时候设置公平性,一般设置为true会更合理,避免线程饿死
获取和释放许可证对线程并无要求,也许是A获取了,然后由B释放,只要合理即可
- Condition接口(又称为条件对象)
作用:
当线程1需要等待某个条件的时候,他就去执行condition.await()方法,一旦执行了await()方法,线程就会进入阻塞状态
通常会有另外一个线程,假设是线程2,去执行对应的条件,直到这个条件达成的时候,线程2就会去执行condition.signal()方法,这时JVM就会从被阻塞的线程里找,找到那些等待该condition的线程,当线程1就会收到可执行信号的时候,他的线程状态就会变成Runnable可执行状态
signalAll()和signal()区别
signalAll()会唤醒所有正在等待的线程
signal()是公平的,只会唤起那个等待时间最长的线程
使用
普通示例
用ReentrantLock创建Condition
注释先用一个线程进行唤醒,然后再用另外一个进行阻塞,就是和正常的逻辑相反
实现生产者和消费者模式
注意点
实际上,如果说Lock用来代替synchronized,那么Conidtion就是用来代替响应的Object.wait/notify的,所以在用法和性质上,几乎一样
await方法会自动释放持有的Lock锁,和Object.wait一样,不需要自己手动先释放锁
调用await的时候,必须持有锁,否则会抛出异常,和Object.wait一样
- CyclicBarrier循环栅栏
概念
CyclicBarrier循环栅栏和CountDownLatch很类似,都能阻塞一组线程
CyclicBarrier可以构造一个集合点,当某一个线程执行完毕,它就会到集结点等待,直到所有线程都到了集合点,那么该栅栏就会被撤销,所欲线程再统一出发,继续执行剩下的任务
使用
可重用
CyclicBarrier和CountDownLatch区别
作用不同:
CyclicBarrier要等到固定数量的线程都到达了栅栏位置才能继续执行,而CountDownLatch只需要等待数字到了0,也可以说,CountDownLatch是基于事件,但是CyclicBarrier是用于线程的
可重用性不同:
CountDownLatch在倒数到0并触发门闩打开后,就不能再次使用了,除非创建新的实例,而CyclicBarrier可以重复使用
AQS - 并发灵魂人物
学习思路
AQS的目的是想理解原理,提高技术,以及面对面试
先从应用层面理解为什么需要他如何使用他,然后再看看我们的设计者如何使用它的,了解他的使用场景
之后再去分析它的结构,这样我们就学习的更加轻松了
为什么需要AQS
锁和写作类的共同点:闸门
ReentrantLock,Semaphore,CountDownLatch,ReentrantReadWriteLock都有类似的协作功能,他们底层都用到一个共同的基类,这就是AQS
因为上面的那些协作类,他们有很多工作是类似的,所以如果能提取一个工具了类,那么就直接可以用了,对于ReentrantLock和Semaphore而言就可以屏蔽很多的细节,只关注他们的“业务逻辑”就可以了
Semaphore和AQS关系
Semaphore内部有一个Sync类,Sync类继承了AQS(CountDownLatch,ReentrantLock也是类似的)
AQS帮我们做的(通用需求)
同步状态的原子性管理
线程的阻塞与解除阻塞
队列的管理
作用
AQS是一个用于构建锁,同步器,协作工具类的工具类(框架),有了AQS以后,更多的协作工具类都可以很方便得被写出来(标准和设计性能)
有了AQS,线程的协作类构件就容易多了
重要性和地位
(AbastractQueuedSynchronizer)是Doug Lea写的,从JDK1.5加入的一个基于FIFO等待队列实现的一个用于实现同步器的基础框架,相关的实现类有
AQS内部原理的解析
核心(三个部分)
state状态
根据类的实现不同而有所区别
在Semaphore里,表示"剩余的许可证数量",在CountDownLatch中"还需要倒数的数量",ReentrantLock(可重入计数)
是volatile修饰的,会被并发的修改,所以所有修改state的方法都需要保证线程安全,比如getState,setState以及compareAndSetState
控制线程抢锁和配合的FIFO队列
存放等待的线程的,AQS就是"排队管理器",当多个线程争用同一把锁的时候,必须有排队机制将那些没能拿到锁的线程串在一起,当锁释放的时候,锁管理器会挑选一个合适的线程来占用这个刚刚释放的锁
AQS会维护一个等待的线程队列,把线程都放在这个队列里(双向链表)
期望协作工具类去实现的获取/释放等重要方法
写作类自己实现,含义各不相同
-
获取操作会依赖state变量,经常会阻塞(比如获取不到锁的情况)
Semaphore - acquire - 获取一个许可证
CountDownLatch - await - 等待,直到倒数结束
-
释放操作不会阻塞
Semaphore - release - 作用是释放一个许可证
CountDownLatch中- countDown- 作用是"倒数一个数"
AQS源码解析
AQS用法
1. 写一个类,想好协作方式,实现获取/释放方法
2. 内部写一个Sync类继承 AbstractQueuedSynchronized
3. 根据是否独占tryAcquire/tryRelease或tryAcquireShared(int acquires)和tryReleaseShared(int release)等方法,在之前写的获取/释放方法中调用AQS的acquire/release或者Shared方法
使用AQS实现一个自己的门闩(线程协作器)
实现一个获取,释放的方法
用state实现状态的标识
Future和Callable - 线程治理的第二大法宝
Runnable的缺陷
不能返回一个返回值
也不能抛出 checked Exception
- 为什么有这样的缺陷?
run方法
Callable接口
有返回值
能抛出异常
Future和Callable关系
Future是一个存储器,它存储了call()这个任务的结果,而这个任务的执行时间是无法提前确定的,因为这完全取决于call()方法执行的情况
Future的主要方法
get方法,取得运行的结果,行为主要取决于Callable任务的状态,只有以下5种情况:
1.任务正常完成:get方法会立刻返回结果
2.任务尚未完成(任务还没开始或进行中):get将阻塞并直到任务完成
3.任务在执行过程中抛出Exception,不论抛出什么异常,最后抛出的是ExecutionException,异常抛出时机是get方法调用的时候才进行抛出
4.任务被取消,get会抛出CancellationException
5.任务超时,get方法会有一个重载方法,是传入一个延迟时间,如果时间到了还没有获得结果,get方法就会抛出TimeoutException
cancel方法
超时不获取,任务需要取消
1.Future.cancel(true)适用于+
任务有能力处理中断的
2.Future.cancel(false)
仅用于避免启动尚未启动的任务
1.未能处理interrupt的任务
2.不清楚任务是否支持取消
3.需要等待已经开始的任务执行完成
isDown
只是说任务是否执行完成了
isCancelled
是否被取消
主要用法
用法1:线程池的submit方法返回Future对象
我们给线程池提交任务,提交线程会立刻返回给我们一个空的Future容器。当线程的任务一旦执行完毕,也就是我们可以获取结果的时候,线程池就会把结果填入到我们刚才的那个Future容器中,
而不是创建一个新的Future,我们此时就可以从Future中获得任务执行的结果。
用法2:用FutureTask来创建Future
是一种包装器,可以把Callable转换成Future和Runnable,它同时实现二者的接口
既可以作为Rannable被线程执行,又可以作为Future得到Callable的返回值
放到线程或者线程池中执行,都会get获取到结果
注意点
1. 当for循环批量获取future结果的时候,容易发生一部分线程很慢的情况,get方法调用时候使用timeout进行限制舍弃慢的结果,或者使用CompletableFuture先出先获取
2. Future的生命周期不能后退
实战
自己写一个高性能缓存
=========================================