一、线程和线程池
1.线程池介绍
- 如果每个任务都新开一个线程,并且还需要销毁开销太大,不需要给每个任务都开一个线程,可以线程复用避免反复开启和销毁线程;
- 在任务少时可以维护固定数量线程,任务多会有最大线程数以及任务队列,动态扩容和缩容应对突发流量以及资源利用率。
2.线程池创建和停止线程池
2.1参数介绍
- corePoolSize:一直会保持的线程数,对每一个到来的任务只要没有空闲线程就会创建;
- maxPoolSize:超过核心线程数的部分会有存活时间,不会一直保存的线程数,当任务队列和corePoolSIze都满了之后会进行创建;
- keepAliveTime:超过核心线程池的线程存活时间;
- workQueue:阻塞队列SynchronousQueue无容量,LinkedBlockingQueue容量很大,ArrayBlockingQueue设置固定容量,放入等待执行的任务;
- threadFactory:创建线程,通常情况默认的即可;
- Handler:当workQueue,corePoolSize,maxPoolSize都满之后的拒绝策略;
2.2创建线程
- newFixedThreadPool:corePoolSize=maxPoolSize=n,keepAliveTime=0,workQueue=LinkedBlockingQueue无界队列;
- newSingleThreadExecutor:corePoolSize=maxPoolSize=1,workQueue=LinkedBlockingQueue无界队列;
- cacheThreadPool:corePoolSize=0,maxPoolSize=2的32方,keepAliveTime=60,workQueue=SynchronousQueue无容量;
- newSheduledThreadPool:定时/延时/周期执行任务corePoolSize=n,maxPoolSize=2的32方,keepAliveTime=0,workQueue=DelayedWorkQueue延时队列;
- workStealingPool:任务可以切分成子任务的场景,线程之间窃取任务来平衡任务量,合理利用线程资源
- threadPoolExecutor:自定义各个参数来创建线程池;
2.3停止线程
- shutdown:将当前存在的任务执行完毕,新的任务不会再增加;
- isShutDown:判断是否进入shutdown状态;
- isTeriminated:判断线程池是否完全停止;
- awaitTeriminated:等待一段时间是否线程池完全停止;
- shutdownNow:立刻终止,清除所有任务和当前线程;
4.线程池任务太多,如何进行拒绝
- 当线程池关闭,或者达到最大线程数并且工作队列饱和都会进行拒绝策略;
- AbortPolicy:抛出异常策略;
- DiscardPolicy:丢弃策略;
- DiscardOldestPolicy:丢弃最老的策略;
- CallerRunsPolicy:提交任务的线程去执行的策略;
5.线程池钩子方法
- 在每个任务执行前后增加功能----日.4志、统计等;
- 重写ThreadPoolExecutor这个类的beforeExecute以及afterExecute方法;
6.线程池实现原理
- Executor接口只有execute方法----->ExecutorService接口增加了shutdown,submit等方法---->AbstractExecutorService抽象类----->ThreadPoolExecutor类;
- Executors类通过ThreadPoolExecutor类创建对应线程池,属于线程池工厂;
6.1线程池的状态
- RUNNING接收新任务并处理排队任务;
- SHUTDOWN不接受新任务但处理排队任务;
- STOP不接收新任务,也不处理排队任务,并中断正在进行的任务;
- TIDYING所有任务都以处理完毕,并运行钩子方法;
- TERMINATED钩子方法也运行完毕;
6.2线程池的组成
- 线程池管理器、工作队列、任务队列、任务接口
7.线程介绍
- Runnable:实现run方法,不能有返回值,不能抛出异常;
- Callable:类似于Runnable,实现call方法,有返回值,可以抛出异常;
8.Future
- 任务异步执行,结果阻塞获取,判断任务是否执行完毕;
- 线程池的submit方法会返回Future对象的,execute方法不会返回,cancle取消任务;
- FutureTask获取Future和任务结果,将Runnable,Future,Callable包装起来(实现Runnable,Future,组装Callable),通过适配器模式,可以执行Runnable,可以作为Future得到Callable的返回值;
二、ThreadLocal
1.ThreadLocal的用途
- 每个线程需要独享对象---线程不安全,可以ThreadLocal实现initialValue方法;
- 单个线程需要保存全局变量---防止传参麻烦,可以ThreadLocal直接set值,当前线程的所有调用方法都可获取到值;
2.主要原理
- 一个Thread包含n个ThreadLocal;
- 一个ThreadLocalMap包含一个Thread;
- 一个ThreadLocalMap包含n个Entry(key和value);
- 实际上每个线程都含有一个不同的ThreadLocalMap,而map里又含有n个ThreadLocal以及对应的value值(key和value组合为Entry);
- 我们定义的ThreadLocal一般为全局静态变量,每个线程的ThreadLocalMap不同,经过相同的ThreadLocal计算得到的index是相同的,所以实际是ThreadLocalMap进行存储;
2.1方法介绍
- initialValue:初始化值,懒加载,get的时候才会初始化;
- set:为这个线程设置一个ThreadLocal值;
- get:得到线程所对应的值,首次调用会触发initialValue方法------获取当前线程的ThreadLocalMap,根据ThreadLocal在Map中获取到对应的value值;
- remove:删除这个对应线程的值;
3.注意点
- ThreadLocal--key是弱引用(每次垃圾回收时会被回收,除非有其他引用指向来改变生命周期),value值对应强引用;
- 只要ThreadLocal没有外部强引用,那么进行gc后key就会被回收,而value在Entry中还有强引用是不会被gc回收的,通过key是获取不到value将他置为null,至此value会永远也没法回收除非线程退出,否则就会造成内存泄露;
- 在set、remove、rehash方法中会扫描key为null的Entry,会把value设置为null;
- 在task任务执行的时候ThreadLocal是拥有外部强引用的,但当任务执行之后ThreadLocal的强引用也就结束了,如果不在任务执行完毕之前手动remove就可能会造成内存泄露;
ps:堆上的ThreadLocal对象,如果方法运行完毕栈上的强引用就会失效,下一次gc弱引用也会失效,那么key对象就会被回收,value也就内存泄露了;
三、各种锁
1.Lock接口
1.1介绍
- Lock可以提供synchronized没有的高级功能,支持wait/signal
1.2方法介绍
- lock:如果无法加锁进行等待;
- tryLock:可立即返回,可设置超时时间(并可响应中断);
- lockInterruptibly:超时时间无限,等待锁的过程中可以被中断;
3.乐观锁和悲观锁
- 要不要锁住同步资源;
- 悲观锁:每次获取资源并修改的时候都要把数据锁住,确保内容正确,比如synchronized,Lock的相关类;
- 乐观锁:先获取到当前值,更新值的时候先和内存中的数据比对是否相同,如果没有改变就更新值成功,如果改变了就重试,比如CAS算法,原子类,并发容器中就大量使用;
- 悲观锁是直接加锁(适合并发写很多/持有锁时间长/大量IO操作的场景),乐观锁不会直接加锁而是在操作系统底层加锁更加轻量(适合并发写少读多的场景,否则重试次数太多失败率大)
4.可重入锁(ReentrantLock)和非可重入锁
- 同一个线程是否可以重复获取同一把锁;
- 可重入锁可以避免死锁,一般来说第二次获取相同的锁需要先释放掉这就会导致死锁,通过累加计数来控制锁的控制次数;
5.公平锁和非公平锁
- 多线程竞争时是否排队;
- 非公平锁效率更高,谁最先获取到锁谁就可以得到,如果要保证公平就可能需要唤醒等待线程带来消耗;
- ReentrantLock锁可以支持公平和非公平锁;
6.共享锁和排他锁(ReentrantReadWriteLock)
- 多线程是否共享一把锁;
- 排他锁:独占,独享锁,比如写锁,synchronized等;
- 共享锁:又称为读锁,可以多个线程共同获取锁;
- ReentrantReadWriteLock:
- 多个线程可以共同申请读锁,申请写锁会等待读锁全部释放,申请读锁会等待写锁释放,读写之间会互相阻塞,要么同时多读要么同时一写;
- 读锁的执行流程 : 读锁的数量为高16位
- 1.在获取读锁时,会尝试判断当前对象是否拥有了写锁,如果已经拥有,则直接失败。
- 2.如果没有写锁,就表示当前对象没有排他锁,则当前线程会尝试给对象加锁。
- 3.如果当前线程已经持有了该对象的锁,那么直接将读锁数量加1。(多个线程的读锁计数使用ThreadLocal来进行存储,可以避免使用一个专门的数据结构存储)
- 写锁的执行流程 : 写锁的数量为低16位
- 1.在获取写锁时,会尝试判断当前对象是否拥有了锁(读锁与写锁),如果已经拥有且持有的线程并非当前线程,直接失败。通过 32bit位c != 0 且 互斥w==0,用于读锁阻塞写锁;
- 2.如果当前对象没有被加锁,那么写锁就会为为当前对象上锁,并且将写锁的个数加1.
- 3 . 将当前对象的排他锁线程持有者设为自己
-
StampedLock :支持乐观读锁(不加锁) 、悲观读锁(使用CAS)、写锁(使用CAS),来更好的提升性能;
7.自旋锁和阻塞锁
- 阻塞等待还是自旋获取锁;
- 阻塞或唤醒线程需要切换操作系统的状态,自旋不需要状态的切换只需要cpu不断重试执行代码,当切换状态耗时比自旋重试还要耗时那么自旋锁性能会更好,否则相反;
- 自旋锁的实现是CAS--乐观锁,阻塞锁的实现是synchronized--悲观锁;
private AtomicReference<Thread> sign = new AtomicReference<>();
public void lock() {
Thread current = Thread.currentThread();
//初始值为null,修改为current
while (!sign.compareAndSet(null, current)) {
System.out.println("自旋获取失败,再次尝试");
}
}
public void unlock() {
Thread current = Thread.currentThread();
//会将值重新设置为null,让其他阻塞的线程重新获取锁
sign.compareAndSet(current, null);
}
8.可中断锁
- 锁是否可以中断,synchronized不可中断,Lock是可中断锁;
9.锁优化
- JVM的优化自旋锁和自适应(尝试一定次数),锁消除(对一定不会出现并发的情况下消除掉),锁粗化(将小的加锁代码块合并为大的);
四、Atomic包
1.介绍
- 保证并发安全;
- 基于乐观锁CAS来实现,高度竞争下导致大量失败,失败率高可能效率不如直接加的悲观锁;
2.基本类型原子类
- AtomicInteger、AtomicLong、AtomicBoolean
- get:获取当前值;
- getAndSet:获取当前值,并设置新值;
- getAndIncrement:获取当前值,并自增;
- getAndDecrement:获取当前值,并自减;
- getAndAdd:获取当前值,并加上预期值;
- compareAndSet:如果预期值expect等于当前值,则以原子方式将该值设置为输入值update;
3.数组类型原子类
- AtomicIntegerArray、AtomicL ongArray、AtomicReferenceArray,还是以基本原子类为基础的;
4.引用类型原子类
- AtomicReference、AtomicStampedReference、AtomicMarkableReference,指定泛型;
- private AtomicReference<Thread> sign = new AtomicReference<>();
5.普通变量升级为原子类
- AtomicIntegerFieldupdater、AtomicLongFieldUpdater、AtomicReferenceFieldUpdater;
6.Adder累加器
- LongAdder、DoubleAdder,使用了多段锁提高了并发性;
- 基本原子类每次累加都要同步到主内存然后刷新到线程的工作内存,而累加器是每个线程不进行同步而是自己累加最后进行汇总(只在汇总的时候进行同步值),高并发下累加器表现更好;
7.Accumulator累加器
- LongAccumulator、DoubleAccumulator,扩展了累加器,还可以进行乘法、最大最小值等操作
五、CAS和final
1.CAS介绍
- 传入当前值A,在要更改成值B前会先判断值A与内存值V是否相等,如果相等修改成功,否则失败;
2.CAS原理介绍
- 加载Unsafe工具,用来直接操作内存数据,来实现底层操作,会调用c语言,然后最终调用汇编和机器的原子指令保证原子性;
- 用volatile修饰value字段,保证可见性和有序性,原子性本身就具备,保证了内存模型的语义;
3.CAS缺点
- ABA问题:值没发生改变,但版本号发生了改变;
- 自旋时间长,会很消耗CPU,高并发会导致大量失败,难以成功;
4.final介绍
- 类防止被继承,方法防止被重写,变量防止被修改,天生保证线程安全,存在方法区(静态变量也会在方法区);
5.final用法
- 如果声明类的final变量,只能等号赋值、构造函数赋值、初始化代码块赋值这三种情况;
- 如果final修饰对象,只是对象的引用不可变,而对象本身的属性可变;
六、并发容器
1.介绍
- HashTable:线程安全的Map性能不好,HashMap线程不安全,LinkedHashMap、TreeMap;
- Vector:线程安全的List性能不好,ArrayList线程不安全;
2.ConcurrentHashMap
- Map线程安全性能好;
- HashMap的死循环造成的CPU100%:链地址法解决冲突,头插法进行扩容,多线程下线程来回切换(头插法扩容节点会反向)会造成链表节点循环,此问题只在1.7造成,1.8使用了尾插法避免扩容节点反向解决了此问题;
- HashMap:JDK7时只是使用了链地址法,而JDK8时使用了链地址法+红黑树;
- ConcurrentHashMap:JDK1.7使用了分段锁(保证总体的原子性)+volatile(保证各个锁之间的可见性与有序性)来提高并发,如果是同一把锁根本就不需要volatile来保证可见性与有序性的,JDK1.8放弃了分段锁使用了CAS+Synchronized来实现;
- put操作的大致过程:判断是否发生哈希冲突,如果没有发生使用CAS设置节点,判断是否是转移节点,如果是那么要等扩容完成才可插入,如果发生了哈希冲突锁住头节点,先进行链表判断如果中途有相同key判断是否可以覆盖,在进行红黑树判断与链表类似(需要锁住树的根节点),最后判断是否链表可以转化为红黑树,如果覆盖了oldVal值会进行返回;(get操作类似put的判断过程)
- 红黑树为什么为8:正常情况下不会转化为红黑树,当数据有针对性时会造成链表过长这时转化为红黑树可以解决极端境况,设置为8是经过概率统计计算得来,达到8的概率非常小并且我么也不希望链表长度会达到8;
3.CopyOnWriteArrayList
- List线程安全性能好,适合读操作多,写操作少的场景;
- 读写规则:读写锁(只有读读不互斥,其他均互斥),mysql(乐观并发控制保证读读和读写不互斥--因为读取的是快照,悲观并发控制读写是互斥的),而CopyOnWriteArrayList(读读和读写不互斥,写的时候修改快照完成之后才会进行设置,借鉴了mysql的乐观并发控制),且迭代器经过了重写会拷贝当前数组(类变量)对list修改不会报错滴但也不可见滴,重写的迭代器不支持增删改;
4.并发队列(阻塞队列、非阻塞队列)
- 阻塞队列在原本的非阻塞队列上加入了阻塞取数据和放数据;
- 阻塞队列:SynchronousQueue(容量为0+不进行存储元素)、ArrayBlockingQueue(有界+指定容量+一把锁+循环队列)、PriorityBlockingQueue(使用堆实现+无界)、LinkedBlockingQueue(无界+容量为Int最大值+两把锁+链表)、DelayQueue(使用优先级队列来实现),线程池一个重要的组成部分也是阻塞队列来存放任务;
- take和put:删除元素并返回,会进行阻塞;插入元素,会进行阻塞;
- add、remove和element:插入元素,会抛出异常;删除元素并返回,会抛出异常;返回队列头元素,会抛出异常;
- offer、poll和peek:添加元素,返回布尔值;删除元素并删除,返回元素值/null;取元素不删除,返回元素/null;
- 非阻塞队列:ConcurrenLinkedQueue(链表+CAS实现非阻塞)
七、控制并发流程
1.介绍
- 线程之间相互配合与合作,满足业务逻辑;
2.CountDownLatch倒计时门闩
- 类似CyclicBarrier线程之间互相等待,数量递减到0,才会触发下一步操作,不可重复使用;
- await:挂起线程,直到count值为0才可继续执行;
- countDown:将count减一,直到为0时,等待的线程会被唤醒;
- 用法一:(一等多)一个线程等待多个线程都执行完毕,再继续自己的工作;
- 用法二:(多等一,比如压测场景)多个线程等待一个线程执行完毕,多个线程才能继续自己的工作;
3.Semaphore信号量
- 通过许可证来控制线程,线程只有拿到许可证才能继续运行,限制和管理有限资源的使用(限制并发量);
- acquire和release:获取许可证,会进行阻塞;释放许可证;
- tryAcquire:尝试获取,立马返回,可以设置超时时间;
4.Condition条件对象
- 控制线程的等待和唤醒;
- await、signal和signalAll:挂起当前线程(必须持有锁+会自动释放锁);唤醒等待的线程(等待时间最长的);唤醒所有线程;
- 可以用于生产者和消费者模式(阻塞队列+lock+notFull条件+notEmpty条件),生产者满了会进行await自身且signal消费者,消费者空了会进行await自身且signal生产者;
5.CyclicBarrier循环栅栏
- 适用于线程之间互相等待场景,直到足够多的线程达到规定好的数目,才可以进行下一步操作(支持重复使用),每次所有线程都到达之后还能进行一个回调方法;
- await:将挂起线程和countDown整合成一个;
- 应用:将线程任务分成多个部分来进行完成,多个部分之间呈依赖关系,线程的一个task任务部分为一个循环;
八、AQS
1.介绍
- 在并发类里面提取出的一个抽象类,对公用部分进行实现其他的交给子类实现,属于模板设计模式;
2.实现原理
- state:代表当前状态,持有锁的情况;
- 等待队列:双链表实现,head表示已经拿到锁的线程,其他的节点会被阻塞;
- 子类去实现尝试获取和释放锁的方法(都要通过CAS操作实现)可以设置公平和非公平,自己实现了在等待队列的共享锁和排他锁的获取和释放的逻辑(将当前线程放入等待队列中);
3.应用
- CountDownLatch中的应用:初始化时会将count值设置为state值,await时会调用tryAcquireShared(只有在state为0才可成功),countDown时会调用tryReleaseShared(每次将state减一);
- Semaphore中的应用:初始化会将许可证数量设置为state值,tryAcquire进行尝试获取许可证,
- ReentrantLock的应用:实现tryAcquire和tryRelease方法,由于可重入所以state代表重入次数(同一个线程对同一个锁可多次获得)